diff --git a/site/astro.config.mjs b/site/astro.config.mjs index b6a54196..579e3a9d 100644 --- a/site/astro.config.mjs +++ b/site/astro.config.mjs @@ -10,6 +10,12 @@ export default defineConfig({ integrations: [ starlight({ title: 'Echo', + defaultLocale: 'root', + locales: { + root: { label: 'English', lang: 'en' }, + 'zh-cn': { label: '简体中文', lang: 'zh-CN' }, + ja: { label: '日本語', lang: 'ja' }, + }, logo: { light: './src/assets/logo-light.svg', dark: './src/assets/logo-dark.svg', @@ -73,9 +79,9 @@ export default defineConfig({ // Autogenerated from the content dirs — new pages appear automatically, // ordered by each page's `sidebar.order` frontmatter. sidebar: [ - { label: 'Guide', items: [{ autogenerate: { directory: 'guide' } }] }, - { label: 'Middleware', items: [{ autogenerate: { directory: 'middleware' } }] }, - { label: 'Cookbook', items: [{ autogenerate: { directory: 'cookbook' } }] }, + { label: 'Guide', translations: { 'zh-CN': '指南', ja: 'ガイド' }, items: [{ autogenerate: { directory: 'guide' } }] }, + { label: 'Middleware', translations: { 'zh-CN': '中间件', ja: 'ミドルウェア' }, items: [{ autogenerate: { directory: 'middleware' } }] }, + { label: 'Cookbook', translations: { 'zh-CN': '示例', ja: 'クックブック' }, items: [{ autogenerate: { directory: 'cookbook' } }] }, ], // tune the built-in code theme toward our terminal palette expressiveCode: { themes: ['github-dark', 'github-light'] }, diff --git a/site/src/content/docs/ja/cookbook/auto-tls.md b/site/src/content/docs/ja/cookbook/auto-tls.md new file mode 100644 index 00000000..f02cdd9b --- /dev/null +++ b/site/src/content/docs/ja/cookbook/auto-tls.md @@ -0,0 +1,108 @@ +--- +title: 自動 TLS +description: Let's Encrypt から TLS 証明書を自動で取得し、更新します。 +sidebar: + order: 3 +--- + +このレシピは、ドメインの TLS 証明書を Let's Encrypt から自動取得します。autocert manager の +`TLSConfig` を使って `StartConfig` を設定し、ポート `443` で待ち受けます。 + +`https://` にアクセスしてください。すべて正しく設定されていれば、TLS 経由で提供される +ウェルカムメッセージが表示されます。 + +:::tip +- セキュリティを高めるには、autocert manager で host policy を指定してください。 +- [Let's Encrypt rate limits](https://letsencrypt.org/docs/rate-limits) に達しないよう、証明書をキャッシュしてください。 +- HTTP トラフィックを HTTPS にリダイレクトするには、[リダイレクトミドルウェア](/ja/middleware/redirect/#https-redirect)を使います。 +::: + +## サーバー + +```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) + } +} +``` + +## カスタム HTTP サーバーを使う + +`http.Server` を完全に制御したい場合は、代わりに autocert manager をカスタム `tls.Config` に接続します。 + +```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/ja/cookbook/cors.md b/site/src/content/docs/ja/cookbook/cors.md new file mode 100644 index 00000000..82c8c05e --- /dev/null +++ b/site/src/content/docs/ja/cookbook/cors.md @@ -0,0 +1,119 @@ +--- +title: CORS +description: allow list またはカスタム origin 関数で Cross-Origin Resource Sharing を有効にします。 +sidebar: + order: 4 +--- + +[CORS ミドルウェア](/ja/middleware/cors/)は、どの origin が API にアクセスできるかを制御します。 +許可する origin の固定リストを渡すことも、リクエストごとに判断する関数を指定することもできます。 + +## origin の allow list + +許可する origin を `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) + } +} +``` + +## カスタム origin 関数 + +動的ポリシーには、`UnsafeAllowOriginFunc` を指定した `CORSWithConfig` を使います。 +この関数はリクエストコンテキストと origin を受け取り、返す origin、リクエストを許可するか、 +任意のエラーを返します。 + +```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/ja/cookbook/crud.md b/site/src/content/docs/ja/cookbook/crud.md new file mode 100644 index 00000000..d046a757 --- /dev/null +++ b/site/src/content/docs/ja/cookbook/crud.md @@ -0,0 +1,180 @@ +--- +title: CRUD +description: Echo と JSON バインディングでリソースを作成、読み取り、更新、削除します。 +sidebar: + order: 2 +--- + +インメモリ store を使った完全な CRUD(create, read, update, delete)API です。 +各ハンドラは JSON リクエストボディを struct にバインドし、ロック下で store を変更し、 +結果を JSON として返します。 + +## サーバー + +```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) + } +} +``` + +## クライアント + +### ユーザーを作成する + +リクエスト: + +```sh +curl -X POST \ + -H 'Content-Type: application/json' \ + -d '{"name":"Joe Smith"}' \ + localhost:1323/users +``` + +レスポンス: + +```json +{ + "id": 1, + "name": "Joe Smith" +} +``` + +### ユーザーを取得する + +リクエスト: + +```sh +curl localhost:1323/users/1 +``` + +レスポンス: + +```json +{ + "id": 1, + "name": "Joe Smith" +} +``` + +### ユーザーを更新する + +リクエスト: + +```sh +curl -X PUT \ + -H 'Content-Type: application/json' \ + -d '{"name":"Joe"}' \ + localhost:1323/users/1 +``` + +レスポンス: + +```json +{ + "id": 1, + "name": "Joe" +} +``` + +### ユーザーを削除する + +リクエスト: + +```sh +curl -X DELETE localhost:1323/users/1 +``` + +レスポンス:`204 No Content`。 diff --git a/site/src/content/docs/ja/cookbook/embed-resources.md b/site/src/content/docs/ja/cookbook/embed-resources.md new file mode 100644 index 00000000..29d694d1 --- /dev/null +++ b/site/src/content/docs/ja/cookbook/embed-resources.md @@ -0,0 +1,63 @@ +--- +title: リソースの埋め込み +description: Go の embed パッケージでバイナリに同梱した静的アセットを配信します。 +sidebar: + order: 5 +--- + +Go の `embed` パッケージ(Go 1.16+)を使うと、静的アセットを直接バイナリにコンパイルできます。 +そのため、単一の実行ファイルにフロントエンドを同梱できます。このレシピは Echo 経由で埋め込み +ファイルシステムを配信し、開発中にディスクから読み込む任意の live モードも備えています。 + +## サーバー + +```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 +`live` 引数(`go run server.go live`)付きでバイナリを実行すると、埋め込みコピーではなく +ディスク上の `app` ディレクトリからアセットを配信できます。開発中に便利です。 +::: diff --git a/site/src/content/docs/ja/cookbook/file-download.md b/site/src/content/docs/ja/cookbook/file-download.md new file mode 100644 index 00000000..a5dea12f --- /dev/null +++ b/site/src/content/docs/ja/cookbook/file-download.md @@ -0,0 +1,175 @@ +--- +title: ファイルダウンロード +description: ダウンロード、インライン表示、名前付き添付ファイルとしてファイルを配信します。 +sidebar: + order: 6 +--- + +Echo はファイルを返すための 3 つのコンテキストヘルパーを提供します。`c.File` はブラウザーの +デフォルト content disposition でファイルを配信し、`c.Inline` はブラウザーにその場で表示するよう促し、 +`c.Attachment` は指定したファイル名でダウンロードを促します。 + +## ファイルをダウンロードする + +### サーバー + +```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) + } +} +``` + +### クライアント + +```html + + + + + File download + + + +

+ File download +

+ + + +``` + +## ファイルをインラインでダウンロードする + +`c.Inline` を使って `Content-Disposition: inline` header を送信し、ブラウザーがファイルを +ダウンロードするのではなくその場でレンダリングするようにします。 + +### サーバー + +```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) + } +} +``` + +### クライアント + +```html + + + + + File download + + + +

+ Inline file download +

+ + + +``` + +## ファイルを添付ファイルとしてダウンロードする + +`c.Attachment` を使って `Content-Disposition: attachment` header を送信し、 +指定した名前でファイルをダウンロードするようブラウザーに促します。 + +### サーバー + +```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) + } +} +``` + +### クライアント + +```html + + + + + File download + + + +

+ Attachment file download +

+ + + +``` diff --git a/site/src/content/docs/ja/cookbook/file-upload.md b/site/src/content/docs/ja/cookbook/file-upload.md new file mode 100644 index 00000000..71e8a431 --- /dev/null +++ b/site/src/content/docs/ja/cookbook/file-upload.md @@ -0,0 +1,198 @@ +--- +title: ファイルアップロード +description: フォームフィールドと一緒に単一または複数の multipart ファイルアップロードを処理します。 +sidebar: + order: 7 +--- + +Echo はリクエストコンテキストを通じて multipart フォームデータを読み取ります。テキストフィールドには +`c.FormValue`、単一ファイルには `c.FormFile`、同じフィールド名の複数ファイルへアクセスするには +`c.MultipartForm` を使います。 + +## フィールド付きで単一ファイルをアップロードする + +### サーバー + +```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) + } +} +``` + +### クライアント + +```html + + + + + Single file upload + + +

Upload single file with fields

+ +
+ Name:
+ Email:
+ Files:

+ +
+ + +``` + +## フィールド付きで複数ファイルをアップロードする + +### サーバー + +```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) + } +} +``` + +### クライアント + +```html + + + + + Multiple file upload + + +

Upload multiple files with fields

+ +
+ Name:
+ Email:
+ Files:

+ +
+ + +``` diff --git a/site/src/content/docs/ja/cookbook/graceful-shutdown.md b/site/src/content/docs/ja/cookbook/graceful-shutdown.md new file mode 100644 index 00000000..b8cfd643 --- /dev/null +++ b/site/src/content/docs/ja/cookbook/graceful-shutdown.md @@ -0,0 +1,85 @@ +--- +title: グレースフルシャットダウン +description: 割り込み信号でサーバーを停止する前に、処理中のリクエストを完了させます。 +sidebar: + order: 8 +--- + +グレースフルシャットダウンは、プロセスが終了する前に処理中のリクエストを完了させます。 +もっとも簡単な方法は、キャンセル可能なコンテキストを `StartConfig.Start` に渡し、 +`GracefulTimeout` を設定することです。割り込み信号でコンテキストがキャンセルされると、 +Echo は新しい接続の受け付けを停止し、アクティブなリクエストが完了するまで最大タイムアウトまで待ちます。 + +## サーバー + +```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) + } +} +``` + +## カスタム HTTP サーバーを使う + +`http.Server` を自分で管理する場合は、goroutine で起動し、signal context を待ってから、 +タイムアウト付きで `Shutdown` を呼び出します。 + +```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/ja/cookbook/hello-world.md b/site/src/content/docs/ja/cookbook/hello-world.md new file mode 100644 index 00000000..c2aee8e7 --- /dev/null +++ b/site/src/content/docs/ja/cookbook/hello-world.md @@ -0,0 +1,43 @@ +--- +title: Hello World +description: あいさつを返す最小構成の Echo サーバーです。 +sidebar: + order: 1 +--- + +最小構成の Echo アプリケーションです。インスタンスを作成し、Logger と Recover ミドルウェアを登録し、 +単一のルートを追加してサーバーを起動します。 + +## サーバー + +```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/ja/cookbook/http2-server-push.md b/site/src/content/docs/ja/cookbook/http2-server-push.md new file mode 100644 index 00000000..deeb9ca5 --- /dev/null +++ b/site/src/content/docs/ja/cookbook/http2-server-push.md @@ -0,0 +1,130 @@ +--- +title: HTTP/2 Server Push +description: HTTP/2 で Web アセットをクライアントへ先行プッシュします。 +sidebar: + order: 10 +--- + +HTTP/2 server push により、サーバーはリソースがリクエストされる前にクライアントへ送信できます。 +ページが必要とすることが分かっているアセットの往復通信をなくせます。このレシピでは、HTML レスポンスと一緒に +ページの CSS、JavaScript、画像をプッシュします。 + +:::note +Server push には HTTP/2 接続が必要です。下で使う証明書を作成するには、 +[自己署名 X.509 TLS 証明書を生成する](/ja/cookbook/http2/#1-generate-a-self-signed-x509-tls-certificate) +に従ってください。 +::: + +## 1. Web アセットを配信するルートを登録する + +```go +e.Static("/", "static") +``` + +## 2. index.html を配信し、その依存関係をプッシュする + +レスポンスを unwrap して基底の `http.ResponseWriter` にアクセスし、writer が `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 +`http.Pusher` がサポートされている場合、Web アセットは先行してプッシュされます。 +そうでない場合、クライアントはそれらを個別にリクエストします。 +::: + +## 3. 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) +} +``` + +## ソースコード + +### 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/ja/cookbook/http2.md b/site/src/content/docs/ja/cookbook/http2.md new file mode 100644 index 00000000..12d7ff51 --- /dev/null +++ b/site/src/content/docs/ja/cookbook/http2.md @@ -0,0 +1,115 @@ +--- +title: HTTP/2 サーバー +description: TLS 証明書で Echo を起動し、HTTP/2 でトラフィックを配信します。 +sidebar: + order: 9 +--- + +HTTP/2 はリクエスト多重化、header 圧縮、server push によりレイテンシーを改善します。 +Go の HTTP サーバーは TLS 上で HTTP/2 を自動的にネゴシエートするため、Echo で HTTP/2 を配信するには +証明書付きでサーバーを起動すれば済みます。 + +## 1. 自己署名 X.509 TLS 証明書を生成する + +次のコマンドで `cert.pem` と `key.pem` を生成します。 + +```sh +go run $GOROOT/src/crypto/tls/generate_cert.go --host localhost +``` + +:::note +デモ目的のため、自己署名証明書を使います。本番では +[certificate authority](https://en.wikipedia.org/wiki/Certificate_authority) から証明書を取得してください。 +::: + +## 2. リクエスト情報を echo するハンドラを作成する + +```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. TLS サーバーを起動する + +生成した証明書と key でサーバーを起動します。 + +```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) +} +``` + +または、独自の `tls.Config` を持つカスタム `http.Server` を使います。 + +```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. 検証する + +サーバーを起動して `https://localhost:1323/request` にアクセスします。次のような出力が表示されます。 + +```sh +Protocol: HTTP/2.0 +Host: localhost:1323 +Remote Address: [::1]:60288 +Method: GET +Path: / +``` + +## ソースコード + +```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/ja/cookbook/jsonp.md b/site/src/content/docs/ja/cookbook/jsonp.md new file mode 100644 index 00000000..fa580c1e --- /dev/null +++ b/site/src/content/docs/ja/cookbook/jsonp.md @@ -0,0 +1,95 @@ +--- +title: JSONP +description: Context#JSONP でクロスドメインリクエスト向け JSONP レスポンスを配信します。 +sidebar: + order: 13 +--- + +JSONP は、ブラウザーからクロスドメインのサーバー呼び出しを可能にする技術です。 +Echo は `c.JSONP()` で JSONP レスポンスを配信し、リクエストで指定された callback 関数呼び出しで +JSON ペイロードをラップします。 + +## サーバー + +```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) + } +} +``` + +## クライアント + +```html + + + + + + + JSONP + + + + + + +
+ +

