feat: core 增加 xpack 节点管理

This commit is contained in:
ssongliu
2024-07-25 14:43:41 +08:00
parent cabca70ee5
commit fafa042ee9
36 changed files with 411 additions and 116 deletions
+12 -12
View File
@@ -8,9 +8,9 @@ import (
"github.com/1Panel-dev/1Panel/core/app/model"
"github.com/1Panel-dev/1Panel/core/constant"
"github.com/1Panel-dev/1Panel/core/global"
// "github.com/1Panel-dev/1Panel/core/middleware"
"github.com/1Panel-dev/1Panel/core/middleware"
"github.com/1Panel-dev/1Panel/core/utils/captcha"
"github.com/1Panel-dev/1Panel/core/utils/qqwry"
"github.com/gin-gonic/gin"
)
@@ -124,10 +124,10 @@ func (b *BaseApi) CheckIsSafety(c *gin.Context) {
return
}
if status == "unpass" {
// if middleware.Get.LoadErrCode("err-entrance") != 200 {
// helper.ErrResponse(c, middleware.LoadErrCode("err-entrance"))
// return
// }
if middleware.LoadErrCode("err-entrance") != 200 {
helper.ErrResponse(c, middleware.LoadErrCode("err-entrance"))
return
}
helper.ErrorWithDetail(c, constant.CodeErrEntrance, constant.ErrTypeInternalServer, nil)
return
}
@@ -175,12 +175,12 @@ func saveLoginLogs(c *gin.Context, err error) {
logs.Status = constant.StatusSuccess
}
logs.IP = c.ClientIP()
// qqWry, err := qqwry.NewQQwry()
// if err != nil {
// global.LOG.Errorf("load qqwry datas failed: %s", err)
// }
// res := qqWry.Find(logs.IP)
qqWry, err := qqwry.NewQQwry()
if err != nil {
global.LOG.Errorf("load qqwry datas failed: %s", err)
}
res := qqWry.Find(logs.IP)
logs.Agent = c.GetHeader("User-Agent")
// logs.Address = res.Area
logs.Address = res.Area
_ = logService.CreateLoginLog(logs)
}
@@ -1,5 +1,15 @@
package dto
type SearchWithPage struct {
PageInfo
Info string `json:"info"`
}
type PageInfo struct {
Page int `json:"page" validate:"required,number"`
PageSize int `json:"pageSize" validate:"required,number"`
}
type PageResult struct {
Total int64 `json:"total"`
Items interface{} `json:"items"`
-11
View File
@@ -1,11 +0,0 @@
package dto
type SearchWithPage struct {
PageInfo
Info string `json:"info"`
}
type PageInfo struct {
Page int `json:"page" validate:"required,number"`
PageSize int `json:"pageSize" validate:"required,number"`
}
+8 -1
View File
@@ -7,14 +7,21 @@ import (
type DBOption func(*gorm.DB) *gorm.DB
type ICommonRepo interface {
WithByID(id uint) DBOption
WithOrderBy(orderStr string) DBOption
}
type CommonRepo struct{}
func NewCommonRepo() ICommonRepo {
func NewICommonRepo() ICommonRepo {
return &CommonRepo{}
}
func (c *CommonRepo) WithByID(id uint) DBOption {
return func(g *gorm.DB) *gorm.DB {
return g.Where("id = ?", id)
}
}
func (c *CommonRepo) WithOrderBy(orderStr string) DBOption {
return func(g *gorm.DB) *gorm.DB {
return g.Order(orderStr)
+1 -1
View File
@@ -3,7 +3,7 @@ package service
import "github.com/1Panel-dev/1Panel/core/app/repo"
var (
commonRepo = repo.NewCommonRepo()
commonRepo = repo.NewICommonRepo()
settingRepo = repo.NewISettingRepo()
logRepo = repo.NewILogRepo()
)
+5 -3
View File
@@ -31,9 +31,11 @@ var (
ErrInitialPassword = errors.New("ErrInitialPassword")
ErrInvalidParams = errors.New("ErrInvalidParams")
ErrTokenParse = errors.New("ErrTokenParse")
ErrPortInUsed = "ErrPortInUsed"
ErrCmdTimeout = "ErrCmdTimeout"
ErrTokenParse = errors.New("ErrTokenParse")
ErrStructTransform = errors.New("ErrStructTransform")
ErrPortInUsed = "ErrPortInUsed"
ErrCmdTimeout = "ErrCmdTimeout"
ErrGroupIsUsed = "ErrGroupIsUsed"
)
// api
+1
View File
@@ -13,6 +13,7 @@ ErrNotSupportType: "The system does not support the current type: {{ .detail }}"
ErrNameIsExist: "Name is already exist"
ErrDemoEnvironment: "Demo server, prohibit this operation!"
ErrEntrance: "Security entrance information error. Please check and try again!"
ErrGroupIsUsed: "The group is in use and cannot be deleted"
#app
ErrPortInUsed: "{{ .detail }} port already in use"
+1
View File
@@ -13,6 +13,7 @@ ErrNotSupportType: "系統暫不支持當前類型: {{ .detail }}"
ErrNameIsExist: "名稱已存在"
ErrDemoEnvironment: "演示伺服器,禁止此操作!"
ErrEntrance: "安全入口信息錯誤,請檢查後重試!"
ErrGroupIsUsed: "分組正在使用中,無法刪除"
#app
ErrPortInUsed: "{{ .detail }} 端口已被佔用!"
+1
View File
@@ -13,6 +13,7 @@ ErrNotSupportType: "系统暂不支持当前类型: {{ .detail }}"
ErrDemoEnvironment: "演示服务器,禁止此操作!"
ErrCmdTimeout: "命令执行超时!"
ErrEntrance: "安全入口信息错误,请检查后重试!"
ErrGroupIsUsed: "分组正在使用中,无法删除"
#app
ErrPortInUsed: "{{ .detail }} 端口已被占用!"
+1 -1
View File
@@ -82,7 +82,7 @@ var InitSetting = &gormigrate.Migration{
if err := tx.Create(&model.Setting{Key: "PrsoxyPasswdKeep", Value: ""}).Error; err != nil {
return err
}
if err := tx.Create(&model.Setting{Key: "XpackHideMenu", Value: "{\"id\":\"1\",\"label\":\"/xpack\",\"isCheck\":true,\"title\":\"xpack.menu\",\"children\":[{\"id\":\"2\",\"title\":\"xpack.waf.name\",\"path\":\"/xpack/waf/dashboard\",\"label\":\"Dashboard\",\"isCheck\":true},{\"id\":\"3\",\"title\":\"xpack.tamper.tamper\",\"path\":\"/xpack/tamper\",\"label\":\"Tamper\",\"isCheck\":true},{\"id\":\"4\",\"title\":\"xpack.gpu.gpu\",\"path\":\"/xpack/gpu\",\"label\":\"GPU\",\"isCheck\":true},{\"id\":\"5\",\"title\":\"xpack.setting.setting\",\"path\":\"/xpack/setting\",\"label\":\"XSetting\",\"isCheck\":true},{\"id\":\"6\",\"title\":\"xpack.monitor.name\",\"path\":\"/xpack/monitor/dashboard\",\"label\":\"MonitorDashboard\",\"isCheck\":true},{\"id\":\"7\",\"title\":\"xpack.multihost.agentManagement\",\"path\":\"/xpack/multihost/manage\",\"label\":\"Multihost\",\"isCheck\":true}]}"}).Error; err != nil {
if err := tx.Create(&model.Setting{Key: "XpackHideMenu", Value: "{\"id\":\"1\",\"label\":\"/xpack\",\"isCheck\":true,\"title\":\"xpack.menu\",\"children\":[{\"id\":\"2\",\"title\":\"xpack.waf.name\",\"path\":\"/xpack/waf/dashboard\",\"label\":\"Dashboard\",\"isCheck\":true},{\"id\":\"3\",\"title\":\"xpack.tamper.tamper\",\"path\":\"/xpack/tamper\",\"label\":\"Tamper\",\"isCheck\":true},{\"id\":\"4\",\"title\":\"xpack.gpu.gpu\",\"path\":\"/xpack/gpu\",\"label\":\"GPU\",\"isCheck\":true},{\"id\":\"5\",\"title\":\"xpack.setting.setting\",\"path\":\"/xpack/setting\",\"label\":\"XSetting\",\"isCheck\":true},{\"id\":\"6\",\"title\":\"xpack.monitor.name\",\"path\":\"/xpack/monitor/dashboard\",\"label\":\"MonitorDashboard\",\"isCheck\":true},{\"id\":\"7\",\"title\":\"xpack.node.nodeManagement\",\"path\":\"/xpack/node\",\"label\":\"Node\",\"isCheck\":true}]}"}).Error; err != nil {
return err
}
+3 -36
View File
@@ -1,8 +1,9 @@
package router
import (
"context"
"fmt"
"net/http"
"github.com/1Panel-dev/1Panel/cmd/server/docs"
"github.com/1Panel-dev/1Panel/cmd/server/web"
"github.com/1Panel-dev/1Panel/core/global"
@@ -13,11 +14,6 @@ import (
"github.com/gin-gonic/gin"
swaggerfiles "github.com/swaggo/files"
ginSwagger "github.com/swaggo/gin-swagger"
"net"
"net/http"
"net/http/httputil"
"os"
"strings"
)
var (
@@ -62,7 +58,7 @@ func Routers() *gin.Engine {
PublicGroup.Use(gzip.Gzip(gzip.DefaultCompression))
setWebStatic(PublicGroup)
}
PrivateGroup := Router.Group("/api/v1")
PrivateGroup := Router.Group("/api/v2/core")
PrivateGroup.Use(middleware.WhiteAllow())
PrivateGroup.Use(middleware.BindDomain())
PrivateGroup.Use(middleware.GlobalLoading())
@@ -70,35 +66,6 @@ func Routers() *gin.Engine {
router.InitRouter(PrivateGroup)
}
// 使用 unix 代理
sockPath := "/tmp/agent.sock"
if _, err := os.Stat(sockPath); err != nil {
panic(err)
}
dialUnix := func(proto, addr string) (conn net.Conn, err error) {
return net.Dial("unix", sockPath)
}
transport := &http.Transport{
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
return dialUnix(network, addr)
},
}
proxy := &httputil.ReverseProxy{
Director: func(req *http.Request) {
req.URL.Scheme = "http"
req.URL.Host = "unix"
},
Transport: transport,
}
Router.Use(func(c *gin.Context) {
if strings.HasPrefix(c.Request.URL.Path, "/api") {
proxy.ServeHTTP(c.Writer, c.Request)
c.Abort()
return
}
c.Next()
})
Router.NoRoute(func(c *gin.Context) {
c.Writer.WriteHeader(http.StatusOK)
_, _ = c.Writer.Write(web.IndexByte)
+28 -12
View File
@@ -1,6 +1,11 @@
package middleware
import (
"context"
"net"
"net/http"
"net/http/httputil"
"os"
"strings"
"github.com/gin-gonic/gin"
@@ -8,20 +13,31 @@ import (
func Proxy() gin.HandlerFunc {
return func(c *gin.Context) {
if strings.HasPrefix(c.Request.URL.Path, "/api/v1/auth") ||
strings.HasPrefix(c.Request.URL.Path, "/api/v1/setting") ||
strings.HasPrefix(c.Request.URL.Path, "/api/v1/log") {
if strings.HasPrefix(c.Request.URL.Path, "/api/v2/core") {
c.Next()
return
} else {
sockPath := "/tmp/agent.sock"
if _, err := os.Stat(sockPath); err != nil {
panic(err)
}
dialUnix := func() (conn net.Conn, err error) {
return net.Dial("unix", sockPath)
}
transport := &http.Transport{
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
return dialUnix()
},
}
proxy := &httputil.ReverseProxy{
Director: func(req *http.Request) {
req.URL.Scheme = "http"
req.URL.Host = "unix"
},
Transport: transport,
}
proxy.ServeHTTP(c.Writer, c.Request)
c.Abort()
}
//target, err := url.Parse("http://127.0.0.1:9998")
//if err != nil {
// fmt.Printf("Failed to parse target URL: %v", err)
//}
//proxy := httputil.NewSingleHostReverseProxy(target)
//c.Request.Host = target.Host
//c.Request.URL.Scheme = target.Scheme
//c.Request.URL.Host = target.Host
//proxy.ServeHTTP(c.Writer, c.Request)
}
}
+2
View File
@@ -1,3 +1,5 @@
//go:build !xpack
package router
func RouterGroups() []CommonRouter {
+1
View File
@@ -36,6 +36,7 @@ func Start() {
cache.Init()
session.Init()
gin.SetMode("debug")
InitOthers()
hook.Init()
rootRouter := router.Routers()
+165
View File
@@ -0,0 +1,165 @@
package qqwry
import (
"encoding/binary"
"net"
"strings"
"github.com/1Panel-dev/1Panel/cmd/server/qqwry"
"golang.org/x/text/encoding/simplifiedchinese"
)
const (
indexLen = 7
redirectMode1 = 0x01
redirectMode2 = 0x02
)
var IpCommonDictionary []byte
type QQwry struct {
Data []byte
Offset int64
}
func NewQQwry() (*QQwry, error) {
IpCommonDictionary := qqwry.QQwryByte
return &QQwry{Data: IpCommonDictionary}, nil
}
// readData 从文件中读取数据
func (q *QQwry) readData(num int, offset ...int64) (rs []byte) {
if len(offset) > 0 {
q.setOffset(offset[0])
}
nums := int64(num)
end := q.Offset + nums
dataNum := int64(len(q.Data))
if q.Offset > dataNum {
return nil
}
if end > dataNum {
end = dataNum
}
rs = q.Data[q.Offset:end]
q.Offset = end
return
}
// setOffset 设置偏移量
func (q *QQwry) setOffset(offset int64) {
q.Offset = offset
}
// Find ip地址查询对应归属地信息
func (q *QQwry) Find(ip string) (res ResultQQwry) {
res = ResultQQwry{}
res.IP = ip
if strings.Count(ip, ".") != 3 {
return res
}
offset := q.searchIndex(binary.BigEndian.Uint32(net.ParseIP(ip).To4()))
if offset <= 0 {
return
}
var area []byte
mode := q.readMode(offset + 4)
if mode == redirectMode1 {
countryOffset := q.readUInt24()
mode = q.readMode(countryOffset)
if mode == redirectMode2 {
c := q.readUInt24()
area = q.readString(c)
} else {
area = q.readString(countryOffset)
}
} else if mode == redirectMode2 {
countryOffset := q.readUInt24()
area = q.readString(countryOffset)
} else {
area = q.readString(offset + 4)
}
enc := simplifiedchinese.GBK.NewDecoder()
res.Area, _ = enc.String(string(area))
return
}
type ResultQQwry struct {
IP string `json:"ip"`
Area string `json:"area"`
}
// readMode 获取偏移值类型
func (q *QQwry) readMode(offset uint32) byte {
mode := q.readData(1, int64(offset))
return mode[0]
}
// readString 获取字符串
func (q *QQwry) readString(offset uint32) []byte {
q.setOffset(int64(offset))
data := make([]byte, 0, 30)
for {
buf := q.readData(1)
if buf[0] == 0 {
break
}
data = append(data, buf[0])
}
return data
}
// searchIndex 查找索引位置
func (q *QQwry) searchIndex(ip uint32) uint32 {
header := q.readData(8, 0)
start := binary.LittleEndian.Uint32(header[:4])
end := binary.LittleEndian.Uint32(header[4:])
for {
mid := q.getMiddleOffset(start, end)
buf := q.readData(indexLen, int64(mid))
_ip := binary.LittleEndian.Uint32(buf[:4])
if end-start == indexLen {
offset := byteToUInt32(buf[4:])
buf = q.readData(indexLen)
if ip < binary.LittleEndian.Uint32(buf[:4]) {
return offset
}
return 0
}
if _ip > ip {
end = mid
} else if _ip < ip {
start = mid
} else if _ip == ip {
return byteToUInt32(buf[4:])
}
}
}
// readUInt24
func (q *QQwry) readUInt24() uint32 {
buf := q.readData(3)
return byteToUInt32(buf)
}
// getMiddleOffset
func (q *QQwry) getMiddleOffset(start uint32, end uint32) uint32 {
records := ((end - start) / indexLen) >> 1
return start + records*indexLen
}
// byteToUInt32 将 byte 转换为uint32
func byteToUInt32(data []byte) uint32 {
i := uint32(data[0]) & 0xff
i |= (uint32(data[1]) << 8) & 0xff00
i |= (uint32(data[2]) << 16) & 0xff0000
return i
}
+142
View File
@@ -0,0 +1,142 @@
package ssh
import (
"bytes"
"fmt"
"io"
"strings"
"sync"
"time"
gossh "golang.org/x/crypto/ssh"
)
type ConnInfo struct {
User string `json:"user"`
Addr string `json:"addr"`
Port int `json:"port"`
AuthMode string `json:"authMode"`
Password string `json:"password"`
PrivateKey []byte `json:"privateKey"`
PassPhrase []byte `json:"passPhrase"`
DialTimeOut time.Duration `json:"dialTimeOut"`
Client *gossh.Client `json:"client"`
Session *gossh.Session `json:"session"`
LastResult string `json:"lastResult"`
}
func (c *ConnInfo) NewClient() (*ConnInfo, error) {
if strings.Contains(c.Addr, ":") {
c.Addr = fmt.Sprintf("[%s]", c.Addr)
}
config := &gossh.ClientConfig{}
config.SetDefaults()
addr := fmt.Sprintf("%s:%d", c.Addr, c.Port)
config.User = c.User
if c.AuthMode == "password" {
config.Auth = []gossh.AuthMethod{gossh.Password(c.Password)}
} else {
signer, err := makePrivateKeySigner(c.PrivateKey, c.PassPhrase)
if err != nil {
return nil, err
}
config.Auth = []gossh.AuthMethod{gossh.PublicKeys(signer)}
}
if c.DialTimeOut == 0 {
c.DialTimeOut = 5 * time.Second
}
config.Timeout = c.DialTimeOut
config.HostKeyCallback = gossh.InsecureIgnoreHostKey()
proto := "tcp"
if strings.Contains(c.Addr, ":") {
proto = "tcp6"
}
client, err := gossh.Dial(proto, addr, config)
if nil != err {
return c, err
}
c.Client = client
return c, nil
}
func (c *ConnInfo) Run(shell string) (string, error) {
if c.Client == nil {
if _, err := c.NewClient(); err != nil {
return "", err
}
}
session, err := c.Client.NewSession()
if err != nil {
return "", err
}
defer session.Close()
buf, err := session.CombinedOutput(shell)
c.LastResult = string(buf)
return c.LastResult, err
}
func (c *ConnInfo) Close() {
_ = c.Client.Close()
}
type SshConn struct {
StdinPipe io.WriteCloser
ComboOutput *wsBufferWriter
Session *gossh.Session
}
func (c *ConnInfo) NewSshConn(cols, rows int) (*SshConn, error) {
sshSession, err := c.Client.NewSession()
if err != nil {
return nil, err
}
stdinP, err := sshSession.StdinPipe()
if err != nil {
return nil, err
}
comboWriter := new(wsBufferWriter)
sshSession.Stdout = comboWriter
sshSession.Stderr = comboWriter
modes := gossh.TerminalModes{
gossh.ECHO: 1,
gossh.TTY_OP_ISPEED: 14400,
gossh.TTY_OP_OSPEED: 14400,
}
if err := sshSession.RequestPty("xterm", rows, cols, modes); err != nil {
return nil, err
}
if err := sshSession.Shell(); err != nil {
return nil, err
}
return &SshConn{StdinPipe: stdinP, ComboOutput: comboWriter, Session: sshSession}, nil
}
func (s *SshConn) Close() {
if s.Session != nil {
s.Session.Close()
}
}
type wsBufferWriter struct {
buffer bytes.Buffer
mu sync.Mutex
}
func (w *wsBufferWriter) Write(p []byte) (int, error) {
w.mu.Lock()
defer w.mu.Unlock()
return w.buffer.Write(p)
}
func makePrivateKeySigner(privateKey []byte, passPhrase []byte) (gossh.Signer, error) {
if len(passPhrase) != 0 {
return gossh.ParsePrivateKeyWithPassphrase(privateKey, passPhrase)
}
return gossh.ParsePrivateKey(privateKey)
}