Init commit for static cache system

Added work in progress static cache system that cache static resources like image on zoraxy node for faster access speed, still experimental and not working
This commit is contained in:
Toby Chui
2025-10-24 17:19:09 +08:00
parent 9d0a2a94f7
commit d6ad0155ab
6 changed files with 297 additions and 3 deletions

View File

@@ -1,6 +1,7 @@
package dpcore
import (
"bytes"
"context"
"crypto/tls"
"errors"
@@ -78,6 +79,9 @@ type ResponseRewriteRuleSet struct {
/* System Information Payload */
DevelopmentMode bool //Inject dev mode information to requests
Version string //Version number of Zoraxy, use for X-Proxy-By
/* Caching Support */
CacheResponse func(requestPath string, contentType string, content []byte) //Callback to cache response, can be nil
}
type requestCanceler interface {
@@ -410,7 +414,29 @@ func (p *ReverseProxy) ProxyHTTP(rw http.ResponseWriter, req *http.Request, rrr
//Get flush interval in real time and start copying the request
flushInterval := p.getFlushInterval(req, res)
p.copyResponse(rw, res.Body, flushInterval)
// Check if we need to cache this response
if rrr.CacheResponse != nil && res.StatusCode == 200 {
// Read the entire response body into memory for caching
bodyBytes, err := io.ReadAll(res.Body)
if err != nil {
if p.Verbal {
p.logf("dpcore read error during caching: %v", err)
}
// Fall back to normal copying without caching
p.copyResponse(rw, res.Body, flushInterval)
} else {
// Cache the response
contentType := res.Header.Get("Content-Type")
rrr.CacheResponse(req.URL.Path, contentType, bodyBytes)
// Send cached content to client
p.copyResponse(rw, bytes.NewReader(bodyBytes), flushInterval)
}
} else {
// Normal response copying without caching
p.copyResponse(rw, res.Body, flushInterval)
}
// close now, instead of defer, to populate res.Trailer
res.Body.Close()

View File

@@ -204,6 +204,20 @@ func (h *ProxyHandler) hostRequest(w http.ResponseWriter, r *http.Request, targe
return
}
/* Static Cache Check */
if r.Method == "GET" && target.staticCacheResourcesPool != nil && target.staticCacheResourcesPool.IsEnabled() {
// Check if this request should be cached and if we have a cached version
if cachedFile, exists := target.staticCacheResourcesPool.GetCachedFile(r.URL.Path); exists {
// Serve from cache
err := target.staticCacheResourcesPool.ServeCachedFile(w, cachedFile)
if err == nil {
h.Parent.logRequest(r, true, 200, "host-cache", reqHostname, "cache", target)
return
}
// If serving from cache failed, continue with normal proxy flow
}
}
if r.URL != nil {
r.Host = r.URL.Host
} else {
@@ -227,6 +241,20 @@ func (h *ProxyHandler) hostRequest(w http.ResponseWriter, r *http.Request, targe
PermissionPolicy: headerRewriteOptions.PermissionPolicy,
})
// Prepare cache callback if static caching is enabled
var cacheCallback func(requestPath string, contentType string, content []byte)
if target.staticCacheResourcesPool != nil && target.staticCacheResourcesPool.IsEnabled() {
// Check if this response should be cached
if target.staticCacheResourcesPool.ShouldCacheRequest(r.URL.Path) {
cacheCallback = func(requestPath string, contentType string, content []byte) {
// Only cache if content length is within limits
if target.staticCacheResourcesPool.CheckFileSizeShouldBeCached(int64(len(content))) {
target.staticCacheResourcesPool.StoreCachedFile(requestPath, contentType, content)
}
}
}
}
//Handle the request reverse proxy
statusCode, err := selectedUpstream.ServeHTTP(w, r, &dpcore.ResponseRewriteRuleSet{
ProxyDomain: selectedUpstream.OriginIpOrDomain,
@@ -241,6 +269,7 @@ func (h *ProxyHandler) hostRequest(w http.ResponseWriter, r *http.Request, targe
NoRemoveHopByHop: headerRewriteOptions.DisableHopByHopHeaderRemoval,
Version: target.parent.Option.HostVersion,
DevelopmentMode: target.parent.Option.DevelopmentMode,
CacheResponse: cacheCallback,
})
//validate the error

View File

@@ -8,6 +8,7 @@ import (
"time"
"imuslab.com/zoraxy/mod/dynamicproxy/dpcore"
"imuslab.com/zoraxy/mod/dynamicproxy/staticcache"
"imuslab.com/zoraxy/mod/utils"
)
@@ -31,6 +32,11 @@ func (router *Router) PrepareProxyRoute(endpoint *ProxyEndpoint) (*ProxyEndpoint
endpoint.parent = router
//Initialize static cache resources pool if static cache is configured
if endpoint.StaticCacheConfig != nil && endpoint.StaticCacheConfig.Enabled {
endpoint.staticCacheResourcesPool = staticcache.NewStaticCacheResourcesPool(endpoint.StaticCacheConfig)
}
//Prepare proxy routing handler for each of the virtual directories
for _, vdir := range endpoint.VirtualDirectories {
domain := vdir.Domain

View File

@@ -0,0 +1,217 @@
package staticcache
import (
"io"
"mime"
"net/http"
"os"
"path/filepath"
"strings"
"sync"
"time"
)
type StaticCachedFile struct {
FilePath string // The file path of the cached file
ContentType string // The MIME type of the cached file
ExpiryTime int64 // The Unix timestamp when the cache expires
}
type StaticCacheConfig struct {
Enabled bool // Whether static caching is enabled on this proxy rule
Timeout int64 // How long to cache static files in seconds
MaxFileSize int64 // Maximum file size to cache in bytes
FileExtensions []string // File extensions to cache, e.g. []string{".css", ".js", ".png"}
SkipSubpaths []string // Subpaths to skip caching, e.g. []string{"/api/", "/admin/"}
CacheFileDir string // Directory to store cached files
}
type StaticCacheResourcesPool struct {
config *StaticCacheConfig
cachedFiles sync.Map // in the type of map[string]*StaticCachedFile
}
func NewStaticCacheResourcesPool(config *StaticCacheConfig) *StaticCacheResourcesPool {
//Check if the config dir exists, if not create it
if config.CacheFileDir != "" {
if _, err := os.Stat(config.CacheFileDir); os.IsNotExist(err) {
os.MkdirAll(config.CacheFileDir, 0755)
}
}
return &StaticCacheResourcesPool{
config: config,
cachedFiles: sync.Map{},
}
}
// GetDefaultStaticCacheConfig returns a default static cache configuration
func GetDefaultStaticCacheConfig(cacheFolderDir string) *StaticCacheConfig {
return &StaticCacheConfig{
Enabled: false,
Timeout: 3600, // 1 hourt
MaxFileSize: 25 * 1024 * 1024, // 25 MB
FileExtensions: []string{".html", ".css", ".js", ".png", ".jpg", ".jpeg", ".gif", ".svg", ".woff", ".woff2", ".ttf", ".eot"},
SkipSubpaths: []string{},
CacheFileDir: cacheFolderDir,
}
}
func (pool *StaticCacheResourcesPool) IsEnabled() bool {
return pool.config.Enabled
}
func (pool *StaticCacheResourcesPool) GetConfig() *StaticCacheConfig {
return pool.config
}
// CheckFileSizeShouldBeCached checks if the file size is within the limit to be cached
func (pool *StaticCacheResourcesPool) CheckFileSizeShouldBeCached(contentLength int64) bool {
return pool.config.MaxFileSize <= 0 || contentLength <= pool.config.MaxFileSize
}
// ShouldCacheRequest checks if a request should be cached based on the configuration
func (pool *StaticCacheResourcesPool) ShouldCacheRequest(requestPath string) bool {
if !pool.config.Enabled {
return false
}
// Check if path should be skipped
for _, skipPath := range pool.config.SkipSubpaths {
if strings.Contains(requestPath, skipPath) {
return false
}
}
ext := strings.ToLower(filepath.Ext(requestPath))
found := false
for _, allowedExt := range pool.config.FileExtensions {
if ext == strings.ToLower(allowedExt) {
found = true
break
}
}
return found
}
// GetCachedFile retrieves a cached file if it exists and is not expired
func (pool *StaticCacheResourcesPool) GetCachedFile(requestPath string) (*StaticCachedFile, bool) {
cacheKey := pool.generateCacheKey(requestPath)
value, exists := pool.cachedFiles.Load(cacheKey)
if !exists {
return nil, false
}
cachedFile, ok := value.(*StaticCachedFile)
if !ok {
return nil, false
}
// Check if cache is expired
if time.Now().Unix() > cachedFile.ExpiryTime {
// Remove expired cache
pool.cachedFiles.Delete(cacheKey)
pool.removeFileFromDisk(cachedFile.FilePath)
return nil, false
}
return cachedFile, true
}
// generateCacheKey creates a unique key for caching based on the request path
func (pool *StaticCacheResourcesPool) generateCacheKey(requestPath string) string {
// Use the request path as the key, normalized
return strings.TrimPrefix(requestPath, "/")
}
// removeFileFromDisk removes a cached file from disk
func (pool *StaticCacheResourcesPool) removeFileFromDisk(filePath string) {
os.Remove(filePath)
}
// StoreCachedFile stores a file in the cache with the given content and expiry time
func (pool *StaticCacheResourcesPool) StoreCachedFile(requestPath, contentType string, content []byte) error {
cacheKey := pool.generateCacheKey(requestPath)
// Create cache directory if it doesn't exist
cacheDir := pool.config.CacheFileDir
if cacheDir == "" {
cacheDir = "./cache" // Default cache directory
}
if err := os.MkdirAll(cacheDir, 0755); err != nil {
return err
}
// Generate file path
fileName := strings.ReplaceAll(cacheKey, "/", "_")
filePath := filepath.Join(cacheDir, fileName)
// Write content to file
if err := os.WriteFile(filePath, content, 0644); err != nil {
return err
}
// Calculate expiry time
expiryTime := time.Now().Add(time.Duration(pool.config.Timeout) * time.Second).Unix()
// Store in memory cache
cachedFile := &StaticCachedFile{
FilePath: filePath,
ContentType: contentType,
ExpiryTime: expiryTime,
}
pool.cachedFiles.Store(cacheKey, cachedFile)
return nil
}
// ServeCachedFile serves a cached file to the HTTP response writer
func (pool *StaticCacheResourcesPool) ServeCachedFile(w http.ResponseWriter, cachedFile *StaticCachedFile) error {
file, err := os.Open(cachedFile.FilePath)
if err != nil {
return err
}
defer file.Close()
// Set content type
if cachedFile.ContentType != "" {
w.Header().Set("Content-Type", cachedFile.ContentType)
} else {
// Try to detect content type from file extension
ext := filepath.Ext(cachedFile.FilePath)
if mimeType := mime.TypeByExtension(ext); mimeType != "" {
w.Header().Set("Content-Type", mimeType)
}
}
// Set cache headers
w.Header().Set("Cache-Control", "public, max-age=3600") // Cache for 1 hour in browser
// Copy file content to response
_, err = io.Copy(w, file)
return err
}
// RemoveExpiredCache removes all expired cached files from memory and disk
func (pool *StaticCacheResourcesPool) RemoveExpiredCache() {
currentTime := time.Now().Unix()
pool.cachedFiles.Range(func(key, value interface{}) bool {
cachedFile, ok := value.(*StaticCachedFile)
if !ok {
return true
}
if currentTime > cachedFile.ExpiryTime {
// Remove from memory
pool.cachedFiles.Delete(key)
// Remove from disk
pool.removeFileFromDisk(cachedFile.FilePath)
}
return true
})
}

View File

@@ -22,6 +22,7 @@ import (
"imuslab.com/zoraxy/mod/dynamicproxy/permissionpolicy"
"imuslab.com/zoraxy/mod/dynamicproxy/redirection"
"imuslab.com/zoraxy/mod/dynamicproxy/rewrite"
"imuslab.com/zoraxy/mod/dynamicproxy/staticcache"
"imuslab.com/zoraxy/mod/geodb"
"imuslab.com/zoraxy/mod/info/logger"
"imuslab.com/zoraxy/mod/plugins"
@@ -220,9 +221,13 @@ type ProxyEndpoint struct {
DefaultSiteOption int //Fallback routing logic options
DefaultSiteValue string //Fallback routing target, optional
//Static Resources Caching
StaticCacheConfig *staticcache.StaticCacheConfig //Static cache configuration for this endpoint
//Internal Logic Elements
parent *Router `json:"-"`
Tags []string // Tags for the proxy endpoint
parent *Router `json:"-"` //Parent router
staticCacheResourcesPool *staticcache.StaticCacheResourcesPool `json:"-"` //Static cache resources pool
Tags []string // Tags for the proxy endpoint
}
/*

View File

@@ -10,11 +10,13 @@ import (
"strings"
"time"
"github.com/google/uuid"
"imuslab.com/zoraxy/mod/auth"
"imuslab.com/zoraxy/mod/dynamicproxy"
"imuslab.com/zoraxy/mod/dynamicproxy/loadbalance"
"imuslab.com/zoraxy/mod/dynamicproxy/permissionpolicy"
"imuslab.com/zoraxy/mod/dynamicproxy/rewrite"
"imuslab.com/zoraxy/mod/dynamicproxy/staticcache"
"imuslab.com/zoraxy/mod/netutils"
"imuslab.com/zoraxy/mod/tlscert"
"imuslab.com/zoraxy/mod/uptime"
@@ -343,6 +345,10 @@ func ReverseProxyHandleAddEndpoint(w http.ResponseWriter, r *http.Request) {
}
tags = filteredTags
// Get a default static cache config with default cache dir
cacheDir := filepath.Join(TMP_FOLDER, "static_cache", uuid.New().String())
staticCacheConfig := staticcache.GetDefaultStaticCacheConfig(cacheDir)
var proxyEndpointCreated *dynamicproxy.ProxyEndpoint
switch eptype {
case "host":
@@ -413,10 +419,15 @@ func ReverseProxyHandleAddEndpoint(w http.ResponseWriter, r *http.Request) {
//Default Site
DefaultSiteOption: 0,
DefaultSiteValue: "",
// Static Cache
StaticCacheConfig: staticCacheConfig,
// Rate Limit
RequireRateLimit: requireRateLimit,
RateLimit: int64(proxyRateLimit),
// Others
Tags: tags,
DisableUptimeMonitor: !enableUtm,
DisableLogging: disableLog,