mirror of
https://github.com/0xJacky/nginx-ui.git
synced 2025-05-11 10:25:52 +02:00
feat: implement encrypted form handling and refactor backup restore logic
This commit is contained in:
parent
ed320f32cc
commit
8860f71bc7
15 changed files with 643 additions and 368 deletions
|
@ -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
134
api/system/restore.go
Normal 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,
|
||||
})
|
||||
}
|
|
@ -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) {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue