diff --git a/site/astro.config.mjs b/site/astro.config.mjs index d457e289..fabbb55e 100644 --- a/site/astro.config.mjs +++ b/site/astro.config.mjs @@ -15,6 +15,7 @@ export default defineConfig({ root: { label: 'English', lang: 'en' }, 'zh-cn': { label: '简体中文', lang: 'zh-CN' }, ja: { label: '日本語', lang: 'ja' }, + es: { label: 'Español', lang: 'es' }, }, logo: { light: './src/assets/logo-light.svg', @@ -103,9 +104,9 @@ export default defineConfig({ // Autogenerated from the content dirs — new pages appear automatically, // ordered by each page's `sidebar.order` frontmatter. sidebar: [ - { label: 'Guide', translations: { 'zh-CN': '指南', ja: 'ガイド' }, items: [{ autogenerate: { directory: 'guide' } }] }, - { label: 'Middleware', translations: { 'zh-CN': '中间件', ja: 'ミドルウェア' }, items: [{ autogenerate: { directory: 'middleware' } }] }, - { label: 'Cookbook', translations: { 'zh-CN': '示例', ja: 'クックブック' }, items: [{ autogenerate: { directory: 'cookbook' } }] }, + { label: 'Guide', translations: { 'zh-CN': '指南', ja: 'ガイド', es: 'Guía' }, items: [{ autogenerate: { directory: 'guide' } }] }, + { label: 'Middleware', translations: { 'zh-CN': '中间件', ja: 'ミドルウェア', es: 'Middleware' }, items: [{ autogenerate: { directory: 'middleware' } }] }, + { label: 'Cookbook', translations: { 'zh-CN': '示例', ja: 'クックブック', es: 'Recetario' }, items: [{ autogenerate: { directory: 'cookbook' } }] }, ], // tune the built-in code theme toward our terminal palette expressiveCode: { themes: ['github-dark', 'github-light'] }, diff --git a/site/src/content/docs/es/cookbook/auto-tls.md b/site/src/content/docs/es/cookbook/auto-tls.md new file mode 100644 index 00000000..b2295c33 --- /dev/null +++ b/site/src/content/docs/es/cookbook/auto-tls.md @@ -0,0 +1,110 @@ +--- +title: Auto TLS +description: Obtén y renueva automáticamente certificados TLS desde Let's Encrypt. +sidebar: + order: 3 +--- + +Esta receta obtiene automáticamente certificados TLS para un dominio desde Let's Encrypt. +Configura un `StartConfig` con el `TLSConfig` del manager autocert y escucha en el +puerto `443`. + +Abre `https://`. Si todo está configurado correctamente, deberías ver +un mensaje de bienvenida servido sobre TLS. + +:::tip +- Para mayor seguridad, especifica una host policy en el manager autocert. +- Cachea certificados para evitar alcanzar los [rate limits de Let's Encrypt](https://letsencrypt.org/docs/rate-limits). +- Para redirigir tráfico HTTP a HTTPS, usa el [middleware redirect](/es/middleware/redirect/#https-redirect). +::: + +## Servidor + +```go +package main + +import ( + "context" + "crypto/tls" + "errors" + "log/slog" + "net/http" + "os" + + "golang.org/x/crypto/acme" + + "github.com/labstack/echo/v5" + "github.com/labstack/echo/v5/middleware" + "golang.org/x/crypto/acme/autocert" +) + +func main() { + e := echo.New() + e.Logger = slog.New(slog.NewJSONHandler(os.Stdout, nil)) + + e.Use(middleware.Recover()) + e.Use(middleware.RequestLogger()) + + e.GET("/", func(c *echo.Context) error { + return c.HTML(http.StatusOK, ` +

Welcome to Echo!

+

TLS certificates automatically installed from Let's Encrypt :)

+ `) + }) + + m := &autocert.Manager{ + Prompt: autocert.AcceptTOS, + HostPolicy: autocert.HostWhitelist("example.com", "www.example.com"), + // Cache certificates to avoid issues with rate limits (https://letsencrypt.org/docs/rate-limits) + Cache: autocert.DirCache("/var/www/.cache"), + // Email: "[email protected]", // optional but recommended + } + + sc := echo.StartConfig{ + Address: ":443", + TLSConfig: m.TLSConfig(), + } + if err := sc.Start(context.Background(), e); err != nil { + e.Logger.Error("failed to start server", "error", err) + } +} +``` + +## Usar un servidor HTTP personalizado + +Si necesitas control total sobre `http.Server`, conecta el manager autocert a un +`tls.Config` personalizado: + +```go +func customHTTPServer() { + e := echo.New() + e.Use(middleware.Recover()) + e.Use(middleware.RequestLogger()) + e.GET("/", func(c *echo.Context) error { + return c.HTML(http.StatusOK, ` +

Welcome to Echo!

+

TLS certificates automatically installed from Let's Encrypt :)

+ `) + }) + + autoTLSManager := autocert.Manager{ + Prompt: autocert.AcceptTOS, + // Cache certificates to avoid issues with rate limits (https://letsencrypt.org/docs/rate-limits) + Cache: autocert.DirCache("/var/www/.cache"), + //HostPolicy: autocert.HostWhitelist(""), + } + s := http.Server{ + Addr: ":443", + Handler: e, // set Echo as handler + TLSConfig: &tls.Config{ + //Certificates: nil, // <-- s.ListenAndServeTLS will populate this field + GetCertificate: autoTLSManager.GetCertificate, + NextProtos: []string{acme.ALPNProto}, + }, + //ReadTimeout: 30 * time.Second, // use custom timeouts + } + if err := s.ListenAndServeTLS("", ""); err != nil && !errors.Is(err, http.ErrServerClosed) { + e.Logger.Error("failed to start server", "error", err) + } +} +``` diff --git a/site/src/content/docs/es/cookbook/cors.md b/site/src/content/docs/es/cookbook/cors.md new file mode 100644 index 00000000..411692bd --- /dev/null +++ b/site/src/content/docs/es/cookbook/cors.md @@ -0,0 +1,120 @@ +--- +title: CORS +description: Habilita Cross-Origin Resource Sharing con una allow list o una función de origin personalizada. +sidebar: + order: 4 +--- + +El [middleware CORS](/es/middleware/cors/) controla qué origins pueden acceder a tu API. +Puedes pasar una lista fija de origins permitidos o proporcionar una función que decida +por request. + +## Allow list de origins + +Pasa los origins permitidos directamente a `middleware.CORS`. + +```go +package main + +import ( + "context" + "net/http" + + "github.com/labstack/echo/v5" + "github.com/labstack/echo/v5/middleware" +) + +var ( + users = []string{"Joe", "Veer", "Zion"} +) + +func getUsers(c *echo.Context) error { + return c.JSON(http.StatusOK, users) +} + +func main() { + e := echo.New() + e.Use(middleware.RequestLogger()) + e.Use(middleware.Recover()) + + // CORS default + // Allows requests from any origin wth GET, HEAD, PUT, POST or DELETE method. + // e.Use(middleware.CORS("*")) + + // CORS restricted + // Allows requests from any `https://labstack.com` or `https://labstack.net` origin + e.Use(middleware.CORS("https://labstack.com", "https://labstack.net")) + + e.GET("/api/users", getUsers) + + sc := echo.StartConfig{Address: ":1323"} + if err := sc.Start(context.Background(), e); err != nil { + e.Logger.Error("failed to start server", "error", err) + } +} +``` + +## Función de origin personalizada + +Para policies dinámicas, usa `CORSWithConfig` con `UnsafeAllowOriginFunc`. La +función recibe el contexto del request y el origin, y devuelve el origin que se debe +reflejar, si el request está permitido y un error opcional. + +```go +package main + +import ( + "context" + "net/http" + "strings" + + "github.com/labstack/echo/v5" + "github.com/labstack/echo/v5/middleware" +) + +var ( + users = []string{"Joe", "Veer", "Zion"} +) + +func getUsers(c *echo.Context) error { + return c.JSON(http.StatusOK, users) +} + +// allowOrigin takes the origin as an argument and returns: +// - origin to add to the response Access-Control-Allow-Origin header +// - whether the request is allowed or not +// - an optional error. this will stop handler chain execution and return an error response. +// +// return origin, true, err // blocks request with error +// return origin, true, nil // allows CSRF request through +// return origin, false, nil // falls back to legacy token logic +func allowOrigin(c *echo.Context, origin string) (string, bool, error) { + // In this example we use a naive suffix check but we can imagine various + // kind of custom logic. For example, an external datasource could be used + // to maintain the list of allowed origins. + if strings.HasSuffix(origin, ".example.com") { + return origin, true, nil + } + return "", false, nil +} + +func main() { + e := echo.New() + e.Use(middleware.RequestLogger()) + e.Use(middleware.Recover()) + + // CORS restricted with a custom function to allow origins + // and with the GET, PUT, POST or DELETE methods allowed. + e.Use(middleware.CORSWithConfig(middleware.CORSConfig{ + UnsafeAllowOriginFunc: allowOrigin, + AllowMethods: []string{http.MethodGet, http.MethodPut, http.MethodPost, http.MethodDelete}, + })) + + e.GET("/api/users", getUsers) + + sc := echo.StartConfig{Address: ":1323"} + if err := sc.Start(context.Background(), e); err != nil { + e.Logger.Error("failed to start server", "error", err) + } +} +``` diff --git a/site/src/content/docs/es/cookbook/crud.md b/site/src/content/docs/es/cookbook/crud.md new file mode 100644 index 00000000..4e2ab848 --- /dev/null +++ b/site/src/content/docs/es/cookbook/crud.md @@ -0,0 +1,180 @@ +--- +title: CRUD +description: Crea, lee, actualiza y elimina recursos con Echo y binding JSON. +sidebar: + order: 2 +--- + +Una API CRUD (create, read, update, delete) completa respaldada por un store en memoria. +Cada handler vincula el body JSON del request a un struct, muta el store bajo un lock y +devuelve el resultado como JSON. + +## Servidor + +```go +package main + +import ( + "context" + "net/http" + "strconv" + "sync" + + "github.com/labstack/echo/v5" + "github.com/labstack/echo/v5/middleware" +) + +type ( + user struct { + ID int `json:"id"` + Name string `json:"name"` + } +) + +var ( + users = map[int]*user{} + seq = 1 + lock = sync.Mutex{} +) + +//---------- +// Handlers +//---------- + +func createUser(c *echo.Context) error { + lock.Lock() + defer lock.Unlock() + u := &user{ + ID: seq, + } + if err := c.Bind(u); err != nil { + return err + } + users[u.ID] = u + seq++ + return c.JSON(http.StatusCreated, u) +} + +func getUser(c *echo.Context) error { + lock.Lock() + defer lock.Unlock() + id, _ := strconv.Atoi(c.Param("id")) + return c.JSON(http.StatusOK, users[id]) +} + +func updateUser(c *echo.Context) error { + lock.Lock() + defer lock.Unlock() + u := new(user) + if err := c.Bind(u); err != nil { + return err + } + id, _ := strconv.Atoi(c.Param("id")) + users[id].Name = u.Name + return c.JSON(http.StatusOK, users[id]) +} + +func deleteUser(c *echo.Context) error { + lock.Lock() + defer lock.Unlock() + id, _ := strconv.Atoi(c.Param("id")) + delete(users, id) + return c.NoContent(http.StatusNoContent) +} + +func getAllUsers(c *echo.Context) error { + lock.Lock() + defer lock.Unlock() + return c.JSON(http.StatusOK, users) +} + +func main() { + e := echo.New() + + // Middleware + e.Use(middleware.RequestLogger()) + e.Use(middleware.Recover()) + + // Routes + e.GET("/users", getAllUsers) + e.POST("/users", createUser) + e.GET("/users/:id", getUser) + e.PUT("/users/:id", updateUser) + e.DELETE("/users/:id", deleteUser) + + // Start server + sc := echo.StartConfig{Address: ":1323"} + if err := sc.Start(context.Background(), e); err != nil { + e.Logger.Error("failed to start server", "error", err) + } +} +``` + +## Cliente + +### Crear usuario + +Request: + +```sh +curl -X POST \ + -H 'Content-Type: application/json' \ + -d '{"name":"Joe Smith"}' \ + localhost:1323/users +``` + +Response: + +```json +{ + "id": 1, + "name": "Joe Smith" +} +``` + +### Obtener usuario + +Request: + +```sh +curl localhost:1323/users/1 +``` + +Response: + +```json +{ + "id": 1, + "name": "Joe Smith" +} +``` + +### Actualizar usuario + +Request: + +```sh +curl -X PUT \ + -H 'Content-Type: application/json' \ + -d '{"name":"Joe"}' \ + localhost:1323/users/1 +``` + +Response: + +```json +{ + "id": 1, + "name": "Joe" +} +``` + +### Eliminar usuario + +Request: + +```sh +curl -X DELETE localhost:1323/users/1 +``` + +Response: `204 No Content`. diff --git a/site/src/content/docs/es/cookbook/embed-resources.md b/site/src/content/docs/es/cookbook/embed-resources.md new file mode 100644 index 00000000..6bb1dc3a --- /dev/null +++ b/site/src/content/docs/es/cookbook/embed-resources.md @@ -0,0 +1,65 @@ +--- +title: Recursos embebidos +description: Sirve assets estáticos empaquetados en el binario con el paquete embed de Go. +sidebar: + order: 5 +--- + +El paquete `embed` de Go (Go 1.16+) te permite compilar assets estáticos directamente en el +binario, por lo que un único ejecutable puede incluir su frontend. Esta receta sirve el +filesystem embebido mediante Echo, con un modo live opcional que lee desde disco durante +el desarrollo. + +## Servidor + +```go +package main + +import ( + "context" + "embed" + "io/fs" + "log" + "net/http" + "os" + + "github.com/labstack/echo/v5" +) + +//go:embed app +var embededFiles embed.FS + +func getFileSystem(useOS bool) http.FileSystem { + if useOS { + log.Print("using live mode") + return http.FS(os.DirFS("app")) + } + + log.Print("using embed mode") + fsys, err := fs.Sub(embededFiles, "app") + if err != nil { + panic(err) + } + + return http.FS(fsys) +} + +func main() { + e := echo.New() + useOS := len(os.Args) > 1 && os.Args[1] == "live" + assetHandler := http.FileServer(getFileSystem(useOS)) + e.GET("/", echo.WrapHandler(assetHandler)) + e.GET("/static/*", echo.WrapHandler(http.StripPrefix("/static/", assetHandler))) + + sc := echo.StartConfig{Address: ":1323"} + if err := sc.Start(context.Background(), e); err != nil { + e.Logger.Error("failed to start server", "error", err) + } +} +``` + +:::tip +Ejecuta el binario con el argumento `live` (`go run server.go live`) para servir assets +desde el directorio `app` en disco en lugar de la copia embebida, lo que resulta útil +durante el desarrollo. +::: diff --git a/site/src/content/docs/es/cookbook/file-download.md b/site/src/content/docs/es/cookbook/file-download.md new file mode 100644 index 00000000..7208b52f --- /dev/null +++ b/site/src/content/docs/es/cookbook/file-download.md @@ -0,0 +1,175 @@ +--- +title: Descarga de archivos +description: Sirve archivos para descarga, visualización inline o como attachments con nombre. +sidebar: + order: 6 +--- + +Echo proporciona tres helpers de contexto para devolver archivos: `c.File` sirve un archivo +usando la content disposition por defecto del navegador, `c.Inline` sugiere al navegador +mostrar el archivo en su lugar, y `c.Attachment` solicita una descarga con un filename dado. + +## Descargar archivo + +### Servidor + +```go +package main + +import ( + "context" + + "github.com/labstack/echo/v5" + "github.com/labstack/echo/v5/middleware" +) + +func main() { + e := echo.New() + + e.Use(middleware.RequestLogger()) + e.Use(middleware.Recover()) + + e.GET("/", func(c *echo.Context) error { + return c.File("index.html") + }) + e.GET("/file", func(c *echo.Context) error { + return c.File("echo.svg") + }) + + sc := echo.StartConfig{Address: ":1323"} + if err := sc.Start(context.Background(), e); err != nil { + e.Logger.Error("failed to start server", "error", err) + } +} +``` + +### Cliente + +```html + + + + + File download + + + +

+ File download +

+ + + +``` + +## Descargar archivo como inline + +Usa `c.Inline` para enviar un header `Content-Disposition: inline`, de modo que el navegador +renderice el archivo en su lugar en vez de descargarlo. + +### Servidor + +```go +package main + +import ( + "context" + + "github.com/labstack/echo/v5" + "github.com/labstack/echo/v5/middleware" +) + +func main() { + e := echo.New() + + e.Use(middleware.RequestLogger()) + e.Use(middleware.Recover()) + + e.GET("/", func(c *echo.Context) error { + return c.File("index.html") + }) + e.GET("/inline", func(c *echo.Context) error { + return c.Inline("inline.txt", "inline.txt") + }) + + sc := echo.StartConfig{Address: ":1323"} + if err := sc.Start(context.Background(), e); err != nil { + e.Logger.Error("failed to start server", "error", err) + } +} +``` + +### Cliente + +```html + + + + + File download + + + +

+ Inline file download +

+ + + +``` + +## Descargar archivo como attachment + +Usa `c.Attachment` para enviar un header `Content-Disposition: attachment`, solicitando +al navegador descargar el archivo con el nombre proporcionado. + +### Servidor + +```go +package main + +import ( + "context" + + "github.com/labstack/echo/v5" + "github.com/labstack/echo/v5/middleware" +) + +func main() { + e := echo.New() + + e.Use(middleware.RequestLogger()) + e.Use(middleware.Recover()) + + e.GET("/", func(c *echo.Context) error { + return c.File("index.html") + }) + e.GET("/attachment", func(c *echo.Context) error { + return c.Attachment("attachment.txt", "attachment.txt") + }) + + sc := echo.StartConfig{Address: ":1323"} + if err := sc.Start(context.Background(), e); err != nil { + e.Logger.Error("failed to start server", "error", err) + } +} +``` + +### Cliente + +```html + + + + + File download + + + +

+ Attachment file download +

+ + + +``` diff --git a/site/src/content/docs/es/cookbook/file-upload.md b/site/src/content/docs/es/cookbook/file-upload.md new file mode 100644 index 00000000..80939905 --- /dev/null +++ b/site/src/content/docs/es/cookbook/file-upload.md @@ -0,0 +1,198 @@ +--- +title: Subida de archivos +description: Maneja uploads multipart de uno o varios archivos junto a campos de formulario. +sidebar: + order: 7 +--- + +Echo lee datos de formularios multipart mediante el contexto del request. Usa `c.FormValue` +para campos de texto, `c.FormFile` para un solo archivo y `c.MultipartForm` para acceder +a múltiples archivos bajo el mismo nombre de campo. + +## Subir un solo archivo con campos + +### Servidor + +```go +package main + +import ( + "context" + "fmt" + "io" + "net/http" + "os" + + "github.com/labstack/echo/v5" + "github.com/labstack/echo/v5/middleware" +) + +func upload(c *echo.Context) error { + // Read form fields + name := c.FormValue("name") + email := c.FormValue("email") + + //----------- + // Read file + //----------- + + // Source + file, err := c.FormFile("file") + if err != nil { + return err + } + src, err := file.Open() + if err != nil { + return err + } + defer src.Close() + + // Destination + dst, err := os.Create(file.Filename) + if err != nil { + return err + } + defer dst.Close() + + // Copy + if _, err = io.Copy(dst, src); err != nil { + return err + } + + return c.HTML(http.StatusOK, fmt.Sprintf("

File %s uploaded successfully with fields name=%s and email=%s.

", file.Filename, name, email)) +} + +func main() { + e := echo.New() + + e.Use(middleware.RequestLogger()) + e.Use(middleware.Recover()) + + e.Static("/", "public") + e.POST("/upload", upload) + + sc := echo.StartConfig{Address: ":1323"} + if err := sc.Start(context.Background(), e); err != nil { + e.Logger.Error("failed to start server", "error", err) + } +} +``` + +### Cliente + +```html + + + + + Single file upload + + +

Upload single file with fields

+ +
+ Name:
+ Email:
+ Files:

+ +
+ + +``` + +## Subir múltiples archivos con campos + +### Servidor + +```go +package main + +import ( + "context" + "fmt" + "io" + "net/http" + "os" + + "github.com/labstack/echo/v5" + "github.com/labstack/echo/v5/middleware" +) + +func upload(c *echo.Context) error { + // Read form fields + name := c.FormValue("name") + email := c.FormValue("email") + + //------------ + // Read files + //------------ + + // Multipart form + form, err := c.MultipartForm() + if err != nil { + return err + } + files := form.File["files"] + + for _, file := range files { + // Source + src, err := file.Open() + if err != nil { + return err + } + defer src.Close() + + // Destination + dst, err := os.Create(file.Filename) + if err != nil { + return err + } + defer dst.Close() + + // Copy + if _, err = io.Copy(dst, src); err != nil { + return err + } + + } + + return c.HTML(http.StatusOK, fmt.Sprintf("

Uploaded successfully %d files with fields name=%s and email=%s.

", len(files), name, email)) +} + +func main() { + e := echo.New() + + e.Use(middleware.RequestLogger()) + e.Use(middleware.Recover()) + + e.Static("/", "public") + e.POST("/upload", upload) + + sc := echo.StartConfig{Address: ":1323"} + if err := sc.Start(context.Background(), e); err != nil { + e.Logger.Error("failed to start server", "error", err) + } +} +``` + +### Cliente + +```html + + + + + Multiple file upload + + +

Upload multiple files with fields

+ +
+ Name:
+ Email:
+ Files:

+ +
+ + +``` diff --git a/site/src/content/docs/es/cookbook/graceful-shutdown.md b/site/src/content/docs/es/cookbook/graceful-shutdown.md new file mode 100644 index 00000000..8479a141 --- /dev/null +++ b/site/src/content/docs/es/cookbook/graceful-shutdown.md @@ -0,0 +1,86 @@ +--- +title: Apagado graceful +description: Drena requests en curso antes de detener el servidor ante una señal de interrupción. +sidebar: + order: 8 +--- + +Un apagado graceful permite que los requests en curso terminen antes de que el proceso salga. +El enfoque más simple es pasar un contexto cancelable a `StartConfig.Start` y establecer +un `GracefulTimeout`. Cuando el contexto se cancela por una señal de interrupción, Echo +deja de aceptar nuevas conexiones y espera hasta el timeout para que los requests activos +se completen. + +## Servidor + +```go +package main + +import ( + "context" + "errors" + "net/http" + "os" + "os/signal" + "syscall" + "time" + + "github.com/labstack/echo/v5" +) + +func main() { + // Setup + e := echo.New() + e.GET("/", func(c *echo.Context) error { + time.Sleep(5 * time.Second) + return c.JSON(http.StatusOK, "OK") + }) + + ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) + defer stop() + + sc := echo.StartConfig{ + Address: ":1323", + GracefulTimeout: 5 * time.Second, + } + if err := sc.Start(ctx, e); err != nil { + e.Logger.Error("failed to start server", "error", err) + } +} +``` + +## Usar un servidor HTTP personalizado + +Si administras el `http.Server` por tu cuenta, inícialo en una goroutine, espera el +contexto de señal y luego llama a `Shutdown` con un timeout: + +```go +func mainWithHTTPServer() { + // Setup + e := echo.New() + e.GET("/", func(c *echo.Context) error { + time.Sleep(5 * time.Second) + return c.JSON(http.StatusOK, "OK") + }) + + ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) + defer stop() + + s := http.Server{Addr: ":1323", Handler: e} + // Start server + go func() { + if err := s.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { + e.Logger.Error("failed to start server", "error", err) + } + }() + + // Wait for interrupt signal to gracefully shut down the server with a timeout of 10 seconds. + <-ctx.Done() + + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + if err := s.Shutdown(ctx); err != nil { + e.Logger.Error("failed to stop server", "error", err) + } +} +``` diff --git a/site/src/content/docs/es/cookbook/hello-world.md b/site/src/content/docs/es/cookbook/hello-world.md new file mode 100644 index 00000000..105d8a92 --- /dev/null +++ b/site/src/content/docs/es/cookbook/hello-world.md @@ -0,0 +1,43 @@ +--- +title: Hello World +description: Un servidor Echo mínimo que responde con un saludo. +sidebar: + order: 1 +--- + +Una aplicación Echo mínima: crea una instancia, registra los middleware Logger y Recover, +agrega una sola ruta e inicia el servidor. + +## Servidor + +```go +package main + +import ( + "context" + "net/http" + + "github.com/labstack/echo/v5" + "github.com/labstack/echo/v5/middleware" +) + +func main() { + // Echo instance + e := echo.New() + + // Middleware + e.Use(middleware.RequestLogger()) + e.Use(middleware.Recover()) + + // Route => handler + e.GET("/", func(c *echo.Context) error { + return c.String(http.StatusOK, "Hello, World!\n") + }) + + // Start server + sc := echo.StartConfig{Address: ":1323"} + if err := sc.Start(context.Background(), e); err != nil { + e.Logger.Error("failed to start server", "error", err) + } +} +``` diff --git a/site/src/content/docs/es/cookbook/http2-server-push.md b/site/src/content/docs/es/cookbook/http2-server-push.md new file mode 100644 index 00000000..fd71df5f --- /dev/null +++ b/site/src/content/docs/es/cookbook/http2-server-push.md @@ -0,0 +1,130 @@ +--- +title: HTTP/2 Server Push +description: Envía web assets al cliente proactivamente sobre HTTP/2. +sidebar: + order: 10 +--- + +HTTP/2 server push permite que el servidor envíe recursos al cliente antes de que se +soliciten, eliminando un viaje de ida y vuelta para assets que la página sabe que necesitará. +Esta receta envía el CSS, JavaScript e imagen de una página junto con la response HTML. + +:::note +Server push requiere una conexión HTTP/2. Sigue [Generar un certificado TLS X.509 +autofirmado](/es/cookbook/http2/#1-generate-a-self-signed-x509-tls-certificate) +para crear el certificado usado abajo. +::: + +## 1. Registrar una ruta para servir web assets + +```go +e.Static("/", "static") +``` + +## 2. Servir index.html y hacer push de sus dependencias + +Desenvuelve la response para acceder al `http.ResponseWriter` subyacente, y luego haz push +de cada asset si el writer implementa `http.Pusher`: + +```go +e.GET("/", func(c *echo.Context) (err error) { + rw, err := echo.UnwrapResponse(c.Response()) + if err != nil { + return + } + if pusher, ok := rw.ResponseWriter.(http.Pusher); ok { + if err = pusher.Push("/app.css", nil); err != nil { + return + } + if err = pusher.Push("/app.js", nil); err != nil { + return + } + if err = pusher.Push("/echo.png", nil); err != nil { + return + } + } + return c.File("index.html") +}) +``` + +:::tip +Cuando `http.Pusher` está soportado, los web assets se envían proactivamente; de lo contrario, +el cliente vuelve a solicitarlos por separado. +::: + +## 3. Iniciar el servidor TLS + +```go +sc := echo.StartConfig{Address: ":1323"} +if err := sc.StartTLS(context.Background(), e, "cert.pem", "key.pem"); err != nil { + e.Logger.Error("failed to start server", "error", err) +} +``` + +## Código fuente + +### index.html + +```html + + + + + + + HTTP/2 Server Push + + + + + +

The following static files are served via HTTP/2 server push

+ + + +``` + +### server.go + +```go +package main + +import ( + "context" + "net/http" + + "github.com/labstack/echo/v5" +) + +func main() { + e := echo.New() + e.Static("/", "static") + e.GET("/", func(c *echo.Context) (err error) { + rw, err := echo.UnwrapResponse(c.Response()) + if err != nil { + return + } + if pusher, ok := rw.ResponseWriter.(http.Pusher); ok { + if err = pusher.Push("/app.css", nil); err != nil { + return + } + if err = pusher.Push("/app.js", nil); err != nil { + return + } + if err = pusher.Push("/echo.png", nil); err != nil { + return + } + } + return c.File("index.html") + }) + + sc := echo.StartConfig{Address: ":1323"} + if err := sc.StartTLS(context.Background(), e, "cert.pem", "key.pem"); err != nil { + e.Logger.Error("failed to start server", "error", err) + } +} +``` diff --git a/site/src/content/docs/es/cookbook/http2.md b/site/src/content/docs/es/cookbook/http2.md new file mode 100644 index 00000000..2ba77367 --- /dev/null +++ b/site/src/content/docs/es/cookbook/http2.md @@ -0,0 +1,116 @@ +--- +title: Servidor HTTP/2 +description: Sirve tráfico sobre HTTP/2 iniciando Echo con un certificado TLS. +sidebar: + order: 9 +--- + +HTTP/2 mejora la latencia mediante multiplexing de requests, compresión de headers y +server push. El servidor HTTP de Go negocia HTTP/2 automáticamente sobre TLS, por lo que +servir HTTP/2 con Echo consiste en iniciar el servidor con un certificado. + +## 1. Generar un certificado TLS X.509 autofirmado + +Ejecuta el siguiente comando para generar `cert.pem` y `key.pem`: + +```sh +go run $GOROOT/src/crypto/tls/generate_cert.go --host localhost +``` + +:::note +Con fines de demostración usamos un certificado autofirmado. En producción, obtén +un certificado de una [certificate authority](https://en.wikipedia.org/wiki/Certificate_authority). +::: + +## 2. Crear un handler que refleje información del request + +```go +e.GET("/request", func(c *echo.Context) error { + req := c.Request() + format := ` + + Protocol: %s
+ Host: %s
+ Remote Address: %s
+ Method: %s
+ Path: %s
+
+ ` + return c.HTML(http.StatusOK, fmt.Sprintf(format, req.Proto, req.Host, req.RemoteAddr, req.Method, req.URL.Path)) +}) +``` + +## 3. Iniciar el servidor TLS + +Inicia el servidor con el certificado y la key generados: + +```go +sc := echo.StartConfig{Address: ":1323"} +if err := sc.StartTLS(context.Background(), e, "cert.pem", "key.pem"); err != nil { + e.Logger.Error("failed to start server", "error", err) +} +``` + +Alternativamente, usa un `http.Server` personalizado con tu propio `tls.Config`: + +```go +s := http.Server{ + Addr: ":8443", + Handler: e, // set Echo as handler + TLSConfig: &tls.Config{ + //Certificates: nil, // <-- s.ListenAndServeTLS will populate this field + }, + //ReadTimeout: 30 * time.Second, // use custom timeouts +} +if err := s.ListenAndServeTLS("cert.pem", "key.pem"); err != http.ErrServerClosed { + log.Fatal(err) +} +``` + +## 4. Verificar + +Inicia el servidor y abre `https://localhost:1323/request`. Deberías ver una salida +similar a: + +```sh +Protocol: HTTP/2.0 +Host: localhost:1323 +Remote Address: [::1]:60288 +Method: GET +Path: / +``` + +## Código fuente + +```go +package main + +import ( + "context" + "fmt" + "net/http" + + "github.com/labstack/echo/v5" +) + +func main() { + e := echo.New() + e.GET("/request", func(c *echo.Context) error { + req := c.Request() + format := ` + + Protocol: %s
+ Host: %s
+ Remote Address: %s
+ Method: %s
+ Path: %s
+
+ ` + return c.HTML(http.StatusOK, fmt.Sprintf(format, req.Proto, req.Host, req.RemoteAddr, req.Method, req.URL.Path)) + }) + sc := echo.StartConfig{Address: ":1323"} + if err := sc.StartTLS(context.Background(), e, "cert.pem", "key.pem"); err != nil { + e.Logger.Error("failed to start server", "error", err) + } +} +``` diff --git a/site/src/content/docs/es/cookbook/jsonp.md b/site/src/content/docs/es/cookbook/jsonp.md new file mode 100644 index 00000000..106b927d --- /dev/null +++ b/site/src/content/docs/es/cookbook/jsonp.md @@ -0,0 +1,95 @@ +--- +title: JSONP +description: Sirve responses JSONP para requests cross-domain con Context#JSONP. +sidebar: + order: 13 +--- + +JSONP es una técnica que permite llamadas de servidor cross-domain desde el navegador. Echo +sirve responses JSONP con `c.JSONP()`, que envuelve el payload JSON en una llamada a la +función callback nombrada en el request. + +## Servidor + +```go +package main + +import ( + "context" + "math/rand" + "net/http" + "time" + + "github.com/labstack/echo/v5" + "github.com/labstack/echo/v5/middleware" +) + +func main() { + e := echo.New() + e.Use(middleware.RequestLogger()) + e.Use(middleware.Recover()) + + e.Static("/", "public") + + // JSONP + e.GET("/jsonp", func(c *echo.Context) error { + callback := c.QueryParam("callback") + var content struct { + Response string `json:"response"` + Timestamp time.Time `json:"timestamp"` + Random int `json:"random"` + } + content.Response = "Sent via JSONP" + content.Timestamp = time.Now().UTC() + content.Random = rand.Intn(1000) + return c.JSONP(http.StatusOK, callback, &content) + }) + + // Start server + sc := echo.StartConfig{Address: ":1323"} + if err := sc.Start(context.Background(), e); err != nil { + e.Logger.Error("failed to start server", "error", err) + } +} +``` + +## Cliente + +```html + + + + + + + JSONP + + + + + + +
+ +

+


+        

+
+ + + +``` diff --git a/site/src/content/docs/es/cookbook/jwt.md b/site/src/content/docs/es/cookbook/jwt.md new file mode 100644 index 00000000..e746c3af --- /dev/null +++ b/site/src/content/docs/es/cookbook/jwt.md @@ -0,0 +1,251 @@ +--- +title: JWT +description: Autentica requests con JSON Web Tokens usando el middleware echo-jwt. +sidebar: + order: 11 +--- + +Esta receta demuestra autenticación JWT con Echo usando el middleware +[`echo-jwt`](https://github.com/labstack/echo-jwt): + +- Autenticación JWT usando el algoritmo HS256. +- El token se lee desde el header de request `Authorization`. + +Consulta la página del [middleware JWT](/es/middleware/jwt/) para ver todas las opciones de configuración. + +## Servidor + +### Usar claims personalizados + +Define un tipo de claims que embebe `jwt.RegisteredClaims`, y luego apunta el middleware a él +con `NewClaimsFunc`. Dentro del handler restringido, recupera el token parseado desde el +contexto con el genérico `echo.ContextGet`. + +```go +package main + +import ( + "context" + "net/http" + "time" + + "github.com/golang-jwt/jwt/v5" + echojwt "github.com/labstack/echo-jwt/v5" + "github.com/labstack/echo/v5" + "github.com/labstack/echo/v5/middleware" +) + +// jwtCustomClaims are custom claims extending default ones. +// See https://github.com/golang-jwt/jwt for more examples +type jwtCustomClaims struct { + Name string `json:"name"` + Admin bool `json:"admin"` + jwt.RegisteredClaims +} + +func login(c *echo.Context) error { + username := c.FormValue("username") + password := c.FormValue("password") + + // Throws unauthorized error + if username != "jon" || password != "shhh!" { + return echo.ErrUnauthorized + } + + // Set custom claims + claims := &jwtCustomClaims{ + "Jon Snow", + true, + jwt.RegisteredClaims{ + ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour * 72)), + }, + } + + // Create token with claims + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + + // Generate encoded token and send it as response. + t, err := token.SignedString([]byte("secret")) + if err != nil { + return err + } + + return c.JSON(http.StatusOK, map[string]string{ + "token": t, + }) +} + +func accessible(c *echo.Context) error { + return c.String(http.StatusOK, "Accessible") +} + +func restricted(c *echo.Context) error { + token, err := echo.ContextGet[*jwt.Token](c, "user") + if err != nil { + return echo.ErrUnauthorized.Wrap(err) + } + claims := token.Claims.(*jwtCustomClaims) + name := claims.Name + return c.String(http.StatusOK, "Welcome "+name+"!") +} + +func main() { + e := echo.New() + + // Middleware + e.Use(middleware.RequestLogger()) + e.Use(middleware.Recover()) + + // Login route + e.POST("/login", login) + + // Unauthenticated route + e.GET("/", accessible) + + // Restricted group + r := e.Group("/restricted") + + // Configure middleware with the custom claims type + config := echojwt.Config{ + NewClaimsFunc: func(c *echo.Context) jwt.Claims { + return new(jwtCustomClaims) + }, + SigningKey: []byte("secret"), + } + r.Use(echojwt.WithConfig(config)) + r.GET("", restricted) + + sc := echo.StartConfig{Address: ":1323"} + if err := sc.Start(context.Background(), e); err != nil { + e.Logger.Error("failed to start server", "error", err) + } +} +``` + +### Usar un KeyFunc definido por el usuario + +Cuando los tokens están firmados por un proveedor de identidad externo, proporciona un `KeyFunc` +que resuelva dinámicamente la signing key. Este ejemplo valida tokens emitidos por Google Sign-In +obteniendo el conjunto de claves públicas de Google. + +```go +package main + +import ( + "context" + "errors" + "fmt" + "net/http" + + echojwt "github.com/labstack/echo-jwt/v5" + + "github.com/golang-jwt/jwt/v5" + "github.com/labstack/echo/v5" + "github.com/labstack/echo/v5/middleware" + "github.com/lestrrat-go/jwx/v3/jwk" +) + +func getKey(token *jwt.Token) (any, error) { + + // For a demonstration purpose, Google Sign-in is used. + // https://developers.google.com/identity/sign-in/web/backend-auth + // + // This user-defined KeyFunc verifies tokens issued by Google Sign-In. + // + // Note: In this example, it downloads the keyset every time the restricted route is accessed. + keySet, err := jwk.Fetch(context.Background(), "https://www.googleapis.com/oauth2/v3/certs") + if err != nil { + return nil, err + } + + keyID, ok := token.Header["kid"].(string) + if !ok { + return nil, errors.New("expecting JWT header to have a key ID in the kid field") + } + + key, found := keySet.LookupKeyID(keyID) + + if !found { + return nil, fmt.Errorf("unable to find key %q", keyID) + } + + return key.PublicKey() +} + +func accessible(c *echo.Context) error { + return c.String(http.StatusOK, "Accessible") +} + +func restricted(c *echo.Context) error { + user, err := echo.ContextGet[*jwt.Token](c, "user") + if err != nil { + return echo.ErrUnauthorized.Wrap(err) + } + claims := user.Claims.(jwt.MapClaims) + name := claims["name"].(string) + return c.String(http.StatusOK, "Welcome "+name+"!") +} + +func main() { + e := echo.New() + + // Middleware + e.Use(middleware.RequestLogger()) + e.Use(middleware.Recover()) + + // Unauthenticated route + e.GET("/", accessible) + + // Restricted group + r := e.Group("/restricted") + { + config := echojwt.Config{ + KeyFunc: getKey, + } + r.Use(echojwt.WithConfig(config)) + r.GET("", restricted) + } + + sc := echo.StartConfig{Address: ":1323"} + if err := sc.Start(context.Background(), e); err != nil { + e.Logger.Error("failed to start server", "error", err) + } +} +``` + +:::caution +Obtener el conjunto de claves en cada request, como se muestra arriba, es solo para demostración. +En producción, cachea el conjunto de claves y actualízalo periódicamente. +::: + +## Cliente + +### Login + +Inicia sesión con un username y password para obtener un token. + +```sh +curl -X POST -d 'username=jon' -d 'password=shhh!' localhost:1323/login +``` + +Response: + +```js +{ + "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE0NjE5NTcxMzZ9.RB3arc4-OyzASAaUhC2W3ReWaXAt_z2Fd3BN4aWTgEY" +} +``` + +### Request + +Solicita un recurso restringido usando el token en el header de request `Authorization`. + +```sh +curl localhost:1323/restricted -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE0NjE5NTcxMzZ9.RB3arc4-OyzASAaUhC2W3ReWaXAt_z2Fd3BN4aWTgEY" +``` + +Response: + +```sh +Welcome Jon Snow! +``` diff --git a/site/src/content/docs/es/cookbook/load-balancing.md b/site/src/content/docs/es/cookbook/load-balancing.md new file mode 100644 index 00000000..281d0519 --- /dev/null +++ b/site/src/content/docs/es/cookbook/load-balancing.md @@ -0,0 +1,117 @@ +--- +title: Load Balancing +description: Usa Nginx como reverse proxy para balancear tráfico entre múltiples servidores Echo. +sidebar: + order: 20 +--- + +Esta receta demuestra cómo usar Nginx como servidor reverse proxy para balancear +tráfico entre múltiples servidores Echo. + +## Echo + +```go +package main + +import ( + "context" + "fmt" + "net/http" + "os" + + "github.com/labstack/echo/v5" + "github.com/labstack/echo/v5/middleware" +) + +var index = ` + + + + + + + Upstream Server + + + +

+ Hello from upstream server %s +

+ + +` + +func main() { + name := os.Args[1] + port := os.Args[2] + + e := echo.New() + e.Use(middleware.Recover()) + e.Use(middleware.RequestLogger()) + + e.GET("/", func(c *echo.Context) error { + return c.HTML(http.StatusOK, fmt.Sprintf(index, name)) + }) + + sc := echo.StartConfig{Address: port} + if err := sc.Start(context.Background(), e); err != nil { + e.Logger.Error("failed to start server", "error", err) + } +} +``` + +### Iniciar servidores + +```sh +cd upstream +go run server.go server1 :8081 +go run server.go server2 :8082 +``` + +## Nginx + +### 1) Instalar Nginx + +Consulta la [guía de instalación de Nginx](https://www.nginx.com/resources/wiki/start/topics/tutorials/install). + +### 2) Configurar Nginx + +Crea un archivo `/etc/nginx/sites-enabled/localhost` con el siguiente contenido: + +```nginx +upstream localhost { + server localhost:8081; + server localhost:8082; +} + +server { + listen 8080; + server_name localhost; + access_log /var/log/nginx/localhost.access.log combined; + + location / { + proxy_pass http://localhost; + } +} +``` + +:::note +Ajusta `listen`, `server_name` y `access_log` según tu entorno. +::: + +### 3) Reiniciar Nginx + +```sh +service nginx restart +``` + +Abre `https://localhost:8080`, y deberías ver una página web servida desde +"server 1" o "server 2". + +```sh +Hello from upstream server server1 +``` diff --git a/site/src/content/docs/es/cookbook/middleware.md b/site/src/content/docs/es/cookbook/middleware.md new file mode 100644 index 00000000..f76a50d7 --- /dev/null +++ b/site/src/content/docs/es/cookbook/middleware.md @@ -0,0 +1,140 @@ +--- +title: Middleware personalizado +description: Escribe middleware Echo personalizado para recopilar estadísticas de requests y establecer headers de response. +sidebar: + order: 12 +--- + +Esta receta muestra cómo escribir middleware personalizado: + +- Un middleware que recopila el conteo de requests, estados de response y uptime. +- Un middleware que escribe un header `Server` personalizado en cada response. + +Un middleware en Echo es una función con la firma +`func(next echo.HandlerFunc) echo.HandlerFunc`. El método `Stats.Process` de abajo +satisface esa firma directamente, mientras que `ServerHeader` es una función normal. + +## Servidor + +```go +package main + +import ( + "context" + "errors" + "net/http" + "sync" + "time" + + "github.com/labstack/echo/v5" +) + +type ( + Stats struct { + Uptime time.Time `json:"uptime"` + RequestCount uint64 `json:"requestCount"` + Statuses map[int]uint64 `json:"statuses"` + mutex sync.RWMutex + } +) + +func NewStats() *Stats { + return &Stats{ + Uptime: time.Now(), + Statuses: map[int]uint64{}, + } +} + +// Process is the middleware function. +func (s *Stats) Process(next echo.HandlerFunc) echo.HandlerFunc { + return func(c *echo.Context) error { + err := next(c) + + status := http.StatusInternalServerError + if err != nil { + var sc echo.HTTPStatusCoder + if ok := errors.As(err, &sc); ok { + status = sc.StatusCode() + } + } else { + rw, uErr := echo.UnwrapResponse(c.Response()) + if uErr == nil { + status = rw.Status + } + err = uErr + } + + s.mutex.Lock() + defer s.mutex.Unlock() + s.RequestCount++ + s.Statuses[status]++ + + return err + } +} + +// Handle is the endpoint to get stats. +func (s *Stats) Handle(c *echo.Context) error { + s.mutex.RLock() + defer s.mutex.RUnlock() + return c.JSON(http.StatusOK, s) +} + +// ServerHeader middleware adds a `Server` header to the response. +func ServerHeader(next echo.HandlerFunc) echo.HandlerFunc { + return func(c *echo.Context) error { + c.Response().Header().Set(echo.HeaderServer, "Echo/5.0") + return next(c) + } +} + +func main() { + e := echo.New() + + //------------------- + // Custom middleware + //------------------- + // Stats + s := NewStats() + e.Use(s.Process) + e.GET("/stats", s.Handle) // Endpoint to get stats + + // Server header + e.Use(ServerHeader) + + // Handler + e.GET("/", func(c *echo.Context) error { + return c.String(http.StatusOK, "Hello, World!") + }) + + // Start server + sc := echo.StartConfig{Address: ":1323"} + if err := sc.Start(context.Background(), e); err != nil { + e.Logger.Error("failed to start server", "error", err) + } +} +``` + +## Response + +### Headers + +```sh +Content-Length:122 +Content-Type:application/json; charset=utf-8 +Date:Thu, 14 Apr 2016 20:31:46 GMT +Server:Echo/5.0 +``` + +### Body + +```js +{ + "uptime": "2016-04-14T13:28:48.486548936-07:00", + "requestCount": 5, + "statuses": { + "200": 4, + "404": 1 + } +} +``` diff --git a/site/src/content/docs/es/cookbook/reverse-proxy.md b/site/src/content/docs/es/cookbook/reverse-proxy.md new file mode 100644 index 00000000..918c310e --- /dev/null +++ b/site/src/content/docs/es/cookbook/reverse-proxy.md @@ -0,0 +1,202 @@ +--- +title: Reverse Proxy +description: Usa Echo como reverse proxy y load balancer delante de aplicaciones upstream. +sidebar: + order: 19 +--- + +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 + +```go +url1, err := url.Parse("http://localhost:8081") +if err != nil { + e.Logger.Error("failed parse url", "error", err) +} +url2, err := url.Parse("http://localhost:8082") +if err != nil { + e.Logger.Error("failed parse url", "error", err) +} +targets := []*middleware.ProxyTarget{ + { + URL: url1, + }, + { + URL: url2, + }, +} +``` + +## 2) Configurar middleware proxy con destinos upstream + +El snippet de abajo usa load balancing round-robin. También puedes usar +`middleware.NewRandomBalancer()`. + +```go +e.Use(middleware.Proxy(middleware.NewRoundRobinBalancer(targets))) +``` + +Para configurar un proxy para una subruta, usa `Echo#Group()`. + +```go +g := e.Group("/blog") +g.Use(middleware.Proxy(...)) +``` + +## 3) Iniciar servidores upstream + +```sh +cd upstream +go run server.go server1 :8081 +go run server.go server2 :8082 +``` + +## 4) Iniciar el servidor proxy + +```sh +go run server.go +``` + +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". + +```sh +HTTP + +Hello from upstream server server1 + +WebSocket + +Hello from upstream server server2! +Hello from upstream server server2! +Hello from upstream server server2! +``` + +## Código fuente + +### Servidor upstream + +```go +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 = ` + + + + + + + Upstream Server + + + +

HTTP

+

+ Hello from upstream server %s +

+

WebSocket

+

+ + + +` + +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 + +```go +package main + +import ( + "context" + "net/url" + + "github.com/labstack/echo/v5" + "github.com/labstack/echo/v5/middleware" +) + +func main() { + e := echo.New() + e.Use(middleware.RequestLogger()) + e.Use(middleware.Recover()) + + // Setup proxy + url1, _ := url.Parse("http://localhost:8081") + url2, _ := url.Parse("http://localhost:8082") + targets := []*middleware.ProxyTarget{ + {URL: url1}, + {URL: url2}, + } + 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) + } +} +``` diff --git a/site/src/content/docs/es/cookbook/sse.md b/site/src/content/docs/es/cookbook/sse.md new file mode 100644 index 00000000..b262f6be --- /dev/null +++ b/site/src/content/docs/es/cookbook/sse.md @@ -0,0 +1,304 @@ +--- +title: Server-Sent Events (SSE) +description: Transmite server-sent events desde un handler Echo, por conexión o como broadcast a muchos clientes. +sidebar: + order: 14 +--- + +[Server-sent events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events#event_stream_format) +se pueden usar de varias formas. El primer ejemplo de abajo es SSE por conexión y por handler. +Para lógica de broadcast más compleja, consulta el segundo ejemplo usando +[r3labs/sse](https://github.com/r3labs/sse). + +:::caution +Las conexiones SSE son de larga duración, por lo que debe deshabilitarse el write timeout del servidor. +Ambos ejemplos establecen `s.WriteTimeout = 0` mediante `BeforeServeFunc`. +::: + +## Usar SSE + +### Servidor + +El handler escribe los headers SSE y luego emite un evento cada segundo hasta que el cliente +se desconecta. `http.NewResponseController(w).Flush()` empuja cada evento al cliente de inmediato. + +```go +package main + +import ( + "context" + "fmt" + "log" + "net/http" + "os" + "os/signal" + "syscall" + "time" + + "github.com/labstack/echo/v5" + "github.com/labstack/echo/v5/middleware" +) + +func main() { + e := echo.New() + + e.Use(middleware.RequestLogger()) + e.Use(middleware.Recover()) + e.File("/", "./index.html") + + e.GET("/sse", func(c *echo.Context) error { + log.Printf("SSE client connected, ip: %v", c.RealIP()) + + w := c.Response() + w.Header().Set("Content-Type", "text/event-stream") + w.Header().Set("Cache-Control", "no-cache") + w.Header().Set("Connection", "keep-alive") + + ticker := time.NewTicker(1 * time.Second) + defer ticker.Stop() + count := uint64(0) + for { + select { + case <-c.Request().Context().Done(): + log.Printf("SSE client disconnected, ip: %v", c.RealIP()) + return nil + case <-ticker.C: + count++ + event := Event{ + Data: []byte(fmt.Sprintf("count: %d, time: %s\n\n", count, time.Now().Format(time.RFC3339Nano))), + } + if err := event.MarshalTo(w); err != nil { + return err + } + if err := http.NewResponseController(w).Flush(); err != nil { + return err + } + } + } + }) + + sc := echo.StartConfig{ + Address: ":8080", + BeforeServeFunc: func(s *http.Server) error { + s.WriteTimeout = 0 // IMPORTANT: disable for SSE + return nil + }, + } + ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) // start shutdown process on ctrl+c + defer cancel() + + if err := sc.Start(ctx, e); err != nil { + e.Logger.Error("failed to start server", "error", err) + } +} +``` + +### Estructura Event y método Marshal + +```go +package main + +import ( + "bytes" + "fmt" + "io" +) + +// Event represents Server-Sent Event. +// SSE explanation: https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events#event_stream_format +type Event struct { + // ID is used to set the EventSource object's last event ID value. + ID []byte + // Data field is for the message. When the EventSource receives multiple consecutive lines + // that begin with data:, it concatenates them, inserting a newline character between each one. + // Trailing newlines are removed. + Data []byte + // Event is a string identifying the type of event described. If this is specified, an event + // will be dispatched on the browser to the listener for the specified event name; the website + // source code should use addEventListener() to listen for named events. The onmessage handler + // is called if no event name is specified for a message. + Event []byte + // Retry is the reconnection time. If the connection to the server is lost, the browser will + // wait for the specified time before attempting to reconnect. This must be an integer, specifying + // the reconnection time in milliseconds. If a non-integer value is specified, the field is ignored. + Retry []byte + // Comment line can be used to prevent connections from timing out; a server can send a comment + // periodically to keep the connection alive. + Comment []byte +} + +// MarshalTo marshals Event to given Writer +func (ev *Event) MarshalTo(w io.Writer) error { + // Marshalling part is taken from: https://github.com/r3labs/sse/blob/c6d5381ee3ca63828b321c16baa008fd6c0b4564/http.go#L16 + if len(ev.Data) == 0 && len(ev.Comment) == 0 { + return nil + } + + if len(ev.Data) > 0 { + if _, err := fmt.Fprintf(w, "id: %s\n", ev.ID); err != nil { + return err + } + + sd := bytes.Split(ev.Data, []byte("\n")) + for i := range sd { + if _, err := fmt.Fprintf(w, "data: %s\n", sd[i]); err != nil { + return err + } + } + + if len(ev.Event) > 0 { + if _, err := fmt.Fprintf(w, "event: %s\n", ev.Event); err != nil { + return err + } + } + + if len(ev.Retry) > 0 { + if _, err := fmt.Fprintf(w, "retry: %s\n", ev.Retry); err != nil { + return err + } + } + } + + if len(ev.Comment) > 0 { + if _, err := fmt.Fprintf(w, ": %s\n", ev.Comment); err != nil { + return err + } + } + + if _, err := fmt.Fprint(w, "\n"); err != nil { + return err + } + + return nil +} +``` + +### HTML que sirve SSE + +```html + + + + +

Getting server-sent updates

+
+ + + + + +``` + +## Broadcast con r3labs/sse + +Cuando necesitas hacer broadcast de un único stream de eventos a muchos suscriptores, la +biblioteca [r3labs/sse](https://github.com/r3labs/sse) maneja por ti la gestión de streams +y suscriptores. + +### Servidor + +```go +package main + +import ( + "context" + "net/http" + "os" + "os/signal" + "syscall" + "time" + + "github.com/labstack/echo/v5" + "github.com/labstack/echo/v5/middleware" + "github.com/r3labs/sse/v2" +) + +func main() { + e := echo.New() + + server := sse.New() // create SSE broadcaster server + server.AutoReplay = false // do not replay messages for each new subscriber that connects + _ = server.CreateStream("time") // EventSource in "index.html" connecting to stream named "time" + + go func(s *sse.Server) { + ticker := time.NewTicker(1 * time.Second) + defer ticker.Stop() + + for { + select { + case <-ticker.C: + s.Publish("time", &sse.Event{ + Data: []byte("time: " + time.Now().Format(time.RFC3339Nano)), + }) + } + } + }(server) + + e.Use(middleware.RequestLogger()) + e.Use(middleware.Recover()) + e.File("/", "./index.html") + + //e.GET("/sse", echo.WrapHandler(server)) + + e.GET("/sse", func(c *echo.Context) error { // longer variant with disconnect logic + e.Logger.Info("New client connected", "ip", c.RealIP()) + go func() { + <-c.Request().Context().Done() // Received Browser Disconnection + e.Logger.Info("Client disconnected", "ip", c.RealIP()) + }() + + server.ServeHTTP(c.Response(), c.Request()) + return nil + }) + + sc := echo.StartConfig{ + Address: ":8080", + BeforeServeFunc: func(s *http.Server) error { + s.WriteTimeout = 0 // IMPORTANT: disable for SSE + return nil + }, + } + ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) // start shutdown process on ctrl+c + defer cancel() + + if err := sc.Start(ctx, e); err != nil { + e.Logger.Error("failed to start server", "error", err) + } +} +``` + +### HTML que sirve SSE + +```html + + + + +

Getting server-sent updates

+
+ + + + + +``` diff --git a/site/src/content/docs/es/cookbook/streaming-response.md b/site/src/content/docs/es/cookbook/streaming-response.md new file mode 100644 index 00000000..b8455836 --- /dev/null +++ b/site/src/content/docs/es/cookbook/streaming-response.md @@ -0,0 +1,95 @@ +--- +title: Response en streaming +description: Transmite datos al cliente a medida que se producen usando chunked transfer encoding. +sidebar: + order: 15 +--- + +Esta receta transmite una response JSON al cliente a medida que se produce cada registro, +usando chunked transfer encoding: + +- Enviar datos a medida que se producen. +- Transmitir una response JSON con chunked transfer encoding. + +El handler codifica un registro a la vez y llama a `http.NewResponseController(...).Flush()` +después de cada uno para empujarlo al cliente de inmediato, con una pausa de un segundo +entre registros. + +## Servidor + +```go +package main + +import ( + "context" + "encoding/json" + "net/http" + "time" + + "github.com/labstack/echo/v5" +) + +type ( + Geolocation struct { + Altitude float64 + Latitude float64 + Longitude float64 + } +) + +var ( + locations = []Geolocation{ + {-97, 37.819929, -122.478255}, + {1899, 39.096849, -120.032351}, + {2619, 37.865101, -119.538329}, + {42, 33.812092, -117.918974}, + {15, 37.77493, -122.419416}, + } +) + +func main() { + e := echo.New() + e.GET("/", func(c *echo.Context) error { + c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSON) + c.Response().WriteHeader(http.StatusOK) + + enc := json.NewEncoder(c.Response()) + for _, l := range locations { + if err := enc.Encode(l); err != nil { + return err + } + if err := http.NewResponseController(c.Response()).Flush(); err != nil { + return err + } + select { + case <-c.Request().Context().Done(): + return nil + case <-time.After(1 * time.Second): + continue + } + } + return nil + }) + + sc := echo.StartConfig{Address: ":1323"} + if err := sc.Start(context.Background(), e); err != nil { + e.Logger.Error("failed to start server", "error", err) + } +} +``` + +## Cliente + +```sh +curl localhost:1323 +``` + +### Salida + +```js +{"Altitude":-97,"Latitude":37.819929,"Longitude":-122.478255} +{"Altitude":1899,"Latitude":39.096849,"Longitude":-120.032351} +{"Altitude":2619,"Latitude":37.865101,"Longitude":-119.538329} +{"Altitude":42,"Latitude":33.812092,"Longitude":-117.918974} +{"Altitude":15,"Latitude":37.77493,"Longitude":-122.419416} +``` diff --git a/site/src/content/docs/es/cookbook/subdomain.md b/site/src/content/docs/es/cookbook/subdomain.md new file mode 100644 index 00000000..82f80d11 --- /dev/null +++ b/site/src/content/docs/es/cookbook/subdomain.md @@ -0,0 +1,78 @@ +--- +title: Subdominio +description: Enruta requests a distintas instancias Echo por host usando un handler de virtual host. +sidebar: + order: 17 +--- + +Esta receta enruta requests a instancias `Echo` separadas según el host del request, +de modo que cada subdominio tenga sus propias rutas y middleware. Las instancias se +combinan con `echo.NewVirtualHostHandler`, que despacha por host name. + +## Servidor + +```go +package main + +import ( + "context" + "net/http" + + "github.com/labstack/echo/v5" + "github.com/labstack/echo/v5/middleware" +) + +func main() { + // Hosts + vHosts := make(map[string]*echo.Echo) + + //----- + // API + //----- + + api := echo.New() + api.Use(middleware.RequestLogger()) + api.Use(middleware.Recover()) + + vHosts["api.localhost:1323"] = api + + api.GET("/", func(c *echo.Context) error { + return c.String(http.StatusOK, "API") + }) + + //------ + // Blog + //------ + + blog := echo.New() + blog.Use(middleware.RequestLogger()) + blog.Use(middleware.Recover()) + + vHosts["blog.localhost:1323"] = blog + + blog.GET("/", func(c *echo.Context) error { + return c.String(http.StatusOK, "Blog") + }) + + //--------- + // Website + //--------- + + site := echo.New() + site.Use(middleware.RequestLogger()) + site.Use(middleware.Recover()) + + vHosts["localhost:1323"] = site + + site.GET("/", func(c *echo.Context) error { + return c.String(http.StatusOK, "Website") + }) + + e := echo.NewVirtualHostHandler(vHosts) + + sc := echo.StartConfig{Address: ":1323"} + if err := sc.Start(context.Background(), e); err != nil { + e.Logger.Error("failed to start server", "error", err) + } +} +``` diff --git a/site/src/content/docs/es/cookbook/timeout.md b/site/src/content/docs/es/cookbook/timeout.md new file mode 100644 index 00000000..082c567b --- /dev/null +++ b/site/src/content/docs/es/cookbook/timeout.md @@ -0,0 +1,53 @@ +--- +title: Timeout +description: Aplica un timeout de request a handlers con el middleware ContextTimeout. +sidebar: + order: 18 +--- + +El middleware [`ContextTimeout`](/es/middleware/context-timeout/) establece un deadline en el +`context.Context` del request. Cuando el deadline vence, el contexto se cancela, y los handlers +que observan `c.Request().Context().Done()` pueden retornar rápidamente en vez de ejecutarse +hasta completar. + +En el ejemplo de abajo, el middleware impone un timeout de 5 segundos, mientras que el handler +tomaría 10 segundos de otro modo, por lo que el request devuelve `408 Request Timeout`. + +## Servidor + +```go +package main + +import ( + "context" + "net/http" + "time" + + "github.com/labstack/echo/v5" + "github.com/labstack/echo/v5/middleware" +) + +func main() { + // Echo instance + e := echo.New() + + // Middleware + e.Use(middleware.ContextTimeout(5 * time.Second)) + + // Route => handler + e.GET("/", func(c *echo.Context) error { + select { + case <-c.Request().Context().Done(): + return echo.NewHTTPError(http.StatusRequestTimeout, "Request timed out") + case <-time.After(10 * time.Second): + return c.String(http.StatusOK, "Hello, World!\n") + } + }) + + // Start server + sc := echo.StartConfig{Address: ":1323"} + if err := sc.Start(context.Background(), e); err != nil { + e.Logger.Error("failed to start server", "error", err) + } +} +``` diff --git a/site/src/content/docs/es/cookbook/websocket.md b/site/src/content/docs/es/cookbook/websocket.md new file mode 100644 index 00000000..54ac1946 --- /dev/null +++ b/site/src/content/docs/es/cookbook/websocket.md @@ -0,0 +1,189 @@ +--- +title: WebSocket +description: Maneja conexiones WebSocket en Echo usando golang.org/x/net/websocket o gorilla/websocket. +sidebar: + order: 16 +--- + +Los handlers de Echo pueden servir conexiones WebSocket actualizando la conexión HTTP +subyacente. Esta receta muestra dos enfoques: el paquete estándar +`golang.org/x/net/websocket` y la biblioteca popular +[`gorilla/websocket`](https://github.com/gorilla/websocket). + +## Usar net WebSocket + +### Servidor + +```go +package main + +import ( + "context" + "fmt" + + "github.com/labstack/echo/v5" + "github.com/labstack/echo/v5/middleware" + "golang.org/x/net/websocket" +) + +func hello(c *echo.Context) error { + websocket.Handler(func(ws *websocket.Conn) { + defer ws.Close() + for { + // Write + if err := websocket.Message.Send(ws, "Hello, Client!"); err != nil { + c.Logger().Error("failed to write WS message", "error", err) + } + + // Read + msg := "" + if err := websocket.Message.Receive(ws, &msg); err != nil { + c.Logger().Error("failed to write WS message", "error", err) + } + fmt.Printf("%s\n", msg) + } + }).ServeHTTP(c.Response(), c.Request()) + return nil +} + +func main() { + e := echo.New() + + e.Use(middleware.RequestLogger()) + e.Use(middleware.Recover()) + + e.Static("/", "../public") + e.GET("/ws", hello) + + sc := echo.StartConfig{Address: ":1323"} + if err := sc.Start(context.Background(), e); err != nil { + e.Logger.Error("failed to start server", "error", err) + } +} +``` + +## Usar gorilla WebSocket + +### Servidor + +```go +package main + +import ( + "context" + "fmt" + + "github.com/gorilla/websocket" + "github.com/labstack/echo/v5" + "github.com/labstack/echo/v5/middleware" +) + +var ( + upgrader = websocket.Upgrader{} +) + +func hello(c *echo.Context) error { + ws, err := upgrader.Upgrade(c.Response(), c.Request(), nil) + if err != nil { + return err + } + defer ws.Close() + + for { + // Write + err := ws.WriteMessage(websocket.TextMessage, []byte("Hello, Client!")) + if err != nil { + c.Logger().Error("failed to write WS message", "error", err) + } + + // Read + _, msg, err := ws.ReadMessage() + if err != nil { + c.Logger().Error("failed to read WS message", "error", err) + } + fmt.Printf("%s\n", msg) + } +} + +func main() { + e := echo.New() + + e.Use(middleware.RequestLogger()) + e.Use(middleware.Recover()) + + e.Static("/", "../public") + + e.GET("/ws", hello) + + sc := echo.StartConfig{Address: ":1323"} + if err := sc.Start(context.Background(), e); err != nil { + e.Logger.Error("failed to start server", "error", err) + } +} +``` + +## Cliente + +```html + + + + + + WebSocket + + + +

+ + + + + +``` + +## Salida + +**Servidor** + +```sh +Hello, Server! +Hello, Server! +Hello, Server! +Hello, Server! +Hello, Server! +``` + +**Cliente** + +```sh +Hello, Client! +Hello, Client! +Hello, Client! +Hello, Client! +Hello, Client! +``` diff --git a/site/src/content/docs/es/guide/binding.md b/site/src/content/docs/es/guide/binding.md new file mode 100644 index 00000000..49309a63 --- /dev/null +++ b/site/src/content/docs/es/guide/binding.md @@ -0,0 +1,150 @@ +--- +title: Binding +description: Analiza datos de request en structs Go tipados desde path, query, header y body. +sidebar: + order: 5 +--- + +Analizar datos de request es una parte crucial de una aplicación web. En Echo esto se llama +_binding_, y puede leer desde cuatro partes de un request HTTP: + +- Parámetros de path de URL +- Parámetros de query de URL +- Headers +- Body del request + +## Binding con tags de struct + +Define un struct con tags que especifican el origen de datos y la clave, y luego llama a `c.Bind()` +con un puntero a él. Aquí el parámetro de query `id` se vincula al campo `ID`: + +```go +type User struct { + ID string `query:"id"` +} + +// handler for /users?id= +var user User +if err := c.Bind(&user); err != nil { + return c.String(http.StatusBadRequest, "bad request") +} +``` + +### Orígenes de datos + +| Tag | Origen | +| -------- | ------ | +| `query` | Parámetro de query | +| `param` | Parámetro de path | +| `header` | Valor de header | +| `form` | Datos de formulario (query + body) | +| `json` | Body del request (`encoding/json`) | +| `xml` | Body del request (`encoding/xml`) | + +Los campos de path, query, header y form requieren un **tag explícito**. JSON y XML usan +el nombre del campo del struct cuando se omite el tag, igual que la biblioteca estándar. + +### Content types del body + +Al decodificar el body del request, el header `Content-Type` selecciona el decoder: + +- `application/json` +- `application/xml` +- `application/x-www-form-urlencoded` + +### Múltiples orígenes y precedencia + +Un campo puede declarar varios orígenes. Los datos se vinculan en este orden, y cada paso +sobrescribe el anterior: + +1. Parámetros de path +2. Parámetros de query (solo GET / DELETE) +3. Body del request + +```go +type User struct { + ID string `param:"id" query:"id" form:"id" json:"id" xml:"id"` +} +``` + +### Binding directo desde un origen + +```go +echo.BindBody(c, &payload) // request body +echo.BindQueryParams(c, &payload) // query parameters +echo.BindPathValues(c, &payload) // path parameters +echo.BindHeaders(c, &payload) // headers +``` + +:::note +Los headers **no** están incluidos por `c.Bind()`. Vincúlalos directamente con `echo.BindHeaders`. +::: + +:::caution[Seguridad] +No vincules directamente a structs de negocio. Si un struct vinculado expone un campo `IsAdmin bool`, +un body de request `{"IsAdmin": true}` lo establecería. Usa un DTO dedicado y mapéalo explícitamente: +::: + +```go +type UserDTO struct { + Name string `json:"name" form:"name" query:"name"` + Email string `json:"email" form:"email" query:"email"` +} + +e.POST("/users", func(c *echo.Context) error { + var dto UserDTO + if err := c.Bind(&dto); err != nil { + return c.String(http.StatusBadRequest, "bad request") + } + user := User{Name: dto.Name, Email: dto.Email, IsAdmin: false} + executeSomeBusinessLogic(user) + return c.JSON(http.StatusOK, user) +}) +``` + +## Binding fluido + +Para binding explícito y type-safe desde un único origen, usa los binders fluidos. Encadenan +la configuración y la ejecutan, recopilando errores: + +```go +// /api/search?active=true&id=1&id=2&id=3&length=25 +var opts struct { + IDs []int64 + Active bool +} +length := int64(50) + +err := echo.QueryParamsBinder(c). + Int64("length", &length). + Int64s("id", &opts.IDs). + Bool("active", &opts.Active). + BindError() // first error, if any +``` + +Binders disponibles: `echo.QueryParamsBinder(c)`, `echo.PathValuesBinder(c)`, +`echo.FormFieldBinder(c)`. Termina una cadena con `BindError()` (primer error) o +`BindErrors()` (todos los errores). `FailFast(false)` ejecuta toda la cadena; está activado por defecto. + +Cada tipo soportado ofrece métodos `Type(...)`, `MustType(...)`, `Types(...)` (slices) y +`MustTypes(...)`, por ejemplo `Int64`, `MustInt64`, `Int64s`. Usa +`BindWithDelimiter("id", &dest, ",")` para separar valores unidos por comas. + +## Binder personalizado + +Registra un binder personalizado mediante `Echo#Binder`: + +```go +type CustomBinder struct{} + +func (cb *CustomBinder) Bind(c *echo.Context, i any) error { + db := new(echo.DefaultBinder) + if err := db.Bind(c, i); err != echo.ErrUnsupportedMediaType { + return err + } + // custom logic here + return nil +} + +e.Binder = &CustomBinder{} +``` diff --git a/site/src/content/docs/es/guide/context.md b/site/src/content/docs/es/guide/context.md new file mode 100644 index 00000000..731fe64f --- /dev/null +++ b/site/src/content/docs/es/guide/context.md @@ -0,0 +1,78 @@ +--- +title: Context +description: El objeto por request que transporta request, response, params y helpers. +sidebar: + order: 4 +--- + +`echo.Context` representa el contexto del request HTTP actual. Se pasa un puntero a él +(`*echo.Context`) a cada handler y middleware, y transporta el request y response, +parámetros de path, datos vinculados y helpers para construir responses. + +```go +func handler(c *echo.Context) error { + // ... + return nil +} +``` + +## Leer entrada + +```go +id := c.Param("id") // path parameter +q := c.QueryParam("q") // query string value +all := c.QueryParams() // url.Values of all query params +name := c.FormValue("name") // form field (URL + body) +ua := c.Request().Header.Get(echo.HeaderUserAgent) +``` + +Hay helpers `*Or` equivalentes que devuelven un valor por defecto cuando el valor no está presente: +`c.ParamOr("id", "0")`, `c.QueryParamOr("page", "1")`, `c.FormValueOr(...)`. + +## Escribir responses + +```go +c.String(http.StatusOK, "plain text") +c.JSON(http.StatusOK, payload) +c.JSONPretty(http.StatusOK, payload, " ") +c.HTML(http.StatusOK, "hi") +c.XML(http.StatusOK, payload) +c.Blob(http.StatusOK, "application/pdf", bytes) +c.Stream(http.StatusOK, "application/octet-stream", reader) +c.NoContent(http.StatusNoContent) +c.Redirect(http.StatusFound, "/elsewhere") +``` + +## Archivos + +```go +c.File("public/report.pdf") // serve a file +c.Attachment("invoice.pdf", "inv.pdf") // prompt download +c.Inline("photo.png", "photo.png") // render inline +``` + +## Almacenamiento por request + +Comparte datos entre middleware y handlers con `Get`/`Set`: + +```go +c.Set("user", u) +u, _ := c.Get("user").(*User) +``` + +El acceso tipado está disponible mediante los helpers genéricos: + +```go +u, err := echo.ContextGet[*User](c, "user") +``` + +## Binding y validación + +`c.Bind()` analiza datos del request en un struct; consulta [Binding](/es/guide/binding/). + +```go +var dto CreateUser +if err := c.Bind(&dto); err != nil { + return echo.ErrBadRequest +} +``` diff --git a/site/src/content/docs/es/guide/cookies.md b/site/src/content/docs/es/guide/cookies.md new file mode 100644 index 00000000..857ad549 --- /dev/null +++ b/site/src/content/docs/es/guide/cookies.md @@ -0,0 +1,72 @@ +--- +title: Cookies +description: Crea, lee y lista HTTP cookies usando el tipo estándar http.Cookie. +sidebar: + order: 11 +--- + +Una cookie es una pequeña pieza de datos que un servidor envía al navegador, que el navegador +almacena y vuelve a enviar en requests posteriores. Las cookies permiten que los sitios web +recuerden información con estado, como un carrito de compras, el estado de autenticación o +valores de formularios ingresados previamente. + +Echo usa el tipo estándar `http.Cookie` de Go para agregar y obtener cookies desde +`echo.Context` en un handler. + +## Atributos de Cookie + +| Atributo | Opcional | +| ---------- | -------- | +| `Name` | No | +| `Value` | No | +| `Path` | Sí | +| `Domain` | Sí | +| `Expires` | Sí | +| `Secure` | Sí | +| `HttpOnly` | Sí | + +## Crear una cookie + +```go +func writeCookie(c *echo.Context) error { + cookie := new(http.Cookie) + cookie.Name = "username" + cookie.Value = "jon" + cookie.Expires = time.Now().Add(24 * time.Hour) + c.SetCookie(cookie) + return c.String(http.StatusOK, "write a cookie") +} +``` + +- Crea la cookie con `new(http.Cookie)`. +- Establece atributos en los campos de `http.Cookie`. +- Llama a `c.SetCookie(cookie)` para agregar un header `Set-Cookie` a la response. + +## Leer una cookie + +```go +func readCookie(c *echo.Context) error { + cookie, err := c.Cookie("username") + if err != nil { + return err + } + fmt.Println(cookie.Name) + fmt.Println(cookie.Value) + return c.String(http.StatusOK, "read a cookie") +} +``` + +- Lee una cookie por nombre con `c.Cookie("username")`. +- Accede a sus atributos mediante los campos de `http.Cookie`. + +## Leer todas las cookies + +```go +func readAllCookies(c *echo.Context) error { + for _, cookie := range c.Cookies() { + fmt.Println(cookie.Name) + fmt.Println(cookie.Value) + } + return c.String(http.StatusOK, "read all the cookies") +} +``` diff --git a/site/src/content/docs/es/guide/customization.md b/site/src/content/docs/es/guide/customization.md new file mode 100644 index 00000000..e9759c52 --- /dev/null +++ b/site/src/content/docs/es/guide/customization.md @@ -0,0 +1,62 @@ +--- +title: Personalización +description: Personaliza el logger, validator, binder, renderer, serializer y manejo de errores de Echo. +sidebar: + order: 12 +--- + +Echo expone un conjunto de campos en la instancia `Echo` que te permiten reemplazar el +comportamiento integrado con tus propias implementaciones. + +## Logging + +`Echo#Logger` escribe logs estructurados. El handler por defecto emite JSON a `os.Stdout`. + +### Logger personalizado + +El logger es un `*slog.Logger`, por lo que puedes registrar cualquier handler de `slog`: + +```go +e.Logger = slog.New(slog.NewJSONHandler(os.Stdout, nil)) +``` + +## Validator + +`Echo#Validator` registra un validator para validar payloads de request. + +[Aprende más](/es/guide/request/#validate-data) + +## Binder personalizado + +`Echo#Binder` registra un binder personalizado para binding de payloads de request. + +[Aprende más](/es/guide/binding/#custom-binder) + +## Serializer JSON personalizado + +`Echo#JSONSerializer` registra un serializer JSON personalizado. Consulta `DefaultJSONSerializer` +en [json.go](https://github.com/labstack/echo/blob/master/json.go). + +## Renderer + +`Echo#Renderer` registra un renderer para renderizado de templates. + +[Aprende más](/es/guide/templates/) + +## Handler de errores HTTP + +`Echo#HTTPErrorHandler` registra un handler de errores HTTP personalizado. + +[Aprende más](/es/guide/error-handling/) + +## Callback de ruta + +`Echo#OnAddRoute` registra un callback que se invoca cada vez que se agrega una nueva ruta al +router. + +## Extractor de IP + +`Echo#IPExtractor` controla cómo se determina la dirección IP real del cliente. Para +obtenerla de forma fiable y segura, tu aplicación debe conocer toda su infraestructura. + +[Aprende más](/es/guide/ip-address/) diff --git a/site/src/content/docs/es/guide/error-handling.md b/site/src/content/docs/es/guide/error-handling.md new file mode 100644 index 00000000..170b2622 --- /dev/null +++ b/site/src/content/docs/es/guide/error-handling.md @@ -0,0 +1,82 @@ +--- +title: Manejo de errores +description: Manejo centralizado de errores HTTP devolviendo errores desde handlers y middleware. +sidebar: + order: 6 +--- + +Echo promueve el manejo **centralizado** de errores: los handlers y middleware devuelven un +`error`, y un único handler de errores lo convierte en una response HTTP. Esto mantiene el logging +y el formato de responses en un solo lugar. + +Devuelve un `error` plano o un `*echo.HTTPError`: + +```go +e.Use(func(next echo.HandlerFunc) echo.HandlerFunc { + return func(c *echo.Context) error { + if !authenticated(c) { + // invalid credentials → abort with 401 + return echo.NewHTTPError(http.StatusUnauthorized, "Please provide valid credentials") + } + return next(c) + } +}) +``` + +`echo.NewHTTPError(code)` sin mensaje usa el texto del estado (por ejemplo, `"Unauthorized"`). +Echo también incluye errores sentinel como `echo.ErrBadRequest`, `echo.ErrNotFound` y +`echo.ErrUnauthorized`. + +## Handler de errores por defecto + +El handler por defecto de Echo responde en JSON: + +```json +{ "message": "error connecting to redis" } +``` + +Un `error` plano se convierte en `500 Internal Server Error` (el mensaje original se incluye +cuando se ejecuta con errores expuestos). Un `*HTTPError` usa su código de estado y mensaje. + +## Handler de errores personalizado + +Define el tuyo mediante `e.HTTPErrorHandler`; es útil para páginas de error, notificaciones o +enviar errores a un sistema centralizado. + +Comprueba si la response ya se envió con `echo.UnwrapResponse()`, y encuentra un código de +estado en la cadena de errores mediante `echo.HTTPStatusCoder`: + +```go +func customHTTPErrorHandler(c *echo.Context, err error) { + if resp, uErr := echo.UnwrapResponse(c.Response()); uErr == nil { + if resp.Committed { + return // already sent by a handler/middleware + } + } + + code := http.StatusInternalServerError + var sc echo.HTTPStatusCoder + if errors.As(err, &sc) { + if tmp := sc.StatusCode(); tmp != 0 { + code = tmp + } + } + + var cErr error + if c.Request().Method == http.MethodHead { + cErr = c.NoContent(code) + } else { + cErr = c.File(fmt.Sprintf("%d.html", code)) // e.g. 404.html, 500.html + } + if cErr != nil { + c.Logger().Error("failed to send error page", "error", errors.Join(err, cErr)) + } +} + +e.HTTPErrorHandler = customHTTPErrorHandler +``` + +:::tip +En lugar del logger, o además de él, reenvía errores a un servicio externo como +Sentry, Elasticsearch o Splunk desde el handler central. +::: diff --git a/site/src/content/docs/es/guide/installation.md b/site/src/content/docs/es/guide/installation.md new file mode 100644 index 00000000..9d3483fd --- /dev/null +++ b/site/src/content/docs/es/guide/installation.md @@ -0,0 +1,57 @@ +--- +title: Instalación +description: Agrega Echo v5 a tu módulo Go. +sidebar: + order: 2 +--- + +Echo se distribuye como un módulo Go: `github.com/labstack/echo/v5`. + +## Requisitos + +Echo v5 requiere **Go 1.25 o posterior**. + +```bash +go version +``` + +## Agregar a un proyecto + +Dentro de un módulo existente: + +```bash +go get github.com/labstack/echo/v5 +``` + +O inicia un módulo nuevo: + +```bash +mkdir myapp && cd myapp +go mod init myapp +go get github.com/labstack/echo/v5 +``` + +Impórtalo en tu código: + +```go +import "github.com/labstack/echo/v5" +``` + +## Versiones + +| Versión | Import path | Estado | +| ------- | ------------------------------- | ------ | +| **v5** | `github.com/labstack/echo/v5` | Actual | +| v4 | `github.com/labstack/echo/v4` | LTS (mantenimiento) | + +:::note +Echo sigue [semantic import versioning](https://go.dev/blog/v2-go-modules): la +versión major forma parte del import path, por lo que v4 y v5 pueden coexistir durante una migración. +::: + +## Mantenerse actualizado + +```bash +go get github.com/labstack/echo/v5 +go mod tidy +``` diff --git a/site/src/content/docs/es/guide/ip-address.md b/site/src/content/docs/es/guide/ip-address.md new file mode 100644 index 00000000..8b160976 --- /dev/null +++ b/site/src/content/docs/es/guide/ip-address.md @@ -0,0 +1,116 @@ +--- +title: Dirección IP +description: Obtén de forma segura la dirección IP real del cliente detrás de proxies. +sidebar: + order: 14 +--- + +La dirección IP cumple un papel fundamental en HTTP: se usa para control de acceso, +auditoría, análisis geográfico y más. Echo expone `Context#RealIP()` para obtenerla. + +Obtener la IP _real_ del cliente no es trivial, especialmente cuando hay proxies L7 delante +de tu aplicación. En ese caso, la IP real debe transmitirse por HTTP desde los proxies hacia +tu app, pero no debes confiar incondicionalmente en headers HTTP, o corres el riesgo de ser +engañado. **Esto es un riesgo de seguridad.** + +Para obtener la IP de forma fiable y segura, tu aplicación debe conocer toda su infraestructura. +En Echo, esto se configura mediante `Echo#IPExtractor`. + +:::caution +Si no estableces `Echo#IPExtractor` explícitamente, Echo vuelve al comportamiento legacy, +que no es un valor por defecto seguro. +::: + +Empieza con dos preguntas para encontrar el enfoque correcto: + +1. ¿Pones algún proxy HTTP (L7) delante de la aplicación? Esto incluye load balancers cloud + (como AWS ALB o GCP HTTP LB) y proxies open-source (como Nginx, Envoy o un Istio ingress gateway). +2. Si es así, ¿qué header HTTP usan tus proxies para pasar la IP del cliente a la + aplicación? + +## Caso 1: Sin proxy + +Si no hay proxy (la app mira directamente a internet), la única dirección en la que puedes +confiar es la de la capa de red. Todos los headers HTTP son no confiables porque los clientes +tienen control total sobre ellos. + +Usa `echo.ExtractIPDirect()`: + +```go +e.IPExtractor = echo.ExtractIPDirect() +``` + +## Caso 2: Proxies que usan el header X-Forwarded-For + +[`X-Forwarded-For` (XFF)](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-For) +es el header más común para retransmitir IPs de cliente. En cada salto, el proxy agrega la +IP del request al final del header. + +```text + ┌──────────┐ ┌──────────┐ ┌──────────┐ +───────────>│ Proxy 1 │───────────>│ Proxy 2 │───────────>│ Your app │ + │ (IP: b) │ │ (IP: c) │ │ │ + └──────────┘ └──────────┘ └──────────┘ + +Case 1. +XFF: "" "a" "a, b" + ~~~~~~ +Case 2. +XFF: "x" "x, a" "x, a, b" + ~~~~~~~~~ + ↑ What your app will see +``` + +En este caso, toma la **primera lectura de IP no confiable desde la derecha**. Nunca tomes +la primera desde la izquierda, ya que el cliente la controla. Aquí "confiable" significa que +estás seguro de que la IP pertenece a tu infraestructura. En el ejemplo anterior, si `b` y +`c` son confiables, la IP del cliente es `a` en ambos casos, nunca `x`. + +Usa `ExtractIPFromXFFHeader(...TrustOption)`: + +```go +e.IPExtractor = echo.ExtractIPFromXFFHeader() +``` + +Por defecto confía en direcciones IP internas: loopback, link-local unicast, +private-use y direcciones locales únicas de +[RFC 6890](https://datatracker.ietf.org/doc/html/rfc6890), +[RFC 4291](https://datatracker.ietf.org/doc/html/rfc4291) y +[RFC 4193](https://datatracker.ietf.org/doc/html/rfc4193). Contrólalo con `TrustOption`s: + +```go +e.IPExtractor = echo.ExtractIPFromXFFHeader( + echo.TrustLoopback(false), // e.g. IPv4 starting with 127. + echo.TrustLinkLocal(false), // e.g. IPv4 starting with 169.254. + echo.TrustPrivateNet(false), // e.g. IPv4 starting with 10. or 192.168. + echo.TrustIPRange(lbIPRange), +) +``` + +## Caso 3: Proxies que usan el header X-Real-IP + +`X-Real-IP` es otro header para retransmitir la IP del cliente, pero a diferencia de XFF +contiene solo una dirección. + +Si tus proxies establecen este header, usa `ExtractIPFromRealIPHeader(...TrustOption)`: + +```go +e.IPExtractor = echo.ExtractIPFromRealIPHeader() +``` + +Al igual que con XFF, confía en direcciones IP internas por defecto y acepta los mismos +`TrustOption`s. + +:::danger +**Nunca olvides** configurar el proxy más externo (en el edge de tu infraestructura) para que +**no deje pasar headers entrantes**. De lo contrario, un cliente puede falsificarlos y abrir +la puerta al fraude. +::: + +## Comportamiento por defecto + +Por defecto, Echo considera al mismo tiempo el primer header XFF, el header X-Real-IP y la IP +de la capa de red. + +Como este artículo debería dejar claro, esa no es una buena elección. Sigue siendo el valor +por defecto solo por compatibilidad hacia atrás. diff --git a/site/src/content/docs/es/guide/quickstart.md b/site/src/content/docs/es/guide/quickstart.md new file mode 100644 index 00000000..a5c39793 --- /dev/null +++ b/site/src/content/docs/es/guide/quickstart.md @@ -0,0 +1,76 @@ +--- +title: Inicio rápido +description: Crea una API Echo lista para producción en menos de cinco minutos. +sidebar: + order: 1 +--- + +Echo es un framework web Go minimalista y de alto rendimiento. Esta guía pone un servidor +en marcha en menos de cinco minutos. + +## Requisitos + +Echo requiere **Go 1.25 o posterior**. Comprueba tu versión: + +```bash +go version +``` + +## Instalar + +Crea un módulo y agrega Echo: + +```bash +go mod init myapp +go get github.com/labstack/echo/v5 +``` + +## Hello, World + +Crea `main.go`: + +```go +package main + +import ( + "net/http" + + "github.com/labstack/echo/v5" + "github.com/labstack/echo/v5/middleware" +) + +func main() { + e := echo.New() + + e.Use(middleware.RequestLogger()) + e.Use(middleware.Recover()) + + e.GET("/", func(c *echo.Context) error { + return c.JSON(http.StatusOK, map[string]string{"message": "Hello, World!"}) + }) + + if err := e.Start(":1323"); err != nil { + e.Logger.Error("failed to start server", "error", err) + } +} +``` + +Ejecútalo: + +```bash +go run main.go +``` + +Tu servidor está disponible en `http://localhost:1323`. El router de Echo despacha requests +con **cero asignaciones dinámicas de memoria** por ruta. + +:::tip[Ask Echo] +¿Atascado? Presiona el botón **Ask Echo** (abajo a la derecha) y pregunta +*"How do I add JWT auth?"*: las respuestas salen directamente de esta documentación. +::: + +## Próximos pasos + +- [Routing](/es/guide/routing/): rutas estáticas, parametrizadas y wildcard. +- [Context](/es/guide/context/): el objeto request/response por request. +- [Binding](/es/guide/binding/): analiza datos de request en structs tipados. diff --git a/site/src/content/docs/es/guide/request.md b/site/src/content/docs/es/guide/request.md new file mode 100644 index 00000000..2e17ed2d --- /dev/null +++ b/site/src/content/docs/es/guide/request.md @@ -0,0 +1,170 @@ +--- +title: Request +description: Obtén datos de form, query y path desde un request, y valídalos. +sidebar: + order: 7 +--- + +Un handler lee datos del request a través de `echo.Context`. Echo puede obtener valores +individualmente por nombre, vincularlos a structs (consulta [Binding](/es/guide/binding/)) y +delegar la validación a un validator que registres. + +## Obtener datos + +### Datos de formulario + +Obtén un campo de formulario por nombre con `Context#FormValue(name string)`: + +```go +e.POST("/form", func(c *echo.Context) error { + name := c.FormValue("name") + return c.String(http.StatusOK, name) +}) +``` + +Para tipos distintos de `string`, usa la función genérica `echo.FormValue[T]`: + +```go +age, err := echo.FormValue[int](c, "age") +if err != nil { + return err +} +``` + +Prueba con: + +```sh +curl -X POST http://localhost:1323/form -d 'name=Joe&age=30' +``` + +Para vincular un tipo de datos personalizado, implementa la interfaz `echo.BindUnmarshaler`: + +```go +type Timestamp time.Time + +func (t *Timestamp) UnmarshalParam(src string) error { + ts, err := time.Parse(time.RFC3339, src) + if err != nil { + return err + } + *t = Timestamp(ts) + return nil +} +``` + +### Parámetros de query + +Obtén un parámetro de query por nombre con `Context#QueryParam(name string)`: + +```go +func(c *echo.Context) error { + name := c.QueryParam("name") + return c.String(http.StatusOK, name) +} +``` + +Para tipos distintos de `string`, usa la función genérica `echo.QueryParam[T]`: + +```go +age, err := echo.QueryParam[int](c, "age") +if err != nil { + return err +} +``` + +```sh +curl -X GET "http://localhost:1323?name=Joe&age=30" +``` + +### Parámetros de path + +Obtén un parámetro de path registrado por nombre con `Context#Param(name string)`: + +```go +e.GET("/users/:name", func(c *echo.Context) error { + name := c.Param("name") + return c.String(http.StatusOK, name) +}) +``` + +Para tipos distintos de `string`, usa la función genérica `echo.PathParam[T]`: + +```go +id, err := echo.PathParam[int](c, "id") +if err != nil { + return err +} +``` + +```sh +curl http://localhost:1323/users/Joe +curl http://localhost:1323/users/123 +``` + +### Binding de datos + +Echo también puede vincular datos de request a structs y variables nativas de Go. Consulta +[Binding](/es/guide/binding/). + +## Validar datos + +Echo no incluye validación de datos integrada. Puedes registrar un validator personalizado mediante +`Echo#Validator` y usar una biblioteca de terceros como +[go-playground/validator](https://github.com/go-playground/validator). + +El ejemplo siguiente valida un struct vinculado: + +```go +package main + +import ( + "net/http" + + "github.com/go-playground/validator/v10" // go get github.com/go-playground/validator/v10 + "github.com/labstack/echo/v5" +) + +type CustomValidator struct { + validator *validator.Validate +} + +func (cv *CustomValidator) Validate(i any) error { + if err := cv.validator.Struct(i); err != nil { + // Optionally return the error to let each route control the status code. + return echo.ErrBadRequest.Wrap(err) + } + return nil +} + +type User struct { + Name string `json:"name" validate:"required"` + Email string `json:"email" validate:"required,email"` +} + +func main() { + e := echo.New() + e.Validator = &CustomValidator{validator: validator.New()} + + e.POST("/users", func(c *echo.Context) error { + u := new(User) + if err := c.Bind(u); err != nil { + return err + } + if err := c.Validate(u); err != nil { + return err + } + return c.JSON(http.StatusOK, u) + }) + + if err := e.Start(":1323"); err != nil { + e.Logger.Error("failed to start server", "error", err) + } +} +``` + +```sh +curl -X POST http://localhost:1323/users \ + -H 'Content-Type: application/json' \ + -d '{"name":"Joe","email":"joe@invalid-domain"}' +{"message":"Key: 'User.Email' Error:Field validation for 'Email' failed on the 'email' tag"} +``` diff --git a/site/src/content/docs/es/guide/response.md b/site/src/content/docs/es/guide/response.md new file mode 100644 index 00000000..17938d4e --- /dev/null +++ b/site/src/content/docs/es/guide/response.md @@ -0,0 +1,318 @@ +--- +title: Response +description: Envía strings, HTML, JSON, XML, archivos, streams, redirects y hooks de response. +sidebar: + order: 8 +--- + +Un handler escribe su response a través de `echo.Context`. Cada helper establece por ti el +`Content-Type` adecuado y el código de estado. + +## Enviar string + +`Context#String(code int, s string)` envía una response de texto plano con un código de estado. + +```go +func(c *echo.Context) error { + return c.String(http.StatusOK, "Hello, World!") +} +``` + +## Enviar HTML + +`Context#HTML(code int, html string)` envía una response HTML simple con un código de estado. +Para generar HTML dinámicamente, consulta [Templates](/es/guide/templates/). + +```go +func(c *echo.Context) error { + return c.HTML(http.StatusOK, "Hello, World!") +} +``` + +### Enviar blob HTML + +`Context#HTMLBlob(code int, b []byte)` envía un blob HTML con un código de estado. Es útil +con un template engine que produce `[]byte`. + +```go +func handler(c *echo.Context) error { + blob := []byte("Hello, World!") + return c.HTMLBlob(http.StatusOK, blob) +} +``` + +## Renderizar template + +Consulta [Templates](/es/guide/templates/). + +## Enviar JSON + +`Context#JSON(code int, i any)` codifica un valor Go como JSON y lo envía con un código de estado. + +```go +type User struct { + Name string `json:"name" xml:"name"` + Email string `json:"email" xml:"email"` +} + +func(c *echo.Context) error { + u := &User{ + Name: "Jon", + Email: "jon@labstack.com", + } + return c.JSON(http.StatusOK, u) +} +``` + +### Stream JSON + +`Context#JSON()` usa `json.Marshal` internamente, lo que puede ser ineficiente para payloads +grandes. En ese caso, transmite el JSON directamente: + +```go +func(c *echo.Context) error { + u := &User{ + Name: "Jon", + Email: "jon@labstack.com", + } + c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8) + c.Response().WriteHeader(http.StatusOK) + return json.NewEncoder(c.Response()).Encode(u) +} +``` + +### JSON pretty + +`Context#JSONPretty(code int, i any, indent string)` envía una response JSON con formato bonito. +La indentación puede ser espacios o tabs. + +```go +func(c *echo.Context) error { + u := &User{ + Name: "Jon", + Email: "jon@labstack.com", + } + return c.JSONPretty(http.StatusOK, u, " ") +} +``` + +```json +{ + "email": "jon@labstack.com", + "name": "Jon" +} +``` + +### Blob JSON + +`Context#JSONBlob(code int, b []byte)` envía directamente un blob JSON precodificado, por +ejemplo desde una base de datos. + +```go +func(c *echo.Context) error { + encodedJSON := []byte{} // Encoded JSON from an external source. + return c.JSONBlob(http.StatusOK, encodedJSON) +} +``` + +## Enviar JSONP + +`Context#JSONP(code int, callback string, i any)` codifica un valor Go como JSON y lo envía +como payload JSONP envuelto en el callback dado. + +```go +func handler(c *echo.Context) error { + callback := c.QueryParam("callback") + return c.JSONP(http.StatusOK, callback, &User{Name: "Jon", Email: "jon@labstack.com"}) +} +``` + +Consulta el [recetario de JSONP](/es/cookbook/jsonp/). + +## Enviar XML + +`Context#XML(code int, i any)` codifica un valor Go como XML y lo envía con un código de estado. + +```go +func(c *echo.Context) error { + u := &User{ + Name: "Jon", + Email: "jon@labstack.com", + } + return c.XML(http.StatusOK, u) +} +``` + +### Stream XML + +`Context#XML` usa `xml.Marshal` internamente, lo que puede ser ineficiente para payloads +grandes. En ese caso, transmite el XML directamente: + +```go +func(c *echo.Context) error { + u := &User{ + Name: "Jon", + Email: "jon@labstack.com", + } + c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationXMLCharsetUTF8) + c.Response().WriteHeader(http.StatusOK) + return xml.NewEncoder(c.Response()).Encode(u) +} +``` + +### XML pretty + +`Context#XMLPretty(code int, i any, indent string)` envía una response XML con formato bonito. +La indentación puede ser espacios o tabs. + +```go +func(c *echo.Context) error { + u := &User{ + Name: "Jon", + Email: "jon@labstack.com", + } + return c.XMLPretty(http.StatusOK, u, " ") +} +``` + +```xml + + + Jon + jon@labstack.com + +``` + +:::tip +También puedes hacer que `Context#XML()` produzca XML con formato bonito agregando `pretty` +al query string de la URL del request. + +```sh +curl http://localhost:1323/users/1?pretty +``` +::: + +### Blob XML + +`Context#XMLBlob(code int, b []byte)` envía directamente un blob XML precodificado, por +ejemplo desde una base de datos. + +```go +func(c *echo.Context) error { + encodedXML := []byte{} // Encoded XML from an external source. + return c.XMLBlob(http.StatusOK, encodedXML) +} +``` + +## Enviar archivo + +`Context#File(file string)` envía el contenido de un archivo como response. Establece +el content type correcto y maneja el caching automáticamente. + +```go +func(c *echo.Context) error { + return c.File("") +} +``` + +## Enviar attachment + +`Context#Attachment(file, name string)` es como `File()`, pero envía el archivo con +`Content-Disposition: attachment` y el nombre dado. + +```go +func(c *echo.Context) error { + return c.Attachment("", "") +} +``` + +## Enviar inline + +`Context#Inline(file, name string)` es como `File()`, pero envía el archivo con +`Content-Disposition: inline` y el nombre dado. + +```go +func(c *echo.Context) error { + return c.Inline("", "") +} +``` + +## Enviar blob + +`Context#Blob(code int, contentType string, b []byte)` envía datos arbitrarios con un +content type y código de estado dados. + +```go +func(c *echo.Context) error { + data := []byte(`0306703,0035866,NO_ACTION,06/19/2006 +0086003,"0005866",UPDATED,06/19/2006`) + return c.Blob(http.StatusOK, "text/csv", data) +} +``` + +## Enviar stream + +`Context#Stream(code int, contentType string, r io.Reader)` envía un stream de datos +arbitrario con un content type, `io.Reader` y código de estado dados. + +```go +func(c *echo.Context) error { + f, err := os.Open("") + if err != nil { + return err + } + defer f.Close() + return c.Stream(http.StatusOK, "image/png", f) +} +``` + +## Enviar sin contenido + +`Context#NoContent(code int)` envía un body vacío con un código de estado. + +```go +func(c *echo.Context) error { + return c.NoContent(http.StatusOK) +} +``` + +## Redirigir request + +`Context#Redirect(code int, url string)` redirige el request a la URL dada con un código de estado. + +```go +func(c *echo.Context) error { + return c.Redirect(http.StatusMovedPermanently, "") +} +``` + +## Hooks + +### Antes de la response + +`Response#Before(func())` registra una función que se ejecuta justo antes de escribir la response. + +### Después de la response + +`Response#After(func())` registra una función que se ejecuta justo después de escribir la response. +Si `Content-Length` es desconocido, no se ejecuta ninguna función after. + +```go +e.GET("/hooks", func(c *echo.Context) error { + resp, err := echo.UnwrapResponse(c.Response()) + if err != nil { + return err + } + resp.Before(func() { + println("before response") + }) + resp.After(func() { + println("after response") + }) + return c.String(http.StatusOK, "Hello, World!") +}) +``` + +:::tip +Puedes registrar varias funciones `Before` y `After`. +::: diff --git a/site/src/content/docs/es/guide/routing.md b/site/src/content/docs/es/guide/routing.md new file mode 100644 index 00000000..bf08c5e7 --- /dev/null +++ b/site/src/content/docs/es/guide/routing.md @@ -0,0 +1,75 @@ +--- +title: Routing +description: Hace coincidir URLs de request con handlers en el árbol radix sin asignaciones de Echo. +sidebar: + order: 3 +--- + +El router optimizado de Echo hace coincidir URLs de request con handlers usando un árbol radix con +**cero asignaciones dinámicas de memoria** y priorización inteligente de rutas. + +## Registrar rutas + +Usa los helpers de métodos HTTP en la instancia `Echo`. Cada uno recibe un patrón de path y una +`HandlerFunc` (`func(c *echo.Context) error`), con middleware opcional a nivel de ruta. + +```go +e := echo.New() + +e.GET("/users/:id", getUser) // named parameter +e.POST("/users", createUser) +e.PUT("/users/:id", updateUser) +e.DELETE("/users/:id", deleteUser) +e.GET("/static/*", serveFiles) // wildcard +``` + +`Any` registra un handler para todos los métodos soportados, y `Match` para un conjunto específico: + +```go +e.Any("/ping", pong) +e.Match([]string{http.MethodGet, http.MethodPost}, "/form", handleForm) +``` + +## Tipos de coincidencia + +| Patrón | Tipo | Ejemplo de coincidencia | +| ------------------ | -------- | -------------------------- | +| `/users/profile` | Static | `/users/profile` | +| `/users/:id` | Param | `/users/42` | +| `/static/*` | Wildcard | `/static/css/app.css` | + +:::note +La prioridad es **static → param → wildcard**, por lo que `/users/profile` siempre gana sobre +`/users/:id`, que a su vez gana sobre `/users/*`. +::: + +## Parámetros de path + +Lee parámetros nombrados desde el contexto con `c.Param()` (o `c.ParamOr()` para un valor por defecto): + +```go +func getUser(c *echo.Context) error { + id := c.Param("id") + return c.String(http.StatusOK, id) +} +``` + +El segmento wildcard está disponible como el parámetro `*`: + +```go +e.GET("/files/*", func(c *echo.Context) error { + return c.String(http.StatusOK, c.Param("*")) +}) +``` + +## Grupos + +Agrupa rutas que comparten un prefijo y middleware con `e.Group()`: + +```go +admin := e.Group("/admin", middleware.BasicAuth(authFn)) +admin.GET("/metrics", metrics) // -> /admin/metrics +admin.GET("/users", listUsers) // -> /admin/users +``` + +Los grupos pueden anidarse para componer árboles de rutas más grandes. diff --git a/site/src/content/docs/es/guide/static-files.md b/site/src/content/docs/es/guide/static-files.md new file mode 100644 index 00000000..bc326d7c --- /dev/null +++ b/site/src/content/docs/es/guide/static-files.md @@ -0,0 +1,89 @@ +--- +title: Servir archivos estáticos +description: Sirve imágenes, JavaScript, CSS, fuentes y otros assets con Echo. +sidebar: + order: 9 +--- + +Echo puede servir assets estáticos como imágenes, JavaScript, CSS, PDFs y fuentes desde +el filesystem o desde un filesystem embebido. + +## Filesystem por defecto + +Echo usa `os.DirFS(".")` como filesystem por defecto, con raíz en el directorio de trabajo +actual. Para cambiarlo, establece el campo `Echo#Filesystem`: + +```go +e := echo.New() +e.Filesystem = os.DirFS("assets") +``` + +## Usar el middleware Static + +Consulta [middleware Static](/es/middleware/static/). + +## Usar Echo#Static() + +`Echo#Static(prefix, root string)` registra una ruta que sirve archivos estáticos bajo +un prefijo de path desde el directorio raíz dado. + +Sirve cualquier archivo de `assets` bajo `/static/*`. Un request a `/static/js/main.js` +sirve `assets/js/main.js`: + +```go +e := echo.New() +e.Static("/static", "assets") +``` + +Sirve cualquier archivo de `assets` bajo `/*`. Un request a `/js/main.js` sirve +`assets/js/main.js`: + +```go +e := echo.New() +e.Static("/", "assets") +``` + +## Usar Echo#StaticFS() + +Los archivos estáticos se pueden servir desde cualquier `fs.FS`, incluido un `embed.FS`. Usa +`echo.MustSubFS` para que los archivos servidos tengan raíz en el subdirectorio correcto: un +`embed.FS` incluye sus subdirectorios como entradas propias. + +```go +//go:embed "assets/images" +var images embed.FS + +func main() { + e := echo.New() + + e.StaticFS("/images", echo.MustSubFS(images, "assets/images")) + + sc := echo.StartConfig{Address: ":1323"} + if err := sc.Start(context.Background(), e); err != nil { + e.Logger.Error("failed to start server", "error", err) + } +} +``` + +## Usar Echo#File() + +`Echo#File(path, file string)` registra una ruta que sirve un único archivo estático. + +Sirve una página index desde `public/index.html`: + +```go +e.File("/", "public/index.html") +``` + +Sirve un favicon desde `app/assets/favicon.ico`: + +```go +e := echo.New() +e.Filesystem = os.DirFS("/") +e.File("/favicon.ico", "app/assets/favicon.ico") // The file path must not have a leading slash. +``` + +:::caution +Un `/` inicial en el path del archivo no funciona con la mayoría de implementaciones de `fs.FS`. +Usa un path relativo. +::: diff --git a/site/src/content/docs/es/guide/templates.md b/site/src/content/docs/es/guide/templates.md new file mode 100644 index 00000000..e5cb290d --- /dev/null +++ b/site/src/content/docs/es/guide/templates.md @@ -0,0 +1,132 @@ +--- +title: Templates +description: Renderiza templates HTML con cualquier engine registrando un renderer. +sidebar: + order: 10 +--- + +`Context#Render(code int, name string, data any) error` renderiza un template con datos +y envía una response `text/html` con un código de estado. Registra un renderer estableciendo +`Echo#Renderer`, lo que te permite usar cualquier template engine. + +## Renderizado + +El ejemplo siguiente usa `html/template` de Go. + +Usa el renderer de templates por defecto: + +```go +e.Renderer = &echo.TemplateRenderer{ + Template: template.Must(template.New("hello").Parse("Hello, {{.}}!")), +} +``` + +O implementa tú mismo la interfaz `echo.Renderer`: + +```go +type Template struct { + templates *template.Template +} + +func (t *Template) Render(c *echo.Context, w io.Writer, name string, data any) error { + return t.templates.ExecuteTemplate(w, name, data) +} +``` + +1. Precompila los templates. + + `public/views/hello.html`: + + ```html + {{define "hello"}}Hello, {{.}}!{{end}} + ``` + + ```go + t := &Template{ + templates: template.Must(template.ParseGlob("public/views/*.html")), + } + ``` + +2. Registra el renderer. + + ```go + e := echo.New() + e.Renderer = t + e.GET("/hello", Hello) + ``` + +3. Renderiza un template dentro del handler. + + ```go + func Hello(c *echo.Context) error { + return c.Render(http.StatusOK, "hello", "World") + } + ``` + +## Avanzado: llamar a Echo desde templates + +A veces es útil generar URIs desde un template llamando a `Echo#Reverse`. `html/template` +de Go no es ideal para esto, pero se puede hacer de dos formas: proporcionando un método común +en cada objeto que se pasa a los templates, o pasando un `map[string]any` y ampliándolo en el +renderer personalizado. La segunda opción es más flexible. Aquí tienes un ejemplo completo. + +`template.html`: + +```html + + +

Hello {{index . "name"}}

+ +

{{ with $x := index . "reverse" }} + {{ call $x "foobar" }} + {{ end }} +

+ + +``` + +`server.go`: + +```go +package main + +import ( + "html/template" + "io" + "net/http" + + "github.com/labstack/echo/v5" +) + +// TemplateRenderer is a custom html/template renderer for Echo. +type TemplateRenderer struct { + templates *template.Template +} + +// Render renders a template document. +func (t *TemplateRenderer) Render(c *echo.Context, w io.Writer, name string, data any) error { + // Add global methods if the data is a map. + if viewContext, isMap := data.(map[string]any); isMap { + viewContext["reverse"] = c.RouteInfo().Reverse + } + + return t.templates.ExecuteTemplate(w, name, data) +} + +func main() { + e := echo.New() + e.Renderer = &TemplateRenderer{ + templates: template.Must(template.ParseGlob("main/*.html")), + } + + e.GET("/something/:name", func(c *echo.Context) error { + return c.Render(http.StatusOK, "template.html", map[string]any{ + "name": "Dolly!", + }) + }) + + if err := e.Start(":1323"); err != nil { + e.Logger.Error("shutting down the server", "error", err) + } +} +``` diff --git a/site/src/content/docs/es/guide/testing.md b/site/src/content/docs/es/guide/testing.md new file mode 100644 index 00000000..b71db7e1 --- /dev/null +++ b/site/src/content/docs/es/guide/testing.md @@ -0,0 +1,264 @@ +--- +title: Testing +description: Prueba handlers y middleware con httptest y los helpers de echotest. +sidebar: + order: 13 +--- + +Los handlers y middleware de Echo son funciones simples sobre un `echo.Context`, por lo que son +fáciles de probar con el paquete estándar `net/http/httptest`. El paquete `echotest` ofrece +helpers que reducen el código repetitivo. + +## Probar un handler + +Considera dos handlers: + +**CreateUser**: `POST /users` + +- Acepta un payload JSON. +- Devuelve `201 Created` si tiene éxito. +- Devuelve `500 Internal Server Error` si hay error. + +**GetUser**: `GET /users/:email` + +- Devuelve `200 OK` si tiene éxito. +- Devuelve `404 Not Found` si el usuario no existe; de lo contrario, `500 Internal Server Error`. + +`handler.go`: + +```go +package handler + +import ( + "net/http" + + "github.com/labstack/echo/v5" +) + +type ( + User struct { + Name string `json:"name" form:"name"` + Email string `json:"email" form:"email"` + } + handler struct { + db map[string]*User + } +) + +func (h *handler) createUser(c *echo.Context) error { + u := new(User) + if err := c.Bind(u); err != nil { + return err + } + return c.JSON(http.StatusCreated, u) +} + +func (h *handler) getUser(c *echo.Context) error { + email := c.Param("email") + user := h.db[email] + if user == nil { + return echo.NewHTTPError(http.StatusNotFound, "user not found") + } + return c.JSON(http.StatusOK, user) +} +``` + +`handler_test.go`: + +```go +package handler + +import ( + "fmt" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/labstack/echo/v5" + "github.com/labstack/echo/v5/echotest" + "github.com/stretchr/testify/assert" +) + +var ( + mockDB = map[string]*User{ + "jon@labstack.com": {Name: "Jon Snow", Email: "jon@labstack.com"}, + } + userJSON = `{"name":"Jon Snow","email":"jon@labstack.com"}` +) + +func TestCreateUser(t *testing.T) { + // Setup + e := echo.New() + req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(userJSON)) + req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) + + rec := httptest.NewRecorder() + c := e.NewContext(req, rec) + + h := &handler{mockDB} + + // Assertions + if assert.NoError(t, h.createUser(c)) { + assert.Equal(t, http.StatusCreated, rec.Code) + assert.Equal(t, userJSON+"\n", rec.Body.String()) + } +} +``` + +### Usar los helpers de echotest + +`echotest.ContextConfig` crea un contexto (y recorder) a partir de una descripción +declarativa del request: + +```go +// Same test as above, using echotest. +func TestCreateUserWithEchoTest(t *testing.T) { + c, rec := echotest.ContextConfig{ + Headers: map[string][]string{ + echo.HeaderContentType: {echo.MIMEApplicationJSON}, + }, + JSONBody: []byte(userJSON), + }.ToContextRecorder(t) + + h := &handler{mockDB} + + // Assertions + if assert.NoError(t, h.createUser(c)) { + assert.Equal(t, http.StatusCreated, rec.Code) + assert.Equal(t, userJSON+"\n", rec.Body.String()) + } +} + +// Even shorter, using ServeWithHandler. +func TestCreateUserWithServeHandler(t *testing.T) { + h := &handler{mockDB} + + rec := echotest.ContextConfig{ + Headers: map[string][]string{ + echo.HeaderContentType: {echo.MIMEApplicationJSON}, + }, + JSONBody: []byte(userJSON), + }.ServeWithHandler(t, h.createUser) + + assert.Equal(t, http.StatusCreated, rec.Code) + assert.Equal(t, userJSON+"\n", rec.Body.String()) +} + +func TestGetUser(t *testing.T) { + c, rec := echotest.ContextConfig{ + PathValues: echo.PathValues{ + {Name: "email", Value: "jon@labstack.com"}, + }, + Headers: map[string][]string{ + echo.HeaderContentType: {echo.MIMEApplicationJSON}, + }, + }.ToContextRecorder(t) + + h := &handler{mockDB} + + // Assertions + if assert.NoError(t, h.getUser(c)) { + assert.Equal(t, http.StatusOK, rec.Code) + assert.Equal(t, userJSON+"\n", rec.Body.String()) + } +} +``` + +### Usar un payload de formulario + +```go +// import "net/url" +f := make(url.Values) +f.Set("name", "Jon Snow") +f.Set("email", "jon@labstack.com") +req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(f.Encode())) +req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationForm) +``` + +Un payload de formulario multipart con `echotest`: + +```go +func TestContext_MultipartForm(t *testing.T) { + testConf := echotest.ContextConfig{ + MultipartForm: &echotest.MultipartForm{ + Fields: map[string]string{ + "key": "value", + }, + Files: []echotest.MultipartFormFile{ + { + Fieldname: "file", + Filename: "test.json", + Content: echotest.LoadBytes(t, "testdata/test.json"), + }, + }, + }, + } + c := testConf.ToContext(t) + + assert.Equal(t, "value", c.FormValue("key")) + assert.Equal(t, http.MethodPost, c.Request().Method) + assert.Equal(t, true, strings.HasPrefix(c.Request().Header.Get(echo.HeaderContentType), "multipart/form-data; boundary=")) + + fv, err := c.FormFile("file") + if err != nil { + t.Fatal(err) + } + assert.Equal(t, "test.json", fv.Filename) +} +``` + +### Establecer parámetros de path + +```go +c.SetPathValues(echo.PathValues{ + {Name: "id", Value: "1"}, + {Name: "email", Value: "jon@labstack.com"}, +}) +``` + +### Establecer parámetros de query + +```go +// import "net/url" +q := make(url.Values) +q.Set("email", "jon@labstack.com") +req := httptest.NewRequest(http.MethodGet, "/?"+q.Encode(), nil) +``` + +## Probar middleware + +```go +func TestMiddleware(t *testing.T) { + handler := func(c *echo.Context) error { + return c.JSON(http.StatusTeapot, fmt.Sprintf("email: %s", c.Param("email"))) + } + middleware := func(next echo.HandlerFunc) echo.HandlerFunc { + return func(c *echo.Context) error { + c.Set("user_id", int64(1234)) + return next(c) + } + } + + c, rec := echotest.ContextConfig{ + PathValues: echo.PathValues{{Name: "email", Value: "jon@labstack.com"}}, + }.ToContextRecorder(t) + + if err := middleware(handler)(c); err != nil { + t.Fatal(err) + } + + // Check that the middleware set the value. + userID, err := echo.ContextGet[int64](c, "user_id") + assert.NoError(t, err) + assert.Equal(t, int64(1234), userID) + + // Check that the handler returned the correct response. + assert.Equal(t, http.StatusTeapot, rec.Code) +} +``` + +:::tip +Para más ejemplos, consulta los [casos de prueba de middleware](https://github.com/labstack/echo/tree/master/middleware) +en el código fuente de Echo. +::: diff --git a/site/src/content/docs/es/middleware/basic-auth.md b/site/src/content/docs/es/middleware/basic-auth.md new file mode 100644 index 00000000..49dc36dc --- /dev/null +++ b/site/src/content/docs/es/middleware/basic-auth.md @@ -0,0 +1,74 @@ +--- +title: Basic Auth +description: Middleware de autenticación HTTP Basic que valida credenciales de username y password. +sidebar: + order: 1 +--- + +El middleware Basic Auth proporciona autenticación HTTP basic. + +- Para credenciales válidas llama al siguiente handler. +- Para credenciales ausentes o inválidas, envía una response `401 Unauthorized`. + +## Uso + +```go +e.Use(middleware.BasicAuth(func(c *echo.Context, username, password string) (bool, error) { + // Use a constant time comparison to prevent timing attacks. + if subtle.ConstantTimeCompare([]byte(username), []byte("joe")) == 1 && + subtle.ConstantTimeCompare([]byte(password), []byte("secret")) == 1 { + return true, nil + } + return false, nil +})) +``` + +## Configuración personalizada + +```go +e.Use(middleware.BasicAuthWithConfig(middleware.BasicAuthConfig{})) +``` + +## Configuración + +```go +type BasicAuthConfig struct { + // Skipper defines a function to skip middleware. + Skipper Skipper + + // Validator validates the credentials. If the request contains multiple basic + // auth headers, it is called once for each header until the first valid result. + // Required. + Validator BasicAuthValidator + + // Realm is the realm attribute of the WWW-Authenticate header. + // Default value "Restricted". + Realm string + + // AllowedCheckLimit sets how many headers are allowed to be checked. This is + // useful in environments such as corporate test setups with application proxies + // restricting access with their own auth scheme. + // Default value 1. + AllowedCheckLimit uint +} +``` + +`Validator` tiene esta firma: + +```go +type BasicAuthValidator func(c *echo.Context, user string, password string) (bool, error) +``` + +### Configuración por defecto + +```go +// Effective defaults applied when fields are left unset. +BasicAuthConfig{ + Skipper: DefaultSkipper, + Realm: "Restricted", +} +``` + +:::caution[Seguridad] +Compara siempre las credenciales con `subtle.ConstantTimeCompare` para prevenir timing attacks. +::: diff --git a/site/src/content/docs/es/middleware/body-dump.md b/site/src/content/docs/es/middleware/body-dump.md new file mode 100644 index 00000000..d500f95a --- /dev/null +++ b/site/src/content/docs/es/middleware/body-dump.md @@ -0,0 +1,72 @@ +--- +title: Body Dump +description: Captura payloads de request y response y pásalos a un handler para logging o debugging. +sidebar: + order: 2 +--- + +El middleware Body Dump captura los payloads de request y response y los pasa a un +handler registrado. Generalmente se usa para debugging o logging. + +:::caution +Evita Body Dump para payloads grandes, como uploads o downloads de archivos. Si debes usarlo +en esas rutas, agrega una excepción en la función skipper. +::: + +## Uso + +```go +e := echo.New() +e.Use(middleware.BodyDump(func(c *echo.Context, reqBody, resBody []byte, err error) { + // Handle the request and response bodies. +})) +``` + +## Configuración personalizada + +```go +e := echo.New() +e.Use(middleware.BodyDumpWithConfig(middleware.BodyDumpConfig{})) +``` + +## Configuración + +```go +type BodyDumpConfig struct { + // Skipper defines a function to skip middleware. + Skipper Skipper + + // Handler receives the request and response payloads and the handler error, if any. + // Required. + Handler BodyDumpHandler + + // MaxRequestBytes limits how much of the request body to dump. If the request body + // exceeds this limit, only the first MaxRequestBytes are dumped and the handler + // receives truncated data. + // Default: 5 * MB (5,242,880 bytes). Set to -1 to disable limits (not recommended). + MaxRequestBytes int64 + + // MaxResponseBytes limits how much of the response body to dump. If the response body + // exceeds this limit, only the first MaxResponseBytes are dumped and the handler + // receives truncated data. + // Default: 5 * MB (5,242,880 bytes). Set to -1 to disable limits (not recommended). + MaxResponseBytes int64 +} +``` + +`Handler` tiene esta firma: + +```go +type BodyDumpHandler func(c *echo.Context, reqBody []byte, resBody []byte, err error) +``` + +### Configuración por defecto + +```go +// Effective defaults applied when fields are left unset (Handler is required). +BodyDumpConfig{ + Skipper: DefaultSkipper, + MaxRequestBytes: 5 * MB, + MaxResponseBytes: 5 * MB, +} +``` diff --git a/site/src/content/docs/es/middleware/body-limit.md b/site/src/content/docs/es/middleware/body-limit.md new file mode 100644 index 00000000..128246e9 --- /dev/null +++ b/site/src/content/docs/es/middleware/body-limit.md @@ -0,0 +1,47 @@ +--- +title: Body Limit +description: Rechaza requests cuyo body supera un tamaño máximo configurado. +sidebar: + order: 3 +--- + +El middleware Body Limit establece el tamaño máximo permitido para un body de request. Si el tamaño +supera el límite configurado, envía una response `413 Request Entity Too Large`. + +El límite se aplica tanto al header de request `Content-Length` como al contenido real leído, +lo que lo hace resistente a headers falsificados. El límite se especifica en bytes. + +## Uso + +```go +e := echo.New() +e.Use(middleware.BodyLimit(2_097_152)) // 2 MB +``` + +## Configuración personalizada + +```go +e := echo.New() +e.Use(middleware.BodyLimitWithConfig(middleware.BodyLimitConfig{})) +``` + +## Configuración + +```go +type BodyLimitConfig struct { + // Skipper defines a function to skip middleware. + Skipper Skipper + + // LimitBytes is the maximum allowed size in bytes for a request body. + LimitBytes int64 +} +``` + +### Configuración por defecto + +```go +// Effective defaults applied when fields are left unset (Limit is required). +BodyLimitConfig{ + Skipper: DefaultSkipper, +} +``` diff --git a/site/src/content/docs/es/middleware/casbin-auth.md b/site/src/content/docs/es/middleware/casbin-auth.md new file mode 100644 index 00000000..d90c98f2 --- /dev/null +++ b/site/src/content/docs/es/middleware/casbin-auth.md @@ -0,0 +1,203 @@ +--- +title: Casbin Auth +description: Autoriza requests con la biblioteca de control de acceso Casbin usando un middleware personalizado pequeño. +sidebar: + order: 4 +--- + +[Casbin](https://github.com/casbin/casbin) es una biblioteca de control de acceso open-source +potente y eficiente para Go. Soporta aplicar autorización en muchos modelos: + +- ACL (Access Control List) +- ACL con superuser +- ACL sin usuarios, útil para sistemas sin autenticación o log-ins de usuario +- ACL sin recursos: apunta a un tipo de recurso (por ejemplo `write-article`, `read-log`) en lugar de a uno individual +- RBAC (Role-Based Access Control) +- RBAC con roles de recursos: tanto usuarios como recursos pueden tener roles +- RBAC con dominios/tenants: los usuarios pueden tener conjuntos de roles distintos por dominio/tenant +- ABAC (Attribute-Based Access Control) +- RESTful +- Deny-override: se soportan reglas allow y deny; deny sobrescribe allow + +Consulta el [resumen de API](https://casbin.org/docs/api-overview) y la +[documentación de Casbin](https://casbin.org/docs/) para más detalles. + +## Dependencias + +```bash +go get github.com/casbin/casbin/v3 +``` + +```go +import ( + "github.com/casbin/casbin/v3" +) +``` + +## Implementación + +Echo no incluye un middleware Casbin; la integración es un wrapper pequeño alrededor del +enforcer de Casbin: + +```go +// NewCasbinMiddleware returns middleware for Casbin (https://casbin.org/). +func NewCasbinMiddleware(enforcer *casbin.Enforcer, userGetter func(*echo.Context) (string, error)) echo.MiddlewareFunc { + return func(next echo.HandlerFunc) echo.HandlerFunc { + return func(c *echo.Context) error { + username, err := userGetter(c) + if err != nil { + return echo.ErrUnauthorized.Wrap(err) + } + if pass, err := enforcer.Enforce(username, c.Request().URL.Path, c.Request().Method); err != nil { + return echo.ErrInternalServerError.Wrap(err) + } else if !pass { + return echo.NewHTTPError(http.StatusForbidden, "access denied") + } + return next(c) + } + } +} +``` + +## Ejemplo + +Crea un archivo de modelo Casbin `auth_model.conf`: + +```ini +[request_definition] +r = sub, obj, act + +[policy_definition] +p = sub, obj, act + +[role_definition] +g = _, _ + +[policy_effect] +e = some(where (p.eft == allow)) + +[matchers] +m = g(r.sub, p.sub) && keyMatch(r.obj, p.obj) && (r.act == p.act || p.act == "*") +``` + +Crea un archivo de policy Casbin `auth_policy.csv`: + +```csv +p, 1234567890, /dataset1/*, GET +p, alice, /dataset1/*, GET +p, alice, /dataset1/resource1, POST +p, bob, /dataset2/resource1, * +p, bob, /dataset2/resource2, GET +p, bob, /dataset2/folder1/*, POST +p, dataset1_admin, /dataset1/*, * +g, cathy, dataset1_admin +``` + +La autenticación y la autorización son responsabilidades separadas. Autentica al usuario con +otro middleware (como JWT o Basic Auth), y luego proporciona un `userGetter` para que Casbin +pueda autorizar el request. + +### Con JWT + +```go +e.Use(echojwt.JWT([]byte("secret"))) // JWT middleware does authentication +jwtUser := func(c *echo.Context) (string, error) { // JWT user getter for Casbin authorization + token, err := echo.ContextGet[*jwt.Token](c, "user") + if err != nil { + return "", err + } + return token.Claims.GetSubject() +} +e.Use(NewCasbinMiddleware(ce, jwtUser)) // Casbin does authorization +``` + +Pruébalo con: + +```bash +curl -v "http://localhost:8080/dataset1/any" -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ" +``` + +### Con Basic Auth + +```go +// BasicAuth middleware does authentication +e.Use(middleware.BasicAuth(func(c *echo.Context, user, password string) (bool, error) { + return subtle.ConstantTimeCompare([]byte(user), []byte("alice")) == 1 && + subtle.ConstantTimeCompare([]byte(password), []byte("password")) == 1, nil +})) +basicAuthUser := func(c *echo.Context) (string, error) { // Basic auth user getter for Casbin authorization + username, _, _ := c.Request().BasicAuth() // password is verified by the BasicAuth middleware above + return username, nil +} +e.Use(NewCasbinMiddleware(ce, basicAuthUser)) // Casbin does authorization +``` + +Pruébalo con: + +```bash +# should pass +curl -v -u "alice:password" http://localhost:8080/dataset1/any +# should fail +curl -v -u "alice:password" http://localhost:8080/dataset2/resource2 +``` + +### Ejemplo completo de Casbin + JWT + +```go +package main + +import ( + "log/slog" + "net/http" + + "github.com/casbin/casbin/v3" + "github.com/golang-jwt/jwt/v5" + echojwt "github.com/labstack/echo-jwt/v5" + "github.com/labstack/echo/v5" +) + +// NewCasbinMiddleware returns middleware for Casbin (https://casbin.org/). +func NewCasbinMiddleware(enforcer *casbin.Enforcer, userGetter func(*echo.Context) (string, error)) echo.MiddlewareFunc { + return func(next echo.HandlerFunc) echo.HandlerFunc { + return func(c *echo.Context) error { + username, err := userGetter(c) + if err != nil { + return echo.ErrUnauthorized.Wrap(err) + } + if pass, err := enforcer.Enforce(username, c.Request().URL.Path, c.Request().Method); err != nil { + return echo.ErrInternalServerError.Wrap(err) + } else if !pass { + return echo.NewHTTPError(http.StatusForbidden, "access denied") + } + return next(c) + } + } +} + +func main() { + e := echo.New() + + ce, err := casbin.NewEnforcer("auth_model.conf", "auth_policy.csv") + if err != nil { + slog.Error("failed to initialize Casbin enforcer", "error", err) + } + + e.Use(echojwt.JWT([]byte("secret"))) // JWT middleware does authentication + jwtUser := func(c *echo.Context) (string, error) { // JWT user getter for Casbin authorization + token, err := echo.ContextGet[*jwt.Token](c, "user") + if err != nil { + return "", err + } + return token.Claims.GetSubject() + } + e.Use(NewCasbinMiddleware(ce, jwtUser)) // Casbin does authorization + + e.GET("/*", func(c *echo.Context) error { + return c.String(http.StatusOK, "Hello, World!") + }) + + if err := e.Start(":8080"); err != nil { + e.Logger.Error("failed to start server", "error", err) + } +} +``` diff --git a/site/src/content/docs/es/middleware/context-timeout.md b/site/src/content/docs/es/middleware/context-timeout.md new file mode 100644 index 00000000..e75ff5e1 --- /dev/null +++ b/site/src/content/docs/es/middleware/context-timeout.md @@ -0,0 +1,47 @@ +--- +title: Context Timeout +description: Aplica un timeout al contexto del request para que las operaciones conscientes del contexto puedan retornar antes. +sidebar: + order: 5 +--- + +El middleware Context Timeout aplica un timeout al contexto del request dentro de un periodo +predefinido, para que los métodos conscientes del contexto puedan retornar antes cuando se supera el deadline. + +## Uso + +```go +e.Use(middleware.ContextTimeout(60 * time.Second)) +``` + +## Configuración personalizada + +```go +e.Use(middleware.ContextTimeoutWithConfig(middleware.ContextTimeoutConfig{ + Timeout: 60 * time.Second, +})) +``` + +## Configuración + +```go +type ContextTimeoutConfig struct { + // Skipper defines a function to skip middleware. + Skipper Skipper + + // ErrorHandler is a function invoked when an error arises during middleware execution. + ErrorHandler func(c *echo.Context, err error) error + + // Timeout configures the timeout for the middleware. + Timeout time.Duration +} +``` + +### Configuración por defecto + +```go +// Effective defaults applied when fields are left unset (Timeout is required). +ContextTimeoutConfig{ + Skipper: DefaultSkipper, +} +``` diff --git a/site/src/content/docs/es/middleware/cors.md b/site/src/content/docs/es/middleware/cors.md new file mode 100644 index 00000000..ce6c57b6 --- /dev/null +++ b/site/src/content/docs/es/middleware/cors.md @@ -0,0 +1,118 @@ +--- +title: CORS +description: Middleware Cross-Origin Resource Sharing para control de acceso seguro entre dominios. +sidebar: + order: 6 +--- + +El middleware CORS implementa la especificación [CORS](https://fetch.spec.whatwg.org/#http-cors-protocol). +CORS da a los servidores web controles de acceso entre dominios, lo que permite transferencias de datos seguras entre dominios. + +## Uso + +```go +e.Use(middleware.CORS("https://example.com", "https://subdomain.example.com")) +``` + +## Configuración personalizada + +```go +e := echo.New() +e.Use(middleware.CORSWithConfig(middleware.CORSConfig{ + AllowOrigins: []string{"https://labstack.com", "https://labstack.net"}, + AllowHeaders: []string{echo.HeaderOrigin, echo.HeaderContentType, echo.HeaderAccept}, +})) +``` + +## Configuración + +```go +type CORSConfig struct { + // Skipper defines a function to skip middleware. + Skipper Skipper + + // AllowOrigins determines the value of the Access-Control-Allow-Origin response + // header, defining the list of origins that may access the resource. + // + // An origin consists of: scheme + "://" + host + optional ":" + port. + // A wildcard may be used, but it must be set explicitly as []string{"*"}. + // Example: `https://example.com`, `http://example.com:8080`, `*`. + // + // Security: use extreme caution when handling the origin and carefully validate any + // logic. Attackers may register hostile domain names. See + // https://blog.portswigger.net/2016/10/exploiting-cors-misconfigurations-for.html + // + // Mandatory. + AllowOrigins []string + + // UnsafeAllowOriginFunc is an optional custom function to validate the origin. It + // takes the origin and returns the allowed origin, whether it is allowed, and an + // error (returned immediately by the handler). If set, AllowOrigins is ignored. + // + // Security: use extreme caution when handling the origin. Attackers may register + // hostile (sub)domain names. + // + // Sub-domain check example: + // UnsafeAllowOriginFunc: func(c *echo.Context, origin string) (string, bool, error) { + // if strings.HasSuffix(origin, ".example.com") { + // return origin, true, nil + // } + // return "", false, nil + // } + // + // Optional. + UnsafeAllowOriginFunc func(c *echo.Context, origin string) (allowedOrigin string, allowed bool, err error) + + // AllowMethods determines the value of the Access-Control-Allow-Methods response + // header, used in response to a preflight request. + // + // Optional. Defaults to GET, HEAD, PUT, PATCH, POST, DELETE. If left empty, the + // middleware fills the preflight Access-Control-Allow-Methods header from the + // `Allow` header that the router set into the context. + AllowMethods []string + + // AllowHeaders determines the value of the Access-Control-Allow-Headers response + // header, indicating which HTTP headers can be used in the actual request. + // + // Optional. Defaults to an empty list. + AllowHeaders []string + + // AllowCredentials determines the value of the Access-Control-Allow-Credentials + // response header, indicating whether the response can be exposed when the + // credentials mode is true. + // + // Optional. Default value false, in which case the header is not set. + // + // Security: avoid using AllowCredentials = true together with AllowOrigins = *. + AllowCredentials bool + + // ExposeHeaders determines the value of Access-Control-Expose-Headers, the list of + // headers clients are allowed to access. + // + // Optional. Default value []string{}, in which case the header is not set. + ExposeHeaders []string + + // MaxAge determines the value of the Access-Control-Max-Age response header, how long + // (in seconds) the results of a preflight request can be cached. The header is set + // only if MaxAge != 0; a negative value sends "0", instructing browsers not to cache. + // + // Optional. Default value 0 — the header is not sent. + MaxAge int +} +``` + +### Configuración por defecto + +```go +// Effective defaults applied when fields are left unset. +CORSConfig{ + Skipper: DefaultSkipper, + AllowMethods: []string{http.MethodGet, http.MethodHead, http.MethodPut, http.MethodPatch, http.MethodPost, http.MethodDelete}, +} +``` + +:::caution[Seguridad] +Nunca combines `AllowCredentials = true` con un wildcard `AllowOrigins`. Cuando necesites +validación dinámica de origin, usa `UnsafeAllowOriginFunc` y valida con cuidado: +los atacantes pueden registrar nombres de (sub)dominio hostiles. +::: diff --git a/site/src/content/docs/es/middleware/csrf.md b/site/src/content/docs/es/middleware/csrf.md new file mode 100644 index 00000000..411e4863 --- /dev/null +++ b/site/src/content/docs/es/middleware/csrf.md @@ -0,0 +1,177 @@ +--- +title: CSRF +description: Protección contra Cross-Site Request Forgery usando metadatos Sec-Fetch-Site y validación de tokens. +sidebar: + order: 7 +--- + +Cross-Site Request Forgery (CSRF, a veces pronunciado "sea-surf", o XSRF) es un tipo de +exploit malicioso en el que se transmiten comandos no autorizados desde un usuario en el que +un sitio web confía. + +## Uso + +```go +e.Use(middleware.CSRF()) +``` + +## Cómo funciona + +El middleware CSRF soporta el header +[`Sec-Fetch-Site`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Sec-Fetch-Site) +como un enfoque moderno de defensa en profundidad para la +[protección CSRF](https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html#fetch-metadata-headers), +implementando la Fetch Metadata API recomendada por OWASP junto al mecanismo tradicional +basado en tokens. + +Los navegadores modernos envían automáticamente el header `Sec-Fetch-Site` con cada request, +indicando la relación entre el origin del request y el destino. El middleware usa esto para +tomar una decisión de seguridad: + +- **`same-origin`** o **`none`**: permitido (coincidencia exacta de origin o navegación directa del usuario) +- **`same-site`**: vuelve a la validación por token (por ejemplo, de subdominio a dominio principal) +- **`cross-site`**: bloqueado por defecto con un error `403` para métodos inseguros (POST, PUT, DELETE, PATCH) + +Para navegadores que no envían este header (navegadores más antiguos), el middleware vuelve +sin interrupciones a la protección CSRF tradicional basada en tokens. + +Dos opciones ajustan el comportamiento de `Sec-Fetch-Site`: + +- `TrustedOrigins []string`: allowlist de origins específicos para requests cross-site (útil para callbacks OAuth, webhooks) +- `AllowSecFetchSiteFunc func(c *echo.Context) (bool, error)`: lógica personalizada para validación same-site/cross-site + +```go +e.Use(middleware.CSRFWithConfig(middleware.CSRFConfig{ + // Allow OAuth callbacks from a trusted provider. + TrustedOrigins: []string{"https://oauth-provider.com"}, + + // Custom validation for same-site/cross-site requests. + AllowSecFetchSiteFunc: func(c *echo.Context) (bool, error) { + // Your custom authorization logic here. + return validateCustomAuth(c), nil + // return true, err // blocks the request with an error + // return true, nil // allows the request through + // return false, nil // falls back to legacy token logic + }, +})) +``` + +## Protección basada en tokens + +```go +e := echo.New() +e.Use(middleware.CSRFWithConfig(middleware.CSRFConfig{ + TokenLookup: "header:X-XSRF-TOKEN", +})) +``` + +El ejemplo anterior extrae el token CSRF del header de request `X-XSRF-TOKEN`. + +Leer el token desde una cookie en su lugar: + +```go +middleware.CSRFWithConfig(middleware.CSRFConfig{ + TokenLookup: "cookie:_csrf", + CookiePath: "/", + CookieDomain: "example.com", + CookieSecure: true, + CookieHTTPOnly: true, + CookieSameSite: http.SameSiteStrictMode, +}) +``` + +## Acceder al token CSRF + +- **Server-side**: el token está disponible desde el contexto bajo `ContextKey` y se puede pasar al cliente mediante un template. +- **Client-side**: el token se puede leer desde la cookie CSRF. + +## Configuración + +```go +type CSRFConfig struct { + // Skipper defines a function to skip middleware. + Skipper Skipper + + // TrustedOrigins permits any request with a `Sec-Fetch-Site` header whose `Origin` + // header exactly matches one of the listed values. Values should be formatted as + // the Origin header: "scheme://host[:port]". + TrustedOrigins []string + + // AllowSecFetchSiteFunc allows custom behaviour for `Sec-Fetch-Site` requests that + // are about to fail with a CSRF error, to be allowed or replaced with a custom + // error. Applies to `same-site` and `cross-site` values. + AllowSecFetchSiteFunc func(c *echo.Context) (bool, error) + + // TokenLength is the length of the generated token. + // Optional. Default value 32. + TokenLength uint8 + + // TokenLookup is a string in the form ":" or + // ":,:" used to extract the token from the request. + // Optional. Default value "header:X-CSRF-Token". + // Possible values: + // - "header:" or "header::" + // - "query:" + // - "form:" + // Multiple sources example: "header:X-CSRF-Token,query:csrf". + TokenLookup string `yaml:"token_lookup"` + + // Generator defines a function to generate the token. + // Optional. Defaults to randomString(TokenLength). + Generator func() string + + // ContextKey is the key under which the generated CSRF token is stored in the context. + // Optional. Default value "csrf". + ContextKey string + + // CookieName is the name of the CSRF cookie that stores the token. + // Optional. Default value "_csrf". + CookieName string + + // CookieDomain is the domain of the CSRF cookie. + // Optional. Default value none. + CookieDomain string + + // CookiePath is the path of the CSRF cookie. + // Optional. Default value none. + CookiePath string + + // CookieMaxAge is the max age (in seconds) of the CSRF cookie. + // Optional. Default value 86400 (24h). + CookieMaxAge int + + // CookieSecure indicates whether the CSRF cookie is secure. + // Optional. Default value false. + CookieSecure bool + + // CookieHTTPOnly indicates whether the CSRF cookie is HTTP only. + // Optional. Default value false. + CookieHTTPOnly bool + + // CookieSameSite indicates the SameSite mode of the CSRF cookie. + // Optional. Default value SameSiteDefaultMode. + CookieSameSite http.SameSite + + // ErrorHandler defines a function that returns custom errors. + ErrorHandler func(c *echo.Context, err error) error +} +``` + +### Configuración por defecto + +```go +var DefaultCSRFConfig = CSRFConfig{ + Skipper: DefaultSkipper, + TokenLength: 32, + TokenLookup: "header:" + echo.HeaderXCSRFToken, + ContextKey: "csrf", + CookieName: "_csrf", + CookieMaxAge: 86400, + CookieSameSite: http.SameSiteDefaultMode, +} +``` + +## Ejemplo completo + +Hay un ejemplo completo y ejecutable disponible en el +[recetario de echox](https://github.com/labstack/echox/blob/master/cookbook/csrf/main.go). diff --git a/site/src/content/docs/es/middleware/decompress.md b/site/src/content/docs/es/middleware/decompress.md new file mode 100644 index 00000000..f2ad7211 --- /dev/null +++ b/site/src/content/docs/es/middleware/decompress.md @@ -0,0 +1,59 @@ +--- +title: Decompress +description: Descomprime de forma transparente bodies de request codificados con gzip. +sidebar: + order: 8 +--- + +El middleware Decompress descomprime el body del request HTTP cuando el header +`Content-Encoding` está establecido en `gzip`. + +:::note +El body se descomprime en memoria y permanece allí durante la vida del request (y hasta +la garbage collection). +::: + +## Uso + +```go +e.Use(middleware.Decompress()) +``` + +## Configuración personalizada + +```go +e := echo.New() +e.Use(middleware.DecompressWithConfig(middleware.DecompressConfig{ + Skipper: middleware.DefaultSkipper, +})) +``` + +## Configuración + +```go +type DecompressConfig struct { + // Skipper defines a function to skip middleware. + Skipper Skipper + + // GzipDecompressPool provides the sync.Pool used to create and store gzip readers. + GzipDecompressPool Decompressor + + // MaxDecompressedSize limits the maximum size of the decompressed request body in + // bytes. If the decompressed body exceeds this limit, the middleware returns an + // HTTP 413 error. This prevents zip-bomb attacks where a small compressed payload + // decompresses to a huge size. + // Default: 100 * MB (104,857,600 bytes). Set to -1 to disable limits (not recommended). + MaxDecompressedSize int64 +} +``` + +### Configuración por defecto + +```go +// Effective defaults applied when fields are left unset. +DecompressConfig{ + Skipper: DefaultSkipper, + GzipDecompressPool: &DefaultGzipDecompressPool{}, + MaxDecompressedSize: 100 * MB, +} +``` diff --git a/site/src/content/docs/es/middleware/gzip.md b/site/src/content/docs/es/middleware/gzip.md new file mode 100644 index 00000000..9ec16acf --- /dev/null +++ b/site/src/content/docs/es/middleware/gzip.md @@ -0,0 +1,69 @@ +--- +title: Gzip +description: Comprime responses HTTP con el esquema de compresión gzip. +sidebar: + order: 9 +--- + +El middleware Gzip comprime la response HTTP usando el esquema de compresión gzip. + +## Uso + +```go +e.Use(middleware.Gzip()) +``` + +## Configuración personalizada + +```go +e := echo.New() +e.Use(middleware.GzipWithConfig(middleware.GzipConfig{ + Level: 5, +})) +``` + +:::tip +Pasa un skipper para deshabilitar gzip en ciertas URLs. +::: + +```go +e := echo.New() +e.Use(middleware.GzipWithConfig(middleware.GzipConfig{ + Skipper: func(c *echo.Context) bool { + return strings.Contains(c.Path(), "metrics") // change "metrics" to your own path + }, +})) +``` + +## Configuración + +```go +type GzipConfig struct { + // Skipper defines a function to skip middleware. + Skipper Skipper + + // Level is the gzip compression level. + // Optional. Default value -1. + Level int + + // MinLength is the length threshold before gzip compression is applied. + // Optional. Default value 0. + // + // Most of the time the default is fine. Compressing a short response might increase + // the transmitted data because of gzip's format overhead, and compression consumes + // CPU and time on both server and client. Depending on your use case such a + // threshold can be useful. + MinLength int +} +``` + +### Configuración por defecto + +```go +// Effective defaults applied when fields are left unset. +GzipConfig{ + Skipper: DefaultSkipper, + Level: -1, + MinLength: 0, +} +``` diff --git a/site/src/content/docs/es/middleware/jwt.md b/site/src/content/docs/es/middleware/jwt.md new file mode 100644 index 00000000..97cbbd8f --- /dev/null +++ b/site/src/content/docs/es/middleware/jwt.md @@ -0,0 +1,126 @@ +--- +title: JWT +description: Middleware de autenticación JSON Web Token provisto por el módulo echo-jwt. +sidebar: + order: 10 +--- + +El middleware JWT proporciona autenticación JSON Web Token (JWT). Vive en un módulo separado: +[github.com/labstack/echo-jwt](https://github.com/labstack/echo-jwt). + +Comportamiento: + +- Para un token válido, establece el usuario en el contexto y llama al siguiente handler. +- Para un token inválido, envía una response `401 Unauthorized`. +- Para un header `Authorization` ausente o inválido, envía una response `400 Bad Request`. + +## Dependencias + +```go +import "github.com/labstack/echo-jwt/v5" +``` + +## Uso + +```go +e.Use(echojwt.JWT([]byte("secret"))) +``` + +## Configuración personalizada + +```go +e.Use(echojwt.WithConfig(echojwt.Config{ + SigningKey: []byte("secret"), +})) +``` + +## Configuración + +```go +type Config struct { + // Skipper defines a function to skip middleware. + Skipper middleware.Skipper + + // BeforeFunc defines a function which is executed just before the middleware. + BeforeFunc middleware.BeforeFunc + + // SuccessHandler defines a function executed for a valid token. If it returns an + // error, the middleware stops the handler chain and returns that error. + SuccessHandler func(c *echo.Context) error + + // ErrorHandler defines a function executed when all lookups have been done and none + // passed the Validator. It runs with the last missing (ErrExtractionValueMissing) + // or invalid key, and may be used to define a custom JWT error. + // + // Note: when the error handler swallows the error (returns nil), the middleware + // continues the handler chain. This is useful when part of your site/api is public + // and offers extra features for authorized users; the handler can set a default + // public JWT token value in the request and continue. + ErrorHandler func(c *echo.Context, err error) error + + // ContinueOnIgnoredError allows the next middleware/handler to be called when the + // ErrorHandler ignores the error (returns nil). + ContinueOnIgnoredError bool + + // ContextKey is the key under which user information from the token is stored in the context. + // Optional. Default value "user". + ContextKey string + + // SigningKey is the signing key used to validate the token. One of the three options + // to provide a token validation key. Order of precedence: user-defined KeyFunc, + // SigningKeys, then SigningKey. + // Required if neither a user-defined KeyFunc nor SigningKeys is provided. + SigningKey any + + // SigningKeys is a map of signing keys to validate tokens using the kid field. One of + // the three options to provide a token validation key. + // Required if neither a user-defined KeyFunc nor SigningKey is provided. + SigningKeys map[string]any + + // SigningMethod is the signing method used to check the token's signing algorithm. + // Not checked when a user-defined KeyFunc is provided. + // Optional. Default value HS256. + SigningMethod string + + // KeyFunc supplies the public key for token validation. It must verify the signing + // algorithm and select the proper key. Useful when tokens are issued by an external + // party. When provided, SigningKey, SigningKeys and SigningMethod are ignored. + // One of the three options to provide a token validation key, and not used if a + // custom ParseTokenFunc is set. + KeyFunc jwt.Keyfunc + + // TokenLookup is a string in the form ":" or + // ":,:" used to extract the token from the request. + // Optional. Default value "header:Authorization". + // Possible values: + // - "header:" or "header::" + // trims a static prefix from the extracted value. For JWT tokens with + // `Authorization: Bearer `, the prefix to cut is `Bearer ` (note the space). + // If the prefix is empty, the whole value is returned. + // - "query:" + // - "param:" + // - "cookie:" + // - "form:" + // Multiple sources example: "header:Authorization:Bearer ,cookie:myowncookie". + TokenLookup string + + // TokenLookupFuncs is a list of user-defined functions that extract the JWT token + // from the context. One of two options to provide a token extractor. Order of + // precedence: TokenLookupFuncs, then TokenLookup. Both may be provided. + TokenLookupFuncs []middleware.ValuesExtractor + + // ParseTokenFunc parses the token from the given auth string, returning an error when + // parsing fails or the token is invalid. + // Defaults to an implementation using github.com/golang-jwt/jwt. + ParseTokenFunc func(c *echo.Context, auth string) (any, error) + + // NewClaimsFunc returns the extendable claims defining token content. Used by the + // default ParseTokenFunc; not used if a custom ParseTokenFunc is set. + // Optional. Defaults to a function returning jwt.MapClaims. + NewClaimsFunc func(c *echo.Context) jwt.Claims +} +``` + +## Ejemplo + +Consulta el [recetario de JWT](/es/cookbook/jwt/) para ver un ejemplo completo. diff --git a/site/src/content/docs/es/middleware/key-auth.md b/site/src/content/docs/es/middleware/key-auth.md new file mode 100644 index 00000000..39d0beb2 --- /dev/null +++ b/site/src/content/docs/es/middleware/key-auth.md @@ -0,0 +1,91 @@ +--- +title: Key Auth +description: Middleware de autenticación basada en clave que valida una API key desde header, query, form o cookie. +sidebar: + order: 11 +--- + +El middleware Key Auth proporciona autenticación basada en clave. + +- Para una clave válida llama al siguiente handler. +- Para una clave inválida, envía una response `401 Unauthorized`. +- Para una clave ausente, envía una response `400 Bad Request`. + +## Uso + +```go +e.Use(middleware.KeyAuth(func(c *echo.Context, key string, source middleware.ExtractorSource) (bool, error) { + return key == "valid-key", nil +})) +``` + +## Configuración personalizada + +```go +e := echo.New() +e.Use(middleware.KeyAuthWithConfig(middleware.KeyAuthConfig{ + KeyLookup: "query:api-key", + Validator: func(c *echo.Context, key string, source middleware.ExtractorSource) (bool, error) { + return key == "valid-key", nil + }, +})) +``` + +## Configuración + +```go +type KeyAuthConfig struct { + // Skipper defines a function to skip middleware. + Skipper Skipper + + // KeyLookup is a string in the form ":" or + // ":,:" used to extract the key from the request. + // Optional. Default value "header:Authorization:Bearer ". + // Possible values: + // - "header:" or "header::" + // trims a static prefix from the extracted value. For + // `Authorization: Basic `, the prefix to remove is `Basic `. + // - "query:" + // - "form:" + // - "cookie:" + // Multiple sources example: "header:Authorization,header:X-Api-Key". + KeyLookup string + + // AllowedCheckLimit sets how many KeyLookup values are allowed to be checked. This is + // useful in environments such as corporate test setups with application proxies + // restricting access with their own auth scheme. + AllowedCheckLimit uint + + // Validator validates the key. + // Required. + Validator KeyAuthValidator + + // ErrorHandler defines a function executed when all lookups have been done and none + // passed the Validator. It runs with the last missing (ErrExtractionValueMissing) or + // invalid key, and may be used to define a custom error. + // + // Note: when the error handler swallows the error (returns nil), the middleware + // continues the handler chain. This is useful when part of your site/api is public + // and offers extra features for authorized users. + ErrorHandler KeyAuthErrorHandler + + // ContinueOnIgnoredError allows the next middleware/handler to be called when the + // ErrorHandler ignores the error (returns nil). + ContinueOnIgnoredError bool +} +``` + +`Validator` tiene esta firma: + +```go +type KeyAuthValidator func(c *echo.Context, key string, source ExtractorSource) (bool, error) +``` + +### Configuración por defecto + +```go +DefaultKeyAuthConfig = KeyAuthConfig{ + Skipper: DefaultSkipper, + KeyLookup: "header:" + echo.HeaderAuthorization + ":Bearer ", +} +``` diff --git a/site/src/content/docs/es/middleware/logger.md b/site/src/content/docs/es/middleware/logger.md new file mode 100644 index 00000000..4858cb7f --- /dev/null +++ b/site/src/content/docs/es/middleware/logger.md @@ -0,0 +1,236 @@ +--- +title: Request Logger +description: Logging de requests totalmente personalizable que se integra con bibliotecas de logging estructurado. +sidebar: + order: 12 +--- + +El middleware `RequestLogger` registra información sobre cada request HTTP. Te permite +personalizar por completo qué se registra y cómo, por lo que encaja bien con bibliotecas +de terceros de logging estructurado. + +Los valores que el logger puede extraer se controlan con los campos booleanos y slices de +`RequestLoggerConfig`. Habilita un campo (por ejemplo `LogStatus: true`) para que su valor +se rellene en `RequestLoggerValues`, que se pasa a tu `LogValuesFunc`. + +```go +type RequestLoggerConfig struct { + // Skipper defines a function to skip middleware. + Skipper Skipper + + // BeforeNextFunc is called before the next middleware or handler in the chain. + BeforeNextFunc func(c *echo.Context) + + // LogValuesFunc is called with the values extracted by the logger from the + // request/response. + // Mandatory. + LogValuesFunc func(c *echo.Context, v RequestLoggerValues) error + + // HandleError instructs the logger to call the global error handler when the next + // middleware/handler returns an error. A side effect is that the response is then + // committed and sent, so middlewares up the chain can no longer change the status + // code or body. + HandleError bool + + // LogLatency records the duration of the rest of the handler chain (the next(c) call). + LogLatency bool + // LogProtocol extracts the request protocol (for example HTTP/1.1 or HTTP/2). + LogProtocol bool + // LogRemoteIP extracts the request remote IP. See echo.Context.RealIP() for details. + LogRemoteIP bool + // LogHost extracts the request host value (for example example.com). + LogHost bool + // LogMethod extracts the request method (for example GET). + LogMethod bool + // LogURI extracts the request URI (for example /list?lang=en&page=1). + LogURI bool + // LogURIPath extracts the request URI path part (for example /list). + LogURIPath bool + // LogRoutePath extracts the route path the request matched (for example /user/:id). + LogRoutePath bool + // LogRequestID extracts the request ID from the X-Request-ID request header, or the + // response if the request did not have a value. + LogRequestID bool + // LogReferer extracts the request referer value. + LogReferer bool + // LogUserAgent extracts the request user agent value. + LogUserAgent bool + // LogStatus extracts the response status code. If the chain returns an echo.HTTPError, + // the status code is taken from it. + LogStatus bool + // LogError extracts the error returned from the handler chain. + LogError bool + // LogContentLength extracts the Content-Length header value. Note: this can differ + // from the actual request body size as it may be spoofed. + LogContentLength bool + // LogResponseSize extracts the response content length. Note: when used with Gzip + // middleware this value may not always be correct. + LogResponseSize bool + // LogHeaders extracts the given list of request headers. A slice of values is logged + // per header since a request can contain more than one. Names are canonicalized with + // http.CanonicalHeaderKey (for example "accept-encoding" becomes "Accept-Encoding"). + LogHeaders []string + // LogQueryParams extracts the given list of query parameters from the request URI. A + // slice of values is logged per name since a request can repeat a parameter. + LogQueryParams []string + // LogFormValues extracts the given list of form values from the request body and URI. + // A slice of values is logged per name since a request can repeat a value. + LogFormValues []string +} +``` + +## Ejemplos + +### fmt.Printf + +```go +skipper := func(c *echo.Context) bool { + // Skip the health check endpoint. + return c.Request().URL.Path == "/health" +} +e.Use(middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{ + LogStatus: true, + LogURI: true, + Skipper: skipper, + BeforeNextFunc: func(c *echo.Context) { + c.Set("customValueFromContext", 42) + }, + LogValuesFunc: func(c *echo.Context, v middleware.RequestLoggerValues) error { + value, _ := c.Get("customValueFromContext").(int) + fmt.Printf("REQUEST: uri: %v, status: %v, custom-value: %v\n", v.URI, v.Status, value) + return nil + }, +})) +``` + +Salida de ejemplo: + +```text +REQUEST: uri: /hello, status: 200, custom-value: 42 +``` + +### slog ([log/slog](https://pkg.go.dev/log/slog)) + +```go +logger := slog.New(slog.NewJSONHandler(os.Stdout, nil)) +e.Use(middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{ + LogStatus: true, + LogURI: true, + LogError: true, + HandleError: true, // forwards the error to the global error handler so it can pick the status code + LogValuesFunc: func(c *echo.Context, v middleware.RequestLoggerValues) error { + if v.Error == nil { + logger.LogAttrs(context.Background(), slog.LevelInfo, "REQUEST", + slog.String("uri", v.URI), + slog.Int("status", v.Status), + ) + } else { + logger.LogAttrs(context.Background(), slog.LevelError, "REQUEST_ERROR", + slog.String("uri", v.URI), + slog.Int("status", v.Status), + slog.String("err", v.Error.Error()), + ) + } + return nil + }, +})) +``` + +Salida de ejemplo: + +```text +{"time":"2024-12-30T20:55:46.2399999+08:00","level":"INFO","msg":"REQUEST","uri":"/hello","status":200} +``` + +### Zerolog ([rs/zerolog](https://github.com/rs/zerolog)) + +```go +logger := zerolog.New(os.Stdout) +e.Use(middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{ + LogURI: true, + LogStatus: true, + LogValuesFunc: func(c *echo.Context, v middleware.RequestLoggerValues) error { + logger.Info(). + Str("URI", v.URI). + Int("status", v.Status). + Msg("request") + return nil + }, +})) +``` + +Salida de ejemplo: + +```text +{"level":"info","URI":"/hello","status":200,"message":"request"} +``` + +### Zap ([uber-go/zap](https://github.com/uber-go/zap)) + +```go +logger, _ := zap.NewProduction() +e.Use(middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{ + LogURI: true, + LogStatus: true, + LogValuesFunc: func(c *echo.Context, v middleware.RequestLoggerValues) error { + logger.Info("request", + zap.String("URI", v.URI), + zap.Int("status", v.Status), + ) + return nil + }, +})) +``` + +Salida de ejemplo: + +```text +{"level":"info","ts":1735564026.3197417,"caller":"cmd/main.go:20","msg":"request","URI":"/hello","status":200} +``` + +### Logrus ([sirupsen/logrus](https://github.com/sirupsen/logrus)) + +```go +log := logrus.New() +e.Use(middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{ + LogURI: true, + LogStatus: true, + LogValuesFunc: func(c *echo.Context, values middleware.RequestLoggerValues) error { + log.WithFields(logrus.Fields{ + "URI": values.URI, + "status": values.Status, + }).Info("request") + return nil + }, +})) +``` + +Salida de ejemplo: + +```text +time="2024-12-30T21:08:49+08:00" level=info msg=request URI=/hello status=200 +``` + +## Solución de problemas + +### panic: missing LogValuesFunc callback function for request logger middleware + +Este panic ocurre cuando el callback obligatorio `LogValuesFunc` se deja sin configurar. +Define una función que coincida con la firma de `LogValuesFunc` y asígnala en la configuración: + +```go +func logValues(c *echo.Context, v middleware.RequestLoggerValues) error { + fmt.Printf("Request Method: %s, URI: %s\n", v.Method, v.URI) + return nil +} + +e.Use(middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{ + LogValuesFunc: logValues, +})) +``` + +### Los parámetros en los logs están vacíos + +Si valores como `v.URI` y `v.Status` están vacíos dentro de `LogValuesFunc`, comprueba que los +flags de extracción correspondientes (`LogStatus`, `LogURI`, etc.) estén establecidos en `true` +en la configuración. Cada valor solo se rellena cuando su flag está habilitado. diff --git a/site/src/content/docs/es/middleware/method-override.md b/site/src/content/docs/es/middleware/method-override.md new file mode 100644 index 00000000..22fbaa4a --- /dev/null +++ b/site/src/content/docs/es/middleware/method-override.md @@ -0,0 +1,52 @@ +--- +title: Method Override +description: Sobrescribe el método HTTP de un request POST mediante header, form o valor de query. +sidebar: + order: 13 +--- + +El middleware Method Override lee el método sobrescrito desde el request y lo usa +en lugar del método original. + +:::note +Por razones de seguridad, solo se puede sobrescribir el método `POST`. +::: + +## Uso + +```go +e.Pre(middleware.MethodOverride()) +``` + +## Configuración personalizada + +```go +e := echo.New() +e.Pre(middleware.MethodOverrideWithConfig(middleware.MethodOverrideConfig{ + Getter: middleware.MethodFromForm("_method"), +})) +``` + +El método puede obtenerse con `MethodFromHeader`, `MethodFromForm` o `MethodFromQuery`. + +## Configuración + +```go +type MethodOverrideConfig struct { + // Skipper defines a function to skip middleware. + Skipper Skipper + + // Getter is a function that gets the overridden method from the request. + // Optional. Default value MethodFromHeader(echo.HeaderXHTTPMethodOverride). + Getter MethodOverrideGetter +} +``` + +### Configuración por defecto + +```go +DefaultMethodOverrideConfig = MethodOverrideConfig{ + Skipper: DefaultSkipper, + Getter: MethodFromHeader(echo.HeaderXHTTPMethodOverride), +} +``` diff --git a/site/src/content/docs/es/middleware/open-telemetry.md b/site/src/content/docs/es/middleware/open-telemetry.md new file mode 100644 index 00000000..4981e981 --- /dev/null +++ b/site/src/content/docs/es/middleware/open-telemetry.md @@ -0,0 +1,82 @@ +--- +title: OpenTelemetry +description: Instrumentación OpenTelemetry para requests HTTP en Echo. +sidebar: + order: 14 +--- + +[Echo OpenTelemetry](https://github.com/labstack/echo-opentelemetry) es un middleware que +proporciona instrumentación OpenTelemetry para requests HTTP. + +OpenTelemetry es un conjunto de herramientas open-source que proporcionan instrumentación para +aplicaciones cloud-native. + +- [OpenTelemetry Exporters](https://opentelemetry.io/docs/languages/go/exporters/) +- [OpenTelemetry HTTP spec](https://opentelemetry.io/docs/specs/semconv/http/) +- [HTTP metrics spec](https://opentelemetry.io/docs/specs/semconv/http/http-metrics/) + +## Uso + +Agrega la dependencia del middleware OpenTelemetry con Go modules: + +```bash +go get github.com/labstack/echo-opentelemetry +``` + +Importa el middleware y la API de tracing de OpenTelemetry: + +```go +import ( + echootel "github.com/labstack/echo-opentelemetry" + "go.opentelemetry.io/otel/trace" +) +``` + +Regístralo con configuración completa: + +```go +e.Use(echootel.NewMiddlewareWithConfig(echootel.Config{ + ServerName: "my-server", + TracerProvider: tp, + + //Skipper: nil, + //OnNextError: nil, + //OnExtractionError: nil, + //MeterProvider: nil, + //Propagators: nil, + //SpanStartOptions: nil, + //SpanStartAttributes: nil, + //SpanEndAttributes: nil, + //MetricAttributes: nil, + //Metrics: nil, +})) +``` + +Para opciones de configuración, consulta el struct +[`Config`](https://github.com/labstack/echo-opentelemetry/blob/main/otel.go#L28). + +Agrega el middleware en forma simplificada proporcionando solo el nombre del servidor: + +```go +e.Use(echootel.NewMiddleware("app.example.com")) +``` + +Agrega el middleware con opciones de configuración: + +```go +e.Use(echootel.NewMiddlewareWithConfig(echootel.Config{ + TracerProvider: tp, +})) +``` + +Obtén el tracer desde el contexto de Echo: + +```go +tracer, err := echo.ContextGet[trace.Tracer](c, echootel.TracerKey) +``` + +## Ejemplo + +El [ejemplo](https://github.com/labstack/echo-opentelemetry/blob/main/example/main.go) exporta +metrics y spans a stdout, pero puedes usar cualquier exporter (OTLP, etc.). Consulta la +documentación de [OpenTelemetry exporters](https://opentelemetry.io/docs/languages/go/exporters). diff --git a/site/src/content/docs/es/middleware/prometheus.md b/site/src/content/docs/es/middleware/prometheus.md new file mode 100644 index 00000000..88398857 --- /dev/null +++ b/site/src/content/docs/es/middleware/prometheus.md @@ -0,0 +1,284 @@ +--- +title: Prometheus +description: Genera metrics Prometheus para requests HTTP en Echo. +sidebar: + order: 15 +--- + +El middleware [Echo Prometheus](https://github.com/labstack/echo-prometheus) genera metrics +Prometheus para requests HTTP. + +## Uso + +Agrega el módulo requerido: + +```bash +go get github.com/labstack/echo-prometheus +``` + +Agrega el middleware Prometheus y una ruta para servir las metrics recopiladas: + +```go +e := echo.New() +e.Use(echoprometheus.NewMiddleware("myapp")) // adds middleware to gather metrics +e.GET("/metrics", echoprometheus.NewHandler()) // adds route to serve gathered metrics +``` + +## Ejemplos + +Servir metrics desde el mismo servidor que las recopila: + +```go +package main + +import ( + "net/http" + + echoprometheus "github.com/labstack/echo-prometheus" + "github.com/labstack/echo/v5" +) + +func main() { + e := echo.New() + + e.Use(echoprometheus.NewMiddleware("myapp")) // adds middleware to gather metrics + e.GET("/metrics", echoprometheus.NewHandler()) // adds route to serve gathered metrics + + e.GET("/hello", func(c *echo.Context) error { + return c.String(http.StatusOK, "hello") + }) + + if err := e.Start(":8080"); err != nil { + e.Logger.Error("failed to start server", "error", err) + } +} +``` + +Servir metrics en un puerto separado: + +```go +func main() { + e := echo.New() // this Echo instance will serve routes on port 8080 + e.Use(echoprometheus.NewMiddleware("myapp")) // adds middleware to gather metrics + + go func() { + metrics := echo.New() // this Echo will run on separate port 8081 + metrics.GET("/metrics", echoprometheus.NewHandler()) // adds route to serve gathered metrics + if err := metrics.Start(":8081"); err != nil { + e.Logger.Error("failed to start metrics server", "error", err) + } + }() + + e.GET("/hello", func(c *echo.Context) error { + return c.String(http.StatusOK, "hello") + }) + + if err := e.Start(":8080"); err != nil { + e.Logger.Error("failed to start server", "error", err) + } +} +``` + +Salida de ejemplo (para el primer ejemplo): + +```bash +curl http://localhost:8080/metrics + +# HELP echo_request_duration_seconds The HTTP request latencies in seconds. +# TYPE echo_request_duration_seconds summary +echo_request_duration_seconds_sum 0.41086482 +echo_request_duration_seconds_count 1 +# HELP echo_request_size_bytes The HTTP request sizes in bytes. +# TYPE echo_request_size_bytes summary +echo_request_size_bytes_sum 56 +echo_request_size_bytes_count 1 +# HELP echo_requests_total How many HTTP requests processed, partitioned by status code and HTTP method. +# TYPE echo_requests_total counter +echo_requests_total{code="200",host="localhost:8080",method="GET",url="/"} 1 +# HELP echo_response_size_bytes The HTTP response sizes in bytes. +# TYPE echo_response_size_bytes summary +echo_response_size_bytes_sum 61 +echo_response_size_bytes_count 1 +... +``` + +## Configuración personalizada + +### Servir metrics Prometheus personalizadas + +Usa metrics personalizadas con el registry por defecto de Prometheus: + +```go +package main + +import ( + "log" + + echoprometheus "github.com/labstack/echo-prometheus" + "github.com/labstack/echo/v5" + "github.com/prometheus/client_golang/prometheus" +) + +func main() { + e := echo.New() // this Echo instance will serve routes on port 8080 + + customCounter := prometheus.NewCounter( // create a new counter metric + prometheus.CounterOpts{ + Name: "custom_requests_total", + Help: "How many HTTP requests processed, partitioned by status code and HTTP method.", + }, + ) + if err := prometheus.Register(customCounter); err != nil { // register the counter with the default registry + log.Fatal(err) + } + + e.Use(echoprometheus.NewMiddlewareWithConfig(echoprometheus.MiddlewareConfig{ + AfterNext: func(c *echo.Context, err error) { + customCounter.Inc() // increment the counter after every request + }, + })) + e.GET("/metrics", echoprometheus.NewHandler()) // register a route to serve gathered metrics + + if err := e.Start(":8080"); err != nil { + e.Logger.Error("failed to start server", "error", err) + } +} +``` + +O crea tu propio registry y registra metrics personalizadas con él: + +```go +package main + +import ( + "log" + + echoprometheus "github.com/labstack/echo-prometheus" + "github.com/labstack/echo/v5" + "github.com/prometheus/client_golang/prometheus" +) + +func main() { + e := echo.New() // this Echo instance will serve routes on port 8080 + + customRegistry := prometheus.NewRegistry() // create a custom registry for your custom metrics + customCounter := prometheus.NewCounter( // create a new counter metric + prometheus.CounterOpts{ + Name: "custom_requests_total", + Help: "How many HTTP requests processed, partitioned by status code and HTTP method.", + }, + ) + if err := customRegistry.Register(customCounter); err != nil { // register the counter with the custom registry + log.Fatal(err) + } + + e.Use(echoprometheus.NewMiddlewareWithConfig(echoprometheus.MiddlewareConfig{ + AfterNext: func(c *echo.Context, err error) { + customCounter.Inc() // increment the counter after every request + }, + Registerer: customRegistry, // use the custom registry instead of the default Prometheus registry + })) + e.GET("/metrics", echoprometheus.NewHandlerWithConfig(echoprometheus.HandlerConfig{Gatherer: customRegistry})) // serve metrics from the custom registry + + if err := e.Start(":8080"); err != nil { + e.Logger.Error("failed to start server", "error", err) + } +} +``` + +### Omitir URLs + +Se puede pasar un skipper para evitar generar metrics en ciertas URLs: + +```go +package main + +import ( + "net/http" + "strings" + + echoprometheus "github.com/labstack/echo-prometheus" + "github.com/labstack/echo/v5" +) + +func main() { + e := echo.New() // this Echo instance will serve routes on port 8080 + + mwConfig := echoprometheus.MiddlewareConfig{ + Skipper: func(c *echo.Context) bool { + return strings.HasPrefix(c.Path(), "/testurl") + }, // does not gather metrics on routes starting with `/testurl` + } + e.Use(echoprometheus.NewMiddlewareWithConfig(mwConfig)) // adds middleware to gather metrics + + e.GET("/metrics", echoprometheus.NewHandler()) // adds route to serve gathered metrics + + e.GET("/", func(c *echo.Context) error { + return c.String(http.StatusOK, "Hello, World!") + }) + + if err := e.Start(":8080"); err != nil { + e.Logger.Error("failed to start server", "error", err) + } +} +``` + +## Escenarios complejos + +Modificar las definiciones de metrics por defecto de `echoprometheus`: + +```go +package main + +import ( + "net/http" + + echoprometheus "github.com/labstack/echo-prometheus" + "github.com/labstack/echo/v5" + "github.com/prometheus/client_golang/prometheus" +) + +func main() { + e := echo.New() // this Echo instance will serve routes on port 8080 + + e.Use(echoprometheus.NewMiddlewareWithConfig(echoprometheus.MiddlewareConfig{ + // Labels of default metrics can be modified or added with the `LabelFuncs` function. + LabelFuncs: map[string]echoprometheus.LabelValueFunc{ + "scheme": func(c *echo.Context, err error) string { // additional custom label + return c.Scheme() + }, + "host": func(c *echo.Context, err error) string { // overrides the default 'host' label value + return "y_" + c.Request().Host + }, + }, + // The `echoprometheus` middleware registers the following metrics by default: + // - Histogram: request_duration_seconds + // - Histogram: response_size_bytes + // - Histogram: request_size_bytes + // - Counter: requests_total + // which can be modified with the `HistogramOptsFunc` and `CounterOptsFunc` functions. + HistogramOptsFunc: func(opts prometheus.HistogramOpts) prometheus.HistogramOpts { + if opts.Name == "request_duration_seconds" { + opts.Buckets = []float64{1000.0, 10_000.0, 100_000.0, 1_000_000.0} // 1KB, 10KB, 100KB, 1MB + } + return opts + }, + CounterOptsFunc: func(opts prometheus.CounterOpts) prometheus.CounterOpts { + if opts.Name == "requests_total" { + opts.ConstLabels = prometheus.Labels{"my_const": "123"} + } + return opts + }, + })) // adds middleware to gather metrics + + e.GET("/metrics", echoprometheus.NewHandler()) // adds route to serve gathered metrics + + e.GET("/hello", func(c *echo.Context) error { + return c.String(http.StatusOK, "hello") + }) + + if err := e.Start(":8080"); err != nil { + e.Logger.Error("failed to start server", "error", err) + } +} +``` diff --git a/site/src/content/docs/es/middleware/proxy.md b/site/src/content/docs/es/middleware/proxy.md new file mode 100644 index 00000000..981fd079 --- /dev/null +++ b/site/src/content/docs/es/middleware/proxy.md @@ -0,0 +1,134 @@ +--- +title: Proxy +description: Middleware de reverse proxy HTTP y WebSocket con load balancing. +sidebar: + order: 16 +--- + +Proxy proporciona un middleware de reverse proxy HTTP/WebSocket. Reenvía un request a un +servidor upstream usando una técnica de load balancing configurada. + +## Uso + +```go +url1, err := url.Parse("http://localhost:8081") +if err != nil { + e.Logger.Error("failed to parse url", "error", err) +} +url2, err := url.Parse("http://localhost:8082") +if err != nil { + e.Logger.Error("failed to parse url", "error", err) +} +e.Use(middleware.Proxy(middleware.NewRoundRobinBalancer([]*middleware.ProxyTarget{ + { + URL: url1, + }, + { + URL: url2, + }, +}))) +``` + +## Configuración personalizada + +```go +e := echo.New() +e.Use(middleware.ProxyWithConfig(middleware.ProxyConfig{})) +``` + +## Configuración + +```go +type ProxyConfig struct { + // Skipper defines a function to skip middleware. + Skipper Skipper + + // Balancer defines a load balancing technique. + // Required. + Balancer ProxyBalancer + + // RetryCount defines the number of times a failed proxied request should be retried + // using the next available ProxyTarget. Defaults to 0, meaning requests are never retried. + RetryCount int + + // RetryFilter defines a function used to determine if a failed request to a + // ProxyTarget should be retried. The RetryFilter will only be called when the number + // of previous retries is less than RetryCount. If the function returns true, the + // request will be retried. The provided error indicates the reason for the request + // failure. When the ProxyTarget is unavailable, the error will be an instance of + // echo.HTTPError with a code of http.StatusBadGateway. In all other cases, the error + // will indicate an internal error in the Proxy middleware. When a RetryFilter is not + // specified, all requests that fail with http.StatusBadGateway will be retried. A custom + // RetryFilter can be provided to only retry specific requests. Note that RetryFilter is + // only called when the request to the target fails, or an internal error in the Proxy + // middleware has occurred. Successful requests that return a non-200 response code cannot + // be retried. + RetryFilter func(c *echo.Context, e error) bool + + // ErrorHandler defines a function which can be used to return custom errors from + // the Proxy middleware. ErrorHandler is only invoked when there has been + // either an internal error in the Proxy middleware or the ProxyTarget is + // unavailable. Due to the way requests are proxied, ErrorHandler is not invoked + // when a ProxyTarget returns a non-200 response. In these cases, the response + // is already written so errors cannot be modified. ErrorHandler is only + // invoked after all retry attempts have been exhausted. + ErrorHandler func(c *echo.Context, err error) error + + // Rewrite defines URL path rewrite rules. The values captured in asterisk can be + // retrieved by index e.g. $1, $2 and so on. + // Examples: + // "/old": "/new", + // "/api/*": "/$1", + // "/js/*": "/public/javascripts/$1", + // "/users/*/orders/*": "/user/$1/order/$2", + Rewrite map[string]string + + // RegexRewrite defines rewrite rules using regexp.Regexp with captures. + // Every capture group in the values can be retrieved by index e.g. $1, $2 and so on. + // Example: + // "^/old/[0.9]+/": "/new", + // "^/api/.+?/(.*)": "/v2/$1", + RegexRewrite map[*regexp.Regexp]string + + // Context key to store selected ProxyTarget into context. + // Optional. Default value "target". + ContextKey string + + // To customize the transport to remote. + // Examples: If custom TLS certificates are required. + Transport http.RoundTripper + + // ModifyResponse defines function to modify response from ProxyTarget. + ModifyResponse func(*http.Response) error +} +``` + +### Configuración por defecto + +| Nombre | Valor | +| ---------- | -------------- | +| Skipper | DefaultSkipper | +| ContextKey | `target` | + +### Reglas basadas en regex + +Para rewriting avanzado de requests proxied, también se pueden definir reglas usando +expresiones regulares. Los grupos de captura normales se pueden definir con `()` y referenciar +por índice (`$1`, `$2`, ...) en el path reescrito. + +Las reglas `RegexRewrite` y `Rewrite` normales se pueden combinar. + +```go +e.Use(middleware.ProxyWithConfig(middleware.ProxyConfig{ + Balancer: rrb, + Rewrite: map[string]string{ + "^/v1/*": "/v2/$1", + }, + RegexRewrite: map[*regexp.Regexp]string{ + regexp.MustCompile("^/foo/([0-9].*)"): "/num/$1", + regexp.MustCompile("^/bar/(.+?)/(.*)"): "/baz/$2/$1", + }, +})) +``` + +Consulta el recetario de [reverse proxy](/es/cookbook/reverse-proxy/) para ver un ejemplo completo. diff --git a/site/src/content/docs/es/middleware/rate-limiter.md b/site/src/content/docs/es/middleware/rate-limiter.md new file mode 100644 index 00000000..f0b3c5d6 --- /dev/null +++ b/site/src/content/docs/es/middleware/rate-limiter.md @@ -0,0 +1,109 @@ +--- +title: Rate Limiter +description: Limita el número de requests desde una IP o identificador particular dentro de un periodo. +sidebar: + order: 17 +--- + +`RateLimiter` proporciona un middleware de rate limiter que limita el número de requests enviados +al servidor desde una IP o identificador particular dentro de un periodo. + +Por defecto, un store en memoria lleva la cuenta de los requests. La implementación en memoria por +defecto se centra en la corrección y puede no ser la mejor opción para un número alto de requests +concurrentes o una gran cantidad de identificadores distintos (>16k). + +## Uso + +Para agregar un rate limit a tu aplicación, agrega el middleware `RateLimiter`. El ejemplo siguiente +limita la aplicación a 20 requests/sec usando el store en memoria por defecto: + +```go +e.Use(middleware.RateLimiter(middleware.NewRateLimiterMemoryStore(20.0))) +``` + +:::note +Si la tasa proporcionada es un número float, `Burst` se trata como el valor redondeado hacia abajo de la tasa. +::: + +## Configuración personalizada + +```go +config := middleware.RateLimiterConfig{ + Skipper: middleware.DefaultSkipper, + Store: middleware.NewRateLimiterMemoryStoreWithConfig( + middleware.RateLimiterMemoryStoreConfig{Rate: 10, Burst: 30, ExpiresIn: 3 * time.Minute}, + ), + IdentifierExtractor: func(c *echo.Context) (string, error) { + id := c.RealIP() + return id, nil + }, + ErrorHandler: func(c *echo.Context, err error) error { + return c.JSON(http.StatusForbidden, nil) + }, + DenyHandler: func(c *echo.Context, identifier string, err error) error { + return c.JSON(http.StatusTooManyRequests, nil) + }, +} + +e.Use(middleware.RateLimiterWithConfig(config)) +``` + +### Errores + +```go +var ( + // ErrRateLimitExceeded denotes an error raised when the rate limit is exceeded. + ErrRateLimitExceeded = echo.NewHTTPError(http.StatusTooManyRequests, "rate limit exceeded") + // ErrExtractorError denotes an error raised when the extractor function is unsuccessful. + ErrExtractorError = echo.NewHTTPError(http.StatusForbidden, "error while extracting identifier") +) +``` + +:::tip +Para implementar tu propio store, satisface la interfaz `RateLimiterStore` y pásalo a +`RateLimiterConfig`. +::: + +## Configuración + +```go +type RateLimiterConfig struct { + Skipper Skipper + BeforeFunc BeforeFunc + // IdentifierExtractor uses echo.Context to extract the identifier for a visitor. + IdentifierExtractor Extractor + // Store defines a store for the rate limiter. + Store RateLimiterStore + // ErrorHandler provides a handler to be called when IdentifierExtractor returns a non-nil error. + ErrorHandler func(c *echo.Context, err error) error + // DenyHandler provides a handler to be called when RateLimiter denies access. + DenyHandler func(c *echo.Context, identifier string, err error) error +} +``` + +### Configuración por defecto + +```go +// DefaultRateLimiterConfig defines default values for RateLimiterConfig. +var DefaultRateLimiterConfig = RateLimiterConfig{ + Skipper: DefaultSkipper, + IdentifierExtractor: func(c *echo.Context) (string, error) { + id := c.RealIP() + return id, nil + }, + ErrorHandler: func(c *echo.Context, err error) error { + return &echo.HTTPError{ + Code: ErrExtractorError.Code, + Message: ErrExtractorError.Message, + Internal: err, + } + }, + DenyHandler: func(c *echo.Context, identifier string, err error) error { + return &echo.HTTPError{ + Code: ErrRateLimitExceeded.Code, + Message: ErrRateLimitExceeded.Message, + Internal: err, + } + }, +} +``` diff --git a/site/src/content/docs/es/middleware/recover.md b/site/src/content/docs/es/middleware/recover.md new file mode 100644 index 00000000..82d94e3e --- /dev/null +++ b/site/src/content/docs/es/middleware/recover.md @@ -0,0 +1,61 @@ +--- +title: Recover +description: Recupérate de panics en cualquier punto de la cadena y delega al handler centralizado de errores. +sidebar: + order: 18 +--- + +El middleware Recover se recupera de panics en cualquier punto de la cadena, imprime el stack trace y +pasa el control al +[HTTPErrorHandler](/es/guide/customization/#http-error-handler) centralizado. + +## Uso + +```go +e.Use(middleware.Recover()) +``` + +## Configuración personalizada + +```go +e := echo.New() +e.Use(middleware.RecoverWithConfig(middleware.RecoverConfig{ + StackSize: 1 << 10, // 1 KB +})) +``` + +El ejemplo anterior usa un `StackSize` de 1 KB y valores por defecto para `DisableStackAll` y +`DisablePrintStack`. + +## Configuración + +```go +type RecoverConfig struct { + // Skipper defines a function to skip middleware. + Skipper Skipper + + // Size of the stack to be printed. + // Optional. Default value 4KB. + StackSize int + + // DisableStackAll disables formatting stack traces of all other goroutines + // into the buffer after the trace for the current goroutine. + // Optional. Default value false. + DisableStackAll bool + + // DisablePrintStack disables printing the stack trace. + // Optional. Default value false. + DisablePrintStack bool +} +``` + +### Configuración por defecto + +```go +var DefaultRecoverConfig = RecoverConfig{ + Skipper: DefaultSkipper, + StackSize: 4 << 10, // 4 KB + DisableStackAll: false, + DisablePrintStack: false, +} +``` diff --git a/site/src/content/docs/es/middleware/redirect.md b/site/src/content/docs/es/middleware/redirect.md new file mode 100644 index 00000000..77a401d9 --- /dev/null +++ b/site/src/content/docs/es/middleware/redirect.md @@ -0,0 +1,101 @@ +--- +title: Redirect +description: Redirige requests entre variantes HTTP/HTTPS y www/non-www. +sidebar: + order: 19 +--- + +## HTTPS Redirect + +El middleware HTTPS redirect redirige requests HTTP a HTTPS. Por ejemplo, +`http://labstack.com` se redirige a `https://labstack.com`. + +### Uso + +```go +e := echo.New() +e.Pre(middleware.HTTPSRedirect()) +``` + +## HTTPS WWW Redirect + +HTTPS WWW redirect redirige requests HTTP a www HTTPS. Por ejemplo, +`http://labstack.com` se redirige a `https://www.labstack.com`. + +### Uso + +```go +e := echo.New() +e.Pre(middleware.HTTPSWWWRedirect()) +``` + +## HTTPS NonWWW Redirect + +HTTPS NonWWW redirect redirige requests HTTP a non-www HTTPS. Por ejemplo, +`http://www.labstack.com` se redirige a `https://labstack.com`. + +### Uso + +```go +e := echo.New() +e.Pre(middleware.HTTPSNonWWWRedirect()) +``` + +## WWW Redirect + +WWW redirect redirige requests non-www a www. Por ejemplo, `http://labstack.com` se +redirige a `http://www.labstack.com`. + +### Uso + +```go +e := echo.New() +e.Pre(middleware.WWWRedirect()) +``` + +## NonWWW Redirect + +NonWWW redirect redirige requests www a non-www. Por ejemplo, `http://www.labstack.com` se +redirige a `http://labstack.com`. + +### Uso + +```go +e := echo.New() +e.Pre(middleware.NonWWWRedirect()) +``` + +## Configuración personalizada + +```go +e := echo.New() +e.Use(middleware.HTTPSRedirectWithConfig(middleware.RedirectConfig{ + Code: http.StatusTemporaryRedirect, +})) +``` + +El ejemplo anterior redirige requests HTTP a HTTPS con el código de estado +`307 - StatusTemporaryRedirect`. + +## Configuración + +```go +type RedirectConfig struct { + // Skipper defines a function to skip middleware. + Skipper Skipper + + // Status code to be used when redirecting the request. + // Optional. Default value http.StatusMovedPermanently. + Code int +} +``` + +### Configuración por defecto + +```go +// Effective defaults applied when fields are left unset. +RedirectConfig{ + Skipper: DefaultSkipper, + Code: http.StatusMovedPermanently, +} +``` diff --git a/site/src/content/docs/es/middleware/request-id.md b/site/src/content/docs/es/middleware/request-id.md new file mode 100644 index 00000000..730297de --- /dev/null +++ b/site/src/content/docs/es/middleware/request-id.md @@ -0,0 +1,89 @@ +--- +title: Request ID +description: Genera un ID único para cada request. +sidebar: + order: 20 +--- + +El middleware Request ID genera un ID único para un request. + +## Uso + +```go +e.Use(middleware.RequestID()) +``` + +Ejemplo: + +```go +func main() { + e := echo.New() + + e.Use(middleware.RequestID()) + + e.GET("/", func(c *echo.Context) error { + return c.String(http.StatusOK, c.Response().Header().Get(echo.HeaderXRequestID)) + }) + + if err := e.Start(":8080"); err != nil { + e.Logger.Error("failed to start server", "error", err) + } +} +``` + +## Configuración personalizada + +```go +e.Use(middleware.RequestIDWithConfig(middleware.RequestIDConfig{ + Generator: func() string { + return customGenerator() + }, +})) +``` + +## Configuración + +```go +type RequestIDConfig struct { + // Skipper defines a function to skip middleware. + Skipper Skipper + + // Generator defines a function to generate an ID. + // Optional. Default value random.String(32). + Generator func() string + + // RequestIDHandler defines a function which is executed for a request id. + RequestIDHandler func(c *echo.Context, requestID string) + + // TargetHeader defines what header to look for to populate the id. + // Optional. Default value is `X-Request-Id`. + TargetHeader string +} +``` + +### Configuración por defecto + +```go +// Effective defaults applied when fields are left unset. +RequestIDConfig{ + Skipper: DefaultSkipper, + Generator: generator, // random 32-character string + TargetHeader: echo.HeaderXRequestID, +} +``` + +## Establecer ID + +Puedes establecer el ID desde quien hace el request con el header `X-Request-ID`. + +### Request + +```sh +curl -H "X-Request-ID: 3" --compressed -v "http://localhost:1323/?my=param" +``` + +### Log + +```js +{"time":"2017-11-13T20:26:28.6438003+01:00","id":"3","remote_ip":"::1","host":"localhost:1323","method":"GET","uri":"/?my=param","my":"param","status":200, "latency":0,"latency_human":"0s","bytes_in":0,"bytes_out":13} +``` diff --git a/site/src/content/docs/es/middleware/rewrite.md b/site/src/content/docs/es/middleware/rewrite.md new file mode 100644 index 00000000..88e7b773 --- /dev/null +++ b/site/src/content/docs/es/middleware/rewrite.md @@ -0,0 +1,87 @@ +--- +title: Rewrite +description: Reescribe el path de la URL según reglas configuradas. +sidebar: + order: 21 +--- + +El middleware Rewrite reescribe el path de la URL según las reglas proporcionadas. Es útil para +compatibilidad hacia atrás o para crear enlaces más limpios y descriptivos. + +## Uso + +```go +e.Pre(middleware.Rewrite(map[string]string{ + "/old": "/new", + "/api/*": "/$1", + "/js/*": "/public/javascripts/$1", + "/users/*/orders/*": "/user/$1/order/$2", +})) +``` + +Los valores capturados en asteriscos pueden obtenerse por índice, por ejemplo `$1`, `$2`, etc. +Cada asterisco es no-greedy (se traduce a un grupo de captura `(.*?)`); al usar múltiples +asteriscos, un `*` final coincide con el resto del path. + +:::caution +El middleware Rewrite debe registrarse mediante `Echo#Pre()` para que se ejecute antes del router. +::: + +## Configuración personalizada + +```go +e := echo.New() +e.Pre(middleware.RewriteWithConfig(middleware.RewriteConfig{})) +``` + +## Configuración + +```go +type RewriteConfig struct { + // Skipper defines a function to skip middleware. + Skipper Skipper + + // Rules defines the URL path rewrite rules. The values captured in asterisk can be + // retrieved by index e.g. $1, $2 and so on. + // Example: + // "/old": "/new", + // "/api/*": "/$1", + // "/js/*": "/public/javascripts/$1", + // "/users/*/orders/*": "/user/$1/order/$2", + // Required. + Rules map[string]string + + // RegexRules defines the URL path rewrite rules using regexp.Regexp with captures. + // Every capture group in the values can be retrieved by index e.g. $1, $2 and so on. + // Example: + // "^/old/[0.9]+/": "/new", + // "^/api/.+?/(.*)": "/v2/$1", + RegexRules map[*regexp.Regexp]string +} +``` + +Configuración por defecto: + +| Nombre | Valor | +| ------- | -------------- | +| Skipper | DefaultSkipper | + +### Reglas basadas en regex + +Para rewriting avanzado de paths, también se pueden definir reglas usando expresiones regulares. +Los grupos de captura normales se pueden definir con `()` y referenciar por índice +(`$1`, `$2`, ...) en el path reescrito. + +`RegexRules` y `Rules` normales se pueden combinar. + +```go +e.Pre(middleware.RewriteWithConfig(middleware.RewriteConfig{ + Rules: map[string]string{ + "^/v1/*": "/v2/$1", + }, + RegexRules: map[*regexp.Regexp]string{ + regexp.MustCompile("^/foo/([0-9].*)"): "/num/$1", + regexp.MustCompile("^/bar/(.+?)/(.*)"): "/baz/$2/$1", + }, +})) +``` diff --git a/site/src/content/docs/es/middleware/secure.md b/site/src/content/docs/es/middleware/secure.md new file mode 100644 index 00000000..7bef8a6f --- /dev/null +++ b/site/src/content/docs/es/middleware/secure.md @@ -0,0 +1,113 @@ +--- +title: Secure +description: Protege contra XSS, content sniffing, clickjacking y otros ataques de inyección. +sidebar: + order: 22 +--- + +El middleware Secure proporciona protección contra cross-site scripting (XSS), content type +sniffing, clickjacking, conexiones inseguras y otros ataques de inyección de código. + +## Uso + +```go +e.Use(middleware.Secure()) +``` + +## Configuración personalizada + +```go +e := echo.New() +e.Use(middleware.SecureWithConfig(middleware.SecureConfig{ + XSSProtection: "", + ContentTypeNosniff: "", + XFrameOptions: "", + HSTSMaxAge: 3600, + ContentSecurityPolicy: "default-src 'self'", +})) +``` + +:::note +Pasar un `XSSProtection`, `ContentTypeNosniff`, `XFrameOptions` o +`ContentSecurityPolicy` vacío deshabilita esa protección. +::: + +## Configuración + +```go +type SecureConfig struct { + // Skipper defines a function to skip middleware. + Skipper Skipper + + // XSSProtection provides protection against cross-site scripting attack (XSS) + // by setting the `X-XSS-Protection` header. + // Optional. Default value "1; mode=block". + XSSProtection string + + // ContentTypeNosniff provides protection against overriding Content-Type + // header by setting the `X-Content-Type-Options` header. + // Optional. Default value "nosniff". + ContentTypeNosniff string + + // XFrameOptions can be used to indicate whether or not a browser should + // be allowed to render a page in a ,