} A Promise that resolves when the conversion is complete.
507 | */
508 | async function remoteSocketToWS(remoteSocket, webSocket, bbbResponseHeader, retry, log) {
509 | // remote--> ws
510 | let remoteChunkCount = 0;
511 | let chunks = [];
512 | /** @type {ArrayBuffer | null} */
513 | let bbbHeader = bbbResponseHeader;
514 | let hasIncomingData = false; // check if remoteSocket has incoming data
515 | await remoteSocket.readable
516 | .pipeTo(
517 | new WritableStream({
518 | start() {
519 | },
520 | /**
521 | *
522 | * @param {Uint8Array} chunk
523 | * @param {*} controller
524 | */
525 | async write(chunk, controller) {
526 | hasIncomingData = true;
527 | remoteChunkCount++;
528 | if (webSocket.readyState !== WS_READY_STATE_OPEN) {
529 | controller.error(
530 | 'webSocket.readyState is not open, maybe close'
531 | );
532 | }
533 | if (bbbHeader) {
534 | webSocket.send(await new Blob([bbbHeader, chunk]).arrayBuffer());
535 | bbbHeader = null;
536 | } else {
537 | // console.log(`remoteSocketToWS send chunk ${chunk.byteLength}`);
538 | // seems no need rate limit this, CF seems fix this??..
539 | // if (remoteChunkCount > 20000) {
540 | // // cf one package is 4096 byte(4kb), 4096 * 20000 = 80M
541 | // await delay(1);
542 | // }
543 | webSocket.send(chunk);
544 | }
545 | },
546 | close() {
547 | log(`remoteConnection!.readable is close with hasIncomingData is ${hasIncomingData}`);
548 | // safeCloseWebSocket(webSocket); // no need server close websocket frist for some case will casue HTTP ERR_CONTENT_LENGTH_MISMATCH issue, client will send close event anyway.
549 | },
550 | abort(reason) {
551 | console.error(`remoteConnection!.readable abort`, reason);
552 | },
553 | })
554 | )
555 | .catch((error) => {
556 | console.error(
557 | `remoteSocketToWS has exception `,
558 | error.stack || error
559 | );
560 | safeCloseWebSocket(webSocket);
561 | });
562 |
563 | // seems is cf connect socket have error,
564 | // 1. Socket.closed will have error
565 | // 2. Socket.readable will be close without any data coming
566 | if (hasIncomingData === false && retry) {
567 | log(`retry`)
568 | retry();
569 | }
570 | }
571 |
572 | /**
573 | * Decodes a base64 string into an ArrayBuffer.
574 | * @param {string} base64Str The base64 string to decode.
575 | * @returns {{earlyData: ArrayBuffer|null, error: Error|null}} An object containing the decoded ArrayBuffer or null if there was an error, and any error that occurred during decoding or null if there was no error.
576 | */
577 | function base64ToArrayBuffer(base64Str) {
578 | if (!base64Str) {
579 | return { earlyData: null, error: null };
580 | }
581 | try {
582 | // go use modified Base64 for URL rfc4648 which js atob not support
583 | base64Str = base64Str.replace(/-/g, '+').replace(/_/g, '/');
584 | const decode = atob(base64Str);
585 | const arryBuffer = Uint8Array.from(decode, (c) => c.charCodeAt(0));
586 | return { earlyData: arryBuffer.buffer, error: null };
587 | } catch (error) {
588 | return { earlyData: null, error };
589 | }
590 | }
591 |
592 | /**
593 | * Checks if a given string is a valid UUID.
594 | * Note: This is not a real UUID validation.
595 | * @param {string} uuid The string to validate as a UUID.
596 | * @returns {boolean} True if the string is a valid UUID, false otherwise.
597 | */
598 | function isValidUUID(uuid) {
599 | const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[4][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
600 | return uuidRegex.test(uuid);
601 | }
602 |
603 | const WS_READY_STATE_OPEN = 1;
604 | const WS_READY_STATE_CLOSING = 2;
605 | /**
606 | * Closes a WebSocket connection safely without throwing exceptions.
607 | * @param {import("@cloudflare/workers-types").WebSocket} socket The WebSocket connection to close.
608 | */
609 | function safeCloseWebSocket(socket) {
610 | try {
611 | if (socket.readyState === WS_READY_STATE_OPEN || socket.readyState === WS_READY_STATE_CLOSING) {
612 | socket.close();
613 | }
614 | } catch (error) {
615 | console.error('safeCloseWebSocket error', error);
616 | }
617 | }
618 |
619 | const byteToHex = [];
620 |
621 | for (let i = 0; i < 256; ++i) {
622 | byteToHex.push((i + 256).toString(16).slice(1));
623 | }
624 |
625 | function unsafeStringify(arr, offset = 0) {
626 | return (byteToHex[arr[offset + 0]] + byteToHex[arr[offset + 1]] + byteToHex[arr[offset + 2]] + byteToHex[arr[offset + 3]] + "-" + byteToHex[arr[offset + 4]] + byteToHex[arr[offset + 5]] + "-" + byteToHex[arr[offset + 6]] + byteToHex[arr[offset + 7]] + "-" + byteToHex[arr[offset + 8]] + byteToHex[arr[offset + 9]] + "-" + byteToHex[arr[offset + 10]] + byteToHex[arr[offset + 11]] + byteToHex[arr[offset + 12]] + byteToHex[arr[offset + 13]] + byteToHex[arr[offset + 14]] + byteToHex[arr[offset + 15]]).toLowerCase();
627 | }
628 |
629 | function stringify(arr, offset = 0) {
630 | const uuid = unsafeStringify(arr, offset);
631 | if (!isValidUUID(uuid)) {
632 | throw TypeError("Stringified UUID is invalid");
633 | }
634 | return uuid;
635 | }
636 |
637 |
638 | /**
639 | * Handles outbound UDP traffic by transforming the data into DNS queries and sending them over a WebSocket connection.
640 | * @param {import("@cloudflare/workers-types").WebSocket} webSocket The WebSocket connection to send the DNS queries over.
641 | * @param {ArrayBuffer} bbbResponseHeader The bbb response header.
642 | * @param {(string) => void} log The logging function.
643 | * @returns {{write: (chunk: Uint8Array) => void}} An object with a write method that accepts a Uint8Array chunk to write to the transform stream.
644 | */
645 | async function handleUDPOutBound(webSocket, bbbResponseHeader, log) {
646 |
647 | let isbbbHeaderSent = false;
648 | const transformStream = new TransformStream({
649 | start(controller) {
650 |
651 | },
652 | transform(chunk, controller) {
653 | // udp message 2 byte is the the length of udp data
654 | // TODO: this should have bug, beacsue maybe udp chunk can be in two websocket message
655 | for (let index = 0; index < chunk.byteLength;) {
656 | const lengthBuffer = chunk.slice(index, index + 2);
657 | const udpPakcetLength = new DataView(lengthBuffer).getUint16(0);
658 | const udpData = new Uint8Array(
659 | chunk.slice(index + 2, index + 2 + udpPakcetLength)
660 | );
661 | index = index + 2 + udpPakcetLength;
662 | controller.enqueue(udpData);
663 | }
664 | },
665 | flush(controller) {
666 | }
667 | });
668 |
669 | // only handle dns udp for now
670 | transformStream.readable.pipeTo(new WritableStream({
671 | async write(chunk) {
672 | const resp = await fetch(dohURL, // dns server url
673 | {
674 | method: 'POST',
675 | headers: {
676 | 'content-type': 'application/dns-message',
677 | },
678 | body: chunk,
679 | })
680 | const dnsQueryResult = await resp.arrayBuffer();
681 | const udpSize = dnsQueryResult.byteLength;
682 | // console.log([...new Uint8Array(dnsQueryResult)].map((x) => x.toString(16)));
683 | const udpSizeBuffer = new Uint8Array([(udpSize >> 8) & 0xff, udpSize & 0xff]);
684 | if (webSocket.readyState === WS_READY_STATE_OPEN) {
685 | log(`doh success and dns message length is ${udpSize}`);
686 | if (isbbbHeaderSent) {
687 | webSocket.send(await new Blob([udpSizeBuffer, dnsQueryResult]).arrayBuffer());
688 | } else {
689 | webSocket.send(await new Blob([bbbResponseHeader, udpSizeBuffer, dnsQueryResult]).arrayBuffer());
690 | isbbbHeaderSent = true;
691 | }
692 | }
693 | }
694 | })).catch((error) => {
695 | log('dns udp has error' + error)
696 | });
697 |
698 | const writer = transformStream.writable.getWriter();
699 |
700 | return {
701 | /**
702 | *
703 | * @param {Uint8Array} chunk
704 | */
705 | write(chunk) {
706 | writer.write(chunk);
707 | }
708 | };
709 | }
710 |
711 | /**
712 | *
713 | * @param {string} userID - single or comma separated userIDs
714 | * @param {string | null} hostName
715 | * @returns {string}
716 | */
717 | function getbbbConfig(userIDs, hostName) {
718 | const commonUrlPart = `:443?encryption=none&security=tls&sni=${hostName}&fp=randomized&type=ws&host=${hostName}&path=%2F%3Fed%3D2048#${hostName}`;
719 | const hashSeparator = "################################################################";
720 |
721 | // Split the userIDs into an array
722 | const userIDArray = userIDs.split(",");
723 |
724 | // Prepare output string for each userID
725 | const output = userIDArray.map((userID) => {
726 | const bbbMain = getprotocol()+'://' + userID + '@' + hostName + commonUrlPart;
727 | const bbbSec = getprotocol()+'://' + userID + '@' + proxyIP + commonUrlPart;
728 | return `UUID: ${userID}
${hashSeparator}\nv2ray default ip
729 | ---------------------------------------------------------------
730 | ${bbbMain}
731 |
732 | ---------------------------------------------------------------
733 | v2ray with bestip
734 | ---------------------------------------------------------------
735 | ${bbbSec}
736 |
737 | ---------------------------------------------------------------`;
738 | }).join('\n');
739 | const sublink = `https://${hostName}/sub/${userIDArray[0]}?format=clash`
740 | const subbestip = `https://${hostName}/bestip/${userIDArray[0]}`;
741 | const clash_link = `https://api.v1.mk/sub?target=clash&url=${encodeURIComponent(sublink)}&insert=false&emoji=true&list=false&tfo=false&scv=true&fdn=false&sort=false&new_name=true`;
742 | // Prepare header string
743 | const header = `
744 |
745 | Welcome! This function generates configuration for bbb protocol. If you found this useful, please check our GitHub project for more:
746 | ��ӭ���������� bbb Э������á���������������Ŀ�ܺ��ã���鿴���ǵ� GitHub ��Ŀ����һ��star��
747 | EDtunnel - https://github.com/3Kmfi6HP/EDtunnel
748 |
749 | bbb �ڵ㶩������
750 | Clash for Windows �ڵ㶩������
751 | Clash �ڵ㶩������
752 | ��ѡIP�Զ��ڵ㶩��
753 | Clash��ѡIP�Զ�
754 | singbox��ѡIP�Զ�
755 | nekobox��ѡIP�Զ�
756 | v2rayNG��ѡIP�Զ�
`;
757 |
758 | // HTML Head with CSS and FontAwesome library
759 | const htmlHead = `
760 |
761 | EDtunnel: bbb configuration
762 |
763 |
764 |
765 |
766 |
767 |
768 |
769 |
770 |
771 |
772 |
773 |
774 |
775 |
776 |
777 |
778 |
779 |
821 |
822 |
823 |
824 |
825 | `;
826 |
827 | // Join output with newlines, wrap inside and
828 | return `
829 |
830 | ${htmlHead}
831 |
832 | ${header}
833 | ${output}
834 |
835 |
846 | `;
847 | }
848 |
849 | const portSet_http = new Set([80, 8080, 8880, 2052, 2086, 2095, 2082]);
850 | const portSet_https = new Set([443, 8443, 2053, 2096, 2087, 2083]);
851 |
852 | function createbbbSub(userID_Path, hostName) {
853 | const userIDArray = userID_Path.includes(',') ? userID_Path.split(',') : [userID_Path];
854 | const commonUrlPart_http = `?encryption=none&security=none&fp=random&type=ws&host=${hostName}&path=%2F%3Fed%3D2048#`;
855 | const commonUrlPart_https = `?encryption=none&security=tls&sni=${hostName}&fp=random&type=ws&host=${hostName}&path=%2F%3Fed%3D2048#`;
856 |
857 | const output = userIDArray.flatMap((userID) => {
858 | const httpConfigurations = Array.from(portSet_http).flatMap((port) => {
859 | if (!hostName.includes('pages.dev')) {
860 | const urlPart = `${hostName}-HTTP-${port}`;
861 | const bbbMainHttp = getprotocol()+'://' + userID + '@' + hostName + ':' + port + commonUrlPart_http + urlPart;
862 | return proxyIPs.flatMap((proxyIP) => {
863 | const bbbSecHttp = getprotocol()+'://' + userID + '@' + proxyIP + ':' + port + commonUrlPart_http + urlPart + '-' + proxyIP + '-EDtunnel';
864 | return [bbbMainHttp, bbbSecHttp];
865 | });
866 | }
867 | return [];
868 | });
869 |
870 | const httpsConfigurations = Array.from(portSet_https).flatMap((port) => {
871 | const urlPart = `${hostName}-HTTPS-${port}`;
872 | const bbbMainHttps = getprotocol() + '://' + userID + '@' + hostName + ':' + port + commonUrlPart_https + urlPart;
873 | return proxyIPs.flatMap((proxyIP) => {
874 | const bbbSecHttps = getprotocol() + '://' + userID + '@' + proxyIP + ':' + port + commonUrlPart_https + urlPart + '-' + proxyIP + '-EDtunnel';
875 | return [bbbMainHttps, bbbSecHttps];
876 | });
877 | });
878 |
879 | return [...httpConfigurations, ...httpsConfigurations];
880 | });
881 |
882 | return output.join('\n');
883 | }
884 |
885 | const cn_hostnames = [
886 | 'weibo.com', // Weibo - A popular social media platform
887 | 'www.baidu.com', // Baidu - The largest search engine in China
888 | 'www.bing.com',
889 |
890 | ];
--------------------------------------------------------------------------------