From 097f583d51608e67d6d0504fb40d1892f41d2dd5 Mon Sep 17 00:00:00 2001 From: adams549659584 <13760614423@163.com> Date: Thu, 4 May 2023 11:29:40 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20:zap:=20=E9=87=8D=E6=9E=84=E6=94=AF?= =?UTF-8?q?=E6=8C=81=20vercel?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- LICENSE | 21 +++++ README.md | 26 +++--- api/chathub.go | 10 +++ api/index.go | 14 +++ api/web.go | 10 +++ common/proxy.go | 202 +++++++++++++++++++++++++++++++++++++++++++ common/utils.go | 12 +++ main.go | 225 ++---------------------------------------------- vercel.json | 20 +++++ web/sw.js | 23 ++++- web/web.go | 6 ++ 11 files changed, 339 insertions(+), 230 deletions(-) create mode 100644 LICENSE create mode 100644 api/chathub.go create mode 100644 api/index.go create mode 100644 api/web.go create mode 100644 common/proxy.go create mode 100644 common/utils.go create mode 100644 vercel.json create mode 100644 web/web.go diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000000..35f05c2065 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License Copyright (c) 2023 adams549659584 + +Permission is hereby granted, free +of charge, to any person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the Software without +restriction, including without limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to the +following conditions: + +The above copyright notice and this permission notice +(including the next paragraph) shall be included in all copies or substantial +portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO +EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR +OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md index 96640fe7a8..93fe7d3f3e 100644 --- a/README.md +++ b/README.md @@ -22,21 +22,19 @@ ![手机未登录](./docs/img/4.png) -### 演示站点: +## 演示站点: > 甲骨文小鸡仔,轻虐 - https://bing.vcanbb.top -> railway +> Railway - https://bing-railway.vcanbb.top - https://go-proxy-bingai-production.up.railway.app - - -### 获取cookies +## 获取cookies - 访问 https://www.bing.com/ 或 https://cn.bing.com/ ,登录 @@ -46,13 +44,15 @@ ![获取Cookie](./docs/img/5.png) -### 部署 +## 部署 > 需 https 域名 (自行配置 nginx 等) > 支持 Linux (amd64 / arm64) -- docker 部署 , 参考 [Dockerfile](./docker/Dockerfile) 、[docker-compose.yml](./docker/docker-compose.yml) +### docker + +> 参考 [Dockerfile](./docker/Dockerfile) 、[docker-compose.yml](./docker/docker-compose.yml) 示例 @@ -61,11 +61,11 @@ docker run -d -p 8080:8080 --name go-proxy-bingai --restart=unless-stopped adams549659584/go-proxy-bingai ``` -- 直接下载 Release 运行 +### Release 在 [Github Releases](https://github.com/adams549659584/go-proxy-bingai/releases) 下载适用于对应平台的压缩包,解压后可得到可执行文件 go-proxy-bingai,直接运行即可。 -- Railway +### Railway > 主要配置 Dockerfile 路径 及 端口就可以 @@ -73,7 +73,7 @@ docker run -d -p 8080:8080 --name go-proxy-bingai --restart=unless-stopped adams PORT=8080 RAILWAY_DOCKERFILE_PATH=docker/Dockerfile ``` -使用模板部署,点这里 => [![Deploy on Railway](https://railway.app/button.svg)](https://railway.app/template/uIckWS?referralCode=BBs747) +一键部署,点这里 => [![Deploy on Railway](https://railway.app/button.svg)](https://railway.app/template/uIckWS?referralCode=BBs747) ![Railway 模板部署](./docs/img/railway-1.png) @@ -81,4 +81,8 @@ RAILWAY_DOCKERFILE_PATH=docker/Dockerfile ![Railway 环境变量](./docs/img/railway-2.png) -![Railway 域名](./docs/img/railway-3.png) \ No newline at end of file +![Railway 域名](./docs/img/railway-3.png) + +### Vercel + +一键部署,点这里 => [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https://github.com/adams549659584/go-proxy-bingai&project-name=go-proxy-bingai&repository-name=go-proxy-bingai) diff --git a/api/chathub.go b/api/chathub.go new file mode 100644 index 0000000000..8ffe83fc49 --- /dev/null +++ b/api/chathub.go @@ -0,0 +1,10 @@ +package api + +import ( + "adams549659584/go-proxy-bingai/common" + "net/http" +) + +func ChatHub(w http.ResponseWriter, r *http.Request) { + common.NewSingleHostReverseProxy(common.BING_CHAT_URL).ServeHTTP(w, r) +} diff --git a/api/index.go b/api/index.go new file mode 100644 index 0000000000..4f0e016bf3 --- /dev/null +++ b/api/index.go @@ -0,0 +1,14 @@ +package api + +import ( + "adams549659584/go-proxy-bingai/common" + "net/http" +) + +func Index(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/" { + http.Redirect(w, r, "/web/chat.html", http.StatusFound) + } else { + common.NewSingleHostReverseProxy(common.BING_URL).ServeHTTP(w, r) + } +} diff --git a/api/web.go b/api/web.go new file mode 100644 index 0000000000..48a04a1e5b --- /dev/null +++ b/api/web.go @@ -0,0 +1,10 @@ +package api + +import ( + "adams549659584/go-proxy-bingai/web" + "net/http" +) + +func WebStatic(w http.ResponseWriter, r *http.Request) { + http.StripPrefix("/web/", http.FileServer(http.FS(web.WebFS))).ServeHTTP(w, r) +} diff --git a/common/proxy.go b/common/proxy.go new file mode 100644 index 0000000000..9917e4d435 --- /dev/null +++ b/common/proxy.go @@ -0,0 +1,202 @@ +package common + +import ( + "bytes" + "compress/gzip" + "fmt" + "io" + "log" + "net/http" + "net/http/httputil" + "net/url" + "strconv" + "strings" + + "github.com/andybalholm/brotli" +) + +var ( + BING_CHAT_DOMAIN = "https://sydney.bing.com" + BING_CHAT_URL, _ = url.Parse(BING_CHAT_DOMAIN + "/sydney/ChatHub") + BING_URL, _ = url.Parse("https://www.bing.com") + KEEP_HEADERS = map[string]bool{ + "Accept": true, + "Accept-Encoding": true, + "Accept-Language": true, + "Referer": true, + "Connection": true, + "Cookie": true, + "Upgrade": true, + "User-Agent": true, + "Sec-Websocket-Extensions": true, + "Sec-Websocket-Key": true, + "Sec-Websocket-Version": true, + "X-Request-Id": true, + "X-Forwarded-For": true, + } +) + +func NewSingleHostReverseProxy(target *url.URL) *httputil.ReverseProxy { + originalScheme := "http" + httpsSchemeName := "https" + var originalHost string + var originalPath string + director := func(req *http.Request) { + if req.URL.Scheme == httpsSchemeName || req.Header.Get("X-Forwarded-Proto") == httpsSchemeName { + originalScheme = httpsSchemeName + } + originalHost = req.Host + originalPath = req.URL.Path + + req.URL.Scheme = target.Scheme + req.URL.Host = target.Host + req.Host = target.Host + + req.Header.Set("Referer", fmt.Sprintf("%s/search?q=Bing+AI", BING_URL.String())) + + // 随机ip + randIp := fmt.Sprintf("%d.%d.%d.%d", RandInt(1, 10), RandInt(1, 255), RandInt(1, 255), RandInt(1, 255)) + req.Header.Set("X-Forwarded-For", randIp) + + for hKey, _ := range req.Header { + if _, isExist := KEEP_HEADERS[hKey]; !isExist { + req.Header.Del(hKey) + } + } + + // reqHeaderByte, _ := json.Marshal(req.Header) + // log.Println("剩余请求头 : ", string(reqHeaderByte)) + } + //改写返回信息 + modifyFunc := func(res *http.Response) error { + contentType := res.Header.Get("Content-Type") + if strings.Contains(contentType, "text/javascript") { + contentEncoding := res.Header.Get("Content-Encoding") + switch contentEncoding { + case "gzip": + // log.Println("ContentEncoding : ", contentEncoding, " Path : ", originalPath) + modifyGzipBody(res, originalScheme, originalHost) + case "br": + // log.Println("ContentEncoding : ", contentEncoding, " Path : ", originalPath) + modifyBrBody(res, originalScheme, originalHost) + default: + log.Println("ContentEncoding default : ", contentEncoding, " Path : ", originalPath) + modifyDefaultBody(res, originalScheme, originalHost) + } + } + + // 修改响应 cookie 域 + // resCookies := res.Header.Values("Set-Cookie") + // if len(resCookies) > 0 { + // for i, v := range resCookies { + // resCookies[i] = strings.ReplaceAll(strings.ReplaceAll(v, ".bing.com", originalHost), "bing.com", originalHost) + // } + // } + res.Header.Del("Set-Cookie") + + return nil + } + errorHandler := func(res http.ResponseWriter, req *http.Request, err error) { + log.Println("代理异常 :", err) + res.Write([]byte(err.Error())) + } + // 代理请求 请求回来的内容 报错自动调用 + return &httputil.ReverseProxy{Director: director, ModifyResponse: modifyFunc, ErrorHandler: errorHandler} +} + +func replaceResBody(originalBody string, originalScheme string, originalHost string) string { + modifiedBodyStr := originalBody + originalDomain := fmt.Sprintf("%s://%s", originalScheme, originalHost) + + if strings.Contains(modifiedBodyStr, BING_URL.String()) { + modifiedBodyStr = strings.ReplaceAll(modifiedBodyStr, BING_URL.String(), originalDomain) + } + + // 对话暂时支持国内网络,而且 Vercel 还不支持 Websocket ,先不用 + // if strings.Contains(modifiedBodyStr, BING_CHAT_DOMAIN) { + // modifiedBodyStr = strings.ReplaceAll(modifiedBodyStr, BING_CHAT_DOMAIN, originalDomain) + // } + + // if strings.Contains(modifiedBodyStr, "https://www.bingapis.com") { + // modifiedBodyStr = strings.ReplaceAll(modifiedBodyStr, "https://www.bingapis.com", "https://bing.vcanbb.top") + // } + return modifiedBodyStr +} + +func modifyGzipBody(res *http.Response, originalScheme string, originalHost string) error { + gz, err := gzip.NewReader(res.Body) + if err != nil { + return err + } + defer gz.Close() + + bodyByte, err := io.ReadAll(gz) + if err != nil { + return err + } + originalBody := string(bodyByte) + modifiedBodyStr := replaceResBody(originalBody, originalScheme, originalHost) + // 修改响应内容 + modifiedBody := []byte(modifiedBodyStr) + // gzip 压缩 + var buf bytes.Buffer + writer := gzip.NewWriter(&buf) + defer writer.Close() + + _, err = writer.Write(modifiedBody) + if err != nil { + return err + } + + // 修改 Content-Length 头 + res.Header.Set("Content-Length", strconv.Itoa(buf.Len())) + // 修改响应内容 + res.Body = io.NopCloser(bytes.NewReader(buf.Bytes())) + + return nil +} + +func modifyBrBody(res *http.Response, originalScheme string, originalHost string) error { + reader := brotli.NewReader(res.Body) + var uncompressed bytes.Buffer + uncompressed.ReadFrom(reader) + + originalBody := uncompressed.String() + + modifiedBodyStr := replaceResBody(originalBody, originalScheme, originalHost) + + // 修改响应内容 + modifiedBody := []byte(modifiedBodyStr) + // br 压缩 + var buf bytes.Buffer + writer := brotli.NewWriter(&buf) + writer.Write(modifiedBody) + writer.Close() + + // 修改 Content-Length 头 + // res.ContentLength = int64(buf.Len()) + res.Header.Set("Content-Length", strconv.Itoa(buf.Len())) + // 修改响应内容 + res.Body = io.NopCloser(bytes.NewReader(buf.Bytes())) + + return nil +} + +func modifyDefaultBody(res *http.Response, originalScheme string, originalHost string) error { + bodyByte, err := io.ReadAll(res.Body) + if err != nil { + return err + } + originalBody := string(bodyByte) + modifiedBodyStr := replaceResBody(originalBody, originalScheme, originalHost) + // 修改响应内容 + modifiedBody := []byte(modifiedBodyStr) + + // 修改 Content-Length 头 + // res.ContentLength = int64(buf.Len()) + res.Header.Set("Content-Length", strconv.Itoa(len(modifiedBody))) + // 修改响应内容 + res.Body = io.NopCloser(bytes.NewReader(modifiedBody)) + + return nil +} diff --git a/common/utils.go b/common/utils.go new file mode 100644 index 0000000000..a3f242bf71 --- /dev/null +++ b/common/utils.go @@ -0,0 +1,12 @@ +package common + +import ( + "math/rand" + "time" +) + +func RandInt(min int, max int) int { + seed := time.Now().UnixNano() + rng := rand.New(rand.NewSource(seed)) + return rng.Intn(max-min+1) + min +} diff --git a/main.go b/main.go index ea7bedddf7..5273d6b615 100644 --- a/main.go +++ b/main.go @@ -1,238 +1,27 @@ package main import ( - "bytes" - "compress/gzip" - "embed" - "fmt" - "io" + "adams549659584/go-proxy-bingai/api" "log" - "math/rand" "net/http" - "net/http/httputil" - "net/url" - "strconv" - "strings" "time" - - "github.com/andybalholm/brotli" -) - -//go:embed web/* -var WebFiles embed.FS - -var ( - ADDR = ":8080" - BING_CHAT_DOMAIN = "https://sydney.bing.com" - BING_CHAT_URL, _ = url.Parse(BING_CHAT_DOMAIN + "/sydney/ChatHub") - BING_URL, _ = url.Parse("https://www.bing.com") - KEEP_HEADERS = map[string]bool{ - "Accept": true, - "Accept-Encoding": true, - "Accept-Language": true, - "Referer": true, - "Connection": true, - "Cookie": true, - "Upgrade": true, - "User-Agent": true, - "Sec-Websocket-Extensions": true, - "Sec-Websocket-Key": true, - "Sec-Websocket-Version": true, - "X-Request-Id": true, - "X-Forwarded-For": true, - } ) func main() { + http.HandleFunc("/sydney/ChatHub", api.ChatHub) - http.HandleFunc("/web/", func(w http.ResponseWriter, r *http.Request) { - http.FileServer(http.FS(WebFiles)).ServeHTTP(w, r) - }) + http.HandleFunc("/web/", api.WebStatic) - http.HandleFunc("/sydney/ChatHub", func(w http.ResponseWriter, r *http.Request) { - newSingleHostReverseProxy(BING_CHAT_URL).ServeHTTP(w, r) - }) + http.HandleFunc("/", api.Index) - http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { - if r.URL.Path == "/" { - http.Redirect(w, r, "/web/chat.html", http.StatusFound) - } else { - newSingleHostReverseProxy(BING_URL).ServeHTTP(w, r) - } - }) + addr := ":8080" - log.Println("Starting BingAI Proxy At " + ADDR) + log.Println("Starting BingAI Proxy At " + addr) srv := &http.Server{ - Addr: ADDR, + Addr: addr, WriteTimeout: 15 * time.Second, ReadTimeout: 15 * time.Second, } log.Fatal(srv.ListenAndServe()) } - -func newSingleHostReverseProxy(target *url.URL) *httputil.ReverseProxy { - originalScheme := "http" - httpsSchemeName := "https" - var originalHost string - var originalPath string - director := func(req *http.Request) { - if req.URL.Scheme == httpsSchemeName || req.Header.Get("X-Forwarded-Proto") == httpsSchemeName { - originalScheme = httpsSchemeName - } - originalHost = req.Host - originalPath = req.URL.Path - - req.URL.Scheme = target.Scheme - req.URL.Host = target.Host - req.Host = target.Host - - req.Header.Set("Referer", fmt.Sprintf("%s/search?q=Bing+AI", BING_URL.String())) - - // 随机ip - randIp := fmt.Sprintf("%d.%d.%d.%d", randInt(1, 10), randInt(1, 255), randInt(1, 255), randInt(1, 255)) - req.Header.Set("X-Forwarded-For", randIp) - - for hKey, _ := range req.Header { - if _, isExist := KEEP_HEADERS[hKey]; !isExist { - req.Header.Del(hKey) - } - } - - // reqHeaderByte, _ := json.Marshal(req.Header) - // log.Println("剩余请求头 : ", string(reqHeaderByte)) - } - //改写返回信息 - modifyFunc := func(res *http.Response) error { - contentType := res.Header.Get("Content-Type") - if strings.Contains(contentType, "text/javascript") { - contentEncoding := res.Header.Get("Content-Encoding") - switch contentEncoding { - case "gzip": - // log.Println("ContentEncoding : ", contentEncoding, " Path : ", originalPath) - modifyGzipBody(res, originalScheme, originalHost) - case "br": - // log.Println("ContentEncoding : ", contentEncoding, " Path : ", originalPath) - modifyBrBody(res, originalScheme, originalHost) - default: - log.Println("ContentEncoding default : ", contentEncoding, " Path : ", originalPath) - modifyDefaultBody(res, originalScheme, originalHost) - } - } - - // 修改响应 cookie 域 - // resCookies := res.Header.Values("Set-Cookie") - // if len(resCookies) > 0 { - // for i, v := range resCookies { - // resCookies[i] = strings.ReplaceAll(strings.ReplaceAll(v, ".bing.com", originalHost), "bing.com", originalHost) - // } - // } - res.Header.Del("Set-Cookie") - - return nil - } - errorHandler := func(res http.ResponseWriter, req *http.Request, err error) { - log.Println("代理异常 :", err) - res.Write([]byte(err.Error())) - } - // 代理请求 请求回来的内容 报错自动调用 - return &httputil.ReverseProxy{Director: director, ModifyResponse: modifyFunc, ErrorHandler: errorHandler} -} - -func replaceResBody(originalBody string, originalScheme string, originalHost string) string { - modifiedBodyStr := originalBody - originalDomain := fmt.Sprintf("%s://%s", originalScheme, originalHost) - if strings.Contains(modifiedBodyStr, BING_URL.String()) { - modifiedBodyStr = strings.ReplaceAll(modifiedBodyStr, BING_URL.String(), originalDomain) - } - if strings.Contains(modifiedBodyStr, BING_CHAT_DOMAIN) { - modifiedBodyStr = strings.ReplaceAll(modifiedBodyStr, BING_CHAT_DOMAIN, originalDomain) - } - // if strings.Contains(modifiedBodyStr, "https://www.bingapis.com") { - // modifiedBodyStr = strings.ReplaceAll(modifiedBodyStr, "https://www.bingapis.com", "https://bing.vcanbb.top") - // } - return modifiedBodyStr -} - -func modifyGzipBody(res *http.Response, originalScheme string, originalHost string) error { - gz, err := gzip.NewReader(res.Body) - if err != nil { - return err - } - defer gz.Close() - - bodyByte, err := io.ReadAll(gz) - if err != nil { - return err - } - originalBody := string(bodyByte) - modifiedBodyStr := replaceResBody(originalBody, originalScheme, originalHost) - // 修改响应内容 - modifiedBody := []byte(modifiedBodyStr) - // gzip 压缩 - var buf bytes.Buffer - writer := gzip.NewWriter(&buf) - defer writer.Close() - - _, err = writer.Write(modifiedBody) - if err != nil { - return err - } - - // 修改 Content-Length 头 - res.Header.Set("Content-Length", strconv.Itoa(buf.Len())) - // 修改响应内容 - res.Body = io.NopCloser(bytes.NewReader(buf.Bytes())) - - return nil -} - -func modifyBrBody(res *http.Response, originalScheme string, originalHost string) error { - reader := brotli.NewReader(res.Body) - var uncompressed bytes.Buffer - uncompressed.ReadFrom(reader) - - originalBody := uncompressed.String() - - modifiedBodyStr := replaceResBody(originalBody, originalScheme, originalHost) - - // 修改响应内容 - modifiedBody := []byte(modifiedBodyStr) - // br 压缩 - var buf bytes.Buffer - writer := brotli.NewWriter(&buf) - writer.Write(modifiedBody) - writer.Close() - - // 修改 Content-Length 头 - // res.ContentLength = int64(buf.Len()) - res.Header.Set("Content-Length", strconv.Itoa(buf.Len())) - // 修改响应内容 - res.Body = io.NopCloser(bytes.NewReader(buf.Bytes())) - - return nil -} - -func modifyDefaultBody(res *http.Response, originalScheme string, originalHost string) error { - bodyByte, err := io.ReadAll(res.Body) - if err != nil { - return err - } - originalBody := string(bodyByte) - modifiedBodyStr := replaceResBody(originalBody, originalScheme, originalHost) - // 修改响应内容 - modifiedBody := []byte(modifiedBodyStr) - - // 修改 Content-Length 头 - // res.ContentLength = int64(buf.Len()) - res.Header.Set("Content-Length", strconv.Itoa(len(modifiedBody))) - // 修改响应内容 - res.Body = io.NopCloser(bytes.NewReader(modifiedBody)) - - return nil -} - -func randInt(min int, max int) int { - rand.Seed(time.Now().UnixNano()) - return rand.Intn(max-min+1) + min -} diff --git a/vercel.json b/vercel.json new file mode 100644 index 0000000000..b8b784eea7 --- /dev/null +++ b/vercel.json @@ -0,0 +1,20 @@ +{ + "name": "go-proxy-bingai", + "version": 2, + "builds": [ + { + "src": "/api/{index,web}.go", + "use": "@vercel/go" + } + ], + "routes": [ + { + "src": "/web/.*", + "dest": "/api/web.go" + }, + { + "src": "/.*", + "dest": "/api/index.go" + } + ] +} \ No newline at end of file diff --git a/web/sw.js b/web/sw.js index 8102bcd947..3d745716f7 100644 --- a/web/sw.js +++ b/web/sw.js @@ -1,7 +1,7 @@ // 引入workbox 框架 importScripts('./js/sw/workbox-sw.js'); -const SW_VERSION = '1.0.0'; +const SW_VERSION = '1.1.0'; const CACHE_PREFIX = 'BingAI'; workbox.setConfig({ debug: false, logLevel: 'warn' }); @@ -34,6 +34,10 @@ workbox.precaching.precacheAndRoute([ url: '/web/js/sw/workbox-window.prod.umd.min.js', revision: '2023.05.03', }, + { + url: '/rp/LOB20GsbD-KR9Gwi_Ukp8-BJZCQ.br.js', + revision: '2023.05.04', + }, // html { url: '/web/chat.html', @@ -97,3 +101,20 @@ self.addEventListener('message', event => { replyPort.postMessage(SW_VERSION); } }); + +// 安装阶段可删除旧缓存等等 +self.addEventListener('install', async event => { + await caches.open(`${CACHE_PREFIX}-js`).then(async cache => { + const requests = await cache.keys(); + return await Promise.all( + requests.map(request => { + if (true || request.url.includes('xxx')) { + console.log(`del old cache : `, request.url); + return cache.delete(request); + } else { + return Promise.resolve(); + } + }) + ); + }); +}); diff --git a/web/web.go b/web/web.go new file mode 100644 index 0000000000..586ac5bb3f --- /dev/null +++ b/web/web.go @@ -0,0 +1,6 @@ +package web + +import "embed" + +//go:embed * +var WebFS embed.FS