import { connect } from 'cloudflare:sockets'; const CHECK_TIMEOUT_MS = 12000; const CONNECT_TIMEOUT_MS = 9999; const READ_TIMEOUT_MS = 8000; const MAX_RESPONSE_BYTES = 96 * 1024; const RESOLVE_BATCH_LIMIT = 50; const PROXY_TYPES = ['socks5', 'http', 'https', 'turn', 'sstp']; const DEFAULT_PORTS = { socks5: 1080, http: 80, https: 443, turn: 3478, sstp: 443 }; const encoder = new TextEncoder(); const decoder = new TextDecoder(); const DEFAULT_BEIAN_CONTENT = `© 2025 - 2026 Check Socks5 · 基于 Cloudflare Workers 构建与运行 · 今日访问人数:··· · 站点维护:CMLiussss `; export default { async fetch(request, env, ctx) { const 备案内容 = env.BEIAN ?? DEFAULT_BEIAN_CONTENT; let urlText = request.url; const hashIndex = urlText.indexOf('#'); const mainUrl = hashIndex === -1 ? urlText : urlText.slice(0, hashIndex); if (!mainUrl.includes('?') && /%3f/i.test(mainUrl)) { urlText = mainUrl.replace(/%3f/i, '?') + (hashIndex === -1 ? '' : urlText.slice(hashIndex)); } const url = new URL(urlText); const origin = request.headers.get('Origin') || ''; if (request.method === 'OPTIONS') { return new Response(null, { status: 204, headers: corsHeaders(origin) }); } try { if (url.pathname === '/ip.json') { const headers = jsonHeaders(origin); const clientIP = url.searchParams.get('ip') || request.headers.get('CF-Connecting-IP') || request.headers.get('X-Forwarded-For') || request.headers.get('X-Real-IP') || request.headers.get('True-Client-IP') || null; const cf = request.cf || {}; const timezone = cf.timezone || 'UTC'; return new Response(JSON.stringify({ ip: clientIP, ipType: clientIP ? (clientIP.includes(':') && !clientIP.includes('.') ? 'ipv6' : 'ipv4') : null, colo: cf.colo || null, asn: cf.asn || null, asOrganization: cf.asOrganization || null, org: cf.asn && cf.asOrganization ? `AS${cf.asn} ${cf.asOrganization}` : null, continent: cf.continent || null, country: cf.country || null, regionCode: cf.regionCode || null, region: cf.region || null, city: cf.city || null, postalCode: cf.postalCode || null, timezone, loc: cf.latitude && cf.longitude ? `${cf.latitude},${cf.longitude}` : null, longitude: cf.longitude || null, latitude: cf.latitude || null, time: new Date().toLocaleString('zh-CN', { timeZone: timezone }), timeStamp: Date.now() }, null, 2), { headers }); } if (url.pathname.toLowerCase() === '/resolve') { const target = url.searchParams.get('proxyip') || url.searchParams.get('target') || url.searchParams.get('host'); if (!target) return jsonResponse({ error: 'Missing proxyip' }, { status: 400, origin }); return jsonResponse(await handleResolve(target), { origin }); } if (url.pathname.toLowerCase() === '/resolve-batch') { const headers = jsonHeaders(origin); if (request.method !== 'POST') { return new Response(JSON.stringify({ error: 'Method not allowed' }), { status: 405, headers: { ...headers, Allow: 'POST, OPTIONS' } }); } let payload; try { payload = await request.json(); } catch (error) { return new Response(JSON.stringify({ error: 'Invalid JSON body' }), { status: 400, headers }); } const inputs = uniqueStrings((Array.isArray(payload?.targets) ? payload.targets : (Array.isArray(payload?.proxyips) ? payload.proxyips : [])) .map(value => String(value || '').trim()) .filter(Boolean)); if (!inputs.length) return new Response(JSON.stringify({ error: 'Missing targets' }), { status: 400, headers }); if (inputs.length > RESOLVE_BATCH_LIMIT) return new Response(JSON.stringify({ error: `Resolve batch limit is ${RESOLVE_BATCH_LIMIT}` }), { status: 400, headers }); const results = await Promise.all(inputs.map(async input => { try { return { input, targets: await handleResolve(input) }; } catch (error) { return { input, targets: [], error: error.message }; } })); return new Response(JSON.stringify({ results }, null, 2), { headers }); } if (url.pathname.toLowerCase().startsWith('/check')) { let checkParams = null; for (const type of PROXY_TYPES) { if (url.searchParams.has(type)) { checkParams = { type, value: url.searchParams.get(type) || '' }; break; } } if (!checkParams && url.searchParams.has('proxy')) { const value = url.searchParams.get('proxy') || ''; const parsed = splitProxyScheme(value); if (parsed?.type) checkParams = { type: parsed.type, value: parsed.rest }; } if (!checkParams) { const tail = decodeURIComponent(url.pathname.slice('/check'.length).replace(/^\/+/, '')); const pathMatch = tail.match(/^(socks5|http|https|turn|sstp|proxy)=(.+)$/i); if (pathMatch) { const key = pathMatch[1].toLowerCase(); const value = pathMatch[2]; if (key === 'proxy') { const parsed = splitProxyScheme(value); if (parsed?.type) checkParams = { type: parsed.type, value: parsed.rest }; } else if (PROXY_TYPES.includes(key)) { checkParams = { type: key, value }; } } } if (!checkParams) { return jsonResponse({ success: false, error: 'Missing proxy parameter. Use /check?socks5=host:port, /check?http=host:port, /check?https=host:port, /check?turn=host:port, /check?sstp=host:port or /check?proxy=socks5://host:port' }, { status: 400, origin }); } const result = await checkProxy(checkParams, request.cf?.colo ?? null); return jsonResponse(result, { origin }); } else if (url.pathname === '/locations') return fetch(new Request('https://speed.cloudflare.com/locations', { headers: { 'Referer': 'https://speed.cloudflare.com/' } })); return new Response(generateHTML(备案内容), { headers: { 'Content-Type': 'text/html; charset=UTF-8', 'Cache-Control': 'no-cache, no-store, must-revalidate' } }); } catch (error) { return jsonResponse({ success: false, error: error?.message || String(error), timestamp: new Date().toISOString() }, { status: 500, origin }); } } }; function corsHeaders(origin = '') { return { 'Access-Control-Allow-Origin': origin || '*', 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS', 'Access-Control-Allow-Headers': 'Content-Type, Authorization', 'Access-Control-Max-Age': '86400' }; } function jsonHeaders(origin = '') { return { ...corsHeaders(origin), 'Content-Type': 'application/json; charset=UTF-8', 'Cache-Control': 'no-cache, no-store, must-revalidate' }; } function jsonResponse(data, { status = 200, origin = '' } = {}) { return new Response(JSON.stringify(data, null, 2), { status, headers: jsonHeaders(origin) }); } async function checkProxy({ type, value }, colo) { const startedAt = Date.now(); let proxy; try { proxy = parseProxyAddress(value, type, DEFAULT_PORTS[type]); } catch (error) { return buildCheckResult({ type, rawValue: value, proxy: null, success: false, colo: colo, error: error.message, responseTime: Date.now() - startedAt }); } let tunnel = null; const targetHost = 'www.iplocate.io'; const targetPort = 443; try { let tunnelPromise; if (proxy.type === 'socks5') tunnelPromise = socks5Connect(proxy, targetHost, targetPort); else if (proxy.type === 'http') tunnelPromise = httpConnect(proxy, targetHost, targetPort, false); else if (proxy.type === 'https') { tunnelPromise = isIPHostname(proxy.hostname) ? httpsConnect(proxy, targetHost, targetPort) : httpConnect(proxy, targetHost, targetPort, true); } else if (proxy.type === 'turn') { tunnelPromise = turnConnect(proxy, targetHost, targetPort); } else if (proxy.type === 'sstp') { tunnelPromise = sstpConnect(proxy, targetHost, targetPort); } else { throw new Error(`Unsupported proxy type: ${proxy.type}`); } tunnel = await withTimeout(tunnelPromise, CHECK_TIMEOUT_MS, 'Proxy connection timed out'); const tlsSocket = new TlsClient(tunnel, { serverName: stripIPv6Brackets(targetHost), timeout: READ_TIMEOUT_MS, allowChacha: true }); let exit; try { await withTimeout(tlsSocket.handshake(), CHECK_TIMEOUT_MS, 'Target TLS handshake timed out'); await tlsSocket.write(encoder.encode([ 'GET /api/lookup HTTP/1.1', `Host: ${targetHost}`, 'User-Agent: Mozilla/5.0 CF-Workers-CheckProxy/2.0', 'Accept: application/json', 'Connection: close', '', '' ].join('\r\n'))); let responseBuffer = new Uint8Array(0); while (responseBuffer.byteLength < MAX_RESPONSE_BYTES) { const value = await withTimeout(tlsSocket.read(), READ_TIMEOUT_MS, 'Reading target response timed out'); if (!value) break; if (!value.byteLength) continue; responseBuffer = concatUint8(responseBuffer, value); const currentHeaderEndIndex = indexOfHeaderEnd(responseBuffer); if (currentHeaderEndIndex === -1) continue; const currentHeaderText = decoder.decode(responseBuffer.slice(0, currentHeaderEndIndex)); const currentLengthMatch = currentHeaderText.match(/\r\ncontent-length:\s*(\d+)/i); if (currentLengthMatch) { if (responseBuffer.byteLength >= currentHeaderEndIndex + Number(currentLengthMatch[1])) break; } else if (/\r\ntransfer-encoding:\s*chunked/i.test(currentHeaderText) && decoder.decode(responseBuffer).includes('\r\n0\r\n\r\n')) { break; } } if (!responseBuffer.byteLength) throw new Error('Target returned no data'); const headerEndIndex = indexOfHeaderEnd(responseBuffer); if (headerEndIndex === -1) throw new Error('Invalid target response headers'); const headerText = decoder.decode(responseBuffer.slice(0, headerEndIndex)); const statusLine = headerText.split('\r\n')[0] || ''; const statusMatch = statusLine.match(/HTTP\/\d(?:\.\d)?\s+(\d+)/i); const statusCode = statusMatch ? Number(statusMatch[1]) : NaN; if (!Number.isFinite(statusCode) || statusCode < 200 || statusCode >= 300) { throw new Error(`Target /api/lookup request failed: ${statusLine || 'invalid status'}`); } let bodyBytes = responseBuffer.slice(headerEndIndex); const lengthMatch = headerText.match(/\r\ncontent-length:\s*(\d+)/i); if (lengthMatch) bodyBytes = bodyBytes.slice(0, Number(lengthMatch[1])); let bodyText = decoder.decode(bodyBytes); if (/\r\ntransfer-encoding:\s*chunked/i.test(headerText)) { let cursor = 0; let output = ''; while (cursor < bodyText.length) { const lineEnd = bodyText.indexOf('\r\n', cursor); if (lineEnd === -1) break; const sizeText = bodyText.slice(cursor, lineEnd).split(';')[0].trim(); const size = parseInt(sizeText, 16); if (!Number.isFinite(size)) break; cursor = lineEnd + 2; if (size === 0) break; output += bodyText.slice(cursor, cursor + size); cursor += size + 2; } bodyText = output || bodyText; } try { exit = JSON.parse(bodyText.trim()); } catch (error) { throw new Error('Target /api/lookup did not return valid JSON'); } // Transform iplocate.io response to maintain compatibility with existing frontend exit = { ...exit, asn: exit.asn ? { ...exit.asn, org: exit.asn.name, descr: exit.asn.name, } : exit.asn, rir: exit.asn?.rir || null, is_datacenter: exit.privacy?.is_hosting || false, is_crawler: false, is_bogon: exit.privacy?.is_bogon || false, is_proxy: exit.privacy?.is_proxy || false, is_vpn: exit.privacy?.is_vpn || false, is_tor: exit.privacy?.is_tor || false, is_abuser: exit.privacy?.is_abuser || false, }; } finally { try { tlsSocket.close(); } catch (e) { } } return buildCheckResult({ type, rawValue: value, proxy, success: true, colo: colo, exit, responseTime: Date.now() - startedAt }); } catch (error) { return buildCheckResult({ type, rawValue: value, proxy, success: false, colo: colo, error: error?.message || String(error), responseTime: Date.now() - startedAt }); } finally { try { tunnel?.close?.(); } catch (e) { } } } function buildCheckResult({ type, rawValue, proxy, success, colo = null, exit = null, error = null, responseTime }) { const candidate = proxy ? formatProxyAuthority(proxy) : stripProxyScheme(rawValue); const result = { candidate, type, username: proxy?.username ?? null, password: proxy?.password ?? null, hostname: proxy?.hostname ?? null, port: proxy?.port ?? null, link: proxy ? `${proxy.type}://${formatProxyAuthority(proxy)}` : `${type}://${candidate}`, success, colo, responseTime }; if (success) result.exit = exit; else result.error = error || 'Proxy check failed'; return result; } function indexOfHeaderEnd(buffer) { for (let i = 0; i < buffer.byteLength - 3; i++) { if (buffer[i] === 0x0d && buffer[i + 1] === 0x0a && buffer[i + 2] === 0x0d && buffer[i + 3] === 0x0a) return i + 4; } return -1; } const TURN_STUN_MAGIC_COOKIE = new Uint8Array([0x21, 0x12, 0xa4, 0x42]); const TURN_STUN_TYPE = { ALLOCATE_REQUEST: 0x0003, ALLOCATE_SUCCESS: 0x0103, ALLOCATE_ERROR: 0x0113, CREATE_PERMISSION_REQUEST: 0x0008, CREATE_PERMISSION_SUCCESS: 0x0108, CONNECT_REQUEST: 0x000a, CONNECT_SUCCESS: 0x010a, CONNECTION_BIND_REQUEST: 0x000b, CONNECTION_BIND_SUCCESS: 0x010b }; const TURN_STUN_ATTR = { USERNAME: 0x0006, MESSAGE_INTEGRITY: 0x0008, ERROR_CODE: 0x0009, XOR_PEER_ADDRESS: 0x0012, REALM: 0x0014, NONCE: 0x0015, REQUESTED_TRANSPORT: 0x0019, CONNECTION_ID: 0x002a }; function turnStunPadding(length) { return -length & 3; } function createTurnStunAttribute(type, value) { const body = 数据转Uint8Array(value); const attribute = new Uint8Array(4 + body.byteLength + turnStunPadding(body.byteLength)); const view = new DataView(attribute.buffer); view.setUint16(0, type); view.setUint16(2, body.byteLength); attribute.set(body, 4); return attribute; } function createTurnStunMessage(type, transactionId, attributes) { const body = concatUint8(...attributes); const header = new Uint8Array(20); const view = new DataView(header.buffer); view.setUint16(0, type); view.setUint16(2, body.byteLength); header.set(TURN_STUN_MAGIC_COOKIE, 4); header.set(transactionId, 8); return concatUint8(header, body); } /** @typedef {{ type: number, attributes: Record }} TurnStunMessage */ function parseTurnErrorCode(data) { return data?.byteLength >= 4 ? (data[2] & 7) * 100 + data[3] : 0; } function randomTurnTransactionId() { return crypto.getRandomValues(new Uint8Array(12)); } async function addTurnMessageIntegrity(message, key) { const signedMessage = new Uint8Array(message); const view = new DataView(signedMessage.buffer); view.setUint16(2, view.getUint16(2) + 24); const hmacKey = await crypto.subtle.importKey( 'raw', key, { name: 'HMAC', hash: 'SHA-1' }, false, ['sign'] ); const signature = await crypto.subtle.sign('HMAC', hmacKey, signedMessage); return concatUint8( signedMessage, createTurnStunAttribute(TURN_STUN_ATTR.MESSAGE_INTEGRITY, new Uint8Array(signature)) ); } /** @returns {Promise<{ message: TurnStunMessage, extraData: Uint8Array | null }>} */ async function readTurnStunMessage(reader, bufferedData = null, timeoutMessage = 'TURN response timed out') { let buffer = 有效数据长度(bufferedData) ? 数据转Uint8Array(bufferedData) : new Uint8Array(0); const pull = async () => { const { done, value } = await withTimeout(reader.read(), CONNECT_TIMEOUT_MS, timeoutMessage); if (done) throw new Error('TURN server closed connection'); if (value?.byteLength) buffer = concatUint8(buffer, value); }; while (buffer.byteLength < 20) await pull(); const messageLength = 20 + ((buffer[2] << 8) | buffer[3]); if (messageLength > 65555) throw new Error('TURN response is too large'); while (buffer.byteLength < messageLength) await pull(); const messageBuffer = buffer.subarray(0, messageLength); if (TURN_STUN_MAGIC_COOKIE.some((value, index) => messageBuffer[4 + index] !== value)) throw new Error('Invalid TURN/STUN response'); const view = new DataView(messageBuffer.buffer, messageBuffer.byteOffset, messageBuffer.byteLength); /** @type {Record} */ const attributes = {}; for (let offset = 20; offset + 4 <= messageLength;) { const type = view.getUint16(offset); const length = view.getUint16(offset + 2); if (offset + 4 + length > messageBuffer.byteLength) break; attributes[type] = messageBuffer.slice(offset + 4, offset + 4 + length); offset += 4 + length + turnStunPadding(length); } return { message: { type: view.getUint16(0), attributes }, extraData: buffer.byteLength > messageLength ? buffer.subarray(messageLength) : null }; } async function writeTurnBytes(writer, bytes, timeoutMessage) { await withTimeout(writer.write(bytes), CONNECT_TIMEOUT_MS, timeoutMessage); } async function allocateTurnRelay(writer, reader, transport, credentials, pipeline) { const requestedTransport = new Uint8Array([transport, 0, 0, 0]); await writeTurnBytes(writer, createTurnStunMessage( TURN_STUN_TYPE.ALLOCATE_REQUEST, randomTurnTransactionId(), [createTurnStunAttribute(TURN_STUN_ATTR.REQUESTED_TRANSPORT, requestedTransport)] ), 'TURN Allocate request timed out'); let turnResponse = await readTurnStunMessage(reader, null, 'TURN Allocate response timed out'); let message = turnResponse.message; let extraData = turnResponse.extraData; let integrityKey = null; let authAttributes = []; const sign = messageToSign => ( integrityKey ? addTurnMessageIntegrity(messageToSign, integrityKey) : Promise.resolve(messageToSign) ); if ( message.type === TURN_STUN_TYPE.ALLOCATE_ERROR && credentials.username !== null && credentials.password !== null && parseTurnErrorCode(message.attributes[TURN_STUN_ATTR.ERROR_CODE]) === 401 ) { const realmBytes = message.attributes[TURN_STUN_ATTR.REALM]; const nonce = message.attributes[TURN_STUN_ATTR.NONCE]; if (!realmBytes || !nonce?.byteLength) throw new Error('TURN authentication challenge is missing realm or nonce'); const realm = decoder.decode(realmBytes); integrityKey = new Uint8Array(await crypto.subtle.digest('MD5', encoder.encode(`${credentials.username}:${realm}:${credentials.password}`))); authAttributes = [ createTurnStunAttribute(TURN_STUN_ATTR.USERNAME, encoder.encode(credentials.username)), createTurnStunAttribute(TURN_STUN_ATTR.REALM, encoder.encode(realm)), createTurnStunAttribute(TURN_STUN_ATTR.NONCE, nonce) ]; const allocateRequest = await addTurnMessageIntegrity(createTurnStunMessage( TURN_STUN_TYPE.ALLOCATE_REQUEST, randomTurnTransactionId(), [ createTurnStunAttribute(TURN_STUN_ATTR.REQUESTED_TRANSPORT, requestedTransport), ...authAttributes ] ), integrityKey); const pipelinedMessages = pipeline ? await Promise.all(pipeline(authAttributes, sign)) : []; await writeTurnBytes( writer, pipelinedMessages.length ? concatUint8(allocateRequest, ...pipelinedMessages) : allocateRequest, 'TURN authenticated Allocate request timed out' ); turnResponse = await readTurnStunMessage(reader, extraData, 'TURN authenticated Allocate response timed out'); message = turnResponse.message; extraData = turnResponse.extraData; } else if (pipeline && message.type === TURN_STUN_TYPE.ALLOCATE_SUCCESS) { const pipelinedMessages = await Promise.all(pipeline(authAttributes, sign)); if (pipelinedMessages.length) { await writeTurnBytes(writer, concatUint8(...pipelinedMessages), 'TURN pipelined request timed out'); } } if (message.type !== TURN_STUN_TYPE.ALLOCATE_SUCCESS) { const errorCode = parseTurnErrorCode(message.attributes[TURN_STUN_ATTR.ERROR_CODE]); throw new Error(errorCode ? `TURN Allocate failed with ${errorCode}` : 'TURN Allocate failed'); } return { authAttributes, extraData, sign }; } async function turnConnect(proxy, targetHost, targetPort) { const resolvedTargetHost = stripIPv6Brackets(targetHost); let targetIp = isIPv4(resolvedTargetHost) ? resolvedTargetHost : null; if (!targetIp) { const records = await DoH查询(resolvedTargetHost, 'A'); const record = records.find(item => item.type === 1 && isIPv4(item.data)); targetIp = record?.data || null; } if (!targetIp) throw new Error(`Could not resolve ${targetHost} to an IPv4 address for TURN CONNECT`); const turnHost = stripIPv6Brackets(proxy.hostname); let controlSocket = null; let dataSocket = null; let controlWriter = null; let controlReader = null; let dataWriter = null; let dataReader = null; let dataReaderReleased = false; const close = () => { try { controlSocket?.close?.(); } catch (e) { } try { dataSocket?.close?.(); } catch (e) { } }; const releaseDataReader = () => { if (dataReaderReleased) return; dataReaderReleased = true; try { dataReader?.releaseLock?.(); } catch (e) { } }; try { controlSocket = connect({ hostname: turnHost, port: proxy.port }); await withTimeout(controlSocket.opened, CONNECT_TIMEOUT_MS, 'TURN server connection timed out'); controlWriter = controlSocket.writable.getWriter(); controlReader = controlSocket.readable.getReader(); if (!isIPv4(targetIp)) throw new Error('TURN CONNECT currently requires an IPv4 target address'); const xorPeerAddress = new Uint8Array(8); xorPeerAddress[1] = 1; new DataView(xorPeerAddress.buffer).setUint16(2, targetPort ^ 0x2112); targetIp.split('.').forEach((value, index) => { xorPeerAddress[4 + index] = Number(value) ^ TURN_STUN_MAGIC_COOKIE[index]; }); const peerAddress = createTurnStunAttribute( TURN_STUN_ATTR.XOR_PEER_ADDRESS, xorPeerAddress ); const allocation = await allocateTurnRelay( controlWriter, controlReader, 6, { username: proxy.username, password: proxy.password }, (authAttributes, sign) => [ sign(createTurnStunMessage( TURN_STUN_TYPE.CREATE_PERMISSION_REQUEST, randomTurnTransactionId(), [peerAddress, ...authAttributes] )), sign(createTurnStunMessage( TURN_STUN_TYPE.CONNECT_REQUEST, randomTurnTransactionId(), [peerAddress, ...authAttributes] )) ] ); let bufferedData = allocation.extraData; dataSocket = connect({ hostname: turnHost, port: proxy.port }); let turnResponse = await readTurnStunMessage(controlReader, bufferedData, 'TURN CreatePermission response timed out'); let message = turnResponse.message; bufferedData = turnResponse.extraData; if (message.type !== TURN_STUN_TYPE.CREATE_PERMISSION_SUCCESS) throw new Error('TURN CreatePermission failed'); turnResponse = await readTurnStunMessage(controlReader, bufferedData, 'TURN CONNECT response timed out'); message = turnResponse.message; bufferedData = turnResponse.extraData; if (message.type !== TURN_STUN_TYPE.CONNECT_SUCCESS || !message.attributes[TURN_STUN_ATTR.CONNECTION_ID]) { throw new Error('TURN CONNECT failed'); } await withTimeout(dataSocket.opened, CONNECT_TIMEOUT_MS, 'TURN data connection timed out'); dataWriter = dataSocket.writable.getWriter(); dataReader = dataSocket.readable.getReader(); await writeTurnBytes(dataWriter, await allocation.sign(createTurnStunMessage( TURN_STUN_TYPE.CONNECTION_BIND_REQUEST, randomTurnTransactionId(), [ createTurnStunAttribute(TURN_STUN_ATTR.CONNECTION_ID, message.attributes[TURN_STUN_ATTR.CONNECTION_ID]), ...allocation.authAttributes ] )), 'TURN ConnectionBind request timed out'); let extraPayload = null; turnResponse = await readTurnStunMessage(dataReader, null, 'TURN ConnectionBind response timed out'); message = turnResponse.message; extraPayload = turnResponse.extraData; if (message.type !== TURN_STUN_TYPE.CONNECTION_BIND_SUCCESS) throw new Error('TURN ConnectionBind failed'); controlWriter.releaseLock(); controlWriter = null; controlReader.releaseLock(); controlReader = null; dataWriter.releaseLock(); dataWriter = null; const readable = new ReadableStream({ start(controller) { if (extraPayload?.byteLength) controller.enqueue(extraPayload); }, pull(controller) { return dataReader.read().then(({ done, value }) => { if (done) { releaseDataReader(); controller.close(); } else if (value?.byteLength) { controller.enqueue(new Uint8Array(value)); } }); }, cancel() { try { dataReader?.cancel?.(); } catch (e) { } releaseDataReader(); close(); } }); return { readable, writable: dataSocket.writable, closed: dataSocket.closed, close }; } catch (error) { try { controlWriter?.releaseLock?.(); } catch (e) { } try { controlReader?.releaseLock?.(); } catch (e) { } try { dataWriter?.releaseLock?.(); } catch (e) { } releaseDataReader(); close(); throw error; } } async function socks5Connect(proxy, targetHost, targetPort) { const socket = connect({ hostname: stripIPv6Brackets(proxy.hostname), port: proxy.port }); const writer = socket.writable.getWriter(); const reader = socket.readable.getReader(); try { await withTimeout(socket.opened, CONNECT_TIMEOUT_MS, 'SOCKS5 proxy connection timed out'); const authMethods = proxy.username !== null && proxy.password !== null ? new Uint8Array([0x05, 0x02, 0x00, 0x02]) : new Uint8Array([0x05, 0x01, 0x00]); await writer.write(authMethods); let response = await withTimeout(reader.read(), CONNECT_TIMEOUT_MS, 'SOCKS5 authentication negotiation timed out'); if (response.done || response.value.byteLength < 2) throw new Error('SOCKS5 method selection failed'); const selectedMethod = response.value[1]; if (selectedMethod === 0x02) { if (proxy.username === null || proxy.password === null) throw new Error('SOCKS5 requires authentication'); const userBytes = encoder.encode(proxy.username); const passBytes = encoder.encode(proxy.password); if (userBytes.byteLength > 255 || passBytes.byteLength > 255) throw new Error('SOCKS5 username/password is too long'); await writer.write(new Uint8Array([0x01, userBytes.length, ...userBytes, passBytes.length, ...passBytes])); response = await withTimeout(reader.read(), CONNECT_TIMEOUT_MS, 'SOCKS5 username/password authentication timed out'); if (response.done || response.value[1] !== 0x00) throw new Error('SOCKS5 authentication failed'); } else if (selectedMethod !== 0x00) { throw new Error(`SOCKS5 unsupported auth method: ${selectedMethod}`); } const hostBytes = encoder.encode(targetHost); if (hostBytes.byteLength > 255) throw new Error('Target hostname is too long for SOCKS5'); const connectPacket = new Uint8Array([0x05, 0x01, 0x00, 0x03, hostBytes.length, ...hostBytes, targetPort >> 8, targetPort & 0xff]); await writer.write(connectPacket); response = await withTimeout(reader.read(), CONNECT_TIMEOUT_MS, 'SOCKS5 CONNECT timed out'); if (response.done || response.value[1] !== 0x00) throw new Error(`SOCKS5 connection failed: ${response.value?.[1] ?? 'closed'}`); writer.releaseLock(); reader.releaseLock(); return socket; } catch (error) { try { writer.releaseLock(); } catch (e) { } try { reader.releaseLock(); } catch (e) { } try { socket.close(); } catch (e) { } throw error; } } async function httpConnect(proxy, targetHost, targetPort, secureProxy = false) { const socket = secureProxy ? connect({ hostname: stripIPv6Brackets(proxy.hostname), port: proxy.port }, { secureTransport: 'on', allowHalfOpen: false }) : connect({ hostname: stripIPv6Brackets(proxy.hostname), port: proxy.port }); const writer = socket.writable.getWriter(); const reader = socket.readable.getReader(); try { await withTimeout(socket.opened, CONNECT_TIMEOUT_MS, `${secureProxy ? 'HTTPS' : 'HTTP'} proxy connection timed out`); const auth = proxy.username !== null && proxy.password !== null ? `Proxy-Authorization: Basic ${btoa(`${proxy.username}:${proxy.password}`)}\r\n` : ''; const request = `CONNECT ${targetHost}:${targetPort} HTTP/1.1\r\nHost: ${targetHost}:${targetPort}\r\n${auth}User-Agent: Mozilla/5.0\r\nProxy-Connection: keep-alive\r\nConnection: keep-alive\r\n\r\n`; await writer.write(encoder.encode(request)); writer.releaseLock(); let responseBuffer = new Uint8Array(0); let headerEndIndex = -1; while (headerEndIndex === -1 && responseBuffer.byteLength < 8192) { const { done, value } = await withTimeout(reader.read(), CONNECT_TIMEOUT_MS, `${secureProxy ? 'HTTPS' : 'HTTP'} proxy CONNECT response timed out`); if (done || !value) throw new Error(`${secureProxy ? 'HTTPS' : 'HTTP'} proxy closed before returning CONNECT response`); responseBuffer = concatUint8(responseBuffer, value); headerEndIndex = indexOfHeaderEnd(responseBuffer); } if (headerEndIndex === -1) throw new Error('Proxy CONNECT response headers are too large or invalid'); const statusLine = decoder.decode(responseBuffer.slice(0, headerEndIndex)).split('\r\n')[0] || ''; const statusMatch = statusLine.match(/HTTP\/\d(?:\.\d)?\s+(\d+)/i); const statusCode = statusMatch ? Number(statusMatch[1]) : NaN; if (!Number.isFinite(statusCode) || statusCode < 200 || statusCode >= 300) throw new Error(`CONNECT failed: ${statusLine || statusCode}`); reader.releaseLock(); if (responseBuffer.byteLength > headerEndIndex) { const bufferedData = responseBuffer.slice(headerEndIndex); const readable = new ReadableStream({ async start(controller) { try { controller.enqueue(bufferedData); const bufferedReader = socket.readable.getReader(); try { while (true) { const { done, value } = await bufferedReader.read(); if (done) break; if (value?.byteLength) controller.enqueue(value); } controller.close(); } finally { try { bufferedReader.releaseLock(); } catch (e) { } } } catch (error) { controller.error(error); } }, cancel() { try { socket.close(); } catch (e) { } } }); return { readable, writable: socket.writable, closed: socket.closed, close: () => socket.close() }; } return socket; } catch (error) { try { writer.releaseLock(); } catch (e) { } try { reader.releaseLock(); } catch (e) { } try { socket.close(); } catch (e) { } throw error; } } async function httpsConnect(proxy, targetHost, targetPort) { let tlsSocket = null; const tlsServerName = isIPHostname(proxy.hostname) ? '' : stripIPv6Brackets(proxy.hostname); const openProxyTls = async (allowChacha = false) => { const proxySocket = connect({ hostname: stripIPv6Brackets(proxy.hostname), port: proxy.port }); try { await withTimeout(proxySocket.opened, CONNECT_TIMEOUT_MS, 'HTTPS proxy TCP connection timed out'); const socket = new TlsClient(proxySocket, { serverName: tlsServerName, timeout: READ_TIMEOUT_MS, allowChacha }); await withTimeout(socket.handshake(), CHECK_TIMEOUT_MS, 'HTTPS proxy TLS handshake timed out'); return socket; } catch (error) { try { proxySocket.close(); } catch (e) { } throw error; } }; try { try { tlsSocket = await openProxyTls(false); } catch (error) { if (!/cipher|handshake|TLS Alert|ServerHello|Finished|Unsupported|Missing TLS/i.test(error?.message || String(error))) throw error; tlsSocket = await openProxyTls(true); } const auth = proxy.username !== null && proxy.password !== null ? `Proxy-Authorization: Basic ${btoa(`${proxy.username}:${proxy.password}`)}\r\n` : ''; const request = `CONNECT ${targetHost}:${targetPort} HTTP/1.1\r\nHost: ${targetHost}:${targetPort}\r\n${auth}User-Agent: Mozilla/5.0\r\nProxy-Connection: keep-alive\r\nConnection: keep-alive\r\n\r\n`; await tlsSocket.write(encoder.encode(request)); let responseBuffer = new Uint8Array(0); let headerEndIndex = -1; while (headerEndIndex === -1 && responseBuffer.byteLength < 8192) { const value = await withTimeout(tlsSocket.read(), CONNECT_TIMEOUT_MS, 'HTTPS proxy CONNECT response timed out'); if (!value) throw new Error('HTTPS proxy closed before returning CONNECT response'); responseBuffer = concatUint8(responseBuffer, value); headerEndIndex = indexOfHeaderEnd(responseBuffer); } if (headerEndIndex === -1) throw new Error('HTTPS proxy CONNECT response headers are too large or invalid'); const statusLine = decoder.decode(responseBuffer.slice(0, headerEndIndex)).split('\r\n')[0] || ''; const statusMatch = statusLine.match(/HTTP\/\d(?:\.\d)?\s+(\d+)/i); const statusCode = statusMatch ? Number(statusMatch[1]) : NaN; if (!Number.isFinite(statusCode) || statusCode < 200 || statusCode >= 300) throw new Error(`CONNECT failed: ${statusLine || statusCode}`); const bufferedData = responseBuffer.byteLength > headerEndIndex ? responseBuffer.slice(headerEndIndex) : null; let closedSettled = false; let resolveClosed; let rejectClosed; const closed = new Promise((resolve, reject) => { resolveClosed = resolve; rejectClosed = reject; }); const settleClosed = (settle, value) => { if (closedSettled) return; closedSettled = true; settle(value); }; const close = () => { try { tlsSocket.close(); } catch (e) { } settleClosed(resolveClosed); }; const readable = new ReadableStream({ async start(controller) { try { if (有效数据长度(bufferedData) > 0) controller.enqueue(数据转Uint8Array(bufferedData)); while (true) { const data = await tlsSocket.read(); if (!data) break; if (data.byteLength > 0) controller.enqueue(data); } controller.close(); settleClosed(resolveClosed); } catch (error) { try { controller.error(error); } catch (e) { } settleClosed(rejectClosed, error); } }, cancel() { close(); } }); const writable = new WritableStream({ async write(chunk) { await tlsSocket.write(数据转Uint8Array(chunk)); }, close, abort(error) { close(); if (error) settleClosed(rejectClosed, error); } }); return { readable, writable, closed, close }; } catch (error) { try { tlsSocket?.close?.(); } catch (e) { } throw error; } } const SSTP_TCP_MSS = 1400; const SSTP_EMPTY_BYTES = new Uint8Array(0); function readSstpUint16(bytes, offset = 0) { return (bytes[offset] << 8) | bytes[offset + 1]; } function readSstpUint32(bytes, offset = 0) { return ((bytes[offset] << 24) | (bytes[offset + 1] << 16) | (bytes[offset + 2] << 8) | bytes[offset + 3]) >>> 0; } function randomSstpBytes(length) { return crypto.getRandomValues(new Uint8Array(length)); } function randomSstpUint16() { return readSstpUint16(randomSstpBytes(2)); } function randomSstpUint32() { return readSstpUint32(randomSstpBytes(4)); } function ipv4ToBytes(ip) { return new Uint8Array(String(ip || '').split('.').map(Number)); } function internetChecksum(bytes, offset, length) { let sum = 0; for (let index = offset; index < offset + length - 1; index += 2) { sum += readSstpUint16(bytes, index); } if (length & 1) sum += bytes[offset + length - 1] << 8; while (sum >> 16) sum = (sum & 0xffff) + (sum >> 16); return (~sum) & 0xffff; } function formatSstpHostHeader(hostname, port) { const host = stripIPv6Brackets(hostname); const displayHost = host.includes(':') ? `[${host}]` : host; return Number(port) === 443 ? displayHost : `${displayHost}:${port}`; } async function resolveSstpTargetIPv4(targetHost) { const host = stripIPv6Brackets(targetHost); if (isIPv4(host)) return host; const records = await DoH查询(host, 'A'); const record = records.find(item => item.type === 1 && isIPv4(item.data)); if (!record?.data) throw new Error(`Could not resolve ${targetHost} to an IPv4 address for SSTP`); return record.data; } function createSstpClient(proxy) { let bufferedBytes = SSTP_EMPTY_BYTES; let pppIdentifier = 1; let socket = null; let reader = null; let writer = null; let serverHost = ''; let serverPort = 443; const readSocketChunk = async () => { const { value, done } = await reader.read(); if (done || !value) throw new Error('SSTP socket closed'); return 数据转Uint8Array(value); }; const readBytes = async length => { while (bufferedBytes.byteLength < length) { const chunk = await readSocketChunk(); bufferedBytes = bufferedBytes.byteLength ? concatUint8(bufferedBytes, chunk) : chunk; } const result = bufferedBytes.subarray(0, length); bufferedBytes = bufferedBytes.subarray(length); return result; }; const readHttpLine = async () => { for (; ;) { const lineEnd = bufferedBytes.indexOf(10); if (lineEnd >= 0) { const line = decoder.decode(bufferedBytes.subarray(0, lineEnd)); bufferedBytes = bufferedBytes.subarray(lineEnd + 1); return line.replace(/\r$/, ''); } const chunk = await readSocketChunk(); bufferedBytes = bufferedBytes.byteLength ? concatUint8(bufferedBytes, chunk) : chunk; } }; const readPacket = async (timeoutMs = CONNECT_TIMEOUT_MS) => { const header = await withTimeout(readBytes(4), timeoutMs, 'SSTP read timeout'); const length = readSstpUint16(header, 2) & 0x0fff; if (length < 4) throw new Error('Invalid SSTP packet length'); return { isControl: (header[1] & 1) !== 0, body: length > 4 ? await withTimeout(readBytes(length - 4), timeoutMs, 'SSTP packet body read timeout') : SSTP_EMPTY_BYTES }; }; const buildSstpDataPacket = pppFrame => { const packetLength = 6 + pppFrame.byteLength; const packet = new Uint8Array(packetLength); packet.set([0x10, 0x00, ((packetLength >> 8) & 0x0f) | 0x80, packetLength & 0xff, 0xff, 0x03]); packet.set(pppFrame, 6); return packet; }; const buildSstpControlPacket = (messageType, attributes = []) => { const attributesLength = attributes.reduce((size, attribute) => size + 4 + attribute.data.byteLength, 0); const packet = new Uint8Array(8 + attributesLength); const view = new DataView(packet.buffer); packet[0] = 0x10; packet[1] = 0x01; view.setUint16(2, (8 + attributesLength) | 0x8000); view.setUint16(4, messageType); view.setUint16(6, attributes.length); attributes.reduce((offset, attribute) => { packet[offset + 1] = attribute.id; view.setUint16(offset + 2, 4 + attribute.data.byteLength); packet.set(attribute.data, offset + 4); return offset + 4 + attribute.data.byteLength; }, 8); return packet; }; const buildPppConfigurePacket = (protocol, code, id, options = []) => { const optionsLength = options.reduce((size, option) => size + 2 + option.data.byteLength, 0); const frame = new Uint8Array(6 + optionsLength); const view = new DataView(frame.buffer); view.setUint16(0, protocol); frame[2] = code; frame[3] = id; view.setUint16(4, 4 + optionsLength); options.reduce((offset, option) => { frame[offset] = option.type; frame[offset + 1] = 2 + option.data.byteLength; frame.set(option.data, offset + 2); return offset + 2 + option.data.byteLength; }, 6); return frame; }; const buildPapAuthenticateRequest = id => { if (proxy.username === null || proxy.password === null) throw new Error('SSTP server requires PAP authentication'); const username = encoder.encode(proxy.username); const password = encoder.encode(proxy.password); if (username.byteLength > 255 || password.byteLength > 255) throw new Error('SSTP username/password is too long'); const papLength = 6 + username.byteLength + password.byteLength; const frame = new Uint8Array(2 + papLength); const view = new DataView(frame.buffer); view.setUint16(0, 0xc023); frame[2] = 1; frame[3] = id; view.setUint16(4, papLength); frame[6] = username.byteLength; frame.set(username, 7); frame[7 + username.byteLength] = password.byteLength; frame.set(password, 8 + username.byteLength); return frame; }; const parsePPPFrame = data => { const offset = data.byteLength >= 2 && data[0] === 0xff && data[1] === 0x03 ? 2 : 0; if (data.byteLength - offset < 4) return null; const protocol = readSstpUint16(data, offset); if (protocol === 0x0021) { return { protocol, ipPacket: data.subarray(offset + 2) }; } if (data.byteLength - offset < 6) return null; return { protocol, code: data[offset + 2], id: data[offset + 3], payload: data.subarray(offset + 6), rawPacket: data.subarray(offset) }; }; const parsePppOptions = data => { const options = []; for (let offset = 0; offset + 2 <= data.byteLength;) { const type = data[offset]; const length = data[offset + 1]; if (length < 2 || offset + length > data.byteLength) break; options.push({ type, data: data.subarray(offset + 2, offset + length) }); offset += length; } return options; }; const connectToServer = async (hostname, port) => { serverHost = stripIPv6Brackets(hostname); serverPort = port; socket = connect({ hostname: serverHost, port: serverPort }, { secureTransport: 'on', allowHalfOpen: false }); await withTimeout(socket.opened, CONNECT_TIMEOUT_MS, 'SSTP server connection timed out'); reader = socket.readable.getReader(); writer = socket.writable.getWriter(); }; const establishTunnel = async () => { const httpRequest = encoder.encode( `SSTP_DUPLEX_POST /sra_{BA195980-CD49-458b-9E23-C84EE0ADCD75}/ HTTP/1.1\r\n` + `Host: ${formatSstpHostHeader(serverHost, serverPort)}\r\n` + 'Content-Length: 18446744073709551615\r\n' + `SSTPCORRELATIONID: {${crypto.randomUUID()}}\r\n\r\n` ); const encapsulatedProtocol = new Uint8Array(2); new DataView(encapsulatedProtocol.buffer).setUint16(0, 1); const maximumReceiveUnit = new Uint8Array(2); new DataView(maximumReceiveUnit.buffer).setUint16(0, 1500); await withTimeout(writer.write(concatUint8( httpRequest, buildSstpControlPacket(0x0001, [{ id: 1, data: encapsulatedProtocol }]), buildSstpDataPacket(buildPppConfigurePacket(0xc021, 1, pppIdentifier++, [ { type: 1, data: maximumReceiveUnit } ])) )), CONNECT_TIMEOUT_MS, 'SSTP HTTP handshake request timed out'); const statusLine = await withTimeout(readHttpLine(), CONNECT_TIMEOUT_MS, 'SSTP HTTP handshake timed out'); for (; ;) { const line = await withTimeout(readHttpLine(), CONNECT_TIMEOUT_MS, 'SSTP HTTP header read timed out'); if (line === '') break; } if (!/HTTP\/\d(?:\.\d)?\s+2\d\d/i.test(statusLine)) throw new Error(`SSTP HTTP handshake failed: ${statusLine || 'invalid status'}`); let sstpAccepted = false; let localLcpAcked = false; let peerLcpAcked = false; let papRequired = false; let papSent = false; let papDone = false; let ipcpStarted = false; let ipcpFinished = false; let assignedIp = null; const sendPapIfReady = async () => { if (!localLcpAcked || !peerLcpAcked || !papRequired || papSent) return; await withTimeout(writer.write(buildSstpDataPacket(buildPapAuthenticateRequest(pppIdentifier++))), CONNECT_TIMEOUT_MS, 'SSTP PAP authentication request timed out'); papSent = true; }; const startIpcpIfReady = async () => { if (!localLcpAcked || !peerLcpAcked || ipcpStarted || (papRequired && !papDone)) return; await withTimeout(writer.write(buildSstpDataPacket(buildPppConfigurePacket(0x8021, 1, pppIdentifier++, [ { type: 3, data: new Uint8Array(4) } ]))), CONNECT_TIMEOUT_MS, 'SSTP IPCP request timed out'); ipcpStarted = true; }; for (let round = 0; round < 50 && !ipcpFinished; round++) { const packet = await readPacket(CONNECT_TIMEOUT_MS); if (packet.isControl) { if (!sstpAccepted && packet.body.byteLength >= 2 && readSstpUint16(packet.body) === 2) sstpAccepted = true; continue; } const ppp = parsePPPFrame(packet.body); if (!ppp) continue; if (ppp.protocol === 0xc021) { if (ppp.code === 1) { const authOption = parsePppOptions(ppp.payload).find(option => option.type === 3); if (authOption?.data?.byteLength >= 2) { const authProtocol = readSstpUint16(authOption.data); if (authProtocol !== 0xc023) throw new Error(`SSTP unsupported PPP authentication protocol: 0x${authProtocol.toString(16)}`); papRequired = true; } const ack = new Uint8Array(ppp.rawPacket); ack[2] = 2; await withTimeout(writer.write(buildSstpDataPacket(ack)), CONNECT_TIMEOUT_MS, 'SSTP LCP Configure-Ack timed out'); peerLcpAcked = true; await sendPapIfReady(); await startIpcpIfReady(); } else if (ppp.code === 2) { localLcpAcked = true; await sendPapIfReady(); await startIpcpIfReady(); } continue; } if (ppp.protocol === 0xc023) { if (ppp.code === 2) { papDone = true; await startIpcpIfReady(); } else if (ppp.code === 3) { throw new Error('SSTP PAP authentication failed'); } continue; } if (ppp.protocol === 0x8021) { if (ppp.code === 1) { const ack = new Uint8Array(ppp.rawPacket); ack[2] = 2; await withTimeout(writer.write(buildSstpDataPacket(ack)), CONNECT_TIMEOUT_MS, 'SSTP IPCP Configure-Ack timed out'); await startIpcpIfReady(); } else if (ppp.code === 3) { const addressOption = parsePppOptions(ppp.payload).find(option => option.type === 3); if (addressOption?.data?.byteLength === 4) { assignedIp = [...addressOption.data].join('.'); await withTimeout(writer.write(buildSstpDataPacket(buildPppConfigurePacket(0x8021, 1, pppIdentifier++, [ { type: 3, data: addressOption.data } ]))), CONNECT_TIMEOUT_MS, 'SSTP IPCP address request timed out'); ipcpStarted = true; } } else if (ppp.code === 2) { const addressOption = parsePppOptions(ppp.payload).find(option => option.type === 3); if (addressOption?.data?.byteLength === 4) assignedIp = [...addressOption.data].join('.'); ipcpFinished = true; } } } if (!assignedIp) throw new Error('SSTP did not assign an IPv4 address'); return assignedIp; }; const close = () => { try { reader?.cancel?.().catch?.(() => { }); } catch (e) { } try { reader?.releaseLock?.(); } catch (e) { } try { writer?.close?.().catch?.(() => { }); } catch (e) { } try { writer?.releaseLock?.(); } catch (e) { } try { socket?.close?.(); } catch (e) { } }; return { connect: connectToServer, establishTunnel, readPacket, parsePPPFrame, close, get bufferedBytes() { return bufferedBytes; }, get writer() { return writer; } }; } function createTcpOverPppSession(sstp, sourceIp, destinationIp, destinationPort) { const sourcePort = 10000 + (randomSstpUint16() % 50000); const sourceAddress = ipv4ToBytes(sourceIp); const destinationAddress = ipv4ToBytes(destinationIp); let sequenceNumber = randomSstpUint32(); let acknowledgementNumber = 0; const ipHeaderTemplate = new Uint8Array(20); ipHeaderTemplate.set([0x45, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 64, 6]); ipHeaderTemplate.set(sourceAddress, 12); ipHeaderTemplate.set(destinationAddress, 16); const tcpPseudoHeader = new Uint8Array(1432); tcpPseudoHeader.set(sourceAddress); tcpPseudoHeader.set(destinationAddress, 4); tcpPseudoHeader[9] = 6; const buildTcpFrame = (flags, payload = SSTP_EMPTY_BYTES) => { const bytes = 数据转Uint8Array(payload); const payloadLength = bytes.byteLength; const tcpLength = 20 + payloadLength; const ipLength = 20 + tcpLength; const sstpLength = 8 + ipLength; const frame = new Uint8Array(sstpLength); const view = new DataView(frame.buffer); frame.set([0x10, 0x00, ((sstpLength >> 8) & 0x0f) | 0x80, sstpLength & 0xff, 0xff, 0x03, 0x00, 0x21]); frame.set(ipHeaderTemplate, 8); view.setUint16(10, ipLength); view.setUint16(12, randomSstpUint16()); view.setUint16(18, internetChecksum(frame, 8, 20)); view.setUint16(28, sourcePort); view.setUint16(30, destinationPort); view.setUint32(32, sequenceNumber); view.setUint32(36, acknowledgementNumber); frame[40] = 0x50; frame[41] = flags; view.setUint16(42, 65535); if (payloadLength) frame.set(bytes, 48); tcpPseudoHeader[10] = tcpLength >> 8; tcpPseudoHeader[11] = tcpLength & 0xff; tcpPseudoHeader.set(frame.subarray(28, 28 + tcpLength), 12); view.setUint16(44, internetChecksum(tcpPseudoHeader, 0, 12 + tcpLength)); return frame; }; const matchIncomingIpPacket = ipPacket => { if (ipPacket.byteLength < 40 || ipPacket[9] !== 6) return null; const ipHeaderLength = (ipPacket[0] & 0x0f) * 4; if (ipPacket.byteLength < ipHeaderLength + 20) return null; if (readSstpUint16(ipPacket, ipHeaderLength) !== destinationPort) return null; if (readSstpUint16(ipPacket, ipHeaderLength + 2) !== sourcePort) return null; return { flags: ipPacket[ipHeaderLength + 13], sequence: readSstpUint32(ipPacket, ipHeaderLength + 4), payloadOffset: ipHeaderLength + ((ipPacket[ipHeaderLength + 12] >> 4) & 0x0f) * 4 }; }; const handshake = async () => { await withTimeout(sstp.writer.write(buildTcpFrame(0x02)), CONNECT_TIMEOUT_MS, 'SSTP TCP SYN write timed out'); sequenceNumber = (sequenceNumber + 1) >>> 0; for (let attempt = 0; attempt < 30; attempt++) { const packet = await sstp.readPacket(CONNECT_TIMEOUT_MS); if (packet.isControl) continue; const ppp = sstp.parsePPPFrame(packet.body); if (!ppp || ppp.protocol !== 0x0021) continue; const tcp = matchIncomingIpPacket(ppp.ipPacket); if (!tcp || (tcp.flags & 0x12) !== 0x12) continue; acknowledgementNumber = (tcp.sequence + 1) >>> 0; await withTimeout(sstp.writer.write(buildTcpFrame(0x10)), CONNECT_TIMEOUT_MS, 'SSTP TCP ACK write timed out'); return; } throw new Error('TCP handshake through SSTP timed out'); }; return { buildTcpFrame, matchIncomingIpPacket, handshake, get sequenceNumber() { return sequenceNumber; }, set sequenceNumber(value) { sequenceNumber = value >>> 0; }, get acknowledgementNumber() { return acknowledgementNumber; }, set acknowledgementNumber(value) { acknowledgementNumber = value >>> 0; } }; } async function sstpConnect(proxy, targetHost, targetPort) { const sstp = createSstpClient(proxy); let closedSettled = false; let resolveClosed; let rejectClosed; const closed = new Promise((resolve, reject) => { resolveClosed = resolve; rejectClosed = reject; }); const settleClosed = (settle, value) => { if (closedSettled) return; closedSettled = true; settle(value); }; const close = () => { try { sstp.close(); } catch (e) { } settleClosed(resolveClosed); }; try { await sstp.connect(proxy.hostname, proxy.port); const targetIpPromise = resolveSstpTargetIPv4(targetHost); const [sourceIp, targetIp] = await Promise.all([ sstp.establishTunnel(), targetIpPromise ]); const tcp = createTcpOverPppSession(sstp, sourceIp, targetIp, targetPort); await tcp.handshake(); /** @type {ReadableStreamDefaultController | null} */ let streamController = null; const readable = new ReadableStream({ start(controller) { streamController = controller; }, cancel() { close(); } }); (async () => { try { let pendingChunks = []; let pendingLength = 0; const flush = () => { if (!pendingLength) return; const controller = streamController; if (!controller) throw new Error('SSTP readable stream is not ready'); controller.enqueue(pendingChunks.length === 1 ? pendingChunks[0] : concatUint8(...pendingChunks)); pendingChunks = []; pendingLength = 0; sstp.writer.write(tcp.buildTcpFrame(0x10)).catch(() => { }); }; for (; ;) { const packet = await sstp.readPacket(60000); if (packet.isControl) continue; const ppp = sstp.parsePPPFrame(packet.body); if (!ppp || ppp.protocol !== 0x0021) continue; const incoming = tcp.matchIncomingIpPacket(ppp.ipPacket); if (!incoming) continue; if (incoming.payloadOffset < ppp.ipPacket.byteLength) { const payload = ppp.ipPacket.subarray(incoming.payloadOffset); if (payload.byteLength) { tcp.acknowledgementNumber = (incoming.sequence + payload.byteLength) >>> 0; pendingChunks.push(new Uint8Array(payload)); pendingLength += payload.byteLength; } } if (incoming.flags & 0x01) { flush(); tcp.acknowledgementNumber = (tcp.acknowledgementNumber + 1) >>> 0; sstp.writer.write(tcp.buildTcpFrame(0x11)).catch(() => { }); try { streamController?.close?.(); } catch (e) { } close(); return; } if (sstp.bufferedBytes.byteLength < 4 || pendingLength >= 32768) flush(); } } catch (error) { try { streamController?.error?.(error); } catch (e) { } settleClosed(rejectClosed, error); try { sstp.close(); } catch (e) { } } })(); const writable = new WritableStream({ async write(chunk) { const bytes = 数据转Uint8Array(chunk); if (!bytes.byteLength) return; if (bytes.byteLength <= SSTP_TCP_MSS) { await sstp.writer.write(tcp.buildTcpFrame(0x18, bytes)); tcp.sequenceNumber = (tcp.sequenceNumber + bytes.byteLength) >>> 0; return; } const frames = []; for (let offset = 0; offset < bytes.byteLength; offset += SSTP_TCP_MSS) { const segment = bytes.subarray(offset, Math.min(offset + SSTP_TCP_MSS, bytes.byteLength)); frames.push(tcp.buildTcpFrame(0x18, segment)); tcp.sequenceNumber = (tcp.sequenceNumber + segment.byteLength) >>> 0; } await sstp.writer.write(concatUint8(...frames)); }, close() { return sstp.writer.write(tcp.buildTcpFrame(0x11)).catch(() => { }); }, abort(error) { close(); if (error) settleClosed(rejectClosed, error); } }); return { readable, writable, closed, close }; } catch (error) { close(); throw error; } } function parseProxyAddress(input, type, defaultPort = 80) { let address = stripProxyScheme(String(input || '').trim()).split('#')[0].trim(); if (!address) throw new Error('Proxy address cannot be empty'); const atIndex = address.lastIndexOf('@'); let authPart = ''; let hostPart = address; if (atIndex !== -1) { authPart = address.slice(0, atIndex).replaceAll('%3D', '='); if (!authPart.includes(':') && BASE64_AUTH_RE.test(authPart)) { try { authPart = atob(authPart); } catch (e) { } } hostPart = address.slice(atIndex + 1); } let username = null; let password = null; if (authPart) { const colonIndex = authPart.indexOf(':'); if (colonIndex === -1) throw new Error('Proxy auth must use username:password format'); username = safeDecode(authPart.slice(0, colonIndex)); password = safeDecode(authPart.slice(colonIndex + 1)); } hostPart = hostPart.split('/')[0].trim(); let hostname = hostPart; let port = defaultPort; if (hostPart.startsWith('[')) { const closeIndex = hostPart.indexOf(']'); if (closeIndex === -1) throw new Error('IPv6 address is missing the closing bracket'); hostname = hostPart.slice(0, closeIndex + 1); if (hostPart.slice(closeIndex + 1).startsWith(':')) { port = Number(hostPart.slice(closeIndex + 2).replace(/[^\d]/g, '')); } } else { const colonCount = (hostPart.match(/:/g) || []).length; if (colonCount === 1) { const separator = hostPart.lastIndexOf(':'); hostname = hostPart.slice(0, separator); port = Number(hostPart.slice(separator + 1).replace(/[^\d]/g, '')); } else if (colonCount > 1) { throw new Error('IPv6 address must be wrapped in brackets, for example [2001:db8::1]:1080'); } } hostname = safeDecode(hostname.trim()); if (!hostname) throw new Error('Missing proxy hostname'); if (!Number.isInteger(port) || port < 1 || port > 65535) throw new Error('Port must be a number between 1 and 65535'); if (hostname.includes(':') && !hostname.startsWith('[')) throw new Error('IPv6 address must be wrapped in brackets'); return { type, username, password, hostname, port }; } const BASE64_AUTH_RE = /^(?:[A-Z0-9+/]{4})*(?:[A-Z0-9+/]{2}==|[A-Z0-9+/]{3}=)?$/i; function splitProxyScheme(input) { const text = String(input || '').trim(); const match = text.match(/^(socks5|http|https|turn|sstp):\/\/(.+)$/i); return match ? { type: match[1].toLowerCase(), rest: match[2] } : null; } function stripProxyScheme(input) { return String(input || '').replace(/^(socks5|http|https|turn|sstp):\/\//i, ''); } function formatProxyAuthority(proxy) { const auth = proxy.username !== null && proxy.password !== null ? `${proxy.username}:${proxy.password}@` : ''; return `${auth}${proxy.hostname}:${proxy.port}`; } function safeDecode(value) { try { return decodeURIComponent(value); } catch (e) { return value; } } async function withTimeout(promise, timeoutMs, message) { let timer; try { return await Promise.race([ promise, new Promise((_, reject) => { timer = setTimeout(() => reject(new Error(message)), timeoutMs); }) ]); } finally { clearTimeout(timer); } } function 数据转Uint8Array(data) { if (data instanceof Uint8Array) return data; if (data instanceof ArrayBuffer) return new Uint8Array(data); if (ArrayBuffer.isView(data)) return new Uint8Array(data.buffer, data.byteOffset, data.byteLength); return new Uint8Array(data || 0); } function 有效数据长度(data) { if (!data) return 0; if (typeof data.byteLength === 'number') return data.byteLength; if (typeof data.length === 'number') return data.length; return 0; } function concatUint8(...chunks) { const arrays = chunks.filter(Boolean).map(数据转Uint8Array); const length = arrays.reduce((sum, chunk) => sum + chunk.byteLength, 0); const output = new Uint8Array(length); let offset = 0; for (const chunk of arrays) { output.set(chunk, offset); offset += chunk.byteLength; } return output; } function stripIPv6Brackets(hostname = '') { const host = String(hostname || '').trim(); return host.startsWith('[') && host.endsWith(']') ? host.slice(1, -1) : host; } function isIPHostname(hostname = '') { const host = stripIPv6Brackets(hostname); if (isIPv4(host)) return true; if (!host.includes(':')) return false; try { new URL(`http://[${host}]/`); return true; } catch (e) { return false; } } function isIPv4(value) { const parts = String(value || '').split('.'); return parts.length === 4 && parts.every(part => { if (!/^\d{1,3}$/.test(part)) return false; const num = Number(part); return num >= 0 && num <= 255; }); } async function handleResolve(input) { let text = String(input || '').trim().split('#')[0].trim(); const proxy = splitProxyScheme(text); // sstp/https 依赖域名建立 TLS(SNI),须保留原始 host,不做 DNS 展平 if (proxy?.type === 'sstp' || proxy?.type === 'https') { const parsed = parseProxyAddress(text, proxy.type, DEFAULT_PORTS[proxy.type]); return [`${parsed.hostname}:${parsed.port}`]; } if (proxy) text = proxy.rest; if (text.includes('@')) text = text.slice(text.lastIndexOf('@') + 1); let host = text; let port = 443; if (host.startsWith('[')) { const ipv6PortIndex = host.lastIndexOf(']:'); if (ipv6PortIndex !== -1) { const maybePort = Number(host.slice(ipv6PortIndex + 2)); if (Number.isInteger(maybePort) && maybePort >= 1 && maybePort <= 65535) { port = maybePort; host = host.slice(0, ipv6PortIndex + 1); } } } else { const colonMatches = host.match(/:/g) || []; if (colonMatches.length === 1) { const separatorIndex = host.lastIndexOf(':'); const maybePort = Number(host.slice(separatorIndex + 1)); if (Number.isInteger(maybePort) && maybePort >= 1 && maybePort <= 65535) { port = maybePort; host = host.slice(0, separatorIndex); } } } const bracketedIPv6 = host.startsWith('[') && host.endsWith(']'); const rawIPv6 = /^[0-9a-fA-F:]+$/.test(host) && host.includes(':'); if (isIPv4(host) || bracketedIPv6 || rawIPv6) { const finalHost = rawIPv6 && !bracketedIPv6 ? `[${host}]` : host; return [`${finalHost}:${port}`]; } let [aRecords, aaaaRecords] = await Promise.all([ DoH查询(host, 'A'), DoH查询(host, 'AAAA') ]); let results = []; for (const record of aRecords.filter(item => item.type === 1 && item.data)) results.push(`${record.data}:${port}`); for (const record of aaaaRecords.filter(item => item.type === 28 && item.data)) results.push(`[${record.data}]:${port}`); if (!results.length) throw new Error('Could not resolve domain'); return uniqueStrings(results); } function uniqueStrings(values) { const seen = new Set(); return values.filter(value => { if (seen.has(value)) return false; seen.add(value); return true; }); } function normalizeTxtValue(value) { const text = String(value ?? '').trim(); if (text.startsWith('"') && text.endsWith('"')) return text.slice(1, -1).replace(/\\"/g, '"'); return text.replace(/\\"/g, '"'); } async function DoH查询(name, type, endpoint = 'https://cloudflare-dns.com/dns-query') { try { const response = await fetch(`${endpoint}?name=${encodeURIComponent(name)}&type=${encodeURIComponent(type)}`, { headers: { accept: 'application/dns-json' } }); if (!response.ok) return []; const payload = await response.json(); if (!Array.isArray(payload.Answer)) return []; return payload.Answer.map(answer => ({ name: answer.name || name, type: answer.type, TTL: answer.TTL, data: answer.type === 16 ? normalizeTxtValue(answer.data) : answer.data })); } catch (error) { return []; } } function generateHTML(备案内容) { return ` Check Socks5

Workspace

开始检测

输入单个代理链接、IP:端口、域名:端口或一整段列表,支持 SOCKS5 / HTTP / HTTPS / TURN / SSTP。缺少协议头时会自动按 socks5:// 处理。

实时解析与验证

单条模式支持历史快速回填,按 Enter 可以直接开始检测。

批量检测
Single / 单目标

Results

检测结果

结果、落地 IP 和地图会在这里按检测进度逐步展开。

Idle

等待开始检测

输入目标后,检测结果、出口信息和地图会在这里展示。

${备案内容}
`; } ////////////////////////////////////////////TLSClient by: @Alexandre_Kojeve//////////////////////////////////////////////// const TLS_VERSION_10 = 769, TLS_VERSION_12 = 771, TLS_VERSION_13 = 772; const CONTENT_TYPE_CHANGE_CIPHER_SPEC = 20, CONTENT_TYPE_ALERT = 21, CONTENT_TYPE_HANDSHAKE = 22, CONTENT_TYPE_APPLICATION_DATA = 23; const HANDSHAKE_TYPE_CLIENT_HELLO = 1, HANDSHAKE_TYPE_SERVER_HELLO = 2, HANDSHAKE_TYPE_NEW_SESSION_TICKET = 4, HANDSHAKE_TYPE_ENCRYPTED_EXTENSIONS = 8, HANDSHAKE_TYPE_CERTIFICATE = 11, HANDSHAKE_TYPE_SERVER_KEY_EXCHANGE = 12, HANDSHAKE_TYPE_CERTIFICATE_REQUEST = 13, HANDSHAKE_TYPE_SERVER_HELLO_DONE = 14, HANDSHAKE_TYPE_CERTIFICATE_VERIFY = 15, HANDSHAKE_TYPE_CLIENT_KEY_EXCHANGE = 16, HANDSHAKE_TYPE_FINISHED = 20, HANDSHAKE_TYPE_KEY_UPDATE = 24; const EXT_SERVER_NAME = 0, EXT_SUPPORTED_GROUPS = 10, EXT_EC_POINT_FORMATS = 11, EXT_SIGNATURE_ALGORITHMS = 13, EXT_APPLICATION_LAYER_PROTOCOL_NEGOTIATION = 16, EXT_SUPPORTED_VERSIONS = 43, EXT_PSK_KEY_EXCHANGE_MODES = 45, EXT_KEY_SHARE = 51; const ALERT_CLOSE_NOTIFY = 0, ALERT_LEVEL_WARNING = 1, ALERT_UNRECOGNIZED_NAME = 112; const shouldIgnoreTlsAlert = fragment => fragment?.[0] === ALERT_LEVEL_WARNING && fragment?.[1] === ALERT_UNRECOGNIZED_NAME; const textEncoder = new TextEncoder(); const textDecoder = new TextDecoder(); const EMPTY_BYTES = new Uint8Array(0); const CIPHER_SUITES_BY_ID = new Map([ [4865, { id: 4865, keyLen: 16, ivLen: 12, hash: "SHA-256", tls13: !0 }], [4866, { id: 4866, keyLen: 32, ivLen: 12, hash: "SHA-384", tls13: !0 }], [4867, { id: 4867, keyLen: 32, ivLen: 12, hash: "SHA-256", tls13: !0, chacha: !0 }], [49199, { id: 49199, keyLen: 16, ivLen: 4, hash: "SHA-256", kex: "ECDHE" }], [49200, { id: 49200, keyLen: 32, ivLen: 4, hash: "SHA-384", kex: "ECDHE" }], [52392, { id: 52392, keyLen: 32, ivLen: 12, hash: "SHA-256", kex: "ECDHE", chacha: !0 }], [49195, { id: 49195, keyLen: 16, ivLen: 4, hash: "SHA-256", kex: "ECDHE" }], [49196, { id: 49196, keyLen: 32, ivLen: 4, hash: "SHA-384", kex: "ECDHE" }], [52393, { id: 52393, keyLen: 32, ivLen: 12, hash: "SHA-256", kex: "ECDHE", chacha: !0 }] ]); const GROUPS_BY_ID = new Map([[29, "X25519"], [23, "P-256"]]); const SUPPORTED_SIGNATURE_ALGORITHMS = [2052, 2053, 2054, 1025, 1281, 1537, 1027, 1283, 1539]; const tlsBytes = (...parts) => { const flattenBytes = values => values.flatMap(value => value instanceof Uint8Array ? [...value] : Array.isArray(value) ? flattenBytes(value) : "number" == typeof value ? [value] : []); return new Uint8Array(flattenBytes(parts)) }; const uint16be = value => [value >> 8 & 255, 255 & value]; const readUint16 = (buffer, offset) => buffer[offset] << 8 | buffer[offset + 1]; const readUint24 = (buffer, offset) => buffer[offset] << 16 | buffer[offset + 1] << 8 | buffer[offset + 2]; const concatBytes = (...chunks) => { const nonEmptyChunks = chunks.filter((chunk => chunk && chunk.length > 0)), length = nonEmptyChunks.reduce(((total, chunk) => total + chunk.length), 0), result = new Uint8Array(length); let offset = 0; for (const chunk of nonEmptyChunks) result.set(chunk, offset), offset += chunk.length; return result }; const randomBytes = length => crypto.getRandomValues(new Uint8Array(length)); const constantTimeEqual = (left, right) => { if (!left || !right || left.length !== right.length) return !1; let diff = 0; for (let index = 0; index < left.length; index++) diff |= left[index] ^ right[index]; return 0 === diff }; const hashByteLength = hash => "SHA-512" === hash ? 64 : "SHA-384" === hash ? 48 : 32; async function hmac(hash, key, data) { const cryptoKey = await crypto.subtle.importKey("raw", key, { name: "HMAC", hash }, !1, ["sign"]); return new Uint8Array(await crypto.subtle.sign("HMAC", cryptoKey, data)) } async function digestBytes(hash, data) { return new Uint8Array(await crypto.subtle.digest(hash, data)) } async function tls12Prf(secret, label, seed, length, hash = "SHA-256") { const labelSeed = concatBytes(textEncoder.encode(label), seed); let output = new Uint8Array(0), currentA = labelSeed; for (; output.length < length;) { currentA = await hmac(hash, secret, currentA); const block = await hmac(hash, secret, concatBytes(currentA, labelSeed)); output = concatBytes(output, block) } return output.slice(0, length) } async function hkdfExtract(hash, salt, inputKeyMaterial) { return salt && salt.length || (salt = new Uint8Array(hashByteLength(hash))), hmac(hash, salt, inputKeyMaterial) } async function hkdfExpandLabel(hash, secret, label, context, length) { const fullLabel = textEncoder.encode("tls13 " + label); return async function (hash, secret, info, length) { const hashLen = hashByteLength(hash), roundCount = Math.ceil(length / hashLen); let output = new Uint8Array(0), previousBlock = new Uint8Array(0); for (let round = 1; round <= roundCount; round++) previousBlock = await hmac(hash, secret, concatBytes(previousBlock, info, [round])), output = concatBytes(output, previousBlock); return output.slice(0, length) }(hash, secret, tlsBytes(uint16be(length), fullLabel.length, fullLabel, context.length, context), length) } async function generateKeyShare(group = "P-256") { const algorithm = "X25519" === group ? { name: "X25519" } : { name: "ECDH", namedCurve: group }; const keyPair = /** @type {CryptoKeyPair} */ (await crypto.subtle.generateKey(algorithm, !0, ["deriveBits"])); const publicKeyRaw = /** @type {ArrayBuffer} */ (await crypto.subtle.exportKey("raw", keyPair.publicKey)); return { keyPair, publicKeyRaw: new Uint8Array(publicKeyRaw) } } async function deriveSharedSecret(privateKey, peerPublicKey, group = "P-256") { const algorithm = "X25519" === group ? { name: "X25519" } : { name: "ECDH", namedCurve: group }, peerKey = await crypto.subtle.importKey("raw", peerPublicKey, algorithm, !1, []), bits = "P-384" === group ? 384 : "P-521" === group ? 528 : 256; return new Uint8Array(await crypto.subtle.deriveBits(/** @type {any} */({ name: algorithm.name, public: peerKey }), privateKey, bits)) } async function importAesGcmKey(key, usages) { return crypto.subtle.importKey("raw", key, { name: "AES-GCM" }, !1, usages) } async function aesGcmEncryptWithKey(cryptoKey, initializationVector, plaintext, additionalData) { return new Uint8Array(await crypto.subtle.encrypt({ name: "AES-GCM", iv: initializationVector, additionalData, tagLength: 128 }, cryptoKey, plaintext)) } async function aesGcmDecryptWithKey(cryptoKey, initializationVector, ciphertext, additionalData) { return new Uint8Array(await crypto.subtle.decrypt({ name: "AES-GCM", iv: initializationVector, additionalData, tagLength: 128 }, cryptoKey, ciphertext)) } function rotateLeft32(value, bits) { return (value << bits | value >>> 32 - bits) >>> 0 } function chachaQuarterRound(state, indexA, indexB, indexC, indexD) { state[indexA] = state[indexA] + state[indexB] >>> 0, state[indexD] = rotateLeft32(state[indexD] ^ state[indexA], 16), state[indexC] = state[indexC] + state[indexD] >>> 0, state[indexB] = rotateLeft32(state[indexB] ^ state[indexC], 12), state[indexA] = state[indexA] + state[indexB] >>> 0, state[indexD] = rotateLeft32(state[indexD] ^ state[indexA], 8), state[indexC] = state[indexC] + state[indexD] >>> 0, state[indexB] = rotateLeft32(state[indexB] ^ state[indexC], 7) } function chacha20Block(key, counter, nonce) { const state = new Uint32Array(16); state[0] = 1634760805, state[1] = 857760878, state[2] = 2036477234, state[3] = 1797285236; const keyView = new DataView(key.buffer, key.byteOffset, key.byteLength); for (let wordIndex = 0; wordIndex < 8; wordIndex++) state[4 + wordIndex] = keyView.getUint32(4 * wordIndex, !0); state[12] = counter; const nonceView = new DataView(nonce.buffer, nonce.byteOffset, nonce.byteLength); state[13] = nonceView.getUint32(0, !0), state[14] = nonceView.getUint32(4, !0), state[15] = nonceView.getUint32(8, !0); const workingState = new Uint32Array(state); for (let round = 0; round < 10; round++) chachaQuarterRound(workingState, 0, 4, 8, 12), chachaQuarterRound(workingState, 1, 5, 9, 13), chachaQuarterRound(workingState, 2, 6, 10, 14), chachaQuarterRound(workingState, 3, 7, 11, 15), chachaQuarterRound(workingState, 0, 5, 10, 15), chachaQuarterRound(workingState, 1, 6, 11, 12), chachaQuarterRound(workingState, 2, 7, 8, 13), chachaQuarterRound(workingState, 3, 4, 9, 14); for (let wordIndex = 0; wordIndex < 16; wordIndex++) workingState[wordIndex] = workingState[wordIndex] + state[wordIndex] >>> 0; return new Uint8Array(workingState.buffer.slice(0)) } function chacha20Xor(key, nonce, data) { const output = new Uint8Array(data.length); let counter = 1; for (let offset = 0; offset < data.length; offset += 64) { const block = chacha20Block(key, counter++, nonce), blockLength = Math.min(64, data.length - offset); for (let index = 0; index < blockLength; index++) output[offset + index] = data[offset + index] ^ block[index] } return output } function poly1305Mac(key, message) { const rKey = function (rBytes) { const clamped = new Uint8Array(rBytes); return clamped[3] &= 15, clamped[7] &= 15, clamped[11] &= 15, clamped[15] &= 15, clamped[4] &= 252, clamped[8] &= 252, clamped[12] &= 252, clamped }(key.slice(0, 16)), sKey = key.slice(16, 32); let accumulator = [0n, 0n, 0n, 0n, 0n]; const rLimbs = [0x3ffffffn & BigInt(rKey[0] | rKey[1] << 8 | rKey[2] << 16 | rKey[3] << 24), 0x3ffffffn & BigInt(rKey[3] >> 2 | rKey[4] << 6 | rKey[5] << 14 | rKey[6] << 22), 0x3ffffffn & BigInt(rKey[6] >> 4 | rKey[7] << 4 | rKey[8] << 12 | rKey[9] << 20), 0x3ffffffn & BigInt(rKey[9] >> 6 | rKey[10] << 2 | rKey[11] << 10 | rKey[12] << 18), 0x3ffffffn & BigInt(rKey[13] | rKey[14] << 8 | rKey[15] << 16)]; for (let offset = 0; offset < message.length; offset += 16) { const chunk = message.slice(offset, offset + 16), paddedChunk = new Uint8Array(17); paddedChunk.set(chunk), paddedChunk[chunk.length] = 1, accumulator[0] += BigInt(paddedChunk[0] | paddedChunk[1] << 8 | paddedChunk[2] << 16 | (3 & paddedChunk[3]) << 24), accumulator[1] += BigInt(paddedChunk[3] >> 2 | paddedChunk[4] << 6 | paddedChunk[5] << 14 | (15 & paddedChunk[6]) << 22), accumulator[2] += BigInt(paddedChunk[6] >> 4 | paddedChunk[7] << 4 | paddedChunk[8] << 12 | (63 & paddedChunk[9]) << 20), accumulator[3] += BigInt(paddedChunk[9] >> 6 | paddedChunk[10] << 2 | paddedChunk[11] << 10 | paddedChunk[12] << 18), accumulator[4] += BigInt(paddedChunk[13] | paddedChunk[14] << 8 | paddedChunk[15] << 16 | paddedChunk[16] << 24); const product = [0n, 0n, 0n, 0n, 0n]; for (let accIndex = 0; accIndex < 5; accIndex++) for (let rIndex = 0; rIndex < 5; rIndex++) { const limbIndex = accIndex + rIndex; limbIndex < 5 ? product[limbIndex] += accumulator[accIndex] * rLimbs[rIndex] : product[limbIndex - 5] += accumulator[accIndex] * rLimbs[rIndex] * 5n } let carry = 0n; for (let index = 0; index < 5; index++) product[index] += carry, accumulator[index] = 0x3ffffffn & product[index], carry = product[index] >> 26n; accumulator[0] += 5n * carry, carry = accumulator[0] >> 26n, accumulator[0] &= 0x3ffffffn, accumulator[1] += carry } let tagValue = accumulator[0] | accumulator[1] << 26n | accumulator[2] << 52n | accumulator[3] << 78n | accumulator[4] << 104n; tagValue = tagValue + sKey.reduce(((total, byte, index) => total + (BigInt(byte) << BigInt(8 * index))), 0n) & (1n << 128n) - 1n; const tag = new Uint8Array(16); for (let index = 0; index < 16; index++) tag[index] = Number(tagValue >> BigInt(8 * index) & 0xffn); return tag } function chacha20Poly1305Encrypt(key, nonce, plaintext, additionalData) { const polyKey = chacha20Block(key, 0, nonce).slice(0, 32), ciphertext = chacha20Xor(key, nonce, plaintext), aadPadding = (16 - additionalData.length % 16) % 16, ciphertextPadding = (16 - ciphertext.length % 16) % 16, macData = new Uint8Array(additionalData.length + aadPadding + ciphertext.length + ciphertextPadding + 16); macData.set(additionalData, 0), macData.set(ciphertext, additionalData.length + aadPadding); const lengthView = new DataView(macData.buffer, additionalData.length + aadPadding + ciphertext.length + ciphertextPadding); lengthView.setBigUint64(0, BigInt(additionalData.length), !0), lengthView.setBigUint64(8, BigInt(ciphertext.length), !0); const tag = poly1305Mac(polyKey, macData); return concatBytes(ciphertext, tag) } function chacha20Poly1305Decrypt(key, nonce, ciphertext, additionalData) { if (ciphertext.length < 16) throw new Error("Ciphertext too short"); const tag = ciphertext.slice(-16), encryptedData = ciphertext.slice(0, -16), polyKey = chacha20Block(key, 0, nonce).slice(0, 32), aadPadding = (16 - additionalData.length % 16) % 16, ciphertextPadding = (16 - encryptedData.length % 16) % 16, macData = new Uint8Array(additionalData.length + aadPadding + encryptedData.length + ciphertextPadding + 16); macData.set(additionalData, 0), macData.set(encryptedData, additionalData.length + aadPadding); const lengthView = new DataView(macData.buffer, additionalData.length + aadPadding + encryptedData.length + ciphertextPadding); lengthView.setBigUint64(0, BigInt(additionalData.length), !0), lengthView.setBigUint64(8, BigInt(encryptedData.length), !0); const expectedTag = poly1305Mac(polyKey, macData); let diff = 0; for (let index = 0; index < 16; index++) diff |= tag[index] ^ expectedTag[index]; if (0 !== diff) throw new Error("ChaCha20-Poly1305 authentication failed"); return chacha20Xor(key, nonce, encryptedData) } const TLS_MAX_PLAINTEXT_FRAGMENT = 16 * 1024; function buildTlsRecord(contentType, fragment, version = TLS_VERSION_12) { const data = 数据转Uint8Array(fragment); const record = new Uint8Array(5 + data.byteLength); record[0] = contentType; record[1] = version >> 8 & 255; record[2] = version & 255; record[3] = data.byteLength >> 8 & 255; record[4] = data.byteLength & 255; record.set(data, 5); return record; } function buildHandshakeMessage(handshakeType, body) { return tlsBytes(handshakeType, (length => [length >> 16 & 255, length >> 8 & 255, 255 & length])(body.length), body) } class TlsRecordParser { constructor() { this.buffer = new Uint8Array(0) } feed(chunk) { const bytes = 数据转Uint8Array(chunk); this.buffer = this.buffer.length ? concatBytes(this.buffer, bytes) : bytes } next() { if (this.buffer.length < 5) return null; const contentType = this.buffer[0], version = readUint16(this.buffer, 1), length = readUint16(this.buffer, 3); if (this.buffer.length < 5 + length) return null; const fragment = this.buffer.subarray(5, 5 + length); return this.buffer = this.buffer.subarray(5 + length), { type: contentType, version, length, fragment } } } class TlsHandshakeParser { constructor() { this.buffer = new Uint8Array(0) } feed(chunk) { const bytes = 数据转Uint8Array(chunk); this.buffer = this.buffer.length ? concatBytes(this.buffer, bytes) : bytes } next() { if (this.buffer.length < 4) return null; const handshakeType = this.buffer[0], length = readUint24(this.buffer, 1); if (this.buffer.length < 4 + length) return null; const body = this.buffer.subarray(4, 4 + length), raw = this.buffer.subarray(0, 4 + length); return this.buffer = this.buffer.subarray(4 + length), { type: handshakeType, length, body, raw } } } function parseServerHello(body) { let offset = 0; const legacyVersion = readUint16(body, offset); offset += 2; const serverRandom = body.slice(offset, offset + 32); offset += 32; const sessionIdLength = body[offset++], sessionId = body.slice(offset, offset + sessionIdLength); offset += sessionIdLength; const cipherSuite = readUint16(body, offset); offset += 2; const compression = body[offset++]; let selectedVersion = legacyVersion, keyShare = null, alpn = null; if (offset < body.length) { const extensionsLength = readUint16(body, offset); offset += 2; const extensionsEnd = offset + extensionsLength; for (; offset + 4 <= extensionsEnd;) { const extensionType = readUint16(body, offset); offset += 2; const extensionLength = readUint16(body, offset); offset += 2; const extensionData = body.slice(offset, offset + extensionLength); if (offset += extensionLength, extensionType === EXT_SUPPORTED_VERSIONS && extensionLength >= 2) selectedVersion = readUint16(extensionData, 0); else if (extensionType === EXT_KEY_SHARE && extensionLength >= 4) { const group = readUint16(extensionData, 0), keyLength = readUint16(extensionData, 2); keyShare = { group, key: extensionData.slice(4, 4 + keyLength) } } else extensionType === EXT_APPLICATION_LAYER_PROTOCOL_NEGOTIATION && extensionLength >= 3 && (alpn = textDecoder.decode(extensionData.slice(3, 3 + extensionData[2]))) } } const helloRetryRequestRandom = new Uint8Array([207, 33, 173, 116, 229, 154, 97, 17, 190, 29, 140, 2, 30, 101, 184, 145, 194, 162, 17, 22, 122, 187, 140, 94, 7, 158, 9, 226, 200, 168, 51, 156]); return { version: legacyVersion, serverRandom, sessionId, cipherSuite, compression, selectedVersion, keyShare, alpn, isHRR: constantTimeEqual(serverRandom, helloRetryRequestRandom), isTls13: selectedVersion === TLS_VERSION_13 } } function parseServerKeyExchange(body) { let offset = 1; const namedCurve = readUint16(body, offset); offset += 2; const keyLength = body[offset++]; return { namedCurve, serverPublicKey: body.slice(offset, offset + keyLength) } } function extractLeafCertificate(body, hasContext = 0) { let offset = 0; if (hasContext) { const contextLength = body[offset++]; offset += contextLength } if (offset + 3 > body.length) return null; const certificateListLength = readUint24(body, offset); if (offset += 3, !certificateListLength || offset + 3 > body.length) return null; const certificateLength = readUint24(body, offset); return offset += 3, certificateLength ? body.slice(offset, offset + certificateLength) : null } function parseEncryptedExtensions(body) { const parsed = { alpn: null }; let offset = 2; const extensionsEnd = 2 + readUint16(body, 0); for (; offset + 4 <= extensionsEnd;) { const extensionType = readUint16(body, offset); offset += 2; const extensionLength = readUint16(body, offset); if (offset += 2, extensionType === EXT_APPLICATION_LAYER_PROTOCOL_NEGOTIATION && extensionLength >= 3) { const protocolLength = body[offset + 2]; protocolLength > 0 && offset + 3 + protocolLength <= offset + extensionLength && (parsed.alpn = textDecoder.decode(body.slice(offset + 3, offset + 3 + protocolLength))) } offset += extensionLength } return parsed } function buildClientHello(clientRandom, serverName, keyShares, { tls13: enableTls13 = !0, tls12: enableTls12 = !0, alpn = null, chacha = !0 } = {}) { const cipherIds = []; enableTls13 && cipherIds.push(4865, 4866, ...(chacha ? [4867] : [])), enableTls12 && cipherIds.push(49199, 49200, 49195, 49196, ...(chacha ? [52392, 52393] : [])); const cipherBytes = tlsBytes(...cipherIds.flatMap(uint16be)), extensions = [tlsBytes(255, 1, 0, 1, 0)]; if (serverName) { const serverNameBytes = textEncoder.encode(serverName), serverNameList = tlsBytes(0, uint16be(serverNameBytes.length), serverNameBytes); extensions.push(tlsBytes(uint16be(EXT_SERVER_NAME), uint16be(serverNameList.length + 2), uint16be(serverNameList.length), serverNameList)) } extensions.push(tlsBytes(uint16be(EXT_EC_POINT_FORMATS), 0, 2, 1, 0)), extensions.push(tlsBytes(uint16be(EXT_SUPPORTED_GROUPS), 0, 6, 0, 4, 0, 29, 0, 23)); const signatureBytes = tlsBytes(...SUPPORTED_SIGNATURE_ALGORITHMS.flatMap(uint16be)); extensions.push(tlsBytes(uint16be(EXT_SIGNATURE_ALGORITHMS), uint16be(signatureBytes.length + 2), uint16be(signatureBytes.length), signatureBytes)); const protocols = Array.isArray(alpn) ? alpn.filter(Boolean) : alpn ? [alpn] : []; if (protocols.length) { const alpnBytes = concatBytes(...protocols.map((protocol => { const protocolBytes = textEncoder.encode(protocol); return tlsBytes(protocolBytes.length, protocolBytes) }))); extensions.push(tlsBytes(uint16be(EXT_APPLICATION_LAYER_PROTOCOL_NEGOTIATION), uint16be(alpnBytes.length + 2), uint16be(alpnBytes.length), alpnBytes)) } if (enableTls13 && keyShares) { let keyShareBytes; if (extensions.push(enableTls12 ? tlsBytes(uint16be(EXT_SUPPORTED_VERSIONS), 0, 5, 4, 3, 4, 3, 3) : tlsBytes(uint16be(EXT_SUPPORTED_VERSIONS), 0, 3, 2, 3, 4)), extensions.push(tlsBytes(uint16be(EXT_PSK_KEY_EXCHANGE_MODES), 0, 2, 1, 1)), keyShares?.x25519 && keyShares?.p256) keyShareBytes = concatBytes(tlsBytes(0, 29, uint16be(keyShares.x25519.length), keyShares.x25519), tlsBytes(0, 23, uint16be(keyShares.p256.length), keyShares.p256)); else if (keyShares?.x25519) keyShareBytes = tlsBytes(0, 29, uint16be(keyShares.x25519.length), keyShares.x25519); else if (keyShares?.p256) keyShareBytes = tlsBytes(0, 23, uint16be(keyShares.p256.length), keyShares.p256); else { if (!(keyShares instanceof Uint8Array)) throw new Error("Invalid keyShares"); keyShareBytes = tlsBytes(0, 23, uint16be(keyShares.length), keyShares) } extensions.push(tlsBytes(uint16be(EXT_KEY_SHARE), uint16be(keyShareBytes.length + 2), uint16be(keyShareBytes.length), keyShareBytes)) } const extensionsBytes = concatBytes(...extensions); return buildHandshakeMessage(HANDSHAKE_TYPE_CLIENT_HELLO, tlsBytes(uint16be(TLS_VERSION_12), clientRandom, 0, uint16be(cipherBytes.length), cipherBytes, 1, 0, uint16be(extensionsBytes.length), extensionsBytes)) } const uint64be = sequenceNumber => { const bytes = new Uint8Array(8); return new DataView(bytes.buffer).setBigUint64(0, sequenceNumber, !1), bytes }, xorSequenceIntoIv = (initializationVector, sequenceNumber) => { const nonce = initializationVector.slice(), sequenceBytes = uint64be(sequenceNumber); for (let index = 0; index < 8; index++) nonce[nonce.length - 8 + index] ^= sequenceBytes[index]; return nonce }, deriveTrafficKeys = (hash, secret, keyLen, ivLen) => Promise.all([hkdfExpandLabel(hash, secret, "key", EMPTY_BYTES, keyLen), hkdfExpandLabel(hash, secret, "iv", EMPTY_BYTES, ivLen)]); class TlsClient { constructor(socket, options = {}) { if (this.socket = socket, this.serverName = options.serverName || "", this.supportTls13 = !1 !== options.tls13, this.supportTls12 = !1 !== options.tls12, !this.supportTls13 && !this.supportTls12) throw new Error("At least one TLS version must be enabled"); this.alpnProtocols = Array.isArray(options.alpn) ? options.alpn : options.alpn ? [options.alpn] : null, this.allowChacha = options.allowChacha !== false, this.timeout = options.timeout ?? 3e4, this.clientRandom = randomBytes(32), this.serverRandom = null, this.handshakeChunks = [], this.handshakeComplete = !1, this.negotiatedAlpn = null, this.cipherSuite = null, this.cipherConfig = null, this.isTls13 = !1, this.masterSecret = null, this.handshakeSecret = null, this.clientWriteKey = null, this.serverWriteKey = null, this.clientWriteIv = null, this.serverWriteIv = 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.clientWriteCryptoKey = null, this.serverWriteCryptoKey = null, this.clientHandshakeCryptoKey = null, this.serverHandshakeCryptoKey = null, this.clientAppCryptoKey = null, this.serverAppCryptoKey = null, this.clientSeqNum = 0n, this.serverSeqNum = 0n, this.recordParser = new TlsRecordParser, this.handshakeParser = new TlsHandshakeParser, this.keyPairs = new Map, this.ecdhKeyPair = null, this.sawCert = !1 } recordHandshake(chunk) { this.handshakeChunks.push(chunk) } transcript() { return 1 === this.handshakeChunks.length ? this.handshakeChunks[0] : concatBytes(...this.handshakeChunks) } getCipherConfig(cipherSuite) { return CIPHER_SUITES_BY_ID.get(cipherSuite) || null } async readChunk(reader) { return this.timeout ? Promise.race([reader.read(), new Promise(((resolve, reject) => setTimeout((() => reject(new Error("TLS read timeout"))), this.timeout)))]) : reader.read() } async readRecordsUntil(reader, predicate, closedError) { for (; ;) { let record; for (; record = this.recordParser.next();) if (await predicate(record)) return; const { value, done } = await this.readChunk(reader); if (done) throw new Error(closedError); this.recordParser.feed(value) } } async readHandshakeUntil(reader, predicate, closedError) { for (let message; message = this.handshakeParser.next();) if (await predicate(message)) return; return this.readRecordsUntil(reader, (async record => { if (record.type === CONTENT_TYPE_ALERT) { if (shouldIgnoreTlsAlert(record.fragment)) return; throw new Error(`TLS Alert: ${record.fragment[1]}`); } if (record.type === CONTENT_TYPE_HANDSHAKE) { this.handshakeParser.feed(record.fragment); for (let message; message = this.handshakeParser.next();) if (await predicate(message)) return 1 } }), closedError) } async acceptCertificate(certificate) { if (!certificate?.length) throw new Error("Empty certificate"); this.sawCert = !0 } async handshake() { const [p256Share, x25519Share] = await Promise.all([generateKeyShare("P-256"), generateKeyShare("X25519")]); this.keyPairs = new Map([[23, p256Share], [29, x25519Share]]), this.ecdhKeyPair = p256Share.keyPair; const reader = this.socket.readable.getReader(), writer = this.socket.writable.getWriter(); try { const clientHello = buildClientHello(this.clientRandom, this.serverName, { x25519: x25519Share.publicKeyRaw, p256: p256Share.publicKeyRaw }, { tls13: this.supportTls13, tls12: this.supportTls12, alpn: this.alpnProtocols, chacha: this.allowChacha }); this.recordHandshake(clientHello), await writer.write(buildTlsRecord(CONTENT_TYPE_HANDSHAKE, clientHello, TLS_VERSION_10)); const serverHello = await this.receiveServerHello(reader); if (serverHello.isHRR) throw new Error("HelloRetryRequest is not supported by TLSClientMini"); if (serverHello.keyShare?.group && this.keyPairs.has(serverHello.keyShare.group)) { const selectedKeyPair = this.keyPairs.get(serverHello.keyShare.group); this.ecdhKeyPair = selectedKeyPair.keyPair } serverHello.isTls13 ? await this.handshakeTls13(reader, writer, serverHello) : await this.handshakeTls12(reader, writer), this.handshakeComplete = !0 } finally { reader.releaseLock(), writer.releaseLock() } } async receiveServerHello(reader) { for (; ;) { const { value, done } = await this.readChunk(reader); if (done) throw new Error("Connection closed waiting for ServerHello"); let record; for (this.recordParser.feed(value); record = this.recordParser.next();) { if (record.type === CONTENT_TYPE_ALERT) { if (shouldIgnoreTlsAlert(record.fragment)) continue; throw new Error(`TLS Alert: level=${record.fragment[0]}, desc=${record.fragment[1]}`); } if (record.type !== CONTENT_TYPE_HANDSHAKE) continue; let message; for (this.handshakeParser.feed(record.fragment); message = this.handshakeParser.next();) { if (message.type !== HANDSHAKE_TYPE_SERVER_HELLO) continue; this.recordHandshake(message.raw); const serverHello = parseServerHello(message.body); if (this.serverRandom = serverHello.serverRandom, this.cipherSuite = serverHello.cipherSuite, this.cipherConfig = this.getCipherConfig(serverHello.cipherSuite), this.isTls13 = serverHello.isTls13, this.negotiatedAlpn = serverHello.alpn || null, !this.cipherConfig) throw new Error(`Unsupported cipher suite: 0x${serverHello.cipherSuite.toString(16)}`); return serverHello } } } } async handshakeTls12(reader, writer) { /** @type {{ namedCurve: number, serverPublicKey: Uint8Array } | null} */ let serverKeyExchange = null; let sawServerHelloDone = !1; let clientCertRequested = !1; if (await this.readHandshakeUntil(reader, (async message => { switch (message.type) { case HANDSHAKE_TYPE_CERTIFICATE: { this.recordHandshake(message.raw); const certificate = extractLeafCertificate(message.body, 1); if (!certificate) throw new Error("Missing TLS 1.2 certificate"); await this.acceptCertificate(certificate); break } case HANDSHAKE_TYPE_SERVER_KEY_EXCHANGE: this.recordHandshake(message.raw), serverKeyExchange = parseServerKeyExchange(message.body); break; case HANDSHAKE_TYPE_SERVER_HELLO_DONE: return this.recordHandshake(message.raw), sawServerHelloDone = !0, 1; case HANDSHAKE_TYPE_CERTIFICATE_REQUEST: this.recordHandshake(message.raw), clientCertRequested = !0; break; default: this.recordHandshake(message.raw) } }), "Connection closed during TLS 1.2 handshake"), !this.sawCert) throw new Error("Missing TLS 1.2 leaf certificate"); const serverKeyExchangeData = /** @type {{ namedCurve: number, serverPublicKey: Uint8Array } | null} */ (serverKeyExchange); if (!serverKeyExchangeData) throw new Error("Missing TLS 1.2 ServerKeyExchange"); const curveName = GROUPS_BY_ID.get(serverKeyExchangeData.namedCurve); if (!curveName) throw new Error(`Unsupported named curve: 0x${serverKeyExchangeData.namedCurve.toString(16)}`); const keyShare = this.keyPairs.get(serverKeyExchangeData.namedCurve); if (!keyShare) throw new Error(`Missing key pair for curve: 0x${serverKeyExchangeData.namedCurve.toString(16)}`); const preMasterSecret = await deriveSharedSecret(keyShare.keyPair.privateKey, serverKeyExchangeData.serverPublicKey, curveName), clientKeyExchange = buildHandshakeMessage(HANDSHAKE_TYPE_CLIENT_KEY_EXCHANGE, tlsBytes(keyShare.publicKeyRaw.length, keyShare.publicKeyRaw)); if (clientCertRequested) { const emptyCertificate = buildHandshakeMessage(HANDSHAKE_TYPE_CERTIFICATE, tlsBytes(0, 0, 0)); this.recordHandshake(emptyCertificate), await writer.write(buildTlsRecord(CONTENT_TYPE_HANDSHAKE, emptyCertificate)) } this.recordHandshake(clientKeyExchange); const hashName = this.cipherConfig.hash; this.masterSecret = await tls12Prf(preMasterSecret, "master secret", concatBytes(this.clientRandom, this.serverRandom), 48, hashName); const keyLen = this.cipherConfig.keyLen, ivLen = this.cipherConfig.ivLen, keyBlock = await tls12Prf(this.masterSecret, "key expansion", concatBytes(this.serverRandom, this.clientRandom), 2 * keyLen + 2 * ivLen, hashName); this.clientWriteKey = keyBlock.slice(0, keyLen), this.serverWriteKey = keyBlock.slice(keyLen, 2 * keyLen), this.clientWriteIv = keyBlock.slice(2 * keyLen, 2 * keyLen + ivLen), this.serverWriteIv = keyBlock.slice(2 * keyLen + ivLen, 2 * keyLen + 2 * ivLen); if (!this.cipherConfig.chacha) [this.clientWriteCryptoKey, this.serverWriteCryptoKey] = await Promise.all([importAesGcmKey(this.clientWriteKey, ["encrypt"]), importAesGcmKey(this.serverWriteKey, ["decrypt"])]); await writer.write(buildTlsRecord(CONTENT_TYPE_HANDSHAKE, clientKeyExchange)), await writer.write(buildTlsRecord(CONTENT_TYPE_CHANGE_CIPHER_SPEC, tlsBytes(1))); const clientVerifyData = await tls12Prf(this.masterSecret, "client finished", await digestBytes(hashName, this.transcript()), 12, hashName), finishedMessage = buildHandshakeMessage(HANDSHAKE_TYPE_FINISHED, clientVerifyData); this.recordHandshake(finishedMessage), await writer.write(buildTlsRecord(CONTENT_TYPE_HANDSHAKE, await this.encryptTls12(finishedMessage, CONTENT_TYPE_HANDSHAKE))); let sawChangeCipherSpec = !1; await this.readRecordsUntil(reader, (async record => { if (record.type === CONTENT_TYPE_ALERT) { if (shouldIgnoreTlsAlert(record.fragment)) return; throw new Error(`TLS Alert: ${record.fragment[1]}`); } if (record.type === CONTENT_TYPE_CHANGE_CIPHER_SPEC) return void (sawChangeCipherSpec = !0); if (record.type !== CONTENT_TYPE_HANDSHAKE || !sawChangeCipherSpec) return; const decrypted = await this.decryptTls12(record.fragment, CONTENT_TYPE_HANDSHAKE); if (decrypted[0] !== HANDSHAKE_TYPE_FINISHED) return; const verifyLength = readUint24(decrypted, 1), verifyData = decrypted.slice(4, 4 + verifyLength), expectedVerifyData = await tls12Prf(this.masterSecret, "server finished", await digestBytes(hashName, this.transcript()), 12, hashName); if (!constantTimeEqual(verifyData, expectedVerifyData)) throw new Error("TLS 1.2 server Finished verify failed"); return 1 }), "Connection closed waiting for TLS 1.2 Finished") } async handshakeTls13(reader, writer, serverHello) { const groupName = GROUPS_BY_ID.get(serverHello.keyShare?.group); if (!groupName || !serverHello.keyShare?.key?.length) throw new Error("Missing TLS 1.3 key_share"); const hashName = this.cipherConfig.hash, hashLen = hashByteLength(hashName), keyLen = this.cipherConfig.keyLen, ivLen = this.cipherConfig.ivLen, sharedSecret = await deriveSharedSecret(this.ecdhKeyPair.privateKey, serverHello.keyShare.key, groupName), earlySecret = await hkdfExtract(hashName, null, new Uint8Array(hashLen)), derivedSecret = await hkdfExpandLabel(hashName, earlySecret, "derived", await digestBytes(hashName, EMPTY_BYTES), hashLen); this.handshakeSecret = await hkdfExtract(hashName, derivedSecret, sharedSecret); const transcriptHash = await digestBytes(hashName, this.transcript()), clientHandshakeTrafficSecret = await hkdfExpandLabel(hashName, this.handshakeSecret, "c hs traffic", transcriptHash, hashLen), serverHandshakeTrafficSecret = await hkdfExpandLabel(hashName, this.handshakeSecret, "s hs traffic", transcriptHash, hashLen); [this.clientHandshakeKey, this.clientHandshakeIv] = await deriveTrafficKeys(hashName, clientHandshakeTrafficSecret, keyLen, ivLen), [this.serverHandshakeKey, this.serverHandshakeIv] = await deriveTrafficKeys(hashName, serverHandshakeTrafficSecret, keyLen, ivLen); if (!this.cipherConfig.chacha) [this.clientHandshakeCryptoKey, this.serverHandshakeCryptoKey] = await Promise.all([importAesGcmKey(this.clientHandshakeKey, ["encrypt"]), importAesGcmKey(this.serverHandshakeKey, ["decrypt"])]); const serverFinishedKey = await hkdfExpandLabel(hashName, serverHandshakeTrafficSecret, "finished", EMPTY_BYTES, hashLen); let serverFinishedReceived = !1; let clientCertRequested = !1; const handleHandshakeMessage = async message => { switch (message.type) { case HANDSHAKE_TYPE_ENCRYPTED_EXTENSIONS: { const encryptedExtensions = parseEncryptedExtensions(message.body); encryptedExtensions.alpn && (this.negotiatedAlpn = encryptedExtensions.alpn), this.recordHandshake(message.raw); break } case HANDSHAKE_TYPE_CERTIFICATE: { const certificate = extractLeafCertificate(message.body); if (!certificate) throw new Error("Missing TLS 1.3 certificate"); await this.acceptCertificate(certificate), this.recordHandshake(message.raw); break } case HANDSHAKE_TYPE_CERTIFICATE_REQUEST: this.recordHandshake(message.raw), clientCertRequested = !0; break; case HANDSHAKE_TYPE_CERTIFICATE_VERIFY: this.recordHandshake(message.raw); break; case HANDSHAKE_TYPE_FINISHED: { const expectedVerifyData = await hmac(hashName, serverFinishedKey, await digestBytes(hashName, this.transcript())); if (!constantTimeEqual(expectedVerifyData, message.body)) throw new Error("TLS 1.3 server Finished verify failed"); this.recordHandshake(message.raw), serverFinishedReceived = !0; break } default: this.recordHandshake(message.raw) } }; await this.readRecordsUntil(reader, (async record => { if (record.type === CONTENT_TYPE_CHANGE_CIPHER_SPEC || record.type === CONTENT_TYPE_HANDSHAKE) return; if (record.type === CONTENT_TYPE_ALERT) { if (shouldIgnoreTlsAlert(record.fragment)) return; throw new Error(`TLS Alert: ${record.fragment[1]}`); } if (record.type !== CONTENT_TYPE_APPLICATION_DATA) return; const decrypted = await this.decryptTls13Handshake(record.fragment), innerType = decrypted[decrypted.length - 1], plaintext = decrypted.slice(0, -1); if (innerType === CONTENT_TYPE_HANDSHAKE) { this.handshakeParser.feed(plaintext); for (let message; message = this.handshakeParser.next();) if (await handleHandshakeMessage(message), serverFinishedReceived) return 1 } }), "Connection closed during TLS 1.3 handshake"); const applicationTranscriptHash = await digestBytes(hashName, this.transcript()), masterDerivedSecret = await hkdfExpandLabel(hashName, this.handshakeSecret, "derived", await digestBytes(hashName, EMPTY_BYTES), hashLen), masterSecret = await hkdfExtract(hashName, masterDerivedSecret, new Uint8Array(hashLen)), clientAppTrafficSecret = await hkdfExpandLabel(hashName, masterSecret, "c ap traffic", applicationTranscriptHash, hashLen), serverAppTrafficSecret = await hkdfExpandLabel(hashName, masterSecret, "s ap traffic", applicationTranscriptHash, hashLen); [this.clientAppKey, this.clientAppIv] = await deriveTrafficKeys(hashName, clientAppTrafficSecret, keyLen, ivLen), [this.serverAppKey, this.serverAppIv] = await deriveTrafficKeys(hashName, serverAppTrafficSecret, keyLen, ivLen); if (!this.cipherConfig.chacha) [this.clientAppCryptoKey, this.serverAppCryptoKey] = await Promise.all([importAesGcmKey(this.clientAppKey, ["encrypt"]), importAesGcmKey(this.serverAppKey, ["decrypt"])]); let clientFlightHandshake = EMPTY_BYTES; if (clientCertRequested) clientFlightHandshake = buildHandshakeMessage(HANDSHAKE_TYPE_CERTIFICATE, tlsBytes(0, 0, 0, 0)), this.recordHandshake(clientFlightHandshake); const clientFinishedKey = await hkdfExpandLabel(hashName, clientHandshakeTrafficSecret, "finished", EMPTY_BYTES, hashLen), clientFinishedVerifyData = await hmac(hashName, clientFinishedKey, await digestBytes(hashName, this.transcript())), clientFinishedMessage = buildHandshakeMessage(HANDSHAKE_TYPE_FINISHED, clientFinishedVerifyData); this.recordHandshake(clientFinishedMessage), await writer.write(buildTlsRecord(CONTENT_TYPE_APPLICATION_DATA, await this.encryptTls13Handshake(concatBytes(clientFlightHandshake, clientFinishedMessage, [CONTENT_TYPE_HANDSHAKE])))), this.clientSeqNum = 0n, this.serverSeqNum = 0n } async encryptTls12(plaintext, contentType) { const sequenceNumber = this.clientSeqNum++, sequenceBytes = uint64be(sequenceNumber), additionalData = concatBytes(sequenceBytes, [contentType], uint16be(TLS_VERSION_12), uint16be(plaintext.length)); if (this.cipherConfig.chacha) { const nonce = xorSequenceIntoIv(this.clientWriteIv, sequenceNumber); return chacha20Poly1305Encrypt(this.clientWriteKey, nonce, plaintext, additionalData) } const explicitNonce = randomBytes(8); if (!this.clientWriteCryptoKey) this.clientWriteCryptoKey = await importAesGcmKey(this.clientWriteKey, ["encrypt"]); return concatBytes(explicitNonce, await aesGcmEncryptWithKey(this.clientWriteCryptoKey, concatBytes(this.clientWriteIv, explicitNonce), plaintext, additionalData)) } async decryptTls12(ciphertext, contentType) { const sequenceNumber = this.serverSeqNum++, sequenceBytes = uint64be(sequenceNumber); if (this.cipherConfig.chacha) { const nonce = xorSequenceIntoIv(this.serverWriteIv, sequenceNumber); return chacha20Poly1305Decrypt(this.serverWriteKey, nonce, ciphertext, concatBytes(sequenceBytes, [contentType], uint16be(TLS_VERSION_12), uint16be(ciphertext.length - 16))) } const explicitNonce = ciphertext.subarray(0, 8), encryptedData = ciphertext.subarray(8); if (!this.serverWriteCryptoKey) this.serverWriteCryptoKey = await importAesGcmKey(this.serverWriteKey, ["decrypt"]); return aesGcmDecryptWithKey(this.serverWriteCryptoKey, concatBytes(this.serverWriteIv, explicitNonce), encryptedData, concatBytes(sequenceBytes, [contentType], uint16be(TLS_VERSION_12), uint16be(encryptedData.length - 16))) } async encryptTls13Handshake(plaintext) { const nonce = xorSequenceIntoIv(this.clientHandshakeIv, this.clientSeqNum++), additionalData = tlsBytes(CONTENT_TYPE_APPLICATION_DATA, 3, 3, uint16be(plaintext.length + 16)); if (this.cipherConfig.chacha) return chacha20Poly1305Encrypt(this.clientHandshakeKey, nonce, plaintext, additionalData); if (!this.clientHandshakeCryptoKey) this.clientHandshakeCryptoKey = await importAesGcmKey(this.clientHandshakeKey, ["encrypt"]); return aesGcmEncryptWithKey(this.clientHandshakeCryptoKey, nonce, plaintext, additionalData) } async decryptTls13Handshake(ciphertext) { const nonce = xorSequenceIntoIv(this.serverHandshakeIv, this.serverSeqNum++), additionalData = tlsBytes(CONTENT_TYPE_APPLICATION_DATA, 3, 3, uint16be(ciphertext.length)); const decrypted = this.cipherConfig.chacha ? await chacha20Poly1305Decrypt(this.serverHandshakeKey, nonce, ciphertext, additionalData) : await aesGcmDecryptWithKey(this.serverHandshakeCryptoKey || (this.serverHandshakeCryptoKey = await importAesGcmKey(this.serverHandshakeKey, ["decrypt"])), nonce, ciphertext, additionalData); let innerTypeIndex = decrypted.length - 1; for (; innerTypeIndex >= 0 && !decrypted[innerTypeIndex];) innerTypeIndex--; return innerTypeIndex < 0 ? EMPTY_BYTES : decrypted.slice(0, innerTypeIndex + 1) } async encryptTls13(data) { const plaintext = concatBytes(data, [CONTENT_TYPE_APPLICATION_DATA]), nonce = xorSequenceIntoIv(this.clientAppIv, this.clientSeqNum++), additionalData = tlsBytes(CONTENT_TYPE_APPLICATION_DATA, 3, 3, uint16be(plaintext.length + 16)); if (this.cipherConfig.chacha) return chacha20Poly1305Encrypt(this.clientAppKey, nonce, plaintext, additionalData); if (!this.clientAppCryptoKey) this.clientAppCryptoKey = await importAesGcmKey(this.clientAppKey, ["encrypt"]); return aesGcmEncryptWithKey(this.clientAppCryptoKey, nonce, plaintext, additionalData) } async decryptTls13(ciphertext) { const nonce = xorSequenceIntoIv(this.serverAppIv, this.serverSeqNum++), additionalData = tlsBytes(CONTENT_TYPE_APPLICATION_DATA, 3, 3, uint16be(ciphertext.length)), plaintext = this.cipherConfig.chacha ? await chacha20Poly1305Decrypt(this.serverAppKey, nonce, ciphertext, additionalData) : await aesGcmDecryptWithKey(this.serverAppCryptoKey || (this.serverAppCryptoKey = await importAesGcmKey(this.serverAppKey, ["decrypt"])), nonce, ciphertext, additionalData); let innerTypeIndex = plaintext.length - 1; for (; innerTypeIndex >= 0 && !plaintext[innerTypeIndex];) innerTypeIndex--; if (innerTypeIndex < 0) return { data: EMPTY_BYTES, type: 0 }; return { data: plaintext.slice(0, innerTypeIndex), type: plaintext[innerTypeIndex] } } async write(data) { if (!this.handshakeComplete) throw new Error("Handshake not complete"); const plaintext = 数据转Uint8Array(data); if (!plaintext.byteLength) return; const writer = this.socket.writable.getWriter(); try { const records = []; for (let offset = 0; offset < plaintext.byteLength; offset += TLS_MAX_PLAINTEXT_FRAGMENT) { const chunk = plaintext.subarray(offset, Math.min(offset + TLS_MAX_PLAINTEXT_FRAGMENT, plaintext.byteLength)); const encrypted = this.isTls13 ? await this.encryptTls13(chunk) : await this.encryptTls12(chunk, CONTENT_TYPE_APPLICATION_DATA); records.push(buildTlsRecord(CONTENT_TYPE_APPLICATION_DATA, encrypted)); } await writer.write(records.length === 1 ? records[0] : concatBytes(...records)) } finally { writer.releaseLock() } } async read() { for (; ;) { let record; for (; record = this.recordParser.next();) { if (record.type === CONTENT_TYPE_ALERT) { if (record.fragment[1] === ALERT_CLOSE_NOTIFY) return null; throw new Error(`TLS Alert: ${record.fragment[1]}`) } if (record.type !== CONTENT_TYPE_APPLICATION_DATA) continue; if (!this.isTls13) return this.decryptTls12(record.fragment, CONTENT_TYPE_APPLICATION_DATA); const { data, type } = await this.decryptTls13(record.fragment); if (type === CONTENT_TYPE_APPLICATION_DATA) return data; if (type === CONTENT_TYPE_ALERT) { if (data[1] === ALERT_CLOSE_NOTIFY) return null; throw new Error(`TLS Alert: ${data[1]}`) } if (type !== CONTENT_TYPE_HANDSHAKE) continue; let message; for (this.handshakeParser.feed(data); message = this.handshakeParser.next();) if (message.type !== HANDSHAKE_TYPE_NEW_SESSION_TICKET && message.type === HANDSHAKE_TYPE_KEY_UPDATE) throw new Error("TLS 1.3 KeyUpdate is not supported by TLSClientMini") } const reader = this.socket.readable.getReader(); try { const { value, done } = await this.readChunk(reader); if (done) return null; this.recordParser.feed(value) } finally { reader.releaseLock() } } } close() { this.socket.close() } }