// Copyright 2024 GoEdge CDN goedge.cdn@gmail.com. All rights reserved. Official site: https://goedge.cn . package nodes import ( "bytes" "compress/gzip" "encoding/base64" "fmt" "io" "net/http" "regexp" "strings" "time" "github.com/TeaOSLab/EdgeNode/internal/encryption" "github.com/TeaOSLab/EdgeNode/internal/remotelogs" "github.com/andybalholm/brotli" ) const encryptionCacheVersion = "xor-v1" // processPageEncryption 处理页面加密 func (this *HTTPRequest) processPageEncryption(resp *http.Response) error { // 首先检查是否是 /waf/loader.js,如果是则直接跳过(不应该被加密) // 这个检查必须在所有其他检查之前,确保 loader.js 永远不会被加密 if strings.Contains(this.URL(), "/waf/loader.js") { return nil } if this.web.Encryption == nil { return nil } if !this.web.Encryption.IsOn { return nil } if !this.web.Encryption.IsEnabled() { return nil } // 检查 URL 白名单 if this.web.Encryption.MatchExcludeURL(this.URL()) { return nil } // 检查 Content-Type 和 URL contentType := resp.Header.Get("Content-Type") contentTypeLower := strings.ToLower(contentType) urlLower := strings.ToLower(this.URL()) var isHTML = strings.Contains(contentTypeLower, "text/html") // 判断是否为 JavaScript 文件:通过 Content-Type 或 URL 后缀 var isJavaScript = strings.Contains(contentTypeLower, "text/javascript") || strings.Contains(contentTypeLower, "application/javascript") || strings.Contains(contentTypeLower, "application/x-javascript") || strings.Contains(contentTypeLower, "text/ecmascript") || strings.HasSuffix(urlLower, ".js") || strings.Contains(urlLower, ".js?") || strings.Contains(urlLower, ".js&") if !isHTML && !isJavaScript { return nil } // 检查内容大小(仅处理小于 10MB 的内容) if resp.ContentLength > 0 && resp.ContentLength > 10*1024*1024 { return nil } // 读取响应体 bodyBytes, err := io.ReadAll(resp.Body) if err != nil { return err } _ = resp.Body.Close() // 如果源站返回了压缩内容,先解压再处理 decodedBody, decoded, err := decodeResponseBody(bodyBytes, resp.Header.Get("Content-Encoding")) if err == nil && decoded { bodyBytes = decodedBody // 已经解压,移除 Content-Encoding resp.Header.Del("Content-Encoding") } // 检查实际大小 if len(bodyBytes) > 10*1024*1024 { // 内容太大,恢复原始响应体 resp.Body = io.NopCloser(bytes.NewReader(bodyBytes)) return nil } var encryptedBytes []byte // 处理 JavaScript 文件 if isJavaScript { // 检查是否需要加密独立的 JavaScript 文件 if this.web.Encryption.Javascript == nil { resp.Body = io.NopCloser(bytes.NewReader(bodyBytes)) return nil } if !this.web.Encryption.Javascript.IsOn { resp.Body = io.NopCloser(bytes.NewReader(bodyBytes)) return nil } // 检查 URL 匹配 if !this.web.Encryption.Javascript.MatchURL(this.URL()) { resp.Body = io.NopCloser(bytes.NewReader(bodyBytes)) return nil } // 跳过 Loader 文件 if strings.Contains(this.URL(), "/waf/loader.js") || strings.Contains(this.URL(), "waf-loader.js") || strings.Contains(this.URL(), "__WAF_") { resp.Body = io.NopCloser(bytes.NewReader(bodyBytes)) return nil } encryptedBytes, err = this.encryptJavaScriptFile(bodyBytes, resp) if err != nil { remotelogs.Warn("HTTP_REQUEST_ENCRYPTION", "encrypt JavaScript file failed: "+err.Error()) resp.Body = io.NopCloser(bytes.NewReader(bodyBytes)) return nil } } else if isHTML { // 加密 HTML 内容 encryptedBytes, err = this.encryptHTMLScripts(bodyBytes, resp) if err != nil { remotelogs.Warn("HTTP_REQUEST_ENCRYPTION", "encrypt HTML failed: "+err.Error()) // 加密失败,恢复原始响应体 resp.Body = io.NopCloser(bytes.NewReader(bodyBytes)) return nil } } else { // 未知类型,恢复原始响应体 resp.Body = io.NopCloser(bytes.NewReader(bodyBytes)) return nil } // 替换响应体 resp.Body = io.NopCloser(bytes.NewReader(encryptedBytes)) resp.ContentLength = int64(len(encryptedBytes)) resp.Header.Set("Content-Length", fmt.Sprintf("%d", len(encryptedBytes))) // 避免旧缓存导致解密算法不匹配 resp.Header.Set("Cache-Control", "no-store, no-cache, must-revalidate") // 删除 Content-Encoding(如果存在),因为我们修改了内容 resp.Header.Del("Content-Encoding") return nil } // encryptHTMLScripts 加密 HTML 中的脚本 func (this *HTTPRequest) encryptHTMLScripts(htmlBytes []byte, resp *http.Response) ([]byte, error) { html := string(htmlBytes) // 检查是否需要加密 HTML 脚本 if this.web.Encryption.HTML == nil || !this.web.Encryption.HTML.IsOn { return htmlBytes, nil } // 检查 URL 匹配 if !this.web.Encryption.HTML.MatchURL(this.URL()) { return htmlBytes, nil } // 生成密钥 remoteIP := this.requestRemoteAddr(true) userAgent := this.RawReq.UserAgent() keyID, actualKey := encryption.GenerateEncryptionKey(remoteIP, userAgent, this.web.Encryption.KeyPolicy) // 检查缓存 var cacheKey string if this.web.Encryption.Cache != nil && this.web.Encryption.Cache.IsOn { // 生成缓存键:keyID + URL + contentHash contentHash := fmt.Sprintf("%x", bytesHash(htmlBytes)) cacheKey = fmt.Sprintf("encrypt_%s_%s_%s_%s", encryptionCacheVersion, keyID, this.URL(), contentHash) cache := encryption.SharedEncryptionCache( int(this.web.Encryption.Cache.MaxSize), time.Duration(this.web.Encryption.Cache.TTL)*time.Second, ) if cached, ok := cache.Get(cacheKey); ok { return cached, nil } } // 提取并加密内联脚本 if this.web.Encryption.HTML.EncryptInlineScripts { html = this.encryptInlineScripts(html, actualKey, keyID) } // 提取并加密外部脚本(通过 src 属性) if this.web.Encryption.HTML.EncryptExternalScripts { html = this.encryptExternalScripts(html, actualKey, keyID) } // 注入 Loader html = this.injectLoader(html) result := []byte(html) // 保存到缓存 if this.web.Encryption.Cache != nil && this.web.Encryption.Cache.IsOn && len(cacheKey) > 0 { cache := encryption.SharedEncryptionCache( int(this.web.Encryption.Cache.MaxSize), time.Duration(this.web.Encryption.Cache.TTL)*time.Second, ) cache.Set(cacheKey, result, this.web.Encryption.Cache.TTL) } return result, nil } // encryptInlineScripts 加密内联脚本 func (this *HTTPRequest) encryptInlineScripts(html string, key string, keyID string) string { // 匹配 (不包含 src 属性) scriptRegex := regexp.MustCompile(`(?i)]*)?>([\s\S]*?)`) return scriptRegex.ReplaceAllStringFunc(html, func(match string) string { // 检查是否有 src 属性(外部脚本) if strings.Contains(strings.ToLower(match), "src=") { return match // 跳过外部脚本 } // 提取脚本内容 contentMatch := regexp.MustCompile(`(?i)]*)?>([\s\S]*?)`) matches := contentMatch.FindStringSubmatch(match) if len(matches) < 2 { return match } scriptContent := matches[1] // 跳过空脚本或仅包含空白字符的脚本 if strings.TrimSpace(scriptContent) == "" { return match } // 跳过已加密的脚本(包含 __WAF_P__) if strings.Contains(scriptContent, "__WAF_P__") { return match } // 加密脚本内容 encrypted, err := this.encryptScript(scriptContent, key) if err != nil { return match // 加密失败,返回原始内容 } // 生成元数据(k 是 keyID,用于缓存;key 是实际密钥,用于解密) meta := fmt.Sprintf(`{"k":"%s","key":"%s","t":%d,"alg":"xor"}`, keyID, key, time.Now().Unix()) // 替换为加密后的脚本(同步解密执行,保证脚本顺序) return fmt.Sprintf( ``, meta, encrypted, ) }) } // encryptExternalScripts 加密外部脚本(通过替换 src 为加密后的内容) // 注意:这里我们实际上是将外部脚本的内容内联并加密 func (this *HTTPRequest) encryptExternalScripts(html string, key string, keyID string) string { // 匹配 scriptRegex := regexp.MustCompile(`(?i)]*src\s*=\s*["']([^"']+)["'][^>]*)>\s*`) return scriptRegex.ReplaceAllStringFunc(html, func(match string) string { // 提取 src URL srcMatch := regexp.MustCompile(`(?i)src\s*=\s*["']([^"']+)["']`) srcMatches := srcMatch.FindStringSubmatch(match) if len(srcMatches) < 2 { return match } srcURL := srcMatches[1] // 跳过已加密的脚本或 Loader if strings.Contains(srcURL, "waf-loader.js") || strings.Contains(srcURL, "__WAF_") { return match } // 注意:实际实现中,我们需要获取外部脚本的内容 // 这里为了简化,我们只是标记需要加密,实际内容获取需要在响应处理时进行 // 当前实现:将外部脚本转换为内联加密脚本的占位符 // 实际生产环境需要:1. 获取外部脚本内容 2. 加密 3. 替换 return match // 暂时返回原始内容,后续可以扩展 }) } // encryptScript 加密脚本内容 func (this *HTTPRequest) encryptScript(scriptContent string, key string) (string, error) { // 1. XOR 加密(不压缩,避免浏览器解压依赖导致顺序问题) encrypted := xorEncrypt([]byte(scriptContent), []byte(key)) // 2. Base64 编码 encoded := base64.StdEncoding.EncodeToString(encrypted) return encoded, nil } // injectLoader 注入 Loader 脚本 func (this *HTTPRequest) injectLoader(html string) string { // 检查是否已经注入 if strings.Contains(html, "waf-loader.js") { return html } // 在 之前注入,如果没有 ,则在 之前注入 // 不使用 async,确保 loader 在解析阶段优先加载并执行 loaderScript := `` if strings.Contains(html, "") { return strings.Replace(html, "", loaderScript+"", 1) } else if strings.Contains(html, "") { return strings.Replace(html, "", loaderScript+"", 1) } else { // 如果都没有,在开头注入 return loaderScript + html } } // compressGzip 使用 Gzip 压缩(浏览器原生支持) func compressGzip(data []byte) ([]byte, error) { var buf bytes.Buffer writer := gzip.NewWriter(&buf) _, err := writer.Write(data) if err != nil { writer.Close() return nil, err } err = writer.Close() if err != nil { return nil, err } return buf.Bytes(), nil } // decodeResponseBody 根据 Content-Encoding 解压响应体 func decodeResponseBody(body []byte, encoding string) ([]byte, bool, error) { enc := strings.ToLower(strings.TrimSpace(encoding)) if enc == "" || enc == "identity" { return body, false, nil } switch enc { case "gzip": reader, err := gzip.NewReader(bytes.NewReader(body)) if err != nil { return body, false, err } defer reader.Close() decoded, err := io.ReadAll(reader) if err != nil { return body, false, err } return decoded, true, nil case "br": reader := brotli.NewReader(bytes.NewReader(body)) decoded, err := io.ReadAll(reader) if err != nil { return body, false, err } return decoded, true, nil default: // 未知编码,保持原样 return body, false, nil } } // compressBrotli 使用 Brotli 压缩(保留用于其他用途) func compressBrotli(data []byte, level int) ([]byte, error) { var buf bytes.Buffer writer := brotli.NewWriterOptions(&buf, brotli.WriterOptions{ Quality: level, LGWin: 14, }) _, err := writer.Write(data) if err != nil { writer.Close() return nil, err } err = writer.Close() if err != nil { return nil, err } return buf.Bytes(), nil } // xorEncrypt XOR 加密 func xorEncrypt(data []byte, key []byte) []byte { result := make([]byte, len(data)) keyLen := len(key) if keyLen == 0 { return data } for i := 0; i < len(data); i++ { result[i] = data[i] ^ key[i%keyLen] } return result } // encryptJavaScriptFile 加密独立的 JavaScript 文件 func (this *HTTPRequest) encryptJavaScriptFile(jsBytes []byte, resp *http.Response) ([]byte, error) { jsContent := string(jsBytes) // 跳过空文件 if strings.TrimSpace(jsContent) == "" { return jsBytes, nil } // 跳过已加密的脚本(包含 __WAF_P__) if strings.Contains(jsContent, "__WAF_P__") { return jsBytes, nil } // 生成密钥 remoteIP := this.requestRemoteAddr(true) userAgent := this.RawReq.UserAgent() keyID, actualKey := encryption.GenerateEncryptionKey(remoteIP, userAgent, this.web.Encryption.KeyPolicy) // 检查缓存 var cacheKey string if this.web.Encryption.Cache != nil && this.web.Encryption.Cache.IsOn { // 生成缓存键:keyID + URL + contentHash contentHash := fmt.Sprintf("%x", bytesHash(jsBytes)) cacheKey = fmt.Sprintf("encrypt_js_%s_%s_%s_%s", encryptionCacheVersion, keyID, this.URL(), contentHash) cache := encryption.SharedEncryptionCache( int(this.web.Encryption.Cache.MaxSize), time.Duration(this.web.Encryption.Cache.TTL)*time.Second, ) if cached, ok := cache.Get(cacheKey); ok { return cached, nil } } // 加密脚本内容 encrypted, err := this.encryptScript(jsContent, actualKey) if err != nil { return nil, err } // 生成元数据(k 是 keyID,用于缓存;key 是实际密钥,用于解密) meta := fmt.Sprintf(`{"k":"%s","key":"%s","t":%d,"alg":"xor"}`, keyID, actualKey, time.Now().Unix()) // 生成加密后的 JavaScript 代码(同步解密执行,保证脚本顺序) encryptedJS := fmt.Sprintf(`(function() { try { function xorDecodeToString(b64, key) { var bin = atob(b64); var out = new Uint8Array(bin.length); for (var i = 0; i < bin.length; i++) { out[i] = bin.charCodeAt(i) ^ key.charCodeAt(i %% key.length); } if (typeof TextDecoder !== 'undefined') { return new TextDecoder().decode(out); } var s = ''; for (var j = 0; j < out.length; j++) { s += String.fromCharCode(out[j]); } return s; } var meta = %s; var code = xorDecodeToString("%s", meta.key); // 使用全局 eval,尽量保持和