diff --git a/pkg/httpserver/dirserver.go b/pkg/httpserver/dirserver.go new file mode 100644 index 0000000000000000000000000000000000000000..0ed518d170c9f15ef21a9f79467e25eb4e3b8234 --- /dev/null +++ b/pkg/httpserver/dirserver.go @@ -0,0 +1,266 @@ +/* +Copyright 2023 KylinSoft Co., Ltd. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package httpserver + +import ( + "errors" + "fmt" + "net/http" + "os" + "strings" + "sync" + + "github.com/sirupsen/logrus" +) + +// HttpDirService encapsulates the properties of the HTTP dir service +type HttpDirService struct { + Port string + DirPath string + server *http.Server + running bool + fileCache map[string][]byte + dirCache map[string][]string + mutex sync.RWMutex +} + +// NewDirService creates a new instance of dir service +func NewDirService(port string, dirPath string) *HttpDirService { + return &HttpDirService{ + Port: port, + DirPath: dirPath, + running: false, + fileCache: make(map[string][]byte), + dirCache: make(map[string][]string), + } +} + +// AddFileToCache add file content to the file cache +func (fs *HttpDirService) addFileToCache(filePath string) ([]byte, error) { + // Check if the file exists in the cache + fileContent, ok := fs.fileCache[filePath] + if ok { + return fileContent, nil + } + + // If the file is not in cache, read its contents + content, err := os.ReadFile(filePath) + if err != nil { + return nil, err + } + + // Cache the file content + fs.fileCache[filePath] = content + + return content, nil +} + +// RemoveFileFromCache removes file content from the file cache +func (fs *HttpDirService) removeFileFromCache(filePath string) { + fs.mutex.Lock() + defer fs.mutex.Unlock() + + delete(fs.fileCache, filePath) +} + +func (fs *HttpDirService) addDirToCache(path string) ([]string, error) { + // Check if the directory exists in the cache + dirContents, ok := fs.dirCache[path] + if ok { + return dirContents, nil + } + + // If not in cache, read the directory contents + files, err := os.ReadDir(path) + if err != nil { + return nil, err + } + + // Cache the directory contents + var fileList []string + for _, file := range files { + fileList = append(fileList, file.Name()) + } + fs.dirCache[path] = fileList + + return fileList, nil +} + +func (fs *HttpDirService) removeDirFromCache(dirPath string) { + fs.mutex.Lock() + defer fs.mutex.Unlock() + + delete(fs.dirCache, dirPath) +} + +func (fs *HttpDirService) Start() error { + // Check if the server is already running + if fs.running { + return errors.New("server is already running") + } + + smux := http.NewServeMux() + // Set up HTTP route + smux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + err := fs.handleDirRequest(w, r) + if err != nil { + logrus.Errorf("Error handling dir request: %v", err) + + var statusCode int + var errorMessage string + + if os.IsNotExist(err) { + statusCode = http.StatusNotFound + errorMessage = "File Not Found" + } else { + statusCode = http.StatusInternalServerError + errorMessage = "Internal Server Error" + } + + http.Error(w, errorMessage, statusCode) + return + } + }) + + fs.server = &http.Server{ + Addr: ":" + fs.Port, + Handler: smux, + } + + go func() { + logrus.Infof("HTTP server is listening on port %s...\n", fs.Port) + fs.running = true + if fs.server != nil { + if err := fs.server.ListenAndServe(); err != nil && err != http.ErrServerClosed { + logrus.Errorf("ListenAndServe(): %v", err) + fs.running = false + return + } + } else { + logrus.Error("Server is nil. Cannot start.") + } + }() + + return nil +} + +// handleDirRequest handles dir requests +func (fs *HttpDirService) handleDirRequest(w http.ResponseWriter, r *http.Request) error { + // Get the requested dir path + rpath := r.URL.Path + + if !strings.HasPrefix(rpath, fs.DirPath) { + // Generate HTML page for 403 Forbidden error + w.Header().Set("Content-Type", "text/html") + w.WriteHeader(http.StatusForbidden) + fmt.Fprintf(w, "
You don't have permission to access this resource.
") + fmt.Fprintf(w, "Additionally, a 403 Forbidden error was encountered while trying to use an ErrorDocument to handle the request.") + fmt.Fprintf(w, "") + return nil + } + + fs.mutex.RLock() + defer fs.mutex.RUnlock() + + // Check if the requested path is a directory + rpathInfo, err := os.Stat(rpath) + if err != nil { + return err + } + + if !rpathInfo.IsDir() { + // File request + fileContent, err := fs.addFileToCache(rpath) + if err != nil { + return err + } + + // Set the content type of the file + contentType := http.DetectContentType(fileContent) + w.Header().Set("Content-Type", contentType) + + // Write file content directly into the response + _, err = w.Write(fileContent) + if err != nil { + errMsg := "unable to write file to response: " + err.Error() + return errors.New(errMsg) + } + + return nil + } + + // // Directory request + if !strings.HasSuffix(rpath, "/") { + // Redirect to the directory path with a trailing slash + http.Redirect(w, r, rpath+"/", http.StatusMovedPermanently) + return nil + } + + dirContents, err := fs.addDirToCache(rpath) + if err != nil { + return err + } + + // Generate HTML page with directory contents + w.Header().Set("Content-Type", "text/html") + fmt.Fprintf(w, "