Compare commits

...

4 Commits

Author SHA1 Message Date
黑歌
5ab20e8565
Merge 6b72e7394d into 7831e2d1f8 2025-01-21 22:23:26 +08:00
naiba
7831e2d1f8 feat: disable nat
Some checks are pending
CodeQL / Analyze (go) (push) Waiting to run
CodeQL / Analyze (javascript) (push) Waiting to run
Contributors / contributors (push) Waiting to run
Sync / sync-to-jihulab (push) Waiting to run
Run Tests / tests (macos) (push) Waiting to run
Run Tests / tests (ubuntu) (push) Waiting to run
Run Tests / tests (windows) (push) Waiting to run
2025-01-21 22:23:15 +08:00
naiba
9791c81a03 feat: upgrade frontend 2025-01-21 21:19:42 +08:00
dysf888
6b72e7394d feat: Telegram Widget Login 2025-01-14 19:33:31 +08:00
6 changed files with 147 additions and 5 deletions

View File

@ -64,6 +64,7 @@ func createNAT(c *gin.Context) (uint64, error) {
uid := getUid(c) uid := getUid(c)
n.UserID = uid n.UserID = uid
n.Enabled = nf.Enabled
n.Name = nf.Name n.Name = nf.Name
n.Domain = nf.Domain n.Domain = nf.Domain
n.Host = nf.Host n.Host = nf.Host
@ -121,6 +122,7 @@ func updateNAT(c *gin.Context) (any, error) {
return nil, singleton.Localizer.ErrorT("permission denied") return nil, singleton.Localizer.ErrorT("permission denied")
} }
n.Enabled = nf.Enabled
n.Name = nf.Name n.Name = nf.Name
n.Domain = nf.Domain n.Domain = nf.Domain
n.Host = nf.Host n.Host = nf.Host

View File

