feat: add compression support and update deployment workflow

add brotli and gzip compression support, update docker-compose and deployment script
This commit is contained in:
wood chen 2024-10-30 07:43:17 +08:00
parent 380d993b6d
commit 723b73d748
14 changed files with 530 additions and 109 deletions

View File

@ -83,4 +83,17 @@ jobs:
tags: | tags: |
woodchen/proxy-go:latest woodchen/proxy-go:latest
woodchen/proxy-go:${{ steps.date.outputs.date }} woodchen/proxy-go:${{ steps.date.outputs.date }}
- name: Execute deployment commands
uses: appleboy/ssh-action@master
with:
host: ${{ secrets.SERVER_HOST }}
username: root
key: ${{ secrets.SERVER_SSH_KEY }}
script: |
docker pull woodchen/proxy-go:latest
docker stop proxy-go || true
docker rm proxy-go || true
docker compose -f /opt/1panel/docker/compose/proxy-go/docker-compose.yml up -d

59
cmd/proxy/main.go Normal file
View File

@ -0,0 +1,59 @@
package main
import (
"log"
"net/http"
"os"
"os/signal"
"proxy-go/internal/compression"
"proxy-go/internal/config"
"proxy-go/internal/handler"
"proxy-go/internal/middleware"
"syscall"
)
func main() {
// 加载配置
cfg, err := config.Load("data/config.json")
if err != nil {
log.Fatal("Error loading config:", err)
}
// 创建压缩管理器,直接使用配置文件中的压缩配置
compManager := compression.NewManager(compression.Config{
Gzip: compression.CompressorConfig(cfg.Compression.Gzip),
Brotli: compression.CompressorConfig(cfg.Compression.Brotli),
})
// 创建代理处理器
proxyHandler := handler.NewProxyHandler(cfg.MAP)
// 添加中间件
var handler http.Handler = proxyHandler
if cfg.Compression.Gzip.Enabled || cfg.Compression.Brotli.Enabled {
handler = middleware.CompressionMiddleware(compManager)(handler)
}
// 创建服务器
server := &http.Server{
Addr: ":80",
Handler: handler,
}
// 优雅关闭
go func() {
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
<-sigChan
log.Println("Shutting down server...")
if err := server.Close(); err != nil {
log.Printf("Error during server shutdown: %v\n", err)
}
}()
// 启动服务器
log.Println("Starting proxy server on :80")
if err := server.ListenAndServe(); err != http.ErrServerClosed {
log.Fatal("Error starting server:", err)
}
}

View File

@ -1,6 +1,16 @@
{ {
"MAP":{ "MAP": {
"/path1": "https://path1.com/path/path/path", "/path1": "https://path1.com/path/path/path",
"/path2": "https://path2.com" "/path2": "https://path2.com"
},
"Compression": {
"Gzip": {
"Enabled": true,
"Level": 6
},
"Brotli": {
"Enabled": true,
"Level": 4
} }
} }
}

2
go.mod
View File

@ -1,3 +1,5 @@
module proxy-go module proxy-go
go 1.23.1 go 1.23.1
require github.com/andybalholm/brotli v1.1.1

4
go.sum Normal file
View File

@ -0,0 +1,4 @@
github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=

View File

@ -0,0 +1,23 @@
package compression
import (
"io"
"github.com/andybalholm/brotli"
)
type BrotliCompressor struct {
level int
}
func NewBrotliCompressor(level int) *BrotliCompressor {
// 确保level在有效范围内 (0-11)
if level < 0 || level > 11 {
level = brotli.DefaultCompression
}
return &BrotliCompressor{level: level}
}
func (b *BrotliCompressor) Compress(w io.Writer) (io.WriteCloser, error) {
return brotli.NewWriterLevel(w, b.level), nil
}

View File

@ -0,0 +1,22 @@
package compression
import (
"compress/gzip"
"io"
)
type GzipCompressor struct {
level int
}
func NewGzipCompressor(level int) *GzipCompressor {
// 确保level在有效范围内
if level < gzip.DefaultCompression || level > gzip.BestCompression {
level = gzip.DefaultCompression
}
return &GzipCompressor{level: level}
}
func (g *GzipCompressor) Compress(w io.Writer) (io.WriteCloser, error) {
return gzip.NewWriterLevel(w, g.level)
}

