api server

This commit is contained in:
ttk
2024-02-01 21:03:43 +08:00
parent c40a736840
commit b5a505a37a
46 changed files with 5736 additions and 2 deletions

View File

@@ -4,7 +4,7 @@
<a href=""><img src="https://img.shields.io/badge/Go-%3E%3D%201.18-%23007d9c" alt="go>=1.18"></a>
<a href="https://goreportcard.com/report/github.com/veops/oneterm"><img src="https://goreportcard.com/badge/github.com/veops/oneterm" alt="API"></a>
</p>
oneterm: sample, lightweight, safe jump service
oneterm simple, lightweight, safe jump service
---

View File

@@ -4,7 +4,7 @@
<a href=""><img src="https://img.shields.io/badge/Go-%3E%3D%201.18-%23007d9c" alt="go>=1.18"></a>
<a href="https://goreportcard.com/report/github.com/veops/oneterm"><img src="https://goreportcard.com/badge/github.com/veops/oneterm" alt="API"></a>
</p>
oneterm: 简单、轻量、安全的跳板机服务
oneterm 简单、轻量、安全的跳板机服务
---

View File

@@ -0,0 +1,109 @@
package acl
import (
"fmt"
"github.com/gin-gonic/gin"
)
const (
WRITE = "write"
DELETE = "delete"
READ = "read"
GRANT = "grant"
)
var (
AllPermissions = []string{WRITE, DELETE, READ, GRANT}
)
type Role struct {
Permissions []string `json:"permissions"`
}
type ResourceResult struct {
Groups []any `json:"groups"`
Resources []*Resource `json:"resources"`
}
type Resource struct {
AppID int `json:"app_id"`
CreatedAt string `json:"created_at"`
Deleted bool `json:"deleted"`
DeletedAt string `json:"-"`
ResourceId int `json:"id"`
Name string `json:"name"`
Permissions []string `json:"permissions"`
ResourceTypeID int `json:"resource_type_id"`
UID int `json:"uid"`
UpdatedAt string `json:"updated_at"`
}
type Perm struct {
Name string `json:"name"`
Rid int `json:"rid"`
}
type ResourcePermissionsRespItem struct {
Perms []*Perm `json:"perms"`
}
type Acl struct {
Uid int `json:"uid"`
UserName string `json:"userName"`
Rid int `json:"rid"`
RoleName string `json:"roleName"`
ParentRoles []string `json:"parentRoles"`
ChildRoles []string `json:"childRoles"`
NickName string `json:"nickName"`
}
type Session struct {
Uid int `json:"uid"`
Acl Acl `json:"acl"`
}
func (s *Session) GetUid() int {
return s.Uid
}
func (s *Session) GetRid() int {
return s.Acl.Rid
}
func (s *Session) GetUserName() string {
return s.Acl.UserName
}
func (a *Acl) GetUserName(ctx *gin.Context) string {
res, exist := ctx.Get("session")
if exist {
if v, ok := res.(*Session); ok {
return v.GetUserName()
}
}
return ""
}
func (a *Acl) GetUserInfo(ctx *gin.Context) (any, error) {
res, exist := ctx.Get("session")
if exist {
if v, ok := res.(*Session); ok {
return v, nil
}
}
return res, fmt.Errorf("no session")
}
type UserInfoResp struct {
Result UserInfoRespResult `json:"result"`
}
type UserInfoRespResult struct {
Avatar string `json:"avatar"`
Email string `json:"email"`
Name string `json:"name"`
Rid int `json:"rid"`
Role Role `json:"role"`
UID int `json:"uid"`
Username string `json:"username"`
}

View File

@@ -0,0 +1,138 @@
package acl
import (
"bytes"
"compress/zlib"
"crypto/hmac"
"crypto/sha1"
"crypto/subtle"
"encoding/base64"
"errors"
"fmt"
"hash"
"io"
"strings"
)
// SigningAlgorithm provides interfaces to generate and verify signature
type SigningAlgorithm interface {
GetSignature(key, value string) []byte
VerifySignature(key, value string, sig []byte) bool
}
// HMACAlgorithm provides signature generation using HMACs.
type HMACAlgorithm struct {
DigestMethod func() hash.Hash
}
// GetSignature returns the signature for the given key and value.
func (a *HMACAlgorithm) GetSignature(key, value string) []byte {
//a.DigestMethod().Reset()
h := hmac.New(a.DigestMethod, []byte(key))
h.Write([]byte(value))
return h.Sum(nil)
}
// VerifySignature verifies the given signature matches the expected signature.
func (a *HMACAlgorithm) VerifySignature(key, value string, sig []byte) bool {
eq := subtle.ConstantTimeCompare(sig, []byte(a.GetSignature(key, value)))
return eq == 1
}
type Signature struct {
SecretKey string
Sep string
Salt string
KeyDerivation string
DigestMethod func() hash.Hash
Algorithm SigningAlgorithm
}
// Unsign the given string.
func (s *Signature) Unsign(signed string) (content []byte, err error) {
if !strings.Contains(signed, s.Sep) {
err = fmt.Errorf("no %s found in value", s.Sep)
return
}
li := strings.LastIndex(signed, s.Sep)
value, sig := signed[:li], signed[li+len(s.Sep):]
if ok, _ := s.Verify(value, sig); ok {
//c, err := base64Decode(strings.Split(strings.Trim(value, "."), ".")[0])
var c []byte
c, err = base64.RawURLEncoding.DecodeString(strings.Split(strings.Trim(value, "."), ".")[0])
if err != nil {
return
}
var r io.ReadCloser
r, err = zlib.NewReader(bytes.NewReader(c))
if err != nil {
return
}
return io.ReadAll(r)
}
err = fmt.Errorf("signature %s does not match", sig)
return
}
func (s *Signature) Verify(value, sig string) (bool, error) {
key, err := s.DeriveKey()
if err != nil {
return false, err
}
signed, err := base64.RawURLEncoding.DecodeString(sig)
if err != nil {
return false, err
}
return s.Algorithm.VerifySignature(key, value, signed), nil
}
func (s *Signature) DeriveKey() (string, error) {
var key string
var err error
switch s.KeyDerivation {
case "hmac":
h := hmac.New(sha1.New, []byte(s.SecretKey))
h.Write([]byte(s.Salt))
key = string(h.Sum(nil))
case "none":
key = s.SecretKey
default:
key, err = "", errors.New("unknown key derivation method")
}
return key, err
}
func NewSignature(secret, salt, sep, derivation string, digest func() hash.Hash, algo SigningAlgorithm) *Signature {
if salt == "" {
salt = "itsdangerous.Signer"
}
if sep == "" {
sep = "."
}
if derivation == "" {
derivation = "hmac"
}
if digest == nil {
digest = sha1.New
}
if algo == nil {
algo = &HMACAlgorithm{DigestMethod: digest}
}
return &Signature{
SecretKey: secret,
Salt: salt,
Sep: sep,
KeyDerivation: derivation,
DigestMethod: digest,
Algorithm: algo,
}
}

View File

@@ -0,0 +1,89 @@
package acl
import (
"bytes"
"compress/zlib"
"context"
"encoding/base64"
"encoding/json"
"fmt"
"github.com/veops/oneterm/pkg/conf"
"github.com/veops/oneterm/pkg/server/remote"
)
func LoginByPassword(ctx context.Context, username string, password string) (cookie string, err error) {
url := fmt.Sprintf("%s/acl/login", conf.Cfg.Auth.Acl.Url)
data := make(map[string]any)
resp, err := remote.RC.R().
SetHeaders(map[string]string{
"User-Agent": "oneterm",
}).
SetQueryParams(map[string]string{
"channel": "ssh",
}).
SetResult(&data).
SetBody(map[string]any{
"username": username,
// "password": fmt.Sprintf("%x", md5.Sum([]byte(password))),
"password": password,
}).
Post(url)
if err = remote.HandleErr(err, resp, func(dt map[string]any) bool { return true }); err != nil {
return
}
cookie = resp.Header().Get("Set-Cookie")
return
}
func LoginByPublicKey(ctx context.Context, username string) (cookie string, err error) {
token, err := remote.GetAclToken(ctx)
if err != nil {
return
}
url := fmt.Sprintf("%s/acl/users/info", conf.Cfg.Auth.Acl.Url)
data := &UserInfoResp{}
resp, err := remote.RC.R().
SetHeaders(map[string]string{
"App-Access-Token": token,
"User-Agent": "oneterm",
}).
SetQueryParams(map[string]string{
"channel": "ssh",
}).
SetQueryParam("username", username).
SetResult(&data).
Get(url)
if err = remote.HandleErr(err, resp, func(dt map[string]any) bool { return true }); err != nil {
return
}
sess := &Session{
Uid: data.Result.UID,
Acl: Acl{
Uid: data.Result.UID,
UserName: data.Result.Username,
Rid: data.Result.Rid,
NickName: data.Result.Name,
ParentRoles: data.Result.Role.Permissions,
},
}
bs, _ := json.Marshal(sess)
s := NewSignature(conf.Cfg.SecretKey, "cookie-session", "", "hmac", nil, nil)
buf := &bytes.Buffer{}
zw := zlib.NewWriter(buf)
_, _ = zw.Write(bs)
_ = zw.Close()
value := "." + base64.RawURLEncoding.EncodeToString(buf.Bytes())
dk, _ := s.DeriveKey()
sign := s.Algorithm.GetSignature(dk, value)
vs := value + "." + base64.RawURLEncoding.EncodeToString(sign)
cookie = "session=" + vs
return
}

View File

@@ -0,0 +1,84 @@
// Package acl
package acl
import (
"context"
"fmt"
"github.com/gin-gonic/gin"
"github.com/samber/lo"
"github.com/veops/oneterm/pkg/conf"
)
func GetSessionFromCtx(ctx *gin.Context) (res *Session, err error) {
res, ok := ctx.Value("session").(*Session)
if !ok || res == nil {
err = fmt.Errorf("empty session")
}
return
}
func HasPerm(resourceId int, rid int, action string) bool {
mapping, err := GetResourcePermissions(context.Background(), resourceId)
if err != nil {
return false
}
for _, v := range mapping {
if lo.ContainsBy(v.Perms, func(p *Perm) bool { return p.Rid == rid && p.Name == action }) {
return true
}
}
return false
}
func IsAdmin(session *Session) bool {
for _, pr := range session.Acl.ParentRoles {
if pr == "admin" || pr == "acl_admin" || pr == "oneterm_admin" {
return true
}
}
return false
}
func GetResourceTypeName(resourceType string) string {
names := conf.Cfg.Auth.Acl.ResourceNames
for _, v := range names {
if v.Key == resourceType {
return v.Value
}
}
return "NONE"
}
func CreateGrantAcl(ctx context.Context, session *Session, resourceType string, resourceName string) (resourceId int, err error) {
resource, err := AddResource(ctx,
session.GetUid(),
GetResourceTypeName(resourceType),
resourceName)
if err != nil {
return
}
if err = GrantRoleResource(ctx, session.GetUid(), session.Acl.Rid, resource.ResourceId, AllPermissions); err != nil {
return
}
resourceId = resource.ResourceId
return
}
func CreateAcl(ctx context.Context, session *Session, resourceType string, resourceName string) (resourceId int, err error) {
resource, err := AddResource(ctx,
session.GetUid(),
GetResourceTypeName(resourceType),
resourceName)
if err != nil {
return
}
resourceId = resource.ResourceId
return
}

View File

@@ -0,0 +1,82 @@
package acl
import (
"context"
"fmt"
"github.com/spf13/cast"
"github.com/veops/oneterm/pkg/conf"
"github.com/veops/oneterm/pkg/server/remote"
)
func AddResource(ctx context.Context, uid int, resourceTypeId string, name string) (res *Resource, err error) {
token, err := remote.GetAclToken(ctx)
if err != nil {
return
}
res = &Resource{}
url := fmt.Sprintf("%s/acl/resources", conf.Cfg.Auth.Acl.Url)
resp, err := remote.RC.R().
SetHeaders(map[string]string{
"App-Access-Token": token,
"X-User-Id": cast.ToString(uid)}).
SetBody(map[string]any{
"type_id": resourceTypeId,
"name": name,
"uid": uid,
}).
SetResult(&res).
Post(url)
err = remote.HandleErr(err, resp, func(dt map[string]any) bool { return true })
return
}
func DeleteResource(ctx context.Context, uid int, resourceId int) (err error) {
token, err := remote.GetAclToken(ctx)
if err != nil {
return
}
url := fmt.Sprintf("%v/acl/resources/%v", conf.Cfg.Auth.Acl.Url, resourceId)
resp, err := remote.RC.R().
SetHeaders(map[string]string{
"App-Access-Token": token,
"X-User-Id": cast.ToString(uid)}).
Delete(url)
err = remote.HandleErr(err, resp, func(dt map[string]any) bool { return true })
return
}
func UpdateResource(ctx context.Context, uid int, resourceId int, updates map[string]string) (err error) {
token, err := remote.GetAclToken(ctx)
if err != nil {
return
}
url := fmt.Sprintf("%s/acl/resources/%d", conf.Cfg.Auth.Acl.Url, resourceId)
resp, err := remote.RC.R().
SetHeaders(map[string]string{
"App-Access-Token": token,
"X-User-Id": cast.ToString(uid)}).
SetFormData(updates).
Put(url)
err = remote.HandleErr(err, resp, func(dt map[string]any) bool { return true })
return
}
func GetResourcePermissions(ctx context.Context, resourceId int) (res map[string]*ResourcePermissionsRespItem, err error) {
token, err := remote.GetAclToken(ctx)
if err != nil {
return
}
res = make(map[string]*ResourcePermissionsRespItem)
url := fmt.Sprintf("%v/acl/resources/%v/permissions", conf.Cfg.Auth.Acl.Url, resourceId) //TODO conf
resp, err := remote.RC.R().
SetHeader("App-Access-Token", token).
SetResult(&res).
Get(url)
err = remote.HandleErr(err, resp, func(dt map[string]any) bool { return true })
return
}

View File

@@ -0,0 +1,130 @@
package acl
import (
"context"
"fmt"
"github.com/spf13/cast"
"golang.org/x/sync/errgroup"
"github.com/veops/oneterm/pkg/conf"
"github.com/veops/oneterm/pkg/server/remote"
)
func GetRoleResources(ctx context.Context, rid int, resourceTypeId string) (res []*Resource, err error) {
token, err := remote.GetAclToken(ctx)
if err != nil {
return
}
data := &ResourceResult{}
url := fmt.Sprintf("%v/acl/roles/%v/resources", conf.Cfg.Auth.Acl.Url, rid)
resp, err := remote.RC.R().
SetHeader("App-Access-Token", token).
SetQueryParams(map[string]string{
"app_id": conf.Cfg.Auth.Acl.AppId,
"resource_type_id": resourceTypeId,
}).
SetResult(data).
Get(url)
if err = remote.HandleErr(err, resp, func(dt map[string]any) bool { return true }); err != nil {
return
}
res = data.Resources
return
}
func HasPermission(ctx context.Context, rid int, resourceName, resourceTypeName, permission string) (res bool, err error) {
token, err := remote.GetAclToken(ctx)
if err != nil {
return false, err
}
data := make(map[string]any)
url := fmt.Sprintf("%s/acl/roles/has_perm", conf.Cfg.Auth.Acl.Url)
resp, err := remote.RC.R().
SetHeader("App-Access-Token", token).
SetQueryParams(map[string]string{
"rid": cast.ToString(rid),
"resource_name": resourceName,
"resource_type_name": resourceTypeName,
"perm": permission,
}).
SetResult(&data).
Get(url)
if err = remote.HandleErr(err, resp, func(dt map[string]any) bool { return true }); err != nil {
return
}
if v, ok := data["result"]; ok {
res = v.(bool)
}
return
}
func GrantRoleResource(ctx context.Context, uid int, roleId int, resourceId int, permissions []string) (err error) {
token, err := remote.GetAclToken(ctx)
if err != nil {
return
}
url := fmt.Sprintf("%s/acl/roles/%d/resources/%d/grant", conf.Cfg.Auth.Acl.Url, roleId, resourceId)
resp, err := remote.RC.R().
SetHeaders(map[string]string{
"App-Access-Token": token,
"X-User-Id": cast.ToString(uid)}).
SetBody(map[string]any{
"perms": permissions,
}).
Post(url)
err = remote.HandleErr(err, resp, func(dt map[string]any) bool { return true })
return
}
func RevokeRoleResource(ctx context.Context, uid int, roleId int, resourceId int, permissions []string) (err error) {
token, err := remote.GetAclToken(ctx)
if err != nil {
return
}
url := fmt.Sprintf("%s/acl/roles/%d/resources/%d/revoke", conf.Cfg.Auth.Acl.Url, roleId, resourceId)
resp, err := remote.RC.R().
SetHeaders(map[string]string{
"App-Access-Token": token,
"X-User-Id": cast.ToString(uid)}).
SetBody(map[string]any{
"perms": permissions,
}).
Post(url)
err = remote.HandleErr(err, resp, func(dt map[string]any) bool { return true })
return
}
func BatchGrantRoleResource(ctx context.Context, uid int, roleIds []int, resourceId int, permissions []string) (err error) {
eg := &errgroup.Group{}
for _, rid := range roleIds {
localRid := rid
eg.Go(func() error {
return GrantRoleResource(ctx, uid, localRid, resourceId, permissions)
})
}
err = eg.Wait()
return
}
func BatchRevokeRoleResource(ctx context.Context, uid int, roleIds []int, resourceId int, permissions []string) (err error) {
eg := &errgroup.Group{}
for _, rid := range roleIds {
localRid := rid
eg.Go(func() error {
return RevokeRoleResource(ctx, uid, localRid, resourceId, permissions)
})
}
err = eg.Wait()
return
}

View File

@@ -0,0 +1,173 @@
package cmdb
import (
"context"
"crypto/sha1"
"encoding/hex"
"errors"
"fmt"
"net/url"
"sort"
"strings"
"time"
"github.com/samber/lo"
"github.com/spf13/cast"
"go.uber.org/zap"
"gorm.io/gorm"
"github.com/veops/oneterm/pkg/conf"
"github.com/veops/oneterm/pkg/logger"
"github.com/veops/oneterm/pkg/server/auth/acl"
"github.com/veops/oneterm/pkg/server/controller"
"github.com/veops/oneterm/pkg/server/model"
"github.com/veops/oneterm/pkg/server/remote"
"github.com/veops/oneterm/pkg/server/storage/db/mysql"
)
var (
ctx, cancel = context.WithCancel(context.Background())
)
func Run() (err error) {
currentUser := &acl.Session{
Uid: conf.Cfg.Worker.Uid,
Acl: acl.Acl{
Rid: conf.Cfg.Worker.Rid,
},
}
tk := time.NewTicker(time.Minute)
last := make(map[int]time.Time)
for {
select {
case <-tk.C:
nodes, err := getNodes()
if err != nil {
logger.L.Error("get nodes faild", zap.Error(err))
continue
}
now := time.Now()
for _, n := range nodes {
d, _ := time.ParseDuration(fmt.Sprintf("%fh", n.Sync.Frequency))
if last[n.Id].Add(d).After(now) {
continue
}
if n.Sync.TypeId <= 0 {
continue
}
cis, err := getCis(n.Sync.TypeId, n.Sync.Filters)
if err != nil {
logger.L.Error("get cmdb failed", zap.Error(err))
continue
}
for _, ci := range cis {
a := &model.Asset{
Ciid: cast.ToInt(ci["_id"]),
ParentId: n.Id,
UpdaterId: conf.Cfg.Worker.Uid,
Ip: cast.ToString(ci[n.Sync.Mapping["ip"]]),
Name: fmt.Sprintf("%s@%v", cast.ToString(ci[n.Sync.Mapping["name"]]), time.Now().Format(time.RFC3339)),
Protocols: n.Protocols,
Authorization: n.Authorization,
AccessAuth: n.AccessAuth,
}
resourceIds := make([]int, 0)
if err := mysql.DB.Model(a).Select("resource_id").Where("ci_id = ?", a.Ciid).Find(&resourceIds).Error; err != nil {
logger.L.Error("insert ci failed", zap.Error(err))
continue
}
if !errors.Is(mysql.DB.Model(a).Where("ci_id = ?", a.Ciid).Where("parent_id = ?", a.ParentId).First(map[string]any{}).Error, gorm.ErrRecordNotFound) {
continue
}
a.CreatorId = conf.Cfg.Worker.Uid
a.ResourceId, err = acl.CreateAcl(ctx, currentUser, acl.GetResourceTypeName(conf.RESOURCE_ASSET), a.Name)
if err != nil {
continue
}
if err = mysql.DB.Transaction(func(tx *gorm.DB) (err error) {
if err = tx.Create(a).Error; err != nil {
return
}
err = controller.HandleAuthorization(currentUser, tx, model.ACTION_CREATE, nil, a)
return
}); err != nil {
continue
}
}
}
case <-ctx.Done():
return
}
}
}
func Stop(err error) {
defer cancel()
}
func getNodes() (res map[int]*model.Node, err error) {
data := make([]*model.Node, 0)
err = mysql.DB.
Model(&model.Node{}).
Where("enable = ?", 1).
Find(&data).
Error
if err != nil {
return
}
res = lo.SliceToMap(data, func(d *model.Node) (int, *model.Node) { return d.Id, d })
return
}
func getCis(typeId int, filters string) (res []map[string]any, err error) {
url := fmt.Sprintf("%s/ci/s", conf.Cfg.Cmdb.Url)
params := map[string]any{
"q": fmt.Sprintf("_type:(%d),%s", typeId, filters),
}
params["_secret"] = buildAPIKey(url, params)
params["_key"] = conf.Cfg.Worker.Key
ps := make(map[string]string)
for k, v := range params {
ps[k] = cast.ToString(v)
}
data := &GetCIResult{}
resp, err := remote.RC.R().
SetQueryParams(ps).
SetResult(data).
Get(url)
if err = remote.HandleErr(err, resp, func(dt map[string]any) bool { return true }); err != nil {
return
}
res = data.Result
return
}
type GetCIResult struct {
Counter map[string]int `json:"counter"`
Facet map[string]any `json:"facet"`
Numfound int `json:"numfound"`
Page int `json:"page"`
Result []map[string]any `json:"result"`
Total int `json:"total"`
}
func buildAPIKey(u string, params map[string]any) string {
pu, _ := url.Parse(u)
keys := lo.Keys(params)
sort.Slice(keys, func(i, j int) bool { return keys[i] < keys[j] })
vals := strings.Join(
lo.Map(keys, func(k string, _ int) string {
return lo.Ternary(strings.HasPrefix(k, "_"), "", cast.ToString(params[k]))
}),
"")
sha := sha1.New()
sha.Write([]byte(strings.Join([]string{pu.Path, conf.Cfg.Worker.Secret, vals}, "")))
return hex.EncodeToString(sha.Sum(nil))
}

View File

@@ -0,0 +1,171 @@
package controller
import (
"errors"
"net/http"
"strings"
"github.com/gin-gonic/gin"
"github.com/samber/lo"
"github.com/spf13/cast"
"golang.org/x/crypto/ssh"
"gorm.io/gorm"
"github.com/veops/oneterm/pkg/conf"
"github.com/veops/oneterm/pkg/server/auth/acl"
"github.com/veops/oneterm/pkg/server/model"
"github.com/veops/oneterm/pkg/server/storage/db/mysql"
"github.com/veops/oneterm/pkg/util"
)
var (
accountPreHooks = []preHook[*model.Account]{
func(ctx *gin.Context, data *model.Account) {
if data.AccountType == model.AUTHMETHOD_PUBLICKEY {
if data.Phrase == "" {
_, err := ssh.ParsePrivateKey([]byte(data.Pk))
if err != nil {
ctx.AbortWithError(http.StatusBadRequest, &ApiError{Code: ErrWrongPk, Data: nil})
return
}
} else {
_, err := ssh.ParsePrivateKeyWithPassphrase([]byte(data.Pk), []byte(data.Phrase))
if err != nil {
ctx.AbortWithError(http.StatusBadRequest, &ApiError{Code: ErrWrongPk, Data: nil})
return
}
}
}
},
func(ctx *gin.Context, data *model.Account) {
data.Password = util.EncryptAES(data.Password)
data.Pk = util.EncryptAES(data.Pk)
data.Phrase = util.EncryptAES(data.Phrase)
},
}
accountPostHooks = []postHook[*model.Account]{
func(ctx *gin.Context, data []*model.Account) {
acs := make([]*model.AccountCount, 0)
if err := mysql.DB.
Model(&model.Authorization{}).
Select("account_id AS id, COUNT(*) as count").
Group("account_id").
Where("account_id IN ?", lo.Map(data, func(d *model.Account, _ int) int { return d.Id })).
Find(&acs).
Error; err != nil {
return
}
m := lo.SliceToMap(acs, func(ac *model.AccountCount) (int, int64) { return ac.Id, ac.Count })
for _, d := range data {
d.AssetCount = m[d.Id]
}
},
func(ctx *gin.Context, data []*model.Account) {
for _, d := range data {
d.Password = lo.Ternary(!cast.ToBool(ctx.Query("info")) || ctx.GetHeader("X-Token") == conf.Cfg.SshServer.Xtoken, util.DecryptAES(d.Password), "")
d.Pk = lo.Ternary(!cast.ToBool(ctx.Query("info")) || ctx.GetHeader("X-Token") == conf.Cfg.SshServer.Xtoken, util.DecryptAES(d.Pk), "")
d.Phrase = lo.Ternary(!cast.ToBool(ctx.Query("info")) || ctx.GetHeader("X-Token") == conf.Cfg.SshServer.Xtoken, util.DecryptAES(d.Phrase), "")
}
},
}
accountDcs = []deleteCheck{
func(ctx *gin.Context, id int) {
assetName := ""
err := mysql.DB.
Model(&model.Asset{}).
Select("name").
Where("id = (?)", mysql.DB.Model(&model.Authorization{}).Select("asset_id").Where("account_id = ?", id).Limit(1)).
First(&assetName).
Error
if errors.Is(err, gorm.ErrRecordNotFound) {
return
}
code := lo.Ternary(err == nil, http.StatusBadRequest, http.StatusInternalServerError)
err = lo.Ternary[error](err == nil, &ApiError{Code: ErrHasDepency, Data: map[string]any{"name": assetName}}, err)
ctx.AbortWithError(code, err)
},
}
)
// CreateAccount godoc
//
// @Tags account
// @Param account body model.Account true "account"
// @Success 200 {object} HttpResponse
// @Router /account [post]
func (c *Controller) CreateAccount(ctx *gin.Context) {
doCreate(ctx, true, &model.Account{}, conf.RESOURCE_ACCOUNT, accountPreHooks...)
}
// DeleteAccount godoc
//
// @Tags account
// @Param id path int true "account id"
// @Success 200 {object} HttpResponse
// @Router /account/:id [delete]
func (c *Controller) DeleteAccount(ctx *gin.Context) {
doDelete(ctx, true, &model.Account{}, accountDcs...)
}
// UpdateAccount godoc
//
// @Tags account
// @Param id path int true "account id"
// @Param account body model.Account true "account"
// @Success 200 {object} HttpResponse
// @Router /account/:id [put]
func (c *Controller) UpdateAccount(ctx *gin.Context) {
doUpdate(ctx, true, &model.Account{}, accountPreHooks...)
}
// GetAccounts godoc
//
// @Tags account
// @Param page_index query int true "page_index"
// @Param page_size query int true "page_size"
// @Param search query string false "name or account"
// @Param id query int false "account id"
// @Param ids query string false "account ids"
// @Param name query string false "account name"
// @Param info query bool false "is info mode"
// @Param type query int false "account type"
// @Success 200 {object} HttpResponse{data=ListData{list=[]model.Account}}
// @Router /account [get]
func (c *Controller) GetAccounts(ctx *gin.Context) {
currentUser, _ := acl.GetSessionFromCtx(ctx)
info := cast.ToBool(ctx.Query("info"))
db := mysql.DB.Model(&model.Account{})
db = filterEqual(ctx, db, "id","type")
db = filterLike(ctx, db, "name")
db = filterSearch(ctx, db, "name", "account")
if q, ok := ctx.GetQuery("ids"); ok {
db = db.Where("id IN ?", lo.Map(strings.Split(q, ","), func(s string, _ int) int { return cast.ToInt(s) }))
}
if info && !acl.IsAdmin(currentUser) {
//rs := make([]*acl.Resource, 0)
rs, err := acl.GetRoleResources(ctx, currentUser.Acl.Rid, acl.GetResourceTypeName(conf.RESOURCE_AUTHORIZATION))
if err != nil {
handleRemoteErr(ctx, err)
return
}
ids := make([]int, 0)
if err = mysql.DB.
Model(&model.Authorization{}).
Where("resource_id IN ?", lo.Map(rs, func(r *acl.Resource, _ int) int { return r.ResourceId })).
Distinct().
Pluck("account_id", &ids).
Error; err != nil {
ctx.AbortWithError(http.StatusInternalServerError, &ApiError{Code: ErrInternal, Data: map[string]any{"err": err}})
return
}
db = db.Where("id IN ?", ids)
}
db = db.Order("name")
doGet[*model.Account](ctx, !info, db, acl.GetResourceTypeName(conf.RESOURCE_ACCOUNT), accountPostHooks...)
}

View File

@@ -0,0 +1,218 @@
package controller
import (
"net/http"
"strings"
"github.com/gin-gonic/gin"
"github.com/samber/lo"
"github.com/spf13/cast"
"go.uber.org/zap"
"github.com/veops/oneterm/pkg/conf"
"github.com/veops/oneterm/pkg/logger"
"github.com/veops/oneterm/pkg/server/auth/acl"
"github.com/veops/oneterm/pkg/server/model"
"github.com/veops/oneterm/pkg/server/storage/db/mysql"
)
var (
assetPostHooks = []postHook[*model.Asset]{
func(ctx *gin.Context, data []*model.Asset) {
post := make([]*model.AssetNodeChain, 0)
if err := mysql.DB.
Model(&model.Node{}).
Raw(`
WITH RECURSIVE cte AS(
SELECT id, name AS chain
FROM node
WHERE
deleted_at = 0
AND parent_id = 0
UNION ALL
SELECT
t.id,
CONCAT(cte.chain, '/', t.name)
FROM cte
INNER JOIN node t on cte.id = t.parent_id
WHERE deleted_at = 0
)
SELECT *
FROM cte
`).
Find(&post).
Error; err != nil {
logger.L.Error("asset posthookfailed", zap.Error(err))
return
}
m := lo.SliceToMap(post, func(p *model.AssetNodeChain) (int, string) { return p.NodeId, p.Chain })
for _, d := range data {
d.NodeChain = m[d.ParentId]
}
}, func(ctx *gin.Context, data []*model.Asset) {
currentUser, _ := acl.GetSessionFromCtx(ctx)
if acl.IsAdmin(currentUser) {
return
}
for _, a := range data {
for k, v := range a.Authorization {
if lo.Contains(v, currentUser.GetRid()) {
continue
}
delete(a.Authorization, k)
}
}
},
}
)
// CreateAsset godoc
//
// @Tags asset
// @Param asset body model.Asset true "asset"
// @Success 200 {object} HttpResponse
// @Router /asset [post]
func (c *Controller) CreateAsset(ctx *gin.Context) {
doCreate(ctx, true, &model.Asset{}, conf.RESOURCE_ASSET)
}
// DeleteAsset godoc
//
// @Tags asset
// @Param id path int true "asset id"
// @Success 200 {object} HttpResponse
// @Router /asset/:id [delete]
func (c *Controller) DeleteAsset(ctx *gin.Context) {
doDelete(ctx, true, &model.Asset{})
}
// UpdateAsset godoc
//
// @Tags asset
// @Param id path int true "asset id"
// @Param asset body model.Asset true "asset"
// @Success 200 {object} HttpResponse
// @Router /asset/:id [put]
func (c *Controller) UpdateAsset(ctx *gin.Context) {
doUpdate(ctx, true, &model.Asset{})
}
// GetAssets godoc
//
// @Tags asset
// @Param page_index query int true "page_index"
// @Param page_size query int true "page_size"
// @Param search query string false "name or ip"
// @Param id query int false "asset id"
// @Param ids query string false "asset ids"
// @Param parent_id query int false "asset's parent id"
// @Param name query string false "asset name"
// @Param ip query string false "asset ip"
// @Param info query bool false "is info mode"
// @Success 200 {object} HttpResponse{data=ListData{list=[]model.Asset}}
// @Router /asset [get]
func (c *Controller) GetAssets(ctx *gin.Context) {
currentUser, _ := acl.GetSessionFromCtx(ctx)
info := cast.ToBool(ctx.Query("info"))
db := mysql.DB.Model(&model.Asset{})
db = filterEqual(ctx, db, "id")
db = filterLike(ctx, db, "name", "ip")
db = filterSearch(ctx, db, "name", "ip")
if q, ok := ctx.GetQuery("ids"); ok {
db = db.Where("id IN ?", lo.Map(strings.Split(q, ","), func(s string, _ int) int { return cast.ToInt(s) }))
}
if q, ok := ctx.GetQuery("parent_id"); ok {
parentIds := make([]int, 0)
if err := mysql.DB.
Model(&model.Node{}).
Raw(`
WITH RECURSIVE cte AS(
SELECT id
FROM node
WHERE deleted_at = 0
AND parent_id = ?
UNION ALL
SELECT t.id
FROM cte
INNER JOIN node t on cte.id = t.parent_id
WHERE deleted_at = 0
)
SELECT id
FROM cte;
`, q).
Find(&parentIds).
Error; err != nil {
logger.L.Error("parent id found failed", zap.Error(err))
return
}
parentIds = append(parentIds, cast.ToInt(q))
db = db.Where("parent_id IN ?", parentIds)
}
if info && !acl.IsAdmin(currentUser) {
//rs := make([]*acl.Resource, 0)
rs, err := acl.GetRoleResources(ctx, currentUser.Acl.Rid, acl.GetResourceTypeName(conf.RESOURCE_AUTHORIZATION))
if err != nil {
handleRemoteErr(ctx, err)
return
}
ids := make([]int, 0)
if err = mysql.DB.
Model(&model.Authorization{}).
Where("resource_id IN ?", lo.Map(rs, func(r *acl.Resource, _ int) int { return r.ResourceId })).
Distinct().
Pluck("asset_id", &ids).
Error; err != nil {
ctx.AbortWithError(http.StatusInternalServerError, &ApiError{Code: ErrInternal, Data: map[string]any{"err": err}})
return
}
db = db.Where("id IN ?", ids)
}
db = db.Order("name")
doGet[*model.Asset](ctx, !info, db, acl.GetResourceTypeName(conf.RESOURCE_AUTHORIZATION), assetPostHooks...)
}
// QueryByServer godoc
//
// @Tags asset
// @Param page_index query int true "page index"
// @Param page_size query int true "page size"
// @Success 200 {object} HttpResponse{data=ListData{list=[]model.Asset}}
// @Router /asset/query_by_server [get]
func (c *Controller) QueryByServer(ctx *gin.Context) {
db := mysql.DB.Model(&model.Asset{})
doGet[*model.Asset](ctx, false, db, acl.GetResourceTypeName(conf.RESOURCE_ASSET), nil)
}
// UpdateByServer godoc
//
// @Tags asset
// @Param id path int true "asset id"
// @Param req body map[int]map[string]any true "asset update request"
// @Success 200 {object} HttpResponse
// @Router /asset/update_by_server [put]
func (c *Controller) UpdateByServer(ctx *gin.Context) {
updates := make(map[int]map[string]any)
if err := ctx.BindJSON(&updates); err != nil {
ctx.AbortWithError(http.StatusBadRequest, &ApiError{Code: ErrInvalidArgument, Data: map[string]any{"err": err}})
return
}
for k, v := range updates {
if err := mysql.DB.
Model(&model.Asset{}).
Where("id = ?", k).
Updates(v).
Error; err != nil {
ctx.AbortWithError(http.StatusInternalServerError, &ApiError{Code: ErrInternal, Data: map[string]any{"err": err}})
return
}
}
ctx.JSON(http.StatusOK, defaultHttpResponse)
}

View File

@@ -0,0 +1,153 @@
package controller
import (
"context"
"errors"
"fmt"
"reflect"
"sort"
"sync"
"github.com/gin-gonic/gin"
"github.com/samber/lo"
"golang.org/x/sync/errgroup"
"gorm.io/gorm"
"github.com/veops/oneterm/pkg/conf"
"github.com/veops/oneterm/pkg/server/auth/acl"
"github.com/veops/oneterm/pkg/server/model"
"github.com/veops/oneterm/pkg/server/storage/db/mysql"
)
func HandleAuthorization(currentUser *acl.Session, tx *gorm.DB, action int, old, new *model.Asset) (err error) {
ctx := context.Background()
assetId := lo.TernaryF(new == nil, func() int { return old.Id }, func() int { return new.Id })
mtx := &sync.Mutex{}
eg := &errgroup.Group{}
if action == model.ACTION_UPDATE {
if sameAuthorization(old.Authorization, new.Authorization) {
return
}
for id := range old.Authorization {
if _, ok := new.Authorization[id]; ok {
continue
}
accountId := id
eg.Go(func() (err error) {
a := &model.Authorization{}
if err = mysql.DB.
Model(a).
Select("id", "resource_id").
Where("asset_id = ? AND account_id = ?", assetId, accountId).
First(a).
Error; err != nil {
return
}
if err = acl.DeleteResource(ctx, currentUser.GetUid(), a.ResourceId); err != nil {
return
}
mtx.Lock()
defer mtx.Unlock()
err = tx.Delete(a, a.Id).Error
return
})
}
}
as := lo.TernaryF(action == model.ACTION_DELETE,
func() model.Map[int, model.Slice[int]] { return old.Authorization },
func() model.Map[int, model.Slice[int]] { return new.Authorization })
for k, v := range as {
accountId := k
curRids := lo.Uniq(v)
eg.Go(func() (err error) {
resourceId := 0
if err = mysql.DB.
Model(&model.Authorization{}).
Select("resource_id").
Where("asset_id = ? AND account_id = ?", assetId, accountId).
First(&resourceId).
Error; err != nil {
notFount := errors.Is(err, gorm.ErrRecordNotFound)
if !notFount || (notFount && action == model.ACTION_DELETE) {
return
}
if resourceId, err = acl.CreateGrantAcl(ctx, currentUser, conf.GetResourceTypeName(conf.RESOURCE_AUTHORIZATION),
fmt.Sprintf("%d-%d", assetId, accountId)); err != nil {
return
}
mtx.Lock()
if err = tx.Create(&model.Authorization{AssetId: assetId, AccountId: accountId, ResourceId: resourceId,
CreatorId: currentUser.GetUid(), UpdaterId: currentUser.GetUid()}).Error; err != nil {
return
}
mtx.Unlock()
}
switch action {
case model.ACTION_CREATE:
err = acl.BatchGrantRoleResource(ctx, currentUser.GetUid(), curRids, resourceId, []string{acl.READ})
case model.ACTION_DELETE:
err = acl.DeleteResource(ctx, currentUser.GetUid(), resourceId)
case model.ACTION_UPDATE:
var res map[string]*acl.ResourcePermissionsRespItem
res, err = acl.GetResourcePermissions(ctx, resourceId)
if err != nil {
return
}
perms := make([]*acl.Perm, 0)
for _, v := range res {
perms = append(perms, v.Perms...)
}
preRids := lo.Map(lo.Filter(perms, func(p *acl.Perm, _ int) bool { return p.Name == acl.READ }), func(p *acl.Perm, _ int) int { return p.Rid })
revokeRids := lo.Without(preRids, curRids...)
if len(revokeRids) > 0 {
if err = acl.BatchRevokeRoleResource(ctx, currentUser.GetUid(), revokeRids, resourceId, []string{acl.READ}); err != nil {
return
}
}
grantRids := lo.Without(curRids, preRids...)
if len(grantRids) > 0 {
err = acl.BatchGrantRoleResource(ctx, currentUser.GetUid(), grantRids, resourceId, []string{acl.READ})
}
return
}
return
})
}
err = eg.Wait()
return
}
func sameAuthorization(old, new model.Map[int, model.Slice[int]]) bool {
if len(old) != len(new) {
return false
}
ks := lo.Uniq(append(lo.Keys(old), lo.Keys(new)...))
for _, k := range ks {
if len(old[k]) != len(new[k]) {
return false
}
o, n := make([]int, 0, len(old[k])), make([]int, 0, len(new[k]))
copy(o, old[k])
copy(n, new[k])
sort.Ints(o)
sort.Ints(n)
if !reflect.DeepEqual(o, n) {
return false
}
}
return true
}
func GetAutorizationResourceIds(ctx *gin.Context) (resourceIds []int, err error) {
currentUser, _ := acl.GetSessionFromCtx(ctx)
var rs []*acl.Resource
rs, err = acl.GetRoleResources(ctx, currentUser.Acl.Rid, conf.RESOURCE_AUTHORIZATION)
if err != nil {
return
}
resourceIds = lo.Map(rs, func(r *acl.Resource, _ int) int { return r.ResourceId })
return
}

View File

@@ -0,0 +1,129 @@
package controller
import (
"errors"
"fmt"
"net/http"
"strings"
"github.com/gin-gonic/gin"
"github.com/samber/lo"
"github.com/spf13/cast"
"gorm.io/gorm"
"github.com/veops/oneterm/pkg/conf"
"github.com/veops/oneterm/pkg/server/auth/acl"
"github.com/veops/oneterm/pkg/server/model"
"github.com/veops/oneterm/pkg/server/storage/db/mysql"
)
var (
commandDcs = []deleteCheck{
func(ctx *gin.Context, id int) {
assetName := ""
err := mysql.DB.
Model(&model.Asset{}).
Select("name").
Where(fmt.Sprintf("JSON_CONTAINS(cmd_ids, '%d')", id)).
First(&assetName).
Error
if errors.Is(err, gorm.ErrRecordNotFound) {
return
}
code := lo.Ternary(err == nil, http.StatusBadRequest, http.StatusInternalServerError)
err = lo.Ternary[error](err == nil, &ApiError{Code: ErrHasDepency, Data: map[string]any{"name": assetName}}, err)
ctx.AbortWithError(code, err)
},
}
)
// CreateCommand godoc
//
// @Tags command
// @Param command body model.Command true "command"
// @Success 200 {object} HttpResponse
// @Router /command [post]
func (c *Controller) CreateCommand(ctx *gin.Context) {
doCreate(ctx, true, &model.Command{}, conf.RESOURCE_COMMAND)
}
// DeleteCommand godoc
//
// @Tags command
// @Param id path int true "command id"
// @Success 200 {object} HttpResponse
// @Router /command/:id [delete]
func (c *Controller) DeleteCommand(ctx *gin.Context) {
doDelete(ctx, true, &model.Command{}, commandDcs...)
}
// UpdateCommand godoc
//
// @Tags command
// @Param id path int true "command id"
// @Param command body model.Command true "command"
// @Success 200 {object} HttpResponse
// @Router /command/:id [put]
func (c *Controller) UpdateCommand(ctx *gin.Context) {
doUpdate(ctx, true, &model.Command{})
}
// GetCommands godoc
//
// @Tags command
// @Param page_index query int true "command id"
// @Param page_size query int true "command id"
// @Param search query string false "name or cmds"
// @Param id query int false "command id"
// @Param ids query string false "command ids"
// @Param name query string false "command name"
// @Param enable query int false "command enable"
// @Param info query bool false "is info mode"
// @Param search query string false "name or cmds"
// @Success 200 {object} HttpResponse{data=ListData{list=[]model.Command}}
// @Router /command [get]
func (c *Controller) GetCommands(ctx *gin.Context) {
currentUser, _ := acl.GetSessionFromCtx(ctx)
info := cast.ToBool(ctx.Query("info"))
db := mysql.DB.Model(&model.Command{})
db = filterEqual(ctx, db, "id", "enable")
db = filterLike(ctx, db, "name")
db = filterSearch(ctx, db, "name", "cmds")
if q, ok := ctx.GetQuery("ids"); ok {
db = db.Where("id IN ?", lo.Map(strings.Split(q, ","), func(s string, _ int) int { return cast.ToInt(s) }))
}
if info && !acl.IsAdmin(currentUser) {
//rs := make([]*acl.Resource, 0)
rs, err := acl.GetRoleResources(ctx, currentUser.Acl.Rid, acl.GetResourceTypeName(conf.RESOURCE_AUTHORIZATION))
if err != nil {
handleRemoteErr(ctx, err)
return
}
sub := mysql.DB.
Model(&model.Authorization{}).
Select("DISTINCT asset_id").
Where("resource_id IN ?", lo.Map(rs, func(r *acl.Resource, _ int) int { return r.ResourceId }))
cmdIds := make([]model.Slice[int], 0)
if err = mysql.DB.
Model(&model.Asset{}).
Select("cmd_ids").
Where("id IN (?)", sub).
Find(&cmdIds).
Error; err != nil {
ctx.AbortWithError(http.StatusInternalServerError, &ApiError{Code: ErrInternal, Data: map[string]any{"err": err}})
}
ids := make([]int, 0)
for _, s := range cmdIds {
ids = append(ids, s...)
}
db = db.Where("id IN ?", lo.Uniq(ids))
}
db = db.Order("name")
doGet[*model.Command](ctx, !info, db, acl.GetResourceTypeName(conf.RESOURCE_COMMAND))
}

