Добавлление gzip

Теперь ответы сжимаются , что увеличивает скорость работы.
This commit is contained in:
2026-02-09 01:14:40 +07:00
parent cff7ef1bd1
commit cb19d0b132
15 changed files with 227 additions and 7 deletions

View File

@@ -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,
}
}

View File

@@ -170,10 +170,19 @@ func isRootFileRoutingEnabled(host string) bool {
return site.Root_file_routing return site.Root_file_routing
} }
} }
// По умолчанию роутинг выключен
return false return false
} }
// Проверяет включено ли сжатие для сайта
func isSiteCompressionEnabled(host string) bool {
for _, site := range config.ConfigData.Site_www {
if site.Host == host {
return site.IsCompressionEnabled()
}
}
return true
}
// Проверка vAccess с обработкой ошибки // Проверка vAccess с обработкой ошибки
// Возвращает true если доступ разрешён, false если заблокирован // Возвращает true если доступ разрешён, false если заблокирован
func checkVAccessAndHandle(w http.ResponseWriter, r *http.Request, filePath string, host string) bool { 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 { if _, err := os.Stat("WebServer/www/" + host + "/public_www"); err != nil {
http.ServeFile(w, r, "WebServer/tools/error_page/index.html") http.ServeFile(w, r, "WebServer/tools/error_page/index.html")

View File

@@ -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) w.WriteHeader(resp.StatusCode)
// Копируем тело ответа с поддержкой streaming (SSE, chunked responses) // Копируем тело ответа с поддержкой streaming (SSE, chunked responses)
// Используем буферизированное копирование с принудительной отправкой данных
flusher, canFlush := w.(http.Flusher) flusher, canFlush := w.(http.Flusher)
// Буфер для чанков (32KB - оптимальный размер для баланса производительности)
buffer := make([]byte, 32*1024) buffer := make([]byte, 32*1024)
for { for {
n, err := resp.Body.Read(buffer) n, err := resp.Body.Read(buffer)
if n > 0 { if n > 0 {
// Записываем прочитанные данные
if _, writeErr := w.Write(buffer[:n]); writeErr != nil { if _, writeErr := w.Write(buffer[:n]); writeErr != nil {
log.Printf("Ошибка записи тела ответа: %v", writeErr) log.Printf("Ошибка записи тела ответа: %v", writeErr)
break break
} }
// Принудительно отправляем данные клиенту (критично для SSE)
if canFlush { if canFlush {
flusher.Flush() flusher.Flush()
} }

View File

@@ -9,6 +9,7 @@ type ProxyInfo struct {
ServiceHTTPSuse bool `json:"ServiceHTTPSuse"` ServiceHTTPSuse bool `json:"ServiceHTTPSuse"`
AutoHTTPS bool `json:"AutoHTTPS"` AutoHTTPS bool `json:"AutoHTTPS"`
AutoCreateSSL bool `json:"AutoCreateSSL"` AutoCreateSSL bool `json:"AutoCreateSSL"`
Compression *bool `json:"Compression"`
Status string `json:"Status"` Status string `json:"Status"`
} }

View File

@@ -156,6 +156,7 @@ func AddSiteToConfig(siteData SiteInfo) error {
Status: siteData.Status, Status: siteData.Status,
Root_file: siteData.RootFile, Root_file: siteData.RootFile,
Root_file_routing: siteData.RootFileRouting, Root_file_routing: siteData.RootFileRouting,
Compression: siteData.Compression,
} }
// Добавляем в массив // Добавляем в массив

View File

@@ -16,6 +16,7 @@ func GetSitesList() []SiteInfo {
RootFile: site.Root_file, RootFile: site.Root_file,
RootFileRouting: site.Root_file_routing, RootFileRouting: site.Root_file_routing,
AutoCreateSSL: site.AutoCreateSSL, AutoCreateSSL: site.AutoCreateSSL,
Compression: site.Compression,
} }
sites = append(sites, siteInfo) sites = append(sites, siteInfo)
} }

View File

@@ -8,5 +8,6 @@ type SiteInfo struct {
RootFile string `json:"root_file"` RootFile string `json:"root_file"`
RootFileRouting bool `json:"root_file_routing"` RootFileRouting bool `json:"root_file_routing"`
AutoCreateSSL bool `json:"auto_create_ssl"` AutoCreateSSL bool `json:"auto_create_ssl"`
Compression *bool `json:"Compression"`
} }

