233 lines
7.5 KiB
Go
233 lines
7.5 KiB
Go
package service
|
||
|
||
import (
|
||
"archive/zip"
|
||
"crypto/md5"
|
||
"encoding/hex"
|
||
"fmt"
|
||
"io"
|
||
"os"
|
||
"path/filepath"
|
||
"time"
|
||
|
||
"github.com/lejianwen/rustdesk-api/v2/global"
|
||
"github.com/lejianwen/rustdesk-api/v2/model"
|
||
"github.com/lejianwen/rustdesk-api/v2/utils"
|
||
"gorm.io/gorm"
|
||
)
|
||
|
||
type ClientConfigService struct {
|
||
}
|
||
|
||
// GenerateClientConfig генерирует конфигурационный файл клиента
|
||
// customConfigFile - опциональный кастомный конфигурационный файл (если пустой, генерируется автоматически)
|
||
func (ccs *ClientConfigService) GenerateClientConfig(userId uint, password, description, customConfigFile string) (*model.ClientConfig, error) {
|
||
// Создаем директорию для хранения клиентов
|
||
clientsDir := filepath.Join(global.Config.Gin.ResourcesPath, "clients")
|
||
if err := os.MkdirAll(clientsDir, 0755); err != nil {
|
||
return nil, fmt.Errorf("failed to create clients directory: %v", err)
|
||
}
|
||
|
||
// Генерируем уникальное имя файла
|
||
timestamp := time.Now().Format("20060102150405")
|
||
fileName := fmt.Sprintf("rustdesk-client-%d-%s.zip", userId, timestamp)
|
||
filePath := filepath.Join(clientsDir, fileName)
|
||
|
||
// Создаем ZIP архив
|
||
zipFile, err := os.Create(filePath)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("failed to create zip file: %v", err)
|
||
}
|
||
defer zipFile.Close()
|
||
|
||
zipWriter := zip.NewWriter(zipFile)
|
||
defer zipWriter.Close()
|
||
|
||
// Создаем конфигурационный файл TOML
|
||
var configContent string
|
||
if customConfigFile != "" {
|
||
// Используем кастомный конфигурационный файл
|
||
configContent = customConfigFile
|
||
} else {
|
||
// Генерируем автоматически
|
||
configContent = ccs.generateTomlConfig()
|
||
}
|
||
configFile, err := zipWriter.Create("RustDesk.toml")
|
||
if err != nil {
|
||
return nil, fmt.Errorf("failed to create config file in zip: %v", err)
|
||
}
|
||
_, err = configFile.Write([]byte(configContent))
|
||
if err != nil {
|
||
return nil, fmt.Errorf("failed to write config to zip: %v", err)
|
||
}
|
||
|
||
// Создаем файл с паролем (опционально, для справки)
|
||
if password != "" {
|
||
passwordFile, err := zipWriter.Create("password.txt")
|
||
if err == nil {
|
||
passwordFile.Write([]byte(fmt.Sprintf("Password: %s\n", password)))
|
||
}
|
||
}
|
||
|
||
// Получаем информацию о файле
|
||
fileInfo, err := os.Stat(filePath)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("failed to get file info: %v", err)
|
||
}
|
||
|
||
// Вычисляем хеш файла
|
||
fileHash, err := ccs.calculateFileHash(filePath)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("failed to calculate file hash: %v", err)
|
||
}
|
||
|
||
// Хешируем пароль для хранения в БД
|
||
hashedPassword, err := utils.EncryptPassword(password)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("failed to hash password: %v", err)
|
||
}
|
||
|
||
// Создаем запись в БД
|
||
clientConfig := &model.ClientConfig{
|
||
UserId: userId,
|
||
Password: hashedPassword,
|
||
FileName: fileName,
|
||
FileHash: fileHash,
|
||
FileSize: fileInfo.Size(),
|
||
FilePath: filePath,
|
||
Description: description,
|
||
Status: model.COMMON_STATUS_ENABLE,
|
||
}
|
||
|
||
// Создаем базовую запись
|
||
if err := DB.Create(clientConfig).Error; err != nil {
|
||
// Удаляем файл в случае ошибки
|
||
os.Remove(filePath)
|
||
return nil, fmt.Errorf("failed to save client config to database: %v", err)
|
||
}
|
||
|
||
// Обновляем кастомные поля, если колонки существуют
|
||
if customConfigFile != "" {
|
||
// Проверяем наличие колонки перед обновлением
|
||
if DB.Migrator().HasColumn(&model.ClientConfig{}, "custom_config_file") {
|
||
DB.Model(clientConfig).Updates(map[string]interface{}{
|
||
"custom_config_file": customConfigFile,
|
||
"use_custom_config": true,
|
||
})
|
||
}
|
||
}
|
||
|
||
return clientConfig, nil
|
||
}
|
||
|
||
// generateTomlConfig генерирует TOML конфигурацию для RustDesk клиента
|
||
func (ccs *ClientConfigService) generateTomlConfig() string {
|
||
// Формируем TOML строку с настройками сервера
|
||
tomlStr := fmt.Sprintf(`rendezvous_server = "%s"
|
||
relay_server = "%s"
|
||
api_server = "%s"
|
||
key = "%s"
|
||
`, global.Config.Rustdesk.IdServer, global.Config.Rustdesk.RelayServer, global.Config.Rustdesk.ApiServer, global.Config.Rustdesk.Key)
|
||
|
||
return tomlStr
|
||
}
|
||
|
||
// calculateFileHash вычисляет MD5 хеш файла
|
||
func (ccs *ClientConfigService) calculateFileHash(filePath string) (string, error) {
|
||
file, err := os.Open(filePath)
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
defer file.Close()
|
||
|
||
hash := md5.New()
|
||
if _, err := io.Copy(hash, file); err != nil {
|
||
return "", err
|
||
}
|
||
|
||
return hex.EncodeToString(hash.Sum(nil)), nil
|
||
}
|
||
|
||
// GetByUserId получает список клиентов пользователя
|
||
func (ccs *ClientConfigService) GetByUserId(userId uint) []*model.ClientConfig {
|
||
var configs []*model.ClientConfig
|
||
DB.Where("user_id = ? AND status = ?", userId, model.COMMON_STATUS_ENABLE).
|
||
Order("created_at DESC").
|
||
Find(&configs)
|
||
return configs
|
||
}
|
||
|
||
// GetById получает клиент по ID
|
||
func (ccs *ClientConfigService) GetById(id uint) *model.ClientConfig {
|
||
config := &model.ClientConfig{}
|
||
DB.Where("id = ? AND status = ?", id, model.COMMON_STATUS_ENABLE).First(config)
|
||
return config
|
||
}
|
||
|
||
// List получает список клиентов с пагинацией
|
||
func (ccs *ClientConfigService) List(page, pageSize uint, scopes ...func(*gorm.DB) *gorm.DB) *model.ClientConfigList {
|
||
var configs []*model.ClientConfig
|
||
var total int64
|
||
|
||
query := DB.Model(&model.ClientConfig{}).Where("status = ?", model.COMMON_STATUS_ENABLE)
|
||
for _, scope := range scopes {
|
||
query = scope(query)
|
||
}
|
||
|
||
query.Count(&total)
|
||
query = Paginate(page, pageSize)(query)
|
||
query.Order("created_at DESC").Find(&configs)
|
||
|
||
return &model.ClientConfigList{
|
||
ClientConfigs: configs,
|
||
Pagination: model.Pagination{
|
||
Page: int64(page),
|
||
PageSize: int64(pageSize),
|
||
Total: total,
|
||
},
|
||
}
|
||
}
|
||
|
||
// Delete удаляет клиент (помечает как удаленный)
|
||
func (ccs *ClientConfigService) Delete(id, userId uint) error {
|
||
// Ищем запись без фильтра по status, чтобы можно было удалить даже если она уже помечена как удаленная
|
||
config := &model.ClientConfig{}
|
||
if err := DB.Where("id = ?", id).First(config).Error; err != nil {
|
||
return fmt.Errorf("client config not found")
|
||
}
|
||
|
||
// Проверяем, что клиент принадлежит пользователю
|
||
if config.UserId != userId {
|
||
return fmt.Errorf("access denied")
|
||
}
|
||
|
||
// Если уже удален, просто возвращаем успех
|
||
if config.Status == model.COMMON_STATUS_DISABLED {
|
||
return nil
|
||
}
|
||
|
||
// Помечаем как удаленный - обновляем только поле status, чтобы избежать проблем с отсутствующими колонками
|
||
if err := DB.Model(&model.ClientConfig{}).Where("id = ?", id).Update("status", model.COMMON_STATUS_DISABLED).Error; err != nil {
|
||
return err
|
||
}
|
||
|
||
// Удаляем файл
|
||
if config.FilePath != "" {
|
||
os.Remove(config.FilePath)
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
// VerifyPassword проверяет пароль клиента
|
||
func (ccs *ClientConfigService) VerifyPassword(configId uint, password string) bool {
|
||
config := ccs.GetById(configId)
|
||
if config.Id == 0 {
|
||
return false
|
||
}
|
||
|
||
ok, _, err := utils.VerifyPassword(config.Password, password)
|
||
return err == nil && ok
|
||
}
|
||
|