From fd6ba5614315be552b86b7d27c78979083ea6c59 Mon Sep 17 00:00:00 2001 From: Toby Chui Date: Sun, 24 Sep 2023 23:44:48 +0800 Subject: [PATCH] Added web directory manager + Added web directory manager + Added dummy service expose proxy page + Moved ACME and renew to a new section in TLS management page --- src/api.go | 18 +- src/main.go | 2 +- src/mod/acme/acme.go | 5 +- src/mod/webserv/filemanager/filemanager.go | 406 +++++++++++++++++++++ src/mod/webserv/filemanager/utils.go | 156 ++++++++ src/mod/webserv/webserv.go | 23 +- src/web/components/cert.html | 31 +- src/web/components/rproot.html | 32 +- src/web/components/rules.html | 6 +- src/web/components/webserv.html | 67 +++- src/web/components/zgrok.html | 10 + src/web/index.html | 5 +- src/web/tools/fs.html | 209 +++-------- 13 files changed, 782 insertions(+), 188 deletions(-) create mode 100644 src/mod/webserv/filemanager/filemanager.go create mode 100644 src/mod/webserv/filemanager/utils.go create mode 100644 src/web/components/zgrok.html diff --git a/src/api.go b/src/api.go index 7f4952b..be55b9a 100644 --- a/src/api.go +++ b/src/api.go @@ -177,16 +177,14 @@ func initAPIs() { authRouter.HandleFunc("/api/webserv/setDirList", staticWebServer.SetEnableDirectoryListing) if *allowWebFileManager { //Web Directory Manager file operation functions - /* - authRouter.HandleFunc("/api/fs/list", staticWebServer.HandleGetStatus) - authRouter.HandleFunc("/api/fs/upload", staticWebServer.HandleGetStatus) - authRouter.HandleFunc("/api/fs/download", staticWebServer.HandleGetStatus) - authRouter.HandleFunc("/api/fs/newFolder", staticWebServer.HandleGetStatus) - authRouter.HandleFunc("/api/fs/copy", staticWebServer.HandleGetStatus) - authRouter.HandleFunc("/api/fs/move", staticWebServer.HandleGetStatus) - authRouter.HandleFunc("/api/fs/properties", staticWebServer.HandleGetStatus) - authRouter.HandleFunc("/api/fs/del", staticWebServer.HandleGetStatus) - */ + authRouter.HandleFunc("/api/fs/list", staticWebServer.FileManager.HandleList) + authRouter.HandleFunc("/api/fs/upload", staticWebServer.FileManager.HandleUpload) + authRouter.HandleFunc("/api/fs/download", staticWebServer.FileManager.HandleDownload) + authRouter.HandleFunc("/api/fs/newFolder", staticWebServer.FileManager.HandleNewFolder) + authRouter.HandleFunc("/api/fs/copy", staticWebServer.FileManager.HandleFileCopy) + authRouter.HandleFunc("/api/fs/move", staticWebServer.FileManager.HandleFileMove) + authRouter.HandleFunc("/api/fs/properties", staticWebServer.FileManager.HandleFileProperties) + authRouter.HandleFunc("/api/fs/del", staticWebServer.FileManager.HandleFileDelete) } //Others diff --git a/src/main.go b/src/main.go index 1dce6ea..4177a75 100644 --- a/src/main.go +++ b/src/main.go @@ -49,7 +49,7 @@ var ( name = "Zoraxy" version = "2.6.7" nodeUUID = "generic" - development = true //Set this to false to use embedded web fs + development = false //Set this to false to use embedded web fs bootTime = time.Now().Unix() /* diff --git a/src/mod/acme/acme.go b/src/mod/acme/acme.go index a127a3b..01e0783 100644 --- a/src/mod/acme/acme.go +++ b/src/mod/acme/acme.go @@ -10,7 +10,6 @@ import ( "encoding/json" "encoding/pem" "fmt" - "io/ioutil" "log" "net" "net/http" @@ -164,12 +163,12 @@ func (a *ACMEHandler) ObtainCert(domains []string, certificateName string, email // Each certificate comes back with the cert bytes, the bytes of the client's // private key, and a certificate URL. - err = ioutil.WriteFile("./conf/certs/"+certificateName+".crt", certificates.Certificate, 0777) + err = os.WriteFile("./conf/certs/"+certificateName+".crt", certificates.Certificate, 0777) if err != nil { log.Println(err) return false, err } - err = ioutil.WriteFile("./conf/certs/"+certificateName+".key", certificates.PrivateKey, 0777) + err = os.WriteFile("./conf/certs/"+certificateName+".key", certificates.PrivateKey, 0777) if err != nil { log.Println(err) return false, err diff --git a/src/mod/webserv/filemanager/filemanager.go b/src/mod/webserv/filemanager/filemanager.go new file mode 100644 index 0000000..8847553 --- /dev/null +++ b/src/mod/webserv/filemanager/filemanager.go @@ -0,0 +1,406 @@ +package filemanager + +import ( + "encoding/json" + "fmt" + "io" + "log" + "net/http" + "os" + "path/filepath" + "strings" + + "imuslab.com/zoraxy/mod/utils" +) + +/* + File Manager + + This is a simple package that handles file management + under the web server directory +*/ + +type FileManager struct { + Directory string +} + +// Create a new file manager with directory as root +func NewFileManager(directory string) *FileManager { + return &FileManager{ + Directory: directory, + } +} + +// Handle listing of a given directory +func (fm *FileManager) HandleList(w http.ResponseWriter, r *http.Request) { + directory, err := utils.GetPara(r, "dir") + if err != nil { + utils.SendErrorResponse(w, "invalid directory given") + return + } + + // Construct the absolute path to the target directory + targetDir := filepath.Join(fm.Directory, directory) + + // Open the target directory + dirEntries, err := os.ReadDir(targetDir) + if err != nil { + utils.SendErrorResponse(w, "unable to open directory") + return + } + + // Create a slice to hold the file information + var files []map[string]interface{} = []map[string]interface{}{} + + // Iterate through the directory entries + for _, dirEntry := range dirEntries { + fileInfo := make(map[string]interface{}) + fileInfo["filename"] = dirEntry.Name() + fileInfo["filepath"] = filepath.Join(directory, dirEntry.Name()) + fileInfo["isDir"] = dirEntry.IsDir() + + // Get file size and last modified time + finfo, err := dirEntry.Info() + if err != nil { + //unable to load its info. Skip this file + continue + } + fileInfo["lastModified"] = finfo.ModTime().Unix() + if !dirEntry.IsDir() { + // If it's a file, get its size + fileInfo["size"] = finfo.Size() + } else { + // If it's a directory, set size to 0 + fileInfo["size"] = 0 + } + + // Append file info to the list + files = append(files, fileInfo) + } + + // Serialize the file info slice to JSON + jsonData, err := json.Marshal(files) + if err != nil { + utils.SendErrorResponse(w, "unable to marshal JSON") + return + } + + // Set response headers and send the JSON response + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + w.Write(jsonData) +} + +// Handle upload of a file (multi-part), 25MB max +func (fm *FileManager) HandleUpload(w http.ResponseWriter, r *http.Request) { + dir, err := utils.PostPara(r, "dir") + if err != nil { + log.Println("no dir given") + utils.SendErrorResponse(w, "invalid dir given") + return + } + + // Parse the multi-part form data + err = r.ParseMultipartForm(25 << 20) + if err != nil { + utils.SendErrorResponse(w, "unable to parse form data") + return + } + + // Get the uploaded file + file, fheader, err := r.FormFile("file") + if err != nil { + log.Println(err.Error()) + utils.SendErrorResponse(w, "unable to get uploaded file") + return + } + defer file.Close() + + // Specify the directory where you want to save the uploaded file + uploadDir := filepath.Join(fm.Directory, dir) + if !utils.FileExists(uploadDir) { + utils.SendErrorResponse(w, "upload target directory not exists") + return + } + + filename := sanitizeFilename(fheader.Filename) + if !isValidFilename(filename) { + utils.SendErrorResponse(w, "filename contain invalid or reserved characters") + return + } + + // Create the file on the server + filePath := filepath.Join(uploadDir, filepath.Base(filename)) + out, err := os.Create(filePath) + if err != nil { + utils.SendErrorResponse(w, "unable to create file on the server") + return + } + defer out.Close() + + // Copy the uploaded file to the server + _, err = io.Copy(out, file) + if err != nil { + utils.SendErrorResponse(w, "unable to copy file to server") + return + } + + // Respond with a success message or appropriate response + utils.SendOK(w) +} + +// Handle download of a selected file, serve with content dispose header +func (fm *FileManager) HandleDownload(w http.ResponseWriter, r *http.Request) { + filename, err := utils.GetPara(r, "file") + if err != nil { + utils.SendErrorResponse(w, "invalid filepath given") + return + } + + previewMode, _ := utils.GetPara(r, "preview") + if previewMode == "true" { + // Serve the file using http.ServeFile + filePath := filepath.Join(fm.Directory, filename) + http.ServeFile(w, r, filePath) + } else { + // Trigger a download with content disposition headers + filePath := filepath.Join(fm.Directory, filename) + w.Header().Set("Content-Disposition", "attachment; filename="+filepath.Base(filename)) + http.ServeFile(w, r, filePath) + } +} + +// HandleNewFolder creates a new folder in the specified directory +func (fm *FileManager) HandleNewFolder(w http.ResponseWriter, r *http.Request) { + // Parse the directory name from the request + dirName, err := utils.GetPara(r, "path") + if err != nil { + utils.SendErrorResponse(w, "invalid directory name") + return + } + + //Prevent path escape + dirName = strings.ReplaceAll(dirName, "\\", "/") + dirName = strings.ReplaceAll(dirName, "../", "") + + // Specify the directory where you want to create the new folder + newFolderPath := filepath.Join(fm.Directory, dirName) + + // Check if the folder already exists + if _, err := os.Stat(newFolderPath); os.IsNotExist(err) { + // Create the new folder + err := os.Mkdir(newFolderPath, os.ModePerm) + if err != nil { + utils.SendErrorResponse(w, "unable to create the new folder") + return + } + + // Respond with a success message or appropriate response + utils.SendOK(w) + } else { + // If the folder already exists, respond with an error + utils.SendErrorResponse(w, "folder already exists") + } +} + +// HandleFileCopy copies a file or directory from the source path to the destination path +func (fm *FileManager) HandleFileCopy(w http.ResponseWriter, r *http.Request) { + // Parse the source and destination paths from the request + srcPath, err := utils.PostPara(r, "srcpath") + if err != nil { + utils.SendErrorResponse(w, "invalid source path") + return + } + + destPath, err := utils.PostPara(r, "destpath") + if err != nil { + utils.SendErrorResponse(w, "invalid destination path") + return + } + + // Validate and sanitize the source and destination paths + srcPath = filepath.Clean(srcPath) + destPath = filepath.Clean(destPath) + + // Construct the absolute paths + absSrcPath := filepath.Join(fm.Directory, srcPath) + absDestPath := filepath.Join(fm.Directory, destPath) + + // Check if the source path exists + if _, err := os.Stat(absSrcPath); os.IsNotExist(err) { + utils.SendErrorResponse(w, "source path does not exist") + return + } + + // Check if the destination path exists + if _, err := os.Stat(absDestPath); os.IsNotExist(err) { + utils.SendErrorResponse(w, "destination path does not exist") + return + } + + //Join the name to create final paste filename + absDestPath = filepath.Join(absDestPath, filepath.Base(absSrcPath)) + //Reject opr if already exists + if utils.FileExists(absDestPath) { + utils.SendErrorResponse(w, "target already exists") + return + } + + // Perform the copy operation based on whether the source is a file or directory + if isDir(absSrcPath) { + // Recursive copy for directories + err := copyDirectory(absSrcPath, absDestPath) + if err != nil { + utils.SendErrorResponse(w, fmt.Sprintf("error copying directory: %v", err)) + return + } + } else { + // Copy a single file + err := copyFile(absSrcPath, absDestPath) + if err != nil { + utils.SendErrorResponse(w, fmt.Sprintf("error copying file: %v", err)) + return + } + } + + utils.SendOK(w) +} + +func (fm *FileManager) HandleFileMove(w http.ResponseWriter, r *http.Request) { + // Parse the source and destination paths from the request + srcPath, err := utils.GetPara(r, "srcpath") + if err != nil { + utils.SendErrorResponse(w, "invalid source path") + return + } + + destPath, err := utils.GetPara(r, "destpath") + if err != nil { + utils.SendErrorResponse(w, "invalid destination path") + return + } + + // Validate and sanitize the source and destination paths + srcPath = filepath.Clean(srcPath) + destPath = filepath.Clean(destPath) + + // Construct the absolute paths + absSrcPath := filepath.Join(fm.Directory, srcPath) + absDestPath := filepath.Join(fm.Directory, destPath) + + // Check if the source path exists + if _, err := os.Stat(absSrcPath); os.IsNotExist(err) { + utils.SendErrorResponse(w, "source path does not exist") + return + } + + // Check if the destination path exists + if _, err := os.Stat(absDestPath); !os.IsNotExist(err) { + utils.SendErrorResponse(w, "destination path already exists") + return + } + + // Rename the source to the destination + err = os.Rename(absSrcPath, absDestPath) + if err != nil { + utils.SendErrorResponse(w, fmt.Sprintf("error moving file/directory: %v", err)) + return + } + utils.SendOK(w) +} + +func (fm *FileManager) HandleFileProperties(w http.ResponseWriter, r *http.Request) { + // Parse the target file or directory path from the request + filePath, err := utils.GetPara(r, "file") + if err != nil { + utils.SendErrorResponse(w, "invalid file path") + return + } + + // Construct the absolute path to the target file or directory + absPath := filepath.Join(fm.Directory, filePath) + + // Check if the target path exists + _, err = os.Stat(absPath) + if err != nil { + utils.SendErrorResponse(w, "file or directory does not exist") + return + } + + // Initialize a map to hold file properties + fileProps := make(map[string]interface{}) + fileProps["filename"] = filepath.Base(absPath) + fileProps["filepath"] = filePath + fileProps["isDir"] = isDir(absPath) + + // Get file size and last modified time + finfo, err := os.Stat(absPath) + if err != nil { + utils.SendErrorResponse(w, "unable to retrieve file properties") + return + } + fileProps["lastModified"] = finfo.ModTime().Unix() + if !isDir(absPath) { + // If it's a file, get its size + fileProps["size"] = finfo.Size() + } else { + // If it's a directory, calculate its total size containing all child files and folders + totalSize, err := calculateDirectorySize(absPath) + if err != nil { + utils.SendErrorResponse(w, "unable to calculate directory size") + return + } + fileProps["size"] = totalSize + } + + // Count the number of sub-files and sub-folders + numSubFiles, numSubFolders, err := countSubFilesAndFolders(absPath) + if err != nil { + utils.SendErrorResponse(w, "unable to count sub-files and sub-folders") + return + } + fileProps["fileCounts"] = numSubFiles + fileProps["folderCounts"] = numSubFolders + + // Serialize the file properties to JSON + jsonData, err := json.Marshal(fileProps) + if err != nil { + utils.SendErrorResponse(w, "unable to marshal JSON") + return + } + + // Set response headers and send the JSON response + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + w.Write(jsonData) +} + +// HandleFileDelete deletes a file or directory +func (fm *FileManager) HandleFileDelete(w http.ResponseWriter, r *http.Request) { + // Parse the target file or directory path from the request + filePath, err := utils.PostPara(r, "target") + if err != nil { + utils.SendErrorResponse(w, "invalid file path") + return + } + + // Construct the absolute path to the target file or directory + absPath := filepath.Join(fm.Directory, filePath) + + // Check if the target path exists + _, err = os.Stat(absPath) + if err != nil { + utils.SendErrorResponse(w, "file or directory does not exist") + return + } + + // Delete the file or directory + err = os.RemoveAll(absPath) + if err != nil { + utils.SendErrorResponse(w, "error deleting file or directory") + return + } + + // Respond with a success message or appropriate response + utils.SendOK(w) +} diff --git a/src/mod/webserv/filemanager/utils.go b/src/mod/webserv/filemanager/utils.go new file mode 100644 index 0000000..bfa8333 --- /dev/null +++ b/src/mod/webserv/filemanager/utils.go @@ -0,0 +1,156 @@ +package filemanager + +import ( + "io" + "os" + "path/filepath" + "strings" +) + +// isValidFilename checks if a given filename is safe and valid. +func isValidFilename(filename string) bool { + // Define a list of disallowed characters and reserved names + disallowedChars := []string{"/", "\\", ":", "*", "?", "\"", "<", ">", "|"} // Add more if needed + reservedNames := []string{"CON", "PRN", "AUX", "NUL", "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9", "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9"} // Add more if needed + + // Check for disallowed characters + for _, char := range disallowedChars { + if strings.Contains(filename, char) { + return false + } + } + + // Check for reserved names (case-insensitive) + lowerFilename := strings.ToUpper(filename) + for _, reserved := range reservedNames { + if lowerFilename == reserved { + return false + } + } + + // Check for empty filename + if filename == "" { + return false + } + + // The filename is considered valid + return true +} + +// sanitizeFilename sanitizes a given filename by removing disallowed characters. +func sanitizeFilename(filename string) string { + disallowedChars := []string{"/", "\\", ":", "*", "?", "\"", "<", ">", "|"} // Add more if needed + + // Replace disallowed characters with underscores + for _, char := range disallowedChars { + filename = strings.ReplaceAll(filename, char, "_") + } + + return filename +} + +// copyFile copies a single file from source to destination +func copyFile(srcPath, destPath string) error { + srcFile, err := os.Open(srcPath) + if err != nil { + return err + } + defer srcFile.Close() + + destFile, err := os.Create(destPath) + if err != nil { + return err + } + defer destFile.Close() + + _, err = io.Copy(destFile, srcFile) + if err != nil { + return err + } + + return nil +} + +// copyDirectory recursively copies a directory and its contents from source to destination +func copyDirectory(srcPath, destPath string) error { + // Create the destination directory + err := os.MkdirAll(destPath, os.ModePerm) + if err != nil { + return err + } + + entries, err := os.ReadDir(srcPath) + if err != nil { + return err + } + + for _, entry := range entries { + srcEntryPath := filepath.Join(srcPath, entry.Name()) + destEntryPath := filepath.Join(destPath, entry.Name()) + + if entry.IsDir() { + err := copyDirectory(srcEntryPath, destEntryPath) + if err != nil { + return err + } + } else { + err := copyFile(srcEntryPath, destEntryPath) + if err != nil { + return err + } + } + } + + return nil +} + +// isDir checks if the given path is a directory +func isDir(path string) bool { + fileInfo, err := os.Stat(path) + if err != nil { + return false + } + return fileInfo.IsDir() +} + +// calculateDirectorySize calculates the total size of a directory and its contents +func calculateDirectorySize(dirPath string) (int64, error) { + var totalSize int64 + err := filepath.Walk(dirPath, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + totalSize += info.Size() + return nil + }) + if err != nil { + return 0, err + } + return totalSize, nil +} + +// countSubFilesAndFolders counts the number of sub-files and sub-folders within a directory +func countSubFilesAndFolders(dirPath string) (int, int, error) { + var numSubFiles, numSubFolders int + + err := filepath.Walk(dirPath, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + + if info.IsDir() { + numSubFolders++ + } else { + numSubFiles++ + } + + return nil + }) + + if err != nil { + return 0, 0, err + } + + // Subtract 1 from numSubFolders to exclude the root directory itself + return numSubFiles, numSubFolders - 1, nil +} diff --git a/src/mod/webserv/webserv.go b/src/mod/webserv/webserv.go index 542810c..9eb7ee7 100644 --- a/src/mod/webserv/webserv.go +++ b/src/mod/webserv/webserv.go @@ -13,6 +13,7 @@ import ( "imuslab.com/zoraxy/mod/database" "imuslab.com/zoraxy/mod/utils" + "imuslab.com/zoraxy/mod/webserv/filemanager" ) /* @@ -33,6 +34,8 @@ type WebServerOptions struct { } type WebServer struct { + FileManager *filemanager.FileManager + mux *http.ServeMux server *http.Server option *WebServerOptions @@ -55,13 +58,21 @@ func NewWebServer(options *WebServerOptions) *WebServer { } + //Create a new file manager if it is enabled + var newDirManager *filemanager.FileManager + if options.EnableWebDirManager { + fm := filemanager.NewFileManager(filepath.Join(options.WebRoot, "/html")) + newDirManager = fm + } + //Create new table to store the config options.Sysdb.NewTable("webserv") return &WebServer{ - mux: http.NewServeMux(), - option: options, - isRunning: false, - mu: sync.Mutex{}, + mux: http.NewServeMux(), + FileManager: newDirManager, + option: options, + isRunning: false, + mu: sync.Mutex{}, } } @@ -90,6 +101,10 @@ func (ws *WebServer) RestorePreviousState() { // ChangePort changes the server's port. func (ws *WebServer) ChangePort(port string) error { + if IsPortInUse(port) { + return errors.New("Selected port is used by another process") + } + if ws.isRunning { if err := ws.Stop(); err != nil { return err diff --git a/src/web/components/cert.html b/src/web/components/cert.html index 5f4b75e..77cea78 100644 --- a/src/web/components/cert.html +++ b/src/web/components/cert.html @@ -77,7 +77,6 @@ -

Sub-domain Certificates

@@ -85,11 +84,36 @@ depending on your certificates coverage, you might need to setup them one by one (i.e. having two seperate certificate for a.example.com and b.example.com).
If you have a wildcard certificate that covers *.example.com, you can just enter example.com as server name in the form below to add a certificate.
+
+

Certificate Authority (CA) and Auto Renew (ACME)

+

Management features regarding CA and ACME

+

The default CA to use when create a new subdomain proxy endpoint with TLS certificate

+
+
+ +
+ +

+ +
Certificate Renew / Generation (ACME) Settings
+

This tool provide you a graphical interface to setup auto certificate renew on your (sub)domains. You can also manually generate a certificate if one of your domain do not have certificate.

+