View File

@@ -0,0 +1,73 @@
package controller
import (
"errors"
"net/http"
"github.com/gin-gonic/gin"
"github.com/spf13/cast"
"gorm.io/gorm"
"github.com/veops/oneterm/pkg/server/auth/acl"
"github.com/veops/oneterm/pkg/server/model"
"github.com/veops/oneterm/pkg/server/storage/db/mysql"
)
// PostConfig godoc
//
// @Tags config
// @Param command body model.Config true "config"
// @Success 200 {object} HttpResponse{}
// @Router /config [post]
func (c *Controller) PostConfig(ctx *gin.Context) {
currentUser, _ := acl.GetSessionFromCtx(ctx)
if !acl.IsAdmin(currentUser) {
ctx.AbortWithError(http.StatusForbidden, &ApiError{Code: ErrNoPerm, Data: map[string]any{"perm": acl.WRITE}})
return
}
cfg := &model.Config{}
if err := ctx.BindJSON(cfg); err != nil {
ctx.AbortWithError(http.StatusBadRequest, &ApiError{Code: ErrInvalidArgument, Data: map[string]any{"err": err}})
return
}
cfg.Id = 0
cfg.CreatorId = currentUser.GetUid()
cfg.UpdaterId = currentUser.GetUid()
if err := mysql.DB.Model(cfg).Transaction(func(tx *gorm.DB) error {
if err := tx.Where("deleted_at = 0").Delete(&model.Config{}).Error; err != nil {
return err
}
return tx.Create(cfg).Error
}); err != nil {
ctx.AbortWithError(http.StatusInternalServerError, &ApiError{Code: ErrInternal, Data: map[string]any{"err": err}})
return
}
ctx.JSON(http.StatusOK, defaultHttpResponse)
}
// GetConfig godoc
//
// @Tags config
// @Param info query bool false "is info mode"
// @Success 200 {object} HttpResponse{data=model.Config}
// @Router /config [get]
func (c *Controller) GetConfig(ctx *gin.Context) {
currentUser, _ := acl.GetSessionFromCtx(ctx)
if !cast.ToBool(ctx.Query("info")) && !acl.IsAdmin(currentUser) {
ctx.AbortWithError(http.StatusForbidden, &ApiError{Code: ErrNoPerm, Data: map[string]any{"perm": acl.READ}})
return
}
cfg := &model.Config{}
if err := mysql.DB.Model(cfg).First(&cfg).Error; err != nil {
if !errors.Is(err, gorm.ErrRecordNotFound) {
ctx.AbortWithError(http.StatusInternalServerError, &ApiError{Code: ErrInternal, Data: map[string]any{"err": err}})
return
}
}
ctx.JSON(http.StatusOK, NewHttpResponseWithData(cfg))
}

View File

@@ -0,0 +1,573 @@
package controller
import (
"bufio"
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"strings"
"sync"
"time"
"unicode/utf8"
"github.com/gin-gonic/gin"
"github.com/gorilla/websocket"
"github.com/nicksnyder/go-i18n/v2/i18n"
"github.com/spf13/cast"
"go.uber.org/zap"
"golang.org/x/crypto/ssh"
"gorm.io/gorm"
"github.com/veops/oneterm/pkg/conf"
myi18n "github.com/veops/oneterm/pkg/i18n"
"github.com/veops/oneterm/pkg/logger"
"github.com/veops/oneterm/pkg/server/auth/acl"
"github.com/veops/oneterm/pkg/server/model"
"github.com/veops/oneterm/pkg/server/storage/db/mysql"
)
var (
Upgrader = websocket.Upgrader{
ReadBufferSize: 4096,
WriteBufferSize: 4096,
CheckOrigin: func(r *http.Request) bool {
return true
},
}
)
// Connect godoc
//
// @Tags connect
// @Success 200 {object} HttpResponse
// @Param session_id path int true "session id"
// @Router /connect/:session_id [get]
func (c *Controller) Connecting(ctx *gin.Context) {
sessionId := ctx.Param("session_id")
ws, err := Upgrader.Upgrade(ctx.Writer, ctx.Request, nil)
if err != nil {
ctx.AbortWithError(http.StatusInternalServerError, err)
return
}
defer ws.Close()
defer func() {
if err == nil {
return
}
logger.L.Debug("connecting failed", zap.String("session_id", sessionId), zap.Error(err))
ae, ok := err.(*ApiError)
if !ok {
return
}
lang := ctx.PostForm("lang")
accept := ctx.GetHeader("Accept-Language")
localizer := i18n.NewLocalizer(conf.Bundle, lang, accept)
ws.WriteMessage(websocket.TextMessage, []byte(ae.Message(localizer)))
}()
v, ok := onlineSession.Load(sessionId)
if !ok {
err = &ApiError{Code: ErrInvalidSessionId, Data: map[string]any{"sessionId": sessionId}}
return
}
session, ok := v.(*model.Session)
if !ok {
err = &ApiError{Code: ErrLoadSession, Data: map[string]any{"err": "invalid type"}}
return
}
if session.Connected.Load() {
err = &ApiError{Code: ErrInvalidSessionId, Data: map[string]any{"sessionId": sessionId}}
return
}
session.Connected.CompareAndSwap(false, true)
chs := session.Chans
chs.WindowChan <- fmt.Sprintf("%s,%s", ctx.Query("w"), ctx.Query("h"))
defer func() {
close(chs.AwayChan)
}()
readWsErrChan := make(chan error)
go func() {
readWsErrChan <- readWsMsg(ctx, ws, chs)
}()
tk, tk1s := time.NewTicker(time.Millisecond*100), time.NewTicker(time.Second)
defer sendMsg(ws, session, chs)
for {
select {
case <-ctx.Done():
return
case err := <-readWsErrChan:
logger.L.Error("websocket read failed", zap.Error(err))
return
case closeBy := <-chs.CloseChan:
out := []byte("\r\n \033[31m closed by admin")
ws.WriteMessage(websocket.TextMessage, out)
writeToMonitors(session.Monitors, out)
logger.L.Warn("close by admin", zap.String("username", closeBy))
return
case err := <-chs.ErrChan:
logger.L.Error("ssh connection failed", zap.Error(err))
return
case in := <-chs.InChan:
rt := in[0]
msg := in[1:]
switch rt {
case '1':
chs.Win.Write(msg)
case '9':
continue
case 'w':
chs.WindowChan <- string(msg)
}
case out := <-chs.OutChan:
chs.Buf.Write(out)
case <-tk.C:
sendMsg(ws, session, chs)
case <-tk1s.C:
ws.WriteMessage(websocket.TextMessage, nil)
writeToMonitors(session.Monitors, nil)
}
}
}
func sendMsg(ws *websocket.Conn, session *model.Session, chs *model.SessionChans) {
out := chs.Buf.Bytes()
if len(out) <= 0 {
return
}
if ws != nil {
ws.WriteMessage(websocket.TextMessage, out)
}
writeToMonitors(session.Monitors, out)
chs.Buf.Reset()
}
// Connect godoc
//
// @Tags connect
// @Success 200 {object} HttpResponse
// @Param w query int false "width"
// @Param h query int false "height"
// @Success 200 {object} HttpResponse{data=model.Session}
// @Router /connect/:asset_id/:account_id/:protocol [post]
func (c *Controller) Connect(ctx *gin.Context) {
chs := makeChans()
resp := &model.SshResp{}
go doSsh(ctx, cast.ToInt(ctx.Query("w")), cast.ToInt(ctx.Query("h")), newSshReq(ctx, model.SESSIONACTION_NEW), chs)
if err := <-chs.ErrChan; err != nil {
logger.L.Error("failed to connect", zap.Error(err))
ctx.AbortWithError(http.StatusInternalServerError, &ApiError{Code: ErrConnectServer, Data: map[string]any{"err": err}})
return
}
resp = <-chs.RespChan
if resp.Code != 0 {
logger.L.Error("failed to connect", zap.Any("resp", *resp))
ctx.AbortWithError(http.StatusInternalServerError, &ApiError{Code: ErrConnectServer, Data: map[string]any{"err": resp.Message}})
return
}
v, ok := onlineSession.Load(resp.SessionId)
if !ok {
ctx.AbortWithError(http.StatusInternalServerError, &ApiError{Code: ErrLoadSession, Data: map[string]any{"err": "cannot find in sync map"}})
return
}
session, ok := v.(*model.Session)
if !ok {
ctx.AbortWithError(http.StatusInternalServerError, &ApiError{Code: ErrLoadSession, Data: map[string]any{"err": "invalid type"}})
return
}
session.Chans = chs
ctx.JSON(http.StatusOK, NewHttpResponseWithData(session))
}
func readWsMsg(ctx context.Context, ws *websocket.Conn, chs *model.SessionChans) error {
for {
select {
case <-ctx.Done():
return fmt.Errorf("ctx done")
default:
t, msg, err := ws.ReadMessage()
if err != nil {
return err
}
if len(msg) <= 0 {
logger.L.Warn("websocket msg length is zero")
continue
}
switch t {
case websocket.TextMessage:
chs.InChan <- msg
}
}
}
}
func doSsh(ctx *gin.Context, w, h int, req *model.SshReq, chs *model.SessionChans) {
var err error
defer func() {
chs.ErrChan <- err
}()
cfg := &ssh.ClientConfig{
User: conf.Cfg.SshServer.Account,
Auth: []ssh.AuthMethod{
ssh.Password(conf.Cfg.SshServer.Password),
},
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
}
conn, err := ssh.Dial("tcp", fmt.Sprintf("%s:%d", conf.Cfg.SshServer.Ip, conf.Cfg.SshServer.Port), cfg)
if err != nil {
logger.L.Error("ssh tcp dail failed", zap.Error(err))
return
}
defer conn.Close()
sess, err := conn.NewSession()
if err != nil {
logger.L.Error("ssh session create failed", zap.Error(err))
return
}
defer sess.Close()
rout, wout := io.Pipe()
sess.Stdout = wout
sess.Stderr = wout
sess.Stdin = chs.Rin
modes := ssh.TerminalModes{
ssh.ECHO: 0,
ssh.TTY_OP_ISPEED: 14400,
ssh.TTY_OP_OSPEED: 14400,
}
if err = sess.RequestPty("xterm", h, w, modes); err != nil {
logger.L.Error("ssh request pty failed", zap.Error(err))
return
}
if err = sess.Shell(); err != nil {
logger.L.Error("ssh start shell failed", zap.Error(err))
return
}
bs, err := json.Marshal(req)
if err != nil {
logger.L.Error("ssh req marshal failed", zap.Error(err))
return
}
if _, err = chs.Win.Write(append(bs, '\r')); err != nil {
logger.L.Error("ssh req", zap.Error(err), zap.String("req content", string(bs)))
return
}
buf := bufio.NewReader(rout)
line, err := buf.ReadBytes('\r')
if err != nil {
logger.L.Error("ssh read bytes failed", zap.Error(err))
return
}
resp := &model.SshResp{}
if err = json.Unmarshal([]byte(line)[0:len(line)-1], resp); err != nil {
logger.L.Error("ssh resp", zap.Error(err), zap.String("resp content", string(line)))
return
}
chs.ErrChan <- nil
chs.RespChan <- resp
waitChan := make(chan error)
go func() {
waitChan <- sess.Wait()
}()
go func() {
for {
rn, size, err := buf.ReadRune()
if err != nil {
logger.L.Debug("buf ReadRune failed", zap.Error(err))
return
}
if size <= 0 || rn == utf8.RuneError {
continue
}
p := make([]byte, utf8.RuneLen(rn))
utf8.EncodeRune(p, rn)
chs.OutChan <- p
}
}()
defer sess.Close()
for {
select {
case err = <-waitChan:
return
case <-chs.AwayChan:
return
case s := <-chs.WindowChan:
wh := strings.Split(s, ",")
if len(wh) < 2 {
continue
}
w = cast.ToInt(wh[0])
h = cast.ToInt(wh[1])
if w <= 0 || h <= 0 {
continue
}
if err := sess.WindowChange(h, w); err != nil {
logger.L.Warn("reset window size failed", zap.Error(err))
}
}
}
}
func makeChans() *model.SessionChans {
rin, win := io.Pipe()
return &model.SessionChans{
Rin: rin,
Win: win,
ErrChan: make(chan error),
RespChan: make(chan *model.SshResp),
InChan: make(chan []byte),
OutChan: make(chan []byte),
Buf: &bytes.Buffer{},
WindowChan: make(chan string),
AwayChan: make(chan struct{}),
CloseChan: make(chan string),
}
}
func newSshReq(ctx *gin.Context, action int) *model.SshReq {
currentUser, _ := acl.GetSessionFromCtx(ctx)
return &model.SshReq{
Uid: currentUser.GetUid(),
UserName: currentUser.GetUserName(),
Cookie: ctx.GetHeader("Cookie"),
AcceptLanguage: ctx.GetHeader("Accept-Language"),
ClientIp: ctx.ClientIP(),
AssetId: cast.ToInt(ctx.Param("asset_id")),
AccountId: cast.ToInt(ctx.Param("account_id")),
Protocol: ctx.Param("protocol"),
Action: action,
SessionId: ctx.Param("session_id"),
}
}
func writeToMonitors(monitors *sync.Map, out []byte) {
monitors.Range(func(key, value any) bool {
ws, ok := value.(*websocket.Conn)
if !ok || ws == nil {
return true
}
ws.WriteMessage(websocket.TextMessage, out)
return true
})
}
// ConnectMonitor godoc
//
// @Tags connect
// @Success 200 {object} HttpResponse
// @Router /connect/monitor/:session_id [get]
func (c *Controller) ConnectMonitor(ctx *gin.Context) {
currentUser, _ := acl.GetSessionFromCtx(ctx)
sessionId := ctx.Param("session_id")
key := fmt.Sprintf("%d-%s-%d", currentUser.Uid, sessionId, time.Now().Nanosecond())
ws, err := Upgrader.Upgrade(ctx.Writer, ctx.Request, nil)
if err != nil {
ctx.AbortWithError(http.StatusInternalServerError, err)
return
}
defer ws.Close()
defer func() {
if err == nil {
return
}
logger.L.Debug("monitor failed", zap.String("session_id", sessionId), zap.Error(err))
ae, ok := err.(*ApiError)
if !ok {
return
}
lang := ctx.PostForm("lang")
accept := ctx.GetHeader("Accept-Language")
localizer := i18n.NewLocalizer(conf.Bundle, lang, accept)
ws.WriteMessage(websocket.TextMessage, []byte(ae.Message(localizer)))
ctx.AbortWithError(http.StatusBadRequest, err)
}()
if !acl.IsAdmin(currentUser) {
ctx.AbortWithError(http.StatusBadRequest, &ApiError{Code: ErrNoPerm, Data: map[string]any{"perm": "monitor session"}})
return
}
session := &model.Session{}
err = mysql.DB.
Where("session_id = ?", sessionId).
Where("status = ?", model.SESSIONSTATUS_ONLINE).
First(session).
Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
onlineSession.Delete(sessionId)
}
ctx.AbortWithError(http.StatusBadRequest, &ApiError{Code: ErrInvalidSessionId, Data: map[string]any{"sessionId": sessionId}})
return
}
v, ok := onlineSession.Load(sessionId)
if !ok {
ctx.AbortWithError(http.StatusBadRequest, &ApiError{Code: ErrInvalidSessionId, Data: map[string]any{"sessionId": sessionId}})
return
}
session, ok = v.(*model.Session)
if !ok {
ctx.AbortWithError(http.StatusBadRequest, &ApiError{Code: ErrInvalidSessionId, Data: map[string]any{"sessionId": sessionId}})
return
}
switch session.SessionType {
case model.SESSIONTYPE_WEB:
case model.SESSIONTYPE_CLIENT:
cur := false
session.Monitors.Range(func(key, value any) bool {
cur = true
return !cur
})
if !cur {
req := newSshReq(ctx, model.SESSIONACTION_MONITOR)
req.SessionId = sessionId
chs := makeChans()
logger.L.Debug("connect to monitor client", zap.String("sessionId", sessionId))
go doSsh(ctx, cast.ToInt(ctx.Query("w")), cast.ToInt(ctx.Query("h")), req, chs)
if err = <-chs.ErrChan; err != nil {
err = &ApiError{Code: ErrConnectServer, Data: map[string]any{"err": err}}
return
}
resp := <-chs.RespChan
if resp.Code != 0 {
err = &ApiError{Code: ErrConnectServer, Data: map[string]any{"err": resp.Message}}
return
}
tk := time.NewTicker(time.Millisecond * 100)
defer sendMsg(nil, session, chs)
go func() {
for {
select {
case <-ctx.Done():
return
case closeBy := <-chs.CloseChan:
writeToMonitors(session.Monitors, []byte("\r\n \033[31m closed by admin"))
logger.L.Warn("close by admin", zap.String("username", closeBy))
return
case err := <-chs.ErrChan:
logger.L.Error("ssh connection failed", zap.Error(err))
return
case out := <-chs.OutChan:
chs.Buf.Write(out)
case <-tk.C:
sendMsg(nil, session, chs)
}
}
}()
}
}
session.Monitors.Store(key, ws)
defer func() {
session.Monitors.Delete(key)
}()
for {
_, _, err = ws.ReadMessage()
if err != nil {
logger.L.Warn("end monitor", zap.Error(err))
return
}
}
}
// ConnectClose godoc
//
// @Tags connect
// @Success 200 {object} HttpResponse
// @Router /connect/close/:session_id [post]
func (c *Controller) ConnectClose(ctx *gin.Context) {
currentUser, _ := acl.GetSessionFromCtx(ctx)
if !acl.IsAdmin(currentUser) {
ctx.AbortWithError(http.StatusBadRequest, &ApiError{Code: ErrNoPerm, Data: map[string]any{"perm": "close session"}})
return
}
session := &model.Session{}
err := mysql.DB.
Model(session).
Where("session_id = ?", ctx.Param("session_id")).
Where("status = ?", model.SESSIONSTATUS_ONLINE).
First(session).
Error
if errors.Is(err, gorm.ErrRecordNotFound) {
ctx.JSON(http.StatusOK, defaultHttpResponse)
return
}
if err != nil {
ctx.AbortWithError(http.StatusBadRequest, &ApiError{Code: ErrInvalidArgument, Data: map[string]any{"err": "invalid session id"}})
return
}
logger.L.Info("closing...", zap.String("sessionId", session.SessionId), zap.Int("type", session.SessionType))
defer doOfflineOnlineSession(ctx, session.SessionId, currentUser.GetUserName())
chs := makeChans()
req := newSshReq(ctx, model.SESSIONACTION_CLOSE)
req.SessionId = session.SessionId
go doSsh(ctx, cast.ToInt(ctx.Query("w")), cast.ToInt(ctx.Query("h")), req, chs)
if err = <-chs.ErrChan; err != nil {
ctx.AbortWithError(http.StatusInternalServerError, &ApiError{Code: ErrConnectServer, Data: map[string]any{"err": err}})
return
}
resp := <-chs.RespChan
if resp.Code != 0 {
ctx.AbortWithError(http.StatusBadRequest, &ApiError{Code: ErrBadRequest, Data: map[string]any{"err": resp.Message}})
return
}
ctx.JSON(http.StatusOK, defaultHttpResponse)
}
func doOfflineOnlineSession(ctx *gin.Context, sessionId string, closer string) {
logger.L.Debug("offline", zap.String("session_id", sessionId), zap.String("closer", closer))
defer onlineSession.Delete(sessionId)
v, ok := onlineSession.Load(sessionId)
if ok {
if session, ok := v.(*model.Session); ok {
if closer != "" && session.Chans != nil {
select {
case session.Chans.CloseChan <- closer:
break
case <-time.After(time.Second):
break
}
}
session.Monitors.Range(func(key, value any) bool {
ws, ok := value.(*websocket.Conn)
if ok && ws != nil {
lang := ctx.PostForm("lang")
accept := ctx.GetHeader("Accept-Language")
localizer := i18n.NewLocalizer(conf.Bundle, lang, accept)
cfg := &i18n.LocalizeConfig{
TemplateData: map[string]any{"sessionId": sessionId},
DefaultMessage: myi18n.MsgSessionEnd,
}
msg, _ := localizer.Localize(cfg)
ws.WriteMessage(websocket.TextMessage, []byte(msg))
ws.Close()
}
return true
})
}
}
}

