Compare commits

..

6 Commits

Author SHA1 Message Date
ben Gutier
8cb096239b 修复多个token请求混乱问题 2023-09-11 01:31:07 +08:00
ben Gutier
6ac0e083e6 修复无法后台运行 2023-09-10 23:10:55 +08:00
ben Gutier
e331cb9885 修复无法后台运行 2023-09-10 23:04:45 +08:00
ben Gutier
d274cf819a 修复无法后台运行 2023-09-10 23:04:14 +08:00
ben Gutier
088c15e54b 1.0.0 2023-09-10 20:57:46 +08:00
ben Gutier
23b7c3e825 修改 2023-09-10 20:48:39 +08:00
5 changed files with 293 additions and 139 deletions

View File

@ -1,12 +1,15 @@
# share-copilot # share-copilot
- 作为代理服务器中转copilot插件的API的请求
- 支持vscode插件和jetbrains插件
- 支持多用户共享一个token
- 优化请求逻辑降低token被ban概率别万人骑基本不可能封
![软件系统网络架构.png](https://img1.imgtp.com/2023/09/10/qTL8A2u9.png)
# 一、自行编译:
``` ```
测试环境:Linux4.18.0-305.3.1.el8.x86_64 GNU/Linux 测试环境:Linux4.18.0-305.3.1.el8.x86_64 GNU/Linux
```
# 一、自编译:
需要 go环境 出问题自行chatgpt 需要 go环境 出问题自行chatgpt
```
```sh ```sh
wget https://dl.google.com/go/go1.21.1.linux-amd64.tar.gz wget https://dl.google.com/go/go1.21.1.linux-amd64.tar.gz
tar -C /usr/local -xzf go1.21.1.linux-amd64.tar.gz tar -C /usr/local -xzf go1.21.1.linux-amd64.tar.gz
@ -45,7 +48,7 @@ bash install.sh
##### 2.1 config.json文件说明 ##### 2.1 config.json文件说明
```js ```js
domain//绑定域名 domain//监听域名或ip 可用nginx反代
host //ip host //ip
@ -90,15 +93,15 @@ verification//自定义验证
### 3.启动 ### 3.启动
``` ```
scop r --运行 scop r --运行 [Esc退出]
scop rb --后台运行 scop rb --后台运行
scop st --停止 scop st --停止
scop v --查看状态 scop v --查看状态
``` ```
### 4.完整示例一 ### 4.完整例子
config.json ##### 4.1 服务端配置修改config.json
```json ```json
{ {
@ -120,25 +123,51 @@ config.json
} }
``` ```
运行截图 ##### 4.2 运行成功截图
![demo.png](https://img1.imgtp.com/2023/09/09/jNTqxhR8.png) ![微信截图_20230910202900.png](https://img1.imgtp.com/2023/09/10/TbXIaziq.png)
时间-合计请求次数: 0 | 成功次数 0 | 失败次数 0 统计次数在00:00:00归零 [Jetbrains]:用于Jetbrains插件
[Vscode]: 用于Vscode插件
[User Request]: 插件请求代理服务器成功次数--统计次数在00:00:00归零
[GithubApi request]: 代理服务器请求githubApi次数
`正常情况下GithubApi request小于User Request因为请求的Token 几十分钟才过期`
修改%userprofile%\AppData\Local\github-copilot\host.json ------
##### 4.3本地配置修改:
**以windows为例**
- **jetbrains插件修改%userprofile%\AppData\Local\github-copilot\host.json**
```json ```json
{ {
"github.com":{ "github.com":{
"user":"suibian", "user":"suibian",//随意填写
"oauth_token":"i_am_free", "oauth_token":"i_am_free",//与上面verification对应
"dev_override":{ "dev_override":{
"copilot_token_url":"https://api.example.com/copilot_internal/v2/token" "copilot_token_url":"https://api.example.com/copilot_internal/v2/token"
//你的地址
} }
} }
} }
``` ```
测试: - **vscode插件修改%userprofile%\.vscode\extensions\github.copilot-xxxx\dist\extension.js**
```js
//添加下列代码注意vscode插件更新需要重新添加jetbrains则不用
process.env.GITHUB_TOKEN="i_am_free"; //与上面verification对应
process.env.GITHUB_API_URL="https://api.example.com"; //你的地址
process.env.CODESPACES="true";
process.env.GITHUB_SERVER_URL="https://github.com";
```
------
##### **4.4测试成功:**
![demo2.png](https://img1.imgtp.com/2023/09/09/FHDNLixL.png) ![demo2.png](https://img1.imgtp.com/2023/09/09/FHDNLixL.png)
# 三、求投喂!谢谢您!
![微信截图_20230910204613.png](https://img1.imgtp.com/2023/09/10/G7qMbyxx.png)

Binary file not shown.

View File

@ -1,7 +1,6 @@
package main package main
import ( import (
"github.com/nsf/termbox-go"
"sync" "sync"
) )
@ -20,17 +19,11 @@ type Config struct {
Verification string `json:"verification"` Verification string `json:"verification"`
} }
// InfoItem 表示要显示的内容项
type InfoItem struct {
Title string
Value string
TitleColor termbox.Attribute // 标题颜色
ValueColor termbox.Attribute // 内容颜色
}
var ( var (
//初始化需要返回给客户端的响应体 //初始化需要返回给客户端的响应体
responseData map[string]interface{} tokenMap = make(map[string]map[string]interface{})
//有效的token列表
validTokenList = make(map[string]bool)
requestCountMutex sync.Mutex requestCountMutex sync.Mutex
githubApiCount = 0 githubApiCount = 0
requestCount = 0 requestCount = 0

View File

@ -4,9 +4,9 @@ import (
"crypto/tls" "crypto/tls"
"encoding/json" "encoding/json"
"fmt" "fmt"
"github.com/fatih/color"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/go-resty/resty/v2" "github.com/go-resty/resty/v2"
"github.com/nsf/termbox-go"
"io" "io"
"log" "log"
"math/rand" "math/rand"
@ -15,6 +15,7 @@ import (
"path/filepath" "path/filepath"
"strconv" "strconv"
"strings" "strings"
"sync"
"time" "time"
) )
@ -30,13 +31,18 @@ func main() {
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
// 初始化有效的token列表
initValidTokenList()
// 定义路由 // 定义路由
domainDefault := engine.Group("/", DomainMiddleware(configFile.Server.Domain)) domainDefault := engine.Group("/", DomainMiddleware(configFile.Server.Domain))
domainDefault.GET("/copilot_internal/v2/token", getToken()) domainDefault.GET("/copilot_internal/v2/token", getGithubToken())
// 初始化服务器
initServer(engine) initServer(engine)
// 显示信息 // 显示信息
showMsg() showMsg()
} }
// 初始化服务器
func initServer(engine *gin.Engine) { func initServer(engine *gin.Engine) {
// 配置支持的应用程序协议 // 配置支持的应用程序协议
server := &http.Server{ server := &http.Server{
@ -62,74 +68,28 @@ func initServer(engine *gin.Engine) {
} }
}() }()
} }
func showMsg() {
exitChan := make(chan bool)
dataGetter := func() []InfoItem {
var url = ""
if configFile.Server.Port == 80 {
url = "http://" + configFile.Server.Domain
} else if configFile.Server.Port == 443 {
url = "https://" + configFile.Server.Domain
} else {
url = "http://" + configFile.Server.Domain + ":" + strconv.Itoa(configFile.Server.Port)
}
requestCountMutex.Lock()
sCount := successCount
gCount := githubApiCount
requestCountMutex.Unlock()
currentTime := time.Now().Format("2006-01-02 15:04:05")
if "00:00:00" == currentTime {
resetRequestCount()
}
return []InfoItem{
{
Title: "[Jetbrains]",
Value: url + "/copilot_internal/v2/token",
TitleColor: termbox.ColorGreen,
ValueColor: termbox.ColorYellow,
},
{
Title: "[Vscode]",
Value: url,
TitleColor: termbox.ColorGreen,
ValueColor: termbox.ColorYellow,
},
{
Title: "[User Request]:",
Value: strconv.Itoa(sCount),
TitleColor: termbox.ColorGreen,
ValueColor: termbox.ColorYellow,
},
{
Title: "[GithubApi request]:",
Value: strconv.Itoa(gCount),
TitleColor: termbox.ColorGreen,
ValueColor: termbox.ColorYellow,
},
{
Title: "Time:",
Value: currentTime,
TitleColor: termbox.ColorGreen,
ValueColor: termbox.ColorYellow,
},
}
}
exitChan = make(chan bool)
// 调用显示函数,传递数据获取器函数
go DisplayInfo(dataGetter, exitChan)
for {
ev := termbox.PollEvent()
if ev.Type == termbox.EventKey && ev.Key == termbox.KeyEsc {
exitChan <- true // 发送退出信号
break
}
}
// 初始化有效的token列表
func initValidTokenList() {
//为了安全起见,应该等待请求完成并处理其响应。
var wg sync.WaitGroup
for _, token := range configFile.CopilotConfig.Token {
wg.Add(1)
go func(token string) {
defer wg.Done()
if getGithubApi(token) {
validTokenList[token] = true
}
}(token)
}
wg.Wait()
} }
func getToken() gin.HandlerFunc {
// 获取Github的token
func getGithubToken() gin.HandlerFunc {
return func(c *gin.Context) { return func(c *gin.Context) {
// 请求计数 // 请求计数
incrementRequestCount() requestCount++
// 如果配置了verification则需要获取请求头中的Authorization令牌 // 如果配置了verification则需要获取请求头中的Authorization令牌
if configFile.Verification != "" { if configFile.Verification != "" {
token := c.GetHeader("Authorization") token := c.GetHeader("Authorization")
@ -137,61 +97,91 @@ func getToken() gin.HandlerFunc {
configCert := strings.ReplaceAll(configFile.Verification, " ", "") configCert := strings.ReplaceAll(configFile.Verification, " ", "")
if tokenStr != "token"+configCert { if tokenStr != "token"+configCert {
// 拒绝不符合Verification的请求 // 拒绝不符合Verification的请求
c.JSON(http.StatusBadRequest, gin.H{ badRequest(c)
"message": "Bad credentials",
"documentation_url": "https://docs.github.com/rest"})
return return
} }
} }
//从有效的token列表中随机获取一个token
token := getRandomToken(validTokenList)
//判断tokenMap 里的token是否存在
if _, exists := tokenMap[token]; exists {
respDataMap := tokenMap[token]
//判断时间戳key是否存在 //判断时间戳key是否存在
if _, exists := responseData["expires_at"]; exists { if _, exists := respDataMap["expires_at"]; exists {
// 获取当前时间的Unix时间戳 // 获取当前时间的Unix时间戳
currentTime := time.Now().Unix() currentTime := time.Now().Unix()
if expiresAt, ok := responseData["expires_at"].(float64); ok { if expiresAt, ok := respDataMap["expires_at"].(float64); ok {
// 判断expires_at是否已经过期 // 判断expires_at是否已经过期
expiresAtInt64 := int64(expiresAt) expiresAtInt64 := int64(expiresAt)
//提前一分钟请求 //提前一分钟请求
if expiresAtInt64 > currentTime+60 { if expiresAtInt64 > currentTime+60 {
//fmt.Println("\n未过期无需请求") //fmt.Println("\n未过期无需请求")
respProxy(c) proxyResp(c, tokenMap[token])
} else { } else {
//fmt.Println("\n已过期重新请求") //fmt.Println("\n已过期重新请求")
getGithubApi(c) if getGithubApi(token) {
respProxy(c) proxyResp(c, tokenMap[token])
return
} else {
badRequest(c)
}
} }
} else { } else {
fmt.Println("Age is not an int") badRequest(c)
} }
} else { } else {
//向githubApi发送请求 //tokenMap里的token对应的返回体不存在expires_at
//fmt.Println("\n第一次请求") if getGithubApi(token) {
getGithubApi(c) proxyResp(c, tokenMap[token])
respProxy(c) return
} else {
badRequest(c)
}
}
} else {
//不存在则githubApi发送请求并存到tokenMap
if getGithubApi(token) {
proxyResp(c, tokenMap[token])
return
} else {
badRequest(c)
}
} }
} }
} }
func respProxy(c *gin.Context) {
// 请求错误
func badRequest(c *gin.Context) {
c.JSON(http.StatusBadRequest, gin.H{
"message": "Bad credentials",
"documentation_url": "https://docs.github.com/rest"})
}
// 本服务器响应
func proxyResp(c *gin.Context, respDataMap map[string]interface{}) {
// 将map转换为JSON字符串 // 将map转换为JSON字符串
responseJSON, err := json.Marshal(responseData) responseJSON, err := json.Marshal(respDataMap)
if err != nil { if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "JSON marshaling error"}) c.JSON(http.StatusInternalServerError, gin.H{"error": "JSON marshaling error"})
} }
// 请求成功统计 // 请求成功统计
incrementSuccessCount() successCount++
// 将JSON字符串作为响应体返回 // 将JSON字符串作为响应体返回
c.Header("Content-Type", "application/json") c.Header("Content-Type", "application/json")
c.String(http.StatusOK, string(responseJSON)) c.String(http.StatusOK, string(responseJSON))
} }
func getGithubApi(c *gin.Context) {
// 请求githubApi
func getGithubApi(token string) bool {
githubApiCount++ githubApiCount++
// 设置请求头 // 设置请求头
headers := map[string]string{ headers := map[string]string{
"Authorization": "token " + getRandomToken(configFile.CopilotConfig.Token), "Authorization": "token " + token,
"editor-version": c.GetHeader("editor-version"), /*"editor-version": c.GetHeader("editor-version"),
"editor-plugin-version": c.GetHeader("editor-plugin-version"), "editor-plugin-version": c.GetHeader("editor-plugin-version"),
"user-agent": c.GetHeader("user-agent"), "user-agent": c.GetHeader("user-agent"),
"accept": c.GetHeader("accept"), "accept": c.GetHeader("accept"),
"accept-encoding": c.GetHeader("accept-encoding"), "accept-encoding": c.GetHeader("accept-encoding"),*/
} }
// 发起GET请求 // 发起GET请求
response, err := resty.New().R(). response, err := resty.New().R().
@ -199,17 +189,28 @@ func getGithubApi(c *gin.Context) {
Get(configFile.CopilotConfig.GithubApiUrl) Get(configFile.CopilotConfig.GithubApiUrl)
if err != nil { if err != nil {
// 处理请求错误 // 处理请求错误
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return false
return
} }
// 判断响应状态码
err = json.Unmarshal(response.Body(), &responseData) if response.StatusCode() == http.StatusOK {
// 响应状态码为200 OK
respDataMap := map[string]interface{}{}
err = json.Unmarshal(response.Body(), &respDataMap)
if err != nil { if err != nil {
// 处理JSON解析错误 // 处理JSON解析错误
c.JSON(http.StatusInternalServerError, gin.H{"error": "JSON parsing error"}) return false
return }
//token map
tokenMap[token] = respDataMap
return true
} else {
// 处理其他状态码
delete(validTokenList, token)
return false
} }
} }
// 初始化配置文件
func initConfig() Config { func initConfig() Config {
// 读取配置文件 // 读取配置文件
exePath, err := os.Executable() exePath, err := os.Executable()
@ -236,26 +237,30 @@ func initConfig() Config {
} }
return config return config
} }
func incrementRequestCount() {
requestCount++ // 重置请求计数
}
func incrementSuccessCount() {
successCount++
}
func resetRequestCount() { func resetRequestCount() {
requestCountMutex.Lock() requestCountMutex.Lock()
defer requestCountMutex.Unlock() defer requestCountMutex.Unlock()
requestCount = 0 requestCount = 0
successCount = 0 successCount = 0
} }
func getRandomToken(tokens []string) string {
if len(tokens) == 0 { // 从map中随机获取一个key
func getRandomToken(m map[string]bool) string {
keys := make([]string, 0, len(m))
for k := range m {
keys = append(keys, k)
}
if len(keys) == 0 {
return "" // 返回空字符串或处理其他错误情况 return "" // 返回空字符串或处理其他错误情况
} }
r := rand.New(rand.NewSource(time.Now().UnixNano())) r := rand.New(rand.NewSource(time.Now().UnixNano()))
randomIndex := r.Intn(len(tokens)) randomIndex := r.Intn(len(keys))
return tokens[randomIndex] return keys[randomIndex]
} }
// DomainMiddleware 域名中间件
func DomainMiddleware(domain string) gin.HandlerFunc { func DomainMiddleware(domain string) gin.HandlerFunc {
return func(c *gin.Context) { return func(c *gin.Context) {
// 检查域名是否匹配 // 检查域名是否匹配
@ -268,3 +273,44 @@ func DomainMiddleware(domain string) gin.HandlerFunc {
} }
} }
} }
// 显示信息
func showMsg() {
var url = ""
if configFile.Server.Port == 80 {
url = "http://" + configFile.Server.Domain
} else if configFile.Server.Port == 443 {
url = "https://" + configFile.Server.Domain
} else {
url = "http://" + configFile.Server.Domain + ":" + strconv.Itoa(configFile.Server.Port)
}
var jetStr = color.WhiteString("[Jetbrains]")
var vsStr = color.WhiteString("[Vscode]")
var valid = color.WhiteString("[Valid tokens]")
fmt.Println(jetStr + ": " + color.HiBlueString(url+"/copilot_internal/v2/token"))
fmt.Println(vsStr + ": " + color.HiBlueString(url))
fmt.Println(valid + ": " + color.HiBlueString(strconv.Itoa(len(validTokenList))))
fmt.Println(color.WhiteString("-----------------------------------------------------------------------"))
for {
requestCountMutex.Lock()
sCount := successCount
tCount := requestCount
gCount := githubApiCount
requestCountMutex.Unlock()
currentTime := time.Now().Format("2006-01-02 15:04:05")
if "00:00:00" == currentTime {
resetRequestCount()
}
var s2 = color.WhiteString("[Succeed]")
var s3 = color.WhiteString("[Failed]")
var s4 = color.WhiteString("[GithubApi]")
// 打印文本
fmt.Printf("\033[G%s - %s: %s %s: %s %s: %s ",
color.HiYellowString(currentTime),
s2, color.GreenString(strconv.Itoa(sCount)),
s3, color.RedString(strconv.Itoa(tCount-sCount)),
s4, color.CyanString(strconv.Itoa(gCount)))
time.Sleep(1 * time.Second) //
}
}