View File

@ -0,0 +1,41 @@
package compression
import "strings"
type compressionManager struct {
gzip Compressor
brotli Compressor
config Config
}
// NewManager 创建新的压缩管理器
func NewManager(config Config) Manager {
m := &compressionManager{
config: config,
}
if config.Gzip.Enabled {
m.gzip = NewGzipCompressor(config.Gzip.Level)
}
if config.Brotli.Enabled {
m.brotli = NewBrotliCompressor(config.Brotli.Level)
}
return m
}
// SelectCompressor 实现 Manager 接口
func (m *compressionManager) SelectCompressor(acceptEncoding string) (Compressor, CompressionType) {
// 优先选择 brotli
if m.brotli != nil && strings.Contains(acceptEncoding, string(CompressionBrotli)) {
return m.brotli, CompressionBrotli
}
// 其次选择 gzip
if m.gzip != nil && strings.Contains(acceptEncoding, string(CompressionGzip)) {
return m.gzip, CompressionGzip
}
return nil, ""
}

View File

@ -0,0 +1,35 @@
package compression
import "io"
// Compressor 定义压缩器接口
type Compressor interface {
Compress(w io.Writer) (io.WriteCloser, error)
}
// CompressionType 表示压缩类型
type CompressionType string
const (
CompressionGzip CompressionType = "gzip"
CompressionBrotli CompressionType = "br"
)
// Config 压缩配置结构体
type Config struct {
Gzip CompressorConfig `json:"Gzip"`
Brotli CompressorConfig `json:"Brotli"`
}
// CompressorConfig 单个压缩器的配置
type CompressorConfig struct {
Enabled bool `json:"Enabled"`
Level int `json:"Level"`
}
// Manager 压缩管理器接口
type Manager interface {
// SelectCompressor 根据 Accept-Encoding 头选择合适的压缩器
// 返回选中的压缩器和对应的压缩类型
SelectCompressor(acceptEncoding string) (Compressor, CompressionType)
}

20
internal/config/config.go Normal file
View File

@ -0,0 +1,20 @@
package config
import (
"encoding/json"
"os"
)
func Load(path string) (*Config, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, err
}
var config Config
if err := json.Unmarshal(data, &config); err != nil {
return nil, err
}
return &config, nil
}

16
internal/config/types.go Normal file
View File

@ -0,0 +1,16 @@
package config
type Config struct {
MAP map[string]string `json:"MAP"`
Compression CompressionConfig `json:"Compression"`
}
type CompressionConfig struct {
Gzip CompressorConfig `json:"Gzip"`
Brotli CompressorConfig `json:"Brotli"`
}
type CompressorConfig struct {
Enabled bool `json:"Enabled"`
Level int `json:"Level"`
}

107
internal/handler/proxy.go Normal file
View File

