mirror of
https://github.com/woodchen-ink/proxy-go.git
synced 2025-07-19 08:51:55 +08:00
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:
parent
380d993b6d
commit
723b73d748
13
.github/workflows/docker-build.yml
vendored
13
.github/workflows/docker-build.yml
vendored
@ -83,4 +83,17 @@ jobs:
|
||||
tags: |
|
||||
woodchen/proxy-go:latest
|
||||
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
59
cmd/proxy/main.go
Normal 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)
|
||||
}
|
||||
}
|
@ -1,6 +1,16 @@
|
||||
{
|
||||
"MAP":{
|
||||
"MAP": {
|
||||
"/path1": "https://path1.com/path/path/path",
|
||||
"/path2": "https://path2.com"
|
||||
},
|
||||
"Compression": {
|
||||
"Gzip": {
|
||||
"Enabled": true,
|
||||
"Level": 6
|
||||
},
|
||||
"Brotli": {
|
||||
"Enabled": true,
|
||||
"Level": 4
|
||||
}
|
||||
}
|
||||
}
|
2
go.mod
2
go.mod
@ -1,3 +1,5 @@
|
||||
module proxy-go
|
||||
|
||||
go 1.23.1
|
||||
|
||||
require github.com/andybalholm/brotli v1.1.1
|
||||
|
4
go.sum
Normal file
4
go.sum
Normal 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=
|
23
internal/compression/brotli.go
Normal file
23
internal/compression/brotli.go
Normal 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
|
||||
}
|
22
internal/compression/gzip.go
Normal file
22
internal/compression/gzip.go
Normal 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)
|
||||
}
|
41
internal/compression/manager.go
Normal file
41
internal/compression/manager.go
Normal 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, ""
|
||||
}
|
35
internal/compression/types.go
Normal file
35
internal/compression/types.go
Normal 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
20
internal/config/config.go
Normal 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
16
internal/config/types.go
Normal 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
107
internal/handler/proxy.go
Normal 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
|
||||
}
|
174
internal/middleware/compression.go
Normal file
174
internal/middleware/compression.go
Normal 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
105
main.go
@ -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)
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user