View File

@@ -0,0 +1,439 @@
package controller
import (
"encoding/json"
"errors"
"fmt"
"net/http"
"reflect"
"time"
"github.com/gin-gonic/gin"
"github.com/samber/lo"
"github.com/spf13/cast"
"golang.org/x/sync/errgroup"
"gorm.io/gorm"
"github.com/veops/oneterm/pkg/server/auth/acl"
"github.com/veops/oneterm/pkg/server/model"
"github.com/veops/oneterm/pkg/server/remote"
"github.com/veops/oneterm/pkg/server/storage/db/mysql"
)
var (
defaultHttpResponse = &HttpResponse{
Code: 0,
Message: "ok",
Data: nil,
}
)
type preHook[T any] func(*gin.Context, T)
type postHook[T any] func(*gin.Context, []T)
type deleteCheck func(*gin.Context, int)
type Controller struct{}
func NewController() *Controller {
return &Controller{}
}
type HttpResponse struct {
Code int `json:"code"`
Message string `json:"message"`
Data any `json:"data"`
}
type ListData struct {
Count int64 `json:"count"`
List []any `json:"list"`
}
func NewHttpResponseWithData(data any) *HttpResponse {
return &HttpResponse{
Code: 0,
Message: "ok",
Data: data,
}
}
func doCreate[T model.Model](ctx *gin.Context, needAcl bool, md T, resourceType string, preHooks ...preHook[T]) (err error) {
currentUser, _ := acl.GetSessionFromCtx(ctx)
if err = ctx.BindJSON(md); err != nil {
ctx.AbortWithError(http.StatusBadRequest, &ApiError{Code: ErrInvalidArgument, Data: map[string]any{"err": err}})
return
}
for _, hook := range preHooks {
if hook == nil {
continue
}
hook(ctx, md)
if ctx.IsAborted() {
return
}
}
resourceId := 0
if needAcl {
if !acl.IsAdmin(currentUser) {
ctx.AbortWithError(http.StatusForbidden, &ApiError{Code: ErrNoPerm, Data: map[string]any{"perm": acl.WRITE}})
return
}
resourceId, err = acl.CreateGrantAcl(ctx, currentUser, resourceType, md.GetName())
if err != nil {
handleRemoteErr(ctx, err)
return
}
md.SetResourceId(resourceId)
}
md.SetCreatorId(currentUser.Uid)
md.SetUpdaterId(currentUser.Uid)
if err = mysql.DB.Transaction(func(tx *gorm.DB) (err error) {
if err = tx.Model(md).Create(md).Error; err != nil {
return
}
switch t := any(md).(type) {
case *model.Asset:
if err = HandleAuthorization(currentUser, tx, model.ACTION_CREATE, nil, t); err != nil {
handleRemoteErr(ctx, err)
return
}
}
if err = tx.Create(&model.History{
RemoteIp: ctx.ClientIP(),
Type: md.TableName(),
TargetId: md.GetId(),
ActionType: model.ACTION_CREATE,
Old: nil,
New: toMap(md),
CreatorId: currentUser.Uid,
CreatedAt: time.Now(),
}).Error; err != nil {
return
}
return
}); err != nil {
ctx.AbortWithError(http.StatusInternalServerError, &ApiError{Code: ErrInternal, Data: map[string]any{"err": err}})
return
}
ctx.JSON(http.StatusOK, defaultHttpResponse)
return
}
func doDelete[T model.Model](ctx *gin.Context, needAcl bool, md T, dcs ...deleteCheck) (err error) {
currentUser, _ := acl.GetSessionFromCtx(ctx)
id, err := cast.ToIntE(ctx.Param("id"))
if err != nil {
ctx.AbortWithError(http.StatusBadRequest, &ApiError{Code: ErrInvalidArgument, Data: map[string]any{"err": err}})
return
}
if err = mysql.DB.Model(md).Where("id = ?", id).First(md).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
ctx.JSON(http.StatusOK, defaultHttpResponse)
return
}
ctx.AbortWithError(http.StatusBadRequest, &ApiError{Code: ErrInternal, Data: map[string]any{"err": err}})
return
}
if needAcl {
if !acl.IsAdmin(currentUser) && acl.HasPerm(md.GetResourceId(), currentUser.Acl.Rid, acl.DELETE) {
ctx.AbortWithError(http.StatusForbidden, &ApiError{Code: ErrNoPerm, Data: map[string]any{"perm": acl.DELETE}})
return
}
}
for _, dc := range dcs {
if dc == nil {
continue
}
dc(ctx, id)
if ctx.IsAborted() {
return
}
}
if needAcl {
if err = acl.DeleteResource(ctx, currentUser.GetUid(), md.GetResourceId()); err != nil {
handleRemoteErr(ctx, err)
return
}
}
if err = mysql.DB.Transaction(func(tx *gorm.DB) (err error) {
switch t := any(md).(type) {
case *model.Asset:
if err = HandleAuthorization(currentUser, tx, model.ACTION_DELETE, t, nil); err != nil {
handleRemoteErr(ctx, err)
return
}
}
if err = tx.Delete(md, id).Error; err != nil {
return
}
err = tx.Create(&model.History{
RemoteIp: ctx.ClientIP(),
Type: md.TableName(),
TargetId: md.GetId(),
ActionType: model.ACTION_DELETE,
Old: toMap(md),
New: nil,
CreatorId: currentUser.Uid,
CreatedAt: time.Now(),
}).Error
return
}); err != nil {
if errors.Is(err, gorm.ErrDuplicatedKey) {
ctx.AbortWithError(http.StatusBadRequest, &ApiError{Code: ErrDuplicateName, Data: map[string]any{"err": err}})
return
}
ctx.AbortWithError(http.StatusInternalServerError, &ApiError{Code: ErrInternal, Data: map[string]any{"err": err}})
return
}
ctx.JSON(http.StatusOK, defaultHttpResponse)
return
}
func doUpdate[T model.Model](ctx *gin.Context, needAcl bool, md T, preHooks ...preHook[T]) (err error) {
currentUser, _ := acl.GetSessionFromCtx(ctx)
id, err := cast.ToIntE(ctx.Param("id"))
if err != nil {
ctx.AbortWithError(http.StatusBadRequest, &ApiError{Code: ErrInvalidArgument, Data: map[string]any{"err": err}})
return
}
if err = ctx.BindJSON(md); err != nil {
ctx.AbortWithError(http.StatusBadRequest, &ApiError{Code: ErrInvalidArgument, Data: map[string]any{"err": err}})
return
}
md.SetUpdaterId(currentUser.Uid)
for _, hook := range preHooks {
if hook == nil {
continue
}
hook(ctx, md)
if ctx.IsAborted() {
return
}
}
old := getEmpty(md)
if err = mysql.DB.Model(md).Where("id = ?", id).First(old).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
ctx.JSON(http.StatusOK, defaultHttpResponse)
return
}
ctx.AbortWithError(http.StatusBadRequest, &ApiError{Code: ErrInternal, Data: map[string]any{"err": err}})
return
}
if needAcl {
if !acl.IsAdmin(currentUser) && acl.HasPerm(md.GetResourceId(), currentUser.Acl.Rid, acl.WRITE) {
ctx.AbortWithError(http.StatusForbidden, &ApiError{Code: ErrNoPerm, Data: map[string]any{"perm": acl.WRITE}})
return
}
if err = acl.UpdateResource(ctx, currentUser.GetUid(), old.GetResourceId(), map[string]string{"name": md.GetName()}); err != nil {
handleRemoteErr(ctx, err)
return
}
}
md.SetId(id)
if err = mysql.DB.Transaction(func(tx *gorm.DB) (err error) {
omits := []string{"resource_id", "created_at", "deleted_at"}
switch t := any(md).(type) {
case *model.Asset:
if err = HandleAuthorization(currentUser, tx, model.ACTION_UPDATE, any(old).(*model.Asset), t); err != nil {
handleRemoteErr(ctx, err)
return
}
omits = append(omits, "ci_id")
}
if err = mysql.DB.Omit(omits...).Save(md).Error; err != nil {
return
}
err = mysql.DB.Create(&model.History{
RemoteIp: ctx.ClientIP(),
Type: md.TableName(),
TargetId: md.GetId(),
ActionType: model.ACTION_UPDATE,
Old: toMap(old),
New: toMap(md),
CreatorId: currentUser.Uid,
CreatedAt: time.Now(),
}).Error
return
}); err != nil {
if errors.Is(err, gorm.ErrDuplicatedKey) {
ctx.AbortWithError(http.StatusBadRequest, &ApiError{Code: ErrDuplicateName, Data: map[string]any{"err": err}})
return
}
ctx.AbortWithError(http.StatusInternalServerError, &ApiError{Code: ErrInternal, Data: map[string]any{"err": err}})
return
}
ctx.JSON(http.StatusOK, defaultHttpResponse)
return
}
func doGet[T any](ctx *gin.Context, needAcl bool, dbFind *gorm.DB, resourceType string, postHooks ...postHook[T]) (err error) {
currentUser, _ := acl.GetSessionFromCtx(ctx)
if needAcl && !acl.IsAdmin(currentUser) {
//rs := make([]*acl.Resource, 0)
var rs []*acl.Resource
rs, err = acl.GetRoleResources(ctx, currentUser.Acl.Rid, resourceType)
if err != nil {
handleRemoteErr(ctx, err)
return
}
dbFind = dbFind.Where("resource_id IN ?", lo.Map(rs, func(r *acl.Resource, _ int) int { return r.ResourceId }))
}
dbCount := dbFind.Session(&gorm.Session{})
dbFind = dbFind.Session(&gorm.Session{})
count, list := int64(0), make([]T, 0)
eg := &errgroup.Group{}
eg.Go(func() error {
return dbCount.Count(&count).
Error
})
eg.Go(func() error {
pi, ps := cast.ToInt(ctx.Query("page_index")), cast.ToInt(ctx.Query("page_size"))
if _, ok := ctx.GetQuery("page_index"); ok {
dbFind = dbFind.Offset((pi - 1) * ps)
}
if _, ok := ctx.GetQuery("page_size"); ok {
dbFind = dbFind.Limit(ps)
}
return dbFind.
Order("id DESC").
Find(&list).
Error
})
if err = eg.Wait(); err != nil {
ctx.AbortWithError(http.StatusInternalServerError, &ApiError{Code: ErrInternal, Data: map[string]any{"err": err}})
return
}
for _, hook := range postHooks {
if hook == nil {
continue
}
hook(ctx, list)
if ctx.IsAborted() {
return
}
}
res := &ListData{
Count: count,
List: lo.Map(list, func(t T, _ int) any { return t }),
}
ctx.JSON(http.StatusOK, NewHttpResponseWithData(res))
return
}
func handleRemoteErr(ctx *gin.Context, err error) {
switch e := err.(type) {
case *remote.RemoteError:
if e.HttpCode == http.StatusBadRequest {
ctx.AbortWithError(http.StatusBadRequest, &ApiError{Code: ErrRemoteClient, Data: e.Resp})
return
}
ctx.AbortWithError(http.StatusInternalServerError, &ApiError{Code: ErrRemoteServer, Data: e.Resp})
default:
ctx.AbortWithError(http.StatusInternalServerError, &ApiError{Code: ErrInternal, Data: map[string]any{"err": err}})
}
}
func filterSearch(ctx *gin.Context, db *gorm.DB, fields ...string) *gorm.DB {
q, ok := ctx.GetQuery("search")
if !ok || len(fields) <= 0 {
return db
}
d := mysql.DB
for _, f := range fields {
d = d.Or(fmt.Sprintf("%s LIKE ?", f), fmt.Sprintf("%%%s%%", q))
}
db = db.Where(d)
return db
}
func filterStartEnd(ctx *gin.Context, db *gorm.DB, fields ...string) (*gorm.DB, error) {
if q, ok := ctx.GetQuery("start"); ok {
t, err := time.Parse(time.RFC3339, q)
if err != nil {
ctx.AbortWithError(http.StatusBadRequest, err)
return db, err
}
db = db.Where("created_at >= ?", t)
}
if q, ok := ctx.GetQuery("end"); ok {
t, err := time.Parse(time.RFC3339, q)
if err != nil {
ctx.AbortWithError(http.StatusBadRequest, err)
return db, err
}
db = db.Where("created_at <= ?", t)
}
return db, nil
}
func filterEqual(ctx *gin.Context, db *gorm.DB, fields ...string) *gorm.DB {
for _, f := range fields {
if q, ok := ctx.GetQuery(f); ok {
db = db.Where(fmt.Sprintf("%s = ?", f), q)
}
}
return db
}
func filterLike(ctx *gin.Context, db *gorm.DB, fields ...string) *gorm.DB {
likes := false
d := mysql.DB
for _, f := range fields {
if q, ok := ctx.GetQuery(f); ok && q != "" {
d = d.Or(fmt.Sprintf("%s LIKE ?", f), fmt.Sprintf("%%%s%%", q))
likes = true
}
}
if !likes {
return db
}
db = db.Where(d)
return db
}
func toMap(data any) model.Map[string, any] {
bs, _ := json.Marshal(data)
res := make(map[string]any)
json.Unmarshal(bs, &res)
return res
}
func getEmpty[T model.Model](data T) T {
t := reflect.TypeOf(data).Elem()
v := reflect.New(t)
return v.Interface().(T)
}

View File

@@ -0,0 +1,68 @@
package controller
import (
"fmt"
"github.com/nicksnyder/go-i18n/v2/i18n"
myi18n "github.com/veops/oneterm/pkg/i18n"
)
const (
ErrBadRequest = 4000
ErrInvalidArgument = 4001
ErrDuplicateName = 4002
ErrHasChild = 4003
ErrHasDepency = 4004
ErrNoPerm = 4005
ErrRemoteClient = 4006
ErrWrongPk = 4007
ErrWrongMac = 4008
ErrInvalidSessionId = 4009
ErrInternal = 5000
ErrRemoteServer = 5001
ErrConnectServer = 5002
ErrLoadSession = 5003
)
var (
Err2Msg = map[int]*i18n.Message{
ErrBadRequest: myi18n.MsgBadRequest,
ErrInvalidArgument: myi18n.MsgInvalidArguemnt,
ErrDuplicateName: myi18n.MsgDupName,
ErrHasChild: myi18n.MsgHasChild,
ErrHasDepency: myi18n.MsgHasDepdency,
ErrNoPerm: myi18n.MsgNoPerm,
ErrRemoteClient: myi18n.MsgRemoteClient,
ErrWrongPk: myi18n.MsgWrongPk,
ErrInvalidSessionId: myi18n.MsgInvalidSessionId,
ErrInternal: myi18n.MsgInternalError,
ErrRemoteServer: myi18n.MsgRemoteServer,
ErrConnectServer: myi18n.MsgConnectServer,
ErrLoadSession: myi18n.MsgLoadSession,
}
)
type ApiError struct {
Code int
Data map[string]any
}
func (ae *ApiError) Error() string {
return fmt.Sprintf("code=%d data=%v", ae.Code, ae.Data)
}
func (ae *ApiError) Message(localizer *i18n.Localizer) (msg string) {
cfg := &i18n.LocalizeConfig{}
cfg.TemplateData = ae.Data
m, ok := Err2Msg[ae.Code]
if !ok {
msg = ae.Error()
return
}
cfg.DefaultMessage = m
msg, _ = localizer.Localize(cfg)
return
}

View File

@@ -0,0 +1,173 @@
package controller
import (
"errors"
"net/http"
"strings"
"github.com/gin-gonic/gin"
"github.com/samber/lo"
"github.com/spf13/cast"
"golang.org/x/crypto/ssh"
"gorm.io/gorm"
"github.com/veops/oneterm/pkg/conf"
"github.com/veops/oneterm/pkg/server/auth/acl"
"github.com/veops/oneterm/pkg/server/model"
"github.com/veops/oneterm/pkg/server/storage/db/mysql"
"github.com/veops/oneterm/pkg/util"
)
var (
gatewayPreHooks = []preHook[*model.Gateway]{
func(ctx *gin.Context, data *model.Gateway) {
if data.AccountType == model.AUTHMETHOD_PUBLICKEY {
if data.Phrase == "" {
_, err := ssh.ParsePrivateKey([]byte(data.Pk))
if err != nil {
ctx.AbortWithError(http.StatusBadRequest, &ApiError{Code: ErrWrongPk, Data: nil})
return
}
} else {
_, err := ssh.ParsePrivateKeyWithPassphrase([]byte(data.Pk), []byte(data.Phrase))
if err != nil {
ctx.AbortWithError(http.StatusBadRequest, &ApiError{Code: ErrWrongPk, Data: nil})
return
}
}
}
},
func(ctx *gin.Context, data *model.Gateway) {
data.Password = util.EncryptAES(data.Password)
data.Pk = util.EncryptAES(data.Pk)
data.Phrase = util.EncryptAES(data.Phrase)
},
}
gatewayPostHooks = []postHook[*model.Gateway]{
func(ctx *gin.Context, data []*model.Gateway) {
post := make([]*model.GatewayCount, 0)
if err := mysql.DB.
Model(&model.Asset{}).
Select("gateway_id AS id, COUNT(*) AS count").
Where("gateway_id IN ?", lo.Map(data, func(d *model.Gateway, _ int) int { return d.Id })).
Group("gateway_id").
Find(&post).
Error; err != nil {
return
}
m := lo.SliceToMap(post, func(p *model.GatewayCount) (int, int64) { return p.Id, p.Count })
for _, d := range data {
d.AssetCount = m[d.Id]
}
},
func(ctx *gin.Context, data []*model.Gateway) {
for _, d := range data {
d.Password = lo.Ternary(!cast.ToBool(ctx.Query("info")) || ctx.GetHeader("X-Token") == conf.Cfg.SshServer.Xtoken, util.DecryptAES(d.Password), "")
d.Pk = lo.Ternary(!cast.ToBool(ctx.Query("info")) || ctx.GetHeader("X-Token") == conf.Cfg.SshServer.Xtoken, util.DecryptAES(d.Pk), "")
d.Phrase = lo.Ternary(!cast.ToBool(ctx.Query("info")) || ctx.GetHeader("X-Token") == conf.Cfg.SshServer.Xtoken, util.DecryptAES(d.Phrase), "")
}
},
}
gatewayDcs = []deleteCheck{
func(ctx *gin.Context, id int) {
assetName := ""
err := mysql.DB.
Model(&model.Asset{}).
Select("name").
Where("gateway_id = ?", id).
First(&assetName).
Error
if errors.Is(err, gorm.ErrRecordNotFound) {
return
}
code := lo.Ternary(err == nil, http.StatusBadRequest, http.StatusInternalServerError)
err = lo.Ternary[error](err == nil, &ApiError{Code: ErrHasDepency, Data: map[string]any{"name": assetName}}, err)
ctx.AbortWithError(code, err)
},
}
)
// CreateGateway godoc
//
// @Tags gateway
// @Param gateway body model.Gateway true "gateway"
// @Success 200 {object} HttpResponse
// @Router /gateway [post]
func (c *Controller) CreateGateway(ctx *gin.Context) {
doCreate(ctx, true, &model.Gateway{}, conf.RESOURCE_GATEWAY, gatewayPreHooks...)
}
// DeleteGateway godoc
//
// @Tags gateway
// @Param id path int true "gateway id"
// @Success 200 {object} HttpResponse
// @Router /gateway/:id [delete]
func (c *Controller) DeleteGateway(ctx *gin.Context) {
doDelete(ctx, true, &model.Gateway{}, gatewayDcs...)
}
// UpdateGateway godoc
//
// @Tags gateway
// @Param id path int true "gateway id"
// @Param gateway body model.Gateway true "gateway"
// @Success 200 {object} HttpResponse
// @Router /gateway/:id [put]
func (c *Controller) UpdateGateway(ctx *gin.Context) {
doUpdate(ctx, true, &model.Gateway{}, gatewayPreHooks...)
}
// GetGateways godoc
//
// @Tags gateway
// @Param page_index query int true "gateway id"
// @Param page_size query int true "gateway id"
// @Param search query string false "name or host or account or port"
// @Param id query int false "gateway id"
// @Param ids query string false "gateway ids"
// @Param name query string false "gateway name"
// @Param info query bool false "is info mode"
// @Param type query int false "account type"
// @Success 200 {object} HttpResponse{data=ListData{list=[]model.Gateway}}
// @Router /gateway [get]
func (c *Controller) GetGateways(ctx *gin.Context) {
currentUser, _ := acl.GetSessionFromCtx(ctx)
info := cast.ToBool(ctx.Query("info"))
db := mysql.DB.Model(&model.Gateway{})
db = filterEqual(ctx, db, "id","type")
db = filterLike(ctx, db, "name")
db = filterSearch(ctx, db, "name", "host", "account","port")
if q, ok := ctx.GetQuery("ids"); ok {
db = db.Where("id IN ?", lo.Map(strings.Split(q, ","), func(s string, _ int) int { return cast.ToInt(s) }))
}
if info && !acl.IsAdmin(currentUser) {
rs := make([]*acl.Resource, 0)
rs, err := acl.GetRoleResources(ctx, currentUser.Acl.Rid, acl.GetResourceTypeName(conf.RESOURCE_AUTHORIZATION))
if err != nil {
handleRemoteErr(ctx, err)
return
}
sub := mysql.DB.
Model(&model.Authorization{}).
Select("DISTINCT asset_id").
Where("resource_id IN ?", lo.Map(rs, func(r *acl.Resource, _ int) int { return r.ResourceId }))
ids := make([]int, 0)
if err = mysql.DB.
Model(&model.Asset{}).
Where("id IN (?)", sub).
Distinct().
Pluck("gateway_id", &ids).
Error; err != nil {
ctx.AbortWithError(http.StatusInternalServerError, &ApiError{Code: ErrInternal, Data: map[string]any{"err": err}})
}
db = db.Where("id IN ?", ids)
}
db = db.Order("name")
doGet[*model.Gateway](ctx, !info, db, acl.GetResourceTypeName(conf.RESOURCE_GATEWAY), gatewayPostHooks...)
}

