Reverse Proxy
Esta receta demuestra cómo usar Echo como reverse proxy y load balancer delante de tus aplicaciones, como WordPress, Node.js, Java, Python, Ruby o Go. Para simplificar, aquí los upstreams son servidores Go que también manejan WebSocket.
1) Identificar URL(s) de destino upstream
Sección titulada «1) Identificar URL(s) de destino upstream»url1, err := url.Parse("http://localhost:8081")if err != nil { e.Logger.Error("failed to parse url", "error", err) return}url2, err := url.Parse("http://localhost:8082")if err != nil { e.Logger.Error("failed to parse url", "error", err) return}targets := []*middleware.ProxyTarget{ {URL: url1}, {URL: url2},}2) Configurar middleware proxy con destinos upstream
Sección titulada «2) Configurar middleware proxy con destinos upstream»El snippet de abajo usa load balancing round-robin. También puedes usar
middleware.NewRandomBalancer().
e.Use(middleware.Proxy(middleware.NewRoundRobinBalancer(targets)))Para configurar un proxy para una subruta, usa Echo#Group().
// Canonicalize the group root so relative WebSocket URLs stay under /blog/.e.Any("/blog", func(c *echo.Context) error { location := "/blog/" if query := c.Request().URL.RawQuery; query != "" { location += "?" + query } return c.Redirect(http.StatusPermanentRedirect, location)})g := e.Group("/blog")g.Use(middleware.ProxyWithConfig(middleware.ProxyConfig{ Balancer: middleware.NewRoundRobinBalancer(targets), Rewrite: map[string]string{"/blog/*": "/$1"},}))// Group middleware needs a matching route. The proxy handles the response.g.Any("/*", func(c *echo.Context) error { return echo.ErrNotFound })3) Iniciar servidores upstream
Sección titulada «3) Iniciar servidores upstream»Desde cookbook/reverse-proxy, inicia cada upstream en su propia terminal:
go run ./upstream server1 :8081go run ./upstream server2 :80824) Iniciar el servidor proxy
Sección titulada «4) Iniciar el servidor proxy»En una tercera terminal, también en cookbook/reverse-proxy, inicia el proxy:
go run .Abre http://localhost:1323, y deberías ver una página web con un request HTTP
servido desde “server 1” y un request WebSocket servido desde “server 2”.
HTTP
Hello from upstream server server1
WebSocket
Hello from upstream server server2!Hello from upstream server server2!Hello from upstream server server2!Para usar el modo de grupo, detén el proxy predeterminado y ejecuta:
go run . -groupedAbre http://localhost:1323/blog/. El proxy elimina /blog antes de reenviar las solicitudes; la URL relativa de WebSocket de la página permanece bajo /blog/.
Código fuente
Sección titulada «Código fuente»Servidor upstream
Sección titulada «Servidor upstream»package main
import ( "context" "fmt" "net/http" "os" "time"
"github.com/labstack/echo/v5" "github.com/labstack/echo/v5/middleware" "golang.org/x/net/websocket")
var index = ` <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>Upstream Server</title> <style> h1, p { font-weight: 300; } </style> </head> <body> <h1>HTTP</h1> <p> Hello from upstream server %s </p> <h1>WebSocket</h1> <p id="output"></p> <script> var wsURL = new URL('ws', window.location.href) wsURL.protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:' var ws = new WebSocket(wsURL)
ws.onmessage = function(evt) { var out = document.getElementById('output'); out.innerHTML += evt.data + '<br>'; } </script> </body> </html>`
func main() { name := os.Args[1] port := os.Args[2] e := echo.New()
e.Use(middleware.RequestLogger()) e.Use(middleware.Recover())
e.GET("/", func(c *echo.Context) error { return c.HTML(http.StatusOK, fmt.Sprintf(index, name)) })
// WebSocket handler e.GET("/ws", func(c *echo.Context) error { websocket.Handler(func(ws *websocket.Conn) { defer ws.Close() for { // Write err := websocket.Message.Send(ws, fmt.Sprintf("Hello from upstream server %s!", name)) if err != nil { e.Logger.Error("failed to send message", "error", err) } select { case <-ws.Request().Context().Done(): return case <-time.After(1 * time.Second): continue } } }).ServeHTTP(c.Response(), c.Request()) return nil })
sc := echo.StartConfig{Address: port} if err := sc.Start(context.Background(), e); err != nil { e.Logger.Error("failed to start server", "error", err) }}Servidor proxy
Sección titulada «Servidor proxy»package main
import ( "context" "flag" "net/http" "net/url"
"github.com/labstack/echo/v5" "github.com/labstack/echo/v5/middleware")
func main() { grouped := flag.Bool("grouped", false, "limit the proxy to the /blog route group") flag.Parse() e := echo.New() e.Use(middleware.RequestLogger()) e.Use(middleware.Recover())
// Setup proxy url1, err := url.Parse("http://localhost:8081") if err != nil { e.Logger.Error("failed to parse url", "error", err) return } url2, err := url.Parse("http://localhost:8082") if err != nil { e.Logger.Error("failed to parse url", "error", err) return } targets := []*middleware.ProxyTarget{ {URL: url1}, {URL: url2}, } if *grouped { configureBlogProxy(e, targets) } else { e.Use(middleware.Proxy(middleware.NewRoundRobinBalancer(targets))) }
sc := echo.StartConfig{Address: ":1323"} if err := sc.Start(context.Background(), e); err != nil { e.Logger.Error("failed to start server", "error", err) }}
// configureBlogProxy limits the proxy middleware to a route group.func configureBlogProxy(e *echo.Echo, targets []*middleware.ProxyTarget) { // Canonicalize the group root so relative WebSocket URLs stay under /blog/. e.Any("/blog", func(c *echo.Context) error { location := "/blog/" if query := c.Request().URL.RawQuery; query != "" { location += "?" + query } return c.Redirect(http.StatusPermanentRedirect, location) }) g := e.Group("/blog") g.Use(middleware.ProxyWithConfig(middleware.ProxyConfig{ Balancer: middleware.NewRoundRobinBalancer(targets), Rewrite: map[string]string{"/blog/*": "/$1"}, })) // Group middleware needs a matching route. The proxy handles the response. g.Any("/*", func(c *echo.Context) error { return echo.ErrNotFound })}Actualización de seguridad de Echo (v5.4.0 / v4.16.0)
Sección titulada «Actualización de seguridad de Echo (v5.4.0 / v4.16.0)»Proxy reenvía X-Forwarded-Proto desde Context#Scheme() y elimina las otras cabeceras de esquema. En v5, X-Real-IP procede de Context#RealIP(). Si hay otro proxy delante de Echo, configura los extractores de esquema e IP para que el upstream reciba valores correctos.