package servlet import ( "errors" util "geniuscartel.xyz/vinegar/vinegarUtil" "log" "net/http" "path" "path/filepath" "strings" ) type ( // FileRoute implements a static file serving route. // It serves files from a given file path. FileRoute struct { // VinegarRoute is the base route containing the URL pattern and handler. VinegarRoute *VinegarWebRoute // srv is the VinegarWebServlet instance that this route is attached to. srv *VinegarWebServlet // fileRoot is the base file path to serve files from. fileRoot string // UseCache indicates whether to use caching for this route. UseCache bool } //RouteConstructor // //Params: // //servlet - The VinegarWebServlet instance to add the route to // //urlPattern - The URL regex pattern for route to match // //pathlike - The file path to serve // //useCache - Whether to use caching for this route // // A RouteConstructor is a function that accepts a VinegarWebServlet, urlPattern, file path, and cache option. It uses // these to construct and return a FileRoute. // The return value is a FileRoute that will serve the files from the given path. // // This function signature allows encapsulating the creation of different types of FileRoutes. It is used to define // constructor functions for each file type, like NewTextRoute or NewImageRoute. RouteConstructor func(servlet *VinegarWebServlet, urlPattern string, pathlike string, useCache bool) (*FileRoute, error) ) // NewTextRoute creates a new FileRoute for serving text files. // // It handles text files as compressible content, gzipping them when the client sends the Accept-Encoding: gzip header. // // Parameters: // // servlet - The VinegarWebServlet instance to attach the route to. // // urlPattern - The URL regex pattern that triggers this route. // // pathlike - The file path on disk to serve files from. // // useCache - Whether to use caching for this route. // // Returns: // // A FileRoute instance configured for serving text files, added to // the provided VinegarWebServlet. var NewTextRoute RouteConstructor = func(servlet *VinegarWebServlet, urlPattern string, pathlike string, useCache bool) (*FileRoute, error) { fileRoot := filepath.Clean(pathlike) if strings.Contains(fileRoot, "../") { return nil, errors.New("Traversing the directory is not allowed, use an absolute filepath instead") } defaultPrune := strings.Replace(urlPattern, ".*", "", -1) route := FileRoute{srv: servlet, fileRoot: fileRoot, UseCache: useCache} textRouteHandler := createCompressibleFileServletFunction(&route, defaultPrune, pathlike) rootRoute := NewServletRoute(urlPattern, textRouteHandler) //i *still* kinda don't like this pattern route.VinegarRoute = rootRoute servlet.Router.AddRoute(route.VinegarRoute) return &route, nil } var NewImageRoute RouteConstructor = func(servlet *VinegarWebServlet, urlPattern string, pathlike string, useCache bool) (*FileRoute, error) { fileRoot := filepath.Clean(pathlike) if strings.Contains(fileRoot, "../") { return nil, errors.New("Traversing the directory is not allowed, use an absolute filepath instead") } defaultPrune := strings.Replace(urlPattern, ".*", "", -1) route := FileRoute{srv: servlet, fileRoot: fileRoot, UseCache: useCache} rootRoute := NewServletRoute(urlPattern, createUncompressedFileServletFunction(&route, defaultPrune, pathlike)) route.VinegarRoute = rootRoute //i *kinda* don't like this pattern servlet.Router.AddRoute(route.VinegarRoute) return &route, nil } var NewSingleFileRoute RouteConstructor = func(servlet *VinegarWebServlet, urlPattern string, pathlike string, useCache bool) (*FileRoute, error) { route := FileRoute{ srv: servlet, fileRoot: pathlike, UseCache: useCache, } singleFileServletHandler := createSingleFileServletFunction(&route) sfCache, err := util.NewSingleFileCache(pathlike) if err != nil { return nil, err } parentRoute := NewServletRoute(urlPattern, singleFileServletHandler) parentRoute.Handler = singleFileServletHandler parentRoute.Cache = sfCache route.VinegarRoute = parentRoute servlet.Router.AddRoute(route.VinegarRoute) return &route, nil } // createSingleFileServletFunction creates a handler function for serving a single file. // // It looks up the file content from the route's cache and writes the appropriate // headers and file content to the response. // // Parameters: // // route - The FileRoute instance this handler is attached to. // // Returns: // // A VinegarHandlerFunction that serves the single file for the provided route. // // The handler function checks the route's cache for the file content. // If caching is enabled, it first checks the cache directly. // If caching is disabled, it calls GetFresh to avoid the cache. // // If the content is not found, it returns a 404 error. // // Otherwise, it sets the Content-Type header based on the cache's MIME type, // and writes the file content to the response. // // If the client accepts gzip encoding, it compresses the content before writing. func createSingleFileServletFunction(route *FileRoute) VinegarHandlerFunction { var fun VinegarHandlerFunction = func(w http.ResponseWriter, req *http.Request) { var cache *util.LruEntry var exists bool if route.UseCache { cache, exists = route.VinegarRoute.Cache.Get("") } else { cache, exists = route.VinegarRoute.Cache.GetFresh("") } if !exists { http.NotFound(w, req) return } var content []byte if clientAcceptsGzip(req) { content = cache.CompressedContent w.Header().Add(ContentEncodingHeaderKey, "gzip") } else { content = cache.Content } w.Header().Add(ContentTypeHeaderKey, cache.MimeType) _, err := w.Write(content) if err != nil { log.Println(err) } } return fun } func createCompressibleFileServletFunction(route *FileRoute, basePattern string, pathlike string) VinegarHandlerFunction { var fun VinegarHandlerFunction = func(w http.ResponseWriter, req *http.Request) { stub := strings.Replace(req.URL.Path, basePattern, "", 1) if strings.Contains(stub, "../") { route.srv.SendError(w, req, 403, "Forbidden", errors.New("Stop trying directory traversal")) return } cachedContent, exists := route.VinegarRoute.Cache.Get(stub) //i don't like this logic below. we need to streamline this a lot better. it's a twisty jungle right now filePath := filepath.Clean(stub) pathRoot := filepath.Clean(pathlike) resourcePath := path.Join(pathRoot, filePath) if !exists { content, err := util.GetDiskContent(resourcePath) if err != nil { http.NotFound(w, req) } if route.UseCache { err := route.VinegarRoute.Cache.Put(stub, resourcePath) if err != nil { route.srv.SendError(w, req, 500, "Internal Server Error", err) } cachedContent, _ = route.VinegarRoute.Cache.Get(stub) } else { w.Header().Add(ContentTypeHeaderKey, util.GuessMimetype(stub)) w.Write(*content) return } } w.Header().Add(ContentTypeHeaderKey, cachedContent.MimeType) var err error = nil if clientAcceptsGzip(req) { w.Header().Add(ContentEncodingHeaderKey, "gzip") _, err = w.Write(cachedContent.CompressedContent) } else { _, err = w.Write(cachedContent.Content) } if err != nil { log.Println(err) } } return fun } func createUncompressedFileServletFunction(route *FileRoute, basePattern string, pathlike string) VinegarHandlerFunction { var fun VinegarHandlerFunction = func(w http.ResponseWriter, req *http.Request) { stub := strings.Replace(req.URL.Path, basePattern, "", 1) if strings.Contains(stub, "../") { route.srv.SendError(w, req, 403, "Forbidden", errors.New("Stop trying directory traversal")) return } rootPath := filepath.Clean(pathlike) filePath := filepath.Clean(stub) resourcePath := path.Join(rootPath, filePath) entry, exists := route.VinegarRoute.Cache.Get(stub) if !exists { route.VinegarRoute.Cache.Put(stub, resourcePath) entry, exists = route.VinegarRoute.Cache.Get(stub) } if exists { w.Header().Add(ContentTypeHeaderKey, util.GuessMimetype(stub)) _, err := w.Write(entry.Content) if err != nil { log.Println(err) } return } else { http.NotFound(w, req) } } return fun } func clientAcceptsGzip(req *http.Request) bool { encodings := req.Header.Get(AcceptEncodingHeaderKey) return strings.Contains(encodings, "gzip") }