View File

@@ -0,0 +1,67 @@
package controller
import (
"net/http"
"github.com/gin-gonic/gin"
"github.com/nicksnyder/go-i18n/v2/i18n"
"github.com/veops/oneterm/pkg/conf"
myi18n "github.com/veops/oneterm/pkg/i18n"
"github.com/veops/oneterm/pkg/server/model"
"github.com/veops/oneterm/pkg/server/storage/db/mysql"
)
// GetHistories godoc
//
// @Tags history
// @Param page_index query int true "page_index"
// @Param page_size query int true "page_size"
// @Param type query string false "type" Enums(account, asset, command, gateway, node, public_key)
// @Param target_id query int false "target_id"
// @Param uid query int false "uid"
// @Param action_type query int false "create=1 delete=2 update=3"
// @Param start query string false "start time, RFC3339"
// @Param end query string false "end time, RFC3339"
// @Param search query string false ""
// @Success 200 {object} HttpResponse{data=ListData{list=[]model.History}}
// @Router /history [get]
func (c *Controller) GetHistories(ctx *gin.Context) {
db := mysql.DB.Model(&model.History{})
db = filterSearch(ctx, db, "old", "new")
db, err := filterStartEnd(ctx, db)
if err != nil {
return
}
db = filterEqual(ctx, db, "type", "target_id", "action_type", "uid")
doGet[*model.History](ctx, false, db, "")
}
// GetSessions godoc
//
// @Tags session
// @Success 200 {object} HttpResponse{data=map[string]string}
// @Router /history/type/mapping [get]
func (c *Controller) GetHistoryTypeMapping(ctx *gin.Context) {
lang := ctx.PostForm("lang")
accept := ctx.GetHeader("Accept-Language")
localizer := i18n.NewLocalizer(conf.Bundle, lang, accept)
cfg := &i18n.LocalizeConfig{}
key2msg := map[string]*i18n.Message{
"account": myi18n.MsgTypeMappingAccount,
"asset": myi18n.MsgTypeMappingAsset,
"command": myi18n.MsgTypeMappingCommand,
"gateway": myi18n.MsgTypeMappingGateway,
"node": myi18n.MsgTypeMappingNode,
"public_key": myi18n.MsgTypeMappingPublicKey,
}
data := make(map[string]string)
for k, v := range key2msg {
cfg.DefaultMessage = v
msg, _ := localizer.Localize(cfg)
data[k] = msg
}
ctx.JSON(http.StatusOK, NewHttpResponseWithData(data))
}

View File

@@ -0,0 +1,217 @@
package controller
import (
"errors"
"fmt"
"net/http"
"strings"
"github.com/gin-gonic/gin"
"github.com/samber/lo"
"github.com/spf13/cast"
"go.uber.org/zap"
"gorm.io/gorm"
"github.com/veops/oneterm/pkg/logger"
"github.com/veops/oneterm/pkg/server/auth/acl"
"github.com/veops/oneterm/pkg/server/model"
"github.com/veops/oneterm/pkg/server/storage/db/mysql"
)
var (
nodePreHooks = []preHook[*model.Node]{
func(ctx *gin.Context, data *model.Node) {
ids := make([]int, 0)
if err := mysql.DB.Raw(fmt.Sprintf(`
WITH RECURSIVE cte AS(
SELECT id
FROM node
WHERE id=%s AND deleted_at = 0
UNION ALL
SELECT t.id
FROM cte
INNER JOIN node t on cte.id = t.parent_id
WHERE deleted_at = 0
)
SELECT
id
FROM cte
`, ctx.Param("id"))).
Find(&ids).
Error; err != nil || lo.Contains(ids, data.ParentId) {
ctx.AbortWithError(http.StatusBadRequest, &ApiError{Code: ErrInvalidArgument})
}
},
}
nodePostHooks = []postHook[*model.Node]{
func(ctx *gin.Context, data []*model.Node) {
currentUser, _ := acl.GetSessionFromCtx(ctx)
isAdmin := acl.IsAdmin(currentUser)
post := make([]*model.NodeCount, 0)
sql := fmt.Sprintf(`
WITH RECURSIVE cte AS(
SELECT parent_id
FROM asset
%s
UNION ALL
SELECT t.parent_id
FROM cte
INNER JOIN node t on cte.parent_id = t.id
WHERE deleted_at = 0
)
SELECT
parent_id,
COUNT(*) AS count
FROM cte
GROUP BY parent_id
`, lo.Ternary(isAdmin, "WHERE deleted_at = 0", "WHERE deleted_at = 0 AND id IN (?)"))
db := mysql.DB.
Model(&model.Asset{})
if isAdmin {
db = db.Raw(sql)
} else {
authorizationResourceIds, err := GetAutorizationResourceIds(ctx)
if err != nil {
ctx.AbortWithError(http.StatusInternalServerError, err)
return
}
db = db.Raw(sql, mysql.DB.Model(&model.Authorization{}).Select("asset_id").Where("resource_id IN ?", authorizationResourceIds))
}
if err := db.
Find(&post).
Error; err != nil {
logger.L.Error("node posthookfailed asset count", zap.Error(err))
return
}
m := lo.SliceToMap(post, func(p *model.NodeCount) (int, int64) { return p.ParentId, p.Count })
for _, d := range data {
d.AssetCount = m[d.Id]
}
}, func(ctx *gin.Context, data []*model.Node) {
ps := make([]int, 0)
if err := mysql.DB.
Model(&model.Node{}).
Where("parent_id IN ?", lo.Map(data, func(n *model.Node, _ int) int { return n.Id })).
Pluck("parent_id", &ps).
Error; err != nil {
logger.L.Error("node posthookfailed has child", zap.Error(err))
return
}
pm := lo.SliceToMap(ps, func(pid int) (int, bool) { return pid, true })
for _, n := range data {
n.HasChild = pm[n.Id]
}
},
}
nodeDcs = []deleteCheck{
func(ctx *gin.Context, id int) {
noChild := true
noChild = noChild && errors.Is(mysql.DB.Model(&model.Node{}).Select("id").Where("parent_id = ?", id).First(map[string]any{}).Error, gorm.ErrRecordNotFound)
noChild = noChild && errors.Is(mysql.DB.Model(&model.Asset{}).Select("id").Where("parent_id = ?", id).First(map[string]any{}).Error, gorm.ErrRecordNotFound)
if noChild {
return
}
err := &ApiError{Code: ErrHasChild, Data: nil}
ctx.AbortWithError(http.StatusBadRequest, err)
},
}
)
// CreateNode godoc
//
// @Tags node
// @Param node body model.Node true "node"
// @Success 200 {object} HttpResponse
// @Router /node [post]
func (c *Controller) CreateNode(ctx *gin.Context) {
doCreate(ctx, false, &model.Node{}, "")
}
// DeleteNode godoc
//
// @Tags node
// @Param id path int true "node id"
// @Success 200 {object} HttpResponse
// @Router /node/:id [delete]
func (c *Controller) DeleteNode(ctx *gin.Context) {
doDelete(ctx, false, &model.Node{}, nodeDcs...)
}
// UpdateNode godoc
//
// @Tags node
// @Param id path int true "node id"
// @Param node body model.Node true "node"
// @Success 200 {object} HttpResponse
// @Router /node/:id [put]
func (c *Controller) UpdateNode(ctx *gin.Context) {
doUpdate(ctx, false, &model.Node{}, nodePreHooks...)
}
// GetNodes godoc
//
// @Tags node
// @Param page_index query int true "node id"
// @Param page_size query int true "node id"
// @Param id query int false "node id"
// @Param ids query string false "node ids"
// @Param parent_id query int false "node's parent id"
// @Param name query string false "node name"
// @Param no_self_child query int false "exclude itself and its child"
// @Param self_parent query int false "include itself and its parent"
// @Success 200 {object} HttpResponse{data=ListData{list=[]model.Node}}
// @Router /node [get]
func (c *Controller) GetNodes(ctx *gin.Context) {
db := mysql.DB.Model(&model.Node{})
db = filterEqual(ctx, db, "id", "parent_id")
db = filterLike(ctx, db, "name")
db = filterSearch(ctx, db, "name")
if q, ok := ctx.GetQuery("ids"); ok {
db = db.Where("id IN ?", lo.Map(strings.Split(q, ","), func(s string, _ int) int { return cast.ToInt(s) }))
}
if id, ok := ctx.GetQuery("no_self_child"); ok {
sql := fmt.Sprintf(`
WITH RECURSIVE cte AS(
SELECT id
FROM node
WHERE id=%s AND deleted_at = 0
UNION ALL
SELECT t.id
FROM cte
INNER JOIN node t on cte.id = t.parent_id
WHERE deleted_at = 0
)
SELECT
id
FROM cte
`, id)
sub := mysql.DB.Raw(sql)
db = db.Where("id NOT IN (?)", sub)
}
if id, ok := ctx.GetQuery("self_parent"); ok {
sql := fmt.Sprintf(`
WITH RECURSIVE cte AS(
SELECT id,parent_id
FROM node
WHERE id=%s AND deleted_at = 0
UNION ALL
SELECT t.id,t.parent_id
FROM cte
INNER JOIN node t on cte.parent_id = t.id
WHERE deleted_at = 0
)
SELECT
id
FROM cte
`, id)
sub := mysql.DB.Raw(sql)
db = db.Where("id IN (?)", sub)
}
db = db.Order("name DESC")
doGet[*model.Node](ctx, false, db, "", nodePostHooks...)
}

View File

@@ -0,0 +1,140 @@
package controller
import (
"net"
"net/http"
"strings"
"github.com/gin-gonic/gin"
"golang.org/x/crypto/ssh"
"github.com/veops/oneterm/pkg/server/auth/acl"
"github.com/veops/oneterm/pkg/server/model"
"github.com/veops/oneterm/pkg/server/storage/db/mysql"
"github.com/veops/oneterm/pkg/util"
)
var (
publicKeyPreHooks = []preHook[*model.PublicKey]{
func(ctx *gin.Context, data *model.PublicKey) {
if _, comment, _, _, err := ssh.ParseAuthorizedKey([]byte(data.Pk)); err != nil {
ctx.AbortWithError(http.StatusBadRequest, &ApiError{Code: ErrWrongPk, Data: nil})
} else {
data.Pk = strings.TrimSpace(strings.TrimSuffix(data.Pk, comment))
}
if _, err := net.ParseMAC(data.Mac); err != nil {
ctx.AbortWithError(http.StatusBadRequest, &ApiError{Code: ErrWrongMac, Data: nil})
}
},
func(ctx *gin.Context, data *model.PublicKey) {
data.Pk = util.EncryptAES(data.Pk)
},
func(ctx *gin.Context, data *model.PublicKey) {
currentUser, _ := acl.GetSessionFromCtx(ctx)
data.Uid = currentUser.GetUid()
data.UserName = currentUser.GetUserName()
},
}
publicKeyPostHooks = []postHook[*model.PublicKey]{
func(ctx *gin.Context, data []*model.PublicKey) {
for _, d := range data {
d.Pk = util.DecryptAES(d.Pk)
}
},
}
)
// CreatePublicKey godoc
//
// @Tags public_key
// @Param publicKey body model.PublicKey true "publicKey"
// @Success 200 {object} HttpResponse
// @Router /public_key [post]
func (c *Controller) CreatePublicKey(ctx *gin.Context) {
doCreate(ctx, false, &model.PublicKey{}, "", publicKeyPreHooks...)
}
// DeletePublicKey godoc
//
// @Tags public_key
// @Param id path int true "publicKey id"
// @Success 200 {object} HttpResponse
// @Router /public_key/:id [delete]
func (c *Controller) DeletePublicKey(ctx *gin.Context) {
doDelete(ctx, false, &model.PublicKey{})
}
// UpdatePublicKey godoc
//
// @Tags public_key
// @Param id path int true "publicKey id"
// @Param publicKey body model.PublicKey true "publicKey"
// @Success 200 {object} HttpResponse
// @Router /public_key/:id [put]
func (c *Controller) UpdatePublicKey(ctx *gin.Context) {
doUpdate(ctx, false, &model.PublicKey{}, publicKeyPreHooks...)
}
// GetPublicKeys godoc
//
// @Tags public_key
// @Param page_index query int true "publicKey id"
// @Param page_size query int true "publicKey id"
// @Param search query string false "name or mac"
// @Param id query int false "publicKey id"
// @Param name query string false "publicKey name"
// @Success 200 {object} HttpResponse{data=ListData{list=[]model.PublicKey}}
// @Router /public_key [get]
func (c *Controller) GetPublicKeys(ctx *gin.Context) {
currentUser, _ := acl.GetSessionFromCtx(ctx)
db := mysql.DB.Model(&model.PublicKey{})
db = filterSearch(ctx, db, "name", "mac")
db = filterEqual(ctx, db, "id")
db = filterLike(ctx, db, "name")
db = db.Where("uid = ?", currentUser.Uid)
doGet[*model.PublicKey](ctx, false, db, "", publicKeyPostHooks...)
}
// Auth godoc
//
// @Tags public_key
// @Param req body model.ReqAuth false "method 1password 2publickey"
// @Success 200 {object} HttpResponse{}
// @Router /public_key/auth [post]
func (c *Controller) Auth(ctx *gin.Context) {
data := &model.ReqAuth{}
if err := ctx.BindJSON(data); err != nil {
ctx.AbortWithError(http.StatusBadRequest, &ApiError{Code: ErrInvalidArgument, Data: map[string]any{"err": err}})
return
}
switch data.Method {
case model.AUTHMETHOD_PASSWORD:
cookie, err := acl.LoginByPassword(ctx, data.UserName, data.Password)
if err != nil {
ctx.AbortWithError(http.StatusUnauthorized, &ApiError{Code: ErrInvalidArgument, Data: map[string]any{"err": err}})
return
}
ctx.JSON(http.StatusOK, NewHttpResponseWithData(map[string]any{"cookie": cookie}))
case model.AUTHMETHOD_PUBLICKEY:
pk := &model.PublicKey{}
if err := mysql.DB.
Where("username = ? AND pk = ?", data.UserName, util.EncryptAES(data.Pk)).
First(pk).Error; err != nil {
ctx.AbortWithError(http.StatusBadRequest, &ApiError{Code: ErrInvalidArgument, Data: map[string]any{"err": err}})
return
}
cookie, err := acl.LoginByPublicKey(ctx, data.UserName)
if err != nil {
ctx.AbortWithError(http.StatusUnauthorized, &ApiError{Code: ErrInvalidArgument, Data: map[string]any{"err": err}})
return
}
ctx.JSON(http.StatusOK, NewHttpResponseWithData(map[string]any{"cookie": cookie}))
default:
ctx.AbortWithError(http.StatusBadRequest, &ApiError{Code: ErrInvalidArgument, Data: map[string]any{"err": "invalid auth method"}})
return
}
}

View File

@@ -0,0 +1,270 @@
package controller
import (
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"sync"
"time"
"github.com/gin-gonic/gin"
"github.com/samber/lo"
"go.uber.org/zap"
"gorm.io/gorm/clause"
"github.com/veops/oneterm/pkg/logger"
"github.com/veops/oneterm/pkg/server/auth/acl"
"github.com/veops/oneterm/pkg/server/model"
"github.com/veops/oneterm/pkg/server/storage/db/mysql"
)
var (
onlineSession = &sync.Map{}
sessionPostHooks = []postHook[*model.Session]{
func(ctx *gin.Context, data []*model.Session) {
sessionIds := lo.Map(data, func(d *model.Session, _ int) string { return d.SessionId })
if len(sessionIds) <= 0 {
return
}
post := make([]*model.CmdCount, 0)
if err := mysql.DB.
Model(&model.SessionCmd{}).
Select("session_id, COUNT(*) AS count").
Where("session_id IN ?", sessionIds).
Group("session_id").
Find(&post).
Error; err != nil {
logger.L.Error("gateway posthookfailed", zap.Error(err))
return
}
m := lo.SliceToMap(post, func(p *model.CmdCount) (string, int64) { return p.SessionId, p.Count })
for _, d := range data {
d.CmdCount = m[d.SessionId]
}
},
func(ctx *gin.Context, data []*model.Session) {
now := time.Now()
for _, d := range data {
t := now
if d.ClosedAt != nil {
t = *d.ClosedAt
}
d.Duration = int64(t.Sub(d.CreatedAt).Seconds())
}
},
}
)
func Init() (err error) {
sessions := make([]*model.Session, 0)
err = mysql.DB.
Model(&model.Session{}).
Where("status = ?", model.SESSIONSTATUS_ONLINE).
Find(&sessions).
Error
if err != nil {
return
}
ctx := &gin.Context{}
for _, s := range sessions {
if s.SessionType == model.SESSIONTYPE_WEB {
doOfflineOnlineSession(ctx, s.SessionId, "")
continue
}
s.Monitors = &sync.Map{}
onlineSession.LoadOrStore(s.SessionId, s)
}
return
}
// UpsertSession godoc
//
// @Tags session
// @Param sessino body model.Session true "session"
// @Success 200 {object} HttpResponse
// @Router /session [post]
func (c *Controller) UpsertSession(ctx *gin.Context) {
data := &model.Session{}
if err := ctx.BindJSON(data); err != nil {
ctx.AbortWithError(http.StatusBadRequest, &ApiError{Code: ErrInvalidArgument, Data: map[string]any{"err": err}})
return
}
if err := mysql.DB.
Clauses(clause.OnConflict{
DoUpdates: clause.AssignmentColumns([]string{"status", "closed_at"}),
}).
Create(data).
Error; err != nil {
ctx.AbortWithError(http.StatusInternalServerError, &ApiError{Code: ErrInternal, Data: map[string]any{"err": err}})
return
}
switch data.Status {
case model.SESSIONSTATUS_ONLINE:
if data.Monitors == nil {
data.Monitors = &sync.Map{}
}
_, ok := onlineSession.LoadOrStore(data.SessionId, data)
if ok {
ctx.AbortWithError(http.StatusInternalServerError, &ApiError{Code: ErrInternal, Data: map[string]any{"err": "failed to loadstore online session"}})
return
}
case model.SESSIONSTATUS_OFFLINE:
// doOfflineOnlineSession(ctx, data.SessionId, "")
}
ctx.JSON(http.StatusOK, defaultHttpResponse)
}
// CreateSessionCommand godoc
//
// @Tags session
// @Param sessioncmd body model.SessionCmd true "SessionCmd"
// @Success 200 {object} HttpResponse
// @Router /session/cmd [post]
func (c *Controller) CreateSessionCmd(ctx *gin.Context) {
data := &model.SessionCmd{}
if err := ctx.BindJSON(data); err != nil {
ctx.AbortWithError(http.StatusBadRequest, &ApiError{Code: ErrInvalidArgument, Data: map[string]any{"err": err}})
return
}
if err := mysql.DB.
Create(data).
Error; err != nil {
ctx.AbortWithError(http.StatusBadRequest, &ApiError{Code: ErrInternal, Data: map[string]any{"err": err}})
return
}
ctx.JSON(http.StatusOK, defaultHttpResponse)
}
// GetSessions godoc
//
// @Tags session
// @Param page_index query int true "page_index"
// @Param page_size query int true "page_size"
// @Param search query string false "search"
// @Param status query int false "status, online=1, offline=2"
// @Param start query string false "start, RFC3339"
// @Param end query string false "end, RFC3339"
// @Param uid query int false "uid"
// @Param asset_id query int false "asset id"
// @Param client_ip query string false "client_ip"
// @Success 200 {object} HttpResponse{data=ListData{list=[]model.Session}}
// @Router /session [get]
func (c *Controller) GetSessions(ctx *gin.Context) {
db := mysql.DB.Model(&model.Session{})
currentUser, _ := acl.GetSessionFromCtx(ctx)
if !acl.IsAdmin(currentUser) {
db = db.Where("uid = ?", currentUser.Uid)
}
db = filterSearch(ctx, db, "user_name", "asset_info", "gateway_info", "account_info")
db, err := filterStartEnd(ctx, db)
if err != nil {
return
}
db = filterEqual(ctx, db, "status", "uid", "asset_id", "client_ip")
doGet[*model.Session](ctx, false, db, "", sessionPostHooks...)
}
// GetSessionCmds godoc
//
// @Tags session
// @Param page_index query int true "page_index"
// @Param page_size query int true "page_size"
// @Param session_id path string true "session id"
// @Param search query string true "search"
// @Success 200 {object} HttpResponse{data=ListData{list=[]model.SessionCmd}}
// @Router /session/:session_id/cmd [get]
func (c *Controller) GetSessionCmds(ctx *gin.Context) {
db := mysql.DB.Model(&model.SessionCmd{})
db = db.Where("session_id = ?", ctx.Param("session_id"))
db = filterSearch(ctx, db, "cmd", "result")
doGet[*model.SessionCmd](ctx, false, db, "")
}
// GetSessionOptionAsset godoc
//
// @Tags session
// @Success 200 {object} HttpResponse{data=ListData{list=[]model.SessionOptionAsset}}
// @Router /session/option/asset [get]
func (c *Controller) GetSessionOptionAsset(ctx *gin.Context) {
opts := make([]*model.SessionOptionAsset, 0)
if err := mysql.DB.
Model(&model.Asset{}).
Select("id, name").
Find(&opts).
Error; err != nil {
ctx.AbortWithError(http.StatusInternalServerError, err)
return
}
ctx.JSON(http.StatusOK, NewHttpResponseWithData(opts))
}
// GetSessionOptionClientIp godoc
//
// @Tags session
// @Success 200 {object} HttpResponse{data=[]string}
// @Router /session/option/clientip [get]
func (c *Controller) GetSessionOptionClientIp(ctx *gin.Context) {
opts := make([]string, 0)
if err := mysql.DB.
Model(&model.Session{}).
Distinct("client_ip").
Find(&opts).
Error; err != nil {
ctx.AbortWithError(http.StatusInternalServerError, err)
return
}
ctx.JSON(http.StatusOK, NewHttpResponseWithData(opts))
}
// CreateSessionReplay godoc
//
// @Tags session
// @Param session_id path string true "session id"
// @Success 200 {object} HttpResponse
// @Router /session/replay/:session_id [post]
func (c *Controller) CreateSessionReplay(ctx *gin.Context) {
file, _, err := ctx.Request.FormFile("replay.cast")
if err != nil {
ctx.AbortWithError(http.StatusBadRequest, &ApiError{Code: ErrInvalidArgument, Data: map[string]any{"err": err}})
return
}
content, err := io.ReadAll(file)
if err != nil {
ctx.AbortWithError(http.StatusBadRequest, &ApiError{Code: ErrInvalidArgument, Data: map[string]any{"err": err}})
return
}
f, err := os.Create(filepath.Join("/replay", fmt.Sprintf("%s.cast", ctx.Param("session_id"))))
if err != nil {
ctx.AbortWithError(http.StatusInternalServerError, &ApiError{Code: ErrInternal, Data: map[string]any{"err": err}})
return
}
defer f.Close()
f.Write(content)
ctx.JSON(http.StatusOK, defaultHttpResponse)
}
// GetSessionReplay godoc
//
// @Tags session
// @Param session_id path string true "session id"
// @Success 200 {object} string
// @Router /session/replay/:session_id [get]
func (c *Controller) GetSessionReplay(ctx *gin.Context) {
sessionId := ctx.Param("session_id")
filename := fmt.Sprintf("%s.cast", sessionId)
ctx.FileAttachment(filepath.Join("/replay", filename), filename)
}