+


+        

+
+ + + +``` diff --git a/site/src/content/docs/ja/cookbook/jwt.md b/site/src/content/docs/ja/cookbook/jwt.md new file mode 100644 index 00000000..95e6bf84 --- /dev/null +++ b/site/src/content/docs/ja/cookbook/jwt.md @@ -0,0 +1,250 @@ +--- +title: JWT +description: echo-jwt ミドルウェアで JSON Web Tokens を使ってリクエストを認証します。 +sidebar: + order: 11 +--- + +このレシピでは、[`echo-jwt`](https://github.com/labstack/echo-jwt) ミドルウェアを使った +Echo の JWT 認証を示します。 + +- HS256 アルゴリズムを使った JWT 認証。 +- token は `Authorization` リクエスト header から読み取ります。 + +完全な設定オプションは [JWT ミドルウェア](/ja/middleware/jwt/)ページを参照してください。 + +## サーバー + +### カスタム claims を使う + +`jwt.RegisteredClaims` を埋め込む claims 型を定義し、`NewClaimsFunc` でミドルウェアに指定します。 +制限付きハンドラ内では、ジェネリック `echo.ContextGet` を使ってコンテキストから解析済み token を取得します。 + +```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) + } +} +``` + +### ユーザー定義 KeyFunc を使う + +token が外部 ID プロバイダーによって署名されている場合は、署名 key を動的に解決する +`KeyFunc` を指定します。この例では、Google の公開 key set を取得して Google Sign-In が発行した +token を検証します。 + +```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 +上のように各リクエストで key set を取得するのはデモ目的のみです。本番では key set をキャッシュし、 +定期的に更新してください。 +::: + +## クライアント + +### ログイン + +ユーザー名とパスワードでログインし、token を取得します。 + +```sh +curl -X POST -d 'username=jon' -d 'password=shhh!' localhost:1323/login +``` + +レスポンス: + +```js +{ + "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE0NjE5NTcxMzZ9.RB3arc4-OyzASAaUhC2W3ReWaXAt_z2Fd3BN4aWTgEY" +} +``` + +### リクエスト + +`Authorization` リクエスト header の token を使って制限付きリソースをリクエストします。 + +```sh +curl localhost:1323/restricted -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE0NjE5NTcxMzZ9.RB3arc4-OyzASAaUhC2W3ReWaXAt_z2Fd3BN4aWTgEY" +``` + +レスポンス: + +```sh +Welcome Jon Snow! +``` diff --git a/site/src/content/docs/ja/cookbook/load-balancing.md b/site/src/content/docs/ja/cookbook/load-balancing.md new file mode 100644 index 00000000..35c715d8 --- /dev/null +++ b/site/src/content/docs/ja/cookbook/load-balancing.md @@ -0,0 +1,116 @@ +--- +title: ロードバランシング +description: Nginx をリバースプロキシとして使い、複数の Echo サーバー間でトラフィックを分散します。 +sidebar: + order: 20 +--- + +このレシピでは、Nginx をリバースプロキシサーバーとして使い、複数の 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) + } +} +``` + +### サーバーを起動する + +```sh +cd upstream +go run server.go server1 :8081 +go run server.go server2 :8082 +``` + +## Nginx + +### 1) Nginx をインストールする + +[Nginx installation guide](https://www.nginx.com/resources/wiki/start/topics/tutorials/install) を参照してください。 + +### 2) Nginx を設定する + +次の内容で `/etc/nginx/sites-enabled/localhost` ファイルを作成します。 + +```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 +`listen`、`server_name`、`access_log` は環境に合わせて調整してください。 +::: + +### 3) Nginx を再起動する + +```sh +service nginx restart +``` + +`https://localhost:8080` にアクセスすると、"server 1" または "server 2" から配信された Web ページが表示されます。 + +```sh +Hello from upstream server server1 +``` diff --git a/site/src/content/docs/ja/cookbook/middleware.md b/site/src/content/docs/ja/cookbook/middleware.md new file mode 100644 index 00000000..72c0a995 --- /dev/null +++ b/site/src/content/docs/ja/cookbook/middleware.md @@ -0,0 +1,139 @@ +--- +title: カスタムミドルウェア +description: リクエスト統計を収集し、レスポンス header を設定するカスタム Echo ミドルウェアを書きます。 +sidebar: + order: 12 +--- + +このレシピでは、カスタムミドルウェアの書き方を示します。 + +- リクエスト数、レスポンスステータス、稼働時間を収集するミドルウェア。 +- すべてのレスポンスにカスタム `Server` header を書き込むミドルウェア。 + +Echo のミドルウェアは `func(next echo.HandlerFunc) echo.HandlerFunc` というシグネチャの関数です。 +下の `Stats.Process` メソッドはそのシグネチャを直接満たし、`ServerHeader` は通常の関数です。 + +## サーバー + +```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) + } +} +``` + +## レスポンス + +### Header + +```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/ja/cookbook/reverse-proxy.md b/site/src/content/docs/ja/cookbook/reverse-proxy.md new file mode 100644 index 00000000..78ec7d2c --- /dev/null +++ b/site/src/content/docs/ja/cookbook/reverse-proxy.md @@ -0,0 +1,202 @@ +--- +title: リバースプロキシ +description: 上流アプリケーションの前段で Echo をリバースプロキシ兼ロードバランサーとして使います。 +sidebar: + order: 19 +--- + +このレシピでは、WordPress、Node.js、Java、Python、Ruby、Go などのアプリケーションの前段で、 +Echo をリバースプロキシ兼ロードバランサーとして使う方法を示します。簡単のため、ここでの上流は +WebSocket も処理する Go サーバーです。 + +## 1) 上流ターゲット URL を特定する + +```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) 上流ターゲットでプロキシミドルウェアを設定する + +下のスニペットは round-robin ロードバランシングを使います。 +`middleware.NewRandomBalancer()` も使えます。 + +```go +e.Use(middleware.Proxy(middleware.NewRoundRobinBalancer(targets))) +``` + +サブルートにプロキシを設定するには、`Echo#Group()` を使います。 + +```go +g := e.Group("/blog") +g.Use(middleware.Proxy(...)) +``` + +## 3) 上流サーバーを起動する + +```sh +cd upstream +go run server.go server1 :8081 +go run server.go server2 :8082 +``` + +## 4) プロキシサーバーを起動する + +```sh +go run server.go +``` + +`http://localhost:1323` にアクセスすると、HTTP リクエストは "server 1" から、 +WebSocket リクエストは "server 2" から配信される Web ページが表示されます。 + +```sh +HTTP + +Hello from upstream server server1 + +WebSocket + +Hello from upstream server server2! +Hello from upstream server server2! +Hello from upstream server server2! +``` + +## ソースコード + +### 上流サーバー + +```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) + } +} +``` + +### プロキシサーバー + +```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/ja/cookbook/sse.md b/site/src/content/docs/ja/cookbook/sse.md new file mode 100644 index 00000000..6622adec --- /dev/null +++ b/site/src/content/docs/ja/cookbook/sse.md @@ -0,0 +1,303 @@ +--- +title: Server-Sent Events (SSE) +description: Echo ハンドラから server-sent events をストリーミングします。接続ごと、または多数のクライアントへのブロードキャストに対応します。 +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) +は複数の方法で使用できます。下の最初の例は、接続ごと、ハンドラごとの SSE です。 +より複雑なブロードキャストロジックについては、[r3labs/sse](https://github.com/r3labs/sse) +を使う 2 つ目の例を参照してください。 + +:::caution +SSE 接続は長時間維持されるため、サーバーの write timeout を無効にする必要があります。 +どちらの例も `BeforeServeFunc` で `s.WriteTimeout = 0` を設定します。 +::: + +## SSE を使う + +### サーバー + +ハンドラは SSE header を書き込み、その後クライアントが切断するまで毎秒イベントを送信します。 +`http.NewResponseController(w).Flush()` は各イベントをすぐにクライアントへ送ります。 + +```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) + } +} +``` + +### Event 構造と 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 +} +``` + +### SSE を提供する HTML + +```html + + + + +

Getting server-sent updates

+
+ + + + + +``` + +## r3labs/sse でブロードキャストする + +単一のイベントストリームを多数の購読者へブロードキャストする必要がある場合、 +[r3labs/sse](https://github.com/r3labs/sse) ライブラリがストリームと購読者の管理を処理します。 + +### サーバー + +```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) + } +} +``` + +### SSE を提供する HTML + +```html + + + + +

Getting server-sent updates

+
+ + + + + +``` diff --git a/site/src/content/docs/ja/cookbook/streaming-response.md b/site/src/content/docs/ja/cookbook/streaming-response.md new file mode 100644 index 00000000..08ad4c9b --- /dev/null +++ b/site/src/content/docs/ja/cookbook/streaming-response.md @@ -0,0 +1,95 @@ +--- +title: ストリーミングレスポンス +description: chunked transfer encoding を使い、生成されたデータをクライアントへストリーミングします。 +sidebar: + order: 15 +--- + +このレシピでは、chunked transfer encoding を使って、各レコードが生成されるたびに JSON レスポンスを +クライアントへストリーミングします。 + +- 生成されたデータをその都度送信します。 +- chunked transfer encoding で JSON レスポンスをストリーミングします。 + +ハンドラは 1 レコードずつエンコードし、各レコードの後に +`http.NewResponseController(...).Flush()` を呼び出してすぐにクライアントへ送り、 +レコード間で 1 秒待機します。 + +## サーバー + +```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) + } +} +``` + +## クライアント + +```sh +curl localhost:1323 +``` + +### 出力 + +```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/ja/cookbook/subdomain.md b/site/src/content/docs/ja/cookbook/subdomain.md new file mode 100644 index 00000000..9c0eb715 --- /dev/null +++ b/site/src/content/docs/ja/cookbook/subdomain.md @@ -0,0 +1,78 @@ +--- +title: サブドメイン +description: 仮想ホストハンドラを使い、host ごとにリクエストを異なる Echo インスタンスへルーティングします。 +sidebar: + order: 17 +--- + +このレシピでは、リクエスト host に基づいてリクエストを別々の `Echo` インスタンスへルーティングします。 +これにより、各サブドメインが独自のルーティングとミドルウェアを持てます。 +これらのインスタンスは `echo.NewVirtualHostHandler` で結合され、host name ごとに振り分けられます。 + +## サーバー + +```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/ja/cookbook/timeout.md b/site/src/content/docs/ja/cookbook/timeout.md new file mode 100644 index 00000000..d3c2d699 --- /dev/null +++ b/site/src/content/docs/ja/cookbook/timeout.md @@ -0,0 +1,52 @@ +--- +title: タイムアウト +description: ContextTimeout ミドルウェアでハンドラにリクエストタイムアウトを適用します。 +sidebar: + order: 18 +--- + +[`ContextTimeout`](/ja/middleware/context-timeout/) ミドルウェアは、リクエストの `context.Context` +に deadline を設定します。deadline を過ぎるとコンテキストがキャンセルされ、 +`c.Request().Context().Done()` を監視しているハンドラは、最後まで実行されるのを待たずにすぐ戻れます。 + +下の例では、ミドルウェアが 5 秒のタイムアウトを適用します。一方、ハンドラは通常なら 10 秒かかるため、 +リクエストは `408 Request Timeout` を返します。 + +## サーバー + +```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/ja/cookbook/websocket.md b/site/src/content/docs/ja/cookbook/websocket.md new file mode 100644 index 00000000..dc7b7582 --- /dev/null +++ b/site/src/content/docs/ja/cookbook/websocket.md @@ -0,0 +1,188 @@ +--- +title: WebSocket +description: Echo で golang.org/x/net/websocket または gorilla/websocket を使って WebSocket 接続を処理します。 +sidebar: + order: 16 +--- + +Echo ハンドラは、基礎となる HTTP 接続をアップグレードして WebSocket 接続を提供できます。 +このレシピでは 2 つの方法を示します。標準の `golang.org/x/net/websocket` パッケージと、 +広く使われている [`gorilla/websocket`](https://github.com/gorilla/websocket) ライブラリです。 + +## net WebSocket を使う + +### サーバー + +```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) + } +} +``` + +## gorilla WebSocket を使う + +### サーバー + +```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) + } +} +``` + +## クライアント + +```html + + + + + + WebSocket + + + +

