diff --git a/site/astro.config.mjs b/site/astro.config.mjs index fabbb55e..5603037f 100644 --- a/site/astro.config.mjs +++ b/site/astro.config.mjs @@ -16,6 +16,7 @@ export default defineConfig({ 'zh-cn': { label: '简体中文', lang: 'zh-CN' }, ja: { label: '日本語', lang: 'ja' }, es: { label: 'Español', lang: 'es' }, + 'pt-br': { label: 'Português', lang: 'pt-BR' }, }, logo: { light: './src/assets/logo-light.svg', @@ -104,9 +105,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: 'ガイド', 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' } }] }, + { label: 'Guide', translations: { 'zh-CN': '指南', ja: 'ガイド', es: 'Guía', 'pt-BR': 'Guia' }, items: [{ autogenerate: { directory: 'guide' } }] }, + { label: 'Middleware', translations: { 'zh-CN': '中间件', ja: 'ミドルウェア', es: 'Middleware', 'pt-BR': 'Middleware' }, items: [{ autogenerate: { directory: 'middleware' } }] }, + { label: 'Cookbook', translations: { 'zh-CN': '示例', ja: 'クックブック', es: 'Recetario', 'pt-BR': 'Receitas' }, 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/pt-br/cookbook/auto-tls.md b/site/src/content/docs/pt-br/cookbook/auto-tls.md new file mode 100644 index 00000000..5c1f68ea --- /dev/null +++ b/site/src/content/docs/pt-br/cookbook/auto-tls.md @@ -0,0 +1,110 @@ +--- +title: Auto TLS +description: Obtenha e renove automaticamente certificados TLS da Let's Encrypt. +sidebar: + order: 3 +--- + +Esta receita obtém certificados TLS para um domínio automaticamente da Let's Encrypt. +Configure um `StartConfig` com o `TLSConfig` do gerenciador autocert e escute na +porta `443`. + +Acesse `https://`. Se tudo estiver configurado corretamente, você deverá ver +uma mensagem de boas-vindas servida por TLS. + +:::tip +- Para segurança adicional, especifique uma política de host no gerenciador autocert. +- Faça cache de certificados para evitar atingir os [limites de taxa da Let's Encrypt](https://letsencrypt.org/docs/rate-limits). +- Para redirecionar tráfego HTTP para HTTPS, use o [middleware de redirect](/pt-br/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) + } +} +``` + +## Usando um servidor HTTP customizado + +Se você precisar de controle total sobre o `http.Server`, conecte o gerenciador autocert a um +`tls.Config` customizado: + +```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/pt-br/cookbook/cors.md b/site/src/content/docs/pt-br/cookbook/cors.md new file mode 100644 index 00000000..b6ea923e --- /dev/null +++ b/site/src/content/docs/pt-br/cookbook/cors.md @@ -0,0 +1,120 @@ +--- +title: CORS +description: Habilite Cross-Origin Resource Sharing com uma allow list ou função de origem customizada. +sidebar: + order: 4 +--- + +O [middleware CORS](/pt-br/middleware/cors/) controla quais origens podem acessar sua API. +Você pode passar uma lista fixa de origens permitidas ou fornecer uma função que decide +por request. + +## Allow list de origens + +Passe as origens permitidas diretamente para `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) + } +} +``` + +## Função de origem customizada + +Para políticas dinâmicas, use `CORSWithConfig` com `UnsafeAllowOriginFunc`. A +função recebe o contexto do request e a origem, e retorna a origem a ser ecoada de volta, +se o request é permitido, e um erro 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/pt-br/cookbook/crud.md b/site/src/content/docs/pt-br/cookbook/crud.md new file mode 100644 index 00000000..9644e58b --- /dev/null +++ b/site/src/content/docs/pt-br/cookbook/crud.md @@ -0,0 +1,180 @@ +--- +title: CRUD +description: Crie, leia, atualize e exclua recursos com Echo e binding JSON. +sidebar: + order: 2 +--- + +Uma API CRUD completa (create, read, update, delete) baseada em um store em memória. +Cada handler faz binding do body JSON do request em uma struct, altera o store sob um +lock e retorna o 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 + +### Criar usuário + +Request: + +```sh +curl -X POST \ + -H 'Content-Type: application/json' \ + -d '{"name":"Joe Smith"}' \ + localhost:1323/users +``` + +Response: + +```json +{ + "id": 1, + "name": "Joe Smith" +} +``` + +### Obter usuário + +Request: + +```sh +curl localhost:1323/users/1 +``` + +Response: + +```json +{ + "id": 1, + "name": "Joe Smith" +} +``` + +### Atualizar usuário + +Request: + +```sh +curl -X PUT \ + -H 'Content-Type: application/json' \ + -d '{"name":"Joe"}' \ + localhost:1323/users/1 +``` + +Response: + +```json +{ + "id": 1, + "name": "Joe" +} +``` + +### Excluir usuário + +Request: + +```sh +curl -X DELETE localhost:1323/users/1 +``` + +Response: `204 No Content`. diff --git a/site/src/content/docs/pt-br/cookbook/embed-resources.md b/site/src/content/docs/pt-br/cookbook/embed-resources.md new file mode 100644 index 00000000..c7bf6130 --- /dev/null +++ b/site/src/content/docs/pt-br/cookbook/embed-resources.md @@ -0,0 +1,65 @@ +--- +title: Recursos incorporados +description: Sirva assets estáticos empacotados no binário com o pacote embed do Go. +sidebar: + order: 5 +--- + +O pacote `embed` do Go (Go 1.16+) permite compilar assets estáticos diretamente no +binário, para que um único executável possa incluir seu frontend. Esta receita serve o +filesystem incorporado por meio do Echo, com um modo live opcional que lê do disco +durante o desenvolvimento. + +## 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 +Execute o binário com o argumento `live` (`go run server.go live`) para servir assets +a partir do diretório `app` no disco em vez da cópia incorporada, o que é útil +durante o desenvolvimento. +::: diff --git a/site/src/content/docs/pt-br/cookbook/file-download.md b/site/src/content/docs/pt-br/cookbook/file-download.md new file mode 100644 index 00000000..b7e1a21a --- /dev/null +++ b/site/src/content/docs/pt-br/cookbook/file-download.md @@ -0,0 +1,176 @@ +--- +title: Download de arquivos +description: Sirva arquivos para download, exibição inline ou como attachments nomeados. +sidebar: + order: 6 +--- + +Echo fornece três helpers de contexto para retornar arquivos: `c.File` serve um arquivo +usando a content disposition padrão do navegador, `c.Inline` sugere que o navegador +exiba o arquivo no local, e `c.Attachment` solicita um download com um +nome de arquivo informado. + +## Baixar arquivo + +### 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 +

+ + + +``` + +## Baixar arquivo como inline + +Use `c.Inline` para enviar um header `Content-Disposition: inline`, de modo que o navegador +renderize o arquivo no local em vez de baixá-lo. + +### 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 +

+ + + +``` + +## Baixar arquivo como attachment + +Use `c.Attachment` para enviar um header `Content-Disposition: attachment`, solicitando +que o navegador baixe o arquivo com o nome fornecido. + +### 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/pt-br/cookbook/file-upload.md b/site/src/content/docs/pt-br/cookbook/file-upload.md new file mode 100644 index 00000000..27be1d93 --- /dev/null +++ b/site/src/content/docs/pt-br/cookbook/file-upload.md @@ -0,0 +1,198 @@ +--- +title: Upload de arquivos +description: Trate uploads multipart de um ou vários arquivos junto de campos de formulário. +sidebar: + order: 7 +--- + +Echo lê dados de formulário multipart por meio do contexto do request. Use `c.FormValue` para +campos de texto, `c.FormFile` para um único arquivo e `c.MultipartForm` para acessar +vários arquivos sob o mesmo nome de campo. + +## Fazer upload de um único arquivo com 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:

+ +
+ + +``` + +## Fazer upload de vários arquivos com 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/pt-br/cookbook/graceful-shutdown.md b/site/src/content/docs/pt-br/cookbook/graceful-shutdown.md new file mode 100644 index 00000000..61268c08 --- /dev/null +++ b/site/src/content/docs/pt-br/cookbook/graceful-shutdown.md @@ -0,0 +1,86 @@ +--- +title: Encerramento graceful +description: Drene requests em andamento antes de parar o servidor em um sinal de interrupção. +sidebar: + order: 8 +--- + +Um graceful shutdown permite que requests em andamento terminem antes de o processo sair. A +abordagem mais simples é passar um contexto cancelável para `StartConfig.Start` e definir +um `GracefulTimeout`. Quando o contexto é cancelado por um sinal de interrupção, Echo +para de aceitar novas conexões e aguarda até o timeout para que requests ativos +terminem. + +## 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) + } +} +``` + +## Usando um servidor HTTP customizado + +Se você gerencia o `http.Server` por conta própria, inicie-o em uma goroutine, aguarde no +contexto de sinal e então chame `Shutdown` com um 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/pt-br/cookbook/hello-world.md b/site/src/content/docs/pt-br/cookbook/hello-world.md new file mode 100644 index 00000000..0bf6e1be --- /dev/null +++ b/site/src/content/docs/pt-br/cookbook/hello-world.md @@ -0,0 +1,43 @@ +--- +title: Hello World +description: Um servidor Echo mínimo que responde com uma saudação. +sidebar: + order: 1 +--- + +Uma aplicação Echo mínima: crie uma instância, registre os middleware Logger e Recover, +adicione uma única rota e inicie o 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/pt-br/cookbook/http2-server-push.md b/site/src/content/docs/pt-br/cookbook/http2-server-push.md new file mode 100644 index 00000000..17495011 --- /dev/null +++ b/site/src/content/docs/pt-br/cookbook/http2-server-push.md @@ -0,0 +1,130 @@ +--- +title: HTTP/2 Server Push +description: Envie web assets ao cliente proativamente por HTTP/2. +sidebar: + order: 10 +--- + +HTTP/2 server push permite que o servidor envie recursos ao cliente antes que eles sejam +solicitados, eliminando uma ida e volta para assets que a página já sabe que vai precisar. Esta +receita envia CSS, JavaScript e imagem de uma página junto da response HTML. + +:::note +Server push requer uma conexão HTTP/2. Siga [Gerar um certificado TLS X.509 +autoassinado](/pt-br/cookbook/http2/#1-generate-a-self-signed-x509-tls-certificate) +para criar o certificado usado abaixo. +::: + +## 1. Registrar uma rota para servir web assets + +```go +e.Static("/", "static") +``` + +## 2. Servir index.html e enviar suas dependências + +Desembrulhe a response para acessar o `http.ResponseWriter` subjacente e então envie cada +asset se o writer implementar `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 +Quando `http.Pusher` tem suporte, os web assets são enviados proativamente; caso contrário, +o cliente volta a solicitá-los separadamente. +::: + +## 3. Iniciar o 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-fonte + +### 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/pt-br/cookbook/http2.md b/site/src/content/docs/pt-br/cookbook/http2.md new file mode 100644 index 00000000..b3b06036 --- /dev/null +++ b/site/src/content/docs/pt-br/cookbook/http2.md @@ -0,0 +1,116 @@ +--- +title: Servidor HTTP/2 +description: Sirva tráfego por HTTP/2 iniciando Echo com um certificado TLS. +sidebar: + order: 9 +--- + +HTTP/2 melhora a latência por meio de multiplexação de requests, compressão de headers e +server push. O servidor HTTP do Go negocia HTTP/2 automaticamente sobre TLS, então servir +HTTP/2 com Echo é uma questão de iniciar o servidor com um certificado. + +## 1. Gerar um certificado TLS X.509 autoassinado + +Execute o comando a seguir para gerar `cert.pem` e `key.pem`: + +```sh +go run $GOROOT/src/crypto/tls/generate_cert.go --host localhost +``` + +:::note +Para fins de demonstração, usamos um certificado autoassinado. Em produção, obtenha +um certificado de uma [certificate authority](https://en.wikipedia.org/wiki/Certificate_authority). +::: + +## 2. Criar um handler que ecoa informações do 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 o servidor TLS + +Inicie o servidor com o certificado e a chave gerados: + +```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) +} +``` + +Como alternativa, use um `http.Server` customizado com seu próprio `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 + +Inicie o servidor e acesse `https://localhost:1323/request`. Você deve ver +uma saída semelhante a: + +```sh +Protocol: HTTP/2.0 +Host: localhost:1323 +Remote Address: [::1]:60288 +Method: GET +Path: / +``` + +## Código-fonte + +```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/pt-br/cookbook/jsonp.md b/site/src/content/docs/pt-br/cookbook/jsonp.md new file mode 100644 index 00000000..1123a225 --- /dev/null +++ b/site/src/content/docs/pt-br/cookbook/jsonp.md @@ -0,0 +1,95 @@ +--- +title: JSONP +description: Sirva responses JSONP para requests entre domínios com Context#JSONP. +sidebar: + order: 13 +--- + +JSONP é uma técnica que permite chamadas de servidor entre domínios a partir do navegador. Echo +serve responses JSONP com `c.JSONP()`, que envolve o payload JSON em uma chamada para +a função de callback nomeada no 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/pt-br/cookbook/jwt.md b/site/src/content/docs/pt-br/cookbook/jwt.md new file mode 100644 index 00000000..f26536cf --- /dev/null +++ b/site/src/content/docs/pt-br/cookbook/jwt.md @@ -0,0 +1,251 @@ +--- +title: JWT +description: Autentique requests com JSON Web Tokens usando o middleware echo-jwt. +sidebar: + order: 11 +--- + +Esta receita demonstra autenticação JWT com Echo usando o middleware +[`echo-jwt`](https://github.com/labstack/echo-jwt): + +- Autenticação JWT usando o algoritmo HS256. +- O token é lido do header de request `Authorization`. + +Veja a página do [middleware JWT](/pt-br/middleware/jwt/) para opções completas de configuração. + +## Servidor + +### Usando claims customizadas + +Defina um tipo de claims que incorpora `jwt.RegisteredClaims`, então aponte o middleware +para ele com `NewClaimsFunc`. Dentro do handler restrito, recupere o token analisado +do contexto com o `echo.ContextGet` genérico. + +```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) + } +} +``` + +### Usando uma KeyFunc definida pelo usuário + +Quando tokens são assinados por um provedor de identidade externo, forneça uma `KeyFunc` que +resolva a chave de assinatura dinamicamente. Este exemplo valida tokens emitidos pelo +Google Sign-In buscando o conjunto de chaves públicas do 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 +Buscar o conjunto de chaves a cada request, como mostrado acima, é apenas para demonstração. +Em produção, faça cache do conjunto de chaves e atualize-o periodicamente. +::: + +## Cliente + +### Login + +Faça login com usuário e senha para recuperar um token. + +```sh +curl -X POST -d 'username=jon' -d 'password=shhh!' localhost:1323/login +``` + +Response: + +```js +{ + "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE0NjE5NTcxMzZ9.RB3arc4-OyzASAaUhC2W3ReWaXAt_z2Fd3BN4aWTgEY" +} +``` + +### Request + +Solicite um recurso restrito usando o token no 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/pt-br/cookbook/load-balancing.md b/site/src/content/docs/pt-br/cookbook/load-balancing.md new file mode 100644 index 00000000..52baa9d4 --- /dev/null +++ b/site/src/content/docs/pt-br/cookbook/load-balancing.md @@ -0,0 +1,117 @@ +--- +title: Load Balancing +description: Use Nginx como reverse proxy para balancear tráfego entre vários servidores Echo. +sidebar: + order: 20 +--- + +Esta receita demonstra como usar Nginx como um servidor reverse proxy para fazer load +balance de tráfego entre vários 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 + +Veja o [guia de instalação do Nginx](https://www.nginx.com/resources/wiki/start/topics/tutorials/install). + +### 2) Configurar Nginx + +Crie um arquivo `/etc/nginx/sites-enabled/localhost` com o seguinte conteúdo: + +```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 +Ajuste `listen`, `server_name` e `access_log` para combinar com seu ambiente. +::: + +### 3) Reiniciar Nginx + +```sh +service nginx restart +``` + +Acesse `https://localhost:8080`, e você deverá ver uma página servida a partir de +"server 1" ou "server 2". + +```sh +Hello from upstream server server1 +``` diff --git a/site/src/content/docs/pt-br/cookbook/middleware.md b/site/src/content/docs/pt-br/cookbook/middleware.md new file mode 100644 index 00000000..936ab352 --- /dev/null +++ b/site/src/content/docs/pt-br/cookbook/middleware.md @@ -0,0 +1,140 @@ +--- +title: Middleware customizado +description: Escreva middleware Echo customizado para coletar estatísticas de request e definir headers de response. +sidebar: + order: 12 +--- + +Esta receita mostra como escrever middleware customizado: + +- Um middleware que coleta a contagem de requests, status de response e uptime. +- Um middleware que escreve um header `Server` customizado em toda response. + +Um middleware no Echo é uma função com a assinatura +`func(next echo.HandlerFunc) echo.HandlerFunc`. O método `Stats.Process` abaixo +satisfaz essa assinatura diretamente, enquanto `ServerHeader` é uma função simples. + +## 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/pt-br/cookbook/reverse-proxy.md b/site/src/content/docs/pt-br/cookbook/reverse-proxy.md new file mode 100644 index 00000000..97ee9c3e --- /dev/null +++ b/site/src/content/docs/pt-br/cookbook/reverse-proxy.md @@ -0,0 +1,202 @@ +--- +title: Reverse Proxy +description: Use Echo como reverse proxy e load balancer na frente de aplicações upstream. +sidebar: + order: 19 +--- + +Esta receita demonstra como usar Echo como reverse proxy e load balancer na +frente das suas aplicações, como WordPress, Node.js, Java, Python, Ruby ou Go. +Por simplicidade, os upstreams aqui são servidores Go que também tratam 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 de proxy com destinos upstream + +O snippet abaixo usa load balancing round-robin. Você também pode usar +`middleware.NewRandomBalancer()`. + +```go +e.Use(middleware.Proxy(middleware.NewRoundRobinBalancer(targets))) +``` + +Para configurar um proxy para uma sub-rota, use `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 o servidor proxy + +```sh +go run server.go +``` + +Acesse `http://localhost:1323`, e você deverá ver uma página com um request HTTP +servido pelo "server 1" e um request WebSocket servido pelo "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-fonte + +### 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/pt-br/cookbook/sse.md b/site/src/content/docs/pt-br/cookbook/sse.md new file mode 100644 index 00000000..5c3416b8 --- /dev/null +++ b/site/src/content/docs/pt-br/cookbook/sse.md @@ -0,0 +1,305 @@ +--- +title: Server-Sent Events (SSE) +description: Faça streaming de server-sent events a partir de um handler Echo, por conexão ou em broadcast para muitos 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) +podem ser usados de várias formas. O primeiro exemplo abaixo é SSE por conexão e por handler. +Para lógica de broadcast mais complexa, veja o segundo exemplo usando +[r3labs/sse](https://github.com/r3labs/sse). + +:::caution +Conexões SSE são long-lived, então o write timeout do servidor precisa ser desabilitado. +Os dois exemplos definem `s.WriteTimeout = 0` via `BeforeServeFunc`. +::: + +## Usando SSE + +### Servidor + +O handler escreve os headers SSE e então emite um evento a cada segundo até que o +cliente desconecte. `http.NewResponseController(w).Flush()` envia cada evento ao +cliente imediatamente. + +```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) + } +} +``` + +### Estrutura Event e 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 servindo SSE + +```html + + + + +

Getting server-sent updates

+
+ + + + + +``` + +## Broadcast com r3labs/sse + +Quando você precisa transmitir um único stream de eventos para muitos subscribers, a +biblioteca [r3labs/sse](https://github.com/r3labs/sse) cuida do gerenciamento de stream e subscribers +para você. + +### 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 servindo SSE + +```html + + + + +

Getting server-sent updates

+
+ + + + + +``` diff --git a/site/src/content/docs/pt-br/cookbook/streaming-response.md b/site/src/content/docs/pt-br/cookbook/streaming-response.md new file mode 100644 index 00000000..0b5bd421 --- /dev/null +++ b/site/src/content/docs/pt-br/cookbook/streaming-response.md @@ -0,0 +1,95 @@ +--- +title: Streaming Response +description: Envie dados ao cliente conforme são produzidos usando chunked transfer encoding. +sidebar: + order: 15 +--- + +Esta receita envia uma response JSON ao cliente conforme cada registro é produzido, +usando chunked transfer encoding: + +- Envie dados conforme são produzidos. +- Faça streaming de uma response JSON com chunked transfer encoding. + +O handler codifica um registro por vez e chama +`http.NewResponseController(...).Flush()` depois de cada um para enviá-lo ao cliente +imediatamente, pausando um 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 +``` + +### Saída + +```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/pt-br/cookbook/subdomain.md b/site/src/content/docs/pt-br/cookbook/subdomain.md new file mode 100644 index 00000000..aa4439e8 --- /dev/null +++ b/site/src/content/docs/pt-br/cookbook/subdomain.md @@ -0,0 +1,78 @@ +--- +title: Subdomínio +description: Roteie requests para diferentes instâncias Echo por host usando um handler de virtual host. +sidebar: + order: 17 +--- + +Esta receita roteia requests para instâncias `Echo` separadas com base no host do request, +para que cada subdomínio tenha suas próprias rotas e middleware. As instâncias são +combinadas com `echo.NewVirtualHostHandler`, que despacha pelo nome do host. + +## 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/pt-br/cookbook/timeout.md b/site/src/content/docs/pt-br/cookbook/timeout.md new file mode 100644 index 00000000..e1c1b1c1 --- /dev/null +++ b/site/src/content/docs/pt-br/cookbook/timeout.md @@ -0,0 +1,53 @@ +--- +title: Timeout +description: Aplique um timeout de request aos handlers com o middleware ContextTimeout. +sidebar: + order: 18 +--- + +O middleware [`ContextTimeout`](/pt-br/middleware/context-timeout/) define um deadline no +`context.Context` do request. Quando o deadline passa, o contexto é cancelado, +e handlers que observam `c.Request().Context().Done()` podem retornar rapidamente em vez +de continuar até o fim. + +No exemplo abaixo, o middleware impõe um timeout de 5 segundos enquanto o handler +levaria 10 segundos; por isso, o request retorna `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/pt-br/cookbook/websocket.md b/site/src/content/docs/pt-br/cookbook/websocket.md new file mode 100644 index 00000000..a8359a20 --- /dev/null +++ b/site/src/content/docs/pt-br/cookbook/websocket.md @@ -0,0 +1,189 @@ +--- +title: WebSocket +description: Trate conexões WebSocket no Echo usando golang.org/x/net/websocket ou gorilla/websocket. +sidebar: + order: 16 +--- + +Handlers Echo podem servir conexões WebSocket fazendo upgrade da conexão +HTTP subjacente. Esta receita mostra duas abordagens: o pacote padrão +`golang.org/x/net/websocket` e a biblioteca popular +[`gorilla/websocket`](https://github.com/gorilla/websocket). + +## Usando 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) + } +} +``` + +## Usando 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 + + + +

+ + + + + +``` + +## Saída + +**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/pt-br/guide/binding.md b/site/src/content/docs/pt-br/guide/binding.md new file mode 100644 index 00000000..36db4b54 --- /dev/null +++ b/site/src/content/docs/pt-br/guide/binding.md @@ -0,0 +1,151 @@ +--- +title: Binding +description: Analise dados de request em structs Go tipadas a partir de path, query, header e body. +sidebar: + order: 5 +--- + +Analisar dados de request é uma parte crucial de uma aplicação web. No Echo isso se chama +_binding_, e ele pode ler de quatro partes de um request HTTP: + +- Parâmetros de caminho da URL +- Parâmetros de query da URL +- Headers +- Body do request + +## Binding com tags de struct + +Defina uma struct com tags que especificam a fonte e a chave dos dados, então chame `c.Bind()` +com um ponteiro para ela. Aqui o parâmetro de query `id` faz binding para o 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") +} +``` + +### Fontes de dados + +| Tag | Fonte | +| -------- | ----- | +| `query` | Parâmetro de query | +| `param` | Parâmetro de caminho | +| `header` | Valor de header | +| `form` | Dados de formulário (query + body) | +| `json` | Body do request (`encoding/json`) | +| `xml` | Body do request (`encoding/xml`) | + +Campos de path, query, header e form exigem uma **tag explícita**. JSON e XML usam +o nome do campo da struct quando a tag é omitida, igual à biblioteca padrão. + +### Tipos de conteúdo do body + +Ao decodificar o body do request, o header `Content-Type` seleciona o decoder: + +- `application/json` +- `application/xml` +- `application/x-www-form-urlencoded` + +### Múltiplas fontes e precedência + +Um campo pode declarar várias fontes. Os dados fazem binding nesta ordem, cada etapa +sobrescrevendo a anterior: + +1. Parâmetros de caminho +2. Parâmetros de query (somente GET / DELETE) +3. Body do request + +```go +type User struct { + ID string `param:"id" query:"id" form:"id" json:"id" xml:"id"` +} +``` + +### Binding direto de uma fonte + +```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 +Headers **não** são incluídos por `c.Bind()`. Faça binding deles diretamente com `echo.BindHeaders`. +::: + +:::caution[Segurança] +Não faça binding diretamente em structs de negócio. Se uma struct vinculada expuser um campo `IsAdmin bool`, +um body de request `{"IsAdmin": true}` o definiria. Use um DTO dedicado e faça o mapeamento +explicitamente: +::: + +```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 fluente + +Para binding explícito e type-safe de uma única fonte, use os binders fluentes. Eles +encadeiam configuração e execução, coletando erros: + +```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 disponíveis: `echo.QueryParamsBinder(c)`, `echo.PathValuesBinder(c)`, +`echo.FormFieldBinder(c)`. Termine uma cadeia com `BindError()` (primeiro erro) ou +`BindErrors()` (todos os erros). `FailFast(false)` executa a cadeia inteira; ele vem ativado por padrão. + +Cada tipo suportado oferece métodos `Type(...)`, `MustType(...)`, `Types(...)` (slices) e +`MustTypes(...)` — por exemplo, `Int64`, `MustInt64`, `Int64s`. Use +`BindWithDelimiter("id", &dest, ",")` para separar valores unidos por vírgula. + +## Binder customizado + +Registre um binder customizado via `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/pt-br/guide/context.md b/site/src/content/docs/pt-br/guide/context.md new file mode 100644 index 00000000..4027be0d --- /dev/null +++ b/site/src/content/docs/pt-br/guide/context.md @@ -0,0 +1,78 @@ +--- +title: Context +description: O objeto por request que carrega request, response, parâmetros e helpers. +sidebar: + order: 4 +--- + +`echo.Context` representa o contexto do request HTTP atual. Um ponteiro para ele +(`*echo.Context`) é passado para todo handler e middleware, carregando o request e a +response, parâmetros de caminho, dados vinculados e helpers para criar responses. + +```go +func handler(c *echo.Context) error { + // ... + return nil +} +``` + +## Ler 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) +``` + +Há helpers `*Or` correspondentes que retornam um padrão quando um valor está ausente — +`c.ParamOr("id", "0")`, `c.QueryParamOr("page", "1")`, `c.FormValueOr(...)`. + +## Escrever 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") +``` + +## Arquivos + +```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 +``` + +## Armazenamento por request + +Compartilhe dados entre middleware e handlers com `Get`/`Set`: + +```go +c.Set("user", u) +u, _ := c.Get("user").(*User) +``` + +Acesso tipado está disponível por meio dos helpers de generics: + +```go +u, err := echo.ContextGet[*User](c, "user") +``` + +## Binding e validação + +`c.Bind()` analisa dados do request em uma struct; veja [Binding](/pt-br/guide/binding/). + +```go +var dto CreateUser +if err := c.Bind(&dto); err != nil { + return echo.ErrBadRequest +} +``` diff --git a/site/src/content/docs/pt-br/guide/cookies.md b/site/src/content/docs/pt-br/guide/cookies.md new file mode 100644 index 00000000..d9309427 --- /dev/null +++ b/site/src/content/docs/pt-br/guide/cookies.md @@ -0,0 +1,72 @@ +--- +title: Cookies +description: Crie, leia e liste HTTP cookies usando o tipo padrão http.Cookie. +sidebar: + order: 11 +--- + +Um cookie é um pequeno pedaço de dados que um servidor envia ao navegador, que o navegador +armazena e envia de volta em requests subsequentes. Cookies permitem que sites lembrem +informações com estado, como carrinho de compras, estado de autenticação ou valores de formulário +inseridos anteriormente. + +Echo usa o tipo padrão `http.Cookie` do Go para adicionar e recuperar cookies do +`echo.Context` em um handler. + +## Atributos de cookie + +| Atributo | Opcional | +| ---------- | -------- | +| `Name` | Não | +| `Value` | Não | +| `Path` | Sim | +| `Domain` | Sim | +| `Expires` | Sim | +| `Secure` | Sim | +| `HttpOnly` | Sim | + +## Criar um 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") +} +``` + +- Crie o cookie com `new(http.Cookie)`. +- Defina atributos nos campos de `http.Cookie`. +- Chame `c.SetCookie(cookie)` para adicionar um header `Set-Cookie` à response. + +## Ler um 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") +} +``` + +- Leia um cookie por nome com `c.Cookie("username")`. +- Acesse seus atributos por meio dos campos de `http.Cookie`. + +## Ler todos os 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/pt-br/guide/customization.md b/site/src/content/docs/pt-br/guide/customization.md new file mode 100644 index 00000000..5503b808 --- /dev/null +++ b/site/src/content/docs/pt-br/guide/customization.md @@ -0,0 +1,63 @@ +--- +title: Customização +description: Customize logger, validator, binder, renderer, serializer e tratamento de erros do Echo. +sidebar: + order: 12 +--- + +Echo expõe um conjunto de campos na instância `Echo` que permite substituir o comportamento +embutido pelas suas próprias implementações. + +## Logging + +`Echo#Logger` escreve logs estruturados. O handler padrão emite JSON para `os.Stdout`. + +### Logger customizado + +O logger é um `*slog.Logger`, então você pode registrar qualquer handler `slog`: + +```go +e.Logger = slog.New(slog.NewJSONHandler(os.Stdout, nil)) +``` + +## Validator + +`Echo#Validator` registra um validador para validação de payloads de request. + +[Saiba mais](/pt-br/guide/request/#validate-data) + +## Binder customizado + +`Echo#Binder` registra um binder customizado para binding de payloads de request. + +[Saiba mais](/pt-br/guide/binding/#custom-binder) + +## Serializer JSON customizado + +`Echo#JSONSerializer` registra um serializer JSON customizado. Veja `DefaultJSONSerializer` +em [json.go](https://github.com/labstack/echo/blob/master/json.go). + +## Renderer + +`Echo#Renderer` registra um renderer para renderização de templates. + +[Saiba mais](/pt-br/guide/templates/) + +## Handler de erro HTTP + +`Echo#HTTPErrorHandler` registra um handler de erro HTTP customizado. + +[Saiba mais](/pt-br/guide/error-handling/) + +## Callback de rota + +`Echo#OnAddRoute` registra um callback chamado sempre que uma nova rota é adicionada ao +router. + +## Extrator de IP + +`Echo#IPExtractor` controla como o endereço IP real do cliente é determinado. Para +recuperá-lo de forma confiável e segura, sua aplicação precisa conhecer toda a sua +infraestrutura. + +[Saiba mais](/pt-br/guide/ip-address/) diff --git a/site/src/content/docs/pt-br/guide/error-handling.md b/site/src/content/docs/pt-br/guide/error-handling.md new file mode 100644 index 00000000..c4b2fefb --- /dev/null +++ b/site/src/content/docs/pt-br/guide/error-handling.md @@ -0,0 +1,82 @@ +--- +title: Tratamento de erros +description: Tratamento centralizado de erros HTTP retornando erros de handlers e middleware. +sidebar: + order: 6 +--- + +Echo defende o tratamento de erros **centralizado**: handlers e middleware retornam um +`error`, e um único handler de erros o transforma em uma response HTTP. Isso mantém logs +e formatação de response em um só lugar. + +Retorne um `error` simples ou um `*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)` sem mensagem usa o texto do status (por exemplo, `"Unauthorized"`). +Echo também fornece erros sentinela como `echo.ErrBadRequest`, `echo.ErrNotFound` e +`echo.ErrUnauthorized`. + +## Handler de erro padrão + +O handler padrão do Echo responde em JSON: + +```json +{ "message": "error connecting to redis" } +``` + +Um `error` simples vira `500 Internal Server Error` (a mensagem original é incluída +quando a aplicação roda com erros expostos). Um `*HTTPError` usa seu código de status e mensagem. + +## Handler de erro customizado + +Defina o seu via `e.HTTPErrorHandler` — útil para páginas de erro, notificações ou +envio de erros para um sistema centralizado. + +Verifique se a response já foi enviada com `echo.UnwrapResponse()`, e encontre um +código de status na cadeia de erros via `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 +Em vez de (ou além de) usar o logger, encaminhe erros para um serviço externo como +Sentry, Elasticsearch ou Splunk a partir do handler central. +::: diff --git a/site/src/content/docs/pt-br/guide/installation.md b/site/src/content/docs/pt-br/guide/installation.md new file mode 100644 index 00000000..a84e4fae --- /dev/null +++ b/site/src/content/docs/pt-br/guide/installation.md @@ -0,0 +1,57 @@ +--- +title: Instalação +description: Adicione o Echo v5 ao seu módulo Go. +sidebar: + order: 2 +--- + +O Echo é distribuído como um módulo Go: `github.com/labstack/echo/v5`. + +## Requisitos + +O Echo v5 requer **Go 1.25 ou mais recente**. + +```bash +go version +``` + +## Adicionar a um projeto + +Dentro de um módulo existente: + +```bash +go get github.com/labstack/echo/v5 +``` + +Ou inicie um novo módulo: + +```bash +mkdir myapp && cd myapp +go mod init myapp +go get github.com/labstack/echo/v5 +``` + +Importe-o no seu código: + +```go +import "github.com/labstack/echo/v5" +``` + +## Versões + +| Versão | Caminho de importação | Status | +| ------ | ------------------------------ | ------ | +| **v5** | `github.com/labstack/echo/v5` | Atual | +| v4 | `github.com/labstack/echo/v4` | LTS (manutenção) | + +:::note +O Echo segue [semantic import versioning](https://go.dev/blog/v2-go-modules) — a +versão principal faz parte do caminho de importação, então v4 e v5 podem coexistir durante uma migração. +::: + +## Manter atualizado + +```bash +go get github.com/labstack/echo/v5 +go mod tidy +``` diff --git a/site/src/content/docs/pt-br/guide/ip-address.md b/site/src/content/docs/pt-br/guide/ip-address.md new file mode 100644 index 00000000..76c18740 --- /dev/null +++ b/site/src/content/docs/pt-br/guide/ip-address.md @@ -0,0 +1,117 @@ +--- +title: Endereço IP +description: Recupere o endereço IP real do cliente com segurança atrás de proxies. +sidebar: + order: 14 +--- + +O endereço IP tem um papel fundamental em HTTP — ele é usado para controle de acesso, +auditoria, análise baseada em geografia e mais. Echo expõe `Context#RealIP()` para recuperá-lo. + +Recuperar o IP _real_ do cliente não é trivial, especialmente quando proxies L7 ficam na +frente da sua aplicação. Nesse caso, o IP real precisa ser repassado por HTTP dos +proxies para sua app — mas você não deve confiar incondicionalmente em headers HTTP, ou +corre o risco de ser enganado. **Isso é um risco de segurança.** + +Para recuperar o IP de forma confiável e segura, sua aplicação precisa conhecer toda a sua +infraestrutura. No Echo, você configura isso por meio de `Echo#IPExtractor`. + +:::caution +Se você não definir `Echo#IPExtractor` explicitamente, Echo recorre ao comportamento legado, +que não é um padrão seguro. +::: + +Comece com duas perguntas para encontrar a abordagem correta: + +1. Você coloca algum proxy HTTP (L7) na frente da aplicação? Isso inclui load balancers + de nuvem (como AWS ALB ou GCP HTTP LB) e proxies open-source (como Nginx, Envoy ou + um gateway de ingresso Istio). +2. Se sim, qual header HTTP seus proxies usam para passar o IP do cliente para a + aplicação? + +## Caso 1: Sem proxy + +Se não há proxy (a app encara a internet diretamente), o único endereço em que você pode +confiar é o da camada de rede. Todo header HTTP não é confiável porque +clientes têm controle total sobre eles. + +Use `echo.ExtractIPDirect()`: + +```go +e.IPExtractor = echo.ExtractIPDirect() +``` + +## Caso 2: Proxies usando o header X-Forwarded-For + +[`X-Forwarded-For` (XFF)](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-For) +é o header mais comum para retransmitir IPs de clientes. A cada salto, o proxy acrescenta o +IP do request ao final do 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 +``` + +Nesse caso, pegue a **primeira leitura de IP não confiável a partir da direita**. Nunca pegue +a primeira da esquerda, pois o cliente a controla. Aqui "confiável" significa +que você tem certeza de que o IP pertence à sua infraestrutura. No exemplo acima, se `b` e +`c` são confiáveis, o IP do cliente é `a` nos dois casos — nunca `x`. + +Use `ExtractIPFromXFFHeader(...TrustOption)`: + +```go +e.IPExtractor = echo.ExtractIPFromXFFHeader() +``` + +Por padrão, ele confia em endereços IP internos — loopback, link-local unicast, +private-use e unique local addresses de +[RFC 6890](https://datatracker.ietf.org/doc/html/rfc6890), +[RFC 4291](https://datatracker.ietf.org/doc/html/rfc4291) e +[RFC 4193](https://datatracker.ietf.org/doc/html/rfc4193). Controle isso com `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 usando o header X-Real-IP + +`X-Real-IP` é outro header para retransmitir o IP do cliente, mas diferentemente de XFF ele carrega +apenas um único endereço. + +Se seus proxies definem este header, use `ExtractIPFromRealIPHeader(...TrustOption)`: + +```go +e.IPExtractor = echo.ExtractIPFromRealIPHeader() +``` + +Assim como com XFF, ele confia em endereços IP internos por padrão e aceita as mesmas +`TrustOption`s. + +:::danger +**Nunca esqueça** de configurar o proxy mais externo (na borda da sua +infraestrutura) para **não repassar headers recebidos**. Caso contrário, um cliente pode +forjá-los, abrindo espaço para fraude. +::: + +## Comportamento padrão + +Por padrão, Echo considera ao mesmo tempo o primeiro header XFF, o header X-Real-IP e o IP +da camada de rede. + +Como este artigo deve deixar claro, essa não é uma boa escolha. Ela continua sendo o padrão +apenas por compatibilidade retroativa. diff --git a/site/src/content/docs/pt-br/guide/quickstart.md b/site/src/content/docs/pt-br/guide/quickstart.md new file mode 100644 index 00000000..feef7f1d --- /dev/null +++ b/site/src/content/docs/pt-br/guide/quickstart.md @@ -0,0 +1,76 @@ +--- +title: Início rápido +description: Crie uma API Echo pronta para produção em menos de cinco minutos. +sidebar: + order: 1 +--- + +Echo é um framework web Go minimalista e de alta performance. Este guia coloca um +servidor em execução em menos de cinco minutos. + +## Requisitos + +Echo requer **Go 1.25 ou mais recente**. Verifique sua versão: + +```bash +go version +``` + +## Instalar + +Crie um módulo e adicione o Echo: + +```bash +go mod init myapp +go get github.com/labstack/echo/v5 +``` + +## Hello, World + +Crie `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) + } +} +``` + +Execute: + +```bash +go run main.go +``` + +Seu servidor está ativo em `http://localhost:1323`. O router do Echo despacha requests +com **zero alocação dinâmica de memória** por rota. + +:::tip[Ask Echo] +Travou? Pressione o botão **Ask Echo** (canto inferior direito) e pergunte +*"How do I add JWT auth?"* — as respostas vêm diretamente destes docs. +::: + +## Próximos passos + +- [Routing](/pt-br/guide/routing/) — rotas estáticas, parametrizadas e com wildcard. +- [Context](/pt-br/guide/context/) — o objeto request/response por request. +- [Binding](/pt-br/guide/binding/) — analise dados do request em structs tipadas. diff --git a/site/src/content/docs/pt-br/guide/request.md b/site/src/content/docs/pt-br/guide/request.md new file mode 100644 index 00000000..05a9c27b --- /dev/null +++ b/site/src/content/docs/pt-br/guide/request.md @@ -0,0 +1,170 @@ +--- +title: Request +description: Recupere dados de formulário, query e path de um request, e valide-os. +sidebar: + order: 7 +--- + +Um handler lê dados do request por meio do `echo.Context`. Echo pode recuperar valores +individualmente por nome, fazer binding deles em structs (veja [Binding](/pt-br/guide/binding/)) e +delegar a validação para um validador que você registra. + +## Recuperar dados + +### Dados de formulário + +Recupere um campo de formulário por nome com `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 diferentes de `string`, use a função genérica `echo.FormValue[T]`: + +```go +age, err := echo.FormValue[int](c, "age") +if err != nil { + return err +} +``` + +Teste com: + +```sh +curl -X POST http://localhost:1323/form -d 'name=Joe&age=30' +``` + +Para fazer binding de um tipo de dado customizado, implemente a interface `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 + +Recupere um parâmetro de query por nome com `Context#QueryParam(name string)`: + +```go +func(c *echo.Context) error { + name := c.QueryParam("name") + return c.String(http.StatusOK, name) +} +``` + +Para tipos diferentes de `string`, use a função 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 caminho + +Recupere um parâmetro de caminho registrado por nome com `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 diferentes de `string`, use a função 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 dados + +Echo também pode fazer binding de dados do request em structs e variáveis nativas do Go. Veja +[Binding](/pt-br/guide/binding/). + +## Validar dados + +Echo não tem validação de dados embutida. Você pode registrar um validador customizado via +`Echo#Validator` e usar uma biblioteca de terceiros como +[go-playground/validator](https://github.com/go-playground/validator). + +O exemplo abaixo valida uma struct vinculada: + +```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/pt-br/guide/response.md b/site/src/content/docs/pt-br/guide/response.md new file mode 100644 index 00000000..eaeac053 --- /dev/null +++ b/site/src/content/docs/pt-br/guide/response.md @@ -0,0 +1,322 @@ +--- +title: Response +description: Envie strings, HTML, JSON, XML, arquivos, streams, redirects e hooks de response. +sidebar: + order: 8 +--- + +Um handler escreve sua response por meio do `echo.Context`. Cada helper define o +`Content-Type` e o código de status apropriados para você. + +## Enviar string + +`Context#String(code int, s string)` envia uma response de texto simples com um código de status. + +```go +func(c *echo.Context) error { + return c.String(http.StatusOK, "Hello, World!") +} +``` + +## Enviar HTML + +`Context#HTML(code int, html string)` envia uma response HTML simples com um código de +status. Para gerar HTML dinamicamente, veja [Templates](/pt-br/guide/templates/). + +```go +func(c *echo.Context) error { + return c.HTML(http.StatusOK, "Hello, World!") +} +``` + +### Enviar blob HTML + +`Context#HTMLBlob(code int, b []byte)` envia um blob HTML com um código de status. Ele é +útil com um mecanismo de templates que produz `[]byte`. + +```go +func handler(c *echo.Context) error { + blob := []byte("Hello, World!") + return c.HTMLBlob(http.StatusOK, blob) +} +``` + +## Renderizar template + +Veja [Templates](/pt-br/guide/templates/). + +## Enviar JSON + +`Context#JSON(code int, i any)` codifica um valor Go como JSON e o envia com um +código de status. + +```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, o que pode ser ineficiente para payloads grandes. +Nesse caso, envie o JSON diretamente como stream: + +```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)` envia uma response JSON formatada. +O indent pode ser espaços ou 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)` envia um blob JSON pré-codificado diretamente, por +exemplo vindo de um banco de dados. + +```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 um valor Go como JSON e +o envia como um payload JSONP envolvido no callback informado. + +```go +func handler(c *echo.Context) error { + callback := c.QueryParam("callback") + return c.JSONP(http.StatusOK, callback, &User{Name: "Jon", Email: "jon@labstack.com"}) +} +``` + +Veja a [receita de JSONP](/pt-br/cookbook/jsonp/). + +## Enviar XML + +`Context#XML(code int, i any)` codifica um valor Go como XML e o envia com um código +de status. + +```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, o que pode ser ineficiente para payloads grandes. +Nesse caso, envie o XML diretamente como stream: + +```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)` envia uma response XML formatada. +O indent pode ser espaços ou 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 +Você também pode fazer `Context#XML()` gerar XML formatado adicionando `pretty` +à query string da URL do request. + +```sh +curl http://localhost:1323/users/1?pretty +``` +::: + +### Blob XML + +`Context#XMLBlob(code int, b []byte)` envia um blob XML pré-codificado diretamente, por +exemplo vindo de um banco de dados. + +```go +func(c *echo.Context) error { + encodedXML := []byte{} // Encoded XML from an external source. + return c.XMLBlob(http.StatusOK, encodedXML) +} +``` + +## Enviar arquivo + +`Context#File(file string)` envia o conteúdo de um arquivo como a response. Ele define +o tipo de conteúdo correto e lida com cache automaticamente. + +```go +func(c *echo.Context) error { + return c.File("") +} +``` + +## Enviar attachment + +`Context#Attachment(file, name string)` é como `File()`, mas envia o arquivo com +`Content-Disposition: attachment` e o nome informado. + +```go +func(c *echo.Context) error { + return c.Attachment("", "") +} +``` + +## Enviar inline + +`Context#Inline(file, name string)` é como `File()`, mas envia o arquivo com +`Content-Disposition: inline` e o nome informado. + +```go +func(c *echo.Context) error { + return c.Inline("", "") +} +``` + +## Enviar blob + +`Context#Blob(code int, contentType string, b []byte)` envia dados arbitrários com um +tipo de conteúdo e código de status informados. + +```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)` envia um stream de dados +arbitrário com um tipo de conteúdo, `io.Reader` e código de status informados. + +```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 sem conteúdo + +`Context#NoContent(code int)` envia um body vazio com um código de status. + +```go +func(c *echo.Context) error { + return c.NoContent(http.StatusOK) +} +``` + +## Redirecionar request + +`Context#Redirect(code int, url string)` redireciona o request para a URL informada com +um código de status. + +```go +func(c *echo.Context) error { + return c.Redirect(http.StatusMovedPermanently, "") +} +``` + +## Hooks + +### Antes da response + +`Response#Before(func())` registra uma função que roda pouco antes de a response ser +escrita. + +### Depois da response + +`Response#After(func())` registra uma função que roda logo depois de a response ser +escrita. Se o `Content-Length` for desconhecido, nenhuma função after roda. + +```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 +Você pode registrar várias funções `Before` e `After`. +::: diff --git a/site/src/content/docs/pt-br/guide/routing.md b/site/src/content/docs/pt-br/guide/routing.md new file mode 100644 index 00000000..2816286d --- /dev/null +++ b/site/src/content/docs/pt-br/guide/routing.md @@ -0,0 +1,75 @@ +--- +title: Routing +description: Associe URLs de request a handlers na árvore radix de alocação zero do Echo. +sidebar: + order: 3 +--- + +O router otimizado do Echo associa URLs de request a handlers usando uma árvore radix com +**zero alocação dinâmica de memória** e priorização inteligente de rotas. + +## Registrar rotas + +Use os helpers de método HTTP na instância `Echo`. Cada um recebe um padrão de caminho e um +`HandlerFunc` (`func(c *echo.Context) error`), com middleware opcional no nível da rota. + +```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 um handler para todos os métodos suportados, e `Match` para um conjunto específico: + +```go +e.Any("/ping", pong) +e.Match([]string{http.MethodGet, http.MethodPost}, "/form", handleForm) +``` + +## Tipos de correspondência + +| Padrão | Tipo | Exemplo correspondente | +| ----------------- | -------- | -------------------------- | +| `/users/profile` | Static | `/users/profile` | +| `/users/:id` | Param | `/users/42` | +| `/static/*` | Wildcard | `/static/css/app.css` | + +:::note +A prioridade é **static → param → wildcard**, então `/users/profile` sempre vence +`/users/:id`, que vence `/users/*`. +::: + +## Parâmetros de caminho + +Leia parâmetros nomeados do contexto com `c.Param()` (ou `c.ParamOr()` para um valor padrão): + +```go +func getUser(c *echo.Context) error { + id := c.Param("id") + return c.String(http.StatusOK, id) +} +``` + +O segmento wildcard fica disponível como o parâmetro `*`: + +```go +e.GET("/files/*", func(c *echo.Context) error { + return c.String(http.StatusOK, c.Param("*")) +}) +``` + +## Grupos + +Agrupe rotas que compartilham prefixo e middleware com `e.Group()`: + +```go +admin := e.Group("/admin", middleware.BasicAuth(authFn)) +admin.GET("/metrics", metrics) // -> /admin/metrics +admin.GET("/users", listUsers) // -> /admin/users +``` + +Grupos podem ser aninhados para compor árvores de rotas maiores. diff --git a/site/src/content/docs/pt-br/guide/static-files.md b/site/src/content/docs/pt-br/guide/static-files.md new file mode 100644 index 00000000..434a3483 --- /dev/null +++ b/site/src/content/docs/pt-br/guide/static-files.md @@ -0,0 +1,89 @@ +--- +title: Servindo arquivos estáticos +description: Sirva imagens, JavaScript, CSS, fontes e outros assets com Echo. +sidebar: + order: 9 +--- + +Echo pode servir assets estáticos como imagens, JavaScript, CSS, PDFs e fontes a partir +do filesystem ou de um filesystem incorporado. + +## Filesystem padrão + +Echo usa `os.DirFS(".")` como seu filesystem padrão, com raiz no diretório de trabalho +atual. Para alterá-lo, defina o campo `Echo#Filesystem`: + +```go +e := echo.New() +e.Filesystem = os.DirFS("assets") +``` + +## Usando o middleware Static + +Veja [middleware Static](/pt-br/middleware/static/). + +## Usando Echo#Static() + +`Echo#Static(prefix, root string)` registra uma rota que serve arquivos estáticos sob +um prefixo de caminho a partir do diretório raiz informado. + +Sirva qualquer arquivo de `assets` sob `/static/*`. Um request para `/static/js/main.js` +serve `assets/js/main.js`: + +```go +e := echo.New() +e.Static("/static", "assets") +``` + +Sirva qualquer arquivo de `assets` sob `/*`. Um request para `/js/main.js` serve +`assets/js/main.js`: + +```go +e := echo.New() +e.Static("/", "assets") +``` + +## Usando Echo#StaticFS() + +Arquivos estáticos podem ser servidos a partir de qualquer `fs.FS`, incluindo um `embed.FS`. Use +`echo.MustSubFS` para que os arquivos servidos tenham raiz no subdiretório correto — um +`embed.FS` inclui seus subdiretórios como entradas próprias. + +```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) + } +} +``` + +## Usando Echo#File() + +`Echo#File(path, file string)` registra uma rota que serve um único arquivo estático. + +Sirva uma página index de `public/index.html`: + +```go +e.File("/", "public/index.html") +``` + +Sirva um favicon de `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 +Um `/` inicial no caminho do arquivo não funciona com a maioria das implementações de `fs.FS`. Use +um caminho relativo. +::: diff --git a/site/src/content/docs/pt-br/guide/templates.md b/site/src/content/docs/pt-br/guide/templates.md new file mode 100644 index 00000000..f8618c85 --- /dev/null +++ b/site/src/content/docs/pt-br/guide/templates.md @@ -0,0 +1,133 @@ +--- +title: Templates +description: Renderize templates HTML com qualquer engine registrando um renderer. +sidebar: + order: 10 +--- + +`Context#Render(code int, name string, data any) error` renderiza um template com dados +e envia uma response `text/html` com um código de status. Registre um renderer definindo +`Echo#Renderer`, o que permite usar qualquer engine de templates. + +## Renderização + +O exemplo abaixo usa `html/template` do Go. + +Use o renderer de templates padrão: + +```go +e.Renderer = &echo.TemplateRenderer{ + Template: template.Must(template.New("hello").Parse("Hello, {{.}}!")), +} +``` + +Ou implemente você mesmo a interface `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. Pré-compile os templates. + + `public/views/hello.html`: + + ```html + {{define "hello"}}Hello, {{.}}!{{end}} + ``` + + ```go + t := &Template{ + templates: template.Must(template.ParseGlob("public/views/*.html")), + } + ``` + +2. Registre o renderer. + + ```go + e := echo.New() + e.Renderer = t + e.GET("/hello", Hello) + ``` + +3. Renderize um template dentro do handler. + + ```go + func Hello(c *echo.Context) error { + return c.Render(http.StatusOK, "hello", "World") + } + ``` + +## Avançado: chamando Echo a partir de templates + +Às vezes é útil gerar URIs de dentro de um template chamando +`Echo#Reverse`. `html/template` do Go não é ideal para isso, mas é possível +fazer de duas formas: fornecendo um método comum em cada objeto passado para templates, +ou passando um `map[string]any` e ampliando-o no renderer customizado. A +segunda opção é mais flexível. Aqui está um exemplo 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/pt-br/guide/testing.md b/site/src/content/docs/pt-br/guide/testing.md new file mode 100644 index 00000000..e08bcfce --- /dev/null +++ b/site/src/content/docs/pt-br/guide/testing.md @@ -0,0 +1,264 @@ +--- +title: Testes +description: Teste handlers e middleware com httptest e os helpers echotest. +sidebar: + order: 13 +--- + +Handlers e middleware do Echo são funções simples sobre um `echo.Context`, então são +diretos de testar com o pacote padrão `net/http/httptest`. O pacote +`echotest` fornece helpers que reduzem boilerplate. + +## Testando um handler + +Considere dois handlers: + +**CreateUser** — `POST /users` + +- Aceita um payload JSON. +- Retorna `201 Created` em caso de sucesso. +- Retorna `500 Internal Server Error` em caso de erro. + +**GetUser** — `GET /users/:email` + +- Retorna `200 OK` em caso de sucesso. +- Retorna `404 Not Found` se o usuário não existir, caso contrário `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()) + } +} +``` + +### Usando os helpers echotest + +`echotest.ContextConfig` constrói um contexto (e recorder) a partir de uma descrição +declarativa do 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()) + } +} +``` + +### Usando um payload de formulário + +```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) +``` + +Um payload de formulário multipart com `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) +} +``` + +### Definindo parâmetros de caminho + +```go +c.SetPathValues(echo.PathValues{ + {Name: "id", Value: "1"}, + {Name: "email", Value: "jon@labstack.com"}, +}) +``` + +### Definindo 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) +``` + +## Testando 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 mais exemplos, veja os [casos de teste de middleware](https://github.com/labstack/echo/tree/master/middleware) +no código-fonte do Echo. +::: diff --git a/site/src/content/docs/pt-br/middleware/basic-auth.md b/site/src/content/docs/pt-br/middleware/basic-auth.md new file mode 100644 index 00000000..83d8a7a4 --- /dev/null +++ b/site/src/content/docs/pt-br/middleware/basic-auth.md @@ -0,0 +1,74 @@ +--- +title: Basic Auth +description: Middleware de autenticação HTTP Basic que valida credenciais de usuário e senha. +sidebar: + order: 1 +--- + +O middleware Basic Auth fornece autenticação básica HTTP. + +- Para credenciais válidas, ele chama o próximo handler. +- Para credenciais ausentes ou inválidas, ele envia uma 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 +})) +``` + +## Configuração customizada + +```go +e.Use(middleware.BasicAuthWithConfig(middleware.BasicAuthConfig{})) +``` + +## Configuração + +```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 +} +``` + +O `Validator` tem a assinatura: + +```go +type BasicAuthValidator func(c *echo.Context, user string, password string) (bool, error) +``` + +### Configuração padrão + +```go +// Effective defaults applied when fields are left unset. +BasicAuthConfig{ + Skipper: DefaultSkipper, + Realm: "Restricted", +} +``` + +:::caution[Segurança] +Sempre compare credenciais com `subtle.ConstantTimeCompare` para evitar timing attacks. +::: diff --git a/site/src/content/docs/pt-br/middleware/body-dump.md b/site/src/content/docs/pt-br/middleware/body-dump.md new file mode 100644 index 00000000..58eb3a5e --- /dev/null +++ b/site/src/content/docs/pt-br/middleware/body-dump.md @@ -0,0 +1,72 @@ +--- +title: Body Dump +description: Capture payloads de request e response e passe-os para um handler para logging ou debugging. +sidebar: + order: 2 +--- + +O middleware Body Dump captura os payloads de request e response e os passa para um +handler registrado. Em geral, ele é usado para debugging ou logging. + +:::caution +Evite Body Dump para payloads grandes, como uploads ou downloads de arquivos. Se precisar usá-lo +nessas rotas, adicione uma exceção na função 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. +})) +``` + +## Configuração customizada + +```go +e := echo.New() +e.Use(middleware.BodyDumpWithConfig(middleware.BodyDumpConfig{})) +``` + +## Configuração + +```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 +} +``` + +O `Handler` tem a assinatura: + +```go +type BodyDumpHandler func(c *echo.Context, reqBody []byte, resBody []byte, err error) +``` + +### Configuração padrão + +```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/pt-br/middleware/body-limit.md b/site/src/content/docs/pt-br/middleware/body-limit.md new file mode 100644 index 00000000..e78c533a --- /dev/null +++ b/site/src/content/docs/pt-br/middleware/body-limit.md @@ -0,0 +1,48 @@ +--- +title: Body Limit +description: Rejeite requests cujo body exceda um tamanho máximo configurado. +sidebar: + order: 3 +--- + +O middleware Body Limit define o tamanho máximo permitido para o body de um request. Se o tamanho +exceder o limite configurado, ele envia uma response `413 Request Entity Too Large`. + +O limite é aplicado tanto ao header de request `Content-Length` quanto ao conteúdo real +lido, o que o torna resistente a headers falsificados. O limite é especificado +em bytes. + +## Uso + +```go +e := echo.New() +e.Use(middleware.BodyLimit(2_097_152)) // 2 MB +``` + +## Configuração customizada + +```go +e := echo.New() +e.Use(middleware.BodyLimitWithConfig(middleware.BodyLimitConfig{})) +``` + +## Configuração + +```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 +} +``` + +### Configuração padrão + +```go +// Effective defaults applied when fields are left unset (Limit is required). +BodyLimitConfig{ + Skipper: DefaultSkipper, +} +``` diff --git a/site/src/content/docs/pt-br/middleware/casbin-auth.md b/site/src/content/docs/pt-br/middleware/casbin-auth.md new file mode 100644 index 00000000..4d7ae7a0 --- /dev/null +++ b/site/src/content/docs/pt-br/middleware/casbin-auth.md @@ -0,0 +1,203 @@ +--- +title: Casbin Auth +description: Autorize requests com a biblioteca de controle de acesso Casbin usando um pequeno middleware customizado. +sidebar: + order: 4 +--- + +[Casbin](https://github.com/casbin/casbin) é uma biblioteca de controle de acesso open-source +poderosa e eficiente para Go. Ela oferece suporte à aplicação de autorização em muitos modelos: + +- ACL (Access Control List) +- ACL com superusuário +- ACL sem usuários — útil para sistemas sem autenticação ou logins de usuário +- ACL sem recursos — mire um tipo de recurso (por exemplo `write-article`, `read-log`) em vez de um individual +- RBAC (Role-Based Access Control) +- RBAC com roles de recurso — usuários e recursos podem ter roles +- RBAC com domínios/tenants — usuários podem ter conjuntos de roles diferentes por domínio/tenant +- ABAC (Attribute-Based Access Control) +- RESTful +- Deny-override — regras de allow e deny têm suporte; deny sobrescreve allow + +Veja a [visão geral da API](https://casbin.org/docs/api-overview) e a +[documentação do Casbin](https://casbin.org/docs/) para detalhes. + +## Dependências + +```bash +go get github.com/casbin/casbin/v3 +``` + +```go +import ( + "github.com/casbin/casbin/v3" +) +``` + +## Implementação + +Echo não inclui um middleware Casbin; a integração é um pequeno wrapper em torno do +enforcer do 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) + } + } +} +``` + +## Exemplo + +Crie um arquivo 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 == "*") +``` + +Crie um arquivo de política 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 +``` + +Autenticação e autorização são preocupações separadas. Autentique o usuário com +outro middleware (como JWT ou Basic Auth) e então forneça um `userGetter` para que Casbin +possa autorizar o request. + +### Com 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 +``` + +Teste com: + +```bash +curl -v "http://localhost:8080/dataset1/any" -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ" +``` + +### Com 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 +``` + +Teste com: + +```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 +``` + +### Exemplo 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/pt-br/middleware/context-timeout.md b/site/src/content/docs/pt-br/middleware/context-timeout.md new file mode 100644 index 00000000..67c87c78 --- /dev/null +++ b/site/src/content/docs/pt-br/middleware/context-timeout.md @@ -0,0 +1,47 @@ +--- +title: Context Timeout +description: Aplique timeout ao contexto do request para que operações context-aware retornem mais cedo. +sidebar: + order: 5 +--- + +O middleware Context Timeout aplica um timeout ao contexto do request dentro de um período +predefinido, para que métodos context-aware possam retornar mais cedo quando o prazo for excedido. + +## Uso + +```go +e.Use(middleware.ContextTimeout(60 * time.Second)) +``` + +## Configuração customizada + +```go +e.Use(middleware.ContextTimeoutWithConfig(middleware.ContextTimeoutConfig{ + Timeout: 60 * time.Second, +})) +``` + +## Configuração + +```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 +} +``` + +### Configuração padrão + +```go +// Effective defaults applied when fields are left unset (Timeout is required). +ContextTimeoutConfig{ + Skipper: DefaultSkipper, +} +``` diff --git a/site/src/content/docs/pt-br/middleware/cors.md b/site/src/content/docs/pt-br/middleware/cors.md new file mode 100644 index 00000000..4ecf8ae5 --- /dev/null +++ b/site/src/content/docs/pt-br/middleware/cors.md @@ -0,0 +1,118 @@ +--- +title: CORS +description: Middleware Cross-Origin Resource Sharing para controle seguro de acesso entre domínios. +sidebar: + order: 6 +--- + +O middleware CORS implementa a especificação [CORS](https://fetch.spec.whatwg.org/#http-cors-protocol). CORS fornece +controles de acesso entre domínios para servidores web, permitindo transferências de dados seguras entre domínios. + +## Uso + +```go +e.Use(middleware.CORS("https://example.com", "https://subdomain.example.com")) +``` + +## Configuração customizada + +```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}, +})) +``` + +## Configuração + +```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 +} +``` + +### Configuração padrão + +```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[Segurança] +Nunca combine `AllowCredentials = true` com um wildcard `AllowOrigins`. Quando precisar +de validação dinâmica de origem, use `UnsafeAllowOriginFunc` e valide cuidadosamente — +atacantes podem registrar nomes de (sub)domínio hostis. +::: diff --git a/site/src/content/docs/pt-br/middleware/csrf.md b/site/src/content/docs/pt-br/middleware/csrf.md new file mode 100644 index 00000000..1ca56e32 --- /dev/null +++ b/site/src/content/docs/pt-br/middleware/csrf.md @@ -0,0 +1,177 @@ +--- +title: CSRF +description: Proteção Cross-Site Request Forgery usando metadados Sec-Fetch-Site e validação de token. +sidebar: + order: 7 +--- + +Cross-Site Request Forgery (CSRF, às vezes pronunciado "sea-surf", ou XSRF) é um tipo de +exploit malicioso em que comandos não autorizados são transmitidos a partir de um usuário em que um site +confia. + +## Uso + +```go +e.Use(middleware.CSRF()) +``` + +## Como funciona + +O middleware CSRF oferece suporte ao header +[`Sec-Fetch-Site`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Sec-Fetch-Site) +como uma abordagem moderna de defense-in-depth para +[proteção CSRF](https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html#fetch-metadata-headers), +implementando a Fetch Metadata API recomendada pela OWASP junto do mecanismo tradicional +baseado em token. + +Navegadores modernos enviam automaticamente o header `Sec-Fetch-Site` com cada request, +indicando a relação entre a origem do request e o destino. O middleware +usa isso para tomar uma decisão de segurança: + +- **`same-origin`** ou **`none`** — permitido (correspondência exata de origem ou navegação direta do usuário) +- **`same-site`** — recorre à validação de token (por exemplo, subdomínio para domínio principal) +- **`cross-site`** — bloqueado por padrão com um erro `403` para métodos inseguros (POST, PUT, DELETE, PATCH) + +Para navegadores que não enviam este header (navegadores mais antigos), o middleware recorre +automaticamente à proteção CSRF tradicional baseada em token. + +Duas opções ajustam o comportamento de `Sec-Fetch-Site`: + +- `TrustedOrigins []string` — allowlist de origens específicas para requests cross-site (útil para callbacks OAuth, webhooks) +- `AllowSecFetchSiteFunc func(c *echo.Context) (bool, error)` — lógica customizada para validação 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 + }, +})) +``` + +## Proteção baseada em token + +```go +e := echo.New() +e.Use(middleware.CSRFWithConfig(middleware.CSRFConfig{ + TokenLookup: "header:X-XSRF-TOKEN", +})) +``` + +O exemplo acima extrai o token CSRF do header de request `X-XSRF-TOKEN`. + +Lendo o token de um cookie: + +```go +middleware.CSRFWithConfig(middleware.CSRFConfig{ + TokenLookup: "cookie:_csrf", + CookiePath: "/", + CookieDomain: "example.com", + CookieSecure: true, + CookieHTTPOnly: true, + CookieSameSite: http.SameSiteStrictMode, +}) +``` + +## Acessando o token CSRF + +- **Server-side** — o token fica disponível no contexto sob `ContextKey` e pode ser passado ao cliente via template. +- **Client-side** — o token pode ser lido do cookie CSRF. + +## Configuração + +```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 +} +``` + +### Configuração padrão + +```go +var DefaultCSRFConfig = CSRFConfig{ + Skipper: DefaultSkipper, + TokenLength: 32, + TokenLookup: "header:" + echo.HeaderXCSRFToken, + ContextKey: "csrf", + CookieName: "_csrf", + CookieMaxAge: 86400, + CookieSameSite: http.SameSiteDefaultMode, +} +``` + +## Exemplo completo + +Um exemplo completo e executável está disponível no +[echox cookbook](https://github.com/labstack/echox/blob/master/cookbook/csrf/main.go). diff --git a/site/src/content/docs/pt-br/middleware/decompress.md b/site/src/content/docs/pt-br/middleware/decompress.md new file mode 100644 index 00000000..25eeb757 --- /dev/null +++ b/site/src/content/docs/pt-br/middleware/decompress.md @@ -0,0 +1,59 @@ +--- +title: Decompress +description: Descomprima transparentemente bodies de request codificados com gzip. +sidebar: + order: 8 +--- + +O middleware Decompress descomprime o body do request HTTP quando o header `Content-Encoding` +está definido como `gzip`. + +:::note +O body é descomprimido em memória e mantido ali durante toda a vida do request (e +até a garbage collection). +::: + +## Uso + +```go +e.Use(middleware.Decompress()) +``` + +## Configuração customizada + +```go +e := echo.New() +e.Use(middleware.DecompressWithConfig(middleware.DecompressConfig{ + Skipper: middleware.DefaultSkipper, +})) +``` + +## Configuração + +```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 +} +``` + +### Configuração padrão + +```go +// Effective defaults applied when fields are left unset. +DecompressConfig{ + Skipper: DefaultSkipper, + GzipDecompressPool: &DefaultGzipDecompressPool{}, + MaxDecompressedSize: 100 * MB, +} +``` diff --git a/site/src/content/docs/pt-br/middleware/gzip.md b/site/src/content/docs/pt-br/middleware/gzip.md new file mode 100644 index 00000000..abf4f872 --- /dev/null +++ b/site/src/content/docs/pt-br/middleware/gzip.md @@ -0,0 +1,69 @@ +--- +title: Gzip +description: Comprima responses HTTP com o esquema de compressão gzip. +sidebar: + order: 9 +--- + +O middleware Gzip comprime a response HTTP usando o esquema de compressão gzip. + +## Uso + +```go +e.Use(middleware.Gzip()) +``` + +## Configuração customizada + +```go +e := echo.New() +e.Use(middleware.GzipWithConfig(middleware.GzipConfig{ + Level: 5, +})) +``` + +:::tip +Passe um skipper para desabilitar gzip em certas 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 + }, +})) +``` + +## Configuração + +```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 +} +``` + +### Configuração padrão + +```go +// Effective defaults applied when fields are left unset. +GzipConfig{ + Skipper: DefaultSkipper, + Level: -1, + MinLength: 0, +} +``` diff --git a/site/src/content/docs/pt-br/middleware/jwt.md b/site/src/content/docs/pt-br/middleware/jwt.md new file mode 100644 index 00000000..f7d0d2af --- /dev/null +++ b/site/src/content/docs/pt-br/middleware/jwt.md @@ -0,0 +1,126 @@ +--- +title: JWT +description: Middleware de autenticação JSON Web Token fornecido pelo módulo echo-jwt. +sidebar: + order: 10 +--- + +O middleware JWT fornece autenticação JSON Web Token (JWT). Ele vive em um módulo separado: +[github.com/labstack/echo-jwt](https://github.com/labstack/echo-jwt). + +Comportamento: + +- Para um token válido, ele define o usuário no contexto e chama o próximo handler. +- Para um token inválido, ele envia uma response `401 Unauthorized`. +- Para um header `Authorization` ausente ou inválido, ele envia uma response `400 Bad Request`. + +## Dependências + +```go +import "github.com/labstack/echo-jwt/v5" +``` + +## Uso + +```go +e.Use(echojwt.JWT([]byte("secret"))) +``` + +## Configuração customizada + +```go +e.Use(echojwt.WithConfig(echojwt.Config{ + SigningKey: []byte("secret"), +})) +``` + +## Configuração + +```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 +} +``` + +## Exemplo + +Veja a [receita de JWT](/pt-br/cookbook/jwt/) para um exemplo completo. diff --git a/site/src/content/docs/pt-br/middleware/key-auth.md b/site/src/content/docs/pt-br/middleware/key-auth.md new file mode 100644 index 00000000..17922eb7 --- /dev/null +++ b/site/src/content/docs/pt-br/middleware/key-auth.md @@ -0,0 +1,91 @@ +--- +title: Key Auth +description: Middleware de autenticação por chave que valida uma API key de header, query, form ou cookie. +sidebar: + order: 11 +--- + +O middleware Key Auth fornece autenticação baseada em chave. + +- Para uma chave válida, ele chama o próximo handler. +- Para uma chave inválida, ele envia uma response `401 Unauthorized`. +- Para uma chave ausente, ele envia uma 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 +})) +``` + +## Configuração customizada + +```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 + }, +})) +``` + +## Configuração + +```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 +} +``` + +O `Validator` tem a assinatura: + +```go +type KeyAuthValidator func(c *echo.Context, key string, source ExtractorSource) (bool, error) +``` + +### Configuração padrão + +```go +DefaultKeyAuthConfig = KeyAuthConfig{ + Skipper: DefaultSkipper, + KeyLookup: "header:" + echo.HeaderAuthorization + ":Bearer ", +} +``` diff --git a/site/src/content/docs/pt-br/middleware/logger.md b/site/src/content/docs/pt-br/middleware/logger.md new file mode 100644 index 00000000..6d343c8b --- /dev/null +++ b/site/src/content/docs/pt-br/middleware/logger.md @@ -0,0 +1,236 @@ +--- +title: Request Logger +description: Logging de requests totalmente customizável que se integra a bibliotecas de logging estruturado. +sidebar: + order: 12 +--- + +O middleware `RequestLogger` registra informações sobre cada request HTTP. Ele permite customizar +totalmente o que é registrado e como, tornando-o adequado para uso com bibliotecas de terceiros +(logging estruturado). + +Os valores que o logger pode extrair são controlados pelos campos booleanos e slices de +`RequestLoggerConfig`. Habilite um campo (por exemplo `LogStatus: true`) para ter seu valor +preenchido em `RequestLoggerValues`, que é passado para seu `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 +} +``` + +## Exemplos + +### 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 + }, +})) +``` + +Saída de exemplo: + +```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 + }, +})) +``` + +Saída de exemplo: + +```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 + }, +})) +``` + +Saída de exemplo: + +```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 + }, +})) +``` + +Saída de exemplo: + +```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 + }, +})) +``` + +Saída de exemplo: + +```text +time="2024-12-30T21:08:49+08:00" level=info msg=request URI=/hello status=200 +``` + +## Solução de problemas + +### panic: missing LogValuesFunc callback function for request logger middleware + +Esse panic ocorre quando o callback obrigatório `LogValuesFunc` não é definido. Defina uma +função compatível com a assinatura `LogValuesFunc` e atribua-a na configuração: + +```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, +})) +``` + +### Parâmetros nos logs estão vazios + +Se valores como `v.URI` e `v.Status` estiverem vazios dentro de `LogValuesFunc`, verifique se as +flags de extração correspondentes (`LogStatus`, `LogURI` e assim por diante) estão definidas como `true` na +configuração. Cada valor só é preenchido quando sua flag está habilitada. diff --git a/site/src/content/docs/pt-br/middleware/method-override.md b/site/src/content/docs/pt-br/middleware/method-override.md new file mode 100644 index 00000000..04273750 --- /dev/null +++ b/site/src/content/docs/pt-br/middleware/method-override.md @@ -0,0 +1,52 @@ +--- +title: Method Override +description: Substitua o método HTTP de um request POST via header, form ou valor de query. +sidebar: + order: 13 +--- + +O middleware Method Override lê o método sobrescrito do request e o usa +no lugar do método original. + +:::note +Por motivos de segurança, apenas o método `POST` pode ser sobrescrito. +::: + +## Uso + +```go +e.Pre(middleware.MethodOverride()) +``` + +## Configuração customizada + +```go +e := echo.New() +e.Pre(middleware.MethodOverrideWithConfig(middleware.MethodOverrideConfig{ + Getter: middleware.MethodFromForm("_method"), +})) +``` + +O método pode vir de `MethodFromHeader`, `MethodFromForm` ou `MethodFromQuery`. + +## Configuração + +```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 +} +``` + +### Configuração padrão + +```go +DefaultMethodOverrideConfig = MethodOverrideConfig{ + Skipper: DefaultSkipper, + Getter: MethodFromHeader(echo.HeaderXHTTPMethodOverride), +} +``` diff --git a/site/src/content/docs/pt-br/middleware/open-telemetry.md b/site/src/content/docs/pt-br/middleware/open-telemetry.md new file mode 100644 index 00000000..792f11ad --- /dev/null +++ b/site/src/content/docs/pt-br/middleware/open-telemetry.md @@ -0,0 +1,81 @@ +--- +title: OpenTelemetry +description: Instrumentação OpenTelemetry para requests HTTP no Echo. +sidebar: + order: 14 +--- + +[Echo OpenTelemetry](https://github.com/labstack/echo-opentelemetry) é um middleware que +fornece instrumentação OpenTelemetry para requests HTTP. + +OpenTelemetry é um conjunto de ferramentas open-source que fornecem instrumentação para aplicações +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 + +Adicione a dependência do middleware OpenTelemetry com Go modules: + +```bash +go get github.com/labstack/echo-opentelemetry +``` + +Importe o middleware e a API de trace do OpenTelemetry: + +```go +import ( + echootel "github.com/labstack/echo-opentelemetry" + "go.opentelemetry.io/otel/trace" +) +``` + +Registre-o com configuração 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 opções de configuração, veja a struct [`Config`](https://github.com/labstack/echo-opentelemetry/blob/main/otel.go#L28). + +Adicione o middleware de forma simplificada fornecendo apenas o nome do servidor: + +```go +e.Use(echootel.NewMiddleware("app.example.com")) +``` + +Adicione o middleware com opções de configuração: + +```go +e.Use(echootel.NewMiddlewareWithConfig(echootel.Config{ + TracerProvider: tp, +})) +``` + +Recupere o tracer a partir do contexto do Echo: + +```go +tracer, err := echo.ContextGet[trace.Tracer](c, echootel.TracerKey) +``` + +## Exemplo + +O [exemplo](https://github.com/labstack/echo-opentelemetry/blob/main/example/main.go) exporta +métricas e spans para stdout, mas você pode usar qualquer exporter (OTLP etc.). Veja a +documentação de [OpenTelemetry exporters](https://opentelemetry.io/docs/languages/go/exporters). diff --git a/site/src/content/docs/pt-br/middleware/prometheus.md b/site/src/content/docs/pt-br/middleware/prometheus.md new file mode 100644 index 00000000..e8693575 --- /dev/null +++ b/site/src/content/docs/pt-br/middleware/prometheus.md @@ -0,0 +1,284 @@ +--- +title: Prometheus +description: Gere métricas Prometheus para requests HTTP no Echo. +sidebar: + order: 15 +--- + +O middleware [Echo Prometheus](https://github.com/labstack/echo-prometheus) gera métricas Prometheus +para requests HTTP. + +## Uso + +Adicione o módulo necessário: + +```bash +go get github.com/labstack/echo-prometheus +``` + +Adicione o middleware Prometheus e uma rota para servir as métricas coletadas: + +```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 +``` + +## Exemplos + +Sirva métricas a partir do mesmo servidor que as coleta: + +```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) + } +} +``` + +Sirva métricas em uma porta separada: + +```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) + } +} +``` + +Saída de exemplo (para o primeiro exemplo): + +```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 +... +``` + +## Configuração customizada + +### Servindo métricas Prometheus customizadas + +Use métricas customizadas com o registry padrão do 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) + } +} +``` + +Ou crie seu próprio registry e registre métricas customizadas nele: + +```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) + } +} +``` + +### Ignorando URLs + +Um skipper pode ser passado para evitar gerar métricas para certas 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) + } +} +``` + +## Cenários complexos + +Modifique as definições padrão de métricas `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/pt-br/middleware/proxy.md b/site/src/content/docs/pt-br/middleware/proxy.md new file mode 100644 index 00000000..9acc87b2 --- /dev/null +++ b/site/src/content/docs/pt-br/middleware/proxy.md @@ -0,0 +1,134 @@ +--- +title: Proxy +description: Middleware de reverse proxy HTTP e WebSocket com load balancing. +sidebar: + order: 16 +--- + +Proxy fornece um middleware de reverse proxy HTTP/WebSocket. Ele encaminha um request para um +servidor upstream usando uma 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, + }, +}))) +``` + +## Configuração customizada + +```go +e := echo.New() +e.Use(middleware.ProxyWithConfig(middleware.ProxyConfig{})) +``` + +## Configuração + +```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 +} +``` + +### Configuração padrão + +| Nome | Valor | +| ---------- | -------------- | +| Skipper | DefaultSkipper | +| ContextKey | `target` | + +### Regras baseadas em regex + +Para reescrita avançada de requests de proxy, as regras também podem ser definidas usando expressões +regulares. Grupos de captura normais podem ser definidos com `()` e referenciados por índice +(`$1`, `$2`, ...) no caminho reescrito. + +Regras `RegexRewrite` e `Rewrite` normais podem ser combinadas. + +```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", + }, +})) +``` + +Veja a receita de [reverse proxy](/pt-br/cookbook/reverse-proxy/) para um exemplo completo. diff --git a/site/src/content/docs/pt-br/middleware/rate-limiter.md b/site/src/content/docs/pt-br/middleware/rate-limiter.md new file mode 100644 index 00000000..d4038d33 --- /dev/null +++ b/site/src/content/docs/pt-br/middleware/rate-limiter.md @@ -0,0 +1,109 @@ +--- +title: Rate Limiter +description: Limite o número de requests de um IP ou identificador específico dentro de um período. +sidebar: + order: 17 +--- + +`RateLimiter` fornece um middleware de rate limiter que limita o número de requests enviados ao +servidor a partir de um IP ou identificador específico dentro de um período. + +Por padrão, um store em memória acompanha os requests. A implementação em memória padrão +é focada em correção e pode não ser a melhor opção para um alto número de requests +concorrentes ou um grande número de identificadores distintos (>16k). + +## Uso + +Para adicionar um limite de taxa à sua aplicação, adicione o middleware `RateLimiter`. O exemplo abaixo +limita a aplicação a 20 requests/segundo usando o store em memória padrão: + +```go +e.Use(middleware.RateLimiter(middleware.NewRateLimiterMemoryStore(20.0))) +``` + +:::note +Se a taxa fornecida for um número float, `Burst` é tratado como o valor arredondado para baixo da taxa. +::: + +## Configuração customizada + +```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)) +``` + +### Erros + +```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 seu próprio store, satisfaça a interface `RateLimiterStore` e passe-o para +`RateLimiterConfig`. +::: + +## Configuração + +```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 +} +``` + +### Configuração padrão + +```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/pt-br/middleware/recover.md b/site/src/content/docs/pt-br/middleware/recover.md new file mode 100644 index 00000000..ae03f386 --- /dev/null +++ b/site/src/content/docs/pt-br/middleware/recover.md @@ -0,0 +1,61 @@ +--- +title: Recover +description: Recupere-se de panics em qualquer ponto da cadeia e delegue ao handler de erro centralizado. +sidebar: + order: 18 +--- + +O middleware Recover recupera panics em qualquer ponto da cadeia, imprime a stack trace e +passa o controle para o +[HTTPErrorHandler](/pt-br/guide/customization/#http-error-handler) centralizado. + +## Uso + +```go +e.Use(middleware.Recover()) +``` + +## Configuração customizada + +```go +e := echo.New() +e.Use(middleware.RecoverWithConfig(middleware.RecoverConfig{ + StackSize: 1 << 10, // 1 KB +})) +``` + +O exemplo acima usa um `StackSize` de 1 KB e valores padrão para `DisableStackAll` e +`DisablePrintStack`. + +## Configuração + +```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 +} +``` + +### Configuração padrão + +```go +var DefaultRecoverConfig = RecoverConfig{ + Skipper: DefaultSkipper, + StackSize: 4 << 10, // 4 KB + DisableStackAll: false, + DisablePrintStack: false, +} +``` diff --git a/site/src/content/docs/pt-br/middleware/redirect.md b/site/src/content/docs/pt-br/middleware/redirect.md new file mode 100644 index 00000000..62e746e3 --- /dev/null +++ b/site/src/content/docs/pt-br/middleware/redirect.md @@ -0,0 +1,101 @@ +--- +title: Redirect +description: Redirecione requests entre HTTP/HTTPS e variantes www/non-www. +sidebar: + order: 19 +--- + +## HTTPS Redirect + +O middleware HTTPS redirect redireciona requests HTTP para HTTPS. Por exemplo, +`http://labstack.com` é redirecionado para `https://labstack.com`. + +### Uso + +```go +e := echo.New() +e.Pre(middleware.HTTPSRedirect()) +``` + +## HTTPS WWW Redirect + +HTTPS WWW redirect redireciona requests HTTP para www HTTPS. Por exemplo, +`http://labstack.com` é redirecionado para `https://www.labstack.com`. + +### Uso + +```go +e := echo.New() +e.Pre(middleware.HTTPSWWWRedirect()) +``` + +## HTTPS NonWWW Redirect + +HTTPS NonWWW redirect redireciona requests HTTP para non-www HTTPS. Por exemplo, +`http://www.labstack.com` é redirecionado para `https://labstack.com`. + +### Uso + +```go +e := echo.New() +e.Pre(middleware.HTTPSNonWWWRedirect()) +``` + +## WWW Redirect + +WWW redirect redireciona requests non-www para www. Por exemplo, `http://labstack.com` é +redirecionado para `http://www.labstack.com`. + +### Uso + +```go +e := echo.New() +e.Pre(middleware.WWWRedirect()) +``` + +## NonWWW Redirect + +NonWWW redirect redireciona requests www para non-www. Por exemplo, `http://www.labstack.com` é +redirecionado para `http://labstack.com`. + +### Uso + +```go +e := echo.New() +e.Pre(middleware.NonWWWRedirect()) +``` + +## Configuração customizada + +```go +e := echo.New() +e.Use(middleware.HTTPSRedirectWithConfig(middleware.RedirectConfig{ + Code: http.StatusTemporaryRedirect, +})) +``` + +O exemplo acima redireciona requests HTTP para HTTPS com o código de status +`307 - StatusTemporaryRedirect`. + +## Configuração + +```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 +} +``` + +### Configuração padrão + +```go +// Effective defaults applied when fields are left unset. +RedirectConfig{ + Skipper: DefaultSkipper, + Code: http.StatusMovedPermanently, +} +``` diff --git a/site/src/content/docs/pt-br/middleware/request-id.md b/site/src/content/docs/pt-br/middleware/request-id.md new file mode 100644 index 00000000..112ca44a --- /dev/null +++ b/site/src/content/docs/pt-br/middleware/request-id.md @@ -0,0 +1,89 @@ +--- +title: Request ID +description: Gere um ID único para cada request. +sidebar: + order: 20 +--- + +O middleware Request ID gera um ID único para um request. + +## Uso + +```go +e.Use(middleware.RequestID()) +``` + +Exemplo: + +```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) + } +} +``` + +## Configuração customizada + +```go +e.Use(middleware.RequestIDWithConfig(middleware.RequestIDConfig{ + Generator: func() string { + return customGenerator() + }, +})) +``` + +## Configuração + +```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 +} +``` + +### Configuração padrão + +```go +// Effective defaults applied when fields are left unset. +RequestIDConfig{ + Skipper: DefaultSkipper, + Generator: generator, // random 32-character string + TargetHeader: echo.HeaderXRequestID, +} +``` + +## Definir ID + +Você pode definir o ID a partir do requester com o 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/pt-br/middleware/rewrite.md b/site/src/content/docs/pt-br/middleware/rewrite.md new file mode 100644 index 00000000..77888bb1 --- /dev/null +++ b/site/src/content/docs/pt-br/middleware/rewrite.md @@ -0,0 +1,87 @@ +--- +title: Rewrite +description: Reescreva o caminho da URL com base em regras configuradas. +sidebar: + order: 21 +--- + +O middleware Rewrite reescreve o caminho da URL com base nas regras fornecidas. Ele é útil para +compatibilidade retroativa ou para criar links mais limpos e descritivos. + +## Uso + +```go +e.Pre(middleware.Rewrite(map[string]string{ + "/old": "/new", + "/api/*": "/$1", + "/js/*": "/public/javascripts/$1", + "/users/*/orders/*": "/user/$1/order/$2", +})) +``` + +Os valores capturados em asteriscos podem ser recuperados por índice, por exemplo `$1`, `$2` e assim por diante. Cada +asterisco é non-greedy (traduzido para um grupo de captura `(.*?)`); ao usar vários asteriscos, +um `*` final corresponde ao restante do caminho. + +:::caution +O middleware Rewrite deve ser registrado via `Echo#Pre()` para rodar antes do router. +::: + +## Configuração customizada + +```go +e := echo.New() +e.Pre(middleware.RewriteWithConfig(middleware.RewriteConfig{})) +``` + +## Configuração + +```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 +} +``` + +Configuração padrão: + +| Nome | Valor | +| ------- | -------------- | +| Skipper | DefaultSkipper | + +### Regras baseadas em regex + +Para reescrita avançada de caminhos, as regras também podem ser definidas usando expressões regulares. Grupos +de captura normais podem ser definidos com `()` e referenciados por índice (`$1`, `$2`, ...) no +caminho reescrito. + +`RegexRules` e `Rules` normais podem ser combinados. + +```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/pt-br/middleware/secure.md b/site/src/content/docs/pt-br/middleware/secure.md new file mode 100644 index 00000000..65c1790b --- /dev/null +++ b/site/src/content/docs/pt-br/middleware/secure.md @@ -0,0 +1,113 @@ +--- +title: Secure +description: Proteja contra XSS, content sniffing, clickjacking e outros ataques de injeção. +sidebar: + order: 22 +--- + +O middleware Secure fornece proteção contra cross-site scripting (XSS), content type +sniffing, clickjacking, conexões inseguras e outros ataques de injeção de código. + +## Uso + +```go +e.Use(middleware.Secure()) +``` + +## Configuração customizada + +```go +e := echo.New() +e.Use(middleware.SecureWithConfig(middleware.SecureConfig{ + XSSProtection: "", + ContentTypeNosniff: "", + XFrameOptions: "", + HSTSMaxAge: 3600, + ContentSecurityPolicy: "default-src 'self'", +})) +``` + +:::note +Passar `XSSProtection`, `ContentTypeNosniff`, `XFrameOptions` ou +`ContentSecurityPolicy` vazios desabilita essa proteção. +::: + +## Configuração + +```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 ,