View File

@@ -0,0 +1,300 @@
package controller
import (
"fmt"
"net/http"
"sort"
"time"
"github.com/gin-gonic/gin"
"github.com/samber/lo"
"golang.org/x/sync/errgroup"
"github.com/veops/oneterm/pkg/server/auth/acl"
"github.com/veops/oneterm/pkg/server/model"
"github.com/veops/oneterm/pkg/server/storage/cache/redis"
"github.com/veops/oneterm/pkg/server/storage/db/mysql"
)
// StatAssetType godoc
//
// @Tags stat
// @Success 200 {object} HttpResponse{data=ListData{list=[]model.StatAssetType}}
// @Router /stat/assettype [get]
func (c *Controller) StatAssetType(ctx *gin.Context) {
stat := make([]*model.StatAssetType, 0)
key := "stat-assettype"
if redis.Get(ctx, key, stat) == nil {
ctx.JSON(http.StatusOK, NewHttpResponseWithData(toListData(stat)))
return
}
t := mysql.DB.
Model(&model.Asset{}).
Raw(`
WITH RECURSIVE cte AS(
SELECT parent_id
FROM asset
WHERE deleted_at = 0
UNION ALL
SELECT t.parent_id
FROM cte
INNER JOIN node t on cte.parent_id = t.id
WHERE deleted_at = 0
)
SELECT
parent_id,
COUNT(*) AS count
FROM cte
GROUP BY parent_id
`)
err := mysql.DB.
Model(&model.Node{}).
Select("node.name, t.count").
Joins("LEFT JOIN (?) t ON node.id = t.parent_id", t).
Where("node.parent_id = 0").
Find(&stat).
Error
if err != nil {
ctx.AbortWithError(http.StatusInternalServerError, err)
return
}
redis.SetEx(ctx, key, stat, time.Minute)
ctx.JSON(http.StatusOK, NewHttpResponseWithData(toListData(stat)))
}
// StatCount godoc
//
// @Tags stat
// @Success 200 {object} HttpResponse{data=model.StatCount}
// @Router /stat/count [get]
func (c *Controller) StatCount(ctx *gin.Context) {
stat := &model.StatCount{}
key := "stat-count"
if redis.Get(ctx, key, stat) == nil {
ctx.JSON(http.StatusOK, NewHttpResponseWithData(stat))
return
}
eg := &errgroup.Group{}
eg.Go(func() error {
return mysql.DB.
Model(&model.Session{}).
Select("COUNT(DISTINCT asset_id, account_id) as connect, COUNT(DISTINCT uid) as user, COUNT(DISTINCT gateway_id) as gateway, COUNT(*) as session").
Where("status = 1").
First(&stat).
Error
})
eg.Go(func() error {
return mysql.DB.Model(&model.Asset{}).Count(&stat.TotalAsset).Error
})
eg.Go(func() error {
return mysql.DB.Model(&model.Asset{}).Where("connectable = 1").Count(&stat.Asset).Error
})
eg.Go(func() error {
return mysql.DB.Model(&model.Gateway{}).Count(&stat.TotalGateway).Error
})
if err := eg.Wait(); err != nil {
ctx.AbortWithError(http.StatusInternalServerError, err)
return
}
stat.Gateway = lo.Ternary(stat.Gateway <= stat.TotalGateway, stat.Gateway, stat.TotalGateway)
redis.SetEx(ctx, key, stat, time.Minute)
ctx.JSON(http.StatusOK, NewHttpResponseWithData(stat))
}
// StatAccount godoc
//
// @Tags stat
// @Param type query string true "account name" Enums(day, week, month)
// @Success 200 {object} HttpResponse{data=ListData{list=[]model.StatAccount}}
// @Router /stat/account [get]
func (c *Controller) StatAccount(ctx *gin.Context) {
start, end := time.Now(), time.Now()
switch ctx.Query("type") {
case "day":
start = start.Add(-time.Hour * 24)
case "week":
start = start.Add(-time.Hour * 24 * 7)
case "month":
start = start.Add(-time.Hour * 24 * 30)
default:
ctx.AbortWithError(http.StatusBadRequest, fmt.Errorf("wrong time range %s", ctx.Query("type")))
return
}
stat := make([]*model.StatAccount, 0)
key := "stat-account-" + ctx.Query("type")
if redis.Get(ctx, key, stat) == nil {
ctx.JSON(http.StatusOK, NewHttpResponseWithData(toListData(stat)))
return
}
err := mysql.DB.
Model(&model.Account{}).
Select("account.name, COUNT(*) AS count").
Joins("LEFT JOIN session ON account.id = session.account_id").
Group("account.id").
Order("count DESC").
Limit(10).
Where("session.created_at >= ? AND session.created_at <= ?", start, end).
Find(&stat).
Error
if err != nil {
ctx.AbortWithError(http.StatusInternalServerError, err)
return
}
redis.SetEx(ctx, key, stat, time.Minute)
ctx.JSON(http.StatusOK, NewHttpResponseWithData(toListData(stat)))
}
// StatAsset godoc
//
// @Tags stat
// @Param type query string true "account name" Enums(day, week, month)
// @Success 200 {object} HttpResponse{data=ListData{list=[]model.StatAsset}}
// @Router /stat/asset [get]
func (c *Controller) StatAsset(ctx *gin.Context) {
start, end := time.Now(), time.Now()
interval := time.Hour * 24
dateFmt := "%Y-%m-%d"
timeFmt := time.DateOnly
switch ctx.Query("type") {
case "day":
start = start.Add(-time.Hour * 24)
interval = time.Hour
dateFmt = "%Y-%m-%d %H:00:00"
timeFmt = time.DateTime
case "week":
start = start.Add(-time.Hour * 24 * 7)
case "month":
start = start.Add(-time.Hour * 24 * 30)
default:
ctx.AbortWithError(http.StatusBadRequest, fmt.Errorf("wrong time range %s", ctx.Query("type")))
return
}
stat := make([]*model.StatAsset, 0)
key := "stat-asset-" + ctx.Query("type")
if redis.Get(ctx, key, stat) == nil {
ctx.JSON(http.StatusOK, NewHttpResponseWithData(toListData(stat)))
return
}
err := mysql.DB.
Model(&model.Session{}).
Select("COUNT(DISTINCT asset_id, uid) AS connect, COUNT(*) AS session, COUNT(DISTINCT asset_id) AS asset, COUNT(DISTINCT uid) AS user, DATE_FORMAT(created_at, ?) AS time", dateFmt).
Where("session.created_at >= ? AND session.created_at <= ?", start, end).
Group("time").
Find(&stat).
Error
if err != nil {
ctx.AbortWithError(http.StatusInternalServerError, err)
return
}
for ; !start.After(end); start = start.Add(interval) {
t := start.Truncate(interval).Format(timeFmt)
if lo.ContainsBy(stat, func(s *model.StatAsset) bool { return t == s.Time }) {
continue
}
stat = append(stat, &model.StatAsset{Time: t})
}
sort.Slice(stat, func(i, j int) bool { return stat[i].Time < stat[j].Time })
redis.SetEx(ctx, key, stat, time.Minute)
ctx.JSON(http.StatusOK, NewHttpResponseWithData(toListData(stat)))
}
// StatCountOfUser godoc
//
// @Tags stat
// @Success 200 {object} HttpResponse{data=model.StatCountOfUser}
// @Router /stat/count/ofuser [get]
func (c *Controller) StatCountOfUser(ctx *gin.Context) {
currentUser, _ := acl.GetSessionFromCtx(ctx)
stat := &model.StatCountOfUser{}
key := fmt.Sprintf("stat-count-%d-", currentUser.Uid)
if redis.Get(ctx, key, stat) == nil {
ctx.JSON(http.StatusOK, NewHttpResponseWithData(stat))
return
}
eg := &errgroup.Group{}
eg.Go(func() error {
return mysql.DB.
Model(&model.Session{}).
Select("COUNT(DISTINCT asset_id, account_id) as connect, COUNT(DISTINCT asset_id) as asset, COUNT(*) as session").
Where("status = 1").
Where("uid = ?", currentUser.Uid).
First(&stat).
Error
})
eg.Go(func() error {
isAdmin := acl.IsAdmin(currentUser)
db := mysql.DB.Model(&model.Asset{})
if !isAdmin {
authorizationResourceIds, err := GetAutorizationResourceIds(ctx)
if err != nil {
return err
}
db = db.Where("id IN (?)", mysql.DB.Model(&model.Authorization{}).Select("asset_id").Where("resource_id IN ?", authorizationResourceIds))
}
return db.Count(&stat.TotalAsset).Error
})
if err := eg.Wait(); err != nil {
ctx.AbortWithError(http.StatusInternalServerError, err)
return
}
redis.SetEx(ctx, key, stat, time.Minute)
ctx.JSON(http.StatusOK, NewHttpResponseWithData(stat))
}
// StatRankOfUser godoc
//
// @Tags stat
// @Success 200 {object} HttpResponse{data=ListData{list=[]model.StatAsset}}
// @Router /stat/rank/ofuser [get]
func (c *Controller) StatRankOfUser(ctx *gin.Context) {
stat := make([]*model.StatRankOfUser, 0)
key := "stat-rank-user"
if redis.Get(ctx, key, stat) == nil {
ctx.JSON(http.StatusOK, NewHttpResponseWithData(toListData(stat)))
return
}
if err := mysql.DB.
Model(&model.Session{}).
Select("uid, COUNT(*) AS count, MAX(created_at) AS last_time").
Group("uid").
Order("count DESC").
Limit(3).
Find(&stat).
Error; err != nil {
ctx.AbortWithError(http.StatusInternalServerError, err)
return
}
redis.SetEx(ctx, key, stat, time.Minute)
ctx.JSON(http.StatusOK, NewHttpResponseWithData(toListData(stat)))
}
func toListData[T any](data []T) *ListData {
return &ListData{
Count: int64(len(data)),
List: lo.Map(data, func(d T, _ int) any { return d }),
}
}

View File

@@ -0,0 +1,56 @@
package model
import (
"time"
"gorm.io/plugin/soft_delete"
)
type Account struct {
Id int `json:"id" gorm:"column:id;primarykey"`
Name string `json:"name" gorm:"column:name"`
AccountType int `json:"account_type" gorm:"column:account_type"`
Account string `json:"account" gorm:"column:account"`
Password string `json:"password" gorm:"column:password"`
Pk string `json:"pk" gorm:"column:pk"`
Phrase string `json:"phrase" gorm:"column:phrase"`
ResourceId int `json:"resource_id" gorm:"column:resource_id"`
CreatorId int `json:"creator_id" gorm:"column:creator_id"`
UpdaterId int `json:"updater_id" gorm:"column:updater_id"`
CreatedAt time.Time `json:"created_at" gorm:"column:created_at"`
UpdatedAt time.Time `json:"updated_at" gorm:"column:updated_at"`
DeletedAt soft_delete.DeletedAt `json:"-" gorm:"column:deleted_at"`
AssetCount int64 `json:"asset_count" gorm:"-"`
}
func (m *Account) TableName() string {
return "account"
}
func (m *Account) SetId(id int) {
m.Id = id
}
func (m *Account) SetCreatorId(creatorId int) {
m.CreatorId = creatorId
}
func (m *Account) SetUpdaterId(updaterId int) {
m.UpdaterId = updaterId
}
func (m *Account) SetResourceId(resourceId int) {
m.ResourceId = resourceId
}
func (m *Account) GetResourceId() int {
return m.ResourceId
}
func (m *Account) GetName() string {
return m.Name
}
func (m *Account) GetId() int {
return m.Id
}
type AccountCount struct {
Id int `json:"id" gorm:"id"`
Count int64 `json:"count" gorm:"count"`
}

View File

@@ -0,0 +1,72 @@
package model
import (
"time"
"gorm.io/plugin/soft_delete"
)
type Asset struct {
Id int `json:"id" gorm:"column:id;primarykey"`
Ciid int `json:"ci_id" gorm:"column:ci_id"`
Name string `json:"name" gorm:"column:name"`
Comment string `json:"comment" gorm:"column:comment"`
ParentId int `json:"parent_id" gorm:"column:parent_id"`
Ip string `json:"ip" gorm:"column:ip"`
Protocols Slice[string] `json:"protocols" gorm:"column:protocols"`
GatewayId int `json:"gateway_id" gorm:"column:gateway_id"`
Authorization Map[int, Slice[int]] `json:"authorization" gorm:"column:authorization"`
*AccessAuth `json:"access_auth" gorm:"column:access_auth"`
Connectable bool `json:"connectable" gorm:"column:connectable"`
NodeChain string `json:"node_chain" gorm:"-"`
ResourceId int `json:"resource_id" gorm:"column:resource_id"`
CreatorId int `json:"creator_id" gorm:"column:creator_id"`
UpdaterId int `json:"updater_id" gorm:"column:updater_id"`
CreatedAt time.Time `json:"created_at" gorm:"column:created_at"`
UpdatedAt time.Time `json:"updated_at" gorm:"column:updated_at"`
DeletedAt soft_delete.DeletedAt `json:"-" gorm:"column:deleted_at"`
}
type AccessAuth struct {
Start *time.Time `json:"start,omitempty" gorm:"column:start"`
End *time.Time `json:"end,omitempty" gorm:"column:end"`
CmdIds Slice[int] `json:"cmd_ids" gorm:"column:cmd_ids"`
Ranges Slice[Range] `json:"ranges" gorm:"column:ranges"`
Allow bool `json:"allow" gorm:"column:allow"`
}
type Range struct {
Week int `json:"week" gorm:"column:week"`
Times Slice[string] `json:"times" gorm:"column:times"`
}
func (m *Asset) TableName() string {
return "asset"
}
func (m *Asset) SetId(id int) {
m.Id = id
}
func (m *Asset) SetCreatorId(creatorId int) {
m.CreatorId = creatorId
}
func (m *Asset) SetUpdaterId(updaterId int) {
m.UpdaterId = updaterId
}
func (m *Asset) SetResourceId(resourceId int) {
m.ResourceId = resourceId
}
func (m *Asset) GetResourceId() int {
return m.ResourceId
}
func (m *Asset) GetName() string {
return m.Name
}
func (m *Asset) GetId() int {
return m.Id
}
type AssetNodeChain struct {
NodeId int `gorm:"column:id"`
Chain string `gorm:"column:chain"`
}

View File

@@ -0,0 +1,86 @@
package model
import (
"time"
"gorm.io/plugin/soft_delete"
)
type Authorization struct {
Id int `json:"id" gorm:"column:id;primarykey"`
AssetId int `json:"asset_id" gorm:"column:asset_id"`
AccountId int `json:"account_id" gorm:"column:account_id"`
ResourceId int `json:"resource_id" gorm:"column:resource_id"`
CreatorId int `json:"creator_id" gorm:"column:creator_id"`
UpdaterId int `json:"updater_id" gorm:"column:updater_id"`
CreatedAt time.Time `json:"created_at" gorm:"column:created_at"`
UpdatedAt time.Time `json:"updated_at" gorm:"column:updated_at"`
DeletedAt soft_delete.DeletedAt `json:"-" gorm:"column:deleted_at"`
}
func (m *Authorization) TableName() string {
return "authorization"
}
type InfoModel interface {
GetId() int
}
type AssetInfo struct {
Id int `json:"id" gorm:"column:id;primarykey"`
Name string `json:"name" gorm:"column:name"`
Comment string `json:"comment" gorm:"column:comment"`
ParentId int `json:"parent_id" gorm:"column:parent_id"`
Ip string `json:"ip" gorm:"column:ip"`
Protocols Slice[string] `json:"protocols" gorm:"column:protocols"`
Connectable bool `json:"connectable" gorm:"column:connectable"`
NodeChain string `json:"node_chain" gorm:"-"`
*AccessAuth `json:"access_auth" gorm:"column:access_auth"`
Authorization Map[int, Slice[int]] `json:"-" gorm:"column:authorization"`
GatewayId int `json:"-" gorm:"column:gateway_id"`
Gateway *GatewayInfo `json:"gateway,omitempty" gorm:"-"`
Accounts []*AccountInfo `json:"accounts" gorm:"-"`
Commands []*CmdInfo `json:"commands" gorm:"-"`
}
func (m *AssetInfo) GetId() int {
return m.Id
}
type AccountInfo struct {
Id int `json:"id" gorm:"column:id;primarykey"`
Name string `json:"name" gorm:"column:name"`
Account string `json:"account" gorm:"column:account"`
AccountType int `json:"account_type,omitempty" gorm:"column:account_type"`
Password string `json:"password,omitempty" gorm:"column:password"`
}
func (m *AccountInfo) GetId() int {
return m.Id
}
type GatewayInfo struct {
Id int `json:"id" gorm:"column:id;primarykey"`
Name string `json:"name" gorm:"column:name"`
Host string `json:"host" gorm:"column:host"`
Port int `json:"port" gorm:"column:port"`
AccountType int `json:"account_type" gorm:"column:account_type"`
Account string `json:"account" gorm:"column:account"`
Password string `json:"password" gorm:"column:password"`
}
func (m *GatewayInfo) GetId() int {
return m.Id
}
type CmdInfo struct {
Id int `json:"id" gorm:"column:id;primarykey"`
Name string `json:"name" gorm:"column:name"`
Cmds Slice[string] `json:"cmds" gorm:"column:cmds"`
Enable int `json:"enable" gorm:"column:enable"`
}
func (m *CmdInfo) GetId() int {
return m.Id
}

View File

@@ -0,0 +1,46 @@
package model
import (
"time"
"gorm.io/plugin/soft_delete"
)
type Command struct {
Id int `json:"id" gorm:"column:id;primarykey"`
Name string `json:"name" gorm:"column:name"`
Cmds Slice[string] `json:"cmds" gorm:"column:cmds"`
Enable bool `json:"enable" gorm:"column:enable"`
ResourceId int `json:"resource_id" gorm:"column:resource_id"`
CreatorId int `json:"creator_id" gorm:"column:creator_id"`
UpdaterId int `json:"updater_id" gorm:"column:updater_id"`
CreatedAt time.Time `json:"created_at" gorm:"column:created_at"`
UpdatedAt time.Time `json:"updated_at" gorm:"column:updated_at"`
DeletedAt soft_delete.DeletedAt `json:"-" gorm:"column:deleted_at"`
}
func (m *Command) TableName() string {
return "command"
}
func (m *Command) SetId(id int) {
m.Id = id
}
func (m *Command) SetCreatorId(creatorId int) {
m.CreatorId = creatorId
}
func (m *Command) SetUpdaterId(updaterId int) {
m.UpdaterId = updaterId
}
func (m *Command) SetResourceId(resourceId int) {
m.ResourceId = resourceId
}
func (m *Command) GetResourceId() int {
return m.ResourceId
}
func (m *Command) GetName() string {
return m.Name
}
func (m *Command) GetId() int {
return m.Id
}

View File

@@ -0,0 +1,22 @@
package model
import (
"time"
"gorm.io/plugin/soft_delete"
)
type Config struct {
Id int `json:"id" gorm:"column:id;primarykey"`
Timeout int `json:"timeout" gorm:"column:timeout"`
CreatorId int `json:"creator_id" gorm:"column:creator_id"`
UpdaterId int `json:"updater_id" gorm:"column:updater_id"`
CreatedAt time.Time `json:"created_at" gorm:"column:created_at"`
UpdatedAt time.Time `json:"updated_at" gorm:"column:updated_at"`
DeletedAt soft_delete.DeletedAt `json:"-" gorm:"column:deleted_at"`
}
func (m *Config) TableName() string {
return "config"
}

View File

