This commit is contained in:
史悦
2025-08-13 19:03:20 +08:00
commit d62a2e9ed9
73 changed files with 7296 additions and 0 deletions

View File

@@ -0,0 +1,153 @@
package auth
import (
_ "embed"
"fmt"
"github.com/gin-gonic/gin"
"net/http"
"os"
"ripper/internal/app/github_auth"
"ripper/internal/middleware"
"ripper/internal/response"
jwtpkg "ripper/pkg/jwt"
"time"
)
type postLoginDeviceCodeRequest struct {
ClientId string `json:"client_id" form:"client_id"`
}
type postLoginDeviceCodeResponse struct {
DeviceCode string `json:"device_code"` // 设备代码
UserCode string `json:"user_code"` // 用户代码
VerificationUrl string `json:"verification_uri"` // 验证地址
ExpiresIn int `json:"expires_in"` // 过期时间
Interval int `json:"interval"` // 间隔时间
}
type loginDeviceRequestInfo struct {
Code string `json:"code"`
Authorization string `json:"authorization"`
DisplayUserName string `json:"displayUserName,omitempty"`
Password string `json:"password"`
}
func postLoginDeviceCode(ctx *gin.Context) {
cli := postLoginDeviceCodeRequest{}
if err := ctx.ShouldBind(&cli); err != nil {
response.FailJson(ctx, response.FailStruct{
Code: -1,
Msg: "Invalid client id.",
}, false)
return
}
if cli.ClientId == "" {
response.FailJson(ctx, response.FailStruct{
Code: -1,
Msg: "Client id is required.",
}, false)
return
}
uid, devid, err := github_auth.BindClientToCode(cli.ClientId, 1800)
if err != nil {
response.FailJson(ctx, response.FailStruct{
Code: -1,
Msg: err.Error(),
}, false)
return
}
ctx.JSON(http.StatusOK, postLoginDeviceCodeResponse{
DeviceCode: devid,
UserCode: uid,
VerificationUrl: fmt.Sprintf("%s/login/device?user_code=%s", os.Getenv("DEFAULT_BASE_URL"), uid),
ExpiresIn: 1800,
Interval: 5,
})
}
func postLoginOauthAccessToken(ctx *gin.Context) {
v, exists := ctx.Get("client_auth_info")
if !exists {
ctx.JSON(http.StatusOK, gin.H{
"error": "authorization_pending",
"error_description": "The authorization request is still pending.",
"error_uri": "https://docs.github.com/developers/apps/authorizing-oauth-apps#error-codes-for-the-device-flow",
})
return
}
cliAuthInfo := v.(*github_auth.ClientAuthInfo)
t := time.Now()
t.Add(24 * 3 * time.Hour)
u, err := github_auth.GetClientAuthInfo(cliAuthInfo.UserCode)
if err != nil {
ctx.JSON(http.StatusOK, gin.H{
"error": "access_denied",
"error_description": "You must make a new request for a device code.",
"error_uri": "https://docs.github.com/developers/apps/authorizing-oauth-apps#error-codes-for-the-device-flow",
})
return
}
tk, _ := jwtpkg.CreateToken(&middleware.UserLoad{
UserDisplayName: cliAuthInfo.DisplayUserName,
CardCode: u.CardCode,
Client: cliAuthInfo.ClientId,
RegisteredClaims: jwtpkg.CreateStandardClaims(t.Unix(), "user"),
})
_ = github_auth.RemoveClientAuthInfoByDeviceCode(cliAuthInfo.ClientId)
ctx.JSON(http.StatusOK, gin.H{
"access_token": tk,
"scope": "",
"token_type": "bearer",
})
}
func postLoginDevice(ctx *gin.Context) {
var info loginDeviceRequestInfo
if err := response.BindStruct(ctx, &info); err != nil {
response.FailJson(ctx, response.FailStruct{
Code: 422,
Msg: "请求参数错误",
}, false)
return
}
// 验证密码
loginPassword := os.Getenv("LOGIN_PASSWORD")
if loginPassword != "" && info.Password != loginPassword {
response.FailJson(ctx, response.FailStruct{
Code: 422,
Msg: "访问密码错误",
}, false)
return
}
// 检查code是否存在
authInfo, err := github_auth.GetClientAuthInfo(info.Code)
if err != nil {
response.FailJson(ctx, response.FailStruct{
Code: 422,
Msg: "授权码填写错误",
}, false)
return
}
err = github_auth.UpdateClientAuthStatusByDeviceCode(authInfo.DeviceCode, info.Authorization, info.DisplayUserName)
if err != nil {
response.FailJson(ctx, response.FailStruct{
Code: 500,
Msg: "系统异常, 请稍后再试",
}, false)
return
}
response.SuccessJson(ctx, "ok")
}
func getLoginDevice(ctx *gin.Context) {
ctx.Header("Content-Type", "text/html; charset=utf-8")
ctx.HTML(http.StatusOK, "code.html", gin.H{})
}
func getHelpPage(ctx *gin.Context) {
ctx.Header("Content-Type", "text/html; charset=utf-8")
ctx.HTML(http.StatusOK, "help.html", gin.H{})
}

