feat: implement encrypted form handling and refactor backup restore logic

This commit is contained in:
Jacky 2025-03-29 14:33:05 +00:00
parent ed320f32cc
commit 8860f71bc7
No known key found for this signature in database
GPG key ID: 215C21B10DF38B4D
15 changed files with 643 additions and 368 deletions

View file

@ -2,26 +2,14 @@ package system
import (
"bytes"
"encoding/base64"
"net/http"
"os"
"path/filepath"
"strings"
"time"
"github.com/0xJacky/Nginx-UI/internal/backup"
"github.com/gin-gonic/gin"
"github.com/jpillora/overseer"
"github.com/uozi-tech/cosy"
)
// RestoreResponse contains the response data for restore operation
type RestoreResponse struct {
NginxUIRestored bool `json:"nginx_ui_restored"`
NginxRestored bool `json:"nginx_restored"`
HashMatch bool `json:"hash_match"`
}
// CreateBackup creates a backup of nginx-ui and nginx configurations
// and sends files directly for download
func CreateBackup(c *gin.Context) {
@ -52,109 +40,3 @@ func CreateBackup(c *gin.Context) {
// Send file content
http.ServeContent(c.Writer, c.Request, fileName, modTime, reader)
}
// RestoreBackup restores from uploaded backup and security info
func RestoreBackup(c *gin.Context) {
// Get restore options
restoreNginx := c.PostForm("restore_nginx") == "true"
restoreNginxUI := c.PostForm("restore_nginx_ui") == "true"
verifyHash := c.PostForm("verify_hash") == "true"
securityToken := c.PostForm("security_token") // Get concatenated key and IV
// Get backup file
backupFile, err := c.FormFile("backup_file")
if err != nil {
cosy.ErrHandler(c, cosy.WrapErrorWithParams(backup.ErrBackupFileNotFound, err.Error()))
return
}
// Validate security token
if securityToken == "" {
cosy.ErrHandler(c, backup.ErrInvalidSecurityToken)
return
}
// Split security token to get Key and IV
parts := strings.Split(securityToken, ":")
if len(parts) != 2 {
cosy.ErrHandler(c, backup.ErrInvalidSecurityToken)
return
}
aesKey := parts[0]
aesIv := parts[1]
// Decode Key and IV from base64
key, err := base64.StdEncoding.DecodeString(aesKey)
if err != nil {
cosy.ErrHandler(c, cosy.WrapErrorWithParams(backup.ErrInvalidAESKey, err.Error()))
return
}
iv, err := base64.StdEncoding.DecodeString(aesIv)
if err != nil {
cosy.ErrHandler(c, cosy.WrapErrorWithParams(backup.ErrInvalidAESIV, err.Error()))
return
}
// Create temporary directory for files
tempDir, err := os.MkdirTemp("", "nginx-ui-restore-upload-*")
if err != nil {
cosy.ErrHandler(c, cosy.WrapErrorWithParams(backup.ErrCreateTempDir, err.Error()))
return
}
defer os.RemoveAll(tempDir)
// Save backup file
backupPath := filepath.Join(tempDir, backupFile.Filename)
if err := c.SaveUploadedFile(backupFile, backupPath); err != nil {
cosy.ErrHandler(c, cosy.WrapErrorWithParams(backup.ErrCreateBackupFile, err.Error()))
return
}
// Create temporary directory for restore operation
restoreDir, err := os.MkdirTemp("", "nginx-ui-restore-*")
if err != nil {
cosy.ErrHandler(c, cosy.WrapErrorWithParams(backup.ErrCreateRestoreDir, err.Error()))
return
}
// Set restore options
options := backup.RestoreOptions{
BackupPath: backupPath,
AESKey: key,
AESIv: iv,
RestoreDir: restoreDir,
RestoreNginx: restoreNginx,
RestoreNginxUI: restoreNginxUI,
VerifyHash: verifyHash,
}
// Perform restore
result, err := backup.Restore(options)
if err != nil {
// Clean up temporary directory on error
os.RemoveAll(restoreDir)
cosy.ErrHandler(c, err)
return
}
// If not actually restoring anything, clean up directory to avoid disk space waste
if !restoreNginx && !restoreNginxUI {
defer os.RemoveAll(restoreDir)
}
if restoreNginxUI {
go func() {
time.Sleep(3 * time.Second)
// gracefully restart
overseer.Restart()
}()
}
c.JSON(http.StatusOK, RestoreResponse{
NginxUIRestored: result.NginxUIRestored,
NginxRestored: result.NginxRestored,
HashMatch: result.HashMatch,
})
}

134
api/system/restore.go Normal file
View file

@ -0,0 +1,134 @@
package system
import (
"encoding/base64"
"net/http"
"os"
"path/filepath"
"strings"
"time"
"github.com/0xJacky/Nginx-UI/internal/backup"
"github.com/gin-gonic/gin"
"github.com/jpillora/overseer"
"github.com/uozi-tech/cosy"
"github.com/uozi-tech/cosy/logger"
)
// RestoreResponse contains the response data for restore operation
type RestoreResponse struct {
NginxUIRestored bool `json:"nginx_ui_restored"`
NginxRestored bool `json:"nginx_restored"`
HashMatch bool `json:"hash_match"`
}
// RestoreBackup restores from uploaded backup and security info
func RestoreBackup(c *gin.Context) {
// Get restore options
restoreNginx := c.PostForm("restore_nginx") == "true"
restoreNginxUI := c.PostForm("restore_nginx_ui") == "true"
verifyHash := c.PostForm("verify_hash") == "true"
securityToken := c.PostForm("security_token") // Get concatenated key and IV
logger.Debug("restoreNginx", restoreNginx)
logger.Debug("restoreNginxUI", restoreNginxUI)
logger.Debug("verifyHash", verifyHash)
logger.Debug("securityToken", securityToken)
// Get backup file
backupFile, err := c.FormFile("backup_file")
if err != nil {
cosy.ErrHandler(c, cosy.WrapErrorWithParams(backup.ErrBackupFileNotFound, err.Error()))
return
}
logger.Debug("backupFile", backupFile.Size)
// Validate security token
if securityToken == "" {
cosy.ErrHandler(c, backup.ErrInvalidSecurityToken)
return
}
// Split security token to get Key and IV
parts := strings.Split(securityToken, ":")
if len(parts) != 2 {
cosy.ErrHandler(c, backup.ErrInvalidSecurityToken)
return
}
aesKey := parts[0]
aesIv := parts[1]
// Decode Key and IV from base64
key, err := base64.StdEncoding.DecodeString(aesKey)
if err != nil {
cosy.ErrHandler(c, cosy.WrapErrorWithParams(backup.ErrInvalidAESKey, err.Error()))
return
}
iv, err := base64.StdEncoding.DecodeString(aesIv)
if err != nil {
cosy.ErrHandler(c, cosy.WrapErrorWithParams(backup.ErrInvalidAESIV, err.Error()))
return
}
// Create temporary directory for files
tempDir, err := os.MkdirTemp("", "nginx-ui-restore-upload-*")
if err != nil {
cosy.ErrHandler(c, cosy.WrapErrorWithParams(backup.ErrCreateTempDir, err.Error()))
return
}
defer os.RemoveAll(tempDir)
// Save backup file
backupPath := filepath.Join(tempDir, backupFile.Filename)
if err := c.SaveUploadedFile(backupFile, backupPath); err != nil {
cosy.ErrHandler(c, cosy.WrapErrorWithParams(backup.ErrCreateBackupFile, err.Error()))
return
}
// Create temporary directory for restore operation
restoreDir, err := os.MkdirTemp("", "nginx-ui-restore-*")
if err != nil {
cosy.ErrHandler(c, cosy.WrapErrorWithParams(backup.ErrCreateRestoreDir, err.Error()))
return
}
// Set restore options
options := backup.RestoreOptions{
BackupPath: backupPath,
AESKey: key,
AESIv: iv,
RestoreDir: restoreDir,
RestoreNginx: restoreNginx,
RestoreNginxUI: restoreNginxUI,
VerifyHash: verifyHash,
}
// Perform restore
result, err := backup.Restore(options)
if err != nil {
// Clean up temporary directory on error
os.RemoveAll(restoreDir)
cosy.ErrHandler(c, err)
return
}
// If not actually restoring anything, clean up directory to avoid disk space waste
if !restoreNginx && !restoreNginxUI {
defer os.RemoveAll(restoreDir)
}
if restoreNginxUI {
go func() {
time.Sleep(3 * time.Second)
// gracefully restart
overseer.Restart()
}()
}
c.JSON(http.StatusOK, RestoreResponse{
NginxUIRestored: result.NginxUIRestored,
NginxRestored: result.NginxRestored,
HashMatch: result.HashMatch,
})
}

View file

@ -19,7 +19,7 @@ func InitPrivateRouter(r *gin.RouterGroup) {
// Backup and restore endpoints
r.GET("system/backup", CreateBackup)
r.POST("system/backup/restore", RestoreBackup)
r.POST("system/backup/restore", middleware.EncryptedForm(), RestoreBackup)
}
func InitWebSocketRouter(r *gin.RouterGroup) {