mirror of
https://github.com/Sagit-chu/flvx.git
synced 2026-09-28 07:36:38 +08:00
2df061a19f
- Fix deadlock when updating tunnel entries with offline nodes - Add test file for tunnel entry SQLite operations - Update contract tests for entry port conflict and limiter sync - Add plan documents for SQLite deadlock fix and contract semantics
395 lines
11 KiB
Go
395 lines
11 KiB
Go
package repo
|
|
|
|
import (
|
|
"database/sql"
|
|
"errors"
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"gorm.io/gorm"
|
|
|
|
"go-backend/internal/store/model"
|
|
)
|
|
|
|
func (r *Repository) UserTunnelExistsByUserAndTunnel(userID, tunnelID int64) (bool, error) {
|
|
if r == nil || r.db == nil {
|
|
return false, errors.New("repository not initialized")
|
|
}
|
|
var count int64
|
|
err := r.db.Model(&model.UserTunnel{}).
|
|
Where("user_id = ? AND tunnel_id = ? AND status = 1", userID, tunnelID).
|
|
Count(&count).Error
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
return count > 0, nil
|
|
}
|
|
|
|
func (r *Repository) ListForwardsByTunnel(tunnelID int64) ([]model.ForwardRecord, error) {
|
|
if r == nil || r.db == nil {
|
|
return nil, errors.New("repository not initialized")
|
|
}
|
|
return r.ListForwardsByTunnelTx(r.db, tunnelID)
|
|
}
|
|
|
|
func (r *Repository) ListForwardsByTunnelTx(tx *gorm.DB, tunnelID int64) ([]model.ForwardRecord, error) {
|
|
if tx == nil {
|
|
return nil, errors.New("database unavailable")
|
|
}
|
|
var forwards []model.Forward
|
|
err := tx.Where("tunnel_id = ?", tunnelID).Order("id ASC").Find(&forwards).Error
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
rows := make([]model.ForwardRecord, 0, len(forwards))
|
|
for _, f := range forwards {
|
|
rows = append(rows, model.ForwardRecord{
|
|
ID: f.ID,
|
|
UserID: f.UserID,
|
|
UserName: f.UserName,
|
|
Name: f.Name,
|
|
TunnelID: f.TunnelID,
|
|
RemoteAddr: f.RemoteAddr,
|
|
Strategy: f.Strategy,
|
|
Status: f.Status,
|
|
SpeedID: f.SpeedID,
|
|
})
|
|
}
|
|
for i := range rows {
|
|
if strings.TrimSpace(rows[i].Strategy) == "" {
|
|
rows[i].Strategy = "fifo"
|
|
}
|
|
}
|
|
return rows, nil
|
|
}
|
|
|
|
func (r *Repository) ListActiveTunnelIDsByNode(nodeID int64) ([]int64, error) {
|
|
if r == nil || r.db == nil {
|
|
return nil, errors.New("repository not initialized")
|
|
}
|
|
var ids []int64
|
|
err := r.db.Model(&model.ChainTunnel{}).
|
|
Joins("JOIN tunnel ON tunnel.id = chain_tunnel.tunnel_id").
|
|
Where("chain_tunnel.node_id = ? AND tunnel.status = 1", nodeID).
|
|
Select("DISTINCT chain_tunnel.tunnel_id").
|
|
Order("chain_tunnel.tunnel_id ASC").
|
|
Pluck("chain_tunnel.tunnel_id", &ids).Error
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return ids, nil
|
|
}
|
|
|
|
func (r *Repository) ListActiveForwardIDsByNode(nodeID int64) ([]int64, error) {
|
|
if r == nil || r.db == nil {
|
|
return nil, errors.New("repository not initialized")
|
|
}
|
|
var ids []int64
|
|
err := r.db.Model(&model.ForwardPort{}).
|
|
Joins("JOIN forward ON forward.id = forward_port.forward_id").
|
|
Where("forward_port.node_id = ? AND forward.status = 1", nodeID).
|
|
Select("DISTINCT forward_port.forward_id").
|
|
Order("forward_port.forward_id ASC").
|
|
Pluck("forward_port.forward_id", &ids).Error
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return ids, nil
|
|
}
|
|
|
|
func (r *Repository) ListForwardPorts(forwardID int64) ([]model.ForwardPortRecord, error) {
|
|
if r == nil || r.db == nil {
|
|
return nil, errors.New("repository not initialized")
|
|
}
|
|
return r.ListForwardPortsTx(r.db, forwardID)
|
|
}
|
|
|
|
func (r *Repository) ListForwardPortsTx(tx *gorm.DB, forwardID int64) ([]model.ForwardPortRecord, error) {
|
|
if tx == nil {
|
|
return nil, errors.New("database unavailable")
|
|
}
|
|
var ports []model.ForwardPort
|
|
err := tx.Where("forward_id = ?", forwardID).Order("id ASC").Find(&ports).Error
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
rows := make([]model.ForwardPortRecord, 0, len(ports))
|
|
for _, p := range ports {
|
|
inIP := ""
|
|
if p.InIP.Valid {
|
|
inIP = p.InIP.String
|
|
}
|
|
rows = append(rows, model.ForwardPortRecord{NodeID: p.NodeID, Port: p.Port, InIP: inIP})
|
|
}
|
|
return rows, nil
|
|
}
|
|
|
|
func (r *Repository) HasOtherForwardOnNodePort(nodeID int64, port int, currentForwardID int64) (bool, error) {
|
|
if r == nil || r.db == nil {
|
|
return false, errors.New("repository not initialized")
|
|
}
|
|
return r.HasOtherForwardOnNodePortTx(r.db, nodeID, port, currentForwardID)
|
|
}
|
|
|
|
func (r *Repository) HasOtherForwardOnNodePortTx(tx *gorm.DB, nodeID int64, port int, currentForwardID int64) (bool, error) {
|
|
if tx == nil {
|
|
return false, errors.New("database unavailable")
|
|
}
|
|
if nodeID <= 0 || port <= 0 {
|
|
return false, nil
|
|
}
|
|
|
|
var count int64
|
|
err := tx.Model(&model.ForwardPort{}).
|
|
Where("node_id = ? AND port = ? AND forward_id <> ?", nodeID, port, currentForwardID).
|
|
Count(&count).Error
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
return count > 0, nil
|
|
}
|
|
|
|
func (r *Repository) GetTunnelOutProtocol(tunnelID int64) (string, error) {
|
|
if r == nil || r.db == nil {
|
|
return "", errors.New("repository not initialized")
|
|
}
|
|
var ct model.ChainTunnel
|
|
err := r.db.Select("protocol").
|
|
Where("tunnel_id = ? AND chain_type = ?", tunnelID, "3").
|
|
Order("id ASC").
|
|
Take(&ct).Error
|
|
if err != nil {
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return "", nil
|
|
}
|
|
return "", err
|
|
}
|
|
if ct.Protocol.Valid {
|
|
return ct.Protocol.String, nil
|
|
}
|
|
return "", nil
|
|
}
|
|
|
|
func (r *Repository) GetNodeRecord(nodeID int64) (*model.NodeRecord, error) {
|
|
if r == nil || r.db == nil {
|
|
return nil, errors.New("repository not initialized")
|
|
}
|
|
var n model.Node
|
|
err := r.db.Where("id = ?", nodeID).First(&n).Error
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return nil, nil
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return nodeRecordFromModel(&n), nil
|
|
}
|
|
|
|
func (r *Repository) GetNodeRecordTx(tx *gorm.DB, nodeID int64) (*model.NodeRecord, error) {
|
|
if tx == nil {
|
|
return nil, errors.New("database unavailable")
|
|
}
|
|
var n model.Node
|
|
err := tx.Where("id = ?", nodeID).First(&n).Error
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return nil, nil
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return nodeRecordFromModel(&n), nil
|
|
}
|
|
|
|
func nodeRecordFromModel(n *model.Node) *model.NodeRecord {
|
|
if n == nil {
|
|
return nil
|
|
}
|
|
rec := &model.NodeRecord{
|
|
ID: n.ID,
|
|
Name: n.Name,
|
|
ServerIP: n.ServerIP,
|
|
Status: n.Status,
|
|
PortRange: n.Port,
|
|
TCPListenAddr: n.TCPListenAddr, UDPListenAddr: n.UDPListenAddr,
|
|
IsRemote: n.IsRemote,
|
|
}
|
|
if n.ServerIPV4.Valid {
|
|
rec.ServerIPv4 = strings.TrimSpace(n.ServerIPV4.String)
|
|
}
|
|
if n.ServerIPV6.Valid {
|
|
rec.ServerIPv6 = strings.TrimSpace(n.ServerIPV6.String)
|
|
}
|
|
if n.ExtraIPs.Valid {
|
|
rec.ExtraIPs = strings.TrimSpace(n.ExtraIPs.String)
|
|
}
|
|
if n.InterfaceName.Valid {
|
|
rec.InterfaceName = strings.TrimSpace(n.InterfaceName.String)
|
|
}
|
|
if n.RemoteURL.Valid {
|
|
rec.RemoteURL = strings.TrimSpace(n.RemoteURL.String)
|
|
}
|
|
if n.RemoteToken.Valid {
|
|
rec.RemoteToken = strings.TrimSpace(n.RemoteToken.String)
|
|
}
|
|
if n.RemoteConfig.Valid {
|
|
rec.RemoteConfig = strings.TrimSpace(n.RemoteConfig.String)
|
|
}
|
|
if rec.TCPListenAddr == "" {
|
|
rec.TCPListenAddr = "[::]"
|
|
}
|
|
if rec.UDPListenAddr == "" {
|
|
rec.UDPListenAddr = "[::]"
|
|
}
|
|
if strings.TrimSpace(rec.Name) == "" {
|
|
rec.Name = fmt.Sprintf("node_%d", rec.ID)
|
|
}
|
|
return rec
|
|
}
|
|
|
|
func (r *Repository) ResolveUserTunnelAndLimiter(userID, tunnelID int64) (*model.UserTunnelLimiterInfo, error) {
|
|
if r == nil || r.db == nil {
|
|
return nil, errors.New("repository not initialized")
|
|
}
|
|
type row struct {
|
|
UserTunnelID int64 `gorm:"column:user_tunnel_id"`
|
|
LimiterID sql.NullInt64 `gorm:"column:limiter_id"`
|
|
Speed sql.NullInt64 `gorm:"column:speed"`
|
|
}
|
|
var rec row
|
|
err := r.db.Model(&model.UserTunnel{}).
|
|
Select("user_tunnel.id AS user_tunnel_id, speed_limit.id AS limiter_id, speed_limit.speed AS speed").
|
|
Joins("LEFT JOIN speed_limit ON speed_limit.id = user_tunnel.speed_id").
|
|
Where("user_tunnel.user_id = ? AND user_tunnel.tunnel_id = ?", userID, tunnelID).
|
|
Order("user_tunnel.id ASC").
|
|
Limit(1).
|
|
Take(&rec).Error
|
|
if err != nil {
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return &model.UserTunnelLimiterInfo{}, nil
|
|
}
|
|
return nil, err
|
|
}
|
|
info := &model.UserTunnelLimiterInfo{UserTunnelID: rec.UserTunnelID}
|
|
if rec.LimiterID.Valid && rec.LimiterID.Int64 > 0 {
|
|
v := rec.LimiterID.Int64
|
|
info.LimiterID = &v
|
|
s := int(rec.Speed.Int64)
|
|
info.Speed = &s
|
|
}
|
|
return info, nil
|
|
}
|
|
|
|
func (r *Repository) ListUserTunnelIDs(userID, tunnelID int64) ([]int64, error) {
|
|
if r == nil || r.db == nil {
|
|
return nil, errors.New("repository not initialized")
|
|
}
|
|
var ids []int64
|
|
err := r.db.Model(&model.UserTunnel{}).
|
|
Where("user_id = ? AND tunnel_id = ?", userID, tunnelID).
|
|
Order("id ASC").Pluck("id", &ids).Error
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return ids, nil
|
|
}
|
|
|
|
func (r *Repository) ListUserTunnelIDsByUser(userID int64) ([]int64, error) {
|
|
if r == nil || r.db == nil {
|
|
return nil, errors.New("repository not initialized")
|
|
}
|
|
var ids []int64
|
|
err := r.db.Model(&model.UserTunnel{}).
|
|
Where("user_id = ?", userID).
|
|
Order("id ASC").Pluck("id", &ids).Error
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return ids, nil
|
|
}
|
|
|
|
func (r *Repository) GetTunnelName(tunnelID int64) (string, error) {
|
|
if r == nil || r.db == nil {
|
|
return "", errors.New("repository not initialized")
|
|
}
|
|
var name string
|
|
err := r.db.Model(&model.Tunnel{}).Where("id = ?", tunnelID).Pluck("name", &name).Error
|
|
if err != nil {
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return "", nil
|
|
}
|
|
return "", err
|
|
}
|
|
return name, nil
|
|
}
|
|
|
|
func (r *Repository) ListChainNodesForTunnel(tunnelID int64) ([]model.ChainNodeRecord, error) {
|
|
if r == nil || r.db == nil {
|
|
return nil, errors.New("repository not initialized")
|
|
}
|
|
type row struct {
|
|
ChainType string
|
|
Inx sql.NullInt64
|
|
NodeID int64
|
|
Port sql.NullInt64
|
|
Name sql.NullString
|
|
Protocol sql.NullString
|
|
Strategy sql.NullString
|
|
ConnectIP sql.NullString
|
|
}
|
|
var rows []row
|
|
err := r.db.Model(&model.ChainTunnel{}).
|
|
Select("chain_tunnel.chain_type, chain_tunnel.inx, chain_tunnel.node_id, chain_tunnel.port, node.name, chain_tunnel.protocol, chain_tunnel.strategy, chain_tunnel.connect_ip").
|
|
Joins("LEFT JOIN node ON node.id = chain_tunnel.node_id").
|
|
Where("chain_tunnel.tunnel_id = ?", tunnelID).
|
|
Order("chain_tunnel.chain_type ASC, chain_tunnel.inx ASC, chain_tunnel.id ASC").
|
|
Find(&rows).Error
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
result := make([]model.ChainNodeRecord, 0, len(rows))
|
|
for _, row := range rows {
|
|
chainType := 0
|
|
if v := strings.TrimSpace(row.ChainType); v != "" {
|
|
if parsed, parseErr := strconv.Atoi(v); parseErr == nil {
|
|
chainType = parsed
|
|
}
|
|
}
|
|
inx := int64(0)
|
|
if row.Inx.Valid {
|
|
inx = row.Inx.Int64
|
|
}
|
|
port := 0
|
|
if row.Port.Valid {
|
|
port = int(row.Port.Int64)
|
|
}
|
|
item := model.ChainNodeRecord{
|
|
ChainType: chainType,
|
|
Inx: inx,
|
|
NodeID: row.NodeID,
|
|
Port: port,
|
|
}
|
|
if strings.TrimSpace(row.Name.String) == "" {
|
|
item.NodeName = fmt.Sprintf("node_%d", row.NodeID)
|
|
} else {
|
|
item.NodeName = row.Name.String
|
|
}
|
|
if strings.TrimSpace(row.Protocol.String) == "" {
|
|
item.Protocol = "tls"
|
|
} else {
|
|
item.Protocol = row.Protocol.String
|
|
}
|
|
if strings.TrimSpace(row.Strategy.String) == "" {
|
|
item.Strategy = "round"
|
|
} else {
|
|
item.Strategy = row.Strategy.String
|
|
}
|
|
if row.ConnectIP.Valid {
|
|
item.ConnectIP = row.ConnectIP.String
|
|
}
|
|
result = append(result, item)
|
|
}
|
|
return result, nil
|
|
}
|