View File

@@ -0,0 +1,105 @@
package auth
import (
"bytes"
"encoding/json"
"net/http"
"os"
"time"
"github.com/gin-gonic/gin"
"ripper/internal/response"
)
const (
clientID = "Iv1.b507a08c87ecfe98"
deviceCodeURL = "https://github.com/login/device/code"
tokenURL = "https://github.com/login/oauth/access_token"
)
type githubLoginDeviceRequest struct {
DeviceCode string `form:"device_code" json:"device_code" binding:"required"`
}
// getDeviceCode returns the device code for GitHub login.
func getDeviceCode(c *gin.Context) {
body := map[string]string{
"client_id": clientID,
}
result, err := makeRequest(c, http.MethodPost, deviceCodeURL, body)
if err != nil {
c.JSON(http.StatusUnprocessableEntity, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, result)
}
// getGhuToken returns the GitHub user token.
func getGhuToken(c *gin.Context) {
var params githubLoginDeviceRequest
if err := c.ShouldBind(&params); err != nil {
response.FailJson(c, response.FailStruct{
Code: -1,
Msg: "Invalid request: " + err.Error(),
}, false)
return
}
body := map[string]string{
"client_id": clientID,
"device_code": params.DeviceCode,
"grant_type": "urn:ietf:params:oauth:grant-type:device_code",
}
result, err := makeRequest(c, http.MethodPost, tokenURL, body)
if err != nil {
c.JSON(http.StatusUnprocessableEntity, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, result)
}
// getGithubLoginDevice returns the login page for GitHub.
func getGithubLoginDevice(ctx *gin.Context) {
ctx.Header("Content-Type", "text/html; charset=utf-8")
ctx.HTML(http.StatusOK, "login.html", gin.H{})
}
// makeRequest makes a request to the given URL with the given method and body.
func makeRequest(c *gin.Context, method, url string, body map[string]string) (interface{}, error) {
jsonBody, err := json.Marshal(body)
if err != nil {
return nil, err
}
req, err := http.NewRequestWithContext(c, method, url, bytes.NewBuffer(jsonBody))
if err != nil {
return nil, err
}
req.Header.Set("accept", "application/json")
req.Header.Set("content-type", "application/json")
req.Header.Set("editor-plugin-version", "copilot-intellij/1.5.21.6667")
req.Header.Set("copilot-language-server-version", "1.228.0")
req.Header.Set("user-agent", "GithubCopilot/1.228.0")
req.Header.Set("editor-version", "JetBrains-IU/242.21829.142")
httpClientTimeout, _ := time.ParseDuration(os.Getenv("HTTP_CLIENT_TIMEOUT") + "s")
client := &http.Client{Timeout: httpClientTimeout}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
var result interface{}
err = json.NewDecoder(resp.Body).Decode(&result)
if err != nil {
return nil, err
}
return result, nil
}

View File

@@ -0,0 +1,100 @@
package auth
import (
"encoding/json"
"github.com/gin-gonic/gin"
"net/http"
"os"
"ripper/internal/app/github_auth"
"ripper/internal/cache"
"ripper/internal/middleware"
"ripper/internal/response"
jwtpkg "ripper/pkg/jwt"
"time"
)
type getLoginOauthAuthorizeRequest struct {
ClientId string `json:"client_id" form:"client_id"`
Prompt string `json:"prompt" form:"prompt"`
RedirectUri string `json:"redirect_uri" form:"redirect_uri"`
Scope string `json:"scope" form:"scope"`
State string `json:"state" form:"state"`
}
func getLoginOauthAuthorize(ctx *gin.Context) {
req := getLoginOauthAuthorizeRequest{}
err := ctx.BindQuery(&req)
if err != nil {
response.FailJson(ctx, response.FailStruct{
Code: -1,
Msg: "Invalid request.",
}, false)
return
}
vsCopilotClientId := os.Getenv("VS_COPILOT_CLIENT_ID")
if req.ClientId != vsCopilotClientId {
response.FailJson(ctx, response.FailStruct{
Code: -1,
Msg: "Invalid client id.",
}, false)
return
}
oauthCode := github_auth.GenDevicesCode(20)
cai := github_auth.ClientOAuthInfo{
ClientId: req.ClientId,
Code: oauthCode,
Scope: req.Scope,
}
cacheKey := "oauth2_authorize_" + req.ClientId
caiInfo, _ := json.Marshal(cai)
err = cache.Set(cacheKey, caiInfo, 300)
if err != nil {
response.FailJson(ctx, response.FailStruct{
Code: -1,
Msg: "Internal error.",
}, false)
return
}
// Redirect to the client's redirect_uri
browserSessionId := github_auth.GenDevicesCode(64)
ctx.Redirect(302, req.RedirectUri+"?browserSessionId="+browserSessionId+"&code="+oauthCode+"&state="+req.State)
}
func postLoginOauthAccessTokenForVs2022(ctx *gin.Context) {
v, exists := ctx.Get("client_auth_info")
if !exists {
response.FailJson(ctx, response.FailStruct{
Code: -1,
Msg: "Invalid client id.",
}, false)
return
}
cliAuthInfo := v.(*github_auth.ClientOAuthInfo)
t := time.Now()
t.Add(24 * 3 * time.Hour)
tk, _ := jwtpkg.CreateToken(&middleware.UserLoad{
CardCode: cliAuthInfo.Code,
Client: cliAuthInfo.ClientId,
RegisteredClaims: jwtpkg.CreateStandardClaims(t.Unix(), "user"),
})
ctx.JSON(http.StatusOK, gin.H{
"access_token": tk,
"scope": cliAuthInfo.Scope,
"token_type": "bearer",
})
}
func getSiteSha(ctx *gin.Context) {
ctx.Header("X-GitHub-Request-Id", "C0E1:6A1A:1A1F:2A1D:1A1F:1A1F:1A1F:1A1F")
ctx.JSON(http.StatusOK, gin.H{})
}
func getLoginConfig(ctx *gin.Context) {
loginPassword := os.Getenv("LOGIN_PASSWORD")
ctx.JSON(http.StatusOK, gin.H{
"is_login_password": loginPassword != "",
})
}

View File

@@ -0,0 +1,42 @@
package auth
import (
"github.com/gin-gonic/gin"
"ripper/internal/middleware"
"strings"
)
func GinApi(g *gin.RouterGroup) {
g.GET("/help", getHelpPage)
// 启动设备代码登录流程
g.POST("/login/device/code", postLoginDeviceCode)
g.POST("/login/device", postLoginDevice)
g.GET("/login/device", getLoginDevice)
g.POST("/login/oauth/access_token", func(ctx *gin.Context) {
if strings.Index(ctx.Request.UserAgent(), "VSTeamExplorer") != -1 {
middleware.AuthCodeFlowCheckAuth(ctx)
} else {
middleware.DeviceCodeCheckAuth(ctx)
}
}, func(ctx *gin.Context) {
if strings.Index(ctx.Request.UserAgent(), "VSTeamExplorer") != -1 {
postLoginOauthAccessTokenForVs2022(ctx)
} else {
postLoginOauthAccessToken(ctx)
}
})
// oauth2 登录
g.GET("/login/oauth/authorize", getLoginOauthAuthorize)
// enterprise 验证
g.GET("/site/sha", getSiteSha)
// 获取登录页面配置
g.GET("/login/config", getLoginConfig)
// GitHub模拟登录获取 ghu_token
g.GET("/github/login/device/code", getGithubLoginDevice)
g.POST("/github/login/device/code", getDeviceCode)
g.POST("/github/login/ghu-token", getGhuToken)
}