|
1 | | -const Version = '2026-08-09 23:10:57'; |
| 1 | +const Version = '2026-08-10 03:09:01'; |
2 | 2 | let config_JSON, 缓存SOCKS5白名单 = null, 调试日志打印 = false; |
3 | 3 | let SOCKS5白名单 = ['*tapecontent.net', '*cloudatacdn.com', '*loadshare.org', '*cdn-centaurus.com', 'scholar.google.com']; |
4 | 4 | const Pages静态页面 = 'https://edt-pages.github.io'; |
@@ -71,7 +71,10 @@ export default { |
71 | 71 | } else if (管理员密码 && !访问路径.startsWith('admin/') && 访问路径 !== 'login' && request.method === 'POST') {// gRPC/XHTTP代理 |
72 | 72 | const 反代上下文 = await 反代参数获取(url, userID, 默认反代IP, 默认反代兜底); |
73 | 73 | const referer = request.headers.get('Referer') || ''; |
74 | | - const 命中XHTTP特征 = referer.includes('x_padding', 14) || referer.includes('x_padding='); |
| 74 | + const { 头: 本机Padding头, 键: 本机Padding键 } = 获取XHTTPPadding标识(userID); |
| 75 | + const 命中XHTTP特征 = referer.includes('x_padding', 14) || referer.includes('x_padding=') |
| 76 | + || !!request.headers.get(本机Padding头) |
| 77 | + || !!url.searchParams.get(本机Padding键); |
75 | 78 | if (!命中XHTTP特征 && contentType.startsWith('application/grpc')) { |
76 | 79 | log(`[gRPC] 命中请求: ${url.pathname}${url.search}`); |
77 | 80 | return await 处理gRPC请求(request, userID, 反代上下文); |
@@ -528,8 +531,92 @@ export default { |
528 | 531 | } |
529 | 532 | }; |
530 | 533 | ///////////////////////////////////////////////////////////////////////XHTTP传输数据/////////////////////////////////////////////// |
| 534 | +// ========== XHTTP obfs padding 支持 ========== |
| 535 | +// 链接生成处(获取传输协议配置)为 xhttp 节点注入: |
| 536 | +// extra={"xPaddingObfsMode":true,"xPaddingMethod":"tokenish","xPaddingPlacement":"queryInHeader", |
| 537 | +// "xPaddingHeader":"<UUID.slice(1,7)>","xPaddingKey":"_<UUID.slice(1,7)>"} |
| 538 | +// 客户端据此把 base62 padding 以「URL 形式放进 xPaddingHeader 头」或「放进 URL query」发送; |
| 539 | +// 服务端需提取并做 HPACK Huffman 字节长度校验(tokenish:100-2 <= len <= 1000+2),校验通过后 padding 直接丢弃。 |
| 540 | +// 以下 HPACK Huffman 码长为 RFC 7541 Appendix B 的完整 257 项(含索引 256 的 EOS 符号),取自 golang.org/x/net/http2/hpack/tables.go, |
| 541 | +const HPACKHuffman码长 = [ |
| 542 | + 13, 23, 28, 28, 28, 28, 28, 28, 28, 24, 30, 28, 28, 30, 28, 28, |
| 543 | + 28, 28, 28, 28, 28, 28, 30, 28, 28, 28, 28, 28, 28, 28, 28, 28, |
| 544 | + 6, 10, 10, 12, 13, 6, 8, 11, 10, 10, 8, 11, 8, 6, 6, 6, |
| 545 | + 5, 5, 5, 6, 6, 6, 6, 6, 6, 6, 7, 8, 15, 6, 12, 10, |
| 546 | + 13, 6, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, |
| 547 | + 7, 7, 7, 7, 7, 7, 7, 7, 8, 7, 8, 13, 19, 13, 14, 6, |
| 548 | + 15, 5, 6, 5, 6, 5, 6, 6, 6, 5, 7, 7, 6, 6, 6, 5, |
| 549 | + 6, 7, 6, 5, 5, 6, 7, 7, 7, 7, 7, 15, 11, 14, 13, 28, |
| 550 | + 20, 22, 20, 20, 22, 22, 22, 23, 22, 23, 23, 23, 23, 23, 24, 23, |
| 551 | + 24, 24, 22, 23, 24, 23, 23, 23, 23, 21, 22, 23, 22, 23, 23, 24, |
| 552 | + 22, 21, 20, 22, 22, 23, 23, 21, 23, 22, 22, 24, 21, 22, 23, 23, |
| 553 | + 21, 21, 22, 21, 23, 22, 23, 23, 20, 22, 22, 22, 23, 22, 22, 23, |
| 554 | + 26, 26, 20, 19, 22, 23, 22, 25, 26, 26, 26, 27, 27, 26, 24, 25, |
| 555 | + 19, 21, 26, 27, 27, 26, 27, 24, 21, 21, 26, 26, 28, 27, 27, 27, |
| 556 | + 20, 24, 20, 21, 22, 21, 21, 23, 22, 22, 25, 25, 24, 24, 26, 23, |
| 557 | + 26, 27, 26, 26, 27, 27, 27, 27, 27, 28, 27, 27, 27, 27, 27, 26, |
| 558 | + 30 |
| 559 | +]; |
| 560 | + |
| 561 | +// 由 UUID 推导本机 XHTTP padding 头名/键名(与链接生成处 extra 的 xPaddingHeader / xPaddingKey 完全一致) |
| 562 | +function 获取XHTTPPadding标识(yourUUID) { |
| 563 | + return { 头: yourUUID.slice(1, 7), 键: '_' + yourUUID.slice(1, 7) }; |
| 564 | +} |
| 565 | + |
| 566 | +// 计算字符串经 HPACK Huffman 编码后的字节长度(向上取整;与官方 hpack.HuffmanEncodeLength 等价,按 UTF-8 字节遍历) |
| 567 | +function 计算HPACKHuffman字节长度(字符串) { |
| 568 | + const 字节 = new TextEncoder().encode(字符串); |
| 569 | + let 总位数 = 0; |
| 570 | + for (let i = 0; i < 字节.length; i++) { |
| 571 | + 总位数 += HPACKHuffman码长[字节[i]]; |
| 572 | + } |
| 573 | + return Math.ceil(总位数 / 8); |
| 574 | +} |
| 575 | + |
| 576 | +// 提取 XHTTP obfs padding 值(对应官方 ExtractXPaddingFromRequest,obfs 模式;本项目客户端使用 queryInHeader,不启用 cookie) |
| 577 | +// 优先级:① Header xPaddingHeader(值为 URL 形式时取其中 query 参数 xPaddingKey,取不到则回退用头值本身)② URL query 参数 xPaddingKey |
| 578 | +// 返回 '' 表示请求未携带 padding |
| 579 | +function 提取XHTTPPadding值(request, 本机Padding头, 本机Padding键) { |
| 580 | + const 头值 = request.headers.get(本机Padding头); |
| 581 | + if (头值) { |
| 582 | + try { |
| 583 | + const 解析URL = new URL(头值, 'https://x.invalid'); |
| 584 | + const 查询值 = 解析URL.searchParams.get(本机Padding键); |
| 585 | + if (查询值) return 查询值; |
| 586 | + } catch (e) { } |
| 587 | + return 头值; |
| 588 | + } |
| 589 | + const 请求URL = new URL(request.url); |
| 590 | + return 请求URL.searchParams.get(本机Padding键) || ''; |
| 591 | +} |
| 592 | + |
| 593 | +// 校验 XHTTP obfs padding(对应官方 IsPaddingValid,tokenish 方法) |
| 594 | +// @returns {boolean} true=校验通过或请求未携带 padding(放行);false=携带 padding 但校验失败(应返回 400) |
| 595 | +function 校验XHTTPPadding(request, 本机Padding头, 本机Padding键) { |
| 596 | + const padding值 = 提取XHTTPPadding值(request, 本机Padding头, 本机Padding键); |
| 597 | + if (!padding值) return true; // 无 padding:兼容旧客户端/非 padding 请求,直接放行 |
| 598 | + const huffman长度 = 计算HPACKHuffman字节长度(padding值); |
| 599 | + // tokenish:huffman 编码字节长度须在 [100-2, 1000+2] 容差范围内 |
| 600 | + return huffman长度 >= 98 && huffman长度 <= 1002; |
| 601 | +} |
| 602 | + |
| 603 | +// 生成随机 base62 padding 串(用于响应端 padding,官方 GeneratePadding tokenish 的简化版;客户端不校验响应 padding) |
| 604 | +const XHTTPBase62字符集 = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'; |
| 605 | +function 生成XHTTPPadding串(长度) { |
| 606 | + const 字符集长度 = XHTTPBase62字符集.length; |
| 607 | + let 结果 = ''; |
| 608 | + for (let i = 0; i < 长度; i++) { |
| 609 | + 结果 += XHTTPBase62字符集[Math.floor(Math.random() * 字符集长度)]; |
| 610 | + } |
| 611 | + return 结果; |
| 612 | +} |
| 613 | + |
531 | 614 | async function 处理XHTTP请求(request, yourUUID, 反代上下文 = {}) { |
532 | 615 | if (!request.body) return new Response('Bad Request', { status: 400 }); |
| 616 | + // XHTTP obfs padding 提取与校验(官方 ExtractXPaddingFromRequest + IsPaddingValid) |
| 617 | + // 校验通过或请求未携带 padding → 放行;携带 padding 但校验失败 → 400(padding 数据不解码、直接丢弃,不影响首包解析) |
| 618 | + const { 头: 本机Padding头, 键: 本机Padding键 } = 获取XHTTPPadding标识(yourUUID); |
| 619 | + if (!校验XHTTPPadding(request, 本机Padding头, 本机Padding键)) return new Response('Bad Request', { status: 400 }); |
533 | 620 | const reader = request.body.getReader(); |
534 | 621 | const 首包 = await 读取XHTTP首包(reader, yourUUID); |
535 | 622 | if (!首包) { |
@@ -557,6 +644,13 @@ async function 处理XHTTP请求(request, yourUUID, 反代上下文 = {}) { |
557 | 644 | 'X-Accel-Buffering': 'no', |
558 | 645 | 'Cache-Control': 'no-store' |
559 | 646 | }); |
| 647 | + // 响应端 padding(官方 ApplyXPaddingToResponse 对应,obfs queryInHeader:头名=本机Padding头,值为含 query 的 URL 形式) |
| 648 | + // 客户端不校验响应 padding,仅作响应特征混淆;随机长度 100~1000 |
| 649 | + try { |
| 650 | + const 响应URL = new URL('https://x.invalid/'); |
| 651 | + 响应URL.searchParams.set(本机Padding键, 生成XHTTPPadding串(100 + Math.floor(Math.random() * 901))); |
| 652 | + responseHeaders.set(本机Padding头, 响应URL.toString()); |
| 653 | + } catch (e) { } |
560 | 654 |
|
561 | 655 | // UDP 分支:拆到独立函数(保留原逻辑) |
562 | 656 | if (首包.isUDP) return 处理XHTTPUDP请求(首包, reader, request, 反代上下文, responseHeaders); |
@@ -4613,8 +4707,9 @@ function base64SecretDecode(encoded, secret) { |
4613 | 4707 |
|
4614 | 4708 | function 获取传输协议配置(配置 = {}) { |
4615 | 4709 | const 是gRPC = 配置.传输协议 === 'grpc'; |
| 4710 | + const { 头: 本机Padding头, 键: 本机Padding键 } = 获取XHTTPPadding标识(配置.UUID); |
4616 | 4711 | return { |
4617 | | - type: 是gRPC ? (配置.gRPC模式 === 'multi' ? 'grpc&mode=multi' : 'grpc&mode=gun') : (配置.传输协议 === 'xhttp' ? 'xhttp&mode=stream-one' : 'ws'), |
| 4712 | + type: 是gRPC ? (配置.gRPC模式 === 'multi' ? 'grpc&mode=multi' : 'grpc&mode=gun') : (配置.传输协议 === 'xhttp' ? `xhttp&mode=stream-one&extra=%7B%22xPaddingObfsMode%22%3Atrue%2C%22xPaddingMethod%22%3A%22tokenish%22%2C%22xPaddingPlacement%22%3A%22queryInHeader%22%2C%22xPaddingHeader%22%3A%22${本机Padding头}%22%2C%22xPaddingKey%22%3A%22${本机Padding键}%22%7D` : 'ws'), |
4618 | 4713 | 路径字段名: 是gRPC ? 'serviceName' : 'path', |
4619 | 4714 | 域名字段名: 是gRPC ? 'authority' : 'host' |
4620 | 4715 | }; |
|
0 commit comments