mirror of
https://github.com/tobychui/zoraxy.git
synced 2025-10-25 20:14:10 +02:00
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:
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
217
src/mod/dynamicproxy/staticcache/staticcache.go
Normal file
217
src/mod/dynamicproxy/staticcache/staticcache.go
Normal 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
|
||||
})
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user