Initial commit
This commit is contained in:
@@ -0,0 +1,284 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.zkcoi.com/zkcoi/meshray/internal/dnsprovider"
|
||||
"git.zkcoi.com/zkcoi/meshray/internal/model"
|
||||
"git.zkcoi.com/zkcoi/meshray/internal/store/sqlite"
|
||||
"github.com/google/uuid"
|
||||
"github.com/libdns/libdns"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// ServiceService 服务管理服务
|
||||
type ServiceService struct {
|
||||
store *sqlite.Store
|
||||
}
|
||||
|
||||
// NewServiceService 创建服务服务实例
|
||||
func NewServiceService(store *sqlite.Store) *ServiceService {
|
||||
return &ServiceService{store: store}
|
||||
}
|
||||
|
||||
// ListServices 获取服务列表(支持按类型筛选)
|
||||
func (s *ServiceService) ListServices(serverType string) ([]model.Service, error) {
|
||||
var services []model.Service
|
||||
query := s.store.DB().Order("created_at desc")
|
||||
if serverType != "" {
|
||||
// 支持大小写不敏感匹配
|
||||
query = query.Where("LOWER(type) = LOWER(?)", serverType)
|
||||
}
|
||||
err := query.Find(&services).Error
|
||||
return services, err
|
||||
}
|
||||
|
||||
// GetServiceByID 根据 ID 获取服务
|
||||
func (s *ServiceService) GetServiceByID(id string) (*model.Service, error) {
|
||||
var service model.Service
|
||||
if err := s.store.DB().Where("id = ?", id).First(&service).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, errors.New("服务不存在")
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return &service, nil
|
||||
}
|
||||
|
||||
// CreateService 创建服务
|
||||
func (s *ServiceService) CreateService(req *model.Service) (*model.Service, error) {
|
||||
if req.Name == "" {
|
||||
return nil, errors.New("服务名称不能为空")
|
||||
}
|
||||
if req.Type == "" {
|
||||
return nil, errors.New("服务类型不能为空")
|
||||
}
|
||||
|
||||
// 强制转换为大写以保持一致性
|
||||
req.Type = strings.ToUpper(req.Type)
|
||||
|
||||
// 校验支持的类型
|
||||
allowedTypes := map[string]bool{
|
||||
"STUN": true, "TURN": true, "DDNS": true, "TUN": true, "CUSTOM": true,
|
||||
}
|
||||
if !allowedTypes[req.Type] {
|
||||
return nil, fmt.Errorf("不支持的服务类型: %s", req.Type)
|
||||
}
|
||||
|
||||
// 地址和端口校验(除 TUN 外通常需要)
|
||||
if req.Type != "TUN" {
|
||||
if req.Address == "" {
|
||||
return nil, errors.New("服务器地址不能为空")
|
||||
}
|
||||
if req.Port <= 0 || req.Port > 65535 {
|
||||
return nil, errors.New("端口号无效")
|
||||
}
|
||||
}
|
||||
|
||||
// DDNS 鉴权校验(基础设施模式)
|
||||
if req.Type == "DDNS" && req.ConfigMode == "infrastructure" {
|
||||
if req.Provider == "" {
|
||||
return nil, errors.New("请选择 DNS 服务商")
|
||||
}
|
||||
if req.Domain == "" {
|
||||
return nil, errors.New("请输入根域名")
|
||||
}
|
||||
// 根据服务商校验认证信息
|
||||
switch req.Provider {
|
||||
case "cloudflare":
|
||||
if req.Token == "" {
|
||||
return nil, errors.New("请输入 API Token")
|
||||
}
|
||||
case "aliyun":
|
||||
if req.AuthUsername == "" || req.AuthPassword == "" {
|
||||
return nil, errors.New("请输入 AccessKey ID 和 Secret")
|
||||
}
|
||||
case "tencent":
|
||||
if req.AuthUsername == "" || req.AuthPassword == "" {
|
||||
return nil, errors.New("请输入 SecretId 和 SecretKey")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DDNS 全功能模式校验
|
||||
if req.Type == "DDNS" && req.ConfigMode == "fullservice" {
|
||||
if req.DDNSConfigID == "" {
|
||||
return nil, errors.New("请选择 DDNS 配置")
|
||||
}
|
||||
if req.RecordType == "" {
|
||||
return nil, errors.New("请选择记录类型")
|
||||
}
|
||||
// 根据记录类型校验字段
|
||||
switch req.RecordType {
|
||||
case "A", "AAAA":
|
||||
if req.Subdomain == "" {
|
||||
return nil, errors.New("请输入主机记录")
|
||||
}
|
||||
if req.TargetIP == "" {
|
||||
return nil, errors.New("请输入目标 IP")
|
||||
}
|
||||
if req.Port <= 0 {
|
||||
return nil, errors.New("请输入检测端口")
|
||||
}
|
||||
case "TXT":
|
||||
if req.TXTRecordName == "" {
|
||||
return nil, errors.New("请输入 TXT 记录名称")
|
||||
}
|
||||
if req.TXTValue == "" {
|
||||
return nil, errors.New("请输入 TXT 记录值")
|
||||
}
|
||||
case "CNAME":
|
||||
if req.CNAMETarget == "" {
|
||||
return nil, errors.New("请输入目标域名")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 生成 ID
|
||||
if req.ID == "" {
|
||||
req.ID = uuid.New().String()
|
||||
}
|
||||
|
||||
// 如果是 DDNS 全功能模式,先创建 DNS 记录
|
||||
if req.Type == "DDNS" && req.ConfigMode == "fullservice" {
|
||||
// 使用事务确保原子性
|
||||
tx := s.store.DB().Begin()
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
tx.Rollback()
|
||||
}
|
||||
}()
|
||||
|
||||
// 1. 获取关联的 DDNS 配置
|
||||
var ddnsConfig model.Service
|
||||
if err := tx.Where("id = ?", req.DDNSConfigID).First(&ddnsConfig).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return nil, fmt.Errorf("获取 DDNS 配置失败:%w", err)
|
||||
}
|
||||
|
||||
// 2. 确定记录类型、名称和值
|
||||
var recordType, name, value string
|
||||
switch req.RecordType {
|
||||
case "A", "AAAA":
|
||||
recordType = req.RecordType
|
||||
name = req.Subdomain
|
||||
value = req.TargetIP
|
||||
case "TXT":
|
||||
recordType = req.RecordType
|
||||
name = req.TXTRecordName
|
||||
value = req.TXTValue
|
||||
case "CNAME":
|
||||
recordType = req.RecordType
|
||||
name = req.Subdomain
|
||||
value = req.CNAMETarget
|
||||
default:
|
||||
tx.Rollback()
|
||||
return nil, errors.New("不支持的记录类型")
|
||||
}
|
||||
|
||||
// 3. 创建 DNS Provider
|
||||
providerConfig := dnsprovider.ProviderConfig{
|
||||
Provider: dnsprovider.ProviderType(ddnsConfig.Provider),
|
||||
Domain: ddnsConfig.Domain,
|
||||
APIToken: ddnsConfig.Token,
|
||||
AccessKeyID: ddnsConfig.AuthUsername,
|
||||
AccessKeySecret: ddnsConfig.AuthPassword,
|
||||
SecretId: ddnsConfig.AuthUsername,
|
||||
SecretKey: ddnsConfig.AuthPassword,
|
||||
}
|
||||
|
||||
provider, err := dnsprovider.NewDNSProvider(providerConfig)
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
return nil, fmt.Errorf("创建 DNS Provider 失败:%w", err)
|
||||
}
|
||||
|
||||
// 4. 构建并添加 DNS 记录
|
||||
dnsRecord := &dnsprovider.DNSRecord{
|
||||
Type: dnsprovider.RecordType(recordType),
|
||||
Name: name,
|
||||
Value: value,
|
||||
TTL: req.TTL,
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
_, err = provider.AppendRecords(ctx, ddnsConfig.Domain, []libdns.Record{dnsRecord.ToLibdnsRecord()})
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
return nil, fmt.Errorf("创建 DNS 记录失败:%w", err)
|
||||
}
|
||||
|
||||
// 5. DNS 记录创建成功,保存到数据库
|
||||
if err := tx.Create(req).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tx.Commit()
|
||||
return req, nil
|
||||
}
|
||||
|
||||
// 其他情况直接保存
|
||||
if err := s.store.DB().Create(req).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return req, nil
|
||||
}
|
||||
|
||||
// UpdateService 更新服务
|
||||
func (s *ServiceService) UpdateService(id string, updates map[string]interface{}) (*model.Service, error) {
|
||||
var service model.Service
|
||||
if err := s.store.DB().Where("id = ?", id).First(&service).Error; err != nil {
|
||||
return nil, errors.New("服务不存在")
|
||||
}
|
||||
|
||||
if err := s.store.DB().Model(&service).Updates(updates).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &service, nil
|
||||
}
|
||||
|
||||
// DeleteService 删除服务
|
||||
func (s *ServiceService) DeleteService(id string) error {
|
||||
var service model.Service
|
||||
if err := s.store.DB().Where("id = ?", id).First(&service).Error; err != nil {
|
||||
return errors.New("服务不存在")
|
||||
}
|
||||
return s.store.DB().Delete(&service).Error
|
||||
}
|
||||
|
||||
// TestServiceConnectivity 测试服务连通性
|
||||
func (s *ServiceService) TestServiceConnectivity(id string) (string, error) {
|
||||
var service model.Service
|
||||
if err := s.store.DB().Where("id = ?", id).First(&service).Error; err != nil {
|
||||
return "", errors.New("服务不存在")
|
||||
}
|
||||
|
||||
addr := net.JoinHostPort(service.Address, strconv.Itoa(service.Port))
|
||||
|
||||
if service.Type == "STUN" {
|
||||
// STUN 使用 UDP 测试
|
||||
conn, err := net.DialTimeout("udp", addr, 5*time.Second)
|
||||
if err != nil {
|
||||
return "unreachable", nil
|
||||
}
|
||||
conn.Close()
|
||||
return "ok", nil
|
||||
}
|
||||
|
||||
// 其他使用 TCP 测试
|
||||
conn, err := net.DialTimeout("tcp", addr, 5*time.Second)
|
||||
if err != nil {
|
||||
return "unreachable", nil
|
||||
}
|
||||
conn.Close()
|
||||
return "ok", nil
|
||||
}
|
||||
Reference in New Issue
Block a user