@@ -0,0 +1,58 @@
package model
import (
"time"
"gorm.io/plugin/soft_delete"
)
type Gateway struct {
Id int `json:"id" gorm:"column:id;primarykey"`
Name string `json:"name" gorm:"column:name"`
Host string `json:"host" gorm:"column:host"`
Port int `json:"port" gorm:"column:port"`
AccountType int `json:"account_type" gorm:"column:account_type"`
Account string `json:"account" gorm:"column:account"`
Password string `json:"password" gorm:"column:password"`
Pk string `json:"pk" gorm:"column:pk"`
Phrase string `json:"phrase" gorm:"column:phrase"`
ResourceId int `json:"resource_id" gorm:"column:resource_id"`
CreatorId int `json:"creator_id" gorm:"column:creator_id"`
UpdaterId int `json:"updater_id" gorm:"column:updater_id"`
CreatedAt time.Time `json:"created_at" gorm:"column:created_at"`
UpdatedAt time.Time `json:"updated_at" gorm:"column:updated_at"`
DeletedAt soft_delete.DeletedAt `json:"-" gorm:"column:deleted_at"`
AssetCount int64 `json:"asset_count" gorm:"-"`
}
func (m *Gateway) TableName() string {
return "gateway"
}
func (m *Gateway) SetId(id int) {
m.Id = id
}
func (m *Gateway) SetCreatorId(creatorId int) {
m.CreatorId = creatorId
}
func (m *Gateway) SetUpdaterId(updaterId int) {
m.UpdaterId = updaterId
}
func (m *Gateway) SetResourceId(resourceId int) {
m.ResourceId = resourceId
}
func (m *Gateway) GetResourceId() int {
return m.ResourceId
}
func (m *Gateway) GetName() string {
return m.Name
}
func (m *Gateway) GetId() int {
return m.Id
}
type GatewayCount struct {
Id int `gorm:"column:id"`
Count int64 `gorm:"column:count"`
}

View File

@@ -0,0 +1,22 @@
package model
import (
"time"
)
type History struct {
Id int `json:"id" gorm:"column:id;primarykey"`
RemoteIp string `json:"remote_ip" gorm:"column:remote_ip"`
Type string `json:"type" gorm:"column:type"`
TargetId int `json:"target_id" gorm:"column:target_id"`
ActionType int `json:"action_type" gorm:"column:action_type"`
Old Map[string, any] `json:"old" gorm:"column:old"`
New Map[string, any] `json:"new" gorm:"column:new"`
CreatorId int `json:"creator_id" gorm:"column:creator_id"`
CreatedAt time.Time `json:"created_at" gorm:"column:created_at"`
}
func (m *History) TableName() string {
return "history"
}

View File

@@ -0,0 +1,44 @@
package model
import (
"database/sql/driver"
"encoding/json"
)
const (
ACTION_CREATE = iota + 1
ACTION_DELETE
ACTION_UPDATE
)
type Slice[T int | string | Range] []T
func (s *Slice[T]) Scan(value any) error {
return json.Unmarshal(value.([]byte), s)
}
func (s Slice[T]) Value() (driver.Value, error) {
return json.Marshal(s)
}
type Map[K comparable, V any] map[K]V
func (m *Map[K, V]) Scan(value any) error {
return json.Unmarshal(value.([]byte), m)
}
func (m Map[K, V]) Value() (driver.Value, error) {
return json.Marshal(m)
}
type Model interface {
TableName() string
SetId(int)
SetCreatorId(int)
SetUpdaterId(int)
SetResourceId(int)
GetResourceId() int
GetId() int
GetName() string
}

View File

@@ -0,0 +1,67 @@
package model
import (
"time"
"gorm.io/plugin/soft_delete"
)
type Node struct {
Id int `json:"id" gorm:"column:id;primarykey"`
Name string `json:"name" gorm:"column:name"`
Comment string `json:"comment" gorm:"column:comment"`
ParentId int `json:"parent_id" gorm:"column:parent_id"`
Authorization Map[int, Slice[int]] `json:"authorization" gorm:"column:authorization"`
*AccessAuth `json:"access_auth" gorm:"column:access_auth"`
*Sync `json:"sync" gorm:"column:sync"`
Protocols Slice[string] `json:"protocols" gorm:"column:protocols"`
GatewayId int `json:"gateway_id" gorm:"column:gateway_id"`
// ResourceId int `json:"resource_id"`
CreatorId int `json:"creator_id" gorm:"column:creator_id"`
UpdaterId int `json:"updater_id" gorm:"column:updater_id"`
CreatedAt time.Time `json:"created_at" gorm:"column:created_at"`
UpdatedAt time.Time `json:"updated_at" gorm:"column:updated_at"`
DeletedAt soft_delete.DeletedAt `json:"-" gorm:"column:deleted_at"`
AssetCount int64 `json:"asset_count" gorm:"-"`
HasChild bool `json:"has_child" gorm:"-"`
}
type Sync struct {
TypeId int `json:"type_id,omitempty" gorm:"column:type_id"`
Mapping Map[string, string] `json:"mapping" gorm:"column:mapping"`
Filters string `json:"filters" gorm:"column:filters"`
Enable bool `json:"enable" gorm:"column:enable"`
Frequency float64 `json:"frequency" gorm:"column:frequency"`
}
func (m *Node) TableName() string {
return "node"
}
func (m *Node) SetId(id int) {
m.Id = id
}
func (m *Node) SetCreatorId(creatorId int) {
m.CreatorId = creatorId
}
func (m *Node) SetUpdaterId(updaterId int) {
m.UpdaterId = updaterId
}
func (m *Node) SetResourceId(resourceId int) {
}
func (m *Node) GetResourceId() int {
return 0
}
func (m *Node) GetName() string {
return m.Name
}
func (m *Node) GetId() int {
return m.Id
}
type NodeCount struct {
ParentId int `gorm:"column:parent_id"`
Count int64 `gorm:"column:count"`
}

View File

@@ -0,0 +1,69 @@
package model
import (
"time"
"gorm.io/plugin/soft_delete"
)
const (
AUTHMETHOD_PASSWORD = 1
AUTHMETHOD_PUBLICKEY = 2
)
type PublicKey struct {
Id int `json:"id" gorm:"column:id;primarykey"`
Uid int `json:"uid" gorm:"column:uid"`
UserName string `json:"username" gorm:"column:username"`
Name string `json:"name" gorm:"column:name"`
Mac string `json:"mac" gorm:"column:mac"`
Pk string `json:"pk" gorm:"column:pk"`
// ResourceId int `json:"resource_id"`
CreatorId int `json:"creator_id" gorm:"column:creator_id"`
UpdaterId int `json:"updater_id" gorm:"column:updater_id"`
CreatedAt time.Time `json:"created_at" gorm:"column:created_at"`
UpdatedAt time.Time `json:"updated_at" gorm:"column:updated_at"`
DeletedAt soft_delete.DeletedAt `json:"-" gorm:"column:deleted_at"`
}
func (m *PublicKey) TableName() string {
return "public_key"
}
func (m *PublicKey) SetId(id int) {
m.Id = id
}
func (m *PublicKey) SetCreatorId(creatorId int) {
m.CreatorId = creatorId
}
func (m *PublicKey) SetUpdaterId(updaterId int) {
m.UpdaterId = updaterId
}
func (m *PublicKey) SetResourceId(resourceId int) {
}
func (m *PublicKey) GetResourceId() int {
return 0
}
func (m *PublicKey) GetName() string {
return m.Name
}
func (m *PublicKey) GetId() int {
return m.Id
}
type ReqAuth struct {
Method int `json:"method"`
UserName string `json:"username"`
Password string `json:"password"`
Pk string `json:"pk"`
}
type UserInfoResp struct {
Result UserInfoRespResult `json:"result"`
}
type UserInfoRespResult struct {
Uid int `json:"uid"`
Rid int `json:"rid"`
}

View File

@@ -0,0 +1,115 @@
package model
import (
"bytes"
"io"
"sync"
"sync/atomic"
"time"
)
const (
SESSIONTYPE_WEB = iota + 1
SESSIONTYPE_CLIENT
)
const (
SESSIONSTATUS_ONLINE = iota + 1
SESSIONSTATUS_OFFLINE
)
const (
SESSIONACTION_NEW = iota + 1
SESSIONACTION_MONITOR
SESSIONACTION_CLOSE
)
type Session struct {
Id int `json:"id" gorm:"column:id;primarykey"`
SessionType int `json:"session_type" gorm:"column:session_type"`
SessionId string `json:"session_id" gorm:"column:session_id"`
Uid int `json:"uid" gorm:"column:uid"`
UserName string `json:"user_name" gorm:"column:user_name"`
AssetId int `json:"asset_id" gorm:"column:asset_id"`
AssetInfo string `json:"asset_info" gorm:"column:asset_info"`
AccountId int `json:"account_id" gorm:"column:account_id"`
AccountInfo string `json:"account_info" gorm:"column:account_info"`
GatewayId int `json:"gateway_id" gorm:"column:gateway_id"`
GatewayInfo string `json:"gateway_info" gorm:"column:gateway_info"`
ClientIp string `json:"client_ip" gorm:"column:client_ip"`
Protocol string `json:"protocol" gorm:"column:protocol"`
Status int `json:"status" gorm:"column:status"`
Duration int64 `json:"duration" gorm:"-"`
ClosedAt *time.Time `json:"closed_at" gorm:"column:closed_at"`
CreatedAt time.Time `json:"created_at" gorm:"column:created_at"`
UpdatedAt time.Time `json:"updated_at" gorm:"column:updated_at"`
CmdCount int64 `json:"cmd_count" gorm:"-"`
Monitors *sync.Map `json:"-" gorm:"-"`
Chans *SessionChans `json:"-" gorm:"-"`
Connected atomic.Bool `json:"-" gorm:"-"`
}
func (m *Session) TableName() string {
return "session"
}
type SessionCmd struct {
Id int `json:"id" gorm:"column:id;primarykey"`
SessionId string `json:"session_id" gorm:"column:session_id"`
Cmd string `json:"cmd" gorm:"column:cmd"`
Result string `json:"result" gorm:"column:result"`
Level int `json:"level" gorm:"column:level"`
CreatedAt time.Time `json:"created_at" gorm:"column:created_at"`
}
func (m *SessionCmd) TableName() string {
return "session_cmd"
}
type CmdCount struct {
SessionId string `gorm:"column:session_id"`
Count int64 `gorm:"column:count"`
}
type SessionOptionAsset struct {
Id int `json:"id" gorm:"column:id;primarykey"`
Name string `json:"name" gorm:"column:name"`
}
type SshReq struct {
Uid int `json:"uid"`
UserName string `json:"username"`
Cookie string `json:"cookie"`
AcceptLanguage string `json:"accept_language"`
ClientIp string `json:"client_ip"`
AssetId int `json:"asset_id"`
AccountId int `json:"account_id"`
Protocol string `json:"protocol"`
Action int `json:"action"`
SessionId string `json:"session_id"`
}
type SshResp struct {
Code int `json:"code"`
Message string `json:"message"`
SessionId string `json:"session_id"`
Uid int `json:"uid"`
UserName string `json:"username"`
}
type SessionChans struct {
Rin io.Reader
Win io.Writer
ErrChan chan error
RespChan chan *SshResp
InChan chan []byte
OutChan chan []byte
Buf *bytes.Buffer
WindowChan chan string
AwayChan chan struct{}
CloseChan chan string
}

View File

@@ -0,0 +1,47 @@
package model
import (
"time"
)
type StatAssetType struct {
Name string `json:"name" gorm:"column:name"`
Count int `json:"count" gorm:"column:count"`
}
type StatCount struct {
Connect int64 `json:"connect" gorm:"column:connect"`
Session int64 `json:"session" gorm:"column:session"`
Asset int64 `json:"asset" gorm:"column:asset"`
TotalAsset int64 `json:"total_asset" gorm:"column:total_asset"`
User int64 `json:"user" gorm:"column:user"`
// TotalUser int64 `json:"total_user"`
Gateway int64 `json:"gateway" gorm:"column:gateway"`
TotalGateway int64 `json:"total_gateway" gorm:"column:total_gateway"`
}
type StatAccount struct {
Name string `json:"name" gorm:"column:name"`
Count int `json:"count" gorm:"column:count"`
}
type StatAsset struct {
Connect int64 `json:"connect" gorm:"column:connect"`
Session int64 `json:"session" gorm:"column:session"`
Asset int64 `json:"asset" gorm:"column:asset"`
User int64 `json:"user" gorm:"column:user"`
Time string `json:"time" gorm:"column:time"`
}
type StatCountOfUser struct {
Connect int64 `json:"connect" gorm:"column:connect"`
Session int64 `json:"session" gorm:"column:session"`
Asset int64 `json:"asset" gorm:"column:asset"`
TotalAsset int64 `json:"total_asset" gorm:"column:total_asset"`
}
type StatRankOfUser struct {
Uid int `json:"uid" gorm:"column:uid"`
Count int64 `json:"count" gorm:"column:count"`
LastTime time.Time `json:"last_time" gorm:"column:last_time"`
}

View File

@@ -0,0 +1,89 @@
package remote
import (
"context"
"crypto/md5"
"encoding/hex"
"encoding/json"
"fmt"
"net/http"
"runtime"
"time"
"github.com/go-resty/resty/v2"
"github.com/spf13/cast"
"go.uber.org/zap"
"github.com/veops/oneterm/pkg/conf"
"github.com/veops/oneterm/pkg/logger"
"github.com/veops/oneterm/pkg/server/storage/cache/redis"
)
var (
RC = resty.NewWithClient(&http.Client{}).SetRetryCount(3)
)
func GetAclToken(ctx context.Context) (res string, err error) {
res, err = redis.RC.Get(ctx, "aclToken").Result()
if err == nil {
return
}
aclConfig := conf.Cfg.Auth.Acl
url := fmt.Sprintf("%s%s", aclConfig.Url, "/acl/apps/token")
secretHash := md5.Sum([]byte(aclConfig.SecretKey))
secretKey := hex.EncodeToString(secretHash[:])
data := make(map[string]string)
resp, err := RC.R().
SetBody(map[string]any{"app_id": aclConfig.AppId, "secret_key": secretKey}).
SetResult(&data).
Post(url)
if err = HandleErr(err, resp, func(dt map[string]any) bool { return dt["token"] != "" }); err != nil {
return
}
res = data["token"]
_, err = redis.RC.SetNX(ctx, "aclToken", res, time.Hour).Result()
return
}
func HandleErr(e error, resp *resty.Response, isOk func(dt map[string]any) bool) (err error) {
pc, _, _, _ := runtime.Caller(1)
defer func() {
if err != nil {
bs, _ := json.Marshal(resp.Request.Body)
logger.L.Error(fmt.Sprintf("%s failed", runtime.FuncForPC(pc).Name()), zap.String("url", resp.Request.URL), zap.String("req", string(bs)), zap.String("resp", resp.String()))
}
}()
err = e
if err != nil {
return err
}
dt := make(map[string]any)
err = json.Unmarshal(resp.Body(), &dt)
if err != nil {
return err
}
if resp.StatusCode() != 200 || (isOk != nil && !isOk(dt)) {
err = &RemoteError{
HttpCode: resp.StatusCode(),
Resp: dt,
}
return
}
return nil
}
type RemoteError struct {
HttpCode int
Resp map[string]any
}
func (r *RemoteError) Error() string {
return cast.ToString(r.Resp["message"])
}

View File

@@ -0,0 +1,98 @@
// Package middleware
package middleware
import (
"encoding/json"
"fmt"
"net/http"
"sync"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
"github.com/veops/oneterm/pkg/conf"
"github.com/veops/oneterm/pkg/logger"
"github.com/veops/oneterm/pkg/server/auth/acl"
)
var (
basicAuthDb = sync.Map{}
)
func init() {
basicAuthDb.Store("admin", "admin")
}
func Auth() gin.HandlerFunc {
return func(c *gin.Context) {
var err error
var ok bool
if conf.Cfg.Auth.Acl != nil && conf.Cfg.Auth.Acl.Url != "" {
err, ok = authAcl(c)
} else {
// TODO: add your auth here
ok = true
}
if !ok {
logger.L.Warn(err.Error())
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
"message": "authorized refused",
})
return
}
c.Next()
}
}
func AuthToken() gin.HandlerFunc {
return func(c *gin.Context) {
if c.GetHeader("X-Token") != conf.Cfg.SshServer.Xtoken {
logger.L.Warn("invalid token", zap.String("X-Token", c.GetHeader("X-Token")))
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
"message": "authorized refused",
})
return
}
c.Next()
}
}
func authAcl(ctx *gin.Context) (error, bool) {
session := &acl.Session{}
sess, err := ctx.Cookie("session")
if err == nil && sess != "" {
s := acl.NewSignature(conf.Cfg.SecretKey, "cookie-session", "", "hmac", nil, nil)
content, err := s.Unsign(sess)
if err != nil {
return err, false
}
err = json.Unmarshal(content, &session)
if err != nil {
return err, false
}
ctx.Set("session", session)
return nil, true
}
return fmt.Errorf("no session"), false
}
//func authBasic(ctx *gin.Context) (error, bool) {
// if user, password, ok := ctx.Request.BasicAuth(); ok {
// if p, ok := basicAuthDb.Load(user); ok && p.(string) == password {
// return nil, true
// } else {
// return fmt.Errorf("invalid user or password"), false
// }
// }
// return fmt.Errorf("invalid user or password"), false
//}
//func authWithWhiteList(ip string) bool {
// return lo.Contains(viper.GetStringSlice("gateway.whiteList"), ip)
//}

View File

@@ -0,0 +1,151 @@
package middleware
import (
"bytes"
"context"
"encoding/gob"
"fmt"
"net/http"
"time"
"github.com/gin-gonic/gin"
"golang.org/x/sync/singleflight"
"github.com/veops/oneterm/pkg/logger"
"github.com/veops/oneterm/pkg/server/storage/cache/redis"
)
var (
cachePrefix = "App::HttpCache"
)
type responseCache struct {
Status int
Header http.Header
Data []byte
}
func (c *responseCache) fillWithCacheWriter(cacheWriter *responseCacheWriter) {
c.Status = cacheWriter.Status()
c.Data = cacheWriter.body.Bytes()
c.Header = cacheWriter.Header().Clone()
}
// responseCacheWriter
type responseCacheWriter struct {
gin.ResponseWriter
body bytes.Buffer
}
func (w *responseCacheWriter) Write(b []byte) (int, error) {
w.body.Write(b)
return w.ResponseWriter.Write(b)
}
func replyWithCache(c *gin.Context, respCache *responseCache) {
c.Writer.WriteHeader(respCache.Status)
for key, values := range respCache.Header {
for _, val := range values {
c.Writer.Header().Set(key, val)
}
}
if _, err := c.Writer.Write(respCache.Data); err != nil {
logger.L.Error(err.Error())
}
c.Abort()
}
func RCache(ctx context.Context, defaultExpire time.Duration) gin.HandlerFunc {
sfGroup := singleflight.Group{}
return func(c *gin.Context) {
cacheKey := fmt.Sprintf("%s::%s", cachePrefix, c.Request.RequestURI)
cacheDuration := defaultExpire
// read cache first
{
respCache := &responseCache{}
err := RGet(ctx, cacheKey, &respCache)
if err == nil {
replyWithCache(c, respCache)
return
}
}
cacheWriter := &responseCacheWriter{ResponseWriter: c.Writer}
c.Writer = cacheWriter
inFlight := false
rawRespCache, _, _ := sfGroup.Do(cacheKey, func() (any, error) {
forgetTimer := time.AfterFunc(time.Second*15, func() {
sfGroup.Forget(cacheKey)
})
defer forgetTimer.Stop()
c.Next()
inFlight = true
respCache := &responseCache{}
respCache.fillWithCacheWriter(cacheWriter)
// only cache 2xx response
if !c.IsAborted() && cacheWriter.Status() < 300 && cacheWriter.Status() >= 200 {
if err := RSet(ctx, cacheKey, respCache, cacheDuration); err != nil {
logger.L.Error(err.Error())
}
}
return respCache, nil
})
if !inFlight {
replyWithCache(c, rawRespCache.(*responseCache))
return
}
}
}
// redis
func RSet(ctx context.Context, key string, value any, expire time.Duration) error {
payload, err := Serialize(value)
if err != nil {
return err
}
_, err = redis.RC.SetEx(ctx, key, payload, expire).Result()
return err
}
func RDelete(ctx context.Context, key string) error {
_, err := redis.RC.Del(ctx, key).Result()
return err
}
func RGet(ctx context.Context, key string, value any) error {
r, err := redis.RC.Get(ctx, key).Bytes()
if err != nil {
return err
}
return Deserialize(r, value)
}
// codec
func Serialize(value any) ([]byte, error) {
var b bytes.Buffer
encoder := gob.NewEncoder(&b)
if err := encoder.Encode(value); err != nil {
return nil, err
}
return b.Bytes(), nil
}
func Deserialize(byt []byte, ptr any) (err error) {
b := bytes.NewBuffer(byt)
decoder := gob.NewDecoder(b)
if err = decoder.Decode(ptr); err != nil {
return err
}
return nil
}

View File

@@ -0,0 +1,63 @@
package middleware
import (
"bytes"
"encoding/json"
"strings"
"github.com/gin-gonic/gin"
"github.com/nicksnyder/go-i18n/v2/i18n"
"github.com/veops/oneterm/pkg/conf"
"github.com/veops/oneterm/pkg/server/controller"
)
type bodyWriter struct {
gin.ResponseWriter
body *bytes.Buffer
}
func (w bodyWriter) Write(b []byte) (int, error) {
return w.body.Write(b)
}
func Error2Resp() gin.HandlerFunc {
return func(ctx *gin.Context) {
if strings.Contains(ctx.Request.URL.String(), "session/replay") {
ctx.Next()
return
}
wb := &bodyWriter{
body: &bytes.Buffer{},
ResponseWriter: ctx.Writer,
}
ctx.Writer = wb
ctx.Next()
obj := make(map[string]any)
json.Unmarshal(wb.body.Bytes(), &obj)
if len(ctx.Errors) > 0 {
if v, ok := obj["code"]; !ok || v == 0 {
obj["code"] = ctx.Writer.Status()
}
if v, ok := obj["message"]; !ok || v == "" {
e := ctx.Errors.Last().Err
obj["message"] = e.Error()
ae, ok := e.(*controller.ApiError)
if ok {
lang := ctx.PostForm("lang")
accept := ctx.GetHeader("Accept-Language")
localizer := i18n.NewLocalizer(conf.Bundle, lang, accept)
obj["message"] = ae.Message(localizer)
}
}
}
bs, _ := json.Marshal(obj)
wb.ResponseWriter.Write(bs)
}
}