@ -0,0 +1,107 @@
package handler
import (
"fmt"
"io"
"net"
"net/http"
"strings"
)
type ProxyHandler struct {
pathMap map[string]string
}
func NewProxyHandler(pathMap map[string]string) *ProxyHandler {
return &ProxyHandler{
pathMap: pathMap,
}
}
func (h *ProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// 处理根路径请求
if r.URL.Path == "/" {
w.WriteHeader(http.StatusOK)
fmt.Fprint(w, "Welcome to CZL proxy.")
return
}
// 查找匹配的代理路径
var matchedPrefix string
var targetBase string
for prefix, target := range h.pathMap {
if strings.HasPrefix(r.URL.Path, prefix) {
matchedPrefix = prefix
targetBase = target
break
}
}
// 如果没有匹配的路径,返回 404
if matchedPrefix == "" {
http.NotFound(w, r)
return
}
// 构建目标 URL
targetPath := strings.TrimPrefix(r.URL.Path, matchedPrefix)
targetURL := targetBase + targetPath
// 创建新的请求
proxyReq, err := http.NewRequest(r.Method, targetURL, r.Body)
if err != nil {
http.Error(w, "Error creating proxy request", http.StatusInternalServerError)
return
}
// 复制原始请求的 header
copyHeader(proxyReq.Header, r.Header)
// 设置一些必要的头部
proxyReq.Header.Set("X-Forwarded-Host", r.Host)
proxyReq.Header.Set("X-Real-IP", getClientIP(r))
// 发送代理请求
client := &http.Client{}
resp, err := client.Do(proxyReq)
if err != nil {
http.Error(w, "Error forwarding request", http.StatusBadGateway)
return
}
defer resp.Body.Close()
// 复制响应 header
copyHeader(w.Header(), resp.Header)
// 设置响应状态码
w.WriteHeader(resp.StatusCode)
// 复制响应体
if _, err := io.Copy(w, resp.Body); err != nil {
// 这里只记录错误,不返回给客户端,因为响应头已经发送
fmt.Printf("Error copying response: %v\n", err)
}
}
func copyHeader(dst, src http.Header) {
for k, vv := range src {
for _, v := range vv {
dst.Add(k, v)
}
}
}
func getClientIP(r *http.Request) string {
// 检查各种可能的请求头
if ip := r.Header.Get("X-Real-IP"); ip != "" {
return ip
}
if ip := r.Header.Get("X-Forwarded-For"); ip != "" {
return strings.Split(ip, ",")[0]
}
// 从RemoteAddr获取
if ip, _, err := net.SplitHostPort(r.RemoteAddr); err == nil {
return ip
}
return r.RemoteAddr
}

View File

@ -0,0 +1,174 @@
package middleware
import (
"bufio"
"io"
"mime"
"net"
"net/http"
"proxy-go/internal/compression"
"strings"
)
const (
defaultBufferSize = 32 * 1024 // 32KB
)
type CompressResponseWriter struct {
http.ResponseWriter
compressor compression.Compressor
writer io.WriteCloser
bufferedWriter *bufio.Writer
statusCode int
written bool
compressed bool
}
func CompressionMiddleware(manager compression.Manager) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// 检查源站是否已经压缩
if r.Header.Get("Content-Encoding") != "" {
next.ServeHTTP(w, r)
return
}
// 选择压缩器
compressor, encoding := manager.SelectCompressor(r.Header.Get("Accept-Encoding"))
if compressor == nil {
next.ServeHTTP(w, r)
return
}
cw := &CompressResponseWriter{
ResponseWriter: w,
compressor: compressor,
statusCode: 0,
written: false,
compressed: false,
}
// 设置Content-Encoding header
cw.Header().Set("Content-Encoding", string(encoding))
cw.Header().Add("Vary", "Accept-Encoding")
defer func() {
if cw.writer != nil {
if cw.bufferedWriter != nil {
cw.bufferedWriter.Flush()
}
cw.writer.Close()
}
}()
next.ServeHTTP(cw, r)
})
}
}
func (cw *CompressResponseWriter) WriteHeader(statusCode int) {
if cw.written {
return
}
cw.statusCode = statusCode
cw.written = true
// 某些状态码不应该压缩
if !shouldCompressForStatus(statusCode) {
cw.compressed = false
cw.Header().Del("Content-Encoding")
cw.ResponseWriter.WriteHeader(statusCode)
return
}
// 检查内容类型是否应该压缩
if !shouldCompressType(cw.Header().Get("Content-Type")) {
cw.compressed = false
cw.Header().Del("Content-Encoding")
cw.ResponseWriter.WriteHeader(statusCode)
return
}
cw.compressed = true
cw.Header().Del("Content-Length") // 因为内容将被压缩,原长度不再有效
cw.ResponseWriter.WriteHeader(statusCode)
}
func (cw *CompressResponseWriter) Write(b []byte) (int, error) {
if !cw.written {
cw.WriteHeader(http.StatusOK)
}
if !cw.compressed {
return cw.ResponseWriter.Write(b)
}
// 延迟初始化压缩写入器
if cw.writer == nil {
var err error
cw.writer, err = cw.compressor.Compress(cw.ResponseWriter)
if err != nil {
return 0, err
}
cw.bufferedWriter = bufio.NewWriterSize(cw.writer, defaultBufferSize)
}
return cw.bufferedWriter.Write(b)
}
// 实现 http.Hijacker 接口
func (cw *CompressResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
if hj, ok := cw.ResponseWriter.(http.Hijacker); ok {
return hj.Hijack()
}
return nil, nil, http.ErrNotSupported
}
// 实现 http.Flusher 接口
func (cw *CompressResponseWriter) Flush() {
if cw.bufferedWriter != nil {
cw.bufferedWriter.Flush()
}
if f, ok := cw.ResponseWriter.(http.Flusher); ok {
f.Flush()
}
}
// 判断是否应该对该状态码的响应进行压缩
func shouldCompressForStatus(status int) bool {
// 只压缩成功的响应
return status == http.StatusOK ||
status == http.StatusCreated ||
status == http.StatusAccepted ||
status == http.StatusNonAuthoritativeInfo ||
status == http.StatusNoContent ||
status == http.StatusPartialContent
}
// 判断是否应该对该内容类型进行压缩
func shouldCompressType(contentType string) bool {
// 解析内容类型
mimeType, _, err := mime.ParseMediaType(contentType)
if err != nil {
return false
}
compressibleTypes := map[string]bool{
"text/": true,
"application/javascript": true,
"application/json": true,
"application/xml": true,
"application/x-yaml": true,
"image/svg+xml": true,
}
// 检查是否是可压缩类型
for prefix := range compressibleTypes {
if strings.HasPrefix(mimeType, prefix) {
return true
}
}
return false
}

