Skip to content

Commit 62057d4

Browse files
committed
Implement HTTP and WebSocket reverse proxy support
Add comprehensive reverse proxy functionality: - HTTPProxier using Go's httputil.ReverseProxy - WebSocketProxier with bidirectional message relay - Proxy handler supporting both protocols - Router integration with proxy routes - Test script for local validation This resolves issue #91 by enabling native proxy support for both HTTP and WebSocket connections.
1 parent f65c654 commit 62057d4

5 files changed

Lines changed: 425 additions & 0 deletions

File tree

internal/pkg/proxy/http_proxier.go

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
package proxy
2+
3+
import (
4+
"net/http"
5+
"net/http/httputil"
6+
"net/url"
7+
"time"
8+
9+
"github.com/xinliangnote/go-gin-api/pkg/errors"
10+
"go.uber.org/zap"
11+
)
12+
13+
type HTTPProxier struct {
14+
target *url.URL
15+
logger *zap.Logger
16+
}
17+
18+
func NewHTTPProxier(target string, logger *zap.Logger) (*HTTPProxier, error) {
19+
targetURL, err := url.Parse(target)
20+
if err != nil {
21+
return nil, errors.Wrap(err, "failed to parse target URL")
22+
}
23+
24+
return &HTTPProxier{
25+
target: targetURL,
26+
logger: logger,
27+
}, nil
28+
}
29+
30+
func (h *HTTPProxier) ServeHTTP(w http.ResponseWriter, r *http.Request) {
31+
proxy := httputil.NewSingleHostReverseProxy(h.target)
32+
33+
originalDirector := proxy.Director
34+
proxy.Director = func(req *http.Request) {
35+
originalDirector(req)
36+
req.Host = h.target.Host
37+
req.URL.Scheme = h.target.Scheme
38+
req.URL.Host = h.target.Host
39+
}
40+
41+
proxy.ErrorHandler = func(w http.ResponseWriter, r *http.Request, err error) {
42+
h.logger.Error("proxy error",
43+
zap.String("target", h.target.String()),
44+
zap.String("path", r.URL.Path),
45+
zap.Error(err),
46+
)
47+
w.WriteHeader(http.StatusBadGateway)
48+
w.Write([]byte("Backend unreachable"))
49+
}
50+
51+
transport := &http.Transport{
52+
ResponseHeaderTimeout: 10 * time.Second,
53+
}
54+
proxy.Transport = transport
55+
56+
proxy.ServeHTTP(w, r)
57+
}