@ -2,9 +2,13 @@ package controller
import ( import (
"context" "context"
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"fmt" "fmt"
"io" "io"
"net/http" "net/http"
"sort"
"strconv" "strconv"
"strings" "strings"
@ -52,7 +56,10 @@ func oauth2redirect(c *gin.Context) (*model.Oauth2LoginResponse, error) {
return nil, singleton.Localizer.ErrorT("provider not found") return nil, singleton.Localizer.ErrorT("provider not found")
} }
o2conf := o2confRaw.Setup(getRedirectURL(c)) o2conf := o2confRaw.Setup(getRedirectURL(c))
if provider == "Telegram" {
// 直接返回配置中的 AuthURL
return &model.Oauth2LoginResponse{Redirect: o2confRaw.Endpoint.AuthURL}, nil
}
randomString, err := utils.GenerateRandomString(32) randomString, err := utils.GenerateRandomString(32)
if err != nil { if err != nil {
return nil, err return nil, err
@ -117,6 +124,69 @@ func unbindOauth2(c *gin.Context) (any, error) {
// @Router /api/v1/oauth2/callback [get] // @Router /api/v1/oauth2/callback [get]
func oauth2callback(jwtConfig *jwt.GinJWTMiddleware) func(c *gin.Context) (any, error) { func oauth2callback(jwtConfig *jwt.GinJWTMiddleware) func(c *gin.Context) (any, error) {
return func(c *gin.Context) (any, error) { return func(c *gin.Context) (any, error) {
// 通过判断请求参数来确定是否是 Telegram 回调
if c.Query("id") != "" && c.Query("auth_date") != "" && c.Query("hash") != "" {
queryParams := make(map[string]string)
for k, v := range c.Request.URL.Query() {
if len(v) > 0 {
queryParams[k] = v[0]
}
}
o2confRaw, has := singleton.Conf.Oauth2["Telegram"]
if !has {
return nil, singleton.Localizer.ErrorT("provider not found")
}
// 验证 Telegram Hash数据
if valid, err := verifyTelegramAuth(queryParams, o2confRaw.ClientID); err != nil {
return nil, err
} else if !valid {
return nil, singleton.Localizer.ErrorT("invalid Telegram auth data")
}
var bind model.Oauth2Bind
provider := "telegram"
openId := queryParams["id"]
u, authorized := c.Get(model.CtxKeyAuthorizedUser)
if authorized {
user := u.(*model.User)
result := singleton.DB.Where("provider = ? AND open_id = ?", provider, openId).Limit(1).Find(&bind)
if result.Error != nil && result.Error != gorm.ErrRecordNotFound {
return nil, newGormError("%v", result.Error)
}
bind.UserID = user.ID
bind.Provider = provider
bind.OpenID = openId
if result.Error == gorm.ErrRecordNotFound {
result = singleton.DB.Create(&bind)
} else {
result = singleton.DB.Save(&bind)
}
if result.Error != nil {
return nil, newGormError("%v", result.Error)
}
c.Redirect(http.StatusFound, "/dashboard/profile?oauth2=true")
} else {
if err := singleton.DB.Where("provider = ? AND open_id = ?", provider, openId).First(&bind).Error; err != nil {
return nil, singleton.Localizer.ErrorT("oauth2 user not binded yet")
}
tokenString, _, err := jwtConfig.TokenGenerator(fmt.Sprintf("%d", bind.UserID))
if err != nil {
return nil, err
}
jwtConfig.SetCookie(c, tokenString)
c.Redirect(http.StatusFound, "/dashboard/login?oauth2=true")
}
return nil, errNoop
}
// 其他 OAuth2 提供商的原有逻辑
callbackData := &model.Oauth2Callback{ callbackData := &model.Oauth2Callback{
State: c.Query("state"), State: c.Query("state"),
Code: c.Query("code"), Code: c.Query("code"),
@ -188,7 +258,68 @@ func oauth2callback(jwtConfig *jwt.GinJWTMiddleware) func(c *gin.Context) (any,
} }
} }
func verifyTelegramAuth(data map[string]string, botToken string) (bool, error) {
// 只保留需要验证的字段
requiredFields := []string{"id", "first_name", "last_name", "username", "photo_url", "auth_date"}
checkData := make(map[string]string)
// 只复制需要的字段
for _, field := range requiredFields {
if value, exists := data[field]; exists {
checkData[field] = value
}
}
var dataCheckString string
keys := make([]string, 0, len(checkData))
for k := range checkData {
if k != "hash" {
keys = append(keys, k)
}
}
sort.Strings(keys)
for _, k := range keys {
if len(dataCheckString) > 0 {
dataCheckString += "\n"
}
dataCheckString += fmt.Sprintf("%s=%s", k, checkData[k])
}
// 先对 bot token 进行 SHA256 哈希作为密钥
secretKeyHash := sha256.Sum256([]byte(botToken))
// 使用哈希后的密钥计算 HMAC
h := hmac.New(sha256.New, secretKeyHash[:])
h.Write([]byte(dataCheckString))
hash := hex.EncodeToString(h.Sum(nil))
return hash == data["hash"], nil
}
func exchangeOpenId(c *gin.Context, o2confRaw *model.Oauth2Config, callbackData *model.Oauth2Callback) (string, error) { func exchangeOpenId(c *gin.Context, o2confRaw *model.Oauth2Config, callbackData *model.Oauth2Callback) (string, error) {
// 处理Telegram Widget OAuth
if strings.ToLower(c.Param("provider")) == "telegram" {
// 解析查询参数
queryParams := make(map[string]string)
for k, v := range c.Request.URL.Query() {
if len(v) > 0 {
queryParams[k] = v[0]
}
}
// 验证数据
if valid, err := verifyTelegramAuth(queryParams, o2confRaw.ClientID); err != nil {
return "", err
} else if !valid {
return "", singleton.Localizer.ErrorT("invalid Telegram auth data")
}
// 返回Telegram用户ID
return queryParams["id"], nil
}
// 原有OAuth2处理逻辑
o2conf := o2confRaw.Setup(getRedirectURL(c)) o2conf := o2confRaw.Setup(getRedirectURL(c))
ctx := context.Background() ctx := context.Background()

View File

@ -13,12 +13,14 @@ import (
"time" "time"
_ "time/tzdata" _ "time/tzdata"
"github.com/gin-gonic/gin"
"github.com/ory/graceful" "github.com/ory/graceful"
"golang.org/x/crypto/bcrypt" "golang.org/x/crypto/bcrypt"
"golang.org/x/net/http2" "golang.org/x/net/http2"
"golang.org/x/net/http2/h2c" "golang.org/x/net/http2/h2c"
"github.com/nezhahq/nezha/cmd/dashboard/controller" "github.com/nezhahq/nezha/cmd/dashboard/controller"
"github.com/nezhahq/nezha/cmd/dashboard/controller/waf"
"github.com/nezhahq/nezha/cmd/dashboard/rpc" "github.com/nezhahq/nezha/cmd/dashboard/rpc"
"github.com/nezhahq/nezha/model" "github.com/nezhahq/nezha/model"
"github.com/nezhahq/nezha/proto" "github.com/nezhahq/nezha/proto"
@ -147,6 +149,11 @@ func newHTTPandGRPCMux(httpHandler http.Handler, grpcHandler http.Handler) http.
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
natConfig := singleton.GetNATConfigByDomain(r.Host) natConfig := singleton.GetNATConfigByDomain(r.Host)
if natConfig != nil { if natConfig != nil {
if !natConfig.Enabled {
c, _ := gin.CreateTestContext(w)
waf.ShowBlockPage(c, fmt.Errorf("nat host %s is disabled", natConfig.Domain))
return
}
rpc.ServeNAT(w, r, natConfig) rpc.ServeNAT(w, r, natConfig)
return return
} }

View File

@ -2,6 +2,7 @@ package model
type NAT struct { type NAT struct {
Common Common
Enabled bool `json:"enabled"`
Name string `json:"name"` Name string `json:"name"`
ServerID uint64 `json:"server_id"` ServerID uint64 `json:"server_id"`
Host string `json:"host"` Host string `json:"host"`

View File

@ -2,6 +2,7 @@ package model
type NATForm struct { type NATForm struct {
Name string `json:"name,omitempty" minLength:"1"` Name string `json:"name,omitempty" minLength:"1"`
Enabled bool `json:"enabled,omitempty"`
ServerID uint64 `json:"server_id,omitempty"` ServerID uint64 `json:"server_id,omitempty"`
Host string `json:"host,omitempty"` Host string `json:"host,omitempty"`
Domain string `json:"domain,omitempty"` Domain string `json:"domain,omitempty"`

View File

@ -2,22 +2,22 @@
name: "OfficialAdmin" name: "OfficialAdmin"
repository: "https://github.com/nezhahq/admin-frontend" repository: "https://github.com/nezhahq/admin-frontend"
author: "nezhahq" author: "nezhahq"
version: "v1.6.0" version: "v1.6.2"
isadmin: true isadmin: true
isofficial: true isofficial: true
- path: "user-dist" - path: "user-dist"
name: "Official" name: "Official"
repository: "https://github.com/hamster1963/nezha-dash-v1" repository: "https://github.com/hamster1963/nezha-dash-v1"
author: "hamster1963" author: "hamster1963"
version: "v1.13.1" version: "v1.13.3"
isofficial: true isofficial: true
- path: "nezha-ascii-dist" - path: "nezha-ascii-dist"
name: "Nezha-ASCII" name: "Nezha-ASCII"
repository: "https://github.com/hamster1963/nezha-ascii" repository: "https://github.com/hamster1963/nezha-ascii"
author: "hamster1963" author: "hamster1963"
version: "v1.0.1" version: "v1.0.2"
- path: "nazhua-dist" - path: "nazhua-dist"
name: "Nazhua" name: "Nazhua"
repository: "https://github.com/hi2shark/nazhua" repository: "https://github.com/hi2shark/nazhua"
author: "hi2hi" author: "hi2hi"
version: "v0.5.1" version: "v0.5.3"