mirror of
https://github.com/tobychui/zoraxy.git
synced 2025-06-06 07:37:21 +02:00
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
This commit is contained in:
parent
b63a0fc246
commit
fd6ba56143
18
src/api.go
18
src/api.go
@ -177,16 +177,14 @@ func initAPIs() {
|
|||||||
authRouter.HandleFunc("/api/webserv/setDirList", staticWebServer.SetEnableDirectoryListing)
|
authRouter.HandleFunc("/api/webserv/setDirList", staticWebServer.SetEnableDirectoryListing)
|
||||||
if *allowWebFileManager {
|
if *allowWebFileManager {
|
||||||
//Web Directory Manager file operation functions
|
//Web Directory Manager file operation functions
|
||||||
/*
|
authRouter.HandleFunc("/api/fs/list", staticWebServer.FileManager.HandleList)
|
||||||
authRouter.HandleFunc("/api/fs/list", staticWebServer.HandleGetStatus)
|
authRouter.HandleFunc("/api/fs/upload", staticWebServer.FileManager.HandleUpload)
|
||||||
authRouter.HandleFunc("/api/fs/upload", staticWebServer.HandleGetStatus)
|
authRouter.HandleFunc("/api/fs/download", staticWebServer.FileManager.HandleDownload)
|
||||||
authRouter.HandleFunc("/api/fs/download", staticWebServer.HandleGetStatus)
|
authRouter.HandleFunc("/api/fs/newFolder", staticWebServer.FileManager.HandleNewFolder)
|
||||||
authRouter.HandleFunc("/api/fs/newFolder", staticWebServer.HandleGetStatus)
|
authRouter.HandleFunc("/api/fs/copy", staticWebServer.FileManager.HandleFileCopy)
|
||||||
authRouter.HandleFunc("/api/fs/copy", staticWebServer.HandleGetStatus)
|
authRouter.HandleFunc("/api/fs/move", staticWebServer.FileManager.HandleFileMove)
|
||||||
authRouter.HandleFunc("/api/fs/move", staticWebServer.HandleGetStatus)
|
authRouter.HandleFunc("/api/fs/properties", staticWebServer.FileManager.HandleFileProperties)
|
||||||
authRouter.HandleFunc("/api/fs/properties", staticWebServer.HandleGetStatus)
|
authRouter.HandleFunc("/api/fs/del", staticWebServer.FileManager.HandleFileDelete)
|
||||||
authRouter.HandleFunc("/api/fs/del", staticWebServer.HandleGetStatus)
|
|
||||||
*/
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//Others
|
//Others
|
||||||
|
@ -49,7 +49,7 @@ var (
|
|||||||
name = "Zoraxy"
|
name = "Zoraxy"
|
||||||
version = "2.6.7"
|
version = "2.6.7"
|
||||||
nodeUUID = "generic"
|
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()
|
bootTime = time.Now().Unix()
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -10,7 +10,6 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"encoding/pem"
|
"encoding/pem"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
|
||||||
"log"
|
"log"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"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
|
// Each certificate comes back with the cert bytes, the bytes of the client's
|
||||||
// private key, and a certificate URL.
|
// 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 {
|
if err != nil {
|
||||||
log.Println(err)
|
log.Println(err)
|
||||||
return false, 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 {
|
if err != nil {
|
||||||
log.Println(err)
|
log.Println(err)
|
||||||
return false, err
|
return false, err
|
||||||
|
406
src/mod/webserv/filemanager/filemanager.go
Normal file
406
src/mod/webserv/filemanager/filemanager.go
Normal file
@ -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)
|
||||||
|
}
|
156
src/mod/webserv/filemanager/utils.go
Normal file
156
src/mod/webserv/filemanager/utils.go
Normal file
@ -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
|
||||||
|
}
|
@ -13,6 +13,7 @@ import (
|
|||||||
|
|
||||||
"imuslab.com/zoraxy/mod/database"
|
"imuslab.com/zoraxy/mod/database"
|
||||||
"imuslab.com/zoraxy/mod/utils"
|
"imuslab.com/zoraxy/mod/utils"
|
||||||
|
"imuslab.com/zoraxy/mod/webserv/filemanager"
|
||||||
)
|
)
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@ -33,6 +34,8 @@ type WebServerOptions struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type WebServer struct {
|
type WebServer struct {
|
||||||
|
FileManager *filemanager.FileManager
|
||||||
|
|
||||||
mux *http.ServeMux
|
mux *http.ServeMux
|
||||||
server *http.Server
|
server *http.Server
|
||||||
option *WebServerOptions
|
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
|
//Create new table to store the config
|
||||||
options.Sysdb.NewTable("webserv")
|
options.Sysdb.NewTable("webserv")
|
||||||
return &WebServer{
|
return &WebServer{
|
||||||
mux: http.NewServeMux(),
|
mux: http.NewServeMux(),
|
||||||
option: options,
|
FileManager: newDirManager,
|
||||||
isRunning: false,
|
option: options,
|
||||||
mu: sync.Mutex{},
|
isRunning: false,
|
||||||
|
mu: sync.Mutex{},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -90,6 +101,10 @@ func (ws *WebServer) RestorePreviousState() {
|
|||||||
|
|
||||||
// ChangePort changes the server's port.
|
// ChangePort changes the server's port.
|
||||||
func (ws *WebServer) ChangePort(port string) error {
|
func (ws *WebServer) ChangePort(port string) error {
|
||||||
|
if IsPortInUse(port) {
|
||||||
|
return errors.New("Selected port is used by another process")
|
||||||
|
}
|
||||||
|
|
||||||
if ws.isRunning {
|
if ws.isRunning {
|
||||||
if err := ws.Stop(); err != nil {
|
if err := ws.Stop(); err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -77,7 +77,6 @@
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<button class="ui basic button" onclick="initManagedDomainCertificateList();"><i class="green refresh icon"></i> Refresh List</button>
|
<button class="ui basic button" onclick="initManagedDomainCertificateList();"><i class="green refresh icon"></i> Refresh List</button>
|
||||||
<button class="ui basic button" onclick="openACMEManager();"><i class="yellow refresh icon"></i> Auto Renew (ACME) Settings</button>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="ui message">
|
<div class="ui message">
|
||||||
<h4><i class="info circle icon"></i> Sub-domain Certificates</h4>
|
<h4><i class="info circle icon"></i> Sub-domain Certificates</h4>
|
||||||
@ -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 <code>a.example.com</code> and <code>b.example.com</code>).<br>
|
depending on your certificates coverage, you might need to setup them one by one (i.e. having two seperate certificate for <code>a.example.com</code> and <code>b.example.com</code>).<br>
|
||||||
If you have a wildcard certificate that covers <code>*.example.com</code>, you can just enter <code>example.com</code> as server name in the form below to add a certificate.
|
If you have a wildcard certificate that covers <code>*.example.com</code>, you can just enter <code>example.com</code> as server name in the form below to add a certificate.
|
||||||
</div>
|
</div>
|
||||||
|
<div class="ui divider"></div>
|
||||||
|
<h4>Certificate Authority (CA) and Auto Renew (ACME)</h4>
|
||||||
|
<p>Management features regarding CA and ACME</p>
|
||||||
|
<p>The default CA to use when create a new subdomain proxy endpoint with TLS certificate</p>
|
||||||
|
<div class="ui fluid form">
|
||||||
|
<div class="field">
|
||||||
|
<div class="ui selection dropdown" id="defaultCA">
|
||||||
|
<input type="hidden" name="defaultCA">
|
||||||
|
<i class="dropdown icon"></i>
|
||||||
|
<div class="default text">Let's Encrypt</div>
|
||||||
|
<div class="menu">
|
||||||
|
<div class="item" data-value="Let's Encrypt">Let's Encrypt</div>
|
||||||
|
<div class="item" data-value="Buypass">Buypass</div>
|
||||||
|
<div class="item" data-value="ZeroSSL">ZeroSSL</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button class="ui basic icon button" onclick="saveDefaultCA();"><i class="ui blue save icon"></i> Save Settings</button>
|
||||||
|
</div><br>
|
||||||
|
|
||||||
|
<h5>Certificate Renew / Generation (ACME) Settings</h5>
|
||||||
|
<p>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.</p>
|
||||||
|
<button class="ui basic button" onclick="openACMEManager();"><i class="yellow external icon"></i> Open ACME Tool</button>
|
||||||
</div>
|
</div>
|
||||||
<script>
|
<script>
|
||||||
var uploadPendingPublicKey = undefined;
|
var uploadPendingPublicKey = undefined;
|
||||||
var uploadPendingPrivateKey = undefined;
|
var uploadPendingPrivateKey = undefined;
|
||||||
|
|
||||||
|
$("#defaultCA").dropdown();
|
||||||
|
|
||||||
//Delete the certificate by its domain
|
//Delete the certificate by its domain
|
||||||
function deleteCertificate(domain){
|
function deleteCertificate(domain){
|
||||||
if (confirm("Confirm delete certificate for " + domain + " ?")){
|
if (confirm("Confirm delete certificate for " + domain + " ?")){
|
||||||
@ -110,6 +134,11 @@
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function saveDefaultCA(){
|
||||||
|
//TODO: Add an endpoint to handle default CA set and load
|
||||||
|
alert("WIP");
|
||||||
|
}
|
||||||
|
|
||||||
//List the stored certificates
|
//List the stored certificates
|
||||||
function initManagedDomainCertificateList(){
|
function initManagedDomainCertificateList(){
|
||||||
$.get("/api/cert/list?date=true", function(data){
|
$.get("/api/cert/list?date=true", function(data){
|
||||||
|
@ -14,6 +14,13 @@
|
|||||||
<label>Root require TLS connection <br><small>Check this if your proxy root URL starts with https://</small></label>
|
<label>Root require TLS connection <br><small>Check this if your proxy root URL starts with https://</small></label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="ui horizontal divider">OR</div>
|
||||||
|
<div class="field">
|
||||||
|
<div class="ui checkbox">
|
||||||
|
<input type="checkbox" id="useStaticWebServer" onchange="handleUseStaticWebServerAsRoot()">
|
||||||
|
<label>Use Static Web Server as Root <br><small>Check this if you prefer a more Apache Web Server like experience</small></label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<br>
|
<br>
|
||||||
<button class="ui basic button" onclick="setProxyRoot()"><i class="teal home icon" ></i> Update Proxy Root</button>
|
<button class="ui basic button" onclick="setProxyRoot()"><i class="teal home icon" ></i> Update Proxy Root</button>
|
||||||
<div class="ui divider"></div>
|
<div class="ui divider"></div>
|
||||||
@ -58,6 +65,30 @@
|
|||||||
<script>
|
<script>
|
||||||
$("#advanceRootSettings").accordion();
|
$("#advanceRootSettings").accordion();
|
||||||
|
|
||||||
|
function handleUseStaticWebServerAsRoot(){
|
||||||
|
let useStaticWebServer = $("#useStaticWebServer")[0].checked;
|
||||||
|
if (useStaticWebServer){
|
||||||
|
let staticWebServerURL = "127.0.0.1:" + $("#webserv_listenPort").val();
|
||||||
|
$("#proxyRoot").val(staticWebServerURL);
|
||||||
|
$("#proxyRoot").parent().addClass("disabled");
|
||||||
|
$("#rootReqTLS").parent().checkbox("set unchecked");
|
||||||
|
$("#rootReqTLS").parent().addClass("disabled");
|
||||||
|
|
||||||
|
//Check if web server is enabled. If not, ask if the user want to enable it
|
||||||
|
if (!$("#webserv_enable").parent().checkbox("is checked")){
|
||||||
|
confirmBox("Enable static web server now?", function(choice){
|
||||||
|
if (choice == true){
|
||||||
|
$("#webserv_enable").parent().checkbox("set checked");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}else{
|
||||||
|
$("#rootReqTLS").parent().removeClass("disabled");
|
||||||
|
$("#proxyRoot").parent().removeClass("disabled");
|
||||||
|
initRootInfo();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function initRootInfo(){
|
function initRootInfo(){
|
||||||
$.get("/api/proxy/list?type=root", function(data){
|
$.get("/api/proxy/list?type=root", function(data){
|
||||||
if (data == null){
|
if (data == null){
|
||||||
@ -68,7 +99,6 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
initRootInfo();
|
initRootInfo();
|
||||||
|
|
||||||
|
@ -188,6 +188,8 @@
|
|||||||
msgbox("Requesting certificate via Let's Encrypt...");
|
msgbox("Requesting certificate via Let's Encrypt...");
|
||||||
console.log("Trying to get a new certificate via ACME");
|
console.log("Trying to get a new certificate via ACME");
|
||||||
obtainCertificate(rootname);
|
obtainCertificate(rootname);
|
||||||
|
}else{
|
||||||
|
msgbox("Proxy Endpoint Added");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}else{
|
}else{
|
||||||
@ -467,7 +469,7 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Obtain certificate from API, only support one domain
|
// Obtain certificate from API, only support one domain
|
||||||
function obtainCertificate(domains) {
|
function obtainCertificate(domains, usingCa = "Let's Encrypt") {
|
||||||
let filename = "";
|
let filename = "";
|
||||||
let email = acmeEmail;
|
let email = acmeEmail;
|
||||||
if (acmeEmail == ""){
|
if (acmeEmail == ""){
|
||||||
@ -494,7 +496,7 @@
|
|||||||
domains: domains,
|
domains: domains,
|
||||||
filename: filename,
|
filename: filename,
|
||||||
email: email,
|
email: email,
|
||||||
ca: "Let's Encrypt",
|
ca: usingCa,
|
||||||
},
|
},
|
||||||
success: function(response) {
|
success: function(response) {
|
||||||
if (response.error) {
|
if (response.error) {
|
||||||
|
@ -78,6 +78,16 @@
|
|||||||
<h2>Web Directory Manager</h2>
|
<h2>Web Directory Manager</h2>
|
||||||
<p>Manage your files inside your web directory</p>
|
<p>Manage your files inside your web directory</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="ui basic segment" style="display:none;" id="webdirManDisabledNotice">
|
||||||
|
<h4 class="ui header">
|
||||||
|
<i class="ui red times icon"></i>
|
||||||
|
<div class="content">
|
||||||
|
Web Directory Manager Disabled
|
||||||
|
<div class="sub header">Web Directory Manager has been disabled by the system administrator</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
<iframe id="webserv_dirManager" src="tools/fs.html" style="width: 100%; height: 800px; border: 0px; overflow-y: hidden;">
|
<iframe id="webserv_dirManager" src="tools/fs.html" style="width: 100%; height: 800px; border: 0px; overflow-y: hidden;">
|
||||||
|
|
||||||
</iframe>
|
</iframe>
|
||||||
@ -100,8 +110,14 @@
|
|||||||
$("#webservRunningState").find(".webserv_status").text("Stopped");
|
$("#webservRunningState").find(".webserv_status").text("Stopped");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateWebServState(){
|
function updateWebServState(){
|
||||||
$.get("/api/webserv/status", function(data){
|
$.get("/api/webserv/status", function(data){
|
||||||
|
//Clear all event listeners
|
||||||
|
$("#webserv_enableDirList").off("change");
|
||||||
|
$("#webserv_enable").off("change");
|
||||||
|
$("#webserv_listenPort").off("change");
|
||||||
|
|
||||||
setWebServerRunningState(data.Running);
|
setWebServerRunningState(data.Running);
|
||||||
|
|
||||||
if (data.EnableDirectoryListing){
|
if (data.EnableDirectoryListing){
|
||||||
@ -113,6 +129,7 @@
|
|||||||
$("#webserv_docRoot").val(data.WebRoot + "/html/");
|
$("#webserv_docRoot").val(data.WebRoot + "/html/");
|
||||||
|
|
||||||
if (!data.EnableWebDirManager){
|
if (!data.EnableWebDirManager){
|
||||||
|
$("#webdirManDisabledNotice").show();
|
||||||
$("#webserv_dirManager").remove();
|
$("#webserv_dirManager").remove();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -141,8 +158,8 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
$("#webserv_enableDirList").off("change").on("change", function(){
|
$("#webserv_enableDirList").off("change").on("change", function(){
|
||||||
let enable = $(this)[0].checked;
|
let enable = $(this)[0].checked;
|
||||||
$.ajax({
|
$.ajax({
|
||||||
@ -160,18 +177,46 @@
|
|||||||
});
|
});
|
||||||
$("#webserv_listenPort").off("change").on("change", function(){
|
$("#webserv_listenPort").off("change").on("change", function(){
|
||||||
let newPort = $(this).val();
|
let newPort = $(this).val();
|
||||||
$.ajax({
|
|
||||||
url: "/api/webserv/setPort",
|
//Check if the new value is same as listening port
|
||||||
method: "POST",
|
let rpListeningPort = $("#incomingPort").val();
|
||||||
data: {"port": newPort},
|
if (rpListeningPort == newPort){
|
||||||
success: function(data){
|
confirmBox("This setting might cause port conflict. Continue Anyway?", function(choice){
|
||||||
if (data.error != undefined){
|
if (choice == true){
|
||||||
msgbox(data.error, false);
|
//Continue anyway
|
||||||
|
$.ajax({
|
||||||
|
url: "/api/webserv/setPort",
|
||||||
|
method: "POST",
|
||||||
|
data: {"port": newPort},
|
||||||
|
success: function(data){
|
||||||
|
if (data.error != undefined){
|
||||||
|
msgbox(data.error, false);
|
||||||
|
}else{
|
||||||
|
msgbox("Listening port updated");
|
||||||
|
}
|
||||||
|
updateWebServState();
|
||||||
|
}
|
||||||
|
});
|
||||||
}else{
|
}else{
|
||||||
msgbox("Listening port updated");
|
//Cancel. Restore to previous value
|
||||||
|
updateWebServState();
|
||||||
|
msgbox("Setting restored");
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
})
|
}else{
|
||||||
|
$.ajax({
|
||||||
|
url: "/api/webserv/setPort",
|
||||||
|
method: "POST",
|
||||||
|
data: {"port": newPort},
|
||||||
|
success: function(data){
|
||||||
|
if (data.error != undefined){
|
||||||
|
msgbox(data.error, false);
|
||||||
|
}else{
|
||||||
|
msgbox("Listening port updated");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
10
src/web/components/zgrok.html
Normal file
10
src/web/components/zgrok.html
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
<div class="standardContainer">
|
||||||
|
<div class="ui basic segment">
|
||||||
|
<h2>Service Expose Proxy</h2>
|
||||||
|
<p>Expose your local test-site on the internet with single command</p>
|
||||||
|
</div>
|
||||||
|
<div class="ui message">
|
||||||
|
<h4>Work In Progress</h4>
|
||||||
|
We are looking for someone to help with implementing this feature in Zoraxy. <br>If you know how to write Golang and want to contribute, feel free to create a pull request to this feature!
|
||||||
|
</div>
|
||||||
|
</div>
|
@ -62,7 +62,7 @@
|
|||||||
<a class="item" tag="gan">
|
<a class="item" tag="gan">
|
||||||
<i class="simplistic globe icon"></i> Global Area Network
|
<i class="simplistic globe icon"></i> Global Area Network
|
||||||
</a>
|
</a>
|
||||||
<a class="item" tag="">
|
<a class="item" tag="zgrok">
|
||||||
<i class="simplistic podcast icon"></i> Service Expose Proxy
|
<i class="simplistic podcast icon"></i> Service Expose Proxy
|
||||||
</a>
|
</a>
|
||||||
<a class="item" tag="tcpprox">
|
<a class="item" tag="tcpprox">
|
||||||
@ -117,6 +117,9 @@
|
|||||||
<!-- Global Area Networking -->
|
<!-- Global Area Networking -->
|
||||||
<div id="gan" class="functiontab" target="gan.html"></div>
|
<div id="gan" class="functiontab" target="gan.html"></div>
|
||||||
|
|
||||||
|
<!-- Service Expose Proxy -->
|
||||||
|
<div id="zgrok" class="functiontab" target="zgrok.html"></div>
|
||||||
|
|
||||||
<!-- TCP Proxy -->
|
<!-- TCP Proxy -->
|
||||||
<div id="tcpprox" class="functiontab" target="tcpprox.html"></div>
|
<div id="tcpprox" class="functiontab" target="tcpprox.html"></div>
|
||||||
|
|
||||||
|
@ -78,10 +78,6 @@
|
|||||||
<br>
|
<br>
|
||||||
</div>
|
</div>
|
||||||
<div id="propertiesView" class="small">
|
<div id="propertiesView" class="small">
|
||||||
<div class="preview" style="margin-top: 0.4em;" align="center">
|
|
||||||
<img class="ui image" style="max-height: 300px;">
|
|
||||||
<audio src="" controls style="display:none; width: 100%;"></audio>
|
|
||||||
</div>
|
|
||||||
<h3 class="ui header" style="margin-top: 0.4em;">
|
<h3 class="ui header" style="margin-top: 0.4em;">
|
||||||
<span class="filename" style="word-break: break-all;" locale="sidebar/default/nofileselected">No File Selected</span>
|
<span class="filename" style="word-break: break-all;" locale="sidebar/default/nofileselected">No File Selected</span>
|
||||||
<div class="sub header vpath" style="word-break: break-all;" locale="sidebar/default/instruction">Select a file to view file properties</div>
|
<div class="sub header vpath" style="word-break: break-all;" locale="sidebar/default/instruction">Select a file to view file properties</div>
|
||||||
@ -91,7 +87,6 @@
|
|||||||
|
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<button id="loadPreviewButton" class="ui small fluid basic disabled button" onclick="loadPreview();">Load Preview</button>
|
|
||||||
</div>
|
</div>
|
||||||
<div id="uploadProgressBar">
|
<div id="uploadProgressBar">
|
||||||
<div class="ui small indicating progress" style="margin-bottom: 0px !important; border-radius: 0 !important;">
|
<div class="ui small indicating progress" style="margin-bottom: 0px !important; border-radius: 0 !important;">
|
||||||
@ -116,9 +111,7 @@
|
|||||||
//File operations
|
//File operations
|
||||||
let cutMode = false;
|
let cutMode = false;
|
||||||
let cutPendingFilepath = [];
|
let cutPendingFilepath = [];
|
||||||
let copyBuffering = false;
|
let copyPendingFiles = [];
|
||||||
let copySrcFilename = "";
|
|
||||||
let copyPendingFile = undefined;
|
|
||||||
|
|
||||||
//History
|
//History
|
||||||
let dirHistory = [];
|
let dirHistory = [];
|
||||||
@ -479,9 +472,9 @@
|
|||||||
msgbox(data.error, false);
|
msgbox(data.error, false);
|
||||||
}else{
|
}else{
|
||||||
data.forEach(function(filedata){
|
data.forEach(function(filedata){
|
||||||
let isDir = filedata.IsDir;
|
let isDir = filedata.isDir;
|
||||||
let filename = filedata.Filename;
|
let filename = filedata.filename;
|
||||||
let filesize = filedata.Filesize;
|
let filesize = filedata.size;
|
||||||
if (isDir){
|
if (isDir){
|
||||||
$("#folderList").append(`<div class="fileObject item" draggable="true" filename="${filename}" filepath="${path + filename}" ondblclick="openthis(this,event);" type="folder">
|
$("#folderList").append(`<div class="fileObject item" draggable="true" filename="${filename}" filepath="${path + filename}" ondblclick="openthis(this,event);" type="folder">
|
||||||
<span style="display:inline-block !important;word-break: break-all; width:100%;" class="normal object">
|
<span style="display:inline-block !important;word-break: break-all; width:100%;" class="normal object">
|
||||||
@ -527,46 +520,7 @@
|
|||||||
listDir(targetPath);
|
listDir(targetPath);
|
||||||
}else{
|
}else{
|
||||||
let ext = $(target).attr("filepath").split(".").pop();
|
let ext = $(target).attr("filepath").split(".").pop();
|
||||||
if (ext == "txt" || ext == "md"){
|
window.open("/api/fs/download?file=" + $(target).attr("filepath") + "&preview=true");
|
||||||
//Open with markdown editor
|
|
||||||
let hash = encodeURIComponent(JSON.stringify({
|
|
||||||
"filename": $(target).attr("filename"),
|
|
||||||
"filepath": $(target).attr("filepath")
|
|
||||||
}))
|
|
||||||
window.open("mde/index.html#" + hash);
|
|
||||||
}else if (ext == "jpg" || ext == "jpeg" || ext == "png" || ext == "gif" || ext == "webp"){
|
|
||||||
//Open with photo viewer
|
|
||||||
let hash = encodeURIComponent(JSON.stringify({
|
|
||||||
"filename": $(target).attr("filename"),
|
|
||||||
"filepath": $(target).attr("filepath")
|
|
||||||
}))
|
|
||||||
window.open("photo.html#" + hash);
|
|
||||||
}else if (ext == "mp3" || ext == "aac" || ext == "ogg"){
|
|
||||||
//Open with music player
|
|
||||||
let hash = encodeURIComponent(JSON.stringify({
|
|
||||||
"filename": $(target).attr("filename"),
|
|
||||||
"filepath": $(target).attr("filepath")
|
|
||||||
}))
|
|
||||||
window.open("music.html#" + hash);
|
|
||||||
}else if (ext == "mp4" || ext == "webm"){
|
|
||||||
//Open with video player
|
|
||||||
let hash = encodeURIComponent(JSON.stringify({
|
|
||||||
"filename": $(target).attr("filename"),
|
|
||||||
"filepath": $(target).attr("filepath")
|
|
||||||
}))
|
|
||||||
window.open("video.html#" + hash);
|
|
||||||
}else if (isCodeFiles(ext)){
|
|
||||||
//Open with notepad
|
|
||||||
//**Notes the array wrapper in JSON object**
|
|
||||||
let hash = encodeURIComponent(JSON.stringify([{
|
|
||||||
"filename": $(target).attr("filename"),
|
|
||||||
"filepath": $(target).attr("filepath")
|
|
||||||
}]))
|
|
||||||
window.open("notepad/index.html#" + hash);
|
|
||||||
}else{
|
|
||||||
window.open("/api/fs/download?file=" + $(target).attr("filepath") + "&preview=true");
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -600,18 +554,6 @@
|
|||||||
let ext = filenameOnly.pop();
|
let ext = filenameOnly.pop();
|
||||||
filenameOnly = filenameOnly.join(".");
|
filenameOnly = filenameOnly.join(".");
|
||||||
|
|
||||||
/*
|
|
||||||
//Skip checking as modern FAT32 file systems can store as LONG FILENAME
|
|
||||||
if (filenameOnly.length > 8){
|
|
||||||
msgbox("File name too long (8 char max)", false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ext.length > 3){
|
|
||||||
msgbox("File extension too long (3 char max)", false);
|
|
||||||
return
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
//OK! Create the file
|
//OK! Create the file
|
||||||
const blob = new Blob(["\n"], { type: 'text/plain' });
|
const blob = new Blob(["\n"], { type: 'text/plain' });
|
||||||
@ -622,18 +564,13 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function newFolder(){
|
function newFolder(){
|
||||||
var folderName = window.prompt("Name for new folder (8 char max): ", "Folder");
|
var folderName = window.prompt("Name for new folder: ", "Folder");
|
||||||
if (folderName.indexOf("/") >= 0){
|
if (folderName.indexOf("/") >= 0){
|
||||||
//Contains /. Reject
|
//Contains /. Reject
|
||||||
msgbox("Folder name cannot contain path seperator", false);
|
msgbox("Folder name cannot contain path seperator", false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (folderName.length > 8){
|
|
||||||
msgbox("Folder name too long (8 char max)", false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$.post("/api/fs/newFolder?path=" + currentPath + folderName, function(data){
|
$.post("/api/fs/newFolder?path=" + currentPath + folderName, function(data){
|
||||||
if (data.error != undefined){
|
if (data.error != undefined){
|
||||||
msgbox(data.error, false);
|
msgbox(data.error, false);
|
||||||
@ -657,19 +594,11 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
//FAT32 allows filename longer than 8.3
|
|
||||||
if (!isFilenameValid(newName)){
|
|
||||||
msgbox("File name too long (8 char max)", false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
if (newName && newName != oldName) {
|
if (newName && newName != oldName) {
|
||||||
// User entered a new name, perform renaming logic here
|
// User entered a new name, perform renaming logic here
|
||||||
console.log(oldPath, currentPath + newName);
|
console.log(oldPath, currentPath + newName);
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: "/api/fs/move?src=" + oldPath + "&dest=" + currentPath + newName,
|
url: "/api/fs/move?srcpath=" + oldPath + "&destpath=" + currentPath + newName,
|
||||||
method: "POST",
|
method: "POST",
|
||||||
success: function(data){
|
success: function(data){
|
||||||
if (data.error != undefined){
|
if (data.error != undefined){
|
||||||
@ -729,7 +658,7 @@
|
|||||||
File Size
|
File Size
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
${bytesToSize(data.filesize)}
|
${bytesToSize(data.size)}
|
||||||
</td>
|
</td>
|
||||||
</tr><tr>
|
</tr><tr>
|
||||||
<td style="${styleOverwrite}">
|
<td style="${styleOverwrite}">
|
||||||
@ -764,19 +693,6 @@
|
|||||||
</td>
|
</td>
|
||||||
</tr>`);
|
</tr>`);
|
||||||
|
|
||||||
//Folder is not previewable
|
|
||||||
$("#propertiesView").find(".preview").find("img").attr("xsrc", "");
|
|
||||||
$("#loadPreviewButton").addClass("disabled");
|
|
||||||
}else{
|
|
||||||
let ext = data.filepath.split(".").pop();
|
|
||||||
if (isPreviewable("." + ext)){
|
|
||||||
$("#propertiesView").find(".preview").find("img").attr("xsrc", "/api/fs/download?preview=true&file=" + data.filepath);
|
|
||||||
$("#loadPreviewButton").removeClass("disabled");
|
|
||||||
}else{
|
|
||||||
$("#propertiesView").find(".preview").find("img").attr("xsrc", "");
|
|
||||||
$("#loadPreviewButton").addClass("disabled");
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -825,7 +741,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function humanFileSize(bytes, si=false, dp=1) {
|
function humanFileSize(bytes, si=true, dp=1) {
|
||||||
const thresh = si ? 1000 : 1024;
|
const thresh = si ? 1000 : 1024;
|
||||||
|
|
||||||
if (Math.abs(bytes) < thresh) {
|
if (Math.abs(bytes) < thresh) {
|
||||||
@ -888,7 +804,7 @@
|
|||||||
$("#pasteButton").addClass("disabled");
|
$("#pasteButton").addClass("disabled");
|
||||||
console.log('Selected file:', file);
|
console.log('Selected file:', file);
|
||||||
var formdata = new FormData();
|
var formdata = new FormData();
|
||||||
formdata.append("file1", file);
|
formdata.append("file", file);
|
||||||
var ajax = new XMLHttpRequest();
|
var ajax = new XMLHttpRequest();
|
||||||
ajax.upload.addEventListener("progress", progressHandler, false);
|
ajax.upload.addEventListener("progress", progressHandler, false);
|
||||||
ajax.addEventListener("load", function(event){
|
ajax.addEventListener("load", function(event){
|
||||||
@ -909,7 +825,7 @@
|
|||||||
}, false); // doesnt appear to ever get called even upon success
|
}, false); // doesnt appear to ever get called even upon success
|
||||||
ajax.addEventListener("error", errorHandler, false);
|
ajax.addEventListener("error", errorHandler, false);
|
||||||
ajax.addEventListener("abort", abortHandler, false);
|
ajax.addEventListener("abort", abortHandler, false);
|
||||||
ajax.open("POST", "/upload?dir=" + dir);
|
ajax.open("POST", "/api/fs/upload?dir=" + dir);
|
||||||
ajax.send(formdata);
|
ajax.send(formdata);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -961,48 +877,23 @@
|
|||||||
|
|
||||||
//Copy file
|
//Copy file
|
||||||
function copy(){
|
function copy(){
|
||||||
//TODO: Add copy API
|
|
||||||
if ($(".fileObject.selected").length == 0){
|
if ($(".fileObject.selected").length == 0){
|
||||||
//No file selected
|
//No file selected
|
||||||
msgbox("No file selected", false);
|
msgbox("No file selected", false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if ($(".fileObject.selected").length > 1){
|
|
||||||
msgbox("WebStick can only support 1 file copy at a time", false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($(".fileObject.selected").attr('type') == "folder"){
|
let selectedFiles = [];
|
||||||
msgbox("Folder copy is not supported", false);
|
$(".fileObject.selected").each(function(){
|
||||||
return;
|
let filepath = $(this).attr("filepath");
|
||||||
}
|
selectedFiles.push(filepath);
|
||||||
|
console.log(filepath);
|
||||||
let url = "/api/fs/download?file=" + $(".fileObject.selected").attr("filepath");
|
|
||||||
let filename = $(".fileObject.selected").attr("filename");
|
|
||||||
|
|
||||||
cutMode = false;
|
|
||||||
copyBuffering = true;
|
|
||||||
copySrcFilename = filename;
|
|
||||||
|
|
||||||
console.log("Buffering " + filename + " from " + url);
|
|
||||||
msgbox("Allocating memory for copy...")
|
|
||||||
$("#pasteButton").addClass("disabled");
|
|
||||||
$("#copyButton").addClass("disabled");
|
|
||||||
// Fetch the content from the URL
|
|
||||||
fetch(url).then(response => response.blob()).then(blob => {
|
|
||||||
// Create a new File object from the Blob
|
|
||||||
|
|
||||||
copyPendingFile = (blob);
|
|
||||||
msgbox("File Ready to Paste");
|
|
||||||
$("#pasteButton").removeClass("disabled");
|
|
||||||
$("#copyButton").removeClass("disabled");
|
|
||||||
copyBuffering = false;
|
|
||||||
}).catch(error => {
|
|
||||||
msgbox("Copy Allocation Failed", false);
|
|
||||||
$("#pasteButton").removeClass("disabled");
|
|
||||||
$("#copyButton").removeClass("disabled");
|
|
||||||
copyBuffering = false;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
copyPendingFiles = selectedFiles;
|
||||||
|
cutMode = false;
|
||||||
|
|
||||||
|
msgbox(`${selectedFiles.length} files ready to paste`, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
function fileExistsInThisFolder(filename){
|
function fileExistsInThisFolder(filename){
|
||||||
@ -1024,7 +915,7 @@
|
|||||||
let filepath = fileToPaste.filepath;
|
let filepath = fileToPaste.filepath;
|
||||||
|
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: "/api/fs/move?src=" + filepath + "&dest=" + currentPath + filename,
|
url: "/api/fs/move?srcpath=" + filepath + "&destpath=" + currentPath + filename,
|
||||||
method: "POST",
|
method: "POST",
|
||||||
success: function(data){
|
success: function(data){
|
||||||
if (data.error != undefined){
|
if (data.error != undefined){
|
||||||
@ -1041,32 +932,42 @@
|
|||||||
});
|
});
|
||||||
}else{
|
}else{
|
||||||
//Copy and Paste
|
//Copy and Paste
|
||||||
if (copyBuffering){
|
copyFirstItemInQueueUntilAllCopied();
|
||||||
msgbox("Copy buffer allocation in progress", false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let pasteFilename = copySrcFilename;
|
|
||||||
let counter = 1;
|
|
||||||
while(fileExistsInThisFolder(pasteFilename)){
|
|
||||||
let nameOnly = copySrcFilename.split(".");
|
|
||||||
let ext = nameOnly.pop();
|
|
||||||
nameOnly = nameOnly.join(".");
|
|
||||||
pasteFilename = nameOnly + "-" + counter + "." + ext;
|
|
||||||
counter++;
|
|
||||||
}
|
|
||||||
|
|
||||||
//Change the name if there is name clash
|
|
||||||
const file = new File([copyPendingFile], pasteFilename);
|
|
||||||
|
|
||||||
//Upload the file
|
|
||||||
msgbox("Writing file to SD card...");
|
|
||||||
handleFile(file, currentPath, function(){
|
|
||||||
msgbox("File Pasted");
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function copyFirstItemInQueueUntilAllCopied(){
|
||||||
|
let file = copyPendingFiles.shift();
|
||||||
|
let startingDir = currentPath;
|
||||||
|
$.ajax({
|
||||||
|
url: "/api/fs/copy",
|
||||||
|
method: "POST",
|
||||||
|
data: {
|
||||||
|
"srcpath": file,
|
||||||
|
"destpath": currentPath
|
||||||
|
},
|
||||||
|
success: function(data){
|
||||||
|
if (data.error != undefined){
|
||||||
|
msgbox(data.error, false);
|
||||||
|
}else{
|
||||||
|
if (copyPendingFiles.length > 0){
|
||||||
|
//Contine to copy and paste the files
|
||||||
|
copyFirstItemInQueueUntilAllCopied();
|
||||||
|
if (startingDir == currentPath){
|
||||||
|
refresh();
|
||||||
|
}
|
||||||
|
}else{
|
||||||
|
//All copy operation done
|
||||||
|
msgbox("Files copied");
|
||||||
|
if (startingDir == currentPath){
|
||||||
|
refresh();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
function sortFileList(){
|
function sortFileList(){
|
||||||
sortFileObjects("folderList");
|
sortFileObjects("folderList");
|
||||||
sortFileObjects("fileList");
|
sortFileObjects("fileList");
|
||||||
|
Loading…
x
Reference in New Issue
Block a user