internal/pkg/proxy/proxy.go

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
package proxy
2+
3+
import (
4+
"net/http"
5+
"strings"
6+
7+
"github.com/xinliangnote/go-gin-api/pkg/errors"
8+
"go.uber.org/zap"
9+
)
10+
11+
const (
12+
ProtocolHTTP = "http"
13+
ProtocolWebSocket = "websocket"
14+
)
15+
16+
type Route struct {
17+
PathPrefix string
18+
BackendURL string
19+
Protocol string
20+
}
21+
22+
type Proxy struct {
23+
routes []Route
24+
httpProxiers map[string]*HTTPProxier
25+
wsProxiers map[string]*WebSocketProxier
26+
logger *zap.Logger
27+
}
28+
29+
func NewProxy(logger *zap.Logger) *Proxy {
30+
return &Proxy{
31+
httpProxiers: make(map[string]*HTTPProxier),
32+
wsProxiers: make(map[string]*WebSocketProxier),
33+
logger: logger,
34+
}
35+
}
36+
37+
func (p *Proxy) AddRoute(route Route) error {
38+
p.routes = append(p.routes, route)
39+
40+
switch route.Protocol {
41+
case ProtocolHTTP:
42+
proxier, err := NewHTTPProxier(route.BackendURL, p.logger)
43+
if err != nil {
44+
return err
45+
}
46+
p.httpProxiers[route.PathPrefix] = proxier
47+
48+
case ProtocolWebSocket:
49+
proxier, err := NewWebSocketProxier(route.BackendURL, p.logger)
50+
if err != nil {
51+
return err
52+
}
53+
p.wsProxiers[route.PathPrefix] = proxier
54+
55+
default:
56+
return errors.Errorf("unsupported protocol: %s", route.Protocol)
57+
}
58+
59+
return nil
60+
}
61+
62+
func (p *Proxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
63+
requestPath := r.URL.Path
64+
65+
for _, route := range p.routes {
66+
prefix := strings.TrimSuffix(route.PathPrefix, "/")
67+
if strings.HasPrefix(requestPath, prefix) {
68+
69+
switch route.Protocol {
70+
case ProtocolHTTP:
71+
if proxier, ok := p.httpProxiers[route.PathPrefix]; ok {
72+
proxier.ServeHTTP(w, r)
73+
return
74+
}
75+
case ProtocolWebSocket:
76+
if proxier, ok := p.wsProxiers[route.PathPrefix]; ok {
77+
proxier.ServeHTTP(w, r)
78+
return
79+
}
80+
}
81+
}
82+
}
83+
84+
http.NotFound(w, r)
85+
}
86+
87+
func (p *Proxy) GetRoutes() []Route {
88+
return p.routes
89+
}
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
package proxy
2+
3+
import (
4+
"context"
5+
"net"
6+
"net/http"
7+
"net/url"
8+
"time"
9+
10+
"github.com/gorilla/websocket"
11+
"github.com/xinliangnote/go-gin-api/pkg/errors"
12+
"go.uber.org/zap"
13+
)
14+
15+
type WebSocketProxier struct {
16+
target *url.URL
17+
dialer *websocket.Dialer
18+
handshakeTimeout time.Duration
19+
logger *zap.Logger
20+
}
21+
22+
func NewWebSocketProxier(target string, logger *zap.Logger) (*WebSocketProxier, error) {
23+
targetURL, err := url.Parse(target)
24+
if err != nil {
25+
return nil, errors.Wrap(err, "failed to parse target URL")
26+
}
27+
28+
return &WebSocketProxier{
29+
target: targetURL,
30+
dialer: &websocket.Dialer{
31+
HandshakeTimeout: 5 * time.Second,
32+
},
33+
handshakeTimeout: 5 * time.Second,
34+
logger: logger,
35+
}, nil
36+
}
37+
38+
func (w *WebSocketProxier) ServeHTTP(resp http.ResponseWriter, req *http.Request) {
39+
hijacker, ok := resp.(http.Hijacker)
40+
if !ok {
41+
w.logger.Error("connection does not support hijacking")
42+
http.Error(resp, "Cannot hijack connection", http.StatusInternalServerError)
43+
return
44+
}
45+
46+
clientConn, _, err := hijacker.Hijack()
47+
if err != nil {
48+
w.logger.Error("failed to hijack connection", zap.Error(err))
49+
http.Error(resp, "Failed to hijack connection", http.StatusInternalServerError)
50+
return
51+
}
52+
defer clientConn.Close()
53+
54+
backendURL := w.target.String() + req.URL.Path + "?" + req.URL.RawQuery
55+
backendConn, _, err := w.dialer.Dial(backendURL, req.Header)
56+
if err != nil {
57+
w.logger.Error("failed to dial backend",
58+
zap.String("backend", backendURL),
59+
zap.Error(err),
60+
)
61+
return
62+
}
63+
defer backendConn.Close()
64+
65+
ctx, cancel := context.WithCancel(context.Background())
66+
defer cancel()
67+
68+
go w.relayWebSocketToTCP(ctx, backendConn, clientConn)
69+
w.relayTCPToWebSocket(backendConn, clientConn)
70+
}
71+
72+
func (w *WebSocketProxier) relayWebSocketToTCP(ctx context.Context, src *websocket.Conn, dst net.Conn) {
73+
for {
74+
select {
75+
case <-ctx.Done():
76+
return
77+
default:
78+
_, data, err := src.ReadMessage()
79+
if err != nil {
80+
return
81+
}
82+
83+
if _, err := dst.Write(data); err != nil {
84+
return
85+
}
86+
}
87+
}
88+
}
89+
90+
func (w *WebSocketProxier) relayTCPToWebSocket(src *websocket.Conn, dst net.Conn) {
91+
buf := make([]byte, 1024)
92+
for {
93+
n, err := dst.Read(buf)
94+
if err != nil {
95+
return
96+
}
97+
98+
if err := src.WriteMessage(websocket.BinaryMessage, buf[:n]); err != nil {
99+
return
100+
}
101+
}
102+
}

internal/router/router_proxy.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package router
2+
3+
import (
4+
"github.com/xinliangnote/go-gin-api/internal/pkg/core"
5+
"github.com/xinliangnote/go-gin-api/internal/pkg/proxy"
6+
)
7+
8+
func setProxyRouter(r *resource) {
9+
proxyHandler := proxy.NewProxy(r.logger)
10+
11+
proxyHandler.AddRoute(proxy.Route{
12+
PathPrefix: "/api/v1/backend",
13+
BackendURL: "http://localhost:8081",
14+
Protocol: proxy.ProtocolHTTP,
15+
})
16+
17+
proxyHandler.AddRoute(proxy.Route{
18+
PathPrefix: "/socket/ws",
19+
BackendURL: "ws://localhost:8081",
20+
Protocol: proxy.ProtocolWebSocket,
21+
})
22+
23+
proxyGroup := r.mux.Group("/api/v1/backend", core.DisableTraceLog)
24+
{
25+
proxyGroup.Any("/*path", func(ctx core.Context) {
26+
proxyHandler.ServeHTTP(ctx.ResponseWriter(), ctx.Request())
27+
})
28+
}
29+
30+
socketProxyGroup := r.mux.Group("/socket", core.DisableTraceLog)
31+
{
32+
socketProxyGroup.GET("/*path", func(ctx core.Context) {
33+
proxyHandler.ServeHTTP(ctx.ResponseWriter(), ctx.Request())
34+
})
35+
}
36+
}

0 commit comments

Comments
 (0)