+ + + + + +``` + +## 出力 + +**サーバー** + +```sh +Hello, Server! +Hello, Server! +Hello, Server! +Hello, Server! +Hello, Server! +``` + +**クライアント** + +```sh +Hello, Client! +Hello, Client! +Hello, Client! +Hello, Client! +Hello, Client! +``` diff --git a/site/src/content/docs/ja/guide/binding.md b/site/src/content/docs/ja/guide/binding.md new file mode 100644 index 00000000..da9283b5 --- /dev/null +++ b/site/src/content/docs/ja/guide/binding.md @@ -0,0 +1,154 @@ +--- +title: バインディング +description: パス、クエリ、header、リクエストボディからリクエストデータを型付き Go struct に解析します。 +sidebar: + order: 5 +--- + +リクエストデータの解析は、Web アプリケーションの重要な要素です。Echo ではこれを +_バインディング_ と呼び、HTTP リクエストの 4 つの部分から読み取れます。 + +- URL パスパラメーター +- URL クエリパラメーター +- Header +- リクエストボディ + +## Struct タグによるバインディング + +データソースとキーを指定するタグ付きの struct を定義し、そのポインターを +`c.Bind()` に渡します。ここではクエリパラメーター `id` が `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") +} +``` + +### データソース + +| タグ | ソース | +| -------- | ------ | +| `query` | クエリパラメーター | +| `param` | パスパラメーター | +| `header` | Header 値 | +| `form` | フォームデータ(クエリ + ボディ) | +| `json` | リクエストボディ(`encoding/json`) | +| `xml` | リクエストボディ(`encoding/xml`) | + +パス、クエリ、header、フォームのフィールドには**明示的なタグ**が必要です。 +JSON と XML はタグが省略された場合、標準ライブラリと同じように struct フィールド名へ +フォールバックします。 + +### ボディのコンテンツタイプ + +リクエストボディをデコードするときは、`Content-Type` header によってデコーダーが選ばれます。 + +- `application/json` +- `application/xml` +- `application/x-www-form-urlencoded` + +### 複数ソースと優先順位 + +1 つのフィールドで複数のソースを宣言できます。データは次の順序でバインドされ、 +各ステップが前の値を上書きします。 + +1. パスパラメーター +2. クエリパラメーター(GET / DELETE のみ) +3. リクエストボディ + +```go +type User struct { + ID string `param:"id" query:"id" form:"id" json:"id" xml:"id"` +} +``` + +### 1 つのソースから直接バインドする + +```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 +Header は `c.Bind()` に**含まれません**。`echo.BindHeaders` で直接バインドしてください。 +::: + +:::caution[セキュリティ] +ビジネス用の struct に直接バインドしないでください。バインド対象の struct が +`IsAdmin bool` フィールドを公開している場合、`{"IsAdmin": true}` というリクエストボディで +その値が設定されます。専用の DTO を使い、明示的にマッピングしてください。 +::: + +```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) +}) +``` + +## フルーエントバインディング + +単一ソースから明示的かつ型安全にバインドするには、フルーエント binder を使います。 +設定をチェーンし、実行時にエラーを収集します。 + +```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 +``` + +利用できる binder は `echo.QueryParamsBinder(c)`、`echo.PathValuesBinder(c)`、 +`echo.FormFieldBinder(c)` です。チェーンは `BindError()`(最初のエラー)または +`BindErrors()`(すべてのエラー)で終了します。`FailFast(false)` はチェーン全体を実行します。 +デフォルトでは早期終了が有効です。 + +各サポート型には `Type(...)`、`MustType(...)`、`Types(...)`(スライス)、 +`MustTypes(...)` メソッドがあります。例:`Int64`、`MustInt64`、`Int64s`。 +カンマ区切りの値を分割するには `BindWithDelimiter("id", &dest, ",")` を使います。 + +## カスタム binder + +`Echo#Binder` でカスタム 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/ja/guide/context.md b/site/src/content/docs/ja/guide/context.md new file mode 100644 index 00000000..3c79522c --- /dev/null +++ b/site/src/content/docs/ja/guide/context.md @@ -0,0 +1,80 @@ +--- +title: コンテキスト +description: リクエスト、レスポンス、パラメーター、ヘルパーを保持するリクエストごとのオブジェクトです。 +sidebar: + order: 4 +--- + +`echo.Context` は現在の HTTP リクエストのコンテキストを表します。そのポインター +(`*echo.Context`)はすべてのハンドラとミドルウェアに渡され、リクエストとレスポンス、 +パスパラメーター、バインド済みデータ、レスポンス作成用のヘルパーを保持します。 + +```go +func handler(c *echo.Context) error { + // ... + return nil +} +``` + +## 入力を読む + +```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) +``` + +値が存在しない場合にデフォルト値を返す、対応する `*Or` ヘルパーもあります。 +`c.ParamOr("id", "0")`、`c.QueryParamOr("page", "1")`、`c.FormValueOr(...)` +などです。 + +## レスポンスを書く + +```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") +``` + +## ファイル + +```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 +``` + +## リクエストごとのストレージ + +`Get`/`Set` を使ってミドルウェアとハンドラの間でデータを共有します。 + +```go +c.Set("user", u) +u, _ := c.Get("user").(*User) +``` + +ジェネリクスヘルパーで型付きアクセスもできます。 + +```go +u, err := echo.ContextGet[*User](c, "user") +``` + +## バインディングと検証 + +`c.Bind()` はリクエストデータを struct に解析します。詳しくは +[バインディング](/ja/guide/binding/)を参照してください。 + +```go +var dto CreateUser +if err := c.Bind(&dto); err != nil { + return echo.ErrBadRequest +} +``` diff --git a/site/src/content/docs/ja/guide/cookies.md b/site/src/content/docs/ja/guide/cookies.md new file mode 100644 index 00000000..11559267 --- /dev/null +++ b/site/src/content/docs/ja/guide/cookies.md @@ -0,0 +1,71 @@ +--- +title: Cookie +description: 標準の http.Cookie 型を使って HTTP Cookie を作成、読み取り、一覧表示します。 +sidebar: + order: 11 +--- + +Cookie はサーバーがブラウザーに送信する小さなデータで、ブラウザーはそれを保存し、 +後続のリクエストで送り返します。Cookie により、ショッピングカート、認証状態、 +以前入力したフォーム値などの状態を Web サイトが記憶できます。 + +Echo は Go 標準の `http.Cookie` 型を使い、ハンドラ内の `echo.Context` から +Cookie を追加および取得します。 + +## Cookie 属性 + +| 属性 | 任意 | +| ---------- | ---- | +| `Name` | いいえ | +| `Value` | いいえ | +| `Path` | はい | +| `Domain` | はい | +| `Expires` | はい | +| `Secure` | はい | +| `HttpOnly` | はい | + +## 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") +} +``` + +- `new(http.Cookie)` で Cookie を作成します。 +- `http.Cookie` フィールドに属性を設定します。 +- `c.SetCookie(cookie)` を呼び出して、レスポンスに `Set-Cookie` header を追加します。 + +## 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") +} +``` + +- `c.Cookie("username")` で名前から Cookie を読み取ります。 +- `http.Cookie` フィールドを通じて属性にアクセスします。 + +## すべての Cookie を読む + +```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/ja/guide/customization.md b/site/src/content/docs/ja/guide/customization.md new file mode 100644 index 00000000..5b69f976 --- /dev/null +++ b/site/src/content/docs/ja/guide/customization.md @@ -0,0 +1,64 @@ +--- +title: カスタマイズ +description: Echo の logger、validator、binder、renderer、serializer、エラー処理をカスタマイズします。 +sidebar: + order: 12 +--- + +Echo は `Echo` インスタンス上に一連のフィールドを公開しており、組み込みの挙動を +独自実装に置き換えられます。 + +## ログ + +`Echo#Logger` は構造化ログを書き込みます。デフォルトのハンドラは JSON を +`os.Stdout` に出力します。 + +### カスタム logger + +logger は `*slog.Logger` なので、任意の `slog` ハンドラを登録できます。 + +```go +e.Logger = slog.New(slog.NewJSONHandler(os.Stdout, nil)) +``` + +## Validator + +`Echo#Validator` はリクエストペイロード検証用の validator を登録します。 + +[詳しく見る](/ja/guide/request/#validate-data) + +## カスタム binder + +`Echo#Binder` はリクエストペイロードをバインドするカスタム binder を登録します。 + +[詳しく見る](/ja/guide/binding/#custom-binder) + +## カスタム JSON serializer + +`Echo#JSONSerializer` はカスタム JSON serializer を登録します。 +[json.go](https://github.com/labstack/echo/blob/master/json.go) の +`DefaultJSONSerializer` を参照してください。 + +## Renderer + +`Echo#Renderer` はテンプレートレンダリング用の renderer を登録します。 + +[詳しく見る](/ja/guide/templates/) + +## HTTP エラーハンドラ + +`Echo#HTTPErrorHandler` はカスタム HTTP エラーハンドラを登録します。 + +[詳しく見る](/ja/guide/error-handling/) + +## ルートコールバック + +`Echo#OnAddRoute` は、新しいルートがルーターに追加されるたびに呼び出される +コールバックを登録します。 + +## IP 抽出器 + +`Echo#IPExtractor` は実際のクライアント IP アドレスをどう判定するかを制御します。 +信頼性と安全性を保って取得するには、アプリケーションがインフラ全体を把握している必要があります。 + +[詳しく見る](/ja/guide/ip-address/) diff --git a/site/src/content/docs/ja/guide/error-handling.md b/site/src/content/docs/ja/guide/error-handling.md new file mode 100644 index 00000000..59a44593 --- /dev/null +++ b/site/src/content/docs/ja/guide/error-handling.md @@ -0,0 +1,82 @@ +--- +title: エラー処理 +description: ハンドラとミドルウェアからエラーを返すことで HTTP エラーを一元処理します。 +sidebar: + order: 6 +--- + +Echo は**一元的な**エラー処理を推奨します。ハンドラとミドルウェアは `error` を返し、 +単一のエラーハンドラがそれを HTTP レスポンスに変換します。これにより、ログ記録と +レスポンス整形を 1 か所にまとめられます。 + +通常の `error` または `*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)` はステータステキスト(例: +`"Unauthorized"`)を使います。Echo には `echo.ErrBadRequest`、`echo.ErrNotFound`、 +`echo.ErrUnauthorized` などの sentinel エラーもあります。 + +## デフォルトエラーハンドラ + +Echo のデフォルトハンドラは JSON で応答します。 + +```json +{ "message": "error connecting to redis" } +``` + +通常の `error` は `500 Internal Server Error` になります(エラーを公開して実行している場合は +元のメッセージが含まれます)。`*HTTPError` は自身のステータスコードとメッセージを使います。 + +## カスタムエラーハンドラ + +`e.HTTPErrorHandler` で独自のハンドラを設定します。エラーページ、通知、 +またはエラーを集中管理システムへ送る場合に有用です。 + +`echo.UnwrapResponse()` でレスポンスがすでに送信済みか確認し、`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 +logger の代わりに、または logger に加えて、中央のハンドラから Sentry、Elasticsearch、 +Splunk などの外部サービスへエラーを転送できます。 +::: diff --git a/site/src/content/docs/ja/guide/installation.md b/site/src/content/docs/ja/guide/installation.md new file mode 100644 index 00000000..e0e776f2 --- /dev/null +++ b/site/src/content/docs/ja/guide/installation.md @@ -0,0 +1,57 @@ +--- +title: インストール +description: Echo v5 を Go module に追加します。 +sidebar: + order: 2 +--- + +Echo は Go module として配布されています:`github.com/labstack/echo/v5`。 + +## 要件 + +Echo v5 には **Go 1.25 以降**が必要です。 + +```bash +go version +``` + +## プロジェクトに追加する + +既存の module 内では次を実行します。 + +```bash +go get github.com/labstack/echo/v5 +``` + +または新しい module を開始します。 + +```bash +mkdir myapp && cd myapp +go mod init myapp +go get github.com/labstack/echo/v5 +``` + +コードで import します。 + +```go +import "github.com/labstack/echo/v5" +``` + +## バージョン + +| バージョン | インポートパス | ステータス | +| ---------- | ----------------------------- | ---------- | +| **v5** | `github.com/labstack/echo/v5` | 現行 | +| v4 | `github.com/labstack/echo/v4` | LTS(保守) | + +:::note +Echo は[セマンティックインポートバージョニング](https://go.dev/blog/v2-go-modules)に従います。 +メジャーバージョンはインポートパスの一部なので、移行中に v4 と v5 を共存させられます。 +::: + +## 最新に保つ + +```bash +go get github.com/labstack/echo/v5 +go mod tidy +``` diff --git a/site/src/content/docs/ja/guide/ip-address.md b/site/src/content/docs/ja/guide/ip-address.md new file mode 100644 index 00000000..fdf710df --- /dev/null +++ b/site/src/content/docs/ja/guide/ip-address.md @@ -0,0 +1,112 @@ +--- +title: IP アドレス +description: プロキシの背後で実際のクライアント IP アドレスを安全に取得します。 +sidebar: + order: 14 +--- + +IP アドレスは HTTP において基本的な役割を持ち、アクセス制御、監査、地理ベースの分析などに +使われます。Echo はそれを取得するために `Context#RealIP()` を提供しています。 + +_実際の_ クライアント IP を取得するのは簡単ではありません。特に L7 プロキシが +アプリケーションの前にある場合はそうです。その場合、実際の IP はプロキシから +HTTP 経由でアプリへ伝えられる必要があります。ただし HTTP header を無条件に信頼してはいけません。 +だまされる可能性があります。**これはセキュリティリスクです。** + +IP を信頼性高く安全に取得するには、アプリケーションがインフラ全体を把握している必要があります。 +Echo では `Echo#IPExtractor` でこれを設定します。 + +:::caution +`Echo#IPExtractor` を明示的に設定しない場合、Echo は従来の挙動にフォールバックします。 +これは安全なデフォルトではありません。 +::: + +適切な方法を見つけるため、まず 2 つの質問から始めます。 + +1. アプリケーションの前に HTTP(L7)プロキシを置いていますか?これにはクラウドの + ロードバランサー(AWS ALB や GCP HTTP LB など)や、オープンソースプロキシ + (Nginx、Envoy、Istio ingress gateway など)が含まれます。 +2. そうであれば、プロキシはどの HTTP header を使ってクライアント IP をアプリケーションへ渡していますか? + +## ケース 1:プロキシなし + +プロキシがない場合(アプリがインターネットに直接向いている場合)、信頼できる唯一のアドレスは +ネットワーク層から得られるものです。クライアントが完全に制御できるため、すべての HTTP header は +信頼できません。 + +`echo.ExtractIPDirect()` を使います。 + +```go +e.IPExtractor = echo.ExtractIPDirect() +``` + +## ケース 2:X-Forwarded-For header を使うプロキシ + +[`X-Forwarded-For` (XFF)](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-For) +は、クライアント IP を中継するもっとも一般的な header です。各ホップで、プロキシは +リクエスト IP を 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 +``` + +この場合、**右から見て最初の信頼できない IP 読み取り値**を使います。左端の最初の値は、 +クライアントが制御するため絶対に使わないでください。ここで「信頼できる」とは、その IP が +自分のインフラに属していると確信できることです。上の例では `b` と `c` が信頼できるなら、 +どちらのケースでもクライアント IP は `a` であり、`x` ではありません。 + +`ExtractIPFromXFFHeader(...TrustOption)` を使います。 + +```go +e.IPExtractor = echo.ExtractIPFromXFFHeader() +``` + +デフォルトでは、loopback、link-local unicast、private-use、 +[RFC 6890](https://datatracker.ietf.org/doc/html/rfc6890)、 +[RFC 4291](https://datatracker.ietf.org/doc/html/rfc4291)、 +[RFC 4193](https://datatracker.ietf.org/doc/html/rfc4193) の unique local アドレスなど、 +内部 IP アドレスを信頼します。これは `TrustOption` で制御できます。 + +```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), +) +``` + +## ケース 3:X-Real-IP header を使うプロキシ + +`X-Real-IP` はクライアント IP を中継するもう 1 つの header ですが、XFF と異なり +単一のアドレスだけを保持します。 + +プロキシがこの header を設定している場合は、`ExtractIPFromRealIPHeader(...TrustOption)` を使います。 + +```go +e.IPExtractor = echo.ExtractIPFromRealIPHeader() +``` + +XFF と同様に、デフォルトでは内部 IP アドレスを信頼し、同じ `TrustOption` を受け付けます。 + +:::danger +インフラのエッジにある最外部プロキシを、**受信 header をそのまま通さない**ように設定することを +**絶対に忘れないでください**。そうしないと、クライアントがそれらを偽造でき、不正への道を開きます。 +::: + +## デフォルトの挙動 + +デフォルトでは、Echo は最初の XFF header、X-Real-IP header、ネットワーク層の IP を同時に考慮します。 + +この記事から分かるように、これは良い選択ではありません。後方互換性のためだけにデフォルトとして残されています。 diff --git a/site/src/content/docs/ja/guide/quickstart.md b/site/src/content/docs/ja/guide/quickstart.md new file mode 100644 index 00000000..fa52bc41 --- /dev/null +++ b/site/src/content/docs/ja/guide/quickstart.md @@ -0,0 +1,76 @@ +--- +title: クイックスタート +description: 5 分以内に本番対応の Echo API を構築します。 +sidebar: + order: 1 +--- + +Echo は高性能でミニマルな Go Web フレームワークです。このガイドでは、5 分以内に +サーバーを起動します。 + +## 要件 + +Echo には **Go 1.25 以降**が必要です。バージョンを確認してください。 + +```bash +go version +``` + +## インストール + +module を作成し、Echo を追加します。 + +```bash +go mod init myapp +go get github.com/labstack/echo/v5 +``` + +## Hello, World + +`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) + } +} +``` + +実行します。 + +```bash +go run main.go +``` + +サーバーは `http://localhost:1323` で動作しています。Echo のルーターは、 +ルートごとのリクエストディスパッチを**動的メモリ割り当てゼロ**で行います。 + +:::tip[Ask Echo] +困ったら右下の **Ask Echo** ボタンを押して +*"How do I add JWT auth?"* と質問してください。回答はこのドキュメントから直接得られます。 +::: + +## 次のステップ + +- [ルーティング](/ja/guide/routing/):静的、パラメーター付き、ワイルドカードのルート。 +- [コンテキスト](/ja/guide/context/):リクエストごとのリクエスト/レスポンスオブジェクト。 +- [バインディング](/ja/guide/binding/):リクエストデータを型付き struct に解析します。 diff --git a/site/src/content/docs/ja/guide/request.md b/site/src/content/docs/ja/guide/request.md new file mode 100644 index 00000000..1f0fc2f1 --- /dev/null +++ b/site/src/content/docs/ja/guide/request.md @@ -0,0 +1,170 @@ +--- +title: リクエスト +description: リクエストからフォーム、クエリ、パスのデータを取得し、検証します。 +sidebar: + order: 7 +--- + +ハンドラは `echo.Context` を通じてリクエストデータを読み取ります。Echo は値を名前で個別に取得し、 +struct にバインドし([バインディング](/ja/guide/binding/)を参照)、登録した validator に +検証を委ねられます。 + +## データを取得する + +### フォームデータ + +`Context#FormValue(name string)` でフォームフィールドを名前から取得します。 + +```go +e.POST("/form", func(c *echo.Context) error { + name := c.FormValue("name") + return c.String(http.StatusOK, name) +}) +``` + +`string` 以外の型には、ジェネリック関数 `echo.FormValue[T]` を使います。 + +```go +age, err := echo.FormValue[int](c, "age") +if err != nil { + return err +} +``` + +次でテストします。 + +```sh +curl -X POST http://localhost:1323/form -d 'name=Joe&age=30' +``` + +カスタムデータ型をバインドするには、`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 +} +``` + +### クエリパラメーター + +`Context#QueryParam(name string)` でクエリパラメーターを名前から取得します。 + +```go +func(c *echo.Context) error { + name := c.QueryParam("name") + return c.String(http.StatusOK, name) +} +``` + +`string` 以外の型には、ジェネリック関数 `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" +``` + +### パスパラメーター + +`Context#Param(name string)` で登録済みのパスパラメーターを名前から取得します。 + +```go +e.GET("/users/:name", func(c *echo.Context) error { + name := c.Param("name") + return c.String(http.StatusOK, name) +}) +``` + +`string` 以外の型には、ジェネリック関数 `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 +``` + +### データをバインドする + +Echo はリクエストデータをネイティブの Go struct や変数にもバインドできます。 +[バインディング](/ja/guide/binding/)を参照してください。 + +## データを検証する + +Echo には組み込みのデータ検証はありません。`Echo#Validator` でカスタム validator を登録し、 +[go-playground/validator](https://github.com/go-playground/validator) などのサードパーティライブラリを +使えます。 + +次の例は、バインド済み struct を検証します。 + +```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/ja/guide/response.md b/site/src/content/docs/ja/guide/response.md new file mode 100644 index 00000000..42611b35 --- /dev/null +++ b/site/src/content/docs/ja/guide/response.md @@ -0,0 +1,318 @@ +--- +title: レスポンス +description: 文字列、HTML、JSON、XML、ファイル、ストリーム、リダイレクト、レスポンスフックを送信します。 +sidebar: + order: 8 +--- + +ハンドラは `echo.Context` を通じてレスポンスを書き込みます。各ヘルパーは適切な +`Content-Type` とステータスコードを設定します。 + +## 文字列を送信する + +`Context#String(code int, s string)` は、ステータスコード付きのプレーンテキストレスポンスを送信します。 + +```go +func(c *echo.Context) error { + return c.String(http.StatusOK, "Hello, World!") +} +``` + +## HTML を送信する + +`Context#HTML(code int, html string)` は、ステータスコード付きのシンプルな HTML レスポンスを送信します。 +HTML を動的に生成するには、[テンプレート](/ja/guide/templates/)を参照してください。 + +```go +func(c *echo.Context) error { + return c.HTML(http.StatusOK, "Hello, World!") +} +``` + +### HTML blob を送信する + +`Context#HTMLBlob(code int, b []byte)` は、ステータスコード付きの HTML blob を送信します。 +`[]byte` を出力するテンプレートエンジンと組み合わせると便利です。 + +```go +func handler(c *echo.Context) error { + blob := []byte("Hello, World!") + return c.HTMLBlob(http.StatusOK, blob) +} +``` + +## テンプレートをレンダリングする + +[テンプレート](/ja/guide/templates/)を参照してください。 + +## JSON を送信する + +`Context#JSON(code int, i any)` は Go の値を JSON としてエンコードし、ステータスコード付きで送信します。 + +```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) +} +``` + +### JSON をストリーミングする + +`Context#JSON()` は内部で `json.Marshal` を使うため、大きなペイロードでは非効率な場合があります。 +その場合は JSON を直接ストリーミングします。 + +```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)` は整形済み JSON レスポンスを送信します。 +インデントにはスペースまたはタブを使えます。 + +```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" +} +``` + +### JSON blob + +`Context#JSONBlob(code int, b []byte)` は、たとえばデータベース由来の +事前エンコード済み JSON blob を直接送信します。 + +```go +func(c *echo.Context) error { + encodedJSON := []byte{} // Encoded JSON from an external source. + return c.JSONBlob(http.StatusOK, encodedJSON) +} +``` + +## JSONP を送信する + +`Context#JSONP(code int, callback string, i any)` は Go の値を JSON としてエンコードし、 +指定した callback でラップした JSONP ペイロードとして送信します。 + +```go +func handler(c *echo.Context) error { + callback := c.QueryParam("callback") + return c.JSONP(http.StatusOK, callback, &User{Name: "Jon", Email: "jon@labstack.com"}) +} +``` + +[JSONP cookbook](/ja/cookbook/jsonp/)を参照してください。 + +## XML を送信する + +`Context#XML(code int, i any)` は Go の値を XML としてエンコードし、ステータスコード付きで送信します。 + +```go +func(c *echo.Context) error { + u := &User{ + Name: "Jon", + Email: "jon@labstack.com", + } + return c.XML(http.StatusOK, u) +} +``` + +### XML をストリーミングする + +`Context#XML` は内部で `xml.Marshal` を使うため、大きなペイロードでは非効率な場合があります。 +その場合は XML を直接ストリーミングします。 + +```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)` は整形済み XML レスポンスを送信します。 +インデントにはスペースまたはタブを使えます。 + +```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 +リクエスト URL のクエリ文字列に `pretty` を追加すると、`Context#XML()` でも +整形済み XML を出力できます。 + +```sh +curl http://localhost:1323/users/1?pretty +``` +::: + +### XML blob + +`Context#XMLBlob(code int, b []byte)` は、たとえばデータベース由来の +事前エンコード済み XML blob を直接送信します。 + +```go +func(c *echo.Context) error { + encodedXML := []byte{} // Encoded XML from an external source. + return c.XMLBlob(http.StatusOK, encodedXML) +} +``` + +## ファイルを送信する + +`Context#File(file string)` はファイルの内容をレスポンスとして送信します。 +正しいコンテンツタイプを設定し、キャッシュも自動的に処理します。 + +```go +func(c *echo.Context) error { + return c.File("") +} +``` + +## 添付ファイルを送信する + +`Context#Attachment(file, name string)` は `File()` と似ていますが、 +`Content-Disposition: attachment` と指定した名前でファイルを送信します。 + +```go +func(c *echo.Context) error { + return c.Attachment("", "") +} +``` + +## インライン送信する + +`Context#Inline(file, name string)` は `File()` と似ていますが、 +`Content-Disposition: inline` と指定した名前でファイルを送信します。 + +```go +func(c *echo.Context) error { + return c.Inline("", "") +} +``` + +## blob を送信する + +`Context#Blob(code int, contentType string, b []byte)` は、指定したコンテンツタイプと +ステータスコードで任意のデータを送信します。 + +```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) +} +``` + +## ストリームを送信する + +`Context#Stream(code int, contentType string, r io.Reader)` は、指定したコンテンツタイプ、 +`io.Reader`、ステータスコードで任意のデータストリームを送信します。 + +```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) +} +``` + +## ボディなしで送信する + +`Context#NoContent(code int)` は、ステータスコード付きの空ボディを送信します。 + +```go +func(c *echo.Context) error { + return c.NoContent(http.StatusOK) +} +``` + +## リクエストをリダイレクトする + +`Context#Redirect(code int, url string)` は、指定した URL とステータスコードでリクエストをリダイレクトします。 + +```go +func(c *echo.Context) error { + return c.Redirect(http.StatusMovedPermanently, "") +} +``` + +## フック + +### レスポンス前 + +`Response#Before(func())` は、レスポンスが書き込まれる直前に実行される関数を登録します。 + +### レスポンス後 + +`Response#After(func())` は、レスポンスが書き込まれた直後に実行される関数を登録します。 +`Content-Length` が不明な場合、after 関数は実行されません。 + +```go +e.GET("/hooks", func(c *echo.Context) error { + resp, err := echo.UnwrapResponse(c.Response()) + if err != nil { + return err + } + resp.Before(func() { + println("before response") + }) + resp.After(func() { + println("after response") + }) + return c.String(http.StatusOK, "Hello, World!") +}) +``` + +:::tip +複数の `Before` 関数と `After` 関数を登録できます。 +::: diff --git a/site/src/content/docs/ja/guide/routing.md b/site/src/content/docs/ja/guide/routing.md new file mode 100644 index 00000000..1da2c349 --- /dev/null +++ b/site/src/content/docs/ja/guide/routing.md @@ -0,0 +1,77 @@ +--- +title: ルーティング +description: Echo のゼロ割り当て radix tree でリクエスト URL をハンドラにマッチさせます。 +sidebar: + order: 3 +--- + +Echo の最適化されたルーターは、radix tree を使ってリクエスト URL をハンドラへマッチさせ、 +**動的メモリ割り当てゼロ**とスマートなルート優先順位を実現します。 + +## ルートを登録する + +`Echo` インスタンス上の HTTP メソッドヘルパーを使います。各ヘルパーはパスパターンと +`HandlerFunc`(`func(c *echo.Context) error`)を受け取り、任意でルートレベルの +ミドルウェアも受け取れます。 + +```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` はサポートされているすべてのメソッドにハンドラを登録し、`Match` は指定した集合に登録します。 + +```go +e.Any("/ping", pong) +e.Match([]string{http.MethodGet, http.MethodPost}, "/form", handleForm) +``` + +## マッチ種別 + +| パターン | 種別 | マッチ例 | +| ------------------ | ------------ | -------------------------- | +| `/users/profile` | 静的 | `/users/profile` | +| `/users/:id` | パラメーター | `/users/42` | +| `/static/*` | ワイルドカード | `/static/css/app.css` | + +:::note +優先順位は**静的 → パラメーター → ワイルドカード**です。そのため +`/users/profile` は常に `/users/:id` に勝ち、`/users/:id` は `/users/*` に勝ちます。 +::: + +## パスパラメーター + +`c.Param()` でコンテキストから名前付きパラメーターを読み取ります +(デフォルト値には `c.ParamOr()` を使います)。 + +```go +func getUser(c *echo.Context) error { + id := c.Param("id") + return c.String(http.StatusOK, id) +} +``` + +ワイルドカードセグメントは `*` パラメーターとして取得できます。 + +```go +e.GET("/files/*", func(c *echo.Context) error { + return c.String(http.StatusOK, c.Param("*")) +}) +``` + +## グループ + +`e.Group()` を使い、プレフィックスとミドルウェアを共有するルートをグループ化します。 + +```go +admin := e.Group("/admin", middleware.BasicAuth(authFn)) +admin.GET("/metrics", metrics) // -> /admin/metrics +admin.GET("/users", listUsers) // -> /admin/users +``` + +グループはネストでき、より大きなルートツリーを構成できます。 diff --git a/site/src/content/docs/ja/guide/static-files.md b/site/src/content/docs/ja/guide/static-files.md new file mode 100644 index 00000000..83d69779 --- /dev/null +++ b/site/src/content/docs/ja/guide/static-files.md @@ -0,0 +1,88 @@ +--- +title: 静的ファイルの配信 +description: Echo で画像、JavaScript、CSS、フォント、その他のアセットを配信します。 +sidebar: + order: 9 +--- + +Echo はファイルシステムまたは埋め込みファイルシステムから、画像、JavaScript、CSS、 +PDF、フォントなどの静的アセットを配信できます。 + +## デフォルトファイルシステム + +Echo は現在の作業ディレクトリをルートにした `os.DirFS(".")` をデフォルトファイルシステムとして使います。 +変更するには `Echo#Filesystem` フィールドを設定します。 + +```go +e := echo.New() +e.Filesystem = os.DirFS("assets") +``` + +## Static ミドルウェアを使う + +[Static ミドルウェア](/ja/middleware/static/)を参照してください。 + +## Echo#Static() を使う + +`Echo#Static(prefix, root string)` は、指定した root ディレクトリからパス prefix 配下の +静的ファイルを配信するルートを登録します。 + +`assets` から `/static/*` 配下で任意のファイルを配信します。`/static/js/main.js` へのリクエストは +`assets/js/main.js` を配信します。 + +```go +e := echo.New() +e.Static("/static", "assets") +``` + +`assets` から `/*` 配下で任意のファイルを配信します。`/js/main.js` へのリクエストは +`assets/js/main.js` を配信します。 + +```go +e := echo.New() +e.Static("/", "assets") +``` + +## Echo#StaticFS() を使う + +静的ファイルは `embed.FS` を含む任意の `fs.FS` から配信できます。配信されるファイルが +正しいサブディレクトリをルートにするよう、`echo.MustSubFS` を使います。`embed.FS` は +サブディレクトリもそれぞれのエントリとして含みます。 + +```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) + } +} +``` + +## Echo#File() を使う + +`Echo#File(path, file string)` は、単一の静的ファイルを配信するルートを登録します。 + +`public/index.html` からインデックスページを配信します。 + +```go +e.File("/", "public/index.html") +``` + +`app/assets/favicon.ico` から favicon を配信します。 + +```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 +ファイルパス先頭の `/` は、ほとんどの `fs.FS` 実装では機能しません。相対パスを使ってください。 +::: diff --git a/site/src/content/docs/ja/guide/templates.md b/site/src/content/docs/ja/guide/templates.md new file mode 100644 index 00000000..4ae9cf26 --- /dev/null +++ b/site/src/content/docs/ja/guide/templates.md @@ -0,0 +1,132 @@ +--- +title: テンプレート +description: renderer を登録し、任意のエンジンで HTML テンプレートをレンダリングします。 +sidebar: + order: 10 +--- + +`Context#Render(code int, name string, data any) error` はデータでテンプレートをレンダリングし、 +ステータスコード付きの `text/html` レスポンスを送信します。`Echo#Renderer` を設定して +renderer を登録すると、任意のテンプレートエンジンを使えます。 + +## レンダリング + +次の例では Go の `html/template` を使います。 + +デフォルトのテンプレート renderer を使います。 + +```go +e.Renderer = &echo.TemplateRenderer{ + Template: template.Must(template.New("hello").Parse("Hello, {{.}}!")), +} +``` + +または `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. テンプレートを事前コンパイルします。 + + `public/views/hello.html`: + + ```html + {{define "hello"}}Hello, {{.}}!{{end}} + ``` + + ```go + t := &Template{ + templates: template.Must(template.ParseGlob("public/views/*.html")), + } + ``` + +2. renderer を登録します。 + + ```go + e := echo.New() + e.Renderer = t + e.GET("/hello", Hello) + ``` + +3. ハンドラ内でテンプレートをレンダリングします。 + + ```go + func Hello(c *echo.Context) error { + return c.Render(http.StatusOK, "hello", "World") + } + ``` + +## 応用:テンプレートから Echo を呼び出す + +テンプレート内から `Echo#Reverse` を呼び出して URI を生成したい場合があります。 +Go の `html/template` はこれに最適というわけではありませんが、2 つの方法で実現できます。 +テンプレートに渡すすべてのオブジェクトに共通メソッドを用意するか、`map[string]any` を渡して +カスタム renderer 内で拡張する方法です。後者の方が柔軟です。完全な例を示します。 + +`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/ja/guide/testing.md b/site/src/content/docs/ja/guide/testing.md new file mode 100644 index 00000000..8e146cda --- /dev/null +++ b/site/src/content/docs/ja/guide/testing.md @@ -0,0 +1,264 @@ +--- +title: テスト +description: httptest と echotest ヘルパーでハンドラとミドルウェアをテストします。 +sidebar: + order: 13 +--- + +Echo のハンドラとミドルウェアは `echo.Context` を扱う通常の関数なので、標準の +`net/http/httptest` パッケージで簡単にテストできます。`echotest` パッケージは、 +定型コードを減らすヘルパーを提供します。 + +## ハンドラをテストする + +2 つのハンドラを考えます。 + +**CreateUser**:`POST /users` + +- JSON ペイロードを受け取ります。 +- 成功時に `201 Created` を返します。 +- エラー時に `500 Internal Server Error` を返します。 + +**GetUser**:`GET /users/:email` + +- 成功時に `200 OK` を返します。 +- ユーザーが存在しない場合は `404 Not Found`、それ以外のエラーでは + `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()) + } +} +``` + +### echotest ヘルパーを使う + +`echotest.ContextConfig` は、リクエストの宣言的な記述からコンテキスト(と recorder)を構築します。 + +```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()) + } +} +``` + +### フォームペイロードを使う + +```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) +``` + +`echotest` を使った multipart フォームペイロード: + +```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) +} +``` + +### パスパラメーターを設定する + +```go +c.SetPathValues(echo.PathValues{ + {Name: "id", Value: "1"}, + {Name: "email", Value: "jon@labstack.com"}, +}) +``` + +### クエリパラメーターを設定する + +```go +// import "net/url" +q := make(url.Values) +q.Set("email", "jon@labstack.com") +req := httptest.NewRequest(http.MethodGet, "/?"+q.Encode(), nil) +``` + +## ミドルウェアをテストする + +```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 +その他の例は、Echo ソース内の +[ミドルウェアテストケース](https://github.com/labstack/echo/tree/master/middleware)を参照してください。 +::: diff --git a/site/src/content/docs/ja/middleware/basic-auth.md b/site/src/content/docs/ja/middleware/basic-auth.md new file mode 100644 index 00000000..40173d18 --- /dev/null +++ b/site/src/content/docs/ja/middleware/basic-auth.md @@ -0,0 +1,74 @@ +--- +title: Basic Auth +description: ユーザー名とパスワードの認証情報を検証する HTTP Basic 認証ミドルウェアです。 +sidebar: + order: 1 +--- + +Basic Auth ミドルウェアは HTTP Basic 認証を提供します。 + +- 有効な認証情報の場合、次のハンドラを呼び出します。 +- 認証情報がない、または無効な場合は `401 Unauthorized` レスポンスを送信します。 + +## 使い方 + +```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 +})) +``` + +## カスタム設定 + +```go +e.Use(middleware.BasicAuthWithConfig(middleware.BasicAuthConfig{})) +``` + +## 設定 + +```go +type BasicAuthConfig struct { + // Skipper defines a function to skip middleware. + Skipper Skipper + + // Validator validates the credentials. If the request contains multiple basic + // auth headers, it is called once for each header until the first valid result. + // Required. + Validator BasicAuthValidator + + // Realm is the realm attribute of the WWW-Authenticate header. + // Default value "Restricted". + Realm string + + // AllowedCheckLimit sets how many headers are allowed to be checked. This is + // useful in environments such as corporate test setups with application proxies + // restricting access with their own auth scheme. + // Default value 1. + AllowedCheckLimit uint +} +``` + +`Validator` のシグネチャは次のとおりです。 + +```go +type BasicAuthValidator func(c *echo.Context, user string, password string) (bool, error) +``` + +### デフォルト設定 + +```go +// Effective defaults applied when fields are left unset. +BasicAuthConfig{ + Skipper: DefaultSkipper, + Realm: "Restricted", +} +``` + +:::caution[セキュリティ] +タイミング攻撃を防ぐため、認証情報の比較には必ず `subtle.ConstantTimeCompare` を使ってください。 +::: diff --git a/site/src/content/docs/ja/middleware/body-dump.md b/site/src/content/docs/ja/middleware/body-dump.md new file mode 100644 index 00000000..cc579a6f --- /dev/null +++ b/site/src/content/docs/ja/middleware/body-dump.md @@ -0,0 +1,72 @@ +--- +title: ボディダンプ +description: リクエストとレスポンスのペイロードをキャプチャし、ログ記録やデバッグ用のハンドラへ渡します。 +sidebar: + order: 2 +--- + +Body Dump ミドルウェアは、リクエストとレスポンスのペイロードをキャプチャして登録済みハンドラへ渡します。 +一般的にはデバッグやログ記録に使います。 + +:::caution +ファイルのアップロードやダウンロードなど、大きなペイロードには Body Dump を避けてください。 +そのようなルートで使う必要がある場合は、skipper 関数に例外を追加してください。 +::: + +## 使い方 + +```go +e := echo.New() +e.Use(middleware.BodyDump(func(c *echo.Context, reqBody, resBody []byte, err error) { + // Handle the request and response bodies. +})) +``` + +## カスタム設定 + +```go +e := echo.New() +e.Use(middleware.BodyDumpWithConfig(middleware.BodyDumpConfig{})) +``` + +## 設定 + +```go +type BodyDumpConfig struct { + // Skipper defines a function to skip middleware. + Skipper Skipper + + // Handler receives the request and response payloads and the handler error, if any. + // Required. + Handler BodyDumpHandler + + // MaxRequestBytes limits how much of the request body to dump. If the request body + // exceeds this limit, only the first MaxRequestBytes are dumped and the handler + // receives truncated data. + // Default: 5 * MB (5,242,880 bytes). Set to -1 to disable limits (not recommended). + MaxRequestBytes int64 + + // MaxResponseBytes limits how much of the response body to dump. If the response body + // exceeds this limit, only the first MaxResponseBytes are dumped and the handler + // receives truncated data. + // Default: 5 * MB (5,242,880 bytes). Set to -1 to disable limits (not recommended). + MaxResponseBytes int64 +} +``` + +`Handler` のシグネチャは次のとおりです。 + +```go +type BodyDumpHandler func(c *echo.Context, reqBody []byte, resBody []byte, err error) +``` + +### デフォルト設定 + +```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/ja/middleware/body-limit.md b/site/src/content/docs/ja/middleware/body-limit.md new file mode 100644 index 00000000..ba377d6b --- /dev/null +++ b/site/src/content/docs/ja/middleware/body-limit.md @@ -0,0 +1,47 @@ +--- +title: ボディ制限 +description: ボディが設定済みの最大サイズを超えるリクエストを拒否します。 +sidebar: + order: 3 +--- + +Body Limit ミドルウェアはリクエストボディに許可される最大サイズを設定します。 +サイズが設定値を超えた場合、`413 Request Entity Too Large` レスポンスを送信します。 + +この制限は `Content-Length` リクエスト header と実際に読み取られた内容の両方に適用されるため、 +偽装された header に対しても耐性があります。制限値はバイト単位で指定します。 + +## 使い方 + +```go +e := echo.New() +e.Use(middleware.BodyLimit(2_097_152)) // 2 MB +``` + +## カスタム設定 + +```go +e := echo.New() +e.Use(middleware.BodyLimitWithConfig(middleware.BodyLimitConfig{})) +``` + +## 設定 + +```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 +} +``` + +### デフォルト設定 + +```go +// Effective defaults applied when fields are left unset (Limit is required). +BodyLimitConfig{ + Skipper: DefaultSkipper, +} +``` diff --git a/site/src/content/docs/ja/middleware/casbin-auth.md b/site/src/content/docs/ja/middleware/casbin-auth.md new file mode 100644 index 00000000..5b18fe8f --- /dev/null +++ b/site/src/content/docs/ja/middleware/casbin-auth.md @@ -0,0 +1,201 @@ +--- +title: Casbin 認可 +description: 小さなカスタムミドルウェアで Casbin アクセス制御ライブラリを使い、リクエストを認可します。 +sidebar: + order: 4 +--- + +[Casbin](https://github.com/casbin/casbin) は、Go 向けの強力で効率的なオープンソース +アクセス制御ライブラリです。多くのモデルで認可を強制できます。 + +- ACL(Access Control List) +- スーパーユーザー付き ACL +- ユーザーなし ACL:認証やユーザーログインがないシステムに有用 +- リソースなし ACL:個別リソースではなく、リソース種別(例:`write-article`、`read-log`)を対象にする +- RBAC(Role-Based Access Control) +- リソースロール付き RBAC:ユーザーとリソースの両方がロールを持てる +- ドメイン/テナント付き RBAC:ユーザーがドメイン/テナントごとに異なるロールセットを持てる +- ABAC(Attribute-Based Access Control) +- RESTful +- Deny-override:allow と deny の両方のルールをサポートし、deny が allow を上書きする + +詳細は [API overview](https://casbin.org/docs/api-overview) と +[Casbin documentation](https://casbin.org/docs/) を参照してください。 + +## 依存関係 + +```bash +go get github.com/casbin/casbin/v3 +``` + +```go +import ( + "github.com/casbin/casbin/v3" +) +``` + +## 実装 + +Echo には Casbin ミドルウェアは同梱されていません。この連携は Casbin enforcer の小さなラッパーです。 + +```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) + } + } +} +``` + +## 例 + +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 == "*") +``` + +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 +``` + +認証と認可は別の関心事です。JWT や Basic Auth など別のミドルウェアでユーザーを認証し、 +Casbin がリクエストを認可できるよう `userGetter` を渡します。 + +### 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 +``` + +次で試します。 + +```bash +curl -v "http://localhost:8080/dataset1/any" -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ" +``` + +### 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 +``` + +次で試します。 + +```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 +``` + +### 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/ja/middleware/context-timeout.md b/site/src/content/docs/ja/middleware/context-timeout.md new file mode 100644 index 00000000..9656a5ba --- /dev/null +++ b/site/src/content/docs/ja/middleware/context-timeout.md @@ -0,0 +1,47 @@ +--- +title: コンテキストタイムアウト +description: リクエストコンテキストにタイムアウトを適用し、コンテキスト対応の処理を早期に返せるようにします。 +sidebar: + order: 5 +--- + +Context Timeout ミドルウェアは、定義済みの期間内でリクエストコンテキストにタイムアウトを適用し、 +期限を超えたらコンテキスト対応メソッドが早期に返れるようにします。 + +## 使い方 + +```go +e.Use(middleware.ContextTimeout(60 * time.Second)) +``` + +## カスタム設定 + +```go +e.Use(middleware.ContextTimeoutWithConfig(middleware.ContextTimeoutConfig{ + Timeout: 60 * time.Second, +})) +``` + +## 設定 + +```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 +} +``` + +### デフォルト設定 + +```go +// Effective defaults applied when fields are left unset (Timeout is required). +ContextTimeoutConfig{ + Skipper: DefaultSkipper, +} +``` diff --git a/site/src/content/docs/ja/middleware/cors.md b/site/src/content/docs/ja/middleware/cors.md new file mode 100644 index 00000000..65bbb97c --- /dev/null +++ b/site/src/content/docs/ja/middleware/cors.md @@ -0,0 +1,118 @@ +--- +title: CORS +description: 安全なクロスドメインアクセス制御のための Cross-Origin Resource Sharing ミドルウェアです。 +sidebar: + order: 6 +--- + +CORS ミドルウェアは [CORS](https://fetch.spec.whatwg.org/#http-cors-protocol) 仕様を実装します。 +CORS は Web サーバーにクロスドメインアクセス制御を提供し、安全なクロスドメインデータ転送を可能にします。 + +## 使い方 + +```go +e.Use(middleware.CORS("https://example.com", "https://subdomain.example.com")) +``` + +## カスタム設定 + +```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}, +})) +``` + +## 設定 + +```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 +} +``` + +### デフォルト設定 + +```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[セキュリティ] +`AllowCredentials = true` とワイルドカードの `AllowOrigins` を組み合わせてはいけません。 +動的な origin 検証が必要な場合は `UnsafeAllowOriginFunc` を使い、慎重に検証してください。 +攻撃者が悪意ある(サブ)ドメイン名を登録する可能性があります。 +::: diff --git a/site/src/content/docs/ja/middleware/csrf.md b/site/src/content/docs/ja/middleware/csrf.md new file mode 100644 index 00000000..25af2235 --- /dev/null +++ b/site/src/content/docs/ja/middleware/csrf.md @@ -0,0 +1,174 @@ +--- +title: CSRF +description: Sec-Fetch-Site メタデータと token 検証を使った Cross-Site Request Forgery 防御です。 +sidebar: + order: 7 +--- + +Cross-Site Request Forgery(CSRF、"sea-surf" と発音されることもある、または XSRF)は、 +Web サイトが信頼するユーザーから未承認のコマンドが送信される悪意ある攻撃の一種です。 + +## 使い方 + +```go +e.Use(middleware.CSRF()) +``` + +## 仕組み + +CSRF ミドルウェアは、現代的な多層防御として +[`Sec-Fetch-Site`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Sec-Fetch-Site) +header をサポートします。 +[CSRF protection](https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html#fetch-metadata-headers) +のために、従来の token ベース機構と並行して OWASP 推奨の Fetch Metadata API を実装しています。 + +現代的なブラウザーはすべてのリクエストに `Sec-Fetch-Site` header を自動送信し、 +リクエスト元とターゲットの関係を示します。ミドルウェアはこれを使ってセキュリティ判断を行います。 + +- **`same-origin`** または **`none`**:許可(完全な同一 origin、またはユーザーの直接ナビゲーション) +- **`same-site`**:token 検証にフォールバック(例:サブドメインからメインドメイン) +- **`cross-site`**:安全でないメソッド(POST、PUT、DELETE、PATCH)ではデフォルトで `403` エラーによりブロック + +この header を送信しないブラウザー(古いブラウザー)では、従来の token ベース CSRF 防御へシームレスに +フォールバックします。 + +`Sec-Fetch-Site` の挙動は 2 つのオプションで調整できます。 + +- `TrustedOrigins []string`:cross-site リクエストで特定 origin を許可リスト化(OAuth callback、webhook などに有用) +- `AllowSecFetchSiteFunc func(c *echo.Context) (bool, error)`: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 + }, +})) +``` + +## token ベース防御 + +```go +e := echo.New() +e.Use(middleware.CSRFWithConfig(middleware.CSRFConfig{ + TokenLookup: "header:X-XSRF-TOKEN", +})) +``` + +上の例は `X-XSRF-TOKEN` リクエスト header から CSRF token を抽出します。 + +代わりに Cookie から token を読む場合: + +```go +middleware.CSRFWithConfig(middleware.CSRFConfig{ + TokenLookup: "cookie:_csrf", + CookiePath: "/", + CookieDomain: "example.com", + CookieSecure: true, + CookieHTTPOnly: true, + CookieSameSite: http.SameSiteStrictMode, +}) +``` + +## CSRF token へのアクセス + +- **サーバー側**:token は `ContextKey` の下でコンテキストから利用でき、テンプレート経由でクライアントへ渡せます。 +- **クライアント側**:token は CSRF Cookie から読み取れます。 + +## 設定 + +```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 +} +``` + +### デフォルト設定 + +```go +var DefaultCSRFConfig = CSRFConfig{ + Skipper: DefaultSkipper, + TokenLength: 32, + TokenLookup: "header:" + echo.HeaderXCSRFToken, + ContextKey: "csrf", + CookieName: "_csrf", + CookieMaxAge: 86400, + CookieSameSite: http.SameSiteDefaultMode, +} +``` + +## 完全な例 + +完全に実行可能な例は +[echox cookbook](https://github.com/labstack/echox/blob/master/cookbook/csrf/main.go) にあります。 diff --git a/site/src/content/docs/ja/middleware/decompress.md b/site/src/content/docs/ja/middleware/decompress.md new file mode 100644 index 00000000..1c8bfeb8 --- /dev/null +++ b/site/src/content/docs/ja/middleware/decompress.md @@ -0,0 +1,58 @@ +--- +title: 解凍 +description: gzip エンコードされたリクエストボディを透過的に解凍します。 +sidebar: + order: 8 +--- + +Decompress ミドルウェアは、`Content-Encoding` header が `gzip` に設定されている場合に +HTTP リクエストボディを解凍します。 + +:::note +ボディはメモリ内で解凍され、リクエストのライフタイム中(およびガベージコレクションまで)保持されます。 +::: + +## 使い方 + +```go +e.Use(middleware.Decompress()) +``` + +## カスタム設定 + +```go +e := echo.New() +e.Use(middleware.DecompressWithConfig(middleware.DecompressConfig{ + Skipper: middleware.DefaultSkipper, +})) +``` + +## 設定 + +```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 +} +``` + +### デフォルト設定 + +```go +// Effective defaults applied when fields are left unset. +DecompressConfig{ + Skipper: DefaultSkipper, + GzipDecompressPool: &DefaultGzipDecompressPool{}, + MaxDecompressedSize: 100 * MB, +} +``` diff --git a/site/src/content/docs/ja/middleware/gzip.md b/site/src/content/docs/ja/middleware/gzip.md new file mode 100644 index 00000000..2ab2ae44 --- /dev/null +++ b/site/src/content/docs/ja/middleware/gzip.md @@ -0,0 +1,69 @@ +--- +title: Gzip +description: gzip 圧縮方式で HTTP レスポンスを圧縮します。 +sidebar: + order: 9 +--- + +Gzip ミドルウェアは gzip 圧縮方式を使って HTTP レスポンスを圧縮します。 + +## 使い方 + +```go +e.Use(middleware.Gzip()) +``` + +## カスタム設定 + +```go +e := echo.New() +e.Use(middleware.GzipWithConfig(middleware.GzipConfig{ + Level: 5, +})) +``` + +:::tip +skipper を渡すと、特定の URL で gzip を無効化できます。 +::: + +```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 + }, +})) +``` + +## 設定 + +```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 +} +``` + +### デフォルト設定 + +```go +// Effective defaults applied when fields are left unset. +GzipConfig{ + Skipper: DefaultSkipper, + Level: -1, + MinLength: 0, +} +``` diff --git a/site/src/content/docs/ja/middleware/jwt.md b/site/src/content/docs/ja/middleware/jwt.md new file mode 100644 index 00000000..b1a393c8 --- /dev/null +++ b/site/src/content/docs/ja/middleware/jwt.md @@ -0,0 +1,126 @@ +--- +title: JWT +description: echo-jwt module が提供する JSON Web Token 認証ミドルウェアです。 +sidebar: + order: 10 +--- + +JWT ミドルウェアは JSON Web Token(JWT)認証を提供します。これは別 module にあります: +[github.com/labstack/echo-jwt](https://github.com/labstack/echo-jwt)。 + +挙動: + +- 有効な token の場合、コンテキストにユーザーを設定して次のハンドラを呼び出します。 +- 無効な token の場合、`401 Unauthorized` レスポンスを送信します。 +- `Authorization` header がない、または無効な場合、`400 Bad Request` レスポンスを送信します。 + +## 依存関係 + +```go +import "github.com/labstack/echo-jwt/v5" +``` + +## 使い方 + +```go +e.Use(echojwt.JWT([]byte("secret"))) +``` + +## カスタム設定 + +```go +e.Use(echojwt.WithConfig(echojwt.Config{ + SigningKey: []byte("secret"), +})) +``` + +## 設定 + +```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 +} +``` + +## 例 + +完全な例は [JWT cookbook](/ja/cookbook/jwt/) を参照してください。 diff --git a/site/src/content/docs/ja/middleware/key-auth.md b/site/src/content/docs/ja/middleware/key-auth.md new file mode 100644 index 00000000..596625d4 --- /dev/null +++ b/site/src/content/docs/ja/middleware/key-auth.md @@ -0,0 +1,91 @@ +--- +title: Key Auth +description: header、query、form、Cookie から API key を検証する key ベース認証ミドルウェアです。 +sidebar: + order: 11 +--- + +Key Auth ミドルウェアは key ベース認証を提供します。 + +- 有効な key の場合、次のハンドラを呼び出します。 +- 無効な key の場合、`401 Unauthorized` レスポンスを送信します。 +- key がない場合、`400 Bad Request` レスポンスを送信します。 + +## 使い方 + +```go +e.Use(middleware.KeyAuth(func(c *echo.Context, key string, source middleware.ExtractorSource) (bool, error) { + return key == "valid-key", nil +})) +``` + +## カスタム設定 + +```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 + }, +})) +``` + +## 設定 + +```go +type KeyAuthConfig struct { + // Skipper defines a function to skip middleware. + Skipper Skipper + + // KeyLookup is a string in the form ":" or + // ":,:" used to extract the key from the request. + // Optional. Default value "header:Authorization:Bearer ". + // Possible values: + // - "header:" or "header::" + // trims a static prefix from the extracted value. For + // `Authorization: Basic `, the prefix to remove is `Basic `. + // - "query:" + // - "form:" + // - "cookie:" + // Multiple sources example: "header:Authorization,header:X-Api-Key". + KeyLookup string + + // AllowedCheckLimit sets how many KeyLookup values are allowed to be checked. This is + // useful in environments such as corporate test setups with application proxies + // restricting access with their own auth scheme. + AllowedCheckLimit uint + + // Validator validates the key. + // Required. + Validator KeyAuthValidator + + // ErrorHandler defines a function executed when all lookups have been done and none + // passed the Validator. It runs with the last missing (ErrExtractionValueMissing) or + // invalid key, and may be used to define a custom error. + // + // Note: when the error handler swallows the error (returns nil), the middleware + // continues the handler chain. This is useful when part of your site/api is public + // and offers extra features for authorized users. + ErrorHandler KeyAuthErrorHandler + + // ContinueOnIgnoredError allows the next middleware/handler to be called when the + // ErrorHandler ignores the error (returns nil). + ContinueOnIgnoredError bool +} +``` + +`Validator` のシグネチャは次のとおりです。 + +```go +type KeyAuthValidator func(c *echo.Context, key string, source ExtractorSource) (bool, error) +``` + +### デフォルト設定 + +```go +DefaultKeyAuthConfig = KeyAuthConfig{ + Skipper: DefaultSkipper, + KeyLookup: "header:" + echo.HeaderAuthorization + ":Bearer ", +} +``` diff --git a/site/src/content/docs/ja/middleware/logger.md b/site/src/content/docs/ja/middleware/logger.md new file mode 100644 index 00000000..2c4656e7 --- /dev/null +++ b/site/src/content/docs/ja/middleware/logger.md @@ -0,0 +1,236 @@ +--- +title: リクエストロガー +description: 構造化ログライブラリと統合できる、完全にカスタマイズ可能なリクエストログです。 +sidebar: + order: 12 +--- + +`RequestLogger` ミドルウェアは各 HTTP リクエストの情報をログに記録します。 +何をどのように記録するかを完全にカスタマイズできるため、サードパーティの +(構造化ログ)ライブラリとの利用に適しています。 + +logger が抽出できる値は、`RequestLoggerConfig` の bool フィールドと slice フィールドで制御されます。 +フィールドを有効化する(例:`LogStatus: true`)と、その値が `LogValuesFunc` に渡される +`RequestLoggerValues` に設定されます。 + +```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 +} +``` + +## 例 + +### 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 + }, +})) +``` + +出力例: + +```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 + }, +})) +``` + +出力例: + +```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 + }, +})) +``` + +出力例: + +```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 + }, +})) +``` + +出力例: + +```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 + }, +})) +``` + +出力例: + +```text +time="2024-12-30T21:08:49+08:00" level=info msg=request URI=/hello status=200 +``` + +## トラブルシューティング + +### panic: missing LogValuesFunc callback function for request logger middleware + +この panic は、必須の `LogValuesFunc` callback が未設定のときに発生します。 +`LogValuesFunc` シグネチャに一致する関数を定義し、設定に割り当ててください。 + +```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, +})) +``` + +### ログ内のパラメーターが空 + +`LogValuesFunc` 内で `v.URI` や `v.Status` などが空の場合、対応する抽出フラグ +(`LogStatus`、`LogURI` など)が設定で `true` になっているか確認してください。 +各値は、そのフラグが有効な場合にのみ設定されます。 diff --git a/site/src/content/docs/ja/middleware/method-override.md b/site/src/content/docs/ja/middleware/method-override.md new file mode 100644 index 00000000..f47fec0e --- /dev/null +++ b/site/src/content/docs/ja/middleware/method-override.md @@ -0,0 +1,52 @@ +--- +title: メソッドオーバーライド +description: header、form、query の値で POST リクエストの HTTP メソッドを上書きします。 +sidebar: + order: 13 +--- + +Method Override ミドルウェアは、リクエストから上書きされたメソッドを読み取り、 +元のメソッドの代わりに使います。 + +:::note +セキュリティ上の理由により、上書きできるのは `POST` メソッドだけです。 +::: + +## 使い方 + +```go +e.Pre(middleware.MethodOverride()) +``` + +## カスタム設定 + +```go +e := echo.New() +e.Pre(middleware.MethodOverrideWithConfig(middleware.MethodOverrideConfig{ + Getter: middleware.MethodFromForm("_method"), +})) +``` + +メソッドは `MethodFromHeader`、`MethodFromForm`、`MethodFromQuery` から取得できます。 + +## 設定 + +```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 +} +``` + +### デフォルト設定 + +```go +DefaultMethodOverrideConfig = MethodOverrideConfig{ + Skipper: DefaultSkipper, + Getter: MethodFromHeader(echo.HeaderXHTTPMethodOverride), +} +``` diff --git a/site/src/content/docs/ja/middleware/open-telemetry.md b/site/src/content/docs/ja/middleware/open-telemetry.md new file mode 100644 index 00000000..cef06a72 --- /dev/null +++ b/site/src/content/docs/ja/middleware/open-telemetry.md @@ -0,0 +1,81 @@ +--- +title: OpenTelemetry +description: Echo の HTTP リクエスト向け OpenTelemetry インストルメンテーションです。 +sidebar: + order: 14 +--- + +[Echo OpenTelemetry](https://github.com/labstack/echo-opentelemetry) は、HTTP リクエスト向けの +OpenTelemetry インストルメンテーションを提供するミドルウェアです。 + +OpenTelemetry は、クラウドネイティブアプリケーション向けのインストルメンテーションを提供する +オープンソースツール群です。 + +- [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/) + +## 使い方 + +Go modules で OpenTelemetry ミドルウェア依存関係を追加します。 + +```bash +go get github.com/labstack/echo-opentelemetry +``` + +ミドルウェアと OpenTelemetry trace API を import します。 + +```go +import ( + echootel "github.com/labstack/echo-opentelemetry" + "go.opentelemetry.io/otel/trace" +) +``` + +完全な設定で登録します。 + +```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, +})) +``` + +設定オプションについては [`Config`](https://github.com/labstack/echo-opentelemetry/blob/main/otel.go#L28) struct を参照してください。 + +サーバー名だけを指定する簡略形式でミドルウェアを追加します。 + +```go +e.Use(echootel.NewMiddleware("app.example.com")) +``` + +設定オプション付きでミドルウェアを追加します。 + +```go +e.Use(echootel.NewMiddlewareWithConfig(echootel.Config{ + TracerProvider: tp, +})) +``` + +Echo コンテキストから tracer を取得します。 + +```go +tracer, err := echo.ContextGet[trace.Tracer](c, echootel.TracerKey) +``` + +## 例 + +[example](https://github.com/labstack/echo-opentelemetry/blob/main/example/main.go) は +metrics と spans を stdout にエクスポートしますが、任意の exporter(OTLP など)を使えます。 +[OpenTelemetry exporters](https://opentelemetry.io/docs/languages/go/exporters) ドキュメントを参照してください。 diff --git a/site/src/content/docs/ja/middleware/prometheus.md b/site/src/content/docs/ja/middleware/prometheus.md new file mode 100644 index 00000000..663ca94b --- /dev/null +++ b/site/src/content/docs/ja/middleware/prometheus.md @@ -0,0 +1,284 @@ +--- +title: Prometheus +description: Echo の HTTP リクエスト向け Prometheus metrics を生成します。 +sidebar: + order: 15 +--- + +[Echo Prometheus](https://github.com/labstack/echo-prometheus) ミドルウェアは、HTTP リクエスト向けの +Prometheus metrics を生成します。 + +## 使い方 + +必要な module を追加します。 + +```bash +go get github.com/labstack/echo-prometheus +``` + +Prometheus ミドルウェアと、収集した metrics を提供するルートを追加します。 + +```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 +``` + +## 例 + +metrics を収集する同じサーバーから metrics を提供します。 + +```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) + } +} +``` + +別ポートで metrics を提供します。 + +```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) + } +} +``` + +出力例(最初の例): + +```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 +... +``` + +## カスタム設定 + +### カスタム Prometheus metrics を提供する + +Prometheus のデフォルト registry でカスタム metrics を使います。 + +```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) + } +} +``` + +または独自の registry を作成し、そこにカスタム metrics を登録します。 + +```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) + } +} +``` + +### URL をスキップする + +skipper を渡すと、特定 URL の metrics 生成を避けられます。 + +```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) + } +} +``` + +## 複雑なシナリオ + +デフォルトの `echoprometheus` metric 定義を変更します。 + +```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/ja/middleware/proxy.md b/site/src/content/docs/ja/middleware/proxy.md new file mode 100644 index 00000000..163d4c28 --- /dev/null +++ b/site/src/content/docs/ja/middleware/proxy.md @@ -0,0 +1,134 @@ +--- +title: プロキシ +description: 負荷分散付きの HTTP と WebSocket のリバースプロキシミドルウェアです。 +sidebar: + order: 16 +--- + +Proxy は HTTP/WebSocket リバースプロキシミドルウェアを提供します。設定された負荷分散方式を使って、 +リクエストを上流サーバーへ転送します。 + +## 使い方 + +```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, + }, +}))) +``` + +## カスタム設定 + +```go +e := echo.New() +e.Use(middleware.ProxyWithConfig(middleware.ProxyConfig{})) +``` + +## 設定 + +```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 +} +``` + +### デフォルト設定 + +| 名前 | 値 | +| ---------- | -------------- | +| Skipper | DefaultSkipper | +| ContextKey | `target` | + +### 正規表現ベースのルール + +プロキシリクエストの高度な書き換えでは、正規表現でルールを定義することもできます。 +通常のキャプチャグループは `()` で定義し、書き換え後のパスでインデックス +(`$1`、`$2` など)により参照できます。 + +`RegexRewrite` と通常の `Rewrite` ルールは組み合わせられます。 + +```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", + }, +})) +``` + +完全な例は[リバースプロキシ](/ja/cookbook/reverse-proxy/) cookbook を参照してください。 diff --git a/site/src/content/docs/ja/middleware/rate-limiter.md b/site/src/content/docs/ja/middleware/rate-limiter.md new file mode 100644 index 00000000..584ed20a --- /dev/null +++ b/site/src/content/docs/ja/middleware/rate-limiter.md @@ -0,0 +1,108 @@ +--- +title: レートリミッター +description: 特定の IP または識別子からのリクエスト数を一定期間内で制限します。 +sidebar: + order: 17 +--- + +`RateLimiter` は、特定の IP または識別子からサーバーへ送信されるリクエスト数を一定期間内で +制限するレートリミッターミドルウェアを提供します。 + +デフォルトでは、インメモリ store がリクエストを追跡します。デフォルトのインメモリ実装は +正確性に重点を置いており、大量の同時リクエストや多数の異なる識別子(>16k)には最適ではない場合があります。 + +## 使い方 + +アプリケーションにレート制限を追加するには、`RateLimiter` ミドルウェアを追加します。 +次の例では、デフォルトのインメモリ store を使ってアプリケーションを 20 requests/sec に制限します。 + +```go +e.Use(middleware.RateLimiter(middleware.NewRateLimiterMemoryStore(20.0))) +``` + +:::note +指定した rate が浮動小数点数の場合、`Burst` は rate を切り捨てた値として扱われます。 +::: + +## カスタム設定 + +```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)) +``` + +### エラー + +```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 +独自の store を実装するには、`RateLimiterStore` インターフェイスを満たして +`RateLimiterConfig` に渡してください。 +::: + +## 設定 + +```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 +} +``` + +### デフォルト設定 + +```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/ja/middleware/recover.md b/site/src/content/docs/ja/middleware/recover.md new file mode 100644 index 00000000..08160d00 --- /dev/null +++ b/site/src/content/docs/ja/middleware/recover.md @@ -0,0 +1,59 @@ +--- +title: Recover +description: チェーン内のどこで起きた panic からも復旧し、一元化されたエラーハンドラへ委譲します。 +sidebar: + order: 18 +--- + +Recover ミドルウェアは、チェーン内のどこで発生した panic からも復旧し、スタックトレースを出力して、 +一元化された [HTTPErrorHandler](/ja/guide/customization/#http-error-handler) に制御を渡します。 + +## 使い方 + +```go +e.Use(middleware.Recover()) +``` + +## カスタム設定 + +```go +e := echo.New() +e.Use(middleware.RecoverWithConfig(middleware.RecoverConfig{ + StackSize: 1 << 10, // 1 KB +})) +``` + +上の例では `StackSize` を 1 KB にし、`DisableStackAll` と `DisablePrintStack` はデフォルト値を使います。 + +## 設定 + +```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 +} +``` + +### デフォルト設定 + +```go +var DefaultRecoverConfig = RecoverConfig{ + Skipper: DefaultSkipper, + StackSize: 4 << 10, // 4 KB + DisableStackAll: false, + DisablePrintStack: false, +} +``` diff --git a/site/src/content/docs/ja/middleware/redirect.md b/site/src/content/docs/ja/middleware/redirect.md new file mode 100644 index 00000000..918c2dd8 --- /dev/null +++ b/site/src/content/docs/ja/middleware/redirect.md @@ -0,0 +1,100 @@ +--- +title: リダイレクト +description: HTTP/HTTPS と www/non-www のバリエーション間でリクエストをリダイレクトします。 +sidebar: + order: 19 +--- + +## HTTPS リダイレクト + +HTTPS redirect ミドルウェアは HTTP リクエストを HTTPS へリダイレクトします。 +たとえば `http://labstack.com` は `https://labstack.com` へリダイレクトされます。 + +### 使い方 + +```go +e := echo.New() +e.Pre(middleware.HTTPSRedirect()) +``` + +## HTTPS WWW リダイレクト + +HTTPS WWW redirect は HTTP リクエストを www HTTPS へリダイレクトします。 +たとえば `http://labstack.com` は `https://www.labstack.com` へリダイレクトされます。 + +### 使い方 + +```go +e := echo.New() +e.Pre(middleware.HTTPSWWWRedirect()) +``` + +## HTTPS NonWWW リダイレクト + +HTTPS NonWWW redirect は HTTP リクエストを non-www HTTPS へリダイレクトします。 +たとえば `http://www.labstack.com` は `https://labstack.com` へリダイレクトされます。 + +### 使い方 + +```go +e := echo.New() +e.Pre(middleware.HTTPSNonWWWRedirect()) +``` + +## WWW リダイレクト + +WWW redirect は non-www リクエストを www へリダイレクトします。 +たとえば `http://labstack.com` は `http://www.labstack.com` へリダイレクトされます。 + +### 使い方 + +```go +e := echo.New() +e.Pre(middleware.WWWRedirect()) +``` + +## NonWWW リダイレクト + +NonWWW redirect は www リクエストを non-www へリダイレクトします。 +たとえば `http://www.labstack.com` は `http://labstack.com` へリダイレクトされます。 + +### 使い方 + +```go +e := echo.New() +e.Pre(middleware.NonWWWRedirect()) +``` + +## カスタム設定 + +```go +e := echo.New() +e.Use(middleware.HTTPSRedirectWithConfig(middleware.RedirectConfig{ + Code: http.StatusTemporaryRedirect, +})) +``` + +上の例は、HTTP リクエストをステータスコード `307 - StatusTemporaryRedirect` で HTTPS へリダイレクトします。 + +## 設定 + +```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 +} +``` + +### デフォルト設定 + +```go +// Effective defaults applied when fields are left unset. +RedirectConfig{ + Skipper: DefaultSkipper, + Code: http.StatusMovedPermanently, +} +``` diff --git a/site/src/content/docs/ja/middleware/request-id.md b/site/src/content/docs/ja/middleware/request-id.md new file mode 100644 index 00000000..331fb759 --- /dev/null +++ b/site/src/content/docs/ja/middleware/request-id.md @@ -0,0 +1,89 @@ +--- +title: リクエスト ID +description: 各リクエストに一意の ID を生成します。 +sidebar: + order: 20 +--- + +Request ID ミドルウェアはリクエストに一意の ID を生成します。 + +## 使い方 + +```go +e.Use(middleware.RequestID()) +``` + +例: + +```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) + } +} +``` + +## カスタム設定 + +```go +e.Use(middleware.RequestIDWithConfig(middleware.RequestIDConfig{ + Generator: func() string { + return customGenerator() + }, +})) +``` + +## 設定 + +```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 +} +``` + +### デフォルト設定 + +```go +// Effective defaults applied when fields are left unset. +RequestIDConfig{ + Skipper: DefaultSkipper, + Generator: generator, // random 32-character string + TargetHeader: echo.HeaderXRequestID, +} +``` + +## ID を設定する + +リクエスト元は `X-Request-ID` header で ID を設定できます。 + +### リクエスト + +```sh +curl -H "X-Request-ID: 3" --compressed -v "http://localhost:1323/?my=param" +``` + +### ログ + +```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/ja/middleware/rewrite.md b/site/src/content/docs/ja/middleware/rewrite.md new file mode 100644 index 00000000..68ba1e1e --- /dev/null +++ b/site/src/content/docs/ja/middleware/rewrite.md @@ -0,0 +1,87 @@ +--- +title: リライト +description: 設定されたルールに基づいて URL パスを書き換えます。 +sidebar: + order: 21 +--- + +Rewrite ミドルウェアは、指定されたルールに基づいて URL パスを書き換えます。 +後方互換性を保つ場合や、より短く分かりやすいリンクを作る場合に役立ちます。 + +## 使い方 + +```go +e.Pre(middleware.Rewrite(map[string]string{ + "/old": "/new", + "/api/*": "/$1", + "/js/*": "/public/javascripts/$1", + "/users/*/orders/*": "/user/$1/order/$2", +})) +``` + +アスタリスクでキャプチャされた値は、`$1`、`$2` などのインデックスで取得できます。 +各アスタリスクは非貪欲です(キャプチャグループ `(.*?)` に変換されます)。 +複数のアスタリスクを使う場合、末尾の `*` は残りのパスにマッチします。 + +:::caution +Rewrite ミドルウェアは、ルーターより前に実行されるよう `Echo#Pre()` で登録してください。 +::: + +## カスタム設定 + +```go +e := echo.New() +e.Pre(middleware.RewriteWithConfig(middleware.RewriteConfig{})) +``` + +## 設定 + +```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 +} +``` + +デフォルト設定: + +| 名前 | 値 | +| ------- | -------------- | +| Skipper | DefaultSkipper | + +### 正規表現ベースのルール + +パスの高度な書き換えでは、正規表現でルールを定義することもできます。 +通常のキャプチャグループは `()` で定義し、書き換え後のパスでインデックス +(`$1`、`$2` など)により参照できます。 + +`RegexRules` と通常の `Rules` は組み合わせられます。 + +```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/ja/middleware/secure.md b/site/src/content/docs/ja/middleware/secure.md new file mode 100644 index 00000000..12121cb7 --- /dev/null +++ b/site/src/content/docs/ja/middleware/secure.md @@ -0,0 +1,113 @@ +--- +title: セキュア +description: XSS、コンテンツスニッフィング、クリックジャッキング、その他のインジェクション攻撃を防ぎます。 +sidebar: + order: 22 +--- + +Secure ミドルウェアは、クロスサイトスクリプティング(XSS)、コンテンツタイプスニッフィング、 +クリックジャッキング、安全でない接続、その他のコードインジェクション攻撃への防御を提供します。 + +## 使い方 + +```go +e.Use(middleware.Secure()) +``` + +## カスタム設定 + +```go +e := echo.New() +e.Use(middleware.SecureWithConfig(middleware.SecureConfig{ + XSSProtection: "", + ContentTypeNosniff: "", + XFrameOptions: "", + HSTSMaxAge: 3600, + ContentSecurityPolicy: "default-src 'self'", +})) +``` + +:::note +空の `XSSProtection`、`ContentTypeNosniff`、`XFrameOptions`、または +`ContentSecurityPolicy` を渡すと、その防御が無効になります。 +::: + +## 設定 + +```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 ,