105
main.go
View File

@ -1,105 +0,0 @@
package main
import (
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"os"
"strings"
)
// Config 结构体用于解析配置文件
type Config struct {
MAP map[string]string `json:"MAP"`
}
func main() {
// 读取配置文件
configFile, err := os.ReadFile("data/config.json")
if err != nil {
log.Fatal("Error reading config file:", err)
}
// 解析配置文件
var config Config
if err := json.Unmarshal(configFile, &config); err != nil {
log.Fatal("Error parsing config file:", err)
}
// 创建 HTTP 处理函数
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
// 处理根路径请求
if r.URL.Path == "/" {
w.WriteHeader(http.StatusOK)
fmt.Fprint(w, "Welcome to CZL proxy.")
return
}
// 查找匹配的代理路径
var matchedPrefix string
var targetBase string
for prefix, target := range config.MAP {
if strings.HasPrefix(r.URL.Path, prefix) {
matchedPrefix = prefix
targetBase = target
break
}
}
// 如果没有匹配的路径,返回 404
if matchedPrefix == "" {
http.NotFound(w, r)
return
}
// 构建目标 URL
targetPath := strings.TrimPrefix(r.URL.Path, matchedPrefix)
targetURL := targetBase + targetPath
// 创建新的请求
proxyReq, err := http.NewRequest(r.Method, targetURL, r.Body)
if err != nil {
http.Error(w, "Error creating proxy request", http.StatusInternalServerError)
return
}
// 复制原始请求的 header
for header, values := range r.Header {
for _, value := range values {
proxyReq.Header.Add(header, value)
}
}
// 发送代理请求
client := &http.Client{}
resp, err := client.Do(proxyReq)
if err != nil {
http.Error(w, "Error forwarding request", http.StatusBadGateway)
return
}
defer resp.Body.Close()
// 复制响应 header
for header, values := range resp.Header {
for _, value := range values {
w.Header().Add(header, value)
}
}
// 设置响应状态码
w.WriteHeader(resp.StatusCode)
// 复制响应体
if _, err := io.Copy(w, resp.Body); err != nil {
log.Printf("Error copying response: %v", err)
}
})
// 启动服务器
log.Println("Starting proxy server on :80")
if err := http.ListenAndServe(":80", nil); err != nil {
log.Fatal("Error starting server:", err)
}
}