View File

@@ -0,0 +1,137 @@
// Package middleware
package middleware
import (
"bytes"
"encoding/json"
"io"
"net"
"net/http"
"net/http/httputil"
"os"
"runtime/debug"
"strings"
"time"
"github.com/gin-gonic/gin"
"github.com/samber/lo"
"go.uber.org/zap"
"github.com/veops/oneterm/pkg/logger"
handler "github.com/veops/oneterm/pkg/server/controller"
)
var (
NotLogUrls = []string{"/favicon.ico"}
)
func GinLogger(logger *zap.Logger) gin.HandlerFunc {
return func(c *gin.Context) {
start := time.Now()
path := c.Request.URL.Path
query := c.Request.URL.RawQuery
c.Next()
if !lo.Contains(NotLogUrls, path) {
cost := time.Since(start)
logger.Info(path,
zap.Int("status", c.Writer.Status()),
zap.String("method", c.Request.Method),
zap.String("path", path),
zap.String("query", query),
zap.String("ip", c.ClientIP()),
zap.String("user-agent", c.Request.UserAgent()),
zap.String("errors", c.Errors.ByType(gin.ErrorTypePrivate).String()),
zap.Duration("cost", cost),
)
}
}
}
func GinRecovery(logger *zap.Logger, stack bool) gin.HandlerFunc {
return func(c *gin.Context) {
defer func() {
if err := recover(); err != nil {
var brokenPipe bool
if ne, ok := err.(*net.OpError); ok {
if se, ok := ne.Err.(*os.SyscallError); ok {
if strings.Contains(strings.ToLower(se.Error()), "broken pipe") ||
strings.Contains(strings.ToLower(se.Error()), "connection reset by peer") {
brokenPipe = true
}
}
}
httpRequest, _ := httputil.DumpRequest(c.Request, false)
if brokenPipe {
logger.Error(c.Request.URL.Path,
zap.Any("error", err),
zap.String("request", string(httpRequest)),
)
err := c.Error(err.(error))
if err != nil {
logger.Error(err.Error())
}
c.Abort()
return
}
if stack {
logger.Error("[Recovery from panic]",
zap.Any("error", err),
zap.String("request", string(httpRequest)),
zap.String("stack", string(debug.Stack())),
)
} else {
logger.Error("[Recovery from panic]",
zap.Any("error", err),
zap.String("request", string(httpRequest)),
)
}
c.AbortWithStatus(http.StatusInternalServerError)
}
}()
c.Next()
}
}
// LogRequest LogUpdate Record the specified HTTP request (including the URL and data) in a log or another location.
func LogRequest() gin.HandlerFunc {
return func(c *gin.Context) {
if !lo.Contains([]string{"GET", "HEAD"}, c.Request.Method) {
data := map[string]any{}
bodyBytes, _ := io.ReadAll(c.Request.Body)
_ = c.Request.Body.Close()
c.Request.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))
if err := json.Unmarshal(bodyBytes, &data); err != nil {
if valid, err := permissionCheck(c, data); err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest,
handler.HttpResponse{Code: http.StatusBadRequest, Message: err.Error()})
return
} else if !valid {
c.AbortWithStatusJSON(http.StatusBadRequest,
handler.HttpResponse{Code: http.StatusForbidden, Message: "no permission"})
return
}
}
excludeUrls := map[string]struct{}{}
if _, ok := excludeUrls[c.Request.RequestURI]; !ok {
if c.Request.Method != "POST" {
w := &responseWriter{ResponseWriter: c.Writer, body: []byte{}}
c.Writer = w
logger.L.Info("request record", zap.String("body", string(w.body)))
}
}
}
c.Next()
}
}
func permissionCheck(ctx *gin.Context, data map[string]any) (bool, error) {
return true, nil
}

View File

@@ -0,0 +1,62 @@
package middleware
import (
"net/http"
"runtime"
"github.com/gin-gonic/gin"
"github.com/veops/oneterm/pkg/logger"
)
func Cors() gin.HandlerFunc {
return func(c *gin.Context) {
method := c.Request.Method
origin := c.Request.Header.Get("Origin")
if origin != "" {
c.Writer.Header().Set("Access-Control-Allow-Origin", origin)
c.Header("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE, UPDATE")
c.Header("Access-Control-Allow-Headers", "Authorization, Content-Length, X-CSRF-Token, Token, session")
c.Header("Access-Control-Expose-Headers", "Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers")
c.Header("Access-Control-Max-Age", "172800")
c.Header("Access-Control-Allow-Credentials", "true")
}
if method == "OPTIONS" {
c.AbortWithStatus(http.StatusNoContent)
return
}
defer func() {
if err := recover(); err != nil {
logger.L.Sugar().Errorf("Panic info is: %v", err)
}
}()
c.Next()
}
}
func RecoveryWithWriter() gin.HandlerFunc {
return func(c *gin.Context) {
defer func() {
if err := recover(); err != nil {
var buf [4096]byte
n := runtime.Stack(buf[:], false)
logger.L.Error(string(buf[:n]))
}
}()
c.Next()
}
}
type responseWriter struct {
gin.ResponseWriter
body []byte
}
func (w *responseWriter) Write(data []byte) (int, error) {
w.body = append(w.body, data...)
return w.ResponseWriter.Write(data)
}

View File

@@ -0,0 +1,119 @@
package router
import (
"context"
"encoding/gob"
"fmt"
"net/http"
"os"
"time"
"github.com/gin-contrib/sessions"
"github.com/gin-contrib/sessions/cookie"
"github.com/gin-gonic/gin"
"github.com/prometheus/client_golang/prometheus/promhttp"
"github.com/spf13/viper"
swaggerFiles "github.com/swaggo/files"
ginSwagger "github.com/swaggo/gin-swagger"
"github.com/veops/oneterm/docs"
"github.com/veops/oneterm/pkg/conf"
"github.com/veops/oneterm/pkg/logger"
"github.com/veops/oneterm/pkg/server/router/middleware"
"github.com/veops/oneterm/pkg/util"
)
var routeGroup []*GroupRoute
func Server(cfg *conf.ConfigYaml) *http.Server {
routeConfig()
srv := &http.Server{
Addr: fmt.Sprintf("%s:%d", cfg.Http.Host, cfg.Http.Port),
Handler: setupRouter(),
}
go func() {
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
logger.L.Error(err.Error())
os.Exit(1)
}
}()
logger.L.Info(fmt.Sprintf("start on server:%s", srv.Addr))
return srv
}
func GracefulExit(srv *http.Server, ch chan struct{}) {
<-ch
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
defer cancel()
if err := srv.Shutdown(ctx); err != nil {
logger.L.Error(err.Error())
}
logger.L.Info("Shutdown server ...")
}
func routeConfig() {
var commonRoute []Route
commonRoute = append(commonRoute, routes...)
routeGroup = []*GroupRoute{
{
Prefix: "/api/oneterm/v1",
GroupMiddleware: gin.HandlersChain{
middleware.Error2Resp(),
middleware.RecoveryWithWriter(),
},
SubRoutes: commonRoute,
},
{
Prefix: "",
SubRoutes: baseRoutes,
},
}
}
func setupRouter() *gin.Engine {
r := gin.New()
r.Use(
middleware.GinLogger(logger.L),
middleware.LogRequest(),
middleware.Cors(),
middleware.GinRecovery(logger.L, true))
// sso
gob.Register(map[string]any{}) // important!
store := cookie.NewStore([]byte(viper.GetString("gateway.secretKey")))
r.Use(sessions.Sessions("session", store))
routeGroupsMap := make(map[string]*gin.RouterGroup)
for _, gRoute := range routeGroup {
if _, ok := routeGroupsMap[gRoute.Prefix]; !ok {
routeGroupsMap[gRoute.Prefix] = r.Group(gRoute.Prefix)
}
for _, gMiddleware := range gRoute.GroupMiddleware {
routeGroupsMap[gRoute.Prefix].Use(gMiddleware)
}
for _, subRoute := range gRoute.SubRoutes {
length := len(subRoute.Middleware) + 2
routes := make([]any, length)
routes[0] = subRoute.Pattern
for i, v := range subRoute.Middleware {
routes[i+1] = v
}
routes[length-1] = subRoute.HandlerFunc
util.CallReflect(
routeGroupsMap[gRoute.Prefix],
subRoute.Method,
routes...)
}
}
r.Handle("GET", "/metrics", gin.WrapH(promhttp.Handler()))
// swagger
docs.SwaggerInfo.Title = "ONETERM API"
docs.SwaggerInfo.BasePath = "/api/oneterm/v1"
r.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
return r
}

View File

@@ -0,0 +1,411 @@
package router
import (
"net/http"
"github.com/gin-gonic/gin"
"github.com/veops/oneterm/pkg/logger"
"github.com/veops/oneterm/pkg/server/controller"
"github.com/veops/oneterm/pkg/server/router/middleware"
)
var (
c = controller.NewController()
baseRoutes = []Route{
{
Name: "a health check, just for monitoring",
Method: "GET",
Pattern: "/-/health",
HandlerFunc: func(ctx *gin.Context) {
ctx.String(http.StatusOK, "OK")
},
},
{
Name: "favicon.ico",
Method: "GET",
Pattern: "/favicon.ico",
HandlerFunc: func(ctx *gin.Context) {
},
},
{
Name: "change the log level",
Method: "PUT",
Pattern: "/-/log/level",
HandlerFunc: func(ctx *gin.Context) {
logger.AtomicLevel.ServeHTTP(ctx.Writer, ctx.Request)
},
},
}
routes = []Route{
// account
{
Name: "create a account",
Method: "POST",
Pattern: "account",
HandlerFunc: c.CreateAccount,
Middleware: gin.HandlersChain{middleware.Auth()},
},
{
Name: "delete a account",
Method: "DELETE",
Pattern: "account/:id",
HandlerFunc: c.DeleteAccount,
Middleware: gin.HandlersChain{middleware.Auth()},
},
{
Name: "update a account",
Method: "PUT",
Pattern: "account/:id",
HandlerFunc: c.UpdateAccount,
Middleware: gin.HandlersChain{middleware.Auth()},
},
{
Name: "query accounts",
Method: "GET",
Pattern: "account",
HandlerFunc: c.GetAccounts,
Middleware: gin.HandlersChain{middleware.Auth()},
},
// asset
{
Name: "create a asset",
Method: "POST",
Pattern: "asset",
HandlerFunc: c.CreateAsset,
Middleware: gin.HandlersChain{middleware.Auth()},
},
{
Name: "delete a asset",
Method: "DELETE",
Pattern: "asset/:id",
HandlerFunc: c.DeleteAsset,
Middleware: gin.HandlersChain{middleware.Auth()},
},
{
Name: "update a asset",
Method: "PUT",
Pattern: "asset/:id",
HandlerFunc: c.UpdateAsset,
Middleware: gin.HandlersChain{middleware.Auth()},
},
{
Name: "query assets",
Method: "GET",
Pattern: "asset",
HandlerFunc: c.GetAssets,
Middleware: gin.HandlersChain{middleware.Auth()},
},
{
Name: "update asset by server",
Method: "PUT",
Pattern: "asset/update_by_server",
HandlerFunc: c.UpdateByServer,
Middleware: gin.HandlersChain{middleware.AuthToken()},
},
{
Name: "query asset by server",
Method: "GET",
Pattern: "asset/query_by_server",
HandlerFunc: c.QueryByServer,
Middleware: gin.HandlersChain{middleware.AuthToken()},
},
// command
{
Name: "create a command",
Method: "POST",
Pattern: "command",
HandlerFunc: c.CreateCommand,
Middleware: gin.HandlersChain{middleware.Auth()},
},
{
Name: "delete a command",
Method: "DELETE",
Pattern: "command/:id",
HandlerFunc: c.DeleteCommand,
Middleware: gin.HandlersChain{middleware.Auth()},
},
{
Name: "update a command",
Method: "PUT",
Pattern: "command/:id",
HandlerFunc: c.UpdateCommand,
Middleware: gin.HandlersChain{middleware.Auth()},
},
{
Name: "query commands",
Method: "GET",
Pattern: "command",
HandlerFunc: c.GetCommands,
Middleware: gin.HandlersChain{middleware.Auth()},
},
{
Name: "modify config",
Method: "POST",
Pattern: "config",
HandlerFunc: c.PostConfig,
Middleware: gin.HandlersChain{middleware.Auth()},
},
{
Name: "query config",
Method: "GET",
Pattern: "config",
HandlerFunc: c.GetConfig,
Middleware: gin.HandlersChain{middleware.Auth()},
},
// gateway
{
Name: "create a gateway",
Method: "POST",
Pattern: "gateway",
HandlerFunc: c.CreateGateway,
Middleware: gin.HandlersChain{middleware.Auth()},
},
{
Name: "delete a gateway",
Method: "DELETE",
Pattern: "gateway/:id",
HandlerFunc: c.DeleteGateway,
Middleware: gin.HandlersChain{middleware.Auth()},
},
{
Name: "update a gateway",
Method: "PUT",
Pattern: "gateway/:id",
HandlerFunc: c.UpdateGateway,
Middleware: gin.HandlersChain{middleware.Auth()},
},
{
Name: "query gateways",
Method: "GET",
Pattern: "gateway",
HandlerFunc: c.GetGateways,
Middleware: gin.HandlersChain{middleware.Auth()},
},
// node
{
Name: "create a node",
Method: "POST",
Pattern: "node",
HandlerFunc: c.CreateNode,
Middleware: gin.HandlersChain{middleware.Auth()},
},
{
Name: "delete a node",
Method: "DELETE",
Pattern: "node/:id",
HandlerFunc: c.DeleteNode,
Middleware: gin.HandlersChain{middleware.Auth()},
},
{
Name: "update a node",
Method: "PUT",
Pattern: "node/:id",
HandlerFunc: c.UpdateNode,
Middleware: gin.HandlersChain{middleware.Auth()},
},
{
Name: "query nodes",
Method: "GET",
Pattern: "node",
HandlerFunc: c.GetNodes,
Middleware: gin.HandlersChain{middleware.Auth()},
},
// publicKey
{
Name: "create a publicKey",
Method: "POST",
Pattern: "public_key",
HandlerFunc: c.CreatePublicKey,
Middleware: gin.HandlersChain{middleware.Auth()},
},
{
Name: "delete a publicKey",
Method: "DELETE",
Pattern: "public_key/:id",
HandlerFunc: c.DeletePublicKey,
Middleware: gin.HandlersChain{middleware.Auth()},
},
{
Name: "update a publicKey",
Method: "PUT",
Pattern: "public_key/:id",
HandlerFunc: c.UpdatePublicKey,
Middleware: gin.HandlersChain{middleware.Auth()},
},
{
Name: "query publicKeys",
Method: "GET",
Pattern: "public_key",
HandlerFunc: c.GetPublicKeys,
Middleware: gin.HandlersChain{middleware.Auth()},
},
{
Name: "auth by publicKey or password",
Method: "POST",
Pattern: "public_key/auth",
HandlerFunc: c.Auth,
Middleware: gin.HandlersChain{middleware.AuthToken()},
},
//stat
{
Name: "query stat asset type",
Method: "GET",
Pattern: "stat/assettype",
HandlerFunc: c.StatAssetType,
Middleware: gin.HandlersChain{middleware.Auth()},
},
{
Name: "query stat count",
Method: "GET",
Pattern: "stat/count",
HandlerFunc: c.StatCount,
Middleware: gin.HandlersChain{middleware.Auth()},
},
{
Name: "query stat count of user",
Method: "GET",
Pattern: "stat/count/ofuser",
HandlerFunc: c.StatCountOfUser,
Middleware: gin.HandlersChain{middleware.Auth()},
},
{
Name: "query stat account",
Method: "GET",
Pattern: "stat/account",
HandlerFunc: c.StatAccount,
Middleware: gin.HandlersChain{middleware.Auth()},
},
{
Name: "query stat asset",
Method: "GET",
Pattern: "stat/asset",
HandlerFunc: c.StatAsset,
Middleware: gin.HandlersChain{middleware.Auth()},
},
{
Name: "query stat rank of user",
Method: "GET",
Pattern: "stat/rank/ofuser",
HandlerFunc: c.StatRankOfUser,
Middleware: gin.HandlersChain{middleware.Auth()},
},
//session
{
Name: "query session",
Method: "GET",
Pattern: "session",
HandlerFunc: c.GetSessions,
Middleware: gin.HandlersChain{middleware.Auth()},
},
{
Name: "query session cmds",
Method: "GET",
Pattern: "session/:session_id/cmd",
HandlerFunc: c.GetSessionCmds,
Middleware: gin.HandlersChain{middleware.Auth()},
},
{
Name: "query session option asset",
Method: "GET",
Pattern: "session/option/asset",
HandlerFunc: c.GetSessionOptionAsset,
Middleware: gin.HandlersChain{middleware.Auth()},
},
{
Name: "query session option client ip",
Method: "GET",
Pattern: "session/option/clientip",
HandlerFunc: c.GetSessionOptionClientIp,
Middleware: gin.HandlersChain{middleware.Auth()},
},
{
Name: "query session replay",
Method: "GET",
Pattern: "session/replay/:session_id",
HandlerFunc: c.GetSessionReplay,
Middleware: gin.HandlersChain{middleware.Auth()},
},
{
Name: "create sesssin replay",
Method: "POST",
Pattern: "session/replay/:session_id",
HandlerFunc: c.CreateSessionReplay,
Middleware: gin.HandlersChain{middleware.AuthToken()},
},
{
Name: "upsert session",
Method: "POST",
Pattern: "session",
HandlerFunc: c.UpsertSession,
Middleware: gin.HandlersChain{middleware.AuthToken()},
},
{
Name: "create sesssin cmd",
Method: "POST",
Pattern: "session/cmd",
HandlerFunc: c.CreateSessionCmd,
Middleware: gin.HandlersChain{middleware.AuthToken()},
},
//history
{
Name: "query history",
Method: "GET",
Pattern: "history",
HandlerFunc: c.GetHistories,
Middleware: gin.HandlersChain{middleware.Auth()},
},
{
Name: "query history type mapping",
Method: "GET",
Pattern: "history/type/mapping",
HandlerFunc: c.GetHistoryTypeMapping,
Middleware: gin.HandlersChain{middleware.Auth()},
},
//connect
{
Name: "connect",
Method: "POST",
Pattern: "connect/:asset_id/:account_id/:protocol",
HandlerFunc: c.Connect,
Middleware: gin.HandlersChain{middleware.Auth()},
},
{
Name: "connecting",
Method: "GET",
Pattern: "connect/:session_id",
HandlerFunc: c.Connecting,
Middleware: gin.HandlersChain{middleware.Auth()},
},
{
Name: "connect monitor",
Method: "GET",
Pattern: "connect/monitor/:session_id",
HandlerFunc: c.ConnectMonitor,
Middleware: gin.HandlersChain{middleware.Auth()},
},
{
Name: "connect close",
Method: "POST",
Pattern: "connect/close/:session_id",
HandlerFunc: c.ConnectClose,
Middleware: gin.HandlersChain{middleware.Auth()},
},
}
)
type Route struct {
Name string
Method string
Pattern string
HandlerFunc func(ctx *gin.Context)
Middleware []gin.HandlerFunc
}
type GroupRoute struct {
Prefix string
GroupMiddleware gin.HandlersChain
SubRoutes []Route
}

View File

@@ -0,0 +1,21 @@
package local
import (
"context"
"time"
"github.com/allegro/bigcache/v3"
)
var (
// LC local cache client
LC *bigcache.BigCache
)
func Init() error {
var err error
if LC, err = bigcache.New(context.Background(), bigcache.DefaultConfig(time.Minute*10)); err != nil {
return err
}
return nil
}

View File

@@ -0,0 +1,56 @@
package redis
import (
"context"
"encoding/json"
"time"
"github.com/redis/go-redis/v9"
"github.com/veops/oneterm/pkg/conf"
)
var (
// RC redis cache client
RC *redis.Client
)
func Init(cfg *conf.RedisConfig) (err error) {
if cfg == nil {
return
}
ctx := context.Background()
readTimeout := time.Duration(30) * time.Second
writeTimeout := time.Duration(30) * time.Second
RC = redis.NewClient(&redis.Options{
Addr: cfg.Addr,
DB: cfg.Db,
Password: cfg.Password,
PoolSize: cfg.PoolSize,
MaxIdleConns: cfg.MaxIdle,
ReadTimeout: readTimeout,
WriteTimeout: writeTimeout,
})
if _, err = RC.Ping(ctx).Result(); err != nil {
return err
}
return nil
}
func Get(ctx context.Context, key string, dst any) (err error) {
bs, err := RC.Get(ctx, key).Bytes()
if err != nil {
return
}
return json.Unmarshal(bs, dst)
}
func SetEx(ctx context.Context, key string, src any, exp time.Duration) (err error) {
bs, err := json.Marshal(src)
if err != nil {
return
}
return RC.SetEx(ctx, key, bs, exp).Err()
}

View File

@@ -0,0 +1,27 @@
package mysql
import (
"fmt"
"gorm.io/driver/mysql"
"gorm.io/gorm"
"github.com/veops/oneterm/pkg/conf"
)
var (
DB *gorm.DB
)
func Init(cfg *conf.MysqlConfig) (err error) {
if cfg == nil {
return
}
dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/oneterm?charset=utf8mb4&parseTime=True&loc=Local", cfg.User, cfg.Password, cfg.Ip, cfg.Port)
if DB, err = gorm.Open(mysql.Open(dsn), &gorm.Config{}); err != nil {
return
}
return
}