vinegar/servlet/staticRoute.go

259 lines
8.2 KiB
Go

package servlet
import (
"errors"
util "geniuscartel.xyz/vinegar/vinegarUtil"
"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 *VinegarRoute
// srv is the VinegarServlet instance that this route is attached to.
srv *VinegarServlet
// 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 VinegarServlet 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 VinegarServlet, 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 *VinegarServlet, urlPattern string, pathlike string, useCache bool) *FileRoute
)
// 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 VinegarServlet 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 VinegarServlet.
var NewTextRoute RouteConstructor = func(servlet *VinegarServlet, urlPattern string, pathlike string, useCache bool) *FileRoute {
fileRoot := filepath.Clean(pathlike)
if strings.Contains(fileRoot, "../") {
panic("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.AddRoute(route.VinegarRoute)
return &route
}
var NewImageRoute RouteConstructor = func(servlet *VinegarServlet, urlPattern string, pathlike string, useCache bool) *FileRoute {
fileRoot := filepath.Clean(pathlike)
if strings.Contains(fileRoot, "../") {
panic("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.AddRoute(route.VinegarRoute)
return &route
}
var NewSingleFileRoute RouteConstructor = func(servlet *VinegarServlet, urlPattern string, pathlike string, useCache bool) *FileRoute {
route := FileRoute{
srv: servlet,
fileRoot: pathlike,
UseCache: useCache,
}
singleFileServletHandler := createSingleFileServletFunction(&route)
sfCache := util.NewSingleFileCache(pathlike)
parentRoute := NewServletRoute(urlPattern, singleFileServletHandler)
parentRoute.Handler = singleFileServletHandler
parentRoute.Cache = sfCache
route.VinegarRoute = parentRoute
servlet.AddRoute(route.VinegarRoute)
return &route
}
// 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 {
route.srv.SendError(w, req, 404, "File not found.", errors.New("could not find file: "+route.fileRoot))
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 {
panic(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, fileExists := util.GetDiskContent(resourcePath)
if fileExists {
if route.UseCache {
route.VinegarRoute.Cache.Put(stub, resourcePath)
cachedContent, _ = route.VinegarRoute.Cache.Get(stub)
} else {
w.Header().Add(ContentTypeHeaderKey, util.GuessMimetype(stub))
w.Write(*content)
return
}
} else {
route.srv.SendError(w, req, 404, "Couldn't find your content.", errors.New("could not find valid file at ["+resourcePath+"]"))
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 {
panic(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 {
panic(err)
}
return
} else {
route.srv.SendError(w, req, 404, "Couldn't find your content.", errors.New("could not find file for ["+stub+"]"))
}
}
return fun
}
func clientAcceptsGzip(req *http.Request) bool {
encodings := req.Header.Get(AcceptEncodingHeaderKey)
return strings.Contains(encodings, "gzip")
}