View File

@@ -22,6 +22,14 @@ type Site_www struct {
Root_file string `json:"root_file"` Root_file string `json:"root_file"`
Root_file_routing bool `json:"root_file_routing"` Root_file_routing bool `json:"root_file_routing"`
AutoCreateSSL bool `json:"AutoCreateSSL"` 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 { type Soft_Settings struct {
@@ -42,6 +50,14 @@ type Proxy_Service struct {
ServiceHTTPSuse bool `json:"ServiceHTTPSuse"` ServiceHTTPSuse bool `json:"ServiceHTTPSuse"`
AutoHTTPS bool `json:"AutoHTTPS"` AutoHTTPS bool `json:"AutoHTTPS"`
AutoCreateSSL bool `json:"AutoCreateSSL"` 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() { 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 // Проверяем Proxy_Service
if rawProxies, ok := rawConfig["Proxy_Service"]; ok { if rawProxies, ok := rawConfig["Proxy_Service"]; ok {
var proxies []map[string]interface{} 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 // Проверяем Soft_Settings на наличие ACME_enabled
if rawSettings, ok := rawConfig["Soft_Settings"]; ok { if rawSettings, ok := rawConfig["Soft_Settings"]; ok {
var settings map[string]interface{} var settings map[string]interface{}

View File

@@ -64,6 +64,8 @@
"formStatus": "Status", "formStatus": "Status",
"formRouting": "Root file routing", "formRouting": "Root file routing",
"formRoutingHint": "If enabled, all requests to non-existent files will be redirected to the root file", "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", "deleteTitle": "Delete Site",
"deleteConfirm": "Are you sure you want to delete site \"{name}\" ({host})?", "deleteConfirm": "Are you sure you want to delete site \"{name}\" ({host})?",
"deleteWarning": "This action is IRREVERSIBLE!" "deleteWarning": "This action is IRREVERSIBLE!"
@@ -88,6 +90,8 @@
"formLocalPort": "Local Port", "formLocalPort": "Local Port",
"formServiceHttps": "HTTPS to service", "formServiceHttps": "HTTPS to service",
"formServiceHttpsHint": "Use HTTPS when connecting to the local 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", "formAutoHttps": "Auto HTTPS",
"formAutoHttpsHint": "Automatically redirect HTTP requests to HTTPS", "formAutoHttpsHint": "Automatically redirect HTTP requests to HTTPS",
"deleteTitle": "Delete Proxy", "deleteTitle": "Delete Proxy",

View File

@@ -64,6 +64,8 @@
"formStatus": "Статус", "formStatus": "Статус",
"formRouting": "Root file routing", "formRouting": "Root file routing",
"formRoutingHint": "Если включено, все запросы к несуществующим файлам будут перенаправляться на root файл", "formRoutingHint": "Если включено, все запросы к несуществующим файлам будут перенаправляться на root файл",
"formCompression": "Gzip сжатие",
"formCompressionHint": "Сжатие текстовых ответов (HTML, CSS, JS, JSON, SVG) для уменьшения трафика",
"deleteTitle": "Удалить сайт", "deleteTitle": "Удалить сайт",
"deleteConfirm": "Вы действительно хотите удалить сайт \"{name}\" ({host})?", "deleteConfirm": "Вы действительно хотите удалить сайт \"{name}\" ({host})?",
"deleteWarning": "Это действие НЕОБРАТИМО!" "deleteWarning": "Это действие НЕОБРАТИМО!"
@@ -88,6 +90,8 @@
"formLocalPort": "Локальный порт", "formLocalPort": "Локальный порт",
"formServiceHttps": "HTTPS к сервису", "formServiceHttps": "HTTPS к сервису",
"formServiceHttpsHint": "Использовать HTTPS при подключении к локальному сервису", "formServiceHttpsHint": "Использовать HTTPS при подключении к локальному сервису",
"formCompression": "Gzip сжатие",
"formCompressionHint": "Сжатие текстовых ответов (HTML, CSS, JS, JSON, SVG) для уменьшения трафика",
"formAutoHttps": "Авто HTTPS", "formAutoHttps": "Авто HTTPS",
"formAutoHttpsHint": "Автоматически перенаправлять HTTP запросы на HTTPS", "formAutoHttpsHint": "Автоматически перенаправлять HTTP запросы на HTTPS",
"deleteTitle": "Удалить прокси", "deleteTitle": "Удалить прокси",

View File

@@ -30,6 +30,7 @@ export const useProxiesStore = defineStore('proxies', {
LocalPort: proxyData.localPort, LocalPort: proxyData.localPort,
ServiceHTTPSuse: proxyData.serviceHttps, ServiceHTTPSuse: proxyData.serviceHttps,
AutoHTTPS: proxyData.autoHttps, AutoHTTPS: proxyData.autoHttps,
Compression: proxyData.compression !== undefined ? proxyData.compression : true,
AutoCreateSSL: proxyData.autoSSL, AutoCreateSSL: proxyData.autoSSL,
}) })
const result = await api.saveConfig(JSON.stringify(config)) const result = await api.saveConfig(JSON.stringify(config))

View File

@@ -10,6 +10,7 @@ const form = reactive({
localAddr: '127.0.0.1', localAddr: '127.0.0.1',
localPort: '', localPort: '',
serviceHttps: false, serviceHttps: false,
compression: true,
certMode: 'none', certMode: 'none',
}) })
@@ -26,6 +27,7 @@ const createProxy = async () => {
enabled: true, enabled: true,
serviceHttps: form.serviceHttps, serviceHttps: form.serviceHttps,
autoHttps: form.serviceHttps, autoHttps: form.serviceHttps,
compression: form.compression,
autoSSL: false, autoSSL: false,
}) })
creating.value = false creating.value = false
@@ -70,6 +72,14 @@ const createProxy = async () => {
<VToggle v-model="form.serviceHttps" :label="t('common.enabled')" /> <VToggle v-model="form.serviceHttps" :label="t('common.enabled')" />
</div> </div>
<div class="form-group">
<div class="form-label-row">
<VTooltip :text="t('proxies.formCompressionHint')" />
<label class="form-label">{{ t('proxies.formCompression') }}</label>
</div>
<VToggle v-model="form.compression" :label="t('common.enabled')" />
</div>
<SslUploadSection v-model="form.certMode" /> <SslUploadSection v-model="form.certMode" />
</div> </div>
</div> </div>

View File

@@ -16,6 +16,7 @@ const form = reactive({
localPort: '', localPort: '',
enabled: true, enabled: true,
serviceHttps: false, serviceHttps: false,
compression: true,
autoHttps: true, autoHttps: true,
autoSSL: false, autoSSL: false,
}) })
@@ -33,6 +34,7 @@ onMounted(async () => {
form.localPort = proxy.LocalPort form.localPort = proxy.LocalPort
form.enabled = proxy.Enable form.enabled = proxy.Enable
form.serviceHttps = proxy.ServiceHTTPSuse form.serviceHttps = proxy.ServiceHTTPSuse
form.compression = proxy.Compression !== false
form.autoHttps = proxy.AutoHTTPS form.autoHttps = proxy.AutoHTTPS
form.autoSSL = proxy.AutoCreateSSL || false form.autoSSL = proxy.AutoCreateSSL || false
} }
@@ -50,6 +52,7 @@ const saveProxy = async () => {
config.Proxy_Service[idx].Enable = form.enabled config.Proxy_Service[idx].Enable = form.enabled
config.Proxy_Service[idx].ServiceHTTPSuse = form.serviceHttps config.Proxy_Service[idx].ServiceHTTPSuse = form.serviceHttps
config.Proxy_Service[idx].AutoHTTPS = form.serviceHttps config.Proxy_Service[idx].AutoHTTPS = form.serviceHttps
config.Proxy_Service[idx].Compression = form.compression
config.Proxy_Service[idx].AutoCreateSSL = form.autoSSL config.Proxy_Service[idx].AutoCreateSSL = form.autoSSL
const result = await api.saveConfig(JSON.stringify(config)) const result = await api.saveConfig(JSON.stringify(config))
if (isSuccess(result)) { if (isSuccess(result)) {
@@ -120,7 +123,11 @@ const confirmDelete = async () => {
</div> </div>
<!-- Toggles --> <!-- Toggles -->
<div class="form-row"> <div class="form-row form-row-3">
<div class="form-group">
<label class="form-label">{{ t('proxies.formCompression') }}:</label>
<VToggle v-model="form.compression" :label="t('common.enabled')" />
</div>
<div class="form-group"> <div class="form-group">
<label class="form-label">HTTPS:</label> <label class="form-label">HTTPS:</label>
<VToggle v-model="form.serviceHttps" :label="t('common.enabled')" /> <VToggle v-model="form.serviceHttps" :label="t('common.enabled')" />

View File

@@ -11,6 +11,7 @@ const form = reactive({
rootFile: 'index.html', rootFile: 'index.html',
status: 'active', status: 'active',
routing: true, routing: true,
compression: true,
certMode: 'none', certMode: 'none',
}) })
@@ -36,6 +37,7 @@ const createSite = async () => {
root_file: form.rootFile, root_file: form.rootFile,
status: form.status, status: form.status,
root_file_routing: form.routing, root_file_routing: form.routing,
Compression: form.compression,
AutoCreateSSL: form.certMode === 'auto', AutoCreateSSL: form.certMode === 'auto',
} }
const result = await sitesStore.create(siteData) const result = await sitesStore.create(siteData)
@@ -82,6 +84,14 @@ const createSite = async () => {
<VToggle v-model="form.routing" :label="t('common.enabled')" /> <VToggle v-model="form.routing" :label="t('common.enabled')" />
</div> </div>
<div class="form-group">
<div class="form-label-row">
<VTooltip :text="t('sites.formCompressionHint')" />
<label class="form-label">{{ t('sites.formCompression') }}</label>
</div>
<VToggle v-model="form.compression" :label="t('common.enabled')" />
</div>
<SslUploadSection v-model="form.certMode" /> <SslUploadSection v-model="form.certMode" />
</div> </div>
</div> </div>

View File

@@ -16,6 +16,7 @@ const form = reactive({
rootFile: 'index.html', rootFile: 'index.html',
status: 'active', status: 'active',
routing: true, routing: true,
compression: true,
autoSSL: false, autoSSL: false,
}) })
@@ -38,6 +39,7 @@ onMounted(async () => {
form.rootFile = site.root_file form.rootFile = site.root_file
form.status = site.status form.status = site.status
form.routing = site.root_file_routing form.routing = site.root_file_routing
form.compression = site.Compression !== false
form.autoSSL = site.AutoCreateSSL || false form.autoSSL = site.AutoCreateSSL || false
} }
}) })
@@ -65,6 +67,7 @@ const saveSite = async () => {
config.Site_www[idx].root_file = form.rootFile config.Site_www[idx].root_file = form.rootFile
config.Site_www[idx].status = form.status config.Site_www[idx].status = form.status
config.Site_www[idx].root_file_routing = form.routing config.Site_www[idx].root_file_routing = form.routing
config.Site_www[idx].Compression = form.compression
config.Site_www[idx].AutoCreateSSL = form.autoSSL config.Site_www[idx].AutoCreateSSL = form.autoSSL
const result = await api.saveConfig(JSON.stringify(config)) const result = await api.saveConfig(JSON.stringify(config))
if (isSuccess(result)) { if (isSuccess(result)) {
@@ -147,7 +150,11 @@ const confirmDelete = async () => {
<VSelect v-model="form.rootFile" :label="t('sites.formRootFile')" :options="rootFileOptions" /> <VSelect v-model="form.rootFile" :label="t('sites.formRootFile')" :options="rootFileOptions" />
<!-- Toggles --> <!-- Toggles -->
<div class="form-row"> <div class="form-row form-row-3">
<div class="form-group">
<label class="form-label">{{ t('sites.formCompression') }}:</label>
<VToggle v-model="form.compression" :label="t('common.enabled')" />
</div>
<div class="form-group"> <div class="form-group">
<label class="form-label">{{ t('sites.formRouting') }}:</label> <label class="form-label">{{ t('sites.formRouting') }}:</label>
<VToggle v-model="form.routing" :label="t('common.enabled')" /> <VToggle v-model="form.routing" :label="t('common.enabled')" />