86
source/test.go Normal file
View File

@ -0,0 +1,86 @@
package main
import (
"encoding/json"
"github.com/gin-gonic/gin"
"net/http"
)
func getGithubTest(c *gin.Context, token string) {
githubApiCount++
data1 := `
{
"chat_enabled": false,
"code_quote_enabled": false,
"code_quote_v2_enabled": false,
"copilotignore_enabled": false,
"expires_at": 3194360727,
"prompt_8k": true,
"public_suggestions": "disabled",
"refresh_in": 1500,
"sku": "free_educational",
"telemetry": "disabled",
"token": "tid=;exp=1694360727;sku=free_educational;st=dotcom;8kp=1:",
"tracking_id": ""
}
`
data2 := `
{
"chat_enabled": false,
"code_quote_enabled": false,
"code_quote_v2_enabled": false,
"copilotignore_enabled": false,
"expires_at": 2294360727,
"prompt_8k": true,
"public_suggestions": "disabled",
"refresh_in": 1500,
"sku": "free_educational",
"telemetry": "disabled",
"token": "tid=;exp=1694360727;sku=free_educational;st=dotcom;8kp=1:",
"tracking_id": ""
}
`
data3 := `
{
"chat_enabled": false,
"code_quote_enabled": false,
"code_quote_v2_enabled": false,
"copilotignore_enabled": false,
"expires_at": 3394360727,
"prompt_8k": true,
"public_suggestions": "disabled",
"refresh_in": 1500,
"sku": "free_educational",
"telemetry": "disabled",
"token": "tid=;exp=1694360727;sku=free_educational;st=dotcom;8kp=1:",
"tracking_id": ""
}
`
//响应体map
var respDataMap = make(map[string]interface{})
if token == "1" {
err := json.Unmarshal([]byte(data1), &respDataMap)
if err != nil {
// 处理JSON解析错误
c.JSON(http.StatusInternalServerError, gin.H{"error": "JSON parsing error"})
return
}
} else if token == "2" {
err := json.Unmarshal([]byte(data2), &respDataMap)
if err != nil {
// 处理JSON解析错误
c.JSON(http.StatusInternalServerError, gin.H{"error": "JSON parsing error"})
return
}
} else {
err := json.Unmarshal([]byte(data3), &respDataMap)
if err != nil {
// 处理JSON解析错误
c.JSON(http.StatusInternalServerError, gin.H{"error": "JSON parsing error"})
return
}
}
//token map
tokenMap[token] = respDataMap
}