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
|
@ -100,18 +100,18 @@ func decryptFile(filePath string, key []byte, iv []byte) error {
|
|||
// Read encrypted file content
|
||||
encryptedData, err := os.ReadFile(filePath)
|
||||
if err != nil {
|
||||
return cosy.WrapErrorWithParams(ErrReadEncryptedFile, filePath)
|
||||
return cosy.WrapErrorWithParams(ErrReadEncryptedFile, err.Error())
|
||||
}
|
||||
|
||||
// Decrypt file content
|
||||
decryptedData, err := AESDecrypt(encryptedData, key, iv)
|
||||
if err != nil {
|
||||
return cosy.WrapErrorWithParams(ErrDecryptFile, filePath)
|
||||
return cosy.WrapErrorWithParams(ErrDecryptFile, err.Error())
|
||||
}
|
||||
|
||||
// Write decrypted content back
|
||||
if err := os.WriteFile(filePath, decryptedData, 0644); err != nil {
|
||||
return cosy.WrapErrorWithParams(ErrWriteDecryptedFile, filePath)
|
||||
return cosy.WrapErrorWithParams(ErrWriteDecryptedFile, err.Error())
|
||||
}
|
||||
|
||||
return nil
|
||||
|
|
|
@ -2,6 +2,7 @@ package backup
|
|||
|
||||
import (
|
||||
"archive/zip"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
@ -51,7 +52,7 @@ func Restore(options RestoreOptions) (RestoreResult, error) {
|
|||
|
||||
// Decrypt hash info file
|
||||
if err := decryptFile(hashInfoPath, options.AESKey, options.AESIv); err != nil {
|
||||
return RestoreResult{}, cosy.WrapErrorWithParams(ErrDecryptFile, HashInfoFile)
|
||||
return RestoreResult{}, cosy.WrapErrorWithParams(ErrDecryptFile, err.Error())
|
||||
}
|
||||
|
||||
// Decrypt nginx-ui.zip
|
||||
|
@ -69,21 +70,21 @@ func Restore(options RestoreOptions) (RestoreResult, error) {
|
|||
nginxDir := filepath.Join(options.RestoreDir, NginxDir)
|
||||
|
||||
if err := os.MkdirAll(nginxUIDir, 0755); err != nil {
|
||||
return RestoreResult{}, cosy.WrapErrorWithParams(ErrCreateDir, nginxUIDir)
|
||||
return RestoreResult{}, cosy.WrapErrorWithParams(ErrCreateDir, err.Error())
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(nginxDir, 0755); err != nil {
|
||||
return RestoreResult{}, cosy.WrapErrorWithParams(ErrCreateDir, nginxDir)
|
||||
return RestoreResult{}, cosy.WrapErrorWithParams(ErrCreateDir, err.Error())
|
||||
}
|
||||
|
||||
// Extract nginx-ui.zip to nginx-ui directory
|
||||
if err := extractZipArchive(nginxUIZipPath, nginxUIDir); err != nil {
|
||||
return RestoreResult{}, cosy.WrapErrorWithParams(ErrExtractArchive, "nginx-ui.zip")
|
||||
return RestoreResult{}, cosy.WrapErrorWithParams(ErrExtractArchive, err.Error())
|
||||
}
|
||||
|
||||
// Extract nginx.zip to nginx directory
|
||||
if err := extractZipArchive(nginxZipPath, nginxDir); err != nil {
|
||||
return RestoreResult{}, cosy.WrapErrorWithParams(ErrExtractArchive, "nginx.zip")
|
||||
return RestoreResult{}, cosy.WrapErrorWithParams(ErrExtractArchive, err.Error())
|
||||
}
|
||||
|
||||
result := RestoreResult{
|
||||
|
@ -125,14 +126,14 @@ func Restore(options RestoreOptions) (RestoreResult, error) {
|
|||
func extractZipArchive(zipPath, destDir string) error {
|
||||
reader, err := zip.OpenReader(zipPath)
|
||||
if err != nil {
|
||||
return cosy.WrapErrorWithParams(ErrOpenZipFile, err.Error())
|
||||
return cosy.WrapErrorWithParams(ErrOpenZipFile, fmt.Sprintf("failed to open zip file %s: %v", zipPath, err))
|
||||
}
|
||||
defer reader.Close()
|
||||
|
||||
for _, file := range reader.File {
|
||||
err := extractZipFile(file, destDir)
|
||||
if err != nil {
|
||||
return err
|
||||
return cosy.WrapErrorWithParams(ErrExtractArchive, fmt.Sprintf("failed to extract file %s: %v", file.Name, err))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -143,37 +144,45 @@ func extractZipArchive(zipPath, destDir string) error {
|
|||
func extractZipFile(file *zip.File, destDir string) error {
|
||||
// Check for directory traversal elements in the file name
|
||||
if strings.Contains(file.Name, "..") {
|
||||
return cosy.WrapErrorWithParams(ErrInvalidFilePath, file.Name)
|
||||
return cosy.WrapErrorWithParams(ErrInvalidFilePath, fmt.Sprintf("file name contains directory traversal: %s", file.Name))
|
||||
}
|
||||
|
||||
// Clean and normalize the file path
|
||||
cleanName := filepath.Clean(file.Name)
|
||||
if cleanName == "." || cleanName == ".." {
|
||||
return cosy.WrapErrorWithParams(ErrInvalidFilePath, fmt.Sprintf("invalid file name after cleaning: %s", file.Name))
|
||||
}
|
||||
|
||||
// Create directory path if needed
|
||||
filePath := filepath.Join(destDir, file.Name)
|
||||
filePath := filepath.Join(destDir, cleanName)
|
||||
|
||||
// Ensure the resulting file path is within the destination directory
|
||||
destDirAbs, err := filepath.Abs(destDir)
|
||||
if err != nil {
|
||||
return cosy.WrapErrorWithParams(ErrInvalidFilePath, "cannot resolve destination path")
|
||||
return cosy.WrapErrorWithParams(ErrInvalidFilePath, fmt.Sprintf("cannot resolve destination path %s: %v", destDir, err))
|
||||
}
|
||||
|
||||
filePathAbs, err := filepath.Abs(filePath)
|
||||
if err != nil {
|
||||
return cosy.WrapErrorWithParams(ErrInvalidFilePath, file.Name)
|
||||
return cosy.WrapErrorWithParams(ErrInvalidFilePath, fmt.Sprintf("cannot resolve file path %s: %v", filePath, err))
|
||||
}
|
||||
|
||||
// Check if the file path is within the destination directory
|
||||
if !strings.HasPrefix(filePathAbs, destDirAbs+string(os.PathSeparator)) {
|
||||
return cosy.WrapErrorWithParams(ErrInvalidFilePath, file.Name)
|
||||
return cosy.WrapErrorWithParams(ErrInvalidFilePath, fmt.Sprintf("file path %s is outside destination directory %s", filePathAbs, destDirAbs))
|
||||
}
|
||||
|
||||
if file.FileInfo().IsDir() {
|
||||
if err := os.MkdirAll(filePath, file.Mode()); err != nil {
|
||||
return cosy.WrapErrorWithParams(ErrCreateDir, filePath)
|
||||
return cosy.WrapErrorWithParams(ErrCreateDir, fmt.Sprintf("failed to create directory %s: %v", filePath, err))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Create parent directory if needed
|
||||
if err := os.MkdirAll(filepath.Dir(filePath), 0755); err != nil {
|
||||
return cosy.WrapErrorWithParams(ErrCreateParentDir, filePath)
|
||||
parentDir := filepath.Dir(filePath)
|
||||
if err := os.MkdirAll(parentDir, 0755); err != nil {
|
||||
return cosy.WrapErrorWithParams(ErrCreateParentDir, fmt.Sprintf("failed to create parent directory %s: %v", parentDir, err))
|
||||
}
|
||||
|
||||
// Check if this is a symlink by examining mode bits
|
||||
|
@ -181,46 +190,83 @@ func extractZipFile(file *zip.File, destDir string) error {
|
|||
// Open source file in zip to read the link target
|
||||
srcFile, err := file.Open()
|
||||
if err != nil {
|
||||
return cosy.WrapErrorWithParams(ErrOpenZipEntry, file.Name)
|
||||
return cosy.WrapErrorWithParams(ErrOpenZipEntry, fmt.Sprintf("failed to open symlink source %s: %v", file.Name, err))
|
||||
}
|
||||
defer srcFile.Close()
|
||||
|
||||
// Read the link target
|
||||
linkTargetBytes, err := io.ReadAll(srcFile)
|
||||
if err != nil {
|
||||
return cosy.WrapErrorWithParams(ErrReadSymlink, file.Name)
|
||||
return cosy.WrapErrorWithParams(ErrReadSymlink, fmt.Sprintf("failed to read symlink target for %s: %v", file.Name, err))
|
||||
}
|
||||
linkTarget := string(linkTargetBytes)
|
||||
|
||||
// Clean and normalize the link target
|
||||
cleanLinkTarget := filepath.Clean(linkTarget)
|
||||
if cleanLinkTarget == "." || cleanLinkTarget == ".." {
|
||||
return cosy.WrapErrorWithParams(ErrInvalidFilePath, fmt.Sprintf("invalid symlink target: %s", linkTarget))
|
||||
}
|
||||
|
||||
// Get nginx modules path
|
||||
modulesPath := nginx.GetModulesPath()
|
||||
|
||||
// Handle system directory symlinks
|
||||
if strings.HasPrefix(cleanLinkTarget, modulesPath) {
|
||||
// For nginx modules, we'll create a relative symlink to the modules directory
|
||||
relPath, err := filepath.Rel(filepath.Dir(filePath), modulesPath)
|
||||
if err != nil {
|
||||
return cosy.WrapErrorWithParams(ErrInvalidFilePath, fmt.Sprintf("failed to convert modules path to relative: %v", err))
|
||||
}
|
||||
cleanLinkTarget = relPath
|
||||
} else if filepath.IsAbs(cleanLinkTarget) {
|
||||
// For other absolute paths, we'll create a directory instead of a symlink
|
||||
if err := os.MkdirAll(filePath, 0755); err != nil {
|
||||
return cosy.WrapErrorWithParams(ErrCreateDir, fmt.Sprintf("failed to create directory %s: %v", filePath, err))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Verify the link target doesn't escape the destination directory
|
||||
absLinkTarget := filepath.Clean(filepath.Join(filepath.Dir(filePath), linkTarget))
|
||||
absLinkTarget := filepath.Clean(filepath.Join(filepath.Dir(filePath), cleanLinkTarget))
|
||||
if !strings.HasPrefix(absLinkTarget, destDirAbs+string(os.PathSeparator)) {
|
||||
return cosy.WrapErrorWithParams(ErrInvalidFilePath, linkTarget)
|
||||
// For nginx modules, we'll create a directory instead of a symlink
|
||||
if strings.HasPrefix(linkTarget, modulesPath) {
|
||||
if err := os.MkdirAll(filePath, 0755); err != nil {
|
||||
return cosy.WrapErrorWithParams(ErrCreateDir, fmt.Sprintf("failed to create modules directory %s: %v", filePath, err))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return cosy.WrapErrorWithParams(ErrInvalidFilePath, fmt.Sprintf("symlink target %s is outside destination directory %s", absLinkTarget, destDirAbs))
|
||||
}
|
||||
|
||||
// Remove any existing file/link at the target path
|
||||
_ = os.Remove(filePath)
|
||||
if err := os.Remove(filePath); err != nil && !os.IsNotExist(err) {
|
||||
// Ignoring error, continue creating symlink
|
||||
}
|
||||
|
||||
// Create the symlink
|
||||
if err := os.Symlink(linkTarget, filePath); err != nil {
|
||||
return cosy.WrapErrorWithParams(ErrCreateSymlink, file.Name)
|
||||
if err := os.Symlink(cleanLinkTarget, filePath); err != nil {
|
||||
return cosy.WrapErrorWithParams(ErrCreateSymlink, fmt.Sprintf("failed to create symlink %s -> %s: %v", filePath, cleanLinkTarget, err))
|
||||
}
|
||||
|
||||
// Verify the resolved symlink path is within destination directory
|
||||
resolvedPath, err := filepath.EvalSymlinks(filePath)
|
||||
if err != nil {
|
||||
return cosy.WrapErrorWithParams(ErrEvalSymlinks, filePath)
|
||||
// If we can't resolve the symlink, it's not a critical error
|
||||
// Just continue
|
||||
return nil
|
||||
}
|
||||
|
||||
resolvedPathAbs, err := filepath.Abs(resolvedPath)
|
||||
if err != nil {
|
||||
return cosy.WrapErrorWithParams(ErrInvalidFilePath, resolvedPath)
|
||||
// Not a critical error, continue
|
||||
return nil
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(resolvedPathAbs, destDirAbs+string(os.PathSeparator)) {
|
||||
// Remove the symlink if it points outside the destination directory
|
||||
_ = os.Remove(filePath)
|
||||
return cosy.WrapErrorWithParams(ErrInvalidFilePath, resolvedPath)
|
||||
return cosy.WrapErrorWithParams(ErrInvalidFilePath, fmt.Sprintf("resolved symlink path %s is outside destination directory %s", resolvedPathAbs, destDirAbs))
|
||||
}
|
||||
|
||||
return nil
|
||||
|
@ -229,20 +275,20 @@ func extractZipFile(file *zip.File, destDir string) error {
|
|||
// Create file
|
||||
destFile, err := os.OpenFile(filePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, file.Mode())
|
||||
if err != nil {
|
||||
return cosy.WrapErrorWithParams(ErrCreateFile, filePath)
|
||||
return cosy.WrapErrorWithParams(ErrCreateFile, fmt.Sprintf("failed to create file %s: %v", filePath, err))
|
||||
}
|
||||
defer destFile.Close()
|
||||
|
||||
// Open source file in zip
|
||||
srcFile, err := file.Open()
|
||||
if err != nil {
|
||||
return cosy.WrapErrorWithParams(ErrOpenZipEntry, file.Name)
|
||||
return cosy.WrapErrorWithParams(ErrOpenZipEntry, fmt.Sprintf("failed to open zip entry %s: %v", file.Name, err))
|
||||
}
|
||||
defer srcFile.Close()
|
||||
|
||||
// Copy content
|
||||
if _, err := io.Copy(destFile, srcFile); err != nil {
|
||||
return cosy.WrapErrorWithParams(ErrCopyContent, file.Name)
|
||||
return cosy.WrapErrorWithParams(ErrCopyContent, fmt.Sprintf("failed to copy content for file %s: %v", file.Name, err))
|
||||
}
|
||||
|
||||
return nil
|
||||
|
|
|
@ -4,17 +4,22 @@ import (
|
|||
"bytes"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/0xJacky/Nginx-UI/internal/crypto"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/uozi-tech/cosy"
|
||||
"github.com/uozi-tech/cosy/logger"
|
||||
)
|
||||
|
||||
var (
|
||||
e = cosy.NewErrorScope("middleware")
|
||||
ErrInvalidRequestFormat = e.New(40000, "invalid request format")
|
||||
ErrDecryptionFailed = e.New(40001, "decryption failed")
|
||||
ErrFormParseFailed = e.New(40002, "form parse failed")
|
||||
)
|
||||
|
||||
func EncryptedParams() gin.HandlerFunc {
|
||||
|
@ -44,3 +49,86 @@ func EncryptedParams() gin.HandlerFunc {
|
|||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// EncryptedForm handles multipart/form-data with encrypted fields while preserving file uploads
|
||||
func EncryptedForm() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
// Only process if the content type is multipart/form-data
|
||||
if !strings.Contains(c.GetHeader("Content-Type"), "multipart/form-data") {
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
|
||||
// Parse the multipart form
|
||||
if err := c.Request.ParseMultipartForm(512 << 20); err != nil { // 512MB max memory
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, ErrFormParseFailed)
|
||||
return
|
||||
}
|
||||
|
||||
// Check if encrypted_params field exists
|
||||
encryptedParams := c.Request.FormValue("encrypted_params")
|
||||
if encryptedParams == "" {
|
||||
// No encryption, continue normally
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
|
||||
// Decrypt the parameters
|
||||
params, err := crypto.Decrypt(encryptedParams)
|
||||
if err != nil {
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, ErrDecryptionFailed)
|
||||
return
|
||||
}
|
||||
|
||||
// Create a new multipart form with the decrypted data
|
||||
newForm := &multipart.Form{
|
||||
Value: make(map[string][]string),
|
||||
File: c.Request.MultipartForm.File, // Keep original file uploads
|
||||
}
|
||||
|
||||
// Add decrypted values to the new form
|
||||
for key, val := range params {
|
||||
strVal, ok := val.(string)
|
||||
if ok {
|
||||
newForm.Value[key] = []string{strVal}
|
||||
} else {
|
||||
// Handle other types if necessary
|
||||
jsonVal, _ := json.Marshal(val)
|
||||
newForm.Value[key] = []string{string(jsonVal)}
|
||||
}
|
||||
}
|
||||
|
||||
// Also copy original non-encrypted form values (except encrypted_params)
|
||||
for key, vals := range c.Request.MultipartForm.Value {
|
||||
if key != "encrypted_params" && newForm.Value[key] == nil {
|
||||
newForm.Value[key] = vals
|
||||
}
|
||||
}
|
||||
|
||||
logger.Debug("newForm values", newForm.Value)
|
||||
logger.Debug("newForm files", newForm.File)
|
||||
|
||||
// Replace the original form with our modified one
|
||||
c.Request.MultipartForm = newForm
|
||||
|
||||
// Remove the encrypted_params field from the form
|
||||
delete(c.Request.MultipartForm.Value, "encrypted_params")
|
||||
|
||||
// Reset ContentLength as form structure has changed
|
||||
c.Request.ContentLength = -1
|
||||
|
||||
// Sync the form values to the request PostForm to ensure Gin can access them
|
||||
if c.Request.PostForm == nil {
|
||||
c.Request.PostForm = make(url.Values)
|
||||
}
|
||||
|
||||
// Copy all values from MultipartForm to PostForm
|
||||
for k, v := range newForm.Value {
|
||||
c.Request.PostForm[k] = v
|
||||
}
|
||||
|
||||
logger.Debug("PostForm after sync", c.Request.PostForm)
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
package nginx
|
||||
|
||||
import (
|
||||
"github.com/0xJacky/Nginx-UI/settings"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/0xJacky/Nginx-UI/settings"
|
||||
)
|
||||
|
||||
var (
|
||||
|
@ -74,6 +76,28 @@ func GetLastOutput() string {
|
|||
return lastOutput
|
||||
}
|
||||
|
||||
// GetModulesPath returns the nginx modules path
|
||||
func GetModulesPath() string {
|
||||
// First try to get from nginx -V output
|
||||
output := execCommand("nginx", "-V")
|
||||
if output != "" {
|
||||
// Look for --modules-path in the output
|
||||
if strings.Contains(output, "--modules-path=") {
|
||||
parts := strings.Split(output, "--modules-path=")
|
||||
if len(parts) > 1 {
|
||||
// Extract the path
|
||||
path := strings.Split(parts[1], " ")[0]
|
||||
// Remove quotes if present
|
||||
path = strings.Trim(path, "\"")
|
||||
return path
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Default path if not found
|
||||
return "/usr/lib/nginx/modules"
|
||||
}
|
||||
|
||||
func execShell(cmd string) (out string) {
|
||||
bytes, err := exec.Command("/bin/sh", "-c", cmd).CombinedOutput()
|
||||
out = string(bytes)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue