diff --git a/Backend/WebServer/compression.go b/Backend/WebServer/compression.go
new file mode 100644
index 0000000..0f6542e
--- /dev/null
+++ b/Backend/WebServer/compression.go
@@ -0,0 +1,109 @@
+package webserver
+
+import (
+ "compress/gzip"
+ "io"
+ "net/http"
+ "strings"
+ "sync"
+)
+
+var compressibleTypes = map[string]bool{
+ "text/html": true,
+ "text/css": true,
+ "text/javascript": true,
+ "text/plain": true,
+ "text/xml": true,
+ "text/csv": true,
+ "application/javascript": true,
+ "application/json": true,
+ "application/xml": true,
+ "application/wasm": true,
+ "application/xhtml+xml": true,
+ "application/rss+xml": true,
+ "application/atom+xml": true,
+ "application/manifest+json": true,
+ "image/svg+xml": true,
+}
+
+var gzipWriterPool = sync.Pool{
+ New: func() interface{} {
+ w, _ := gzip.NewWriterLevel(io.Discard, gzip.DefaultCompression)
+ return w
+ },
+}
+
+type gzipResponseWriter struct {
+ http.ResponseWriter
+ gzWriter *gzip.Writer
+ headerWritten bool
+ compressed bool
+}
+
+func (g *gzipResponseWriter) Write(data []byte) (int, error) {
+ if !g.headerWritten {
+ g.detectAndSetHeaders()
+ }
+ if g.compressed {
+ return g.gzWriter.Write(data)
+ }
+ return g.ResponseWriter.Write(data)
+}
+
+func (g *gzipResponseWriter) WriteHeader(statusCode int) {
+ if !g.headerWritten {
+ g.detectAndSetHeaders()
+ }
+ g.ResponseWriter.WriteHeader(statusCode)
+}
+
+func (g *gzipResponseWriter) detectAndSetHeaders() {
+ g.headerWritten = true
+ contentType := g.ResponseWriter.Header().Get("Content-Type")
+ if contentType == "" {
+ return
+ }
+
+ mimeType := strings.SplitN(contentType, ";", 2)[0]
+ mimeType = strings.TrimSpace(mimeType)
+
+ if compressibleTypes[mimeType] {
+ g.ResponseWriter.Header().Set("Content-Encoding", "gzip")
+ g.ResponseWriter.Header().Add("Vary", "Accept-Encoding")
+ g.ResponseWriter.Header().Del("Content-Length")
+ g.compressed = true
+ }
+}
+
+func (g *gzipResponseWriter) Flush() {
+ if g.compressed {
+ g.gzWriter.Flush()
+ }
+ if flusher, ok := g.ResponseWriter.(http.Flusher); ok {
+ flusher.Flush()
+ }
+}
+
+func (g *gzipResponseWriter) close() {
+ if g.compressed {
+ g.gzWriter.Close()
+ gzipWriterPool.Put(g.gzWriter)
+ }
+}
+
+func clientAcceptsGzip(r *http.Request) bool {
+ return strings.Contains(r.Header.Get("Accept-Encoding"), "gzip")
+}
+
+func isAlreadyCompressed(header http.Header) bool {
+ return header.Get("Content-Encoding") != ""
+}
+
+func newGzipResponseWriter(w http.ResponseWriter) *gzipResponseWriter {
+ gz := gzipWriterPool.Get().(*gzip.Writer)
+ gz.Reset(w)
+ return &gzipResponseWriter{
+ ResponseWriter: w,
+ gzWriter: gz,
+ }
+}
diff --git a/Backend/WebServer/handler.go b/Backend/WebServer/handler.go
index 368c938..95e65af 100644
--- a/Backend/WebServer/handler.go
+++ b/Backend/WebServer/handler.go
@@ -170,10 +170,19 @@ func isRootFileRoutingEnabled(host string) bool {
return site.Root_file_routing
}
}
- // По умолчанию роутинг выключен
return false
}
+// Проверяет включено ли сжатие для сайта
+func isSiteCompressionEnabled(host string) bool {
+ for _, site := range config.ConfigData.Site_www {
+ if site.Host == host {
+ return site.IsCompressionEnabled()
+ }
+ }
+ return true
+}
+
// Проверка vAccess с обработкой ошибки
// Возвращает true если доступ разрешён, false если заблокирован
func checkVAccessAndHandle(w http.ResponseWriter, r *http.Request, filePath string, host string) bool {
@@ -246,6 +255,13 @@ func handler(w http.ResponseWriter, r *http.Request) {
}
+ // Сжатие ответа (gzip)
+ if isSiteCompressionEnabled(host) && clientAcceptsGzip(r) {
+ gzw := newGzipResponseWriter(w)
+ defer gzw.close()
+ w = gzw
+ }
+
// Проверяем существование директории сайта
if _, err := os.Stat("WebServer/www/" + host + "/public_www"); err != nil {
http.ServeFile(w, r, "WebServer/tools/error_page/index.html")
diff --git a/Backend/WebServer/proxy_server.go b/Backend/WebServer/proxy_server.go
index b09795f..667053b 100644
--- a/Backend/WebServer/proxy_server.go
+++ b/Backend/WebServer/proxy_server.go
@@ -306,26 +306,30 @@ func StartHandlerProxy(w http.ResponseWriter, r *http.Request) (valid bool) {
}
}
+ // Сжатие ответа (gzip) — только если бэкенд не сжал сам
+ var gzw *gzipResponseWriter
+ if proxyConfig.IsCompressionEnabled() && clientAcceptsGzip(r) && !isAlreadyCompressed(resp.Header) {
+ gzw = newGzipResponseWriter(w)
+ defer gzw.close()
+ w = gzw
+ }
+
// Устанавливаем статус код
w.WriteHeader(resp.StatusCode)
// Копируем тело ответа с поддержкой streaming (SSE, chunked responses)
- // Используем буферизированное копирование с принудительной отправкой данных
flusher, canFlush := w.(http.Flusher)
- // Буфер для чанков (32KB - оптимальный размер для баланса производительности)
buffer := make([]byte, 32*1024)
for {
n, err := resp.Body.Read(buffer)
if n > 0 {
- // Записываем прочитанные данные
if _, writeErr := w.Write(buffer[:n]); writeErr != nil {
log.Printf("Ошибка записи тела ответа: %v", writeErr)
break
}
- // Принудительно отправляем данные клиенту (критично для SSE)
if canFlush {
flusher.Flush()
}
diff --git a/Backend/admin/go/proxy/types.go b/Backend/admin/go/proxy/types.go
index 640f71f..e842ba8 100644
--- a/Backend/admin/go/proxy/types.go
+++ b/Backend/admin/go/proxy/types.go
@@ -9,6 +9,7 @@ type ProxyInfo struct {
ServiceHTTPSuse bool `json:"ServiceHTTPSuse"`
AutoHTTPS bool `json:"AutoHTTPS"`
AutoCreateSSL bool `json:"AutoCreateSSL"`
+ Compression *bool `json:"Compression"`
Status string `json:"Status"`
}
diff --git a/Backend/admin/go/sites/methods.go b/Backend/admin/go/sites/methods.go
index 8b4cba1..455aaa5 100644
--- a/Backend/admin/go/sites/methods.go
+++ b/Backend/admin/go/sites/methods.go
@@ -156,6 +156,7 @@ func AddSiteToConfig(siteData SiteInfo) error {
Status: siteData.Status,
Root_file: siteData.RootFile,
Root_file_routing: siteData.RootFileRouting,
+ Compression: siteData.Compression,
}
// Добавляем в массив
diff --git a/Backend/admin/go/sites/sites.go b/Backend/admin/go/sites/sites.go
index a6a11a6..7287b87 100644
--- a/Backend/admin/go/sites/sites.go
+++ b/Backend/admin/go/sites/sites.go
@@ -16,6 +16,7 @@ func GetSitesList() []SiteInfo {
RootFile: site.Root_file,
RootFileRouting: site.Root_file_routing,
AutoCreateSSL: site.AutoCreateSSL,
+ Compression: site.Compression,
}
sites = append(sites, siteInfo)
}
diff --git a/Backend/admin/go/sites/types.go b/Backend/admin/go/sites/types.go
index 3a84c77..dd85548 100644
--- a/Backend/admin/go/sites/types.go
+++ b/Backend/admin/go/sites/types.go
@@ -8,5 +8,6 @@ type SiteInfo struct {
RootFile string `json:"root_file"`
RootFileRouting bool `json:"root_file_routing"`
AutoCreateSSL bool `json:"auto_create_ssl"`
+ Compression *bool `json:"Compression"`
}
diff --git a/Backend/config/config.go b/Backend/config/config.go
index 089c28c..8e15d27 100644
--- a/Backend/config/config.go
+++ b/Backend/config/config.go
@@ -22,6 +22,14 @@ type Site_www struct {
Root_file string `json:"root_file"`
Root_file_routing bool `json:"root_file_routing"`
AutoCreateSSL bool `json:"AutoCreateSSL"`
+ Compression *bool `json:"Compression"`
+}
+
+func (s Site_www) IsCompressionEnabled() bool {
+ if s.Compression == nil {
+ return true
+ }
+ return *s.Compression
}
type Soft_Settings struct {
@@ -42,6 +50,14 @@ type Proxy_Service struct {
ServiceHTTPSuse bool `json:"ServiceHTTPSuse"`
AutoHTTPS bool `json:"AutoHTTPS"`
AutoCreateSSL bool `json:"AutoCreateSSL"`
+ Compression *bool `json:"Compression"`
+}
+
+func (p Proxy_Service) IsCompressionEnabled() bool {
+ if p.Compression == nil {
+ return true
+ }
+ return *p.Compression
}
func LoadConfig() {
@@ -88,6 +104,20 @@ func migrateConfig(originalData []byte) {
}
}
+ // Проверяем Site_www на наличие Compression
+ if rawSites, ok := rawConfig["Site_www"]; ok {
+ var sites2 []map[string]interface{}
+ if err := json.Unmarshal(rawSites, &sites2); err == nil {
+ for i, site := range sites2 {
+ if _, exists := site["Compression"]; !exists {
+ needsSave = true
+ compressionTrue := true
+ ConfigData.Site_www[i].Compression = &compressionTrue
+ }
+ }
+ }
+ }
+
// Проверяем Proxy_Service
if rawProxies, ok := rawConfig["Proxy_Service"]; ok {
var proxies []map[string]interface{}
@@ -101,6 +131,20 @@ func migrateConfig(originalData []byte) {
}
}
+ // Проверяем Proxy_Service на наличие Compression
+ if rawProxies, ok := rawConfig["Proxy_Service"]; ok {
+ var proxies2 []map[string]interface{}
+ if err := json.Unmarshal(rawProxies, &proxies2); err == nil {
+ for i, proxy := range proxies2 {
+ if _, exists := proxy["Compression"]; !exists {
+ needsSave = true
+ compressionTrue := true
+ ConfigData.Proxy_Service[i].Compression = &compressionTrue
+ }
+ }
+ }
+ }
+
// Проверяем Soft_Settings на наличие ACME_enabled
if rawSettings, ok := rawConfig["Soft_Settings"]; ok {
var settings map[string]interface{}
diff --git a/front_vue/src/Core/i18n/en.json b/front_vue/src/Core/i18n/en.json
index ff994ad..fce52f2 100644
--- a/front_vue/src/Core/i18n/en.json
+++ b/front_vue/src/Core/i18n/en.json
@@ -64,6 +64,8 @@
"formStatus": "Status",
"formRouting": "Root file routing",
"formRoutingHint": "If enabled, all requests to non-existent files will be redirected to the root file",
+ "formCompression": "Gzip compression",
+ "formCompressionHint": "Compress text responses (HTML, CSS, JS, JSON, SVG) to reduce traffic",
"deleteTitle": "Delete Site",
"deleteConfirm": "Are you sure you want to delete site \"{name}\" ({host})?",
"deleteWarning": "This action is IRREVERSIBLE!"
@@ -88,6 +90,8 @@
"formLocalPort": "Local Port",
"formServiceHttps": "HTTPS to service",
"formServiceHttpsHint": "Use HTTPS when connecting to the local service",
+ "formCompression": "Gzip compression",
+ "formCompressionHint": "Compress text responses (HTML, CSS, JS, JSON, SVG) to reduce traffic",
"formAutoHttps": "Auto HTTPS",
"formAutoHttpsHint": "Automatically redirect HTTP requests to HTTPS",
"deleteTitle": "Delete Proxy",
diff --git a/front_vue/src/Core/i18n/ru.json b/front_vue/src/Core/i18n/ru.json
index 3a9af22..497dde5 100644
--- a/front_vue/src/Core/i18n/ru.json
+++ b/front_vue/src/Core/i18n/ru.json
@@ -64,6 +64,8 @@
"formStatus": "Статус",
"formRouting": "Root file routing",
"formRoutingHint": "Если включено, все запросы к несуществующим файлам будут перенаправляться на root файл",
+ "formCompression": "Gzip сжатие",
+ "formCompressionHint": "Сжатие текстовых ответов (HTML, CSS, JS, JSON, SVG) для уменьшения трафика",
"deleteTitle": "Удалить сайт",
"deleteConfirm": "Вы действительно хотите удалить сайт \"{name}\" ({host})?",
"deleteWarning": "Это действие НЕОБРАТИМО!"
@@ -88,6 +90,8 @@
"formLocalPort": "Локальный порт",
"formServiceHttps": "HTTPS к сервису",
"formServiceHttpsHint": "Использовать HTTPS при подключении к локальному сервису",
+ "formCompression": "Gzip сжатие",
+ "formCompressionHint": "Сжатие текстовых ответов (HTML, CSS, JS, JSON, SVG) для уменьшения трафика",
"formAutoHttps": "Авто HTTPS",
"formAutoHttpsHint": "Автоматически перенаправлять HTTP запросы на HTTPS",
"deleteTitle": "Удалить прокси",
diff --git a/front_vue/src/Core/stores/proxies.js b/front_vue/src/Core/stores/proxies.js
index 97393cc..06817ac 100644
--- a/front_vue/src/Core/stores/proxies.js
+++ b/front_vue/src/Core/stores/proxies.js
@@ -30,6 +30,7 @@ export const useProxiesStore = defineStore('proxies', {
LocalPort: proxyData.localPort,
ServiceHTTPSuse: proxyData.serviceHttps,
AutoHTTPS: proxyData.autoHttps,
+ Compression: proxyData.compression !== undefined ? proxyData.compression : true,
AutoCreateSSL: proxyData.autoSSL,
})
const result = await api.saveConfig(JSON.stringify(config))
diff --git a/front_vue/src/Design/views/ProxyCreateView.vue b/front_vue/src/Design/views/ProxyCreateView.vue
index 9a50b7e..1f71867 100644
--- a/front_vue/src/Design/views/ProxyCreateView.vue
+++ b/front_vue/src/Design/views/ProxyCreateView.vue
@@ -10,6 +10,7 @@ const form = reactive({
localAddr: '127.0.0.1',
localPort: '',
serviceHttps: false,
+ compression: true,
certMode: 'none',
})
@@ -26,6 +27,7 @@ const createProxy = async () => {
enabled: true,
serviceHttps: form.serviceHttps,
autoHttps: form.serviceHttps,
+ compression: form.compression,
autoSSL: false,
})
creating.value = false
@@ -70,6 +72,14 @@ const createProxy = async () => {