mirror of
https://github.com/komari-monitor/komari.git
synced 2026-11-11 00:30:54 +00:00
Compare commits
13
Commits
1.0.4
...
1.0.4-fix3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8e8033bb35 | ||
|
|
71b0903b0e | ||
|
|
d0eefe2dd2 | ||
|
|
2c547bab68 | ||
|
|
2dc04b5ecf | ||
|
|
e2dae1ec7b | ||
|
|
59d923e168 | ||
|
|
51844c27f1 | ||
|
|
53171affca | ||
|
|
d31d12e59f | ||
|
|
3715f2b3f9 | ||
|
|
cc3d54bff4 | ||
|
|
ef376a88b8 |
No files matched your search
@@ -96,3 +96,5 @@ jobs:
|
||||
platforms: linux/amd64,linux/arm64
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
no-cache: true
|
||||
provenance: false
|
||||
@@ -65,7 +65,7 @@ func WebSocketReport(c *gin.Context) {
|
||||
}
|
||||
upgrader := websocket.Upgrader{
|
||||
CheckOrigin: func(r *http.Request) bool {
|
||||
return true
|
||||
return true // 被控
|
||||
},
|
||||
}
|
||||
// Upgrade the HTTP connection to a WebSocket connection
|
||||
|
||||
@@ -22,7 +22,7 @@ func EstablishConnection(c *gin.Context) {
|
||||
}
|
||||
upgrader := websocket.Upgrader{
|
||||
CheckOrigin: func(r *http.Request) bool {
|
||||
return true // TODO: 检查源站
|
||||
return true // 被控
|
||||
},
|
||||
}
|
||||
conn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
|
||||
|
||||
+9
-7
@@ -48,13 +48,15 @@ func Login(c *gin.Context) {
|
||||
}
|
||||
// 2FA
|
||||
user, _ := accounts.GetUserByUUID(uuid)
|
||||
if user.TwoFactor != "" && data.TwoFa == "" {
|
||||
RespondError(c, http.StatusUnauthorized, "2FA code is required")
|
||||
return
|
||||
}
|
||||
if ok, err := accounts.Verify2Fa(uuid, data.TwoFa); err != nil && ok {
|
||||
RespondError(c, http.StatusUnauthorized, "Invalid 2FA code")
|
||||
return
|
||||
if user.TwoFactor != "" { // 开启了2FA
|
||||
if data.TwoFa == "" {
|
||||
RespondError(c, http.StatusUnauthorized, "2FA code is required")
|
||||
return
|
||||
}
|
||||
if ok, err := accounts.Verify2Fa(uuid, data.TwoFa); err != nil || !ok {
|
||||
RespondError(c, http.StatusUnauthorized, "Invalid 2FA code")
|
||||
return
|
||||
}
|
||||
}
|
||||
// Create session
|
||||
session, err := accounts.CreateSession(uuid, 2592000, c.Request.UserAgent(), c.ClientIP(), "password")
|
||||
|
||||
+18
-4
@@ -31,9 +31,23 @@ func OAuthCallback(c *gin.Context) {
|
||||
// 验证state防止CSRF攻击
|
||||
state, _ := c.Cookie("oauth_state")
|
||||
c.SetCookie("oauth_state", "", -1, "/", "", false, true)
|
||||
if state == "" || state != c.Query("state") {
|
||||
c.JSON(400, gin.H{"status": "error", "error": "Invalid state"})
|
||||
return
|
||||
|
||||
// 获取当前OAuth提供商名称
|
||||
providerName := oauth.CurrentProvider().GetName()
|
||||
|
||||
// 对于QQ登录,由于是通过QQ聚合登录平台中转,state可能会不匹配
|
||||
// 但我们仍然需要验证state的存在性(不能是空的)
|
||||
if providerName == "qq" {
|
||||
if state == "" {
|
||||
c.JSON(400, gin.H{"status": "error", "error": "Invalid state"})
|
||||
return
|
||||
}
|
||||
} else {
|
||||
// 对于其他提供商,严格验证state匹配
|
||||
if state == "" || state != c.Query("state") {
|
||||
c.JSON(400, gin.H{"status": "error", "error": "Invalid state"})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
queries := make(map[string]string)
|
||||
@@ -94,4 +108,4 @@ func OAuthCallback(c *gin.Context) {
|
||||
c.SetCookie("session_token", session, 2592000, "/", "", false, true)
|
||||
auditlog.Log(c.ClientIP(), user.UUID, "logged in (OAuth)", "login")
|
||||
c.Redirect(302, "/admin")
|
||||
}
|
||||
}
|
||||
+1
-3
@@ -30,9 +30,7 @@ func RequestTerminal(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
upgrader := websocket.Upgrader{
|
||||
CheckOrigin: func(r *http.Request) bool {
|
||||
return true
|
||||
},
|
||||
CheckOrigin: ws.CheckOrigin,
|
||||
}
|
||||
conn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
|
||||
if err != nil {
|
||||
|
||||
+2
-1
@@ -4,8 +4,9 @@ import (
|
||||
_ "github.com/komari-monitor/komari/utils/oauth/factory"
|
||||
_ "github.com/komari-monitor/komari/utils/oauth/generic"
|
||||
_ "github.com/komari-monitor/komari/utils/oauth/github"
|
||||
_ "github.com/komari-monitor/komari/utils/oauth/qq"
|
||||
)
|
||||
|
||||
func All() {
|
||||
//empty function to ensure all OIDC providers are registered
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package qq
|
||||
|
||||
import (
|
||||
"github.com/komari-monitor/komari/utils/oauth/factory"
|
||||
"github.com/patrickmn/go-cache"
|
||||
)
|
||||
|
||||
func init() {
|
||||
factory.RegisterOidcProvider(func() factory.IOidcProvider {
|
||||
return &QQ{}
|
||||
})
|
||||
}
|
||||
|
||||
type QQ struct {
|
||||
Addition
|
||||
stateCache *cache.Cache // 用于存储state和用户信息的映射
|
||||
}
|
||||
|
||||
type Addition struct {
|
||||
AggregationURL string `json:"aggregation_url" required:"true" default:"https://login.qjqq.cn"` // 聚合登录地址
|
||||
AppId string `json:"app_id" required:"true"`
|
||||
AppKey string `json:"app_key" required:"true"`
|
||||
LoginType string `json:"login_type" required:"true"` // 登录方式,如qq, google等
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
package qq
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/komari-monitor/komari/utils"
|
||||
"github.com/komari-monitor/komari/utils/oauth/factory"
|
||||
"github.com/patrickmn/go-cache"
|
||||
)
|
||||
|
||||
func (q *QQ) GetName() string {
|
||||
return "qq"
|
||||
}
|
||||
|
||||
func (q *QQ) GetConfiguration() factory.Configuration {
|
||||
return &q.Addition
|
||||
}
|
||||
|
||||
func (q *QQ) GetAuthorizationURL(redirectURI string) (string, string) {
|
||||
state := utils.GenerateRandomString(16)
|
||||
|
||||
// 构建请求QQ聚合登录平台的URL
|
||||
requestURL := fmt.Sprintf(
|
||||
"%s/connect.php?act=login&appid=%s&appkey=%s&type=%s&redirect_uri=%s",
|
||||
q.Addition.AggregationURL,
|
||||
url.QueryEscape(q.Addition.AppId),
|
||||
url.QueryEscape(q.Addition.AppKey),
|
||||
url.QueryEscape(q.Addition.LoginType),
|
||||
url.QueryEscape(redirectURI),
|
||||
)
|
||||
|
||||
// 向聚合登录平台发送请求
|
||||
resp, err := http.Get(requestURL)
|
||||
if err != nil {
|
||||
// 如果请求失败,返回错误信息
|
||||
return "", state
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// 读取响应内容
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return "", state
|
||||
}
|
||||
|
||||
// 解析响应JSON
|
||||
var result struct {
|
||||
Code int `json:"code"`
|
||||
Msg string `json:"msg"`
|
||||
URL string `json:"url"`
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(body, &result); err != nil {
|
||||
return "", state
|
||||
}
|
||||
|
||||
// 检查响应状态
|
||||
if result.Code != 0 {
|
||||
return "", state
|
||||
}
|
||||
|
||||
q.stateCache.Set(state, true, cache.DefaultExpiration)
|
||||
return result.URL, state
|
||||
}
|
||||
|
||||
// OnCallback 处理QQ OAuth回调
|
||||
// 例如:http://localhost:25774/api/oauth_callback?type=qq&code=XXXXXXXXXXXXXXXX
|
||||
// 然后我们使用code参数向聚合登录平台请求用户信息
|
||||
func (q *QQ) OnCallback(ctx context.Context, state string, query map[string]string, callbackURI string) (factory.OidcCallback, error) {
|
||||
// 根据文档,回调地址会附带type和code参数
|
||||
code := query["code"]
|
||||
loginType := query["type"]
|
||||
|
||||
// 如果回调中没有type参数,则使用配置中的LoginType
|
||||
if loginType == "" {
|
||||
loginType = q.Addition.LoginType
|
||||
}
|
||||
|
||||
// 验证state防止CSRF攻击
|
||||
if q.stateCache == nil {
|
||||
return factory.OidcCallback{}, fmt.Errorf("state cache not initialized")
|
||||
}
|
||||
if _, ok := q.stateCache.Get(state); !ok {
|
||||
return factory.OidcCallback{}, fmt.Errorf("invalid state")
|
||||
}
|
||||
if state == "" {
|
||||
return factory.OidcCallback{}, fmt.Errorf("invalid state")
|
||||
}
|
||||
|
||||
// 检查是否提供了Authorization Code
|
||||
if code == "" {
|
||||
return factory.OidcCallback{}, fmt.Errorf("no authorization code provided")
|
||||
}
|
||||
|
||||
// 通过Authorization Code获取用户信息
|
||||
// 根据文档,请求URL应为: {AggregationURL}/connect.php?act=callback&appid={appid}&appkey={appkey}&type={登录方式}&code={code}
|
||||
callbackURL := fmt.Sprintf(
|
||||
"%s/connect.php?act=callback&appid=%s&appkey=%s&type=%s&code=%s",
|
||||
q.Addition.AggregationURL,
|
||||
url.QueryEscape(q.Addition.AppId),
|
||||
url.QueryEscape(q.Addition.AppKey),
|
||||
url.QueryEscape(loginType),
|
||||
url.QueryEscape(code),
|
||||
)
|
||||
|
||||
resp, err := http.Get(callbackURL)
|
||||
if err != nil {
|
||||
return factory.OidcCallback{}, fmt.Errorf("failed to get user info: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// 检查HTTP响应状态
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return factory.OidcCallback{}, fmt.Errorf("HTTP request failed with status code: %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
// 读取响应
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return factory.OidcCallback{}, fmt.Errorf("failed to read response: %v", err)
|
||||
}
|
||||
|
||||
// 解析响应
|
||||
var result struct {
|
||||
Code int `json:"code"`
|
||||
Msg string `json:"msg"`
|
||||
Type string `json:"type"`
|
||||
SocialUid string `json:"social_uid"`
|
||||
AccessToken string `json:"access_token"`
|
||||
FaceImg string `json:"faceimg"`
|
||||
Nickname string `json:"nickname"`
|
||||
Gender string `json:"gender"`
|
||||
Location string `json:"location"`
|
||||
IP string `json:"ip"`
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(body, &result); err != nil {
|
||||
return factory.OidcCallback{}, fmt.Errorf("failed to parse callback response: %v, response body: %s", err, string(body))
|
||||
}
|
||||
|
||||
// 检查返回状态码
|
||||
if result.Code != 0 {
|
||||
return factory.OidcCallback{}, fmt.Errorf("QQ login callback failed with code %d: %s", result.Code, result.Msg)
|
||||
}
|
||||
|
||||
// 检查是否返回了用户唯一标识
|
||||
if result.SocialUid == "" {
|
||||
return factory.OidcCallback{}, fmt.Errorf("empty social_uid returned, full response: %s", string(body))
|
||||
}
|
||||
|
||||
// 返回用户唯一标识
|
||||
return factory.OidcCallback{UserId: result.SocialUid}, nil
|
||||
}
|
||||
|
||||
func (q *QQ) Init() error {
|
||||
q.stateCache = cache.New(time.Minute*5, time.Minute*10)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (q *QQ) Destroy() error {
|
||||
q.stateCache.Flush()
|
||||
return nil
|
||||
}
|
||||
|
||||
var _ factory.IOidcProvider = (*QQ)(nil)
|
||||
+1
-3
@@ -16,9 +16,7 @@ func GetClients(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
upgrader := websocket.Upgrader{
|
||||
CheckOrigin: func(r *http.Request) bool {
|
||||
return true
|
||||
},
|
||||
CheckOrigin: CheckOrigin,
|
||||
}
|
||||
// Upgrade the HTTP connection to a WebSocket connection
|
||||
conn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
package ws
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func CheckOrigin(r *http.Request) bool {
|
||||
origin := r.Header.Get("Origin")
|
||||
// 显式关闭校验
|
||||
if strings.EqualFold(os.Getenv("KOMARI_WS_DISABLE_ORIGIN"), "true") {
|
||||
return true
|
||||
}
|
||||
if origin == "" {
|
||||
return false
|
||||
}
|
||||
host := r.Host
|
||||
originUrl, err := url.Parse(origin)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return originUrl.Host == host
|
||||
}
|
||||
Reference in new issue
Block a user