mirror of
https://github.com/nezhahq/nezha.git
synced 2025-01-22 12:48:14 -05:00
Compare commits
4 Commits
da1af18878
...
5ab20e8565
Author | SHA1 | Date | |
---|---|---|---|
|
5ab20e8565 | ||
|
7831e2d1f8 | ||
|
9791c81a03 | ||
|
6b72e7394d |
@ -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
|
||||||
|
@ -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()
|
||||||
|
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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"`
|
||||||
|
@ -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"`
|
||||||
|
@ -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"
|
||||||
|
Loading…
Reference in New Issue
Block a user