From 6d002e954385fa91062d62bcad432468651909f0 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sat, 17 May 2025 06:50:16 +0200 Subject: [PATCH] Adding upstream version 2.52.6. Signed-off-by: Daniel Baumann --- .editorconfig | 8 + .gitattributes | 12 + .github/.editorconfig | 23 + .github/.hound.yml | 2 + .github/CODEOWNERS | 1 + .github/CODE_OF_CONDUCT.md | 81 + .github/CONTRIBUTING.md | 27 + .github/FUNDING.yml | 8 + .github/ISSUE_TEMPLATE/bug-report.yaml | 85 + .github/ISSUE_TEMPLATE/feature-request.yaml | 60 + .github/ISSUE_TEMPLATE/question.yaml | 55 + .github/README.md | 716 ++ .github/README_az.md | 716 ++ .github/README_ckb.md | 714 ++ .github/README_de.md | 687 ++ .github/README_eg.md | 717 ++ .github/README_es.md | 687 ++ .github/README_fa.md | 861 +++ .github/README_fr.md | 689 ++ .github/README_he.md | 865 +++ .github/README_id.md | 689 ++ .github/README_it.md | 712 ++ .github/README_ja.md | 691 ++ .github/README_ko.md | 693 ++ .github/README_nl.md | 693 ++ .github/README_pl.md | 715 ++ .github/README_pt.md | 689 ++ .github/README_ru.md | 696 ++ .github/README_sa.md | 758 +++ .github/README_tr.md | 686 ++ .github/README_uk.md | 721 ++ .github/README_zh-CN.md | 695 ++ .github/README_zh-TW.md | 725 ++ .github/SECURITY.md | 76 + .github/config.yml | 17 + .github/dependabot.yml | 16 + .github/index.html | 14 + .github/labeler.yml | 20 + .github/pull_request_template.md | 31 + .github/release-drafter.yml | 53 + .github/release.yml | 26 + .github/scripts/sync_docs.sh | 75 + .github/testdata/ca-chain.cert.pem | 65 + .github/testdata/favicon.ico | Bin 0 -> 4286 bytes .github/testdata/fs/css/style.css | 4 + .github/testdata/fs/img/fiber.png | Bin 0 -> 1542 bytes .github/testdata/fs/index.html | 12 + .github/testdata/index.html | 1 + .github/testdata/index.tmpl | 1 + .github/testdata/main.tmpl | 1 + .github/testdata/ssl.key | 28 + .github/testdata/ssl.pem | 17 + .github/testdata/template-invalid.html | 1 + .github/testdata/template.tmpl | 1 + .github/testdata/template/hello_world.gohtml | 1 + .github/testdata/testRoutes.json | 1916 ++++++ .github/testdata2/bruh.tmpl | 1 + .github/workflows/auto-labeler.yml | 23 + .github/workflows/benchmark.yml | 50 + .github/workflows/codeql-analysis.yml | 61 + .github/workflows/linter.yml | 28 + .github/workflows/release-drafter.yml | 25 + .github/workflows/sync-docs.yml | 33 + .github/workflows/test.yml | 37 + .github/workflows/vulncheck.yml | 37 + .gitignore | 30 + .golangci.yml | 197 + LICENSE | 21 + Makefile | 55 + app.go | 1120 ++++ app_test.go | 1979 ++++++ client.go | 1021 +++ client_test.go | 1249 ++++ color.go | 107 + ctx.go | 1962 ++++++ ctx_test.go | 5819 +++++++++++++++++ docs/api/_category_.json | 8 + docs/api/app.md | 657 ++ docs/api/client.md | 663 ++ docs/api/constants.md | 291 + docs/api/ctx.md | 2159 ++++++ docs/api/fiber.md | 120 + docs/api/log.md | 155 + docs/api/middleware/_category_.json | 9 + docs/api/middleware/adaptor.md | 169 + docs/api/middleware/basicauth.md | 85 + docs/api/middleware/cache.md | 99 + docs/api/middleware/compress.md | 85 + docs/api/middleware/cors.md | 214 + docs/api/middleware/csrf.md | 256 + docs/api/middleware/earlydata.md | 82 + docs/api/middleware/encryptcookie.md | 101 + docs/api/middleware/envvar.md | 69 + docs/api/middleware/etag.md | 62 + docs/api/middleware/expvar.md | 72 + docs/api/middleware/favicon.md | 63 + docs/api/middleware/filesystem.md | 300 + docs/api/middleware/healthcheck.md | 106 + docs/api/middleware/helmet.md | 82 + docs/api/middleware/idempotency.md | 83 + docs/api/middleware/keyauth.md | 243 + docs/api/middleware/limiter.md | 125 + docs/api/middleware/logger.md | 168 + docs/api/middleware/monitor.md | 81 + docs/api/middleware/pprof.md | 53 + docs/api/middleware/proxy.md | 165 + docs/api/middleware/recover.md | 54 + docs/api/middleware/redirect.md | 68 + docs/api/middleware/requestid.md | 62 + docs/api/middleware/rewrite.md | 58 + docs/api/middleware/session.md | 139 + docs/api/middleware/skip.md | 47 + docs/api/middleware/timeout.md | 146 + docs/extra/_category_.json | 8 + docs/extra/benchmarks.md | 112 + docs/extra/faq.md | 169 + docs/guide/_category_.json | 8 + docs/guide/error-handling.md | 128 + docs/guide/faster-fiber.md | 36 + docs/guide/grouping.md | 79 + docs/guide/hooks.md | 218 + docs/guide/routing.md | 294 + docs/guide/templates.md | 267 + docs/guide/validation.md | 168 + docs/intro.md | 195 + docs/partials/routing/handler.md | 69 + error.go | 40 + error_test.go | 76 + go.mod | 22 + go.sum | 29 + group.go | 209 + helpers.go | 1226 ++++ helpers_fuzz_test.go | 23 + helpers_test.go | 801 +++ hooks.go | 218 + hooks_test.go | 288 + internal/go-ole/LICENSE | 21 + internal/go-ole/com.go | 344 + internal/go-ole/com_func.go | 174 + internal/go-ole/connect.go | 192 + internal/go-ole/constants.go | 153 + internal/go-ole/error.go | 51 + internal/go-ole/error_func.go | 8 + internal/go-ole/error_windows.go | 24 + internal/go-ole/guid.go | 286 + internal/go-ole/iconnectionpoint.go | 20 + internal/go-ole/iconnectionpoint_func.go | 21 + internal/go-ole/iconnectionpoint_windows.go | 43 + internal/go-ole/iconnectionpointcontainer.go | 17 + .../go-ole/iconnectionpointcontainer_func.go | 11 + .../iconnectionpointcontainer_windows.go | 25 + internal/go-ole/idispatch.go | 94 + internal/go-ole/idispatch_func.go | 19 + internal/go-ole/idispatch_windows.go | 200 + internal/go-ole/ienumvariant.go | 19 + internal/go-ole/ienumvariant_func.go | 19 + internal/go-ole/ienumvariant_windows.go | 63 + internal/go-ole/iinspectable.go | 18 + internal/go-ole/iinspectable_func.go | 15 + internal/go-ole/iinspectable_windows.go | 72 + internal/go-ole/iprovideclassinfo.go | 21 + internal/go-ole/iprovideclassinfo_func.go | 7 + internal/go-ole/iprovideclassinfo_windows.go | 21 + internal/go-ole/itypeinfo.go | 34 + internal/go-ole/itypeinfo_func.go | 7 + internal/go-ole/itypeinfo_windows.go | 21 + internal/go-ole/iunknown.go | 57 + internal/go-ole/iunknown_func.go | 19 + internal/go-ole/iunknown_windows.go | 58 + internal/go-ole/ole.go | 157 + internal/go-ole/oleutil/connection.go | 100 + internal/go-ole/oleutil/connection_func.go | 10 + internal/go-ole/oleutil/connection_windows.go | 58 + internal/go-ole/oleutil/go-get.go | 6 + internal/go-ole/oleutil/oleutil.go | 127 + internal/go-ole/safearray.go | 27 + internal/go-ole/safearray_func.go | 211 + internal/go-ole/safearray_windows.go | 337 + internal/go-ole/safearrayconversion.go | 140 + internal/go-ole/safearrayslices.go | 33 + internal/go-ole/utility.go | 101 + internal/go-ole/variables.go | 15 + internal/go-ole/variant.go | 105 + internal/go-ole/variant32.go | 11 + internal/go-ole/variant64.go | 12 + internal/go-ole/variant_date_386.go | 22 + internal/go-ole/variant_date_amd64.go | 20 + internal/go-ole/vt_string.go | 58 + internal/go-ole/winrt.go | 99 + internal/go-ole/winrt_doc.go | 36 + internal/gopsutil/LICENSE | 61 + internal/gopsutil/common/binary.go | 636 ++ internal/gopsutil/common/common.go | 379 ++ internal/gopsutil/common/common_darwin.go | 69 + internal/gopsutil/common/common_freebsd.go | 85 + internal/gopsutil/common/common_linux.go | 269 + internal/gopsutil/common/common_openbsd.go | 69 + internal/gopsutil/common/common_unix.go | 66 + internal/gopsutil/common/common_windows.go | 234 + internal/gopsutil/common/sleep.go | 21 + internal/gopsutil/cpu/cpu.go | 187 + internal/gopsutil/cpu/cpu_darwin.go | 119 + internal/gopsutil/cpu/cpu_darwin_cgo.go | 110 + internal/gopsutil/cpu/cpu_darwin_nocgo.go | 13 + internal/gopsutil/cpu/cpu_dragonfly.go | 164 + internal/gopsutil/cpu/cpu_dragonfly_amd64.go | 9 + internal/gopsutil/cpu/cpu_fallback.go | 30 + internal/gopsutil/cpu/cpu_freebsd.go | 176 + internal/gopsutil/cpu/cpu_freebsd_386.go | 9 + internal/gopsutil/cpu/cpu_freebsd_amd64.go | 9 + internal/gopsutil/cpu/cpu_freebsd_arm.go | 9 + internal/gopsutil/cpu/cpu_freebsd_arm64.go | 9 + internal/gopsutil/cpu/cpu_linux.go | 369 ++ internal/gopsutil/cpu/cpu_openbsd.go | 187 + internal/gopsutil/cpu/cpu_solaris.go | 286 + internal/gopsutil/cpu/cpu_windows.go | 256 + internal/gopsutil/load/load.go | 30 + internal/gopsutil/load/load_bsd.go | 84 + internal/gopsutil/load/load_darwin.go | 75 + internal/gopsutil/load/load_fallback.go | 25 + internal/gopsutil/load/load_freebsd.go | 7 + internal/gopsutil/load/load_linux.go | 135 + internal/gopsutil/load/load_openbsd.go | 17 + internal/gopsutil/load/load_solaris.go | 44 + internal/gopsutil/load/load_windows.go | 84 + internal/gopsutil/mem/mem.go | 106 + internal/gopsutil/mem/mem_darwin.go | 69 + internal/gopsutil/mem/mem_darwin_cgo.go | 58 + internal/gopsutil/mem/mem_darwin_nocgo.go | 93 + internal/gopsutil/mem/mem_fallback.go | 25 + internal/gopsutil/mem/mem_freebsd.go | 167 + internal/gopsutil/mem/mem_linux.go | 283 + internal/gopsutil/mem/mem_openbsd.go | 106 + internal/gopsutil/mem/mem_openbsd_386.go | 37 + internal/gopsutil/mem/mem_openbsd_amd64.go | 32 + internal/gopsutil/mem/mem_solaris.go | 121 + internal/gopsutil/mem/mem_windows.go | 99 + internal/gopsutil/mem/types_openbsd.go | 28 + internal/gopsutil/net/net.go | 262 + internal/gopsutil/net/net_aix.go | 420 ++ internal/gopsutil/net/net_darwin.go | 294 + internal/gopsutil/net/net_fallback.go | 92 + internal/gopsutil/net/net_freebsd.go | 133 + internal/gopsutil/net/net_linux.go | 887 +++ internal/gopsutil/net/net_openbsd.go | 320 + internal/gopsutil/net/net_unix.go | 223 + internal/gopsutil/net/net_windows.go | 779 +++ internal/gopsutil/process/process.go | 322 + internal/gopsutil/process/process_darwin.go | 689 ++ .../gopsutil/process/process_darwin_386.go | 234 + .../gopsutil/process/process_darwin_amd64.go | 234 + .../gopsutil/process/process_darwin_arm64.go | 205 + .../gopsutil/process/process_darwin_cgo.go | 30 + .../gopsutil/process/process_darwin_nocgo.go | 33 + internal/gopsutil/process/process_fallback.go | 371 ++ internal/gopsutil/process/process_freebsd.go | 549 ++ .../gopsutil/process/process_freebsd_386.go | 192 + .../gopsutil/process/process_freebsd_amd64.go | 192 + .../gopsutil/process/process_freebsd_arm.go | 192 + .../gopsutil/process/process_freebsd_arm64.go | 201 + internal/gopsutil/process/process_linux.go | 1299 ++++ internal/gopsutil/process/process_openbsd.go | 570 ++ .../gopsutil/process/process_openbsd_386.go | 201 + .../gopsutil/process/process_openbsd_amd64.go | 200 + internal/gopsutil/process/process_posix.go | 194 + internal/gopsutil/process/process_windows.go | 1024 +++ .../gopsutil/process/process_windows_386.go | 102 + .../gopsutil/process/process_windows_amd64.go | 76 + internal/gopsutil/process/types_darwin.go | 163 + internal/gopsutil/process/types_freebsd.go | 95 + internal/gopsutil/process/types_openbsd.go | 103 + internal/memory/memory.go | 97 + internal/memory/memory_test.go | 81 + internal/schema/LICENSE | 27 + internal/schema/cache.go | 305 + internal/schema/converter.go | 145 + internal/schema/decoder.go | 534 ++ internal/schema/doc.go | 148 + internal/schema/encoder.go | 202 + internal/storage/memory/config.go | 33 + internal/storage/memory/memory.go | 145 + internal/storage/memory/memory_test.go | 158 + internal/template/html/html.go | 206 + internal/template/utils/utils.go | 110 + internal/tlstest/tls.go | 118 + internal/wmi/LICENSE | 20 + internal/wmi/swbemservices.go | 261 + internal/wmi/wmi.go | 501 ++ listen.go | 502 ++ listen_test.go | 342 + log/default.go | 209 + log/default_test.go | 212 + log/fiberlog.go | 141 + log/fiberlog_test.go | 24 + log/log.go | 100 + middleware/adaptor/adaptor.go | 171 + middleware/adaptor/adaptor_test.go | 492 ++ middleware/basicauth/basicauth.go | 60 + middleware/basicauth/basicauth_test.go | 154 + middleware/basicauth/config.go | 105 + middleware/cache/cache.go | 252 + middleware/cache/cache_test.go | 901 +++ middleware/cache/config.go | 128 + middleware/cache/heap.go | 92 + middleware/cache/manager.go | 132 + middleware/cache/manager_msgp.go | 300 + middleware/compress/compress.go | 65 + middleware/compress/compress_test.go | 192 + middleware/compress/config.go | 56 + middleware/cors/cors.go | 289 + middleware/cors/cors_test.go | 1335 ++++ middleware/cors/utils.go | 66 + middleware/cors/utils_test.go | 196 + middleware/csrf/config.go | 243 + middleware/csrf/csrf.go | 239 + middleware/csrf/csrf_test.go | 1060 +++ middleware/csrf/extractors.go | 70 + middleware/csrf/helpers.go | 13 + middleware/csrf/session_manager.go | 68 + middleware/csrf/storage_manager.go | 70 + middleware/csrf/storage_manager_msgp.go | 90 + middleware/csrf/token.go | 11 + middleware/earlydata/config.go | 73 + middleware/earlydata/earlydata.go | 47 + middleware/earlydata/earlydata_test.go | 193 + middleware/encryptcookie/config.go | 78 + middleware/encryptcookie/encryptcookie.go | 57 + .../encryptcookie/encryptcookie_test.go | 192 + middleware/encryptcookie/utils.go | 98 + middleware/envvar/envvar.go | 68 + middleware/envvar/envvar_test.go | 172 + middleware/etag/config.go | 44 + middleware/etag/etag.go | 116 + middleware/etag/etag_test.go | 291 + middleware/expvar/config.go | 34 + middleware/expvar/expvar.go | 35 + middleware/expvar/expvar_test.go | 103 + middleware/favicon/favicon.go | 146 + middleware/favicon/favicon_test.go | 208 + middleware/filesystem/filesystem.go | 287 + middleware/filesystem/filesystem_test.go | 235 + middleware/filesystem/utils.go | 66 + middleware/healthcheck/config.go | 84 + middleware/healthcheck/healthcheck.go | 61 + middleware/healthcheck/healthcheck_test.go | 237 + middleware/helmet/config.go | 154 + middleware/helmet/helmet.go | 94 + middleware/helmet/helmet_test.go | 201 + middleware/idempotency/config.go | 125 + middleware/idempotency/idempotency.go | 153 + middleware/idempotency/idempotency_test.go | 177 + middleware/idempotency/locker.go | 53 + middleware/idempotency/locker_test.go | 59 + middleware/idempotency/response.go | 10 + middleware/idempotency/response_msgp.go | 131 + middleware/idempotency/response_msgp_test.go | 67 + middleware/keyauth/config.go | 95 + middleware/keyauth/keyauth.go | 121 + middleware/keyauth/keyauth_test.go | 461 ++ middleware/limiter/config.go | 128 + middleware/limiter/limiter.go | 25 + middleware/limiter/limiter_fixed.go | 106 + middleware/limiter/limiter_sliding.go | 137 + middleware/limiter/limiter_test.go | 727 ++ middleware/limiter/manager.go | 92 + middleware/limiter/manager_msgp.go | 160 + middleware/logger/config.go | 136 + middleware/logger/data.go | 16 + middleware/logger/logger.go | 182 + middleware/logger/logger_test.go | 650 ++ middleware/logger/tags.go | 209 + middleware/logger/template_chain.go | 70 + middleware/logger/utils.go | 39 + middleware/monitor/config.go | 132 + middleware/monitor/config_test.go | 163 + middleware/monitor/index.go | 271 + middleware/monitor/monitor.go | 137 + middleware/monitor/monitor_test.go | 198 + middleware/pprof/config.go | 41 + middleware/pprof/pprof.go | 95 + middleware/pprof/pprof_test.go | 200 + middleware/proxy/config.go | 88 + middleware/proxy/proxy.go | 267 + middleware/proxy/proxy_test.go | 689 ++ middleware/recover/config.go | 47 + middleware/recover/recover.go | 45 + middleware/recover/recover_test.go | 61 + middleware/redirect/config.go | 53 + middleware/redirect/redirect.go | 61 + middleware/redirect/redirect_test.go | 295 + middleware/requestid/config.go | 66 + middleware/requestid/requestid.go | 33 + middleware/requestid/requestid_test.go | 103 + middleware/rewrite/config.go | 38 + middleware/rewrite/rewrite.go | 54 + middleware/rewrite/rewrite_test.go | 173 + middleware/session/config.go | 128 + middleware/session/data.go | 63 + middleware/session/session.go | 311 + middleware/session/session_test.go | 904 +++ middleware/session/store.go | 136 + middleware/session/store_test.go | 119 + middleware/skip/skip.go | 21 + middleware/skip/skip_test.go | 57 + middleware/timeout/timeout.go | 74 + middleware/timeout/timeout_test.go | 88 + mount.go | 230 + mount_test.go | 594 ++ path.go | 740 +++ path_test.go | 268 + path_testcases_test.go | 718 ++ prefork.go | 179 + prefork_test.go | 104 + router.go | 518 ++ router_test.go | 914 +++ utils/README.md | 90 + utils/assertions.go | 68 + utils/assertions_test.go | 15 + utils/bytes.go | 69 + utils/bytes_test.go | 218 + utils/common.go | 160 + utils/common_test.go | 127 + utils/convert.go | 117 + utils/convert_b2s_new.go | 12 + utils/convert_b2s_old.go | 14 + utils/convert_s2b_new.go | 12 + utils/convert_s2b_old.go | 24 + utils/convert_test.go | 83 + utils/deprecated.go | 16 + utils/http.go | 267 + utils/http_test.go | 148 + utils/ips.go | 143 + utils/ips_test.go | 91 + utils/json.go | 9 + utils/json_test.go | 58 + utils/strings.go | 75 + utils/strings_test.go | 217 + utils/time.go | 32 + utils/time_test.go | 48 + utils/xml.go | 4 + utils/xml_test.go | 59 + 441 files changed, 95392 insertions(+) create mode 100644 .editorconfig create mode 100644 .gitattributes create mode 100644 .github/.editorconfig create mode 100644 .github/.hound.yml create mode 100644 .github/CODEOWNERS create mode 100644 .github/CODE_OF_CONDUCT.md create mode 100644 .github/CONTRIBUTING.md create mode 100644 .github/FUNDING.yml create mode 100644 .github/ISSUE_TEMPLATE/bug-report.yaml create mode 100644 .github/ISSUE_TEMPLATE/feature-request.yaml create mode 100644 .github/ISSUE_TEMPLATE/question.yaml create mode 100644 .github/README.md create mode 100644 .github/README_az.md create mode 100644 .github/README_ckb.md create mode 100644 .github/README_de.md create mode 100644 .github/README_eg.md create mode 100644 .github/README_es.md create mode 100644 .github/README_fa.md create mode 100644 .github/README_fr.md create mode 100644 .github/README_he.md create mode 100644 .github/README_id.md create mode 100644 .github/README_it.md create mode 100644 .github/README_ja.md create mode 100644 .github/README_ko.md create mode 100644 .github/README_nl.md create mode 100644 .github/README_pl.md create mode 100644 .github/README_pt.md create mode 100644 .github/README_ru.md create mode 100644 .github/README_sa.md create mode 100644 .github/README_tr.md create mode 100644 .github/README_uk.md create mode 100644 .github/README_zh-CN.md create mode 100644 .github/README_zh-TW.md create mode 100644 .github/SECURITY.md create mode 100644 .github/config.yml create mode 100644 .github/dependabot.yml create mode 100644 .github/index.html create mode 100644 .github/labeler.yml create mode 100644 .github/pull_request_template.md create mode 100644 .github/release-drafter.yml create mode 100644 .github/release.yml create mode 100755 .github/scripts/sync_docs.sh create mode 100644 .github/testdata/ca-chain.cert.pem create mode 100644 .github/testdata/favicon.ico create mode 100644 .github/testdata/fs/css/style.css create mode 100644 .github/testdata/fs/img/fiber.png create mode 100644 .github/testdata/fs/index.html create mode 100644 .github/testdata/index.html create mode 100644 .github/testdata/index.tmpl create mode 100644 .github/testdata/main.tmpl create mode 100644 .github/testdata/ssl.key create mode 100644 .github/testdata/ssl.pem create mode 100644 .github/testdata/template-invalid.html create mode 100644 .github/testdata/template.tmpl create mode 100644 .github/testdata/template/hello_world.gohtml create mode 100644 .github/testdata/testRoutes.json create mode 100644 .github/testdata2/bruh.tmpl create mode 100644 .github/workflows/auto-labeler.yml create mode 100644 .github/workflows/benchmark.yml create mode 100644 .github/workflows/codeql-analysis.yml create mode 100644 .github/workflows/linter.yml create mode 100644 .github/workflows/release-drafter.yml create mode 100644 .github/workflows/sync-docs.yml create mode 100644 .github/workflows/test.yml create mode 100644 .github/workflows/vulncheck.yml create mode 100644 .gitignore create mode 100644 .golangci.yml create mode 100644 LICENSE create mode 100644 Makefile create mode 100644 app.go create mode 100644 app_test.go create mode 100644 client.go create mode 100644 client_test.go create mode 100644 color.go create mode 100644 ctx.go create mode 100644 ctx_test.go create mode 100644 docs/api/_category_.json create mode 100644 docs/api/app.md create mode 100644 docs/api/client.md create mode 100644 docs/api/constants.md create mode 100644 docs/api/ctx.md create mode 100644 docs/api/fiber.md create mode 100644 docs/api/log.md create mode 100644 docs/api/middleware/_category_.json create mode 100644 docs/api/middleware/adaptor.md create mode 100644 docs/api/middleware/basicauth.md create mode 100644 docs/api/middleware/cache.md create mode 100644 docs/api/middleware/compress.md create mode 100644 docs/api/middleware/cors.md create mode 100644 docs/api/middleware/csrf.md create mode 100644 docs/api/middleware/earlydata.md create mode 100644 docs/api/middleware/encryptcookie.md create mode 100644 docs/api/middleware/envvar.md create mode 100644 docs/api/middleware/etag.md create mode 100644 docs/api/middleware/expvar.md create mode 100644 docs/api/middleware/favicon.md create mode 100644 docs/api/middleware/filesystem.md create mode 100644 docs/api/middleware/healthcheck.md create mode 100644 docs/api/middleware/helmet.md create mode 100644 docs/api/middleware/idempotency.md create mode 100644 docs/api/middleware/keyauth.md create mode 100644 docs/api/middleware/limiter.md create mode 100644 docs/api/middleware/logger.md create mode 100644 docs/api/middleware/monitor.md create mode 100644 docs/api/middleware/pprof.md create mode 100644 docs/api/middleware/proxy.md create mode 100644 docs/api/middleware/recover.md create mode 100644 docs/api/middleware/redirect.md create mode 100644 docs/api/middleware/requestid.md create mode 100644 docs/api/middleware/rewrite.md create mode 100644 docs/api/middleware/session.md create mode 100644 docs/api/middleware/skip.md create mode 100644 docs/api/middleware/timeout.md create mode 100644 docs/extra/_category_.json create mode 100644 docs/extra/benchmarks.md create mode 100644 docs/extra/faq.md create mode 100644 docs/guide/_category_.json create mode 100644 docs/guide/error-handling.md create mode 100644 docs/guide/faster-fiber.md create mode 100644 docs/guide/grouping.md create mode 100644 docs/guide/hooks.md create mode 100644 docs/guide/routing.md create mode 100644 docs/guide/templates.md create mode 100644 docs/guide/validation.md create mode 100644 docs/intro.md create mode 100644 docs/partials/routing/handler.md create mode 100644 error.go create mode 100644 error_test.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 group.go create mode 100644 helpers.go create mode 100644 helpers_fuzz_test.go create mode 100644 helpers_test.go create mode 100644 hooks.go create mode 100644 hooks_test.go create mode 100644 internal/go-ole/LICENSE create mode 100644 internal/go-ole/com.go create mode 100644 internal/go-ole/com_func.go create mode 100644 internal/go-ole/connect.go create mode 100644 internal/go-ole/constants.go create mode 100644 internal/go-ole/error.go create mode 100644 internal/go-ole/error_func.go create mode 100644 internal/go-ole/error_windows.go create mode 100644 internal/go-ole/guid.go create mode 100644 internal/go-ole/iconnectionpoint.go create mode 100644 internal/go-ole/iconnectionpoint_func.go create mode 100644 internal/go-ole/iconnectionpoint_windows.go create mode 100644 internal/go-ole/iconnectionpointcontainer.go create mode 100644 internal/go-ole/iconnectionpointcontainer_func.go create mode 100644 internal/go-ole/iconnectionpointcontainer_windows.go create mode 100644 internal/go-ole/idispatch.go create mode 100644 internal/go-ole/idispatch_func.go create mode 100644 internal/go-ole/idispatch_windows.go create mode 100644 internal/go-ole/ienumvariant.go create mode 100644 internal/go-ole/ienumvariant_func.go create mode 100644 internal/go-ole/ienumvariant_windows.go create mode 100644 internal/go-ole/iinspectable.go create mode 100644 internal/go-ole/iinspectable_func.go create mode 100644 internal/go-ole/iinspectable_windows.go create mode 100644 internal/go-ole/iprovideclassinfo.go create mode 100644 internal/go-ole/iprovideclassinfo_func.go create mode 100644 internal/go-ole/iprovideclassinfo_windows.go create mode 100644 internal/go-ole/itypeinfo.go create mode 100644 internal/go-ole/itypeinfo_func.go create mode 100644 internal/go-ole/itypeinfo_windows.go create mode 100644 internal/go-ole/iunknown.go create mode 100644 internal/go-ole/iunknown_func.go create mode 100644 internal/go-ole/iunknown_windows.go create mode 100644 internal/go-ole/ole.go create mode 100644 internal/go-ole/oleutil/connection.go create mode 100644 internal/go-ole/oleutil/connection_func.go create mode 100644 internal/go-ole/oleutil/connection_windows.go create mode 100644 internal/go-ole/oleutil/go-get.go create mode 100644 internal/go-ole/oleutil/oleutil.go create mode 100644 internal/go-ole/safearray.go create mode 100644 internal/go-ole/safearray_func.go create mode 100644 internal/go-ole/safearray_windows.go create mode 100644 internal/go-ole/safearrayconversion.go create mode 100644 internal/go-ole/safearrayslices.go create mode 100644 internal/go-ole/utility.go create mode 100644 internal/go-ole/variables.go create mode 100644 internal/go-ole/variant.go create mode 100644 internal/go-ole/variant32.go create mode 100644 internal/go-ole/variant64.go create mode 100644 internal/go-ole/variant_date_386.go create mode 100644 internal/go-ole/variant_date_amd64.go create mode 100644 internal/go-ole/vt_string.go create mode 100644 internal/go-ole/winrt.go create mode 100644 internal/go-ole/winrt_doc.go create mode 100644 internal/gopsutil/LICENSE create mode 100644 internal/gopsutil/common/binary.go create mode 100644 internal/gopsutil/common/common.go create mode 100644 internal/gopsutil/common/common_darwin.go create mode 100644 internal/gopsutil/common/common_freebsd.go create mode 100644 internal/gopsutil/common/common_linux.go create mode 100644 internal/gopsutil/common/common_openbsd.go create mode 100644 internal/gopsutil/common/common_unix.go create mode 100644 internal/gopsutil/common/common_windows.go create mode 100644 internal/gopsutil/common/sleep.go create mode 100644 internal/gopsutil/cpu/cpu.go create mode 100644 internal/gopsutil/cpu/cpu_darwin.go create mode 100644 internal/gopsutil/cpu/cpu_darwin_cgo.go create mode 100644 internal/gopsutil/cpu/cpu_darwin_nocgo.go create mode 100644 internal/gopsutil/cpu/cpu_dragonfly.go create mode 100644 internal/gopsutil/cpu/cpu_dragonfly_amd64.go create mode 100644 internal/gopsutil/cpu/cpu_fallback.go create mode 100644 internal/gopsutil/cpu/cpu_freebsd.go create mode 100644 internal/gopsutil/cpu/cpu_freebsd_386.go create mode 100644 internal/gopsutil/cpu/cpu_freebsd_amd64.go create mode 100644 internal/gopsutil/cpu/cpu_freebsd_arm.go create mode 100644 internal/gopsutil/cpu/cpu_freebsd_arm64.go create mode 100644 internal/gopsutil/cpu/cpu_linux.go create mode 100644 internal/gopsutil/cpu/cpu_openbsd.go create mode 100644 internal/gopsutil/cpu/cpu_solaris.go create mode 100644 internal/gopsutil/cpu/cpu_windows.go create mode 100644 internal/gopsutil/load/load.go create mode 100644 internal/gopsutil/load/load_bsd.go create mode 100644 internal/gopsutil/load/load_darwin.go create mode 100644 internal/gopsutil/load/load_fallback.go create mode 100644 internal/gopsutil/load/load_freebsd.go create mode 100644 internal/gopsutil/load/load_linux.go create mode 100644 internal/gopsutil/load/load_openbsd.go create mode 100644 internal/gopsutil/load/load_solaris.go create mode 100644 internal/gopsutil/load/load_windows.go create mode 100644 internal/gopsutil/mem/mem.go create mode 100644 internal/gopsutil/mem/mem_darwin.go create mode 100644 internal/gopsutil/mem/mem_darwin_cgo.go create mode 100644 internal/gopsutil/mem/mem_darwin_nocgo.go create mode 100644 internal/gopsutil/mem/mem_fallback.go create mode 100644 internal/gopsutil/mem/mem_freebsd.go create mode 100644 internal/gopsutil/mem/mem_linux.go create mode 100644 internal/gopsutil/mem/mem_openbsd.go create mode 100644 internal/gopsutil/mem/mem_openbsd_386.go create mode 100644 internal/gopsutil/mem/mem_openbsd_amd64.go create mode 100644 internal/gopsutil/mem/mem_solaris.go create mode 100644 internal/gopsutil/mem/mem_windows.go create mode 100644 internal/gopsutil/mem/types_openbsd.go create mode 100644 internal/gopsutil/net/net.go create mode 100644 internal/gopsutil/net/net_aix.go create mode 100644 internal/gopsutil/net/net_darwin.go create mode 100644 internal/gopsutil/net/net_fallback.go create mode 100644 internal/gopsutil/net/net_freebsd.go create mode 100644 internal/gopsutil/net/net_linux.go create mode 100644 internal/gopsutil/net/net_openbsd.go create mode 100644 internal/gopsutil/net/net_unix.go create mode 100644 internal/gopsutil/net/net_windows.go create mode 100644 internal/gopsutil/process/process.go create mode 100644 internal/gopsutil/process/process_darwin.go create mode 100644 internal/gopsutil/process/process_darwin_386.go create mode 100644 internal/gopsutil/process/process_darwin_amd64.go create mode 100644 internal/gopsutil/process/process_darwin_arm64.go create mode 100644 internal/gopsutil/process/process_darwin_cgo.go create mode 100644 internal/gopsutil/process/process_darwin_nocgo.go create mode 100644 internal/gopsutil/process/process_fallback.go create mode 100644 internal/gopsutil/process/process_freebsd.go create mode 100644 internal/gopsutil/process/process_freebsd_386.go create mode 100644 internal/gopsutil/process/process_freebsd_amd64.go create mode 100644 internal/gopsutil/process/process_freebsd_arm.go create mode 100644 internal/gopsutil/process/process_freebsd_arm64.go create mode 100644 internal/gopsutil/process/process_linux.go create mode 100644 internal/gopsutil/process/process_openbsd.go create mode 100644 internal/gopsutil/process/process_openbsd_386.go create mode 100644 internal/gopsutil/process/process_openbsd_amd64.go create mode 100644 internal/gopsutil/process/process_posix.go create mode 100644 internal/gopsutil/process/process_windows.go create mode 100644 internal/gopsutil/process/process_windows_386.go create mode 100644 internal/gopsutil/process/process_windows_amd64.go create mode 100644 internal/gopsutil/process/types_darwin.go create mode 100644 internal/gopsutil/process/types_freebsd.go create mode 100644 internal/gopsutil/process/types_openbsd.go create mode 100644 internal/memory/memory.go create mode 100644 internal/memory/memory_test.go create mode 100644 internal/schema/LICENSE create mode 100644 internal/schema/cache.go create mode 100644 internal/schema/converter.go create mode 100644 internal/schema/decoder.go create mode 100644 internal/schema/doc.go create mode 100644 internal/schema/encoder.go create mode 100644 internal/storage/memory/config.go create mode 100644 internal/storage/memory/memory.go create mode 100644 internal/storage/memory/memory_test.go create mode 100644 internal/template/html/html.go create mode 100644 internal/template/utils/utils.go create mode 100644 internal/tlstest/tls.go create mode 100644 internal/wmi/LICENSE create mode 100644 internal/wmi/swbemservices.go create mode 100644 internal/wmi/wmi.go create mode 100644 listen.go create mode 100644 listen_test.go create mode 100644 log/default.go create mode 100644 log/default_test.go create mode 100644 log/fiberlog.go create mode 100644 log/fiberlog_test.go create mode 100644 log/log.go create mode 100644 middleware/adaptor/adaptor.go create mode 100644 middleware/adaptor/adaptor_test.go create mode 100644 middleware/basicauth/basicauth.go create mode 100644 middleware/basicauth/basicauth_test.go create mode 100644 middleware/basicauth/config.go create mode 100644 middleware/cache/cache.go create mode 100644 middleware/cache/cache_test.go create mode 100644 middleware/cache/config.go create mode 100644 middleware/cache/heap.go create mode 100644 middleware/cache/manager.go create mode 100644 middleware/cache/manager_msgp.go create mode 100644 middleware/compress/compress.go create mode 100644 middleware/compress/compress_test.go create mode 100644 middleware/compress/config.go create mode 100644 middleware/cors/cors.go create mode 100644 middleware/cors/cors_test.go create mode 100644 middleware/cors/utils.go create mode 100644 middleware/cors/utils_test.go create mode 100644 middleware/csrf/config.go create mode 100644 middleware/csrf/csrf.go create mode 100644 middleware/csrf/csrf_test.go create mode 100644 middleware/csrf/extractors.go create mode 100644 middleware/csrf/helpers.go create mode 100644 middleware/csrf/session_manager.go create mode 100644 middleware/csrf/storage_manager.go create mode 100644 middleware/csrf/storage_manager_msgp.go create mode 100644 middleware/csrf/token.go create mode 100644 middleware/earlydata/config.go create mode 100644 middleware/earlydata/earlydata.go create mode 100644 middleware/earlydata/earlydata_test.go create mode 100644 middleware/encryptcookie/config.go create mode 100644 middleware/encryptcookie/encryptcookie.go create mode 100644 middleware/encryptcookie/encryptcookie_test.go create mode 100644 middleware/encryptcookie/utils.go create mode 100644 middleware/envvar/envvar.go create mode 100644 middleware/envvar/envvar_test.go create mode 100644 middleware/etag/config.go create mode 100644 middleware/etag/etag.go create mode 100644 middleware/etag/etag_test.go create mode 100644 middleware/expvar/config.go create mode 100644 middleware/expvar/expvar.go create mode 100644 middleware/expvar/expvar_test.go create mode 100644 middleware/favicon/favicon.go create mode 100644 middleware/favicon/favicon_test.go create mode 100644 middleware/filesystem/filesystem.go create mode 100644 middleware/filesystem/filesystem_test.go create mode 100644 middleware/filesystem/utils.go create mode 100644 middleware/healthcheck/config.go create mode 100644 middleware/healthcheck/healthcheck.go create mode 100644 middleware/healthcheck/healthcheck_test.go create mode 100644 middleware/helmet/config.go create mode 100644 middleware/helmet/helmet.go create mode 100644 middleware/helmet/helmet_test.go create mode 100644 middleware/idempotency/config.go create mode 100644 middleware/idempotency/idempotency.go create mode 100644 middleware/idempotency/idempotency_test.go create mode 100644 middleware/idempotency/locker.go create mode 100644 middleware/idempotency/locker_test.go create mode 100644 middleware/idempotency/response.go create mode 100644 middleware/idempotency/response_msgp.go create mode 100644 middleware/idempotency/response_msgp_test.go create mode 100644 middleware/keyauth/config.go create mode 100644 middleware/keyauth/keyauth.go create mode 100644 middleware/keyauth/keyauth_test.go create mode 100644 middleware/limiter/config.go create mode 100644 middleware/limiter/limiter.go create mode 100644 middleware/limiter/limiter_fixed.go create mode 100644 middleware/limiter/limiter_sliding.go create mode 100644 middleware/limiter/limiter_test.go create mode 100644 middleware/limiter/manager.go create mode 100644 middleware/limiter/manager_msgp.go create mode 100644 middleware/logger/config.go create mode 100644 middleware/logger/data.go create mode 100644 middleware/logger/logger.go create mode 100644 middleware/logger/logger_test.go create mode 100644 middleware/logger/tags.go create mode 100644 middleware/logger/template_chain.go create mode 100644 middleware/logger/utils.go create mode 100644 middleware/monitor/config.go create mode 100644 middleware/monitor/config_test.go create mode 100644 middleware/monitor/index.go create mode 100644 middleware/monitor/monitor.go create mode 100644 middleware/monitor/monitor_test.go create mode 100644 middleware/pprof/config.go create mode 100644 middleware/pprof/pprof.go create mode 100644 middleware/pprof/pprof_test.go create mode 100644 middleware/proxy/config.go create mode 100644 middleware/proxy/proxy.go create mode 100644 middleware/proxy/proxy_test.go create mode 100644 middleware/recover/config.go create mode 100644 middleware/recover/recover.go create mode 100644 middleware/recover/recover_test.go create mode 100644 middleware/redirect/config.go create mode 100644 middleware/redirect/redirect.go create mode 100644 middleware/redirect/redirect_test.go create mode 100644 middleware/requestid/config.go create mode 100644 middleware/requestid/requestid.go create mode 100644 middleware/requestid/requestid_test.go create mode 100644 middleware/rewrite/config.go create mode 100644 middleware/rewrite/rewrite.go create mode 100644 middleware/rewrite/rewrite_test.go create mode 100644 middleware/session/config.go create mode 100644 middleware/session/data.go create mode 100644 middleware/session/session.go create mode 100644 middleware/session/session_test.go create mode 100644 middleware/session/store.go create mode 100644 middleware/session/store_test.go create mode 100644 middleware/skip/skip.go create mode 100644 middleware/skip/skip_test.go create mode 100644 middleware/timeout/timeout.go create mode 100644 middleware/timeout/timeout_test.go create mode 100644 mount.go create mode 100644 mount_test.go create mode 100644 path.go create mode 100644 path_test.go create mode 100644 path_testcases_test.go create mode 100644 prefork.go create mode 100644 prefork_test.go create mode 100644 router.go create mode 100644 router_test.go create mode 100644 utils/README.md create mode 100644 utils/assertions.go create mode 100644 utils/assertions_test.go create mode 100644 utils/bytes.go create mode 100644 utils/bytes_test.go create mode 100644 utils/common.go create mode 100644 utils/common_test.go create mode 100644 utils/convert.go create mode 100644 utils/convert_b2s_new.go create mode 100644 utils/convert_b2s_old.go create mode 100644 utils/convert_s2b_new.go create mode 100644 utils/convert_s2b_old.go create mode 100644 utils/convert_test.go create mode 100644 utils/deprecated.go create mode 100644 utils/http.go create mode 100644 utils/http_test.go create mode 100644 utils/ips.go create mode 100644 utils/ips_test.go create mode 100644 utils/json.go create mode 100644 utils/json_test.go create mode 100644 utils/strings.go create mode 100644 utils/strings_test.go create mode 100644 utils/time.go create mode 100644 utils/time_test.go create mode 100644 utils/xml.go create mode 100644 utils/xml_test.go diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..6a4ec76 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,8 @@ +; This file is for unifying the coding style for different editors and IDEs. +; More information at http://editorconfig.org +; This style originates from https://github.com/fewagency/best-practices +root = true + +[*] +charset = utf-8 +end_of_line = lf diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..963a68e --- /dev/null +++ b/.gitattributes @@ -0,0 +1,12 @@ +# Handle line endings automatically for files detected as text +# and leave all files detected as binary untouched. +* text=auto eol=lf + +# Force batch scripts to always use CRLF line endings so that if a repo is accessed +# in Windows via a file share from Linux, the scripts will work. +*.{cmd,[cC][mM][dD]} text eol=crlf +*.{bat,[bB][aA][tT]} text eol=crlf + +# Force bash scripts to always use LF line endings so that if a repo is accessed +# in Unix via a file share from Windows, the scripts will work. +*.sh text eol=lf diff --git a/.github/.editorconfig b/.github/.editorconfig new file mode 100644 index 0000000..85a920c --- /dev/null +++ b/.github/.editorconfig @@ -0,0 +1,23 @@ +; https://editorconfig.org/ + +root = true + +[*] +insert_final_newline = true +charset = utf-8 +trim_trailing_whitespace = true +indent_style = space +indent_size = 2 + +[{Makefile,go.mod,go.sum,*.go,.gitmodules}] +indent_style = tab +indent_size = 8 + +[*.md] +indent_size = 4 +trim_trailing_whitespace = false + +eclint_indent_style = unset + +[Dockerfile] +indent_size = 4 \ No newline at end of file diff --git a/.github/.hound.yml b/.github/.hound.yml new file mode 100644 index 0000000..59d7bb8 --- /dev/null +++ b/.github/.hound.yml @@ -0,0 +1,2 @@ +golint: + enabled: false \ No newline at end of file diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..957d1b1 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @gofiber/maintainers diff --git a/.github/CODE_OF_CONDUCT.md b/.github/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..c8d7ee4 --- /dev/null +++ b/.github/CODE_OF_CONDUCT.md @@ -0,0 +1,81 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our community include: + +- Demonstrating empathy and kindness toward other people +- Being respectful of differing opinions, viewpoints, and experiences +- Giving and gracefully accepting constructive feedback +- Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience +- Focusing on what is best not just for us as individuals, but for the overall community + +Examples of unacceptable behavior include: + +- The use of sexualized language or imagery, and sexual attention or advances of any kind +- Trolling, insulting or derogatory comments, and personal or political attacks +- Public or private harassment +- Publishing others' private information, such as a physical or email address, without their explicit permission +- Other conduct which could reasonably be considered inappropriate in a professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at [Discord](https://gofiber.io/discord). All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series of actions. + +**Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within the community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0, +available at https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. + +Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity). + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see the FAQ at +https://www.contributor-covenant.org/faq. Translations are available at https://www.contributor-covenant.org/translations. diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md new file mode 100644 index 0000000..e35ce19 --- /dev/null +++ b/.github/CONTRIBUTING.md @@ -0,0 +1,27 @@ +# Contributing + +Before making any changes to this repository, we kindly request you to initiate discussions for proposed changes that do not yet have an associated [issue](https://github.com/gofiber/fiber/issues). Please use our [Discord](https://gofiber.io/discord) server to initiate these discussions. For [issue](https://github.com/gofiber/fiber/issues) that already exist, you may proceed with discussions using our [issue](https://github.com/gofiber/fiber/issues) tracker or any other suitable method, in consultation with the repository owners. Your collaboration is greatly appreciated. + +Please note: we have a [code of conduct](https://github.com/gofiber/fiber/blob/master/.github/CODE_OF_CONDUCT.md), please follow it in all your interactions with the `Fiber` project. + +## Pull Requests or Commits +Titles always we must use prefix according to below: + +> 🔥 Feature, ♻️ Refactor, 🩹 Fix, 🚨 Test, 📚 Doc, 🎨 Style +- 🔥 Feature: Add flow to add person +- ♻️ Refactor: Rename file X to Y +- 🩹 Fix: Improve flow +- 🚨 Test: Validate to add a new person +- 📚 Doc: Translate to Portuguese middleware redirect +- 🎨 Style: Respected pattern Golint + +All pull requests that contain a feature or fix are mandatory to have unit tests. Your PR is only to be merged if you respect this flow. + +# 👍 Contribute + +If you want to say **thank you** and/or support the active development of `Fiber`: + +1. Add a [GitHub Star](https://github.com/gofiber/fiber/stargazers) to the project. +2. Tweet about the project [on your 𝕏 (Twitter)](https://x.com/intent/tweet?text=%F0%9F%9A%80%20Fiber%20%E2%80%94%20is%20an%20Express.js%20inspired%20web%20framework%20build%20on%20Fasthttp%20for%20%23Go%20https%3A%2F%2Fgithub.com%2Fgofiber%2Ffiber). +3. Write a review or tutorial on [Medium](https://medium.com/), [Dev.to](https://dev.to/) or personal blog. +4. Support the project by donating a [cup of coffee](https://buymeacoff.ee/fenny). diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..d041abf --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,8 @@ +# These are supported funding model platforms + +github: [gofiber] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] +patreon: # Replace with a single Patreon username +open_collective: # Replace with a single Open Collective username +ko_fi: # Replace with a single Ko-fi username +tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel +custom: https://gofiber.io/support diff --git a/.github/ISSUE_TEMPLATE/bug-report.yaml b/.github/ISSUE_TEMPLATE/bug-report.yaml new file mode 100644 index 0000000..c48df1d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug-report.yaml @@ -0,0 +1,85 @@ +name: "\U0001F41B Bug Report" +title: "\U0001F41B [Bug]: " +description: Create a bug report to help us fix it. +labels: ["☢️ Bug"] + +body: + - type: markdown + id: notice + attributes: + value: | + ### Notice + **This repository is not related to external or third-part Fiber modules. If you have a problem with them, open an issue under their repos. If you think the problem is related to Fiber, open the issue here.** + - Don't forget you can ask your questions in our [Discord server](https://gofiber.io/discord). + - If you have a suggestion for a Fiber feature you would like to see, open the issue with the **✏️ Feature Request** template. + - Write your issue with clear and understandable English. + - type: textarea + id: description + attributes: + label: "Bug Description" + description: "A clear and detailed description of what the bug is." + placeholder: "Explain your problem clearly and in detail." + validations: + required: true + - type: textarea + id: how-to-reproduce + attributes: + label: How to Reproduce + description: "Steps to reproduce the behavior and what should be observed in the end." + placeholder: "Tell us step by step how we can replicate your problem and what we should see in the end." + value: | + Steps to reproduce the behavior: + 1. Go to '....' + 2. Click on '....' + 3. Do '....' + 4. See '....' + validations: + required: true + - type: textarea + id: expected-behavior + attributes: + label: Expected Behavior + description: "A clear and detailed description of what you think should happen." + placeholder: "Tell us what Fiber should normally do." + validations: + required: true + - type: input + id: version + attributes: + label: "Fiber Version" + description: "Some bugs may be fixed in future Fiber releases, so we have to know your Fiber version." + placeholder: "Write your Fiber version. (v2.33.0, v2.34.1...)" + validations: + required: true + - type: textarea + id: snippet + attributes: + label: "Code Snippet (optional)" + description: "For some issues, we need to know some parts of your code." + placeholder: "Share a code snippet that you think is related to the issue." + render: go + value: | + package main + + import "github.com/gofiber/fiber/v2" + import "log" + + func main() { + app := fiber.New() + + // Steps to reproduce + + log.Fatal(app.Listen(":3000")) + } + - type: checkboxes + id: terms + attributes: + label: "Checklist:" + description: "By submitting this issue, you confirm that:" + options: + - label: "I agree to follow Fiber's [Code of Conduct](https://github.com/gofiber/fiber/blob/master/.github/CODE_OF_CONDUCT.md)." + required: true + - label: "I have checked for existing issues that describe my problem prior to opening this one." + required: true + - label: "I understand that improperly formatted bug reports may be closed without explanation." + required: true diff --git a/.github/ISSUE_TEMPLATE/feature-request.yaml b/.github/ISSUE_TEMPLATE/feature-request.yaml new file mode 100644 index 0000000..fbdb871 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature-request.yaml @@ -0,0 +1,60 @@ +name: "\U0001F680 Feature Request" +title: "\U0001F680 [Feature]: " +description: Suggest an idea to improve this project. +labels: ["✏️ Feature"] + +body: + - type: markdown + id: notice + attributes: + value: | + ### Notice + - Don't forget you can ask your questions in our [Discord server](https://gofiber.io/discord). + - If you think this is just a bug, open the issue with the **☢️ Bug Report** template. + - Write your issue with clear and understandable English. + - type: textarea + id: description + attributes: + label: "Feature Description" + description: "A clear and detailed description of the feature you would like to see added." + placeholder: "Explain your feature clearly, and in detail." + validations: + required: true + - type: textarea + id: additional-context + attributes: + label: "Additional Context (optional)" + description: "If you have something else to describe, write them here." + placeholder: "Write here what you can describe differently." + - type: textarea + id: snippet + attributes: + label: "Code Snippet (optional)" + description: "Code snippet may be really helpful to describe some features." + placeholder: "Share a code snippet to explain the feature better." + render: go + value: | + package main + + import "github.com/gofiber/fiber/v2" + import "log" + + func main() { + app := fiber.New() + + // An example to describe the feature + + log.Fatal(app.Listen(":3000")) + } + - type: checkboxes + id: terms + attributes: + label: "Checklist:" + description: "By submitting this issue, you confirm that:" + options: + - label: "I agree to follow Fiber's [Code of Conduct](https://github.com/gofiber/fiber/blob/master/.github/CODE_OF_CONDUCT.md)." + required: true + - label: "I have checked for existing issues that describe my suggestion prior to opening this one." + required: true + - label: "I understand that improperly formatted feature requests may be closed without explanation." + required: true diff --git a/.github/ISSUE_TEMPLATE/question.yaml b/.github/ISSUE_TEMPLATE/question.yaml new file mode 100644 index 0000000..7204183 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/question.yaml @@ -0,0 +1,55 @@ +name: "🤔 Question" +title: "\U0001F917 [Question]: " +description: Ask a question so we can help you easily. +labels: ["🤔 Question"] + +body: + - type: markdown + id: notice + attributes: + value: | + ### Notice + - Don't forget you can ask your questions in our [Discord server](https://gofiber.io/discord). + - If you think this is just a bug, open the issue with the **☢️ Bug Report** template. + - If you have a suggestion for a Fiber feature you would like to see, open the issue with the **✏️ Feature Request** template. + - Write your issue with clear and understandable English. + - type: textarea + id: description + attributes: + label: "Question Description" + description: "A clear and detailed description of the question." + placeholder: "Explain your question clearly, and in detail." + validations: + required: true + - type: textarea + id: snippet + attributes: + label: "Code Snippet (optional)" + description: "Code snippet may be really helpful to describe some features." + placeholder: "Share a code snippet to explain the feature better." + render: go + value: | + package main + + import "github.com/gofiber/fiber/v2" + import "log" + + func main() { + app := fiber.New() + + // An example to describe the question + + log.Fatal(app.Listen(":3000")) + } + - type: checkboxes + id: terms + attributes: + label: "Checklist:" + description: "By submitting this issue, you confirm that:" + options: + - label: "I agree to follow Fiber's [Code of Conduct](https://github.com/gofiber/fiber/blob/master/.github/CODE_OF_CONDUCT.md)." + required: true + - label: "I have checked for existing issues that describe my questions prior to opening this one." + required: true + - label: "I understand that improperly formatted questions may be closed without explanation." + required: true diff --git a/.github/README.md b/.github/README.md new file mode 100644 index 0000000..172ecc5 --- /dev/null +++ b/.github/README.md @@ -0,0 +1,716 @@ +

+ + + + Fiber + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + +

+

+ Fiber is an Express inspired web framework built on top of Fasthttp, the fastest HTTP engine for Go. Designed to ease things up for fast development with zero memory allocation and performance in mind. +

+ +## ⚡️ Quickstart + +```go +package main + +import "github.com/gofiber/fiber/v2" + +func main() { + app := fiber.New() + + app.Get("/", func(c *fiber.Ctx) error { + return c.SendString("Hello, World 👋!") + }) + + app.Listen(":3000") +} +``` + +## 🤖 Benchmarks + +These tests are performed by [TechEmpower](https://www.techempower.com/benchmarks/#section=data-r19&hw=ph&test=plaintext) and [Go Web](https://github.com/smallnest/go-web-framework-benchmark). If you want to see all the results, please visit our [Wiki](https://docs.gofiber.io/extra/benchmarks). + +

+ + +

+ +## ⚙️ Installation + +Make sure you have Go installed ([download](https://go.dev/dl/)). Version `1.17` or higher is required. + +Initialize your project by creating a folder and then running `go mod init github.com/your/repo` ([learn more](https://go.dev/blog/using-go-modules)) inside the folder. Then install Fiber with the [`go get`](https://pkg.go.dev/cmd/go/#hdr-Add_dependencies_to_current_module_and_install_them) command: + +```bash +go get -u github.com/gofiber/fiber/v2 +``` + +## 🎯 Features + +- Robust [routing](https://docs.gofiber.io/guide/routing) +- Serve [static files](https://docs.gofiber.io/api/app#static) +- Extreme [performance](https://docs.gofiber.io/extra/benchmarks) +- [Low memory](https://docs.gofiber.io/extra/benchmarks) footprint +- [API endpoints](https://docs.gofiber.io/api/ctx) +- [Middleware](https://docs.gofiber.io/category/-middleware) & [Next](https://docs.gofiber.io/api/ctx#next) support +- [Rapid](https://dev.to/koddr/welcome-to-fiber-an-express-js-styled-fastest-web-framework-written-with-on-golang-497) server-side programming +- [Template engines](https://github.com/gofiber/template) +- [WebSocket support](https://github.com/gofiber/websocket) +- [Server-Sent events](https://github.com/gofiber/recipes/tree/master/sse) +- [Rate Limiter](https://docs.gofiber.io/api/middleware/limiter) +- Translated in [19 languages](https://docs.gofiber.io/) +- And much more, [explore Fiber](https://docs.gofiber.io/) + +## 💡 Philosophy + +New gophers that make the switch from [Node.js](https://nodejs.org/en/about/) to [Go](https://go.dev/doc/) are dealing with a learning curve before they can start building their web applications or microservices. Fiber, as a **web framework**, was created with the idea of **minimalism** and follows the **UNIX way**, so that new gophers can quickly enter the world of Go with a warm and trusted welcome. + +Fiber is **inspired** by Express, the most popular web framework on the Internet. We combined the **ease** of Express and **raw performance** of Go. If you have ever implemented a web application in Node.js (_using Express or similar_), then many methods and principles will seem **very common** to you. + +We **listen** to our users in [issues](https://github.com/gofiber/fiber/issues), Discord [channel](https://gofiber.io/discord) _and all over the Internet_ to create a **fast**, **flexible** and **friendly** Go web framework for **any** task, **deadline** and developer **skill**! Just like Express does in the JavaScript world. + +## ⚠️ Limitations + +- Due to Fiber's usage of unsafe, the library may not always be compatible with the latest Go version. Fiber 2.40.0 has been tested with Go versions 1.17 to 1.21. +- Fiber is not compatible with net/http interfaces. This means you will not be able to use projects like gqlgen, go-swagger, or any others which are part of the net/http ecosystem. + +## 👀 Examples + +Listed below are some of the common examples. If you want to see more code examples, please visit our [Recipes repository](https://github.com/gofiber/recipes) or visit our hosted [API documentation](https://docs.gofiber.io). + +#### 📖 [**Basic Routing**](https://docs.gofiber.io/#basic-routing) + +```go +func main() { + app := fiber.New() + + // GET /api/register + app.Get("/api/*", func(c *fiber.Ctx) error { + msg := fmt.Sprintf("✋ %s", c.Params("*")) + return c.SendString(msg) // => ✋ register + }) + + // GET /flights/LAX-SFO + app.Get("/flights/:from-:to", func(c *fiber.Ctx) error { + msg := fmt.Sprintf("💸 From: %s, To: %s", c.Params("from"), c.Params("to")) + return c.SendString(msg) // => 💸 From: LAX, To: SFO + }) + + // GET /dictionary.txt + app.Get("/:file.:ext", func(c *fiber.Ctx) error { + msg := fmt.Sprintf("📃 %s.%s", c.Params("file"), c.Params("ext")) + return c.SendString(msg) // => 📃 dictionary.txt + }) + + // GET /john/75 + app.Get("/:name/:age/:gender?", func(c *fiber.Ctx) error { + msg := fmt.Sprintf("👴 %s is %s years old", c.Params("name"), c.Params("age")) + return c.SendString(msg) // => 👴 john is 75 years old + }) + + // GET /john + app.Get("/:name", func(c *fiber.Ctx) error { + msg := fmt.Sprintf("Hello, %s 👋!", c.Params("name")) + return c.SendString(msg) // => Hello john 👋! + }) + + log.Fatal(app.Listen(":3000")) +} + +``` + +#### 📖 [**Route Naming**](https://docs.gofiber.io/api/app#name) + +```go +func main() { + app := fiber.New() + + // GET /api/register + app.Get("/api/*", func(c *fiber.Ctx) error { + msg := fmt.Sprintf("✋ %s", c.Params("*")) + return c.SendString(msg) // => ✋ register + }).Name("api") + + data, _ := json.MarshalIndent(app.GetRoute("api"), "", " ") + fmt.Print(string(data)) + // Prints: + // { + // "method": "GET", + // "name": "api", + // "path": "/api/*", + // "params": [ + // "*1" + // ] + // } + + + log.Fatal(app.Listen(":3000")) +} + +``` + +#### 📖 [**Serving Static Files**](https://docs.gofiber.io/api/app#static) + +```go +func main() { + app := fiber.New() + + app.Static("/", "./public") + // => http://localhost:3000/js/script.js + // => http://localhost:3000/css/style.css + + app.Static("/prefix", "./public") + // => http://localhost:3000/prefix/js/script.js + // => http://localhost:3000/prefix/css/style.css + + app.Static("*", "./public/index.html") + // => http://localhost:3000/any/path/shows/index/html + + log.Fatal(app.Listen(":3000")) +} + +``` + +#### 📖 [**Middleware & Next**](https://docs.gofiber.io/api/ctx#next) + +```go +func main() { + app := fiber.New() + + // Match any route + app.Use(func(c *fiber.Ctx) error { + fmt.Println("🥇 First handler") + return c.Next() + }) + + // Match all routes starting with /api + app.Use("/api", func(c *fiber.Ctx) error { + fmt.Println("🥈 Second handler") + return c.Next() + }) + + // GET /api/list + app.Get("/api/list", func(c *fiber.Ctx) error { + fmt.Println("🥉 Last handler") + return c.SendString("Hello, World 👋!") + }) + + log.Fatal(app.Listen(":3000")) +} + +``` + +
+ 📚 Show more code examples + +### Views engines + +📖 [Config](https://docs.gofiber.io/api/fiber#config) +📖 [Engines](https://github.com/gofiber/template) +📖 [Render](https://docs.gofiber.io/api/ctx#render) + +Fiber defaults to the [html/template](https://pkg.go.dev/html/template/) when no view engine is set. + +If you want to execute partials or use a different engine like [amber](https://github.com/eknkc/amber), [handlebars](https://github.com/aymerick/raymond), [mustache](https://github.com/cbroglie/mustache) or [pug](https://github.com/Joker/jade) etc.. + +Checkout our [Template](https://github.com/gofiber/template) package that support multiple view engines. + +```go +package main + +import ( + "github.com/gofiber/fiber/v2" + "github.com/gofiber/template/pug" +) + +func main() { + // You can setup Views engine before initiation app: + app := fiber.New(fiber.Config{ + Views: pug.New("./views", ".pug"), + }) + + // And now, you can call template `./views/home.pug` like this: + app.Get("/", func(c *fiber.Ctx) error { + return c.Render("home", fiber.Map{ + "title": "Homepage", + "year": 1999, + }) + }) + + log.Fatal(app.Listen(":3000")) +} +``` + +### Grouping routes into chains + +📖 [Group](https://docs.gofiber.io/api/app#group) + +```go +func middleware(c *fiber.Ctx) error { + fmt.Println("Don't mind me!") + return c.Next() +} + +func handler(c *fiber.Ctx) error { + return c.SendString(c.Path()) +} + +func main() { + app := fiber.New() + + // Root API route + api := app.Group("/api", middleware) // /api + + // API v1 routes + v1 := api.Group("/v1", middleware) // /api/v1 + v1.Get("/list", handler) // /api/v1/list + v1.Get("/user", handler) // /api/v1/user + + // API v2 routes + v2 := api.Group("/v2", middleware) // /api/v2 + v2.Get("/list", handler) // /api/v2/list + v2.Get("/user", handler) // /api/v2/user + + // ... +} + +``` + +### Middleware logger + +📖 [Logger](https://docs.gofiber.io/api/middleware/logger) + +```go +package main + +import ( + "log" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/logger" +) + +func main() { + app := fiber.New() + + app.Use(logger.New()) + + // ... + + log.Fatal(app.Listen(":3000")) +} +``` + +### Cross-Origin Resource Sharing (CORS) + +📖 [CORS](https://docs.gofiber.io/api/middleware/cors) + +```go +import ( + "log" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/cors" +) + +func main() { + app := fiber.New() + + app.Use(cors.New()) + + // ... + + log.Fatal(app.Listen(":3000")) +} +``` + +Check CORS by passing any domain in `Origin` header: + +```bash +curl -H "Origin: http://example.com" --verbose http://localhost:3000 +``` + +### Custom 404 response + +📖 [HTTP Methods](https://docs.gofiber.io/api/ctx#status) + +```go +func main() { + app := fiber.New() + + app.Static("/", "./public") + + app.Get("/demo", func(c *fiber.Ctx) error { + return c.SendString("This is a demo!") + }) + + app.Post("/register", func(c *fiber.Ctx) error { + return c.SendString("Welcome!") + }) + + // Last middleware to match anything + app.Use(func(c *fiber.Ctx) error { + return c.SendStatus(404) + // => 404 "Not Found" + }) + + log.Fatal(app.Listen(":3000")) +} +``` + +### JSON Response + +📖 [JSON](https://docs.gofiber.io/api/ctx#json) + +```go +type User struct { + Name string `json:"name"` + Age int `json:"age"` +} + +func main() { + app := fiber.New() + + app.Get("/user", func(c *fiber.Ctx) error { + return c.JSON(&User{"John", 20}) + // => {"name":"John", "age":20} + }) + + app.Get("/json", func(c *fiber.Ctx) error { + return c.JSON(fiber.Map{ + "success": true, + "message": "Hi John!", + }) + // => {"success":true, "message":"Hi John!"} + }) + + log.Fatal(app.Listen(":3000")) +} +``` + +### WebSocket Upgrade + +📖 [Websocket](https://github.com/gofiber/websocket) + +```go +import ( + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/websocket" +) + +func main() { + app := fiber.New() + + app.Get("/ws", websocket.New(func(c *websocket.Conn) { + for { + mt, msg, err := c.ReadMessage() + if err != nil { + log.Println("read:", err) + break + } + log.Printf("recv: %s", msg) + err = c.WriteMessage(mt, msg) + if err != nil { + log.Println("write:", err) + break + } + } + })) + + log.Fatal(app.Listen(":3000")) + // ws://localhost:3000/ws +} +``` + +### Server-Sent Events + +📖 [More Info](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events) + +```go +import ( + "github.com/gofiber/fiber/v2" + "github.com/valyala/fasthttp" +) + +func main() { + app := fiber.New() + + app.Get("/sse", func(c *fiber.Ctx) error { + c.Set("Content-Type", "text/event-stream") + c.Set("Cache-Control", "no-cache") + c.Set("Connection", "keep-alive") + c.Set("Transfer-Encoding", "chunked") + + c.Context().SetBodyStreamWriter(fasthttp.StreamWriter(func(w *bufio.Writer) { + fmt.Println("WRITER") + var i int + + for { + i++ + msg := fmt.Sprintf("%d - the time is %v", i, time.Now()) + fmt.Fprintf(w, "data: Message: %s\n\n", msg) + fmt.Println(msg) + + w.Flush() + time.Sleep(5 * time.Second) + } + })) + + return nil + }) + + log.Fatal(app.Listen(":3000")) +} +``` + +### Recover middleware + +📖 [Recover](https://docs.gofiber.io/api/middleware/recover) + +```go +import ( + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/recover" +) + +func main() { + app := fiber.New() + + app.Use(recover.New()) + + app.Get("/", func(c *fiber.Ctx) error { + panic("normally this would crash your app") + }) + + log.Fatal(app.Listen(":3000")) +} +``` + +
+ +### Using Trusted Proxy + +📖 [Config](https://docs.gofiber.io/api/fiber#config) + +```go +import ( + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/recover" +) + +func main() { + app := fiber.New(fiber.Config{ + EnableTrustedProxyCheck: true, + TrustedProxies: []string{"0.0.0.0", "1.1.1.1/30"}, // IP address or IP address range + ProxyHeader: fiber.HeaderXForwardedFor, + }) + + // ... + + log.Fatal(app.Listen(":3000")) +} +``` + + + +## 🧬 Internal Middleware + +Here is a list of middleware that are included within the Fiber framework. + +| Middleware | Description | +| :------------------------------------------------------------------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [basicauth](https://github.com/gofiber/fiber/tree/master/middleware/basicauth) | Basic auth middleware provides an HTTP basic authentication. It calls the next handler for valid credentials and 401 Unauthorized for missing or invalid credentials. | +| [cache](https://github.com/gofiber/fiber/tree/master/middleware/cache) | Intercept and cache responses | +| [compress](https://github.com/gofiber/fiber/tree/master/middleware/compress) | Compression middleware for Fiber, it supports `deflate`, `gzip` and `brotli` by default. | +| [cors](https://github.com/gofiber/fiber/tree/master/middleware/cors) | Enable cross-origin resource sharing \(CORS\) with various options. | +| [csrf](https://github.com/gofiber/fiber/tree/master/middleware/csrf) | Protect from CSRF exploits. | +| [encryptcookie](https://github.com/gofiber/fiber/tree/master/middleware/encryptcookie) | Encrypt middleware which encrypts cookie values. | +| [envvar](https://github.com/gofiber/fiber/tree/master/middleware/envvar) | Expose environment variables with providing an optional config. | +| [etag](https://github.com/gofiber/fiber/tree/master/middleware/etag) | ETag middleware that lets caches be more efficient and save bandwidth, as a web server does not need to resend a full response if the content has not changed. | +| [expvar](https://github.com/gofiber/fiber/tree/master/middleware/expvar) | Expvar middleware that serves via its HTTP server runtime exposed variants in the JSON format. | +| [favicon](https://github.com/gofiber/fiber/tree/master/middleware/favicon) | Ignore favicon from logs or serve from memory if a file path is provided. | +| [filesystem](https://github.com/gofiber/fiber/tree/master/middleware/filesystem) | FileSystem middleware for Fiber, special thanks and credits to Alireza Salary | +| [limiter](https://github.com/gofiber/fiber/tree/master/middleware/limiter) | Rate-limiting middleware for Fiber. Use to limit repeated requests to public APIs and/or endpoints such as password reset. | +| [logger](https://github.com/gofiber/fiber/tree/master/middleware/logger) | HTTP request/response logger. | +| [monitor](https://github.com/gofiber/fiber/tree/master/middleware/monitor) | Monitor middleware that reports server metrics, inspired by express-status-monitor | +| [pprof](https://github.com/gofiber/fiber/tree/master/middleware/pprof) | Special thanks to Matthew Lee \(@mthli\) | +| [proxy](https://github.com/gofiber/fiber/tree/master/middleware/proxy) | Allows you to proxy requests to a multiple servers | +| [recover](https://github.com/gofiber/fiber/tree/master/middleware/recover) | Recover middleware recovers from panics anywhere in the stack chain and handles the control to the centralized[ ErrorHandler](https://docs.gofiber.io/guide/error-handling). | +| [requestid](https://github.com/gofiber/fiber/tree/master/middleware/requestid) | Adds a requestid to every request. | +| [session](https://github.com/gofiber/fiber/tree/master/middleware/session) | Session middleware. NOTE: This middleware uses our Storage package. | +| [skip](https://github.com/gofiber/fiber/tree/master/middleware/skip) | Skip middleware that skips a wrapped handler if a predicate is true. | +| [rewrite](https://github.com/gofiber/rewrite) | Rewrite middleware rewrites the URL path based on provided rules. It can be helpful for backward compatibility or just creating cleaner and more descriptive links. | +| [timeout](https://github.com/gofiber/fiber/tree/master/middleware/timeout) | Adds a max time for a request and forwards to ErrorHandler if it is exceeded. | +| [adaptor](https://github.com/gofiber/adaptor) | Converter for net/http handlers to/from Fiber request handlers, special thanks to @arsmn! | +| [helmet](https://github.com/gofiber/helmet) | Helps secure your apps by setting various HTTP headers. | +| [redirect](https://github.com/gofiber/redirect) | Redirect middleware | +| [keyauth](https://github.com/gofiber/keyauth) | Key auth middleware provides a key based authentication. | + +## 🧬 External Middleware + +List of externally hosted middleware modules and maintained by the [Fiber team](https://github.com/orgs/gofiber/people). + +| Middleware | Description | +| :------------------------------------------------ | :-------------------------------------------------------------------------------------------------------------------- | +| [jwt](https://github.com/gofiber/jwt) | JWT returns a JSON Web Token \(JWT\) auth middleware. | +| [storage](https://github.com/gofiber/storage) | Premade storage drivers that implement the Storage interface, designed to be used with various Fiber middlewares. | +| [template](https://github.com/gofiber/template) | This package contains 8 template engines that can be used with Fiber `v1.10.x` Go version 1.13 or higher is required. | +| [websocket](https://github.com/gofiber/websocket) | Based on Fasthttp WebSocket for Fiber with Locals support! | + +## 🕶️ Awesome List + +For more articles, middlewares, examples or tools check our [awesome list](https://github.com/gofiber/awesome-fiber). + +## 👍 Contribute + +If you want to say **thank you** and/or support the active development of `Fiber`: + +1. Add a [GitHub Star](https://github.com/gofiber/fiber/stargazers) to the project. +2. Tweet about the project [on your 𝕏 (Twitter)](https://x.com/intent/tweet?text=Fiber%20is%20an%20Express%20inspired%20%23web%20%23framework%20built%20on%20top%20of%20Fasthttp%2C%20the%20fastest%20HTTP%20engine%20for%20%23Go.%20Designed%20to%20ease%20things%20up%20for%20%23fast%20development%20with%20zero%20memory%20allocation%20and%20%23performance%20in%20mind%20%F0%9F%9A%80%20https%3A%2F%2Fgithub.com%2Fgofiber%2Ffiber). +3. Write a review or tutorial on [Medium](https://medium.com/), [Dev.to](https://dev.to/) or personal blog. +4. Support the project by donating a [cup of coffee](https://buymeacoff.ee/fenny). + +## ☕ Supporters + +Fiber is an open source project that runs on donations to pay the bills e.g. our domain name, gitbook, netlify and serverless hosting. If you want to support Fiber, you can ☕ [**buy a coffee here**](https://buymeacoff.ee/fenny). + +| | User | Donation | +| :--------------------------------------------------------- | :----------------------------------------------- | :------- | +| ![](https://avatars.githubusercontent.com/u/204341?s=25) | [@destari](https://github.com/destari) | ☕ x 10 | +| ![](https://avatars.githubusercontent.com/u/63164982?s=25) | [@dembygenesis](https://github.com/dembygenesis) | ☕ x 5 | +| ![](https://avatars.githubusercontent.com/u/56607882?s=25) | [@thomasvvugt](https://github.com/thomasvvugt) | ☕ x 5 | +| ![](https://avatars.githubusercontent.com/u/27820675?s=25) | [@hendratommy](https://github.com/hendratommy) | ☕ x 5 | +| ![](https://avatars.githubusercontent.com/u/1094221?s=25) | [@ekaputra07](https://github.com/ekaputra07) | ☕ x 5 | +| ![](https://avatars.githubusercontent.com/u/194590?s=25) | [@jorgefuertes](https://github.com/jorgefuertes) | ☕ x 5 | +| ![](https://avatars.githubusercontent.com/u/186637?s=25) | [@candidosales](https://github.com/candidosales) | ☕ x 5 | +| ![](https://avatars.githubusercontent.com/u/29659953?s=25) | [@l0nax](https://github.com/l0nax) | ☕ x 3 | +| ![](https://avatars.githubusercontent.com/u/635852?s=25) | [@bihe](https://github.com/bihe) | ☕ x 3 | +| ![](https://avatars.githubusercontent.com/u/307334?s=25) | [@justdave](https://github.com/justdave) | ☕ x 3 | +| ![](https://avatars.githubusercontent.com/u/11155743?s=25) | [@koddr](https://github.com/koddr) | ☕ x 1 | +| ![](https://avatars.githubusercontent.com/u/29042462?s=25) | [@lapolinar](https://github.com/lapolinar) | ☕ x 1 | +| ![](https://avatars.githubusercontent.com/u/2978730?s=25) | [@diegowifi](https://github.com/diegowifi) | ☕ x 1 | +| ![](https://avatars.githubusercontent.com/u/44171355?s=25) | [@ssimk0](https://github.com/ssimk0) | ☕ x 1 | +| ![](https://avatars.githubusercontent.com/u/5638101?s=25) | [@raymayemir](https://github.com/raymayemir) | ☕ x 1 | +| ![](https://avatars.githubusercontent.com/u/619996?s=25) | [@melkorm](https://github.com/melkorm) | ☕ x 1 | +| ![](https://avatars.githubusercontent.com/u/31022056?s=25) | [@marvinjwendt](https://github.com/marvinjwendt) | ☕ x 1 | +| ![](https://avatars.githubusercontent.com/u/31921460?s=25) | [@toishy](https://github.com/toishy) | ☕ x 1 | + +## ‎‍💻 Code Contributors + +Code Contributors + +## ⭐️ Stargazers + +Stargazers over time + +## ⚠️ License + +Copyright (c) 2019-present [Fenny](https://github.com/fenny) and [Contributors](https://github.com/gofiber/fiber/graphs/contributors). `Fiber` is free and open-source software licensed under the [MIT License](https://github.com/gofiber/fiber/blob/master/LICENSE). Official logo was created by [Vic Shóstak](https://github.com/koddr) and distributed under [Creative Commons](https://creativecommons.org/licenses/by-sa/4.0/) license (CC BY-SA 4.0 International). + +**Third-party library licenses** + +- [colorable](https://github.com/mattn/go-colorable/blob/master/LICENSE) +- [isatty](https://github.com/mattn/go-isatty/blob/master/LICENSE) +- [runewidth](https://github.com/mattn/go-runewidth/blob/master/LICENSE) +- [fasthttp](https://github.com/valyala/fasthttp/blob/master/LICENSE) +- [bytebufferpool](https://github.com/valyala/bytebufferpool/blob/master/LICENSE) +- [fwd](https://github.com/philhofer/fwd/blob/master/LICENSE.md) +- [go-ole](https://github.com/go-ole/go-ole/blob/master/LICENSE) +- [gopsutil](https://github.com/shirou/gopsutil/blob/master/LICENSE) +- [msgp](https://github.com/tinylib/msgp/blob/master/LICENSE) +- [schema](https://github.com/gorilla/schema/blob/master/LICENSE) +- [uuid](https://github.com/google/uuid/blob/master/LICENSE) +- [wmi](https://github.com/StackExchange/wmi/blob/master/LICENSE) diff --git a/.github/README_az.md b/.github/README_az.md new file mode 100644 index 0000000..f1b6174 --- /dev/null +++ b/.github/README_az.md @@ -0,0 +1,716 @@ +

+ + + + + Fiber + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + +

+

+ Fiber Go dili üçün ən sürətli HTTP mühərriki FasthttpExpress kitabxanasına bənzər arxitektura üzərində qurulmuş bir web framework-dür. Sıfır yaddaş ayrılması (zero-memory allocation) və performans səbəbilə development prosesini sürətləndirməkasanlaşdırmaq üçün tərtib edilmişdir. +

+ +## ⚡️ Sürətli Başlanğıc + +```go +package main + +import "github.com/gofiber/fiber/v2" + +func main() { + app := fiber.New() + + app.Get("/", func(c *fiber.Ctx) error { + return c.SendString("Hello, World 👋!") + }) + + app.Listen(":3000") +} +``` + +## 🤖 Performans Dəyərləri + +Bu testlər [TechEmpower](https://www.techempower.com/benchmarks/#section=data-r19&hw=ph&test=plaintext) və [Go Web](https://github.com/smallnest/go-web-framework-benchmark) tərəfindən aparılıb. Bütün nəticələri görmək üçün [Wiki](https://docs.gofiber.io/extra/benchmarks) səhifəsinə keçid edə bilərsiniz. + +

+ + +

+ +## ⚙️ Quraşdırılması + +Go dilinin `1.17` və ya daha yuxarı versiyanın [yükləndiyindən](https://go.dev/dl/) əmin olun. + +Bir qovluq yaratdıqdan sonra, `go mod init github.com/your/repo` komandasını eyni qovluğun daxilində işə salaraq layihənizi başladın ([go modulları haqqında əlavə bilgilər](https://go.dev/blog/using-go-modules)). Növbəti addım olaraq Fiber-i [`go get`](https://pkg.go.dev/cmd/go/#hdr-Add_dependencies_to_current_module_and_install_them) komandasını işlədərək yükləyin: + +```bash +go get -u github.com/gofiber/fiber/v2 +``` + +## 🎯 Özəllikləri + +- Güclü [routing](https://docs.gofiber.io/guide/routing) +- [Static faylların](https://docs.gofiber.io/api/app#static) təqdimatı +- Yüksək [performans](https://docs.gofiber.io/extra/benchmarks) +- [Daha az yaddaş istifadəsi](https://docs.gofiber.io/extra/benchmarks) +- [API son nöqtələri (endpoint)](https://docs.gofiber.io/api/ctx) +- [Middleware](https://docs.gofiber.io/category/-middleware) & [Next](https://docs.gofiber.io/api/ctx#next) dəstəyi +- [Rapid](https://dev.to/koddr/welcome-to-fiber-an-express-js-styled-fastest-web-framework-written-with-on-golang-497) server yönümlü proqramlaşdırma +- [Template mühərrikləri](https://github.com/gofiber/template) +- [WebSocket dəstəyi](https://github.com/gofiber/websocket) +- [Server-Sent events](https://github.com/gofiber/recipes/tree/master/sse) +- [Rate Limiter](https://docs.gofiber.io/api/middleware/limiter) +- [19 dildə](https://docs.gofiber.io/) mövcudluğu + +Daha ətraflı məlumat üçün [rəsmi sənədləşməyə](https://docs.gofiber.io/) baxış keçirə bilərsiniz. + +## 💡 Fəlsəfə + +[Node.js](https://nodejs.org/en/about/)-dən [Go](https://go.dev/doc/)-ya yeni keçən gopher-lər veb tətbiqlər və mikroservislər yazmadan öncə dilin özünəməxsus sintaksisini öyrənməklə məşğul olurlar. Fiber MinimalizmUNIX-in yaradılış prinsiplərinə uyğun şəkildə qurulmuş bir web framework-dür. Bu sahədə yeni olan gopher-lər Go dünyasında özlərini doğma və güvənli hiss edə biləcək şəkildə bir ab-hava ilə rastlaşa bilərlər. + +Fiber internet üzərində olan ən məşhur web framework-lərdən biri olan Express-dən ilhamlanaraq ərsəyə gəlmişdir. Biz Express-in rahatlıq və asanlıq xüsusiyyətlərini, Go-nun çiy performansı ilə birləşdirmişik; əgər əvvəldən Node.js üzərində (Express və ya bənzərləri) veb tətbiqi yaratmısınızsa, onda əksər metodlar və prinsiplər sizə tanış gələcəkdir. + +Biz istifadəçilərdən gələn [issue-a](https://github.com/gofiber/fiber/issues), Discord [kanalımıza](https://gofiber.io/discord) və bütün interneti əhatə edən vasitələrdən gələn rəyləri nəzərə alırıq. Bunun nəzdində, biz sürətli və rahat şəkildə hər bir tapşırığın səviyyəsinə uyğun olan — dostcasına bir Go web framework-ü olmağı hədəfləmişik (Express-in JavaScript dünyasında etdiyi kimi). + +## ⚠️ Limitlər + +- Fiber unsafe prinsiplərə əsaslanaraq çalışdığından, o hər zaman Go-nun son versiyası ilə uyğunlaşmaya bilər. Buna görə də, Fiber 2.40.0 — Go 1.17 və 1.21 versiyaları ilə test edilərək saz vəziyyətə gətirilmişdir. +- Fiber net/http interfeysləri ilə uyğun deyil. Yəni gqlgen, go-swagger kimi net/http ekosisteminin parçası olan layihələri istifadə edə bilməzsiniz. + +## 👀 Misallar + +Aşağıda geniş istifadə olunan misallardan bəziləri siyahı şəklində qeyd olunub. Əgər daha çox koda dair misalları görmək istəyirsinizsə, onda [Əlavə misallardan ibarət github deposunu](https://github.com/gofiber/recipes) və ya [API sənədləşməni](https://docs.gofiber.io) nəzərdən keçirin. + +#### 📖 [**Sadə Routing**](https://docs.gofiber.io/#basic-routing) + +```go +func main() { + app := fiber.New() + + // GET /api/register + app.Get("/api/*", func(c *fiber.Ctx) error { + msg := fmt.Sprintf("✋ %s", c.Params("*")) + return c.SendString(msg) // => ✋ register + }) + + // GET /flights/LAX-SFO + app.Get("/flights/:from-:to", func(c *fiber.Ctx) error { + msg := fmt.Sprintf("💸 From: %s, To: %s", c.Params("from"), c.Params("to")) + return c.SendString(msg) // => 💸 From: LAX, To: SFO + }) + + // GET /dictionary.txt + app.Get("/:file.:ext", func(c *fiber.Ctx) error { + msg := fmt.Sprintf("📃 %s.%s", c.Params("file"), c.Params("ext")) + return c.SendString(msg) // => 📃 dictionary.txt + }) + + // GET /john/75 + app.Get("/:name/:age/:gender?", func(c *fiber.Ctx) error { + msg := fmt.Sprintf("👴 %s is %s years old", c.Params("name"), c.Params("age")) + return c.SendString(msg) // => 👴 john is 75 years old + }) + + // GET /john + app.Get("/:name", func(c *fiber.Ctx) error { + msg := fmt.Sprintf("Hello, %s 👋!", c.Params("name")) + return c.SendString(msg) // => Hello john 👋! + }) + + log.Fatal(app.Listen(":3000")) +} + +``` + +#### 📖 [**Route-un Adlandırılması**](https://docs.gofiber.io/api/app#name) + +```go +func main() { + app := fiber.New() + + // GET /api/register + app.Get("/api/*", func(c *fiber.Ctx) error { + msg := fmt.Sprintf("✋ %s", c.Params("*")) + return c.SendString(msg) // => ✋ register + }).Name("api") + + data, _ := json.MarshalIndent(app.GetRoute("api"), "", " ") + fmt.Print(string(data)) + // Prints: + // { + // "method": "GET", + // "name": "api", + // "path": "/api/*", + // "params": [ + // "*1" + // ] + // } + + + log.Fatal(app.Listen(":3000")) +} + +``` + +#### 📖 [**Static Fayl Təqdimatı**](https://docs.gofiber.io/api/app#static) + +```go +func main() { + app := fiber.New() + + app.Static("/", "./public") + // => http://localhost:3000/js/script.js + // => http://localhost:3000/css/style.css + + app.Static("/prefix", "./public") + // => http://localhost:3000/prefix/js/script.js + // => http://localhost:3000/prefix/css/style.css + + app.Static("*", "./public/index.html") + // => http://localhost:3000/any/path/shows/index/html + + log.Fatal(app.Listen(":3000")) +} + +``` + +#### 📖 [**Middleware & Next**](https://docs.gofiber.io/api/ctx#next) + +```go +func main() { + app := fiber.New() + + // Match any route + app.Use(func(c *fiber.Ctx) error { + fmt.Println("🥇 First handler") + return c.Next() + }) + + // Match all routes starting with /api + app.Use("/api", func(c *fiber.Ctx) error { + fmt.Println("🥈 Second handler") + return c.Next() + }) + + // GET /api/list + app.Get("/api/list", func(c *fiber.Ctx) error { + fmt.Println("🥉 Last handler") + return c.SendString("Hello, World 👋!") + }) + + log.Fatal(app.Listen(":3000")) +} + +``` + +
+ 📚 Daha çox misalllar + +### Baxış mühərriki (View Engine) + +📖 [Config](https://docs.gofiber.io/api/fiber#config) +📖 [Mühərriklər](https://github.com/gofiber/template) +📖 [Render](https://docs.gofiber.io/api/ctx#render) + +Fiber baxış mühərriki təyin edilmədikdə [html/template-in](https://pkg.go.dev/html/template/) default formasını alır. + +Əgər siz partial-ı və ya müxtəlif tipdə olan mühərrikləri istifadə etmək istəyirsinizsə, o zaman [amber](https://github.com/eknkc/amber), [handlebars](https://github.com/aymerick/raymond), [mustache](https://github.com/cbroglie/mustache), [pug](https://github.com/Joker/jade) və s. kimi misallara baxa bilərsiniz. + +Çoxsaylı baxış mühərriklərini dəstəkləyən [template](https://github.com/gofiber/template) package-ə göstərilən link vasitəsilə nəzərdən keçirə bilərsiniz. + +```go +package main + +import ( + "github.com/gofiber/fiber/v2" + "github.com/gofiber/template/pug" +) + +func main() { + // Baxış mühərrikini tətbiqi başlatzmadan əvvəl quraşdıra bilərsiniz: + app := fiber.New(fiber.Config{ + Views: pug.New("./views", ".pug"), + }) + + // Və indi `./views/home.pug` template-i bu şəkildə çağıra bilərsiniz: + app.Get("/", func(c *fiber.Ctx) error { + return c.Render("home", fiber.Map{ + "title": "Homepage", + "year": 1999, + }) + }) + + log.Fatal(app.Listen(":3000")) +} +``` + +### Route-ın zəncirlərdə qruplaşdırılması + +📖 [Group](https://docs.gofiber.io/api/app#group) + +```go +func middleware(c *fiber.Ctx) error { + fmt.Println("Don't mind me!") + return c.Next() +} + +func handler(c *fiber.Ctx) error { + return c.SendString(c.Path()) +} + +func main() { + app := fiber.New() + + // Root API route + api := app.Group("/api", middleware) // /api + + // API v1 routes + v1 := api.Group("/v1", middleware) // /api/v1 + v1.Get("/list", handler) // /api/v1/list + v1.Get("/user", handler) // /api/v1/user + + // API v2 routes + v2 := api.Group("/v2", middleware) // /api/v2 + v2.Get("/list", handler) // /api/v2/list + v2.Get("/user", handler) // /api/v2/user + + // ... +} + +``` + +### Middleware Logger + +📖 [Logger](https://docs.gofiber.io/api/middleware/logger) + +```go +package main + +import ( + "log" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/logger" +) + +func main() { + app := fiber.New() + + app.Use(logger.New()) + + // ... + + log.Fatal(app.Listen(":3000")) +} +``` + +### Cross-Origin Resource Sharing (CORS) + +📖 [CORS](https://docs.gofiber.io/api/middleware/cors) + +```go +import ( + "log" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/cors" +) + +func main() { + app := fiber.New() + + app.Use(cors.New()) + + // ... + + log.Fatal(app.Listen(":3000")) +} +``` + +"Origin" başlığında istənilən domeni keçməklə CORS-un yoxlanması: + +```bash +curl -H "Origin: http://example.com" --verbose http://localhost:3000 +``` + +### Custom 404 response + +📖 [HTTP Methods](https://docs.gofiber.io/api/ctx#status) + +```go +func main() { + app := fiber.New() + + app.Static("/", "./public") + + app.Get("/demo", func(c *fiber.Ctx) error { + return c.SendString("This is a demo!") + }) + + app.Post("/register", func(c *fiber.Ctx) error { + return c.SendString("Welcome!") + }) + + // Sonuncu middleware-in hər şeyə uyğunlaşdırılması + app.Use(func(c *fiber.Ctx) error { + return c.SendStatus(404) + // => 404 "Not Found" + }) + + log.Fatal(app.Listen(":3000")) +} +``` + +### JSON Response + +📖 [JSON](https://docs.gofiber.io/api/ctx#json) + +```go +type User struct { + Name string `json:"name"` + Age int `json:"age"` +} + +func main() { + app := fiber.New() + + app.Get("/user", func(c *fiber.Ctx) error { + return c.JSON(&User{"John", 20}) + // => {"name":"John", "age":20} + }) + + app.Get("/json", func(c *fiber.Ctx) error { + return c.JSON(fiber.Map{ + "success": true, + "message": "Hi John!", + }) + // => {"success":true, "message":"Hi John!"} + }) + + log.Fatal(app.Listen(":3000")) +} +``` + +### WebSocket-in təkminləşdirilməsi (upgrade) + +📖 [Websocket](https://github.com/gofiber/websocket) + +```go +import ( + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/websocket" +) + +func main() { + app := fiber.New() + + app.Get("/ws", websocket.New(func(c *websocket.Conn) { + for { + mt, msg, err := c.ReadMessage() + if err != nil { + log.Println("read:", err) + break + } + log.Printf("recv: %s", msg) + err = c.WriteMessage(mt, msg) + if err != nil { + log.Println("write:", err) + break + } + } + })) + + log.Fatal(app.Listen(":3000")) + // ws://localhost:3000/ws +} +``` + +### Server-Sent Events + +📖 [More Info](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events) + +```go +import ( + "github.com/gofiber/fiber/v2" + "github.com/valyala/fasthttp" +) + +func main() { + app := fiber.New() + + app.Get("/sse", func(c *fiber.Ctx) error { + c.Set("Content-Type", "text/event-stream") + c.Set("Cache-Control", "no-cache") + c.Set("Connection", "keep-alive") + c.Set("Transfer-Encoding", "chunked") + + c.Context().SetBodyStreamWriter(fasthttp.StreamWriter(func(w *bufio.Writer) { + fmt.Println("WRITER") + var i int + + for { + i++ + msg := fmt.Sprintf("%d - the time is %v", i, time.Now()) + fmt.Fprintf(w, "data: Message: %s\n\n", msg) + fmt.Println(msg) + + w.Flush() + time.Sleep(5 * time.Second) + } + })) + + return nil + }) + + log.Fatal(app.Listen(":3000")) +} +``` + +### Middleware-in Bərpası + +📖 [Recover](https://docs.gofiber.io/api/middleware/recover) + +```go +import ( + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/recover" +) + +func main() { + app := fiber.New() + + app.Use(recover.New()) + + app.Get("/", func(c *fiber.Ctx) error { + panic("normally this would crash your app") + }) + + log.Fatal(app.Listen(":3000")) +} +``` + +
+ +### Etibarlı Proxy İstifadəsi + +📖 [Config](https://docs.gofiber.io/api/fiber#config) + +```go +import ( + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/recover" +) + +func main() { + app := fiber.New(fiber.Config{ + EnableTrustedProxyCheck: true, + TrustedProxies: []string{"0.0.0.0", "1.1.1.1/30"}, // IP address or IP address range + ProxyHeader: fiber.HeaderXForwardedFor, + }) + + // ... + + log.Fatal(app.Listen(":3000")) +} +``` + + + +## 🧬 Daxili Middleware + +Aşağıda Fiber-in daxilində olan middleware-lər siyahı şəklində göstərilmişdir. + +| Middleware | Açıqlama | +| :------------------------------------------------------------------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [basicauth](https://github.com/gofiber/fiber/tree/master/middleware/basicauth) | Sadə bir auth middleware-dir və HTTP Basic Auth yaratmaq üçün istifadə olunur. Keçərli vəsiqə (credentials) bilgiləri üçün sonrakı handler-i, əksik və ya keçərsiz vəsiqə bilgiləri üçün 401 qaytarır. | +| [cache](https://github.com/gofiber/fiber/tree/master/middleware/cache) | Response-ı dayandırır və keşə yerləşdirir. | +| [compress](https://github.com/gofiber/fiber/tree/master/middleware/compress) | Fiber üçün sıxışdırma (compression) middleware-dir. Default olaraq `deflate`, `gzip` və `brotli` dəstəkləyir. | +| [cors](https://github.com/gofiber/fiber/tree/master/middleware/cors) | Çeşidli seçimlərlə başlanğıclar arası mənbə paylaşımı (CORS) aktivləşdirir. | +| [csrf](https://github.com/gofiber/fiber/tree/master/middleware/csrf) | CSRF exploit-dən qorunmasını təmin edir. | +| [encryptcookie](https://github.com/gofiber/fiber/tree/master/middleware/encryptcookie) | Encrypt middleware-i cookie dəyərlərini şifrələyir. | +| [envvar](https://github.com/gofiber/fiber/tree/master/middleware/envvar) | Environment dəyərlərini göstərilən config-ə görə təyin edir. | +| [etag](https://github.com/gofiber/fiber/tree/master/middleware/etag) | Keşlərin daha səmərəli istifadəsinə və bant genişliyinə qənaət etməyə imkan verən ETag middleware-i; məzmun dəyişməyibsə veb serverin response-nı təkrar göndərməsinin qarşısını alır. | +| [expvar](https://github.com/gofiber/fiber/tree/master/middleware/expvar) | Expvar middleware, HTTP serverlərinin bəzi runtime dəyərlərini JSON formatında göstərir. | +| [favicon](https://github.com/gofiber/fiber/tree/master/middleware/favicon) | Əgər faylın yolu (path) göstərilmişdirsə, artıq loglarda olan favicon-u yox sayıb onu saxlanan depodan götürür. | +| [filesystem](https://github.com/gofiber/fiber/tree/master/middleware/filesystem) | Fiber üçün fayl sistem middleware-i. Alireza Salary-ə xüsusi təşəkkürlər. | +| [limiter](https://github.com/gofiber/fiber/tree/master/middleware/limiter) | Fiber üçün rate limitləyən middleware. Açıq API-ə və ya şifrə yeniləmə kimi endpoint-ə yönəlik təkrarlanan request-in qarşısını alır. | +| [logger](https://github.com/gofiber/fiber/tree/master/middleware/logger) | HTTP istək/cavab (request/response) logger-i. | +| [monitor](https://github.com/gofiber/fiber/tree/master/middleware/monitor) | Monitor middleware-i serverin metriklərini report edər ("Express-status-monitor"-dan qaynaqlanıb). | +| [pprof](https://github.com/gofiber/fiber/tree/master/middleware/pprof) | Matthew Lee-yə xüsusi təşəkkürlər \(@mthli\). | +| [proxy](https://github.com/gofiber/fiber/tree/master/middleware/proxy) | Birdən çox server-ə proxy istəyi göndərməyiniz üçündür. | +| [recover](https://github.com/gofiber/fiber/tree/master/middleware/recover) | Recover middleware-i stack chain-ni hər hansı bir yerindəki paniklərdən qurtulmasına kömək edir və kontrolu mərkəzləşdirilmiş [ErrorHandler-ə](https://docs.gofiber.io/guide/error-handling) ötürür. | +| [requestid](https://github.com/gofiber/fiber/tree/master/middleware/requestid) | Hər request üçün ayrı request id yaradır. | +| [session](https://github.com/gofiber/fiber/tree/master/middleware/session) | Session üçün middleware. Qeyd: Bu middleware Fiber-in öz storage struktrunu istifadə edir. | +| [skip](https://github.com/gofiber/fiber/tree/master/middleware/skip) | Skip middleware-i verilən şərt true olduğu halda handler-i görməyərək üstündən ötüb keçir. | +| [timeout](https://github.com/gofiber/fiber/tree/master/middleware/timeout) | Bir request üçün maksimum vaxt əlavə edir. Əgər arada fasilə yaranarsa, onda proses məhz ErrorHandler-ə göndərilərək icra edilir. | +| [keyauth](https://github.com/gofiber/keyauth) | Key giriş middleware-i, key əsaslı bir authentication metodudur. | +| [redirect](https://github.com/gofiber/redirect) | Yönləndirmə üçün middleware. | +| [rewrite](https://github.com/gofiber/rewrite) | Rewrite middleware-i verilən qanunlara əsasən URL yolunu (path) yenidən yazır. Geri dönüşün icrası üçün uyğunluq təşkil edən təsviri linklərin yaradılması üçün nəzərdə tutulmuşdur. | +| [adaptor](https://github.com/gofiber/adaptor) | Fiber request handler-dən net/http handler-ə çevirici. @arsmn-ə xüsusi təşəkkürlər! | +| [helmet](https://github.com/gofiber/helmet) | Fərqli HTTP header istifadə edərək tətbiqi daha təhlükəsiz saxlamağa kömək edir. | + +## 🧬 Xarici Middleware + +[Fiber komandası](https://github.com/orgs/gofiber/people) tərəfindən dəstəklənən və inkişaf etdirilən middleware-in siyahısı. + +| Middleware | Description | +| :------------------------------------------------ | :----------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [jwt](https://github.com/gofiber/jwt) | JWT, JSON Web Token(JWT) girişi qaytaran bir middleware-dir. | +| [storage](https://github.com/gofiber/storage) | Fiber-in Storage arxitekturasını dəstəkləyən bir sıra storage driver verir. Bu sayədə storage-ə ehtiyac duyan Fiber middleware-də rahatlıqla istifadə oluna bilər. | +| [template](https://github.com/gofiber/template) | Bu paket, Fiber `v1.10.x`, Go versiyası 1.13 və ya daha yuxarı olduqda istifadə oluna bilər. 8 template mühərriki var. | +| [websocket](https://github.com/gofiber/websocket) | Yerlilərin dəstəyi ilə WebSocket-ə əsaslanan Fiber üçün Fasthttp. | + +## 🕶️ Möhtəşəm Siyahı + +Əlavə yazılar, middleware-lər, misallar, və alətlər üçün bizim [möhtəşəm siyahımıza](https://github.com/gofiber/awesome-fiber) göz atın. + +## 👍 Dəstək Nümayişi + +Əgər `Fiber`-ə dəstək olmaq və ya **təşəkkür etmək** istəyirsinizsə: + +1. Layihəni [GitHub Ulduzu](https://github.com/gofiber/fiber/stargazers) ilə işarələyin. +2. Layihə haqqında [şəxsi 𝕏 (Twitter) hesabınızda](https://x.com/intent/tweet?text=Fiber%20is%20an%20Express%20inspired%20%23web%20%23framework%20built%20on%20top%20of%20Fasthttp%2C%20the%20fastest%20HTTP%20engine%20for%20%23Go.%20Designed%20to%20ease%20things%20up%20for%20%23fast%20development%20with%20zero%20memory%20allocation%20and%20%23performance%20in%20mind%20%F0%9F%9A%80%20https%3A%2F%2Fgithub.com%2Fgofiber%2Ffiber) paylaşın. +3. [Medium](https://medium.com/), [Dev.to](https://dev.to/) və ya şəxsi bloqunuz üzərindən bir incələmə və ya tədris yönümlü bir yazı dərc edin. +4. Bizim üçün, sadəcə bir [fincan kofe alın](https://buymeacoff.ee/fenny). + +## ☕ "Bir fincan kofe almaq" məsələsi + +Fiber açıq qaynaqlı bir layihə olduğu üçün, gəlirlərini yalnız ianələr vasitəsilə təmin edir və bu da domain adı, gitbook, netlify, serverless hosting xərcləri üçün istifadə olunur. Belə olduğu halda, Fiber-ə ən yaxşı dəstək elə bizim üçün ☕ [**bir kofe almaqdan gələ bilər**](https://buymeacoff.ee/fenny). + +| | İstifadəçi | İanə | +| :--------------------------------------------------------- | :----------------------------------------------- | :------ | +| ![](https://avatars.githubusercontent.com/u/204341?s=25) | [@destari](https://github.com/destari) | ☕ x 10 | +| ![](https://avatars.githubusercontent.com/u/63164982?s=25) | [@dembygenesis](https://github.com/dembygenesis) | ☕ x 5 | +| ![](https://avatars.githubusercontent.com/u/56607882?s=25) | [@thomasvvugt](https://github.com/thomasvvugt) | ☕ x 5 | +| ![](https://avatars.githubusercontent.com/u/27820675?s=25) | [@hendratommy](https://github.com/hendratommy) | ☕ x 5 | +| ![](https://avatars.githubusercontent.com/u/1094221?s=25) | [@ekaputra07](https://github.com/ekaputra07) | ☕ x 5 | +| ![](https://avatars.githubusercontent.com/u/194590?s=25) | [@jorgefuertes](https://github.com/jorgefuertes) | ☕ x 5 | +| ![](https://avatars.githubusercontent.com/u/186637?s=25) | [@candidosales](https://github.com/candidosales) | ☕ x 5 | +| ![](https://avatars.githubusercontent.com/u/29659953?s=25) | [@l0nax](https://github.com/l0nax) | ☕ x 3 | +| ![](https://avatars.githubusercontent.com/u/635852?s=25) | [@bihe](https://github.com/bihe) | ☕ x 3 | +| ![](https://avatars.githubusercontent.com/u/307334?s=25) | [@justdave](https://github.com/justdave) | ☕ x 3 | +| ![](https://avatars.githubusercontent.com/u/11155743?s=25) | [@koddr](https://github.com/koddr) | ☕ x 1 | +| ![](https://avatars.githubusercontent.com/u/29042462?s=25) | [@lapolinar](https://github.com/lapolinar) | ☕ x 1 | +| ![](https://avatars.githubusercontent.com/u/2978730?s=25) | [@diegowifi](https://github.com/diegowifi) | ☕ x 1 | +| ![](https://avatars.githubusercontent.com/u/44171355?s=25) | [@ssimk0](https://github.com/ssimk0) | ☕ x 1 | +| ![](https://avatars.githubusercontent.com/u/5638101?s=25) | [@raymayemir](https://github.com/raymayemir) | ☕ x 1 | +| ![](https://avatars.githubusercontent.com/u/619996?s=25) | [@melkorm](https://github.com/melkorm) | ☕ x 1 | +| ![](https://avatars.githubusercontent.com/u/31022056?s=25) | [@marvinjwendt](https://github.com/marvinjwendt) | ☕ x 1 | +| ![](https://avatars.githubusercontent.com/u/31921460?s=25) | [@toishy](https://github.com/toishy) | ☕ x 1 | + +## ‎‍💻 Koda Töhfə Verənlər + +Code Contributors + +## ⭐️ Layihəni Ulduzlayanlar + +Stargazers over time + +## ⚠️ Lisenziya Haqqında + +Müəllif Hüququ (c) 2019-bugün [Fenny](https://github.com/fenny) və [Contributors](https://github.com/gofiber/fiber/graphs/contributors). `Fiber` pulsuz və açıq qaynaqlı bir proqram təminatıdır və [MIT License](https://github.com/gofiber/fiber/blob/master/LICENSE) altında lisenziyalaşmışdır. Rəsmi loqo [Vic Shóstak](https://github.com/koddr) tərəfindən yaradılmış və [Creative Commons](https://creativecommons.org/licenses/by-sa/4.0/) lisenziyası altında paylanmışdır (CC BY-SA 4.0 International). + +**Üçüncü Tərəf Kitabxana Lisenziyaları** + +- [colorable](https://github.com/mattn/go-colorable/blob/master/LICENSE) +- [isatty](https://github.com/mattn/go-isatty/blob/master/LICENSE) +- [runewidth](https://github.com/mattn/go-runewidth/blob/master/LICENSE) +- [fasthttp](https://github.com/valyala/fasthttp/blob/master/LICENSE) +- [bytebufferpool](https://github.com/valyala/bytebufferpool/blob/master/LICENSE) +- [fwd](https://github.com/philhofer/fwd/blob/master/LICENSE.md) +- [go-ole](https://github.com/go-ole/go-ole/blob/master/LICENSE) +- [gopsutil](https://github.com/shirou/gopsutil/blob/master/LICENSE) +- [msgp](https://github.com/tinylib/msgp/blob/master/LICENSE) +- [schema](https://github.com/gorilla/schema/blob/master/LICENSE) +- [uuid](https://github.com/google/uuid/blob/master/LICENSE) +- [wmi](https://github.com/StackExchange/wmi/blob/master/LICENSE) diff --git a/.github/README_ckb.md b/.github/README_ckb.md new file mode 100644 index 0000000..5f52bf8 --- /dev/null +++ b/.github/README_ckb.md @@ -0,0 +1,714 @@ +

+ + + + + Fiber + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + +

+

+ فایبەر فرەیموۆرکێکی هاوشێوەی Expressـە کە لەسەر Fasthttp دروست کراوە و خێراترین مەکینەی HTTPـە بۆ Go. دیزاین کراوە بۆ بەفیڕۆنەدانی میمۆری و گوێدانە کارکردن بەخێرایی. +

+ +## ⚡️ دەستپێکێکی خێرا + +```go +package main + +import "github.com/gofiber/fiber/v2" + +func main() { + app := fiber.New() + + app.Get("/", func(c *fiber.Ctx) error { + return c.SendString("سڵاو، دنیا 👋!") + }) + + app.Listen(":3000") +} +``` + +## 🤖 بێنچمارکەکان + +ئەم تاقیکردنەوانە لەلایەن [TechEmpower](https://www.techempower.com/benchmarks/#section=data-r19&hw=ph&test=plaintext) و [Go Web](https://github.com/smallnest/go-web-framework-benchmark) ئەنجام دراون. دەتوانیت هەموو ئەنجامەکان [لێرە](https://docs.gofiber.io/extra/benchmarks) ببینیت. + +

+ + +

+ +## ⚙️ دامەزراندن + +دڵنیا بە لەوەی کە لەناو ئامێرەکەت Go دامەزراوە ([دای بگرە](https://go.dev/dl/)). دەبێت وەشانەکەشی `1.17` یان سەرووتر بێت. + +پڕۆژەکەت دەست پێ بکە بە دروستکردنی بوخچەیەک و کار پێ کردنی فەرمانی `go mod init github.com/your/repo` ([زیاتر](https://go.dev/blog/using-go-modules)) لەناو بوخچەکە. دواتریش بەم فەرمانەی خوارەوە فایبەر دامەزرێنە: + +```bash +go get -u github.com/gofiber/fiber/v2 +``` + +## 🎯 تایبەتمەندییەکان + +- [ناونیشانی ئاڵۆز](https://docs.gofiber.io/guide/routing) +- [فایلی جێگیر](https://docs.gofiber.io/api/app#static) +- [خێراییەکی](https://docs.gofiber.io/extra/benchmarks) بێوێنە +- بەکارهێنانی [میمۆریی کەم](https://docs.gofiber.io/extra/benchmarks) +- توانای هەبوونی لقی [API](https://docs.gofiber.io/api/ctx) +- پشتگیریی [Middleware](https://docs.gofiber.io/category/-middleware) و [Next](https://docs.gofiber.io/api/ctx#next) وەک Express +- پڕۆگرامکردنی [خێرا](https://dev.to/koddr/welcome-to-fiber-an-express-js-styled-fastest-web-framework-written-with-on-golang-497)ی ڕاژە +- [داڕێژە](https://github.com/gofiber/template) +- پشتگیریی [WebSocket](https://github.com/gofiber/websocket) +- [Server-Sent events](https://github.com/gofiber/recipes/tree/master/sse) +- [Rate Limiter](https://docs.gofiber.io/api/middleware/limiter) +- وەرگێڕراوە بۆ [19 زمان](https://docs.gofiber.io/) +- زیاتریش، [فایبەر بپشکنە](https://docs.gofiber.io/) + +## 💡 فەلسەفە + +پڕۆگرامەرە نوێکانی گۆ کە لە نۆد جەی ئێسەوە دێن، مامەڵە لەگەڵ کەوانەیەکی فێربوون دەکەن + +فایبەر بە شێوەیەکی نایاب بە سادەییەکەی پێشوازییەکی گەرم لە بەکارهێنەرە نوێکانی گۆ دەکات، کە زۆریان لە [نۆد](https://nodejs.org/en/about/)ـەوە هاتوون. + +فایبەر لە Express دەچێت، کە کتێبخانەیەکی بەناوبانگی نۆدە. لەگەڵ تایبەتمەندییەکان و خێرایی گۆ تێکەڵ کراوە. ئەگەر پێشتر شتێکی وەک ئێکسپرێست بەکار هێنابێت، فایبەر زۆر ئاسان دەبێت. + +گوێ لە هەموو بەکارهێنەرەکانیشمان دەگرین لە [کەناڵی دیسکۆردەکەمان](https://gofiber.io/discord) و [بەشی کێشەکان](https://github.com/gofiber/fiber/issues). + +## ⚠️ سنوورەکان + +- تایبەتمەندیی `unsafe` بەکار دەهێنێت کە وای لێ دەکات لەگەڵ هەندێک وەشانی گۆ نەگونجێت. +- لەگەڵ `net/http` ناگونجێت. + +## 👀 نموونەکان + +لە خوارەوە کۆمەڵێک نموونەی سادە هەن. ئەگەر دەتەوێت زیاتر ببینیت، تکایە سەردانی [ئێرە](https://github.com/gofiber/recipes) یان [ئێرە](https://docs.gofiber.io) بکە. + +#### 📖 [**Basic Routing**](https://docs.gofiber.io/#basic-routing) + +```go +func main() { + app := fiber.New() + + // GET /api/register + app.Get("/api/*", func(c *fiber.Ctx) error { + msg := fmt.Sprintf("✋ %s", c.Params("*")) + return c.SendString(msg) // => ✋ register + }) + + // GET /flights/LAX-SFO + app.Get("/flights/:from-:to", func(c *fiber.Ctx) error { + msg := fmt.Sprintf("💸 From: %s, To: %s", c.Params("from"), c.Params("to")) + return c.SendString(msg) // => 💸 From: LAX, To: SFO + }) + + // GET /dictionary.txt + app.Get("/:file.:ext", func(c *fiber.Ctx) error { + msg := fmt.Sprintf("📃 %s.%s", c.Params("file"), c.Params("ext")) + return c.SendString(msg) // => 📃 dictionary.txt + }) + + // GET /john/75 + app.Get("/:name/:age/:gender?", func(c *fiber.Ctx) error { + msg := fmt.Sprintf("👴 %s is %s years old", c.Params("name"), c.Params("age")) + return c.SendString(msg) // => 👴 john is 75 years old + }) + + // GET /john + app.Get("/:name", func(c *fiber.Ctx) error { + msg := fmt.Sprintf("Hello, %s 👋!", c.Params("name")) + return c.SendString(msg) // => Hello john 👋! + }) + + log.Fatal(app.Listen(":3000")) +} + +``` + +#### 📖 [**Route Naming**](https://docs.gofiber.io/api/app#name) + +```go +func main() { + app := fiber.New() + + // GET /api/register + app.Get("/api/*", func(c *fiber.Ctx) error { + msg := fmt.Sprintf("✋ %s", c.Params("*")) + return c.SendString(msg) // => ✋ register + }).Name("api") + + data, _ := json.MarshalIndent(app.GetRoute("api"), "", " ") + fmt.Print(string(data)) + // Prints: + // { + // "method": "GET", + // "name": "api", + // "path": "/api/*", + // "params": [ + // "*1" + // ] + // } + + + log.Fatal(app.Listen(":3000")) +} + +``` + +#### 📖 [**Serving Static Files**](https://docs.gofiber.io/api/app#static) + +```go +func main() { + app := fiber.New() + + app.Static("/", "./public") + // => http://localhost:3000/js/script.js + // => http://localhost:3000/css/style.css + + app.Static("/prefix", "./public") + // => http://localhost:3000/prefix/js/script.js + // => http://localhost:3000/prefix/css/style.css + + app.Static("*", "./public/index.html") + // => http://localhost:3000/any/path/shows/index/html + + log.Fatal(app.Listen(":3000")) +} + +``` + +#### 📖 [**Middleware & Next**](https://docs.gofiber.io/api/ctx#next) + +```go +func main() { + app := fiber.New() + + // Match any route + app.Use(func(c *fiber.Ctx) error { + fmt.Println("🥇 First handler") + return c.Next() + }) + + // Match all routes starting with /api + app.Use("/api", func(c *fiber.Ctx) error { + fmt.Println("🥈 Second handler") + return c.Next() + }) + + // GET /api/list + app.Get("/api/list", func(c *fiber.Ctx) error { + fmt.Println("🥉 Last handler") + return c.SendString("Hello, World 👋!") + }) + + log.Fatal(app.Listen(":3000")) +} + +``` + +
+ 📚 نموونەی زیاتر نیشان بدە + +### داڕێژەکان + +📖 [Config](https://docs.gofiber.io/api/fiber#config) +📖 [Engines](https://github.com/gofiber/template) +📖 [Render](https://docs.gofiber.io/api/ctx#render) + +ئەگەر هیچ داڕێژەیەکت دەستنیشان نەکردبێت، [html/template](https://pkg.go.dev/html/template/) خۆکارانە دەستنیشان دەکرێت بۆ بەکارهێنان. + +بۆ تایبەتمەندیی زیاتر، ئەمانە ببینە: [amber](https://github.com/eknkc/amber), [handlebars](https://github.com/aymerick/raymond), [mustache](https://github.com/cbroglie/mustache), [pug](https://github.com/Joker/jade). + +پاکێجی [Template](https://github.com/gofiber/template)ـمان ببینە کە پشتگیریی چەندان داڕێژە دەکات. + +```go +package main + +import ( + "github.com/gofiber/fiber/v2" + "github.com/gofiber/template/pug" +) + +func main() { + // You can setup Views engine before initiation app: + app := fiber.New(fiber.Config{ + Views: pug.New("./views", ".pug"), + }) + + // And now, you can call template `./views/home.pug` like this: + app.Get("/", func(c *fiber.Ctx) error { + return c.Render("home", fiber.Map{ + "title": "Homepage", + "year": 1999, + }) + }) + + log.Fatal(app.Listen(":3000")) +} +``` + +### زنجیرەکان + +📖 [Group](https://docs.gofiber.io/api/app#group) + +```go +func middleware(c *fiber.Ctx) error { + fmt.Println("Don't mind me!") + return c.Next() +} + +func handler(c *fiber.Ctx) error { + return c.SendString(c.Path()) +} + +func main() { + app := fiber.New() + + // Root API route + api := app.Group("/api", middleware) // /api + + // API v1 routes + v1 := api.Group("/v1", middleware) // /api/v1 + v1.Get("/list", handler) // /api/v1/list + v1.Get("/user", handler) // /api/v1/user + + // API v2 routes + v2 := api.Group("/v2", middleware) // /api/v2 + v2.Get("/list", handler) // /api/v2/list + v2.Get("/user", handler) // /api/v2/user + + // ... +} + +``` + +### Middleware logger + +📖 [Logger](https://docs.gofiber.io/api/middleware/logger) + +```go +package main + +import ( + "log" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/logger" +) + +func main() { + app := fiber.New() + + app.Use(logger.New()) + + // ... + + log.Fatal(app.Listen(":3000")) +} +``` + +### Cross-Origin Resource Sharing (CORS) + +📖 [CORS](https://docs.gofiber.io/api/middleware/cors) + +```go +import ( + "log" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/cors" +) + +func main() { + app := fiber.New() + + app.Use(cors.New()) + + // ... + + log.Fatal(app.Listen(":3000")) +} +``` + +Check CORS by passing any domain in `Origin` header: + +```bash +curl -H "Origin: http://example.com" --verbose http://localhost:3000 +``` + +### گۆڕینی 404 + +📖 [HTTP Methods](https://docs.gofiber.io/api/ctx#status) + +```go +func main() { + app := fiber.New() + + app.Static("/", "./public") + + app.Get("/demo", func(c *fiber.Ctx) error { + return c.SendString("This is a demo!") + }) + + app.Post("/register", func(c *fiber.Ctx) error { + return c.SendString("Welcome!") + }) + + // Last middleware to match anything + app.Use(func(c *fiber.Ctx) error { + return c.SendStatus(404) + // => 404 "Not Found" + }) + + log.Fatal(app.Listen(":3000")) +} +``` + +### وەڵامی JSON + +📖 [JSON](https://docs.gofiber.io/api/ctx#json) + +```go +type User struct { + Name string `json:"name"` + Age int `json:"age"` +} + +func main() { + app := fiber.New() + + app.Get("/user", func(c *fiber.Ctx) error { + return c.JSON(&User{"John", 20}) + // => {"name":"John", "age":20} + }) + + app.Get("/json", func(c *fiber.Ctx) error { + return c.JSON(fiber.Map{ + "success": true, + "message": "Hi John!", + }) + // => {"success":true, "message":"Hi John!"} + }) + + log.Fatal(app.Listen(":3000")) +} +``` + +### WebSocket Upgrade + +📖 [Websocket](https://github.com/gofiber/websocket) + +```go +import ( + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/websocket" +) + +func main() { + app := fiber.New() + + app.Get("/ws", websocket.New(func(c *websocket.Conn) { + for { + mt, msg, err := c.ReadMessage() + if err != nil { + log.Println("read:", err) + break + } + log.Printf("recv: %s", msg) + err = c.WriteMessage(mt, msg) + if err != nil { + log.Println("write:", err) + break + } + } + })) + + log.Fatal(app.Listen(":3000")) + // ws://localhost:3000/ws +} +``` + +### Server-Sent Events + +📖 [More Info](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events) + +```go +import ( + "github.com/gofiber/fiber/v2" + "github.com/valyala/fasthttp" +) + +func main() { + app := fiber.New() + + app.Get("/sse", func(c *fiber.Ctx) error { + c.Set("Content-Type", "text/event-stream") + c.Set("Cache-Control", "no-cache") + c.Set("Connection", "keep-alive") + c.Set("Transfer-Encoding", "chunked") + + c.Context().SetBodyStreamWriter(fasthttp.StreamWriter(func(w *bufio.Writer) { + fmt.Println("WRITER") + var i int + + for { + i++ + msg := fmt.Sprintf("%d - the time is %v", i, time.Now()) + fmt.Fprintf(w, "data: Message: %s\n\n", msg) + fmt.Println(msg) + + w.Flush() + time.Sleep(5 * time.Second) + } + })) + + return nil + }) + + log.Fatal(app.Listen(":3000")) +} +``` + +### Recover middleware + +📖 [Recover](https://docs.gofiber.io/api/middleware/recover) + +```go +import ( + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/recover" +) + +func main() { + app := fiber.New() + + app.Use(recover.New()) + + app.Get("/", func(c *fiber.Ctx) error { + panic("normally this would crash your app") + }) + + log.Fatal(app.Listen(":3000")) +} +``` + +
+ +### بەکارهێنانی پڕۆکسی + +📖 [Config](https://docs.gofiber.io/api/fiber#config) + +```go +import ( + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/recover" +) + +func main() { + app := fiber.New(fiber.Config{ + EnableTrustedProxyCheck: true, + TrustedProxies: []string{"0.0.0.0", "1.1.1.1/30"}, // IP address or IP address range + ProxyHeader: fiber.HeaderXForwardedFor, + }) + + // ... + + log.Fatal(app.Listen(":3000")) +} +``` + + + +## 🧬 کاڵا ناوەکییکان + +ئەمە لیستی ئەو کاڵانەیە کە لەناو فایبەر جێگیر کراون. + +| کاڵا | دەربارە | +| :------------------------------------------------------------------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [basicauth](https://github.com/gofiber/fiber/tree/master/middleware/basicauth) | Basic auth middleware provides an HTTP basic authentication. It calls the next handler for valid credentials and 401 Unauthorized for missing or invalid credentials. | +| [cache](https://github.com/gofiber/fiber/tree/master/middleware/cache) | Intercept and cache responses | +| [compress](https://github.com/gofiber/fiber/tree/master/middleware/compress) | Compression middleware for Fiber, it supports `deflate`, `gzip` and `brotli` by default. | +| [cors](https://github.com/gofiber/fiber/tree/master/middleware/cors) | Enable cross-origin resource sharing \(CORS\) with various options. | +| [csrf](https://github.com/gofiber/fiber/tree/master/middleware/csrf) | Protect from CSRF exploits. | +| [encryptcookie](https://github.com/gofiber/fiber/tree/master/middleware/encryptcookie) | Encrypt middleware which encrypts cookie values. | +| [envvar](https://github.com/gofiber/fiber/tree/master/middleware/envvar) | Expose environment variables with providing an optional config. | +| [etag](https://github.com/gofiber/fiber/tree/master/middleware/etag) | ETag middleware that lets caches be more efficient and save bandwidth, as a web server does not need to resend a full response if the content has not changed. | +| [expvar](https://github.com/gofiber/fiber/tree/master/middleware/expvar) | Expvar middleware that serves via its HTTP server runtime exposed variants in the JSON format. | +| [favicon](https://github.com/gofiber/fiber/tree/master/middleware/favicon) | Ignore favicon from logs or serve from memory if a file path is provided. | +| [filesystem](https://github.com/gofiber/fiber/tree/master/middleware/filesystem) | FileSystem middleware for Fiber, special thanks and credits to Alireza Salary | +| [limiter](https://github.com/gofiber/fiber/tree/master/middleware/limiter) | Rate-limiting middleware for Fiber. Use to limit repeated requests to public APIs and/or endpoints such as password reset. | +| [logger](https://github.com/gofiber/fiber/tree/master/middleware/logger) | HTTP request/response logger. | +| [monitor](https://github.com/gofiber/fiber/tree/master/middleware/monitor) | Monitor middleware that reports server metrics, inspired by express-status-monitor | +| [pprof](https://github.com/gofiber/fiber/tree/master/middleware/pprof) | Special thanks to Matthew Lee \(@mthli\) | +| [proxy](https://github.com/gofiber/fiber/tree/master/middleware/proxy) | Allows you to proxy requests to a multiple servers | +| [recover](https://github.com/gofiber/fiber/tree/master/middleware/recover) | Recover middleware recovers from panics anywhere in the stack chain and handles the control to the centralized[ ErrorHandler](https://docs.gofiber.io/guide/error-handling). | +| [requestid](https://github.com/gofiber/fiber/tree/master/middleware/requestid) | Adds a requestid to every request. | +| [session](https://github.com/gofiber/fiber/tree/master/middleware/session) | Session middleware. NOTE: This middleware uses our Storage package. | +| [skip](https://github.com/gofiber/fiber/tree/master/middleware/skip) | Skip middleware that skips a wrapped handler is a predicate is true. | +| [timeout](https://github.com/gofiber/fiber/tree/master/middleware/timeout) | Adds a max time for a request and forwards to ErrorHandler if it is exceeded. | +| [keyauth](https://github.com/gofiber/keyauth) | Key auth middleware provides a key based authentication. | +| [redirect](https://github.com/gofiber/redirect) | Redirect middleware | +| [rewrite](https://github.com/gofiber/rewrite) | Rewrite middleware rewrites the URL path based on provided rules. It can be helpful for backward compatibility or just creating cleaner and more descriptive links. | +| [adaptor](https://github.com/gofiber/adaptor) | Converter for net/http handlers to/from Fiber request handlers, special thanks to @arsmn! | +| [helmet](https://github.com/gofiber/helmet) | Helps secure your apps by setting various HTTP headers. | + +## 🧬 کاڵا دەرەکییەکان + +ئەمە لیستی ئەو کاڵا دەرەکییانەیە کە لەلایەن [تیمی فایبەر](https://github.com/orgs/gofiber/people) بەڕێوە دەبرێن. + +| کاڵا | دەربارە | +| :------------------------------------------------ | :-------------------------------------------------------------------------------------------------------------------- | +| [jwt](https://github.com/gofiber/jwt) | JWT returns a JSON Web Token \(JWT\) auth middleware. | +| [storage](https://github.com/gofiber/storage) | Premade storage drivers that implement the Storage interface, designed to be used with various Fiber middlewares. | +| [template](https://github.com/gofiber/template) | This package contains 8 template engines that can be used with Fiber `v1.10.x` Go version 1.13 or higher is required. | +| [websocket](https://github.com/gofiber/websocket) | Based on Fasthttp WebSocket for Fiber with Locals support! | + +## 🕶️ Awesome List + +For more articles, middlewares, examples or tools check our [awesome list](https://github.com/gofiber/awesome-fiber). + +## 👍 هاوکار بە + +ئەگەر دەتەوێت دەستخۆشی لە فایبەر بکەیت: + +1. [ئەستێرەیەک](https://github.com/gofiber/fiber/stargazers)ـی بۆ دا بنێ. +2. لە تویتەر [تویتی لەسەر بکە](https://x.com/intent/tweet?text=Fiber%20is%20an%20Express%20inspired%20%23web%20%23framework%20built%20on%20top%20of%20Fasthttp%2C%20the%20fastest%20HTTP%20engine%20for%20%23Go.%20Designed%20to%20ease%20things%20up%20for%20%23fast%20development%20with%20zero%20memory%20allocation%20and%20%23performance%20in%20mind%20%F0%9F%9A%80%20https%3A%2F%2Fgithub.com%2Fgofiber%2Ffiber). +3. بە وێبڵۆگێک باسی بکە. +4. [پشتگیری بکە](https://buymeacoff.ee/fenny). + +## ☕ پشتگیرییەکان + +فایبەر لەسەر پشتگیریی ئێوە بەندە بۆ پارەدانەکانی، بۆ نموونە دۆمەینەکەی، GitBook و خزمەتگوزارییەکانی تر. تۆش دەتوانیت تۆش دەتوانیت سەردانی [ئێرە](https://buymeacoff.ee/fenny) بکەیت و پشتگیریی فایبەر بکەیت. + +| | User | Donation | +| :--------------------------------------------------------- | :----------------------------------------------- | :------- | +| ![](https://avatars.githubusercontent.com/u/204341?s=25) | [@destari](https://github.com/destari) | ☕ x 10 | +| ![](https://avatars.githubusercontent.com/u/63164982?s=25) | [@dembygenesis](https://github.com/dembygenesis) | ☕ x 5 | +| ![](https://avatars.githubusercontent.com/u/56607882?s=25) | [@thomasvvugt](https://github.com/thomasvvugt) | ☕ x 5 | +| ![](https://avatars.githubusercontent.com/u/27820675?s=25) | [@hendratommy](https://github.com/hendratommy) | ☕ x 5 | +| ![](https://avatars.githubusercontent.com/u/1094221?s=25) | [@ekaputra07](https://github.com/ekaputra07) | ☕ x 5 | +| ![](https://avatars.githubusercontent.com/u/194590?s=25) | [@jorgefuertes](https://github.com/jorgefuertes) | ☕ x 5 | +| ![](https://avatars.githubusercontent.com/u/186637?s=25) | [@candidosales](https://github.com/candidosales) | ☕ x 5 | +| ![](https://avatars.githubusercontent.com/u/29659953?s=25) | [@l0nax](https://github.com/l0nax) | ☕ x 3 | +| ![](https://avatars.githubusercontent.com/u/635852?s=25) | [@bihe](https://github.com/bihe) | ☕ x 3 | +| ![](https://avatars.githubusercontent.com/u/307334?s=25) | [@justdave](https://github.com/justdave) | ☕ x 3 | +| ![](https://avatars.githubusercontent.com/u/11155743?s=25) | [@koddr](https://github.com/koddr) | ☕ x 1 | +| ![](https://avatars.githubusercontent.com/u/29042462?s=25) | [@lapolinar](https://github.com/lapolinar) | ☕ x 1 | +| ![](https://avatars.githubusercontent.com/u/2978730?s=25) | [@diegowifi](https://github.com/diegowifi) | ☕ x 1 | +| ![](https://avatars.githubusercontent.com/u/44171355?s=25) | [@ssimk0](https://github.com/ssimk0) | ☕ x 1 | +| ![](https://avatars.githubusercontent.com/u/5638101?s=25) | [@raymayemir](https://github.com/raymayemir) | ☕ x 1 | +| ![](https://avatars.githubusercontent.com/u/619996?s=25) | [@melkorm](https://github.com/melkorm) | ☕ x 1 | +| ![](https://avatars.githubusercontent.com/u/31022056?s=25) | [@marvinjwendt](https://github.com/marvinjwendt) | ☕ x 1 | +| ![](https://avatars.githubusercontent.com/u/31921460?s=25) | [@toishy](https://github.com/toishy) | ☕ x 1 | + +## ‎‍💻 هاوکارەکان + +Code Contributors + +## ⭐️ ئەستێرەکان + +Stargazers over time + +## ⚠️ مۆڵەت + +مافی لەبەرگرتنەوە (c) 2019-ئێستا پارێزراوە بۆ [Fenny](https://github.com/fenny) و [هاوکاران](https://github.com/gofiber/fiber/graphs/contributors). فایبەر نەرمەوەڵایەکی خۆڕایی و سەرچاوەکراوەیە لەژێر مۆڵەتنامەی [MIT](https://github.com/gofiber/fiber/blob/master/LICENSE). لۆگۆی فایبەر لەلایەن [Vic Shóstak](https://github.com/koddr) دروست کراوە و مۆڵەتنامەی [Creative Commons](https://creativecommons.org/licenses/by-sa/4.0/) بەکار دەهێنێت (CC BY-SA 4.0 نێودەوڵەتی). + +**مۆڵەتەکانی دیکە** + +- [colorable](https://github.com/mattn/go-colorable/blob/master/LICENSE) +- [isatty](https://github.com/mattn/go-isatty/blob/master/LICENSE) +- [runewidth](https://github.com/mattn/go-runewidth/blob/master/LICENSE) +- [fasthttp](https://github.com/valyala/fasthttp/blob/master/LICENSE) +- [bytebufferpool](https://github.com/valyala/bytebufferpool/blob/master/LICENSE) +- [fwd](https://github.com/philhofer/fwd/blob/master/LICENSE.md) +- [go-ole](https://github.com/go-ole/go-ole/blob/master/LICENSE) +- [gopsutil](https://github.com/shirou/gopsutil/blob/master/LICENSE) +- [msgp](https://github.com/tinylib/msgp/blob/master/LICENSE) +- [schema](https://github.com/gorilla/schema/blob/master/LICENSE) +- [uuid](https://github.com/google/uuid/blob/master/LICENSE) +- [wmi](https://github.com/StackExchange/wmi/blob/master/LICENSE) diff --git a/.github/README_de.md b/.github/README_de.md new file mode 100644 index 0000000..8fd8935 --- /dev/null +++ b/.github/README_de.md @@ -0,0 +1,687 @@ +

+ + + + + Fiber + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + +

+

+Fiber ist ein von Expressjs inspiriertes Web-Framework, aufgebaut auf Fasthttp - die schnellste HTTP engine für Go. Kreiert, um Dinge zu vereinfachen, für schnelle Entwicklung mit keinen Speicherzuweisungen und Performance im Hinterkopf. +

+ +## ⚡️ Schnellstart + +```go +package main + +import "github.com/gofiber/fiber/v2" + +func main() { + app := fiber.New() + + app.Get("/", func(c *fiber.Ctx) error { + return c.SendString("Hallo, Welt 👋!") + }) + + app.Listen(":3000") +} +``` + +## 🤖 Benchmarks + +Diese Tests wurden von [TechEmpower](https://www.techempower.com/benchmarks/#section=data-r19&hw=ph&test=plaintext) und [Go Web](https://github.com/smallnest/go-web-framework-benchmark) ausgeführt. Falls du alle Resultate sehen möchtest, besuche bitte unser [Wiki](https://docs.gofiber.io/extra/benchmarks). + +

+ + +

+ +## ⚙️ Installation + +Stelle sicher, dass du Go installiert hast ([Download hier](https://go.dev/dl/)). Version `1.17` oder neuer wird zu der Nutzung Fibers benötigt. + +Erstelle ein neues Project, indem du zunächst einen neuen Ordner erstellst und dort in diesem Ordner `go mod init github.com/dein/repo` ausführst ([hier mehr dazu](https://go.dev/blog/using-go-modules)). Daraufhin kannst du Fiber mit dem [`go get`](https://pkg.go.dev/cmd/go/#hdr-Add_dependencies_to_current_module_and_install_them) Kommandozeilenbefehl installieren: + +```bash +go get -u github.com/gofiber/fiber/v2 +``` + +## 🎯 Eigenschaften + +- Robustes [Routing](https://docs.gofiber.io/guide/routing) +- Bereitstellen von [statischen Dateien](https://docs.gofiber.io/api/app#static) +- Extreme [Performance](https://docs.gofiber.io/extra/benchmarks) +- [Geringe Arbeitsspeichernutzung](https://docs.gofiber.io/extra/benchmarks) +- Express [API Endpunkte](https://docs.gofiber.io/api/ctx) +- [Middleware](https://docs.gofiber.io/category/-middleware) & [Next](https://docs.gofiber.io/api/ctx#next) Support +- [Schnelle](https://dev.to/koddr/welcome-to-fiber-an-express-js-styled-fastest-web-framework-written-with-on-golang-497) serverseitige Programmierung +- [Template engines](https://github.com/gofiber/template) +- [WebSocket support](https://github.com/gofiber/websocket) +- [Server-Sent events](https://github.com/gofiber/recipes/tree/master/sse) +- [Rate Limiter](https://docs.gofiber.io/api/middleware/limiter) +- Verfügbar in [19 Sprachen](https://docs.gofiber.io/) +- Und vieles mehr - [erkunde Fiber](https://docs.gofiber.io/) + +## 💡 Philosophie + +Neue Gopher, welche von [Node.js](https://nodejs.org/en/about/) zu [Go](https://go.dev/doc/) umsteigen, müssen eine Lernkurve durchlaufen, bevor sie ihre Webanwendungen oder Microservices erstellen können. Fiber als **Web-Framework**, wurde mit der Idee von **Minimalismus** erschaffen und folgt dem **UNIX Weg**, damit neue Gophers mit einem herzlichen und vertrauenswürdigen Willkommen schnell in der Welt von Go durchstarten können. + +Fiber ist **inspiriert** von Express.js, dem beliebtesten Web-Framework im Internet. Wir haben die **Leichtigkeit** von Express und die **Rohleistung** von Go kombiniert. Wenn du jemals eine Webanwendung mit Node.js implementiert hast (_mit Express.js oder ähnlichem_), werden dir viele Methoden und Prinzipien **sehr vertraut** vorkommen. + +## ⚠️ Limitations + +- Due to Fiber's usage of unsafe, the library may not always be compatible with the latest Go version. Fiber 2.40.0 has been tested with Go versions 1.17 to 1.21. +- Fiber is not compatible with net/http interfaces. This means you will not be able to use projects like gqlgen, go-swagger, or any others which are part of the net/http ecosystem. + +## 👀 Beispiele + +Nachfolgend sind einige der gängigsten Beispiele aufgeführt. Wenn du weitere Codebeispiele sehen möchtest, besuche bitte unser ["Recipes Repository"](https://github.com/gofiber/recipes) oder besuche unsere [API Dokumentation](https://docs.gofiber.io). + +#### 📖 [**Basic Routing**](https://docs.gofiber.io/#basic-routing) + +```go +func main() { + app := fiber.New() + + // GET /api/register + app.Get("/api/*", func(c *fiber.Ctx) error { + msg := fmt.Sprintf("✋ %s", c.Params("*")) + return c.SendString(msg) // => ✋ register + }) + + // GET /flights/LAX-SFO + app.Get("/flights/:from-:to", func(c *fiber.Ctx) error { + msg := fmt.Sprintf("💸 From: %s, To: %s", c.Params("from"), c.Params("to")) + return c.SendString(msg) // => 💸 From: LAX, To: SFO + }) + + // GET /dictionary.txt + app.Get("/:file.:ext", func(c *fiber.Ctx) error { + msg := fmt.Sprintf("📃 %s.%s", c.Params("file"), c.Params("ext")) + return c.SendString(msg) // => 📃 dictionary.txt + }) + + // GET /john/75 + app.Get("/:name/:age/:gender?", func(c *fiber.Ctx) error { + msg := fmt.Sprintf("👴 %s is %s years old", c.Params("name"), c.Params("age")) + return c.SendString(msg) // => 👴 john is 75 years old + }) + + // GET /john + app.Get("/:name", func(c *fiber.Ctx) error { + msg := fmt.Sprintf("Hello, %s 👋!", c.Params("name")) + return c.SendString(msg) // => Hello john 👋! + }) + + log.Fatal(app.Listen(":3000")) +} + +``` + +#### 📖 [**Route Naming**](https://docs.gofiber.io/api/app#name) + +```go +func main() { + app := fiber.New() + + // GET /api/register + app.Get("/api/*", func(c *fiber.Ctx) error { + msg := fmt.Sprintf("✋ %s", c.Params("*")) + return c.SendString(msg) // => ✋ register + }).Name("api") + + data, _ := json.MarshalIndent(app.GetRoute("api"), "", " ") + fmt.Print(string(data)) + // Prints: + // { + // "method": "GET", + // "name": "api", + // "path": "/api/*", + // "params": [ + // "*1" + // ] + // } + + + log.Fatal(app.Listen(":3000")) +} + +``` + +#### 📖 [**Serving Static Files**](https://docs.gofiber.io/api/app#static) + +```go +func main() { + app := fiber.New() + + app.Static("/", "./public") + // => http://localhost:3000/js/script.js + // => http://localhost:3000/css/style.css + + app.Static("/prefix", "./public") + // => http://localhost:3000/prefix/js/script.js + // => http://localhost:3000/prefix/css/style.css + + app.Static("*", "./public/index.html") + // => http://localhost:3000/any/path/shows/index/html + + log.Fatal(app.Listen(":3000")) +} + +``` + +#### 📖 [**Middleware & Next**](https://docs.gofiber.io/api/ctx#next) + +```go +func main() { + app := fiber.New() + + // Match any route + app.Use(func(c *fiber.Ctx) error { + fmt.Println("🥇 First handler") + return c.Next() + }) + + // Match all routes starting with /api + app.Use("/api", func(c *fiber.Ctx) error { + fmt.Println("🥈 Second handler") + return c.Next() + }) + + // GET /api/register + app.Get("/api/list", func(c *fiber.Ctx) error { + fmt.Println("🥉 Last handler") + return c.SendString("Hello, World 👋!") + }) + + log.Fatal(app.Listen(":3000")) +} + +``` + +
+ 📚 Show more code examples + +### Views engines + +📖 [Config](https://docs.gofiber.io/api/fiber#config) +📖 [Engines](https://github.com/gofiber/template) +📖 [Render](https://docs.gofiber.io/api/ctx#render) + +Fiber defaults to the [html/template](https://pkg.go.dev/html/template/) when no view engine is set. + +If you want to execute partials or use a different engine like [amber](https://github.com/eknkc/amber), [handlebars](https://github.com/aymerick/raymond), [mustache](https://github.com/cbroglie/mustache) or [pug](https://github.com/Joker/jade) etc.. + +Checkout our [Template](https://github.com/gofiber/template) package that support multiple view engines. + +```go +package main + +import ( + "github.com/gofiber/fiber/v2" + "github.com/gofiber/template/pug" +) + +func main() { + // You can setup Views engine before initiation app: + app := fiber.New(fiber.Config{ + Views: pug.New("./views", ".pug"), + }) + + // And now, you can call template `./views/home.pug` like this: + app.Get("/", func(c *fiber.Ctx) error { + return c.Render("home", fiber.Map{ + "title": "Homepage", + "year": 1999, + }) + }) + + log.Fatal(app.Listen(":3000")) +} +``` + +### Grouping routes into chains + +📖 [Group](https://docs.gofiber.io/api/app#group) + +```go +func middleware(c *fiber.Ctx) error { + fmt.Println("Don't mind me!") + return c.Next() +} + +func handler(c *fiber.Ctx) error { + return c.SendString(c.Path()) +} + +func main() { + app := fiber.New() + + // Root API route + api := app.Group("/api", middleware) // /api + + // API v1 routes + v1 := api.Group("/v1", middleware) // /api/v1 + v1.Get("/list", handler) // /api/v1/list + v1.Get("/user", handler) // /api/v1/user + + // API v2 routes + v2 := api.Group("/v2", middleware) // /api/v2 + v2.Get("/list", handler) // /api/v2/list + v2.Get("/user", handler) // /api/v2/user + + // ... +} + +``` + +### Middleware logger + +📖 [Logger](https://docs.gofiber.io/api/middleware/logger) + +```go +package main + +import ( + "log" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/logger" +) + +func main() { + app := fiber.New() + + app.Use(logger.New()) + + // ... + + log.Fatal(app.Listen(":3000")) +} +``` + +### Cross-Origin Resource Sharing (CORS) + +📖 [CORS](https://docs.gofiber.io/api/middleware/cors) + +```go +import ( + "log" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/cors" +) + +func main() { + app := fiber.New() + + app.Use(cors.New()) + + // ... + + log.Fatal(app.Listen(":3000")) +} +``` + +Check CORS by passing any domain in `Origin` header: + +```bash +curl -H "Origin: http://example.com" --verbose http://localhost:3000 +``` + +### Custom 404 response + +📖 [HTTP Methods](https://docs.gofiber.io/api/ctx#status) + +```go +func main() { + app := fiber.New() + + app.Static("/", "./public") + + app.Get("/demo", func(c *fiber.Ctx) error { + return c.SendString("This is a demo!") + }) + + app.Post("/register", func(c *fiber.Ctx) error { + return c.SendString("Welcome!") + }) + + // Last middleware to match anything + app.Use(func(c *fiber.Ctx) error { + return c.SendStatus(404) + // => 404 "Not Found" + }) + + log.Fatal(app.Listen(":3000")) +} +``` + +### JSON Response + +📖 [JSON](https://docs.gofiber.io/api/ctx#json) + +```go +type User struct { + Name string `json:"name"` + Age int `json:"age"` +} + +func main() { + app := fiber.New() + + app.Get("/user", func(c *fiber.Ctx) error { + return c.JSON(&User{"John", 20}) + // => {"name":"John", "age":20} + }) + + app.Get("/json", func(c *fiber.Ctx) error { + return c.JSON(fiber.Map{ + "success": true, + "message": "Hi John!", + }) + // => {"success":true, "message":"Hi John!"} + }) + + log.Fatal(app.Listen(":3000")) +} +``` + +### WebSocket Upgrade + +📖 [Websocket](https://github.com/gofiber/websocket) + +```go +import ( + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/websocket" +) + +func main() { + app := fiber.New() + + app.Get("/ws", websocket.New(func(c *websocket.Conn) { + for { + mt, msg, err := c.ReadMessage() + if err != nil { + log.Println("read:", err) + break + } + log.Printf("recv: %s", msg) + err = c.WriteMessage(mt, msg) + if err != nil { + log.Println("write:", err) + break + } + } + })) + + log.Fatal(app.Listen(":3000")) + // ws://localhost:3000/ws +} +``` + +### Server-Sent Events + +📖 [More Info](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events) + +```go +import ( + "github.com/gofiber/fiber/v2" + "github.com/valyala/fasthttp" +) + +func main() { + app := fiber.New() + + app.Get("/sse", func(c *fiber.Ctx) error { + c.Set("Content-Type", "text/event-stream") + c.Set("Cache-Control", "no-cache") + c.Set("Connection", "keep-alive") + c.Set("Transfer-Encoding", "chunked") + + c.Context().SetBodyStreamWriter(fasthttp.StreamWriter(func(w *bufio.Writer) { + fmt.Println("WRITER") + var i int + + for { + i++ + msg := fmt.Sprintf("%d - the time is %v", i, time.Now()) + fmt.Fprintf(w, "data: Message: %s\n\n", msg) + fmt.Println(msg) + + w.Flush() + time.Sleep(5 * time.Second) + } + })) + + return nil + }) + + log.Fatal(app.Listen(":3000")) +} +``` + +### Recover middleware + +📖 [Recover](https://docs.gofiber.io/api/middleware/recover) + +```go +import ( + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/recover" +) + +func main() { + app := fiber.New() + + app.Use(recover.New()) + + app.Get("/", func(c *fiber.Ctx) error { + panic("normally this would crash your app") + }) + + log.Fatal(app.Listen(":3000")) +} +``` + +
+ +## 🧬 Internal Middleware + +Hier finden Sie eine Liste der Middleware, die im Fiber-Framework enthalten ist. + +| Middleware | Description | +| :------------------------------------------------------------------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [basicauth](https://github.com/gofiber/fiber/tree/master/middleware/basicauth) | Basic auth middleware provides an HTTP basic authentication. It calls the next handler for valid credentials and 401 Unauthorized for missing or invalid credentials. | +| [cache](https://github.com/gofiber/fiber/tree/master/middleware/cache) | Intercept and cache responses | +| [compress](https://github.com/gofiber/fiber/tree/master/middleware/compress) | Compression middleware for Fiber, it supports `deflate`, `gzip` and `brotli` by default. | +| [cors](https://github.com/gofiber/fiber/tree/master/middleware/cors) | Enable cross-origin resource sharing \(CORS\) with various options. | +| [csrf](https://github.com/gofiber/fiber/tree/master/middleware/csrf) | Protect from CSRF exploits. | +| [encryptcookie](https://github.com/gofiber/fiber/tree/master/middleware/encryptcookie) | Encrypt middleware which encrypts cookie values. | +| [envvar](https://github.com/gofiber/fiber/tree/master/middleware/envvar) | Expose environment variables with providing an optional config. | +| [etag](https://github.com/gofiber/fiber/tree/master/middleware/etag) | ETag middleware that lets caches be more efficient and save bandwidth, as a web server does not need to resend a full response if the content has not changed. | +| [expvar](https://github.com/gofiber/fiber/tree/master/middleware/expvar) | Expvar middleware that serves via its HTTP server runtime exposed variants in the JSON format. | +| [favicon](https://github.com/gofiber/fiber/tree/master/middleware/favicon) | Ignore favicon from logs or serve from memory if a file path is provided. | +| [filesystem](https://github.com/gofiber/fiber/tree/master/middleware/filesystem) | FileSystem middleware for Fiber, special thanks and credits to Alireza Salary | +| [limiter](https://github.com/gofiber/fiber/tree/master/middleware/limiter) | Rate-limiting middleware for Fiber. Use to limit repeated requests to public APIs and/or endpoints such as password reset. | +| [logger](https://github.com/gofiber/fiber/tree/master/middleware/logger) | HTTP request/response logger. | +| [monitor](https://github.com/gofiber/fiber/tree/master/middleware/monitor) | Monitor middleware that reports server metrics, inspired by express-status-monitor | +| [pprof](https://github.com/gofiber/fiber/tree/master/middleware/pprof) | Special thanks to Matthew Lee \(@mthli\) | +| [proxy](https://github.com/gofiber/fiber/tree/master/middleware/proxy) | Allows you to proxy requests to a multiple servers | +| [recover](https://github.com/gofiber/fiber/tree/master/middleware/recover) | Recover middleware recovers from panics anywhere in the stack chain and handles the control to the centralized[ ErrorHandler](https://docs.gofiber.io/guide/error-handling). | +| [requestid](https://github.com/gofiber/fiber/tree/master/middleware/requestid) | Adds a requestid to every request. | +| [session](https://github.com/gofiber/fiber/tree/master/middleware/session) | Session middleware. NOTE: This middleware uses our Storage package. | +| [skip](https://github.com/gofiber/fiber/tree/master/middleware/skip) | Skip middleware that skips a wrapped handler is a predicate is true. | +| [timeout](https://github.com/gofiber/fiber/tree/master/middleware/timeout) | Adds a max time for a request and forwards to ErrorHandler if it is exceeded. | +| [keyauth](https://github.com/gofiber/keyauth) | Key auth middleware provides a key based authentication. | +| [redirect](https://github.com/gofiber/redirect) | Redirect middleware | +| [rewrite](https://github.com/gofiber/rewrite) | Rewrite middleware rewrites the URL path based on provided rules. It can be helpful for backward compatibility or just creating cleaner and more descriptive links. | +| [adaptor](https://github.com/gofiber/adaptor) | Converter for net/http handlers to/from Fiber request handlers, special thanks to @arsmn! | +| [helmet](https://github.com/gofiber/helmet) | Helps secure your apps by setting various HTTP headers. | + +## 🧬 External Middleware + +Liste der extern gehosteten Middleware-Module, die vom [Fiber team](https://github.com/orgs/gofiber/people) gepflegt werden. + +| Middleware | Description | +| :------------------------------------------------ | :-------------------------------------------------------------------------------------------------------------------- | --- | --------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- | +| [jwt](https://github.com/gofiber/jwt) | JWT returns a JSON Web Token \(JWT\) auth middleware. | +| [storage](https://github.com/gofiber/storage) | Premade storage drivers that implement the Storage interface, designed to be used with various Fiber middlewares. | | [storage](https://github.com/gofiber/storage) | Premade storage drivers that implement the Storage interface, designed to be used with various Fiber middlewares. | +| [template](https://github.com/gofiber/template) | This package contains 8 template engines that can be used with Fiber `v1.10.x` Go version 1.13 or higher is required. | +| [websocket](https://github.com/gofiber/websocket) | Based on Fasthttp WebSocket for Fiber with Locals support! | + +## 🕶️ Awesome List + +Weitere Artikel, Middlewares, Beispiele oder Tools finden Sie in unserer [awesome list](https://github.com/gofiber/awesome-fiber). + +## 👍 Mitwirken + +Falls du **danke** sagen möchtest und/oder aktiv die Entwicklung von `fiber` fördern möchtest: + +1. Füge dem Projekt einen [GitHub Stern](https://github.com/gofiber/fiber/stargazers) hinzu. +2. Twittere über das Projekt [auf deinem 𝕏 (Twitter)](https://x.com/intent/tweet?text=Fiber%20is%20an%20Express%20inspired%20%23web%20%23framework%20built%20on%20top%20of%20Fasthttp%2C%20the%20fastest%20HTTP%20engine%20for%20%23Go.%20Designed%20to%20ease%20things%20up%20for%20%23fast%20development%20with%20zero%20memory%20allocation%20and%20%23performance%20in%20mind%20%F0%9F%9A%80%20https%3A%2F%2Fgithub.com%2Fgofiber%2Ffiber). +3. Schreibe eine Rezension auf [Medium](https://medium.com/), [Dev.to](https://dev.to/) oder einem persönlichem Blog. +4. Unterstütze das Projekt, indem du ☕ [uns einen Kaffee kaufst](https://buymeacoff.ee/fenny). + +## ☕ Supporters + +Fiber ist ein Open-Source-Projekt, dass durch Spenden finanziert wird und welches auch seine Ausgaben (Domain, Hosting) durch Spenden deckt. Wenn du Fiber unterstützen möchtest, kannst du uns [**hier einen Kaffee kaufen**](https://buymeacoff.ee/fenny) ☕. + +| | User | Donation | +| :--------------------------------------------------------- | :----------------------------------------------- | :------- | +| ![](https://avatars.githubusercontent.com/u/204341?s=25) | [@destari](https://github.com/destari) | ☕ x 10 | +| ![](https://avatars.githubusercontent.com/u/63164982?s=25) | [@dembygenesis](https://github.com/dembygenesis) | ☕ x 5 | +| ![](https://avatars.githubusercontent.com/u/56607882?s=25) | [@thomasvvugt](https://github.com/thomasvvugt) | ☕ x 5 | +| ![](https://avatars.githubusercontent.com/u/27820675?s=25) | [@hendratommy](https://github.com/hendratommy) | ☕ x 5 | +| ![](https://avatars.githubusercontent.com/u/1094221?s=25) | [@ekaputra07](https://github.com/ekaputra07) | ☕ x 5 | +| ![](https://avatars.githubusercontent.com/u/194590?s=25) | [@jorgefuertes](https://github.com/jorgefuertes) | ☕ x 5 | +| ![](https://avatars.githubusercontent.com/u/186637?s=25) | [@candidosales](https://github.com/candidosales) | ☕ x 5 | +| ![](https://avatars.githubusercontent.com/u/29659953?s=25) | [@l0nax](https://github.com/l0nax) | ☕ x 3 | +| ![](https://avatars.githubusercontent.com/u/635852?s=25) | [@bihe](https://github.com/bihe) | ☕ x 3 | +| ![](https://avatars.githubusercontent.com/u/307334?s=25) | [@justdave](https://github.com/justdave) | ☕ x 3 | +| ![](https://avatars.githubusercontent.com/u/11155743?s=25) | [@koddr](https://github.com/koddr) | ☕ x 1 | +| ![](https://avatars.githubusercontent.com/u/29042462?s=25) | [@lapolinar](https://github.com/lapolinar) | ☕ x 1 | +| ![](https://avatars.githubusercontent.com/u/2978730?s=25) | [@diegowifi](https://github.com/diegowifi) | ☕ x 1 | +| ![](https://avatars.githubusercontent.com/u/44171355?s=25) | [@ssimk0](https://github.com/ssimk0) | ☕ x 1 | +| ![](https://avatars.githubusercontent.com/u/5638101?s=25) | [@raymayemir](https://github.com/raymayemir) | ☕ x 1 | +| ![](https://avatars.githubusercontent.com/u/619996?s=25) | [@melkorm](https://github.com/melkorm) | ☕ x 1 | +| ![](https://avatars.githubusercontent.com/u/31022056?s=25) | [@marvinjwendt](https://github.com/thomasvvugt) | ☕ x 1 | +| ![](https://avatars.githubusercontent.com/u/31921460?s=25) | [@toishy](https://github.com/toishy) | ☕ x 1 | + +## ‎‍💻 Code Contributors + +Code Contributors + +## ⭐️ Stargazers + +Stargazers over time + +## ⚠️ License + +Copyright (c) 2019-present [Fenny](https://github.com/fenny) and [Contributors](https://github.com/gofiber/fiber/graphs/contributors). `Fiber` is free and open-source software licensed under the [MIT License](https://github.com/gofiber/fiber/blob/master/LICENSE). Official logo was created by [Vic Shóstak](https://github.com/koddr) and distributed under [Creative Commons](https://creativecommons.org/licenses/by-sa/4.0/) license (CC BY-SA 4.0 International). + +**Third-party MIT licenses** + +- [colorable](https://github.com/mattn/go-colorable/blob/master/LICENSE) +- [isatty](https://github.com/mattn/go-isatty/blob/master/LICENSE) +- [runewidth](https://github.com/mattn/go-runewidth/blob/master/LICENSE) +- [fasthttp](https://github.com/valyala/fasthttp/blob/master/LICENSE) +- [bytebufferpool](https://github.com/valyala/bytebufferpool/blob/master/LICENSE) +- [fwd](https://github.com/philhofer/fwd/blob/master/LICENSE.md) +- [go-ole](https://github.com/go-ole/go-ole/blob/master/LICENSE) +- [gopsutil](https://github.com/shirou/gopsutil/blob/master/LICENSE) +- [msgp](https://github.com/tinylib/msgp/blob/master/LICENSE) +- [schema](https://github.com/gorilla/schema/blob/master/LICENSE) +- [uuid](https://github.com/google/uuid/blob/master/LICENSE) +- [wmi](https://github.com/StackExchange/wmi/blob/master/LICENSE) diff --git a/.github/README_eg.md b/.github/README_eg.md new file mode 100644 index 0000000..3eea117 --- /dev/null +++ b/.github/README_eg.md @@ -0,0 +1,717 @@ +

+ + + + Fiber + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + +

+

+ فايبر هي ويب فريمورك مستوحاه من اكسبريس ومبنيه على فاست اتش تي تي بي وهي اسرع محركات الويب للغه جو. مصممة عشان تسهل و تسرع التطوير ومابتعملش memory allocation زيادة. وبتهتم بالبيرفورمانس. +

+ +## ⚡️ بداية سريعة + +```go +package main + +import "github.com/gofiber/fiber/v2" + +func main() { + app := fiber.New() + + app.Get("/", func(c *fiber.Ctx) error { + return c.SendString("Hello, World 👋!") + }) + + app.Listen(":3000") +} +``` + +## 🤖 القياسات + +القياسات دي اتعملت عن طريق [TechEmpower](https://www.techempower.com/benchmarks/#section=data-r19&hw=ph&test=plaintext) و [Go Web](https://github.com/smallnest/go-web-framework-benchmark). لو عاوز تشوف كل النتايج زور [الويكي بتاعتنا](https://docs.gofiber.io/extra/benchmarks). + +

+ + +

+ +## ⚙️ التسطيب + +أتأكد انك مسطب جو ([تحميل](https://go.dev/dl/)). الاصدار `1.17` او اعلى. + +ابدأ البروجكت بتاعك بعمل فولدر وبعدين رن الكوماند ده `go mod init github.com/your/repo` ([اعرف اكتر](https://go.dev/blog/using-go-modules)) بعدين سطب فايبر بكوماند [`go get`](https://pkg.go.dev/cmd/go/#hdr-Add_dependencies_to_current_module_and_install_them): + +```bash +go get -u github.com/gofiber/fiber/v2 +``` + +## 🎯 المميزات + +- [راوتنج](https://docs.gofiber.io/guide/routing) متين +- سيرف [فايلات ستاتك](https://docs.gofiber.io/api/app#static) +- [بيرفورمانس](https://docs.gofiber.io/extra/benchmarks) فشيخ +- [استهلاك قليل للميموري](https://docs.gofiber.io/extra/benchmarks) +- [APIs](https://docs.gofiber.io/api/ctx) +- [ميدلويرز](https://docs.gofiber.io/category/-middleware) و بتدعم [Next](https://docs.gofiber.io/api/ctx#next) +- برمجة سيرفر [سريعة](https://dev.to/koddr/welcome-to-fiber-an-express-js-styled-fastest-web-framework-written-with-on-golang-497) +- [تيمبلت اينجنز](https://github.com/gofiber/template) +- [بتدعم الويب سوكتس](https://github.com/gofiber/websocket) +- [Server-Sent events](https://github.com/gofiber/recipes/tree/master/sse) +- [ريت ليمت](https://docs.gofiber.io/api/middleware/limiter) +- مترجمة [لـ19 لغة](https://docs.gofiber.io/) +- وحاجات اكتر, [تصفح فايبر](https://docs.gofiber.io/) + +## 💡 الفكرة + +الجوفرز الجداد اللي بيسوتشوا من [نود جي اس](https://nodejs.org/en/about/) لـ[جو](https://go.dev/doc/) بيتعاملوا مع مرحلة تعلم قبل ما يبدأوا يبنوا تطبيقاتهم و مايكروسيرفساتهم. فايبر, كـ**ويب فريمورك**, اتعملت بفكرة **البساطة** و بتتبع **طريقة يونكس**, عشان الجوفرز الجداد يقدروا يدخلوا عالم جو بسرعة و بثقة. + +فايبر **مستوحاة** من اكسبريس اللي هي اشهر ويب فريمورك عالانترنت. احنا جمعنا بين **سهولة** اكسبريس و **سرعة** جو. لو انت عملت تطبيق ويب في نود جي اس (_باستخدام اكسبريس او حاجة شبهها_), هتلاقي ان معظم الطرق و المبادئ بتاعت فايبر **مألوفة** جدا. + +احنا **بنسمع** لمستخدمينا في [الايشوز](https://github.com/gofiber/fiber/issues) و [قناة الديسكورد](https://gofiber.io/discord) و _في كل حتة عالنت_ عشان نعمل فريمورك ويب جو **سريع**, **مرن** و **سهل** **لاي تاسك**, **ديدلاين** واي **مستوى** مبرمج! زي اكسبريس في عالم الجافاسكريبت. + +## ⚠️ القيود + +- بسبب استخدام فايبر لـunsafe ممكن انها متتوافقش مع اخر اصدار من جو. فايبر 2.40.0 اتتست بـجو من اصدار 1.17 لـ1.21 +- فايبر مش متوافقة مع واجهات net/http. ده يعني انك مش هتقدر تستخدم مشاريع زي gqlgen, go-swagger, او اي حاجة تانية متعلقة بـnet/http + +## 👀 أمثلة + +دي بعض الامثلة الشائعة. لو عايز تشوف امثلة اكتر, زور [Recipes repository](https://github.com/gofiber/recipes) او زور [API documentation](https://docs.gofiber.io). + +#### 📖 [**الراوتنج البسيط**](https://docs.gofiber.io/#basic-routing) + +```go +func main() { + app := fiber.New() + + // GET /api/register + app.Get("/api/*", func(c *fiber.Ctx) error { + msg := fmt.Sprintf("✋ %s", c.Params("*")) + return c.SendString(msg) // => ✋ register + }) + + // GET /flights/LAX-SFO + app.Get("/flights/:from-:to", func(c *fiber.Ctx) error { + msg := fmt.Sprintf("💸 From: %s, To: %s", c.Params("from"), c.Params("to")) + return c.SendString(msg) // => 💸 From: LAX, To: SFO + }) + + // GET /dictionary.txt + app.Get("/:file.:ext", func(c *fiber.Ctx) error { + msg := fmt.Sprintf("📃 %s.%s", c.Params("file"), c.Params("ext")) + return c.SendString(msg) // => 📃 dictionary.txt + }) + + // GET /john/75 + app.Get("/:name/:age/:gender?", func(c *fiber.Ctx) error { + msg := fmt.Sprintf("👴 %s is %s years old", c.Params("name"), c.Params("age")) + return c.SendString(msg) // => 👴 john is 75 years old + }) + + // GET /john + app.Get("/:name", func(c *fiber.Ctx) error { + msg := fmt.Sprintf("Hello, %s 👋!", c.Params("name")) + return c.SendString(msg) // => Hello john 👋! + }) + + log.Fatal(app.Listen(":3000")) +} + +``` + +#### 📖 [**تسمية الراوتس**](https://docs.gofiber.io/api/app#name) + +```go +func main() { + app := fiber.New() + + // GET /api/register + app.Get("/api/*", func(c *fiber.Ctx) error { + msg := fmt.Sprintf("✋ %s", c.Params("*")) + return c.SendString(msg) // => ✋ register + }).Name("api") + + data, _ := json.MarshalIndent(app.GetRoute("api"), "", " ") + fmt.Print(string(data)) + // Prints: + // { + // "method": "GET", + // "name": "api", + // "path": "/api/*", + // "params": [ + // "*1" + // ] + // } + + + log.Fatal(app.Listen(":3000")) +} + +``` + +#### 📖 [**ازاي تسيرف فايلات ستاتك**](https://docs.gofiber.io/api/app#static) + +```go +func main() { + app := fiber.New() + + app.Static("/", "./public") + // => http://localhost:3000/js/script.js + // => http://localhost:3000/css/style.css + + app.Static("/prefix", "./public") + // => http://localhost:3000/prefix/js/script.js + // => http://localhost:3000/prefix/css/style.css + + app.Static("*", "./public/index.html") + // => http://localhost:3000/any/path/shows/index/html + + log.Fatal(app.Listen(":3000")) +} + +``` + +#### 📖 [**الميدلويرز ونيكست**](https://docs.gofiber.io/api/ctx#next) + +```go +func main() { + app := fiber.New() + + // Match any route + app.Use(func(c *fiber.Ctx) error { + fmt.Println("🥇 First handler") + return c.Next() + }) + + // Match all routes starting with /api + app.Use("/api", func(c *fiber.Ctx) error { + fmt.Println("🥈 Second handler") + return c.Next() + }) + + // GET /api/list + app.Get("/api/list", func(c *fiber.Ctx) error { + fmt.Println("🥉 Last handler") + return c.SendString("Hello, World 👋!") + }) + + log.Fatal(app.Listen(":3000")) +} + +``` + +
+ 📚 اعرض امثلة اكتر + +### محركات الفيوز + +📖 [Config](https://docs.gofiber.io/api/fiber#config) +📖 [Engines](https://github.com/gofiber/template) +📖 [Render](https://docs.gofiber.io/api/ctx#render) + +فايبر بتستخدم [html/template](https://pkg.go.dev/html/template/) لما مايكونش في محرك فيوز متعرف + +لو عاوز تستخدم فيوز جزئية او محرك فيوز تاني زي [amber](https://github.com/eknkc/amber), [handlebars](https://github.com/aymerick/raymond), [mustache](https://github.com/cbroglie/mustache) او [pug](https://github.com/Joker/jade) وغيره.. + +بص على [الباكدج](https://github.com/gofiber/template) بتاعنا اللي بيدعم محركات فيوز متعددة + +```go +package main + +import ( + "github.com/gofiber/fiber/v2" + "github.com/gofiber/template/pug" +) + +func main() { + // You can setup Views engine before initiation app: + app := fiber.New(fiber.Config{ + Views: pug.New("./views", ".pug"), + }) + + // And now, you can call template `./views/home.pug` like this: + app.Get("/", func(c *fiber.Ctx) error { + return c.Render("home", fiber.Map{ + "title": "Homepage", + "year": 1999, + }) + }) + + log.Fatal(app.Listen(":3000")) +} +``` + +### تجميع الراوتس في سلسلة + +📖 [Group](https://docs.gofiber.io/api/app#group) + +```go +func middleware(c *fiber.Ctx) error { + fmt.Println("Don't mind me!") + return c.Next() +} + +func handler(c *fiber.Ctx) error { + return c.SendString(c.Path()) +} + +func main() { + app := fiber.New() + + // Root API route + api := app.Group("/api", middleware) // /api + + // API v1 routes + v1 := api.Group("/v1", middleware) // /api/v1 + v1.Get("/list", handler) // /api/v1/list + v1.Get("/user", handler) // /api/v1/user + + // API v2 routes + v2 := api.Group("/v2", middleware) // /api/v2 + v2.Get("/list", handler) // /api/v2/list + v2.Get("/user", handler) // /api/v2/user + + // ... +} + +``` + +### ميدل وير لوجر + +📖 [Logger](https://docs.gofiber.io/api/middleware/logger) + +```go +package main + +import ( + "log" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/logger" +) + +func main() { + app := fiber.New() + + app.Use(logger.New()) + + // ... + + log.Fatal(app.Listen(":3000")) +} +``` + +### هيدر الكروس اوريجن (CORS) + +📖 [CORS](https://docs.gofiber.io/api/middleware/cors) + +```go +import ( + "log" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/cors" +) + +func main() { + app := fiber.New() + + app.Use(cors.New()) + + // ... + + log.Fatal(app.Listen(":3000")) +} +``` + +جرب الCORS بانك تبعت اي دومين في هيدر `Origin` وتشوف الرد بتاع السيرفر + +```bash +curl -H "Origin: http://example.com" --verbose http://localhost:3000 +``` + +### ريسبومس 404 معدل + +📖 [HTTP Methods](https://docs.gofiber.io/api/ctx#status) + +```go +func main() { + app := fiber.New() + + app.Static("/", "./public") + + app.Get("/demo", func(c *fiber.Ctx) error { + return c.SendString("This is a demo!") + }) + + app.Post("/register", func(c *fiber.Ctx) error { + return c.SendString("Welcome!") + }) + + // Last middleware to match anything + app.Use(func(c *fiber.Ctx) error { + return c.SendStatus(404) + // => 404 "Not Found" + }) + + log.Fatal(app.Listen(":3000")) +} +``` + +### JSON ريبسونس + +📖 [JSON](https://docs.gofiber.io/api/ctx#json) + +```go +type User struct { + Name string `json:"name"` + Age int `json:"age"` +} + +func main() { + app := fiber.New() + + app.Get("/user", func(c *fiber.Ctx) error { + return c.JSON(&User{"John", 20}) + // => {"name":"John", "age":20} + }) + + app.Get("/json", func(c *fiber.Ctx) error { + return c.JSON(fiber.Map{ + "success": true, + "message": "Hi John!", + }) + // => {"success":true, "message":"Hi John!"} + }) + + log.Fatal(app.Listen(":3000")) +} +``` + +### اضافة ويبسوكيت + +📖 [Websocket](https://github.com/gofiber/websocket) + +```go +import ( + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/websocket" +) + +func main() { + app := fiber.New() + + app.Get("/ws", websocket.New(func(c *websocket.Conn) { + for { + mt, msg, err := c.ReadMessage() + if err != nil { + log.Println("read:", err) + break + } + log.Printf("recv: %s", msg) + err = c.WriteMessage(mt, msg) + if err != nil { + log.Println("write:", err) + break + } + } + })) + + log.Fatal(app.Listen(":3000")) + // ws://localhost:3000/ws +} +``` + +### Server-Sent Events + +📖 [More Info](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events) + +```go +import ( + "github.com/gofiber/fiber/v2" + "github.com/valyala/fasthttp" +) + +func main() { + app := fiber.New() + + app.Get("/sse", func(c *fiber.Ctx) error { + c.Set("Content-Type", "text/event-stream") + c.Set("Cache-Control", "no-cache") + c.Set("Connection", "keep-alive") + c.Set("Transfer-Encoding", "chunked") + + c.Context().SetBodyStreamWriter(fasthttp.StreamWriter(func(w *bufio.Writer) { + fmt.Println("WRITER") + var i int + + for { + i++ + msg := fmt.Sprintf("%d - the time is %v", i, time.Now()) + fmt.Fprintf(w, "data: Message: %s\n\n", msg) + fmt.Println(msg) + + w.Flush() + time.Sleep(5 * time.Second) + } + })) + + return nil + }) + + log.Fatal(app.Listen(":3000")) +} +``` + +### ميدلوير ريكوفر + +📖 [Recover](https://docs.gofiber.io/api/middleware/recover) + +```go +import ( + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/recover" +) + +func main() { + app := fiber.New() + + app.Use(recover.New()) + + app.Get("/", func(c *fiber.Ctx) error { + panic("normally this would crash your app") + }) + + log.Fatal(app.Listen(":3000")) +} +``` + +
+ +### استخدام بروكسي موثوق + +📖 [Config](https://docs.gofiber.io/api/fiber#config) + +```go +import ( + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/recover" +) + +func main() { + app := fiber.New(fiber.Config{ + EnableTrustedProxyCheck: true, + TrustedProxies: []string{"0.0.0.0", "1.1.1.1/30"}, // IP address or IP address range + ProxyHeader: fiber.HeaderXForwardedFor, + }) + + // ... + + log.Fatal(app.Listen(":3000")) +} +``` + + + +## 🧬 ميدلوير داخلي + +Here is a list of middleware that are included within the Fiber framework. +دي ليستة بالميدلوير الموجودة في فايبر + +| Middleware | Description | +| :------------------------------------------------------------------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [basicauth](https://github.com/gofiber/fiber/tree/master/middleware/basicauth) | Basic auth middleware provides an HTTP basic authentication. It calls the next handler for valid credentials and 401 Unauthorized for missing or invalid credentials. | +| [cache](https://github.com/gofiber/fiber/tree/master/middleware/cache) | Intercept and cache responses | +| [compress](https://github.com/gofiber/fiber/tree/master/middleware/compress) | Compression middleware for Fiber, it supports `deflate`, `gzip` and `brotli` by default. | +| [cors](https://github.com/gofiber/fiber/tree/master/middleware/cors) | Enable cross-origin resource sharing \(CORS\) with various options. | +| [csrf](https://github.com/gofiber/fiber/tree/master/middleware/csrf) | Protect from CSRF exploits. | +| [encryptcookie](https://github.com/gofiber/fiber/tree/master/middleware/encryptcookie) | Encrypt middleware which encrypts cookie values. | +| [envvar](https://github.com/gofiber/fiber/tree/master/middleware/envvar) | Expose environment variables with providing an optional config. | +| [etag](https://github.com/gofiber/fiber/tree/master/middleware/etag) | ETag middleware that lets caches be more efficient and save bandwidth, as a web server does not need to resend a full response if the content has not changed. | +| [expvar](https://github.com/gofiber/fiber/tree/master/middleware/expvar) | Expvar middleware that serves via its HTTP server runtime exposed variants in the JSON format. | +| [favicon](https://github.com/gofiber/fiber/tree/master/middleware/favicon) | Ignore favicon from logs or serve from memory if a file path is provided. | +| [filesystem](https://github.com/gofiber/fiber/tree/master/middleware/filesystem) | FileSystem middleware for Fiber, special thanks and credits to Alireza Salary | +| [limiter](https://github.com/gofiber/fiber/tree/master/middleware/limiter) | Rate-limiting middleware for Fiber. Use to limit repeated requests to public APIs and/or endpoints such as password reset. | +| [logger](https://github.com/gofiber/fiber/tree/master/middleware/logger) | HTTP request/response logger. | +| [monitor](https://github.com/gofiber/fiber/tree/master/middleware/monitor) | Monitor middleware that reports server metrics, inspired by express-status-monitor | +| [pprof](https://github.com/gofiber/fiber/tree/master/middleware/pprof) | Special thanks to Matthew Lee \(@mthli\) | +| [proxy](https://github.com/gofiber/fiber/tree/master/middleware/proxy) | Allows you to proxy requests to a multiple servers | +| [recover](https://github.com/gofiber/fiber/tree/master/middleware/recover) | Recover middleware recovers from panics anywhere in the stack chain and handles the control to the centralized[ ErrorHandler](https://docs.gofiber.io/guide/error-handling). | +| [requestid](https://github.com/gofiber/fiber/tree/master/middleware/requestid) | Adds a requestid to every request. | +| [session](https://github.com/gofiber/fiber/tree/master/middleware/session) | Session middleware. NOTE: This middleware uses our Storage package. | +| [skip](https://github.com/gofiber/fiber/tree/master/middleware/skip) | Skip middleware that skips a wrapped handler if a predicate is true. | +| [rewrite](https://github.com/gofiber/rewrite) | Rewrite middleware rewrites the URL path based on provided rules. It can be helpful for backward compatibility or just creating cleaner and more descriptive links. | +| [timeout](https://github.com/gofiber/fiber/tree/master/middleware/timeout) | Adds a max time for a request and forwards to ErrorHandler if it is exceeded. | +| [adaptor](https://github.com/gofiber/adaptor) | Converter for net/http handlers to/from Fiber request handlers, special thanks to @arsmn! | +| [helmet](https://github.com/gofiber/helmet) | Helps secure your apps by setting various HTTP headers. | +| [redirect](https://github.com/gofiber/redirect) | Redirect middleware | +| [keyauth](https://github.com/gofiber/keyauth) | Key auth middleware provides a key based authentication. | + +## 🧬 ميدلوير خارجي + +لستة ميدلويرز خارجية بتطور من [تيم فايبر](https://github.com/orgs/gofiber/people). + +| Middleware | Description | +| :------------------------------------------------ | :-------------------------------------------------------------------------------------------------------------------- | +| [jwt](https://github.com/gofiber/jwt) | JWT returns a JSON Web Token \(JWT\) auth middleware. | +| [storage](https://github.com/gofiber/storage) | Premade storage drivers that implement the Storage interface, designed to be used with various Fiber middlewares. | +| [template](https://github.com/gofiber/template) | This package contains 8 template engines that can be used with Fiber `v1.10.x` Go version 1.13 or higher is required. | +| [websocket](https://github.com/gofiber/websocket) | Based on Fasthttp WebSocket for Fiber with Locals support! | + +## 🕶️ لستة الجامدين + +لو عاوز تشوف مقالات او ميدل وير او امثلة او ادوات بص على اللستة دي [awesome list](https://github.com/gofiber/awesome-fiber). + +## 👍 شاركنا + +لو عاوز تقول **شكرا** او تدعمنا في تطوير `فايبر`: + +1. اعمل [GitHub Star](https://github.com/gofiber/fiber/stargazers) للبروجكت. +2. تويت عن البروجكت [على تويتر](https://x.com/intent/tweet?text=Fiber%20is%20an%20Express%20inspired%20%23web%20%23framework%20built%20on%20top%20of%20Fasthttp%2C%20the%20fastest%20HTTP%20engine%20for%20%23Go.%20Designed%20to%20ease%20things%20up%20for%20%23fast%20development%20with%20zero%20memory%20allocation%20and%20%23performance%20in%20mind%20%F0%9F%9A%80%20https%3A%2F%2Fgithub.com%2Fgofiber%2Ffiber). +3. اكتب ريفيو او توتوريال على [Medium](https://medium.com/), [Dev.to](https://dev.to/) او البلوج بتاعتك. +4. او ادعم المشروع [بكوباية شاي](https://buymeacoff.ee/fenny). + +## ☕ الداعمين + +فايبر مشروع اوبن سورس وشغال على التبرعات عشان ندفع فواتير الدومين والجيت بوك والنتليفاي والسيرفرات. لو عاوز تدعم فايبر تقدر تشتري كوباية شاي من [هنا](https://buymeacoff.ee/fenny). + +| | User | Donation | +| :--------------------------------------------------------- | :----------------------------------------------- | :------- | +| ![](https://avatars.githubusercontent.com/u/204341?s=25) | [@destari](https://github.com/destari) | ☕ x 10 | +| ![](https://avatars.githubusercontent.com/u/63164982?s=25) | [@dembygenesis](https://github.com/dembygenesis) | ☕ x 5 | +| ![](https://avatars.githubusercontent.com/u/56607882?s=25) | [@thomasvvugt](https://github.com/thomasvvugt) | ☕ x 5 | +| ![](https://avatars.githubusercontent.com/u/27820675?s=25) | [@hendratommy](https://github.com/hendratommy) | ☕ x 5 | +| ![](https://avatars.githubusercontent.com/u/1094221?s=25) | [@ekaputra07](https://github.com/ekaputra07) | ☕ x 5 | +| ![](https://avatars.githubusercontent.com/u/194590?s=25) | [@jorgefuertes](https://github.com/jorgefuertes) | ☕ x 5 | +| ![](https://avatars.githubusercontent.com/u/186637?s=25) | [@candidosales](https://github.com/candidosales) | ☕ x 5 | +| ![](https://avatars.githubusercontent.com/u/29659953?s=25) | [@l0nax](https://github.com/l0nax) | ☕ x 3 | +| ![](https://avatars.githubusercontent.com/u/635852?s=25) | [@bihe](https://github.com/bihe) | ☕ x 3 | +| ![](https://avatars.githubusercontent.com/u/307334?s=25) | [@justdave](https://github.com/justdave) | ☕ x 3 | +| ![](https://avatars.githubusercontent.com/u/11155743?s=25) | [@koddr](https://github.com/koddr) | ☕ x 1 | +| ![](https://avatars.githubusercontent.com/u/29042462?s=25) | [@lapolinar](https://github.com/lapolinar) | ☕ x 1 | +| ![](https://avatars.githubusercontent.com/u/2978730?s=25) | [@diegowifi](https://github.com/diegowifi) | ☕ x 1 | +| ![](https://avatars.githubusercontent.com/u/44171355?s=25) | [@ssimk0](https://github.com/ssimk0) | ☕ x 1 | +| ![](https://avatars.githubusercontent.com/u/5638101?s=25) | [@raymayemir](https://github.com/raymayemir) | ☕ x 1 | +| ![](https://avatars.githubusercontent.com/u/619996?s=25) | [@melkorm](https://github.com/melkorm) | ☕ x 1 | +| ![](https://avatars.githubusercontent.com/u/31022056?s=25) | [@marvinjwendt](https://github.com/marvinjwendt) | ☕ x 1 | +| ![](https://avatars.githubusercontent.com/u/31921460?s=25) | [@toishy](https://github.com/toishy) | ☕ x 1 | + +## ‎‍💻 Code Contributors + +Code Contributors + +## ⭐️ Stargazers + +Stargazers over time + +## ⚠️ License + +Copyright (c) 2019-present [Fenny](https://github.com/fenny) and [Contributors](https://github.com/gofiber/fiber/graphs/contributors). `Fiber` is free and open-source software licensed under the [MIT License](https://github.com/gofiber/fiber/blob/master/LICENSE). Official logo was created by [Vic Shóstak](https://github.com/koddr) and distributed under [Creative Commons](https://creativecommons.org/licenses/by-sa/4.0/) license (CC BY-SA 4.0 International). + +**Third-party library licenses** + +- [colorable](https://github.com/mattn/go-colorable/blob/master/LICENSE) +- [isatty](https://github.com/mattn/go-isatty/blob/master/LICENSE) +- [runewidth](https://github.com/mattn/go-runewidth/blob/master/LICENSE) +- [fasthttp](https://github.com/valyala/fasthttp/blob/master/LICENSE) +- [bytebufferpool](https://github.com/valyala/bytebufferpool/blob/master/LICENSE) +- [fwd](https://github.com/philhofer/fwd/blob/master/LICENSE.md) +- [go-ole](https://github.com/go-ole/go-ole/blob/master/LICENSE) +- [gopsutil](https://github.com/shirou/gopsutil/blob/master/LICENSE) +- [msgp](https://github.com/tinylib/msgp/blob/master/LICENSE) +- [schema](https://github.com/gorilla/schema/blob/master/LICENSE) +- [uuid](https://github.com/google/uuid/blob/master/LICENSE) +- [wmi](https://github.com/StackExchange/wmi/blob/master/LICENSE) diff --git a/.github/README_es.md b/.github/README_es.md new file mode 100644 index 0000000..b3e5b9f --- /dev/null +++ b/.github/README_es.md @@ -0,0 +1,687 @@ +

+ + + + + Fiber + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + +

+

+Fiber es un framework web inspirado en Express construido sobre Fasthttp, el motor HTTP más rápido para Go. Diseñado para facilitar las cosas y tener un menor tiempo de desarrollo con cero asignación de memoria y pensado para un mejor rendimiento. +

+ +## ⚡️ Inicio rápido + +```go +package main + +import "github.com/gofiber/fiber/v2" + +func main() { + app := fiber.New() + + app.Get("/", func(c *fiber.Ctx) error { + return c.SendString("Hello, World 👋!") + }) + + app.Listen(":3000") +} +``` + +## 🤖 Puntos de referencia + +Estas pruebas son realizadas por [TechEmpower](https://www.techempower.com/benchmarks/#section=data-r19&hw=ph&test=plaintext) y [Go Web](https://github.com/smallnest/go-web-framework-benchmark). Si desea ver todos los resultados, visite nuestra [Wiki](https://docs.gofiber.io/extra/benchmarks). + +

+ + +

+ +## ⚙️ Instalación + +Asegúrese de tener instalado Go ([descargar](https://go.dev/dl/)). Versión `1.17` o superior. + +Arranque su proyecto creando una nueva carpeta y ejecutando `go mod init github.com/your/repo` ([mas información](https://go.dev/blog/using-go-modules)) dentro del mismo directorio. Después instale Fiber mediante el comando [`go get`](https://pkg.go.dev/cmd/go/#hdr-Add_dependencies_to_current_module_and_install_them): + +```bash +go get -u github.com/gofiber/fiber/v2 +``` + +## 🎯 Características + +- [Enrutamiento](https://docs.gofiber.io/guide/routing) robusto +- Servir [archivos estáticos](https://docs.gofiber.io/api/app#static) +- [Rendimiento](https://docs.gofiber.io/extra/benchmarks) extremo +- [Bajo](https://docs.gofiber.io/extra/benchmarks) uso de [memoria](https://docs.gofiber.io/extra/benchmarks) +- [Puntos finales de API](https://docs.gofiber.io/api/ctx) Express +- Middleware y [próximo](https://docs.gofiber.io/api/ctx#next) soporte +- Programación [rápida](https://dev.to/koddr/welcome-to-fiber-an-express-js-styled-fastest-web-framework-written-with-on-golang-497) del lado del servidor +- [Template engines](https://github.com/gofiber/template) +- [WebSocket support](https://github.com/gofiber/websocket) +- [Server-Sent events](https://github.com/gofiber/recipes/tree/master/sse) +- [Rate Limiter](https://docs.gofiber.io/api/middleware/limiter) +- Disponible en [19 idiomas](https://docs.gofiber.io/) +- Y mucho más, [explora Fiber](https://docs.gofiber.io/) + +## 💡 Filosofía + +Los nuevos gophers que hacen el cambio de [Node.js](https://nodejs.org/en/about/) a [Go](https://go.dev/doc/) están lidiando con una curva de aprendizaje antes de que puedan comenzar a construir sus aplicaciones web o micro-servicios. Fiber, como un **marco web** , fue creado con la idea del **minimalismo** y sigue el **camino de UNIX** , para que los nuevos gophers puedan ingresar rápidamente al mundo de Go con una cálida y confiable bienvenida. + +Fiber está **inspirado** en Expressjs, el framework web más popular en Internet. Combinamos la **facilidad** de Express y **el rendimiento bruto** de Go. Si alguna vez ha implementado una aplicación web en Node.js ( _utilizando Express.js o similar_ ), muchos métodos y principios le parecerán **muy comunes** . + +## ⚠️ Limitantes + +- Debido a que Fiber utiliza unsafe, la biblioteca no siempre será compatible con la última versión de Go. Fiber 2.40.0 ha sido probado con las versiones de Go 1.17 a 1.21. +- Fiber no es compatible con interfaces net/http. Esto significa que no lo podrá usar en proyectos como qglgen, go-swagger, u otros que son parte del ecosistema net/http. + +## 👀 Ejemplos + +A continuación se enumeran algunos de los ejemplos comunes. Si desea ver más ejemplos de código, visite nuestro [repositorio de Recetas](https://github.com/gofiber/recipes) o nuestra [documentación de API](https://docs.gofiber.io) . + +#### 📖 [**Ruteo Básico**](https://docs.gofiber.io/#basic-routing) + +```go +func main() { + app := fiber.New() + + // GET /api/register + app.Get("/api/*", func(c *fiber.Ctx) error { + msg := fmt.Sprintf("✋ %s", c.Params("*")) + return c.SendString(msg) // => ✋ register + }) + + // GET /flights/LAX-SFO + app.Get("/flights/:from-:to", func(c *fiber.Ctx) error { + msg := fmt.Sprintf("💸 From: %s, To: %s", c.Params("from"), c.Params("to")) + return c.SendString(msg) // => 💸 From: LAX, To: SFO + }) + + // GET /dictionary.txt + app.Get("/:file.:ext", func(c *fiber.Ctx) error { + msg := fmt.Sprintf("📃 %s.%s", c.Params("file"), c.Params("ext")) + return c.SendString(msg) // => 📃 dictionary.txt + }) + + // GET /john/75 + app.Get("/:name/:age/:gender?", func(c *fiber.Ctx) error { + msg := fmt.Sprintf("👴 %s is %s years old", c.Params("name"), c.Params("age")) + return c.SendString(msg) // => 👴 john is 75 years old + }) + + // GET /john + app.Get("/:name", func(c *fiber.Ctx) error { + msg := fmt.Sprintf("Hello, %s 👋!", c.Params("name")) + return c.SendString(msg) // => Hello john 👋! + }) + + log.Fatal(app.Listen(":3000")) +} + +``` + +#### 📖 [**Route Naming**](https://docs.gofiber.io/api/app#name) + +```go +func main() { + app := fiber.New() + + // GET /api/register + app.Get("/api/*", func(c *fiber.Ctx) error { + msg := fmt.Sprintf("✋ %s", c.Params("*")) + return c.SendString(msg) // => ✋ register + }).Name("api") + + data, _ := json.MarshalIndent(app.GetRoute("api"), "", " ") + fmt.Print(string(data)) + // Prints: + // { + // "method": "GET", + // "name": "api", + // "path": "/api/*", + // "params": [ + // "*1" + // ] + // } + + + log.Fatal(app.Listen(":3000")) +} + +``` + +#### 📖 [**Sirviendo Archivos Estáticos**](https://docs.gofiber.io/api/app#static) + +```go +func main() { + app := fiber.New() + + app.Static("/", "./public") + // => http://localhost:3000/js/script.js + // => http://localhost:3000/css/style.css + + app.Static("/prefix", "./public") + // => http://localhost:3000/prefix/js/script.js + // => http://localhost:3000/prefix/css/style.css + + app.Static("*", "./public/index.html") + // => http://localhost:3000/any/path/shows/index/html + + log.Fatal(app.Listen(":3000")) +} + +``` + +#### 📖 [**Middleware y Next**](https://docs.gofiber.io/api/ctx#next) + +```go +func main() { + app := fiber.New() + + // Match any route + app.Use(func(c *fiber.Ctx) error { + fmt.Println("🥇 First handler") + return c.Next() + }) + + // Match all routes starting with /api + app.Use("/api", func(c *fiber.Ctx) error { + fmt.Println("🥈 Second handler") + return c.Next() + }) + + // GET /api/register + app.Get("/api/list", func(c *fiber.Ctx) error { + fmt.Println("🥉 Last handler") + return c.SendString("Hello, World 👋!") + }) + + log.Fatal(app.Listen(":3000")) +} + +``` + +
+ 📚 Mostrar más ejemplos de código + +### Motores de Vistas + +📖 [Configuración](https://docs.gofiber.io/api/fiber#config) +📖 [Motores](https://github.com/gofiber/template) +📖 [Renderizado](https://docs.gofiber.io/api/ctx#render) + +El motor por defecto de Fiber, cuando no es especificado, es [html/template](https://pkg.go.dev/html/template/). + +Si quiere ejecutar vistas parciales, o usar un motor diferente (como [amber](https://github.com/eknkc/amber), [handlebars](https://github.com/aymerick/raymond), [mustache](https://github.com/cbroglie/mustache) o [pug](https://github.com/Joker/jade), etc). + +Revise nuestro paquete para [Plantillas](https://github.com/gofiber/template) que soporta múltiples motores de vistas. + +```go +package main + +import ( + "github.com/gofiber/fiber/v2" + "github.com/gofiber/template/pug" +) + +func main() { + // You can setup Views engine before initiation app: + app := fiber.New(fiber.Config{ + Views: pug.New("./views", ".pug"), + }) + + // And now, you can call template `./views/home.pug` like this: + app.Get("/", func(c *fiber.Ctx) error { + return c.Render("home", fiber.Map{ + "title": "Homepage", + "year": 1999, + }) + }) + + log.Fatal(app.Listen(":3000")) +} +``` + +### Agrupando rutas en cadenas + +📖 [Group](https://docs.gofiber.io/api/app#group) + +```go +func middleware(c *fiber.Ctx) error { + fmt.Println("Don't mind me!") + return c.Next() +} + +func handler(c *fiber.Ctx) error { + return c.SendString(c.Path()) +} + +func main() { + app := fiber.New() + + // Root API route + api := app.Group("/api", middleware) // /api + + // API v1 routes + v1 := api.Group("/v1", middleware) // /api/v1 + v1.Get("/list", handler) // /api/v1/list + v1.Get("/user", handler) // /api/v1/user + + // API v2 routes + v2 := api.Group("/v2", middleware) // /api/v2 + v2.Get("/list", handler) // /api/v2/list + v2.Get("/user", handler) // /api/v2/user + + // ... +} + +``` + +### Middleware logger + +📖 [Logger](https://docs.gofiber.io/api/middleware/logger) + +```go +package main + +import ( + "log" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/logger" +) + +func main() { + app := fiber.New() + + app.Use(logger.New()) + + // ... + + log.Fatal(app.Listen(":3000")) +} +``` + +### Intercambio de recursos de origen cruzado (CORS) + +📖 [CORS](https://docs.gofiber.io/api/middleware/cors) + +```go +import ( + "log" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/cors" +) + +func main() { + app := fiber.New() + + app.Use(cors.New()) + + // ... + + log.Fatal(app.Listen(":3000")) +} +``` + +Revisar CORS, pasando cualquier dominio en el encabezado `Origin`: + +```bash +curl -H "Origin: http://example.com" --verbose http://localhost:3000 +``` + +### Respuesta 404 personalizada + +📖 [HTTP Methods](https://docs.gofiber.io/api/ctx#status) + +```go +func main() { + app := fiber.New() + + app.Static("/", "./public") + + app.Get("/demo", func(c *fiber.Ctx) error { + return c.SendString("This is a demo!") + }) + + app.Post("/register", func(c *fiber.Ctx) error { + return c.SendString("Welcome!") + }) + + // Last middleware to match anything + app.Use(func(c *fiber.Ctx) error { + return c.SendStatus(404) + // => 404 "Not Found" + }) + + log.Fatal(app.Listen(":3000")) +} +``` + +### Respuesta JSON + +📖 [JSON](https://docs.gofiber.io/api/ctx#json) + +```go +type User struct { + Name string `json:"name"` + Age int `json:"age"` +} + +func main() { + app := fiber.New() + + app.Get("/user", func(c *fiber.Ctx) error { + return c.JSON(&User{"John", 20}) + // => {"name":"John", "age":20} + }) + + app.Get("/json", func(c *fiber.Ctx) error { + return c.JSON(fiber.Map{ + "success": true, + "message": "Hi John!", + }) + // => {"success":true, "message":"Hi John!"} + }) + + log.Fatal(app.Listen(":3000")) +} +``` + +### Actualización a WebSockets + +📖 [Websocket](https://github.com/gofiber/websocket) + +```go +import ( + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/websocket" +) + +func main() { + app := fiber.New() + + app.Get("/ws", websocket.New(func(c *websocket.Conn) { + for { + mt, msg, err := c.ReadMessage() + if err != nil { + log.Println("read:", err) + break + } + log.Printf("recv: %s", msg) + err = c.WriteMessage(mt, msg) + if err != nil { + log.Println("write:", err) + break + } + } + })) + + log.Fatal(app.Listen(":3000")) + // ws://localhost:3000/ws +} +``` + +### Server-Sent Events + +📖 [More Info](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events) + +```go +import ( + "github.com/gofiber/fiber/v2" + "github.com/valyala/fasthttp" +) + +func main() { + app := fiber.New() + + app.Get("/sse", func(c *fiber.Ctx) error { + c.Set("Content-Type", "text/event-stream") + c.Set("Cache-Control", "no-cache") + c.Set("Connection", "keep-alive") + c.Set("Transfer-Encoding", "chunked") + + c.Context().SetBodyStreamWriter(fasthttp.StreamWriter(func(w *bufio.Writer) { + fmt.Println("WRITER") + var i int + + for { + i++ + msg := fmt.Sprintf("%d - the time is %v", i, time.Now()) + fmt.Fprintf(w, "data: Message: %s\n\n", msg) + fmt.Println(msg) + + w.Flush() + time.Sleep(5 * time.Second) + } + })) + + return nil + }) + + log.Fatal(app.Listen(":3000")) +} +``` + +### Middleware de Recuperación + +📖 [Recover](https://docs.gofiber.io/api/middleware/recover) + +```go +import ( + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/recover" +) + +func main() { + app := fiber.New() + + app.Use(recover.New()) + + app.Get("/", func(c *fiber.Ctx) error { + panic("normally this would crash your app") + }) + + log.Fatal(app.Listen(":3000")) +} +``` + +
+ +## 🧬 Middleware Interno + +Aquí está una lista del middleware incluido en el marco web Fiber. + +| Middleware | Descripción | +| :------------------------------------------------------------------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [basicauth](https://github.com/gofiber/fiber/tree/master/middleware/basicauth) | Basic auth middleware provides an HTTP basic authentication. It calls the next handler for valid credentials and 401 Unauthorized for missing or invalid credentials. | +| [cache](https://github.com/gofiber/fiber/tree/master/middleware/cache) | Intercept and cache responses | +| [compress](https://github.com/gofiber/fiber/tree/master/middleware/compress) | Compression middleware for Fiber, it supports `deflate`, `gzip` and `brotli` by default. | +| [cors](https://github.com/gofiber/fiber/tree/master/middleware/cors) | Enable cross-origin resource sharing \(CORS\) with various options. | +| [csrf](https://github.com/gofiber/fiber/tree/master/middleware/csrf) | Protect from CSRF exploits. | +| [encryptcookie](https://github.com/gofiber/fiber/tree/master/middleware/encryptcookie) | Encrypt middleware which encrypts cookie values. | +| [envvar](https://github.com/gofiber/fiber/tree/master/middleware/envvar) | Expose environment variables with providing an optional config. | +| [etag](https://github.com/gofiber/fiber/tree/master/middleware/etag) | ETag middleware that lets caches be more efficient and save bandwidth, as a web server does not need to resend a full response if the content has not changed. | +| [expvar](https://github.com/gofiber/fiber/tree/master/middleware/expvar) | Expvar middleware that serves via its HTTP server runtime exposed variants in the JSON format. | +| [favicon](https://github.com/gofiber/fiber/tree/master/middleware/favicon) | Ignore favicon from logs or serve from memory if a file path is provided. | +| [filesystem](https://github.com/gofiber/fiber/tree/master/middleware/filesystem) | FileSystem middleware for Fiber, special thanks and credits to Alireza Salary | +| [limiter](https://github.com/gofiber/fiber/tree/master/middleware/limiter) | Rate-limiting middleware for Fiber. Use to limit repeated requests to public APIs and/or endpoints such as password reset. | +| [logger](https://github.com/gofiber/fiber/tree/master/middleware/logger) | HTTP request/response logger. | +| [monitor](https://github.com/gofiber/fiber/tree/master/middleware/monitor) | Monitor middleware that reports server metrics, inspired by express-status-monitor | +| [pprof](https://github.com/gofiber/fiber/tree/master/middleware/pprof) | Special thanks to Matthew Lee \(@mthli\) | +| [proxy](https://github.com/gofiber/fiber/tree/master/middleware/proxy) | Allows you to proxy requests to a multiple servers | +| [recover](https://github.com/gofiber/fiber/tree/master/middleware/recover) | Recover middleware recovers from panics anywhere in the stack chain and handles the control to the centralized[ ErrorHandler](https://docs.gofiber.io/guide/error-handling). | +| [requestid](https://github.com/gofiber/fiber/tree/master/middleware/requestid) | Adds a requestid to every request. | +| [session](https://github.com/gofiber/fiber/tree/master/middleware/session) | Session middleware. NOTE: This middleware uses our Storage package. | +| [skip](https://github.com/gofiber/fiber/tree/master/middleware/skip) | Skip middleware that skips a wrapped handler is a predicate is true. | +| [timeout](https://github.com/gofiber/fiber/tree/master/middleware/timeout) | Adds a max time for a request and forwards to ErrorHandler if it is exceeded. | +| [keyauth](https://github.com/gofiber/keyauth) | Key auth middleware provides a key based authentication. | +| [redirect](https://github.com/gofiber/redirect) | Redirect middleware | +| [rewrite](https://github.com/gofiber/rewrite) | Rewrite middleware rewrites the URL path based on provided rules. It can be helpful for backward compatibility or just creating cleaner and more descriptive links. | +| [adaptor](https://github.com/gofiber/adaptor) | Converter for net/http handlers to/from Fiber request handlers, special thanks to @arsmn! | +| [helmet](https://github.com/gofiber/helmet) | Helps secure your apps by setting various HTTP headers. | + +## 🧬 Middleware Externo + +Lista de módulos de middleware alojados externamente, y mantenidos por el [equipo de Fiber](https://github.com/orgs/gofiber/people). + +| Middleware | Descripción | +| :------------------------------------------------ | :-------------------------------------------------------------------------------------------------------------------- | +| [jwt](https://github.com/gofiber/jwt) | JWT returns a JSON Web Token \(JWT\) auth middleware. | +| [storage](https://github.com/gofiber/storage) | Premade storage drivers that implement the Storage interface, designed to be used with various Fiber middlewares. | +| [template](https://github.com/gofiber/template) | This package contains 8 template engines that can be used with Fiber `v1.10.x` Go version 1.13 or higher is required. | +| [websocket](https://github.com/gofiber/websocket) | Based on Fasthttp WebSocket for Fiber with Locals support! | + +## 🕶️ Awesome List + +For more articles, middlewares, examples or tools check our [awesome list](https://github.com/gofiber/awesome-fiber). + +## 👍 Contribuir + +Si quiere **agradecer** y/o apoyar el desarrollo activo de `Fiber`: + +1. Agrega una [estrella de GitHub](https://github.com/gofiber/fiber/stargazers) al proyecto. +2. Tuitea sobre el proyecto [en tu 𝕏 (Twitter)](https://x.com/intent/tweet?text=Fiber%20is%20an%20Express%20inspired%20%23web%20%23framework%20built%20on%20top%20of%20Fasthttp%2C%20the%20fastest%20HTTP%20engine%20for%20%23Go.%20Designed%20to%20ease%20things%20up%20for%20%23fast%20development%20with%20zero%20memory%20allocation%20and%20%23performance%20in%20mind%20%F0%9F%9A%80%20https%3A%2F%2Fgithub.com%2Fgofiber%2Ffiber). +3. Escribe una reseña o tutorial en [Medium](https://medium.com/) , [Dev.to](https://dev.to/) o blog personal. +4. Apoya el proyecto donando una [tasa de café](https://buymeacoff.ee/fenny). + +## ☕ Personas que han mostrado su apoyo + +Fiber es un proyecto de código abierto que se mantiene a través de donaciones para pagar las cuentas e.g. nuestro nombre de dominio, gitbook, netlify y hosting serverless. Si quieres apoyar a Fiber, puedes ☕ [**comprar un café**](https://buymeacoff.ee/fenny). + +| | User | Donation | +| :--------------------------------------------------------- | :----------------------------------------------- | :------- | +| ![](https://avatars.githubusercontent.com/u/204341?s=25) | [@destari](https://github.com/destari) | ☕ x 10 | +| ![](https://avatars.githubusercontent.com/u/63164982?s=25) | [@dembygenesis](https://github.com/dembygenesis) | ☕ x 5 | +| ![](https://avatars.githubusercontent.com/u/56607882?s=25) | [@thomasvvugt](https://github.com/thomasvvugt) | ☕ x 5 | +| ![](https://avatars.githubusercontent.com/u/27820675?s=25) | [@hendratommy](https://github.com/hendratommy) | ☕ x 5 | +| ![](https://avatars.githubusercontent.com/u/1094221?s=25) | [@ekaputra07](https://github.com/ekaputra07) | ☕ x 5 | +| ![](https://avatars.githubusercontent.com/u/194590?s=25) | [@jorgefuertes](https://github.com/jorgefuertes) | ☕ x 5 | +| ![](https://avatars.githubusercontent.com/u/186637?s=25) | [@candidosales](https://github.com/candidosales) | ☕ x 5 | +| ![](https://avatars.githubusercontent.com/u/29659953?s=25) | [@l0nax](https://github.com/l0nax) | ☕ x 3 | +| ![](https://avatars.githubusercontent.com/u/635852?s=25) | [@bihe](https://github.com/bihe) | ☕ x 3 | +| ![](https://avatars.githubusercontent.com/u/307334?s=25) | [@justdave](https://github.com/justdave) | ☕ x 3 | +| ![](https://avatars.githubusercontent.com/u/11155743?s=25) | [@koddr](https://github.com/koddr) | ☕ x 1 | +| ![](https://avatars.githubusercontent.com/u/29042462?s=25) | [@lapolinar](https://github.com/lapolinar) | ☕ x 1 | +| ![](https://avatars.githubusercontent.com/u/2978730?s=25) | [@diegowifi](https://github.com/diegowifi) | ☕ x 1 | +| ![](https://avatars.githubusercontent.com/u/44171355?s=25) | [@ssimk0](https://github.com/ssimk0) | ☕ x 1 | +| ![](https://avatars.githubusercontent.com/u/5638101?s=25) | [@raymayemir](https://github.com/raymayemir) | ☕ x 1 | +| ![](https://avatars.githubusercontent.com/u/619996?s=25) | [@melkorm](https://github.com/melkorm) | ☕ x 1 | +| ![](https://avatars.githubusercontent.com/u/31022056?s=25) | [@marvinjwendt](https://github.com/thomasvvugt) | ☕ x 1 | +| ![](https://avatars.githubusercontent.com/u/31921460?s=25) | [@toishy](https://github.com/toishy) | ☕ x 1 | + +## ‎‍💻 Contribuyentes de código + +Code Contributors + +## ⭐️ Stargazers + +Stargazers over time + +## ⚠️ Licencia + +Copyright (c) 2019-presente [Fenny](https://github.com/fenny) y [contribuyentes](https://github.com/gofiber/fiber/graphs/contributors). `Fiber` es software libre y de código abierto bajo la licencia [MIT](https://github.com/gofiber/fiber/blob/master/LICENSE). El logo oficial fue creado por [Vic Shóstak](https://github.com/koddr) y distribuido bajo la licencia [Creative Commons](https://creativecommons.org/licenses/by-sa/4.0/) (CC BY-SA 4.0 International). + +**Third-party library licenses** + +- [colorable](https://github.com/mattn/go-colorable/blob/master/LICENSE) +- [isatty](https://github.com/mattn/go-isatty/blob/master/LICENSE) +- [runewidth](https://github.com/mattn/go-runewidth/blob/master/LICENSE) +- [fasthttp](https://github.com/valyala/fasthttp/blob/master/LICENSE) +- [bytebufferpool](https://github.com/valyala/bytebufferpool/blob/master/LICENSE) +- [fwd](https://github.com/philhofer/fwd/blob/master/LICENSE.md) +- [go-ole](https://github.com/go-ole/go-ole/blob/master/LICENSE) +- [gopsutil](https://github.com/shirou/gopsutil/blob/master/LICENSE) +- [msgp](https://github.com/tinylib/msgp/blob/master/LICENSE) +- [schema](https://github.com/gorilla/schema/blob/master/LICENSE) +- [uuid](https://github.com/google/uuid/blob/master/LICENSE) +- [wmi](https://github.com/StackExchange/wmi/blob/master/LICENSE) diff --git a/.github/README_fa.md b/.github/README_fa.md new file mode 100644 index 0000000..390922d --- /dev/null +++ b/.github/README_fa.md @@ -0,0 +1,861 @@ +

+ + + + + Fiber + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + +

+
+

+

+ Fiber الهام گرفته از فریمورک وب Express است که برپایه Fasthttp ساخته شده است, که سریعترین موتور HTTP برای زبان Go است. طراحی شده است تا با تخصیص حافظه و عملکرد صفر ، کارها را برای توسعه سریع آسان کند. +

+

+ +## ⚡️ شروع سریع + +
+
+ +```go +package main + +import "github.com/gofiber/fiber/v2" + +func main() { + app := fiber.New() + + app.Get("/", func(c *fiber.Ctx) error { + return c.SendString("Hello, World 👋!") + }) + + app.Listen(":3000") +} +``` + +
+
+ +## 🤖 بنچمارک + +
+

+

+این تست ها توسط TechEmpower و Go Web انجام شده است. اگر شما تمامی نتایج را می خواهید ببینید, لطفا صفحه ویکی ما را بازدید کنید. +
+
+

+ +

+ + +

+
+ +## ⚙️ نصب و راه اندازی + +
+

+

+مطمئن شوید Go را نصب (دانلود) کرده اید. نسخه 1.17 یا بیشتر مورد نیاز است.
+پروژه خود را با ساختن یک پوشه و سپس اجرای go mod init github.com/your/repo داخل پوشه (یادگیری بیشتر) راه اندازی کنید. سپس Fiber را با دستور go get نصب کنید : +
+
+

+ +
+ +```bash +go get -u github.com/gofiber/fiber/v2 +``` + +
+ +
+ +## 🎯 ویژگی ها + +
+ +- [مسیریابی](https://docs.gofiber.io/guide/routing) قدرتمند +- Serve [پرونده های ثابت](https://docs.gofiber.io/api/app#static) +- حداکثر [عملکرد](https://docs.gofiber.io/extra/benchmarks) +- مصرف [حافظه کم](https://docs.gofiber.io/extra/benchmarks) +- قابلیت [API endpoints](https://docs.gofiber.io/api/ctx) +- پشتیبانی از [Middleware](https://docs.gofiber.io/category/-middleware) & [Next](https://docs.gofiber.io/api/ctx#next) +- برنامه نویسی سمت سرور [سریع](https://dev.to/koddr/welcome-to-fiber-an-express-js-styled-fastest-web-framework-written-with-on-golang-497) +- دارای [Template engines](https://github.com/gofiber/template) اختصاصی +- [پشتیبانی از وب سوکت](https://github.com/gofiber/websocket) +- [Server-Sent events](https://github.com/gofiber/recipes/tree/master/sse) +- قابلیت [Rate Limiter](https://docs.gofiber.io/api/middleware/limiter) +- ترجمه در [19 زبان](https://docs.gofiber.io/) +- و امکانات بیشتر, [دیدن در داکیومنت](https://docs.gofiber.io/) + +
+ +## 💡 فلسفه + +
+

+

+گوفر های جدیدی که از Node.js به Go تغییر می دهند قبل از شروع ساخت برنامه های وب یا میکروسرویس ها با یک منحنی یادگیری رو به رو هستند. Fiber, یک فریمورک وب است و با ایده مینیمالیسم ایجاد شده است و روش های unix را دنبال می کند, بنابرین با استقبال گرم و اعتماد کامل وارد دنیای Go شده است.
+ +Fiber از Express الهام گرفته, که محبوب ترین فریمورک وب روی اینترنت است. ما سادگی Express را با عملکرد بالا زبان Go ترکیب کردیم. اگر شما قبلا یک برنامه وب را در Node.js (با استفاده از Express یا موارد مشابه) پیاده سازی کرده اید. بسیاری از روش ها و اصول ها برای شما ساده به نظر خواهد رسید.
+ +ما در مورد مشکلات کاربران, کانال Discord سراسر دنیا گوش می دهیم و تا بتوانیم یک وب فریم ورک سریع, مطمئن, ساده ایجاد کنیم. درست مثل Express برای برنامه نویس های جاوا اسکریپت. + +
+
+

+ +## ⚠️ محدودیت ها + +- به دلیل استفاده ناامن از Fiber, ممکن است کتابخانه همیشه با آخرین نسخه Go سازگار نباشد. Fiber 2.40.0 با زبان گو نسخه 1.17 تا 1.21 تست شده است. +- فریمورک Fiber با پکیج net/http سازگار نیست. این بدان معناست شما نمی توانید از پکیج های مانند go-swagger, gqlgen یا سایر پروژه هایی که بخشی از اکوسیستم net/http هستند استفاده کنید. + +
+ +## 👀 مثال ها + +

+

+در پایین تعدادی نمونه مثال هستش. اگر شما مثال های بیشتر می خواهید ببینید, لطفا مخزن کد و داکیومنت های ما را بازدید کنید. +
+
+

+ +#### 📖 [**Basic Routing**](https://docs.gofiber.io/#basic-routing) + +
+ +```go +func main() { + app := fiber.New() + + // GET /api/register + app.Get("/api/*", func(c *fiber.Ctx) error { + msg := fmt.Sprintf("✋ %s", c.Params("*")) + return c.SendString(msg) // => ✋ register + }) + + // GET /flights/LAX-SFO + app.Get("/flights/:from-:to", func(c *fiber.Ctx) error { + msg := fmt.Sprintf("💸 From: %s, To: %s", c.Params("from"), c.Params("to")) + return c.SendString(msg) // => 💸 From: LAX, To: SFO + }) + + // GET /dictionary.txt + app.Get("/:file.:ext", func(c *fiber.Ctx) error { + msg := fmt.Sprintf("📃 %s.%s", c.Params("file"), c.Params("ext")) + return c.SendString(msg) // => 📃 dictionary.txt + }) + + // GET /john/75 + app.Get("/:name/:age/:gender?", func(c *fiber.Ctx) error { + msg := fmt.Sprintf("👴 %s is %s years old", c.Params("name"), c.Params("age")) + return c.SendString(msg) // => 👴 john is 75 years old + }) + + // GET /john + app.Get("/:name", func(c *fiber.Ctx) error { + msg := fmt.Sprintf("Hello, %s 👋!", c.Params("name")) + return c.SendString(msg) // => Hello john 👋! + }) + + log.Fatal(app.Listen(":3000")) +} + +``` + +
+ +
+ +#### 📖 [**Route Naming**](https://docs.gofiber.io/api/app#name) + +```go +func main() { + app := fiber.New() + + // GET /api/register + app.Get("/api/*", func(c *fiber.Ctx) error { + msg := fmt.Sprintf("✋ %s", c.Params("*")) + return c.SendString(msg) // => ✋ register + }).Name("api") + + data, _ := json.MarshalIndent(app.GetRoute("api"), "", " ") + fmt.Print(string(data)) + // Prints: + // { + // "method": "GET", + // "name": "api", + // "path": "/api/*", + // "params": [ + // "*1" + // ] + // } + + + log.Fatal(app.Listen(":3000")) +} + +``` + +
+ +#### 📖 [**Serving Static Files**](https://docs.gofiber.io/api/app#static) + +
+ +```go +func main() { + app := fiber.New() + + app.Static("/", "./public") + // => http://localhost:3000/js/script.js + // => http://localhost:3000/css/style.css + + app.Static("/prefix", "./public") + // => http://localhost:3000/prefix/js/script.js + // => http://localhost:3000/prefix/css/style.css + + app.Static("*", "./public/index.html") + // => http://localhost:3000/any/path/shows/index/html + + log.Fatal(app.Listen(":3000")) +} + +``` + +
+ +#### 📖 [**Middleware & Next**](https://docs.gofiber.io/api/ctx#next) + +
+ +```go +func main() { + app := fiber.New() + + // Match any route + app.Use(func(c *fiber.Ctx) error { + fmt.Println("🥇 First handler") + return c.Next() + }) + + // Match all routes starting with /api + app.Use("/api", func(c *fiber.Ctx) error { + fmt.Println("🥈 Second handler") + return c.Next() + }) + + // GET /api/list + app.Get("/api/list", func(c *fiber.Ctx) error { + fmt.Println("🥉 Last handler") + return c.SendString("Hello, World 👋!") + }) + + log.Fatal(app.Listen(":3000")) +} + +``` + +
+
+ +
+ 📚 نمایش نمونه کدهای بیشتر +
+ +### View engines + +
+ +📖 [پیکربندی](https://docs.gofiber.io/api/fiber#config) +📖 [موتورها](https://github.com/gofiber/template) +📖 [رندر](https://docs.gofiber.io/api/ctx#render) + +
+

+

+Fiber زمانیکه view engine تنظیم نشده باشد بطور پیش فرض از html/template استفاده می کند.
+شما می توانید از موتورهای مختلفی نظیر amber, handlebars, mustache یا pug استفاده کنید.
+پکیج موتورهای پشتیبانی شده را می توانید در اینجا ببینید. +
+
+

+ +
+ +```go +package main + +import ( + "github.com/gofiber/fiber/v2" + "github.com/gofiber/template/pug" +) + +func main() { + // You can setup Views engine before initiation app: + app := fiber.New(fiber.Config{ + Views: pug.New("./views", ".pug"), + }) + + // And now, you can call template `./views/home.pug` like this: + app.Get("/", func(c *fiber.Ctx) error { + return c.Render("home", fiber.Map{ + "title": "Homepage", + "year": 1999, + }) + }) + + log.Fatal(app.Listen(":3000")) +} +``` + +
+ +### Grouping routes into chains + +📖 [Group](https://docs.gofiber.io/api/app#group) + +
+ +```go +func middleware(c *fiber.Ctx) error { + fmt.Println("Don't mind me!") + return c.Next() +} + +func handler(c *fiber.Ctx) error { + return c.SendString(c.Path()) +} + +func main() { + app := fiber.New() + + // Root API route + api := app.Group("/api", middleware) // /api + + // API v1 routes + v1 := api.Group("/v1", middleware) // /api/v1 + v1.Get("/list", handler) // /api/v1/list + v1.Get("/user", handler) // /api/v1/user + + // API v2 routes + v2 := api.Group("/v2", middleware) // /api/v2 + v2.Get("/list", handler) // /api/v2/list + v2.Get("/user", handler) // /api/v2/user + + // ... +} + +``` + +
+ +### Middleware logger + +📖 [Logger](https://docs.gofiber.io/api/middleware/logger) + +
+ +```go +package main + +import ( + "log" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/logger" +) + +func main() { + app := fiber.New() + + app.Use(logger.New()) + + // ... + + log.Fatal(app.Listen(":3000")) +} +``` + +
+ +### Cross-Origin Resource Sharing (CORS) + +📖 [CORS](https://docs.gofiber.io/api/middleware/cors) + +
+ +```go +import ( + "log" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/cors" +) + +func main() { + app := fiber.New() + + app.Use(cors.New()) + + // ... + + log.Fatal(app.Listen(":3000")) +} +``` + +
+ +Check CORS by passing any domain in `Origin` header: + +
+ +```bash +curl -H "Origin: http://example.com" --verbose http://localhost:3000 +``` + +
+ +### Custom 404 response + +📖 [HTTP Methods](https://docs.gofiber.io/api/ctx#status) + +
+ +```go +func main() { + app := fiber.New() + + app.Static("/", "./public") + + app.Get("/demo", func(c *fiber.Ctx) error { + return c.SendString("This is a demo!") + }) + + app.Post("/register", func(c *fiber.Ctx) error { + return c.SendString("Welcome!") + }) + + // Last middleware to match anything + app.Use(func(c *fiber.Ctx) error { + return c.SendStatus(404) + // => 404 "Not Found" + }) + + log.Fatal(app.Listen(":3000")) +} +``` + +
+ +### JSON Response + +📖 [JSON](https://docs.gofiber.io/api/ctx#json) + +
+ +```go +type User struct { + Name string `json:"name"` + Age int `json:"age"` +} + +func main() { + app := fiber.New() + + app.Get("/user", func(c *fiber.Ctx) error { + return c.JSON(&User{"John", 20}) + // => {"name":"John", "age":20} + }) + + app.Get("/json", func(c *fiber.Ctx) error { + return c.JSON(fiber.Map{ + "success": true, + "message": "Hi John!", + }) + // => {"success":true, "message":"Hi John!"} + }) + + log.Fatal(app.Listen(":3000")) +} +``` + +
+ +### WebSocket Upgrade + +📖 [Websocket](https://github.com/gofiber/websocket) + +
+ +```go +import ( + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/websocket" +) + +func main() { + app := fiber.New() + + app.Get("/ws", websocket.New(func(c *websocket.Conn) { + for { + mt, msg, err := c.ReadMessage() + if err != nil { + log.Println("read:", err) + break + } + log.Printf("recv: %s", msg) + err = c.WriteMessage(mt, msg) + if err != nil { + log.Println("write:", err) + break + } + } + })) + + log.Fatal(app.Listen(":3000")) + // ws://localhost:3000/ws +} +``` + +### Server-Sent Events + +📖 [More Info](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events) + +```go +import ( + "github.com/gofiber/fiber/v2" + "github.com/valyala/fasthttp" +) + +func main() { + app := fiber.New() + + app.Get("/sse", func(c *fiber.Ctx) error { + c.Set("Content-Type", "text/event-stream") + c.Set("Cache-Control", "no-cache") + c.Set("Connection", "keep-alive") + c.Set("Transfer-Encoding", "chunked") + + c.Context().SetBodyStreamWriter(fasthttp.StreamWriter(func(w *bufio.Writer) { + fmt.Println("WRITER") + var i int + + for { + i++ + msg := fmt.Sprintf("%d - the time is %v", i, time.Now()) + fmt.Fprintf(w, "data: Message: %s\n\n", msg) + fmt.Println(msg) + + w.Flush() + time.Sleep(5 * time.Second) + } + })) + + return nil + }) + + log.Fatal(app.Listen(":3000")) +} +``` + +
+ +### Recover middleware + +📖 [Recover](https://docs.gofiber.io/api/middleware/recover) + +
+ +```go +import ( + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/recover" +) + +func main() { + app := fiber.New() + + app.Use(recover.New()) + + app.Get("/", func(c *fiber.Ctx) error { + panic("normally this would crash your app") + }) + + log.Fatal(app.Listen(":3000")) +} +``` + +### Using Trusted Proxy + +📖 [Config](https://docs.gofiber.io/api/fiber#config) + +```go +import ( + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/recover" +) + +func main() { + app := fiber.New(fiber.Config{ + EnableTrustedProxyCheck: true, + TrustedProxies: []string{"0.0.0.0", "1.1.1.1/30"}, // IP address or IP address range + ProxyHeader: fiber.HeaderXForwardedFor, + }) + + // ... + + log.Fatal(app.Listen(":3000")) +} +``` + +
+ +
+

+ +## 🧬 Middleware داخلی + +
+ +در اینجا لیستی از middleware های Fiber موجود است. + +
+ +
+ +| Middleware | توضیحات | +| :------------------------------------------------------------------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------- | --- | +| [basicauth](https://github.com/gofiber/fiber/tree/master/middleware/basicauth) | یک میدلور پایه که سیستم احراز هویت پایه ای را فراهم میکند. در صورت معتبر بودن درخواست روتر بعدی صدا زده شده و در صورت نامعتبر بودن خطای ۴۰۱ نمایش داده میشود. | +| [cache](https://github.com/gofiber/fiber/tree/master/middleware/cache) | پاسخ هارا رهگیری کرده و انها را به صورت موقت ذخیره میکند. | +| [compress](https://github.com/gofiber/fiber/tree/master/middleware/compress) | یک میدلور فشرده سازی برای Fiber که به طور پیشفرض از `deflate`, `gzip` و `brotli`. پشتیبانی میکند. | | +| [cors](https://github.com/gofiber/fiber/tree/master/middleware/cors) | فعال سازی هدر های cross-origin با گزینه های مختلف. | +| [csrf](https://github.com/gofiber/fiber/tree/master/middleware/csrf) | در برابر حملات CSRF ایمنی ایجاد میکند. | +| [encryptcookie](https://github.com/gofiber/fiber/tree/master/middleware/encryptcookie) | مقادیر کوکی هارا رمزنگاری میکند. | +| [envvar](https://github.com/gofiber/fiber/tree/master/middleware/envvar) | با ارائه تنظیمات اختیاری، متغیرهای محیط را در معرض دید قرار دهید. | +| [etag](https://github.com/gofiber/fiber/tree/master/middleware/etag) | میدلور ETag به کش ها اجازه میدهد کارآمد تر عمل کرده و در پهنای باند صرفه جویی کنند. به عنوان یک وب سرور نیازی به دادن پاسخ کامل نیست اگر محتوا تغییر نکرده باشد. | +| [expvar](https://github.com/gofiber/fiber/tree/master/middleware/expvar) | میدلور Expvar میتواند متغیر هایی را تعریف کرده و مقادیر انها را در زمان اجرا با فرمت JSON به شما نشان دهد. | +| [favicon](https://github.com/gofiber/fiber/tree/master/middleware/favicon) | جلوگیری و یا کش کردن درخواست های favicon در صورتی که مسیر یک فایل را داده باشید. | +| [filesystem](https://github.com/gofiber/fiber/tree/master/middleware/filesystem) | میدلور FileSystem به شما اجازه میدهد فایل های یک مسیر را عمومی کنید. | +| [limiter](https://github.com/gofiber/fiber/tree/master/middleware/limiter) | میدلور محدود کننده تعداد درخواست برای Fiber. | +| [logger](https://github.com/gofiber/fiber/tree/master/middleware/logger) | لاگ گرفتن از درخواست و پاسخ های HTTP. | +| [monitor](https://github.com/gofiber/fiber/tree/master/middleware/monitor) | وضعیت سرور را مانیتور و گزارش میکند، از express-status-monitor الهام گرفته شده است. | +| [pprof](https://github.com/gofiber/fiber/tree/master/middleware/pprof) | تشکر ویژه از Matthew Lee \(@mthli\) | +| [proxy](https://github.com/gofiber/fiber/tree/master/middleware/proxy) | اجازه میدهد درخواست هارا بر روی چند سرور پروکسی کنید. | +| [recover](https://github.com/gofiber/fiber/tree/master/middleware/recover) | خطا های زمان اجرا را در وب سرور HTTP شما مدیریت میکنند[ ErrorHandler](https://docs.gofiber.io/guide/error-handling). | +| [requestid](https://github.com/gofiber/fiber/tree/master/middleware/requestid) | به تمامی درخواست ها شناسه ای را اختصاص میدهد. | +| [session](https://github.com/gofiber/fiber/tree/master/middleware/session) | برای ذخیره و مدیریت شناسه کاربری یا session بازدید کنندگان استفاده .میشود | +| [skip](https://github.com/gofiber/fiber/tree/master/middleware/skip) | این میدلور میتواند با استفاده از شرط های تعیین شده درخواست هایی را نادیده بگیرد. | +| [timeout](https://github.com/gofiber/fiber/tree/master/middleware/timeout) | این میدلور محدودیت زمانی ای را برای درخواست ها تنظیم میکند، در صورتی که محدودیت به پایان برسد ErrorHandler صدا زده میشود. | +| [keyauth](https://github.com/gofiber/keyauth) | این میدلور احراز هویت مبتنی بر کلید را فراهم می کند. | +| [redirect](https://github.com/gofiber/redirect) | برای ریدایرکت کردن از این میدلور میتوانید استفاده کنید. | +| [rewrite](https://github.com/gofiber/rewrite) | مسیر URL را براساس قوانین مشخص شده بازنویسی می کند. این میتواند برای سازگاری با ورژن های قبلی یا برای ساخت لینک های تمیز تر و توصیفی تر مفید باشد. | +| [adaptor](https://github.com/gofiber/adaptor) | Converter for net/http handlers to/from Fiber request handlers, special thanks to @arsmn! | +| [helmet](https://github.com/gofiber/helmet) | با استفاده از HTTP هدر های مختلف به ایمن سازی برنامه شما کمک می کند. | + +
+

+ +## 🧬 Middleware خارجی + +
+ +لیست middleware های خارجی که توسط [تیم Fiber](https://github.com/orgs/gofiber/people) نگه داری می شود. + +
+ +
+ +| Middleware | توضیحات | +| :------------------------------------------------ | :-------------------------------------------------------------------------------------------------------------------- | +| [jwt](https://github.com/gofiber/jwt) | JWT returns a JSON Web Token \(JWT\) auth middleware. | +| [storage](https://github.com/gofiber/storage) | Premade storage drivers that implement the Storage interface, designed to be used with various Fiber middlewares. | +| [template](https://github.com/gofiber/template) | This package contains 8 template engines that can be used with Fiber `v1.10.x` Go version 1.13 or higher is required. | +| [websocket](https://github.com/gofiber/websocket) | Based on Fasthttp WebSocket for Fiber with Locals support! | + +## 🕶️ Awesome List + +[awesome list](https://github.com/gofiber/awesome-fiber) برای مقاله، میدلور، مثال ها و ابزار های بیشتر لطفا از این لینک بازدید کنید + +
+ +## 👍 مشارکت کنندگان + +
+ +اگر شما میخواهید **تشکر** کنید و یا از توسعه فعال Fiber حمایت کنید : + +1. یک [GitHub Star](https://github.com/gofiber/fiber/stargazers) به پروژه اضافه کنید. +2. ارسال توییت درباره Fiber برروی [صفحه توییتر شما](https://x.com/intent/tweet?text=Fiber%20is%20an%20Express%20inspired%20%23web%20%23framework%20built%20on%20top%20of%20Fasthttp%2C%20the%20fastest%20HTTP%20engine%20for%20%23Go.%20Designed%20to%20ease%20things%20up%20for%20%23fast%20development%20with%20zero%20memory%20allocation%20and%20%23performance%20in%20mind%20%F0%9F%9A%80%20https%3A%2F%2Fgithub.com%2Fgofiber%2Ffiber). +3. یک آموزش یا نظر برروی [Medium](https://medium.com/), [Dev.to](https://dev.to/) یا وبلاگ شخصیتان. +4. پشتیبانی پروژه با حمایت مالی از طریق [یک فنجان قهوه](https://buymeacoff.ee/fenny). + +
+ +## ☕ حامیان مالی + +
+ +Fiber یک پروژه متن باز است که با کمک مالی برای پرداخت قبض های دامنه, gitbook, netlify, هاست انجام می شود. اگر می خواهید از Fiber حمایت کنید شما می توانید [**از اینجا یک قهوه بخرید**](https://buymeacoff.ee/fenny). + +
+ +| | کاربر | حمایت مالی | +| :--------------------------------------------------------- | :----------------------------------------------- | :--------- | +| ![](https://avatars.githubusercontent.com/u/204341?s=25) | [@destari](https://github.com/destari) | ☕ x 10 | +| ![](https://avatars.githubusercontent.com/u/63164982?s=25) | [@dembygenesis](https://github.com/dembygenesis) | ☕ x 5 | +| ![](https://avatars.githubusercontent.com/u/56607882?s=25) | [@thomasvvugt](https://github.com/thomasvvugt) | ☕ x 5 | +| ![](https://avatars.githubusercontent.com/u/27820675?s=25) | [@hendratommy](https://github.com/hendratommy) | ☕ x 5 | +| ![](https://avatars.githubusercontent.com/u/1094221?s=25) | [@ekaputra07](https://github.com/ekaputra07) | ☕ x 5 | +| ![](https://avatars.githubusercontent.com/u/194590?s=25) | [@jorgefuertes](https://github.com/jorgefuertes) | ☕ x 5 | +| ![](https://avatars.githubusercontent.com/u/186637?s=25) | [@candidosales](https://github.com/candidosales) | ☕ x 5 | +| ![](https://avatars.githubusercontent.com/u/29659953?s=25) | [@l0nax](https://github.com/l0nax) | ☕ x 3 | +| ![](https://avatars.githubusercontent.com/u/635852?s=25) | [@bihe](https://github.com/bihe) | ☕ x 3 | +| ![](https://avatars.githubusercontent.com/u/307334?s=25) | [@justdave](https://github.com/justdave) | ☕ x 3 | +| ![](https://avatars.githubusercontent.com/u/11155743?s=25) | [@koddr](https://github.com/koddr) | ☕ x 1 | +| ![](https://avatars.githubusercontent.com/u/29042462?s=25) | [@lapolinar](https://github.com/lapolinar) | ☕ x 1 | +| ![](https://avatars.githubusercontent.com/u/2978730?s=25) | [@diegowifi](https://github.com/diegowifi) | ☕ x 1 | +| ![](https://avatars.githubusercontent.com/u/44171355?s=25) | [@ssimk0](https://github.com/ssimk0) | ☕ x 1 | +| ![](https://avatars.githubusercontent.com/u/5638101?s=25) | [@raymayemir](https://github.com/raymayemir) | ☕ x 1 | +| ![](https://avatars.githubusercontent.com/u/619996?s=25) | [@melkorm](https://github.com/melkorm) | ☕ x 1 | +| ![](https://avatars.githubusercontent.com/u/31022056?s=25) | [@marvinjwendt](https://github.com/marvinjwendt) | ☕ x 1 | +| ![](https://avatars.githubusercontent.com/u/31921460?s=25) | [@toishy](https://github.com/toishy) | ☕ x 1 | + +
+ +## ‎‍💻 مشارکت کنندگان کد + +
+ +Code Contributors + +
+ +## ⭐️ ستاره ها + +
+ +Stargazers over time + +

+ +## ⚠️ لایسنس + +
+ +
+ +Copyright (c) 2019-present [Fenny](https://github.com/fenny) and [Contributors](https://github.com/gofiber/fiber/graphs/contributors). `Fiber` is free and open-source software licensed under the [MIT License](https://github.com/gofiber/fiber/blob/master/LICENSE). Official logo was created by [Vic Shóstak](https://github.com/koddr) and distributed under [Creative Commons](https://creativecommons.org/licenses/by-sa/4.0/) license (CC BY-SA 4.0 International). + +
+ +
+ +**مجوزهای کتابخانه شخص ثالث** + +- [colorable](https://github.com/mattn/go-colorable/blob/master/LICENSE) +- [isatty](https://github.com/mattn/go-isatty/blob/master/LICENSE) +- [runewidth](https://github.com/mattn/go-runewidth/blob/master/LICENSE) +- [fasthttp](https://github.com/valyala/fasthttp/blob/master/LICENSE) +- [bytebufferpool](https://github.com/valyala/bytebufferpool/blob/master/LICENSE) +- [fwd](https://github.com/philhofer/fwd/blob/master/LICENSE.md) +- [go-ole](https://github.com/go-ole/go-ole/blob/master/LICENSE) +- [gopsutil](https://github.com/shirou/gopsutil/blob/master/LICENSE) +- [msgp](https://github.com/tinylib/msgp/blob/master/LICENSE) +- [schema](https://github.com/gorilla/schema/blob/master/LICENSE) +- [uuid](https://github.com/google/uuid/blob/master/LICENSE) +- [wmi](https://github.com/StackExchange/wmi/blob/master/LICENSE) diff --git a/.github/README_fr.md b/.github/README_fr.md new file mode 100644 index 0000000..2f93e56 --- /dev/null +++ b/.github/README_fr.md @@ -0,0 +1,689 @@ +

+ + + + + Fiber + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + +

+

+ Fiber est un framework web inspiré d' Express. Il se base sur Fasthttp, l'implémentation HTTP de Go la plus rapide. Conçu pour faciliter les choses pour des développements rapides, Fiber garde à l'esprit l'absence d'allocations mémoires, ainsi que les performances. +

+ +## ⚡️ Quickstart + +```go +package main + +import "github.com/gofiber/fiber/v2" + +func main() { + app := fiber.New() + + app.Get("/", func(c *fiber.Ctx) error { + return c.SendString("Hello, World 👋!") + }) + + app.Listen(":3000") +} +``` + +## 🤖 Benchmarks + +Ces tests sont effectués par [TechEmpower](https://www.techempower.com/benchmarks/#section=data-r19&hw=ph&test=plaintext) et [Go Web](https://github.com/smallnest/go-web-framework-benchmark). Si vous voulez voir tous les résultats, n'hésitez pas à consulter notre [Wiki](https://docs.gofiber.io/extra/benchmarks). + +

+ + +

+ +## ⚙️ Installation + +Make sure you have Go installed ([download](https://go.dev/dl/)). Version `1.17` or higher is required. + +Initialize your project by creating a folder and then running `go mod init github.com/your/repo` ([learn more](https://go.dev/blog/using-go-modules)) inside the folder. Then install Fiber with the [`go get`](https://pkg.go.dev/cmd/go/#hdr-Add_dependencies_to_current_module_and_install_them) command: + +```bash +go get -u github.com/gofiber/fiber/v2 +``` + +## 🎯 Features + +- [Routing](https://docs.gofiber.io/guide/routing) robuste +- Serve [static files](https://docs.gofiber.io/api/app#static) +- [Performances](https://docs.gofiber.io/extra/benchmarks) extrêmes +- [Faible empreinte mémoire](https://docs.gofiber.io/extra/benchmarks) +- [API endpoints](https://docs.gofiber.io/api/ctx) +- Middleware & [Next](https://docs.gofiber.io/api/ctx#next) support +- Programmation côté serveur [rapide](https://dev.to/koddr/welcome-to-fiber-an-express-js-styled-fastest-web-framework-written-with-on-golang-497) +- [Template engines](https://github.com/gofiber/template) +- [WebSocket support](https://github.com/gofiber/websocket) +- [Server-Sent events](https://github.com/gofiber/recipes/tree/master/sse) +- [Rate Limiter](https://docs.gofiber.io/api/middleware/limiter) +- Available in [19 languages](https://docs.gofiber.io/) +- Et plus encore, [explorez Fiber](https://docs.gofiber.io/) + +## 💡 Philosophie + +Les nouveaux gophers qui passent de [Node.js](https://nodejs.org/en/about/) à [Go](https://go.dev/doc/) sont confrontés à une courbe d'apprentissage, avant de pouvoir construire leurs applications web et microservices. Fiber, en tant que **framework web**, a été mis au point avec en tête l'idée de **minimalisme**, tout en suivant l'**UNIX way**, afin que les nouveaux gophers puissent rapidement entrer dans le monde de Go, avec un accueil chaleureux, de confiance. + +Fiber est **inspiré** par Express, le framework web le plus populaire d'Internet. Nous avons combiné la **facilité** d'Express, et la **performance brute** de Go. Si vous avez déja développé une application web en Node.js (_en utilisant Express ou équivalent_), alors de nombreuses méthodes et principes vous sembleront **familiers**. + +## ⚠️ Limitations + +- Due to Fiber's usage of unsafe, the library may not always be compatible with the latest Go version. Fiber 2.40.0 has been tested with Go versions 1.17 to 1.21. +- Fiber is not compatible with net/http interfaces. This means you will not be able to use projects like gqlgen, go-swagger, or any others which are part of the net/http ecosystem. + +## 👀 Exemples + +Ci-dessous quelques exemples courants. Si vous voulez voir plus d'exemples, rendez-vous sur notre ["Recipes repository"](https://github.com/gofiber/recipes) ou visitez notre [documentation API](https://docs.gofiber.io). + +Listed below are some of the common examples. If you want to see more code examples , please visit our [Recipes repository](https://github.com/gofiber/recipes) or visit our hosted [API documentation](https://docs.gofiber.io). + +#### 📖 [**Basic Routing**](https://docs.gofiber.io/#basic-routing) + +```go +func main() { + app := fiber.New() + + // GET /api/register + app.Get("/api/*", func(c *fiber.Ctx) error { + msg := fmt.Sprintf("✋ %s", c.Params("*")) + return c.SendString(msg) // => ✋ register + }) + + // GET /flights/LAX-SFO + app.Get("/flights/:from-:to", func(c *fiber.Ctx) error { + msg := fmt.Sprintf("💸 From: %s, To: %s", c.Params("from"), c.Params("to")) + return c.SendString(msg) // => 💸 From: LAX, To: SFO + }) + + // GET /dictionary.txt + app.Get("/:file.:ext", func(c *fiber.Ctx) error { + msg := fmt.Sprintf("📃 %s.%s", c.Params("file"), c.Params("ext")) + return c.SendString(msg) // => 📃 dictionary.txt + }) + + // GET /john/75 + app.Get("/:name/:age/:gender?", func(c *fiber.Ctx) error { + msg := fmt.Sprintf("👴 %s is %s years old", c.Params("name"), c.Params("age")) + return c.SendString(msg) // => 👴 john is 75 years old + }) + + // GET /john + app.Get("/:name", func(c *fiber.Ctx) error { + msg := fmt.Sprintf("Hello, %s 👋!", c.Params("name")) + return c.SendString(msg) // => Hello john 👋! + }) + + log.Fatal(app.Listen(":3000")) +} + +``` + +#### 📖 [**Route Naming**](https://docs.gofiber.io/api/app#name) + +```go +func main() { + app := fiber.New() + + // GET /api/register + app.Get("/api/*", func(c *fiber.Ctx) error { + msg := fmt.Sprintf("✋ %s", c.Params("*")) + return c.SendString(msg) // => ✋ register + }).Name("api") + + data, _ := json.MarshalIndent(app.GetRoute("api"), "", " ") + fmt.Print(string(data)) + // Prints: + // { + // "method": "GET", + // "name": "api", + // "path": "/api/*", + // "params": [ + // "*1" + // ] + // } + + + log.Fatal(app.Listen(":3000")) +} + +``` + +#### 📖 [**Serving Static Files**](https://docs.gofiber.io/api/app#static) + +```go +func main() { + app := fiber.New() + + app.Static("/", "./public") + // => http://localhost:3000/js/script.js + // => http://localhost:3000/css/style.css + + app.Static("/prefix", "./public") + // => http://localhost:3000/prefix/js/script.js + // => http://localhost:3000/prefix/css/style.css + + app.Static("*", "./public/index.html") + // => http://localhost:3000/any/path/shows/index/html + + log.Fatal(app.Listen(":3000")) +} + +``` + +#### 📖 [**Middleware & Next**](https://docs.gofiber.io/api/ctx#next) + +```go +func main() { + app := fiber.New() + + // Match any route + app.Use(func(c *fiber.Ctx) error { + fmt.Println("🥇 First handler") + return c.Next() + }) + + // Match all routes starting with /api + app.Use("/api", func(c *fiber.Ctx) error { + fmt.Println("🥈 Second handler") + return c.Next() + }) + + // GET /api/register + app.Get("/api/list", func(c *fiber.Ctx) error { + fmt.Println("🥉 Last handler") + return c.SendString("Hello, World 👋!") + }) + + log.Fatal(app.Listen(":3000")) +} + +``` + +
+ 📚 Show more code examples + +### Views engines + +📖 [Config](https://docs.gofiber.io/api/fiber#config) +📖 [Engines](https://github.com/gofiber/template) +📖 [Render](https://docs.gofiber.io/api/ctx#render) + +Fiber defaults to the [html/template](https://pkg.go.dev/html/template/) when no view engine is set. + +If you want to execute partials or use a different engine like [amber](https://github.com/eknkc/amber), [handlebars](https://github.com/aymerick/raymond), [mustache](https://github.com/cbroglie/mustache) or [pug](https://github.com/Joker/jade) etc.. + +Checkout our [Template](https://github.com/gofiber/template) package that support multiple view engines. + +```go +package main + +import ( + "github.com/gofiber/fiber/v2" + "github.com/gofiber/template/pug" +) + +func main() { + // You can setup Views engine before initiation app: + app := fiber.New(fiber.Config{ + Views: pug.New("./views", ".pug"), + }) + + // And now, you can call template `./views/home.pug` like this: + app.Get("/", func(c *fiber.Ctx) error { + return c.Render("home", fiber.Map{ + "title": "Homepage", + "year": 1999, + }) + }) + + log.Fatal(app.Listen(":3000")) +} +``` + +### Grouping routes into chains + +📖 [Group](https://docs.gofiber.io/api/app#group) + +```go +func middleware(c *fiber.Ctx) error { + fmt.Println("Don't mind me!") + return c.Next() +} + +func handler(c *fiber.Ctx) error { + return c.SendString(c.Path()) +} + +func main() { + app := fiber.New() + + // Root API route + api := app.Group("/api", middleware) // /api + + // API v1 routes + v1 := api.Group("/v1", middleware) // /api/v1 + v1.Get("/list", handler) // /api/v1/list + v1.Get("/user", handler) // /api/v1/user + + // API v2 routes + v2 := api.Group("/v2", middleware) // /api/v2 + v2.Get("/list", handler) // /api/v2/list + v2.Get("/user", handler) // /api/v2/user + + // ... +} + +``` + +### Middleware logger + +📖 [Logger](https://docs.gofiber.io/api/middleware/logger) + +```go +package main + +import ( + "log" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/logger" +) + +func main() { + app := fiber.New() + + app.Use(logger.New()) + + // ... + + log.Fatal(app.Listen(":3000")) +} +``` + +### Cross-Origin Resource Sharing (CORS) + +📖 [CORS](https://docs.gofiber.io/api/middleware/cors) + +```go +import ( + "log" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/cors" +) + +func main() { + app := fiber.New() + + app.Use(cors.New()) + + // ... + + log.Fatal(app.Listen(":3000")) +} +``` + +Check CORS by passing any domain in `Origin` header: + +```bash +curl -H "Origin: http://example.com" --verbose http://localhost:3000 +``` + +### Custom 404 response + +📖 [HTTP Methods](https://docs.gofiber.io/api/ctx#status) + +```go +func main() { + app := fiber.New() + + app.Static("/", "./public") + + app.Get("/demo", func(c *fiber.Ctx) error { + return c.SendString("This is a demo!") + }) + + app.Post("/register", func(c *fiber.Ctx) error { + return c.SendString("Welcome!") + }) + + // Last middleware to match anything + app.Use(func(c *fiber.Ctx) error { + return c.SendStatus(404) + // => 404 "Not Found" + }) + + log.Fatal(app.Listen(":3000")) +} +``` + +### JSON Response + +📖 [JSON](https://docs.gofiber.io/api/ctx#json) + +```go +type User struct { + Name string `json:"name"` + Age int `json:"age"` +} + +func main() { + app := fiber.New() + + app.Get("/user", func(c *fiber.Ctx) error { + return c.JSON(&User{"John", 20}) + // => {"name":"John", "age":20} + }) + + app.Get("/json", func(c *fiber.Ctx) error { + return c.JSON(fiber.Map{ + "success": true, + "message": "Hi John!", + }) + // => {"success":true, "message":"Hi John!"} + }) + + log.Fatal(app.Listen(":3000")) +} +``` + +### WebSocket Upgrade + +📖 [Websocket](https://github.com/gofiber/websocket) + +```go +import ( + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/websocket" +) + +func main() { + app := fiber.New() + + app.Get("/ws", websocket.New(func(c *websocket.Conn) { + for { + mt, msg, err := c.ReadMessage() + if err != nil { + log.Println("read:", err) + break + } + log.Printf("recv: %s", msg) + err = c.WriteMessage(mt, msg) + if err != nil { + log.Println("write:", err) + break + } + } + })) + + log.Fatal(app.Listen(":3000")) + // ws://localhost:3000/ws +} +``` + +### Server-Sent Events + +📖 [More Info](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events) + +```go +import ( + "github.com/gofiber/fiber/v2" + "github.com/valyala/fasthttp" +) + +func main() { + app := fiber.New() + + app.Get("/sse", func(c *fiber.Ctx) error { + c.Set("Content-Type", "text/event-stream") + c.Set("Cache-Control", "no-cache") + c.Set("Connection", "keep-alive") + c.Set("Transfer-Encoding", "chunked") + + c.Context().SetBodyStreamWriter(fasthttp.StreamWriter(func(w *bufio.Writer) { + fmt.Println("WRITER") + var i int + + for { + i++ + msg := fmt.Sprintf("%d - the time is %v", i, time.Now()) + fmt.Fprintf(w, "data: Message: %s\n\n", msg) + fmt.Println(msg) + + w.Flush() + time.Sleep(5 * time.Second) + } + })) + + return nil + }) + + log.Fatal(app.Listen(":3000")) +} +``` + +### Recover middleware + +📖 [Recover](https://docs.gofiber.io/api/middleware/recover) + +```go +import ( + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/recover" +) + +func main() { + app := fiber.New() + + app.Use(recover.New()) + + app.Get("/", func(c *fiber.Ctx) error { + panic("normally this would crash your app") + }) + + log.Fatal(app.Listen(":3000")) +} +``` + +
+ +## 🧬 Internal Middleware + +Here is a list of middleware that are included within the Fiber framework. + +| Middleware | Description | +| :------------------------------------------------------------------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [basicauth](https://github.com/gofiber/fiber/tree/master/middleware/basicauth) | Basic auth middleware provides an HTTP basic authentication. It calls the next handler for valid credentials and 401 Unauthorized for missing or invalid credentials. | +| [cache](https://github.com/gofiber/fiber/tree/master/middleware/cache) | Intercept and cache responses | +| [compress](https://github.com/gofiber/fiber/tree/master/middleware/compress) | Compression middleware for Fiber, it supports `deflate`, `gzip` and `brotli` by default. | +| [cors](https://github.com/gofiber/fiber/tree/master/middleware/cors) | Enable cross-origin resource sharing \(CORS\) with various options. | +| [csrf](https://github.com/gofiber/fiber/tree/master/middleware/csrf) | Protect from CSRF exploits. | +| [encryptcookie](https://github.com/gofiber/fiber/tree/master/middleware/encryptcookie) | Encrypt middleware which encrypts cookie values. | +| [envvar](https://github.com/gofiber/fiber/tree/master/middleware/envvar) | Expose environment variables with providing an optional config. | +| [etag](https://github.com/gofiber/fiber/tree/master/middleware/etag) | ETag middleware that lets caches be more efficient and save bandwidth, as a web server does not need to resend a full response if the content has not changed. | +| [expvar](https://github.com/gofiber/fiber/tree/master/middleware/expvar) | Expvar middleware that serves via its HTTP server runtime exposed variants in the JSON format. | +| [favicon](https://github.com/gofiber/fiber/tree/master/middleware/favicon) | Ignore favicon from logs or serve from memory if a file path is provided. | +| [filesystem](https://github.com/gofiber/fiber/tree/master/middleware/filesystem) | FileSystem middleware for Fiber, special thanks and credits to Alireza Salary | +| [limiter](https://github.com/gofiber/fiber/tree/master/middleware/limiter) | Rate-limiting middleware for Fiber. Use to limit repeated requests to public APIs and/or endpoints such as password reset. | +| [logger](https://github.com/gofiber/fiber/tree/master/middleware/logger) | HTTP request/response logger. | +| [monitor](https://github.com/gofiber/fiber/tree/master/middleware/monitor) | Monitor middleware that reports server metrics, inspired by express-status-monitor | +| [pprof](https://github.com/gofiber/fiber/tree/master/middleware/pprof) | Special thanks to Matthew Lee \(@mthli\) | +| [proxy](https://github.com/gofiber/fiber/tree/master/middleware/proxy) | Allows you to proxy requests to a multiple servers | +| [recover](https://github.com/gofiber/fiber/tree/master/middleware/recover) | Recover middleware recovers from panics anywhere in the stack chain and handles the control to the centralized[ ErrorHandler](https://docs.gofiber.io/guide/error-handling). | +| [requestid](https://github.com/gofiber/fiber/tree/master/middleware/requestid) | Adds a requestid to every request. | +| [session](https://github.com/gofiber/fiber/tree/master/middleware/session) | Session middleware. NOTE: This middleware uses our Storage package. | +| [skip](https://github.com/gofiber/fiber/tree/master/middleware/skip) | Skip middleware that skips a wrapped handler is a predicate is true. | +| [timeout](https://github.com/gofiber/fiber/tree/master/middleware/timeout) | Adds a max time for a request and forwards to ErrorHandler if it is exceeded. | +| [keyauth](https://github.com/gofiber/keyauth) | Key auth middleware provides a key based authentication. | +| [redirect](https://github.com/gofiber/redirect) | Redirect middleware | +| [rewrite](https://github.com/gofiber/rewrite) | Rewrite middleware rewrites the URL path based on provided rules. It can be helpful for backward compatibility or just creating cleaner and more descriptive links. | +| [adaptor](https://github.com/gofiber/adaptor) | Converter for net/http handlers to/from Fiber request handlers, special thanks to @arsmn! | +| [helmet](https://github.com/gofiber/helmet) | Helps secure your apps by setting various HTTP headers. | + +## 🧬 External Middleware + +List of externally hosted middleware modules and maintained by the [Fiber team](https://github.com/orgs/gofiber/people). + +| Middleware | Description | +| :------------------------------------------------ | :-------------------------------------------------------------------------------------------------------------------- | +| [jwt](https://github.com/gofiber/jwt) | JWT returns a JSON Web Token \(JWT\) auth middleware. | +| [storage](https://github.com/gofiber/storage) | Premade storage drivers that implement the Storage interface, designed to be used with various Fiber middlewares. | +| [template](https://github.com/gofiber/template) | This package contains 8 template engines that can be used with Fiber `v1.10.x` Go version 1.13 or higher is required. | +| [websocket](https://github.com/gofiber/websocket) | Based on Fasthttp WebSocket for Fiber with Locals support! | + +## 🕶️ Awesome List + +For more articles, middlewares, examples or tools check our [awesome list](https://github.com/gofiber/awesome-fiber). + +## 👍 Contribuer + +Si vous voulez nous remercier et/ou soutenir le développement actif de `Fiber`: + +1. Ajoutez une [GitHub Star](https://github.com/gofiber/fiber/stargazers) à ce projet. +2. Twittez à propos de ce projet [sur votre 𝕏 (Twitter)](https://x.com/intent/tweet?text=Fiber%20is%20an%20Express%20inspired%20%23web%20%23framework%20built%20on%20top%20of%20Fasthttp%2C%20the%20fastest%20HTTP%20engine%20for%20%23Go.%20Designed%20to%20ease%20things%20up%20for%20%23fast%20development%20with%20zero%20memory%20allocation%20and%20%23performance%20in%20mind%20%F0%9F%9A%80%20https%3A%2F%2Fgithub.com%2Fgofiber%2Ffiber). +3. Ecrivez un article (review, tutorial) sur [Medium](https://medium.com/), [Dev.to](https://dev.to/), ou encore un blog personnel. +4. Support the project by donating a [cup of coffee](https://buymeacoff.ee/fenny). + +## ☕ Supporters + +Fiber is an open source project that runs on donations to pay the bills e.g. our domain name, gitbook, netlify and serverless hosting. If you want to support Fiber, you can ☕ [**buy a coffee here**](https://buymeacoff.ee/fenny). + +| | User | Donation | +| :--------------------------------------------------------- | :----------------------------------------------- | :------- | +| ![](https://avatars.githubusercontent.com/u/204341?s=25) | [@destari](https://github.com/destari) | ☕ x 10 | +| ![](https://avatars.githubusercontent.com/u/63164982?s=25) | [@dembygenesis](https://github.com/dembygenesis) | ☕ x 5 | +| ![](https://avatars.githubusercontent.com/u/56607882?s=25) | [@thomasvvugt](https://github.com/thomasvvugt) | ☕ x 5 | +| ![](https://avatars.githubusercontent.com/u/27820675?s=25) | [@hendratommy](https://github.com/hendratommy) | ☕ x 5 | +| ![](https://avatars.githubusercontent.com/u/1094221?s=25) | [@ekaputra07](https://github.com/ekaputra07) | ☕ x 5 | +| ![](https://avatars.githubusercontent.com/u/194590?s=25) | [@jorgefuertes](https://github.com/jorgefuertes) | ☕ x 5 | +| ![](https://avatars.githubusercontent.com/u/186637?s=25) | [@candidosales](https://github.com/candidosales) | ☕ x 5 | +| ![](https://avatars.githubusercontent.com/u/29659953?s=25) | [@l0nax](https://github.com/l0nax) | ☕ x 3 | +| ![](https://avatars.githubusercontent.com/u/635852?s=25) | [@bihe](https://github.com/bihe) | ☕ x 3 | +| ![](https://avatars.githubusercontent.com/u/307334?s=25) | [@justdave](https://github.com/justdave) | ☕ x 3 | +| ![](https://avatars.githubusercontent.com/u/11155743?s=25) | [@koddr](https://github.com/koddr) | ☕ x 1 | +| ![](https://avatars.githubusercontent.com/u/29042462?s=25) | [@lapolinar](https://github.com/lapolinar) | ☕ x 1 | +| ![](https://avatars.githubusercontent.com/u/2978730?s=25) | [@diegowifi](https://github.com/diegowifi) | ☕ x 1 | +| ![](https://avatars.githubusercontent.com/u/44171355?s=25) | [@ssimk0](https://github.com/ssimk0) | ☕ x 1 | +| ![](https://avatars.githubusercontent.com/u/5638101?s=25) | [@raymayemir](https://github.com/raymayemir) | ☕ x 1 | +| ![](https://avatars.githubusercontent.com/u/619996?s=25) | [@melkorm](https://github.com/melkorm) | ☕ x 1 | +| ![](https://avatars.githubusercontent.com/u/31022056?s=25) | [@marvinjwendt](https://github.com/thomasvvugt) | ☕ x 1 | +| ![](https://avatars.githubusercontent.com/u/31921460?s=25) | [@toishy](https://github.com/toishy) | ☕ x 1 | + +## ‎‍💻 Code Contributors + +Code Contributors + +## ⭐️ Stargazers + +Stargazers over time + +## ⚠️ License + +Copyright (c) 2019-present [Fenny](https://github.com/fenny) and [Contributors](https://github.com/gofiber/fiber/graphs/contributors). `Fiber` is free and open-source software licensed under the [MIT License](https://github.com/gofiber/fiber/blob/master/LICENSE). Official logo was created by [Vic Shóstak](https://github.com/koddr) and distributed under [Creative Commons](https://creativecommons.org/licenses/by-sa/4.0/) license (CC BY-SA 4.0 International). + +**Third-party library licenses** + +- [colorable](https://github.com/mattn/go-colorable/blob/master/LICENSE) +- [isatty](https://github.com/mattn/go-isatty/blob/master/LICENSE) +- [runewidth](https://github.com/mattn/go-runewidth/blob/master/LICENSE) +- [fasthttp](https://github.com/valyala/fasthttp/blob/master/LICENSE) +- [bytebufferpool](https://github.com/valyala/bytebufferpool/blob/master/LICENSE) +- [fwd](https://github.com/philhofer/fwd/blob/master/LICENSE.md) +- [go-ole](https://github.com/go-ole/go-ole/blob/master/LICENSE) +- [gopsutil](https://github.com/shirou/gopsutil/blob/master/LICENSE) +- [msgp](https://github.com/tinylib/msgp/blob/master/LICENSE) +- [schema](https://github.com/gorilla/schema/blob/master/LICENSE) +- [uuid](https://github.com/google/uuid/blob/master/LICENSE) +- [wmi](https://github.com/StackExchange/wmi/blob/master/LICENSE) diff --git a/.github/README_he.md b/.github/README_he.md new file mode 100644 index 0000000..0ddda26 --- /dev/null +++ b/.github/README_he.md @@ -0,0 +1,865 @@ +

+ + + + + Fiber + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + +

+

+

+ +Fiber היא web framework בהשראת Express הבנויה על גבי Fasthttp, מנוע ה-HTTP המהיר ביותר עבור Go. +נועדה להקל על העניינים למען פיתוח מהיר, ללא הקצאות זכרון ולוקחת ביצועים בחשבון. + +
+

+ +
+ +## ⚡️ התחלה מהירה + +
+ +```go +package main + +import "github.com/gofiber/fiber/v2" + +func main() { + app := fiber.New() + + app.Get("/", func(c *fiber.Ctx) error { + return c.SendString("Hello, World 👋!") + }) + + app.Listen(":3000") +} +``` + +
+ +## 🤖 מדדים + +
+ +
+ +הבדיקות מבוצעות על ידי [TechEmpower](https://www.techempower.com/benchmarks/#section=data-r19&hw=ph&test=plaintext) ו-[Go Web](https://github.com/smallnest/go-web-framework-benchmark). אם אתם רוצים לראות את כל התוצאות, אנא בקרו ב-[Wiki](https://docs.gofiber.io/extra/benchmarks) שלנו. + +
+ +

+ + +

+ +
+ +## ⚙️ התקנה + +Make sure you have Go installed ([download](https://go.dev/dl/)). Version `1.17` or higher is required. + +Initialize your project by creating a folder and then running `go mod init github.com/your/repo` ([learn more](https://go.dev/blog/using-go-modules)) inside the folder. Then install Fiber with the [`go get`](https://pkg.go.dev/cmd/go/#hdr-Add_dependencies_to_current_module_and_install_them) command: + +```bash +go get -u github.com/gofiber/fiber/v2 +``` + +## 🎯 יכולות + +
+ +
+ +- [ניתוב](https://docs.gofiber.io/guide/routing) רובסטי +- הנגשת [קבצים סטטיים](https://docs.gofiber.io/api/app#static) +- [ביצועים](https://docs.gofiber.io/extra/benchmarks) גבוהים במיוחד +- צורך כמות [זכרון קטנה](https://docs.gofiber.io/extra/benchmarks) +- [נקודות קצה עבור API](https://docs.gofiber.io/api/ctx) +- תמיכה ב-[Middleware](https://docs.gofiber.io/category/-middleware) & [Next](https://docs.gofiber.io/api/ctx#next) +- תכנות [מהיר](https://dev.to/koddr/welcome-to-fiber-an-express-js-styled-fastest-web-framework-written-with-on-golang-497) של צד שרת +- [מנועי תבניות](https://docs.gofiber.io/category/-middleware#template) +- [תמיכה ב-WebSocket](https://github.com/gofiber/websocket) +- [Server-Sent events](https://github.com/gofiber/recipes/tree/master/sse) +- [הגבלת קצבים ובקשות](https://docs.gofiber.io/api/middleware/limiter) +- Available in [12 languages](https://docs.gofiber.io/) +- והרבה יותר, [חקור את Fiber](https://docs.gofiber.io/) +
+ +
+ +## 💡 פילוסופיה + +
+ +
+ +gophers חדשים שעושים את המעבר מ-[Node.js](https://nodejs.org/en/about/) ל-[Go](https://go.dev/doc/) מתמודדים עם עקומת למידה לפני שהם יכולים להתחיל לבנות את יישומי האינטרנט או המיקרו-שירותים שלהם. +Fiber כ-**web framework**, נוצרה עם רעיון **המינימליזם** ועוקבת אחרי **הדרך של UNIX**, כך ש-gophers חדשים יוכלו להיכנס במהירות לעולם של Go עם קבלת פנים חמה ואמינה. + +
+ +
+ +Fiber נוצרה **בהשראת** Express, ה-web framework הפופולרית ביותר ברחבי האינטרנט. שילבנו את **הקלות** של Express ו**הביצועים הגולמיים** של Go. אם אי-פעם מימשתם יישום web ב-Node.js (_באמצעות Express או דומיו_), אז הרבה מהפונקציות והעקרונות ייראו לכם **מאוד מוכרים**. + +
+ +
+ +אנחנו **מקשיבים** למשתמשים שלנו ב-[issues](https://github.com/gofiber/fiber/issues) (_ובכל רחבי האינטרנט_) כדי ליצור web framework **מהירה**, **גמישה**, ו**ידידותית** בשפת Go עבור **כל** משימה, **תאריך יעד** ו**כישורי** מפתח! בדיוק כמו ש-Express מבצע בעולם של JavaScript. + +
+ +
+ +## ⚠️ Limitations + +- Due to Fiber's usage of unsafe, the library may not always be compatible with the latest Go version. Fiber 2.40.0 has been tested with Go versions 1.17 to 1.21. +- Fiber is not compatible with net/http interfaces. This means you will not be able to use projects like gqlgen, go-swagger, or any others which are part of the net/http ecosystem. + +## 👀 דוגמאות + +
+ +
+ +להלן כמה מהדוגמאות הנפוצות. אם ברצונכם לראות דוגמאות קוד נוספות, אנא בקרו ב[מאגר המתכונים](https://github.com/gofiber/recipes) שלנו או בקרו ב[תיעוד ה-API](https://docs.gofiber.io) שלנו. + +
+ +
+ +#### 📖 [**Basic Routing**](https://docs.gofiber.io/#basic-routing) + +
+ +```go +func main() { + app := fiber.New() + + // GET /api/register + app.Get("/api/*", func(c *fiber.Ctx) error { + msg := fmt.Sprintf("✋ %s", c.Params("*")) + return c.SendString(msg) // => ✋ register + }) + + // GET /flights/LAX-SFO + app.Get("/flights/:from-:to", func(c *fiber.Ctx) error { + msg := fmt.Sprintf("💸 From: %s, To: %s", c.Params("from"), c.Params("to")) + return c.SendString(msg) // => 💸 From: LAX, To: SFO + }) + + // GET /dictionary.txt + app.Get("/:file.:ext", func(c *fiber.Ctx) error { + msg := fmt.Sprintf("📃 %s.%s", c.Params("file"), c.Params("ext")) + return c.SendString(msg) // => 📃 dictionary.txt + }) + + // GET /john/75 + app.Get("/:name/:age/:gender?", func(c *fiber.Ctx) error { + msg := fmt.Sprintf("👴 %s is %s years old", c.Params("name"), c.Params("age")) + return c.SendString(msg) // => 👴 john is 75 years old + }) + + // GET /john + app.Get("/:name", func(c *fiber.Ctx) error { + msg := fmt.Sprintf("Hello, %s 👋!", c.Params("name")) + return c.SendString(msg) // => Hello john 👋! + }) + + log.Fatal(app.Listen(":3000")) +} + +``` + +
+ +#### 📖 [**Route Naming**](https://docs.gofiber.io/api/app#name) + +
+ +```go +func main() { + app := fiber.New() + + // GET /api/register + app.Get("/api/*", func(c *fiber.Ctx) error { + msg := fmt.Sprintf("✋ %s", c.Params("*")) + return c.SendString(msg) // => ✋ register + }).Name("api") + + data, _ := json.MarshalIndent(app.GetRoute("api"), "", " ") + fmt.Print(string(data)) + // Prints: + // { + // "method": "GET", + // "name": "api", + // "path": "/api/*", + // "params": [ + // "*1" + // ] + // } + + + log.Fatal(app.Listen(":3000")) +} + +``` + +
+ +#### 📖 [**Serving Static Files**](https://docs.gofiber.io/api/app#static) + +
+ +```go +func main() { + app := fiber.New() + + app.Static("/", "./public") + // => http://localhost:3000/js/script.js + // => http://localhost:3000/css/style.css + + app.Static("/prefix", "./public") + // => http://localhost:3000/prefix/js/script.js + // => http://localhost:3000/prefix/css/style.css + + app.Static("*", "./public/index.html") + // => http://localhost:3000/any/path/shows/index/html + + log.Fatal(app.Listen(":3000")) +} + +``` + +
+ +#### 📖 [**Middleware & Next**](https://docs.gofiber.io/api/ctx#next) + +
+ +```go +func main() { + app := fiber.New() + + // Match any route + app.Use(func(c *fiber.Ctx) error { + fmt.Println("🥇 First handler") + return c.Next() + }) + + // Match all routes starting with /api + app.Use("/api", func(c *fiber.Ctx) error { + fmt.Println("🥈 Second handler") + return c.Next() + }) + + // GET /api/register + app.Get("/api/list", func(c *fiber.Ctx) error { + fmt.Println("🥉 Last handler") + return c.SendString("Hello, World 👋!") + }) + + log.Fatal(app.Listen(":3000")) +} + +``` + +
+
+ 📚 הצג דוגמאות קוד נוספות + +### Views engines + +📖 [Config](https://docs.gofiber.io/api/fiber#config) +📖 [Engines](https://github.com/gofiber/template) +📖 [Render](https://docs.gofiber.io/api/ctx#render) + +Fiber defaults to the [html/template](https://pkg.go.dev/html/template/) when no view engine is set. + +If you want to execute partials or use a different engine like [amber](https://github.com/eknkc/amber), [handlebars](https://github.com/aymerick/raymond), [mustache](https://github.com/cbroglie/mustache) or [pug](https://github.com/Joker/jade) etc.. + +Checkout our [Template](https://github.com/gofiber/template) package that support multiple view engines. + +
+ +```go +package main + +import ( + "github.com/gofiber/fiber/v2" + "github.com/gofiber/template/pug" +) + +func main() { + // You can setup Views engine before initiation app: + app := fiber.New(fiber.Config{ + Views: pug.New("./views", ".pug"), + }) + + // And now, you can call template `./views/home.pug` like this: + app.Get("/", func(c *fiber.Ctx) error { + return c.Render("home", fiber.Map{ + "title": "Homepage", + "year": 1999, + }) + }) + + log.Fatal(app.Listen(":3000")) +} +``` + +
+ +### קיבוץ routes ל-chains + +📖 [קבוצות](https://docs.gofiber.io/api/app#group) + +
+ +```go +func middleware(c *fiber.Ctx) error { + fmt.Println("Don't mind me!") + return c.Next() +} + +func handler(c *fiber.Ctx) error { + return c.SendString(c.Path()) +} + +func main() { + app := fiber.New() + + // Root API route + api := app.Group("/api", middleware) // /api + + // API v1 routes + v1 := api.Group("/v1", middleware) // /api/v1 + v1.Get("/list", handler) // /api/v1/list + v1.Get("/user", handler) // /api/v1/user + + // API v2 routes + v2 := api.Group("/v2", middleware) // /api/v2 + v2.Get("/list", handler) // /api/v2/list + v2.Get("/user", handler) // /api/v2/user + + // ... +} + +``` + +
+ +### Middleware של לוגים + +📖 [Logger](https://docs.gofiber.io/api/middleware/logger) + +
+ +```go +package main + +import ( + "log" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/logger" +) + +func main() { + app := fiber.New() + + app.Use(logger.New()) + + // ... + + log.Fatal(app.Listen(":3000")) +} +``` + +
+ +### שיתוף משאבים בין מקורות (CORS) + +📖 [CORS](https://docs.gofiber.io/api/middleware/cors) + +
+ +```go +import ( + "log" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/cors" +) + +func main() { + app := fiber.New() + + app.Use(cors.New()) + + // ... + + log.Fatal(app.Listen(":3000")) +} +``` + +
+ +בדוק את ה-CORS על ידי העברת כל domain ב-header של `Origin`: + +
+ +```bash +curl -H "Origin: http://example.com" --verbose http://localhost:3000 +``` + +
+ +### תגובת 404 מותאמת אישית + +📖 [שיטות HTTP](https://docs.gofiber.io/api/ctx#status) + +
+ +```go +func main() { + app := fiber.New() + + app.Static("/", "./public") + + app.Get("/demo", func(c *fiber.Ctx) error { + return c.SendString("This is a demo!") + }) + + app.Post("/register", func(c *fiber.Ctx) error { + return c.SendString("Welcome!") + }) + + // Last middleware to match anything + app.Use(func(c *fiber.Ctx) error { + return c.SendStatus(404) + // => 404 "Not Found" + }) + + log.Fatal(app.Listen(":3000")) +} +``` + +
+ +### תגובת JSON + +📖 [JSON](https://docs.gofiber.io/api/ctx#json) + +
+ +```go +type User struct { + Name string `json:"name"` + Age int `json:"age"` +} + +func main() { + app := fiber.New() + + app.Get("/user", func(c *fiber.Ctx) error { + return c.JSON(&User{"John", 20}) + // => {"name":"John", "age":20} + }) + + app.Get("/json", func(c *fiber.Ctx) error { + return c.JSON(fiber.Map{ + "success": true, + "message": "Hi John!", + }) + // => {"success":true, "message":"Hi John!"} + }) + + log.Fatal(app.Listen(":3000")) +} +``` + +
+ +### WebSocket Upgrade + +📖 [Websocket](https://github.com/gofiber/websocket) + +
+ +```go +import ( + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/websocket" +) + +func main() { + app := fiber.New() + + app.Get("/ws", websocket.New(func(c *websocket.Conn) { + for { + mt, msg, err := c.ReadMessage() + if err != nil { + log.Println("read:", err) + break + } + log.Printf("recv: %s", msg) + err = c.WriteMessage(mt, msg) + if err != nil { + log.Println("write:", err) + break + } + } + })) + + log.Fatal(app.Listen(":3000")) + // ws://localhost:3000/ws +} +``` + +
+ +### Server-Sent Events + +📖 [More Info](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events) + +
+ +```go +import ( + "github.com/gofiber/fiber/v2" + "github.com/valyala/fasthttp" +) + +func main() { + app := fiber.New() + + app.Get("/sse", func(c *fiber.Ctx) error { + c.Set("Content-Type", "text/event-stream") + c.Set("Cache-Control", "no-cache") + c.Set("Connection", "keep-alive") + c.Set("Transfer-Encoding", "chunked") + + c.Context().SetBodyStreamWriter(fasthttp.StreamWriter(func(w *bufio.Writer) { + fmt.Println("WRITER") + var i int + + for { + i++ + msg := fmt.Sprintf("%d - the time is %v", i, time.Now()) + fmt.Fprintf(w, "data: Message: %s\n\n", msg) + fmt.Println(msg) + + w.Flush() + time.Sleep(5 * time.Second) + } + })) + + return nil + }) + + log.Fatal(app.Listen(":3000")) +} +``` + +
+ +### Middleware של התאוששות + +📖 [התאוששות](https://docs.gofiber.io/api/middleware/recover) + +
+ +```go +import ( + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/recover" +) + +func main() { + app := fiber.New() + + app.Use(recover.New()) + + app.Get("/", func(c *fiber.Ctx) error { + panic("normally this would crash your app") + }) + + log.Fatal(app.Listen(":3000")) +} +``` + +
+
+
+ +
+ +## 🧬 Internal Middleware + +
+ +
+ +Here is a list of middleware that are included within the Fiber framework. + +
+ +
+ +| Middleware | Description | +| :------------------------------------------------------------------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [basicauth](https://github.com/gofiber/fiber/tree/master/middleware/basicauth) | Basic auth middleware provides an HTTP basic authentication. It calls the next handler for valid credentials and 401 Unauthorized for missing or invalid credentials. | +| [cache](https://github.com/gofiber/fiber/tree/master/middleware/cache) | Intercept and cache responses | +| [compress](https://github.com/gofiber/fiber/tree/master/middleware/compress) | Compression middleware for Fiber, it supports `deflate`, `gzip` and `brotli` by default. | +| [cors](https://github.com/gofiber/fiber/tree/master/middleware/cors) | Enable cross-origin resource sharing \(CORS\) with various options. | +| [csrf](https://github.com/gofiber/fiber/tree/master/middleware/csrf) | Protect from CSRF exploits. | +| [encryptcookie](https://github.com/gofiber/fiber/tree/master/middleware/encryptcookie) | Encrypt middleware which encrypts cookie values. | +| [envvar](https://github.com/gofiber/fiber/tree/master/middleware/envvar) | Expose environment variables with providing an optional config. | +| [etag](https://github.com/gofiber/fiber/tree/master/middleware/etag) | ETag middleware that lets caches be more efficient and save bandwidth, as a web server does not need to resend a full response if the content has not changed. | +| [expvar](https://github.com/gofiber/fiber/tree/master/middleware/expvar) | Expvar middleware that serves via its HTTP server runtime exposed variants in the JSON format. | +| [favicon](https://github.com/gofiber/fiber/tree/master/middleware/favicon) | Ignore favicon from logs or serve from memory if a file path is provided. | +| [filesystem](https://github.com/gofiber/fiber/tree/master/middleware/filesystem) | FileSystem middleware for Fiber, special thanks and credits to Alireza Salary | +| [limiter](https://github.com/gofiber/fiber/tree/master/middleware/limiter) | Rate-limiting middleware for Fiber. Use to limit repeated requests to public APIs and/or endpoints such as password reset. | +| [logger](https://github.com/gofiber/fiber/tree/master/middleware/logger) | HTTP request/response logger. | +| [monitor](https://github.com/gofiber/fiber/tree/master/middleware/monitor) | Monitor middleware that reports server metrics, inspired by express-status-monitor | +| [pprof](https://github.com/gofiber/fiber/tree/master/middleware/pprof) | Special thanks to Matthew Lee \(@mthli\) | +| [proxy](https://github.com/gofiber/fiber/tree/master/middleware/proxy) | Allows you to proxy requests to a multiple servers | +| [recover](https://github.com/gofiber/fiber/tree/master/middleware/recover) | Recover middleware recovers from panics anywhere in the stack chain and handles the control to the centralized[ ErrorHandler](https://docs.gofiber.io/guide/error-handling). | +| [requestid](https://github.com/gofiber/fiber/tree/master/middleware/requestid) | Adds a requestid to every request. | +| [session](https://github.com/gofiber/fiber/tree/master/middleware/session) | Session middleware. NOTE: This middleware uses our Storage package. | +| [skip](https://github.com/gofiber/fiber/tree/master/middleware/skip) | Skip middleware that skips a wrapped handler is a predicate is true. | +| [timeout](https://github.com/gofiber/fiber/tree/master/middleware/timeout) | Adds a max time for a request and forwards to ErrorHandler if it is exceeded. | +| [keyauth](https://github.com/gofiber/keyauth) | Key auth middleware provides a key based authentication. | +| [redirect](https://github.com/gofiber/redirect) | Redirect middleware | +| [rewrite](https://github.com/gofiber/rewrite) | Rewrite middleware rewrites the URL path based on provided rules. It can be helpful for backward compatibility or just creating cleaner and more descriptive links. | +| [adaptor](https://github.com/gofiber/adaptor) | Converter for net/http handlers to/from Fiber request handlers, special thanks to @arsmn! | +| [helmet](https://github.com/gofiber/helmet) | Helps secure your apps by setting various HTTP headers. | + +
+ +
+ +## 🧬 External Middleware + +
+ +
+ +Here is a list of middleware that are included within the Fiber framework. + +
+ +
+ +| Middleware | Description | +| :------------------------------------------------ | :-------------------------------------------------------------------------------------------------------------------- | +| [jwt](https://github.com/gofiber/jwt) | JWT returns a JSON Web Token \(JWT\) auth middleware. | +| [storage](https://github.com/gofiber/storage) | Premade storage drivers that implement the Storage interface, designed to be used with various Fiber middlewares. | +| [template](https://github.com/gofiber/template) | This package contains 8 template engines that can be used with Fiber `v1.10.x` Go version 1.13 or higher is required. | +| [websocket](https://github.com/gofiber/websocket) | Based on Fasthttp WebSocket for Fiber with Locals support! | + +
+ +
+ +## 🕶️ Awesome List + +
+ +
+ +For more articles, middlewares, examples or tools check our [awesome list](https://github.com/gofiber/awesome-fiber). + +
+ +
+ +## 👍 לתרום + +
+ +
+ +אם אתם רוצים לומר **תודה** או/ו לתמוך בפיתוח הפעיל של `Fiber`: + +
+ +
+ +1. תוסיפו [GitHub Star](https://github.com/gofiber/fiber/stargazers) לפרויקט. +2. צייצו לגבי הפרויקט [בטוויטר שלכם](https://x.com/intent/tweet?text=Fiber%20is%20an%20Express%20inspired%20%23web%20%23framework%20built%20on%20top%20of%20Fasthttp%2C%20the%20fastest%20HTTP%20engine%20for%20%23Go.%20Designed%20to%20ease%20things%20up%20for%20%23fast%20development%20with%20zero%20memory%20allocation%20and%20%23performance%20in%20mind%20%F0%9F%9A%80%20https%3A%2F%2Fgithub.com%2Fgofiber%2Ffiber). +3. כתבו ביקורת או מדריך ב-[Medium](https://medium.com/), [Dev.to](https://dev.to/) או בבלוג האישי שלכם. +4. תמכו בפרויקט על ידי תרומת [כוס קפה](https://buymeacoff.ee/fenny). +
+ +
+ +## ☕ תומכים + +
+ +
+ +Fiber היא פרויקט קוד פתוח שתשלום חשובונתיו מסתמך על תרומות, כגון שם ה-domain שלנו, gitbook, netlify ו-serverless hosting. אם אתם רוצים לתמוך ב-Fiber, אתם יכולים ☕ [**קנו קפה כאן**](https://buymeacoff.ee/fenny). + +
+ +| | משתמש | תרומה | +| :--------------------------------------------------------- | :----------------------------------------------- | :------ | +| ![](https://avatars.githubusercontent.com/u/204341?s=25) | [@destari](https://github.com/destari) | ☕ x 10 | +| ![](https://avatars.githubusercontent.com/u/63164982?s=25) | [@dembygenesis](https://github.com/dembygenesis) | ☕ x 5 | +| ![](https://avatars.githubusercontent.com/u/56607882?s=25) | [@thomasvvugt](https://github.com/thomasvvugt) | ☕ x 5 | +| ![](https://avatars.githubusercontent.com/u/27820675?s=25) | [@hendratommy](https://github.com/hendratommy) | ☕ x 5 | +| ![](https://avatars.githubusercontent.com/u/1094221?s=25) | [@ekaputra07](https://github.com/ekaputra07) | ☕ x 5 | +| ![](https://avatars.githubusercontent.com/u/194590?s=25) | [@jorgefuertes](https://github.com/jorgefuertes) | ☕ x 5 | +| ![](https://avatars.githubusercontent.com/u/186637?s=25) | [@candidosales](https://github.com/candidosales) | ☕ x 5 | +| ![](https://avatars.githubusercontent.com/u/29659953?s=25) | [@l0nax](https://github.com/l0nax) | ☕ x 3 | +| ![](https://avatars.githubusercontent.com/u/635852?s=25) | [@bihe](https://github.com/bihe) | ☕ x 3 | +| ![](https://avatars.githubusercontent.com/u/307334?s=25) | [@justdave](https://github.com/justdave) | ☕ x 3 | +| ![](https://avatars.githubusercontent.com/u/11155743?s=25) | [@koddr](https://github.com/koddr) | ☕ x 1 | +| ![](https://avatars.githubusercontent.com/u/29042462?s=25) | [@lapolinar](https://github.com/lapolinar) | ☕ x 1 | +| ![](https://avatars.githubusercontent.com/u/2978730?s=25) | [@diegowifi](https://github.com/diegowifi) | ☕ x 1 | +| ![](https://avatars.githubusercontent.com/u/44171355?s=25) | [@ssimk0](https://github.com/ssimk0) | ☕ x 1 | +| ![](https://avatars.githubusercontent.com/u/5638101?s=25) | [@raymayemir](https://github.com/raymayemir) | ☕ x 1 | +| ![](https://avatars.githubusercontent.com/u/619996?s=25) | [@melkorm](https://github.com/melkorm) | ☕ x 1 | +| ![](https://avatars.githubusercontent.com/u/31022056?s=25) | [@marvinjwendt](https://github.com/thomasvvugt) | ☕ x 1 | +| ![](https://avatars.githubusercontent.com/u/31921460?s=25) | [@toishy](https://github.com/toishy) | ☕ x 1 | + +
+ +## ‎‍💻 תורמי קוד + +
+ +Code Contributors + +
+ +## ⭐️ Stargazers + +
+ +Stargazers over time + +
+ +## ⚠️ רישיון + +
+ +
+ +Copyright (c) 2019-present [Fenny](https://github.com/fenny) and [Contributors](https://github.com/gofiber/fiber/graphs/contributors). `Fiber` is free and open-source software licensed under the [MIT License](https://github.com/gofiber/fiber/blob/master/LICENSE). Official logo was created by [Vic Shóstak](https://github.com/koddr) and distributed under [Creative Commons](https://creativecommons.org/licenses/by-sa/4.0/) license (CC BY-SA 4.0 International). + +
+ +
+ +**רישיונות של ספריות צד שלישי** + +- [colorable](https://github.com/mattn/go-colorable/blob/master/LICENSE) +- [isatty](https://github.com/mattn/go-isatty/blob/master/LICENSE) +- [runewidth](https://github.com/mattn/go-runewidth/blob/master/LICENSE) +- [fasthttp](https://github.com/valyala/fasthttp/blob/master/LICENSE) +- [bytebufferpool](https://github.com/valyala/bytebufferpool/blob/master/LICENSE) +- [fwd](https://github.com/philhofer/fwd/blob/master/LICENSE.md) +- [go-ole](https://github.com/go-ole/go-ole/blob/master/LICENSE) +- [gopsutil](https://github.com/shirou/gopsutil/blob/master/LICENSE) +- [msgp](https://github.com/tinylib/msgp/blob/master/LICENSE) +- [schema](https://github.com/gorilla/schema/blob/master/LICENSE) +- [uuid](https://github.com/google/uuid/blob/master/LICENSE) +- [wmi](https://github.com/StackExchange/wmi/blob/master/LICENSE) +
diff --git a/.github/README_id.md b/.github/README_id.md new file mode 100644 index 0000000..658e86a --- /dev/null +++ b/.github/README_id.md @@ -0,0 +1,689 @@ +

+ + + + + Fiber + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + +

+

+ Fiber adalah kerangka kerja web yang terinspirasi dari Express yang berbasiskan Fasthttp, HTTP engine paling cepat untuk Go. Dirancang untuk mempermudah, mempercepat pengembangan aplikasi dengan alokasi memori nol-nya serta kinerja yang selalu diperhatikan. +

+ +## ⚡️ Cara Memulai + +```go +package main + +import "github.com/gofiber/fiber/v2" + +func main() { + app := fiber.New() + + app.Get("/", func(c *fiber.Ctx) error { + return c.SendString("Hello, World 👋!") + }) + + app.Listen(":3000") +} +``` + +## 🤖 Pengukuran Kinerja + +Pengukuran ini dilakukan oleh [TechEmpower](https://www.techempower.com/benchmarks/#section=data-r19&hw=ph&test=plaintext) dan [Go Web](https://github.com/smallnest/go-web-framework-benchmark). Apabila anda ingin melihat hasil lengkapnya, silahkan kunjungi halaman [Wiki](https://docs.gofiber.io/extra/benchmarks) kami. + +

+ + +

+ +## ⚙️ Instalasi + +Pastikan kamu sudah menginstalasi Golang ([unduh](https://go.dev/dl/)). Dengan versi `1.17` atau lebih tinggi [ Direkomendasikan ]. + +Inisialisasi proyek kamu dengan membuat folder lalu jalankan `go mod init github.com/nama-kamu/repo` ([belajar lebih banyak](https://go.dev/blog/using-go-modules)) di dalam folder. Kemudian instal Fiber dengan perintah [`go get`](https://pkg.go.dev/cmd/go/#hdr-Add_dependencies_to_current_module_and_install_them): + +```bash +go get -u github.com/gofiber/fiber/v2 +``` + +## 🎯 Fitur + +- Sistem [Routing](https://docs.gofiber.io/guide/routing) yang padu +- Menyajikan [file statis](https://docs.gofiber.io/api/app#static) +- [Kinerja](https://docs.gofiber.io/extra/benchmarks) ekstrim +- [Penggunaan memori](https://docs.gofiber.io/extra/benchmarks) yang kecil +- Cocok untuk [API](https://docs.gofiber.io/api/ctx) +- Mendukung Middleware & [Next](https://docs.gofiber.io/api/ctx#next) seperti Express +- Kembangkan aplikasi dengan [Cepat](https://dev.to/koddr/welcome-to-fiber-an-express-js-styled-fastest-web-framework-written-with-on-golang-497) +- [Template engines](https://github.com/gofiber/template) +- [Mendukung WebSocket](https://github.com/gofiber/websocket) +- [Server-Sent events](https://github.com/gofiber/recipes/tree/master/sse) +- [Rate Limiter](https://docs.gofiber.io/api/middleware/limiter) +- Tersedia dalam [19 bahasa](https://docs.gofiber.io/) +- Dan masih banyak lagi, [kunjungi Fiber](https://docs.gofiber.io/) + +## 💡 Filosofi + +Bagi yang baru yang beralih dari [Node.js](https://nodejs.org/en/about/) ke [Go](https://go.dev/doc/) terkadang perlu waktu yang cukup lama sebelum mereka mampu membuat aplikasi web dengan Go. Fiber, sebagai **kerangka kerja web** dirancang secara **minimalis** dan mengikuti filosofi dari **UNIX**, sehingga pengguna baru dapat dengan cepat memasuki dunia Go dengan sambutan yang hangat dan dapat diandalkan. + +Fiber terinspirasi dari Express, salah satu kerangka kerja web yang paling terkenal di Internet. Kami menggabungkan **kemudahan** dari Express dan **kinerja luar biasa** dari Go. Apabila anda pernah membuat aplikasi dengan Node.js (_dengan Express atau yang lainnya_), maka banyak metode dan prinsip yang akan terasa **sangat umum** bagi anda. + +Kami **mendengarkan** para pengguna di [GitHub Issues](https://github.com/gofiber/fiber/issues), Discord [channel](https://gofiber.io/discord), _dan berbagai platform lainnya_ untuk menciptakan kerangka kerja web yang **cepat**, **fleksibel** dan **bersahabat** untuk berbagai macam keperluan, **tenggat waktu** dan **keahlian** para pengguna! Sama halnya seperti yang dilakukan Express di dunia JavaScript. + +## ⚠️ Limitasi + +- Karena penggunaan Fiber yang tidak aman, perpustakaan mungkin tidak selalu kompatibel dengan versi Go terbaru. Fiber 2.40.0 telah diuji dengan Go versi 1.17 hingga 1.21. +- Fiber tidak kompatibel dengan antarmuka net/http. Ini berarti kamu tidak akan dapat menggunakan proyek seperti gqlgen, go-swagger, atau lainnya yang merupakan bagian dari ekosistem net/http. + +## 👀 Contoh + +Dibawah ini terdapat beberapa contoh penggunaan. Jika anda ingin melihat contoh lainnya, silahkan kunjungi [Gudang resep](https://github.com/gofiber/recipes) atau kunjungi [Dokumentasi API](https://docs.gofiber.io) kami. + +#### 📖 [**Routing Dasar**](https://docs.gofiber.io/#basic-routing) + +```go +func main() { + app := fiber.New() + + // GET /api/register + app.Get("/api/*", func(c *fiber.Ctx) error { + msg := fmt.Sprintf("✋ %s", c.Params("*")) + return c.SendString(msg) // => ✋ register + }) + + // GET /flights/LAX-SFO + app.Get("/flights/:from-:to", func(c *fiber.Ctx) error { + msg := fmt.Sprintf("💸 From: %s, To: %s", c.Params("from"), c.Params("to")) + return c.SendString(msg) // => 💸 From: LAX, To: SFO + }) + + // GET /dictionary.txt + app.Get("/:file.:ext", func(c *fiber.Ctx) error { + msg := fmt.Sprintf("📃 %s.%s", c.Params("file"), c.Params("ext")) + return c.SendString(msg) // => 📃 dictionary.txt + }) + + // GET /john/75 + app.Get("/:name/:age/:gender?", func(c *fiber.Ctx) error { + msg := fmt.Sprintf("👴 %s is %s years old", c.Params("name"), c.Params("age")) + return c.SendString(msg) // => 👴 john is 75 years old + }) + + // GET /john + app.Get("/:name", func(c *fiber.Ctx) error { + msg := fmt.Sprintf("Hello, %s 👋!", c.Params("name")) + return c.SendString(msg) // => Hello john 👋! + }) + + log.Fatal(app.Listen(":3000")) +} + +``` + +#### 📖 [**Penamaan Route**](https://docs.gofiber.io/api/app#name) + +```go +func main() { + app := fiber.New() + + // GET /api/register + app.Get("/api/*", func(c *fiber.Ctx) error { + msg := fmt.Sprintf("✋ %s", c.Params("*")) + return c.SendString(msg) // => ✋ register + }).Name("api") + + data, _ := json.MarshalIndent(app.GetRoute("api"), "", " ") + fmt.Print(string(data)) + // Prints: + // { + // "method": "GET", + // "name": "api", + // "path": "/api/*", + // "params": [ + // "*1" + // ] + // } + + + log.Fatal(app.Listen(":3000")) +} + +``` + +#### 📖 [**Menyajikan File Statis**](https://docs.gofiber.io/api/app#static) + +```go +func main() { + app := fiber.New() + + app.Static("/", "./public") + // => http://localhost:3000/js/script.js + // => http://localhost:3000/css/style.css + + app.Static("/prefix", "./public") + // => http://localhost:3000/prefix/js/script.js + // => http://localhost:3000/prefix/css/style.css + + app.Static("*", "./public/index.html") + // => http://localhost:3000/any/path/shows/index/html + + log.Fatal(app.Listen(":3000")) +} + +``` + +#### 📖 [**Middleware & Next**](https://docs.gofiber.io/api/ctx#next) + +```go +func main() { + app := fiber.New() + + // Match any route + app.Use(func(c *fiber.Ctx) error { + fmt.Println("🥇 First handler") + return c.Next() + }) + + // Match all routes starting with /api + app.Use("/api", func(c *fiber.Ctx) error { + fmt.Println("🥈 Second handler") + return c.Next() + }) + + // GET /api/register + app.Get("/api/list", func(c *fiber.Ctx) error { + fmt.Println("🥉 Last handler") + return c.SendString("Hello, World 👋!") + }) + + log.Fatal(app.Listen(":3000")) +} + +``` + +
+ 📚 Tampilkan lebih banyak contoh kode + +### Views engines + +📖 [Config](https://docs.gofiber.io/api/fiber#config) +📖 [Engines](https://github.com/gofiber/template) +📖 [Render](https://docs.gofiber.io/api/ctx#render) + +Fiber mengalihkan ke [html/template](https://pkg.go.dev/html/template/) saat tidak ada mesin tampilan yang disetel. + +Jika kamu ingin menjalankan sebagian atau menggunakan mesin yang berbeda seperti [amber](https://github.com/eknkc/amber), [handlebars](https://github.com/aymerick/raymond), [mustache](https://github.com/cbroglie/mustache) atau [pug](https://github.com/Joker/jade) dan lain-lain.. + +Lihat paket [contoh](https://github.com/gofiber/template) kami yang mendukung beberapa mesin tampilan. + +```go +package main + +import ( + "github.com/gofiber/fiber/v2" + "github.com/gofiber/template/pug" +) + +func main() { + // You can setup Views engine before initiation app: + app := fiber.New(fiber.Config{ + Views: pug.New("./views", ".pug"), + }) + + // And now, you can call template `./views/home.pug` like this: + app.Get("/", func(c *fiber.Ctx) error { + return c.Render("home", fiber.Map{ + "title": "Homepage", + "year": 1999, + }) + }) + + log.Fatal(app.Listen(":3000")) +} +``` + +### Grouping routes into chains + +📖 [Group](https://docs.gofiber.io/api/app#group) + +```go +func middleware(c *fiber.Ctx) error { + fmt.Println("Don't mind me!") + return c.Next() +} + +func handler(c *fiber.Ctx) error { + return c.SendString(c.Path()) +} + +func main() { + app := fiber.New() + + // Root API route + api := app.Group("/api", middleware) // /api + + // API v1 routes + v1 := api.Group("/v1", middleware) // /api/v1 + v1.Get("/list", handler) // /api/v1/list + v1.Get("/user", handler) // /api/v1/user + + // API v2 routes + v2 := api.Group("/v2", middleware) // /api/v2 + v2.Get("/list", handler) // /api/v2/list + v2.Get("/user", handler) // /api/v2/user + + // ... +} + +``` + +### Middleware logger + +📖 [Logger](https://docs.gofiber.io/api/middleware/logger) + +```go +package main + +import ( + "log" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/logger" +) + +func main() { + app := fiber.New() + + app.Use(logger.New()) + + // ... + + log.Fatal(app.Listen(":3000")) +} +``` + +### Cross-Origin Resource Sharing (CORS) + +📖 [CORS](https://docs.gofiber.io/api/middleware/cors) + +```go +import ( + "log" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/cors" +) + +func main() { + app := fiber.New() + + app.Use(cors.New()) + + // ... + + log.Fatal(app.Listen(":3000")) +} +``` + +Periksa CORS dengan meneruskan domain apa pun di `Origin` header: + +```bash +curl -H "Origin: http://example.com" --verbose http://localhost:3000 +``` + +### Custom 404 response + +📖 [HTTP Methods](https://docs.gofiber.io/api/ctx#status) + +```go +func main() { + app := fiber.New() + + app.Static("/", "./public") + + app.Get("/demo", func(c *fiber.Ctx) error { + return c.SendString("This is a demo!") + }) + + app.Post("/register", func(c *fiber.Ctx) error { + return c.SendString("Welcome!") + }) + + // Last middleware to match anything + app.Use(func(c *fiber.Ctx) error { + return c.SendStatus(404) + // => 404 "Not Found" + }) + + log.Fatal(app.Listen(":3000")) +} +``` + +### JSON Response + +📖 [JSON](https://docs.gofiber.io/api/ctx#json) + +```go +type User struct { + Name string `json:"name"` + Age int `json:"age"` +} + +func main() { + app := fiber.New() + + app.Get("/user", func(c *fiber.Ctx) error { + return c.JSON(&User{"John", 20}) + // => {"name":"John", "age":20} + }) + + app.Get("/json", func(c *fiber.Ctx) error { + return c.JSON(fiber.Map{ + "success": true, + "message": "Hi John!", + }) + // => {"success":true, "message":"Hi John!"} + }) + + log.Fatal(app.Listen(":3000")) +} +``` + +### WebSocket Upgrade + +📖 [Websocket](https://github.com/gofiber/websocket) + +```go +import ( + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/websocket" +) + +func main() { + app := fiber.New() + + app.Get("/ws", websocket.New(func(c *websocket.Conn) { + for { + mt, msg, err := c.ReadMessage() + if err != nil { + log.Println("read:", err) + break + } + log.Printf("recv: %s", msg) + err = c.WriteMessage(mt, msg) + if err != nil { + log.Println("write:", err) + break + } + } + })) + + log.Fatal(app.Listen(":3000")) + // ws://localhost:3000/ws +} +``` + +### Server-Sent Events + +📖 [Informasi lebih lanjut](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events) + +```go +import ( + "github.com/gofiber/fiber/v2" + "github.com/valyala/fasthttp" +) + +func main() { + app := fiber.New() + + app.Get("/sse", func(c *fiber.Ctx) error { + c.Set("Content-Type", "text/event-stream") + c.Set("Cache-Control", "no-cache") + c.Set("Connection", "keep-alive") + c.Set("Transfer-Encoding", "chunked") + + c.Context().SetBodyStreamWriter(fasthttp.StreamWriter(func(w *bufio.Writer) { + fmt.Println("WRITER") + var i int + + for { + i++ + msg := fmt.Sprintf("%d - the time is %v", i, time.Now()) + fmt.Fprintf(w, "data: Message: %s\n\n", msg) + fmt.Println(msg) + + w.Flush() + time.Sleep(5 * time.Second) + } + })) + + return nil + }) + + log.Fatal(app.Listen(":3000")) +} +``` + +### Recover middleware + +📖 [Recover](https://docs.gofiber.io/api/middleware/recover) + +```go +import ( + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/recover" +) + +func main() { + app := fiber.New() + + app.Use(recover.New()) + + app.Get("/", func(c *fiber.Ctx) error { + panic("normally this would crash your app") + }) + + log.Fatal(app.Listen(":3000")) +} +``` + +
+ +## 🧬 Middleware Internal + +Kumpulan `middleware` yang ada didalam kerangka kerja Fiber. + +| Middleware | Description | +| :------------------------------------------------------------------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [basicauth](https://github.com/gofiber/fiber/tree/master/middleware/basicauth) | Basic auth middleware provides an HTTP basic authentication. It calls the next handler for valid credentials and 401 Unauthorized for missing or invalid credentials. | +| [cache](https://github.com/gofiber/fiber/tree/master/middleware/cache) | Intercept and cache responses | +| [compress](https://github.com/gofiber/fiber/tree/master/middleware/compress) | Compression middleware for Fiber, it supports `deflate`, `gzip` and `brotli` by default. | +| [cors](https://github.com/gofiber/fiber/tree/master/middleware/cors) | Enable cross-origin resource sharing \(CORS\) with various options. | +| [csrf](https://github.com/gofiber/fiber/tree/master/middleware/csrf) | Protect from CSRF exploits. | +| [encryptcookie](https://github.com/gofiber/fiber/tree/master/middleware/encryptcookie) | Encrypt middleware which encrypts cookie values. | +| [envvar](https://github.com/gofiber/fiber/tree/master/middleware/envvar) | Expose environment variables with providing an optional config. | +| [etag](https://github.com/gofiber/fiber/tree/master/middleware/etag) | ETag middleware that lets caches be more efficient and save bandwidth, as a web server does not need to resend a full response if the content has not changed. | +| [expvar](https://github.com/gofiber/fiber/tree/master/middleware/expvar) | Expvar middleware that serves via its HTTP server runtime exposed variants in the JSON format. | +| [favicon](https://github.com/gofiber/fiber/tree/master/middleware/favicon) | Ignore favicon from logs or serve from memory if a file path is provided. | +| [filesystem](https://github.com/gofiber/fiber/tree/master/middleware/filesystem) | FileSystem middleware for Fiber, special thanks and credits to Alireza Salary | +| [limiter](https://github.com/gofiber/fiber/tree/master/middleware/limiter) | Rate-limiting middleware for Fiber. Use to limit repeated requests to public APIs and/or endpoints such as password reset. | +| [logger](https://github.com/gofiber/fiber/tree/master/middleware/logger) | HTTP request/response logger. | +| [monitor](https://github.com/gofiber/fiber/tree/master/middleware/monitor) | Monitor middleware that reports server metrics, inspired by express-status-monitor | +| [pprof](https://github.com/gofiber/fiber/tree/master/middleware/pprof) | Special thanks to Matthew Lee \(@mthli\) | +| [proxy](https://github.com/gofiber/fiber/tree/master/middleware/proxy) | Allows you to proxy requests to a multiple servers | +| [recover](https://github.com/gofiber/fiber/tree/master/middleware/recover) | Recover middleware recovers from panics anywhere in the stack chain and handles the control to the centralized[ ErrorHandler](https://docs.gofiber.io/guide/error-handling). | +| [requestid](https://github.com/gofiber/fiber/tree/master/middleware/requestid) | Adds a requestid to every request. | +| [session](https://github.com/gofiber/fiber/tree/master/middleware/session) | Session middleware. NOTE: This middleware uses our Storage package. | +| [skip](https://github.com/gofiber/fiber/tree/master/middleware/skip) | Skip middleware that skips a wrapped handler is a predicate is true. | +| [timeout](https://github.com/gofiber/fiber/tree/master/middleware/timeout) | Adds a max time for a request and forwards to ErrorHandler if it is exceeded. | +| [keyauth](https://github.com/gofiber/keyauth) | Key auth middleware provides a key based authentication. | +| [redirect](https://github.com/gofiber/redirect) | Redirect middleware | +| [rewrite](https://github.com/gofiber/rewrite) | Rewrite middleware rewrites the URL path based on provided rules. It can be helpful for backward compatibility or just creating cleaner and more descriptive links. | +| [adaptor](https://github.com/gofiber/adaptor) | Converter for net/http handlers to/from Fiber request handlers, special thanks to @arsmn! | +| [helmet](https://github.com/gofiber/helmet) | Helps secure your apps by setting various HTTP headers. | + +## 🧬 Middleware External + +Kumpulan `middleware` yang dihost external dan diurus oleh [Tim Fiber](https://github.com/orgs/gofiber/people). + +| Middleware | Description | +| :------------------------------------------------ | :-------------------------------------------------------------------------------------------------------------------- | +| [jwt](https://github.com/gofiber/jwt) | JWT returns a JSON Web Token \(JWT\) auth middleware. | +| [storage](https://github.com/gofiber/storage) | Premade storage drivers that implement the Storage interface, designed to be used with various Fiber middlewares. | +| [template](https://github.com/gofiber/template) | This package contains 8 template engines that can be used with Fiber `v1.10.x` Go version 1.13 or higher is required. | +| [websocket](https://github.com/gofiber/websocket) | Based on Fasthttp WebSocket for Fiber with Locals support! | + +## 🕶️ Awesome List + +Untuk artikel lainnya, middlewares, contoh atau tools check kami [awesome list](https://github.com/gofiber/awesome-fiber). + +## 👍 Berkontribusi + +Apabila anda ingin mengucapkan **terima kasih** dan/atau mendukung pengembangan `Fiber`: + +1. Berikan bintang atau [GitHub Star](https://github.com/gofiber/fiber/stargazers) ke proyek ini. +2. Bagikan [di 𝕏 (Twitter) anda](https://x.com/intent/tweet?text=Fiber%20is%20an%20Express%20inspired%20%23web%20%23framework%20built%20on%20top%20of%20Fasthttp%2C%20the%20fastest%20HTTP%20engine%20for%20%23Go.%20Designed%20to%20ease%20things%20up%20for%20%23fast%20development%20with%20zero%20memory%20allocation%20and%20%23performance%20in%20mind%20%F0%9F%9A%80%20https%3A%2F%2Fgithub.com%2Fgofiber%2Ffiber). +3. Buat ulasan atau tutorial di [Medium](https://medium.com/), [Dev.to](https://dev.to/) atau blog pribadi anda. +4. Dukung proyek ini dengan membelikan [secangkir kopi](https://buymeacoff.ee/fenny). + +## ☕ Pendukung + +Fiber adalah proyek sumber terbuka yang beroperasi dalam donasi untuk membayar tagihan, seperti nama domain, _gitbook, netlify_ dan _serverless hosting_. Jika anda mau mendukung Fiber, anda dapat [**membelikan kami kopi disini**](https://buymeacoff.ee/fenny). + +| | User | Donation | +| :--------------------------------------------------------- | :----------------------------------------------- | :------- | +| ![](https://avatars.githubusercontent.com/u/204341?s=25) | [@destari](https://github.com/destari) | ☕ x 10 | +| ![](https://avatars.githubusercontent.com/u/63164982?s=25) | [@dembygenesis](https://github.com/dembygenesis) | ☕ x 5 | +| ![](https://avatars.githubusercontent.com/u/56607882?s=25) | [@thomasvvugt](https://github.com/thomasvvugt) | ☕ x 5 | +| ![](https://avatars.githubusercontent.com/u/27820675?s=25) | [@hendratommy](https://github.com/hendratommy) | ☕ x 5 | +| ![](https://avatars.githubusercontent.com/u/1094221?s=25) | [@ekaputra07](https://github.com/ekaputra07) | ☕ x 5 | +| ![](https://avatars.githubusercontent.com/u/194590?s=25) | [@jorgefuertes](https://github.com/jorgefuertes) | ☕ x 5 | +| ![](https://avatars.githubusercontent.com/u/186637?s=25) | [@candidosales](https://github.com/candidosales) | ☕ x 5 | +| ![](https://avatars.githubusercontent.com/u/29659953?s=25) | [@l0nax](https://github.com/l0nax) | ☕ x 3 | +| ![](https://avatars.githubusercontent.com/u/635852?s=25) | [@bihe](https://github.com/bihe) | ☕ x 3 | +| ![](https://avatars.githubusercontent.com/u/307334?s=25) | [@justdave](https://github.com/justdave) | ☕ x 3 | +| ![](https://avatars.githubusercontent.com/u/11155743?s=25) | [@koddr](https://github.com/koddr) | ☕ x 1 | +| ![](https://avatars.githubusercontent.com/u/29042462?s=25) | [@lapolinar](https://github.com/lapolinar) | ☕ x 1 | +| ![](https://avatars.githubusercontent.com/u/2978730?s=25) | [@diegowifi](https://github.com/diegowifi) | ☕ x 1 | +| ![](https://avatars.githubusercontent.com/u/44171355?s=25) | [@ssimk0](https://github.com/ssimk0) | ☕ x 1 | +| ![](https://avatars.githubusercontent.com/u/5638101?s=25) | [@raymayemir](https://github.com/raymayemir) | ☕ x 1 | +| ![](https://avatars.githubusercontent.com/u/619996?s=25) | [@melkorm](https://github.com/melkorm) | ☕ x 1 | +| ![](https://avatars.githubusercontent.com/u/31022056?s=25) | [@marvinjwendt](https://github.com/thomasvvugt) | ☕ x 1 | +| ![](https://avatars.githubusercontent.com/u/31921460?s=25) | [@toishy](https://github.com/toishy) | ☕ x 1 | + +## ‎‍💻 Kontributor Kode + +Code Contributors + +## ⭐️ Pengamat bintang + +Stargazers over time + +## ⚠️ Lisensi + +Copyright (c) 2019-sekarang [Fenny](https://github.com/fenny) dan [Kontributor](https://github.com/gofiber/fiber/graphs/contributors). `Fiber` adalah proyek gratis dan open-source dibawah [lisensi MIT](https://github.com/gofiber/fiber/blob/master/LICENSE). Logo resmi diciptakan oleh [Vic Shóstak](https://github.com/koddr) dan didistribusikan dalam lisensi [Creative Commons](https://creativecommons.org/licenses/by-sa/4.0/) (CC BY-SA 4.0 International). + +**Lisensi perpustakaan pihak-ketiga** + +- [colorable](https://github.com/mattn/go-colorable/blob/master/LICENSE) +- [isatty](https://github.com/mattn/go-isatty/blob/master/LICENSE) +- [runewidth](https://github.com/mattn/go-runewidth/blob/master/LICENSE) +- [fasthttp](https://github.com/valyala/fasthttp/blob/master/LICENSE) +- [bytebufferpool](https://github.com/valyala/bytebufferpool/blob/master/LICENSE) +- [fwd](https://github.com/philhofer/fwd/blob/master/LICENSE.md) +- [go-ole](https://github.com/go-ole/go-ole/blob/master/LICENSE) +- [gopsutil](https://github.com/shirou/gopsutil/blob/master/LICENSE) +- [msgp](https://github.com/tinylib/msgp/blob/master/LICENSE) +- [schema](https://github.com/gorilla/schema/blob/master/LICENSE) +- [uuid](https://github.com/google/uuid/blob/master/LICENSE) +- [wmi](https://github.com/StackExchange/wmi/blob/master/LICENSE) diff --git a/.github/README_it.md b/.github/README_it.md new file mode 100644 index 0000000..159bcff --- /dev/null +++ b/.github/README_it.md @@ -0,0 +1,712 @@ +

+ + + + + Fiber + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + +

+

+Fiber è un framework web inspirato a Express costruito sopra Fasthttp, un motore HTTP molto veloce per Go. Progettato per semplificare le cose per uno sviluppo veloce con zero allocazione di memoria e le prestazioni in mente. +

+ +## ⚡️ Inizia velocemente + +```go +package main + +import "github.com/gofiber/fiber/v2" + +func main() { + app := fiber.New() + + app.Get("/", func(c *fiber.Ctx) error { + return c.SendString("Hello, World 👋!") + }) + + app.Listen(":3000") +} +``` + +## 🤖 Benchmark + +Questi test sono stati eseguiti da [TechEmpower](https://www.techempower.com/benchmarks/#section=data-r19&hw=ph&test=plaintext) e [Go Web](https://github.com/smallnest/go-web-framework-benchmark). Se vuoi vedere tutti i risultati, visita la nostra [Wiki](https://docs.gofiber.io/extra/benchmarks). + +

+ + +

+ +## ⚙️ Installazione + +Assicurati di avere Go ([per scaricarlo](https://go.dev/dl/)) installato. Devi avere la versione `1.17` o superiore. + +Inizializza il tuo progetto creando una cartella e successivamente usando il comando `go mod init github.com/la-tua/repo` ([per maggiori informazioni](https://go.dev/blog/using-go-modules)) dentro la cartella. Dopodiche installa Fiber con il comando [`go get`](https://pkg.go.dev/cmd/go/#hdr-Add_dependencies_to_current_module_and_install_them): + +```bash +go get -u github.com/gofiber/fiber/v2 +``` + +## 🎯 Caratteristiche + +- [Routing](https://docs.gofiber.io/guide/routing) solido +- Serve [file statici](https://docs.gofiber.io/api/app#static) +- [Perfomance](https://docs.gofiber.io/extra/benchmarks) estreme +- [Basso](https://docs.gofiber.io/extra/benchmarks) utilizzo di [memoria](https://docs.gofiber.io/extra/benchmarks) +- [API endpoints](https://docs.gofiber.io/api/ctx) +- Supporta Middleware e [Next](https://docs.gofiber.io/api/ctx#next) +- Programmazione server-side [veloce](https://dev.to/koddr/welcome-to-fiber-an-express-js-styled-fastest-web-framework-written-with-on-golang-497) +- [Template engine](https://github.com/gofiber/template) +- [Supporto WebSocket](https://github.com/gofiber/websocket) +- [Server-Sent events](https://github.com/gofiber/recipes/tree/master/sse) +- [Rate Limiter](https://docs.gofiber.io/api/middleware/limiter) +- Disponible in [19 lingue](https://docs.gofiber.io/) +- E molto altro ancora, [esplora Fiber](https://docs.gofiber.io/) + +## 💡 Filosofia + +I nuovi gopher che passano da [Node.js](https://nodejs.org/en/about/) a [Go](https://go.dev/doc/) hanno a che fare con una curva di apprendimento prima di poter iniziare a creare le proprie applicazioni web o microservizi. Fiber, come **web framework** , è stato creato con l'idea di **minimalismo** e seguendo lo '**UNIX way**' , così i nuovi gopher posso entrare rapidamente nel mondo di Go con un caldo e fidato benvenuto. + +Fiber è **ispirato** da Express, il web framework più popolare su internet. Abbiamo combinato la **facilità** di Express e **le prestazioni** di Go. Se hai mai implementato una applicazione web in Node.js (_utilizzando Express o simili_), allora i tanti metodi e principi ti saranno **molto familiari**. + +## ⚠️ Limitazioni + +- Dato che Fiber utilizza unsafe, la libreria non sempre potrebbe essere compatibile con l'ultima versione di Go. Fiber 2.40.0 è stato testato con la versioni 1.17 alla 1.21 di Go. +- Fiber non è compatibile con le interfacce net/http. Questo significa che non è possibile utilizzare progetti come qglgen, go-swagger, o altri che fanno parte dell'ecosistema net/http. + +## 👀 Esempi + +Qui sotto trovi molti dei più comuni esempi. Se vuoi vedere ulteriori esempi, visita il nostro [repository delle ricette](https://github.com/gofiber/recipes) o la nostra [documentazione API](https://docs.gofiber.io) . + +#### 📖 [**Routing di base**](https://docs.gofiber.io/#basic-routing) + +```go +func main() { + app := fiber.New() + + // GET /api/register + app.Get("/api/*", func(c *fiber.Ctx) error { + msg := fmt.Sprintf("✋ %s", c.Params("*")) + return c.SendString(msg) // => ✋ register + }) + + // GET /flights/LAX-SFO + app.Get("/flights/:from-:to", func(c *fiber.Ctx) error { + msg := fmt.Sprintf("💸 From: %s, To: %s", c.Params("from"), c.Params("to")) + return c.SendString(msg) // => 💸 From: LAX, To: SFO + }) + + // GET /dictionary.txt + app.Get("/:file.:ext", func(c *fiber.Ctx) error { + msg := fmt.Sprintf("📃 %s.%s", c.Params("file"), c.Params("ext")) + return c.SendString(msg) // => 📃 dictionary.txt + }) + + // GET /john/75 + app.Get("/:name/:age/:gender?", func(c *fiber.Ctx) error { + msg := fmt.Sprintf("👴 %s is %s years old", c.Params("name"), c.Params("age")) + return c.SendString(msg) // => 👴 john is 75 years old + }) + + // GET /john + app.Get("/:name", func(c *fiber.Ctx) error { + msg := fmt.Sprintf("Hello, %s 👋!", c.Params("name")) + return c.SendString(msg) // => Hello john 👋! + }) + + log.Fatal(app.Listen(":3000")) +} + +``` + +#### 📖 [**Dare nomi alle Route**](https://docs.gofiber.io/api/app#name) + +```go +func main() { + app := fiber.New() + + // GET /api/register + app.Get("/api/*", func(c *fiber.Ctx) error { + msg := fmt.Sprintf("✋ %s", c.Params("*")) + return c.SendString(msg) // => ✋ register + }).Name("api") + + data, _ := json.MarshalIndent(app.GetRoute("api"), "", " ") + fmt.Print(string(data)) + // Prints: + // { + // "method": "GET", + // "name": "api", + // "path": "/api/*", + // "params": [ + // "*1" + // ] + // } + + + log.Fatal(app.Listen(":3000")) +} + +``` + +#### 📖 [**Servire File Statici**](https://docs.gofiber.io/api/app#static) + +```go +func main() { + app := fiber.New() + + app.Static("/", "./public") + // => http://localhost:3000/js/script.js + // => http://localhost:3000/css/style.css + + app.Static("/prefix", "./public") + // => http://localhost:3000/prefix/js/script.js + // => http://localhost:3000/prefix/css/style.css + + app.Static("*", "./public/index.html") + // => http://localhost:3000/any/path/shows/index/html + + log.Fatal(app.Listen(":3000")) +} + +``` + +#### 📖 [**Middleware e Next**](https://docs.gofiber.io/api/ctx#next) + +```go +func main() { + app := fiber.New() + + // Match any route + app.Use(func(c *fiber.Ctx) error { + fmt.Println("🥇 First handler") + return c.Next() + }) + + // Match all routes starting with /api + app.Use("/api", func(c *fiber.Ctx) error { + fmt.Println("🥈 Second handler") + return c.Next() + }) + + // GET /api/register + app.Get("/api/list", func(c *fiber.Ctx) error { + fmt.Println("🥉 Last handler") + return c.SendString("Hello, World 👋!") + }) + + log.Fatal(app.Listen(":3000")) +} + +``` + +
+ 📚 Mostra altri esempi + +### Motori di template + +📖 [Configurazione](https://docs.gofiber.io/api/fiber#config) +📖 [Motori](https://github.com/gofiber/template) +📖 [Render](https://docs.gofiber.io/api/ctx#render) + +Fiber usa di default [html/template](https://pkg.go.dev/html/template/) quando nessun motore template è stato impostato. + +Se vuoi eseguire parzialmente o utilizzare un motore differente come [amber](https://github.com/eknkc/amber), [handlebars](https://github.com/aymerick/raymond), [mustache](https://github.com/cbroglie/mustache) o [pug](https://github.com/Joker/jade) ecc.. + +Dai un'occhiata al pacchetto [Template](https://github.com/gofiber/template) che supporta multipli motore template. + +```go +package main + +import ( + "github.com/gofiber/fiber/v2" + "github.com/gofiber/template/pug" +) + +func main() { + // You can setup Views engine before initiation app: + app := fiber.New(fiber.Config{ + Views: pug.New("./views", ".pug"), + }) + + // And now, you can call template `./views/home.pug` like this: + app.Get("/", func(c *fiber.Ctx) error { + return c.Render("home", fiber.Map{ + "title": "Homepage", + "year": 1999, + }) + }) + + log.Fatal(app.Listen(":3000")) +} +``` + +### Raggruppare le route + +📖 [Gruppi](https://docs.gofiber.io/api/app#group) + +```go +func middleware(c *fiber.Ctx) error { + fmt.Println("Don't mind me!") + return c.Next() +} + +func handler(c *fiber.Ctx) error { + return c.SendString(c.Path()) +} + +func main() { + app := fiber.New() + + // Root API route + api := app.Group("/api", middleware) // /api + + // API v1 routes + v1 := api.Group("/v1", middleware) // /api/v1 + v1.Get("/list", handler) // /api/v1/list + v1.Get("/user", handler) // /api/v1/user + + // API v2 routes + v2 := api.Group("/v2", middleware) // /api/v2 + v2.Get("/list", handler) // /api/v2/list + v2.Get("/user", handler) // /api/v2/user + + // ... +} + +``` + +### Middleware logger + +📖 [Logger](https://docs.gofiber.io/api/middleware/logger) + +```go +package main + +import ( + "log" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/logger" +) + +func main() { + app := fiber.New() + + app.Use(logger.New()) + + // ... + + log.Fatal(app.Listen(":3000")) +} +``` + +### Cross-Origin Resource Sharing (CORS) + +📖 [CORS](https://docs.gofiber.io/api/middleware/cors) + +```go +import ( + "log" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/cors" +) + +func main() { + app := fiber.New() + + app.Use(cors.New()) + + // ... + + log.Fatal(app.Listen(":3000")) +} +``` + +Controlla il CORS passando un dominio qualsiasi nell'header `Origin`: + +```bash +curl -H "Origin: http://example.com" --verbose http://localhost:3000 +``` + +### Risposte 404 personalizzate + +📖 [HTTP Methods](https://docs.gofiber.io/api/ctx#status) + +```go +func main() { + app := fiber.New() + + app.Static("/", "./public") + + app.Get("/demo", func(c *fiber.Ctx) error { + return c.SendString("This is a demo!") + }) + + app.Post("/register", func(c *fiber.Ctx) error { + return c.SendString("Welcome!") + }) + + // Last middleware to match anything + app.Use(func(c *fiber.Ctx) error { + return c.SendStatus(404) + // => 404 "Not Found" + }) + + log.Fatal(app.Listen(":3000")) +} +``` + +### Risposte JSON + +📖 [JSON](https://docs.gofiber.io/api/ctx#json) + +```go +type User struct { + Name string `json:"name"` + Age int `json:"age"` +} + +func main() { + app := fiber.New() + + app.Get("/user", func(c *fiber.Ctx) error { + return c.JSON(&User{"John", 20}) + // => {"name":"John", "age":20} + }) + + app.Get("/json", func(c *fiber.Ctx) error { + return c.JSON(fiber.Map{ + "success": true, + "message": "Hi John!", + }) + // => {"success":true, "message":"Hi John!"} + }) + + log.Fatal(app.Listen(":3000")) +} +``` + +### WebSocket + +📖 [Websocket](https://github.com/gofiber/websocket) + +```go +import ( + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/websocket" +) + +func main() { + app := fiber.New() + + app.Get("/ws", websocket.New(func(c *websocket.Conn) { + for { + mt, msg, err := c.ReadMessage() + if err != nil { + log.Println("read:", err) + break + } + log.Printf("recv: %s", msg) + err = c.WriteMessage(mt, msg) + if err != nil { + log.Println("write:", err) + break + } + } + })) + + log.Fatal(app.Listen(":3000")) + // ws://localhost:3000/ws +} +``` + +### Eventi dal server + +📖 [More Info](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events) + +```go +import ( + "github.com/gofiber/fiber/v2" + "github.com/valyala/fasthttp" +) + +func main() { + app := fiber.New() + + app.Get("/sse", func(c *fiber.Ctx) error { + c.Set("Content-Type", "text/event-stream") + c.Set("Cache-Control", "no-cache") + c.Set("Connection", "keep-alive") + c.Set("Transfer-Encoding", "chunked") + + c.Context().SetBodyStreamWriter(fasthttp.StreamWriter(func(w *bufio.Writer) { + fmt.Println("WRITER") + var i int + + for { + i++ + msg := fmt.Sprintf("%d - the time is %v", i, time.Now()) + fmt.Fprintf(w, "data: Message: %s\n\n", msg) + fmt.Println(msg) + + w.Flush() + time.Sleep(5 * time.Second) + } + })) + + return nil + }) + + log.Fatal(app.Listen(":3000")) +} +``` + +### Recupera middleware + +📖 [Recover](https://docs.gofiber.io/api/middleware/recover) + +```go +import ( + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/recover" +) + +func main() { + app := fiber.New() + + app.Use(recover.New()) + + app.Get("/", func(c *fiber.Ctx) error { + panic("normally this would crash your app") + }) + + log.Fatal(app.Listen(":3000")) +} +``` + +
+ +### Utilizzare i Proxy fidati + +📖 [Config](https://docs.gofiber.io/api/fiber#config) + +```go +import ( + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/recover" +) + +func main() { + app := fiber.New(fiber.Config{ + EnableTrustedProxyCheck: true, + TrustedProxies: []string{"0.0.0.0", "1.1.1.1/30"}, // IP address or IP address range + ProxyHeader: fiber.HeaderXForwardedFor, + }) + + // ... + + log.Fatal(app.Listen(":3000")) +} +``` + + + +## 🧬 Middleware Interni + +Qui una lista dei middleware inclusi con Fiber. + +| Middleware | Descrizione | +| :------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| [basicauth](https://github.com/gofiber/fiber/tree/master/middleware/basicauth) | Middleware basico di autenticazione usando http. Chiama il suo handler se le credenziali sono giuste e il codice 401 Unauthorized per credenziali mancanti o invalide. | +| [cache](https://github.com/gofiber/fiber/tree/master/middleware/cache) | Intercetta e mette nella cache la risposta | +| [compress](https://github.com/gofiber/fiber/tree/master/middleware/compress) | Middleware di compressione per Fiber, supporta `deflate`, `gzip` e `brotli` di default. | +| [cors](https://github.com/gofiber/fiber/tree/master/middleware/cors) | Ti permette di usare cross-origin resource sharing \(CORS\) con tante opzioni. | +| [csrf](https://github.com/gofiber/fiber/tree/master/middleware/csrf) | Ti protegge da attachi CSRF. | +| [encryptcookie](https://github.com/gofiber/fiber/tree/master/middleware/encryptcookie) | Middleware che encrypta i valori dei cookie. | +| [envvar](https://github.com/gofiber/fiber/tree/master/middleware/envvar) | Esporre le variabili di ambiente fornendo una configurazione facoltativa. | +| [etag](https://github.com/gofiber/fiber/tree/master/middleware/etag) | Middleware che permette alle cache di essere più efficienti e salvare banda, come un web server che non deve rimandare il messagio pieno se il contenuto non è cambiato. | +| [expvar](https://github.com/gofiber/fiber/tree/master/middleware/expvar) | Middleware che serve via il suo runtime server HTTP varianti esposte in formato JSON. | +| [favicon](https://github.com/gofiber/fiber/tree/master/middleware/favicon) | Ignora favicon dai logs o serve dalla memoria se un filepath è specificato. | +| [filesystem](https://github.com/gofiber/fiber/tree/master/middleware/filesystem) | Middleware per il FileSystem per Fiber, grazie tante e crediti a Alireza Salary | +| [limiter](https://github.com/gofiber/fiber/tree/master/middleware/limiter) | Middleware per Rate-limiting per Fiber. Usato per limitare richieste continue agli APIs publici e/o endpoints come un password reset. | +| [logger](https://github.com/gofiber/fiber/tree/master/middleware/logger) | Logger HTTP per richiesta/risposta. | +| [monitor](https://github.com/gofiber/fiber/tree/master/middleware/monitor) | Middleware per monitorare che riporta metriche server, ispirato da express-status-monitor | +| [pprof](https://github.com/gofiber/fiber/tree/master/middleware/pprof) | Grazie tante a Matthew Lee \(@mthli\) | +| [proxy](https://github.com/gofiber/fiber/tree/master/middleware/proxy) | Ti permette di fare richieste proxy a multipli server. | +| [recover](https://github.com/gofiber/fiber/tree/master/middleware/recover) | Middleware per recuperare dagli attachi di panico da tutte le parti nella stack chain e affida il controllo al [ ErrorHandler](https://docs.gofiber.io/guide/error-handling) centralizzato. | +| [requestid](https://github.com/gofiber/fiber/tree/master/middleware/requestid) | Aggiunge un requestid a ogni richiesta. | +| [session](https://github.com/gofiber/fiber/tree/master/middleware/session) | Middleware per sessioni. NOTA: Questo middleware usa il nostro Storage package. | +| [skip](https://github.com/gofiber/fiber/tree/master/middleware/skip) | Middleware che salta un wrapped handler se un predicate è vero. | +| [timeout](https://github.com/gofiber/fiber/tree/master/middleware/timeout) | Aggiunge un tempo massimo per una richiesta e lo manda a ErrorHandler se si supera. | +| [keyauth](https://github.com/gofiber/keyauth) | Usa auth basato su chiavi. | +| [redirect](https://github.com/gofiber/redirect) | Middleware per reinderizzare | +| [rewrite](https://github.com/gofiber/rewrite) | Riscrive la path all URL con le regole date. Può essere di aiuto per compatibilità o per creare link puliti e più descrittivi. | +| [adaptor](https://github.com/gofiber/adaptor) | Converte gli handler net/http a/da i request handlers di Fiber, grazie tante a @arsmn! | +| [helmet](https://github.com/gofiber/helmet) | Aiuta a mettere sicurezza alla tua app usando vari header HTTP. | + +## 🧬 Middleware Esterni + +La lista dei moduli middleware hostati esternamente e mantenuti dal [team di Fiber](https://github.com/orgs/gofiber/people). + +| Middleware | Descrizione | +| :------------------------------------------------ | :------------------------------------------------------------------------------------------------------------------------- | +| [jwt](https://github.com/gofiber/jwt) | Usa JSON Web Token \(JWT\) auth. | +| [storage](https://github.com/gofiber/storage) | Dirver di storage che implementa la interfaccia Storage, fatto per essere usato con vari Fiber middleware. | +| [template](https://github.com/gofiber/template) | Questo pachetto contiene 8 motori template che possono essere usati con Fiber `v1.10.x`. Versione di go neccesaria: 1.13+. | +| [websocket](https://github.com/gofiber/websocket) | Basato su Fasthttp WebSocket per Fiber con supporto per Locals! | + +## 🕶️ Awesome List + +Per piu articoli, middlewares, esempi o attrezzi puoi usare la [awesome list](https://github.com/gofiber/awesome-fiber). + +## 👍 Contribuire + +Se vuoi dirci **grazie** e/o supportare lo sviluppo di `Fiber`: + +1. Aggiungi una [stella GitHub](https://github.com/gofiber/fiber/stargazers) al progetto. +2. Twitta del progetto [su 𝕏 (Twitter)](https://x.com/intent/tweet?text=Fiber%20is%20an%20Express%20inspired%20%23web%20%23framework%20built%20on%20top%20of%20Fasthttp%2C%20the%20fastest%20HTTP%20engine%20for%20%23Go.%20Designed%20to%20ease%20things%20up%20for%20%23fast%20development%20with%20zero%20memory%20allocation%20and%20%23performance%20in%20mind%20%F0%9F%9A%80%20https%3A%2F%2Fgithub.com%2Fgofiber%2Ffiber). +3. Scrivi una recensione o un tutorial su [Medium](https://medium.com/), [Dev.to](https://dev.to/) o sul tuo blog personale. +4. Supporta il progetto donando una [tazza di caffè](https://buymeacoff.ee/fenny). + +## ☕ Supporter + +Fiber è un progetto open source che va avanti grazie alle donazioni per pagare le spese e.g. il nostro nome dominio, gitbook, netlify e hosting serverless. Se vuoi supportare Fiber, puoi ☕ [**comprarci un caffè qui**](https://buymeacoff.ee/fenny). + +| | Utente | Donazione | +| :--------------------------------------------------------- | :----------------------------------------------- | :-------- | +| ![](https://avatars.githubusercontent.com/u/204341?s=25) | [@destari](https://github.com/destari) | ☕ x 10 | +| ![](https://avatars.githubusercontent.com/u/63164982?s=25) | [@dembygenesis](https://github.com/dembygenesis) | ☕ x 5 | +| ![](https://avatars.githubusercontent.com/u/56607882?s=25) | [@thomasvvugt](https://github.com/thomasvvugt) | ☕ x 5 | +| ![](https://avatars.githubusercontent.com/u/27820675?s=25) | [@hendratommy](https://github.com/hendratommy) | ☕ x 5 | +| ![](https://avatars.githubusercontent.com/u/1094221?s=25) | [@ekaputra07](https://github.com/ekaputra07) | ☕ x 5 | +| ![](https://avatars.githubusercontent.com/u/194590?s=25) | [@jorgefuertes](https://github.com/jorgefuertes) | ☕ x 5 | +| ![](https://avatars.githubusercontent.com/u/186637?s=25) | [@candidosales](https://github.com/candidosales) | ☕ x 5 | +| ![](https://avatars.githubusercontent.com/u/29659953?s=25) | [@l0nax](https://github.com/l0nax) | ☕ x 3 | +| ![](https://avatars.githubusercontent.com/u/635852?s=25) | [@bihe](https://github.com/bihe) | ☕ x 3 | +| ![](https://avatars.githubusercontent.com/u/307334?s=25) | [@justdave](https://github.com/justdave) | ☕ x 3 | +| ![](https://avatars.githubusercontent.com/u/11155743?s=25) | [@koddr](https://github.com/koddr) | ☕ x 1 | +| ![](https://avatars.githubusercontent.com/u/29042462?s=25) | [@lapolinar](https://github.com/lapolinar) | ☕ x 1 | +| ![](https://avatars.githubusercontent.com/u/2978730?s=25) | [@diegowifi](https://github.com/diegowifi) | ☕ x 1 | +| ![](https://avatars.githubusercontent.com/u/44171355?s=25) | [@ssimk0](https://github.com/ssimk0) | ☕ x 1 | +| ![](https://avatars.githubusercontent.com/u/5638101?s=25) | [@raymayemir](https://github.com/raymayemir) | ☕ x 1 | +| ![](https://avatars.githubusercontent.com/u/619996?s=25) | [@melkorm](https://github.com/melkorm) | ☕ x 1 | +| ![](https://avatars.githubusercontent.com/u/31022056?s=25) | [@marvinjwendt](https://github.com/marvinjwendt) | ☕ x 1 | +| ![](https://avatars.githubusercontent.com/u/31921460?s=25) | [@toishy](https://github.com/toishy) | ☕ x 1 | + +## ‎‍💻 Contributori + +Code Contributors + +## ⭐️ Stargazers + +Stargazers over time + +## ⚠️ Licenza + +Copyright (c) 2019-ora [Fenny](https://github.com/fenny) e [Contributors](https://github.com/gofiber/fiber/graphs/contributors). `Fiber` è un software free e open-source licenzato sotto [MIT License](https://github.com/gofiber/fiber/blob/master/LICENSE). Il logo ufficiale è stato creato da [Vic Shóstak](https://github.com/koddr) e distribuito sotto licenza [Creative Commons](https://creativecommons.org/licenses/by-sa/4.0/) (CC BY-SA 4.0 International). + +**Licenze di Terze parti ** + +- [colorable](https://github.com/mattn/go-colorable/blob/master/LICENSE) +- [isatty](https://github.com/mattn/go-isatty/blob/master/LICENSE) +- [runewidth](https://github.com/mattn/go-runewidth/blob/master/LICENSE) +- [fasthttp](https://github.com/valyala/fasthttp/blob/master/LICENSE) +- [bytebufferpool](https://github.com/valyala/bytebufferpool/blob/master/LICENSE) +- [fwd](https://github.com/philhofer/fwd/blob/master/LICENSE.md) +- [go-ole](https://github.com/go-ole/go-ole/blob/master/LICENSE) +- [gopsutil](https://github.com/shirou/gopsutil/blob/master/LICENSE) +- [msgp](https://github.com/tinylib/msgp/blob/master/LICENSE) +- [schema](https://github.com/gorilla/schema/blob/master/LICENSE) +- [uuid](https://github.com/google/uuid/blob/master/LICENSE) +- [wmi](https://github.com/StackExchange/wmi/blob/master/LICENSE) diff --git a/.github/README_ja.md b/.github/README_ja.md new file mode 100644 index 0000000..23dfb60 --- /dev/null +++ b/.github/README_ja.md @@ -0,0 +1,691 @@ +

+ + + + + Fiber + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + +

+

+Fiberは、Expressに触発されたWebフレームワークです。Go 最速のHTTPエンジンであるFasthttpで作られています。ゼロメモリアロケーションパフォーマンスを念頭に置いて設計されており、迅速な開発をサポートします。 + +

+ +## ⚡️ クイックスタート + +```go +package main + +import "github.com/gofiber/fiber/v2" + +func main() { + app := fiber.New() + + app.Get("/", func(c *fiber.Ctx) error { + return c.SendString("Hello, World 👋!") + }) + + app.Listen(":3000") +} +``` + +## 🤖 ベンチマーク + +これらのテストは[TechEmpower](https://www.techempower.com/benchmarks/#section=data-r19&hw=ph&test=plaintext)および[Go Web](https://github.com/smallnest/go-web-framework-benchmark)によって計測を行っています 。すべての結果を表示するには、 [Wiki](https://docs.gofiber.io/extra/benchmarks)にアクセスしてください。 + +

+ + +

+ +## ⚙️ インストール + +Go がインストールされていることを確認してください ([ダウンロード](https://go.dev/dl/)). バージョン `1.17` またはそれ以上であることが必要です。 + +フォルダを作成し、フォルダ内で `go mod init github.com/your/repo` ([learn more](https://go.dev/blog/using-go-modules)) を実行してプロジェクトを初期化してください。その後、 Fiber を以下の [`go get`](https://pkg.go.dev/cmd/go/#hdr-Add_dependencies_to_current_module_and_install_them) コマンドでインストールしてください。 + +```bash +go get -u github.com/gofiber/fiber/v2 +``` + +## 🎯 機能 + +- 堅牢な[ルーティング](https://docs.gofiber.io/guide/routing) +- [静的ファイル](https://docs.gofiber.io/api/app#static)のサポート +- 究極の[パフォーマンス](https://docs.gofiber.io/extra/benchmarks) +- [低メモリ](https://docs.gofiber.io/extra/benchmarks)フットプリント +- [API エンドポイント](https://docs.gofiber.io/api/ctx) +- [Middleware](https://docs.gofiber.io/api/middleware) と[Next](https://docs.gofiber.io/api/ctx#next)のサポート +- [迅速](https://dev.to/koddr/welcome-to-fiber-an-express-js-styled-fastest-web-framework-written-with-on-golang-497)なサーバーサイドプログラミング +- [Template engines](https://github.com/gofiber/template) +- [WebSocket support](https://github.com/gofiber/websocket) +- [Server-Sent events](https://github.com/gofiber/recipes/tree/master/sse) +- [Rate Limiter](https://docs.gofiber.io/api/middleware/limiter) +- [19 ヶ国語](https://docs.gofiber.io/)に翻訳 +- [Fiber](https://docs.gofiber.io/)をもっと知る + +## 💡 哲学 + +[Node.js](https://nodejs.org/en/about/)から[Go](https://go.dev/doc/) に乗り換えようとしている新しい Gopher は Web フレームワークやマイクロサービスの構築を始める前に多くを学ばなければなりません。 +しかし、 **Web フレームワーク**である Fiber は**ミニマリズム**と**UNIX 哲学**をもとに作られているため、新しい Gopher はスムーズに Go の世界に入ることができます。 + +Fiber は人気の高い Web フレームワークである Expressjs に**インスパイア**されています。 +わたしたちは Express の**手軽さ**と Go の**パフォーマンス**を組み合わせました。 +もしも、Web アプリケーションを Express 等の Node.js フレームワークで実装した経験があれば、多くの方法や原理がとても**馴染み深い**でしょう。 + +## ⚠️ 制限事項 + +- Fiber は unsafe パッケージを使用しているため、最新の Go バージョンと互換性がない場合があります。Fiber 2.40.0 は、Go のバージョン 1.17 から 1.21 でテストされています。 +- Fiber は net/http インターフェースと互換性がありません。つまり、gqlgen や go-swagger など、net/http のエコシステムの一部であるプロジェクトを使用することができません。 + +## 👀 例 + +以下に一般的な例をいくつか示します。他のコード例をご覧になりたい場合は、 [Recipes リポジトリ](https://github.com/gofiber/recipes)または[API ドキュメント](https://docs.gofiber.io)にアクセスしてください。 + +#### 📖 [**基本的なルーティング**](https://docs.gofiber.io/#basic-routing) + +```go +func main() { + app := fiber.New() + + // GET /api/register + app.Get("/api/*", func(c *fiber.Ctx) error { + msg := fmt.Sprintf("✋ %s", c.Params("*")) + return c.SendString(msg) // => ✋ register + }) + + // GET /flights/LAX-SFO + app.Get("/flights/:from-:to", func(c *fiber.Ctx) error { + msg := fmt.Sprintf("💸 From: %s, To: %s", c.Params("from"), c.Params("to")) + return c.SendString(msg) // => 💸 From: LAX, To: SFO + }) + + // GET /dictionary.txt + app.Get("/:file.:ext", func(c *fiber.Ctx) error { + msg := fmt.Sprintf("📃 %s.%s", c.Params("file"), c.Params("ext")) + return c.SendString(msg) // => 📃 dictionary.txt + }) + + // GET /john/75 + app.Get("/:name/:age/:gender?", func(c *fiber.Ctx) error { + msg := fmt.Sprintf("👴 %s is %s years old", c.Params("name"), c.Params("age")) + return c.SendString(msg) // => 👴 john is 75 years old + }) + + // GET /john + app.Get("/:name", func(c *fiber.Ctx) error { + msg := fmt.Sprintf("Hello, %s 👋!", c.Params("name")) + return c.SendString(msg) // => Hello john 👋! + }) + + log.Fatal(app.Listen(":3000")) +} + +``` + +#### 📖 [**ルートの命名**](https://docs.gofiber.io/api/app#name) + +```go +func main() { + app := fiber.New() + + // GET /api/register + app.Get("/api/*", func(c *fiber.Ctx) error { + msg := fmt.Sprintf("✋ %s", c.Params("*")) + return c.SendString(msg) // => ✋ register + }).Name("api") + + data, _ := json.MarshalIndent(app.GetRoute("api"), "", " ") + fmt.Print(string(data)) + // Prints: + // { + // "method": "GET", + // "name": "api", + // "path": "/api/*", + // "params": [ + // "*1" + // ] + // } + + + log.Fatal(app.Listen(":3000")) +} + +``` + +#### 📖 [**静的ファイルの提供**](https://docs.gofiber.io/api/app#static) + +```go +func main() { + app := fiber.New() + + app.Static("/", "./public") + // => http://localhost:3000/js/script.js + // => http://localhost:3000/css/style.css + + app.Static("/prefix", "./public") + // => http://localhost:3000/prefix/js/script.js + // => http://localhost:3000/prefix/css/style.css + + app.Static("*", "./public/index.html") + // => http://localhost:3000/any/path/shows/index/html + + log.Fatal(app.Listen(":3000")) +} + +``` + +#### 📖 [**ミドルウェア & Next**](https://docs.gofiber.io/api/ctx#next) + +```go +func main() { + app := fiber.New() + + // Match any route + app.Use(func(c *fiber.Ctx) error { + fmt.Println("🥇 First handler") + return c.Next() + }) + + // Match all routes starting with /api + app.Use("/api", func(c *fiber.Ctx) error { + fmt.Println("🥈 Second handler") + return c.Next() + }) + + // GET /api/register + app.Get("/api/list", func(c *fiber.Ctx) error { + fmt.Println("🥉 Last handler") + return c.SendString("Hello, World 👋!") + }) + + log.Fatal(app.Listen(":3000")) +} + +``` + +
+ 📚 その他のコード例を見る + +### Views engines + +📖 [Config](https://docs.gofiber.io/api/fiber#config) +📖 [Engines](https://github.com/gofiber/template) +📖 [Render](https://docs.gofiber.io/api/ctx#render) + +view engine が設定されていない時は、Fiber のデフォルトは[html/template](https://pkg.go.dev/html/template/) になります。 + +パーシャルを実行したい場合や、[amber](https://github.com/eknkc/amber), [handlebars](https://github.com/aymerick/raymond), [mustache](https://github.com/cbroglie/mustache) ,[pug](https://github.com/Joker/jade) などの別のエンジンを使用したい場合など、 + +複数の view engine をサポートする [Template](https://github.com/gofiber/template) パッケージをご覧ください。 + +```go +package main + +import ( + "github.com/gofiber/fiber/v2" + "github.com/gofiber/template/pug" +) + +func main() { + // You can setup Views engine before initiation app: + app := fiber.New(fiber.Config{ + Views: pug.New("./views", ".pug"), + }) + + // And now, you can call template `./views/home.pug` like this: + app.Get("/", func(c *fiber.Ctx) error { + return c.Render("home", fiber.Map{ + "title": "Homepage", + "year": 1999, + }) + }) + + log.Fatal(app.Listen(":3000")) +} +``` + +### Grouping routes into chains + +📖 [Group](https://docs.gofiber.io/api/app#group) + +```go +func middleware(c *fiber.Ctx) error { + fmt.Println("Don't mind me!") + return c.Next() +} + +func handler(c *fiber.Ctx) error { + return c.SendString(c.Path()) +} + +func main() { + app := fiber.New() + + // Root API route + api := app.Group("/api", middleware) // /api + + // API v1 routes + v1 := api.Group("/v1", middleware) // /api/v1 + v1.Get("/list", handler) // /api/v1/list + v1.Get("/user", handler) // /api/v1/user + + // API v2 routes + v2 := api.Group("/v2", middleware) // /api/v2 + v2.Get("/list", handler) // /api/v2/list + v2.Get("/user", handler) // /api/v2/user + + // ... +} + +``` + +### Middleware logger + +📖 [Logger](https://docs.gofiber.io/api/middleware/logger) + +```go +package main + +import ( + "log" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/logger" +) + +func main() { + app := fiber.New() + + app.Use(logger.New()) + + // ... + + log.Fatal(app.Listen(":3000")) +} +``` + +### Cross-Origin Resource Sharing (CORS) + +📖 [CORS](https://docs.gofiber.io/api/middleware/cors) + +```go +import ( + "log" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/cors" +) + +func main() { + app := fiber.New() + + app.Use(cors.New()) + + // ... + + log.Fatal(app.Listen(":3000")) +} +``` + +`Origin`ヘッダーに任意のドメインを渡して CORS のチェックをします: + +```bash +curl -H "Origin: http://example.com" --verbose http://localhost:3000 +``` + +### Custom 404 response + +📖 [HTTP Methods](https://docs.gofiber.io/api/ctx#status) + +```go +func main() { + app := fiber.New() + + app.Static("/", "./public") + + app.Get("/demo", func(c *fiber.Ctx) error { + return c.SendString("This is a demo!") + }) + + app.Post("/register", func(c *fiber.Ctx) error { + return c.SendString("Welcome!") + }) + + // Last middleware to match anything + app.Use(func(c *fiber.Ctx) error { + return c.SendStatus(404) + // => 404 "Not Found" + }) + + log.Fatal(app.Listen(":3000")) +} +``` + +### JSON Response + +📖 [JSON](https://docs.gofiber.io/api/ctx#json) + +```go +type User struct { + Name string `json:"name"` + Age int `json:"age"` +} + +func main() { + app := fiber.New() + + app.Get("/user", func(c *fiber.Ctx) error { + return c.JSON(&User{"John", 20}) + // => {"name":"John", "age":20} + }) + + app.Get("/json", func(c *fiber.Ctx) error { + return c.JSON(fiber.Map{ + "success": true, + "message": "Hi John!", + }) + // => {"success":true, "message":"Hi John!"} + }) + + log.Fatal(app.Listen(":3000")) +} +``` + +### WebSocket Upgrade + +📖 [Websocket](https://github.com/gofiber/websocket) + +```go +import ( + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/websocket" +) + +func main() { + app := fiber.New() + + app.Get("/ws", websocket.New(func(c *websocket.Conn) { + for { + mt, msg, err := c.ReadMessage() + if err != nil { + log.Println("read:", err) + break + } + log.Printf("recv: %s", msg) + err = c.WriteMessage(mt, msg) + if err != nil { + log.Println("write:", err) + break + } + } + })) + + log.Fatal(app.Listen(":3000")) + // ws://localhost:3000/ws +} +``` + +### Server-Sent Events + +📖 [More Info](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events) + +```go +import ( + "github.com/gofiber/fiber/v2" + "github.com/valyala/fasthttp" +) + +func main() { + app := fiber.New() + + app.Get("/sse", func(c *fiber.Ctx) error { + c.Set("Content-Type", "text/event-stream") + c.Set("Cache-Control", "no-cache") + c.Set("Connection", "keep-alive") + c.Set("Transfer-Encoding", "chunked") + + c.Context().SetBodyStreamWriter(fasthttp.StreamWriter(func(w *bufio.Writer) { + fmt.Println("WRITER") + var i int + + for { + i++ + msg := fmt.Sprintf("%d - the time is %v", i, time.Now()) + fmt.Fprintf(w, "data: Message: %s\n\n", msg) + fmt.Println(msg) + + w.Flush() + time.Sleep(5 * time.Second) + } + })) + + return nil + }) + + log.Fatal(app.Listen(":3000")) +} +``` + +### Recover middleware + +📖 [Recover](https://docs.gofiber.io/api/middleware/recover) + +```go +import ( + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/recover" +) + +func main() { + app := fiber.New() + + app.Use(recover.New()) + + app.Get("/", func(c *fiber.Ctx) error { + panic("normally this would crash your app") + }) + + log.Fatal(app.Listen(":3000")) +} +``` + +
+ +## 🧬 内部ミドルウェア + +以下は Fiber フレームワークに含まれるミドルウェアの一覧です。 + +| Middleware | Description | +| :------------------------------------------------------------------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [basicauth](https://github.com/gofiber/fiber/tree/master/middleware/basicauth) | Basic auth middleware provides an HTTP basic authentication. It calls the next handler for valid credentials and 401 Unauthorized for missing or invalid credentials. | +| [cache](https://github.com/gofiber/fiber/tree/master/middleware/cache) | Intercept and cache responses | +| [compress](https://github.com/gofiber/fiber/tree/master/middleware/compress) | Compression middleware for Fiber, it supports `deflate`, `gzip` and `brotli` by default. | +| [cors](https://github.com/gofiber/fiber/tree/master/middleware/cors) | Enable cross-origin resource sharing \(CORS\) with various options. | +| [csrf](https://github.com/gofiber/fiber/tree/master/middleware/csrf) | Protect from CSRF exploits. | +| [encryptcookie](https://github.com/gofiber/fiber/tree/master/middleware/encryptcookie) | Encrypt middleware which encrypts cookie values. | +| [envvar](https://github.com/gofiber/fiber/tree/master/middleware/envvar) | Expose environment variables with providing an optional config. | +| [etag](https://github.com/gofiber/fiber/tree/master/middleware/etag) | ETag middleware that lets caches be more efficient and save bandwidth, as a web server does not need to resend a full response if the content has not changed. | +| [expvar](https://github.com/gofiber/fiber/tree/master/middleware/expvar) | Expvar middleware that serves via its HTTP server runtime exposed variants in the JSON format. | +| [favicon](https://github.com/gofiber/fiber/tree/master/middleware/favicon) | Ignore favicon from logs or serve from memory if a file path is provided. | +| [filesystem](https://github.com/gofiber/fiber/tree/master/middleware/filesystem) | FileSystem middleware for Fiber, special thanks and credits to Alireza Salary | +| [limiter](https://github.com/gofiber/fiber/tree/master/middleware/limiter) | Rate-limiting middleware for Fiber. Use to limit repeated requests to public APIs and/or endpoints such as password reset. | +| [logger](https://github.com/gofiber/fiber/tree/master/middleware/logger) | HTTP request/response logger. | +| [monitor](https://github.com/gofiber/fiber/tree/master/middleware/monitor) | Monitor middleware that reports server metrics, inspired by express-status-monitor | +| [pprof](https://github.com/gofiber/fiber/tree/master/middleware/pprof) | Special thanks to Matthew Lee \(@mthli\) | +| [proxy](https://github.com/gofiber/fiber/tree/master/middleware/proxy) | Allows you to proxy requests to a multiple servers | +| [recover](https://github.com/gofiber/fiber/tree/master/middleware/recover) | Recover middleware recovers from panics anywhere in the stack chain and handles the control to the centralized[ ErrorHandler](https://docs.gofiber.io/guide/error-handling). | +| [requestid](https://github.com/gofiber/fiber/tree/master/middleware/requestid) | Adds a requestid to every request. | +| [session](https://github.com/gofiber/fiber/tree/master/middleware/session) | Session middleware. NOTE: This middleware uses our Storage package. | +| [skip](https://github.com/gofiber/fiber/tree/master/middleware/skip) | Skip middleware that skips a wrapped handler is a predicate is true. | +| [timeout](https://github.com/gofiber/fiber/tree/master/middleware/timeout) | Adds a max time for a request and forwards to ErrorHandler if it is exceeded. | +| [keyauth](https://github.com/gofiber/keyauth) | Key auth middleware provides a key based authentication. | +| [redirect](https://github.com/gofiber/redirect) | Redirect middleware | +| [rewrite](https://github.com/gofiber/rewrite) | Rewrite middleware rewrites the URL path based on provided rules. It can be helpful for backward compatibility or just creating cleaner and more descriptive links. | +| [adaptor](https://github.com/gofiber/adaptor) | Converter for net/http handlers to/from Fiber request handlers, special thanks to @arsmn! | +| [helmet](https://github.com/gofiber/helmet) | Helps secure your apps by setting various HTTP headers. | + +## 🧬 外部ミドルウェア + +[Fiber team](https://github.com/orgs/gofiber/people) により管理・運用されているミドルウェアの一覧です。 + +| Middleware | Description | +| :------------------------------------------------ | :-------------------------------------------------------------------------------------------------------------------- | +| [jwt](https://github.com/gofiber/jwt) | JWT returns a JSON Web Token \(JWT\) auth middleware. | +| [storage](https://github.com/gofiber/storage) | Premade storage drivers that implement the Storage interface, designed to be used with various Fiber middlewares. | +| [template](https://github.com/gofiber/template) | This package contains 8 template engines that can be used with Fiber `v1.10.x` Go version 1.13 or higher is required. | +| [websocket](https://github.com/gofiber/websocket) | Based on Fasthttp WebSocket for Fiber with Locals support! | + +## 🕶️ Awesome List + +その他の記事、ミドルウェア、サンプル、ツールについては、私たちの[awesome list](https://github.com/gofiber/awesome-fiber)をご覧ください。 + +## 👍 貢献する + +`Fiber`に開発支援してくださるなら: + +1. [GitHub Star](https://github.com/gofiber/fiber/stargazers)をつけてください 。 +2. [あなたの 𝕏 (Twitter) で](https://x.com/intent/tweet?text=Fiber%20is%20an%20Express%20inspired%20%23web%20%23framework%20built%20on%20top%20of%20Fasthttp%2C%20the%20fastest%20HTTP%20engine%20for%20%23Go.%20Designed%20to%20ease%20things%20up%20for%20%23fast%20development%20with%20zero%20memory%20allocation%20and%20%23performance%20in%20mind%20%F0%9F%9A%80%20https%3A%2F%2Fgithub.com%2Fgofiber%2Ffiber)プロジェクトについてツイートしてください。 +3. [Medium](https://medium.com/) 、 [Dev.to](https://dev.to/)、または個人のブログでレビューやチュートリアルを書いてください。 +4. [cup of coffee](https://buymeacoff.ee/fenny)の寄付でプロジェクトを支援しましょう。 + +## ☕ サポーター + +Fiber はオープンソースプロジェクトで、寄付によってドメイン名や gitbook、 netlify、そしてサーバーレスホスティングなどの費用を賄っています。もし Fiber を支援したければ ☕ [**こちらから**](https://buymeacoff.ee/fenny)。 + +| | User | Donation | +| :--------------------------------------------------------- | :----------------------------------------------- | :------- | +| ![](https://avatars.githubusercontent.com/u/204341?s=25) | [@destari](https://github.com/destari) | ☕ x 10 | +| ![](https://avatars.githubusercontent.com/u/63164982?s=25) | [@dembygenesis](https://github.com/dembygenesis) | ☕ x 5 | +| ![](https://avatars.githubusercontent.com/u/56607882?s=25) | [@thomasvvugt](https://github.com/thomasvvugt) | ☕ x 5 | +| ![](https://avatars.githubusercontent.com/u/27820675?s=25) | [@hendratommy](https://github.com/hendratommy) | ☕ x 5 | +| ![](https://avatars.githubusercontent.com/u/1094221?s=25) | [@ekaputra07](https://github.com/ekaputra07) | ☕ x 5 | +| ![](https://avatars.githubusercontent.com/u/194590?s=25) | [@jorgefuertes](https://github.com/jorgefuertes) | ☕ x 5 | +| ![](https://avatars.githubusercontent.com/u/186637?s=25) | [@candidosales](https://github.com/candidosales) | ☕ x 5 | +| ![](https://avatars.githubusercontent.com/u/29659953?s=25) | [@l0nax](https://github.com/l0nax) | ☕ x 3 | +| ![](https://avatars.githubusercontent.com/u/635852?s=25) | [@bihe](https://github.com/bihe) | ☕ x 3 | +| ![](https://avatars.githubusercontent.com/u/307334?s=25) | [@justdave](https://github.com/justdave) | ☕ x 3 | +| ![](https://avatars.githubusercontent.com/u/11155743?s=25) | [@koddr](https://github.com/koddr) | ☕ x 1 | +| ![](https://avatars.githubusercontent.com/u/29042462?s=25) | [@lapolinar](https://github.com/lapolinar) | ☕ x 1 | +| ![](https://avatars.githubusercontent.com/u/2978730?s=25) | [@diegowifi](https://github.com/diegowifi) | ☕ x 1 | +| ![](https://avatars.githubusercontent.com/u/44171355?s=25) | [@ssimk0](https://github.com/ssimk0) | ☕ x 1 | +| ![](https://avatars.githubusercontent.com/u/5638101?s=25) | [@raymayemir](https://github.com/raymayemir) | ☕ x 1 | +| ![](https://avatars.githubusercontent.com/u/619996?s=25) | [@melkorm](https://github.com/melkorm) | ☕ x 1 | +| ![](https://avatars.githubusercontent.com/u/31022056?s=25) | [@marvinjwendt](https://github.com/thomasvvugt) | ☕ x 1 | +| ![](https://avatars.githubusercontent.com/u/31921460?s=25) | [@toishy](https://github.com/toishy) | ☕ x 1 | + +## ‎‍💻 Code Contributors + +Code Contributors + +## ⭐️ Stargazers + +Stargazers over time + +## ⚠️ License + +Copyright (c) 2019-present [Fenny](https://github.com/fenny) and [Contributors](https://github.com/gofiber/fiber/graphs/contributors). `Fiber` is free and open-source software licensed under the [MIT License](https://github.com/gofiber/fiber/blob/master/LICENSE). Official logo was created by [Vic Shóstak](https://github.com/koddr) and distributed under [Creative Commons](https://creativecommons.org/licenses/by-sa/4.0/) license (CC BY-SA 4.0 International). + +**Third-party library licenses** + +- [colorable](https://github.com/mattn/go-colorable/blob/master/LICENSE) +- [isatty](https://github.com/mattn/go-isatty/blob/master/LICENSE) +- [runewidth](https://github.com/mattn/go-runewidth/blob/master/LICENSE) +- [fasthttp](https://github.com/valyala/fasthttp/blob/master/LICENSE) +- [bytebufferpool](https://github.com/valyala/bytebufferpool/blob/master/LICENSE) +- [fwd](https://github.com/philhofer/fwd/blob/master/LICENSE.md) +- [go-ole](https://github.com/go-ole/go-ole/blob/master/LICENSE) +- [gopsutil](https://github.com/shirou/gopsutil/blob/master/LICENSE) +- [msgp](https://github.com/tinylib/msgp/blob/master/LICENSE) +- [schema](https://github.com/gorilla/schema/blob/master/LICENSE) +- [uuid](https://github.com/google/uuid/blob/master/LICENSE) +- [wmi](https://github.com/StackExchange/wmi/blob/master/LICENSE) diff --git a/.github/README_ko.md b/.github/README_ko.md new file mode 100644 index 0000000..c2ed6af --- /dev/null +++ b/.github/README_ko.md @@ -0,0 +1,693 @@ +

+ + + + + Fiber + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + +

+

+ FiberExpress에서 영감을 받고, Go를 위한 가장 빠른 HTTP 엔진인 Fasthttp를 토대로 만들어진 웹 프레임워크 입니다. 비 메모리 할당성능을 고려한 빠른 개발을 위해 손쉽게 사용되도록 설계되었습니다. +

+ +## ⚡️ 빠른 시작 + +```go +package main + +import "github.com/gofiber/fiber/v2" + +func main() { + app := fiber.New() + + app.Get("/", func(c *fiber.Ctx) error { + return c.SendString("Hello, World 👋!") + }) + + app.Listen(":3000") +} +``` + +## 🤖 벤치마크 + +이 테스트들은 [TechEmpower](https://www.techempower.com/benchmarks/#section=data-r19&hw=ph&test=plaintext)와 [Go Web](https://github.com/smallnest/go-web-framework-benchmark)을 통해 측정되었습니다. 만약 모든 결과를 보고 싶다면, [Wiki](https://docs.gofiber.io/extra/benchmarks)를 확인해 주세요. + +

+ + +

+ +## ⚙️ 설치 + +Go가 설치되어 있는 것을 확인해 주세요 ([download](https://go.dev/dl/)). 버전 1.17 또는 그 이상이어야 합니다. + +폴더를 생성하여 당신의 프로젝트를 초기화하고, 폴더 안에서 `go mod init github.com/your/repo` ([learn more](https://go.dev/blog/using-go-modules)) 를 실행하세요. 그리고 [`go get`](https://pkg.go.dev/cmd/go/#hdr-Add_dependencies_to_current_module_and_install_them) 명령어로 Fiber를 설치하세요: + +```bash +go get -u github.com/gofiber/fiber/v2 +``` + +## 🎯 특징 + +- 견고한 [라우팅](https://docs.gofiber.io/guide/routing) +- [정적 파일](https://docs.gofiber.io/api/app#static) 제공 +- 뛰어난 [성능](https://docs.gofiber.io/extra/benchmarks) +- [적은 메모리](https://docs.gofiber.io/extra/benchmarks) 공간 +- [API 엔드포인트](https://docs.gofiber.io/api/ctx) +- 미들웨어 & [Next](https://docs.gofiber.io/api/ctx#next) 지원 +- [빠른](https://dev.to/koddr/welcome-to-fiber-an-express-js-styled-fastest-web-framework-written-with-on-golang-497) 서버 사이드 프로그래밍 +- [Template engines](https://github.com/gofiber/template) +- [WebSocket support](https://github.com/gofiber/websocket) +- [Server-Sent events](https://github.com/gofiber/recipes/tree/master/sse) +- [Rate Limiter](https://docs.gofiber.io/api/middleware/limiter) +- Available in [19 languages](https://docs.gofiber.io/) +- 더 알고 싶다면, [Fiber 둘러보기](https://docs.gofiber.io/) + +## 💡 철학 + +[Node.js](https://nodejs.org/en/about/)에서 [Go](https://go.dev/doc/)로 전환하는 새로운 고퍼분들은 웹 어플리케이션이나 마이크로 서비스 개발을 시작할 수 있게 되기 전에 학습 곡선에 시달리고 있습니다. Fiber는 **웹 프레임워크**로서, 새로운 고퍼분들이 따뜻하고 믿음직한 환영을 가지고 빠르게 Go의 세상에 진입할 수 있게 **미니멀리즘**의 개념과 **UNIX 방식**에 따라 개발되었습니다. + +Fiber는 인터넷에서 가장 인기있는 웹 프레임워크인 Express에서 **영감을 받았습니다.** 우리는 Express의 **쉬운** 사용과 Go의 **성능**을 결합하였습니다. 만약 당신이 Node.js (Express 또는 비슷한 것을 사용하여) 로 웹 어플리케이션을 개발한 경험이 있다면, 많은 메소드들과 원리들이 **매우 비슷하게** 느껴질 것 입니다. + +우리는 **어떤한** 작업, **마감일정**, 개발자의 **기술**이던간에 **빠르고**, **유연하고**, **익숙한** Go 웹 프레임워크를 만들기 위해 사용자들의 [이슈들](https://github.com/gofiber/fiber/issues)을(그리고 모든 인터넷을 통해) **듣고 있습니다**! Express가 자바스크립트 세계에서 하는 것 처럼요. + +## ⚠️ 한계점 + +- Fiber는 unsafe 패키지를 사용하기 때문에 최신 Go버전과 호환되지 않을 수 있습니다. Fiber 2.40.0은 Go 버전 1.17부터 1.21까지 테스트되고 있습니다. +- Fiber는 net/http 인터페이스와 호환되지 않습니다.즉, gqlgen이나 go-swagger 등 net/http 생태계의 일부인 프로젝트를 사용할 수 없습니다. + +## 👀 예제 + +다음은 일반적인 예제들 입니다. + +> 더 많은 코드 예제를 보고 싶다면, [Recipes 저장소](https://github.com/gofiber/recipes) 또는 [API 문서](https://docs.gofiber.io)를 방문하세요. + +Listed below are some of the common examples. If you want to see more code examples , please visit our [Recipes repository](https://github.com/gofiber/recipes) or visit our hosted [API documentation](https://docs.gofiber.io). + +#### 📖 [**Basic Routing**](https://docs.gofiber.io/#basic-routing) + +```go +func main() { + app := fiber.New() + + // GET /api/register + app.Get("/api/*", func(c *fiber.Ctx) error { + msg := fmt.Sprintf("✋ %s", c.Params("*")) + return c.SendString(msg) // => ✋ register + }) + + // GET /flights/LAX-SFO + app.Get("/flights/:from-:to", func(c *fiber.Ctx) error { + msg := fmt.Sprintf("💸 From: %s, To: %s", c.Params("from"), c.Params("to")) + return c.SendString(msg) // => 💸 From: LAX, To: SFO + }) + + // GET /dictionary.txt + app.Get("/:file.:ext", func(c *fiber.Ctx) error { + msg := fmt.Sprintf("📃 %s.%s", c.Params("file"), c.Params("ext")) + return c.SendString(msg) // => 📃 dictionary.txt + }) + + // GET /john/75 + app.Get("/:name/:age/:gender?", func(c *fiber.Ctx) error { + msg := fmt.Sprintf("👴 %s is %s years old", c.Params("name"), c.Params("age")) + return c.SendString(msg) // => 👴 john is 75 years old + }) + + // GET /john + app.Get("/:name", func(c *fiber.Ctx) error { + msg := fmt.Sprintf("Hello, %s 👋!", c.Params("name")) + return c.SendString(msg) // => Hello john 👋! + }) + + log.Fatal(app.Listen(":3000")) +} + +``` + +#### 📖 [**Route Naming**](https://docs.gofiber.io/api/app#name) + +```go +func main() { + app := fiber.New() + + // GET /api/register + app.Get("/api/*", func(c *fiber.Ctx) error { + msg := fmt.Sprintf("✋ %s", c.Params("*")) + return c.SendString(msg) // => ✋ register + }).Name("api") + + data, _ := json.MarshalIndent(app.GetRoute("api"), "", " ") + fmt.Print(string(data)) + // Prints: + // { + // "method": "GET", + // "name": "api", + // "path": "/api/*", + // "params": [ + // "*1" + // ] + // } + + + log.Fatal(app.Listen(":3000")) +} + +``` + +#### 📖 [**Serving Static Files**](https://docs.gofiber.io/api/app#static) + +```go +func main() { + app := fiber.New() + + app.Static("/", "./public") + // => http://localhost:3000/js/script.js + // => http://localhost:3000/css/style.css + + app.Static("/prefix", "./public") + // => http://localhost:3000/prefix/js/script.js + // => http://localhost:3000/prefix/css/style.css + + app.Static("*", "./public/index.html") + // => http://localhost:3000/any/path/shows/index/html + + log.Fatal(app.Listen(":3000")) +} + +``` + +#### 📖 [**Middleware & Next**](https://docs.gofiber.io/api/ctx#next) + +```go +func main() { + app := fiber.New() + + // Match any route + app.Use(func(c *fiber.Ctx) error { + fmt.Println("🥇 First handler") + return c.Next() + }) + + // Match all routes starting with /api + app.Use("/api", func(c *fiber.Ctx) error { + fmt.Println("🥈 Second handler") + return c.Next() + }) + + // GET /api/register + app.Get("/api/list", func(c *fiber.Ctx) error { + fmt.Println("🥉 Last handler") + return c.SendString("Hello, World 👋!") + }) + + log.Fatal(app.Listen(":3000")) +} + +``` + +
+ 📚 Show more code examples + +### Views engines + +📖 [Config](https://docs.gofiber.io/api/fiber#config) +📖 [Engines](https://github.com/gofiber/template) +📖 [Render](https://docs.gofiber.io/api/ctx#render) + +Fiber defaults to the [html/template](https://pkg.go.dev/html/template/) when no view engine is set. + +If you want to execute partials or use a different engine like [amber](https://github.com/eknkc/amber), [handlebars](https://github.com/aymerick/raymond), [mustache](https://github.com/cbroglie/mustache) or [pug](https://github.com/Joker/jade) etc.. + +Checkout our [Template](https://github.com/gofiber/template) package that support multiple view engines. + +```go +package main + +import ( + "github.com/gofiber/fiber/v2" + "github.com/gofiber/template/pug" +) + +func main() { + // You can setup Views engine before initiation app: + app := fiber.New(fiber.Config{ + Views: pug.New("./views", ".pug"), + }) + + // And now, you can call template `./views/home.pug` like this: + app.Get("/", func(c *fiber.Ctx) error { + return c.Render("home", fiber.Map{ + "title": "Homepage", + "year": 1999, + }) + }) + + log.Fatal(app.Listen(":3000")) +} +``` + +### Grouping routes into chains + +📖 [Group](https://docs.gofiber.io/api/app#group) + +```go +func middleware(c *fiber.Ctx) error { + fmt.Println("Don't mind me!") + return c.Next() +} + +func handler(c *fiber.Ctx) error { + return c.SendString(c.Path()) +} + +func main() { + app := fiber.New() + + // Root API route + api := app.Group("/api", middleware) // /api + + // API v1 routes + v1 := api.Group("/v1", middleware) // /api/v1 + v1.Get("/list", handler) // /api/v1/list + v1.Get("/user", handler) // /api/v1/user + + // API v2 routes + v2 := api.Group("/v2", middleware) // /api/v2 + v2.Get("/list", handler) // /api/v2/list + v2.Get("/user", handler) // /api/v2/user + + // ... +} + +``` + +### Middleware logger + +📖 [Logger](https://docs.gofiber.io/api/middleware/logger) + +```go +package main + +import ( + "log" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/logger" +) + +func main() { + app := fiber.New() + + app.Use(logger.New()) + + // ... + + log.Fatal(app.Listen(":3000")) +} +``` + +### Cross-Origin Resource Sharing (CORS) + +📖 [CORS](https://docs.gofiber.io/api/middleware/cors) + +```go +import ( + "log" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/cors" +) + +func main() { + app := fiber.New() + + app.Use(cors.New()) + + // ... + + log.Fatal(app.Listen(":3000")) +} +``` + +Check CORS by passing any domain in `Origin` header: + +```bash +curl -H "Origin: http://example.com" --verbose http://localhost:3000 +``` + +### Custom 404 response + +📖 [HTTP Methods](https://docs.gofiber.io/api/ctx#status) + +```go +func main() { + app := fiber.New() + + app.Static("/", "./public") + + app.Get("/demo", func(c *fiber.Ctx) error { + return c.SendString("This is a demo!") + }) + + app.Post("/register", func(c *fiber.Ctx) error { + return c.SendString("Welcome!") + }) + + // Last middleware to match anything + app.Use(func(c *fiber.Ctx) error { + return c.SendStatus(404) + // => 404 "Not Found" + }) + + log.Fatal(app.Listen(":3000")) +} +``` + +### JSON Response + +📖 [JSON](https://docs.gofiber.io/api/ctx#json) + +```go +type User struct { + Name string `json:"name"` + Age int `json:"age"` +} + +func main() { + app := fiber.New() + + app.Get("/user", func(c *fiber.Ctx) error { + return c.JSON(&User{"John", 20}) + // => {"name":"John", "age":20} + }) + + app.Get("/json", func(c *fiber.Ctx) error { + return c.JSON(fiber.Map{ + "success": true, + "message": "Hi John!", + }) + // => {"success":true, "message":"Hi John!"} + }) + + log.Fatal(app.Listen(":3000")) +} +``` + +### WebSocket Upgrade + +📖 [Websocket](https://github.com/gofiber/websocket) + +```go +import ( + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/websocket" +) + +func main() { + app := fiber.New() + + app.Get("/ws", websocket.New(func(c *websocket.Conn) { + for { + mt, msg, err := c.ReadMessage() + if err != nil { + log.Println("read:", err) + break + } + log.Printf("recv: %s", msg) + err = c.WriteMessage(mt, msg) + if err != nil { + log.Println("write:", err) + break + } + } + })) + + log.Fatal(app.Listen(":3000")) + // ws://localhost:3000/ws +} +``` + +### Server-Sent Events + +📖 [More Info](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events) + +```go +import ( + "github.com/gofiber/fiber/v2" + "github.com/valyala/fasthttp" +) + +func main() { + app := fiber.New() + + app.Get("/sse", func(c *fiber.Ctx) error { + c.Set("Content-Type", "text/event-stream") + c.Set("Cache-Control", "no-cache") + c.Set("Connection", "keep-alive") + c.Set("Transfer-Encoding", "chunked") + + c.Context().SetBodyStreamWriter(fasthttp.StreamWriter(func(w *bufio.Writer) { + fmt.Println("WRITER") + var i int + + for { + i++ + msg := fmt.Sprintf("%d - the time is %v", i, time.Now()) + fmt.Fprintf(w, "data: Message: %s\n\n", msg) + fmt.Println(msg) + + w.Flush() + time.Sleep(5 * time.Second) + } + })) + + return nil + }) + + log.Fatal(app.Listen(":3000")) +} +``` + +### Recover middleware + +📖 [Recover](https://docs.gofiber.io/api/middleware/recover) + +```go +import ( + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/recover" +) + +func main() { + app := fiber.New() + + app.Use(recover.New()) + + app.Get("/", func(c *fiber.Ctx) error { + panic("normally this would crash your app") + }) + + log.Fatal(app.Listen(":3000")) +} +``` + +
+ +## 🧬 Internal Middleware + +Fiber 프레임워크에 포함되는 미들웨어 목록입니다. + +| Middleware | Description | +| :------------------------------------------------------------------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [basicauth](https://github.com/gofiber/fiber/tree/master/middleware/basicauth) | Basic auth middleware provides an HTTP basic authentication. It calls the next handler for valid credentials and 401 Unauthorized for missing or invalid credentials. | +| [cache](https://github.com/gofiber/fiber/tree/master/middleware/cache) | Intercept and cache responses | +| [compress](https://github.com/gofiber/fiber/tree/master/middleware/compress) | Compression middleware for Fiber, it supports `deflate`, `gzip` and `brotli` by default. | +| [cors](https://github.com/gofiber/fiber/tree/master/middleware/cors) | Enable cross-origin resource sharing \(CORS\) with various options. | +| [csrf](https://github.com/gofiber/fiber/tree/master/middleware/csrf) | Protect from CSRF exploits. | +| [encryptcookie](https://github.com/gofiber/fiber/tree/master/middleware/encryptcookie) | Encrypt middleware which encrypts cookie values. | +| [envvar](https://github.com/gofiber/fiber/tree/master/middleware/envvar) | Expose environment variables with providing an optional config. | +| [etag](https://github.com/gofiber/fiber/tree/master/middleware/etag) | ETag middleware that lets caches be more efficient and save bandwidth, as a web server does not need to resend a full response if the content has not changed. | +| [expvar](https://github.com/gofiber/fiber/tree/master/middleware/expvar) | Expvar middleware that serves via its HTTP server runtime exposed variants in the JSON format. | +| [favicon](https://github.com/gofiber/fiber/tree/master/middleware/favicon) | Ignore favicon from logs or serve from memory if a file path is provided. | +| [filesystem](https://github.com/gofiber/fiber/tree/master/middleware/filesystem) | FileSystem middleware for Fiber, special thanks and credits to Alireza Salary | +| [limiter](https://github.com/gofiber/fiber/tree/master/middleware/limiter) | Rate-limiting middleware for Fiber. Use to limit repeated requests to public APIs and/or endpoints such as password reset. | +| [logger](https://github.com/gofiber/fiber/tree/master/middleware/logger) | HTTP request/response logger. | +| [monitor](https://github.com/gofiber/fiber/tree/master/middleware/monitor) | Monitor middleware that reports server metrics, inspired by express-status-monitor | +| [pprof](https://github.com/gofiber/fiber/tree/master/middleware/pprof) | Special thanks to Matthew Lee \(@mthli\) | +| [proxy](https://github.com/gofiber/fiber/tree/master/middleware/proxy) | Allows you to proxy requests to a multiple servers | +| [recover](https://github.com/gofiber/fiber/tree/master/middleware/recover) | Recover middleware recovers from panics anywhere in the stack chain and handles the control to the centralized[ ErrorHandler](https://docs.gofiber.io/guide/error-handling). | +| [requestid](https://github.com/gofiber/fiber/tree/master/middleware/requestid) | Adds a requestid to every request. | +| [session](https://github.com/gofiber/fiber/tree/master/middleware/session) | Session middleware. NOTE: This middleware uses our Storage package. | +| [skip](https://github.com/gofiber/fiber/tree/master/middleware/skip) | Skip middleware that skips a wrapped handler is a predicate is true. | +| [timeout](https://github.com/gofiber/fiber/tree/master/middleware/timeout) | Adds a max time for a request and forwards to ErrorHandler if it is exceeded. | +| [keyauth](https://github.com/gofiber/keyauth) | Key auth middleware provides a key based authentication. | +| [redirect](https://github.com/gofiber/redirect) | Redirect middleware | +| [rewrite](https://github.com/gofiber/rewrite) | Rewrite middleware rewrites the URL path based on provided rules. It can be helpful for backward compatibility or just creating cleaner and more descriptive links. | +| [adaptor](https://github.com/gofiber/adaptor) | Converter for net/http handlers to/from Fiber request handlers, special thanks to @arsmn! | +| [helmet](https://github.com/gofiber/helmet) | Helps secure your apps by setting various HTTP headers. | + +## 🧬 External Middleware + +[Fiber team](https://github.com/orgs/gofiber/people)에 의해 관리 및 운용되고 있는 미들웨어 목록입니다. + +| Middleware | Description | +| :------------------------------------------------ | :-------------------------------------------------------------------------------------------------------------------- | +| [jwt](https://github.com/gofiber/jwt) | JWT returns a JSON Web Token \(JWT\) auth middleware. | +| [storage](https://github.com/gofiber/storage) | Premade storage drivers that implement the Storage interface, designed to be used with various Fiber middlewares. | +| [template](https://github.com/gofiber/template) | This package contains 8 template engines that can be used with Fiber `v1.10.x` Go version 1.13 or higher is required. | +| [websocket](https://github.com/gofiber/websocket) | Based on Fasthttp WebSocket for Fiber with Locals support! | + +## 🕶️ Awesome List + +For more articles, middlewares, examples or tools check our [awesome list](https://github.com/gofiber/awesome-fiber). + +## 👍 기여 + +`Fiber`의 활발한 개발을 지원하고 감사 인사를 하고 싶다면: + +1. 프로젝트에 [GitHub Star](https://github.com/gofiber/fiber/stargazers)를 추가하세요. +2. [트위터에서](https://x.com/intent/tweet?text=Fiber%20is%20an%20Express%20inspired%20%23web%20%23framework%20built%20on%20top%20of%20Fasthttp%2C%20the%20fastest%20HTTP%20engine%20for%20%23Go.%20Designed%20to%20ease%20things%20up%20for%20%23fast%20development%20with%20zero%20memory%20allocation%20and%20%23performance%20in%20mind%20%F0%9F%9A%80%20https%3A%2F%2Fgithub.com%2Fgofiber%2Ffiber) 프로젝트에 대해 트윗하세요. +3. [Medium](https://medium.com/), [Dev.to](https://dev.to/) 또는 개인 블로그에 리뷰 또는 튜토리얼을 작성하세요. +4. Support the project by donating a [cup of coffee](https://buymeacoff.ee/fenny). + +## ☕ 후원자 + +Fiber는 오픈소스 프로젝트로, 기부를 통해 도메인 이름, gitbook, netlify, 서버리스 호스팅 등의 비용을 충당하고 있습니다. Fiber를 후원하고 싶다면 ☕ [**여기서 커피를 사주세요**](https://buymeacoff.ee/fenny). + +| | User | Donation | +| :--------------------------------------------------------- | :----------------------------------------------- | :------- | +| ![](https://avatars.githubusercontent.com/u/204341?s=25) | [@destari](https://github.com/destari) | ☕ x 10 | +| ![](https://avatars.githubusercontent.com/u/63164982?s=25) | [@dembygenesis](https://github.com/dembygenesis) | ☕ x 5 | +| ![](https://avatars.githubusercontent.com/u/56607882?s=25) | [@thomasvvugt](https://github.com/thomasvvugt) | ☕ x 5 | +| ![](https://avatars.githubusercontent.com/u/27820675?s=25) | [@hendratommy](https://github.com/hendratommy) | ☕ x 5 | +| ![](https://avatars.githubusercontent.com/u/1094221?s=25) | [@ekaputra07](https://github.com/ekaputra07) | ☕ x 5 | +| ![](https://avatars.githubusercontent.com/u/194590?s=25) | [@jorgefuertes](https://github.com/jorgefuertes) | ☕ x 5 | +| ![](https://avatars.githubusercontent.com/u/186637?s=25) | [@candidosales](https://github.com/candidosales) | ☕ x 5 | +| ![](https://avatars.githubusercontent.com/u/29659953?s=25) | [@l0nax](https://github.com/l0nax) | ☕ x 3 | +| ![](https://avatars.githubusercontent.com/u/635852?s=25) | [@bihe](https://github.com/bihe) | ☕ x 3 | +| ![](https://avatars.githubusercontent.com/u/307334?s=25) | [@justdave](https://github.com/justdave) | ☕ x 3 | +| ![](https://avatars.githubusercontent.com/u/11155743?s=25) | [@koddr](https://github.com/koddr) | ☕ x 1 | +| ![](https://avatars.githubusercontent.com/u/29042462?s=25) | [@lapolinar](https://github.com/lapolinar) | ☕ x 1 | +| ![](https://avatars.githubusercontent.com/u/2978730?s=25) | [@diegowifi](https://github.com/diegowifi) | ☕ x 1 | +| ![](https://avatars.githubusercontent.com/u/44171355?s=25) | [@ssimk0](https://github.com/ssimk0) | ☕ x 1 | +| ![](https://avatars.githubusercontent.com/u/5638101?s=25) | [@raymayemir](https://github.com/raymayemir) | ☕ x 1 | +| ![](https://avatars.githubusercontent.com/u/619996?s=25) | [@melkorm](https://github.com/melkorm) | ☕ x 1 | +| ![](https://avatars.githubusercontent.com/u/31022056?s=25) | [@marvinjwendt](https://github.com/thomasvvugt) | ☕ x 1 | +| ![](https://avatars.githubusercontent.com/u/31921460?s=25) | [@toishy](https://github.com/toishy) | ☕ x 1 | + +## ‎‍💻 Code Contributors + +Code Contributors + +## ⭐️ Stargazers + +Stargazers over time + +## ⚠️ License + +Copyright (c) 2019-present [Fenny](https://github.com/fenny) and [Contributors](https://github.com/gofiber/fiber/graphs/contributors). `Fiber` is free and open-source software licensed under the [MIT License](https://github.com/gofiber/fiber/blob/master/LICENSE). Official logo was created by [Vic Shóstak](https://github.com/koddr) and distributed under [Creative Commons](https://creativecommons.org/licenses/by-sa/4.0/) license (CC BY-SA 4.0 International). + +**Third-party library licenses** + +- [colorable](https://github.com/mattn/go-colorable/blob/master/LICENSE) +- [isatty](https://github.com/mattn/go-isatty/blob/master/LICENSE) +- [runewidth](https://github.com/mattn/go-runewidth/blob/master/LICENSE) +- [fasthttp](https://github.com/valyala/fasthttp/blob/master/LICENSE) +- [bytebufferpool](https://github.com/valyala/bytebufferpool/blob/master/LICENSE) +- [fwd](https://github.com/philhofer/fwd/blob/master/LICENSE.md) +- [go-ole](https://github.com/go-ole/go-ole/blob/master/LICENSE) +- [gopsutil](https://github.com/shirou/gopsutil/blob/master/LICENSE) +- [msgp](https://github.com/tinylib/msgp/blob/master/LICENSE) +- [schema](https://github.com/gorilla/schema/blob/master/LICENSE) +- [uuid](https://github.com/google/uuid/blob/master/LICENSE) +- [wmi](https://github.com/StackExchange/wmi/blob/master/LICENSE) diff --git a/.github/README_nl.md b/.github/README_nl.md new file mode 100644 index 0000000..e31040a --- /dev/null +++ b/.github/README_nl.md @@ -0,0 +1,693 @@ +

+ + + + + Fiber + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + +

+

+ Fiber is een web framework geïnspireerd door Express gebouwd bovenop Fasthttp, de snelste HTTP-engine voor Go. Ontworpen om snelle ontwikkeling gemakkelijker te maken zonder geheugenallocatie tezamen met hoge prestaties. +

+ +## ⚡️ Bliksemsnelle start + +```go +package main + +import "github.com/gofiber/fiber/v2" + +func main() { + app := fiber.New() + + app.Get("/", func(c *fiber.Ctx) error { + return c.SendString("Hello, World 👋!") + }) + + app.Listen(":3000") +} +``` + +## 🤖 Benchmarks + +Deze tests zijn uitgevoerd door [TechEmpower](https://www.techempower.com/benchmarks/#section=data-r19&hw=ph&test=plaintext) en [Go Web](https://github.com/smallnest/go-web-framework-benchmark). Bezoek onze [Wiki](https://fiber.wiki/benchmarks) voor alle benchmark resultaten. + +

+ + +

+ +## ⚙️ Installatie + +Make sure you have Go installed ([download](https://go.dev/dl/)). Version `1.17` or higher is required. + +Initialize your project by creating a folder and then running `go mod init github.com/your/repo` ([learn more](https://go.dev/blog/using-go-modules)) inside the folder. Then install Fiber with the [`go get`](https://pkg.go.dev/cmd/go/#hdr-Add_dependencies_to_current_module_and_install_them) command: + +```bash +go get -u github.com/gofiber/fiber/v2 +``` + +## 🎯 Features + +- Robuuste [routing](https://docs.gofiber.io/guide/routing) +- Serveer [statische bestanden](https://docs.gofiber.io/api/app#static) +- Extreme [prestaties](https://docs.gofiber.io/extra/benchmarks) +- [Weinig geheugenruimte](https://docs.gofiber.io/extra/benchmarks) +- [API endpoints](https://docs.gofiber.io/api/ctx) +- [Middleware](https://docs.gofiber.io/category/-middleware) & [Next](https://docs.gofiber.io/api/ctx#next) ondersteuning +- [Snelle](https://dev.to/koddr/welcome-to-fiber-an-express-js-styled-fastest-web-framework-written-with-on-golang-497) server-side programmering +- [Template engines](https://github.com/gofiber/template) +- [WebSocket ondersteuning](https://github.com/gofiber/websocket) +- [Server-Sent events](https://github.com/gofiber/recipes/tree/master/sse) +- [Rate Limiter](https://docs.gofiber.io/category/-middleware/limiter) +- Vertaald in [19 talen](https://docs.gofiber.io/) +- En nog veel meer, [ontdek Fiber](https://docs.gofiber.io/) + +## 💡 Filosofie + +Nieuwe gophers die de overstap maken van [Node.js](https://nodejs.org/en/about/) naar [Go](https://go.dev/doc/), hebben te maken met een leercurve voordat ze kunnen beginnen met het bouwen van hun webapplicaties of microservices. Fiber, als een **web framework**, is gebouwd met het idee van **minimalisme** en volgt de **UNIX-manier**, zodat nieuwe gophers snel de wereld van Go kunnen betreden met een warm en vertrouwd welkom.\ + +Fiber is **geïnspireerd** door Express, het populairste webframework op internet. We hebben het **gemak** van Express gecombineerd met de **onbewerkte prestaties** van Go. Als je ooit een webapplicatie in Node.js hebt geïmplementeerd (_zoals Express of vergelijkbaar_), dan zullen veel methoden en principes **heel gewoon** voor je lijken. + +We **luisteren** naar onze gebruikers in [issues](https://github.com/gofiber/fiber/issues) (_en overal op het internet_) om een **snelle**, **flexibele** en **vriendelijk** Go web framework te maken voor **elke** taak, **deadline** en ontwikkelaar **vaardigheid**! Net zoals Express dat doet in de JavaScript-wereld. + +## ⚠️ Limitations + +- Due to Fiber's usage of unsafe, the library may not always be compatible with the latest Go version. Fiber 2.40.0 has been tested with Go versions 1.17 to 1.21. +- Fiber is not compatible with net/http interfaces. This means you will not be able to use projects like gqlgen, go-swagger, or any others which are part of the net/http ecosystem. + +## 👀 Voorbeelden + +Hieronder staan enkele van de meest voorkomende voorbeelden. + +> Bekijk ons [Recepten repository](https://github.com/gofiber/recipes) voor meer voorbeelden met code of bezoek onze [API documentatie](https://fiber.wiki). + +Listed below are some of the common examples. If you want to see more code examples , please visit our [Recipes repository](https://github.com/gofiber/recipes) or visit our hosted [API documentation](https://docs.gofiber.io). + +#### 📖 [**Basic Routing**](https://docs.gofiber.io/#basic-routing) + +```go +func main() { + app := fiber.New() + + // GET /api/register + app.Get("/api/*", func(c *fiber.Ctx) error { + msg := fmt.Sprintf("✋ %s", c.Params("*")) + return c.SendString(msg) // => ✋ register + }) + + // GET /flights/LAX-SFO + app.Get("/flights/:from-:to", func(c *fiber.Ctx) error { + msg := fmt.Sprintf("💸 From: %s, To: %s", c.Params("from"), c.Params("to")) + return c.SendString(msg) // => 💸 From: LAX, To: SFO + }) + + // GET /dictionary.txt + app.Get("/:file.:ext", func(c *fiber.Ctx) error { + msg := fmt.Sprintf("📃 %s.%s", c.Params("file"), c.Params("ext")) + return c.SendString(msg) // => 📃 dictionary.txt + }) + + // GET /john/75 + app.Get("/:name/:age/:gender?", func(c *fiber.Ctx) error { + msg := fmt.Sprintf("👴 %s is %s years old", c.Params("name"), c.Params("age")) + return c.SendString(msg) // => 👴 john is 75 years old + }) + + // GET /john + app.Get("/:name", func(c *fiber.Ctx) error { + msg := fmt.Sprintf("Hello, %s 👋!", c.Params("name")) + return c.SendString(msg) // => Hello john 👋! + }) + + log.Fatal(app.Listen(":3000")) +} + +``` + +#### 📖 [**Route Naming**](https://docs.gofiber.io/api/app#name) + +```go +func main() { + app := fiber.New() + + // GET /api/register + app.Get("/api/*", func(c *fiber.Ctx) error { + msg := fmt.Sprintf("✋ %s", c.Params("*")) + return c.SendString(msg) // => ✋ register + }).Name("api") + + data, _ := json.MarshalIndent(app.GetRoute("api"), "", " ") + fmt.Print(string(data)) + // Prints: + // { + // "method": "GET", + // "name": "api", + // "path": "/api/*", + // "params": [ + // "*1" + // ] + // } + + + log.Fatal(app.Listen(":3000")) +} + +``` + +#### 📖 [**Serving Static Files**](https://docs.gofiber.io/api/app#static) + +```go +func main() { + app := fiber.New() + + app.Static("/", "./public") + // => http://localhost:3000/js/script.js + // => http://localhost:3000/css/style.css + + app.Static("/prefix", "./public") + // => http://localhost:3000/prefix/js/script.js + // => http://localhost:3000/prefix/css/style.css + + app.Static("*", "./public/index.html") + // => http://localhost:3000/any/path/shows/index/html + + log.Fatal(app.Listen(":3000")) +} + +``` + +#### 📖 [**Middleware & Next**](https://docs.gofiber.io/api/ctx#next) + +```go +func main() { + app := fiber.New() + + // Match any route + app.Use(func(c *fiber.Ctx) error { + fmt.Println("🥇 First handler") + return c.Next() + }) + + // Match all routes starting with /api + app.Use("/api", func(c *fiber.Ctx) error { + fmt.Println("🥈 Second handler") + return c.Next() + }) + + // GET /api/register + app.Get("/api/list", func(c *fiber.Ctx) error { + fmt.Println("🥉 Last handler") + return c.SendString("Hello, World 👋!") + }) + + log.Fatal(app.Listen(":3000")) +} + +``` + +
+ 📚 Show more code examples + +### Views engines + +📖 [Config](https://docs.gofiber.io/api/fiber#config) +📖 [Engines](https://github.com/gofiber/template) +📖 [Render](https://docs.gofiber.io/api/ctx#render) + +Fiber defaults to the [html/template](https://pkg.go.dev/html/template/) when no view engine is set. + +If you want to execute partials or use a different engine like [amber](https://github.com/eknkc/amber), [handlebars](https://github.com/aymerick/raymond), [mustache](https://github.com/cbroglie/mustache) or [pug](https://github.com/Joker/jade) etc.. + +Checkout our [Template](https://github.com/gofiber/template) package that support multiple view engines. + +```go +package main + +import ( + "github.com/gofiber/fiber/v2" + "github.com/gofiber/template/pug" +) + +func main() { + // You can setup Views engine before initiation app: + app := fiber.New(fiber.Config{ + Views: pug.New("./views", ".pug"), + }) + + // And now, you can call template `./views/home.pug` like this: + app.Get("/", func(c *fiber.Ctx) error { + return c.Render("home", fiber.Map{ + "title": "Homepage", + "year": 1999, + }) + }) + + log.Fatal(app.Listen(":3000")) +} +``` + +### Grouping routes into chains + +📖 [Group](https://docs.gofiber.io/api/app#group) + +```go +func middleware(c *fiber.Ctx) error { + fmt.Println("Don't mind me!") + return c.Next() +} + +func handler(c *fiber.Ctx) error { + return c.SendString(c.Path()) +} + +func main() { + app := fiber.New() + + // Root API route + api := app.Group("/api", middleware) // /api + + // API v1 routes + v1 := api.Group("/v1", middleware) // /api/v1 + v1.Get("/list", handler) // /api/v1/list + v1.Get("/user", handler) // /api/v1/user + + // API v2 routes + v2 := api.Group("/v2", middleware) // /api/v2 + v2.Get("/list", handler) // /api/v2/list + v2.Get("/user", handler) // /api/v2/user + + // ... +} + +``` + +### Middleware logger + +📖 [Logger](https://docs.gofiber.io/api/middleware/logger) + +```go +package main + +import ( + "log" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/logger" +) + +func main() { + app := fiber.New() + + app.Use(logger.New()) + + // ... + + log.Fatal(app.Listen(":3000")) +} +``` + +### Cross-Origin Resource Sharing (CORS) + +📖 [CORS](https://docs.gofiber.io/api/middleware/cors) + +```go +import ( + "log" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/cors" +) + +func main() { + app := fiber.New() + + app.Use(cors.New()) + + // ... + + log.Fatal(app.Listen(":3000")) +} +``` + +Check CORS by passing any domain in `Origin` header: + +```bash +curl -H "Origin: http://example.com" --verbose http://localhost:3000 +``` + +### Custom 404 response + +📖 [HTTP Methods](https://docs.gofiber.io/api/ctx#status) + +```go +func main() { + app := fiber.New() + + app.Static("/", "./public") + + app.Get("/demo", func(c *fiber.Ctx) error { + return c.SendString("This is a demo!") + }) + + app.Post("/register", func(c *fiber.Ctx) error { + return c.SendString("Welcome!") + }) + + // Last middleware to match anything + app.Use(func(c *fiber.Ctx) error { + return c.SendStatus(404) + // => 404 "Not Found" + }) + + log.Fatal(app.Listen(":3000")) +} +``` + +### JSON Response + +📖 [JSON](https://docs.gofiber.io/api/ctx#json) + +```go +type User struct { + Name string `json:"name"` + Age int `json:"age"` +} + +func main() { + app := fiber.New() + + app.Get("/user", func(c *fiber.Ctx) error { + return c.JSON(&User{"John", 20}) + // => {"name":"John", "age":20} + }) + + app.Get("/json", func(c *fiber.Ctx) error { + return c.JSON(fiber.Map{ + "success": true, + "message": "Hi John!", + }) + // => {"success":true, "message":"Hi John!"} + }) + + log.Fatal(app.Listen(":3000")) +} +``` + +### WebSocket Upgrade + +📖 [Websocket](https://github.com/gofiber/websocket) + +```go +import ( + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/websocket" +) + +func main() { + app := fiber.New() + + app.Get("/ws", websocket.New(func(c *websocket.Conn) { + for { + mt, msg, err := c.ReadMessage() + if err != nil { + log.Println("read:", err) + break + } + log.Printf("recv: %s", msg) + err = c.WriteMessage(mt, msg) + if err != nil { + log.Println("write:", err) + break + } + } + })) + + log.Fatal(app.Listen(":3000")) + // ws://localhost:3000/ws +} +``` + +### Server-Sent Events + +📖 [More Info](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events) + +```go +import ( + "github.com/gofiber/fiber/v2" + "github.com/valyala/fasthttp" +) + +func main() { + app := fiber.New() + + app.Get("/sse", func(c *fiber.Ctx) error { + c.Set("Content-Type", "text/event-stream") + c.Set("Cache-Control", "no-cache") + c.Set("Connection", "keep-alive") + c.Set("Transfer-Encoding", "chunked") + + c.Context().SetBodyStreamWriter(fasthttp.StreamWriter(func(w *bufio.Writer) { + fmt.Println("WRITER") + var i int + + for { + i++ + msg := fmt.Sprintf("%d - the time is %v", i, time.Now()) + fmt.Fprintf(w, "data: Message: %s\n\n", msg) + fmt.Println(msg) + + w.Flush() + time.Sleep(5 * time.Second) + } + })) + + return nil + }) + + log.Fatal(app.Listen(":3000")) +} +``` + +### Recover middleware + +📖 [Recover](https://docs.gofiber.io/api/middleware/recover) + +```go +import ( + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/recover" +) + +func main() { + app := fiber.New() + + app.Use(recover.New()) + + app.Get("/", func(c *fiber.Ctx) error { + panic("normally this would crash your app") + }) + + log.Fatal(app.Listen(":3000")) +} +``` + +
+ +## 🧬 Internal Middleware + +Here is a list of middleware that are included within the Fiber framework. + +| Middleware | Description | +| :------------------------------------------------------------------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [basicauth](https://github.com/gofiber/fiber/tree/master/middleware/basicauth) | Basic auth middleware provides an HTTP basic authentication. It calls the next handler for valid credentials and 401 Unauthorized for missing or invalid credentials. | +| [cache](https://github.com/gofiber/fiber/tree/master/middleware/cache) | Intercept and cache responses | +| [compress](https://github.com/gofiber/fiber/tree/master/middleware/compress) | Compression middleware for Fiber, it supports `deflate`, `gzip` and `brotli` by default. | +| [cors](https://github.com/gofiber/fiber/tree/master/middleware/cors) | Enable cross-origin resource sharing \(CORS\) with various options. | +| [csrf](https://github.com/gofiber/fiber/tree/master/middleware/csrf) | Protect from CSRF exploits. | +| [encryptcookie](https://github.com/gofiber/fiber/tree/master/middleware/encryptcookie) | Encrypt middleware which encrypts cookie values. | +| [envvar](https://github.com/gofiber/fiber/tree/master/middleware/envvar) | Expose environment variables with providing an optional config. | +| [etag](https://github.com/gofiber/fiber/tree/master/middleware/etag) | ETag middleware that lets caches be more efficient and save bandwidth, as a web server does not need to resend a full response if the content has not changed. | +| [expvar](https://github.com/gofiber/fiber/tree/master/middleware/expvar) | Expvar middleware that serves via its HTTP server runtime exposed variants in the JSON format. | +| [favicon](https://github.com/gofiber/fiber/tree/master/middleware/favicon) | Ignore favicon from logs or serve from memory if a file path is provided. | +| [filesystem](https://github.com/gofiber/fiber/tree/master/middleware/filesystem) | FileSystem middleware for Fiber, special thanks and credits to Alireza Salary | +| [limiter](https://github.com/gofiber/fiber/tree/master/middleware/limiter) | Rate-limiting middleware for Fiber. Use to limit repeated requests to public APIs and/or endpoints such as password reset. | +| [logger](https://github.com/gofiber/fiber/tree/master/middleware/logger) | HTTP request/response logger. | +| [monitor](https://github.com/gofiber/fiber/tree/master/middleware/monitor) | Monitor middleware that reports server metrics, inspired by express-status-monitor | +| [pprof](https://github.com/gofiber/fiber/tree/master/middleware/pprof) | Special thanks to Matthew Lee \(@mthli\) | +| [proxy](https://github.com/gofiber/fiber/tree/master/middleware/proxy) | Allows you to proxy requests to a multiple servers | +| [recover](https://github.com/gofiber/fiber/tree/master/middleware/recover) | Recover middleware recovers from panics anywhere in the stack chain and handles the control to the centralized[ ErrorHandler](https://docs.gofiber.io/guide/error-handling). | +| [requestid](https://github.com/gofiber/fiber/tree/master/middleware/requestid) | Adds a requestid to every request. | +| [session](https://github.com/gofiber/fiber/tree/master/middleware/session) | Session middleware. NOTE: This middleware uses our Storage package. | +| [skip](https://github.com/gofiber/fiber/tree/master/middleware/skip) | Skip middleware that skips a wrapped handler is a predicate is true. | +| [timeout](https://github.com/gofiber/fiber/tree/master/middleware/timeout) | Adds a max time for a request and forwards to ErrorHandler if it is exceeded. | +| [adaptor](https://github.com/gofiber/adaptor) | Converter for net/http handlers to/from Fiber request handlers, special thanks to @arsmn! | +| [helmet](https://github.com/gofiber/helmet) | Helps secure your apps by setting various HTTP headers. | +| [keyauth](https://github.com/gofiber/keyauth) | Key auth middleware provides a key based authentication. | +| [redirect](https://github.com/gofiber/redirect) | Redirect middleware | +| [rewrite](https://github.com/gofiber/rewrite) | Rewrite middleware rewrites the URL path based on provided rules. It can be helpful for backward compatibility or just creating cleaner and more descriptive links. | + +## 🧬 External Middleware + +List of externally hosted middleware modules and maintained by the [Fiber team](https://github.com/orgs/gofiber/people). + +| Middleware | Description | +| :------------------------------------------------ | :-------------------------------------------------------------------------------------------------------------------- | +| [jwt](https://github.com/gofiber/jwt) | JWT returns a JSON Web Token \(JWT\) auth middleware. | +| [storage](https://github.com/gofiber/storage) | Premade storage drivers that implement the Storage interface, designed to be used with various Fiber middlewares. | +| [template](https://github.com/gofiber/template) | This package contains 8 template engines that can be used with Fiber `v1.10.x` Go version 1.13 or higher is required. | +| [websocket](https://github.com/gofiber/websocket) | Based on Fasthttp WebSocket for Fiber with Locals support! | + +## 🕶️ Awesome List + +For more articles, middlewares, examples or tools check our [awesome list](https://github.com/gofiber/awesome-fiber). + +## 👍 Bijdragen + +Om de actieve ontwikkelingen van `Fiber` te ondersteunen of om een **bedankje** te geven: + +1. Voeg een [GitHub Star](https://github.com/gofiber/fiber/stargazers) toe aan het project. +2. Tweet over het project [op je 𝕏 (Twitter) account](https://x.com/intent/tweet?text=Fiber%20is%20an%20Express%20inspired%20%23web%20%23framework%20built%20on%20top%20of%20Fasthttp%2C%20the%20fastest%20HTTP%20engine%20for%20%23Go.%20Designed%20to%20ease%20things%20up%20for%20%23fast%20development%20with%20zero%20memory%20allocation%20and%20%23performance%20in%20mind%20%F0%9F%9A%80%20https%3A%2F%2Fgithub.com%2Fgofiber%2Ffiber). +3. Schrijf een recensie of tutorial op [Medium](https://medium.com/), [Dev.to](https://dev.to/) of een persoonlijke blog. +4. Support the project by donating a [cup of coffee](https://buymeacoff.ee/fenny). + +## ☕ Supporters + +Fiber is an open source project that runs on donations to pay the bills e.g. our domain name, gitbook, netlify and serverless hosting. If you want to support Fiber, you can ☕ [**buy a coffee here**](https://buymeacoff.ee/fenny). + +| | User | Donation | +| :--------------------------------------------------------- | :----------------------------------------------- | :------- | +| ![](https://avatars.githubusercontent.com/u/204341?s=25) | [@destari](https://github.com/destari) | ☕ x 10 | +| ![](https://avatars.githubusercontent.com/u/63164982?s=25) | [@dembygenesis](https://github.com/dembygenesis) | ☕ x 5 | +| ![](https://avatars.githubusercontent.com/u/56607882?s=25) | [@thomasvvugt](https://github.com/thomasvvugt) | ☕ x 5 | +| ![](https://avatars.githubusercontent.com/u/27820675?s=25) | [@hendratommy](https://github.com/hendratommy) | ☕ x 5 | +| ![](https://avatars.githubusercontent.com/u/1094221?s=25) | [@ekaputra07](https://github.com/ekaputra07) | ☕ x 5 | +| ![](https://avatars.githubusercontent.com/u/194590?s=25) | [@jorgefuertes](https://github.com/jorgefuertes) | ☕ x 5 | +| ![](https://avatars.githubusercontent.com/u/186637?s=25) | [@candidosales](https://github.com/candidosales) | ☕ x 5 | +| ![](https://avatars.githubusercontent.com/u/29659953?s=25) | [@l0nax](https://github.com/l0nax) | ☕ x 3 | +| ![](https://avatars.githubusercontent.com/u/635852?s=25) | [@bihe](https://github.com/bihe) | ☕ x 3 | +| ![](https://avatars.githubusercontent.com/u/307334?s=25) | [@justdave](https://github.com/justdave) | ☕ x 3 | +| ![](https://avatars.githubusercontent.com/u/11155743?s=25) | [@koddr](https://github.com/koddr) | ☕ x 1 | +| ![](https://avatars.githubusercontent.com/u/29042462?s=25) | [@lapolinar](https://github.com/lapolinar) | ☕ x 1 | +| ![](https://avatars.githubusercontent.com/u/2978730?s=25) | [@diegowifi](https://github.com/diegowifi) | ☕ x 1 | +| ![](https://avatars.githubusercontent.com/u/44171355?s=25) | [@ssimk0](https://github.com/ssimk0) | ☕ x 1 | +| ![](https://avatars.githubusercontent.com/u/5638101?s=25) | [@raymayemir](https://github.com/raymayemir) | ☕ x 1 | +| ![](https://avatars.githubusercontent.com/u/619996?s=25) | [@melkorm](https://github.com/melkorm) | ☕ x 1 | +| ![](https://avatars.githubusercontent.com/u/31022056?s=25) | [@marvinjwendt](https://github.com/thomasvvugt) | ☕ x 1 | +| ![](https://avatars.githubusercontent.com/u/31921460?s=25) | [@toishy](https://github.com/toishy) | ☕ x 1 | + +## ‎‍💻 Code Contributors + +Code Contributors + +## ⭐️ Stargazers + +Stargazers over time + +## ⚠️ License + +Copyright (c) 2019-present [Fenny](https://github.com/fenny) and [Contributors](https://github.com/gofiber/fiber/graphs/contributors). `Fiber` is free and open-source software licensed under the [MIT License](https://github.com/gofiber/fiber/blob/master/LICENSE). Official logo was created by [Vic Shóstak](https://github.com/koddr) and distributed under [Creative Commons](https://creativecommons.org/licenses/by-sa/4.0/) license (CC BY-SA 4.0 International). + +**Third-party library licenses** + +- [colorable](https://github.com/mattn/go-colorable/blob/master/LICENSE) +- [isatty](https://github.com/mattn/go-isatty/blob/master/LICENSE) +- [runewidth](https://github.com/mattn/go-runewidth/blob/master/LICENSE) +- [fasthttp](https://github.com/valyala/fasthttp/blob/master/LICENSE) +- [bytebufferpool](https://github.com/valyala/bytebufferpool/blob/master/LICENSE) +- [fwd](https://github.com/philhofer/fwd/blob/master/LICENSE.md) +- [go-ole](https://github.com/go-ole/go-ole/blob/master/LICENSE) +- [gopsutil](https://github.com/shirou/gopsutil/blob/master/LICENSE) +- [msgp](https://github.com/tinylib/msgp/blob/master/LICENSE) +- [schema](https://github.com/gorilla/schema/blob/master/LICENSE) +- [uuid](https://github.com/google/uuid/blob/master/LICENSE) +- [wmi](https://github.com/StackExchange/wmi/blob/master/LICENSE) diff --git a/.github/README_pl.md b/.github/README_pl.md new file mode 100644 index 0000000..5cff10c --- /dev/null +++ b/.github/README_pl.md @@ -0,0 +1,715 @@ +

+ + + + Fiber + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + +

+

+ Fiber jest frameworkiem webowym inspirowanym javascriptowym frameworkiem Express. Został zbudowany na podstawie Fasthttp, najszybszym silniku HTTP powstałym w Go. Został zaprojektowany tak, aby ułatwić szybkie programowanie +z myślą o wydajności oraz zerowej alokacji pamięci. +

+ +## ⚡️ Szybki start + +```go +package main + +import "github.com/gofiber/fiber/v2" + +func main() { + app := fiber.New() + + app.Get("/", func(c *fiber.Ctx) error { + return c.SendString("Hello, World 👋!") + }) + + app.Listen(":3000") +} +``` + +## 🤖 Testy wydajności +Testy te zostały przeprowadzone przez [TechEmpower](https://www.techempower.com/benchmarks/#section=data-r19&hw=ph&test=plaintext) oraz [Go Web](https://github.com/smallnest/go-web-framework-benchmark). Jeżeli chcesz zobaczyć wszystkie wyniki, proszę, odwiedź naszą [Wiki](https://docs.gofiber.io/extra/benchmarks). + + +

+ + +

+ +## ⚙️ Instalacja + +Upewnij się, że masz zainstalowane Go ([pobierz](https://go.dev/dl/)). Wymagana jest wersja `1.17` lub wyższa. + +Zainicjalizuj swój projekt poprzez stworzenie folderu i użycie komendy `go mod init github.com/your/repo` ([zobacz więcej](https://go.dev/blog/using-go-modules)) w tym folderze. Następnie zainstaluj Fiber'a przy użyciu komendy `go get`: + +```bash +go get -u github.com/gofiber/fiber/v2 +``` + +## 🎯 Funkcjonalności + +- Stabilny [routing](https://docs.gofiber.io/guide/routing) +- Serwowanie [pliki statyczne](https://docs.gofiber.io/api/app#static) +- Ekstremalna [wydajność](https://docs.gofiber.io/extra/benchmarks) +- [Niskie zużycie](https://docs.gofiber.io/extra/benchmarks) pamięci +- [Endpointy API](https://docs.gofiber.io/api/ctx) +- Wsparcie [Middleware](https://docs.gofiber.io/category/-middleware) oraz [Next](https://docs.gofiber.io/api/ctx#next) +- [Szybkie](https://dev.to/koddr/welcome-to-fiber-an-express-js-styled-fastest-web-framework-written-with-on-golang-497) programowanie po stronie servera +- [Silniki szablonów HTML](https://github.com/gofiber/template) +- [Wsparcie WebSocket](https://github.com/gofiber/websocket) +- [Wydarzenia wysyłane przez serwer](https://github.com/gofiber/recipes/tree/master/sse) +- [Rate Limiter](https://docs.gofiber.io/api/middleware/limiter) +- Tłumaczenia w [20 językach](https://docs.gofiber.io/) +- Oraz wiele więcej, [odkryj Fiber'a](https://docs.gofiber.io/) + +## 💡 Nasza filozofia +Nowi gophersi, którzy przenoszą się z [Node.js](https://nodejs.org/en/about/) na [Go](https://go.dev/doc/), mierzą się z problemami nauczania, zanim będą mogli rozpocząć budowanie swoich aplikacji internetowych lub mikroserwisów. Fiber, jako framework, został stworzony z myślą o minimalizmie i podąża za filozofią UNIX, aby nowi programiści w Go mogli szybko wkroczyć do świata Go, ciesząc się serdecznym i godnym zaufania przyjęciem. + +Fiber jest **inspirowany** javascriptowym frameworkiem Express, najpopularniejszym frameworkiem webowym w internecie. Połączyliśmy **łatwość** Express'a z **czystą wydajnością** Go. Jeżeli kiedykolwiek tworzyłeś aplikację webową w Node.js (_korzystając z Express'a lub podobnych_), wtedy wiele metod i zasad będzie dla ciebie **bardzo znajomych**. + + +**Słuchamy** naszych użytkowników w [issues](https://github.com/gofiber/fiber/issues), na kanale [Discord](https://gofiber.io/discord) _i wszędzie w Internecie_, aby stworzyć **szybki**, **elastyczny** i **przyjazny** framework webowy dla Go, który nadaje się do **wszelkich** zadań, **terminów** i **umiejętności** programistów! Tak jak Express w świecie JavaScript. + +## ⚠️ Ograniczenia + +- Z uwagi na użycie unsafe przez Fiber'a, biblioteka nie zawsze będzie kompatybilna z najnowszą wersją Go. Fiber 2.40.0 został przetestowany z Go w wersjach 1.17 i 1.21. +- Fiber nie jest kompatybilny z interfejsami net/http. To oznacza, że nie będziesz w stanie korzystać (bezpośrednio) z projektów takich jak gqlgen, go-swagger lub innych, które są częścią ekosystemu net/http. + + +## 👀 Przykłady + + +Poniżej znajdują się niektóre przykłady. Jeśli chcesz zobaczyć więcej przykładów kodu, odwiedź nasze [repozytorium Recipes](https://github.com/gofiber/recipes) lub odwiedź naszą [dokumentację API](https://docs.gofiber.io). + +#### 📖 [**Podstawowy Routing**](https://docs.gofiber.io/#basic-routing) + +```go +func main() { + app := fiber.New() + + // GET /api/register + app.Get("/api/*", func(c *fiber.Ctx) error { + msg := fmt.Sprintf("✋ %s", c.Params("*")) + return c.SendString(msg) // => ✋ register + }) + + // GET /flights/LAX-SFO + app.Get("/flights/:from-:to", func(c *fiber.Ctx) error { + msg := fmt.Sprintf("💸 From: %s, To: %s", c.Params("from"), c.Params("to")) + return c.SendString(msg) // => 💸 From: LAX, To: SFO + }) + + // GET /dictionary.txt + app.Get("/:file.:ext", func(c *fiber.Ctx) error { + msg := fmt.Sprintf("📃 %s.%s", c.Params("file"), c.Params("ext")) + return c.SendString(msg) // => 📃 dictionary.txt + }) + + // GET /john/75 + app.Get("/:name/:age/:gender?", func(c *fiber.Ctx) error { + msg := fmt.Sprintf("👴 %s is %s years old", c.Params("name"), c.Params("age")) + return c.SendString(msg) // => 👴 john is 75 years old + }) + + // GET /john + app.Get("/:name", func(c *fiber.Ctx) error { + msg := fmt.Sprintf("Hello, %s 👋!", c.Params("name")) + return c.SendString(msg) // => Hello john 👋! + }) + + log.Fatal(app.Listen(":3000")) +} + +``` + +#### 📖 [**Nazywanie Route'ów**](https://docs.gofiber.io/api/app#name) + +```go +func main() { + app := fiber.New() + + // GET /api/register + app.Get("/api/*", func(c *fiber.Ctx) error { + msg := fmt.Sprintf("✋ %s", c.Params("*")) + return c.SendString(msg) // => ✋ register + }).Name("api") + + data, _ := json.MarshalIndent(app.GetRoute("api"), "", " ") + fmt.Print(string(data)) + // Prints: + // { + // "method": "GET", + // "name": "api", + // "path": "/api/*", + // "params": [ + // "*1" + // ] + // } + + + log.Fatal(app.Listen(":3000")) +} + +``` + +#### 📖 [**Serwowanie plików statycznych**](https://docs.gofiber.io/api/app#static) + +```go +func main() { + app := fiber.New() + + app.Static("/", "./public") + // => http://localhost:3000/js/script.js + // => http://localhost:3000/css/style.css + + app.Static("/prefix", "./public") + // => http://localhost:3000/prefix/js/script.js + // => http://localhost:3000/prefix/css/style.css + + app.Static("*", "./public/index.html") + // => http://localhost:3000/any/path/shows/index/html + + log.Fatal(app.Listen(":3000")) +} + +``` + +#### 📖 [**Middleware i Next**](https://docs.gofiber.io/api/ctx#next) + +```go +func main() { + app := fiber.New() + + // Match any route + app.Use(func(c *fiber.Ctx) error { + fmt.Println("🥇 First handler") + return c.Next() + }) + + // Match all routes starting with /api + app.Use("/api", func(c *fiber.Ctx) error { + fmt.Println("🥈 Second handler") + return c.Next() + }) + + // GET /api/list + app.Get("/api/list", func(c *fiber.Ctx) error { + fmt.Println("🥉 Last handler") + return c.SendString("Hello, World 👋!") + }) + + log.Fatal(app.Listen(":3000")) +} + +``` + +
+ 📚 Pokaż więcej przykładów + +### Silniki widoków + +📖 [Config](https://docs.gofiber.io/api/fiber#config) +📖 [Silniki](https://github.com/gofiber/template) +📖 [Render](https://docs.gofiber.io/api/ctx#render) + +Fiber domyślnie korzysta z [html/template](https://pkg.go.dev/html/template/), kiedy nie wybrano żadnego silnika. + +Jeżeli chcesz wykonywać lub korzystać z innego silnika jak [amber](https://github.com/eknkc/amber), [handlebars](https://github.com/aymerick/raymond), [mustache](https://github.com/cbroglie/mustache), [pug](https://github.com/Joker/jade) itd. sprawdź naszą paczkę [Template](https://github.com/gofiber/template), która wspiera wiele silników widoków. + +```go +package main + +import ( + "github.com/gofiber/fiber/v2" + "github.com/gofiber/template/pug" +) + +func main() { + // You can setup Views engine before initiation app: + app := fiber.New(fiber.Config{ + Views: pug.New("./views", ".pug"), + }) + + // And now, you can call template `./views/home.pug` like this: + app.Get("/", func(c *fiber.Ctx) error { + return c.Render("home", fiber.Map{ + "title": "Homepage", + "year": 1999, + }) + }) + + log.Fatal(app.Listen(":3000")) +} +``` + +### Grupowanie route'ów w łańcuchy + +📖 [Group](https://docs.gofiber.io/api/app#group) + +```go +func middleware(c *fiber.Ctx) error { + fmt.Println("Don't mind me!") + return c.Next() +} + +func handler(c *fiber.Ctx) error { + return c.SendString(c.Path()) +} + +func main() { + app := fiber.New() + + // Root API route + api := app.Group("/api", middleware) // /api + + // API v1 routes + v1 := api.Group("/v1", middleware) // /api/v1 + v1.Get("/list", handler) // /api/v1/list + v1.Get("/user", handler) // /api/v1/user + + // API v2 routes + v2 := api.Group("/v2", middleware) // /api/v2 + v2.Get("/list", handler) // /api/v2/list + v2.Get("/user", handler) // /api/v2/user + + // ... +} + +``` + +### Middleware Logger + +📖 [Logger](https://docs.gofiber.io/api/middleware/logger) + +```go +package main + +import ( + "log" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/logger" +) + +func main() { + app := fiber.New() + + app.Use(logger.New()) + + // ... + + log.Fatal(app.Listen(":3000")) +} +``` + +### Cross-Origin Resource Sharing (CORS) + +📖 [CORS](https://docs.gofiber.io/api/middleware/cors) + +```go +import ( + "log" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/cors" +) + +func main() { + app := fiber.New() + + app.Use(cors.New()) + + // ... + + log.Fatal(app.Listen(":3000")) +} +``` +Sprawdź CORS poprzez przesłanie jakiejkolwiek domeny w nagłówku `Origin`: + +```bash +curl -H "Origin: http://example.com" --verbose http://localhost:3000 +``` + +### Niestandardowa odpowiedź 404 + +📖 [Metody HTTP](https://docs.gofiber.io/api/ctx#status) + +```go +func main() { + app := fiber.New() + + app.Static("/", "./public") + + app.Get("/demo", func(c *fiber.Ctx) error { + return c.SendString("This is a demo!") + }) + + app.Post("/register", func(c *fiber.Ctx) error { + return c.SendString("Welcome!") + }) + + // Last middleware to match anything + app.Use(func(c *fiber.Ctx) error { + return c.SendStatus(404) + // => 404 "Not Found" + }) + + log.Fatal(app.Listen(":3000")) +} +``` + +### Odpowiedź JSON + +📖 [JSON](https://docs.gofiber.io/api/ctx#json) + +```go +type User struct { + Name string `json:"name"` + Age int `json:"age"` +} + +func main() { + app := fiber.New() + + app.Get("/user", func(c *fiber.Ctx) error { + return c.JSON(&User{"John", 20}) + // => {"name":"John", "age":20} + }) + + app.Get("/json", func(c *fiber.Ctx) error { + return c.JSON(fiber.Map{ + "success": true, + "message": "Hi John!", + }) + // => {"success":true, "message":"Hi John!"} + }) + + log.Fatal(app.Listen(":3000")) +} +``` + +### Dodanie WebSocket + +📖 [Websocket](https://github.com/gofiber/websocket) + +```go +import ( + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/websocket" +) + +func main() { + app := fiber.New() + + app.Get("/ws", websocket.New(func(c *websocket.Conn) { + for { + mt, msg, err := c.ReadMessage() + if err != nil { + log.Println("read:", err) + break + } + log.Printf("recv: %s", msg) + err = c.WriteMessage(mt, msg) + if err != nil { + log.Println("write:", err) + break + } + } + })) + + log.Fatal(app.Listen(":3000")) + // ws://localhost:3000/ws +} +``` + +### Wydarzenia wysyłane przez serwer + +📖 [More Info](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events) + +```go +import ( + "github.com/gofiber/fiber/v2" + "github.com/valyala/fasthttp" +) + +func main() { + app := fiber.New() + + app.Get("/sse", func(c *fiber.Ctx) error { + c.Set("Content-Type", "text/event-stream") + c.Set("Cache-Control", "no-cache") + c.Set("Connection", "keep-alive") + c.Set("Transfer-Encoding", "chunked") + + c.Context().SetBodyStreamWriter(fasthttp.StreamWriter(func(w *bufio.Writer) { + fmt.Println("WRITER") + var i int + + for { + i++ + msg := fmt.Sprintf("%d - the time is %v", i, time.Now()) + fmt.Fprintf(w, "data: Message: %s\n\n", msg) + fmt.Println(msg) + + w.Flush() + time.Sleep(5 * time.Second) + } + })) + + return nil + }) + + log.Fatal(app.Listen(":3000")) +} +``` + +### Middleware Recover + +📖 [Recover](https://docs.gofiber.io/api/middleware/recover) + +```go +import ( + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/recover" +) + +func main() { + app := fiber.New() + + app.Use(recover.New()) + + app.Get("/", func(c *fiber.Ctx) error { + panic("normally this would crash your app") + }) + + log.Fatal(app.Listen(":3000")) +} +``` + +
+ +### Używanie zaufanego proxy + +📖 [Config](https://docs.gofiber.io/api/fiber#config) + +```go +import ( + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/recover" +) + +func main() { + app := fiber.New(fiber.Config{ + EnableTrustedProxyCheck: true, + TrustedProxies: []string{"0.0.0.0", "1.1.1.1/30"}, // IP address or IP address range + ProxyHeader: fiber.HeaderXForwardedFor, + }) + + // ... + + log.Fatal(app.Listen(":3000")) +} +``` + + + +## 🧬 Wbudowane Middleware + +Poniżej znajduje się lista middleware, które są zawarte wraz z frameworkiem Fiber. + +| Middleware | Opis | +| :------------------------------------------------------------------------------------- |:--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| [basicauth](https://github.com/gofiber/fiber/tree/master/middleware/basicauth) | Podstawowe middleware zapewniające podstawowe uwierzytelnienie HTTP. Wywołuje ono handler Next dla poprawnych danych uwierzytelniających oraz 401 Unauthorized dla niepoprawnych lub brakujacych danych. | +| [cache](https://github.com/gofiber/fiber/tree/master/middleware/cache) | Przechwytuje i cache'uje odpowiedzi | +| [compress](https://github.com/gofiber/fiber/tree/master/middleware/compress) | Middleware kompresji dla Fiber'a, podstawowo wspiera `deflate`, `gzip` i `brotli`. | +| [cors](https://github.com/gofiber/fiber/tree/master/middleware/cors) | Zezwala na cross-origin resource sharing \(CORS\) z wieloma opcjami konfiguracji. | +| [csrf](https://github.com/gofiber/fiber/tree/master/middleware/csrf) | Chroni przed exploitami CSRF. | +| [encryptcookie](https://github.com/gofiber/fiber/tree/master/middleware/encryptcookie) | Middleware szyfrujące wartości ciasteczek. | +| [envvar](https://github.com/gofiber/fiber/tree/master/middleware/envvar) | Odsłania zmienne środowiskowe oraz zapewnia dodatkową konfigurację. | +| [etag](https://github.com/gofiber/fiber/tree/master/middleware/etag) | Middleware ETag, które pozwala cache być bardziej wydajnym i oszczędzać transfer danych, jako, że serwer web nie musi wysyłać pełnej odpowiedzi, jeżeli dane się nie zmieniły. | +| [expvar](https://github.com/gofiber/fiber/tree/master/middleware/expvar) | Middleware Expvar, które udostępnia warianty uruchomieniowe przez swój serwer HTTP, w formacie JSON | +| [favicon](https://github.com/gofiber/fiber/tree/master/middleware/favicon) | Ignoruje favicony z logów lub serwuje je z pamięci, gdy ścieżka do pliku została podana | +| [filesystem](https://github.com/gofiber/fiber/tree/master/middleware/filesystem) | FileSystem middleware for Fiber, special thanks and credits to Alireza Salary | +| [limiter](https://github.com/gofiber/fiber/tree/master/middleware/limiter) | Rate-limiting middleware for Fiber. Use to limit repeated requests to public APIs and/or endpoints such as password reset. | +| [logger](https://github.com/gofiber/fiber/tree/master/middleware/logger) | Logger zapytań/odpowiedzi HTTP. | +| [monitor](https://github.com/gofiber/fiber/tree/master/middleware/monitor) | Middleware Monitor, które reportuje metryki serwera, inspirowane express-status-monitor | +| [pprof](https://github.com/gofiber/fiber/tree/master/middleware/pprof) | Specjalne podziękowania dla Matthew Lee \(@mthli\) | +| [proxy](https://github.com/gofiber/fiber/tree/master/middleware/proxy) | Pozwala ci przesyłać zapytania dalej do wielu serwerów | +| [recover](https://github.com/gofiber/fiber/tree/master/middleware/recover) | Middleware Recover przywraca działanie po wystąpieniu awarii w dowolnym miejscu w programie i przekazuje kontrolę do scentralizowanego typu [ ErrorHandler](https://docs.gofiber.io/guide/error-handling). | +| [requestid](https://github.com/gofiber/fiber/tree/master/middleware/requestid) | Dodaje requestid do każdego zapytania. | +| [session](https://github.com/gofiber/fiber/tree/master/middleware/session) | Middleware sesji. UWAGA: To middleware korzysta z naszej paczki Storage | +| [skip](https://github.com/gofiber/fiber/tree/master/middleware/skip) | Middleware skip, które pomija opakowany handler, jeżeli założona zasada jest spełniona | +| [rewrite](https://github.com/gofiber/rewrite) | Middleware Rewrite przepisuje scieżkę URL bazując na podanych zasadach. Może być przydatne w przypadku potrzeby kompatybilności wstecznej lub po prostu tworzeniu czystszych i bardziej przejrzystych linków. | +| [timeout](https://github.com/gofiber/fiber/tree/master/middleware/timeout) | Dodaje maksymalny czas dla zapytania i podaje go dalej do ErrorHandler, gdy limit został przekroczony. | +| [adaptor](https://github.com/gofiber/adaptor) | Konwertuje handlery net/http do/z zapytania Fiber'a, specjalne podziękowania dla @arsmn! | +| [helmet](https://github.com/gofiber/helmet) | Pomaga zabezpieczyć twoją aplikację poprzez ustawianie wielu nagłówków HTTP. | +| [redirect](https://github.com/gofiber/redirect) | Middleware przekierowywujące | +| [keyauth](https://github.com/gofiber/keyauth) | Middleware Key auth zapewnia uwierzytelnienie na podstawie klucza. | + +## 🧬 Zewnętrzne middleware +Lista zewnętrznie hostowanych modułów middleware i utrzymywanych przez [zpesół Fiber'a](https://github.com/orgs/gofiber/people). + +| Middleware | Description | +| :------------------------------------------------ | :-------------------------------------------------------------------------------------------------------------------- | +| [jwt](https://github.com/gofiber/jwt) | JWT returns a JSON Web Token \(JWT\) auth middleware. | +| [storage](https://github.com/gofiber/storage) | Premade storage drivers that implement the Storage interface, designed to be used with various Fiber middlewares. | +| [template](https://github.com/gofiber/template) | This package contains 8 template engines that can be used with Fiber `v1.10.x` Go version 1.13 or higher is required. | +| [websocket](https://github.com/gofiber/websocket) | Based on Fasthttp WebSocket for Fiber with Locals support! | + +## 🕶️ Awesome List + +Po więcej artykułów, middleware, przykładów lub narzędzi sprawdź naszą [awesome list](https://github.com/gofiber/awesome-fiber). + +## 👍 Wspomaganie + +Jeżeli chcesz podziękować i/lub wesprzeć aktywny rozwój `Fiber'a`: + +1. Dodaj [Gwiazdkę GitHub](https://github.com/gofiber/fiber/stargazers) dla tego projektu. +2. Zatweetuj o tym projekcie [na twoim 𝕏 (Twitter)](https://x.com/intent/tweet?text=Fiber%20is%20an%20Express%20inspired%20%23web%20%23framework%20built%20on%20top%20of%20Fasthttp%2C%20the%20fastest%20HTTP%20engine%20for%20%23Go.%20Designed%20to%20ease%20things%20up%20for%20%23fast%20development%20with%20zero%20memory%20allocation%20and%20%23performance%20in%20mind%20%F0%9F%9A%80%20https%3A%2F%2Fgithub.com%2Fgofiber%2Ffiber). +3. Napisz recenzję lub tutorial na [Medium](https://medium.com/), [Dev.to](https://dev.to/) lub personalnym blogu. +4. Wesprzyj projekt, przekazując darowiznę w postaci [filiżanki kawy](https://buymeacoff.ee/fenny). + +## ☕ Wspierający + +Fiber to projekt open source, który działa dzięki darowiznom, aby pokryć koszty, takie jak nasza nazwa domeny, GitBook, Netlify oraz hosting serverless. Jeśli chcesz wesprzeć Fiber, możesz ☕ [**tutaj kupić kawę**](https://buymeacoff.ee/fenny). + +| | Użytkownik | Dotacja | +| :--------------------------------------------------------- |:-------------------------------------------------|:--------| +| ![](https://avatars.githubusercontent.com/u/204341?s=25) | [@destari](https://github.com/destari) | ☕ x 10 | +| ![](https://avatars.githubusercontent.com/u/63164982?s=25) | [@dembygenesis](https://github.com/dembygenesis) | ☕ x 5 | +| ![](https://avatars.githubusercontent.com/u/56607882?s=25) | [@thomasvvugt](https://github.com/thomasvvugt) | ☕ x 5 | +| ![](https://avatars.githubusercontent.com/u/27820675?s=25) | [@hendratommy](https://github.com/hendratommy) | ☕ x 5 | +| ![](https://avatars.githubusercontent.com/u/1094221?s=25) | [@ekaputra07](https://github.com/ekaputra07) | ☕ x 5 | +| ![](https://avatars.githubusercontent.com/u/194590?s=25) | [@jorgefuertes](https://github.com/jorgefuertes) | ☕ x 5 | +| ![](https://avatars.githubusercontent.com/u/186637?s=25) | [@candidosales](https://github.com/candidosales) | ☕ x 5 | +| ![](https://avatars.githubusercontent.com/u/29659953?s=25) | [@l0nax](https://github.com/l0nax) | ☕ x 3 | +| ![](https://avatars.githubusercontent.com/u/635852?s=25) | [@bihe](https://github.com/bihe) | ☕ x 3 | +| ![](https://avatars.githubusercontent.com/u/307334?s=25) | [@justdave](https://github.com/justdave) | ☕ x 3 | +| ![](https://avatars.githubusercontent.com/u/11155743?s=25) | [@koddr](https://github.com/koddr) | ☕ x 1 | +| ![](https://avatars.githubusercontent.com/u/29042462?s=25) | [@lapolinar](https://github.com/lapolinar) | ☕ x 1 | +| ![](https://avatars.githubusercontent.com/u/2978730?s=25) | [@diegowifi](https://github.com/diegowifi) | ☕ x 1 | +| ![](https://avatars.githubusercontent.com/u/44171355?s=25) | [@ssimk0](https://github.com/ssimk0) | ☕ x 1 | +| ![](https://avatars.githubusercontent.com/u/5638101?s=25) | [@raymayemir](https://github.com/raymayemir) | ☕ x 1 | +| ![](https://avatars.githubusercontent.com/u/619996?s=25) | [@melkorm](https://github.com/melkorm) | ☕ x 1 | +| ![](https://avatars.githubusercontent.com/u/31022056?s=25) | [@marvinjwendt](https://github.com/marvinjwendt) | ☕ x 1 | +| ![](https://avatars.githubusercontent.com/u/31921460?s=25) | [@toishy](https://github.com/toishy) | ☕ x 1 | + +## ‎‍💻 Współtwórcy projektu + +Code Contributors + +## ⭐️ Obserwujący projekt + +Stargazers over time + +## ⚠️ Licencja + +Copyright (c) 2019-present [Fenny](https://github.com/fenny) and [Contributors](https://github.com/gofiber/fiber/graphs/contributors). `Fiber` is free and open-source software licensed under the [MIT License](https://github.com/gofiber/fiber/blob/master/LICENSE). Official logo was created by [Vic Shóstak](https://github.com/koddr) and distributed under [Creative Commons](https://creativecommons.org/licenses/by-sa/4.0/) license (CC BY-SA 4.0 International). + +**Licencje bibliotek od innych twórców** + +- [colorable](https://github.com/mattn/go-colorable/blob/master/LICENSE) +- [isatty](https://github.com/mattn/go-isatty/blob/master/LICENSE) +- [runewidth](https://github.com/mattn/go-runewidth/blob/master/LICENSE) +- [fasthttp](https://github.com/valyala/fasthttp/blob/master/LICENSE) +- [bytebufferpool](https://github.com/valyala/bytebufferpool/blob/master/LICENSE) +- [fwd](https://github.com/philhofer/fwd/blob/master/LICENSE.md) +- [go-ole](https://github.com/go-ole/go-ole/blob/master/LICENSE) +- [gopsutil](https://github.com/shirou/gopsutil/blob/master/LICENSE) +- [msgp](https://github.com/tinylib/msgp/blob/master/LICENSE) +- [schema](https://github.com/gorilla/schema/blob/master/LICENSE) +- [uuid](https://github.com/google/uuid/blob/master/LICENSE) +- [wmi](https://github.com/StackExchange/wmi/blob/master/LICENSE) diff --git a/.github/README_pt.md b/.github/README_pt.md new file mode 100644 index 0000000..e55f4f5 --- /dev/null +++ b/.github/README_pt.md @@ -0,0 +1,689 @@ +

+ + + + + Fiber + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + +

+

+Fiber é um framework web inspirado no Express, construído sobre o Fasthttp, o motor HTTP mais rápido do Go. Projetado para facilitar e acelerar o desenvolvimento, com zero de alocação de memória e desempenho em mente. +

+ +## ⚡️ Início rápido + +```go +package main + +import "github.com/gofiber/fiber/v2" + +func main() { + app := fiber.New() + + app.Get("/", func(c *fiber.Ctx) error { + return c.SendString("Hello, World 👋!") + }) + + app.Listen(":3000") +} +``` + +## 🤖 Benchmarks + +Esses testes são realizados pelo [TechEmpower](https://www.techempower.com/benchmarks/#section=data-r19&hw=ph&test=plaintext) e [Go Web](https://github.com/smallnest/go-web-framework-benchmark). Se você quiser ver todos os resultados, visite nosso [Wiki](https://docs.gofiber.io/extra/benchmarks) . + +

+ + +

+ +## ⚙️ Instalação + +Certifique-se de ter o Go instalado ([download](https://go.dev/dl/)). Versão `1.17` ou superior é obrigatória. + +Inicie seu projeto criando um diretório e então execute `go mod init github.com/your/repo` ([saiba mais](https://go.dev/blog/using-go-modules)) dentro dele. Então, instale o Fiber com o comando [`go get`](https://pkg.go.dev/cmd/go/#hdr-Add_dependencies_to_current_module_and_install_them): + +```bash +go get -u github.com/gofiber/fiber/v2 +``` + +## 🎯 Recursos + +- [Roteamento](https://docs.gofiber.io/guide/routing) robusto +- Servir [arquivos estáticos](https://docs.gofiber.io/api/app#static) +- [Desempenho](https://docs.gofiber.io/extra/benchmarks) extremo +- [Baixo consumo de memória](https://docs.gofiber.io/extra/benchmarks) +- [API de rotas](https://docs.gofiber.io/api/ctx) +- Suporte à Middleware e [Next](https://docs.gofiber.io/api/ctx#next) +- Programação [rápida](https://dev.to/koddr/welcome-to-fiber-an-express-js-styled-fastest-web-framework-written-with-on-golang-497) de aplicações de servidor +- [Templates](https://github.com/gofiber/template) +- [Suporte à WebSockets](https://github.com/gofiber/websocket) +- [Server-Sent events](https://github.com/gofiber/recipes/tree/master/sse) +- [Limitador de requisições](https://docs.gofiber.io/api/middleware/limiter) +- Disponível em [19 línguas](https://docs.gofiber.io/) +- E muito mais, [explore o Fiber](https://docs.gofiber.io/) + +## 💡 Filosofia + +Os novos gophers que mudaram do [Node.js](https://nodejs.org/en/about/) para o [Go](https://go.dev/doc/) estão tendo que lidar com uma curva de aprendizado antes que possam começar a criar seus aplicativos web ou microsserviços. O Fiber, como um **framework web**, foi criado com a ideia de ser **minimalista** e seguindo a **filosofia UNIX**, para que novos gophers possam, rapidamente, entrar no mundo do Go com uma recepção calorosa e confiável. + +O Fiber é **inspirado** no Express, o framework web mais popular da Internet. Combinamos a **facilidade** do Express e com o **desempenho bruto** do Go. Se você já implementou um aplicativo web com Node.js ( _usando Express.js ou similar_ ), então muitos métodos e princípios parecerão **muito familiares** para você. + +## ⚠️ Limitações + +- Devido ao uso de "unsafe" pelo Fiber, a biblioteca pode nem sempre ser compatível com a última versão do Go. Fiber 2.40.0 foi testado com as versões Go de 1.17 a 1.21. +- Fiber não é compatível com as interfaces net/http. Isso significa que você não poderá usar projetos como gqlgen, go-swagger ou quaisquer outros que fazem parte do ecossistema net/http. + +## 👀 Exemplos + +Listados abaixo estão alguns exemplos comuns. Se você quiser ver mais exemplos de código, +visite nosso [repositório de receitas](https://github.com/gofiber/recipes) ou +a [documentação da API](https://docs.gofiber.io). + +#### 📖 [**Roteamento básico**](https://docs.gofiber.io/#basic-routing) + +```go +func main() { + app := fiber.New() + + // GET /api/register + app.Get("/api/*", func(c *fiber.Ctx) error { + msg := fmt.Sprintf("✋ %s", c.Params("*")) + return c.SendString(msg) // => ✋ register + }) + + // GET /flights/LAX-SFO + app.Get("/flights/:from-:to", func(c *fiber.Ctx) error { + msg := fmt.Sprintf("💸 From: %s, To: %s", c.Params("from"), c.Params("to")) + return c.SendString(msg) // => 💸 From: LAX, To: SFO + }) + + // GET /dictionary.txt + app.Get("/:file.:ext", func(c *fiber.Ctx) error { + msg := fmt.Sprintf("📃 %s.%s", c.Params("file"), c.Params("ext")) + return c.SendString(msg) // => 📃 dictionary.txt + }) + + // GET /john/75 + app.Get("/:name/:age/:gender?", func(c *fiber.Ctx) error { + msg := fmt.Sprintf("👴 %s is %s years old", c.Params("name"), c.Params("age")) + return c.SendString(msg) // => 👴 john is 75 years old + }) + + // GET /john + app.Get("/:name", func(c *fiber.Ctx) error { + msg := fmt.Sprintf("Hello, %s 👋!", c.Params("name")) + return c.SendString(msg) // => Hello john 👋! + }) + + log.Fatal(app.Listen(":3000")) +} + +``` + +#### 📖 [**Nome de Rotas**](https://docs.gofiber.io/api/app#name) + +```go +func main() { + app := fiber.New() + + // GET /api/register + app.Get("/api/*", func(c *fiber.Ctx) error { + msg := fmt.Sprintf("✋ %s", c.Params("*")) + return c.SendString(msg) // => ✋ register + }).Name("api") + + data, _ := json.MarshalIndent(app.GetRoute("api"), "", " ") + fmt.Print(string(data)) + // Prints: + // { + // "method": "GET", + // "name": "api", + // "path": "/api/*", + // "params": [ + // "*1" + // ] + // } + + + log.Fatal(app.Listen(":3000")) +} + +``` + +#### 📖 [**Servindo arquivos estáticos**](https://docs.gofiber.io/api/app#static) + +```go +func main() { + app := fiber.New() + + app.Static("/", "./public") + // => http://localhost:3000/js/script.js + // => http://localhost:3000/css/style.css + + app.Static("/prefix", "./public") + // => http://localhost:3000/prefix/js/script.js + // => http://localhost:3000/prefix/css/style.css + + app.Static("*", "./public/index.html") + // => http://localhost:3000/any/path/shows/index/html + + log.Fatal(app.Listen(":3000")) +} + +``` + +#### 📖 [**Middleware & Next**](https://docs.gofiber.io/api/ctx#next) + +```go +func main() { + app := fiber.New() + + // Match any route + app.Use(func(c *fiber.Ctx) error { + fmt.Println("🥇 First handler") + return c.Next() + }) + + // Match all routes starting with /api + app.Use("/api", func(c *fiber.Ctx) error { + fmt.Println("🥈 Second handler") + return c.Next() + }) + + // GET /api/register + app.Get("/api/list", func(c *fiber.Ctx) error { + fmt.Println("🥉 Last handler") + return c.SendString("Hello, World 👋!") + }) + + log.Fatal(app.Listen(":3000")) +} + +``` + +
+ 📚 Mostrar mais exemplos + +### Engines de visualização + +📖 [Config](https://docs.gofiber.io/api/fiber#config) +📖 [Engines](https://github.com/gofiber/template) +📖 [Render](https://docs.gofiber.io/api/ctx#render) + +O Fiber usa por padrão o [html/template](https://pkg.go.dev/html/template/) quando nenhuma engine é selecionada. + +Se você quiser uma execução parcial ou usar uma engine diferente como [amber](https://github.com/eknkc/amber), [handlebars](https://github.com/aymerick/raymond), [mustache](https://github.com/cbroglie/mustache) ou [pug](https://github.com/Joker/jade) etc.. Dê uma olhada no package [Template](https://github.com/gofiber/template) que suporta multiplas engines de visualização. + +```go +package main + +import ( + "github.com/gofiber/fiber/v2" + "github.com/gofiber/template/pug" +) + +func main() { + // You can setup Views engine before initiation app: + app := fiber.New(fiber.Config{ + Views: pug.New("./views", ".pug"), + }) + + // And now, you can call template `./views/home.pug` like this: + app.Get("/", func(c *fiber.Ctx) error { + return c.Render("home", fiber.Map{ + "title": "Homepage", + "year": 1999, + }) + }) + + log.Fatal(app.Listen(":3000")) +} +``` + +### Agrupamento de rotas + +📖 [Group](https://docs.gofiber.io/api/app#group) + +```go +func middleware(c *fiber.Ctx) error { + fmt.Println("Don't mind me!") + return c.Next() +} + +func handler(c *fiber.Ctx) error { + return c.SendString(c.Path()) +} + +func main() { + app := fiber.New() + + // Root API route + api := app.Group("/api", middleware) // /api + + // API v1 routes + v1 := api.Group("/v1", middleware) // /api/v1 + v1.Get("/list", handler) // /api/v1/list + v1.Get("/user", handler) // /api/v1/user + + // API v2 routes + v2 := api.Group("/v2", middleware) // /api/v2 + v2.Get("/list", handler) // /api/v2/list + v2.Get("/user", handler) // /api/v2/user + + // ... +} + +``` + +### Middleware Logger + +📖 [Logger](https://docs.gofiber.io/api/middleware/logger) + +```go +package main + +import ( + "log" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/logger" +) + +func main() { + app := fiber.New() + + app.Use(logger.New()) + + // ... + + log.Fatal(app.Listen(":3000")) +} +``` + +### Cross-Origin Resource Sharing (CORS) + +📖 [CORS](https://docs.gofiber.io/api/middleware/cors) + +```go +import ( + "log" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/cors" +) + +func main() { + app := fiber.New() + + app.Use(cors.New()) + + // ... + + log.Fatal(app.Listen(":3000")) +} +``` + +Verifique o CORS passando qualquer domínio no header `Origin`: + +```bash +curl -H "Origin: http://example.com" --verbose http://localhost:3000 +``` + +### Resposta 404 customizada + +📖 [HTTP Methods](https://docs.gofiber.io/api/ctx#status) + +```go +func main() { + app := fiber.New() + + app.Static("/", "./public") + + app.Get("/demo", func(c *fiber.Ctx) error { + return c.SendString("This is a demo!") + }) + + app.Post("/register", func(c *fiber.Ctx) error { + return c.SendString("Welcome!") + }) + + // Last middleware to match anything + app.Use(func(c *fiber.Ctx) error { + return c.SendStatus(404) + // => 404 "Not Found" + }) + + log.Fatal(app.Listen(":3000")) +} +``` + +### Resposta JSON + +📖 [JSON](https://docs.gofiber.io/api/ctx#json) + +```go +type User struct { + Name string `json:"name"` + Age int `json:"age"` +} + +func main() { + app := fiber.New() + + app.Get("/user", func(c *fiber.Ctx) error { + return c.JSON(&User{"John", 20}) + // => {"name":"John", "age":20} + }) + + app.Get("/json", func(c *fiber.Ctx) error { + return c.JSON(fiber.Map{ + "success": true, + "message": "Hi John!", + }) + // => {"success":true, "message":"Hi John!"} + }) + + log.Fatal(app.Listen(":3000")) +} +``` + +### WebSocket + +📖 [Websocket](https://github.com/gofiber/websocket) + +```go +import ( + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/websocket" +) + +func main() { + app := fiber.New() + + app.Get("/ws", websocket.New(func(c *websocket.Conn) { + for { + mt, msg, err := c.ReadMessage() + if err != nil { + log.Println("read:", err) + break + } + log.Printf("recv: %s", msg) + err = c.WriteMessage(mt, msg) + if err != nil { + log.Println("write:", err) + break + } + } + })) + + log.Fatal(app.Listen(":3000")) + // ws://localhost:3000/ws +} +``` + +### Server-Sent Events + +📖 [More Info](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events) + +```go +import ( + "github.com/gofiber/fiber/v2" + "github.com/valyala/fasthttp" +) + +func main() { + app := fiber.New() + + app.Get("/sse", func(c *fiber.Ctx) error { + c.Set("Content-Type", "text/event-stream") + c.Set("Cache-Control", "no-cache") + c.Set("Connection", "keep-alive") + c.Set("Transfer-Encoding", "chunked") + + c.Context().SetBodyStreamWriter(fasthttp.StreamWriter(func(w *bufio.Writer) { + fmt.Println("WRITER") + var i int + + for { + i++ + msg := fmt.Sprintf("%d - the time is %v", i, time.Now()) + fmt.Fprintf(w, "data: Message: %s\n\n", msg) + fmt.Println(msg) + + w.Flush() + time.Sleep(5 * time.Second) + } + })) + + return nil + }) + + log.Fatal(app.Listen(":3000")) +} +``` + +### Middleware Recover + +📖 [Recover](https://docs.gofiber.io/api/middleware/recover) + +```go +import ( + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/recover" +) + +func main() { + app := fiber.New() + + app.Use(recover.New()) + + app.Get("/", func(c *fiber.Ctx) error { + panic("normally this would crash your app") + }) + + log.Fatal(app.Listen(":3000")) +} +``` + +
+ +## 🧬 Middleware Interno + +Aqui está uma lista de middlewares que estão incluídos no framework Fiber. + +| Middleware | Descrição | +| :------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| [basicauth](https://github.com/gofiber/fiber/tree/master/middleware/basicauth) | Autenticação básica fornece uma autenticação HTTP básica. Ele chama o próximo manipulador para credenciais válidas e 401 Não Autorizado para credenciais ausentes ou inválidas. | +| [cache](https://github.com/gofiber/fiber/tree/master/middleware/cache) | Intercepta e armazena em cache as respostas | +| [compress](https://github.com/gofiber/fiber/tree/master/middleware/compress) | Middleware de compressão para o Fiber, suporta `deflate`, `gzip` e `brotli` por padrão. | +| [cors](https://github.com/gofiber/fiber/tree/master/middleware/cors) | Habilita o compartilhamento de recursos de origem cruzada (CORS) com várias opções. | +| [csrf](https://github.com/gofiber/fiber/tree/master/middleware/csrf) | Protege contra exploits CSRF. | +| [encryptcookie](https://github.com/gofiber/fiber/tree/master/middleware/encryptcookie) | Criptografa valores de cookie. | +| [envvar](https://github.com/gofiber/fiber/tree/master/middleware/envvar) | Expõe variáveis de ambiente fornecendo uma configuração opcional. | +| [etag](https://github.com/gofiber/fiber/tree/master/middleware/etag) | Permite que caches sejam mais eficientes e economizem largura de banda, pois um servidor web não precisa reenviar uma resposta completa se o conteúdo não mudou. | +| [expvar](https://github.com/gofiber/fiber/tree/master/middleware/expvar) | Serve via seu servidor HTTP variantes expostas em tempo de execução no formato JSON. | +| [favicon](https://github.com/gofiber/fiber/tree/master/middleware/favicon) | Ignora favicon dos logs ou serve da memória se um caminho de arquivo for fornecido. | +| [filesystem](https://github.com/gofiber/fiber/tree/master/middleware/filesystem) | Sistema de Arquivos para o Fiber, agradecimentos especiais e créditos a Alireza Salary | +| [limiter](https://github.com/gofiber/fiber/tree/master/middleware/limiter) | Limitação de taxa para o Fiber. Use para limitar solicitações repetidas para APIs públicas e/ou endpoints como redefinição de senha. | +| [logger](https://github.com/gofiber/fiber/tree/master/middleware/logger) | Logger de solicitação/resposta HTTP. | +| [monitor](https://github.com/gofiber/fiber/tree/master/middleware/monitor) | Middleware de monitoramento que relata métricas do servidor, inspirado pelo express-status-monitor | +| [pprof](https://github.com/gofiber/fiber/tree/master/middleware/pprof) | Agradecimentos especiais a Matthew Lee (@mthli) | +| [proxy](https://github.com/gofiber/fiber/tree/master/middleware/proxy) | Permite que você faça proxy de solicitações a vários servidores | +| [recover](https://github.com/gofiber/fiber/tree/master/middleware/recover) | Recupera de panics em qualquer lugar da cadeia de chamadas e passa o controle para o [ErrorHandler](https://docs.gofiber.io/guide/error-handling) centralizado. | +| [requestid](https://github.com/gofiber/fiber/tree/master/middleware/requestid) | Adiciona um ID de solicitação a cada pedido. | +| [session](https://github.com/gofiber/fiber/tree/master/middleware/session) | Middleware de sessão. NOTA: Este middleware usa nosso pacote Storage. | +| [skip](https://github.com/gofiber/fiber/tree/master/middleware/skip) | Pula um handler envolto se um predicado for verdadeiro. | +| [timeout](https://github.com/gofiber/fiber/tree/master/middleware/timeout) | Adiciona um tempo máximo para uma solicitação e encaminha para ErrorHandler se ele for excedido. | +| [keyauth](https://github.com/gofiber/keyauth) | Autenticação por chave fornece uma autenticação baseada em chave. | +| [redirect](https://github.com/gofiber/redirect) | Middleware de redirecionamento | +| [rewrite](https://github.com/gofiber/rewrite) | Reescreve o caminho da URL com base nas regras fornecidas. Pode ser útil para compatibilidade retroativa ou para criar links mais limpos e descritivos. | +| [adaptor](https://github.com/gofiber/adaptor) | Conversor para handlers net/http para/para manipuladores de solicitação Fiber, agradecimentos especiais ao @arsmn! | +| [helmet](https://github.com/gofiber/helmet) | Ajuda a proteger seus aplicativos definindo vários cabeçalhos HTTP. | + +## 🧬 Middleware Externo + +Lista de módulos de middleware hospedados externamente e mantidos pela [equipe Fiber](https://github.com/orgs/gofiber/people). + +| Middleware | Descrição | +| :------------------------------------------------ | :---------------------------------------------------------------------------------------------------------------------------------- | +| [jwt](https://github.com/gofiber/jwt) | JWT retorna um middleware de autenticação com tokens JWT. | +| [storage](https://github.com/gofiber/storage) | Drivers de armazenamento prontos que implementam a interface Storage, projetados para serem usados com vários middlewares do Fiber. | +| [template](https://github.com/gofiber/template) | Este pacote contém 8 mecanismos de template que podem ser usados com Fiber `v1.10.x`. É necessário Go versão 1.13 ou superior. | +| [websocket](https://github.com/gofiber/websocket) | Baseado no WebSocket do Fasthttp para Fiber com suporte a Locals | + +## 🕶️ Lista Incrível + +Para mais artigos, middlewares, exemplos ou ferramentas, confira nossa [lista incrível](https://github.com/gofiber/awesome-fiber). + +## 👍 Contribuindo + +Se você quer **agradecer** e/ou apoiar o desenvolvimento ativo do `Fiber`: + +1. Deixe uma [estrela no GitHub](https://github.com/gofiber/fiber/stargazers) do projeto. +2. Tweet sobre o projeto [no seu 𝕏 (Twitter)](https://x.com/intent/tweet?text=Fiber%20is%20an%20Express%20inspired%20%23web%20%23framework%20built%20on%20top%20of%20Fasthttp%2C%20the%20fastest%20HTTP%20engine%20for%20%23Go.%20Designed%20to%20ease%20things%20up%20for%20%23fast%20development%20with%20zero%20memory%20allocation%20and%20%23performance%20in%20mind%20%F0%9F%9A%80%20https%3A%2F%2Fgithub.com%2Fgofiber%2Ffiber). +3. Escreva um review ou tutorial no [Medium](https://medium.com/), [Dev.to](https://dev.to/) ou blog pessoal. +4. Apoie o projeto pagando uma [xícara de café](https://buymeacoff.ee/fenny). + +## ☕ Apoiadores + +Fiber é um projeto open source que usa de doações para pagar seus custos (domínio, GitBook, Netlify e hospedagem serverless). Se você quiser apoiar o projeto, você pode ☕ [**pagar um café**](https://buymeacoff.ee/fenny). + +| | User | Donation | +| :--------------------------------------------------------- | :----------------------------------------------- | :------- | +| ![](https://avatars.githubusercontent.com/u/204341?s=25) | [@destari](https://github.com/destari) | ☕ x 10 | +| ![](https://avatars.githubusercontent.com/u/63164982?s=25) | [@dembygenesis](https://github.com/dembygenesis) | ☕ x 5 | +| ![](https://avatars.githubusercontent.com/u/56607882?s=25) | [@thomasvvugt](https://github.com/thomasvvugt) | ☕ x 5 | +| ![](https://avatars.githubusercontent.com/u/27820675?s=25) | [@hendratommy](https://github.com/hendratommy) | ☕ x 5 | +| ![](https://avatars.githubusercontent.com/u/1094221?s=25) | [@ekaputra07](https://github.com/ekaputra07) | ☕ x 5 | +| ![](https://avatars.githubusercontent.com/u/194590?s=25) | [@jorgefuertes](https://github.com/jorgefuertes) | ☕ x 5 | +| ![](https://avatars.githubusercontent.com/u/186637?s=25) | [@candidosales](https://github.com/candidosales) | ☕ x 5 | +| ![](https://avatars.githubusercontent.com/u/29659953?s=25) | [@l0nax](https://github.com/l0nax) | ☕ x 3 | +| ![](https://avatars.githubusercontent.com/u/635852?s=25) | [@bihe](https://github.com/bihe) | ☕ x 3 | +| ![](https://avatars.githubusercontent.com/u/307334?s=25) | [@justdave](https://github.com/justdave) | ☕ x 3 | +| ![](https://avatars.githubusercontent.com/u/11155743?s=25) | [@koddr](https://github.com/koddr) | ☕ x 1 | +| ![](https://avatars.githubusercontent.com/u/29042462?s=25) | [@lapolinar](https://github.com/lapolinar) | ☕ x 1 | +| ![](https://avatars.githubusercontent.com/u/2978730?s=25) | [@diegowifi](https://github.com/diegowifi) | ☕ x 1 | +| ![](https://avatars.githubusercontent.com/u/44171355?s=25) | [@ssimk0](https://github.com/ssimk0) | ☕ x 1 | +| ![](https://avatars.githubusercontent.com/u/5638101?s=25) | [@raymayemir](https://github.com/raymayemir) | ☕ x 1 | +| ![](https://avatars.githubusercontent.com/u/619996?s=25) | [@melkorm](https://github.com/melkorm) | ☕ x 1 | +| ![](https://avatars.githubusercontent.com/u/31022056?s=25) | [@marvinjwendt](https://github.com/thomasvvugt) | ☕ x 1 | +| ![](https://avatars.githubusercontent.com/u/31921460?s=25) | [@toishy](https://github.com/toishy) | ☕ x 1 | + +## ‎‍💻 Contribuidores de código + +Code Contributors + +## ⭐️ Stargazers + +Stargazers over time + +## ⚠️ Licença + +Todos os direitos reservados (c) 2019-presente [Fenny](https://github.com/fenny) e [Contribuidores](https://github.com/gofiber/fiber/graphs/contributors). +`Fiber` é software livre e aberto sob a [licença MIT](https://github.com/gofiber/fiber/blob/master/LICENSE). +O logo oficial foi criado por [Vic Shóstak](https://github.com/koddr) e distribuído sob a licença [Creative Commons](https://creativecommons.org/licenses/by-sa/4.0/) (CC BY-SA 4.0 International). + +**Licença das bibliotecas de terceiros** + +- [colorable](https://github.com/mattn/go-colorable/blob/master/LICENSE) +- [isatty](https://github.com/mattn/go-isatty/blob/master/LICENSE) +- [runewidth](https://github.com/mattn/go-runewidth/blob/master/LICENSE) +- [fasthttp](https://github.com/valyala/fasthttp/blob/master/LICENSE) +- [bytebufferpool](https://github.com/valyala/bytebufferpool/blob/master/LICENSE) +- [fwd](https://github.com/philhofer/fwd/blob/master/LICENSE.md) +- [go-ole](https://github.com/go-ole/go-ole/blob/master/LICENSE) +- [gopsutil](https://github.com/shirou/gopsutil/blob/master/LICENSE) +- [msgp](https://github.com/tinylib/msgp/blob/master/LICENSE) +- [schema](https://github.com/gorilla/schema/blob/master/LICENSE) +- [uuid](https://github.com/google/uuid/blob/master/LICENSE) +- [wmi](https://github.com/StackExchange/wmi/blob/master/LICENSE) diff --git a/.github/README_ru.md b/.github/README_ru.md new file mode 100644 index 0000000..bfb4166 --- /dev/null +++ b/.github/README_ru.md @@ -0,0 +1,696 @@ +

+ + + + + Fiber + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + +

+

+ Fiber — это веб фреймворк, который был вдохновлен Express и основан на Fasthttp, самом быстром HTTP-движке написанном на Go. Фреймворк был разработан с целью упростить процесс быстрой разработки высокопроизводительных веб-приложений с нулевым распределением памяти. +

+ +## ⚡️ Быстрый старт + +```go +package main + +import "github.com/gofiber/fiber/v2" + +func main() { + app := fiber.New() + + app.Get("/", func(c *fiber.Ctx) error { + return c.SendString("Hello, World 👋!") + }) + + app.Listen(":3000") +} +``` + +## 🤖 Бенчмарки + +Тестирование проводилось с помощью [TechEmpower](https://www.techempower.com/benchmarks/#section=data-r19&hw=ph&test=plaintext) и [Go Web](https://github.com/smallnest/go-web-framework-benchmark). Если вы хотите увидеть все результаты, пожалуйста, посетите наш [Wiki](https://docs.gofiber.io/extra/benchmarks). + +

+ + +

+ +## ⚙️ Установка + +Убедитесь, что Go установлен ([скачать](https://go.dev/dl/)). Требуется версия `1.17` или выше. + +Инициализируйте проект, создав папку, а затем запустив `go mod init github.com/your/repo` ([подробнее](https://go.dev/blog/using-go-modules)) внутри этой папки. Далее, установите Fiber с помощью команды [`go get`](https://pkg.go.dev/cmd/go/#hdr-Add_dependencies_to_current_module_and_install_them): + +```bash +go get -u github.com/gofiber/fiber/v2 +``` + +## 🎯 Особенности + +- Надежная [маршрутизация](https://docs.gofiber.io/guide/routing) +- Доступ к [статичным файлам](https://docs.gofiber.io/api/app#static) +- Экстремальная [производительность](https://docs.gofiber.io/extra/benchmarks) +- [Низкий объем потребления памяти](https://docs.gofiber.io/extra/benchmarks) +- [Эндпоинты](https://docs.gofiber.io/context), как в [API](https://docs.gofiber.io/api/ctx) Express +- [Middleware](https://docs.gofiber.io/category/-middleware) и поддержка [Next](https://docs.gofiber.io/api/ctx#next) +- [Быстрое](https://dev.to/koddr/welcome-to-fiber-an-express-js-styled-fastest-web-framework-written-with-on-golang-497) программирование на стороне сервера +- [Template engines](https://github.com/gofiber/template) +- [Поддержка WebSocket](https://github.com/gofiber/websocket) +- [Server-Sent events](https://github.com/gofiber/recipes/tree/master/sse) +- [Rate Limiter](https://docs.gofiber.io/api/middleware/limiter) +- Документация доступна на [19 языках](https://docs.gofiber.io/) +- И многое другое, [посетите наш Wiki](https://docs.gofiber.io/) + +## 💡 Философия + +Новые Go-программисты, которые переключаются с [Node.js](https://nodejs.org/en/about/) на [Go](https://go.dev/doc/), имеют дело с очень извилистой кривой обучения, прежде чем они смогут начать создавать свои веб-приложения или микросервисы. Fiber, как **веб-фреймворк**, был создан с идеей **минимализма** и следовал **принципу UNIX**, так что новички смогут быстро войти в мир Go без особых проблем. + +Fiber **вдохновлен** Express, самым популярным веб фреймворком в Интернете. Мы объединили **простоту** Express и **чистую производительность** Go. Если вы когда-либо реализовывали веб-приложение на Node.js (_с использованием Express или аналогичного фреймворка_), то многие методы и принципы покажутся вам **очень знакомыми**. + +Мы **прислушиваемся** к нашим пользователям в [issues](https://github.com/gofiber/fiber/issues), Discord [канале](https://gofiber.io/discord) _и в остальном Интернете_, чтобы создать **быстрый**, **гибкий** и **дружелюбный** веб фреймворк на Go для **любых** задач, **дедлайнов** и **уровней** разработчиков! Как это делает Express в мире JavaScript. + +## ⚠️ Ограничения + +- Из-за того, что Fiber использует пакет unsafe, библиотека не всегда может быть совместима с последней версией Go. Fiber 2.40.0 был протестирован с версиями Go от 1.17 до 1.21. +- Fiber не совместим с интерфейсами net/http. Это означает, что вы не сможете использовать такие проекты, как gqlgen, go-swagger или любые другие, которые являются частью экосистемы net/http. + +## 👀 Примеры + +Ниже перечислены некоторые из распространенных примеров. Если вы хотите увидеть больше примеров кода, пожалуйста, посетите наш [репозиторий рецептов](https://github.com/gofiber/recipes) или [документацию по API](https://docs.gofiber.io). + +#### 📖 [**Basic Routing**](https://docs.gofiber.io/#basic-routing) + +```go +func main() { + app := fiber.New() + + // GET /api/register + app.Get("/api/*", func(c *fiber.Ctx) error { + msg := fmt.Sprintf("✋ %s", c.Params("*")) + return c.SendString(msg) // => ✋ register + }) + + // GET /flights/LAX-SFO + app.Get("/flights/:from-:to", func(c *fiber.Ctx) error { + msg := fmt.Sprintf("💸 From: %s, To: %s", c.Params("from"), c.Params("to")) + return c.SendString(msg) // => 💸 From: LAX, To: SFO + }) + + // GET /dictionary.txt + app.Get("/:file.:ext", func(c *fiber.Ctx) error { + msg := fmt.Sprintf("📃 %s.%s", c.Params("file"), c.Params("ext")) + return c.SendString(msg) // => 📃 dictionary.txt + }) + + // GET /john/75 + app.Get("/:name/:age/:gender?", func(c *fiber.Ctx) error { + msg := fmt.Sprintf("👴 %s is %s years old", c.Params("name"), c.Params("age")) + return c.SendString(msg) // => 👴 john is 75 years old + }) + + // GET /john + app.Get("/:name", func(c *fiber.Ctx) error { + msg := fmt.Sprintf("Hello, %s 👋!", c.Params("name")) + return c.SendString(msg) // => Hello john 👋! + }) + + log.Fatal(app.Listen(":3000")) +} + +``` + +#### 📖 [**Route Naming**](https://docs.gofiber.io/api/app#name) + +```go +func main() { + app := fiber.New() + + // GET /api/register + app.Get("/api/*", func(c *fiber.Ctx) error { + msg := fmt.Sprintf("✋ %s", c.Params("*")) + return c.SendString(msg) // => ✋ register + }).Name("api") + + data, _ := json.MarshalIndent(app.GetRoute("api"), "", " ") + fmt.Print(string(data)) + // Prints: + // { + // "method": "GET", + // "name": "api", + // "path": "/api/*", + // "params": [ + // "*1" + // ] + // } + + + log.Fatal(app.Listen(":3000")) +} + +``` + +#### 📖 [**Serving Static Files**](https://docs.gofiber.io/api/app#static) + +```go +func main() { + app := fiber.New() + + app.Static("/", "./public") + // => http://localhost:3000/js/script.js + // => http://localhost:3000/css/style.css + + app.Static("/prefix", "./public") + // => http://localhost:3000/prefix/js/script.js + // => http://localhost:3000/prefix/css/style.css + + app.Static("*", "./public/index.html") + // => http://localhost:3000/any/path/shows/index/html + + log.Fatal(app.Listen(":3000")) +} + +``` + +#### 📖 [**Middleware & Next**](https://docs.gofiber.io/api/ctx#next) + +```go +func main() { + app := fiber.New() + + // Match any route + app.Use(func(c *fiber.Ctx) error { + fmt.Println("🥇 First handler") + return c.Next() + }) + + // Match all routes starting with /api + app.Use("/api", func(c *fiber.Ctx) error { + fmt.Println("🥈 Second handler") + return c.Next() + }) + + // GET /api/register + app.Get("/api/list", func(c *fiber.Ctx) error { + fmt.Println("🥉 Last handler") + return c.SendString("Hello, World 👋!") + }) + + log.Fatal(app.Listen(":3000")) +} + +``` + +
+ 📚 Показать больше примеров кода + +### Views engines + +📖 [Config](https://docs.gofiber.io/api/fiber#config) +📖 [Engines](https://github.com/gofiber/template) +📖 [Render](https://docs.gofiber.io/api/ctx#render) + +Если не указано ни одного движка для views, Fiber использует [html/template](https://pkg.go.dev/html/template/). + +Вы можете использовать движки [amber](https://github.com/eknkc/amber), [handlebars](https://github.com/aymerick/raymond), [mustache](https://github.com/cbroglie/mustache) или [pug](https://github.com/Joker/jade) и другие... + +Ознакомьтесь с пакетом [Template](https://github.com/gofiber/template), который поддерживает множество движков для views. + +```go +import ( + "github.com/gofiber/fiber/v2" + "github.com/gofiber/template/pug" +) + +func main() { + // You can setup Views engine before initiation app: + app := fiber.New(fiber.Config{ + Views: pug.New("./views", ".pug"), + }) + + // And now, you can call template `./views/home.pug` like this: + app.Get("/", func(c *fiber.Ctx) error { + return c.Render("home", fiber.Map{ + "title": "Homepage", + "year": 1999, + }) + }) + + log.Fatal(app.Listen(":3000")) +} + +``` + +### Группировка путей в цепочки + +📖 [Group](https://docs.gofiber.io/api/app#group) + +```go +func middleware(c *fiber.Ctx) error { + fmt.Println("Don't mind me!") + return c.Next() +} + +func handler(c *fiber.Ctx) error { + return c.SendString(c.Path()) +} + +func main() { + app := fiber.New() + + // Root API route + api := app.Group("/api", middleware) // /api + + // API v1 routes + v1 := api.Group("/v1", middleware) // /api/v1 + v1.Get("/list", handler) // /api/v1/list + v1.Get("/user", handler) // /api/v1/user + + // API v2 routes + v2 := api.Group("/v2", middleware) // /api/v2 + v2.Get("/list", handler) // /api/v2/list + v2.Get("/user", handler) // /api/v2/user + + // ... +} + +``` + +### Middleware logger + +📖 [Logger](https://docs.gofiber.io/api/middleware/logger) + +```go +import ( + "log" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/logger" +) + +func main() { + app := fiber.New() + + app.Use(logger.New()) + + // ... + + log.Fatal(app.Listen(":3000")) +} + +``` + +### Cross-Origin Resource Sharing (CORS) + +📖 [CORS](https://docs.gofiber.io/api/middleware/cors) + +```go +import ( + "log" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/cors" +) + +func main() { + app := fiber.New() + + app.Use(cors.New()) + + // ... + + log.Fatal(app.Listen(":3000")) +} + +``` + +Проверем CORS, присвоив домен в заголовок `Origin`, отличный от `localhost`: + +```bash +curl -H "Origin: http://example.com" --verbose http://localhost:3000 +``` + +### Custom 404 response + +📖 [HTTP Methods](https://docs.gofiber.io/api/ctx#status) + +```go +func main() { + app := fiber.New() + + app.Static("/", "./public") + + app.Get("/demo", func(c *fiber.Ctx) error { + return c.SendString("This is a demo!") + }) + + app.Post("/register", func(c *fiber.Ctx) error { + return c.SendString("Welcome!") + }) + + // Last middleware to match anything + app.Use(func(c *fiber.Ctx) error { + return c.SendStatus(404) + // => 404 "Not Found" + }) + + log.Fatal(app.Listen(":3000")) +} + +``` + +### JSON Response + +📖 [JSON](https://docs.gofiber.io/api/ctx#json) + +```go +type User struct { + Name string `json:"name"` + Age int `json:"age"` +} + +func main() { + app := fiber.New() + + app.Get("/user", func(c *fiber.Ctx) error { + return c.JSON(&User{"John", 20}) + // => {"name":"John", "age":20} + }) + + app.Get("/json", func(c *fiber.Ctx) error { + return c.JSON(fiber.Map{ + "success": true, + "message": "Hi John!", + }) + // => {"success":true, "message":"Hi John!"} + }) + + log.Fatal(app.Listen(":3000")) +} + +``` + +### WebSocket Upgrade + +📖 [Websocket](https://github.com/gofiber/websocket) + +```go +import ( + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/websocket" +) + +func main() { + app := fiber.New() + + app.Get("/ws", websocket.New(func(c *websocket.Conn) { + for { + mt, msg, err := c.ReadMessage() + if err != nil { + log.Println("read:", err) + break + } + + log.Printf("recv: %s", msg) + + err = c.WriteMessage(mt, msg) + if err != nil { + log.Println("write:", err) + break + } + } + })) + + log.Fatal(app.Listen(":3000")) + // => ws://localhost:3000/ws +} + +``` + +### Server-Sent Events + +📖 [More Info](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events) + +```go +import ( + "github.com/gofiber/fiber/v2" + "github.com/valyala/fasthttp" +) + +func main() { + app := fiber.New() + + app.Get("/sse", func(c *fiber.Ctx) error { + c.Set("Content-Type", "text/event-stream") + c.Set("Cache-Control", "no-cache") + c.Set("Connection", "keep-alive") + c.Set("Transfer-Encoding", "chunked") + + c.Context().SetBodyStreamWriter(fasthttp.StreamWriter(func(w *bufio.Writer) { + fmt.Println("WRITER") + var i int + + for { + i++ + msg := fmt.Sprintf("%d - the time is %v", i, time.Now()) + fmt.Fprintf(w, "data: Message: %s\n\n", msg) + fmt.Println(msg) + + w.Flush() + time.Sleep(5 * time.Second) + } + })) + + return nil + }) + + log.Fatal(app.Listen(":3000")) +} +``` + +### Recover middleware + +📖 [Recover](https://docs.gofiber.io/api/middleware/recover) + +```go +import ( + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/recover" +) + +func main() { + app := fiber.New() + + app.Use(recover.New()) + + app.Get("/", func(c *fiber.Ctx) error { + panic("normally this would crash your app") + }) + + log.Fatal(app.Listen(":3000")) +} + +``` + +
+ +## 🧬 Внутренние Middleware + +Вот список middleware, входящих в состав фреймворка Fiber. + +| Middleware | Описание | +| :------------------------------------------------------------------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [basicauth](https://github.com/gofiber/fiber/tree/master/middleware/basicauth) | Basic auth middleware provides an HTTP basic authentication. It calls the next handler for valid credentials and 401 Unauthorized for missing or invalid credentials. | +| [cache](https://github.com/gofiber/fiber/tree/master/middleware/cache) | Intercept and cache responses | +| [compress](https://github.com/gofiber/fiber/tree/master/middleware/compress) | Compression middleware for Fiber, it supports `deflate`, `gzip` and `brotli` by default. | +| [cors](https://github.com/gofiber/fiber/tree/master/middleware/cors) | Enable cross-origin resource sharing \(CORS\) with various options. | +| [csrf](https://github.com/gofiber/fiber/tree/master/middleware/csrf) | Protect from CSRF exploits. | +| [encryptcookie](https://github.com/gofiber/fiber/tree/master/middleware/encryptcookie) | Encrypt middleware which encrypts cookie values. | +| [envvar](https://github.com/gofiber/fiber/tree/master/middleware/envvar) | Expose environment variables with providing an optional config. | +| [etag](https://github.com/gofiber/fiber/tree/master/middleware/etag) | ETag middleware that lets caches be more efficient and save bandwidth, as a web server does not need to resend a full response if the content has not changed. | +| [expvar](https://github.com/gofiber/fiber/tree/master/middleware/expvar) | Expvar middleware that serves via its HTTP server runtime exposed variants in the JSON format. | +| [favicon](https://github.com/gofiber/fiber/tree/master/middleware/favicon) | Ignore favicon from logs or serve from memory if a file path is provided. | +| [filesystem](https://github.com/gofiber/fiber/tree/master/middleware/filesystem) | FileSystem middleware for Fiber, special thanks and credits to Alireza Salary | +| [limiter](https://github.com/gofiber/fiber/tree/master/middleware/limiter) | Rate-limiting middleware for Fiber. Use to limit repeated requests to public APIs and/or endpoints such as password reset. | +| [logger](https://github.com/gofiber/fiber/tree/master/middleware/logger) | HTTP request/response logger. | +| [monitor](https://github.com/gofiber/fiber/tree/master/middleware/monitor) | Monitor middleware that reports server metrics, inspired by express-status-monitor | +| [pprof](https://github.com/gofiber/fiber/tree/master/middleware/pprof) | Special thanks to Matthew Lee \(@mthli\) | +| [proxy](https://github.com/gofiber/fiber/tree/master/middleware/proxy) | Allows you to proxy requests to a multiple servers | +| [recover](https://github.com/gofiber/fiber/tree/master/middleware/recover) | Recover middleware recovers from panics anywhere in the stack chain and handles the control to the centralized[ ErrorHandler](https://docs.gofiber.io/guide/error-handling). | +| [requestid](https://github.com/gofiber/fiber/tree/master/middleware/requestid) | Adds a requestid to every request. | +| [session](https://github.com/gofiber/fiber/tree/master/middleware/session) | Session middleware. NOTE: This middleware uses our Storage package. | +| [skip](https://github.com/gofiber/fiber/tree/master/middleware/skip) | Skip middleware that skips a wrapped handler is a predicate is true. | +| [timeout](https://github.com/gofiber/fiber/tree/master/middleware/timeout) | Adds a max time for a request and forwards to ErrorHandler if it is exceeded. | +| [keyauth](https://github.com/gofiber/keyauth) | Key auth middleware provides a key based authentication. | +| [redirect](https://github.com/gofiber/redirect) | Redirect middleware | +| [rewrite](https://github.com/gofiber/rewrite) | Rewrite middleware rewrites the URL path based on provided rules. It can be helpful for backward compatibility or just creating cleaner and more descriptive links. | +| [adaptor](https://github.com/gofiber/adaptor) | Converter for net/http handlers to/from Fiber request handlers, special thanks to @arsmn! | +| [helmet](https://github.com/gofiber/helmet) | Helps secure your apps by setting various HTTP headers. | + +## 🧬 Внешние Middleware + +Список модулей middleware, размещенных на внешнем хостинге от [Fiber team](https://github.com/orgs/gofiber/people). + +| Middleware | Описание | +| :------------------------------------------------ | :-------------------------------------------------------------------------------------------------------------------- | +| [jwt](https://github.com/gofiber/jwt) | JWT returns a JSON Web Token \(JWT\) auth middleware. | +| [storage](https://github.com/gofiber/storage) | Premade storage drivers that implement the Storage interface, designed to be used with various Fiber middlewares. | +| [template](https://github.com/gofiber/template) | This package contains 8 template engines that can be used with Fiber `v1.10.x` Go version 1.13 or higher is required. | +| [websocket](https://github.com/gofiber/websocket) | Based on Fasthttp WebSocket for Fiber with Locals support! | + +## 🕶️ Полезный список + +Дополнительные статьи, middleware, примеры или инструменты смотри в нашем [полезном списке](https://github.com/gofiber/awesome-fiber). + +## 👍 Помощь проекту + +Если вы хотите сказать **спасибо** и/или поддержать активное развитие `Fiber`: + +1. Добавьте [GitHub Star](https://github.com/gofiber/fiber/stargazers) в проект. +2. Напишите о проекте [в вашем 𝕏 (Twitter)](https://x.com/intent/tweet?text=Fiber%20is%20an%20Express%20inspired%20%23web%20%23framework%20built%20on%20top%20of%20Fasthttp%2C%20the%20fastest%20HTTP%20engine%20for%20%23Go.%20Designed%20to%20ease%20things%20up%20for%20%23fast%20development%20with%20zero%20memory%20allocation%20and%20%23performance%20in%20mind%20%F0%9F%9A%80%20https%3A%2F%2Fgithub.com%2Fgofiber%2Ffiber). +3. Сделайте обзор фреймворка на [Medium](https://medium.com/), [Dev.to](https://dev.to/) или в личном блоге. +4. Поддержите проект, купив [чашку кофе](https://buymeacoff.ee/fenny). + +## ☕ Поддержка проекта + +Fiber — это проект с открытым исходным кодом, который работает на пожертвования для оплаты счетов, например, нашего доменного имени, GitBook, Netlify и serverless-хостинга. + +Если вы хотите поддержать, то ☕ [**купите чашку кофе**](https://buymeacoff.ee/fenny). + +| | Пользователи | Пожертвования | +| :--------------------------------------------------------- | :----------------------------------------------- | :------------ | +| ![](https://avatars.githubusercontent.com/u/204341?s=25) | [@destari](https://github.com/destari) | ☕ x 10 | +| ![](https://avatars.githubusercontent.com/u/63164982?s=25) | [@dembygenesis](https://github.com/dembygenesis) | ☕ x 5 | +| ![](https://avatars.githubusercontent.com/u/56607882?s=25) | [@thomasvvugt](https://github.com/thomasvvugt) | ☕ x 5 | +| ![](https://avatars.githubusercontent.com/u/27820675?s=25) | [@hendratommy](https://github.com/hendratommy) | ☕ x 5 | +| ![](https://avatars.githubusercontent.com/u/1094221?s=25) | [@ekaputra07](https://github.com/ekaputra07) | ☕ x 5 | +| ![](https://avatars.githubusercontent.com/u/194590?s=25) | [@jorgefuertes](https://github.com/jorgefuertes) | ☕ x 5 | +| ![](https://avatars.githubusercontent.com/u/186637?s=25) | [@candidosales](https://github.com/candidosales) | ☕ x 5 | +| ![](https://avatars.githubusercontent.com/u/29659953?s=25) | [@l0nax](https://github.com/l0nax) | ☕ x 3 | +| ![](https://avatars.githubusercontent.com/u/635852?s=25) | [@bihe](https://github.com/bihe) | ☕ x 3 | +| ![](https://avatars.githubusercontent.com/u/307334?s=25) | [@justdave](https://github.com/justdave) | ☕ x 3 | +| ![](https://avatars.githubusercontent.com/u/11155743?s=25) | [@koddr](https://github.com/koddr) | ☕ x 1 | +| ![](https://avatars.githubusercontent.com/u/29042462?s=25) | [@lapolinar](https://github.com/lapolinar) | ☕ x 1 | +| ![](https://avatars.githubusercontent.com/u/2978730?s=25) | [@diegowifi](https://github.com/diegowifi) | ☕ x 1 | +| ![](https://avatars.githubusercontent.com/u/44171355?s=25) | [@ssimk0](https://github.com/ssimk0) | ☕ x 1 | +| ![](https://avatars.githubusercontent.com/u/5638101?s=25) | [@raymayemir](https://github.com/raymayemir) | ☕ x 1 | +| ![](https://avatars.githubusercontent.com/u/619996?s=25) | [@melkorm](https://github.com/melkorm) | ☕ x 1 | +| ![](https://avatars.githubusercontent.com/u/31022056?s=25) | [@marvinjwendt](https://github.com/thomasvvugt) | ☕ x 1 | +| ![](https://avatars.githubusercontent.com/u/31921460?s=25) | [@toishy](https://github.com/toishy) | ☕ x 1 | + +## ‎‍💻 Контрибьютеры + +Code Contributors + +## ⭐️ Stargazers + +Stargazers over time + +## ⚠️ Лицензия + +Copyright (c) 2019-настоящее время [Fenny](https://github.com/fenny) и [Контрибьютеры](https://github.com/gofiber/fiber/graphs/contributors). `Fiber` - это свободное программное обсепечение с открытым исходным кодом лицензированное под [MIT License](https://github.com/gofiber/fiber/blob/master/LICENSE). Официальный логотип создан [Vic Shóstak](https://github.com/koddr) и распространяется под [Creative Commons](https://creativecommons.org/licenses/by-sa/4.0/) лицензией (CC BY-SA 4.0 International). + +**Third-party library licenses** + +- [colorable](https://github.com/mattn/go-colorable/blob/master/LICENSE) +- [isatty](https://github.com/mattn/go-isatty/blob/master/LICENSE) +- [runewidth](https://github.com/mattn/go-runewidth/blob/master/LICENSE) +- [fasthttp](https://github.com/valyala/fasthttp/blob/master/LICENSE) +- [bytebufferpool](https://github.com/valyala/bytebufferpool/blob/master/LICENSE) +- [fwd](https://github.com/philhofer/fwd/blob/master/LICENSE.md) +- [go-ole](https://github.com/go-ole/go-ole/blob/master/LICENSE) +- [gopsutil](https://github.com/shirou/gopsutil/blob/master/LICENSE) +- [msgp](https://github.com/tinylib/msgp/blob/master/LICENSE) +- [schema](https://github.com/gorilla/schema/blob/master/LICENSE) +- [uuid](https://github.com/google/uuid/blob/master/LICENSE) +- [wmi](https://github.com/StackExchange/wmi/blob/master/LICENSE) diff --git a/.github/README_sa.md b/.github/README_sa.md new file mode 100644 index 0000000..a5a54e9 --- /dev/null +++ b/.github/README_sa.md @@ -0,0 +1,758 @@ +

+ + + + + Fiber + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + +

+

+

+ Fiber هو إطار ويب مستوحى من Express مبني على Fasthttp, اسرع محرك HTTP لـ Go. مصمم ليكون سهل لأغراض السرعة مع عدم تخصيص ذاكرة والأداء و الاداء العالي دائما. +
+

+ +## ⚡️ بداية سريعة + +
+ +```go +package main + +import "github.com/gofiber/fiber/v2" + +func main() { + app := fiber.New() + + app.Get("/", func(c *fiber.Ctx) error { + return c.SendString("Hello, World 👋!") + }) + + app.Listen(":3000") +} +``` + +
+ +## 🤖 مقايس الاداء + +يتم تنفيذ هذه الاختبارات من قبل [TechEmpower](https://www.techempower.com/benchmarks/#section=data-r19&hw=ph&test=plaintext) و [Go Web](https://github.com/smallnest/go-web-framework-benchmark). إذا كنت تريد رؤية جميع النتائج ، يرجى زيارة موقعنا [Wiki](https://docs.gofiber.io/extra/benchmarks). + +

+ + +

+ +## ⚙️ تثبيت + +تأكد من تثبيت Go ([تحميل](https://go.dev/dl/)). الإصدار `1.17` أو أعلى مطلوب. + +ابدأ مشروعك بإنشاء مجلد ثم تشغيله `go mod init github.com/your/repo` ([أعرف أكثر](https://go.dev/blog/using-go-modules)) +داخل المجلد. ثم قم بتثبيت Fiber باستخدام ملف [`go get`](https://pkg.go.dev/cmd/go/#hdr-Add_dependencies_to_current_module_and_install_them) أمر: + +
+ +
+ +```bash +go get -u github.com/gofiber/fiber/v2 +``` + +
+ +## 🎯 الميزات + +- قوي [routing](https://docs.gofiber.io/guide/routing) +- يقدم خدمة [static files](https://docs.gofiber.io/api/app#static) +- أقصى [أداء](https://docs.gofiber.io/extra/benchmarks) +- [ذاكرة منخفضة](https://docs.gofiber.io/extra/benchmarks) +- [API endpoints](https://docs.gofiber.io/api/ctx) +- [Middleware](https://docs.gofiber.io/category/-middleware) & [Next](https://docs.gofiber.io/api/ctx#next) مدعوم +- [سريع](https://dev.to/koddr/welcome-to-fiber-an-express-js-styled-fastest-web-framework-written-with-on-golang-497) server-side programming +- [Template engines](https://github.com/gofiber/template) +- [WebSocket دعم](https://github.com/gofiber/websocket) +- [Server-Sent events](https://github.com/gofiber/recipes/tree/master/sse) +- [Rate Limiter](https://docs.gofiber.io/api/middleware/limiter) +- ترجم الى [19 لغة أخرى](https://docs.gofiber.io/) +- وأكثر بكثير, [استكشف Fiber](https://docs.gofiber.io/) + +## 💡 فلسفة + +قوفر(مستخدمي لغة Go الجدد) جديد يجعل التبديل من [Node.js](https://nodejs.org/en/about/) الى [Go](https://go.dev/doc/)تتعامل مع منحنى التعلم قبل أن يتمكنوا من البدء في بناءتطبيقات الويب . Fiber, كـ **إطار الويب**, تم إنشاؤه بفكرة **minimalism** ويتبع **UNIX way**, حتى يتمكن القوفرون الجدد من دخول عالم Go بترحيب حار وموثوق. + +Fiber هو **مستوحى** من Express, إطار الويب الأكثر شعبية على الإنترنت. قمنا بدمج **سهولة** الـ Express و **الأداء الخام** لـ Go. إذا كنت قد قمت بتطبيق تطبيق ويب في Node.js (_using Express or similar_), ستظهر العديد من الأساليب والمبادئ **الاكثر شيوعاً** لك. + +نحن **نصغي** لمستخدمينا [issues](https://github.com/gofiber/fiber/issues), نناقش [channel](https://gofiber.io/discord) _وفي جميع أنحاء الإنترنت_ لإنشاء **سريع**, **مرن** و **مألوف** Go إطار الويب لـ **لأي** مهمة, **الموعد الأخير +** و تطوير **مهارات**! فقط مثل Express تفعل لـ JavaScript عالم. + +## ⚠️ Limitations + +- Due to Fiber's usage of unsafe, the library may not always be compatible with the latest Go version. Fiber 2.40.0 has been tested with Go versions 1.17 to 1.21. +- Fiber is not compatible with net/http interfaces. This means you will not be able to use projects like gqlgen, go-swagger, or any others which are part of the net/http ecosystem. + +## 👀 أمثلة + +فيما يلي بعض الأمثلة الشائعة. إذا كنت ترغب في رؤية المزيد من أمثلة التعليمات البرمجية, يرجى زيارة [Recipes repository](https://github.com/gofiber/recipes) او زيارة [API documentation](https://docs.gofiber.io). + +#### 📖 [**Basic Routing**](https://docs.gofiber.io/#basic-routing) + +
+ +```go +func main() { + app := fiber.New() + + // GET /api/register + app.Get("/api/*", func(c *fiber.Ctx) error { + msg := fmt.Sprintf("✋ %s", c.Params("*")) + return c.SendString(msg) // => ✋ register + }) + + // GET /flights/LAX-SFO + app.Get("/flights/:from-:to", func(c *fiber.Ctx) error { + msg := fmt.Sprintf("💸 From: %s, To: %s", c.Params("from"), c.Params("to")) + return c.SendString(msg) // => 💸 From: LAX, To: SFO + }) + + // GET /dictionary.txt + app.Get("/:file.:ext", func(c *fiber.Ctx) error { + msg := fmt.Sprintf("📃 %s.%s", c.Params("file"), c.Params("ext")) + return c.SendString(msg) // => 📃 dictionary.txt + }) + + // GET /john/75 + app.Get("/:name/:age/:gender?", func(c *fiber.Ctx) error { + msg := fmt.Sprintf("👴 %s is %s years old", c.Params("name"), c.Params("age")) + return c.SendString(msg) // => 👴 john is 75 years old + }) + + // GET /john + app.Get("/:name", func(c *fiber.Ctx) error { + msg := fmt.Sprintf("Hello, %s 👋!", c.Params("name")) + return c.SendString(msg) // => Hello john 👋! + }) + + log.Fatal(app.Listen(":3000")) +} + +``` + +
+ +
+ +#### 📖 [**Route Naming**](https://docs.gofiber.io/api/app#name) + +```go +func main() { + app := fiber.New() + + // GET /api/register + app.Get("/api/*", func(c *fiber.Ctx) error { + msg := fmt.Sprintf("✋ %s", c.Params("*")) + return c.SendString(msg) // => ✋ register + }).Name("api") + + data, _ := json.MarshalIndent(app.GetRoute("api"), "", " ") + fmt.Print(string(data)) + // Prints: + // { + // "method": "GET", + // "name": "api", + // "path": "/api/*", + // "params": [ + // "*1" + // ] + // } + + + log.Fatal(app.Listen(":3000")) +} + +``` + +
+ +#### 📖 [**Serving Static Files**](https://docs.gofiber.io/api/app#static) + +
+ +```go +func main() { + app := fiber.New() + + app.Static("/", "./public") + // => http://localhost:3000/js/script.js + // => http://localhost:3000/css/style.css + + app.Static("/prefix", "./public") + // => http://localhost:3000/prefix/js/script.js + // => http://localhost:3000/prefix/css/style.css + + app.Static("*", "./public/index.html") + // => http://localhost:3000/any/path/shows/index/html + + log.Fatal(app.Listen(":3000")) +} + +``` + +
+ +#### 📖 [**Middleware & Next**](https://docs.gofiber.io/api/ctx#next) + +
+ +```go +func main() { + app := fiber.New() + + // Match any route + app.Use(func(c *fiber.Ctx) error { + fmt.Println("🥇 First handler") + return c.Next() + }) + + // Match all routes starting with /api + app.Use("/api", func(c *fiber.Ctx) error { + fmt.Println("🥈 Second handler") + return c.Next() + }) + + // GET /api/register + app.Get("/api/list", func(c *fiber.Ctx) error { + fmt.Println("🥉 Last handler") + return c.SendString("Hello, World 👋!") + }) + + log.Fatal(app.Listen(":3000")) +} + +``` + +
+ +
+ 📚 إظهار المزيد من أمثلة التعليمات البرمجية + +### Views engines + +📖 [Config](https://docs.gofiber.io/api/fiber#config) +📖 [Engines](https://github.com/gofiber/template) +📖 [Render](https://docs.gofiber.io/api/ctx#render) + +Fiber defaults to the [html/template](https://pkg.go.dev/html/template/) when no view engine is set. + +If you want to execute partials or use a different engine like [amber](https://github.com/eknkc/amber), [handlebars](https://github.com/aymerick/raymond), [mustache](https://github.com/cbroglie/mustache) or [pug](https://github.com/Joker/jade) etc.. + +Checkout our [Template](https://github.com/gofiber/template) package that support multiple view engines. + +
+ +```go +package main + +import ( + "github.com/gofiber/fiber/v2" + "github.com/gofiber/template/pug" +) + +func main() { + // You can setup Views engine before initiation app: + app := fiber.New(fiber.Config{ + Views: pug.New("./views", ".pug"), + }) + + // And now, you can call template `./views/home.pug` like this: + app.Get("/", func(c *fiber.Ctx) error { + return c.Render("home", fiber.Map{ + "title": "Homepage", + "year": 1999, + }) + }) + + log.Fatal(app.Listen(":3000")) +} +``` + +
+ +### Grouping routes into chains + +📖 [Group](https://docs.gofiber.io/api/app#group) + +
+ +```go +func middleware(c *fiber.Ctx) error { + fmt.Println("Don't mind me!") + return c.Next() +} + +func handler(c *fiber.Ctx) error { + return c.SendString(c.Path()) +} + +func main() { + app := fiber.New() + + // Root API route + api := app.Group("/api", middleware) // /api + + // API v1 routes + v1 := api.Group("/v1", middleware) // /api/v1 + v1.Get("/list", handler) // /api/v1/list + v1.Get("/user", handler) // /api/v1/user + + // API v2 routes + v2 := api.Group("/v2", middleware) // /api/v2 + v2.Get("/list", handler) // /api/v2/list + v2.Get("/user", handler) // /api/v2/user + + // ... +} + +``` + +
+ +### Middleware logger + +📖 [Logger](https://docs.gofiber.io/api/middleware/logger) + +
+ +```go +package main + +import ( + "log" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/logger" +) + +func main() { + app := fiber.New() + + app.Use(logger.New()) + + // ... + + log.Fatal(app.Listen(":3000")) +} +``` + +
+ +### Cross-Origin Resource Sharing (CORS) + +📖 [CORS](https://docs.gofiber.io/api/middleware/cors) + +
+ +```go +import ( + "log" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/cors" +) + +func main() { + app := fiber.New() + + app.Use(cors.New()) + + // ... + + log.Fatal(app.Listen(":3000")) +} +``` + +
+ +التحقق من CORS عن طريق تمرير أي مجال `Origin` العنوان: + +
+ +```bash +curl -H "Origin: http://example.com" --verbose http://localhost:3000 +``` + +
+ +### مخصص 404 response + +📖 [HTTP Methods](https://docs.gofiber.io/api/ctx#status) + +
+ +```go +func main() { + app := fiber.New() + + app.Static("/", "./public") + + app.Get("/demo", func(c *fiber.Ctx) error { + return c.SendString("This is a demo!") + }) + + app.Post("/register", func(c *fiber.Ctx) error { + return c.SendString("Welcome!") + }) + + // Last middleware to match anything + app.Use(func(c *fiber.Ctx) error { + return c.SendStatus(404) + // => 404 "Not Found" + }) + + log.Fatal(app.Listen(":3000")) +} +``` + +
+ +### JSON Response + +📖 [JSON](https://docs.gofiber.io/api/ctx#json) + +
+ +```go +type User struct { + Name string `json:"name"` + Age int `json:"age"` +} + +func main() { + app := fiber.New() + + app.Get("/user", func(c *fiber.Ctx) error { + return c.JSON(&User{"John", 20}) + // => {"name":"John", "age":20} + }) + + app.Get("/json", func(c *fiber.Ctx) error { + return c.JSON(fiber.Map{ + "success": true, + "message": "Hi John!", + }) + // => {"success":true, "message":"Hi John!"} + }) + + log.Fatal(app.Listen(":3000")) +} +``` + +
+ +### WebSocket Upgrade + +📖 [Websocket](https://github.com/gofiber/websocket) + +
+ +```go +import ( + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/websocket" +) + +func main() { + app := fiber.New() + + app.Get("/ws", websocket.New(func(c *websocket.Conn) { + for { + mt, msg, err := c.ReadMessage() + if err != nil { + log.Println("read:", err) + break + } + log.Printf("recv: %s", msg) + err = c.WriteMessage(mt, msg) + if err != nil { + log.Println("write:", err) + break + } + } + })) + + log.Fatal(app.Listen(":3000")) + // ws://localhost:3000/ws +} +``` + +
+ +### Server-Sent Events + +📖 [More Info](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events) + +
+ +```go +import ( + "github.com/gofiber/fiber/v2" + "github.com/valyala/fasthttp" +) + +func main() { + app := fiber.New() + + app.Get("/sse", func(c *fiber.Ctx) error { + c.Set("Content-Type", "text/event-stream") + c.Set("Cache-Control", "no-cache") + c.Set("Connection", "keep-alive") + c.Set("Transfer-Encoding", "chunked") + + c.Context().SetBodyStreamWriter(fasthttp.StreamWriter(func(w *bufio.Writer) { + fmt.Println("WRITER") + var i int + + for { + i++ + msg := fmt.Sprintf("%d - the time is %v", i, time.Now()) + fmt.Fprintf(w, "data: Message: %s\n\n", msg) + fmt.Println(msg) + + w.Flush() + time.Sleep(5 * time.Second) + } + })) + + return nil + }) + + log.Fatal(app.Listen(":3000")) +} +``` + +
+ +### Recover middleware + +📖 [Recover](https://docs.gofiber.io/api/middleware/recover) + +
+ +```go +import ( + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/recover" +) + +func main() { + app := fiber.New() + + app.Use(recover.New()) + + app.Get("/", func(c *fiber.Ctx) error { + panic("normally this would crash your app") + }) + + log.Fatal(app.Listen(":3000")) +} +``` + +
+
+ +## 🧬 Internal Middleware + +Here is a list of middleware that are included within the Fiber framework. + +| Middleware | Description | +| :------------------------------------------------------------------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [basicauth](https://github.com/gofiber/fiber/tree/master/middleware/basicauth) | Basic auth middleware provides an HTTP basic authentication. It calls the next handler for valid credentials and 401 Unauthorized for missing or invalid credentials. | +| [cache](https://github.com/gofiber/fiber/tree/master/middleware/cache) | Intercept and cache responses | +| [compress](https://github.com/gofiber/fiber/tree/master/middleware/compress) | Compression middleware for Fiber, it supports `deflate`, `gzip` and `brotli` by default. | +| [cors](https://github.com/gofiber/fiber/tree/master/middleware/cors) | Enable cross-origin resource sharing \(CORS\) with various options. | +| [csrf](https://github.com/gofiber/fiber/tree/master/middleware/csrf) | Protect from CSRF exploits. | +| [encryptcookie](https://github.com/gofiber/fiber/tree/master/middleware/encryptcookie) | Encrypt middleware which encrypts cookie values. | +| [envvar](https://github.com/gofiber/fiber/tree/master/middleware/envvar) | Expose environment variables with providing an optional config. | +| [etag](https://github.com/gofiber/fiber/tree/master/middleware/etag) | ETag middleware that lets caches be more efficient and save bandwidth, as a web server does not need to resend a full response if the content has not changed. | +| [expvar](https://github.com/gofiber/fiber/tree/master/middleware/expvar) | Expvar middleware that serves via its HTTP server runtime exposed variants in the JSON format. | +| [favicon](https://github.com/gofiber/fiber/tree/master/middleware/favicon) | Ignore favicon from logs or serve from memory if a file path is provided. | +| [filesystem](https://github.com/gofiber/fiber/tree/master/middleware/filesystem) | FileSystem middleware for Fiber, special thanks and credits to Alireza Salary | +| [limiter](https://github.com/gofiber/fiber/tree/master/middleware/limiter) | Rate-limiting middleware for Fiber. Use to limit repeated requests to public APIs and/or endpoints such as password reset. | +| [logger](https://github.com/gofiber/fiber/tree/master/middleware/logger) | HTTP request/response logger. | +| [monitor](https://github.com/gofiber/fiber/tree/master/middleware/monitor) | Monitor middleware that reports server metrics, inspired by express-status-monitor | +| [pprof](https://github.com/gofiber/fiber/tree/master/middleware/pprof) | Special thanks to Matthew Lee \(@mthli\) | +| [proxy](https://github.com/gofiber/fiber/tree/master/middleware/proxy) | Allows you to proxy requests to a multiple servers | +| [recover](https://github.com/gofiber/fiber/tree/master/middleware/recover) | Recover middleware recovers from panics anywhere in the stack chain and handles the control to the centralized[ ErrorHandler](https://docs.gofiber.io/guide/error-handling). | +| [requestid](https://github.com/gofiber/fiber/tree/master/middleware/requestid) | Adds a requestid to every request. | +| [session](https://github.com/gofiber/fiber/tree/master/middleware/session) | Session middleware. NOTE: This middleware uses our Storage package. | +| [skip](https://github.com/gofiber/fiber/tree/master/middleware/skip) | Skip middleware that skips a wrapped handler is a predicate is true. | +| [timeout](https://github.com/gofiber/fiber/tree/master/middleware/timeout) | Adds a max time for a request and forwards to ErrorHandler if it is exceeded. | +| [keyauth](https://github.com/gofiber/keyauth) | Key auth middleware provides a key based authentication. | +| [redirect](https://github.com/gofiber/redirect) | Redirect middleware | +| [rewrite](https://github.com/gofiber/rewrite) | Rewrite middleware rewrites the URL path based on provided rules. It can be helpful for backward compatibility or just creating cleaner and more descriptive links. | +| [adaptor](https://github.com/gofiber/adaptor) | Converter for net/http handlers to/from Fiber request handlers, special thanks to @arsmn! | +| [helmet](https://github.com/gofiber/helmet) | Helps secure your apps by setting various HTTP headers. | + +## 🧬 External Middleware + +List of externally hosted middleware modules and maintained by the [Fiber team](https://github.com/orgs/gofiber/people). + +| Middleware | Description | +| :------------------------------------------------ | :-------------------------------------------------------------------------------------------------------------------- | +| [jwt](https://github.com/gofiber/jwt) | JWT returns a JSON Web Token \(JWT\) auth middleware. | +| [storage](https://github.com/gofiber/storage) | Premade storage drivers that implement the Storage interface, designed to be used with various Fiber middlewares. | +| [template](https://github.com/gofiber/template) | This package contains 8 template engines that can be used with Fiber `v1.10.x` Go version 1.13 or higher is required. | +| [websocket](https://github.com/gofiber/websocket) | Based on Fasthttp WebSocket for Fiber with Locals support! | + +## 🕶️ Awesome List + +For more articles, middlewares, examples or tools check our [awesome list](https://github.com/gofiber/awesome-fiber). + +## 👍 مساهمة + +إذا كنت تريد أن تقول **شكرا جزيل** و/او دعم التنمية النشطة للـ `Fiber`: + +1. اضف [GitHub نجمة](https://github.com/gofiber/fiber/stargazers) للمشروع. +2. غرد عن المشروع [في تويتر ](https://x.com/intent/tweet?text=Fiber%20is%20an%20Express%20inspired%20%23web%20%23framework%20built%20on%20top%20of%20Fasthttp%2C%20the%20fastest%20HTTP%20engine%20for%20%23Go.%20Designed%20to%20ease%20things%20up%20for%20%23fast%20development%20with%20zero%20memory%20allocation%20and%20%23performance%20in%20mind%20%F0%9F%9A%80%20https%3A%2F%2Fgithub.com%2Fgofiber%2Ffiber). +3. اكتب مراجعة أو برنامج تعليمي عن [Medium](https://medium.com/), [Dev.to](https://dev.to/) او في موقعك الشخصي. +4. دعم المشروع بالتبرع بـ [كوب من القهوة](https://buymeacoff.ee/fenny). + +## ☕ الداعمين + +Fiber هو مشروع مفتوح المصدر يعمل على التبرعات لدفع الفواتير ، على سبيل المثال اسم النطاق الخاص بنا , gitbook, netlify and serverless الاستضافة. إذا كنت تريد دعم Fiber, تستطيع ☕ [**شراء كوب قهوة هنا**](https://buymeacoff.ee/fenny). + +| | المستخدم | التبرع | +| :--------------------------------------------------------- | :----------------------------------------------- | :------ | +| ![](https://avatars.githubusercontent.com/u/204341?s=25) | [@destari](https://github.com/destari) | ☕ x 10 | +| ![](https://avatars.githubusercontent.com/u/63164982?s=25) | [@dembygenesis](https://github.com/dembygenesis) | ☕ x 5 | +| ![](https://avatars.githubusercontent.com/u/56607882?s=25) | [@thomasvvugt](https://github.com/thomasvvugt) | ☕ x 5 | +| ![](https://avatars.githubusercontent.com/u/27820675?s=25) | [@hendratommy](https://github.com/hendratommy) | ☕ x 5 | +| ![](https://avatars.githubusercontent.com/u/1094221?s=25) | [@ekaputra07](https://github.com/ekaputra07) | ☕ x 5 | +| ![](https://avatars.githubusercontent.com/u/194590?s=25) | [@jorgefuertes](https://github.com/jorgefuertes) | ☕ x 5 | +| ![](https://avatars.githubusercontent.com/u/186637?s=25) | [@candidosales](https://github.com/candidosales) | ☕ x 5 | +| ![](https://avatars.githubusercontent.com/u/29659953?s=25) | [@l0nax](https://github.com/l0nax) | ☕ x 3 | +| ![](https://avatars.githubusercontent.com/u/635852?s=25) | [@bihe](https://github.com/bihe) | ☕ x 3 | +| ![](https://avatars.githubusercontent.com/u/307334?s=25) | [@justdave](https://github.com/justdave) | ☕ x 3 | +| ![](https://avatars.githubusercontent.com/u/11155743?s=25) | [@koddr](https://github.com/koddr) | ☕ x 1 | +| ![](https://avatars.githubusercontent.com/u/29042462?s=25) | [@lapolinar](https://github.com/lapolinar) | ☕ x 1 | +| ![](https://avatars.githubusercontent.com/u/2978730?s=25) | [@diegowifi](https://github.com/diegowifi) | ☕ x 1 | +| ![](https://avatars.githubusercontent.com/u/44171355?s=25) | [@ssimk0](https://github.com/ssimk0) | ☕ x 1 | +| ![](https://avatars.githubusercontent.com/u/5638101?s=25) | [@raymayemir](https://github.com/raymayemir) | ☕ x 1 | +| ![](https://avatars.githubusercontent.com/u/619996?s=25) | [@melkorm](https://github.com/melkorm) | ☕ x 1 | +| ![](https://avatars.githubusercontent.com/u/31022056?s=25) | [@marvinjwendt](https://github.com/thomasvvugt) | ☕ x 1 | +| ![](https://avatars.githubusercontent.com/u/31921460?s=25) | [@toishy](https://github.com/toishy) | ☕ x 1 | + +## ‎‍💻 المساهمون في كتابة الكود + +Code Contributors + +## ⭐️ Stargazers + +Stargazers over time + +## ⚠️ رخصة + +Copyright (c) 2019-present [Fenny](https://github.com/fenny) and [Contributors](https://github.com/gofiber/fiber/graphs/contributors). `Fiber` هو برنامج مجاني ومفتوح المصدر مرخص بموجب [MIT License](https://github.com/gofiber/fiber/blob/master/LICENSE). تم إنشاء الشعار الرسمي من قبل [Vic Shóstak](https://github.com/koddr) ووزعت تحت [Creative Commons](https://creativecommons.org/licenses/by-sa/4.0/) رخصة (CC BY-SA 4.0 International). + +**Third-party library licenses** + +- [colorable](https://github.com/mattn/go-colorable/blob/master/LICENSE) +- [isatty](https://github.com/mattn/go-isatty/blob/master/LICENSE) +- [runewidth](https://github.com/mattn/go-runewidth/blob/master/LICENSE) +- [fasthttp](https://github.com/valyala/fasthttp/blob/master/LICENSE) +- [bytebufferpool](https://github.com/valyala/bytebufferpool/blob/master/LICENSE) +- [fwd](https://github.com/philhofer/fwd/blob/master/LICENSE.md) +- [go-ole](https://github.com/go-ole/go-ole/blob/master/LICENSE) +- [gopsutil](https://github.com/shirou/gopsutil/blob/master/LICENSE) +- [msgp](https://github.com/tinylib/msgp/blob/master/LICENSE) +- [schema](https://github.com/gorilla/schema/blob/master/LICENSE) +- [uuid](https://github.com/google/uuid/blob/master/LICENSE) +- [wmi](https://github.com/StackExchange/wmi/blob/master/LICENSE) diff --git a/.github/README_tr.md b/.github/README_tr.md new file mode 100644 index 0000000..3ab0f15 --- /dev/null +++ b/.github/README_tr.md @@ -0,0 +1,686 @@ +

+ + + + + Fiber + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + +

+

+ Fiber, Go için en hızlı HTTP sunucusu olan Fasthttp üzerine inşa edilmiş ve Express'ten ilham almış bir web framework'üdür. Sıfır bellek ataması ve performans göz önünde bulundurularak hızlı geliştirme ve kolay geliştirme için tasarlanmıştır. +

+ +## ⚡️ Hızlı Başlangıç + +```go +package main + +import "github.com/gofiber/fiber/v2" + +func main() { + app := fiber.New() + + app.Get("/", func(c *fiber.Ctx) error { + return c.SendString("Hello, World 👋!") + }) + + app.Listen(":3000") +} +``` + +## 🤖 Performans Ölçümleri + +Bu testler [TechEmpower](https://www.techempower.com/benchmarks/#section=data-r19&hw=ph&test=plaintext) ve [Go Web](https://github.com/smallnest/go-web-framework-benchmark) tarafından gerçekleştirildi. Bütün sonuçları görmek için lütfen [Wiki](https://docs.gofiber.io/extra/benchmarks) sayfasını ziyaret ediniz. + +

+ + +

+ +## ⚙️ Kurulum + +Go'nun `1.17` sürümü ([indir](https://go.dev/dl/)) veya daha yüksek bir sürüm gerekli. + +Bir dizin oluşturup dizinin içinde `go mod init github.com/your/repo` komutunu yazarak projenizi geliştirmeye başlayın ([daha fazla öğren](https://go.dev/blog/using-go-modules)). Ardından Fiber'ı kurmak için [`go get`](https://pkg.go.dev/cmd/go/#hdr-Add_dependencies_to_current_module_and_install_them) komutunu çalıştırın: + +```bash +go get -u github.com/gofiber/fiber/v2 +``` + +## 🎯 Özellikler + +- Güçlü [routing](https://docs.gofiber.io/guide/routing) +- [Statik dosya](https://docs.gofiber.io/api/app#static) sunumu +- Olağanüstü [performans](https://docs.gofiber.io/extra/benchmarks) +- [Düşük bellek](https://docs.gofiber.io/extra/benchmarks) kullanımı +- [API uç noktaları](https://docs.gofiber.io/api/ctx) +- Middleware'lar & [Next](https://docs.gofiber.io/api/ctx#next) desteği +- [Hızlı](https://dev.to/koddr/welcome-to-fiber-an-express-js-styled-fastest-web-framework-written-with-on-golang-497) sunucu taraflı programlama +- [Template motorları](https://github.com/gofiber/template) +- [WebSocket desteği](https://github.com/gofiber/websocket) +- [Server-Sent eventler](https://github.com/gofiber/recipes/tree/master/sse) +- [Rate Limiter](https://docs.gofiber.io/api/middleware/limiter) +- [19 dilde](https://docs.gofiber.io/) mevcut +- Ve daha fazlası, [Fiber'ı keşfet](https://docs.gofiber.io/) + +## 💡 Felsefe + +[Node.js](https://nodejs.org/en/about/)'ten [Go](https://go.dev/doc/)'ya geçen yeni gopherlar kendi web uygulamalarını ve mikroservislerini yazmaya başlamadan önce dili öğrenmek ile uğraşıyorlar. Fiber, bir **framework** olarak, **minimalizm** ve **UNIX yolu**nu izleme fikri ile oluşturuldu. Böylece yeni gopherlar sıcak ve güvenilir bir hoş geldin ile Go dünyasına giriş yapabilirler. + +Fiber, internet üzerinde en popüler web framework'ü olan Express'ten **esinlenmiştir**. Biz Express'in **kolaylığını** ve Go'nun **ham performansını** birleştirdik. Daha önce Node.js üzerinde (Express veya benzerini kullanarak) bir web uygulaması geliştirdiyseniz, pek çok metod ve prensip size **oldukça tanıdık** gelecektir. + +## ⚠️ Sınırlamalar + +- Fiber unsafe kullanımı sebebiyle Go'nun son sürümüyle her zaman uyumlu olmayabilir. Fiber 2.40.0, Go 1.17 ile 1.21 sürümleriyle test edildi. +- Fiber net/http arabirimiyle uyumlu değildir. Yani gqlgen veya go-swagger gibi net/http ekosisteminin parçası olan projeleri kullanamazsınız. + +## 👀 Örnekler + +Aşağıda yaygın örneklerden bazıları listelenmiştir. Daha fazla kod örneği görmek için lütfen [Github reposunu](https://github.com/gofiber/recipes) veya [API dokümantasyonunu](https://docs.gofiber.io) ziyaret ediniz. + +#### 📖 [**Basit Rotalama**](https://docs.gofiber.io/#basic-routing) + +```go +func main() { + app := fiber.New() + + // GET /api/kayit + app.Get("/api/*", func(c *fiber.Ctx) error { + msg := fmt.Sprintf("✋ %s", c.Params("*")) + return c.SendString(msg) // => ✋ kayit + }) + + // GET /flights/IST-ESB + app.Get("/flights/:kalkis-:inis", func(c *fiber.Ctx) error { + msg := fmt.Sprintf("💸 Kalkış: %s, İniş: %s", c.Params("kalkis"), c.Params("inis")) + return c.SendString(msg) // => 💸 Kalkış: IST, İniş: ESB + }) + + // GET /sozluk.txt + app.Get("/:file.:ext", func(c *fiber.Ctx) error { + msg := fmt.Sprintf("📃 %s.%s", c.Params("file"), c.Params("ext")) + return c.SendString(msg) // => 📃 sozluk.txt + }) + + // GET /muhittin/75 + app.Get("/:isim/:yas/:cinsiyet?", func(c *fiber.Ctx) error { + msg := fmt.Sprintf("👴 %s %s yaşında", c.Params("isim"), c.Params("yas")) + return c.SendString(msg) // => 👴 muhittin 75 yaşında + }) + + // GET /muhittin + app.Get("/:isim", func(c *fiber.Ctx) error { + msg := fmt.Sprintf("Merhaba, %s 👋!", c.Params("isim")) + return c.SendString(msg) // => Merhaba Muhittin 👋! + }) + + log.Fatal(app.Listen(":3000")) +} + +``` + +#### 📖 [**Route İsimlendirmesi**](https://docs.gofiber.io/api/app#name) + +```go +func main() { + app := fiber.New() + + // GET /api/register + app.Get("/api/*", func(c *fiber.Ctx) error { + msg := fmt.Sprintf("✋ %s", c.Params("*")) + return c.SendString(msg) // => ✋ kayit + }).Name("api") + + data, _ := json.MarshalIndent(app.GetRoute("api"), "", " ") + fmt.Print(string(data)) + // Cikti: + // { + // "method": "GET", + // "name": "api", + // "path": "/api/*", + // "params": [ + // "*1" + // ] + // } + + + log.Fatal(app.Listen(":3000")) +} + +``` + +#### 📖 [**Statik Dosya Sunumu**](https://docs.gofiber.io/api/app#static) + +```go +func main() { + app := fiber.New() + + app.Static("/", "./public") + // => http://localhost:3000/js/script.js + // => http://localhost:3000/css/style.css + + app.Static("/prefix", "./public") + // => http://localhost:3000/prefix/js/script.js + // => http://localhost:3000/prefix/css/style.css + + app.Static("*", "./public/index.html") + // => http://localhost:3000/any/path/shows/index/html + + log.Fatal(app.Listen(":3000")) +} + +``` + +#### 📖 [**Middleware & Next**](https://docs.gofiber.io/api/ctx#next) + +```go +func main() { + app := fiber.New() + + // Bütün routelara etki eder. + app.Use(func(c *fiber.Ctx) error { + fmt.Println("🥇 İlk handler") + return c.Next() + }) + + // /api ile başlayan bütün routelara etki eder. + app.Use("/api", func(c *fiber.Ctx) error { + fmt.Println("🥈 İkinci handler") + return c.Next() + }) + + // GET /api/register + app.Get("/api/list", func(c *fiber.Ctx) error { + fmt.Println("🥉 Son handler") + return c.SendString("Merhaba, Dünya 👋!") + }) + + log.Fatal(app.Listen(":3000")) +} + +``` + +
+ 📚 Daha fazla kod örneği göster + +### View Motorları + +📖 [Yapılandırma](https://docs.gofiber.io/api/fiber#config) +📖 [Motorlar](https://github.com/gofiber/template) +📖 [Render](https://docs.gofiber.io/api/ctx#render) + +Hiçbir View Motoru ayarlanmadığında Fiber varsayılan olarak [html/template'a](https://pkg.go.dev/html/template/) geçer. + +Kısmi yürütmek istiyorsanız veya [amber](https://github.com/eknkc/amber), [handlebars](https://github.com/aymerick/raymond), [mustache](https://github.com/cbroglie/mustache) veya [pug](https://github.com/Joker/jade) gibi farklı motorlar kullanmak istiyorsanız + +Çoklu View Engine destekleyen [Template'ımıza](https://github.com/gofiber/template göz atın. + +```go +package main + +import ( + "github.com/gofiber/fiber/v2" + "github.com/gofiber/template/pug" +) + +func main() { + // Uygulamayı başlatmadan önce View Engine tanımlayabilirsiniz: + app := fiber.New(fiber.Config{ + Views: pug.New("./views", ".pug"), + }) + + // Ve şimdi `./views/home.pug` templateni şu şekilde çağırabilirsiniz: + app.Get("/", func(c *fiber.Ctx) error { + return c.Render("home", fiber.Map{ + "title": "Homepage", + "year": 1999, + }) + }) + + log.Fatal(app.Listen(":3000")) +} +``` + +### Routeları Zincirler Halinde Gruplama + +📖 [Group](https://docs.gofiber.io/api/app#group) + +```go +func middleware(c *fiber.Ctx) error { + fmt.Println("Beni umursama!") + return c.Next() +} + +func handler(c *fiber.Ctx) error { + return c.SendString(c.Path()) +} + +func main() { + app := fiber.New() + + // Root API route + api := app.Group("/api", middleware) // /api + + // API v1 routeları + v1 := api.Group("/v1", middleware) // /api/v1 + v1.Get("/list", handler) // /api/v1/list + v1.Get("/user", handler) // /api/v1/user + + // API v2 routeları + v2 := api.Group("/v2", middleware) // /api/v2 + v2.Get("/list", handler) // /api/v2/list + v2.Get("/user", handler) // /api/v2/user + + // ... +} +``` + +### Middleware Loglama (Logger) + +📖 [Logger](https://docs.gofiber.io/api/middleware/logger) + +```go +package main + +import ( + "fmt" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/logger" +) + +func main() { + app := fiber.New() + + app.Use(logger.New()) + + // ... + + log.Fatal(app.Listen(":3000")) +} +``` + +### Farklı Originler Arası Kaynak Paylaşımı (CORS) + +📖 [CORS](https://docs.gofiber.io/api/middleware/cors) + +```go +import ( + "log" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/cors" +) + +func main() { + app := fiber.New() + + app.Use(cors.New()) + + // ... + + log.Fatal(app.Listen(":3000")) +} +``` + +`Origin` başlığı içinde herhangi bir alan adı kullanarak CORS'u kontrol edin: + +```bash +curl -H "Origin: http://example.com" --verbose http://localhost:3000 +``` + +### Özelleştirilebilir 404 yanıtları + +📖 [HTTP Methodları](https://docs.gofiber.io/api/ctx#status) + +```go +func main() { + app := fiber.New() + + app.Static("/", "./public") + + app.Get("/demo", func(c *fiber.Ctx) error { + return c.SendString("Bu bir demodur!") + }) + + app.Post("/register", func(c *fiber.Ctx) error { + return c.SendString("Hoşgeldiniz!") + }) + + // Hiçbir endpointle eşleşmezse gideceği middleware ve yanıtı. + app.Use(func(c *fiber.Ctx) error { + return c.SendStatus(404) + // => 404 "Sayfa bulunamadı" + }) + + log.Fatal(app.Listen(":3000")) +} +``` + +### JSON Yanıtları + +📖 [JSON](https://docs.gofiber.io/api/ctx#json) + +```go +type User struct { + Isim string `json:"name"` + Yas int `json:"age"` +} + +func main() { + app := fiber.New() + + app.Get("/user", func(c *fiber.Ctx) error { + return c.JSON(&User{"Muhittin Topalak", 20}) + // => {"Isim":"Muhittin Topalak", "Yas":20} + }) + + app.Get("/json", func(c *fiber.Ctx) error { + return c.JSON(fiber.Map{ + "success": true, + "mesaj": "Merhaba Muhittin Topalak!", + }) + // => {"success":true, "message":"Merhaba Muhittin Topalak!"} + }) + + log.Fatal(app.Listen(":3000")) +} +``` + +### WebSocket Desteği + +📖 [Websocket](https://github.com/gofiber/websocket) + +```go +import ( + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/websocket" +) + +func main() { + app := fiber.New() + + app.Get("/ws", websocket.New(func(c *websocket.Conn) { + for { + mt, msg, err := c.ReadMessage() + if err != nil { + log.Println("read:", err) + break + } + log.Printf("recv: %s", msg) + err = c.WriteMessage(mt, msg) + if err != nil { + log.Println("write:", err) + break + } + } + })) + + log.Fatal(app.Listen(":3000")) + // ws://localhost:3000/ws +} +``` + +### Server-Sent Eventler + +📖 [Daha Fazla Bilgi](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events) + +```go +import ( + "github.com/gofiber/fiber/v2" + "github.com/valyala/fasthttp" +) + +func main() { + app := fiber.New() + + app.Get("/sse", func(c *fiber.Ctx) error { + c.Set("Content-Type", "text/event-stream") + c.Set("Cache-Control", "no-cache") + c.Set("Connection", "keep-alive") + c.Set("Transfer-Encoding", "chunked") + + c.Context().SetBodyStreamWriter(fasthttp.StreamWriter(func(w *bufio.Writer) { + fmt.Println("WRITER") + var i int + + for { + i++ + msg := fmt.Sprintf("%d - the time is %v", i, time.Now()) + fmt.Fprintf(w, "data: Message: %s\n\n", msg) + fmt.Println(msg) + + w.Flush() + time.Sleep(5 * time.Second) + } + })) + + return nil + }) + + log.Fatal(app.Listen(":3000")) +} +``` + +### Middleware Kurtarıcısı + +📖 [Recover](https://docs.gofiber.io/api/middleware/recover) + +```go +import ( + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/recover" +) + +func main() { + app := fiber.New() + + app.Use(recover.New()) + + app.Get("/", func(c *fiber.Ctx) error { + panic("normalde bu uygulamanızı çökertir.") + }) + + log.Fatal(app.Listen(":3000")) +} +``` + +
+ +## 🧬 Dahili Middlewarelar + +Fiber'a dahil edilen middlewareların bir listesi aşağıda verilmiştir. + +| Middleware | Açıklama | +| :------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [basicauth](https://github.com/gofiber/fiber/tree/master/middleware/basicauth) | Basic auth middleware'ı, bir HTTP Basic auth sağlar. Geçerli kimlik bilgileri için sonraki handlerı ve eksik veya geçersiz kimlik bilgileri için 401 döndürür. | +| [cache](https://github.com/gofiber/fiber/tree/master/middleware/cache) | Reponseları durdur ve önbelleğe al. | +| [compress](https://github.com/gofiber/fiber/tree/master/middleware/compress) | Fiber için sıkıştırma middleware, varsayılan olarak `deflate`, `gzip` ve `brotli`yi destekler. | +| [cors](https://github.com/gofiber/fiber/tree/master/middleware/cors) | Çeşitli seçeneklerle başlangıçlar arası kaynak paylaşımını \(CORS\) etkinleştirin. | +| [csrf](https://github.com/gofiber/fiber/tree/master/middleware/csrf) | CSRF exploitlerinden korunun. | +| [encryptcookie](https://github.com/gofiber/fiber/tree/master/middleware/encryptcookie) | Encrypt middleware'ı cookie değerlerini şifreler. | +| [envvar](https://github.com/gofiber/fiber/tree/master/middleware/envvar) | Environment değişkenlerini belirtilen ayarlara göre dışarıya açar. | +| [etag](https://github.com/gofiber/fiber/tree/master/middleware/etag) | ETag middleware'ı sayfa içeriği değişmediyse bant genişliğini daha verimli kullanmak için tam sayfa içeriğini tekrar göndermez. | +| [expvar](https://github.com/gofiber/fiber/tree/master/middleware/expvar) | Expvar middleware, HTTP serverinin bazı runtime değişkenlerini JSON formatında sunar. | +| [favicon](https://github.com/gofiber/fiber/tree/master/middleware/favicon) | Bir dosya yolu sağlanmışsa, loglardaki favicon'u yoksayar veya bellekten sunar. | +| [filesystem](https://github.com/gofiber/fiber/tree/master/middleware/filesystem) | Fiber için FileSystem middleware, Alireza Salary'e özel teşekkürler. | +| [limiter](https://github.com/gofiber/fiber/tree/master/middleware/limiter) | Fiber için hız sınırlayıcı middleware'i. Açık API'lere ve/veya parola sıfırlama gibi endpointlere yönelik tekrarlanan istekleri sınırlamak için kullanın. | +| [logger](https://github.com/gofiber/fiber/tree/master/middleware/logger) | HTTP istek/yanıt logger'ı. | +| [monitor](https://github.com/gofiber/fiber/tree/master/middleware/monitor) | Monitor middleware'ı sunucu metriklerini rapor eder, express-status-monitor'den esinlenildi. | +| [pprof](https://github.com/gofiber/fiber/tree/master/middleware/pprof) | Matthew Lee'ye özel teşekkürler \(@mthli\). | +| [proxy](https://github.com/gofiber/fiber/tree/master/middleware/proxy) | Birden çok sunucuya proxy istekleri yapmanızı sağlar. | +| [recover](https://github.com/gofiber/fiber/tree/master/middleware/recover) | Recover middleware'i, stack chain'ini herhangi bir yerindeki paniklerden kurtulur ve kontrolü merkezileştirilmiş [ErrorHandler'e](https://docs.gofiber.io/guide/error-handling) verir. | +| [requestid](https://github.com/gofiber/fiber/tree/master/middleware/requestid) | Her requeste id verir. | +| [session](https://github.com/gofiber/fiber/tree/master/middleware/session) | Session için middleware. NOTE: Bu middleware Fiber'in Storage yapısını kullanır. | +| [skip](https://github.com/gofiber/fiber/tree/master/middleware/skip) | Skip middleware'ı verilen koşul `true` olduğunda handler'ı atlar ve işlemez. | +| [timeout](https://github.com/gofiber/fiber/tree/master/middleware/timeout) | Bir request için maksimum süre ekler ve aşılırsa ErrorHandler'a iletir. | +| [keyauth](https://github.com/gofiber/keyauth) | Key auth middleware, key tabanlı bir authentication sağlar. | +| [redirect](https://github.com/gofiber/redirect) | Yönlendirme middleware 'ı. | +| [rewrite](https://github.com/gofiber/rewrite) | Rewrite middleware, sağlanan kurallara göre URL yolunu yeniden yazar. Geriye dönük uyumluluk için veya yalnızca daha temiz ve daha açıklayıcı bağlantılar oluşturmak için yardımcı olabilir. | +| [adaptor](https://github.com/gofiber/adaptor) | Fiber request handlerdan net/http handlerları için dönüştürücü, @arsmn'a özel teşekkürler! | +| [helmet](https://github.com/gofiber/helmet) | Çeşitli HTTP headerları ayarlayarak uygulamalarınızın güvenliğini sağlamaya yardımcı olur. | + +## 🧬 Harici Middlewarelar + +Harici olarak barındırılan middlewareların modüllerinin listesi. Bu middlewarelar, [Fiber ekibi](https://github.com/orgs/gofiber/people) tarafından geliştirilir. + +| Middleware | Açıklama | +| :------------------------------------------------ | :---------------------------------------------------------------------------------------------------------------------------------------------------- | +| [jwt](https://github.com/gofiber/jwt) | JWT, bir JSON Web Token \(JWT\) yetkilendirmesi döndüren middleware. | +| [storage](https://github.com/gofiber/storage) | Fiber'in Storage yapısını destekleyen birçok storage driver'ı verir. Bu sayede depolama gerektiren Fiber middlewarelarında kolaylıkla kullanılabilir. | +| [template](https://github.com/gofiber/template) | Bu paket, Fiber `v2.x.x`, Go sürüm 1.17 veya üzeri gerekli olduğunda kullanılabilecek 9 template motoru içerir. | +| [websocket](https://github.com/gofiber/websocket) | Yereller desteğiyle Fiber için Fasthttp WebSocket'a dayalıdır! | + +## 🕶️ Awesome Listesi + +Daha fazla yazı, middleware, örnek veya araç için [awesome list](https://github.com/gofiber/awesome-fiber) reposunu kontrol etmeyi unutmayın. + +## 👍 Destek + +Eğer **teşekkür etmek** veya `Fiber`'ın aktif geliştirilmesini desteklemek istiyorsanız: + +1. Projeye [yıldız](https://github.com/gofiber/fiber/stargazers) verebilirsiniz. +2. [𝕏 (Twitter) hesabınızdan](https://x.com/intent/tweet?text=Fiber%20is%20an%20Express%20inspired%20%23web%20%23framework%20built%20on%20top%20of%20Fasthttp%2C%20the%20fastest%20HTTP%20engine%20for%20%23Go.%20Designed%20to%20ease%20things%20up%20for%20%23fast%20development%20with%20zero%20memory%20allocation%20and%20%23performance%20in%20mind%20%F0%9F%9A%80%20https%3A%2F%2Fgithub.com%2Fgofiber%2Ffiber) proje hakkında tweet atabilirsiniz. +3. [Medium](https://medium.com/), [Dev.to](https://dev.to/) veya kişisel blogunuz üzerinden bir inceleme veya eğitici yazı yazabilirsiniz. +4. Projeye [bir fincan kahve](https://buymeacoff.ee/fenny) bağışlayarak destek olabilirsiniz. + +## ☕ Destekçiler + +Fiber; alan adı, gitbook, netlify, serverless yer sağlayıcısı giderleri ve benzeri şeyleri ödemek için bağışlarla yaşayan bir açık kaynaklı projedir. Eğer Fiber'a destek olmak isterseniz, ☕ [**buradan kahve ısmarlayabilirsiniz**](https://buymeacoff.ee/fenny). + +| | User | Donation | +| :--------------------------------------------------------- | :----------------------------------------------- | :------- | +| ![](https://avatars.githubusercontent.com/u/204341?s=25) | [@destari](https://github.com/destari) | ☕ x 10 | +| ![](https://avatars.githubusercontent.com/u/63164982?s=25) | [@dembygenesis](https://github.com/dembygenesis) | ☕ x 5 | +| ![](https://avatars.githubusercontent.com/u/56607882?s=25) | [@thomasvvugt](https://github.com/thomasvvugt) | ☕ x 5 | +| ![](https://avatars.githubusercontent.com/u/27820675?s=25) | [@hendratommy](https://github.com/hendratommy) | ☕ x 5 | +| ![](https://avatars.githubusercontent.com/u/1094221?s=25) | [@ekaputra07](https://github.com/ekaputra07) | ☕ x 5 | +| ![](https://avatars.githubusercontent.com/u/194590?s=25) | [@jorgefuertes](https://github.com/jorgefuertes) | ☕ x 5 | +| ![](https://avatars.githubusercontent.com/u/186637?s=25) | [@candidosales](https://github.com/candidosales) | ☕ x 5 | +| ![](https://avatars.githubusercontent.com/u/29659953?s=25) | [@l0nax](https://github.com/l0nax) | ☕ x 3 | +| ![](https://avatars.githubusercontent.com/u/635852?s=25) | [@bihe](https://github.com/bihe) | ☕ x 3 | +| ![](https://avatars.githubusercontent.com/u/307334?s=25) | [@justdave](https://github.com/justdave) | ☕ x 3 | +| ![](https://avatars.githubusercontent.com/u/11155743?s=25) | [@koddr](https://github.com/koddr) | ☕ x 1 | +| ![](https://avatars.githubusercontent.com/u/29042462?s=25) | [@lapolinar](https://github.com/lapolinar) | ☕ x 1 | +| ![](https://avatars.githubusercontent.com/u/2978730?s=25) | [@diegowifi](https://github.com/diegowifi) | ☕ x 1 | +| ![](https://avatars.githubusercontent.com/u/44171355?s=25) | [@ssimk0](https://github.com/ssimk0) | ☕ x 1 | +| ![](https://avatars.githubusercontent.com/u/5638101?s=25) | [@raymayemir](https://github.com/raymayemir) | ☕ x 1 | +| ![](https://avatars.githubusercontent.com/u/619996?s=25) | [@melkorm](https://github.com/melkorm) | ☕ x 1 | +| ![](https://avatars.githubusercontent.com/u/31022056?s=25) | [@marvinjwendt](https://github.com/thomasvvugt) | ☕ x 1 | +| ![](https://avatars.githubusercontent.com/u/31921460?s=25) | [@toishy](https://github.com/toishy) | ☕ x 1 | + +## ‎‍💻 Koda Katkı Sağlayanlar + +Code Contributors + +## ⭐️ Projeyi Yıldızlayanlar + +Stargazers over time + +## ⚠️ Lisans + +Telif (c) 2019-günümüz [Fenny](https://github.com/fenny) ve [katkıda bulunanlar](https://github.com/gofiber/fiber/graphs/contributors). `Fiber`, [MIT Lisansı](https://github.com/gofiber/fiber/blob/master/LICENSE) altında özgür ve açık kaynaklı bir yazılımdır. Resmî logosu [Vic Shóstak](https://github.com/koddr) tarafından tasarlanmıştır ve [Creative Commons](https://creativecommons.org/licenses/by-sa/4.0/) lisansı altında dağıtımı yapılıyor. (CC BY-SA 4.0 International). + +**Üçüncü Parti Kütüphane Lisansları** + +- [colorable](https://github.com/mattn/go-colorable/blob/master/LICENSE) +- [isatty](https://github.com/mattn/go-isatty/blob/master/LICENSE) +- [runewidth](https://github.com/mattn/go-runewidth/blob/master/LICENSE) +- [fasthttp](https://github.com/valyala/fasthttp/blob/master/LICENSE) +- [bytebufferpool](https://github.com/valyala/bytebufferpool/blob/master/LICENSE) +- [fwd](https://github.com/philhofer/fwd/blob/master/LICENSE.md) +- [go-ole](https://github.com/go-ole/go-ole/blob/master/LICENSE) +- [gopsutil](https://github.com/shirou/gopsutil/blob/master/LICENSE) +- [msgp](https://github.com/tinylib/msgp/blob/master/LICENSE) +- [schema](https://github.com/gorilla/schema/blob/master/LICENSE) +- [uuid](https://github.com/google/uuid/blob/master/LICENSE) +- [wmi](https://github.com/StackExchange/wmi/blob/master/LICENSE) diff --git a/.github/README_uk.md b/.github/README_uk.md new file mode 100644 index 0000000..ef9f365 --- /dev/null +++ b/.github/README_uk.md @@ -0,0 +1,721 @@ +

+ + + + + Fiber + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + +

+ +

+ Fiber — це веб фреймворк, який був натхненний Express + і заснований на Fasthttp, найшвидшому HTTP-двигунові написаному на + Go. Фреймворк розроблено з метою спростити процес швидкої розробки + високопродуктивних веб-додатків з нульовим розподілом пам'яті. +

+ +## ⚡️ Швидкий старт + +```go +package main + +import "github.com/gofiber/fiber/v2" + +func main() { + app := fiber.New() + + app.Get("/", func(c *fiber.Ctx) error { + return c.SendString("Hello, World 👋!") + }) + + app.Listen(":3000") +} +``` + +## 🤖 Еталонні показники + +Тестування проводилося за допомогою [TechEmpower](https://www.techempower.com/benchmarks/#section=data-r19&hw=ph&test=plaintext) +та [Go Web](https://github.com/smallnest/go-web-framework-benchmark). Якщо ви хочете побачити всі результати, будь ласка +відвідайте наш [Wiki](https://docs.gofiber.io/extra/benchmarks). + +

+ + +

+ +## ⚙️ Встановлення + +Переконайтеся, що Go встановлено ([завантажити](https://go.dev/dl/)). Потрібна версія `1.17` або вища. + +Ініціалізуйте проект, створивши папку, а потім запустивши `go mod init github.com/your/repo` +([детальніше](https://go.dev/blog/using-go-modules)) всередині цієї папки. Далі встановіть Fiber за допомогою +команди [`go get`](https://pkg.go.dev/cmd/go/#hdr-Add_dependencies_to_current_module_and_install_them): + +```bash +go get -u github.com/gofiber/fiber/v2 +``` + +## 🎯 Особливості + +- Надійна [маршрутизація](https://docs.gofiber.io/routing) +- Доступ до [статичних файлів](https://docs.gofiber.io/api/app#static) +- Екстремальна [продуктивність](https://docs.gofiber.io/extra/benchmarks) +- [Низький обсяг споживання пам'яті](https://docs.gofiber.io/extra/benchmarks) +- [Кінцеві точки API](https://docs.gofiber.io/api/ctx) +- [Middleware](https://docs.gofiber.io/middleware) та підтримка [Next](https://docs.gofiber.io/api/ctx#next) +- [Швидке](https://dev.to/koddr/welcome-to-fiber-an-express-js-styled-fastest-web-framework-written-with-on-golang-497) програмування на стороні сервера +- [Двигуни шаблонів](https://github.com/gofiber/template) +- [Підтримка WebSocket](https://github.com/gofiber/websocket) +- [Server-Sent Events](https://github.com/gofiber/recipes/tree/master/sse) +- [Обмежувач швидкості](https://docs.gofiber.io/api/middleware/limiter) +- Документація доступна [19 мовами](https://docs.gofiber.io/) +- І багато іншого, [відвідайте наш Wiki](https://docs.gofiber.io/) + +## 💡 Філософія + +Нові програмісти, які переходять із [Node.js](https://nodejs.org/en/about/) на [Go](https://go.dev/doc/), мають справу зі звивистою кривою навчання, перш ніж можуть розпочати створення своїх веб-додатків або мікросервісів. Fiber, як **веб-фреймворк**, було створено з ідеєю **мінімалізму** та слідує **шляху UNIX**, щоб нові програмісти могли швидко увійти у світ Go з теплим та надійним прийомом. + +Fiber **натхненний** Express, найпопулярнішим веб-фреймворком в Інтернеті. Ми поєднали **легкість** Express і **чисту продуктивність** Go. Якщо ви коли-небудь реалізовували веб-додаток у Node.js (_з використанням Express або подібного_), то багато методів і принципів здадуться вам **дуже звичайними**. + +Ми **прислухаємося** до наших користувачів у [issues](https://github.com/gofiber/fiber/issues), Discord [сервері](https://gofiber.io/discord) та в інших місцях Інтернета, щоб створити **швидкий**, **гнучкий** та **доброзичливий** веб фреймворк на Go для **будь-яких** завдань, **дедлайнів** та **рівнів** розробників! Як це робить Express у світі JavaScript. + +## ⚠️ Обмеження + +- Через те, що Fiber використовує unsafe, бібліотека не завжди може бути сумісною з останньою версією Go. Fiber 2.40.0 було протестовано з Go версій 1.17 до 1.21. +- Fiber не сумісний з інтерфейсами net/http. Це означає, що ви не зможете використовувати такі проекти, як gqlgen, go-swagger або будь-які інші, які є частиною екосистеми net/http. + +## 👀 Приклади + +Нижче наведено деякі типові приклади. Якщо ви хочете переглянути більше прикладів коду, відвідайте наше [репозиторій рецептів](https://github.com/gofiber/recipes) або відвідайте нашу розміщену [документацію API](https://docs.gofiber.io). + +#### 📖 [**Основна маршрутизація**](https://docs.gofiber.io/#basic-routing) + +```go +func main() { + app := fiber.New() + + // GET /api/register + app.Get("/api/*", func(c *fiber.Ctx) error { + msg := fmt.Sprintf("✋ %s", c.Params("*")) + return c.SendString(msg) // => ✋ register + }) + + // GET /flights/LAX-SFO + app.Get("/flights/:from-:to", func(c *fiber.Ctx) error { + msg := fmt.Sprintf("💸 From: %s, To: %s", c.Params("from"), c.Params("to")) + return c.SendString(msg) // => 💸 From: LAX, To: SFO + }) + + // GET /dictionary.txt + app.Get("/:file.:ext", func(c *fiber.Ctx) error { + msg := fmt.Sprintf("📃 %s.%s", c.Params("file"), c.Params("ext")) + return c.SendString(msg) // => 📃 dictionary.txt + }) + + // GET /john/75 + app.Get("/:name/:age/:gender?", func(c *fiber.Ctx) error { + msg := fmt.Sprintf("👴 %s is %s years old", c.Params("name"), c.Params("age")) + return c.SendString(msg) // => 👴 john is 75 years old + }) + + // GET /john + app.Get("/:name", func(c *fiber.Ctx) error { + msg := fmt.Sprintf("Hello, %s 👋!", c.Params("name")) + return c.SendString(msg) // => Hello john 👋! + }) + + log.Fatal(app.Listen(":3000")) +} +``` + +#### 📖 [**Назви маршруту**](https://docs.gofiber.io/api/app#name) + +```go +func main() { + app := fiber.New() + + // GET /api/register + app.Get("/api/*", func(c *fiber.Ctx) error { + msg := fmt.Sprintf("✋ %s", c.Params("*")) + return c.SendString(msg) // => ✋ register + }).Name("api") + + data, _ := json.MarshalIndent(app.GetRoute("api"), "", " ") + fmt.Print(string(data)) + // Prints: + // { + // "method": "GET", + // "name": "api", + // "path": "/api/*", + // "params": [ + // "*1" + // ] + // } + + + log.Fatal(app.Listen(":3000")) +} +``` + +#### 📖 [**Обслуговування статичних файлів**](https://docs.gofiber.io/api/app#static) + +```go +func main() { + app := fiber.New() + + app.Static("/", "./public") + // => http://localhost:3000/js/script.js + // => http://localhost:3000/css/style.css + + app.Static("/prefix", "./public") + // => http://localhost:3000/prefix/js/script.js + // => http://localhost:3000/prefix/css/style.css + + app.Static("*", "./public/index.html") + // => http://localhost:3000/any/path/shows/index/html + + log.Fatal(app.Listen(":3000")) +} +``` + +#### 📖 [**Middleware & Next**](https://docs.gofiber.io/api/ctx#next) + +```go +func main() { + app := fiber.New() + + // Match any route + app.Use(func(c *fiber.Ctx) error { + fmt.Println("🥇 First handler") + return c.Next() + }) + + // Match all routes starting with /api + app.Use("/api", func(c *fiber.Ctx) error { + fmt.Println("🥈 Second handler") + return c.Next() + }) + + // GET /api/list + app.Get("/api/list", func(c *fiber.Ctx) error { + fmt.Println("🥉 Last handler") + return c.SendString("Hello, World 👋!") + }) + + log.Fatal(app.Listen(":3000")) +} +``` + +
+ 📚 Показати більше прикладів коду + +### Двигуни перегляду + +📖 [Конфігурація](https://docs.gofiber.io/api/fiber#config) +📖 [Двигуни](https://github.com/gofiber/template) +📖 [Рендер](https://docs.gofiber.io/api/ctx#render) + +Fiber за умовчанням використовує [html/template](https://pkg.go.dev/html/template/), якщо жодного двигуна не було вказано. + +Якщо ви хочете виконати частково або використовувати інший двигун, наприклад [amber](https://github.com/eknkc/amber), [handlebars](https://github.com/aymerick/raymond), [mustache](https://github.com/cbroglie/mustache) або [jade](https://github.com/Joker/jade), тощо. + +Перегляньте наш пакет [Шаблон](https://github.com/gofiber/template), який підтримує кілька двигунів перегляду. + +```go +package main + +import ( + "github.com/gofiber/fiber/v2" + "github.com/gofiber/template/pug" +) + +func main() { + // You can setup Views engine before initiation app: + app := fiber.New(fiber.Config{ + Views: pug.New("./views", ".pug"), + }) + + // And now, you can call template `./views/home.pug` like this: + app.Get("/", func(c *fiber.Ctx) error { + return c.Render("home", fiber.Map{ + "title": "Homepage", + "year": 1999, + }) + }) + + log.Fatal(app.Listen(":3000")) +} +``` + +### Групування маршрутів у ланцюги + +📖 [Група](https://docs.gofiber.io/api/app#group) + +```go +func middleware(c *fiber.Ctx) error { + fmt.Println("Don't mind me!") + return c.Next() +} + +func handler(c *fiber.Ctx) error { + return c.SendString(c.Path()) +} + +func main() { + app := fiber.New() + + // Root API route + api := app.Group("/api", middleware) // /api + + // API v1 routes + v1 := api.Group("/v1", middleware) // /api/v1 + v1.Get("/list", handler) // /api/v1/list + v1.Get("/user", handler) // /api/v1/user + + // API v2 routes + v2 := api.Group("/v2", middleware) // /api/v2 + v2.Get("/list", handler) // /api/v2/list + v2.Get("/user", handler) // /api/v2/user + + // ... +} +``` + +### Middleware логування + +📖 [Логування](https://docs.gofiber.io/api/middleware/logger) + +```go +package main + +import ( + "log" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/logger" +) + +func main() { + app := fiber.New() + + app.Use(logger.New()) + + // ... + + log.Fatal(app.Listen(":3000")) +} +``` + +### Спільне використання ресурсів між джерелами (CORS) + +📖 [CORS](https://docs.gofiber.io/api/middleware/cors) + +```go +import ( + "log" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/cors" +) + +func main() { + app := fiber.New() + + app.Use(cors.New()) + + // ... + + log.Fatal(app.Listen(":3000")) +} +``` + +Перевірте CORS, передавши будь-який домен у заголовку `Origin`: + +```bash +curl -H "Origin: http://example.com" --verbose http://localhost:3000 +``` + +### Власна відповідь 404 + +📖 [HTTP Методи](https://docs.gofiber.io/api/ctx#status) + +```go +func main() { + app := fiber.New() + + app.Static("/", "./public") + + app.Get("/demo", func(c *fiber.Ctx) error { + return c.SendString("This is a demo!") + }) + + app.Post("/register", func(c *fiber.Ctx) error { + return c.SendString("Welcome!") + }) + + // Last middleware to match anything + app.Use(func(c *fiber.Ctx) error { + return c.SendStatus(404) + // => 404 "Not Found" + }) + + log.Fatal(app.Listen(":3000")) +} +``` + +### JSON Відповідь + +📖 [JSON](https://docs.gofiber.io/ctx#json) + +```go +type User struct { + Name string `json:"name"` + Age int `json:"age"` +} + +func main() { + app := fiber.New() + + app.Get("/user", func(c *fiber.Ctx) error { + return c.JSON(&User{"John", 20}) + // => {"name":"John", "age":20} + }) + + app.Get("/json", func(c *fiber.Ctx) error { + return c.JSON(fiber.Map{ + "success": true, + "message": "Hi John!", + }) + // => {"success":true, "message":"Hi John!"} + }) + + log.Fatal(app.Listen(":3000")) +} +``` + +### WebSocket Upgrade + +📖 [Websocket](https://github.com/gofiber/websocket) + +```go +import ( + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/websocket" +) + +func main() { + app := fiber.New() + + app.Get("/ws", websocket.New(func(c *websocket.Conn) { + for { + mt, msg, err := c.ReadMessage() + if err != nil { + log.Println("read:", err) + break + } + log.Printf("recv: %s", msg) + err = c.WriteMessage(mt, msg) + if err != nil { + log.Println("write:", err) + break + } + } + })) + + log.Fatal(app.Listen(":3000")) + // ws://localhost:3000/ws +} +``` + +### Server-Sent Events + +📖 [Більше інформації](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events) + +```go +import ( + "github.com/gofiber/fiber/v2" + "github.com/valyala/fasthttp" +) + +func main() { + app := fiber.New() + + app.Get("/sse", func(c *fiber.Ctx) error { + c.Set("Content-Type", "text/event-stream") + c.Set("Cache-Control", "no-cache") + c.Set("Connection", "keep-alive") + c.Set("Transfer-Encoding", "chunked") + + c.Context().SetBodyStreamWriter(fasthttp.StreamWriter(func(w *bufio.Writer) { + fmt.Println("WRITER") + var i int + + for { + i++ + msg := fmt.Sprintf("%d - the time is %v", i, time.Now()) + fmt.Fprintf(w, "data: Message: %s\n\n", msg) + fmt.Println(msg) + + w.Flush() + time.Sleep(5 * time.Second) + } + })) + + return nil + }) + + log.Fatal(app.Listen(":3000")) +} +``` + +### Recover middleware + +📖 [Recover](https://docs.gofiber.io/api/middleware/recover) + +```go +import ( + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/recover" +) + +func main() { + app := fiber.New() + + app.Use(recover.New()) + + app.Get("/", func(c *fiber.Ctx) error { + panic("normally this would crash your app") + }) + + log.Fatal(app.Listen(":3000")) +} +``` + +
+ +### Використання довіреного проксі + +📖 [Конфігурація](https://docs.gofiber.io/api/fiber#config) + +```go +import ( + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/recover" +) + +func main() { + app := fiber.New(fiber.Config{ + EnableTrustedProxyCheck: true, + TrustedProxies: []string{"0.0.0.0", "1.1.1.1/30"}, // IP address or IP address range + ProxyHeader: fiber.HeaderXForwardedFor, + }) + + // ... + + log.Fatal(app.Listen(":3000")) +} +``` + + + +## 🧬 Внутрішні Middleware + +Ось список middleware, яке входить до складу Fiber фреймворку. + +| Middleware | Опис | +| :------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [basicauth](https://github.com/gofiber/fiber/tree/master/middleware/basicauth) | Middleware який забезпечує базову автентифікацію по HTTP. | +| [cache](https://github.com/gofiber/fiber/tree/master/middleware/cache) | Middleware який перехоплює та кешує відповіді | +| [compress](https://github.com/gofiber/fiber/tree/master/middleware/compress) | стиснення для Fiber, воно за замовчуванням підтримує `deflate`, `gzip` і `brotli`. | +| [cors](https://github.com/gofiber/fiber/tree/master/middleware/cors) | Middleware який вмикає перехресне використання ресурсів \(CORS\) із різними параметрами. | +| [csrf](https://github.com/gofiber/fiber/tree/master/middleware/csrf) | Захист від експлойтів CSRF. | +| [encryptcookie](https://github.com/gofiber/fiber/tree/master/middleware/encryptcookie) | Шифрування значень файлів cookie. | +| [envvar](https://github.com/gofiber/fiber/tree/master/middleware/envvar) | Middleware для відкриття змінних середевищ. | +| [etag](https://github.com/gofiber/fiber/tree/master/middleware/etag) | Middleware яке робить кеш-пам’ять більш ефективним і заощаджує пропускну здатність, оскільки веб-серверу не потрібно повторно надсилати повну відповідь, якщо вміст не змінився. | +| [expvar](https://github.com/gofiber/fiber/tree/master/middleware/expvar) | Middleware який обслуговує доступні варіанти середовища виконання HTTP у форматі JSON. | +| [favicon](https://github.com/gofiber/fiber/tree/master/middleware/favicon) | Ігнорування значка із журналів або обслуговувати з пам’яті, якщо вказано шлях до файлу. | +| [filesystem](https://github.com/gofiber/fiber/tree/master/middleware/filesystem) | Middleware файлової системи, особлива подяка та кредити Alireza Salary. | +| [limiter](https://github.com/gofiber/fiber/tree/master/middleware/limiter) | Ообмеження швидкості для Fiber. Використовуйте для обмеження повторних запитів до загальнодоступних API та/або кінцевих точок, таких як скидання пароля. | +| [logger](https://github.com/gofiber/fiber/tree/master/middleware/logger) | Реєстратор запитів/відповідей HTTP. | +| [monitor](https://github.com/gofiber/fiber/tree/master/middleware/monitor) | Middleware який повідомляє показники сервера. | +| [pprof](https://github.com/gofiber/fiber/tree/master/middleware/pprof) | Особлива подяка Метью Лі \(@mthli\) . | +| [proxy](https://github.com/gofiber/fiber/tree/master/middleware/proxy) | Дозволяє надсилати проксі-запити до кількох серверів. | +| [recover](https://github.com/gofiber/fiber/tree/master/middleware/recover) | Middleware який відновлює паніки будь-де в ланцюжку стека та передає керування централізованому [обробнику помилок](https://docs.gofiber.io/guide/error-handling). | +| [requestid](https://github.com/gofiber/fiber/tree/master/middleware/requestid) | До кожного запиту додає ідентифікатор запиту. | +| [session](https://github.com/gofiber/fiber/tree/master/middleware/session) | Middleware для сеансів. ПРИМІТКА: Цей middleware використовує наш пакет зберігання. | +| [skip](https://github.com/gofiber/fiber/tree/master/middleware/skip) | Middleware який пропускає упакований обробник, якщо предикат є істинним. | +| [timeout](https://github.com/gofiber/fiber/tree/master/middleware/timeout) | Додає максимальний час для запиту та пересилає до ErrorHandler, якщо його перевищено. | +| [keyauth](https://github.com/gofiber/keyauth) | Middleware для автентифікації по ключам. | +| [redirect](https://github.com/gofiber/redirect) | Middleware для перенаправлення. | +| [rewrite](https://github.com/gofiber/rewrite) | Middleware для перезапису URL-адреси на основі наданих правил. | +| [adaptor](https://github.com/gofiber/adaptor) | Конвентор для обробників net/http до/з обробників запитів Fiber, особлива подяка @arsmn! | +| [helmet](https://github.com/gofiber/helmet) | Допомагає захистити ваші програми, встановлюючи різні заголовки HTTP. | + +## 🧬 Зовнішні Middleware + +Список зовнішніх middleware модулів, які підтримуються [командою Fiber](https://github.com/orgs/gofiber/people). + +| Middleware | Опис | +| :------------------------------------------------ | :-------------------------------------------------------------------------------------------------------------------- | +| [jwt](https://github.com/gofiber/jwt) | JWT повертає middleware автентифікації JSON Web Token \(JWT\). | +| [storage](https://github.com/gofiber/storage) | Драйвер зберігання який може використовуватися в різних middleware. | +| [template](https://github.com/gofiber/template) | Цей пакет містить 8 модулів шаблонів, які можна використовувати з Fiber `v1.10.x` Потрібно версія Go 1.13 або новішу. | +| [websocket](https://github.com/gofiber/websocket) | На основі Fasthttp WebSocket для Fiber з підтримкою місцевих користувачів! | + +## 🕶️ Чудовий список + +Більше статей, middleware, прикладів або інструментів дивіться у нашому [чудовому списку](https://github.com/gofiber/awesome-fiber). + +## 👍 Внести свій внесок + +Якщо ви хочете сказати **дякую** та/або підтримати активний розвиток `Fiber`: + +1. Додайте [зірку GitHub](https://github.com/gofiber/fiber/stargazers) до проекту. +2. Напишіть про проект [у своєму 𝕏 (Twitter)](https://x.com/intent/tweet?text=Fiber%20is%20an%20Express%20inspired%20%23web%20%23framework%20built%20on%20top%20of%20Fasthttp%2C%20the%20fastest%20HTTP%20engine%20for%20%23Go.%20Designed%20to%20ease%20things%20up%20for%20%23fast%20development%20with%20zero%20memory%20allocation%20and%20%23performance%20in%20mind%20%F0%9F%9A%80%20https%3A%2F%2Fgithub.com%2Fgofiber%2Ffiber). +3. Напишіть огляд або підручник на [Medium](https://medium.com/), [Dev.to](https://dev.to/) або особистому блогу. +4. Підтримайте проект, пожертвувавши [чашку кави](https://buymeacoff.ee/fenny). + +## ☕ Прихильники + +Fiber – це проект із відкритим вихідним кодом, який працює за рахунок пожертвувань для оплати рахунків, наприклад наше доменне ім’я, gitbook, netlify і безсерверний хостинг. Якщо ви хочете підтримати Fiber, ви можете ☕ [**купити каву тут**](https://buymeacoff.ee/fenny). + +| | Користувач | Пожертвування | +| :--------------------------------------------------------- | :----------------------------------------------- | :------------ | +| ![](https://avatars.githubusercontent.com/u/204341?s=25) | [@destari](https://github.com/destari) | ☕ x 10 | +| ![](https://avatars.githubusercontent.com/u/63164982?s=25) | [@dembygenesis](https://github.com/dembygenesis) | ☕ x 5 | +| ![](https://avatars.githubusercontent.com/u/56607882?s=25) | [@thomasvvugt](https://github.com/thomasvvugt) | ☕ x 5 | +| ![](https://avatars.githubusercontent.com/u/27820675?s=25) | [@hendratommy](https://github.com/hendratommy) | ☕ x 5 | +| ![](https://avatars.githubusercontent.com/u/1094221?s=25) | [@ekaputra07](https://github.com/ekaputra07) | ☕ x 5 | +| ![](https://avatars.githubusercontent.com/u/194590?s=25) | [@jorgefuertes](https://github.com/jorgefuertes) | ☕ x 5 | +| ![](https://avatars.githubusercontent.com/u/186637?s=25) | [@candidosales](https://github.com/candidosales) | ☕ x 5 | +| ![](https://avatars.githubusercontent.com/u/29659953?s=25) | [@l0nax](https://github.com/l0nax) | ☕ x 3 | +| ![](https://avatars.githubusercontent.com/u/635852?s=25) | [@bihe](https://github.com/bihe) | ☕ x 3 | +| ![](https://avatars.githubusercontent.com/u/307334?s=25) | [@justdave](https://github.com/justdave) | ☕ x 3 | +| ![](https://avatars.githubusercontent.com/u/11155743?s=25) | [@koddr](https://github.com/koddr) | ☕ x 1 | +| ![](https://avatars.githubusercontent.com/u/29042462?s=25) | [@lapolinar](https://github.com/lapolinar) | ☕ x 1 | +| ![](https://avatars.githubusercontent.com/u/2978730?s=25) | [@diegowifi](https://github.com/diegowifi) | ☕ x 1 | +| ![](https://avatars.githubusercontent.com/u/44171355?s=25) | [@ssimk0](https://github.com/ssimk0) | ☕ x 1 | +| ![](https://avatars.githubusercontent.com/u/5638101?s=25) | [@raymayemir](https://github.com/raymayemir) | ☕ x 1 | +| ![](https://avatars.githubusercontent.com/u/619996?s=25) | [@melkorm](https://github.com/melkorm) | ☕ x 1 | +| ![](https://avatars.githubusercontent.com/u/31022056?s=25) | [@marvinjwendt](https://github.com/marvinjwendt) | ☕ x 1 | +| ![](https://avatars.githubusercontent.com/u/31921460?s=25) | [@toishy](https://github.com/toishy) | ☕ x 1 | + +## ‎‍💻 Автори коду + +Code Contributors + +## ⭐️ Звіздарі + +Stargazers over time + +## ⚠️ Ліцензія + +Авторське право (c) 2019-дотепер [Fenny](https://github.com/fenny) та [Contributors](https://github.com/gofiber/fiber/graphs/contributors). `Fiber` це безкоштовне програмне забезпечення з відкритим вихідним кодом, ліцензоване згідно [MIT License](https://github.com/gofiber/fiber/blob/master/LICENSE). Офіційний логотип створено [Vic Shóstak](https://github.com/koddr) і поширюється під [Creative Commons](https://creativecommons.org/licenses/by-sa/4.0/) ліцензією (CC BY-SA 4.0 International). + +**Ліцензії сторонніх бібліотек** + +- [colorable](https://github.com/mattn/go-colorable/blob/master/LICENSE) +- [isatty](https://github.com/mattn/go-isatty/blob/master/LICENSE) +- [runewidth](https://github.com/mattn/go-runewidth/blob/master/LICENSE) +- [fasthttp](https://github.com/valyala/fasthttp/blob/master/LICENSE) +- [bytebufferpool](https://github.com/valyala/bytebufferpool/blob/master/LICENSE) +- [fwd](https://github.com/philhofer/fwd/blob/master/LICENSE.md) +- [go-ole](https://github.com/go-ole/go-ole/blob/master/LICENSE) +- [gopsutil](https://github.com/shirou/gopsutil/blob/master/LICENSE) +- [msgp](https://github.com/tinylib/msgp/blob/master/LICENSE) +- [schema](https://github.com/gorilla/schema/blob/master/LICENSE) +- [uuid](https://github.com/google/uuid/blob/master/LICENSE) +- [wmi](https://github.com/StackExchange/wmi/blob/master/LICENSE) diff --git a/.github/README_zh-CN.md b/.github/README_zh-CN.md new file mode 100644 index 0000000..4314e05 --- /dev/null +++ b/.github/README_zh-CN.md @@ -0,0 +1,695 @@ +

+ + + + + Fiber + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + +

+

+ Fiber是一个受到 Express 启发的Web框架,基于使用 + Go 语言编写的最快的 HTTP 引擎 + Fasthttp 构建。旨在通过零内存分配高性能服务,使快速开发更加简便。 +

+ +## ⚡️ 快速入门 + +```go +package main + +import "github.com/gofiber/fiber/v2" + +func main() { + app := fiber.New() + + app.Get("/", func(c *fiber.Ctx) error { + return c.SendString("Hello, World 👋!") + }) + + app.Listen(":3000") +} +``` + +## 🤖 基准测试 + +这些测试由 [TechEmpower](https://www.techempower.com/benchmarks/#section=data-r19&hw=ph&test=plaintext) 和 [Go Web](https://github.com/smallnest/go-web-framework-benchmark) 完成。如果您想查看所有结果,请访问我们的 [Wiki](https://docs.gofiber.io/extra/benchmarks) 。 + +

+ + +

+ +## ⚙️ 安装 + +确保已安装 `1.17` 或更高版本的 Go ([下载](https://go.dev/dl/))。 + +通过创建文件夹并在文件夹内运行 `go mod init github.com/your/repo` ([了解更多](https://go.dev/blog/using-go-modules)) 来初始化项目,然后使用 [`go get`](https://pkg.go.dev/cmd/go/#hdr-Add_dependencies_to_current_module_and_install_them) 命令安装 Fiber: + +```bash +go get -u github.com/gofiber/fiber/v2 +``` + +## 🎯 特点 + +- 强大的[路由](https://docs.gofiber.io/guide/routing) +- [静态文件](https://docs.gofiber.io/api/app#static)服务 +- 极致[性能](https://docs.gofiber.io/extra/benchmarks) +- [低内存占用](https://docs.gofiber.io/extra/benchmarks) +- [API 接口](https://docs.gofiber.io/api/ctx) +- 支持[中间件](https://docs.gofiber.io/category/-middleware)和 [Next](https://docs.gofiber.io/api/ctx#next) +- [快速上手](https://dev.to/koddr/welcome-to-fiber-an-express-js-styled-fastest-web-framework-written-with-on-golang-497) +- [模版引擎](https://github.com/gofiber/template) +- [支持 WebSocket](https://github.com/gofiber/websocket) +- [Server-Sent events](https://github.com/gofiber/recipes/tree/master/sse) +- [频率限制](https://docs.gofiber.io/api/middleware/limiter) +- [被翻译成 19 种语言](https://docs.gofiber.io/) +- 更多请[探索文档](https://docs.gofiber.io/) + +## 💡 哲学 + +从 [Node.js](https://nodejs.org/en/about/) 切换到 [Go](https://go.dev/doc/) 的新 `gopher` 在开始构建 `Web` +应用程序或微服务之前需要经历一段艰难的学习过程。 而 `Fiber`,一个基于**极简主义**并且遵循 **UNIX 方式**创建的 **Web 框架**, +使新的 `gopher` 可以在热烈和可信赖的欢迎中迅速进入 `Go` 的世界。 + +`Fiber`受到了互联网上最流行的`Web`框架`Express`的**启发** 。我们结合了`Express`的**易用性**和`Go`的**原始性能** 。如果您曾经使用`Node.js`构建`Web`应用程序(_使用 Express 或类似框架_),那么许多方法和原理对您来说应该**非常易懂**。 + +我们会**倾听**用户在 [issues](https://github.com/gofiber/fiber/issues),Discord [channel](https://gofiber.io/discord) +以及在互联网上的所有诉求,为了创建一个能让有着任何技术栈的开发者都能在 deadline 前完成任务的**迅速**,**灵活**以及**友好**的 `Go web` 框架,就像 `Express` 在 `JavaScript` 世界中一样。 + +## ⚠️ 限制 +* 由于 Fiber 使用了 unsafe 特性,导致其可能与最新的 Go 版本不兼容。Fiber 2.40.0 已经在 Go 1.17 到 1.21 上测试过。 +* Fiber 与 net/http 接口不兼容。也就是说你无法直接使用例如 gqlen,go-swagger 或者任何其他属于 net/http 生态的项目。 + +## 👀 示例 + +下面列出了一些常见示例。如果您想查看更多代码示例,请访问我们的 [Recipes](https://github.com/gofiber/recipes) 代码库或 [API 文档](https://docs.gofiber.io) 。 + +#### 📖 [**基础路由**](https://docs.gofiber.io/#basic-routing) + +```go +func main() { + app := fiber.New() + + // GET /api/register + app.Get("/api/*", func(c *fiber.Ctx) error { + msg := fmt.Sprintf("✋ %s", c.Params("*")) + return c.SendString(msg) // => ✋ register + }) + + // GET /flights/LAX-SFO + app.Get("/flights/:from-:to", func(c *fiber.Ctx) error { + msg := fmt.Sprintf("💸 From: %s, To: %s", c.Params("from"), c.Params("to")) + return c.SendString(msg) // => 💸 From: LAX, To: SFO + }) + + // GET /dictionary.txt + app.Get("/:file.:ext", func(c *fiber.Ctx) error { + msg := fmt.Sprintf("📃 %s.%s", c.Params("file"), c.Params("ext")) + return c.SendString(msg) // => 📃 dictionary.txt + }) + + // GET /john/75 + app.Get("/:name/:age/:gender?", func(c *fiber.Ctx) error { + msg := fmt.Sprintf("👴 %s is %s years old", c.Params("name"), c.Params("age")) + return c.SendString(msg) // => 👴 john is 75 years old + }) + + // GET /john + app.Get("/:name", func(c *fiber.Ctx) error { + msg := fmt.Sprintf("Hello, %s 👋!", c.Params("name")) + return c.SendString(msg) // => Hello john 👋! + }) + + log.Fatal(app.Listen(":3000")) +} + +``` + +#### 📖 [**路由命名**](https://docs.gofiber.io/api/app#name) + +```go +func main() { + app := fiber.New() + + // GET /api/register + app.Get("/api/*", func(c *fiber.Ctx) error { + msg := fmt.Sprintf("✋ %s", c.Params("*")) + return c.SendString(msg) // => ✋ register + }).Name("api") + + data, _ := json.MarshalIndent(app.GetRoute("api"), "", " ") + fmt.Print(string(data)) + // Prints: + // { + // "method": "GET", + // "name": "api", + // "path": "/api/*", + // "params": [ + // "*1" + // ] + // } + + + log.Fatal(app.Listen(":3000")) +} + +``` + +#### 📖 [**静态文件**](https://docs.gofiber.io/api/app#static)服务 + +```go +func main() { + app := fiber.New() + + app.Static("/", "./public") + // => http://localhost:3000/js/script.js + // => http://localhost:3000/css/style.css + + app.Static("/prefix", "./public") + // => http://localhost:3000/prefix/js/script.js + // => http://localhost:3000/prefix/css/style.css + + app.Static("*", "./public/index.html") + // => http://localhost:3000/any/path/shows/index/html + + log.Fatal(app.Listen(":3000")) +} + +``` + +#### 📖 [**中间件**](https://docs.gofiber.io/category/-middleware)和 [**Next**](https://docs.gofiber.io/api/ctx#next) + +```go +func main() { + app := fiber.New() + + // Match any route + app.Use(func(c *fiber.Ctx) error { + fmt.Println("🥇 First handler") + return c.Next() + }) + + // Match all routes starting with /api + app.Use("/api", func(c *fiber.Ctx) error { + fmt.Println("🥈 Second handler") + return c.Next() + }) + + // GET /api/register + app.Get("/api/list", func(c *fiber.Ctx) error { + fmt.Println("🥉 Last handler") + return c.SendString("Hello, World 👋!") + }) + + log.Fatal(app.Listen(":3000")) +} + +``` + +
+ 📚 展示更多代码示例 + +### 模版引擎 + +📖 [配置](https://docs.gofiber.io/api/fiber#config) +📖 [模版引擎](https://github.com/gofiber/template) +📖 [渲染](https://docs.gofiber.io/api/ctx#render) + +如果未设置模版引擎,则`Fiber`默认使用 [html/template](https://pkg.go.dev/html/template/)。 + +如果您要执行部分模版或使用其他引擎,例如[amber](https://github.com/eknkc/amber),[handlebars](https://github.com/aymerick/raymond), +[mustache](https://github.com/cbroglie/mustache) 或者 [pug](https://github.com/Joker/jade)等 + +请查看我们的 [Template](https://github.com/gofiber/template) 包,该包支持多个模版引擎。 + +```go +package main + +import ( + "github.com/gofiber/fiber/v2" + "github.com/gofiber/template/pug" +) + +func main() { + // You can setup Views engine before initiation app: + app := fiber.New(fiber.Config{ + Views: pug.New("./views", ".pug"), + }) + + // And now, you can call template `./views/home.pug` like this: + app.Get("/", func(c *fiber.Ctx) error { + return c.Render("home", fiber.Map{ + "title": "Homepage", + "year": 1999, + }) + }) + + log.Fatal(app.Listen(":3000")) +} +``` + +### 组合路由链 + +📖 [路由分组](https://docs.gofiber.io/api/app#group) + +```go +func middleware(c *fiber.Ctx) error { + fmt.Println("Don't mind me!") + return c.Next() +} + +func handler(c *fiber.Ctx) error { + return c.SendString(c.Path()) +} + +func main() { + app := fiber.New() + + // Root API route + api := app.Group("/api", middleware) // /api + + // API v1 routes + v1 := api.Group("/v1", middleware) // /api/v1 + v1.Get("/list", handler) // /api/v1/list + v1.Get("/user", handler) // /api/v1/user + + // API v2 routes + v2 := api.Group("/v2", middleware) // /api/v2 + v2.Get("/list", handler) // /api/v2/list + v2.Get("/user", handler) // /api/v2/user + + // ... +} + +``` + +### 日志中间件 + +📖 [Logger](https://docs.gofiber.io/api/middleware/logger) + +```go +package main + +import ( + "log" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/logger" +) + +func main() { + app := fiber.New() + + app.Use(logger.New()) + + // ... + + log.Fatal(app.Listen(":3000")) +} +``` + +### 跨域资源共享(CORS)中间件 + +📖 [CORS](https://docs.gofiber.io/api/middleware/cors) + +```go +import ( + "log" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/cors" +) + +func main() { + app := fiber.New() + + app.Use(cors.New()) + + // ... + + log.Fatal(app.Listen(":3000")) +} +``` + +通过在请求头中设置 `Origin` 传递任何域来检查 CORS : + +```bash +curl -H "Origin: http://example.com" --verbose http://localhost:3000 +``` + +### 自定义 404 响应 + +📖 [HTTP Methods](https://docs.gofiber.io/api/ctx#status) + +```go +func main() { + app := fiber.New() + + app.Static("/", "./public") + + app.Get("/demo", func(c *fiber.Ctx) error { + return c.SendString("This is a demo!") + }) + + app.Post("/register", func(c *fiber.Ctx) error { + return c.SendString("Welcome!") + }) + + // Last middleware to match anything + app.Use(func(c *fiber.Ctx) error { + return c.SendStatus(404) + // => 404 "Not Found" + }) + + log.Fatal(app.Listen(":3000")) +} +``` + +### JSON 响应 + +📖 [JSON](https://docs.gofiber.io/api/ctx#json) + +```go +type User struct { + Name string `json:"name"` + Age int `json:"age"` +} + +func main() { + app := fiber.New() + + app.Get("/user", func(c *fiber.Ctx) error { + return c.JSON(&User{"John", 20}) + // => {"name":"John", "age":20} + }) + + app.Get("/json", func(c *fiber.Ctx) error { + return c.JSON(fiber.Map{ + "success": true, + "message": "Hi John!", + }) + // => {"success":true, "message":"Hi John!"} + }) + + log.Fatal(app.Listen(":3000")) +} +``` + +### 使用 WebSocket 中间件 + +📖 [Websocket](https://github.com/gofiber/websocket) + +```go +import ( + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/websocket" +) + +func main() { + app := fiber.New() + + app.Get("/ws", websocket.New(func(c *websocket.Conn) { + for { + mt, msg, err := c.ReadMessage() + if err != nil { + log.Println("read:", err) + break + } + log.Printf("recv: %s", msg) + err = c.WriteMessage(mt, msg) + if err != nil { + log.Println("write:", err) + break + } + } + })) + + log.Fatal(app.Listen(":3000")) + // ws://localhost:3000/ws +} +``` + +### Server-Sent Events + +📖 [More Info](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events) + +```go +import ( + "github.com/gofiber/fiber/v2" + "github.com/valyala/fasthttp" +) + +func main() { + app := fiber.New() + + app.Get("/sse", func(c *fiber.Ctx) error { + c.Set("Content-Type", "text/event-stream") + c.Set("Cache-Control", "no-cache") + c.Set("Connection", "keep-alive") + c.Set("Transfer-Encoding", "chunked") + + c.Context().SetBodyStreamWriter(fasthttp.StreamWriter(func(w *bufio.Writer) { + fmt.Println("WRITER") + var i int + + for { + i++ + msg := fmt.Sprintf("%d - the time is %v", i, time.Now()) + fmt.Fprintf(w, "data: Message: %s\n\n", msg) + fmt.Println(msg) + + w.Flush() + time.Sleep(5 * time.Second) + } + })) + + return nil + }) + + log.Fatal(app.Listen(":3000")) +} +``` + +### 恢复(panic)中间件 + +📖 [Recover](https://docs.gofiber.io/api/middleware/recover) + +```go +import ( + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/recover" +) + +func main() { + app := fiber.New() + + app.Use(recover.New()) + + app.Get("/", func(c *fiber.Ctx) error { + panic("normally this would crash your app") + }) + + log.Fatal(app.Listen(":3000")) +} +``` + +
+ +## 🧬 内部中间件 + +以下为包含在Fiber框架中的中间件列表. + +| 中间件 | 描述 | +|:---------------------------------------------------------------------------------------|:-------------------------------------------------------------------------------------------------------| +| [basicauth](https://github.com/gofiber/fiber/tree/master/middleware/basicauth) | 基本身份验证中间件提供 HTTP 基本身份验证。 它为有效凭证调用下一个处理程序,为丢失或无效凭证调用 401 Unauthorized | +| [cache](https://github.com/gofiber/fiber/tree/master/middleware/cache) | 用于拦截和缓存响应 | +| [compress](https://github.com/gofiber/fiber/tree/master/middleware/compress) | Fiber 的压缩中间件,默认支持 `deflate`,`gzip` 和 `brotli` | +| [cors](https://github.com/gofiber/fiber/tree/master/middleware/cors) | 使用各种选项启用跨源资源共享\(CORS\) | +| [csrf](https://github.com/gofiber/fiber/tree/master/middleware/csrf) | 保护来自 CSRF 的漏洞 | +| [encryptcookie](https://github.com/gofiber/fiber/tree/master/middleware/encryptcookie) | 加密 cookie 值的加密中间件 | +| [envvar](https://github.com/gofiber/fiber/tree/master/middleware/envvar) | 通过提供可选配置来公开环境变量 | +| [etag](https://github.com/gofiber/fiber/tree/master/middleware/etag) | 让缓存更加高效并且节省带宽, 让 web 服务在响应内容未变更的情况下不再需要重发送整个响应体 | +| [expvar](https://github.com/gofiber/fiber/tree/master/middleware/expvar) | 通过其 HTTP 服务器运行时间提供 JSON 格式的暴露变体 | +| [favicon](https://github.com/gofiber/fiber/tree/master/middleware/favicon) | 如果提供了文件路径,则忽略日志中的图标或从内存中服务 | +| [filesystem](https://github.com/gofiber/fiber/tree/master/middleware/filesystem) | Fiber 文件系统中间件,特别感谢 Alireza Salary | +| [limiter](https://github.com/gofiber/fiber/tree/master/middleware/limiter) | 用于 Fiber 的限速中间件。 用于限制对公共 api 或对端点的重复请求,如密码重置 | +| [logger](https://github.com/gofiber/fiber/tree/master/middleware/logger) | HTTP 请求/响应日志 | +| [monitor](https://github.com/gofiber/fiber/tree/master/middleware/monitor) | 用于报告服务器指标,受 Express-status-monitor 启发 | +| [pprof](https://github.com/gofiber/fiber/tree/master/middleware/pprof) | 特别感谢 Matthew Lee \(@mthli\) | +| [proxy](https://github.com/gofiber/fiber/tree/master/middleware/proxy) | 允许您将请求proxy到多个服务器 | +| [recover](https://github.com/gofiber/fiber/tree/master/middleware/recover) | Recover 中间件将可以堆栈链中的任何位置将 panic 恢复,并将处理集中到 [ErrorHandler](https://docs.gofiber.io/guide/error-handling) | +| [requestid](https://github.com/gofiber/fiber/tree/master/middleware/requestid) | 为每个请求添加一个 requestid. | +| [session](https://github.com/gofiber/fiber/tree/master/middleware/session) | Session 中间件. 注意: 此中间件使用了我们的存储包. | +| [skip](https://github.com/gofiber/fiber/tree/master/middleware/skip) | Skip 中间件会在判断条为 true 时忽略此次请求 | +| [timeout](https://github.com/gofiber/fiber/tree/master/middleware/timeout) | 添加请求的最大时间,如果超时则发送给ErrorHandler 进行处理. | +| [adaptor](https://github.com/gofiber/adaptor) | net/http 处理程序与 Fiber 请求处理程序之间的转换器,特别感谢 @arsmn! | +| [helmet](https://github.com/gofiber/helmet) | 通过设置各种 HTTP 头帮助保护您的应用程序 | +| [keyauth](https://github.com/gofiber/keyauth) | Key auth 中间件提供基于密钥的身份验证 | +| [redirect](https://github.com/gofiber/redirect) | 用于重定向请求的中间件 | +| [rewrite](https://github.com/gofiber/rewrite) | Rewrite 中间件根据提供的规则重写URL路径。它有助于向后兼容或者创建更清晰、更具描述性的链接 | + +## 🧬 外部中间件 + +以下为外部托管的中间件列表,由 [Fiber团队](https://github.com/orgs/gofiber/people) 维护。 + +| 中间件 | 描述 | +|:--------------------------------------------------|:-------------------------------------------------------------------------------------------| +| [jwt](https://github.com/gofiber/jwt) | JWT 返回一个 JSON Web Token\(JWT\) 身份验证中间件 | +| [storage](https://github.com/gofiber/storage) | 包含实现 Storage 接口的数据库驱动,它的设计旨在配合 fiber 的其他中间件来进行使用 | +| [template](https://github.com/gofiber/template) | 该中间件包含 8 个模板引擎,可与 Fiber `v1.10.x` Go 1.13或更高版本一起使用 | +| [websocket](https://github.com/gofiber/websocket) | 基于 Fasthttp WebSocket for Fiber 实现,支持使用 [Locals](https://docs.gofiber.io/api/ctx#locals) ! | + +## 🕶️ Awesome List + +For more articles, middlewares, examples or tools check our [awesome list](https://github.com/gofiber/awesome-fiber). + +## 👍 贡献 + +如果想**感谢**我们或支持 `Fiber` 的积极发展: + +1. 为 [`Fiber`](https://github.com/gofiber/fiber/stargazers) 点个 ⭐ 星星。 +2. 在 [𝕏 (Twitter)](https://x.com/intent/tweet?text=Fiber%20is%20an%20Express%20inspired%20%23web%20%23framework%20built%20on%20top%20of%20Fasthttp%2C%20the%20fastest%20HTTP%20engine%20for%20%23Go.%20Designed%20to%20ease%20things%20up%20for%20%23fast%20development%20with%20zero%20memory%20allocation%20and%20%23performance%20in%20mind%20%F0%9F%9A%80%20https%3A%2F%2Fgithub.com%2Fgofiber%2Ffiber) 上发布有关项目的[推文](https://twitter.com/intent/tweet?text=Fiber%20is%20an%20Express%20inspired%20%23web%20%23framework%20built%20on%20top%20of%20Fasthttp%2C%20the%20fastest%20HTTP%20engine%20for%20%23Go.%20Designed%20to%20ease%20things%20up%20for%20%23fast%20development%20with%20zero%20memory%20allocation%20and%20%23performance%20in%20mind%20%F0%9F%9A%80%20https%3A%2F%2Fgithub.com%2Fgofiber%2Ffiber)。 +3. 在 [Medium](https://medium.com/),[Dev.to](https://dev.to/) 或个人博客上写评论或教程。 +4. 通过捐赠[一杯咖啡](https://buymeacoff.ee/fenny)来支持本项目。 + +## ☕ 支持者 + +`Fiber`是一个开源项目,依靠捐赠来支付账单,例如我们的域名,`gitbook`,`netlify` 和无服务器托管。如果要支持 `Fiber`, +可以 ☕ [**在这里买一杯咖啡**](https://buymeacoff.ee/fenny) + +| | User | Donation | +| :--------------------------------------------------------- | :----------------------------------------------- | :------- | +| ![](https://avatars.githubusercontent.com/u/204341?s=25) | [@destari](https://github.com/destari) | ☕ x 10 | +| ![](https://avatars.githubusercontent.com/u/63164982?s=25) | [@dembygenesis](https://github.com/dembygenesis) | ☕ x 5 | +| ![](https://avatars.githubusercontent.com/u/56607882?s=25) | [@thomasvvugt](https://github.com/thomasvvugt) | ☕ x 5 | +| ![](https://avatars.githubusercontent.com/u/27820675?s=25) | [@hendratommy](https://github.com/hendratommy) | ☕ x 5 | +| ![](https://avatars.githubusercontent.com/u/1094221?s=25) | [@ekaputra07](https://github.com/ekaputra07) | ☕ x 5 | +| ![](https://avatars.githubusercontent.com/u/194590?s=25) | [@jorgefuertes](https://github.com/jorgefuertes) | ☕ x 5 | +| ![](https://avatars.githubusercontent.com/u/186637?s=25) | [@candidosales](https://github.com/candidosales) | ☕ x 5 | +| ![](https://avatars.githubusercontent.com/u/29659953?s=25) | [@l0nax](https://github.com/l0nax) | ☕ x 3 | +| ![](https://avatars.githubusercontent.com/u/635852?s=25) | [@bihe](https://github.com/bihe) | ☕ x 3 | +| ![](https://avatars.githubusercontent.com/u/307334?s=25) | [@justdave](https://github.com/justdave) | ☕ x 3 | +| ![](https://avatars.githubusercontent.com/u/11155743?s=25) | [@koddr](https://github.com/koddr) | ☕ x 1 | +| ![](https://avatars.githubusercontent.com/u/29042462?s=25) | [@lapolinar](https://github.com/lapolinar) | ☕ x 1 | +| ![](https://avatars.githubusercontent.com/u/2978730?s=25) | [@diegowifi](https://github.com/diegowifi) | ☕ x 1 | +| ![](https://avatars.githubusercontent.com/u/44171355?s=25) | [@ssimk0](https://github.com/ssimk0) | ☕ x 1 | +| ![](https://avatars.githubusercontent.com/u/5638101?s=25) | [@raymayemir](https://github.com/raymayemir) | ☕ x 1 | +| ![](https://avatars.githubusercontent.com/u/619996?s=25) | [@melkorm](https://github.com/melkorm) | ☕ x 1 | +| ![](https://avatars.githubusercontent.com/u/31022056?s=25) | [@marvinjwendt](https://github.com/thomasvvugt) | ☕ x 1 | +| ![](https://avatars.githubusercontent.com/u/31921460?s=25) | [@toishy](https://github.com/toishy) | ☕ x 1 | + +## ‎‍💻 贡献者 + +Code Contributors + +## ⭐️ Star 数增长情况 + +Stargazers over time + +## ⚠️ License + +Copyright (c) 2019-present [Fenny](https://github.com/fenny) and [Contributors](https://github.com/gofiber/fiber/graphs/contributors). `Fiber` is free and open-source software licensed under the [MIT License](https://github.com/gofiber/fiber/blob/master/LICENSE). Official logo was created by [Vic Shóstak](https://github.com/koddr) and distributed under [Creative Commons](https://creativecommons.org/licenses/by-sa/4.0/) license (CC BY-SA 4.0 International). + +**Third-party library licenses** + +- [colorable](https://github.com/mattn/go-colorable/blob/master/LICENSE) +- [isatty](https://github.com/mattn/go-isatty/blob/master/LICENSE) +- [runewidth](https://github.com/mattn/go-runewidth/blob/master/LICENSE) +- [fasthttp](https://github.com/valyala/fasthttp/blob/master/LICENSE) +- [bytebufferpool](https://github.com/valyala/bytebufferpool/blob/master/LICENSE) +- [fwd](https://github.com/philhofer/fwd/blob/master/LICENSE.md) +- [go-ole](https://github.com/go-ole/go-ole/blob/master/LICENSE) +- [gopsutil](https://github.com/shirou/gopsutil/blob/master/LICENSE) +- [msgp](https://github.com/tinylib/msgp/blob/master/LICENSE) +- [schema](https://github.com/gorilla/schema/blob/master/LICENSE) +- [uuid](https://github.com/google/uuid/blob/master/LICENSE) +- [wmi](https://github.com/StackExchange/wmi/blob/master/LICENSE) diff --git a/.github/README_zh-TW.md b/.github/README_zh-TW.md new file mode 100644 index 0000000..7bf431a --- /dev/null +++ b/.github/README_zh-TW.md @@ -0,0 +1,725 @@ +

+ + + + + Fiber + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + +

+

+ Fiber 是款啟發自 ExpressWeb 框架,建基於 Fasthttp——Go最快的 HTTP 引擎。設計旨在 減輕 快速開發的負擔,兼顧 零記憶體分配效能。 +

+ +## ⚡️ 快速入門 + +```go +package main + +import "github.com/gofiber/fiber/v2" + +func main() { + app := fiber.New() + + app.Get("/", func(c *fiber.Ctx) error { + return c.SendString("Hello, World 👋!") + }) + + app.Listen(":3000") +} +``` + +## 🤖 效能評定結果 + +這些測試由 [TechEmpower](https://www.techempower.com/benchmarks/#section=data-r19&hw=ph&test=plaintext) 和 [Go Web 框架效能測試](https://github.com/smallnest/go-web-framework-benchmark) 完成。若需參閱所有結果,請參閱我們的 [Wiki](https://docs.gofiber.io/extra/benchmarks) 資訊。 + +

+ + +

+ +## ⚙️ 安裝 + +先確定您已經安裝 `1.17` 或更新版本的 Go([點此下載](https://go.dev/dl/))。 + +要初始化專案,首先建立檔案夾,然後在檔案夾中執行 `go mod init github.com/名稱/儲存庫`([深入了解](https://go.dev/blog/using-go-modules))。接著,使用 [`go get`](https://pkg.go.dev/cmd/go/#hdr-Add_dependencies_to_current_module_and_install_them) 命令安裝 Fiber: + +```bash +go get -u github.com/gofiber/fiber/v2 +``` + +## 🎯 特色 + +- 強固的[路由系統](https://docs.gofiber.io/guide/routing) +- 可以寄存[靜態檔案](https://docs.gofiber.io/api/app#static) +- 疾速[效能](https://docs.gofiber.io/extra/benchmarks) +- 相當低的[記憶體使用量](https://docs.gofiber.io/extra/benchmarks) +- [API 端點](https://docs.gofiber.io/api/ctx) +- 支援 [中介模組](https://docs.gofiber.io/category/-middleware) 和 [接續函式 (Next)](https://docs.gofiber.io/api/ctx#next) +- [迅速開發](https://dev.to/koddr/welcome-to-fiber-an-express-js-styled-fastest-web-framework-written-with-on-golang-497) 伺服器端服務 +- 強大的[路由](https://docs.gofiber.io/guide/routing) +- [靜態檔案](https://docs.gofiber.io/api/app#static)服務 +- [超快速](https://docs.gofiber.io/extra/benchmarks) +- [佔用很少記憶體](https://docs.gofiber.io/extra/benchmarks) +- 支援 Express 的[API](https://docs.gofiber.io/api/ctx) +- 支援中介器和[下一步](https://docs.gofiber.io/api/ctx#next) +- [立即上手](https://dev.to/koddr/welcome-to-fiber-an-express-js-styled-fastest-web-framework-written-with-on-golang-497) +- [樣板引擎](https://github.com/gofiber/template) +- [支援 WebSocket](https://github.com/gofiber/websocket) +- [Server-Sent Events](https://github.com/gofiber/recipes/tree/master/sse) +- 支援[速率限制](https://docs.gofiber.io/api/middleware/limiter) +- 有 [19 門語言](https://docs.gofiber.io/)的翻譯 +- 還有很多功能,[開始探索 Fiber](https://docs.gofiber.io/) + +## 💡 設計哲學 + +從 [Node.js](https://nodejs.org/en/about/) 轉到 [Go](https://go.dev/doc/) 的新進 Go 開發者,得先面對 Go 的各種知識點,才能開始建構自己的 Web 應用程式或微服務。Fiber 作為一款 **Web 框架**,設計之初便以 **極簡主義** 為理念,並遵循 **UNIX 之道**,讓新進 Go 開發者能夠快速隨著友善且值得信賴的社群,進入 Go 的世界。 + +Fiber **啟發自** Express——網際網路上最知名的 Web 框架,我們將 Express 的 **易用性** 和 Go 的 **原始效能** 結合在一起。如果您曾經在 Node.js(使用 Express 或類似框架)實作過 Web 應用程式,那麼許多方法和開發準則,將讓您感到 **無比熟悉**。 + +我們 **傾聽** 使用者在 [Issues](https://github.com/gofiber/fiber/issues)、Discord [群組](https://gofiber.io/discord) 和 **網路上任何角落** 的意見和建議,製作出 **快速**、**靈活** 且 **易於上手** 的 Go Web 框架,來應對**任何**工作、**繳件期限**以及開發者的**能力區間**——如同 Express 在 JavaScript 世界所扮演的角色一樣! + +## ⚠️ 限制 + +- 由於 Fiber 有用到 Unsafe,本函式庫有時可能無法相容最新版的 Go 語言。Fiber 2.40.0 已在 Go 1.17 至 1.21 的版本測試過。 +- Fiber 不相容 net/http 的介面,意味著您無法使用像是 gqlgen、go-swagger 或其他任何屬於 net/http 生態系統的專案。 + +## 👀 範例 + +下方列出一些常見範例。如果您想查看更多程式碼範例,請參閱我們的 [Recipes 儲存庫](https://github.com/gofiber/recipes),或前往我們提供的 [API 文件](https://docs.gofiber.io)。 + +#### 📖 [**基礎路由**](https://docs.gofiber.io/#basic-routing) + +```go +func main() { + app := fiber.New() + + // GET /api/register + app.Get("/api/*", func(c *fiber.Ctx) error { + msg := fmt.Sprintf("✋ %s", c.Params("*")) + return c.SendString(msg) // => ✋ register + }) + + // GET /flights/LAX-SFO + app.Get("/flights/:from-:to", func(c *fiber.Ctx) error { + msg := fmt.Sprintf("💸 從:%s,到:%s", c.Params("from"), c.Params("to")) + return c.SendString(msg) // => 💸 從:LAX,到:SFO + }) + + // GET /dictionary.txt + app.Get("/:file.:ext", func(c *fiber.Ctx) error { + msg := fmt.Sprintf("📃 %s.%s", c.Params("file"), c.Params("ext")) + return c.SendString(msg) // => 📃 dictionary.txt + }) + + // GET /john/75 + app.Get("/:name/:age/:gender?", func(c *fiber.Ctx) error { + msg := fmt.Sprintf("👴 %s 已經 %s 歲了", c.Params("name"), c.Params("age")) + return c.SendString(msg) // => 👴 john 已經 75 歲了 + }) + + // GET /john + app.Get("/:name", func(c *fiber.Ctx) error { + msg := fmt.Sprintf("哈囉,%s 👋!", c.Params("name")) + return c.SendString(msg) // => 哈囉,john 👋! + }) + + log.Fatal(app.Listen(":3000")) +} + +``` + +#### 📖 [**路由命名**](https://docs.gofiber.io/api/app#name) + +```go +func main() { + app := fiber.New() + + // GET /api/register + app.Get("/api/*", func(c *fiber.Ctx) error { + msg := fmt.Sprintf("✋ %s", c.Params("*")) + return c.SendString(msg) // => ✋ register + }).Name("api") + + data, _ := json.MarshalIndent(app.GetRoute("api"), "", " ") + fmt.Print(string(data)) + // 會輸出: + // { + // "method": "GET", + // "name": "api", + // "path": "/api/*", + // "params": [ + // "*1" + // ] + // } + + + log.Fatal(app.Listen(":3000")) +} + +``` + +#### 📖 [**寄存靜態檔案**](https://docs.gofiber.io/api/app#static) + +```go +func main() { + app := fiber.New() + + app.Static("/", "./public") + // => http://localhost:3000/js/script.js + // => http://localhost:3000/css/style.css + + app.Static("/prefix", "./public") + // => http://localhost:3000/prefix/js/script.js + // => http://localhost:3000/prefix/css/style.css + + app.Static("*", "./public/index.html") + // => http://localhost:3000/any/path/shows/index/html + + log.Fatal(app.Listen(":3000")) +} + +``` + +#### 📖 [**中介模組和接續函式 (Next)**](https://docs.gofiber.io/api/ctx#next) + +```go +func main() { + app := fiber.New() + + // 符合任何路由 + app.Use(func(c *fiber.Ctx) error { + fmt.Println("🥇 第一個處理常式") + return c.Next() + }) + + // 符合所有 /api 開頭的路由 + app.Use("/api", func(c *fiber.Ctx) error { + fmt.Println("🥈 第二個處理常式") + return c.Next() + }) + + // GET /api/list + app.Get("/api/list", func(c *fiber.Ctx) error { + fmt.Println("🥉 最後一個處理常式") + return c.SendString("Hello, World 👋!") + }) + + log.Fatal(app.Listen(":3000")) +} + +``` + +
+ 📚 展示更多程式碼範例 + +### 檢視引擎 + +📖 [組態設定](https://docs.gofiber.io/api/fiber#config) +📖 [引擎](https://github.com/gofiber/template) +📖 [轉譯 (render)](https://docs.gofiber.io/api/ctx#render) + +若未指定檢視引擎,Fiber 預設採用 [html/template](https://pkg.go.dev/html/template/)。 + +如果您想執行部分檢視 (partials),或者是使用如 [amber](https://github.com/eknkc/amber)、[handlebars](https://github.com/aymerick/raymond)、[mustache](https://github.com/cbroglie/mustache) 或 [pug](https://github.com/Joker/jade) 等等不同的引擎…… + +請參考我們的 [Template](https://github.com/gofiber/template) 套件——它支援多種檢視引擎。 + +```go +package main + +import ( + "github.com/gofiber/fiber/v2" + "github.com/gofiber/template/pug" +) + +func main() { + // 您可以在 app 初始化前設定檢視 (Views) 引擎: + app := fiber.New(fiber.Config{ + Views: pug.New("./views", ".pug"), + }) + + // 現在,您可以用下面這種方式呼叫 `./views/home.pug` 樣板: + app.Get("/", func(c *fiber.Ctx) error { + return c.Render("home", fiber.Map{ + "title": "Homepage", + "year": 1999, + }) + }) + + log.Fatal(app.Listen(":3000")) +} +``` + +### 組合路由鏈 + +📖 [分組](https://docs.gofiber.io/api/app#group) + +```go +func middleware(c *fiber.Ctx) error { + fmt.Println("不要理我!") + return c.Next() +} + +func handler(c *fiber.Ctx) error { + return c.SendString(c.Path()) +} + +func main() { + app := fiber.New() + + // 根 API 路由 + api := app.Group("/api", middleware) // /api + + // API v1 路由 + v1 := api.Group("/v1", middleware) // /api/v1 + v1.Get("/list", handler) // /api/v1/list + v1.Get("/user", handler) // /api/v1/user + + // API v2 路由 + v2 := api.Group("/v2", middleware) // /api/v2 + v2.Get("/list", handler) // /api/v2/list + v2.Get("/user", handler) // /api/v2/user + + // ... +} + +``` + +### 中介模組記錄器 + +📖 [記錄器](https://docs.gofiber.io/api/middleware/logger) + +```go +package main + +import ( + "log" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/logger" +) + +func main() { + app := fiber.New() + + app.Use(logger.New()) + + // ... + + log.Fatal(app.Listen(":3000")) +} +``` + +### 跨原始來源資源共用 (CORS) + +📖 [CORS](https://docs.gofiber.io/api/middleware/cors) + +```go +import ( + "log" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/cors" +) + +func main() { + app := fiber.New() + + app.Use(cors.New()) + + // ... + + log.Fatal(app.Listen(":3000")) +} +``` + +在 `Origin` 標頭傳入任何網域來檢查 CORS 的效果: + +```bash +curl -H "Origin: http://example.com" --verbose http://localhost:3000 +``` + +### 自訂 404 回應 + +📖 [HTTP 方法](https://docs.gofiber.io/api/ctx#status) + +```go +func main() { + app := fiber.New() + + app.Static("/", "./public") + + app.Get("/demo", func(c *fiber.Ctx) error { + return c.SendString("This is a demo!") + }) + + app.Post("/register", func(c *fiber.Ctx) error { + return c.SendString("Welcome!") + }) + + // 最後一個中介模組,符合所有條件 + app.Use(func(c *fiber.Ctx) error { + return c.SendStatus(404) + // => 404 "Not Found" + }) + + log.Fatal(app.Listen(":3000")) +} +``` + +### JSON 回應 + +📖 [JSON](https://docs.gofiber.io/api/ctx#json) + +```go +type User struct { + Name string `json:"name"` + Age int `json:"age"` +} + +func main() { + app := fiber.New() + + app.Get("/user", func(c *fiber.Ctx) error { + return c.JSON(&User{"John", 20}) + // => {"name":"John", "age":20} + }) + + app.Get("/json", func(c *fiber.Ctx) error { + return c.JSON(fiber.Map{ + "success": true, + "message": "Hi John!", + }) + // => {"success":true, "message":"Hi John!"} + }) + + log.Fatal(app.Listen(":3000")) +} +``` + +### WebSocket 升級 (Upgrade) + +📖 [Websocket](https://github.com/gofiber/websocket) + +```go +import ( + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/websocket" +) + +func main() { + app := fiber.New() + + app.Get("/ws", websocket.New(func(c *websocket.Conn) { + for { + mt, msg, err := c.ReadMessage() + if err != nil { + log.Println("read:", err) + break + } + log.Printf("recv: %s", msg) + err = c.WriteMessage(mt, msg) + if err != nil { + log.Println("write:", err) + break + } + } + })) + + log.Fatal(app.Listen(":3000")) + // ws://localhost:3000/ws +} +``` + +### Server-Sent Events + +📖 [更多資訊](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events) + +```go +import ( + "github.com/gofiber/fiber/v2" + "github.com/valyala/fasthttp" +) + +func main() { + app := fiber.New() + + app.Get("/sse", func(c *fiber.Ctx) error { + c.Set("Content-Type", "text/event-stream") + c.Set("Cache-Control", "no-cache") + c.Set("Connection", "keep-alive") + c.Set("Transfer-Encoding", "chunked") + + c.Context().SetBodyStreamWriter(fasthttp.StreamWriter(func(w *bufio.Writer) { + fmt.Println("WRITER") + var i int + + for { + i++ + msg := fmt.Sprintf("%d - 目前時間為 %v", i, time.Now()) + fmt.Fprintf(w, "data: 訊息: %s\n\n", msg) + fmt.Println(msg) + + w.Flush() + time.Sleep(5 * time.Second) + } + })) + + return nil + }) + + log.Fatal(app.Listen(":3000")) +} +``` + +### Recover 中介模組 + +📖 [Recover](https://docs.gofiber.io/api/middleware/recover) + +```go +import ( + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/recover" +) + +func main() { + app := fiber.New() + + app.Use(recover.New()) + + app.Get("/", func(c *fiber.Ctx) error { + panic("正常來說,這會導致 app 當機") + }) + + log.Fatal(app.Listen(":3000")) +} +``` + +
+ +### 使用信任的代理伺服器 + +📖 [組態設定](https://docs.gofiber.io/api/fiber#config) + +```go +import ( + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/recover" +) + +func main() { + app := fiber.New(fiber.Config{ + EnableTrustedProxyCheck: true, + TrustedProxies: []string{"0.0.0.0", "1.1.1.1/30"}, // IP 地址或 IP 地址區間 + ProxyHeader: fiber.HeaderXForwardedFor, + }) + + // ... + + log.Fatal(app.Listen(":3000")) +} +``` + + + +## 🧬 內建中介模組 + +這裡列出了 Fiber 框架內建的中介模組。 + +| 中介模組 | 描述 | +| :------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [basicauth](https://github.com/gofiber/fiber/tree/master/middleware/basicauth) | 提供 HTTP Basic 認證的基本認證中介模組。如果憑證有效,則會呼叫接續函式 (next);如果沒有憑證或失效,則回傳 401 Unauthorized。 | +| [cache](https://github.com/gofiber/fiber/tree/master/middleware/cache) | 攔截並快取回應。 | +| [compress](https://github.com/gofiber/fiber/tree/master/middleware/compress) | 適用於 Fiber 的壓縮中介模組。預設支援 `deflate`、`gzip` 和 `brotli`。 | +| [cors](https://github.com/gofiber/fiber/tree/master/middleware/cors) | 啟用跨來源資源共用 (CORS),可調整多種選項。 | +| [csrf](https://github.com/gofiber/fiber/tree/master/middleware/csrf) | 保護資源防止 CSRF 利用。 | +| [encryptcookie](https://github.com/gofiber/fiber/tree/master/middleware/encryptcookie) | 加密中介模組,會將 Cookie 的值進行加密。 | +| [envvar](https://github.com/gofiber/fiber/tree/master/middleware/envvar) | 公開環境變數,並提供可調整設定。 | +| [etag](https://github.com/gofiber/fiber/tree/master/middleware/etag) | ETag 中介模組,讓快取更高效,同時因為 Web 伺服器不需要在內容未更動時重新傳送完整請求,因此可以減少流量使用。 | +| [expvar](https://github.com/gofiber/fiber/tree/master/middleware/expvar) | Expvar 中介模組,透過其 HTTP 伺服器執行階段,提供 JSON 格式的公用變數。 | +| [favicon](https://github.com/gofiber/fiber/tree/master/middleware/favicon) | 不輸出 Favicons 請求記錄;若有提供檔案路徑,則從記憶體提供圖示。 | +| [filesystem](https://github.com/gofiber/fiber/tree/master/middleware/filesystem) | 適用於 Fiber 的檔案系統中介模組。特別銘謝 Alireza Salary! | +| [limiter](https://github.com/gofiber/fiber/tree/master/middleware/limiter) | 適用於 Fiber 的速率限制中介模組。用來限制傳入公開 API 或者(以及)端點(如密碼重設)的重複請求。 | +| [logger](https://github.com/gofiber/fiber/tree/master/middleware/logger) | HTTP 請求/回應記錄工具。 | +| [monitor](https://github.com/gofiber/fiber/tree/master/middleware/monitor) | 監控中介模組,用來回報伺服器指標。啟發自 express-status-monitor。 | +| [pprof](https://github.com/gofiber/fiber/tree/master/middleware/pprof) | 特別感謝 Matthew Lee \(@mthli\) | +| [proxy](https://github.com/gofiber/fiber/tree/master/middleware/proxy) | 讓您可以將請求代理 (proxy) 至多台伺服器。 | +| [recover](https://github.com/gofiber/fiber/tree/master/middleware/recover) | Recover 中介模組:可以從呼叫堆疊鏈中任何部分的當機 (panic) 中復原,並將控制權交由中央的 [錯誤處理常式 (ErrorHandler)](https://docs.gofiber.io/guide/error-handling) 處理。 | +| [requestid](https://github.com/gofiber/fiber/tree/master/middleware/requestid) | 為每個請求加上 requestid。 | +| [session](https://github.com/gofiber/fiber/tree/master/middleware/session) | 連線階段中介模組。注意:這個中介模組有用到我們的 Storage 套件。 | +| [skip](https://github.com/gofiber/fiber/tree/master/middleware/skip) | 略過中介模組,會在條件成立時略過封裝過的處理常式。 | +| [timeout](https://github.com/gofiber/fiber/tree/master/middleware/timeout) | 為請求加上最長時限,並在逾時後轉送至錯誤處理常式 (ErrorHandler)。 | +| [keyauth](https://github.com/gofiber/keyauth) | Key auth 中介模組提供以金鑰為基礎的認證模式。 | +| [redirect](https://github.com/gofiber/redirect) | 用來重新導向的中介模組。 | +| [rewrite](https://github.com/gofiber/rewrite) | 重寫 (Rewrite) 中介模組:根據提供規則重寫 URL 路徑,適合用來向後相容,或者是製作更乾淨且更好懂的連結。 | +| [adaptor](https://github.com/gofiber/adaptor) | 將 net/http 處理常式轉換至 Fiber 處理常式,或者是反著做。特別感謝 @arsmn! | +| [helmet](https://github.com/gofiber/helmet) | 透過設定多種 HTTP 標頭,協助保護您應用程式的安全。 | + +## 🧬 外掛中介模組 + +這裡列出由 [Fiber 團隊](https://github.com/orgs/gofiber/people) 維護、存放在外部的中介模組。 + +| 中介模組 | 描述 | +| :------------------------------------------------ | :----------------------------------------------------------------------------------- | +| [jwt](https://github.com/gofiber/jwt) | JWT 回傳 JSON Web Token \(JWT\) 認證中介模組。 | +| [storage](https://github.com/gofiber/storage) | 已經做好,實作 Storage 介面的儲存區驅動模組,設計用來與各種 Fiber 中介模組搭配使用。 | +| [template](https://github.com/gofiber/template) | 本套件包含 8 種樣板引擎,可以和 Fiber `v1.10.x` 一起使用。需要 Go 1.13 或更新版本。 | +| [websocket](https://github.com/gofiber/websocket) | 適用於 Fiber,建基於 Fasthttp 的 WebSocket。支援本機空間 (Locals)! | + +## 🕶️ Awesome List + +更多文章、中介模組、範例或工具,請參考我們的 [Awesome List](https://github.com/gofiber/awesome-fiber)。 + +## 👍 貢獻 + +如果您想和我們 **道謝**,或者是支持 `Fiber` 繼續積極開發下去(也可以兩個都做): + +1. 送給專案一顆 [GitHub 星星](https://github.com/gofiber/fiber/stargazers)。 +2. [在您的 𝕏 (Twitter) 上](https://x.com/intent/tweet?text=Fiber%20is%20an%20Express%20inspired%20%23web%20%23framework%20built%20on%20top%20of%20Fasthttp%2C%20the%20fastest%20HTTP%20engine%20for%20%23Go.%20Designed%20to%20ease%20things%20up%20for%20%23fast%20development%20with%20zero%20memory%20allocation%20and%20%23performance%20in%20mind%20%F0%9F%9A%80%20https%3A%2F%2Fgithub.com%2Fgofiber%2Ffiber)發出關於本專案的推文。 +3. 在 [Medium](https://medium.com/)、[Dev.to](https://dev.to/) 或者是個人部落格上寫下評論或教學。 +4. 捐專案 [一杯咖啡的費用](https://buymeacoff.ee/fenny) 以示支持。 + +## ☕ 支持者 + +Fiber 是個仰賴捐款的開放原始碼專案——用來支付如域名、Gitbook、Netlify 和無服務器運算服務的費用。如果您想支持 Fiber,可以在 ☕ [**這裡捐杯咖啡**](https://buymeacoff.ee/fenny)。 + +| | 使用者 | 捐款 | +| :--------------------------------------------------------- | :----------------------------------------------- | :------ | +| ![](https://avatars.githubusercontent.com/u/204341?s=25) | [@destari](https://github.com/destari) | ☕ x 10 | +| ![](https://avatars.githubusercontent.com/u/63164982?s=25) | [@dembygenesis](https://github.com/dembygenesis) | ☕ x 5 | +| ![](https://avatars.githubusercontent.com/u/56607882?s=25) | [@thomasvvugt](https://github.com/thomasvvugt) | ☕ x 5 | +| ![](https://avatars.githubusercontent.com/u/27820675?s=25) | [@hendratommy](https://github.com/hendratommy) | ☕ x 5 | +| ![](https://avatars.githubusercontent.com/u/1094221?s=25) | [@ekaputra07](https://github.com/ekaputra07) | ☕ x 5 | +| ![](https://avatars.githubusercontent.com/u/194590?s=25) | [@jorgefuertes](https://github.com/jorgefuertes) | ☕ x 5 | +| ![](https://avatars.githubusercontent.com/u/186637?s=25) | [@candidosales](https://github.com/candidosales) | ☕ x 5 | +| ![](https://avatars.githubusercontent.com/u/29659953?s=25) | [@l0nax](https://github.com/l0nax) | ☕ x 3 | +| ![](https://avatars.githubusercontent.com/u/635852?s=25) | [@bihe](https://github.com/bihe) | ☕ x 3 | +| ![](https://avatars.githubusercontent.com/u/307334?s=25) | [@justdave](https://github.com/justdave) | ☕ x 3 | +| ![](https://avatars.githubusercontent.com/u/11155743?s=25) | [@koddr](https://github.com/koddr) | ☕ x 1 | +| ![](https://avatars.githubusercontent.com/u/29042462?s=25) | [@lapolinar](https://github.com/lapolinar) | ☕ x 1 | +| ![](https://avatars.githubusercontent.com/u/2978730?s=25) | [@diegowifi](https://github.com/diegowifi) | ☕ x 1 | +| ![](https://avatars.githubusercontent.com/u/44171355?s=25) | [@ssimk0](https://github.com/ssimk0) | ☕ x 1 | +| ![](https://avatars.githubusercontent.com/u/5638101?s=25) | [@raymayemir](https://github.com/raymayemir) | ☕ x 1 | +| ![](https://avatars.githubusercontent.com/u/619996?s=25) | [@melkorm](https://github.com/melkorm) | ☕ x 1 | +| ![](https://avatars.githubusercontent.com/u/31022056?s=25) | [@marvinjwendt](https://github.com/marvinjwendt) | ☕ x 1 | +| ![](https://avatars.githubusercontent.com/u/31921460?s=25) | [@toishy](https://github.com/toishy) | ☕ x 1 | + +## ‎‍💻 程式碼貢獻者 + +Code Contributors + +## ⭐️ 收藏數 + +Stargazers over time + +## ⚠️ 授權條款 + +著作權所有 (c) 2019-現在 [Fenny](https://github.com/fenny) 和[貢獻者](https://github.com/gofiber/fiber/graphs/contributors)。`Fiber` 是款依照 [MIT License](https://github.com/gofiber/fiber/blob/master/LICENSE) 授權,免費且開放原始碼的軟體。官方圖示 (logo) 由 [Vic Shóstak](https://github.com/koddr) 製作,並依據 [創用 CC](https://creativecommons.org/licenses/by-sa/4.0/) 授權條款散佈 (CC BY-SA 4.0 International)。 + +**第三方函式庫的授權條款** + +- [colorable](https://github.com/mattn/go-colorable/blob/master/LICENSE) +- [isatty](https://github.com/mattn/go-isatty/blob/master/LICENSE) +- [runewidth](https://github.com/mattn/go-runewidth/blob/master/LICENSE) +- [fasthttp](https://github.com/valyala/fasthttp/blob/master/LICENSE) +- [bytebufferpool](https://github.com/valyala/bytebufferpool/blob/master/LICENSE) +- [fwd](https://github.com/philhofer/fwd/blob/master/LICENSE.md) +- [go-ole](https://github.com/go-ole/go-ole/blob/master/LICENSE) +- [gopsutil](https://github.com/shirou/gopsutil/blob/master/LICENSE) +- [msgp](https://github.com/tinylib/msgp/blob/master/LICENSE) +- [schema](https://github.com/gorilla/schema/blob/master/LICENSE) +- [uuid](https://github.com/google/uuid/blob/master/LICENSE) +- [wmi](https://github.com/StackExchange/wmi/blob/master/LICENSE) diff --git a/.github/SECURITY.md b/.github/SECURITY.md new file mode 100644 index 0000000..9d4826f --- /dev/null +++ b/.github/SECURITY.md @@ -0,0 +1,76 @@ +# Security Policy + +1. [Supported Versions](#versions) +2. [Reporting security problems to Fiber](#reporting) +3. [Security Point of Contact](#contact) +4. [Incident Response Process](#process) + + +## Supported Versions + +The table below shows the supported versions for Fiber which include security updates. + +| Version | Supported | +| --------- | ------------------ | +| >= 1.12.6 | :white_check_mark: | +| < 1.12.6 | :x: | + + +## Reporting security problems to Fiber + +**DO NOT CREATE AN ISSUE** to report a security problem. Instead, please +send us an e-mail at `team@gofiber.io` or join our discord server via +[this invite link](https://gofiber.io/discord) and send a private message +to Fenny or any of the maintainers. + + +## Security Point of Contact + +The security point of contact is [Fenny](https://github.com/Fenny). Fenny responds +to security incident reports as fast as possible, within one business day at the +latest. + +In case Fenny does not respond within a reasonable time, the secondary point +of contact are any of the [@maintainers](https://github.com/orgs/gofiber/teams/maintainers). +The maintainers are the only other persons with administrative access to Fiber's source code. + + +## Incident Response Process + +In case an incident is discovered or reported, we will follow the following +process to contain, respond and remediate: + +### 1. Containment + +The first step is to find out the root cause, nature and scope of the incident. + +- Is still ongoing? If yes, first priority is to stop it. +- Is the incident outside of our influence? If yes, first priority is to contain it. +- Find out knows about the incident and who is affected. +- Find out what data was potentially exposed. + +### 2. Response + +After the initial assessment and containment to our best abilities, we will +document all actions taken in a response plan. + +We will create a comment in the official `#announcements` channel to inform users about +the incident and what actions we took to contain it. + +### 3. Remediation + +Once the incident is confirmed to be resolved, we will summarize the lessons +learned from the incident and create a list of actions we will take to prevent +it from happening again. + +### Secure accounts with access + +The [Fiber Organization](https://github.com/gofiber) requires 2FA authorization +for all of it's members. + +### Critical Updates And Security Notices + +We learn about critical software updates and security threats from these sources + +1. GitHub Security Alerts +2. GitHub: https://status.github.com/ & [@githubstatus](https://twitter.com/githubstatus) diff --git a/.github/config.yml b/.github/config.yml new file mode 100644 index 0000000..dff880c --- /dev/null +++ b/.github/config.yml @@ -0,0 +1,17 @@ +# Configuration for new-issue-welcome - https://github.com/behaviorbot/new-issue-welcome +# Comment to be posted to on first time issues +newIssueWelcomeComment: > + Thanks for opening your first issue here! 🎉 Be sure to follow the issue template! + If you need help or want to chat with us, join us on Discord https://gofiber.io/discord + +# Configuration for new-pr-welcome - https://github.com/behaviorbot/new-pr-welcome +# Comment to be posted to on PRs from first time contributors in your repository +newPRWelcomeComment: > + Thanks for opening this pull request! 🎉 Please check out our contributing guidelines. + If you need help or want to chat with us, join us on Discord https://gofiber.io/discord + +# Configuration for first-pr-merge - https://github.com/behaviorbot/first-pr-merge +# Comment to be posted to on pull requests merged by a first time user +firstPRMergeComment: > + Congrats on merging your first pull request! 🎉 We here at Fiber are proud of you! + If you need help or want to chat with us, join us on Discord https://gofiber.io/discord diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..55253d3 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,16 @@ +# https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates + +version: 2 +updates: + - package-ecosystem: "gomod" + directory: "/" # Location of package manifests + labels: + - "🤖 Dependencies" + schedule: + interval: "daily" + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: daily + labels: + - "🤖 Dependencies" diff --git a/.github/index.html b/.github/index.html new file mode 100644 index 0000000..4115b43 --- /dev/null +++ b/.github/index.html @@ -0,0 +1,14 @@ + + + + + + + Test file + + + + Hello, World! + + + \ No newline at end of file diff --git a/.github/labeler.yml b/.github/labeler.yml new file mode 100644 index 0000000..ea3e61d --- /dev/null +++ b/.github/labeler.yml @@ -0,0 +1,20 @@ +version: v1 +labels: + - label: '📒 Documentation' + matcher: + title: '\b(docs|doc:|\[doc\]|README|typos|comment|documentation)\b' + - label: '☢️ Bug' + matcher: + title: '\b(fix|race|bug|missing|correct)\b' + - label: '🧹 Updates' + matcher: + title: '\b(improve|update|refactor|deprecated|remove|unused|test)\b' + - label: '🤖 Dependencies' + matcher: + title: '\b(bumb|bdependencies)\b' + - label: '✏️ Feature' + matcher: + title: '\b(feature|feat|create|implement|add)\b' + - label: '🤔 Question' + matcher: + title: '\b(question|how)\b' diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..222b26d --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,31 @@ +## Description + +Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. List any dependencies that are required for this change. +Explain the *details* for making this change. What existing problem does the pull request solve? + +Fixes # (issue) + +## Type of change + +Please delete options that are not relevant. + +- [ ] Bug fix (non-breaking change which fixes an issue) +- [ ] New feature (non-breaking change which adds functionality) +- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) +- [ ] This change requires a documentation update + +## Checklist: + +- [ ] For new functionalities I follow the inspiration of the express js framework and built them similar in usage +- [ ] I have performed a self-review of my own code +- [ ] I have commented my code, particularly in hard-to-understand areas +- [ ] I have made corresponding changes to the documentation - /docs/ directory for https://docs.gofiber.io/ +- [ ] I have added tests that prove my fix is effective or that my feature works +- [ ] New and existing unit tests pass locally with my changes +- [ ] If new dependencies exist, I have checked that they are really necessary and agreed with the maintainers/community (we want to have as few dependencies as possible) +- [ ] I tried to make my code as fast as possible with as few allocations as possible +- [ ] For new code I have written benchmarks so that they can be analyzed and improved + +## Commit formatting: + +Use emojis on commit messages so it provides an easy way of identifying the purpose or intention of a commit. Check out the emoji cheatsheet here: [CONTRIBUTING.md](https://github.com/gofiber/fiber/blob/master/.github/CONTRIBUTING.md#pull-requests-or-commits) diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml new file mode 100644 index 0000000..c9af318 --- /dev/null +++ b/.github/release-drafter.yml @@ -0,0 +1,53 @@ +name-template: 'v$RESOLVED_VERSION' +tag-template: 'v$RESOLVED_VERSION' +commitish: v2 +filter-by-commitish: true +include-labels: + - 'v2' +exclude-labels: + - 'v3' +categories: + - title: '❗ Breaking Changes' + labels: + - '❗ BreakingChange' + - title: '🚀 New' + labels: + - '✏️ Feature' + - title: '🧹 Updates' + labels: + - '🧹 Updates' + - '🤖 Dependencies' + - title: '🐛 Fixes' + labels: + - '☢️ Bug' + - title: '📚 Documentation' + labels: + - '📒 Documentation' +change-template: '- $TITLE (#$NUMBER)' +change-title-escapes: '\<*_&' # You can add # and @ to disable mentions, and add ` to disable code blocks. +exclude-contributors: + - dependabot + - dependabot[bot] +version-resolver: + major: + labels: + - 'major' + - '❗ BreakingChange' + minor: + labels: + - 'minor' + - '✏️ Feature' + patch: + labels: + - 'patch' + - '📒 Documentation' + - '☢️ Bug' + - '🤖 Dependencies' + - '🧹 Updates' + default: patch +template: | + $CHANGES + + **Full Changelog**: https://github.com/$OWNER/$REPOSITORY/compare/$PREVIOUS_TAG...v$RESOLVED_VERSION + + Thank you $CONTRIBUTORS for making this update possible. diff --git a/.github/release.yml b/.github/release.yml new file mode 100644 index 0000000..8f1601c --- /dev/null +++ b/.github/release.yml @@ -0,0 +1,26 @@ +# .github/release.yml + +changelog: + categories: + - title: '❗ Breaking Changes' + labels: + - '❗ BreakingChange' + - title: '🚀 New Features' + labels: + - '✏️ Feature' + - '📝 Proposal' + - title: '🧹 Updates' + labels: + - '🧹 Updates' + - title: '🐛 Bug Fixes' + labels: + - '☢️ Bug' + - title: '🛠️ Maintenance' + labels: + - '🤖 Dependencies' + - title: '📚 Documentation' + labels: + - '📒 Documentation' + - title: 'Other Changes' + labels: + - '*' diff --git a/.github/scripts/sync_docs.sh b/.github/scripts/sync_docs.sh new file mode 100755 index 0000000..8175f26 --- /dev/null +++ b/.github/scripts/sync_docs.sh @@ -0,0 +1,75 @@ +#!/usr/bin/env bash +set -e + +# Some env variables +BRANCH="master" +REPO_URL="github.com/gofiber/docs.git" +AUTHOR_EMAIL="github-actions[bot]@users.noreply.github.com" +AUTHOR_USERNAME="github-actions[bot]" +VERSION_FILE="versions.json" +REPO_DIR="core" +COMMIT_URL="https://github.com/gofiber/fiber" +DOCUSAURUS_COMMAND="npm run docusaurus -- docs:version" + +# Set commit author +git config --global user.email "${AUTHOR_EMAIL}" +git config --global user.name "${AUTHOR_USERNAME}" + +git clone https://${TOKEN}@${REPO_URL} fiber-docs + +# Handle push event +#if [ "$EVENT" == "push" ]; then +# latest_commit=$(git rev-parse --short HEAD) +# log_output=$(git log --oneline ${BRANCH} HEAD~1..HEAD --name-status -- docs/) +# if [[ $log_output != "" ]]; then +# cp -a docs/* fiber-docs/docs/${REPO_DIR} +# fi +# +## Handle release event +#el +if [ "$EVENT" == "release" ]; then + major_version="${TAG_NAME%%.*}" + + # Form new version name + new_version="${major_version}.x" + + cd fiber-docs/ || true + npm ci + + # Check if contrib_versions.json exists and modify it if required + if [[ -f $VERSION_FILE ]]; then + jq --arg new_version "$new_version" 'del(.[] | select(. == $new_version))' $VERSION_FILE > temp.json && mv temp.json $VERSION_FILE + fi + + # Run docusaurus versioning command + $DOCUSAURUS_COMMAND "${new_version}" + + if [[ -f $VERSION_FILE ]]; then + jq 'sort | reverse' ${VERSION_FILE} > temp.json && mv temp.json ${VERSION_FILE} + fi +fi + +# Push changes +cd fiber-docs/ || true +git add . +if [[ $EVENT == "push" ]]; then + git commit -m "Add docs from ${COMMIT_URL}/commit/${latest_commit}" +elif [[ $EVENT == "release" ]]; then + git commit -m "Sync docs for release ${COMMIT_URL}/releases/tag/${TAG_NAME}" +fi + +MAX_RETRIES=5 +DELAY=5 +retry=0 + +while ((retry < MAX_RETRIES)); do + git push https://${TOKEN}@${REPO_URL} && break + retry=$((retry + 1)) + git pull --rebase + sleep $DELAY +done + +if ((retry == MAX_RETRIES)); then + echo "Failed to push after $MAX_RETRIES attempts. Exiting with 1." + exit 1 +fi diff --git a/.github/testdata/ca-chain.cert.pem b/.github/testdata/ca-chain.cert.pem new file mode 100644 index 0000000..90d9f30 --- /dev/null +++ b/.github/testdata/ca-chain.cert.pem @@ -0,0 +1,65 @@ +-----BEGIN CERTIFICATE----- +MIIFeTCCA2GgAwIBAgIDEAISMA0GCSqGSIb3DQEBCwUAMFYxCzAJBgNVBAYTAlVT +MQ8wDQYDVQQIDAZEZW5pYWwxFDASBgNVBAcMC1NwcmluZ2ZpZWxkMQwwCgYDVQQK +DANEaXMxEjAQBgNVBAMMCWxvY2FsaG9zdDAeFw0yMjAyMDgyMDA3MjlaFw0zMjAy +MDYyMDA3MjlaMEAxCzAJBgNVBAYTAlVTMQ8wDQYDVQQIDAZEZW5pYWwxDDAKBgNV +BAoMA0RpczESMBAGA1UEAwwJbG9jYWxob3N0MIICIjANBgkqhkiG9w0BAQEFAAOC +Ag8AMIICCgKCAgEA5Cho0kbBDi1cy8bURStc95hK2RzjBQMd2hN5gFxZdF5knBfC +LSiPMxtAn9zJYzYc9+Cq7hIOK19cgG4yKk9GFZaUe+mU4yWxRg1ViSu/jzQ04sVc +JRSbSklXY1RNyxpUtGelxnluUvdvuXXlCPmKob4IsUtI1FTcumG1mzIO+cAzBd1J +KQtNTUO9XfSHYusV/FQO2hIbaXcFgSAg50JJfYZaUw51J07j3vdb6lb1x4rRmIaq +8txrdHo0Y2tXHsq6jry1QrOZfoz4WbYcoID3JU1MC5f1HyR5uYiCA1RJVGnQ3iSX +3yM+gRy3SFPeaASs2useSzGkMr/pDlbcSVmsbXsasBxZq85T1FE8vuY6K4XlU2sN +PyiPrNjDgVkQ8Lbj1B9oKEYKkmSieBx9YwRLarfru1kt+g3kdXuel7DyHpm+j/13 +vqjyF9DAyx4wAEZC+DzeqBsbuiDdRkzwFMcKPxYpgSTLawnCjlFapPvE5kGN+O/j +To2qWbWUU/upzBvHu4tnICSapJJ0VqA+7M7yaBAsIWK/yjNTzpHfx0oHudl8wBOG +ySfOE52uouFsp2vs06YpEg2nGn/7Iu0Rbbwt4iFcSZlEnSk0cQlyMZxdj3M2fMKa +/nrRQm7guPbVmBJOFHZuTTiilNSduSsDwCjJkGdJkSVYbj3+eJzKwYstnT8CAwEA +AaNmMGQwHQYDVR0OBBYEFHwli1hTCVJLHPTHWW8O8BCaHci9MB8GA1UdIwQYMBaA +FDlb/7rpDA2ZzsLZmqbW/krUtmGOMBIGA1UdEwEB/wQIMAYBAf8CAQAwDgYDVR0P +AQH/BAQDAgGGMA0GCSqGSIb3DQEBCwUAA4ICAQBtMxa2/w6kGF9cmqpTdQ1La8nY +R4Zoewnn+SCmcSOwCyBC32g0Ry6nKKUpJBpJEid5lBzWveIw4K7pdWvmuqmeMuWI +ilvlCLzqYPigmnEIW96hc6XiQvl9NC5j+SAZSC+4uCNhEUx5pEbE1FU1gIX+szdJ +tLdPwwg63Ce/us6QZ7Tx8qLIr+XU+DrCgjIheQFShtoNYDw0GxEtjeo8vHynj8EZ ++p0OZgqoNlnRbQbllruDFPXDJVI23DVhNpJhT86iQDMtMV53ypMu62LXmdQIKa7l +ITnEMGO626RKqw2kDHt7yinBlt1nHskaeeLya6K/08uJkqRCjzOshJgsjQ3e62vQ +Mht9QvGBCAoY09fIGxRihtTWCFDe7MEnbh1PPYB7cZTOMnL3wxRPzLLYhclX+pt0 +bBf7Dn84b3tdC5BFXBJeZMs5QSCvn4yrTew+NvvX3oL6Ny1JDZYaG5PhKf00J6iy +TkXzK2n9U9RX+krPk8fU9Ae1nayD0vrmGaVcBdRQPn4XUuS3LhdlkHfr28z1nF9m +ffd0WBrJlNX9SoKtsMj8VJFZ/nJ0EcCcY1mG3k/IGAY1HUeo4A+C5E/UO3h4+tOL +uqUa8rkl9HoE4fIWdQVxtQjEdATSuJusaK7CFpWH8A0w9VchDx74saiwwGhVyXYk +yBwSA5U88ymkQ7qNJg== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFnTCCA4WgAwIBAgIUQnfvDIm6z+973AYRTLorZHEQA30wDQYJKoZIhvcNAQEL +BQAwVjELMAkGA1UEBhMCVVMxDzANBgNVBAgMBkRlbmlhbDEUMBIGA1UEBwwLU3By +aW5nZmllbGQxDDAKBgNVBAoMA0RpczESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTIy +MDIwODIwMDcyOVoXDTQyMDIwMzIwMDcyOVowVjELMAkGA1UEBhMCVVMxDzANBgNV +BAgMBkRlbmlhbDEUMBIGA1UEBwwLU3ByaW5nZmllbGQxDDAKBgNVBAoMA0RpczES +MBAGA1UEAwwJbG9jYWxob3N0MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKC +AgEA+Cf/fKPvI5+Nh81wpxghLrpAjM1MyhdHUDXc8bu7NTNYZ+6ArMqDeKSszTWT +gLV9EeJs57KwjwXIoYZDTcLvpjanrZ2s7JDEqsGl7S6Xr67qzYghlF/GaB3f3lAi +GsvhmDgC4jkdCvkVBKOB4tE0dy6fnNCmIKhhJDje51p90LWFuX5sIKO2trgte6b/ +P1PW8rOjedPd5Z+QCG4Mi8JnbJicX1YaOySGMcXHm/eM2wy5I3pEdUreZDFbjB7l +CKa1kFAnDXBiQAoErggMlcXe6C+avB13wYCfi+R+9m2X0svmerSz+oHDCOhvnD52 +EE7fBv89VS6pR8mykz1eHiBKkVT1qdmONThUQ8mqwxlo06bZyoykKSxG6ffJnGbM +GkTWlaNEdZuY0pITQLxEX2SwLJuZKfTPFheno83bLCqTOTuWo7h1mLe0ogJMxl// +NHzyoMJ0b1bbrRgsLcMaDn1MsI+gVdRY89+cZ2uedEgrr7fl3KFWiF2S57bxX1fJ +P8HC0bzMny4jMtIf6YUqDtpGDPjZq3PGqcrcO0dVYkuaC96H3xLcvQxgkMDK0sm0 +pUbWlECzAag/lxeeC22VnedqgCpiq/9z1b4j884ZkhIJyht0HR7L4I0gO/R/mWUY +8bO9XCMYmP0CjO7u93IlzQ7aSIpWprTHxjpiPelmcO001jkCAwEAAaNjMGEwHQYD +VR0OBBYEFDlb/7rpDA2ZzsLZmqbW/krUtmGOMB8GA1UdIwQYMBaAFDlb/7rpDA2Z +zsLZmqbW/krUtmGOMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMA0G +CSqGSIb3DQEBCwUAA4ICAQDk02Tu0ypqnS897WBx98l2nYIrEhcrJg8ZMmSwEa9J +7TANofzsP9931YoQMh6BI6hB3OkyL6FYTUDykpGMMasojtL/F2iXEsjema2ilZ/7 +hNAZ+j5mBemMwXfkfmRguXvnl7EWaZETgEoxhcOTYoYUYqDcyzuwK63fOs+YA5ke +O8E3F1aLHzLpqVpiG7t740L7LdibNPko9JOd31Gqcq3nhXMf6/rOdL8VSj/F+4BG +opgJBruJV9NxWRI/b0G6eImvaYL/Ljfd8wzwNpmYkNkHbhAiaHeXJQ05mebmr2Dr +wne9QeSJkXCs/K5A/8+0CYNN4homt8xNNN02SnJ5e6nv1A1ntMW9n6n2KYo87tz9 +VmqWXg7Y1BqXj287WRaWPJsBa2RBP1W2d3BQfHKJfu15blyXaczTi87WayEsBnQq +TXy+1QP0IwQerSTOxdW25UoJmH18SRbLEIQs9Htvcpz2AncTYjeiLFa15FO2r5hP +LYc9QOKn6yIZP9lYztleEqOLTmHnRnFcupDol+/x88d+kVLqmXDiKmWbVIz7C735 +xgImsyrCPPYYiEA7/yaP5G1o5XU93kRPrtg/7jjyF+uBZ70fcbED3prpuiJYrL0O +gvQUgmGUU30mPHjAKkEACeRXtoqucRDxvIkBb5zUvZG8RmSFae5siAWwLD7D7VJa +IA== +-----END CERTIFICATE----- diff --git a/.github/testdata/favicon.ico b/.github/testdata/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..b752c26641bfb32f51273965f080cb3bedac61f1 GIT binary patch literal 4286 zcmc(jy>7xV5XTQOWHeJ(Jcf^9iE6dVOr_&3r!h zk8H#s7BNW(^~iINWGc0twRS{Wl=+A512J3nf2{qyHaZKr_StN<;y zHBXDz{2G`2x8&jX$A=cL`I)2j*XH$MQ*DjQ{%iP>J()~C(YMrcLqBu0K8y{=93Nu` zbIUL%Hs)BZkL74STDPpzwGT30+8RsxV{ERK(!eoVFFBe`=b+UfzowxpPvgb>UA6jX zh;dTB+!|6WZ7!z9}PMAq%EZZ=ay1`ZoZ}=#->=w&-L3k+y}`{ zeRrXrI;VyXc8$Ef+=D*031p`Nj6k zd(>+9CVv>$cO28uYTIG|8op!I0K8z7_uD02U3vcwK?n)KKjH08#Qer2qf` literal 0 HcmV?d00001 diff --git a/.github/testdata/fs/css/style.css b/.github/testdata/fs/css/style.css new file mode 100644 index 0000000..9085d4f --- /dev/null +++ b/.github/testdata/fs/css/style.css @@ -0,0 +1,4 @@ +h1 { + color: red; + text-align: center; +} \ No newline at end of file diff --git a/.github/testdata/fs/img/fiber.png b/.github/testdata/fs/img/fiber.png new file mode 100644 index 0000000000000000000000000000000000000000..fa981bcf261035838ef96c52ef4ae9339ca1e788 GIT binary patch literal 1542 zcmb7E`#aMM82`GJW=h3enp<|(j?0?d3#qx}X>DtSA}qHsp`1jAaM(C9BKKV9I*v`! zlv_5}VidXM9%&}z5-J@2fu5(&^S;mLeLv6Vecs#4);8|453)(Sv{H8Pa&<2yML-Oh`C@g@>s zjXR5esQ7LZXQdnlejIO-js*Y_O-s{r*7pSFo5FW5fw0`IQt48`z42FYAIGL;iiD$p z#<2E?+*N+a`p!xaG9kh-6S7+>gU)ta*|i(|B^<*vDgV@{2rOvy2Ks*jn{QfgMhd zUdV&-#afzSwu5wPQP~iS(g(XpO3vqLPaF-2sVjEk5T7gIxjRJK3)*@u*U6*Omrp%HsHi?OQ44Fk@t$x9`ealid73L|jHnlTUkLAT;wz znU)<8C%PQ*gOF6dgl5gns-3HryP99^HNdA!(V~nR%)CFxK0^3?W!MEr25dGLa!~Zp z{VL>WZjXYByjrJsv-_BdgJbebEQL7KT-pb_4`fhCv#*#%3z7AA|8%5f%D&RGZ9zU# z$jhI9CY0o`e@ZUZQ_91%$WM5_6KJSc4b(dU9)0FQz*&~tSJ0qm9b}kygf_P6BbS0? zmVM)64rmJ1PyJAOi_cb6CG~Je~O$Ar7}EcY^uR^NGI@v7~=A4hbQXPYtta%IQc_>B;TXtjUL{RqIzw*-kO!RsE$I z=dCOv9!=)p-yw`=xjot%YEN!oYbzjXgJZJ|RkI)aJgi(0T+^vT6=V5n*5Z^rW$v@X z5@X=&Mt$?Wa%Z2o>DUolkMkFnO;~Pi1*6U~5otYBsWUzpapDrNFSPZLG1uH&T%J}A zyRE=DxE7gz*sF>-C@p+h zaw_T{4jm$f*6UXnRn5mve-WS-Wz=brP{ZZVYpzl)Cci`U;a`{*%-LI|(HMy1xF*%I zt3B~bRXq`HHf%jfyWint0G&;fba@ca;fSz?!$FFp-rGQEo(n+_Qghi#HWl$VN!r&| zmUU4+iCVFUB%}vSZoX!j8jdc<7zTOhKX=ucFG}fL;jvewd}9_;LFj(jIe^x+sk2qpd|_N`G79t}s`D;$S7BA7Tq6kSR+SkcwS5~?hCejxXHcDVJHje8MO^cCFEQ4D3?G$)~hZp08FAN^mzl5WA~RXMtmr&QT9P z4iSO(@L=ZJzs~C8_CU{qDTzqaZ0UbbozK*$YxqbZa*E*VxHCRV)SVC96m|mcf=@}C zSAl^iBJb<1f~D{IbFuC8D><#j*tL5 + + + + + Document + + + +

Hello, World!

+ + \ No newline at end of file diff --git a/.github/testdata/index.html b/.github/testdata/index.html new file mode 100644 index 0000000..cefca8f --- /dev/null +++ b/.github/testdata/index.html @@ -0,0 +1 @@ +

Hello, Fiber!

\ No newline at end of file diff --git a/.github/testdata/index.tmpl b/.github/testdata/index.tmpl new file mode 100644 index 0000000..131044d --- /dev/null +++ b/.github/testdata/index.tmpl @@ -0,0 +1 @@ +

{{.Title}}

\ No newline at end of file diff --git a/.github/testdata/main.tmpl b/.github/testdata/main.tmpl new file mode 100644 index 0000000..456d6bb --- /dev/null +++ b/.github/testdata/main.tmpl @@ -0,0 +1 @@ +

I'm main

\ No newline at end of file diff --git a/.github/testdata/ssl.key b/.github/testdata/ssl.key new file mode 100644 index 0000000..cd9e251 --- /dev/null +++ b/.github/testdata/ssl.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQD4IQusAs8PJdnG +3mURt/AXtgC+ceqLOatJ49JJE1VPTkMAy+oE1f1XvkMrYsHqmDf6GWVzgVXryL4U +wq2/nJSm56ddhN55nI8oSN3dtywUB8/ShelEN73nlN77PeD9tl6NksPwWaKrqxq0 +FlabRPZSQCfmgZbhDV8Sa8mfCkFU0G0lit6kLGceCKMvmW+9Bz7ebsYmVdmVMxmf +IJStFD44lWFTdUc65WISKEdW2ELcUefb0zOLw+0PCbXFGJH5x5ktksW8+BBk2Hkg +GeQRL/qPCccthbScO0VgNj3zJ3ZZL0ObSDAbvNDG85joeNjDNq5DT/BAZ0bOSbEF +sh+f9BAzAgMBAAECggEBAJWv2cq7Jw6MVwSRxYca38xuD6TUNBopgBvjREixURW2 +sNUaLuMb9Omp7fuOaE2N5rcJ+xnjPGIxh/oeN5MQctz9gwn3zf6vY+15h97pUb4D +uGvYPRDaT8YVGS+X9NMZ4ZCmqW2lpWzKnCFoGHcy8yZLbcaxBsRdvKzwOYGoPiFb +K2QuhXZ/1UPmqK9i2DFKtj40X6vBszTNboFxOVpXrPu0FJwLVSDf2hSZ4fMM0DH3 +YqwKcYf5te+hxGKgrqRA3tn0NCWii0in6QIwXMC+kMw1ebg/tZKqyDLMNptAK8J+ +DVw9m5X1seUHS5ehU/g2jrQrtK5WYn7MrFK4lBzlRwECgYEA/d1TeANYECDWRRDk +B0aaRZs87Rwl/J9PsvbsKvtU/bX+OfSOUjOa9iQBqn0LmU8GqusEET/QVUfocVwV +Bggf/5qDLxz100Rj0ags/yE/kNr0Bb31kkkKHFMnCT06YasR7qKllwrAlPJvQv9x +IzBKq+T/Dx08Wep9bCRSFhzRCnsCgYEA+jdeZXTDr/Vz+D2B3nAw1frqYFfGnEVY +wqmoK3VXMDkGuxsloO2rN+SyiUo3JNiQNPDub/t7175GH5pmKtZOlftePANsUjBj +wZ1D0rI5Bxu/71ibIUYIRVmXsTEQkh/ozoh3jXCZ9+bLgYiYx7789IUZZSokFQ3D +FICUT9KJ36kCgYAGoq9Y1rWJjmIrYfqj2guUQC+CfxbbGIrrwZqAsRsSmpwvhZ3m +tiSZxG0quKQB+NfSxdvQW5ulbwC7Xc3K35F+i9pb8+TVBdeaFkw+yu6vaZmxQLrX +fQM/pEjD7A7HmMIaO7QaU5SfEAsqdCTP56Y8AftMuNXn/8IRfo2KuGwaWwKBgFpU +ILzJoVdlad9E/Rw7LjYhZfkv1uBVXIyxyKcfrkEXZSmozDXDdxsvcZCEfVHM6Ipk +K/+7LuMcqp4AFEAEq8wTOdq6daFaHLkpt/FZK6M4TlruhtpFOPkoNc3e45eM83OT +6mziKINJC1CQ6m65sQHpBtjxlKMRG8rL/D6wx9s5AoGBAMRlqNPMwglT3hvDmsAt +9Lf9pdmhERUlHhD8bj8mDaBj2Aqv7f6VRJaYZqP403pKKQexuqcn80mtjkSAPFkN +Cj7BVt/RXm5uoxDTnfi26RF9F6yNDEJ7UU9+peBr99aazF/fTgW/1GcMkQnum8uV +c257YgaWmjK9uB0Y2r2VxS0G +-----END PRIVATE KEY----- \ No newline at end of file diff --git a/.github/testdata/ssl.pem b/.github/testdata/ssl.pem new file mode 100644 index 0000000..4cf41c9 --- /dev/null +++ b/.github/testdata/ssl.pem @@ -0,0 +1,17 @@ +-----BEGIN CERTIFICATE----- +MIICujCCAaKgAwIBAgIJAMbXnKZ/cikUMA0GCSqGSIb3DQEBCwUAMBUxEzARBgNV +BAMTCnVidW50dS5uYW4wHhcNMTUwMjA0MDgwMTM5WhcNMjUwMjAxMDgwMTM5WjAV +MRMwEQYDVQQDEwp1YnVudHUubmFuMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB +CgKCAQEA+CELrALPDyXZxt5lEbfwF7YAvnHqizmrSePSSRNVT05DAMvqBNX9V75D +K2LB6pg3+hllc4FV68i+FMKtv5yUpuenXYTeeZyPKEjd3bcsFAfP0oXpRDe955Te ++z3g/bZejZLD8Fmiq6satBZWm0T2UkAn5oGW4Q1fEmvJnwpBVNBtJYrepCxnHgij +L5lvvQc+3m7GJlXZlTMZnyCUrRQ+OJVhU3VHOuViEihHVthC3FHn29Mzi8PtDwm1 +xRiR+ceZLZLFvPgQZNh5IBnkES/6jwnHLYW0nDtFYDY98yd2WS9Dm0gwG7zQxvOY +6HjYwzauQ0/wQGdGzkmxBbIfn/QQMwIDAQABow0wCzAJBgNVHRMEAjAAMA0GCSqG +SIb3DQEBCwUAA4IBAQBQjKm/4KN/iTgXbLTL3i7zaxYXFLXsnT1tF+ay4VA8aj98 +L3JwRTciZ3A5iy/W4VSCt3eASwOaPWHKqDBB5RTtL73LoAqsWmO3APOGQAbixcQ2 +45GXi05OKeyiYRi1Nvq7Unv9jUkRDHUYVPZVSAjCpsXzPhFkmZoTRxmx5l0ZF7Li +K91lI5h+eFq0dwZwrmlPambyh1vQUi70VHv8DNToVU29kel7YLbxGbuqETfhrcy6 +X+Mha6RYITkAn5FqsZcKMsc9eYGEF4l3XV+oS7q6xfTxktYJMFTI18J0lQ2Lv/CI +whdMnYGntDQBE/iFCrJEGNsKGc38796GBOb5j+zd +-----END CERTIFICATE----- \ No newline at end of file diff --git a/.github/testdata/template-invalid.html b/.github/testdata/template-invalid.html new file mode 100644 index 0000000..ac4f6d1 --- /dev/null +++ b/.github/testdata/template-invalid.html @@ -0,0 +1 @@ +

{{.Title}

diff --git a/.github/testdata/template.tmpl b/.github/testdata/template.tmpl new file mode 100644 index 0000000..ab6e560 --- /dev/null +++ b/.github/testdata/template.tmpl @@ -0,0 +1 @@ +

{{.Title}} {{.Summary}}

\ No newline at end of file diff --git a/.github/testdata/template/hello_world.gohtml b/.github/testdata/template/hello_world.gohtml new file mode 100644 index 0000000..d47d8c5 --- /dev/null +++ b/.github/testdata/template/hello_world.gohtml @@ -0,0 +1 @@ +

Hello {{ .Name }}!

\ No newline at end of file diff --git a/.github/testdata/testRoutes.json b/.github/testdata/testRoutes.json new file mode 100644 index 0000000..0503d18 --- /dev/null +++ b/.github/testdata/testRoutes.json @@ -0,0 +1,1916 @@ +{ + "test_routes": [{ + "method": "GET", + "path": "/authorizations" + }, + { + "method": "GET", + "path": "/authorizations/1337" + }, + { + "method": "POST", + "path": "/authorizations" + }, + { + "method": "PUT", + "path": "/authorizations/clients/inf1nd873nf8912g9t" + }, + { + "method": "PATCH", + "path": "/authorizations/1337" + }, + { + "method": "DELETE", + "path": "/authorizations/1337" + }, + { + "method": "GET", + "path": "/applications/2nds981mng6azl127y/tokens/sn108hbe1geheibf13f" + }, + { + "method": "DELETE", + "path": "/applications/2nds981mng6azl127y/tokens" + }, + { + "method": "DELETE", + "path": "/applications/2nds981mng6azl127y/tokens/sn108hbe1geheibf13f" + }, + { + "method": "GET", + "path": "/events" + }, + { + "method": "GET", + "path": "/repos/fenny/fiber/events" + }, + { + "method": "GET", + "path": "/networks/fenny/fiber/events" + }, + { + "method": "GET", + "path": "/orgs/gofiber/events" + }, + { + "method": "GET", + "path": "/users/fenny/received_events" + }, + { + "method": "GET", + "path": "/users/fenny/received_events/public" + }, + { + "method": "GET", + "path": "/users/fenny/events" + }, + { + "method": "GET", + "path": "/users/fenny/events/public" + }, + { + "method": "GET", + "path": "/users/fenny/events/orgs/gofiber" + }, + { + "method": "GET", + "path": "/feeds" + }, + { + "method": "GET", + "path": "/notifications" + }, + { + "method": "GET", + "path": "/repos/fenny/fiber/notifications" + }, + { + "method": "PUT", + "path": "/notifications" + }, + { + "method": "PUT", + "path": "/repos/fenny/fiber/notifications" + }, + { + "method": "GET", + "path": "/notifications/threads/1337" + }, + { + "method": "PATCH", + "path": "/notifications/threads/1337" + }, + { + "method": "GET", + "path": "/notifications/threads/1337/subscription" + }, + { + "method": "PUT", + "path": "/notifications/threads/1337/subscription" + }, + { + "method": "DELETE", + "path": "/notifications/threads/1337/subscription" + }, + { + "method": "GET", + "path": "/repos/fenny/fiber/stargazers" + }, + { + "method": "GET", + "path": "/users/fenny/starred" + }, + { + "method": "GET", + "path": "/user/starred" + }, + { + "method": "GET", + "path": "/user/starred/fenny/fiber" + }, + { + "method": "PUT", + "path": "/user/starred/fenny/fiber" + }, + { + "method": "DELETE", + "path": "/user/starred/fenny/fiber" + }, + { + "method": "GET", + "path": "/repos/fenny/fiber/subscribers" + }, + { + "method": "GET", + "path": "/users/fenny/subscriptions" + }, + { + "method": "GET", + "path": "/user/subscriptions" + }, + { + "method": "GET", + "path": "/repos/fenny/fiber/subscription" + }, + { + "method": "PUT", + "path": "/repos/fenny/fiber/subscription" + }, + { + "method": "DELETE", + "path": "/repos/fenny/fiber/subscription" + }, + { + "method": "GET", + "path": "/user/subscriptions/fenny/fiber" + }, + { + "method": "PUT", + "path": "/user/subscriptions/fenny/fiber" + }, + { + "method": "DELETE", + "path": "/user/subscriptions/fenny/fiber" + }, + { + "method": "GET", + "path": "/users/fenny/gists" + }, + { + "method": "GET", + "path": "/gists" + }, + { + "method": "GET", + "path": "/gists/public" + }, + { + "method": "GET", + "path": "/gists/starred" + }, + { + "method": "GET", + "path": "/gists/1337" + }, + { + "method": "POST", + "path": "/gists" + }, + { + "method": "PATCH", + "path": "/gists/1337" + }, + { + "method": "PUT", + "path": "/gists/1337/star" + }, + { + "method": "DELETE", + "path": "/gists/1337/star" + }, + { + "method": "GET", + "path": "/gists/1337/star" + }, + { + "method": "POST", + "path": "/gists/1337/forks" + }, + { + "method": "DELETE", + "path": "/gists/1337" + }, + { + "method": "GET", + "path": "/repos/fenny/fiber/git/blobs/v948b24g98ubngw9082bn02giub" + }, + { + "method": "POST", + "path": "/repos/fenny/fiber/git/blobs" + }, + { + "method": "GET", + "path": "/repos/fenny/fiber/git/commits/v948b24g98ubngw9082bn02giub" + }, + { + "method": "POST", + "path": "/repos/fenny/fiber/git/commits" + }, + { + "method": "GET", + "path": "/repos/fenny/fiber/git/refs/im/a/wildcard" + }, + { + "method": "GET", + "path": "/repos/fenny/fiber/git/refs" + }, + { + "method": "POST", + "path": "/repos/fenny/fiber/git/refs" + }, + { + "method": "PATCH", + "path": "/repos/fenny/fiber/git/refs/im/a/wildcard" + }, + { + "method": "DELETE", + "path": "/repos/fenny/fiber/git/refs/im/a/wildcard" + }, + { + "method": "GET", + "path": "/repos/fenny/fiber/git/tags/v948b24g98ubngw9082bn02giub" + }, + { + "method": "POST", + "path": "/repos/fenny/fiber/git/tags" + }, + { + "method": "GET", + "path": "/repos/fenny/fiber/git/trees/v948b24g98ubngw9082bn02giub" + }, + { + "method": "POST", + "path": "/repos/fenny/fiber/git/trees" + }, + { + "method": "GET", + "path": "/issues" + }, + { + "method": "GET", + "path": "/user/issues" + }, + { + "method": "GET", + "path": "/orgs/gofiber/issues" + }, + { + "method": "GET", + "path": "/repos/fenny/fiber/issues" + }, + { + "method": "GET", + "path": "/repos/fenny/fiber/issues/1000" + }, + { + "method": "POST", + "path": "/repos/fenny/fiber/issues" + }, + { + "method": "PATCH", + "path": "/repos/fenny/fiber/issues/1000" + }, + { + "method": "GET", + "path": "/repos/fenny/fiber/assignees" + }, + { + "method": "GET", + "path": "/repos/fenny/fiber/assignees/nic" + }, + { + "method": "GET", + "path": "/repos/fenny/fiber/issues/1000/comments" + }, + { + "method": "GET", + "path": "/repos/fenny/fiber/issues/comments" + }, + { + "method": "GET", + "path": "/repos/fenny/fiber/issues/comments/1337" + }, + { + "method": "POST", + "path": "/repos/fenny/fiber/issues/1000/comments" + }, + { + "method": "PATCH", + "path": "/repos/fenny/fiber/issues/comments/1337" + }, + { + "method": "DELETE", + "path": "/repos/fenny/fiber/issues/comments/1337" + }, + { + "method": "GET", + "path": "/repos/fenny/fiber/issues/1000/events" + }, + { + "method": "GET", + "path": "/repos/fenny/fiber/issues/events" + }, + { + "method": "GET", + "path": "/repos/fenny/fiber/issues/events/1337" + }, + { + "method": "GET", + "path": "/repos/fenny/fiber/labels" + }, + { + "method": "GET", + "path": "/repos/fenny/fiber/labels/john" + }, + { + "method": "POST", + "path": "/repos/fenny/fiber/labels" + }, + { + "method": "PATCH", + "path": "/repos/fenny/fiber/labels/john" + }, + { + "method": "DELETE", + "path": "/repos/fenny/fiber/labels/john" + }, + { + "method": "GET", + "path": "/repos/fenny/fiber/issues/1000/labels" + }, + { + "method": "POST", + "path": "/repos/fenny/fiber/issues/1000/labels" + }, + { + "method": "DELETE", + "path": "/repos/fenny/fiber/issues/1000/labels/john" + }, + { + "method": "PUT", + "path": "/repos/fenny/fiber/issues/1000/labels" + }, + { + "method": "DELETE", + "path": "/repos/fenny/fiber/issues/1000/labels" + }, + { + "method": "GET", + "path": "/repos/fenny/fiber/milestones/1000/labels" + }, + { + "method": "GET", + "path": "/repos/fenny/fiber/milestones" + }, + { + "method": "GET", + "path": "/repos/fenny/fiber/milestones/1000" + }, + { + "method": "POST", + "path": "/repos/fenny/fiber/milestones" + }, + { + "method": "PATCH", + "path": "/repos/fenny/fiber/milestones/1000" + }, + { + "method": "DELETE", + "path": "/repos/fenny/fiber/milestones/1000" + }, + { + "method": "GET", + "path": "/emojis" + }, + { + "method": "GET", + "path": "/gitignore/templates" + }, + { + "method": "GET", + "path": "/gitignore/templates/john" + }, + { + "method": "POST", + "path": "/markdown" + }, + { + "method": "POST", + "path": "/markdown/raw" + }, + { + "method": "GET", + "path": "/meta" + }, + { + "method": "GET", + "path": "/rate_limit" + }, + { + "method": "GET", + "path": "/users/fenny/orgs" + }, + { + "method": "GET", + "path": "/user/orgs" + }, + { + "method": "GET", + "path": "/orgs/gofiber" + }, + { + "method": "PATCH", + "path": "/orgs/gofiber" + }, + { + "method": "GET", + "path": "/orgs/gofiber/members" + }, + { + "method": "GET", + "path": "/orgs/gofiber/members/fenny" + }, + { + "method": "DELETE", + "path": "/orgs/gofiber/members/fenny" + }, + { + "method": "GET", + "path": "/orgs/gofiber/public_members" + }, + { + "method": "GET", + "path": "/orgs/gofiber/public_members/fenny" + }, + { + "method": "PUT", + "path": "/orgs/gofiber/public_members/fenny" + }, + { + "method": "DELETE", + "path": "/orgs/gofiber/public_members/fenny" + }, + { + "method": "GET", + "path": "/orgs/gofiber/teams" + }, + { + "method": "GET", + "path": "/teams/1337" + }, + { + "method": "POST", + "path": "/orgs/gofiber/teams" + }, + { + "method": "PATCH", + "path": "/teams/1337" + }, + { + "method": "DELETE", + "path": "/teams/1337" + }, + { + "method": "GET", + "path": "/teams/1337/members" + }, + { + "method": "GET", + "path": "/teams/1337/members/fenny" + }, + { + "method": "PUT", + "path": "/teams/1337/members/fenny" + }, + { + "method": "DELETE", + "path": "/teams/1337/members/fenny" + }, + { + "method": "GET", + "path": "/teams/1337/repos" + }, + { + "method": "GET", + "path": "/teams/1337/repos/fenny/fiber" + }, + { + "method": "PUT", + "path": "/teams/1337/repos/fenny/fiber" + }, + { + "method": "DELETE", + "path": "/teams/1337/repos/fenny/fiber" + }, + { + "method": "GET", + "path": "/user/teams" + }, + { + "method": "GET", + "path": "/repos/fenny/fiber/pulls" + }, + { + "method": "GET", + "path": "/repos/fenny/fiber/pulls/1000" + }, + { + "method": "POST", + "path": "/repos/fenny/fiber/pulls" + }, + { + "method": "PATCH", + "path": "/repos/fenny/fiber/pulls/1000" + }, + { + "method": "GET", + "path": "/repos/fenny/fiber/pulls/1000/commits" + }, + { + "method": "GET", + "path": "/repos/fenny/fiber/pulls/1000/files" + }, + { + "method": "GET", + "path": "/repos/fenny/fiber/pulls/1000/merge" + }, + { + "method": "PUT", + "path": "/repos/fenny/fiber/pulls/1000/merge" + }, + { + "method": "GET", + "path": "/repos/fenny/fiber/pulls/1000/comments" + }, + { + "method": "GET", + "path": "/repos/fenny/fiber/pulls/comments" + }, + { + "method": "GET", + "path": "/repos/fenny/fiber/pulls/comments/1000" + }, + { + "method": "PUT", + "path": "/repos/fenny/fiber/pulls/1000/comments" + }, + { + "method": "PATCH", + "path": "/repos/fenny/fiber/pulls/comments/1000" + }, + { + "method": "DELETE", + "path": "/repos/fenny/fiber/pulls/comments/1000" + }, + { + "method": "GET", + "path": "/user/repos" + }, + { + "method": "GET", + "path": "/users/fenny/repos" + }, + { + "method": "GET", + "path": "/orgs/gofiber/repos" + }, + { + "method": "GET", + "path": "/repositories" + }, + { + "method": "POST", + "path": "/user/repos" + }, + { + "method": "POST", + "path": "/orgs/gofiber/repos" + }, + { + "method": "GET", + "path": "/repos/fenny/fiber" + }, + { + "method": "PATCH", + "path": "/repos/fenny/fiber" + }, + { + "method": "GET", + "path": "/repos/fenny/fiber/contributors" + }, + { + "method": "GET", + "path": "/repos/fenny/fiber/languages" + }, + { + "method": "GET", + "path": "/repos/fenny/fiber/teams" + }, + { + "method": "GET", + "path": "/repos/fenny/fiber/tags" + }, + { + "method": "GET", + "path": "/repos/fenny/fiber/branches" + }, + { + "method": "GET", + "path": "/repos/fenny/fiber/branches/master" + }, + { + "method": "DELETE", + "path": "/repos/fenny/fiber" + }, + { + "method": "GET", + "path": "/repos/fenny/fiber/collaborators" + }, + { + "method": "GET", + "path": "/repos/fenny/fiber/collaborators/fenny" + }, + { + "method": "PUT", + "path": "/repos/fenny/fiber/collaborators/fenny" + }, + { + "method": "DELETE", + "path": "/repos/fenny/fiber/collaborators/fenny" + }, + { + "method": "GET", + "path": "/repos/fenny/fiber/comments" + }, + { + "method": "GET", + "path": "/repos/fenny/fiber/commits/v948b24g98ubngw9082bn02giub/comments" + }, + { + "method": "POST", + "path": "/repos/fenny/fiber/commits/v948b24g98ubngw9082bn02giub/comments" + }, + { + "method": "GET", + "path": "/repos/fenny/fiber/comments/1337" + }, + { + "method": "PATCH", + "path": "/repos/fenny/fiber/comments/1337" + }, + { + "method": "DELETE", + "path": "/repos/fenny/fiber/comments/1337" + }, + { + "method": "GET", + "path": "/repos/fenny/fiber/commits" + }, + { + "method": "GET", + "path": "/repos/fenny/fiber/commits/v948b24g98ubngw9082bn02giub" + }, + { + "method": "GET", + "path": "/repos/fenny/fiber/readme" + }, + { + "method": "GET", + "path": "/repos/fenny/fiber/contents/im/a/wildcard" + }, + { + "method": "PUT", + "path": "/repos/fenny/fiber/contents/im/a/wildcard" + }, + { + "method": "DELETE", + "path": "/repos/fenny/fiber/contents/im/a/wildcard" + }, + { + "method": "GET", + "path": "/repos/fenny/fiber/gzip/google" + }, + { + "method": "GET", + "path": "/repos/fenny/fiber/keys" + }, + { + "method": "GET", + "path": "/repos/fenny/fiber/keys/1337" + }, + { + "method": "POST", + "path": "/repos/fenny/fiber/keys" + }, + { + "method": "PATCH", + "path": "/repos/fenny/fiber/keys/1337" + }, + { + "method": "DELETE", + "path": "/repos/fenny/fiber/keys/1337" + }, + { + "method": "GET", + "path": "/repos/fenny/fiber/downloads" + }, + { + "method": "GET", + "path": "/repos/fenny/fiber/downloads/1337" + }, + { + "method": "DELETE", + "path": "/repos/fenny/fiber/downloads/1337" + }, + { + "method": "GET", + "path": "/repos/fenny/fiber/forks" + }, + { + "method": "POST", + "path": "/repos/fenny/fiber/forks" + }, + { + "method": "GET", + "path": "/repos/fenny/fiber/hooks" + }, + { + "method": "GET", + "path": "/repos/fenny/fiber/hooks/1337" + }, + { + "method": "POST", + "path": "/repos/fenny/fiber/hooks" + }, + { + "method": "PATCH", + "path": "/repos/fenny/fiber/hooks/1337" + }, + { + "method": "POST", + "path": "/repos/fenny/fiber/hooks/1337/tests" + }, + { + "method": "DELETE", + "path": "/repos/fenny/fiber/hooks/1337" + }, + { + "method": "POST", + "path": "/repos/fenny/fiber/merges" + }, + { + "method": "GET", + "path": "/repos/fenny/fiber/releases" + }, + { + "method": "GET", + "path": "/repos/fenny/fiber/releases/1337" + }, + { + "method": "POST", + "path": "/repos/fenny/fiber/releases" + }, + { + "method": "PATCH", + "path": "/repos/fenny/fiber/releases/1337" + }, + { + "method": "DELETE", + "path": "/repos/fenny/fiber/releases/1337" + }, + { + "method": "GET", + "path": "/repos/fenny/fiber/releases/1337/assets" + }, + { + "method": "GET", + "path": "/repos/fenny/fiber/stats/contributors" + }, + { + "method": "GET", + "path": "/repos/fenny/fiber/stats/commit_activity" + }, + { + "method": "GET", + "path": "/repos/fenny/fiber/stats/code_frequency" + }, + { + "method": "GET", + "path": "/repos/fenny/fiber/stats/participation" + }, + { + "method": "GET", + "path": "/repos/fenny/fiber/stats/punch_card" + }, + { + "method": "GET", + "path": "/repos/fenny/fiber/statuses/google" + }, + { + "method": "POST", + "path": "/repos/fenny/fiber/statuses/google" + }, + { + "method": "GET", + "path": "/search/repositories" + }, + { + "method": "GET", + "path": "/search/code" + }, + { + "method": "GET", + "path": "/search/issues" + }, + { + "method": "GET", + "path": "/search/users" + }, + { + "method": "GET", + "path": "/legacy/issues/search/fenny/fibersitory/locked/finish" + }, + { + "method": "GET", + "path": "/legacy/repos/search/finish" + }, + { + "method": "GET", + "path": "/legacy/user/search/finish" + }, + { + "method": "GET", + "path": "/legacy/user/email/info@gofiber.io" + }, + { + "method": "GET", + "path": "/users/fenny" + }, + { + "method": "GET", + "path": "/user" + }, + { + "method": "PATCH", + "path": "/user" + }, + { + "method": "GET", + "path": "/users" + }, + { + "method": "GET", + "path": "/user/emails" + }, + { + "method": "POST", + "path": "/user/emails" + }, + { + "method": "DELETE", + "path": "/user/emails" + }, + { + "method": "GET", + "path": "/users/fenny/followers" + }, + { + "method": "GET", + "path": "/user/followers" + }, + { + "method": "GET", + "path": "/users/fenny/following" + }, + { + "method": "GET", + "path": "/user/following" + }, + { + "method": "GET", + "path": "/user/following/fenny" + }, + { + "method": "GET", + "path": "/users/fenny/following/renan" + }, + { + "method": "PUT", + "path": "/user/following/fenny" + }, + { + "method": "DELETE", + "path": "/user/following/fenny" + }, + { + "method": "GET", + "path": "/users/fenny/keys" + }, + { + "method": "GET", + "path": "/user/keys" + }, + { + "method": "GET", + "path": "/user/keys/1337" + }, + { + "method": "POST", + "path": "/user/keys" + }, + { + "method": "PATCH", + "path": "/user/keys/1337" + }, + { + "method": "DELETE", + "path": "/user/keys/1337" + } + ], + "github_api": [{ + "method": "GET", + "path": "/authorizations" + }, + { + "method": "GET", + "path": "/authorizations/:id" + }, + { + "method": "POST", + "path": "/authorizations" + }, + { + "method": "PUT", + "path": "/authorizations/clients/:client_id" + }, + { + "method": "PATCH", + "path": "/authorizations/:id" + }, + { + "method": "DELETE", + "path": "/authorizations/:id" + }, + { + "method": "GET", + "path": "/applications/:client_id/tokens/:access_token" + }, + { + "method": "DELETE", + "path": "/applications/:client_id/tokens" + }, + { + "method": "DELETE", + "path": "/applications/:client_id/tokens/:access_token" + }, + { + "method": "GET", + "path": "/events" + }, + { + "method": "GET", + "path": "/repos/:owner/:repo/events" + }, + { + "method": "GET", + "path": "/networks/:owner/:repo/events" + }, + { + "method": "GET", + "path": "/orgs/:org/events" + }, + { + "method": "GET", + "path": "/users/:user/received_events" + }, + { + "method": "GET", + "path": "/users/:user/received_events/public" + }, + { + "method": "GET", + "path": "/users/:user/events" + }, + { + "method": "GET", + "path": "/users/:user/events/public" + }, + { + "method": "GET", + "path": "/users/:user/events/orgs/:org" + }, + { + "method": "GET", + "path": "/feeds" + }, + { + "method": "GET", + "path": "/notifications" + }, + { + "method": "GET", + "path": "/repos/:owner/:repo/notifications" + }, + { + "method": "PUT", + "path": "/notifications" + }, + { + "method": "PUT", + "path": "/repos/:owner/:repo/notifications" + }, + { + "method": "GET", + "path": "/notifications/threads/:id" + }, + { + "method": "PATCH", + "path": "/notifications/threads/:id" + }, + { + "method": "GET", + "path": "/notifications/threads/:id/subscription" + }, + { + "method": "PUT", + "path": "/notifications/threads/:id/subscription" + }, + { + "method": "DELETE", + "path": "/notifications/threads/:id/subscription" + }, + { + "method": "GET", + "path": "/repos/:owner/:repo/stargazers" + }, + { + "method": "GET", + "path": "/users/:user/starred" + }, + { + "method": "GET", + "path": "/user/starred" + }, + { + "method": "GET", + "path": "/user/starred/:owner/:repo" + }, + { + "method": "PUT", + "path": "/user/starred/:owner/:repo" + }, + { + "method": "DELETE", + "path": "/user/starred/:owner/:repo" + }, + { + "method": "GET", + "path": "/repos/:owner/:repo/subscribers" + }, + { + "method": "GET", + "path": "/users/:user/subscriptions" + }, + { + "method": "GET", + "path": "/user/subscriptions" + }, + { + "method": "GET", + "path": "/repos/:owner/:repo/subscription" + }, + { + "method": "PUT", + "path": "/repos/:owner/:repo/subscription" + }, + { + "method": "DELETE", + "path": "/repos/:owner/:repo/subscription" + }, + { + "method": "GET", + "path": "/user/subscriptions/:owner/:repo" + }, + { + "method": "PUT", + "path": "/user/subscriptions/:owner/:repo" + }, + { + "method": "DELETE", + "path": "/user/subscriptions/:owner/:repo" + }, + { + "method": "GET", + "path": "/users/:user/gists" + }, + { + "method": "GET", + "path": "/gists" + }, + { + "method": "GET", + "path": "/gists/public" + }, + { + "method": "GET", + "path": "/gists/starred" + }, + { + "method": "GET", + "path": "/gists/:id" + }, + { + "method": "POST", + "path": "/gists" + }, + { + "method": "PATCH", + "path": "/gists/:id" + }, + { + "method": "PUT", + "path": "/gists/:id/star" + }, + { + "method": "DELETE", + "path": "/gists/:id/star" + }, + { + "method": "GET", + "path": "/gists/:id/star" + }, + { + "method": "POST", + "path": "/gists/:id/forks" + }, + { + "method": "DELETE", + "path": "/gists/:id" + }, + { + "method": "GET", + "path": "/repos/:owner/:repo/git/blobs/:sha" + }, + { + "method": "POST", + "path": "/repos/:owner/:repo/git/blobs" + }, + { + "method": "GET", + "path": "/repos/:owner/:repo/git/commits/:sha" + }, + { + "method": "POST", + "path": "/repos/:owner/:repo/git/commits" + }, + { + "method": "GET", + "path": "/repos/:owner/:repo/git/refs/*" + }, + { + "method": "GET", + "path": "/repos/:owner/:repo/git/refs" + }, + { + "method": "POST", + "path": "/repos/:owner/:repo/git/refs" + }, + { + "method": "PATCH", + "path": "/repos/:owner/:repo/git/refs/*" + }, + { + "method": "DELETE", + "path": "/repos/:owner/:repo/git/refs/*" + }, + { + "method": "GET", + "path": "/repos/:owner/:repo/git/tags/:sha" + }, + { + "method": "POST", + "path": "/repos/:owner/:repo/git/tags" + }, + { + "method": "GET", + "path": "/repos/:owner/:repo/git/trees/:sha" + }, + { + "method": "POST", + "path": "/repos/:owner/:repo/git/trees" + }, + { + "method": "GET", + "path": "/issues" + }, + { + "method": "GET", + "path": "/user/issues" + }, + { + "method": "GET", + "path": "/orgs/:org/issues" + }, + { + "method": "GET", + "path": "/repos/:owner/:repo/issues" + }, + { + "method": "GET", + "path": "/repos/:owner/:repo/issues/:number" + }, + { + "method": "POST", + "path": "/repos/:owner/:repo/issues" + }, + { + "method": "PATCH", + "path": "/repos/:owner/:repo/issues/:number" + }, + { + "method": "GET", + "path": "/repos/:owner/:repo/assignees" + }, + { + "method": "GET", + "path": "/repos/:owner/:repo/assignees/:assignee" + }, + { + "method": "GET", + "path": "/repos/:owner/:repo/issues/:number/comments" + }, + { + "method": "GET", + "path": "/repos/:owner/:repo/issues/comments" + }, + { + "method": "GET", + "path": "/repos/:owner/:repo/issues/comments/:id" + }, + { + "method": "POST", + "path": "/repos/:owner/:repo/issues/:number/comments" + }, + { + "method": "PATCH", + "path": "/repos/:owner/:repo/issues/comments/:id" + }, + { + "method": "DELETE", + "path": "/repos/:owner/:repo/issues/comments/:id" + }, + { + "method": "GET", + "path": "/repos/:owner/:repo/issues/:number/events" + }, + { + "method": "GET", + "path": "/repos/:owner/:repo/issues/events" + }, + { + "method": "GET", + "path": "/repos/:owner/:repo/issues/events/:id" + }, + { + "method": "GET", + "path": "/repos/:owner/:repo/labels" + }, + { + "method": "GET", + "path": "/repos/:owner/:repo/labels/:name" + }, + { + "method": "POST", + "path": "/repos/:owner/:repo/labels" + }, + { + "method": "PATCH", + "path": "/repos/:owner/:repo/labels/:name" + }, + { + "method": "DELETE", + "path": "/repos/:owner/:repo/labels/:name" + }, + { + "method": "GET", + "path": "/repos/:owner/:repo/issues/:number/labels" + }, + { + "method": "POST", + "path": "/repos/:owner/:repo/issues/:number/labels" + }, + { + "method": "DELETE", + "path": "/repos/:owner/:repo/issues/:number/labels/:name" + }, + { + "method": "PUT", + "path": "/repos/:owner/:repo/issues/:number/labels" + }, + { + "method": "DELETE", + "path": "/repos/:owner/:repo/issues/:number/labels" + }, + { + "method": "GET", + "path": "/repos/:owner/:repo/milestones/:number/labels" + }, + { + "method": "GET", + "path": "/repos/:owner/:repo/milestones" + }, + { + "method": "GET", + "path": "/repos/:owner/:repo/milestones/:number" + }, + { + "method": "POST", + "path": "/repos/:owner/:repo/milestones" + }, + { + "method": "PATCH", + "path": "/repos/:owner/:repo/milestones/:number" + }, + { + "method": "DELETE", + "path": "/repos/:owner/:repo/milestones/:number" + }, + { + "method": "GET", + "path": "/emojis" + }, + { + "method": "GET", + "path": "/gitignore/templates" + }, + { + "method": "GET", + "path": "/gitignore/templates/:name" + }, + { + "method": "POST", + "path": "/markdown" + }, + { + "method": "POST", + "path": "/markdown/raw" + }, + { + "method": "GET", + "path": "/meta" + }, + { + "method": "GET", + "path": "/rate_limit" + }, + { + "method": "GET", + "path": "/users/:user/orgs" + }, + { + "method": "GET", + "path": "/user/orgs" + }, + { + "method": "GET", + "path": "/orgs/:org" + }, + { + "method": "PATCH", + "path": "/orgs/:org" + }, + { + "method": "GET", + "path": "/orgs/:org/members" + }, + { + "method": "GET", + "path": "/orgs/:org/members/:user" + }, + { + "method": "DELETE", + "path": "/orgs/:org/members/:user" + }, + { + "method": "GET", + "path": "/orgs/:org/public_members" + }, + { + "method": "GET", + "path": "/orgs/:org/public_members/:user" + }, + { + "method": "PUT", + "path": "/orgs/:org/public_members/:user" + }, + { + "method": "DELETE", + "path": "/orgs/:org/public_members/:user" + }, + { + "method": "GET", + "path": "/orgs/:org/teams" + }, + { + "method": "GET", + "path": "/teams/:id" + }, + { + "method": "POST", + "path": "/orgs/:org/teams" + }, + { + "method": "PATCH", + "path": "/teams/:id" + }, + { + "method": "DELETE", + "path": "/teams/:id" + }, + { + "method": "GET", + "path": "/teams/:id/members" + }, + { + "method": "GET", + "path": "/teams/:id/members/:user" + }, + { + "method": "PUT", + "path": "/teams/:id/members/:user" + }, + { + "method": "DELETE", + "path": "/teams/:id/members/:user" + }, + { + "method": "GET", + "path": "/teams/:id/repos" + }, + { + "method": "GET", + "path": "/teams/:id/repos/:owner/:repo" + }, + { + "method": "PUT", + "path": "/teams/:id/repos/:owner/:repo" + }, + { + "method": "DELETE", + "path": "/teams/:id/repos/:owner/:repo" + }, + { + "method": "GET", + "path": "/user/teams" + }, + { + "method": "GET", + "path": "/repos/:owner/:repo/pulls" + }, + { + "method": "GET", + "path": "/repos/:owner/:repo/pulls/:number" + }, + { + "method": "POST", + "path": "/repos/:owner/:repo/pulls" + }, + { + "method": "PATCH", + "path": "/repos/:owner/:repo/pulls/:number" + }, + { + "method": "GET", + "path": "/repos/:owner/:repo/pulls/:number/commits" + }, + { + "method": "GET", + "path": "/repos/:owner/:repo/pulls/:number/files" + }, + { + "method": "GET", + "path": "/repos/:owner/:repo/pulls/:number/merge" + }, + { + "method": "PUT", + "path": "/repos/:owner/:repo/pulls/:number/merge" + }, + { + "method": "GET", + "path": "/repos/:owner/:repo/pulls/:number/comments" + }, + { + "method": "GET", + "path": "/repos/:owner/:repo/pulls/comments" + }, + { + "method": "GET", + "path": "/repos/:owner/:repo/pulls/comments/:number" + }, + { + "method": "PUT", + "path": "/repos/:owner/:repo/pulls/:number/comments" + }, + { + "method": "PATCH", + "path": "/repos/:owner/:repo/pulls/comments/:number" + }, + { + "method": "DELETE", + "path": "/repos/:owner/:repo/pulls/comments/:number" + }, + { + "method": "GET", + "path": "/user/repos" + }, + { + "method": "GET", + "path": "/users/:user/repos" + }, + { + "method": "GET", + "path": "/orgs/:org/repos" + }, + { + "method": "GET", + "path": "/repositories" + }, + { + "method": "POST", + "path": "/user/repos" + }, + { + "method": "POST", + "path": "/orgs/:org/repos" + }, + { + "method": "GET", + "path": "/repos/:owner/:repo" + }, + { + "method": "PATCH", + "path": "/repos/:owner/:repo" + }, + { + "method": "GET", + "path": "/repos/:owner/:repo/contributors" + }, + { + "method": "GET", + "path": "/repos/:owner/:repo/languages" + }, + { + "method": "GET", + "path": "/repos/:owner/:repo/teams" + }, + { + "method": "GET", + "path": "/repos/:owner/:repo/tags" + }, + { + "method": "GET", + "path": "/repos/:owner/:repo/branches" + }, + { + "method": "GET", + "path": "/repos/:owner/:repo/branches/:branch" + }, + { + "method": "DELETE", + "path": "/repos/:owner/:repo" + }, + { + "method": "GET", + "path": "/repos/:owner/:repo/collaborators" + }, + { + "method": "GET", + "path": "/repos/:owner/:repo/collaborators/:user" + }, + { + "method": "PUT", + "path": "/repos/:owner/:repo/collaborators/:user" + }, + { + "method": "DELETE", + "path": "/repos/:owner/:repo/collaborators/:user" + }, + { + "method": "GET", + "path": "/repos/:owner/:repo/comments" + }, + { + "method": "GET", + "path": "/repos/:owner/:repo/commits/:sha/comments" + }, + { + "method": "POST", + "path": "/repos/:owner/:repo/commits/:sha/comments" + }, + { + "method": "GET", + "path": "/repos/:owner/:repo/comments/:id" + }, + { + "method": "PATCH", + "path": "/repos/:owner/:repo/comments/:id" + }, + { + "method": "DELETE", + "path": "/repos/:owner/:repo/comments/:id" + }, + { + "method": "GET", + "path": "/repos/:owner/:repo/commits" + }, + { + "method": "GET", + "path": "/repos/:owner/:repo/commits/:sha" + }, + { + "method": "GET", + "path": "/repos/:owner/:repo/readme" + }, + { + "method": "GET", + "path": "/repos/:owner/:repo/contents/*" + }, + { + "method": "PUT", + "path": "/repos/:owner/:repo/contents/*" + }, + { + "method": "DELETE", + "path": "/repos/:owner/:repo/contents/*" + }, + { + "method": "GET", + "path": "/repos/:owner/:repo/:archive_format/:ref" + }, + { + "method": "GET", + "path": "/repos/:owner/:repo/keys" + }, + { + "method": "GET", + "path": "/repos/:owner/:repo/keys/:id" + }, + { + "method": "POST", + "path": "/repos/:owner/:repo/keys" + }, + { + "method": "PATCH", + "path": "/repos/:owner/:repo/keys/:id" + }, + { + "method": "DELETE", + "path": "/repos/:owner/:repo/keys/:id" + }, + { + "method": "GET", + "path": "/repos/:owner/:repo/downloads" + }, + { + "method": "GET", + "path": "/repos/:owner/:repo/downloads/:id" + }, + { + "method": "DELETE", + "path": "/repos/:owner/:repo/downloads/:id" + }, + { + "method": "GET", + "path": "/repos/:owner/:repo/forks" + }, + { + "method": "POST", + "path": "/repos/:owner/:repo/forks" + }, + { + "method": "GET", + "path": "/repos/:owner/:repo/hooks" + }, + { + "method": "GET", + "path": "/repos/:owner/:repo/hooks/:id" + }, + { + "method": "POST", + "path": "/repos/:owner/:repo/hooks" + }, + { + "method": "PATCH", + "path": "/repos/:owner/:repo/hooks/:id" + }, + { + "method": "POST", + "path": "/repos/:owner/:repo/hooks/:id/tests" + }, + { + "method": "DELETE", + "path": "/repos/:owner/:repo/hooks/:id" + }, + { + "method": "POST", + "path": "/repos/:owner/:repo/merges" + }, + { + "method": "GET", + "path": "/repos/:owner/:repo/releases" + }, + { + "method": "GET", + "path": "/repos/:owner/:repo/releases/:id" + }, + { + "method": "POST", + "path": "/repos/:owner/:repo/releases" + }, + { + "method": "PATCH", + "path": "/repos/:owner/:repo/releases/:id" + }, + { + "method": "DELETE", + "path": "/repos/:owner/:repo/releases/:id" + }, + { + "method": "GET", + "path": "/repos/:owner/:repo/releases/:id/assets" + }, + { + "method": "GET", + "path": "/repos/:owner/:repo/stats/contributors" + }, + { + "method": "GET", + "path": "/repos/:owner/:repo/stats/commit_activity" + }, + { + "method": "GET", + "path": "/repos/:owner/:repo/stats/code_frequency" + }, + { + "method": "GET", + "path": "/repos/:owner/:repo/stats/participation" + }, + { + "method": "GET", + "path": "/repos/:owner/:repo/stats/punch_card" + }, + { + "method": "GET", + "path": "/repos/:owner/:repo/statuses/:ref" + }, + { + "method": "POST", + "path": "/repos/:owner/:repo/statuses/:ref" + }, + { + "method": "GET", + "path": "/search/repositories" + }, + { + "method": "GET", + "path": "/search/code" + }, + { + "method": "GET", + "path": "/search/issues" + }, + { + "method": "GET", + "path": "/search/users" + }, + { + "method": "GET", + "path": "/legacy/issues/search/:owner/:repository/:state/:keyword" + }, + { + "method": "GET", + "path": "/legacy/repos/search/:keyword" + }, + { + "method": "GET", + "path": "/legacy/user/search/:keyword" + }, + { + "method": "GET", + "path": "/legacy/user/email/:email" + }, + { + "method": "GET", + "path": "/users/:user" + }, + { + "method": "GET", + "path": "/user" + }, + { + "method": "PATCH", + "path": "/user" + }, + { + "method": "GET", + "path": "/users" + }, + { + "method": "GET", + "path": "/user/emails" + }, + { + "method": "POST", + "path": "/user/emails" + }, + { + "method": "DELETE", + "path": "/user/emails" + }, + { + "method": "GET", + "path": "/users/:user/followers" + }, + { + "method": "GET", + "path": "/user/followers" + }, + { + "method": "GET", + "path": "/users/:user/following" + }, + { + "method": "GET", + "path": "/user/following" + }, + { + "method": "GET", + "path": "/user/following/:user" + }, + { + "method": "GET", + "path": "/users/:user/following/:target_user" + }, + { + "method": "PUT", + "path": "/user/following/:user" + }, + { + "method": "DELETE", + "path": "/user/following/:user" + }, + { + "method": "GET", + "path": "/users/:user/keys" + }, + { + "method": "GET", + "path": "/user/keys" + }, + { + "method": "GET", + "path": "/user/keys/:id" + }, + { + "method": "POST", + "path": "/user/keys" + }, + { + "method": "PATCH", + "path": "/user/keys/:id" + }, + { + "method": "DELETE", + "path": "/user/keys/:id" + } + ] +} diff --git a/.github/testdata2/bruh.tmpl b/.github/testdata2/bruh.tmpl new file mode 100644 index 0000000..28d75b4 --- /dev/null +++ b/.github/testdata2/bruh.tmpl @@ -0,0 +1 @@ +

I'm Bruh

\ No newline at end of file diff --git a/.github/workflows/auto-labeler.yml b/.github/workflows/auto-labeler.yml new file mode 100644 index 0000000..3a18ed8 --- /dev/null +++ b/.github/workflows/auto-labeler.yml @@ -0,0 +1,23 @@ +name: Auto labeler + +on: + issues: + types: [opened, edited, milestoned] + pull_request_target: + types: [opened] +permissions: + contents: read + issues: write + pull-requests: write + statuses: write + checks: write + +jobs: + labeler: + runs-on: ubuntu-latest + steps: + - name: Check Labels + id: labeler + uses: fuxingloh/multi-labeler@v2 + with: + github-token: ${{secrets.GITHUB_TOKEN}} diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml new file mode 100644 index 0000000..79e2c0d --- /dev/null +++ b/.github/workflows/benchmark.yml @@ -0,0 +1,50 @@ +on: + push: + branches: + - v2 + paths: + - "**" + - "!docs/**" + - "!**.md" + pull_request: + paths: + - "**" + - "!docs/**" + - "!**.md" + +name: Benchmark +jobs: + Compare: + runs-on: ubuntu-latest + steps: + - name: Fetch Repository + uses: actions/checkout@v4 + + - name: Install Go + uses: actions/setup-go@v5 + with: + # NOTE: Keep this in sync with the version from go.mod + go-version: "1.20.x" + + - name: Run Benchmark + run: set -o pipefail; go test ./... -benchmem -run=^$ -bench . | tee output.txt + + - name: Get Previous Benchmark Results + uses: actions/cache@v3 + with: + path: ./cache + key: ${{ runner.os }}-benchmark + + - name: Save Benchmark Results + uses: benchmark-action/github-action-benchmark@v1.20.3 + with: + tool: "go" + output-file-path: output.txt + github-token: ${{ secrets.BENCHMARK_TOKEN }} + benchmark-data-dir-path: "benchmarks" + fail-on-alert: true + comment-on-alert: ${{ github.event_name == 'push' || github.event_name == 'workflow_dispatch' }} + # Enable Job Summary for PRs - deactivated because of issues + #summary-always: ${{ github.event_name != 'push' && github.event_name != 'workflow_dispatch' }} + auto-push: ${{ github.event_name == 'push' || github.event_name == 'workflow_dispatch' }} + save-data-file: ${{ github.event_name == 'push' || github.event_name == 'workflow_dispatch' }} diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml new file mode 100644 index 0000000..0476abf --- /dev/null +++ b/.github/workflows/codeql-analysis.yml @@ -0,0 +1,61 @@ +name: "CodeQL" + +on: + push: + branches: + - v2 + paths: + - "**" + - "!docs/**" + - "!**.md" + pull_request: + paths: + - "**" + - "!docs/**" + - "!**.md" + schedule: + - cron: "0 3 * * 6" + +jobs: + analyse: + name: Analyse + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + # We must fetch at least the immediate parents so that if this is + # a pull request then we can checkout the head. + fetch-depth: 2 + + # If this run was triggered by a pull request event, then checkout + # the head of the pull request instead of the merge commit. + - run: git checkout HEAD^2 + if: ${{ github.event_name == 'pull_request' }} + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + # Override language selection by uncommenting this and choosing your languages + with: + languages: go + + # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). + # If this step fails, then you should remove it and run the build manually (see below) + - name: Autobuild + uses: github/codeql-action/autobuild@v3 + + # ℹ️ Command-line programs to run using the OS shell. + # 📚 https://git.io/JvXDl + + # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines + # and modify them (or add more) to build your code if your project + # uses a compiled language + + #- run: | + # make bootstrap + # make release + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml new file mode 100644 index 0000000..607e0fa --- /dev/null +++ b/.github/workflows/linter.yml @@ -0,0 +1,28 @@ +# Adapted from https://github.com/golangci/golangci-lint-action/blob/b56f6f529003f1c81d4d759be6bd5f10bf9a0fa0/README.md#how-to-use + +name: golangci-lint +on: + push: + branches: + - v2 + pull_request: +permissions: + contents: read + +jobs: + golangci: + name: lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-go@v5 + with: + # NOTE: Keep this in sync with the version from go.mod + go-version: "1.20.x" + + - name: golangci-lint + uses: golangci/golangci-lint-action@v3 + with: + # NOTE: Keep this in sync with the version from .golangci.yml + version: v1.51.0 diff --git a/.github/workflows/release-drafter.yml b/.github/workflows/release-drafter.yml new file mode 100644 index 0000000..3ac6e95 --- /dev/null +++ b/.github/workflows/release-drafter.yml @@ -0,0 +1,25 @@ +name: Release Drafter + +on: + push: + # branches to consider in the event; optional, defaults to all + branches: + - v2 + +jobs: + update_release_draft: + runs-on: ubuntu-latest + steps: + # (Optional) GitHub Enterprise requires GHE_HOST variable set + #- name: Set GHE_HOST + # run: | + # echo "GHE_HOST=${GITHUB_SERVER_URL##https:\/\/}" >> $GITHUB_ENV + + # Drafts your next Release notes as Pull Requests are merged into "master" + - uses: release-drafter/release-drafter@v5 + # (Optional) specify config name to use, relative to .github/. Default: release-drafter.yml + # with: + # config-name: my-config.yml + # disable-autolabeler: true + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/sync-docs.yml b/.github/workflows/sync-docs.yml new file mode 100644 index 0000000..4016883 --- /dev/null +++ b/.github/workflows/sync-docs.yml @@ -0,0 +1,33 @@ +name: "Sync docs" + +on: + push: + branches: + - v2 + paths: + - "docs/**" + release: + types: [published] + +jobs: + sync-docs: + if: false + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + ref: ${{ github.event.release.tag_name }} + fetch-depth: 2 + + - name: Setup Node.js environment + uses: actions/setup-node@v4 + with: + node-version: "18" + + - name: Sync docs + run: ./.github/scripts/sync_docs.sh + env: + EVENT: ${{ github.event_name }} + TAG_NAME: ${{ github.ref_name }} + TOKEN: ${{ secrets.DOC_SYNC_TOKEN }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..00566dd --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,37 @@ +on: + push: + branches: + - v2 + paths: + - "**" + - "!docs/**" + - "!**.md" + pull_request: + paths: + - "**" + - "!docs/**" + - "!**.md" + +name: Test +jobs: + Build: + strategy: + matrix: + go-version: [1.17.x, 1.18.x, 1.19.x, 1.20.x, 1.21.x, 1.22.x, 1.23.x] + platform: [ubuntu-latest, windows-latest, macos-latest] + runs-on: ${{ matrix.platform }} + steps: + - name: Fetch Repository + uses: actions/checkout@v4 + + - name: Install Go + uses: actions/setup-go@v5 + with: + go-version: ${{ matrix.go-version }} + + - name: Run Test + uses: nick-fields/retry@v2 + with: + max_attempts: 3 + timeout_minutes: 15 + command: go test ./... -v -race -count=1 diff --git a/.github/workflows/vulncheck.yml b/.github/workflows/vulncheck.yml new file mode 100644 index 0000000..f28ab5b --- /dev/null +++ b/.github/workflows/vulncheck.yml @@ -0,0 +1,37 @@ +name: Run govulncheck + +on: + push: + branches: + - v2 + paths: + - "**" + - "!docs/**" + - "!**.md" + pull_request: + paths: + - "**" + - "!docs/**" + - "!**.md" + +jobs: + govulncheck-check: + runs-on: ubuntu-latest + env: + GO111MODULE: on + steps: + - name: Fetch Repository + uses: actions/checkout@v4 + + - name: Install Go + uses: actions/setup-go@v5 + with: + go-version: "stable" + check-latest: true + cache: false + + - name: Install Govulncheck + run: go install golang.org/x/vuln/cmd/govulncheck@latest + + - name: Run Govulncheck + run: govulncheck ./... diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..119b111 --- /dev/null +++ b/.gitignore @@ -0,0 +1,30 @@ +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test +*.tmp + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# IDE files +.vscode +.DS_Store +.idea + +# Misc +*.fiber.gz +*.fasthttp.gz +*.pprof +*.workspace + +# Dependencies +/vendor/ +vendor/ +vendor +/Godeps/ \ No newline at end of file diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..c58d525 --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,197 @@ +# Created based on v1.51.0 +# NOTE: Keep this in sync with the version in .github/workflows/linter.yml + +run: + modules-download-mode: readonly + skip-dirs-use-default: false + skip-dirs: + - internal + +output: + sort-results: true + +linters-settings: + errcheck: + check-type-assertions: true + check-blank: true + disable-default-exclusions: true + + errchkjson: + report-no-exported: true + + exhaustive: + default-signifies-exhaustive: true + + forbidigo: + forbid: + - ^(fmt\.Print(|f|ln)|print|println)$ + - 'http\.Default(Client|Transport)' + # TODO: Eventually enable these patterns + # - 'time\.Sleep' + # - 'panic' + + gocritic: + disabled-checks: + - ifElseChain + + gofumpt: + module-path: github.com/gofiber/fiber + extra-rules: true + + gosec: + config: + global: + audit: true + + govet: + check-shadowing: true + enable-all: true + disable: + - shadow + - fieldalignment + - loopclosure + + grouper: + import-require-single-import: true + import-require-grouping: true + + misspell: + locale: US + + nolintlint: + require-explanation: true + require-specific: true + + nonamedreturns: + report-error-in-defer: true + + predeclared: + q: true + + promlinter: + strict: true + + revive: + enable-all-rules: true + rules: + # Provided by gomnd linter + - name: add-constant + disabled: true + - name: argument-limit + disabled: true + # Provided by bidichk + - name: banned-characters + disabled: true + - name: cognitive-complexity + disabled: true + - name: cyclomatic + disabled: true + - name: early-return + severity: warning + disabled: true + - name: exported + disabled: true + - name: file-header + disabled: true + - name: function-result-limit + disabled: true + - name: function-length + disabled: true + - name: line-length-limit + disabled: true + - name: max-public-structs + disabled: true + - name: modifies-parameter + disabled: true + - name: nested-structs + disabled: true + - name: package-comments + disabled: true + + stylecheck: + checks: + - all + - -ST1000 + - -ST1020 + - -ST1021 + - -ST1022 + + tagliatelle: + case: + rules: + json: snake + + #tenv: + # all: true + + #unparam: + # check-exported: true + + wrapcheck: + ignorePackageGlobs: + - github.com/gofiber/fiber/* + - github.com/valyala/fasthttp + +issues: + exclude-use-default: false + +linters: + enable: + - asasalint + - asciicheck + - bidichk + - bodyclose + - containedctx + - contextcheck + - depguard + - dogsled + - durationcheck + - errcheck + - errchkjson + - errname + - errorlint + - execinquery + - exhaustive + - exportloopref + - forbidigo + - forcetypeassert + - goconst + - gocritic + - gofmt + - gofumpt + - goimports + - gomoddirectives + - goprintffuncname + - gosec + - gosimple + - govet + - grouper + - loggercheck + - misspell + - nakedret + - nilerr + - nilnil + - noctx + - nolintlint + - nonamedreturns + - nosprintfhostport + - predeclared + - promlinter + - reassign + - revive + - rowserrcheck + - sqlclosecheck + - staticcheck + - stylecheck + - tagliatelle + # - testpackage # TODO: Enable once https://github.com/gofiber/fiber/issues/2252 is implemented + - thelper + # - tparallel # TODO: Enable once https://github.com/gofiber/fiber/issues/2254 is implemented + - typecheck + - unconvert + - unparam + - unused + - usestdlibvars + - wastedassign + - whitespace + - wrapcheck diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..5188bb8 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019-present Fenny and Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..d219104 --- /dev/null +++ b/Makefile @@ -0,0 +1,55 @@ +## help: 💡 Display available commands +.PHONY: help +help: + @echo '⚡️ GoFiber/Fiber Development:' + @sed -n 's/^##//p' ${MAKEFILE_LIST} | column -t -s ':' | sed -e 's/^/ /' + +## audit: 🚀 Conduct quality checks +.PHONY: audit +audit: + go mod verify + go vet ./... + go run golang.org/x/vuln/cmd/govulncheck@latest ./... + +## benchmark: 📈 Benchmark code performance +.PHONY: benchmark +benchmark: + go test ./... -benchmem -bench=. -run=^Benchmark_$ + +## coverage: ☂️ Generate coverage report +.PHONY: coverage +coverage: + go run gotest.tools/gotestsum@latest -f testname -- ./... -race -count=1 -coverprofile=/tmp/coverage.out -covermode=atomic + go tool cover -html=/tmp/coverage.out + +## format: 🎨 Fix code format issues +.PHONY: format +format: + go run mvdan.cc/gofumpt@latest -w -l . + +## lint: 🚨 Run lint checks +.PHONY: lint +lint: + go run github.com/golangci/golangci-lint/cmd/golangci-lint@v1.51.0 run ./... + +## test: 🚦 Execute all tests +.PHONY: test +test: + go run gotest.tools/gotestsum@latest -f testname -- ./... -race -count=1 -shuffle=on + +## longtest: 🚦 Execute all tests 10x +.PHONY: longtest +longtest: + go run gotest.tools/gotestsum@latest -f testname -- ./... -race -count=15 -shuffle=on + +## tidy: 📌 Clean and tidy dependencies +.PHONY: tidy +tidy: + go mod tidy -v + +## generate: ⚡️ Generate msgp && interface implementations +.PHONY: generate +generate: + go install github.com/tinylib/msgp@latest + go install github.com/vburenin/ifacemaker@975a95966976eeb2d4365a7fb236e274c54da64c + go generate ./... diff --git a/app.go b/app.go new file mode 100644 index 0000000..751df9f --- /dev/null +++ b/app.go @@ -0,0 +1,1120 @@ +// ⚡️ Fiber is an Express inspired web framework written in Go with ☕️ +// 🤖 Github Repository: https://github.com/gofiber/fiber +// 📌 API Documentation: https://docs.gofiber.io + +// Package fiber is an Express inspired web framework built on top of Fasthttp, +// the fastest HTTP engine for Go. Designed to ease things up for fast +// development with zero memory allocation and performance in mind. +package fiber + +import ( + "bufio" + "context" + "encoding/json" + "encoding/xml" + "errors" + "fmt" + "net" + "net/http" + "net/http/httputil" + "reflect" + "strconv" + "strings" + "sync" + "time" + + "github.com/gofiber/fiber/v2/log" + "github.com/gofiber/fiber/v2/utils" + + "github.com/valyala/fasthttp" +) + +// Version of current fiber package +const Version = "2.52.6" + +// Handler defines a function to serve HTTP requests. +type Handler = func(*Ctx) error + +// Map is a shortcut for map[string]interface{}, useful for JSON returns +type Map map[string]interface{} + +// Storage interface for communicating with different database/key-value +// providers +type Storage interface { + // Get gets the value for the given key. + // `nil, nil` is returned when the key does not exist + Get(key string) ([]byte, error) + + // Set stores the given value for the given key along + // with an expiration value, 0 means no expiration. + // Empty key or value will be ignored without an error. + Set(key string, val []byte, exp time.Duration) error + + // Delete deletes the value for the given key. + // It returns no error if the storage does not contain the key, + Delete(key string) error + + // Reset resets the storage and delete all keys. + Reset() error + + // Close closes the storage and will stop any running garbage + // collectors and open connections. + Close() error +} + +// ErrorHandler defines a function that will process all errors +// returned from any handlers in the stack +// +// cfg := fiber.Config{} +// cfg.ErrorHandler = func(c *Ctx, err error) error { +// code := StatusInternalServerError +// var e *fiber.Error +// if errors.As(err, &e) { +// code = e.Code +// } +// c.Set(HeaderContentType, MIMETextPlainCharsetUTF8) +// return c.Status(code).SendString(err.Error()) +// } +// app := fiber.New(cfg) +type ErrorHandler = func(*Ctx, error) error + +// Error represents an error that occurred while handling a request. +type Error struct { + Code int `json:"code"` + Message string `json:"message"` +} + +// App denotes the Fiber application. +type App struct { + mutex sync.Mutex + // Route stack divided by HTTP methods + stack [][]*Route + // Route stack divided by HTTP methods and route prefixes + treeStack []map[string][]*Route + // contains the information if the route stack has been changed to build the optimized tree + routesRefreshed bool + // Amount of registered routes + routesCount uint32 + // Amount of registered handlers + handlersCount uint32 + // Ctx pool + pool sync.Pool + // Fasthttp server + server *fasthttp.Server + // App config + config Config + // Converts string to a byte slice + getBytes func(s string) (b []byte) + // Converts byte slice to a string + getString func(b []byte) string + // Hooks + hooks *Hooks + // Latest route & group + latestRoute *Route + // TLS handler + tlsHandler *TLSHandler + // Mount fields + mountFields *mountFields + // Indicates if the value was explicitly configured + configured Config +} + +// Config is a struct holding the server settings. +type Config struct { + // When set to true, this will spawn multiple Go processes listening on the same port. + // + // Default: false + Prefork bool `json:"prefork"` + + // Enables the "Server: value" HTTP header. + // + // Default: "" + ServerHeader string `json:"server_header"` + + // When set to true, the router treats "/foo" and "/foo/" as different. + // By default this is disabled and both "/foo" and "/foo/" will execute the same handler. + // + // Default: false + StrictRouting bool `json:"strict_routing"` + + // When set to true, enables case sensitive routing. + // E.g. "/FoO" and "/foo" are treated as different routes. + // By default this is disabled and both "/FoO" and "/foo" will execute the same handler. + // + // Default: false + CaseSensitive bool `json:"case_sensitive"` + + // When set to true, this relinquishes the 0-allocation promise in certain + // cases in order to access the handler values (e.g. request bodies) in an + // immutable fashion so that these values are available even if you return + // from handler. + // + // Default: false + Immutable bool `json:"immutable"` + + // When set to true, converts all encoded characters in the route back + // before setting the path for the context, so that the routing, + // the returning of the current url from the context `ctx.Path()` + // and the parameters `ctx.Params(%key%)` with decoded characters will work + // + // Default: false + UnescapePath bool `json:"unescape_path"` + + // Enable or disable ETag header generation, since both weak and strong etags are generated + // using the same hashing method (CRC-32). Weak ETags are the default when enabled. + // + // Default: false + ETag bool `json:"etag"` + + // Max body size that the server accepts. + // -1 will decline any body size + // + // Default: 4 * 1024 * 1024 + BodyLimit int `json:"body_limit"` + + // Maximum number of concurrent connections. + // + // Default: 256 * 1024 + Concurrency int `json:"concurrency"` + + // Views is the interface that wraps the Render function. + // + // Default: nil + Views Views `json:"-"` + + // Views Layout is the global layout for all template render until override on Render function. + // + // Default: "" + ViewsLayout string `json:"views_layout"` + + // PassLocalsToViews Enables passing of the locals set on a fiber.Ctx to the template engine + // + // Default: false + PassLocalsToViews bool `json:"pass_locals_to_views"` + + // The amount of time allowed to read the full request including body. + // It is reset after the request handler has returned. + // The connection's read deadline is reset when the connection opens. + // + // Default: unlimited + ReadTimeout time.Duration `json:"read_timeout"` + + // The maximum duration before timing out writes of the response. + // It is reset after the request handler has returned. + // + // Default: unlimited + WriteTimeout time.Duration `json:"write_timeout"` + + // The maximum amount of time to wait for the next request when keep-alive is enabled. + // If IdleTimeout is zero, the value of ReadTimeout is used. + // + // Default: unlimited + IdleTimeout time.Duration `json:"idle_timeout"` + + // Per-connection buffer size for requests' reading. + // This also limits the maximum header size. + // Increase this buffer if your clients send multi-KB RequestURIs + // and/or multi-KB headers (for example, BIG cookies). + // + // Default: 4096 + ReadBufferSize int `json:"read_buffer_size"` + + // Per-connection buffer size for responses' writing. + // + // Default: 4096 + WriteBufferSize int `json:"write_buffer_size"` + + // CompressedFileSuffix adds suffix to the original file name and + // tries saving the resulting compressed file under the new file name. + // + // Default: ".fiber.gz" + CompressedFileSuffix string `json:"compressed_file_suffix"` + + // ProxyHeader will enable c.IP() to return the value of the given header key + // By default c.IP() will return the Remote IP from the TCP connection + // This property can be useful if you are behind a load balancer: X-Forwarded-* + // NOTE: headers are easily spoofed and the detected IP addresses are unreliable. + // + // Default: "" + ProxyHeader string `json:"proxy_header"` + + // GETOnly rejects all non-GET requests if set to true. + // This option is useful as anti-DoS protection for servers + // accepting only GET requests. The request size is limited + // by ReadBufferSize if GETOnly is set. + // + // Default: false + GETOnly bool `json:"get_only"` + + // ErrorHandler is executed when an error is returned from fiber.Handler. + // + // Default: DefaultErrorHandler + ErrorHandler ErrorHandler `json:"-"` + + // When set to true, disables keep-alive connections. + // The server will close incoming connections after sending the first response to client. + // + // Default: false + DisableKeepalive bool `json:"disable_keepalive"` + + // When set to true, causes the default date header to be excluded from the response. + // + // Default: false + DisableDefaultDate bool `json:"disable_default_date"` + + // When set to true, causes the default Content-Type header to be excluded from the response. + // + // Default: false + DisableDefaultContentType bool `json:"disable_default_content_type"` + + // When set to true, disables header normalization. + // By default all header names are normalized: conteNT-tYPE -> Content-Type. + // + // Default: false + DisableHeaderNormalizing bool `json:"disable_header_normalizing"` + + // When set to true, it will not print out the «Fiber» ASCII art and listening address. + // + // Default: false + DisableStartupMessage bool `json:"disable_startup_message"` + + // This function allows to setup app name for the app + // + // Default: nil + AppName string `json:"app_name"` + + // StreamRequestBody enables request body streaming, + // and calls the handler sooner when given body is + // larger then the current limit. + StreamRequestBody bool + + // Will not pre parse Multipart Form data if set to true. + // + // This option is useful for servers that desire to treat + // multipart form data as a binary blob, or choose when to parse the data. + // + // Server pre parses multipart form data by default. + DisablePreParseMultipartForm bool + + // Aggressively reduces memory usage at the cost of higher CPU usage + // if set to true. + // + // Try enabling this option only if the server consumes too much memory + // serving mostly idle keep-alive connections. This may reduce memory + // usage by more than 50%. + // + // Default: false + ReduceMemoryUsage bool `json:"reduce_memory_usage"` + + // FEATURE: v2.3.x + // The router executes the same handler by default if StrictRouting or CaseSensitive is disabled. + // Enabling RedirectFixedPath will change this behavior into a client redirect to the original route path. + // Using the status code 301 for GET requests and 308 for all other request methods. + // + // Default: false + // RedirectFixedPath bool + + // When set by an external client of Fiber it will use the provided implementation of a + // JSONMarshal + // + // Allowing for flexibility in using another json library for encoding + // Default: json.Marshal + JSONEncoder utils.JSONMarshal `json:"-"` + + // When set by an external client of Fiber it will use the provided implementation of a + // JSONUnmarshal + // + // Allowing for flexibility in using another json library for decoding + // Default: json.Unmarshal + JSONDecoder utils.JSONUnmarshal `json:"-"` + + // XMLEncoder set by an external client of Fiber it will use the provided implementation of a + // XMLMarshal + // + // Allowing for flexibility in using another XML library for encoding + // Default: xml.Marshal + XMLEncoder utils.XMLMarshal `json:"-"` + + // Known networks are "tcp", "tcp4" (IPv4-only), "tcp6" (IPv6-only) + // WARNING: When prefork is set to true, only "tcp4" and "tcp6" can be chose. + // + // Default: NetworkTCP4 + Network string + + // If you find yourself behind some sort of proxy, like a load balancer, + // then certain header information may be sent to you using special X-Forwarded-* headers or the Forwarded header. + // For example, the Host HTTP header is usually used to return the requested host. + // But when you’re behind a proxy, the actual host may be stored in an X-Forwarded-Host header. + // + // If you are behind a proxy, you should enable TrustedProxyCheck to prevent header spoofing. + // If you enable EnableTrustedProxyCheck and leave TrustedProxies empty Fiber will skip + // all headers that could be spoofed. + // If request ip in TrustedProxies whitelist then: + // 1. c.Protocol() get value from X-Forwarded-Proto, X-Forwarded-Protocol, X-Forwarded-Ssl or X-Url-Scheme header + // 2. c.IP() get value from ProxyHeader header. + // 3. c.Hostname() get value from X-Forwarded-Host header + // But if request ip NOT in Trusted Proxies whitelist then: + // 1. c.Protocol() WON't get value from X-Forwarded-Proto, X-Forwarded-Protocol, X-Forwarded-Ssl or X-Url-Scheme header, + // will return https in case when tls connection is handled by the app, of http otherwise + // 2. c.IP() WON'T get value from ProxyHeader header, will return RemoteIP() from fasthttp context + // 3. c.Hostname() WON'T get value from X-Forwarded-Host header, fasthttp.Request.URI().Host() + // will be used to get the hostname. + // + // Default: false + EnableTrustedProxyCheck bool `json:"enable_trusted_proxy_check"` + + // Read EnableTrustedProxyCheck doc. + // + // Default: []string + TrustedProxies []string `json:"trusted_proxies"` + trustedProxiesMap map[string]struct{} + trustedProxyRanges []*net.IPNet + + // If set to true, c.IP() and c.IPs() will validate IP addresses before returning them. + // Also, c.IP() will return only the first valid IP rather than just the raw header + // WARNING: this has a performance cost associated with it. + // + // Default: false + EnableIPValidation bool `json:"enable_ip_validation"` + + // If set to true, will print all routes with their method, path and handler. + // Default: false + EnablePrintRoutes bool `json:"enable_print_routes"` + + // You can define custom color scheme. They'll be used for startup message, route list and some middlewares. + // + // Optional. Default: DefaultColors + ColorScheme Colors `json:"color_scheme"` + + // RequestMethods provides customizibility for HTTP methods. You can add/remove methods as you wish. + // + // Optional. Default: DefaultMethods + RequestMethods []string + + // EnableSplittingOnParsers splits the query/body/header parameters by comma when it's true. + // For example, you can use it to parse multiple values from a query parameter like this: + // /api?foo=bar,baz == foo[]=bar&foo[]=baz + // + // Optional. Default: false + EnableSplittingOnParsers bool `json:"enable_splitting_on_parsers"` +} + +// Static defines configuration options when defining static assets. +type Static struct { + // When set to true, the server tries minimizing CPU usage by caching compressed files. + // This works differently than the github.com/gofiber/compression middleware. + // Optional. Default value false + Compress bool `json:"compress"` + + // When set to true, enables byte range requests. + // Optional. Default value false + ByteRange bool `json:"byte_range"` + + // When set to true, enables directory browsing. + // Optional. Default value false. + Browse bool `json:"browse"` + + // When set to true, enables direct download. + // Optional. Default value false. + Download bool `json:"download"` + + // The name of the index file for serving a directory. + // Optional. Default value "index.html". + Index string `json:"index"` + + // Expiration duration for inactive file handlers. + // Use a negative time.Duration to disable it. + // + // Optional. Default value 10 * time.Second. + CacheDuration time.Duration `json:"cache_duration"` + + // The value for the Cache-Control HTTP-header + // that is set on the file response. MaxAge is defined in seconds. + // + // Optional. Default value 0. + MaxAge int `json:"max_age"` + + // ModifyResponse defines a function that allows you to alter the response. + // + // Optional. Default: nil + ModifyResponse Handler + + // Next defines a function to skip this middleware when returned true. + // + // Optional. Default: nil + Next func(c *Ctx) bool +} + +// RouteMessage is some message need to be print when server starts +type RouteMessage struct { + name string + method string + path string + handlers string +} + +// Default Config values +const ( + DefaultBodyLimit = 4 * 1024 * 1024 + DefaultConcurrency = 256 * 1024 + DefaultReadBufferSize = 4096 + DefaultWriteBufferSize = 4096 + DefaultCompressedFileSuffix = ".fiber.gz" +) + +// HTTP methods enabled by default +var DefaultMethods = []string{ + MethodGet, + MethodHead, + MethodPost, + MethodPut, + MethodDelete, + MethodConnect, + MethodOptions, + MethodTrace, + MethodPatch, +} + +// DefaultErrorHandler that process return errors from handlers +func DefaultErrorHandler(c *Ctx, err error) error { + code := StatusInternalServerError + var e *Error + if errors.As(err, &e) { + code = e.Code + } + c.Set(HeaderContentType, MIMETextPlainCharsetUTF8) + return c.Status(code).SendString(err.Error()) +} + +// New creates a new Fiber named instance. +// +// app := fiber.New() +// +// You can pass optional configuration options by passing a Config struct: +// +// app := fiber.New(fiber.Config{ +// Prefork: true, +// ServerHeader: "Fiber", +// }) +func New(config ...Config) *App { + // Create a new app + app := &App{ + // Create Ctx pool + pool: sync.Pool{ + New: func() interface{} { + return new(Ctx) + }, + }, + // Create config + config: Config{}, + getBytes: utils.UnsafeBytes, + getString: utils.UnsafeString, + latestRoute: &Route{}, + } + + // Define hooks + app.hooks = newHooks(app) + + // Define mountFields + app.mountFields = newMountFields(app) + + // Override config if provided + if len(config) > 0 { + app.config = config[0] + } + + // Initialize configured before defaults are set + app.configured = app.config + + if app.config.ETag { + if !IsChild() { + log.Warn("Config.ETag is deprecated since v2.0.6, please use 'middleware/etag'.") + } + } + + // Override default values + if app.config.BodyLimit == 0 { + app.config.BodyLimit = DefaultBodyLimit + } + if app.config.Concurrency <= 0 { + app.config.Concurrency = DefaultConcurrency + } + if app.config.ReadBufferSize <= 0 { + app.config.ReadBufferSize = DefaultReadBufferSize + } + if app.config.WriteBufferSize <= 0 { + app.config.WriteBufferSize = DefaultWriteBufferSize + } + if app.config.CompressedFileSuffix == "" { + app.config.CompressedFileSuffix = DefaultCompressedFileSuffix + } + if app.config.Immutable { + app.getBytes, app.getString = getBytesImmutable, getStringImmutable + } + + if app.config.ErrorHandler == nil { + app.config.ErrorHandler = DefaultErrorHandler + } + + if app.config.JSONEncoder == nil { + app.config.JSONEncoder = json.Marshal + } + if app.config.JSONDecoder == nil { + app.config.JSONDecoder = json.Unmarshal + } + if app.config.XMLEncoder == nil { + app.config.XMLEncoder = xml.Marshal + } + if app.config.Network == "" { + app.config.Network = NetworkTCP4 + } + if len(app.config.RequestMethods) == 0 { + app.config.RequestMethods = DefaultMethods + } + + app.config.trustedProxiesMap = make(map[string]struct{}, len(app.config.TrustedProxies)) + for _, ipAddress := range app.config.TrustedProxies { + app.handleTrustedProxy(ipAddress) + } + + // Create router stack + app.stack = make([][]*Route, len(app.config.RequestMethods)) + app.treeStack = make([]map[string][]*Route, len(app.config.RequestMethods)) + + // Override colors + app.config.ColorScheme = defaultColors(app.config.ColorScheme) + + // Init app + app.init() + + // Return app + return app +} + +// Adds an ip address to trustedProxyRanges or trustedProxiesMap based on whether it is an IP range or not +func (app *App) handleTrustedProxy(ipAddress string) { + if strings.Contains(ipAddress, "/") { + _, ipNet, err := net.ParseCIDR(ipAddress) + if err != nil { + log.Warnf("IP range %q could not be parsed: %v", ipAddress, err) + } else { + app.config.trustedProxyRanges = append(app.config.trustedProxyRanges, ipNet) + } + } else { + app.config.trustedProxiesMap[ipAddress] = struct{}{} + } +} + +// SetTLSHandler You can use SetTLSHandler to use ClientHelloInfo when using TLS with Listener. +func (app *App) SetTLSHandler(tlsHandler *TLSHandler) { + // Attach the tlsHandler to the config + app.mutex.Lock() + app.tlsHandler = tlsHandler + app.mutex.Unlock() +} + +// Name Assign name to specific route. +func (app *App) Name(name string) Router { + app.mutex.Lock() + defer app.mutex.Unlock() + + for _, routes := range app.stack { + for _, route := range routes { + isMethodValid := route.Method == app.latestRoute.Method || app.latestRoute.use || + (app.latestRoute.Method == MethodGet && route.Method == MethodHead) + + if route.Path == app.latestRoute.Path && isMethodValid { + route.Name = name + if route.group != nil { + route.Name = route.group.name + route.Name + } + } + } + } + + if err := app.hooks.executeOnNameHooks(*app.latestRoute); err != nil { + panic(err) + } + + return app +} + +// GetRoute Get route by name +func (app *App) GetRoute(name string) Route { + for _, routes := range app.stack { + for _, route := range routes { + if route.Name == name { + return *route + } + } + } + + return Route{} +} + +// GetRoutes Get all routes. When filterUseOption equal to true, it will filter the routes registered by the middleware. +func (app *App) GetRoutes(filterUseOption ...bool) []Route { + var rs []Route + var filterUse bool + if len(filterUseOption) != 0 { + filterUse = filterUseOption[0] + } + for _, routes := range app.stack { + for _, route := range routes { + if filterUse && route.use { + continue + } + rs = append(rs, *route) + } + } + return rs +} + +// Use registers a middleware route that will match requests +// with the provided prefix (which is optional and defaults to "/"). +// +// app.Use(func(c *fiber.Ctx) error { +// return c.Next() +// }) +// app.Use("/api", func(c *fiber.Ctx) error { +// return c.Next() +// }) +// app.Use("/api", handler, func(c *fiber.Ctx) error { +// return c.Next() +// }) +// +// This method will match all HTTP verbs: GET, POST, PUT, HEAD etc... +func (app *App) Use(args ...interface{}) Router { + var prefix string + var prefixes []string + var handlers []Handler + + for i := 0; i < len(args); i++ { + switch arg := args[i].(type) { + case string: + prefix = arg + case []string: + prefixes = arg + case Handler: + handlers = append(handlers, arg) + default: + panic(fmt.Sprintf("use: invalid handler %v\n", reflect.TypeOf(arg))) + } + } + + if len(prefixes) == 0 { + prefixes = append(prefixes, prefix) + } + + for _, prefix := range prefixes { + app.register(methodUse, prefix, nil, handlers...) + } + + return app +} + +// Get registers a route for GET methods that requests a representation +// of the specified resource. Requests using GET should only retrieve data. +func (app *App) Get(path string, handlers ...Handler) Router { + return app.Head(path, handlers...).Add(MethodGet, path, handlers...) +} + +// Head registers a route for HEAD methods that asks for a response identical +// to that of a GET request, but without the response body. +func (app *App) Head(path string, handlers ...Handler) Router { + return app.Add(MethodHead, path, handlers...) +} + +// Post registers a route for POST methods that is used to submit an entity to the +// specified resource, often causing a change in state or side effects on the server. +func (app *App) Post(path string, handlers ...Handler) Router { + return app.Add(MethodPost, path, handlers...) +} + +// Put registers a route for PUT methods that replaces all current representations +// of the target resource with the request payload. +func (app *App) Put(path string, handlers ...Handler) Router { + return app.Add(MethodPut, path, handlers...) +} + +// Delete registers a route for DELETE methods that deletes the specified resource. +func (app *App) Delete(path string, handlers ...Handler) Router { + return app.Add(MethodDelete, path, handlers...) +} + +// Connect registers a route for CONNECT methods that establishes a tunnel to the +// server identified by the target resource. +func (app *App) Connect(path string, handlers ...Handler) Router { + return app.Add(MethodConnect, path, handlers...) +} + +// Options registers a route for OPTIONS methods that is used to describe the +// communication options for the target resource. +func (app *App) Options(path string, handlers ...Handler) Router { + return app.Add(MethodOptions, path, handlers...) +} + +// Trace registers a route for TRACE methods that performs a message loop-back +// test along the path to the target resource. +func (app *App) Trace(path string, handlers ...Handler) Router { + return app.Add(MethodTrace, path, handlers...) +} + +// Patch registers a route for PATCH methods that is used to apply partial +// modifications to a resource. +func (app *App) Patch(path string, handlers ...Handler) Router { + return app.Add(MethodPatch, path, handlers...) +} + +// Add allows you to specify a HTTP method to register a route +func (app *App) Add(method, path string, handlers ...Handler) Router { + app.register(method, path, nil, handlers...) + + return app +} + +// Static will create a file server serving static files +func (app *App) Static(prefix, root string, config ...Static) Router { + app.registerStatic(prefix, root, config...) + + return app +} + +// All will register the handler on all HTTP methods +func (app *App) All(path string, handlers ...Handler) Router { + for _, method := range app.config.RequestMethods { + _ = app.Add(method, path, handlers...) + } + return app +} + +// Group is used for Routes with common prefix to define a new sub-router with optional middleware. +// +// api := app.Group("/api") +// api.Get("/users", handler) +func (app *App) Group(prefix string, handlers ...Handler) Router { + grp := &Group{Prefix: prefix, app: app} + if len(handlers) > 0 { + app.register(methodUse, prefix, grp, handlers...) + } + if err := app.hooks.executeOnGroupHooks(*grp); err != nil { + panic(err) + } + + return grp +} + +// Route is used to define routes with a common prefix inside the common function. +// Uses Group method to define new sub-router. +func (app *App) Route(prefix string, fn func(router Router), name ...string) Router { + // Create new group + group := app.Group(prefix) + if len(name) > 0 { + group.Name(name[0]) + } + + // Define routes + fn(group) + + return group +} + +// Error makes it compatible with the `error` interface. +func (e *Error) Error() string { + return e.Message +} + +// NewError creates a new Error instance with an optional message +func NewError(code int, message ...string) *Error { + err := &Error{ + Code: code, + Message: utils.StatusMessage(code), + } + if len(message) > 0 { + err.Message = message[0] + } + return err +} + +// Config returns the app config as value ( read-only ). +func (app *App) Config() Config { + return app.config +} + +// Handler returns the server handler. +func (app *App) Handler() fasthttp.RequestHandler { //revive:disable-line:confusing-naming // Having both a Handler() (uppercase) and a handler() (lowercase) is fine. TODO: Use nolint:revive directive instead. See https://github.com/golangci/golangci-lint/issues/3476 + // prepare the server for the start + app.startupProcess() + return app.handler +} + +// Stack returns the raw router stack. +func (app *App) Stack() [][]*Route { + return app.stack +} + +// HandlersCount returns the amount of registered handlers. +func (app *App) HandlersCount() uint32 { + return app.handlersCount +} + +// Shutdown gracefully shuts down the server without interrupting any active connections. +// Shutdown works by first closing all open listeners and then waiting indefinitely for all connections to return to idle before shutting down. +// +// Make sure the program doesn't exit and waits instead for Shutdown to return. +// +// Shutdown does not close keepalive connections so its recommended to set ReadTimeout to something else than 0. +func (app *App) Shutdown() error { + return app.ShutdownWithContext(context.Background()) +} + +// ShutdownWithTimeout gracefully shuts down the server without interrupting any active connections. However, if the timeout is exceeded, +// ShutdownWithTimeout will forcefully close any active connections. +// ShutdownWithTimeout works by first closing all open listeners and then waiting for all connections to return to idle before shutting down. +// +// Make sure the program doesn't exit and waits instead for ShutdownWithTimeout to return. +// +// ShutdownWithTimeout does not close keepalive connections so its recommended to set ReadTimeout to something else than 0. +func (app *App) ShutdownWithTimeout(timeout time.Duration) error { + ctx, cancelFunc := context.WithTimeout(context.Background(), timeout) + defer cancelFunc() + return app.ShutdownWithContext(ctx) +} + +// ShutdownWithContext shuts down the server including by force if the context's deadline is exceeded. +// +// Make sure the program doesn't exit and waits instead for ShutdownWithTimeout to return. +// +// ShutdownWithContext does not close keepalive connections so its recommended to set ReadTimeout to something else than 0. +func (app *App) ShutdownWithContext(ctx context.Context) error { + if app.hooks != nil { + defer app.hooks.executeOnShutdownHooks() + } + + app.mutex.Lock() + defer app.mutex.Unlock() + if app.server == nil { + return fmt.Errorf("shutdown: server is not running") + } + return app.server.ShutdownWithContext(ctx) +} + +// Server returns the underlying fasthttp server +func (app *App) Server() *fasthttp.Server { + return app.server +} + +// Hooks returns the hook struct to register hooks. +func (app *App) Hooks() *Hooks { + return app.hooks +} + +// Test is used for internal debugging by passing a *http.Request. +// Timeout is optional and defaults to 1s, -1 will disable it completely. +func (app *App) Test(req *http.Request, msTimeout ...int) (*http.Response, error) { + // Set timeout + timeout := 1000 + if len(msTimeout) > 0 { + timeout = msTimeout[0] + } + + // Add Content-Length if not provided with body + if req.Body != http.NoBody && req.Header.Get(HeaderContentLength) == "" { + req.Header.Add(HeaderContentLength, strconv.FormatInt(req.ContentLength, 10)) + } + + // Dump raw http request + dump, err := httputil.DumpRequest(req, true) + if err != nil { + return nil, fmt.Errorf("failed to dump request: %w", err) + } + + // Create test connection + conn := new(testConn) + + // Write raw http request + if _, err := conn.r.Write(dump); err != nil { + return nil, fmt.Errorf("failed to write: %w", err) + } + // prepare the server for the start + app.startupProcess() + + // Serve conn to server + channel := make(chan error) + go func() { + var returned bool + defer func() { + if !returned { + channel <- fmt.Errorf("runtime.Goexit() called in handler or server panic") + } + }() + + channel <- app.server.ServeConn(conn) + returned = true + }() + + // Wait for callback + if timeout >= 0 { + // With timeout + select { + case err = <-channel: + case <-time.After(time.Duration(timeout) * time.Millisecond): + return nil, fmt.Errorf("test: timeout error %vms", timeout) + } + } else { + // Without timeout + err = <-channel + } + + // Check for errors + if err != nil && !errors.Is(err, fasthttp.ErrGetOnly) { + return nil, err + } + + // Read response + buffer := bufio.NewReader(&conn.w) + + // Convert raw http response to *http.Response + res, err := http.ReadResponse(buffer, req) + if err != nil { + return nil, fmt.Errorf("failed to read response: %w", err) + } + + return res, nil +} + +type disableLogger struct{} + +func (*disableLogger) Printf(_ string, _ ...interface{}) { + // fmt.Println(fmt.Sprintf(format, args...)) +} + +func (app *App) init() *App { + // lock application + app.mutex.Lock() + + // Only load templates if a view engine is specified + if app.config.Views != nil { + if err := app.config.Views.Load(); err != nil { + log.Warnf("failed to load views: %v", err) + } + } + + // create fasthttp server + app.server = &fasthttp.Server{ + Logger: &disableLogger{}, + LogAllErrors: false, + ErrorHandler: app.serverErrorHandler, + } + + // fasthttp server settings + app.server.Handler = app.handler + app.server.Name = app.config.ServerHeader + app.server.Concurrency = app.config.Concurrency + app.server.NoDefaultDate = app.config.DisableDefaultDate + app.server.NoDefaultContentType = app.config.DisableDefaultContentType + app.server.DisableHeaderNamesNormalizing = app.config.DisableHeaderNormalizing + app.server.DisableKeepalive = app.config.DisableKeepalive + app.server.MaxRequestBodySize = app.config.BodyLimit + app.server.NoDefaultServerHeader = app.config.ServerHeader == "" + app.server.ReadTimeout = app.config.ReadTimeout + app.server.WriteTimeout = app.config.WriteTimeout + app.server.IdleTimeout = app.config.IdleTimeout + app.server.ReadBufferSize = app.config.ReadBufferSize + app.server.WriteBufferSize = app.config.WriteBufferSize + app.server.GetOnly = app.config.GETOnly + app.server.ReduceMemoryUsage = app.config.ReduceMemoryUsage + app.server.StreamRequestBody = app.config.StreamRequestBody + app.server.DisablePreParseMultipartForm = app.config.DisablePreParseMultipartForm + + // unlock application + app.mutex.Unlock() + return app +} + +// ErrorHandler is the application's method in charge of finding the +// appropriate handler for the given request. It searches any mounted +// sub fibers by their prefixes and if it finds a match, it uses that +// error handler. Otherwise it uses the configured error handler for +// the app, which if not set is the DefaultErrorHandler. +func (app *App) ErrorHandler(ctx *Ctx, err error) error { + var ( + mountedErrHandler ErrorHandler + mountedPrefixParts int + ) + + for prefix, subApp := range app.mountFields.appList { + if prefix != "" && strings.HasPrefix(ctx.path, prefix) { + parts := len(strings.Split(prefix, "/")) + if mountedPrefixParts <= parts { + if subApp.configured.ErrorHandler != nil { + mountedErrHandler = subApp.config.ErrorHandler + } + + mountedPrefixParts = parts + } + } + } + + if mountedErrHandler != nil { + return mountedErrHandler(ctx, err) + } + + return app.config.ErrorHandler(ctx, err) +} + +// serverErrorHandler is a wrapper around the application's error handler method +// user for the fasthttp server configuration. It maps a set of fasthttp errors to fiber +// errors before calling the application's error handler method. +func (app *App) serverErrorHandler(fctx *fasthttp.RequestCtx, err error) { + c := app.AcquireCtx(fctx) + defer app.ReleaseCtx(c) + + var ( + errNetOP *net.OpError + netErr net.Error + ) + + switch { + case errors.As(err, new(*fasthttp.ErrSmallBuffer)): + err = ErrRequestHeaderFieldsTooLarge + case errors.As(err, &errNetOP) && errNetOP.Timeout(): + err = ErrRequestTimeout + case errors.As(err, &netErr): + err = ErrBadGateway + case errors.Is(err, fasthttp.ErrBodyTooLarge): + err = ErrRequestEntityTooLarge + case errors.Is(err, fasthttp.ErrGetOnly): + err = ErrMethodNotAllowed + case strings.Contains(err.Error(), "timeout"): + err = ErrRequestTimeout + default: + err = NewError(StatusBadRequest, err.Error()) + } + + if catch := app.ErrorHandler(c, err); catch != nil { + log.Errorf("serverErrorHandler: failed to call ErrorHandler: %v", catch) + _ = c.SendStatus(StatusInternalServerError) //nolint:errcheck // It is fine to ignore the error here + return + } +} + +// startupProcess Is the method which executes all the necessary processes just before the start of the server. +func (app *App) startupProcess() *App { + app.mutex.Lock() + defer app.mutex.Unlock() + + app.mountStartupProcess() + + // build route tree stack + app.buildTree() + + return app +} + +// Run onListen hooks. If they return an error, panic. +func (app *App) runOnListenHooks(listenData ListenData) { + if err := app.hooks.executeOnListenHooks(listenData); err != nil { + panic(err) + } +} diff --git a/app_test.go b/app_test.go new file mode 100644 index 0000000..cef3d51 --- /dev/null +++ b/app_test.go @@ -0,0 +1,1979 @@ +// ⚡️ Fiber is an Express inspired web framework written in Go with ☕️ +// 🤖 Github Repository: https://github.com/gofiber/fiber +// 📌 API Documentation: https://docs.gofiber.io + +//nolint:bodyclose // Much easier to just ignore memory leaks in tests +package fiber + +import ( + "bytes" + "context" + "crypto/tls" + "errors" + "fmt" + "io" + "mime/multipart" + "net" + "net/http" + "net/http/httptest" + "reflect" + "regexp" + "runtime" + "strings" + "testing" + "time" + + "github.com/gofiber/fiber/v2/utils" + + "github.com/valyala/fasthttp" + "github.com/valyala/fasthttp/fasthttputil" +) + +func testEmptyHandler(_ *Ctx) error { + return nil +} + +func testStatus200(t *testing.T, app *App, url, method string) { + t.Helper() + + req := httptest.NewRequest(method, url, nil) + + resp, err := app.Test(req) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, 200, resp.StatusCode, "Status code") +} + +func testErrorResponse(t *testing.T, err error, resp *http.Response, expectedBodyError string) { + t.Helper() + + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, 500, resp.StatusCode, "Status code") + + body, err := io.ReadAll(resp.Body) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, expectedBodyError, string(body), "Response body") +} + +func Test_App_MethodNotAllowed(t *testing.T) { + t.Parallel() + app := New() + + app.Use(func(c *Ctx) error { + return c.Next() + }) + + app.Post("/", testEmptyHandler) + + app.Options("/", testEmptyHandler) + + resp, err := app.Test(httptest.NewRequest(MethodPost, "/", nil)) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, 200, resp.StatusCode) + utils.AssertEqual(t, "", resp.Header.Get(HeaderAllow)) + + resp, err = app.Test(httptest.NewRequest(MethodGet, "/", nil)) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, 405, resp.StatusCode) + utils.AssertEqual(t, "POST, OPTIONS", resp.Header.Get(HeaderAllow)) + + resp, err = app.Test(httptest.NewRequest(MethodPatch, "/", nil)) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, 405, resp.StatusCode) + utils.AssertEqual(t, "POST, OPTIONS", resp.Header.Get(HeaderAllow)) + + resp, err = app.Test(httptest.NewRequest(MethodPut, "/", nil)) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, 405, resp.StatusCode) + utils.AssertEqual(t, "POST, OPTIONS", resp.Header.Get(HeaderAllow)) + + app.Get("/", testEmptyHandler) + + resp, err = app.Test(httptest.NewRequest(MethodTrace, "/", nil)) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, 405, resp.StatusCode) + utils.AssertEqual(t, "GET, HEAD, POST, OPTIONS", resp.Header.Get(HeaderAllow)) + + resp, err = app.Test(httptest.NewRequest(MethodPatch, "/", nil)) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, 405, resp.StatusCode) + utils.AssertEqual(t, "GET, HEAD, POST, OPTIONS", resp.Header.Get(HeaderAllow)) + + resp, err = app.Test(httptest.NewRequest(MethodPut, "/", nil)) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, 405, resp.StatusCode) + utils.AssertEqual(t, "GET, HEAD, POST, OPTIONS", resp.Header.Get(HeaderAllow)) +} + +func Test_App_Custom_Middleware_404_Should_Not_SetMethodNotAllowed(t *testing.T) { + t.Parallel() + app := New() + + app.Use(func(c *Ctx) error { + return c.SendStatus(404) + }) + + app.Post("/", testEmptyHandler) + + resp, err := app.Test(httptest.NewRequest(MethodGet, "/", nil)) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, 404, resp.StatusCode) + + g := app.Group("/with-next", func(c *Ctx) error { + return c.Status(404).Next() + }) + + g.Post("/", testEmptyHandler) + + resp, err = app.Test(httptest.NewRequest(MethodGet, "/with-next", nil)) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, 404, resp.StatusCode) +} + +func Test_App_ServerErrorHandler_SmallReadBuffer(t *testing.T) { + t.Parallel() + expectedError := regexp.MustCompile( + `error when reading request headers: small read buffer\. Increase ReadBufferSize\. Buffer size=4096, contents: "GET / HTTP/1.1\\r\\nHost: example\.com\\r\\nVery-Long-Header: -+`, + ) + app := New() + + app.Get("/", func(c *Ctx) error { + panic(errors.New("should never called")) + }) + + request := httptest.NewRequest(MethodGet, "/", nil) + logHeaderSlice := make([]string, 5000) + request.Header.Set("Very-Long-Header", strings.Join(logHeaderSlice, "-")) + _, err := app.Test(request) + if err == nil { + t.Error("Expect an error at app.Test(request)") + } + + utils.AssertEqual( + t, + true, + expectedError.MatchString(err.Error()), + fmt.Sprintf("Has: %s, expected pattern: %s", err.Error(), expectedError.String()), + ) +} + +func Test_App_Errors(t *testing.T) { + t.Parallel() + app := New(Config{ + BodyLimit: 4, + }) + + app.Get("/", func(c *Ctx) error { + return errors.New("hi, i'm an error") + }) + + resp, err := app.Test(httptest.NewRequest(MethodGet, "/", nil)) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, 500, resp.StatusCode, "Status code") + + body, err := io.ReadAll(resp.Body) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, "hi, i'm an error", string(body)) + + _, err = app.Test(httptest.NewRequest(MethodGet, "/", strings.NewReader("big body"))) + if err != nil { + utils.AssertEqual(t, "body size exceeds the given limit", err.Error(), "app.Test(req)") + } +} + +func Test_App_ErrorHandler_Custom(t *testing.T) { + t.Parallel() + app := New(Config{ + ErrorHandler: func(c *Ctx, err error) error { + return c.Status(200).SendString("hi, i'm an custom error") + }, + }) + + app.Get("/", func(c *Ctx) error { + return errors.New("hi, i'm an error") + }) + + resp, err := app.Test(httptest.NewRequest(MethodGet, "/", nil)) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, 200, resp.StatusCode, "Status code") + + body, err := io.ReadAll(resp.Body) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, "hi, i'm an custom error", string(body)) +} + +func Test_App_ErrorHandler_HandlerStack(t *testing.T) { + t.Parallel() + app := New(Config{ + ErrorHandler: func(c *Ctx, err error) error { + utils.AssertEqual(t, "1: USE error", err.Error()) + return DefaultErrorHandler(c, err) + }, + }) + app.Use("/", func(c *Ctx) error { + err := c.Next() // call next USE + utils.AssertEqual(t, "2: USE error", err.Error()) + return errors.New("1: USE error") + }, func(c *Ctx) error { + err := c.Next() // call [0] GET + utils.AssertEqual(t, "0: GET error", err.Error()) + return errors.New("2: USE error") + }) + app.Get("/", func(c *Ctx) error { + return errors.New("0: GET error") + }) + + resp, err := app.Test(httptest.NewRequest(MethodGet, "/", nil)) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, 500, resp.StatusCode, "Status code") + + body, err := io.ReadAll(resp.Body) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, "1: USE error", string(body)) +} + +func Test_App_ErrorHandler_RouteStack(t *testing.T) { + t.Parallel() + app := New(Config{ + ErrorHandler: func(c *Ctx, err error) error { + utils.AssertEqual(t, "1: USE error", err.Error()) + return DefaultErrorHandler(c, err) + }, + }) + app.Use("/", func(c *Ctx) error { + err := c.Next() + utils.AssertEqual(t, "0: GET error", err.Error()) + return errors.New("1: USE error") // [2] call ErrorHandler + }) + app.Get("/test", func(c *Ctx) error { + return errors.New("0: GET error") // [1] return to USE + }) + + resp, err := app.Test(httptest.NewRequest(MethodGet, "/test", nil)) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, 500, resp.StatusCode, "Status code") + + body, err := io.ReadAll(resp.Body) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, "1: USE error", string(body)) +} + +func Test_App_serverErrorHandler_Internal_Error(t *testing.T) { + t.Parallel() + app := New() + msg := "test err" + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + app.serverErrorHandler(c.fasthttp, errors.New(msg)) + utils.AssertEqual(t, string(c.fasthttp.Response.Body()), msg) + utils.AssertEqual(t, c.fasthttp.Response.StatusCode(), StatusBadRequest) +} + +func Test_App_serverErrorHandler_Network_Error(t *testing.T) { + t.Parallel() + app := New() + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + app.serverErrorHandler(c.fasthttp, &net.DNSError{ + Err: "test error", + Name: "test host", + IsTimeout: false, + }) + utils.AssertEqual(t, string(c.fasthttp.Response.Body()), utils.StatusMessage(StatusBadGateway)) + utils.AssertEqual(t, c.fasthttp.Response.StatusCode(), StatusBadGateway) +} + +func Test_App_Nested_Params(t *testing.T) { + t.Parallel() + app := New() + + app.Get("/test", func(c *Ctx) error { + return c.Status(400).Send([]byte("Should move on")) + }) + app.Get("/test/:param", func(c *Ctx) error { + return c.Status(400).Send([]byte("Should move on")) + }) + app.Get("/test/:param/test", func(c *Ctx) error { + return c.Status(400).Send([]byte("Should move on")) + }) + app.Get("/test/:param/test/:param2", func(c *Ctx) error { + return c.Status(200).Send([]byte("Good job")) + }) + + req := httptest.NewRequest(MethodGet, "/test/john/test/doe", nil) + resp, err := app.Test(req) + + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, 200, resp.StatusCode, "Status code") +} + +func Test_App_Use_Params(t *testing.T) { + t.Parallel() + app := New() + + app.Use("/prefix/:param", func(c *Ctx) error { + utils.AssertEqual(t, "john", c.Params("param")) + return nil + }) + + app.Use("/foo/:bar?", func(c *Ctx) error { + utils.AssertEqual(t, "foobar", c.Params("bar", "foobar")) + return nil + }) + + app.Use("/:param/*", func(c *Ctx) error { + utils.AssertEqual(t, "john", c.Params("param")) + utils.AssertEqual(t, "doe", c.Params("*")) + return nil + }) + + resp, err := app.Test(httptest.NewRequest(MethodGet, "/prefix/john", nil)) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, 200, resp.StatusCode, "Status code") + + resp, err = app.Test(httptest.NewRequest(MethodGet, "/john/doe", nil)) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, 200, resp.StatusCode, "Status code") + + resp, err = app.Test(httptest.NewRequest(MethodGet, "/foo", nil)) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, 200, resp.StatusCode, "Status code") + + defer func() { + if err := recover(); err != nil { + utils.AssertEqual(t, "use: invalid handler func()\n", fmt.Sprintf("%v", err)) + } + }() + + app.Use("/:param/*", func() { + // this should panic + }) +} + +func Test_App_Use_UnescapedPath(t *testing.T) { + t.Parallel() + app := New(Config{UnescapePath: true, CaseSensitive: true}) + + app.Use("/cRéeR/:param", func(c *Ctx) error { + utils.AssertEqual(t, "/cRéeR/اختبار", c.Path()) + return c.SendString(c.Params("param")) + }) + + app.Use("/abc", func(c *Ctx) error { + utils.AssertEqual(t, "/AbC", c.Path()) + return nil + }) + + resp, err := app.Test(httptest.NewRequest(MethodGet, "/cR%C3%A9eR/%D8%A7%D8%AE%D8%AA%D8%A8%D8%A7%D8%B1", nil)) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, StatusOK, resp.StatusCode, "Status code") + + body, err := io.ReadAll(resp.Body) + utils.AssertEqual(t, nil, err, "app.Test(req)") + // check the param result + utils.AssertEqual(t, "اختبار", app.getString(body)) + + // with lowercase letters + resp, err = app.Test(httptest.NewRequest(MethodGet, "/cr%C3%A9er/%D8%A7%D8%AE%D8%AA%D8%A8%D8%A7%D8%B1", nil)) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, StatusNotFound, resp.StatusCode, "Status code") +} + +func Test_App_Use_CaseSensitive(t *testing.T) { + t.Parallel() + app := New(Config{CaseSensitive: true}) + + app.Use("/abc", func(c *Ctx) error { + return c.SendString(c.Path()) + }) + + // wrong letters in the requested route -> 404 + resp, err := app.Test(httptest.NewRequest(MethodGet, "/AbC", nil)) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, StatusNotFound, resp.StatusCode, "Status code") + + // right letters in the requrested route -> 200 + resp, err = app.Test(httptest.NewRequest(MethodGet, "/abc", nil)) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, StatusOK, resp.StatusCode, "Status code") + + // check the detected path when the case insensitive recognition is activated + app.config.CaseSensitive = false + // check the case sensitive feature + resp, err = app.Test(httptest.NewRequest(MethodGet, "/AbC", nil)) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, StatusOK, resp.StatusCode, "Status code") + + body, err := io.ReadAll(resp.Body) + utils.AssertEqual(t, nil, err, "app.Test(req)") + // check the detected path result + utils.AssertEqual(t, "/AbC", app.getString(body)) +} + +func Test_App_Not_Use_StrictRouting(t *testing.T) { + t.Parallel() + app := New() + + app.Use("/abc", func(c *Ctx) error { + return c.SendString(c.Path()) + }) + + g := app.Group("/foo") + g.Use("/", func(c *Ctx) error { + return c.SendString(c.Path()) + }) + + // wrong path in the requested route -> 404 + resp, err := app.Test(httptest.NewRequest(MethodGet, "/abc/", nil)) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, StatusOK, resp.StatusCode, "Status code") + + // right path in the requrested route -> 200 + resp, err = app.Test(httptest.NewRequest(MethodGet, "/abc", nil)) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, StatusOK, resp.StatusCode, "Status code") + + // wrong path with group in the requested route -> 404 + resp, err = app.Test(httptest.NewRequest(MethodGet, "/foo", nil)) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, StatusOK, resp.StatusCode, "Status code") + + // right path with group in the requrested route -> 200 + resp, err = app.Test(httptest.NewRequest(MethodGet, "/foo/", nil)) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, StatusOK, resp.StatusCode, "Status code") +} + +func Test_App_Use_MultiplePrefix(t *testing.T) { + t.Parallel() + app := New() + + app.Use([]string{"/john", "/doe"}, func(c *Ctx) error { + return c.SendString(c.Path()) + }) + + g := app.Group("/test") + g.Use([]string{"/john", "/doe"}, func(c *Ctx) error { + return c.SendString(c.Path()) + }) + + resp, err := app.Test(httptest.NewRequest(MethodGet, "/john", nil)) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, StatusOK, resp.StatusCode, "Status code") + + body, err := io.ReadAll(resp.Body) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, "/john", string(body)) + + resp, err = app.Test(httptest.NewRequest(MethodGet, "/doe", nil)) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, StatusOK, resp.StatusCode, "Status code") + + body, err = io.ReadAll(resp.Body) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, "/doe", string(body)) + + resp, err = app.Test(httptest.NewRequest(MethodGet, "/test/john", nil)) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, StatusOK, resp.StatusCode, "Status code") + + body, err = io.ReadAll(resp.Body) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, "/test/john", string(body)) + + resp, err = app.Test(httptest.NewRequest(MethodGet, "/test/doe", nil)) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, StatusOK, resp.StatusCode, "Status code") + + body, err = io.ReadAll(resp.Body) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, "/test/doe", string(body)) +} + +func Test_App_Use_StrictRouting(t *testing.T) { + t.Parallel() + app := New(Config{StrictRouting: true}) + + app.Get("/abc", func(c *Ctx) error { + return c.SendString(c.Path()) + }) + + g := app.Group("/foo") + g.Get("/", func(c *Ctx) error { + return c.SendString(c.Path()) + }) + + // wrong path in the requested route -> 404 + resp, err := app.Test(httptest.NewRequest(MethodGet, "/abc/", nil)) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, StatusNotFound, resp.StatusCode, "Status code") + + // right path in the requrested route -> 200 + resp, err = app.Test(httptest.NewRequest(MethodGet, "/abc", nil)) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, StatusOK, resp.StatusCode, "Status code") + + // wrong path with group in the requested route -> 404 + resp, err = app.Test(httptest.NewRequest(MethodGet, "/foo", nil)) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, StatusNotFound, resp.StatusCode, "Status code") + + // right path with group in the requrested route -> 200 + resp, err = app.Test(httptest.NewRequest(MethodGet, "/foo/", nil)) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, StatusOK, resp.StatusCode, "Status code") +} + +func Test_App_Add_Method_Test(t *testing.T) { + t.Parallel() + defer func() { + if err := recover(); err != nil { + utils.AssertEqual(t, "add: invalid http method JANE\n", fmt.Sprintf("%v", err)) + } + }() + + methods := append(DefaultMethods, "JOHN") //nolint:gocritic // We want a new slice here + app := New(Config{ + RequestMethods: methods, + }) + + app.Add("JOHN", "/doe", testEmptyHandler) + + resp, err := app.Test(httptest.NewRequest("JOHN", "/doe", nil)) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, StatusOK, resp.StatusCode, "Status code") + + resp, err = app.Test(httptest.NewRequest(MethodGet, "/doe", nil)) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, StatusMethodNotAllowed, resp.StatusCode, "Status code") + + resp, err = app.Test(httptest.NewRequest("UNKNOWN", "/doe", nil)) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, StatusBadRequest, resp.StatusCode, "Status code") + + app.Add("JANE", "/doe", testEmptyHandler) +} + +// go test -run Test_App_GETOnly +func Test_App_GETOnly(t *testing.T) { + t.Parallel() + app := New(Config{ + GETOnly: true, + }) + + app.Post("/", func(c *Ctx) error { + return c.SendString("Hello 👋!") + }) + + req := httptest.NewRequest(MethodPost, "/", nil) + resp, err := app.Test(req) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, StatusMethodNotAllowed, resp.StatusCode, "Status code") +} + +func Test_App_Use_Params_Group(t *testing.T) { + t.Parallel() + app := New() + + group := app.Group("/prefix/:param/*") + group.Use("/", func(c *Ctx) error { + return c.Next() + }) + group.Get("/test", func(c *Ctx) error { + utils.AssertEqual(t, "john", c.Params("param")) + utils.AssertEqual(t, "doe", c.Params("*")) + return nil + }) + + resp, err := app.Test(httptest.NewRequest(MethodGet, "/prefix/john/doe/test", nil)) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, 200, resp.StatusCode, "Status code") +} + +func Test_App_Chaining(t *testing.T) { + t.Parallel() + n := func(c *Ctx) error { + return c.Next() + } + app := New() + app.Use("/john", n, n, n, n, func(c *Ctx) error { + return c.SendStatus(202) + }) + // check handler count for registered HEAD route + utils.AssertEqual(t, 5, len(app.stack[app.methodInt(MethodHead)][0].Handlers), "app.Test(req)") + + req := httptest.NewRequest(MethodPost, "/john", nil) + + resp, err := app.Test(req) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, 202, resp.StatusCode, "Status code") + + app.Get("/test", n, n, n, n, func(c *Ctx) error { + return c.SendStatus(203) + }) + + req = httptest.NewRequest(MethodGet, "/test", nil) + + resp, err = app.Test(req) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, 203, resp.StatusCode, "Status code") +} + +func Test_App_Order(t *testing.T) { + t.Parallel() + app := New() + + app.Get("/test", func(c *Ctx) error { + _, err := c.Write([]byte("1")) + utils.AssertEqual(t, nil, err) + return c.Next() + }) + + app.All("/test", func(c *Ctx) error { + _, err := c.Write([]byte("2")) + utils.AssertEqual(t, nil, err) + return c.Next() + }) + + app.Use(func(c *Ctx) error { + _, err := c.Write([]byte("3")) + utils.AssertEqual(t, nil, err) + return nil + }) + + req := httptest.NewRequest(MethodGet, "/test", nil) + + resp, err := app.Test(req) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, 200, resp.StatusCode, "Status code") + + body, err := io.ReadAll(resp.Body) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, "123", string(body)) +} + +func Test_App_Methods(t *testing.T) { + t.Parallel() + dummyHandler := testEmptyHandler + + app := New() + + app.Connect("/:john?/:doe?", dummyHandler) + testStatus200(t, app, "/john/doe", "CONNECT") + + app.Put("/:john?/:doe?", dummyHandler) + testStatus200(t, app, "/john/doe", MethodPut) + + app.Post("/:john?/:doe?", dummyHandler) + testStatus200(t, app, "/john/doe", MethodPost) + + app.Delete("/:john?/:doe?", dummyHandler) + testStatus200(t, app, "/john/doe", MethodDelete) + + app.Head("/:john?/:doe?", dummyHandler) + testStatus200(t, app, "/john/doe", MethodHead) + + app.Patch("/:john?/:doe?", dummyHandler) + testStatus200(t, app, "/john/doe", MethodPatch) + + app.Options("/:john?/:doe?", dummyHandler) + testStatus200(t, app, "/john/doe", MethodOptions) + + app.Trace("/:john?/:doe?", dummyHandler) + testStatus200(t, app, "/john/doe", MethodTrace) + + app.Get("/:john?/:doe?", dummyHandler) + testStatus200(t, app, "/john/doe", MethodGet) + + app.All("/:john?/:doe?", dummyHandler) + testStatus200(t, app, "/john/doe", MethodPost) + + app.Use("/:john?/:doe?", dummyHandler) + testStatus200(t, app, "/john/doe", MethodGet) +} + +func Test_App_Route_Naming(t *testing.T) { + t.Parallel() + app := New() + handler := func(c *Ctx) error { + return c.SendStatus(StatusOK) + } + app.Get("/john", handler).Name("john") + app.Delete("/doe", handler) + app.Name("doe") + + jane := app.Group("/jane").Name("jane.") + group := app.Group("/group") + subGroup := jane.Group("/sub-group").Name("sub.") + + jane.Get("/test", handler).Name("test") + jane.Trace("/trace", handler).Name("trace") + + group.Get("/test", handler).Name("test") + + app.Post("/post", handler).Name("post") + + subGroup.Get("/done", handler).Name("done") + + utils.AssertEqual(t, "post", app.GetRoute("post").Name) + utils.AssertEqual(t, "john", app.GetRoute("john").Name) + utils.AssertEqual(t, "jane.test", app.GetRoute("jane.test").Name) + utils.AssertEqual(t, "jane.trace", app.GetRoute("jane.trace").Name) + utils.AssertEqual(t, "jane.sub.done", app.GetRoute("jane.sub.done").Name) + utils.AssertEqual(t, "test", app.GetRoute("test").Name) +} + +func Test_App_New(t *testing.T) { + t.Parallel() + app := New() + app.Get("/", testEmptyHandler) + + appConfig := New(Config{ + Immutable: true, + }) + appConfig.Get("/", testEmptyHandler) +} + +func Test_App_Config(t *testing.T) { + t.Parallel() + app := New(Config{ + DisableStartupMessage: true, + }) + utils.AssertEqual(t, true, app.Config().DisableStartupMessage) +} + +func Test_App_Shutdown(t *testing.T) { + t.Parallel() + t.Run("success", func(t *testing.T) { + t.Parallel() + app := New(Config{ + DisableStartupMessage: true, + }) + utils.AssertEqual(t, true, app.Shutdown() == nil) + }) + + t.Run("no server", func(t *testing.T) { + t.Parallel() + app := &App{} + if err := app.Shutdown(); err != nil { + utils.AssertEqual(t, "shutdown: server is not running", err.Error()) + } + }) +} + +func Test_App_ShutdownWithTimeout(t *testing.T) { + t.Parallel() + app := New() + app.Get("/", func(ctx *Ctx) error { + time.Sleep(5 * time.Second) + return ctx.SendString("body") + }) + ln := fasthttputil.NewInmemoryListener() + go func() { + utils.AssertEqual(t, nil, app.Listener(ln)) + }() + time.Sleep(1 * time.Second) + go func() { + conn, err := ln.Dial() + if err != nil { + t.Errorf("unexepcted error: %v", err) + } + + if _, err = conn.Write([]byte("GET / HTTP/1.1\r\nHost: google.com\r\n\r\n")); err != nil { + t.Errorf("unexpected error: %v", err) + } + }() + time.Sleep(1 * time.Second) + + shutdownErr := make(chan error) + go func() { + shutdownErr <- app.ShutdownWithTimeout(1 * time.Second) + }() + + timer := time.NewTimer(time.Second * 5) + select { + case <-timer.C: + t.Fatal("idle connections not closed on shutdown") + case err := <-shutdownErr: + if err == nil || !errors.Is(err, context.DeadlineExceeded) { + t.Fatalf("unexpected err %v. Expecting %v", err, context.DeadlineExceeded) + } + } +} + +func Test_App_ShutdownWithContext(t *testing.T) { + t.Parallel() + + app := New() + app.Get("/", func(ctx *Ctx) error { + time.Sleep(5 * time.Second) + return ctx.SendString("body") + }) + + ln := fasthttputil.NewInmemoryListener() + + go func() { + utils.AssertEqual(t, nil, app.Listener(ln)) + }() + + time.Sleep(1 * time.Second) + + go func() { + conn, err := ln.Dial() + if err != nil { + t.Errorf("unexepcted error: %v", err) + } + + if _, err = conn.Write([]byte("GET / HTTP/1.1\r\nHost: google.com\r\n\r\n")); err != nil { + t.Errorf("unexpected error: %v", err) + } + }() + + time.Sleep(1 * time.Second) + + shutdownErr := make(chan error) + go func() { + ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) + defer cancel() + shutdownErr <- app.ShutdownWithContext(ctx) + }() + + select { + case <-time.After(5 * time.Second): + t.Fatal("idle connections not closed on shutdown") + case err := <-shutdownErr: + if err == nil || !errors.Is(err, context.DeadlineExceeded) { + t.Fatalf("unexpected err %v. Expecting %v", err, context.DeadlineExceeded) + } + } +} + +// go test -run Test_App_Static_Index_Default +func Test_App_Static_Index_Default(t *testing.T) { + t.Parallel() + app := New() + + app.Static("/prefix", "./.github/workflows") + app.Static("", "./.github/") + app.Static("test", "", Static{Index: "index.html"}) + + resp, err := app.Test(httptest.NewRequest(MethodGet, "/", nil)) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, 200, resp.StatusCode, "Status code") + utils.AssertEqual(t, false, resp.Header.Get(HeaderContentLength) == "") + utils.AssertEqual(t, MIMETextHTMLCharsetUTF8, resp.Header.Get(HeaderContentType)) + + body, err := io.ReadAll(resp.Body) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, true, strings.Contains(string(body), "Hello, World!")) + + resp, err = app.Test(httptest.NewRequest(MethodGet, "/not-found", nil)) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, 404, resp.StatusCode, "Status code") + utils.AssertEqual(t, false, resp.Header.Get(HeaderContentLength) == "") + utils.AssertEqual(t, MIMETextPlainCharsetUTF8, resp.Header.Get(HeaderContentType)) + + body, err = io.ReadAll(resp.Body) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, "Cannot GET /not-found", string(body)) +} + +// go test -run Test_App_Static_Index +func Test_App_Static_Direct(t *testing.T) { + t.Parallel() + app := New() + + app.Static("/", "./.github") + + resp, err := app.Test(httptest.NewRequest(MethodGet, "/index.html", nil)) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, 200, resp.StatusCode, "Status code") + utils.AssertEqual(t, false, resp.Header.Get(HeaderContentLength) == "") + utils.AssertEqual(t, MIMETextHTMLCharsetUTF8, resp.Header.Get(HeaderContentType)) + + body, err := io.ReadAll(resp.Body) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, true, strings.Contains(string(body), "Hello, World!")) + + resp, err = app.Test(httptest.NewRequest(MethodGet, "/testdata/testRoutes.json", nil)) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, 200, resp.StatusCode, "Status code") + utils.AssertEqual(t, false, resp.Header.Get(HeaderContentLength) == "") + utils.AssertEqual(t, MIMEApplicationJSON, resp.Header.Get("Content-Type")) + utils.AssertEqual(t, "", resp.Header.Get(HeaderCacheControl), "CacheControl Control") + + body, err = io.ReadAll(resp.Body) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, true, strings.Contains(string(body), "test_routes")) +} + +// go test -run Test_App_Static_MaxAge +func Test_App_Static_MaxAge(t *testing.T) { + t.Parallel() + app := New() + + app.Static("/", "./.github", Static{MaxAge: 100}) + + resp, err := app.Test(httptest.NewRequest(MethodGet, "/index.html", nil)) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, 200, resp.StatusCode, "Status code") + utils.AssertEqual(t, false, resp.Header.Get(HeaderContentLength) == "") + utils.AssertEqual(t, "text/html; charset=utf-8", resp.Header.Get(HeaderContentType)) + utils.AssertEqual(t, "public, max-age=100", resp.Header.Get(HeaderCacheControl), "CacheControl Control") +} + +// go test -run Test_App_Static_Custom_CacheControl +func Test_App_Static_Custom_CacheControl(t *testing.T) { + t.Parallel() + app := New() + + app.Static("/", "./.github", Static{ModifyResponse: func(c *Ctx) error { + if strings.Contains(c.GetRespHeader("Content-Type"), "text/html") { + c.Response().Header.Set("Cache-Control", "no-cache, no-store, must-revalidate") + } + return nil + }}) + + resp, err := app.Test(httptest.NewRequest(MethodGet, "/index.html", nil)) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, "no-cache, no-store, must-revalidate", resp.Header.Get(HeaderCacheControl), "CacheControl Control") + + respNormal, errNormal := app.Test(httptest.NewRequest(MethodGet, "/config.yml", nil)) + utils.AssertEqual(t, nil, errNormal, "app.Test(req)") + utils.AssertEqual(t, "", respNormal.Header.Get(HeaderCacheControl), "CacheControl Control") +} + +// go test -run Test_App_Static_Download +func Test_App_Static_Download(t *testing.T) { + t.Parallel() + app := New() + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + + app.Static("/fiber.png", "./.github/testdata/fs/img/fiber.png", Static{Download: true}) + + resp, err := app.Test(httptest.NewRequest(MethodGet, "/fiber.png", nil)) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, 200, resp.StatusCode, "Status code") + utils.AssertEqual(t, false, resp.Header.Get(HeaderContentLength) == "") + utils.AssertEqual(t, "image/png", resp.Header.Get(HeaderContentType)) + utils.AssertEqual(t, `attachment`, resp.Header.Get(HeaderContentDisposition)) +} + +// go test -run Test_App_Static_Group +func Test_App_Static_Group(t *testing.T) { + t.Parallel() + app := New() + + grp := app.Group("/v1", func(c *Ctx) error { + c.Set("Test-Header", "123") + return c.Next() + }) + + grp.Static("/v2", "./.github/index.html") + + req := httptest.NewRequest(MethodGet, "/v1/v2", nil) + resp, err := app.Test(req) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, 200, resp.StatusCode, "Status code") + utils.AssertEqual(t, false, resp.Header.Get(HeaderContentLength) == "") + utils.AssertEqual(t, MIMETextHTMLCharsetUTF8, resp.Header.Get(HeaderContentType)) + utils.AssertEqual(t, "123", resp.Header.Get("Test-Header")) + + grp = app.Group("/v2") + grp.Static("/v3*", "./.github/index.html") + + req = httptest.NewRequest(MethodGet, "/v2/v3/john/doe", nil) + resp, err = app.Test(req) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, 200, resp.StatusCode, "Status code") + utils.AssertEqual(t, false, resp.Header.Get(HeaderContentLength) == "") + utils.AssertEqual(t, MIMETextHTMLCharsetUTF8, resp.Header.Get(HeaderContentType)) +} + +func Test_App_Static_Wildcard(t *testing.T) { + t.Parallel() + app := New() + + app.Static("*", "./.github/index.html") + + req := httptest.NewRequest(MethodGet, "/yesyes/john/doe", nil) + resp, err := app.Test(req) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, 200, resp.StatusCode, "Status code") + utils.AssertEqual(t, false, resp.Header.Get(HeaderContentLength) == "") + utils.AssertEqual(t, MIMETextHTMLCharsetUTF8, resp.Header.Get(HeaderContentType)) + + body, err := io.ReadAll(resp.Body) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, true, strings.Contains(string(body), "Test file")) +} + +func Test_App_Static_Prefix_Wildcard(t *testing.T) { + t.Parallel() + app := New() + + app.Static("/test/*", "./.github/index.html") + + req := httptest.NewRequest(MethodGet, "/test/john/doe", nil) + resp, err := app.Test(req) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, 200, resp.StatusCode, "Status code") + utils.AssertEqual(t, false, resp.Header.Get(HeaderContentLength) == "") + utils.AssertEqual(t, MIMETextHTMLCharsetUTF8, resp.Header.Get(HeaderContentType)) + + app.Static("/my/nameisjohn*", "./.github/index.html") + + resp, err = app.Test(httptest.NewRequest(MethodGet, "/my/nameisjohn/no/its/not", nil)) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, 200, resp.StatusCode, "Status code") + utils.AssertEqual(t, false, resp.Header.Get(HeaderContentLength) == "") + utils.AssertEqual(t, MIMETextHTMLCharsetUTF8, resp.Header.Get(HeaderContentType)) + + body, err := io.ReadAll(resp.Body) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, true, strings.Contains(string(body), "Test file")) +} + +func Test_App_Static_Prefix(t *testing.T) { + t.Parallel() + app := New() + app.Static("/john", "./.github") + + req := httptest.NewRequest(MethodGet, "/john/index.html", nil) + resp, err := app.Test(req) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, 200, resp.StatusCode, "Status code") + utils.AssertEqual(t, false, resp.Header.Get(HeaderContentLength) == "") + utils.AssertEqual(t, MIMETextHTMLCharsetUTF8, resp.Header.Get(HeaderContentType)) + + app.Static("/prefix", "./.github/testdata") + + req = httptest.NewRequest(MethodGet, "/prefix/index.html", nil) + resp, err = app.Test(req) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, 200, resp.StatusCode, "Status code") + utils.AssertEqual(t, false, resp.Header.Get(HeaderContentLength) == "") + utils.AssertEqual(t, MIMETextHTMLCharsetUTF8, resp.Header.Get(HeaderContentType)) + + app.Static("/single", "./.github/testdata/testRoutes.json") + + req = httptest.NewRequest(MethodGet, "/single", nil) + resp, err = app.Test(req) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, 200, resp.StatusCode, "Status code") + utils.AssertEqual(t, false, resp.Header.Get(HeaderContentLength) == "") + utils.AssertEqual(t, MIMEApplicationJSON, resp.Header.Get(HeaderContentType)) +} + +func Test_App_Static_Trailing_Slash(t *testing.T) { + t.Parallel() + app := New() + app.Static("/john", "./.github") + + req := httptest.NewRequest(MethodGet, "/john/", nil) + resp, err := app.Test(req) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, 200, resp.StatusCode, "Status code") + utils.AssertEqual(t, false, resp.Header.Get(HeaderContentLength) == "") + utils.AssertEqual(t, MIMETextHTMLCharsetUTF8, resp.Header.Get(HeaderContentType)) + + app.Static("/john_without_index", "./.github/testdata/fs/css") + + req = httptest.NewRequest(MethodGet, "/john_without_index/", nil) + resp, err = app.Test(req) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, 404, resp.StatusCode, "Status code") + utils.AssertEqual(t, false, resp.Header.Get(HeaderContentLength) == "") + utils.AssertEqual(t, MIMETextPlainCharsetUTF8, resp.Header.Get(HeaderContentType)) + + app.Static("/john/", "./.github") + + req = httptest.NewRequest(MethodGet, "/john/", nil) + resp, err = app.Test(req) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, 200, resp.StatusCode, "Status code") + utils.AssertEqual(t, false, resp.Header.Get(HeaderContentLength) == "") + utils.AssertEqual(t, MIMETextHTMLCharsetUTF8, resp.Header.Get(HeaderContentType)) + + req = httptest.NewRequest(MethodGet, "/john", nil) + resp, err = app.Test(req) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, 200, resp.StatusCode, "Status code") + utils.AssertEqual(t, false, resp.Header.Get(HeaderContentLength) == "") + utils.AssertEqual(t, MIMETextHTMLCharsetUTF8, resp.Header.Get(HeaderContentType)) + + app.Static("/john_without_index/", "./.github/testdata/fs/css") + + req = httptest.NewRequest(MethodGet, "/john_without_index/", nil) + resp, err = app.Test(req) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, 404, resp.StatusCode, "Status code") + utils.AssertEqual(t, false, resp.Header.Get(HeaderContentLength) == "") + utils.AssertEqual(t, MIMETextPlainCharsetUTF8, resp.Header.Get(HeaderContentType)) +} + +func Test_App_Static_Next(t *testing.T) { + t.Parallel() + app := New() + app.Static("/", ".github", Static{ + Next: func(c *Ctx) bool { + // If value of the header is any other from "skip" + // c.Next() will be invoked + return c.Get("X-Custom-Header") == "skip" + }, + }) + app.Get("/", func(c *Ctx) error { + return c.SendString("You've skipped app.Static") + }) + + t.Run("app.Static is skipped: invoking Get handler", func(t *testing.T) { + t.Parallel() + req := httptest.NewRequest(MethodGet, "/", nil) + req.Header.Set("X-Custom-Header", "skip") + resp, err := app.Test(req) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, 200, resp.StatusCode) + utils.AssertEqual(t, false, resp.Header.Get(HeaderContentLength) == "") + utils.AssertEqual(t, MIMETextPlainCharsetUTF8, resp.Header.Get(HeaderContentType)) + + body, err := io.ReadAll(resp.Body) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, true, strings.Contains(string(body), "You've skipped app.Static")) + }) + + t.Run("app.Static is not skipped: serving index.html", func(t *testing.T) { + t.Parallel() + req := httptest.NewRequest(MethodGet, "/", nil) + req.Header.Set("X-Custom-Header", "don't skip") + resp, err := app.Test(req) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, 200, resp.StatusCode) + utils.AssertEqual(t, false, resp.Header.Get(HeaderContentLength) == "") + utils.AssertEqual(t, MIMETextHTMLCharsetUTF8, resp.Header.Get(HeaderContentType)) + + body, err := io.ReadAll(resp.Body) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, true, strings.Contains(string(body), "Hello, World!")) + }) +} + +// go test -run Test_App_Mixed_Routes_WithSameLen +func Test_App_Mixed_Routes_WithSameLen(t *testing.T) { + t.Parallel() + app := New() + + // middleware + app.Use(func(c *Ctx) error { + c.Set("TestHeader", "TestValue") + return c.Next() + }) + // routes with the same length + app.Static("/tesbar", "./.github") + app.Get("/foobar", func(c *Ctx) error { + c.Type("html") + return c.Send([]byte("FOO_BAR")) + }) + + // match get route + req := httptest.NewRequest(MethodGet, "/foobar", nil) + resp, err := app.Test(req) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, 200, resp.StatusCode, "Status code") + utils.AssertEqual(t, false, resp.Header.Get(HeaderContentLength) == "") + utils.AssertEqual(t, "TestValue", resp.Header.Get("TestHeader")) + utils.AssertEqual(t, "text/html", resp.Header.Get(HeaderContentType)) + + body, err := io.ReadAll(resp.Body) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, "FOO_BAR", string(body)) + + // match static route + req = httptest.NewRequest(MethodGet, "/tesbar", nil) + resp, err = app.Test(req) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, 200, resp.StatusCode, "Status code") + utils.AssertEqual(t, false, resp.Header.Get(HeaderContentLength) == "") + utils.AssertEqual(t, "TestValue", resp.Header.Get("TestHeader")) + utils.AssertEqual(t, "text/html; charset=utf-8", resp.Header.Get(HeaderContentType)) + + body, err = io.ReadAll(resp.Body) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, true, strings.Contains(string(body), "Hello, World!"), "Response: "+string(body)) + utils.AssertEqual(t, true, strings.HasPrefix(string(body), ""), "Response: "+string(body)) +} + +func Test_App_Group_Invalid(t *testing.T) { + t.Parallel() + defer func() { + if err := recover(); err != nil { + utils.AssertEqual(t, "use: invalid handler int\n", fmt.Sprintf("%v", err)) + } + }() + New().Group("/").Use(1) +} + +func Test_App_Group(t *testing.T) { + t.Parallel() + dummyHandler := testEmptyHandler + + app := New() + + grp := app.Group("/test") + grp.Get("/", dummyHandler) + testStatus200(t, app, "/test", MethodGet) + + grp.Get("/:demo?", dummyHandler) + testStatus200(t, app, "/test/john", MethodGet) + + grp.Connect("/CONNECT", dummyHandler) + testStatus200(t, app, "/test/CONNECT", MethodConnect) + + grp.Put("/PUT", dummyHandler) + testStatus200(t, app, "/test/PUT", MethodPut) + + grp.Post("/POST", dummyHandler) + testStatus200(t, app, "/test/POST", MethodPost) + + grp.Delete("/DELETE", dummyHandler) + testStatus200(t, app, "/test/DELETE", MethodDelete) + + grp.Head("/HEAD", dummyHandler) + testStatus200(t, app, "/test/HEAD", MethodHead) + + grp.Patch("/PATCH", dummyHandler) + testStatus200(t, app, "/test/PATCH", MethodPatch) + + grp.Options("/OPTIONS", dummyHandler) + testStatus200(t, app, "/test/OPTIONS", MethodOptions) + + grp.Trace("/TRACE", dummyHandler) + testStatus200(t, app, "/test/TRACE", MethodTrace) + + grp.All("/ALL", dummyHandler) + testStatus200(t, app, "/test/ALL", MethodPost) + + grp.Use(dummyHandler) + testStatus200(t, app, "/test/oke", MethodGet) + + grp.Use("/USE", dummyHandler) + testStatus200(t, app, "/test/USE/oke", MethodGet) + + api := grp.Group("/v1") + api.Post("/", dummyHandler) + + resp, err := app.Test(httptest.NewRequest(MethodPost, "/test/v1/", nil)) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, 200, resp.StatusCode, "Status code") + // utils.AssertEqual(t, "/test/v1", resp.Header.Get("Location"), "Location") + + api.Get("/users", dummyHandler) + resp, err = app.Test(httptest.NewRequest(MethodGet, "/test/v1/UsErS", nil)) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, 200, resp.StatusCode, "Status code") + // utils.AssertEqual(t, "/test/v1/users", resp.Header.Get("Location"), "Location") +} + +func Test_App_Route(t *testing.T) { + t.Parallel() + dummyHandler := testEmptyHandler + + app := New() + + grp := app.Route("/test", func(grp Router) { + grp.Get("/", dummyHandler) + grp.Get("/:demo?", dummyHandler) + grp.Connect("/CONNECT", dummyHandler) + grp.Put("/PUT", dummyHandler) + grp.Post("/POST", dummyHandler) + grp.Delete("/DELETE", dummyHandler) + grp.Head("/HEAD", dummyHandler) + grp.Patch("/PATCH", dummyHandler) + grp.Options("/OPTIONS", dummyHandler) + grp.Trace("/TRACE", dummyHandler) + grp.All("/ALL", dummyHandler) + grp.Use(dummyHandler) + grp.Use("/USE", dummyHandler) + }) + + testStatus200(t, app, "/test", MethodGet) + testStatus200(t, app, "/test/john", MethodGet) + testStatus200(t, app, "/test/CONNECT", MethodConnect) + testStatus200(t, app, "/test/PUT", MethodPut) + testStatus200(t, app, "/test/POST", MethodPost) + testStatus200(t, app, "/test/DELETE", MethodDelete) + testStatus200(t, app, "/test/HEAD", MethodHead) + testStatus200(t, app, "/test/PATCH", MethodPatch) + testStatus200(t, app, "/test/OPTIONS", MethodOptions) + testStatus200(t, app, "/test/TRACE", MethodTrace) + testStatus200(t, app, "/test/ALL", MethodPost) + testStatus200(t, app, "/test/oke", MethodGet) + testStatus200(t, app, "/test/USE/oke", MethodGet) + + grp.Route("/v1", func(grp Router) { + grp.Post("/", dummyHandler) + grp.Get("/users", dummyHandler) + }) + + resp, err := app.Test(httptest.NewRequest(MethodPost, "/test/v1/", nil)) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, 200, resp.StatusCode, "Status code") + + resp, err = app.Test(httptest.NewRequest(MethodGet, "/test/v1/UsErS", nil)) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, 200, resp.StatusCode, "Status code") +} + +func Test_App_Deep_Group(t *testing.T) { + t.Parallel() + runThroughCount := 0 + dummyHandler := func(c *Ctx) error { + runThroughCount++ + return c.Next() + } + + app := New() + gAPI := app.Group("/api", dummyHandler) + gV1 := gAPI.Group("/v1", dummyHandler) + gUser := gV1.Group("/user", dummyHandler) + gUser.Get("/authenticate", func(c *Ctx) error { + runThroughCount++ + return c.SendStatus(200) + }) + testStatus200(t, app, "/api/v1/user/authenticate", MethodGet) + utils.AssertEqual(t, 4, runThroughCount, "Loop count") +} + +// go test -run Test_App_Next_Method +func Test_App_Next_Method(t *testing.T) { + t.Parallel() + app := New() + app.config.DisableStartupMessage = true + + app.Use(func(c *Ctx) error { + utils.AssertEqual(t, MethodGet, c.Method()) + err := c.Next() + utils.AssertEqual(t, MethodGet, c.Method()) + return err + }) + + resp, err := app.Test(httptest.NewRequest(MethodGet, "/", nil)) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, 404, resp.StatusCode, "Status code") +} + +// go test -v -run=^$ -bench=Benchmark_App_ETag -benchmem -count=4 +func Benchmark_App_ETag(b *testing.B) { + app := New() + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + err := c.Send([]byte("Hello, World!")) + utils.AssertEqual(b, nil, err) + for n := 0; n < b.N; n++ { + setETag(c, false) + } + utils.AssertEqual(b, `"13-1831710635"`, string(c.Response().Header.Peek(HeaderETag))) +} + +// go test -v -run=^$ -bench=Benchmark_App_ETag_Weak -benchmem -count=4 +func Benchmark_App_ETag_Weak(b *testing.B) { + app := New() + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + utils.AssertEqual(b, nil, c.Send([]byte("Hello, World!"))) + for n := 0; n < b.N; n++ { + setETag(c, true) + } + utils.AssertEqual(b, `W/"13-1831710635"`, string(c.Response().Header.Peek(HeaderETag))) +} + +// go test -run Test_NewError +func Test_NewError(t *testing.T) { + t.Parallel() + err := NewError(StatusForbidden, "permission denied") + utils.AssertEqual(t, StatusForbidden, err.Code) + utils.AssertEqual(t, "permission denied", err.Message) +} + +// go test -run Test_Test_Timeout +func Test_Test_Timeout(t *testing.T) { + t.Parallel() + app := New() + app.config.DisableStartupMessage = true + + app.Get("/", testEmptyHandler) + + resp, err := app.Test(httptest.NewRequest(MethodGet, "/", nil), -1) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, 200, resp.StatusCode, "Status code") + + app.Get("timeout", func(c *Ctx) error { + time.Sleep(200 * time.Millisecond) + return nil + }) + + _, err = app.Test(httptest.NewRequest(MethodGet, "/timeout", nil), 20) + utils.AssertEqual(t, true, err != nil, "app.Test(req)") +} + +type errorReader int + +func (errorReader) Read([]byte) (int, error) { + return 0, errors.New("errorReader") +} + +// go test -run Test_Test_DumpError +func Test_Test_DumpError(t *testing.T) { + t.Parallel() + app := New() + app.config.DisableStartupMessage = true + + app.Get("/", testEmptyHandler) + + resp, err := app.Test(httptest.NewRequest(MethodGet, "/", errorReader(0))) + utils.AssertEqual(t, true, resp == nil) + utils.AssertEqual(t, "failed to dump request: errorReader", err.Error()) +} + +// go test -run Test_App_Handler +func Test_App_Handler(t *testing.T) { + t.Parallel() + h := New().Handler() + utils.AssertEqual(t, "fasthttp.RequestHandler", reflect.TypeOf(h).String()) +} + +type invalidView struct{} + +func (invalidView) Load() error { return errors.New("invalid view") } + +func (invalidView) Render(io.Writer, string, interface{}, ...string) error { panic("implement me") } + +// go test -run Test_App_Init_Error_View +func Test_App_Init_Error_View(t *testing.T) { + app := New(Config{Views: invalidView{}}) + + defer func() { + if err := recover(); err != nil { + utils.AssertEqual(t, "implement me", fmt.Sprintf("%v", err)) + } + }() + + err := app.config.Views.Render(nil, "", nil) + utils.AssertEqual(t, nil, err) +} + +// go test -run Test_App_Stack +func Test_App_Stack(t *testing.T) { + t.Parallel() + app := New() + + app.Use("/path0", testEmptyHandler) + app.Get("/path1", testEmptyHandler) + app.Get("/path2", testEmptyHandler) + app.Post("/path3", testEmptyHandler) + + stack := app.Stack() + methodList := app.config.RequestMethods + utils.AssertEqual(t, len(methodList), len(stack)) + utils.AssertEqual(t, 3, len(stack[app.methodInt(MethodGet)])) + utils.AssertEqual(t, 3, len(stack[app.methodInt(MethodHead)])) + utils.AssertEqual(t, 2, len(stack[app.methodInt(MethodPost)])) + utils.AssertEqual(t, 1, len(stack[app.methodInt(MethodPut)])) + utils.AssertEqual(t, 1, len(stack[app.methodInt(MethodPatch)])) + utils.AssertEqual(t, 1, len(stack[app.methodInt(MethodDelete)])) + utils.AssertEqual(t, 1, len(stack[app.methodInt(MethodConnect)])) + utils.AssertEqual(t, 1, len(stack[app.methodInt(MethodOptions)])) + utils.AssertEqual(t, 1, len(stack[app.methodInt(MethodTrace)])) +} + +// go test -run Test_App_HandlersCount +func Test_App_HandlersCount(t *testing.T) { + t.Parallel() + app := New() + + app.Use("/path0", testEmptyHandler) + app.Get("/path2", testEmptyHandler) + app.Post("/path3", testEmptyHandler) + + count := app.HandlersCount() + utils.AssertEqual(t, uint32(4), count) +} + +// go test -run Test_App_ReadTimeout +func Test_App_ReadTimeout(t *testing.T) { + t.Parallel() + app := New(Config{ + ReadTimeout: time.Nanosecond, + IdleTimeout: time.Minute, + DisableStartupMessage: true, + DisableKeepalive: true, + }) + + app.Get("/read-timeout", func(c *Ctx) error { + return c.SendString("I should not be sent") + }) + + go func() { + time.Sleep(500 * time.Millisecond) + + conn, err := net.Dial(NetworkTCP4, "127.0.0.1:4004") + utils.AssertEqual(t, nil, err) + defer func(conn net.Conn) { + err := conn.Close() + utils.AssertEqual(t, nil, err) + }(conn) + + _, err = conn.Write([]byte("HEAD /read-timeout HTTP/1.1\r\n")) + utils.AssertEqual(t, nil, err) + + buf := make([]byte, 1024) + var n int + n, err = conn.Read(buf) + + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, true, bytes.Contains(buf[:n], []byte("408 Request Timeout"))) + + utils.AssertEqual(t, nil, app.Shutdown()) + }() + + utils.AssertEqual(t, nil, app.Listen(":4004")) +} + +// go test -run Test_App_BadRequest +func Test_App_BadRequest(t *testing.T) { + t.Parallel() + app := New(Config{ + DisableStartupMessage: true, + }) + + app.Get("/bad-request", func(c *Ctx) error { + return c.SendString("I should not be sent") + }) + + go func() { + time.Sleep(500 * time.Millisecond) + conn, err := net.Dial(NetworkTCP4, "127.0.0.1:4005") + utils.AssertEqual(t, nil, err) + defer func(conn net.Conn) { + err := conn.Close() + utils.AssertEqual(t, nil, err) + }(conn) + + _, err = conn.Write([]byte("BadRequest\r\n")) + utils.AssertEqual(t, nil, err) + + buf := make([]byte, 1024) + var n int + n, err = conn.Read(buf) + utils.AssertEqual(t, nil, err) + + utils.AssertEqual(t, true, bytes.Contains(buf[:n], []byte("400 Bad Request"))) + + utils.AssertEqual(t, nil, app.Shutdown()) + }() + + utils.AssertEqual(t, nil, app.Listen(":4005")) +} + +// go test -run Test_App_SmallReadBuffer +func Test_App_SmallReadBuffer(t *testing.T) { + t.Parallel() + app := New(Config{ + ReadBufferSize: 1, + DisableStartupMessage: true, + }) + + app.Get("/small-read-buffer", func(c *Ctx) error { + return c.SendString("I should not be sent") + }) + + go func() { + time.Sleep(500 * time.Millisecond) + req, err := http.NewRequestWithContext(context.Background(), MethodGet, "http://127.0.0.1:4006/small-read-buffer", http.NoBody) + utils.AssertEqual(t, nil, err) + var client http.Client + resp, err := client.Do(req) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, 431, resp.StatusCode) + utils.AssertEqual(t, nil, app.Shutdown()) + }() + + utils.AssertEqual(t, nil, app.Listen(":4006")) +} + +func Test_App_Server(t *testing.T) { + t.Parallel() + app := New() + + utils.AssertEqual(t, false, app.Server() == nil) +} + +func Test_App_Error_In_Fasthttp_Server(t *testing.T) { + app := New() + app.config.ErrorHandler = func(ctx *Ctx, err error) error { + return errors.New("fake error") + } + app.server.GetOnly = true + + resp, err := app.Test(httptest.NewRequest(MethodPost, "/", nil)) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, 500, resp.StatusCode) +} + +// go test -race -run Test_App_New_Test_Parallel +func Test_App_New_Test_Parallel(t *testing.T) { + t.Parallel() + t.Run("Test_App_New_Test_Parallel_1", func(t *testing.T) { + t.Parallel() + app := New(Config{Immutable: true}) + _, err := app.Test(httptest.NewRequest(MethodGet, "/", nil)) + utils.AssertEqual(t, nil, err) + }) + t.Run("Test_App_New_Test_Parallel_2", func(t *testing.T) { + t.Parallel() + app := New(Config{Immutable: true}) + _, err := app.Test(httptest.NewRequest(MethodGet, "/", nil)) + utils.AssertEqual(t, nil, err) + }) +} + +func Test_App_ReadBodyStream(t *testing.T) { + t.Parallel() + app := New(Config{StreamRequestBody: true}) + app.Post("/", func(c *Ctx) error { + // Calling c.Body() automatically reads the entire stream. + return c.SendString(fmt.Sprintf("%v %s", c.Request().IsBodyStream(), c.Body())) + }) + testString := "this is a test" + resp, err := app.Test(httptest.NewRequest(MethodPost, "/", bytes.NewBufferString(testString))) + utils.AssertEqual(t, nil, err, "app.Test(req)") + body, err := io.ReadAll(resp.Body) + utils.AssertEqual(t, nil, err, "io.ReadAll(resp.Body)") + utils.AssertEqual(t, fmt.Sprintf("true %s", testString), string(body)) +} + +func Test_App_DisablePreParseMultipartForm(t *testing.T) { + t.Parallel() + // Must be used with both otherwise there is no point. + testString := "this is a test" + + app := New(Config{DisablePreParseMultipartForm: true, StreamRequestBody: true}) + app.Post("/", func(c *Ctx) error { + req := c.Request() + mpf, err := req.MultipartForm() + if err != nil { + return err + } + if !req.IsBodyStream() { + return fmt.Errorf("not a body stream") + } + file, err := mpf.File["test"][0].Open() + if err != nil { + return fmt.Errorf("failed to open: %w", err) + } + buffer := make([]byte, len(testString)) + n, err := file.Read(buffer) + if err != nil { + return fmt.Errorf("failed to read: %w", err) + } + if n != len(testString) { + return fmt.Errorf("bad read length") + } + return c.Send(buffer) + }) + b := &bytes.Buffer{} + w := multipart.NewWriter(b) + writer, err := w.CreateFormFile("test", "test") + utils.AssertEqual(t, nil, err, "w.CreateFormFile") + n, err := writer.Write([]byte(testString)) + utils.AssertEqual(t, nil, err, "writer.Write") + utils.AssertEqual(t, len(testString), n, "writer n") + utils.AssertEqual(t, nil, w.Close(), "w.Close()") + + req := httptest.NewRequest(MethodPost, "/", b) + req.Header.Set("Content-Type", w.FormDataContentType()) + resp, err := app.Test(req) + utils.AssertEqual(t, nil, err, "app.Test(req)") + body, err := io.ReadAll(resp.Body) + utils.AssertEqual(t, nil, err, "io.ReadAll(resp.Body)") + + utils.AssertEqual(t, testString, string(body)) +} + +func Test_App_Test_no_timeout_infinitely(t *testing.T) { + t.Parallel() + var err error + c := make(chan int) + + go func() { + defer func() { c <- 0 }() + app := New() + app.Get("/", func(c *Ctx) error { + runtime.Goexit() + return nil + }) + + req := httptest.NewRequest(MethodGet, "/", http.NoBody) + _, err = app.Test(req, -1) + }() + + tk := time.NewTimer(5 * time.Second) + defer tk.Stop() + + select { + case <-tk.C: + t.Error("hanging test") + t.FailNow() + case <-c: + } + + if err == nil { + t.Error("unexpected success request") + t.FailNow() + } +} + +func Test_App_SetTLSHandler(t *testing.T) { + t.Parallel() + tlsHandler := &TLSHandler{clientHelloInfo: &tls.ClientHelloInfo{ + ServerName: "example.golang", + }} + + app := New() + app.SetTLSHandler(tlsHandler) + + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + + utils.AssertEqual(t, "example.golang", c.ClientHelloInfo().ServerName) +} + +func Test_App_AddCustomRequestMethod(t *testing.T) { + t.Parallel() + methods := append(DefaultMethods, "TEST") //nolint:gocritic // We want a new slice here + app := New(Config{ + RequestMethods: methods, + }) + appMethods := app.config.RequestMethods + + // method name is always uppercase - https://datatracker.ietf.org/doc/html/rfc7231#section-4.1 + utils.AssertEqual(t, len(app.stack), len(appMethods)) + utils.AssertEqual(t, len(app.stack), len(appMethods)) + utils.AssertEqual(t, "TEST", appMethods[len(appMethods)-1]) +} + +func TestApp_GetRoutes(t *testing.T) { + t.Parallel() + app := New() + app.Use(func(c *Ctx) error { + return c.Next() + }) + handler := func(c *Ctx) error { + return c.SendStatus(StatusOK) + } + app.Delete("/delete", handler).Name("delete") + app.Post("/post", handler).Name("post") + routes := app.GetRoutes(false) + utils.AssertEqual(t, 2+len(app.config.RequestMethods), len(routes)) + methodMap := map[string]string{"/delete": "delete", "/post": "post"} + for _, route := range routes { + name, ok := methodMap[route.Path] + if ok { + utils.AssertEqual(t, name, route.Name) + } + } + + routes = app.GetRoutes(true) + utils.AssertEqual(t, 2, len(routes)) + for _, route := range routes { + name, ok := methodMap[route.Path] + utils.AssertEqual(t, true, ok) + utils.AssertEqual(t, name, route.Name) + } +} + +func Test_Middleware_Route_Naming_With_Use(t *testing.T) { + named := "named" + app := New() + + app.Get("/unnamed", func(c *Ctx) error { + return c.Next() + }) + + app.Post("/named", func(c *Ctx) error { + return c.Next() + }).Name(named) + + app.Use(func(c *Ctx) error { + return c.Next() + }) // no name - logging MW + + app.Use(func(c *Ctx) error { + return c.Next() + }).Name("corsMW") + + app.Use(func(c *Ctx) error { + return c.Next() + }).Name("compressMW") + + app.Use(func(c *Ctx) error { + return c.Next() + }) // no name - cache MW + + grp := app.Group("/pages").Name("pages.") + grp.Use(func(c *Ctx) error { + return c.Next() + }).Name("csrfMW") + + grp.Get("/home", func(c *Ctx) error { + return c.Next() + }).Name("home") + + grp.Get("/unnamed", func(c *Ctx) error { + return c.Next() + }) + + for _, route := range app.GetRoutes() { + switch route.Path { + case "/": + utils.AssertEqual(t, "compressMW", route.Name) + case "/unnamed": + utils.AssertEqual(t, "", route.Name) + case "named": + utils.AssertEqual(t, named, route.Name) + case "/pages": + utils.AssertEqual(t, "pages.csrfMW", route.Name) + case "/pages/home": + utils.AssertEqual(t, "pages.home", route.Name) + case "/pages/unnamed": + utils.AssertEqual(t, "", route.Name) + } + } +} + +func Test_Route_Naming_Issue_2671_2685(t *testing.T) { + app := New() + + app.Get("/", emptyHandler).Name("index") + utils.AssertEqual(t, "/", app.GetRoute("index").Path) + + app.Get("/a/:a_id", emptyHandler).Name("a") + utils.AssertEqual(t, "/a/:a_id", app.GetRoute("a").Path) + + app.Post("/b/:bId", emptyHandler).Name("b") + utils.AssertEqual(t, "/b/:bId", app.GetRoute("b").Path) + + c := app.Group("/c") + c.Get("", emptyHandler).Name("c.get") + utils.AssertEqual(t, "/c", app.GetRoute("c.get").Path) + + c.Post("", emptyHandler).Name("c.post") + utils.AssertEqual(t, "/c", app.GetRoute("c.post").Path) + + c.Get("/d", emptyHandler).Name("c.get.d") + utils.AssertEqual(t, "/c/d", app.GetRoute("c.get.d").Path) + + d := app.Group("/d/:d_id") + d.Get("", emptyHandler).Name("d.get") + utils.AssertEqual(t, "/d/:d_id", app.GetRoute("d.get").Path) + + d.Post("", emptyHandler).Name("d.post") + utils.AssertEqual(t, "/d/:d_id", app.GetRoute("d.post").Path) + + e := app.Group("/e/:eId") + e.Get("", emptyHandler).Name("e.get") + utils.AssertEqual(t, "/e/:eId", app.GetRoute("e.get").Path) + + e.Post("", emptyHandler).Name("e.post") + utils.AssertEqual(t, "/e/:eId", app.GetRoute("e.post").Path) + + e.Get("f", emptyHandler).Name("e.get.f") + utils.AssertEqual(t, "/e/:eId/f", app.GetRoute("e.get.f").Path) + + postGroup := app.Group("/post/:postId") + postGroup.Get("", emptyHandler).Name("post.get") + utils.AssertEqual(t, "/post/:postId", app.GetRoute("post.get").Path) + + postGroup.Post("", emptyHandler).Name("post.update") + utils.AssertEqual(t, "/post/:postId", app.GetRoute("post.update").Path) + + // Add testcase for routes use the same PATH on different methods + app.Get("/users", nil).Name("get-users") + app.Post("/users", nil).Name("add-user") + getUsers := app.GetRoute("get-users") + utils.AssertEqual(t, getUsers.Path, "/users") + + addUser := app.GetRoute("add-user") + utils.AssertEqual(t, addUser.Path, "/users") + + // Add testcase for routes use the same PATH on different methods (for groups) + newGrp := app.Group("/name-test") + newGrp.Get("/users", nil).Name("grp-get-users") + newGrp.Post("/users", nil).Name("grp-add-user") + getUsers = app.GetRoute("grp-get-users") + utils.AssertEqual(t, getUsers.Path, "/name-test/users") + + addUser = app.GetRoute("grp-add-user") + utils.AssertEqual(t, addUser.Path, "/name-test/users") + + // Add testcase for HEAD route naming + app.Get("/simple-route", emptyHandler).Name("simple-route") + app.Head("/simple-route", emptyHandler).Name("simple-route2") + + sRoute := app.GetRoute("simple-route") + utils.AssertEqual(t, sRoute.Path, "/simple-route") + + sRoute2 := app.GetRoute("simple-route2") + utils.AssertEqual(t, sRoute2.Path, "/simple-route") +} + +// go test -v -run=^$ -bench=Benchmark_Communication_Flow -benchmem -count=4 +func Benchmark_Communication_Flow(b *testing.B) { + app := New() + + app.Get("/", func(c *Ctx) error { + return c.SendString("Hello, World!") + }) + + h := app.Handler() + + fctx := &fasthttp.RequestCtx{} + fctx.Request.Header.SetMethod(MethodGet) + fctx.Request.SetRequestURI("/") + + b.ReportAllocs() + b.ResetTimer() + + for n := 0; n < b.N; n++ { + h(fctx) + } + + utils.AssertEqual(b, 200, fctx.Response.Header.StatusCode()) + utils.AssertEqual(b, "Hello, World!", string(fctx.Response.Body())) +} + +// go test -v -run=^$ -bench=Benchmark_Ctx_AcquireReleaseFlow -benchmem -count=4 +func Benchmark_Ctx_AcquireReleaseFlow(b *testing.B) { + app := New() + + fctx := &fasthttp.RequestCtx{} + + b.Run("withoutRequestCtx", func(b *testing.B) { + b.ReportAllocs() + b.ResetTimer() + + for n := 0; n < b.N; n++ { + c := app.AcquireCtx(fctx) + app.ReleaseCtx(c) + } + }) + + b.Run("withRequestCtx", func(b *testing.B) { + b.ReportAllocs() + b.ResetTimer() + + for n := 0; n < b.N; n++ { + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + app.ReleaseCtx(c) + } + }) +} diff --git a/client.go b/client.go new file mode 100644 index 0000000..ee191a6 --- /dev/null +++ b/client.go @@ -0,0 +1,1021 @@ +package fiber + +import ( + "bytes" + "crypto/tls" + "encoding/json" + "encoding/xml" + "fmt" + "io" + "mime/multipart" + "os" + "path/filepath" + "strconv" + "sync" + "time" + + "github.com/gofiber/fiber/v2/utils" + + "github.com/valyala/fasthttp" +) + +// Request represents HTTP request. +// +// It is forbidden copying Request instances. Create new instances +// and use CopyTo instead. +// +// Request instance MUST NOT be used from concurrently running goroutines. +// Copy from fasthttp +type Request = fasthttp.Request + +// Response represents HTTP response. +// +// It is forbidden copying Response instances. Create new instances +// and use CopyTo instead. +// +// Response instance MUST NOT be used from concurrently running goroutines. +// Copy from fasthttp +type Response = fasthttp.Response + +// Args represents query arguments. +// +// It is forbidden copying Args instances. Create new instances instead +// and use CopyTo(). +// +// Args instance MUST NOT be used from concurrently running goroutines. +// Copy from fasthttp +type Args = fasthttp.Args + +// RetryIfFunc signature of retry if function +// Request argument passed to RetryIfFunc, if there are any request errors. +// Copy from fasthttp +type RetryIfFunc = fasthttp.RetryIfFunc + +var defaultClient Client + +// Client implements http client. +// +// It is safe calling Client methods from concurrently running goroutines. +type Client struct { + mutex sync.RWMutex + // UserAgent is used in User-Agent request header. + UserAgent string + + // NoDefaultUserAgentHeader when set to true, causes the default + // User-Agent header to be excluded from the Request. + NoDefaultUserAgentHeader bool + + // When set by an external client of Fiber it will use the provided implementation of a + // JSONMarshal + // + // Allowing for flexibility in using another json library for encoding + JSONEncoder utils.JSONMarshal + + // When set by an external client of Fiber it will use the provided implementation of a + // JSONUnmarshal + // + // Allowing for flexibility in using another json library for decoding + JSONDecoder utils.JSONUnmarshal +} + +// Get returns an agent with http method GET. +func Get(url string) *Agent { return defaultClient.Get(url) } + +// Get returns an agent with http method GET. +func (c *Client) Get(url string) *Agent { + return c.createAgent(MethodGet, url) +} + +// Head returns an agent with http method HEAD. +func Head(url string) *Agent { return defaultClient.Head(url) } + +// Head returns an agent with http method GET. +func (c *Client) Head(url string) *Agent { + return c.createAgent(MethodHead, url) +} + +// Post sends POST request to the given URL. +func Post(url string) *Agent { return defaultClient.Post(url) } + +// Post sends POST request to the given URL. +func (c *Client) Post(url string) *Agent { + return c.createAgent(MethodPost, url) +} + +// Put sends PUT request to the given URL. +func Put(url string) *Agent { return defaultClient.Put(url) } + +// Put sends PUT request to the given URL. +func (c *Client) Put(url string) *Agent { + return c.createAgent(MethodPut, url) +} + +// Patch sends PATCH request to the given URL. +func Patch(url string) *Agent { return defaultClient.Patch(url) } + +// Patch sends PATCH request to the given URL. +func (c *Client) Patch(url string) *Agent { + return c.createAgent(MethodPatch, url) +} + +// Delete sends DELETE request to the given URL. +func Delete(url string) *Agent { return defaultClient.Delete(url) } + +// Delete sends DELETE request to the given URL. +func (c *Client) Delete(url string) *Agent { + return c.createAgent(MethodDelete, url) +} + +func (c *Client) createAgent(method, url string) *Agent { + a := AcquireAgent() + a.req.Header.SetMethod(method) + a.req.SetRequestURI(url) + + c.mutex.RLock() + a.Name = c.UserAgent + a.NoDefaultUserAgentHeader = c.NoDefaultUserAgentHeader + a.jsonDecoder = c.JSONDecoder + a.jsonEncoder = c.JSONEncoder + if a.jsonDecoder == nil { + a.jsonDecoder = json.Unmarshal + } + c.mutex.RUnlock() + + if err := a.Parse(); err != nil { + a.errs = append(a.errs, err) + } + + return a +} + +// Agent is an object storing all request data for client. +// Agent instance MUST NOT be used from concurrently running goroutines. +type Agent struct { + // Name is used in User-Agent request header. + Name string + + // NoDefaultUserAgentHeader when set to true, causes the default + // User-Agent header to be excluded from the Request. + NoDefaultUserAgentHeader bool + + // HostClient is an embedded fasthttp HostClient + *fasthttp.HostClient + + req *Request + resp *Response + dest []byte + args *Args + timeout time.Duration + errs []error + formFiles []*FormFile + debugWriter io.Writer + mw multipartWriter + jsonEncoder utils.JSONMarshal + jsonDecoder utils.JSONUnmarshal + maxRedirectsCount int + boundary string + reuse bool + parsed bool +} + +// Parse initializes URI and HostClient. +func (a *Agent) Parse() error { + if a.parsed { + return nil + } + a.parsed = true + + uri := a.req.URI() + + var isTLS bool + scheme := uri.Scheme() + if bytes.Equal(scheme, []byte(schemeHTTPS)) { + isTLS = true + } else if !bytes.Equal(scheme, []byte(schemeHTTP)) { + return fmt.Errorf("unsupported protocol %q. http and https are supported", scheme) + } + + name := a.Name + if name == "" && !a.NoDefaultUserAgentHeader { + name = defaultUserAgent + } + + a.HostClient = &fasthttp.HostClient{ + Addr: fasthttp.AddMissingPort(string(uri.Host()), isTLS), + Name: name, + NoDefaultUserAgentHeader: a.NoDefaultUserAgentHeader, + IsTLS: isTLS, + } + + return nil +} + +/************************** Header Setting **************************/ + +// Set sets the given 'key: value' header. +// +// Use Add for setting multiple header values under the same key. +func (a *Agent) Set(k, v string) *Agent { + a.req.Header.Set(k, v) + + return a +} + +// SetBytesK sets the given 'key: value' header. +// +// Use AddBytesK for setting multiple header values under the same key. +func (a *Agent) SetBytesK(k []byte, v string) *Agent { + a.req.Header.SetBytesK(k, v) + + return a +} + +// SetBytesV sets the given 'key: value' header. +// +// Use AddBytesV for setting multiple header values under the same key. +func (a *Agent) SetBytesV(k string, v []byte) *Agent { + a.req.Header.SetBytesV(k, v) + + return a +} + +// SetBytesKV sets the given 'key: value' header. +// +// Use AddBytesKV for setting multiple header values under the same key. +func (a *Agent) SetBytesKV(k, v []byte) *Agent { + a.req.Header.SetBytesKV(k, v) + + return a +} + +// Add adds the given 'key: value' header. +// +// Multiple headers with the same key may be added with this function. +// Use Set for setting a single header for the given key. +func (a *Agent) Add(k, v string) *Agent { + a.req.Header.Add(k, v) + + return a +} + +// AddBytesK adds the given 'key: value' header. +// +// Multiple headers with the same key may be added with this function. +// Use SetBytesK for setting a single header for the given key. +func (a *Agent) AddBytesK(k []byte, v string) *Agent { + a.req.Header.AddBytesK(k, v) + + return a +} + +// AddBytesV adds the given 'key: value' header. +// +// Multiple headers with the same key may be added with this function. +// Use SetBytesV for setting a single header for the given key. +func (a *Agent) AddBytesV(k string, v []byte) *Agent { + a.req.Header.AddBytesV(k, v) + + return a +} + +// AddBytesKV adds the given 'key: value' header. +// +// Multiple headers with the same key may be added with this function. +// Use SetBytesKV for setting a single header for the given key. +func (a *Agent) AddBytesKV(k, v []byte) *Agent { + a.req.Header.AddBytesKV(k, v) + + return a +} + +// ConnectionClose sets 'Connection: close' header. +func (a *Agent) ConnectionClose() *Agent { + a.req.Header.SetConnectionClose() + + return a +} + +// UserAgent sets User-Agent header value. +func (a *Agent) UserAgent(userAgent string) *Agent { + a.req.Header.SetUserAgent(userAgent) + + return a +} + +// UserAgentBytes sets User-Agent header value. +func (a *Agent) UserAgentBytes(userAgent []byte) *Agent { + a.req.Header.SetUserAgentBytes(userAgent) + + return a +} + +// Cookie sets one 'key: value' cookie. +func (a *Agent) Cookie(key, value string) *Agent { + a.req.Header.SetCookie(key, value) + + return a +} + +// CookieBytesK sets one 'key: value' cookie. +func (a *Agent) CookieBytesK(key []byte, value string) *Agent { + a.req.Header.SetCookieBytesK(key, value) + + return a +} + +// CookieBytesKV sets one 'key: value' cookie. +func (a *Agent) CookieBytesKV(key, value []byte) *Agent { + a.req.Header.SetCookieBytesKV(key, value) + + return a +} + +// Cookies sets multiple 'key: value' cookies. +func (a *Agent) Cookies(kv ...string) *Agent { + for i := 1; i < len(kv); i += 2 { + a.req.Header.SetCookie(kv[i-1], kv[i]) + } + + return a +} + +// CookiesBytesKV sets multiple 'key: value' cookies. +func (a *Agent) CookiesBytesKV(kv ...[]byte) *Agent { + for i := 1; i < len(kv); i += 2 { + a.req.Header.SetCookieBytesKV(kv[i-1], kv[i]) + } + + return a +} + +// Referer sets Referer header value. +func (a *Agent) Referer(referer string) *Agent { + a.req.Header.SetReferer(referer) + + return a +} + +// RefererBytes sets Referer header value. +func (a *Agent) RefererBytes(referer []byte) *Agent { + a.req.Header.SetRefererBytes(referer) + + return a +} + +// ContentType sets Content-Type header value. +func (a *Agent) ContentType(contentType string) *Agent { + a.req.Header.SetContentType(contentType) + + return a +} + +// ContentTypeBytes sets Content-Type header value. +func (a *Agent) ContentTypeBytes(contentType []byte) *Agent { + a.req.Header.SetContentTypeBytes(contentType) + + return a +} + +/************************** End Header Setting **************************/ + +/************************** URI Setting **************************/ + +// Host sets host for the URI. +func (a *Agent) Host(host string) *Agent { + a.req.URI().SetHost(host) + + return a +} + +// HostBytes sets host for the URI. +func (a *Agent) HostBytes(host []byte) *Agent { + a.req.URI().SetHostBytes(host) + + return a +} + +// QueryString sets URI query string. +func (a *Agent) QueryString(queryString string) *Agent { + a.req.URI().SetQueryString(queryString) + + return a +} + +// QueryStringBytes sets URI query string. +func (a *Agent) QueryStringBytes(queryString []byte) *Agent { + a.req.URI().SetQueryStringBytes(queryString) + + return a +} + +// BasicAuth sets URI username and password. +func (a *Agent) BasicAuth(username, password string) *Agent { + a.req.URI().SetUsername(username) + a.req.URI().SetPassword(password) + + return a +} + +// BasicAuthBytes sets URI username and password. +func (a *Agent) BasicAuthBytes(username, password []byte) *Agent { + a.req.URI().SetUsernameBytes(username) + a.req.URI().SetPasswordBytes(password) + + return a +} + +/************************** End URI Setting **************************/ + +/************************** Request Setting **************************/ + +// BodyString sets request body. +func (a *Agent) BodyString(bodyString string) *Agent { + a.req.SetBodyString(bodyString) + + return a +} + +// Body sets request body. +func (a *Agent) Body(body []byte) *Agent { + a.req.SetBody(body) + + return a +} + +// BodyStream sets request body stream and, optionally body size. +// +// If bodySize is >= 0, then the bodyStream must provide exactly bodySize bytes +// before returning io.EOF. +// +// If bodySize < 0, then bodyStream is read until io.EOF. +// +// bodyStream.Close() is called after finishing reading all body data +// if it implements io.Closer. +// +// Note that GET and HEAD requests cannot have body. +func (a *Agent) BodyStream(bodyStream io.Reader, bodySize int) *Agent { + a.req.SetBodyStream(bodyStream, bodySize) + + return a +} + +// JSON sends a JSON request. +func (a *Agent) JSON(v interface{}, ctype ...string) *Agent { + if a.jsonEncoder == nil { + a.jsonEncoder = json.Marshal + } + + if len(ctype) > 0 { + a.req.Header.SetContentType(ctype[0]) + } else { + a.req.Header.SetContentType(MIMEApplicationJSON) + } + + if body, err := a.jsonEncoder(v); err != nil { + a.errs = append(a.errs, err) + } else { + a.req.SetBody(body) + } + + return a +} + +// XML sends an XML request. +func (a *Agent) XML(v interface{}) *Agent { + a.req.Header.SetContentType(MIMEApplicationXML) + + if body, err := xml.Marshal(v); err != nil { + a.errs = append(a.errs, err) + } else { + a.req.SetBody(body) + } + + return a +} + +// Form sends form request with body if args is non-nil. +// +// It is recommended obtaining args via AcquireArgs and release it +// manually in performance-critical code. +func (a *Agent) Form(args *Args) *Agent { + a.req.Header.SetContentType(MIMEApplicationForm) + + if args != nil { + a.req.SetBody(args.QueryString()) + } + + return a +} + +// FormFile represents multipart form file +type FormFile struct { + // Fieldname is form file's field name + Fieldname string + // Name is form file's name + Name string + // Content is form file's content + Content []byte + // autoRelease indicates if returns the object + // acquired via AcquireFormFile to the pool. + autoRelease bool +} + +// FileData appends files for multipart form request. +// +// It is recommended obtaining formFile via AcquireFormFile and release it +// manually in performance-critical code. +func (a *Agent) FileData(formFiles ...*FormFile) *Agent { + a.formFiles = append(a.formFiles, formFiles...) + + return a +} + +// SendFile reads file and appends it to multipart form request. +func (a *Agent) SendFile(filename string, fieldname ...string) *Agent { + content, err := os.ReadFile(filepath.Clean(filename)) + if err != nil { + a.errs = append(a.errs, err) + return a + } + + ff := AcquireFormFile() + if len(fieldname) > 0 && fieldname[0] != "" { + ff.Fieldname = fieldname[0] + } else { + ff.Fieldname = "file" + strconv.Itoa(len(a.formFiles)+1) + } + ff.Name = filepath.Base(filename) + ff.Content = append(ff.Content, content...) + ff.autoRelease = true + + a.formFiles = append(a.formFiles, ff) + + return a +} + +// SendFiles reads files and appends them to multipart form request. +// +// Examples: +// +// SendFile("/path/to/file1", "fieldname1", "/path/to/file2") +func (a *Agent) SendFiles(filenamesAndFieldnames ...string) *Agent { + pairs := len(filenamesAndFieldnames) + if pairs&1 == 1 { + filenamesAndFieldnames = append(filenamesAndFieldnames, "") + } + + for i := 0; i < pairs; i += 2 { + a.SendFile(filenamesAndFieldnames[i], filenamesAndFieldnames[i+1]) + } + + return a +} + +// Boundary sets boundary for multipart form request. +func (a *Agent) Boundary(boundary string) *Agent { + a.boundary = boundary + + return a +} + +// MultipartForm sends multipart form request with k-v and files. +// +// It is recommended obtaining args via AcquireArgs and release it +// manually in performance-critical code. +func (a *Agent) MultipartForm(args *Args) *Agent { + if a.mw == nil { + a.mw = multipart.NewWriter(a.req.BodyWriter()) + } + + if a.boundary != "" { + if err := a.mw.SetBoundary(a.boundary); err != nil { + a.errs = append(a.errs, err) + return a + } + } + + a.req.Header.SetMultipartFormBoundary(a.mw.Boundary()) + + if args != nil { + args.VisitAll(func(key, value []byte) { + if err := a.mw.WriteField(utils.UnsafeString(key), utils.UnsafeString(value)); err != nil { + a.errs = append(a.errs, err) + } + }) + } + + for _, ff := range a.formFiles { + w, err := a.mw.CreateFormFile(ff.Fieldname, ff.Name) + if err != nil { + a.errs = append(a.errs, err) + continue + } + if _, err = w.Write(ff.Content); err != nil { + a.errs = append(a.errs, err) + } + } + + if err := a.mw.Close(); err != nil { + a.errs = append(a.errs, err) + } + + return a +} + +/************************** End Request Setting **************************/ + +/************************** Agent Setting **************************/ + +// Debug mode enables logging request and response detail +func (a *Agent) Debug(w ...io.Writer) *Agent { + a.debugWriter = os.Stdout + if len(w) > 0 { + a.debugWriter = w[0] + } + + return a +} + +// Timeout sets request timeout duration. +func (a *Agent) Timeout(timeout time.Duration) *Agent { + a.timeout = timeout + + return a +} + +// Reuse enables the Agent instance to be used again after one request. +// +// If agent is reusable, then it should be released manually when it is no +// longer used. +func (a *Agent) Reuse() *Agent { + a.reuse = true + + return a +} + +// InsecureSkipVerify controls whether the Agent verifies the server +// certificate chain and host name. +func (a *Agent) InsecureSkipVerify() *Agent { + if a.HostClient.TLSConfig == nil { + a.HostClient.TLSConfig = &tls.Config{InsecureSkipVerify: true} //nolint:gosec // We explicitly let the user set insecure mode here + } else { + a.HostClient.TLSConfig.InsecureSkipVerify = true + } + + return a +} + +// TLSConfig sets tls config. +func (a *Agent) TLSConfig(config *tls.Config) *Agent { + a.HostClient.TLSConfig = config + + return a +} + +// MaxRedirectsCount sets max redirect count for GET and HEAD. +func (a *Agent) MaxRedirectsCount(count int) *Agent { + a.maxRedirectsCount = count + + return a +} + +// JSONEncoder sets custom json encoder. +func (a *Agent) JSONEncoder(jsonEncoder utils.JSONMarshal) *Agent { + a.jsonEncoder = jsonEncoder + + return a +} + +// JSONDecoder sets custom json decoder. +func (a *Agent) JSONDecoder(jsonDecoder utils.JSONUnmarshal) *Agent { + a.jsonDecoder = jsonDecoder + + return a +} + +// Request returns Agent request instance. +func (a *Agent) Request() *Request { + return a.req +} + +// SetResponse sets custom response for the Agent instance. +// +// It is recommended obtaining custom response via AcquireResponse and release it +// manually in performance-critical code. +func (a *Agent) SetResponse(customResp *Response) *Agent { + a.resp = customResp + + return a +} + +// Dest sets custom dest. +// +// The contents of dest will be replaced by the response body, if the dest +// is too small a new slice will be allocated. +func (a *Agent) Dest(dest []byte) *Agent { + a.dest = dest + + return a +} + +// RetryIf controls whether a retry should be attempted after an error. +// +// By default, will use isIdempotent function from fasthttp +func (a *Agent) RetryIf(retryIf RetryIfFunc) *Agent { + a.HostClient.RetryIf = retryIf + return a +} + +/************************** End Agent Setting **************************/ + +// Bytes returns the status code, bytes body and errors of url. +// +// it's not safe to use Agent after calling [Agent.Bytes] +func (a *Agent) Bytes() (int, []byte, []error) { + defer a.release() + return a.bytes() +} + +func (a *Agent) bytes() (code int, body []byte, errs []error) { //nolint:nonamedreturns,revive // We want to overwrite the body in a deferred func. TODO: Check if we really need to do this. We eventually want to get rid of all named returns. + if errs = append(errs, a.errs...); len(errs) > 0 { + return code, body, errs + } + + var ( + req = a.req + resp *Response + nilResp bool + ) + + if a.resp == nil { + resp = AcquireResponse() + nilResp = true + } else { + resp = a.resp + } + + defer func() { + if a.debugWriter != nil { + printDebugInfo(req, resp, a.debugWriter) + } + + if len(errs) == 0 { + code = resp.StatusCode() + } + + body = append(a.dest, resp.Body()...) //nolint:gocritic // We want to append to the returned slice here + + if nilResp { + ReleaseResponse(resp) + } + }() + + if a.timeout > 0 { + if err := a.HostClient.DoTimeout(req, resp, a.timeout); err != nil { + errs = append(errs, err) + return code, body, errs + } + } else if a.maxRedirectsCount > 0 && (string(req.Header.Method()) == MethodGet || string(req.Header.Method()) == MethodHead) { + if err := a.HostClient.DoRedirects(req, resp, a.maxRedirectsCount); err != nil { + errs = append(errs, err) + return code, body, errs + } + } else if err := a.HostClient.Do(req, resp); err != nil { + errs = append(errs, err) + } + + return code, body, errs +} + +func printDebugInfo(req *Request, resp *Response, w io.Writer) { + msg := fmt.Sprintf("Connected to %s(%s)\r\n\r\n", req.URI().Host(), resp.RemoteAddr()) + _, _ = w.Write(utils.UnsafeBytes(msg)) //nolint:errcheck // This will never fail + _, _ = req.WriteTo(w) //nolint:errcheck // This will never fail + _, _ = resp.WriteTo(w) //nolint:errcheck // This will never fail +} + +// String returns the status code, string body and errors of url. +// +// it's not safe to use Agent after calling [Agent.String] +func (a *Agent) String() (int, string, []error) { + defer a.release() + code, body, errs := a.bytes() + // TODO: There might be a data race here on body. Maybe use utils.CopyBytes on it? + + return code, utils.UnsafeString(body), errs +} + +// Struct returns the status code, bytes body and errors of URL. +// And bytes body will be unmarshalled to given v. +// +// it's not safe to use Agent after calling [Agent.Struct] +func (a *Agent) Struct(v interface{}) (int, []byte, []error) { + defer a.release() + + code, body, errs := a.bytes() + if len(errs) > 0 { + return code, body, errs + } + + // TODO: This should only be done once + if a.jsonDecoder == nil { + a.jsonDecoder = json.Unmarshal + } + + if err := a.jsonDecoder(body, v); err != nil { + errs = append(errs, err) + } + + return code, body, errs +} + +func (a *Agent) release() { + if !a.reuse { + ReleaseAgent(a) + } else { + a.errs = a.errs[:0] + } +} + +func (a *Agent) reset() { + a.HostClient = nil + a.req.Reset() + a.resp = nil + a.dest = nil + a.timeout = 0 + a.args = nil + a.errs = a.errs[:0] + a.debugWriter = nil + a.mw = nil + a.reuse = false + a.parsed = false + a.maxRedirectsCount = 0 + a.boundary = "" + a.Name = "" + a.NoDefaultUserAgentHeader = false + for i, ff := range a.formFiles { + if ff.autoRelease { + ReleaseFormFile(ff) + } + a.formFiles[i] = nil + } + a.formFiles = a.formFiles[:0] +} + +var ( + clientPool sync.Pool + agentPool = sync.Pool{ + New: func() interface{} { + return &Agent{req: &Request{}} + }, + } + responsePool sync.Pool + argsPool sync.Pool + formFilePool sync.Pool +) + +// AcquireClient returns an empty Client instance from client pool. +// +// The returned Client instance may be passed to ReleaseClient when it is +// no longer needed. This allows Client recycling, reduces GC pressure +// and usually improves performance. +func AcquireClient() *Client { + v := clientPool.Get() + if v == nil { + return &Client{} + } + c, ok := v.(*Client) + if !ok { + panic(fmt.Errorf("failed to type-assert to *Client")) + } + return c +} + +// ReleaseClient returns c acquired via AcquireClient to client pool. +// +// It is forbidden accessing req and/or it's members after returning +// it to client pool. +func ReleaseClient(c *Client) { + c.UserAgent = "" + c.NoDefaultUserAgentHeader = false + c.JSONEncoder = nil + c.JSONDecoder = nil + + clientPool.Put(c) +} + +// AcquireAgent returns an empty Agent instance from Agent pool. +// +// The returned Agent instance may be passed to ReleaseAgent when it is +// no longer needed. This allows Agent recycling, reduces GC pressure +// and usually improves performance. +func AcquireAgent() *Agent { + a, ok := agentPool.Get().(*Agent) + if !ok { + panic(fmt.Errorf("failed to type-assert to *Agent")) + } + return a +} + +// ReleaseAgent returns an acquired via AcquireAgent to Agent pool. +// +// It is forbidden accessing req and/or it's members after returning +// it to Agent pool. +func ReleaseAgent(a *Agent) { + a.reset() + agentPool.Put(a) +} + +// AcquireResponse returns an empty Response instance from response pool. +// +// The returned Response instance may be passed to ReleaseResponse when it is +// no longer needed. This allows Response recycling, reduces GC pressure +// and usually improves performance. +// Copy from fasthttp +func AcquireResponse() *Response { + v := responsePool.Get() + if v == nil { + return &Response{} + } + r, ok := v.(*Response) + if !ok { + panic(fmt.Errorf("failed to type-assert to *Response")) + } + return r +} + +// ReleaseResponse return resp acquired via AcquireResponse to response pool. +// +// It is forbidden accessing resp and/or it's members after returning +// it to response pool. +// Copy from fasthttp +func ReleaseResponse(resp *Response) { + resp.Reset() + responsePool.Put(resp) +} + +// AcquireArgs returns an empty Args object from the pool. +// +// The returned Args may be returned to the pool with ReleaseArgs +// when no longer needed. This allows reducing GC load. +// Copy from fasthttp +func AcquireArgs() *Args { + v := argsPool.Get() + if v == nil { + return &Args{} + } + a, ok := v.(*Args) + if !ok { + panic(fmt.Errorf("failed to type-assert to *Args")) + } + return a +} + +// ReleaseArgs returns the object acquired via AcquireArgs to the pool. +// +// String not access the released Args object, otherwise data races may occur. +// Copy from fasthttp +func ReleaseArgs(a *Args) { + a.Reset() + argsPool.Put(a) +} + +// AcquireFormFile returns an empty FormFile object from the pool. +// +// The returned FormFile may be returned to the pool with ReleaseFormFile +// when no longer needed. This allows reducing GC load. +func AcquireFormFile() *FormFile { + v := formFilePool.Get() + if v == nil { + return &FormFile{} + } + ff, ok := v.(*FormFile) + if !ok { + panic(fmt.Errorf("failed to type-assert to *FormFile")) + } + return ff +} + +// ReleaseFormFile returns the object acquired via AcquireFormFile to the pool. +// +// String not access the released FormFile object, otherwise data races may occur. +func ReleaseFormFile(ff *FormFile) { + ff.Fieldname = "" + ff.Name = "" + ff.Content = ff.Content[:0] + ff.autoRelease = false + + formFilePool.Put(ff) +} + +const ( + defaultUserAgent = "fiber" +) + +type multipartWriter interface { + Boundary() string + SetBoundary(boundary string) error + CreateFormFile(fieldname, filename string) (io.Writer, error) + WriteField(fieldname, value string) error + Close() error +} diff --git a/client_test.go b/client_test.go new file mode 100644 index 0000000..580e2d3 --- /dev/null +++ b/client_test.go @@ -0,0 +1,1249 @@ +//nolint:wrapcheck // We must not wrap errors in tests +package fiber + +import ( + "bytes" + "crypto/tls" + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "io" + "mime/multipart" + "net" + "os" + "path/filepath" + "regexp" + "strings" + "testing" + "time" + + "github.com/gofiber/fiber/v2/internal/tlstest" + "github.com/gofiber/fiber/v2/utils" + + "github.com/valyala/fasthttp/fasthttputil" +) + +func Test_Client_Invalid_URL(t *testing.T) { + t.Parallel() + + ln := fasthttputil.NewInmemoryListener() + + app := New(Config{DisableStartupMessage: true}) + + app.Get("/", func(c *Ctx) error { + return c.SendString(c.Hostname()) + }) + + go func() { utils.AssertEqual(t, nil, app.Listener(ln)) }() + + a := Get("http://example.com\r\n\r\nGET /\r\n\r\n") + + a.HostClient.Dial = func(addr string) (net.Conn, error) { return ln.Dial() } + + _, body, errs := a.String() + + utils.AssertEqual(t, "", body) + utils.AssertEqual(t, 1, len(errs)) + utils.AssertEqual(t, "missing required Host header in request", errs[0].Error()) +} + +func Test_Client_Unsupported_Protocol(t *testing.T) { + t.Parallel() + + a := Get("ftp://example.com") + + _, body, errs := a.String() + + utils.AssertEqual(t, "", body) + utils.AssertEqual(t, 1, len(errs)) + utils.AssertEqual(t, `unsupported protocol "ftp". http and https are supported`, + errs[0].Error()) +} + +func Test_Client_Get(t *testing.T) { + t.Parallel() + + ln := fasthttputil.NewInmemoryListener() + + app := New(Config{DisableStartupMessage: true}) + + app.Get("/", func(c *Ctx) error { + return c.SendString(c.Hostname()) + }) + + go func() { utils.AssertEqual(t, nil, app.Listener(ln)) }() + + for i := 0; i < 5; i++ { + a := Get("http://example.com") + + a.HostClient.Dial = func(addr string) (net.Conn, error) { return ln.Dial() } + + code, body, errs := a.String() + + utils.AssertEqual(t, StatusOK, code) + utils.AssertEqual(t, "example.com", body) + utils.AssertEqual(t, 0, len(errs)) + } +} + +func Test_Client_Head(t *testing.T) { + t.Parallel() + + ln := fasthttputil.NewInmemoryListener() + + app := New(Config{DisableStartupMessage: true}) + + app.Get("/", func(c *Ctx) error { + return c.SendString(c.Hostname()) + }) + + go func() { utils.AssertEqual(t, nil, app.Listener(ln)) }() + + for i := 0; i < 5; i++ { + a := Head("http://example.com") + + a.HostClient.Dial = func(addr string) (net.Conn, error) { return ln.Dial() } + + code, body, errs := a.String() + + utils.AssertEqual(t, StatusOK, code) + utils.AssertEqual(t, "", body) + utils.AssertEqual(t, 0, len(errs)) + } +} + +func Test_Client_Post(t *testing.T) { + t.Parallel() + + ln := fasthttputil.NewInmemoryListener() + + app := New(Config{DisableStartupMessage: true}) + + app.Post("/", func(c *Ctx) error { + return c.Status(StatusCreated). + SendString(c.FormValue("foo")) + }) + + go func() { utils.AssertEqual(t, nil, app.Listener(ln)) }() + + for i := 0; i < 5; i++ { + args := AcquireArgs() + + args.Set("foo", "bar") + + a := Post("http://example.com"). + Form(args) + + a.HostClient.Dial = func(addr string) (net.Conn, error) { return ln.Dial() } + + code, body, errs := a.String() + + utils.AssertEqual(t, StatusCreated, code) + utils.AssertEqual(t, "bar", body) + utils.AssertEqual(t, 0, len(errs)) + + ReleaseArgs(args) + } +} + +func Test_Client_Put(t *testing.T) { + t.Parallel() + + ln := fasthttputil.NewInmemoryListener() + + app := New(Config{DisableStartupMessage: true}) + + app.Put("/", func(c *Ctx) error { + return c.SendString(c.FormValue("foo")) + }) + + go func() { utils.AssertEqual(t, nil, app.Listener(ln)) }() + + for i := 0; i < 5; i++ { + args := AcquireArgs() + + args.Set("foo", "bar") + + a := Put("http://example.com"). + Form(args) + + a.HostClient.Dial = func(addr string) (net.Conn, error) { return ln.Dial() } + + code, body, errs := a.String() + + utils.AssertEqual(t, StatusOK, code) + utils.AssertEqual(t, "bar", body) + utils.AssertEqual(t, 0, len(errs)) + + ReleaseArgs(args) + } +} + +func Test_Client_Patch(t *testing.T) { + t.Parallel() + + ln := fasthttputil.NewInmemoryListener() + + app := New(Config{DisableStartupMessage: true}) + + app.Patch("/", func(c *Ctx) error { + return c.SendString(c.FormValue("foo")) + }) + + go func() { utils.AssertEqual(t, nil, app.Listener(ln)) }() + + for i := 0; i < 5; i++ { + args := AcquireArgs() + + args.Set("foo", "bar") + + a := Patch("http://example.com"). + Form(args) + + a.HostClient.Dial = func(addr string) (net.Conn, error) { return ln.Dial() } + + code, body, errs := a.String() + + utils.AssertEqual(t, StatusOK, code) + utils.AssertEqual(t, "bar", body) + utils.AssertEqual(t, 0, len(errs)) + + ReleaseArgs(args) + } +} + +func Test_Client_Delete(t *testing.T) { + t.Parallel() + + ln := fasthttputil.NewInmemoryListener() + + app := New(Config{DisableStartupMessage: true}) + + app.Delete("/", func(c *Ctx) error { + return c.Status(StatusNoContent). + SendString("deleted") + }) + + go func() { utils.AssertEqual(t, nil, app.Listener(ln)) }() + + for i := 0; i < 5; i++ { + args := AcquireArgs() + + a := Delete("http://example.com") + + a.HostClient.Dial = func(addr string) (net.Conn, error) { return ln.Dial() } + + code, body, errs := a.String() + + utils.AssertEqual(t, StatusNoContent, code) + utils.AssertEqual(t, "", body) + utils.AssertEqual(t, 0, len(errs)) + + ReleaseArgs(args) + } +} + +func Test_Client_UserAgent(t *testing.T) { + t.Parallel() + + ln := fasthttputil.NewInmemoryListener() + + app := New(Config{DisableStartupMessage: true}) + + app.Get("/", func(c *Ctx) error { + return c.Send(c.Request().Header.UserAgent()) + }) + + go func() { utils.AssertEqual(t, nil, app.Listener(ln)) }() + + t.Run("default", func(t *testing.T) { + t.Parallel() + for i := 0; i < 5; i++ { + a := Get("http://example.com") + + a.HostClient.Dial = func(addr string) (net.Conn, error) { return ln.Dial() } + + code, body, errs := a.String() + + utils.AssertEqual(t, StatusOK, code) + utils.AssertEqual(t, defaultUserAgent, body) + utils.AssertEqual(t, 0, len(errs)) + } + }) + + t.Run("custom", func(t *testing.T) { + t.Parallel() + for i := 0; i < 5; i++ { + c := AcquireClient() + c.UserAgent = "ua" + + a := c.Get("http://example.com") + + a.HostClient.Dial = func(addr string) (net.Conn, error) { return ln.Dial() } + + code, body, errs := a.String() + + utils.AssertEqual(t, StatusOK, code) + utils.AssertEqual(t, "ua", body) + utils.AssertEqual(t, 0, len(errs)) + ReleaseClient(c) + } + }) +} + +func Test_Client_Agent_Set_Or_Add_Headers(t *testing.T) { + t.Parallel() + handler := func(c *Ctx) error { + c.Request().Header.VisitAll(func(key, value []byte) { + if k := string(key); k == "K1" || k == "K2" { + _, err := c.Write(key) + utils.AssertEqual(t, nil, err) + _, err = c.Write(value) + utils.AssertEqual(t, nil, err) + } + }) + return nil + } + + wrapAgent := func(a *Agent) { + a.Set("k1", "v1"). + SetBytesK([]byte("k1"), "v1"). + SetBytesV("k1", []byte("v1")). + AddBytesK([]byte("k1"), "v11"). + AddBytesV("k1", []byte("v22")). + AddBytesKV([]byte("k1"), []byte("v33")). + SetBytesKV([]byte("k2"), []byte("v2")). + Add("k2", "v22") + } + + testAgent(t, handler, wrapAgent, "K1v1K1v11K1v22K1v33K2v2K2v22") +} + +func Test_Client_Agent_Connection_Close(t *testing.T) { + t.Parallel() + handler := func(c *Ctx) error { + if c.Request().Header.ConnectionClose() { + return c.SendString("close") + } + return c.SendString("not close") + } + + wrapAgent := func(a *Agent) { + a.ConnectionClose() + } + + testAgent(t, handler, wrapAgent, "close") +} + +func Test_Client_Agent_UserAgent(t *testing.T) { + t.Parallel() + handler := func(c *Ctx) error { + return c.Send(c.Request().Header.UserAgent()) + } + + wrapAgent := func(a *Agent) { + a.UserAgent("ua"). + UserAgentBytes([]byte("ua")) + } + + testAgent(t, handler, wrapAgent, "ua") +} + +func Test_Client_Agent_Cookie(t *testing.T) { + t.Parallel() + handler := func(c *Ctx) error { + return c.SendString( + c.Cookies("k1") + c.Cookies("k2") + c.Cookies("k3") + c.Cookies("k4")) + } + + wrapAgent := func(a *Agent) { + a.Cookie("k1", "v1"). + CookieBytesK([]byte("k2"), "v2"). + CookieBytesKV([]byte("k2"), []byte("v2")). + Cookies("k3", "v3", "k4", "v4"). + CookiesBytesKV([]byte("k3"), []byte("v3"), []byte("k4"), []byte("v4")) + } + + testAgent(t, handler, wrapAgent, "v1v2v3v4") +} + +func Test_Client_Agent_Referer(t *testing.T) { + t.Parallel() + handler := func(c *Ctx) error { + return c.Send(c.Request().Header.Referer()) + } + + wrapAgent := func(a *Agent) { + a.Referer("http://referer.com"). + RefererBytes([]byte("http://referer.com")) + } + + testAgent(t, handler, wrapAgent, "http://referer.com") +} + +func Test_Client_Agent_ContentType(t *testing.T) { + t.Parallel() + handler := func(c *Ctx) error { + return c.Send(c.Request().Header.ContentType()) + } + + wrapAgent := func(a *Agent) { + a.ContentType("custom-type"). + ContentTypeBytes([]byte("custom-type")) + } + + testAgent(t, handler, wrapAgent, "custom-type") +} + +func Test_Client_Agent_Host(t *testing.T) { + t.Parallel() + + ln := fasthttputil.NewInmemoryListener() + + app := New(Config{DisableStartupMessage: true}) + + app.Get("/", func(c *Ctx) error { + return c.SendString(c.Hostname()) + }) + + go func() { utils.AssertEqual(t, nil, app.Listener(ln)) }() + + a := Get("http://1.1.1.1:8080"). + Host("example.com"). + HostBytes([]byte("example.com")) + + utils.AssertEqual(t, "1.1.1.1:8080", a.HostClient.Addr) + + a.HostClient.Dial = func(addr string) (net.Conn, error) { return ln.Dial() } + + code, body, errs := a.String() + + utils.AssertEqual(t, StatusOK, code) + utils.AssertEqual(t, "example.com", body) + utils.AssertEqual(t, 0, len(errs)) +} + +func Test_Client_Agent_QueryString(t *testing.T) { + t.Parallel() + handler := func(c *Ctx) error { + return c.Send(c.Request().URI().QueryString()) + } + + wrapAgent := func(a *Agent) { + a.QueryString("foo=bar&bar=baz"). + QueryStringBytes([]byte("foo=bar&bar=baz")) + } + + testAgent(t, handler, wrapAgent, "foo=bar&bar=baz") +} + +func Test_Client_Agent_BasicAuth(t *testing.T) { + t.Parallel() + handler := func(c *Ctx) error { + // Get authorization header + auth := c.Get(HeaderAuthorization) + // Decode the header contents + raw, err := base64.StdEncoding.DecodeString(auth[6:]) + utils.AssertEqual(t, nil, err) + + return c.Send(raw) + } + + wrapAgent := func(a *Agent) { + a.BasicAuth("foo", "bar"). + BasicAuthBytes([]byte("foo"), []byte("bar")) + } + + testAgent(t, handler, wrapAgent, "foo:bar") +} + +func Test_Client_Agent_BodyString(t *testing.T) { + t.Parallel() + handler := func(c *Ctx) error { + return c.Send(c.Request().Body()) + } + + wrapAgent := func(a *Agent) { + a.BodyString("foo=bar&bar=baz") + } + + testAgent(t, handler, wrapAgent, "foo=bar&bar=baz") +} + +func Test_Client_Agent_Body(t *testing.T) { + t.Parallel() + handler := func(c *Ctx) error { + return c.Send(c.Request().Body()) + } + + wrapAgent := func(a *Agent) { + a.Body([]byte("foo=bar&bar=baz")) + } + + testAgent(t, handler, wrapAgent, "foo=bar&bar=baz") +} + +func Test_Client_Agent_BodyStream(t *testing.T) { + t.Parallel() + handler := func(c *Ctx) error { + return c.Send(c.Request().Body()) + } + + wrapAgent := func(a *Agent) { + a.BodyStream(strings.NewReader("body stream"), -1) + } + + testAgent(t, handler, wrapAgent, "body stream") +} + +func Test_Client_Agent_Custom_Response(t *testing.T) { + t.Parallel() + + ln := fasthttputil.NewInmemoryListener() + + app := New(Config{DisableStartupMessage: true}) + + app.Get("/", func(c *Ctx) error { + return c.SendString("custom") + }) + + go func() { utils.AssertEqual(t, nil, app.Listener(ln)) }() + + for i := 0; i < 5; i++ { + a := AcquireAgent() + resp := AcquireResponse() + + req := a.Request() + req.Header.SetMethod(MethodGet) + req.SetRequestURI("http://example.com") + + utils.AssertEqual(t, nil, a.Parse()) + + a.HostClient.Dial = func(addr string) (net.Conn, error) { return ln.Dial() } + + code, body, errs := a.SetResponse(resp). + String() + + utils.AssertEqual(t, StatusOK, code) + utils.AssertEqual(t, "custom", body) + utils.AssertEqual(t, "custom", string(resp.Body())) + utils.AssertEqual(t, 0, len(errs)) + + ReleaseResponse(resp) + } +} + +func Test_Client_Agent_Dest(t *testing.T) { + t.Parallel() + + ln := fasthttputil.NewInmemoryListener() + + app := New(Config{DisableStartupMessage: true}) + + app.Get("/", func(c *Ctx) error { + return c.SendString("dest") + }) + + go func() { utils.AssertEqual(t, nil, app.Listener(ln)) }() + + t.Run("small dest", func(t *testing.T) { + t.Parallel() + dest := []byte("de") + + a := Get("http://example.com") + + a.HostClient.Dial = func(addr string) (net.Conn, error) { return ln.Dial() } + + code, body, errs := a.Dest(dest[:0]).String() + + utils.AssertEqual(t, StatusOK, code) + utils.AssertEqual(t, "dest", body) + utils.AssertEqual(t, "de", string(dest)) + utils.AssertEqual(t, 0, len(errs)) + }) + + t.Run("enough dest", func(t *testing.T) { + t.Parallel() + dest := []byte("foobar") + + a := Get("http://example.com") + + a.HostClient.Dial = func(addr string) (net.Conn, error) { return ln.Dial() } + + code, body, errs := a.Dest(dest[:0]).String() + + utils.AssertEqual(t, StatusOK, code) + utils.AssertEqual(t, "dest", body) + utils.AssertEqual(t, "destar", string(dest)) + utils.AssertEqual(t, 0, len(errs)) + }) +} + +// readErrorConn is a struct for testing retryIf +type readErrorConn struct { + net.Conn +} + +func (*readErrorConn) Read(_ []byte) (int, error) { + return 0, fmt.Errorf("error") +} + +func (*readErrorConn) Write(p []byte) (int, error) { + return len(p), nil +} + +func (*readErrorConn) Close() error { + return nil +} + +func (*readErrorConn) LocalAddr() net.Addr { + return nil +} + +func (*readErrorConn) RemoteAddr() net.Addr { + return nil +} + +func (*readErrorConn) SetReadDeadline(_ time.Time) error { + return nil +} + +func (*readErrorConn) SetWriteDeadline(_ time.Time) error { + return nil +} + +func Test_Client_Agent_RetryIf(t *testing.T) { + t.Parallel() + + ln := fasthttputil.NewInmemoryListener() + + app := New(Config{DisableStartupMessage: true}) + + go func() { utils.AssertEqual(t, nil, app.Listener(ln)) }() + + a := Post("http://example.com"). + RetryIf(func(req *Request) bool { + return true + }) + dialsCount := 0 + a.HostClient.Dial = func(addr string) (net.Conn, error) { + dialsCount++ + switch dialsCount { + case 1: + return &readErrorConn{}, nil + case 2: + return &readErrorConn{}, nil + case 3: + return &readErrorConn{}, nil + case 4: + return ln.Dial() + default: + t.Fatalf("unexpected number of dials: %d", dialsCount) + } + panic("unreachable") + } + + _, _, errs := a.String() + utils.AssertEqual(t, dialsCount, 4) + utils.AssertEqual(t, 0, len(errs)) +} + +func Test_Client_Agent_Json(t *testing.T) { + t.Parallel() + // Test without ctype parameter + handler := func(c *Ctx) error { + utils.AssertEqual(t, MIMEApplicationJSON, string(c.Request().Header.ContentType())) + + return c.Send(c.Request().Body()) + } + + wrapAgent := func(a *Agent) { + a.JSON(data{Success: true}) + } + + testAgent(t, handler, wrapAgent, `{"success":true}`) + + // Test with ctype parameter + handler = func(c *Ctx) error { + utils.AssertEqual(t, "application/problem+json", string(c.Request().Header.ContentType())) + + return c.Send(c.Request().Body()) + } + + wrapAgent = func(a *Agent) { + a.JSON(data{Success: true}, "application/problem+json") + } + + testAgent(t, handler, wrapAgent, `{"success":true}`) +} + +func Test_Client_Agent_Json_Error(t *testing.T) { + t.Parallel() + a := Get("http://example.com"). + JSONEncoder(json.Marshal). + JSON(complex(1, 1)) + + _, body, errs := a.String() + + utils.AssertEqual(t, "", body) + utils.AssertEqual(t, 1, len(errs)) + utils.AssertEqual(t, "json: unsupported type: complex128", errs[0].Error()) +} + +func Test_Client_Agent_XML(t *testing.T) { + t.Parallel() + handler := func(c *Ctx) error { + utils.AssertEqual(t, MIMEApplicationXML, string(c.Request().Header.ContentType())) + + return c.Send(c.Request().Body()) + } + + wrapAgent := func(a *Agent) { + a.XML(data{Success: true}) + } + + testAgent(t, handler, wrapAgent, "true") +} + +func Test_Client_Agent_XML_Error(t *testing.T) { + t.Parallel() + a := Get("http://example.com"). + XML(complex(1, 1)) + + _, body, errs := a.String() + + utils.AssertEqual(t, "", body) + utils.AssertEqual(t, 1, len(errs)) + utils.AssertEqual(t, "xml: unsupported type: complex128", errs[0].Error()) +} + +func Test_Client_Agent_Form(t *testing.T) { + t.Parallel() + handler := func(c *Ctx) error { + utils.AssertEqual(t, MIMEApplicationForm, string(c.Request().Header.ContentType())) + + return c.Send(c.Request().Body()) + } + + args := AcquireArgs() + + args.Set("foo", "bar") + + wrapAgent := func(a *Agent) { + a.Form(args) + } + + testAgent(t, handler, wrapAgent, "foo=bar") + + ReleaseArgs(args) +} + +func Test_Client_Agent_MultipartForm(t *testing.T) { + t.Parallel() + + ln := fasthttputil.NewInmemoryListener() + + app := New(Config{DisableStartupMessage: true}) + + app.Post("/", func(c *Ctx) error { + utils.AssertEqual(t, "multipart/form-data; boundary=myBoundary", c.Get(HeaderContentType)) + + mf, err := c.MultipartForm() + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, "bar", mf.Value["foo"][0]) + + return c.Send(c.Request().Body()) + }) + + go func() { utils.AssertEqual(t, nil, app.Listener(ln)) }() + + args := AcquireArgs() + + args.Set("foo", "bar") + + a := Post("http://example.com"). + Boundary("myBoundary"). + MultipartForm(args) + + a.HostClient.Dial = func(addr string) (net.Conn, error) { return ln.Dial() } + + code, body, errs := a.String() + + utils.AssertEqual(t, StatusOK, code) + utils.AssertEqual(t, "--myBoundary\r\nContent-Disposition: form-data; name=\"foo\"\r\n\r\nbar\r\n--myBoundary--\r\n", body) + utils.AssertEqual(t, 0, len(errs)) + ReleaseArgs(args) +} + +func Test_Client_Agent_MultipartForm_Errors(t *testing.T) { + t.Parallel() + + a := AcquireAgent() + a.mw = &errorMultipartWriter{} + + args := AcquireArgs() + args.Set("foo", "bar") + + ff1 := &FormFile{"", "name1", []byte("content"), false} + ff2 := &FormFile{"", "name2", []byte("content"), false} + a.FileData(ff1, ff2). + MultipartForm(args) + + utils.AssertEqual(t, 4, len(a.errs)) + ReleaseArgs(args) +} + +func Test_Client_Agent_MultipartForm_SendFiles(t *testing.T) { + t.Parallel() + + ln := fasthttputil.NewInmemoryListener() + + app := New(Config{DisableStartupMessage: true}) + + app.Post("/", func(c *Ctx) error { + utils.AssertEqual(t, "multipart/form-data; boundary=myBoundary", c.Get(HeaderContentType)) + + fh1, err := c.FormFile("field1") + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, fh1.Filename, "name") + buf := make([]byte, fh1.Size) + f, err := fh1.Open() + utils.AssertEqual(t, nil, err) + defer func() { + err := f.Close() + utils.AssertEqual(t, nil, err) + }() + _, err = f.Read(buf) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, "form file", string(buf)) + + fh2, err := c.FormFile("index") + utils.AssertEqual(t, nil, err) + checkFormFile(t, fh2, ".github/testdata/index.html") + + fh3, err := c.FormFile("file3") + utils.AssertEqual(t, nil, err) + checkFormFile(t, fh3, ".github/testdata/index.tmpl") + + return c.SendString("multipart form files") + }) + + go func() { utils.AssertEqual(t, nil, app.Listener(ln)) }() + + for i := 0; i < 5; i++ { + ff := AcquireFormFile() + ff.Fieldname = "field1" + ff.Name = "name" + ff.Content = []byte("form file") + + a := Post("http://example.com"). + Boundary("myBoundary"). + FileData(ff). + SendFiles(".github/testdata/index.html", "index", ".github/testdata/index.tmpl"). + MultipartForm(nil) + + a.HostClient.Dial = func(addr string) (net.Conn, error) { return ln.Dial() } + + code, body, errs := a.String() + + utils.AssertEqual(t, StatusOK, code) + utils.AssertEqual(t, "multipart form files", body) + utils.AssertEqual(t, 0, len(errs)) + + ReleaseFormFile(ff) + } +} + +func checkFormFile(t *testing.T, fh *multipart.FileHeader, filename string) { + t.Helper() + + basename := filepath.Base(filename) + utils.AssertEqual(t, fh.Filename, basename) + + b1, err := os.ReadFile(filename) //nolint:gosec // We're in a test so reading user-provided files by name is fine + utils.AssertEqual(t, nil, err) + + b2 := make([]byte, fh.Size) + f, err := fh.Open() + utils.AssertEqual(t, nil, err) + defer func() { + err := f.Close() + utils.AssertEqual(t, nil, err) + }() + _, err = f.Read(b2) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, b1, b2) +} + +func Test_Client_Agent_Multipart_Random_Boundary(t *testing.T) { + t.Parallel() + + a := Post("http://example.com"). + MultipartForm(nil) + + reg := regexp.MustCompile(`multipart/form-data; boundary=\w{30}`) + + utils.AssertEqual(t, true, reg.Match(a.req.Header.Peek(HeaderContentType))) +} + +func Test_Client_Agent_Multipart_Invalid_Boundary(t *testing.T) { + t.Parallel() + + a := Post("http://example.com"). + Boundary("*"). + MultipartForm(nil) + + utils.AssertEqual(t, 1, len(a.errs)) + utils.AssertEqual(t, "mime: invalid boundary character", a.errs[0].Error()) +} + +func Test_Client_Agent_SendFile_Error(t *testing.T) { + t.Parallel() + + a := Post("http://example.com"). + SendFile("non-exist-file!", "") + + utils.AssertEqual(t, 1, len(a.errs)) + utils.AssertEqual(t, true, strings.Contains(a.errs[0].Error(), "open non-exist-file!")) +} + +func Test_Client_Debug(t *testing.T) { + t.Parallel() + handler := func(c *Ctx) error { + return c.SendString("debug") + } + + var output bytes.Buffer + + wrapAgent := func(a *Agent) { + a.Debug(&output) + } + + testAgent(t, handler, wrapAgent, "debug", 1) + + str := output.String() + + utils.AssertEqual(t, true, strings.Contains(str, "Connected to example.com(InmemoryListener)")) + utils.AssertEqual(t, true, strings.Contains(str, "GET / HTTP/1.1")) + utils.AssertEqual(t, true, strings.Contains(str, "User-Agent: fiber")) + utils.AssertEqual(t, true, strings.Contains(str, "Host: example.com\r\n\r\n")) + utils.AssertEqual(t, true, strings.Contains(str, "HTTP/1.1 200 OK")) + utils.AssertEqual(t, true, strings.Contains(str, "Content-Type: text/plain; charset=utf-8\r\nContent-Length: 5\r\n\r\ndebug")) +} + +func Test_Client_Agent_Timeout(t *testing.T) { + t.Parallel() + + ln := fasthttputil.NewInmemoryListener() + + app := New(Config{DisableStartupMessage: true}) + + app.Get("/", func(c *Ctx) error { + time.Sleep(time.Millisecond * 200) + return c.SendString("timeout") + }) + + go func() { utils.AssertEqual(t, nil, app.Listener(ln)) }() + + a := Get("http://example.com"). + Timeout(time.Millisecond * 50) + + a.HostClient.Dial = func(addr string) (net.Conn, error) { return ln.Dial() } + + _, body, errs := a.String() + + utils.AssertEqual(t, "", body) + utils.AssertEqual(t, 1, len(errs)) + utils.AssertEqual(t, "timeout", errs[0].Error()) +} + +func Test_Client_Agent_Reuse(t *testing.T) { + t.Parallel() + + ln := fasthttputil.NewInmemoryListener() + + app := New(Config{DisableStartupMessage: true}) + + app.Get("/", func(c *Ctx) error { + return c.SendString("reuse") + }) + + go func() { utils.AssertEqual(t, nil, app.Listener(ln)) }() + + a := Get("http://example.com"). + Reuse() + + a.HostClient.Dial = func(addr string) (net.Conn, error) { return ln.Dial() } + + code, body, errs := a.String() + + utils.AssertEqual(t, StatusOK, code) + utils.AssertEqual(t, "reuse", body) + utils.AssertEqual(t, 0, len(errs)) + + code, body, errs = a.String() + + utils.AssertEqual(t, StatusOK, code) + utils.AssertEqual(t, "reuse", body) + utils.AssertEqual(t, 0, len(errs)) +} + +func Test_Client_Agent_InsecureSkipVerify(t *testing.T) { + t.Parallel() + + cer, err := tls.LoadX509KeyPair("./.github/testdata/ssl.pem", "./.github/testdata/ssl.key") + utils.AssertEqual(t, nil, err) + + //nolint:gosec // We're in a test so using old ciphers is fine + serverTLSConf := &tls.Config{ + Certificates: []tls.Certificate{cer}, + } + + ln, err := net.Listen(NetworkTCP4, "127.0.0.1:0") + utils.AssertEqual(t, nil, err) + + ln = tls.NewListener(ln, serverTLSConf) + + app := New(Config{DisableStartupMessage: true}) + + app.Get("/", func(c *Ctx) error { + return c.SendString("ignore tls") + }) + + go func() { utils.AssertEqual(t, nil, app.Listener(ln)) }() + + code, body, errs := Get("https://" + ln.Addr().String()). + InsecureSkipVerify(). + InsecureSkipVerify(). + String() + + utils.AssertEqual(t, 0, len(errs)) + utils.AssertEqual(t, StatusOK, code) + utils.AssertEqual(t, "ignore tls", body) +} + +func Test_Client_Agent_TLS(t *testing.T) { + t.Parallel() + + serverTLSConf, clientTLSConf, err := tlstest.GetTLSConfigs() + utils.AssertEqual(t, nil, err) + + ln, err := net.Listen(NetworkTCP4, "127.0.0.1:0") + utils.AssertEqual(t, nil, err) + + ln = tls.NewListener(ln, serverTLSConf) + + app := New(Config{DisableStartupMessage: true}) + + app.Get("/", func(c *Ctx) error { + return c.SendString("tls") + }) + + go func() { utils.AssertEqual(t, nil, app.Listener(ln)) }() + + code, body, errs := Get("https://" + ln.Addr().String()). + TLSConfig(clientTLSConf). + String() + + utils.AssertEqual(t, 0, len(errs)) + utils.AssertEqual(t, StatusOK, code) + utils.AssertEqual(t, "tls", body) +} + +func Test_Client_Agent_MaxRedirectsCount(t *testing.T) { + t.Parallel() + + ln := fasthttputil.NewInmemoryListener() + + app := New(Config{DisableStartupMessage: true}) + + app.Get("/", func(c *Ctx) error { + if c.Request().URI().QueryArgs().Has("foo") { + return c.Redirect("/foo") + } + return c.Redirect("/") + }) + app.Get("/foo", func(c *Ctx) error { + return c.SendString("redirect") + }) + + go func() { utils.AssertEqual(t, nil, app.Listener(ln)) }() + + t.Run("success", func(t *testing.T) { + t.Parallel() + a := Get("http://example.com?foo"). + MaxRedirectsCount(1) + + a.HostClient.Dial = func(addr string) (net.Conn, error) { return ln.Dial() } + + code, body, errs := a.String() + + utils.AssertEqual(t, 200, code) + utils.AssertEqual(t, "redirect", body) + utils.AssertEqual(t, 0, len(errs)) + }) + + t.Run("error", func(t *testing.T) { + t.Parallel() + a := Get("http://example.com"). + MaxRedirectsCount(1) + + a.HostClient.Dial = func(addr string) (net.Conn, error) { return ln.Dial() } + + _, body, errs := a.String() + + utils.AssertEqual(t, "", body) + utils.AssertEqual(t, 1, len(errs)) + utils.AssertEqual(t, "too many redirects detected when doing the request", errs[0].Error()) + }) +} + +func Test_Client_Agent_Struct(t *testing.T) { + t.Parallel() + + ln := fasthttputil.NewInmemoryListener() + + app := New(Config{DisableStartupMessage: true}) + + app.Get("/", func(c *Ctx) error { + return c.JSON(data{true}) + }) + + app.Get("/error", func(c *Ctx) error { + return c.SendString(`{"success"`) + }) + + go func() { utils.AssertEqual(t, nil, app.Listener(ln)) }() + + t.Run("success", func(t *testing.T) { + t.Parallel() + + a := Get("http://example.com") + + a.HostClient.Dial = func(addr string) (net.Conn, error) { return ln.Dial() } + + var d data + + code, body, errs := a.Struct(&d) + + utils.AssertEqual(t, StatusOK, code) + utils.AssertEqual(t, `{"success":true}`, string(body)) + utils.AssertEqual(t, 0, len(errs)) + utils.AssertEqual(t, true, d.Success) + }) + + t.Run("pre error", func(t *testing.T) { + t.Parallel() + a := Get("http://example.com") + + a.HostClient.Dial = func(addr string) (net.Conn, error) { return ln.Dial() } + a.errs = append(a.errs, errors.New("pre errors")) + + var d data + _, body, errs := a.Struct(&d) + + utils.AssertEqual(t, "", string(body)) + utils.AssertEqual(t, 1, len(errs)) + utils.AssertEqual(t, "pre errors", errs[0].Error()) + utils.AssertEqual(t, false, d.Success) + }) + + t.Run("error", func(t *testing.T) { + t.Parallel() + a := Get("http://example.com/error") + + a.HostClient.Dial = func(addr string) (net.Conn, error) { return ln.Dial() } + + var d data + + code, body, errs := a.JSONDecoder(json.Unmarshal).Struct(&d) + + utils.AssertEqual(t, StatusOK, code) + utils.AssertEqual(t, `{"success"`, string(body)) + utils.AssertEqual(t, 1, len(errs)) + utils.AssertEqual(t, "unexpected end of JSON input", errs[0].Error()) + }) + + t.Run("nil jsonDecoder", func(t *testing.T) { + t.Parallel() + a := AcquireAgent() + defer ReleaseAgent(a) + defer a.ConnectionClose() + request := a.Request() + request.Header.SetMethod(MethodGet) + request.SetRequestURI("http://example.com") + err := a.Parse() + utils.AssertEqual(t, nil, err) + a.HostClient.Dial = func(addr string) (net.Conn, error) { return ln.Dial() } + var d data + code, body, errs := a.Struct(&d) + utils.AssertEqual(t, StatusOK, code) + utils.AssertEqual(t, `{"success":true}`, string(body)) + utils.AssertEqual(t, 0, len(errs)) + utils.AssertEqual(t, true, d.Success) + }) +} + +func Test_Client_Agent_Parse(t *testing.T) { + t.Parallel() + + a := Get("https://example.com:10443") + + utils.AssertEqual(t, nil, a.Parse()) +} + +func testAgent(t *testing.T, handler Handler, wrapAgent func(agent *Agent), excepted string, count ...int) { + t.Helper() + + ln := fasthttputil.NewInmemoryListener() + + app := New(Config{DisableStartupMessage: true}) + + app.Get("/", handler) + + go func() { utils.AssertEqual(t, nil, app.Listener(ln)) }() + + c := 1 + if len(count) > 0 { + c = count[0] + } + + for i := 0; i < c; i++ { + a := Get("http://example.com") + + wrapAgent(a) + + a.HostClient.Dial = func(addr string) (net.Conn, error) { return ln.Dial() } + + code, body, errs := a.String() + + utils.AssertEqual(t, StatusOK, code) + utils.AssertEqual(t, excepted, body) + utils.AssertEqual(t, 0, len(errs)) + } +} + +type data struct { + Success bool `json:"success" xml:"success"` +} + +type errorMultipartWriter struct { + count int +} + +func (*errorMultipartWriter) Boundary() string { return "myBoundary" } +func (*errorMultipartWriter) SetBoundary(_ string) error { return nil } +func (e *errorMultipartWriter) CreateFormFile(_, _ string) (io.Writer, error) { + if e.count == 0 { + e.count++ + return nil, errors.New("CreateFormFile error") + } + return errorWriter{}, nil +} +func (*errorMultipartWriter) WriteField(_, _ string) error { return errors.New("WriteField error") } +func (*errorMultipartWriter) Close() error { return errors.New("Close error") } + +type errorWriter struct{} + +func (errorWriter) Write(_ []byte) (int, error) { return 0, errors.New("Write error") } diff --git a/color.go b/color.go new file mode 100644 index 0000000..cbccd2e --- /dev/null +++ b/color.go @@ -0,0 +1,107 @@ +// ⚡️ Fiber is an Express inspired web framework written in Go with ☕️ +// 🤖 Github Repository: https://github.com/gofiber/fiber +// 📌 API Documentation: https://docs.gofiber.io + +package fiber + +// Colors is a struct to define custom colors for Fiber app and middlewares. +type Colors struct { + // Black color. + // + // Optional. Default: "\u001b[90m" + Black string + + // Red color. + // + // Optional. Default: "\u001b[91m" + Red string + + // Green color. + // + // Optional. Default: "\u001b[92m" + Green string + + // Yellow color. + // + // Optional. Default: "\u001b[93m" + Yellow string + + // Blue color. + // + // Optional. Default: "\u001b[94m" + Blue string + + // Magenta color. + // + // Optional. Default: "\u001b[95m" + Magenta string + + // Cyan color. + // + // Optional. Default: "\u001b[96m" + Cyan string + + // White color. + // + // Optional. Default: "\u001b[97m" + White string + + // Reset color. + // + // Optional. Default: "\u001b[0m" + Reset string +} + +// DefaultColors Default color codes +var DefaultColors = Colors{ + Black: "\u001b[90m", + Red: "\u001b[91m", + Green: "\u001b[92m", + Yellow: "\u001b[93m", + Blue: "\u001b[94m", + Magenta: "\u001b[95m", + Cyan: "\u001b[96m", + White: "\u001b[97m", + Reset: "\u001b[0m", +} + +// defaultColors is a function to override default colors to config +func defaultColors(colors Colors) Colors { + if colors.Black == "" { + colors.Black = DefaultColors.Black + } + + if colors.Red == "" { + colors.Red = DefaultColors.Red + } + + if colors.Green == "" { + colors.Green = DefaultColors.Green + } + + if colors.Yellow == "" { + colors.Yellow = DefaultColors.Yellow + } + + if colors.Blue == "" { + colors.Blue = DefaultColors.Blue + } + + if colors.Magenta == "" { + colors.Magenta = DefaultColors.Magenta + } + + if colors.Cyan == "" { + colors.Cyan = DefaultColors.Cyan + } + + if colors.White == "" { + colors.White = DefaultColors.White + } + + if colors.Reset == "" { + colors.Reset = DefaultColors.Reset + } + + return colors +} diff --git a/ctx.go b/ctx.go new file mode 100644 index 0000000..c0a4413 --- /dev/null +++ b/ctx.go @@ -0,0 +1,1962 @@ +// ⚡️ Fiber is an Express inspired web framework written in Go with ☕️ +// 🤖 Github Repository: https://github.com/gofiber/fiber +// 📌 API Documentation: https://docs.gofiber.io + +package fiber + +import ( + "bytes" + "context" + "crypto/tls" + "encoding/xml" + "errors" + "fmt" + "io" + "mime/multipart" + "net" + "net/http" + "path/filepath" + "reflect" + "strconv" + "strings" + "sync" + "text/template" + "time" + + "github.com/gofiber/fiber/v2/internal/schema" + "github.com/gofiber/fiber/v2/utils" + + "github.com/valyala/bytebufferpool" + "github.com/valyala/fasthttp" +) + +const ( + schemeHTTP = "http" + schemeHTTPS = "https" +) + +// maxParams defines the maximum number of parameters per route. +const maxParams = 30 + +// Some constants for BodyParser, QueryParser, CookieParser and ReqHeaderParser. +const ( + queryTag = "query" + reqHeaderTag = "reqHeader" + bodyTag = "form" + paramsTag = "params" + cookieTag = "cookie" +) + +// userContextKey define the key name for storing context.Context in *fasthttp.RequestCtx +const userContextKey = "__local_user_context__" + +var ( + // decoderPoolMap helps to improve BodyParser's, QueryParser's, CookieParser's and ReqHeaderParser's performance + decoderPoolMap = map[string]*sync.Pool{} + // tags is used to classify parser's pool + tags = []string{queryTag, bodyTag, reqHeaderTag, paramsTag, cookieTag} +) + +func init() { + for _, tag := range tags { + decoderPoolMap[tag] = &sync.Pool{New: func() interface{} { + return decoderBuilder(ParserConfig{ + IgnoreUnknownKeys: true, + ZeroEmpty: true, + }) + }} + } +} + +// SetParserDecoder allow globally change the option of form decoder, update decoderPool +func SetParserDecoder(parserConfig ParserConfig) { + for _, tag := range tags { + decoderPoolMap[tag] = &sync.Pool{New: func() interface{} { + return decoderBuilder(parserConfig) + }} + } +} + +// Ctx represents the Context which hold the HTTP request and response. +// It has methods for the request query string, parameters, body, HTTP headers and so on. +type Ctx struct { + app *App // Reference to *App + route *Route // Reference to *Route + indexRoute int // Index of the current route + indexHandler int // Index of the current handler + method string // HTTP method + methodINT int // HTTP method INT equivalent + baseURI string // HTTP base uri + path string // HTTP path with the modifications by the configuration -> string copy from pathBuffer + pathBuffer []byte // HTTP path buffer + detectionPath string // Route detection path -> string copy from detectionPathBuffer + detectionPathBuffer []byte // HTTP detectionPath buffer + treePath string // Path for the search in the tree + pathOriginal string // Original HTTP path + values [maxParams]string // Route parameter values + fasthttp *fasthttp.RequestCtx // Reference to *fasthttp.RequestCtx + matched bool // Non use route matched + viewBindMap sync.Map // Default view map to bind template engine +} + +// TLSHandler object +type TLSHandler struct { + clientHelloInfo *tls.ClientHelloInfo +} + +// GetClientInfo Callback function to set ClientHelloInfo +// Must comply with the method structure of https://cs.opensource.google/go/go/+/refs/tags/go1.20:src/crypto/tls/common.go;l=554-563 +// Since we overlay the method of the tls config in the listener method +func (t *TLSHandler) GetClientInfo(info *tls.ClientHelloInfo) (*tls.Certificate, error) { + t.clientHelloInfo = info + return nil, nil //nolint:nilnil // Not returning anything useful here is probably fine +} + +// Range data for c.Range +type Range struct { + Type string + Ranges []RangeSet +} + +// RangeSet represents a single content range from a request. +type RangeSet struct { + Start int + End int +} + +// Cookie data for c.Cookie +type Cookie struct { + Name string `json:"name"` + Value string `json:"value"` + Path string `json:"path"` + Domain string `json:"domain"` + MaxAge int `json:"max_age"` + Expires time.Time `json:"expires"` + Secure bool `json:"secure"` + HTTPOnly bool `json:"http_only"` + SameSite string `json:"same_site"` + SessionOnly bool `json:"session_only"` +} + +// Views is the interface that wraps the Render function. +type Views interface { + Load() error + Render(io.Writer, string, interface{}, ...string) error +} + +// ParserType require two element, type and converter for register. +// Use ParserType with BodyParser for parsing custom type in form data. +type ParserType struct { + Customtype interface{} + Converter func(string) reflect.Value +} + +// ParserConfig form decoder config for SetParserDecoder +type ParserConfig struct { + IgnoreUnknownKeys bool + SetAliasTag string + ParserType []ParserType + ZeroEmpty bool +} + +// AcquireCtx retrieves a new Ctx from the pool. +func (app *App) AcquireCtx(fctx *fasthttp.RequestCtx) *Ctx { + c, ok := app.pool.Get().(*Ctx) + if !ok { + panic(fmt.Errorf("failed to type-assert to *Ctx")) + } + // Set app reference + c.app = app + // Reset route and handler index + c.indexRoute = -1 + c.indexHandler = 0 + // Reset matched flag + c.matched = false + // Set paths + c.pathOriginal = app.getString(fctx.URI().PathOriginal()) + // Set method + c.method = app.getString(fctx.Request.Header.Method()) + c.methodINT = app.methodInt(c.method) + // Attach *fasthttp.RequestCtx to ctx + c.fasthttp = fctx + // reset base uri + c.baseURI = "" + // Prettify path + c.configDependentPaths() + return c +} + +// ReleaseCtx releases the ctx back into the pool. +func (app *App) ReleaseCtx(c *Ctx) { + // Reset values + c.route = nil + c.fasthttp = nil + c.viewBindMap = sync.Map{} + app.pool.Put(c) +} + +// Accepts checks if the specified extensions or content types are acceptable. +func (c *Ctx) Accepts(offers ...string) string { + return getOffer(c.Get(HeaderAccept), acceptsOfferType, offers...) +} + +// AcceptsCharsets checks if the specified charset is acceptable. +func (c *Ctx) AcceptsCharsets(offers ...string) string { + return getOffer(c.Get(HeaderAcceptCharset), acceptsOffer, offers...) +} + +// AcceptsEncodings checks if the specified encoding is acceptable. +func (c *Ctx) AcceptsEncodings(offers ...string) string { + return getOffer(c.Get(HeaderAcceptEncoding), acceptsOffer, offers...) +} + +// AcceptsLanguages checks if the specified language is acceptable. +func (c *Ctx) AcceptsLanguages(offers ...string) string { + return getOffer(c.Get(HeaderAcceptLanguage), acceptsOffer, offers...) +} + +// App returns the *App reference to the instance of the Fiber application +func (c *Ctx) App() *App { + return c.app +} + +// Append the specified value to the HTTP response header field. +// If the header is not already set, it creates the header with the specified value. +func (c *Ctx) Append(field string, values ...string) { + if len(values) == 0 { + return + } + h := c.app.getString(c.fasthttp.Response.Header.Peek(field)) + originalH := h + for _, value := range values { + if len(h) == 0 { + h = value + } else if h != value && !strings.HasPrefix(h, value+",") && !strings.HasSuffix(h, " "+value) && + !strings.Contains(h, " "+value+",") { + h += ", " + value + } + } + if originalH != h { + c.Set(field, h) + } +} + +// Attachment sets the HTTP response Content-Disposition header field to attachment. +func (c *Ctx) Attachment(filename ...string) { + if len(filename) > 0 { + fname := filepath.Base(filename[0]) + c.Type(filepath.Ext(fname)) + + c.setCanonical(HeaderContentDisposition, `attachment; filename="`+c.app.quoteString(fname)+`"`) + return + } + c.setCanonical(HeaderContentDisposition, "attachment") +} + +// BaseURL returns (protocol + host + base path). +func (c *Ctx) BaseURL() string { + // TODO: Could be improved: 53.8 ns/op 32 B/op 1 allocs/op + // Should work like https://codeigniter.com/user_guide/helpers/url_helper.html + if c.baseURI != "" { + return c.baseURI + } + c.baseURI = c.Protocol() + "://" + c.Hostname() + return c.baseURI +} + +// BodyRaw contains the raw body submitted in a POST request. +// Returned value is only valid within the handler. Do not store any references. +// Make copies or use the Immutable setting instead. +func (c *Ctx) BodyRaw() []byte { + return c.getBody() +} + +func (c *Ctx) tryDecodeBodyInOrder( + originalBody *[]byte, + encodings []string, +) ([]byte, uint8, error) { + var ( + err error + body []byte + decodesRealized uint8 + ) + + for index, encoding := range encodings { + decodesRealized++ + switch encoding { + case StrGzip: + body, err = c.fasthttp.Request.BodyGunzip() + case StrBr, StrBrotli: + body, err = c.fasthttp.Request.BodyUnbrotli() + case StrDeflate: + body, err = c.fasthttp.Request.BodyInflate() + default: + decodesRealized-- + if len(encodings) == 1 { + body = c.fasthttp.Request.Body() + } + return body, decodesRealized, nil + } + + if err != nil { + return nil, decodesRealized, err + } + + // Only execute body raw update if it has a next iteration to try to decode + if index < len(encodings)-1 && decodesRealized > 0 { + if index == 0 { + tempBody := c.fasthttp.Request.Body() + *originalBody = make([]byte, len(tempBody)) + copy(*originalBody, tempBody) + } + c.fasthttp.Request.SetBodyRaw(body) + } + } + + return body, decodesRealized, nil +} + +// Body contains the raw body submitted in a POST request. +// This method will decompress the body if the 'Content-Encoding' header is provided. +// It returns the original (or decompressed) body data which is valid only within the handler. +// Don't store direct references to the returned data. +// If you need to keep the body's data later, make a copy or use the Immutable option. +func (c *Ctx) Body() []byte { + var ( + err error + body, originalBody []byte + headerEncoding string + encodingOrder = []string{"", "", ""} + ) + + // faster than peek + c.Request().Header.VisitAll(func(key, value []byte) { + if c.app.getString(key) == HeaderContentEncoding { + headerEncoding = c.app.getString(value) + } + }) + + // Split and get the encodings list, in order to attend the + // rule defined at: https://www.rfc-editor.org/rfc/rfc9110#section-8.4-5 + encodingOrder = getSplicedStrList(headerEncoding, encodingOrder) + if len(encodingOrder) == 0 { + return c.getBody() + } + + var decodesRealized uint8 + body, decodesRealized, err = c.tryDecodeBodyInOrder(&originalBody, encodingOrder) + + // Ensure that the body will be the original + if originalBody != nil && decodesRealized > 0 { + c.fasthttp.Request.SetBodyRaw(originalBody) + } + if err != nil { + return []byte(err.Error()) + } + + if c.app.config.Immutable { + return utils.CopyBytes(body) + } + return body +} + +func decoderBuilder(parserConfig ParserConfig) interface{} { + decoder := schema.NewDecoder() + decoder.IgnoreUnknownKeys(parserConfig.IgnoreUnknownKeys) + if parserConfig.SetAliasTag != "" { + decoder.SetAliasTag(parserConfig.SetAliasTag) + } + for _, v := range parserConfig.ParserType { + decoder.RegisterConverter(reflect.ValueOf(v.Customtype).Interface(), v.Converter) + } + decoder.ZeroEmpty(parserConfig.ZeroEmpty) + return decoder +} + +// BodyParser binds the request body to a struct. +// It supports decoding the following content types based on the Content-Type header: +// application/json, application/xml, application/x-www-form-urlencoded, multipart/form-data +// All JSON extenstion mime types are supported (eg. application/problem+json) +// If none of the content types above are matched, it will return a ErrUnprocessableEntity error +func (c *Ctx) BodyParser(out interface{}) error { + // Get content-type + ctype := utils.ToLower(c.app.getString(c.fasthttp.Request.Header.ContentType())) + + ctype = utils.ParseVendorSpecificContentType(ctype) + + // Only use ctype string up to and excluding byte ';' + ctypeEnd := strings.IndexByte(ctype, ';') + if ctypeEnd != -1 { + ctype = ctype[:ctypeEnd] + } + + // Parse body accordingly + if strings.HasSuffix(ctype, "json") { + return c.app.config.JSONDecoder(c.Body(), out) + } + if strings.HasPrefix(ctype, MIMEApplicationForm) { + data := make(map[string][]string) + var err error + + c.fasthttp.PostArgs().VisitAll(func(key, val []byte) { + if err != nil { + return + } + + k := c.app.getString(key) + v := c.app.getString(val) + + err = formatParserData(out, data, bodyTag, k, v, c.app.config.EnableSplittingOnParsers, true) + }) + + if err != nil { + return err + } + + return c.parseToStruct(bodyTag, out, data) + } + if strings.HasPrefix(ctype, MIMEMultipartForm) { + multipartForm, err := c.fasthttp.MultipartForm() + if err != nil { + return err + } + + data := make(map[string][]string) + for key, values := range multipartForm.Value { + err = formatParserData(out, data, bodyTag, key, values, c.app.config.EnableSplittingOnParsers, true) + if err != nil { + return err + } + } + + return c.parseToStruct(bodyTag, out, data) + } + if strings.HasPrefix(ctype, MIMETextXML) || strings.HasPrefix(ctype, MIMEApplicationXML) { + if err := xml.Unmarshal(c.Body(), out); err != nil { + return fmt.Errorf("failed to unmarshal: %w", err) + } + return nil + } + // No suitable content type found + return ErrUnprocessableEntity +} + +// ClearCookie expires a specific cookie by key on the client side. +// If no key is provided it expires all cookies that came with the request. +func (c *Ctx) ClearCookie(key ...string) { + if len(key) > 0 { + for i := range key { + c.fasthttp.Response.Header.DelClientCookie(key[i]) + } + return + } + c.fasthttp.Request.Header.VisitAllCookie(func(k, v []byte) { + c.fasthttp.Response.Header.DelClientCookieBytes(k) + }) +} + +// Context returns *fasthttp.RequestCtx that carries a deadline +// a cancellation signal, and other values across API boundaries. +func (c *Ctx) Context() *fasthttp.RequestCtx { + return c.fasthttp +} + +// UserContext returns a context implementation that was set by +// user earlier or returns a non-nil, empty context,if it was not set earlier. +func (c *Ctx) UserContext() context.Context { + ctx, ok := c.fasthttp.UserValue(userContextKey).(context.Context) + if !ok { + ctx = context.Background() + c.SetUserContext(ctx) + } + + return ctx +} + +// SetUserContext sets a context implementation by user. +func (c *Ctx) SetUserContext(ctx context.Context) { + c.fasthttp.SetUserValue(userContextKey, ctx) +} + +// Cookie sets a cookie by passing a cookie struct. +func (c *Ctx) Cookie(cookie *Cookie) { + fcookie := fasthttp.AcquireCookie() + fcookie.SetKey(cookie.Name) + fcookie.SetValue(cookie.Value) + fcookie.SetPath(cookie.Path) + fcookie.SetDomain(cookie.Domain) + // only set max age and expiry when SessionOnly is false + // i.e. cookie supposed to last beyond browser session + // refer: https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies#define_the_lifetime_of_a_cookie + if !cookie.SessionOnly { + fcookie.SetMaxAge(cookie.MaxAge) + fcookie.SetExpire(cookie.Expires) + } + fcookie.SetSecure(cookie.Secure) + fcookie.SetHTTPOnly(cookie.HTTPOnly) + + switch utils.ToLower(cookie.SameSite) { + case CookieSameSiteStrictMode: + fcookie.SetSameSite(fasthttp.CookieSameSiteStrictMode) + case CookieSameSiteNoneMode: + fcookie.SetSameSite(fasthttp.CookieSameSiteNoneMode) + case CookieSameSiteDisabled: + fcookie.SetSameSite(fasthttp.CookieSameSiteDisabled) + default: + fcookie.SetSameSite(fasthttp.CookieSameSiteLaxMode) + } + + c.fasthttp.Response.Header.SetCookie(fcookie) + fasthttp.ReleaseCookie(fcookie) +} + +// Cookies are used for getting a cookie value by key. +// Defaults to the empty string "" if the cookie doesn't exist. +// If a default value is given, it will return that value if the cookie doesn't exist. +// The returned value is only valid within the handler. Do not store any references. +// Make copies or use the Immutable setting to use the value outside the Handler. +func (c *Ctx) Cookies(key string, defaultValue ...string) string { + return defaultString(c.app.getString(c.fasthttp.Request.Header.Cookie(key)), defaultValue) +} + +// CookieParser is used to bind cookies to a struct +func (c *Ctx) CookieParser(out interface{}) error { + data := make(map[string][]string) + var err error + + // loop through all cookies + c.fasthttp.Request.Header.VisitAllCookie(func(key, val []byte) { + if err != nil { + return + } + + k := c.app.getString(key) + v := c.app.getString(val) + + err = formatParserData(out, data, cookieTag, k, v, c.app.config.EnableSplittingOnParsers, true) + }) + if err != nil { + return err + } + + return c.parseToStruct(cookieTag, out, data) +} + +// Download transfers the file from path as an attachment. +// Typically, browsers will prompt the user for download. +// By default, the Content-Disposition header filename= parameter is the filepath (this typically appears in the browser dialog). +// Override this default with the filename parameter. +func (c *Ctx) Download(file string, filename ...string) error { + var fname string + if len(filename) > 0 { + fname = filename[0] + } else { + fname = filepath.Base(file) + } + c.setCanonical(HeaderContentDisposition, `attachment; filename="`+c.app.quoteString(fname)+`"`) + return c.SendFile(file) +} + +// Request return the *fasthttp.Request object +// This allows you to use all fasthttp request methods +// https://godoc.org/github.com/valyala/fasthttp#Request +func (c *Ctx) Request() *fasthttp.Request { + return &c.fasthttp.Request +} + +// Response return the *fasthttp.Response object +// This allows you to use all fasthttp response methods +// https://godoc.org/github.com/valyala/fasthttp#Response +func (c *Ctx) Response() *fasthttp.Response { + return &c.fasthttp.Response +} + +// Format performs content-negotiation on the Accept HTTP header. +// It uses Accepts to select a proper format. +// If the header is not specified or there is no proper format, text/plain is used. +func (c *Ctx) Format(body interface{}) error { + // Get accepted content type + accept := c.Accepts("html", "json", "txt", "xml") + // Set accepted content type + c.Type(accept) + // Type convert provided body + var b string + switch val := body.(type) { + case string: + b = val + case []byte: + b = c.app.getString(val) + default: + b = fmt.Sprintf("%v", val) + } + + // Format based on the accept content type + switch accept { + case "html": + return c.SendString("

" + b + "

") + case "json": + return c.JSON(body) + case "txt": + return c.SendString(b) + case "xml": + return c.XML(body) + } + return c.SendString(b) +} + +// FormFile returns the first file by key from a MultipartForm. +func (c *Ctx) FormFile(key string) (*multipart.FileHeader, error) { + return c.fasthttp.FormFile(key) +} + +// FormValue returns the first value by key from a MultipartForm. +// Search is performed in QueryArgs, PostArgs, MultipartForm and FormFile in this particular order. +// Defaults to the empty string "" if the form value doesn't exist. +// If a default value is given, it will return that value if the form value does not exist. +// Returned value is only valid within the handler. Do not store any references. +// Make copies or use the Immutable setting instead. +func (c *Ctx) FormValue(key string, defaultValue ...string) string { + return defaultString(c.app.getString(c.fasthttp.FormValue(key)), defaultValue) +} + +// Fresh returns true when the response is still “fresh” in the client's cache, +// otherwise false is returned to indicate that the client cache is now stale +// and the full response should be sent. +// When a client sends the Cache-Control: no-cache request header to indicate an end-to-end +// reload request, this module will return false to make handling these requests transparent. +// https://github.com/jshttp/fresh/blob/10e0471669dbbfbfd8de65bc6efac2ddd0bfa057/index.js#L33 +func (c *Ctx) Fresh() bool { + // fields + modifiedSince := c.Get(HeaderIfModifiedSince) + noneMatch := c.Get(HeaderIfNoneMatch) + + // unconditional request + if modifiedSince == "" && noneMatch == "" { + return false + } + + // Always return stale when Cache-Control: no-cache + // to support end-to-end reload requests + // https://tools.ietf.org/html/rfc2616#section-14.9.4 + cacheControl := c.Get(HeaderCacheControl) + if cacheControl != "" && isNoCache(cacheControl) { + return false + } + + // if-none-match + if noneMatch != "" && noneMatch != "*" { + etag := c.app.getString(c.fasthttp.Response.Header.Peek(HeaderETag)) + if etag == "" { + return false + } + if c.app.isEtagStale(etag, c.app.getBytes(noneMatch)) { + return false + } + + if modifiedSince != "" { + lastModified := c.app.getString(c.fasthttp.Response.Header.Peek(HeaderLastModified)) + if lastModified != "" { + lastModifiedTime, err := http.ParseTime(lastModified) + if err != nil { + return false + } + modifiedSinceTime, err := http.ParseTime(modifiedSince) + if err != nil { + return false + } + return lastModifiedTime.Before(modifiedSinceTime) + } + } + } + return true +} + +// Get returns the HTTP request header specified by field. +// Field names are case-insensitive +// Returned value is only valid within the handler. Do not store any references. +// Make copies or use the Immutable setting instead. +func (c *Ctx) Get(key string, defaultValue ...string) string { + return defaultString(c.app.getString(c.fasthttp.Request.Header.Peek(key)), defaultValue) +} + +// GetRespHeader returns the HTTP response header specified by field. +// Field names are case-insensitive +// Returned value is only valid within the handler. Do not store any references. +// Make copies or use the Immutable setting instead. +func (c *Ctx) GetRespHeader(key string, defaultValue ...string) string { + return defaultString(c.app.getString(c.fasthttp.Response.Header.Peek(key)), defaultValue) +} + +// GetReqHeaders returns the HTTP request headers. +// Returned value is only valid within the handler. Do not store any references. +// Make copies or use the Immutable setting instead. +func (c *Ctx) GetReqHeaders() map[string][]string { + headers := make(map[string][]string) + c.Request().Header.VisitAll(func(k, v []byte) { + key := c.app.getString(k) + headers[key] = append(headers[key], c.app.getString(v)) + }) + + return headers +} + +// GetRespHeaders returns the HTTP response headers. +// Returned value is only valid within the handler. Do not store any references. +// Make copies or use the Immutable setting instead. +func (c *Ctx) GetRespHeaders() map[string][]string { + headers := make(map[string][]string) + c.Response().Header.VisitAll(func(k, v []byte) { + key := c.app.getString(k) + headers[key] = append(headers[key], c.app.getString(v)) + }) + + return headers +} + +// Hostname contains the hostname derived from the X-Forwarded-Host or Host HTTP header. +// Returned value is only valid within the handler. Do not store any references. +// Make copies or use the Immutable setting instead. +// Please use Config.EnableTrustedProxyCheck to prevent header spoofing, in case when your app is behind the proxy. +func (c *Ctx) Hostname() string { + if c.IsProxyTrusted() { + if host := c.Get(HeaderXForwardedHost); len(host) > 0 { + commaPos := strings.Index(host, ",") + if commaPos != -1 { + return host[:commaPos] + } + return host + } + } + return c.app.getString(c.fasthttp.Request.URI().Host()) +} + +// Port returns the remote port of the request. +func (c *Ctx) Port() string { + tcpaddr, ok := c.fasthttp.RemoteAddr().(*net.TCPAddr) + if !ok { + panic(fmt.Errorf("failed to type-assert to *net.TCPAddr")) + } + return strconv.Itoa(tcpaddr.Port) +} + +// IP returns the remote IP address of the request. +// If ProxyHeader and IP Validation is configured, it will parse that header and return the first valid IP address. +// Please use Config.EnableTrustedProxyCheck to prevent header spoofing, in case when your app is behind the proxy. +func (c *Ctx) IP() string { + if c.IsProxyTrusted() && len(c.app.config.ProxyHeader) > 0 { + return c.extractIPFromHeader(c.app.config.ProxyHeader) + } + + return c.fasthttp.RemoteIP().String() +} + +// extractIPsFromHeader will return a slice of IPs it found given a header name in the order they appear. +// When IP validation is enabled, any invalid IPs will be omitted. +func (c *Ctx) extractIPsFromHeader(header string) []string { + // TODO: Reuse the c.extractIPFromHeader func somehow in here + + headerValue := c.Get(header) + + // We can't know how many IPs we will return, but we will try to guess with this constant division. + // Counting ',' makes function slower for about 50ns in general case. + const maxEstimatedCount = 8 + estimatedCount := len(headerValue) / maxEstimatedCount + if estimatedCount > maxEstimatedCount { + estimatedCount = maxEstimatedCount // Avoid big allocation on big header + } + + ipsFound := make([]string, 0, estimatedCount) + + i := 0 + j := -1 + +iploop: + for { + var v4, v6 bool + + // Manually splitting string without allocating slice, working with parts directly + i, j = j+1, j+2 + + if j > len(headerValue) { + break + } + + for j < len(headerValue) && headerValue[j] != ',' { + if headerValue[j] == ':' { + v6 = true + } else if headerValue[j] == '.' { + v4 = true + } + j++ + } + + for i < j && (headerValue[i] == ' ' || headerValue[i] == ',') { + i++ + } + + s := utils.TrimRight(headerValue[i:j], ' ') + + if c.app.config.EnableIPValidation { + // Skip validation if IP is clearly not IPv4/IPv6, otherwise validate without allocations + if (!v6 && !v4) || (v6 && !utils.IsIPv6(s)) || (v4 && !utils.IsIPv4(s)) { + continue iploop + } + } + + ipsFound = append(ipsFound, s) + } + + return ipsFound +} + +// extractIPFromHeader will attempt to pull the real client IP from the given header when IP validation is enabled. +// currently, it will return the first valid IP address in header. +// when IP validation is disabled, it will simply return the value of the header without any inspection. +// Implementation is almost the same as in extractIPsFromHeader, but without allocation of []string. +func (c *Ctx) extractIPFromHeader(header string) string { + if c.app.config.EnableIPValidation { + headerValue := c.Get(header) + + i := 0 + j := -1 + + iploop: + for { + var v4, v6 bool + + // Manually splitting string without allocating slice, working with parts directly + i, j = j+1, j+2 + + if j > len(headerValue) { + break + } + + for j < len(headerValue) && headerValue[j] != ',' { + if headerValue[j] == ':' { + v6 = true + } else if headerValue[j] == '.' { + v4 = true + } + j++ + } + + for i < j && headerValue[i] == ' ' { + i++ + } + + s := utils.TrimRight(headerValue[i:j], ' ') + + if c.app.config.EnableIPValidation { + if (!v6 && !v4) || (v6 && !utils.IsIPv6(s)) || (v4 && !utils.IsIPv4(s)) { + continue iploop + } + } + + return s + } + + return c.fasthttp.RemoteIP().String() + } + + // default behavior if IP validation is not enabled is just to return whatever value is + // in the proxy header. Even if it is empty or invalid + return c.Get(c.app.config.ProxyHeader) +} + +// IPs returns a string slice of IP addresses specified in the X-Forwarded-For request header. +// When IP validation is enabled, only valid IPs are returned. +func (c *Ctx) IPs() []string { + return c.extractIPsFromHeader(HeaderXForwardedFor) +} + +// Is returns the matching content type, +// if the incoming request's Content-Type HTTP header field matches the MIME type specified by the type parameter +func (c *Ctx) Is(extension string) bool { + extensionHeader := utils.GetMIME(extension) + if extensionHeader == "" { + return false + } + + return strings.HasPrefix( + utils.TrimLeft(c.app.getString(c.fasthttp.Request.Header.ContentType()), ' '), + extensionHeader, + ) +} + +// JSON converts any interface or string to JSON. +// Array and slice values encode as JSON arrays, +// except that []byte encodes as a base64-encoded string, +// and a nil slice encodes as the null JSON value. +// If the ctype parameter is given, this method will set the +// Content-Type header equal to ctype. If ctype is not given, +// The Content-Type header will be set to application/json. +func (c *Ctx) JSON(data interface{}, ctype ...string) error { + raw, err := c.app.config.JSONEncoder(data) + if err != nil { + return err + } + c.fasthttp.Response.SetBodyRaw(raw) + if len(ctype) > 0 { + c.fasthttp.Response.Header.SetContentType(ctype[0]) + } else { + c.fasthttp.Response.Header.SetContentType(MIMEApplicationJSON) + } + return nil +} + +// JSONP sends a JSON response with JSONP support. +// This method is identical to JSON, except that it opts-in to JSONP callback support. +// By default, the callback name is simply callback. +func (c *Ctx) JSONP(data interface{}, callback ...string) error { + raw, err := c.app.config.JSONEncoder(data) + if err != nil { + return err + } + + var result, cb string + + if len(callback) > 0 { + cb = callback[0] + } else { + cb = "callback" + } + + result = cb + "(" + c.app.getString(raw) + ");" + + c.setCanonical(HeaderXContentTypeOptions, "nosniff") + c.fasthttp.Response.Header.SetContentType(MIMETextJavaScriptCharsetUTF8) + return c.SendString(result) +} + +// XML converts any interface or string to XML. +// This method also sets the content header to application/xml. +func (c *Ctx) XML(data interface{}) error { + raw, err := c.app.config.XMLEncoder(data) + if err != nil { + return err + } + c.fasthttp.Response.SetBodyRaw(raw) + c.fasthttp.Response.Header.SetContentType(MIMEApplicationXML) + return nil +} + +// Links joins the links followed by the property to populate the response's Link HTTP header field. +func (c *Ctx) Links(link ...string) { + if len(link) == 0 { + return + } + bb := bytebufferpool.Get() + for i := range link { + if i%2 == 0 { + _ = bb.WriteByte('<') //nolint:errcheck // This will never fail + _, _ = bb.WriteString(link[i]) //nolint:errcheck // This will never fail + _ = bb.WriteByte('>') //nolint:errcheck // This will never fail + } else { + _, _ = bb.WriteString(`; rel="` + link[i] + `",`) //nolint:errcheck // This will never fail + } + } + c.setCanonical(HeaderLink, utils.TrimRight(c.app.getString(bb.Bytes()), ',')) + bytebufferpool.Put(bb) +} + +// Locals makes it possible to pass interface{} values under keys scoped to the request +// and therefore available to all following routes that match the request. +// +// All the values are removed from ctx after returning from the top +// RequestHandler. Additionally, Close method is called on each value +// implementing io.Closer before removing the value from ctx. +func (c *Ctx) Locals(key interface{}, value ...interface{}) interface{} { + if len(value) == 0 { + return c.fasthttp.UserValue(key) + } + c.fasthttp.SetUserValue(key, value[0]) + return value[0] +} + +// Location sets the response Location HTTP header to the specified path parameter. +func (c *Ctx) Location(path string) { + c.setCanonical(HeaderLocation, path) +} + +// Method returns the HTTP request method for the context, optionally overridden by the provided argument. +// If no override is given or if the provided override is not a valid HTTP method, it returns the current method from the context. +// Otherwise, it updates the context's method and returns the overridden method as a string. +func (c *Ctx) Method(override ...string) string { + if len(override) == 0 { + // Nothing to override, just return current method from context + return c.method + } + + method := utils.ToUpper(override[0]) + mINT := c.app.methodInt(method) + if mINT == -1 { + // Provided override does not valid HTTP method, no override, return current method + return c.method + } + + c.method = method + c.methodINT = mINT + return c.method +} + +// MultipartForm parse form entries from binary. +// This returns a map[string][]string, so given a key the value will be a string slice. +func (c *Ctx) MultipartForm() (*multipart.Form, error) { + return c.fasthttp.MultipartForm() +} + +// ClientHelloInfo return CHI from context +func (c *Ctx) ClientHelloInfo() *tls.ClientHelloInfo { + if c.app.tlsHandler != nil { + return c.app.tlsHandler.clientHelloInfo + } + + return nil +} + +// Next executes the next method in the stack that matches the current route. +func (c *Ctx) Next() error { + // Increment handler index + c.indexHandler++ + var err error + // Did we execute all route handlers? + if c.indexHandler < len(c.route.Handlers) { + // Continue route stack + err = c.route.Handlers[c.indexHandler](c) + } else { + // Continue handler stack + _, err = c.app.next(c) + } + return err +} + +// RestartRouting instead of going to the next handler. This may be useful after +// changing the request path. Note that handlers might be executed again. +func (c *Ctx) RestartRouting() error { + c.indexRoute = -1 + _, err := c.app.next(c) + return err +} + +// OriginalURL contains the original request URL. +// Returned value is only valid within the handler. Do not store any references. +// Make copies or use the Immutable setting to use the value outside the Handler. +func (c *Ctx) OriginalURL() string { + return c.app.getString(c.fasthttp.Request.Header.RequestURI()) +} + +// Params is used to get the route parameters. +// Defaults to empty string "" if the param doesn't exist. +// If a default value is given, it will return that value if the param doesn't exist. +// Returned value is only valid within the handler. Do not store any references. +// Make copies or use the Immutable setting to use the value outside the Handler. +func (c *Ctx) Params(key string, defaultValue ...string) string { + if key == "*" || key == "+" { + key += "1" + } + for i := range c.route.Params { + if len(key) != len(c.route.Params[i]) { + continue + } + if c.route.Params[i] == key || (!c.app.config.CaseSensitive && utils.EqualFold(c.route.Params[i], key)) { + // in case values are not here + if len(c.values) <= i || len(c.values[i]) == 0 { + break + } + return c.values[i] + } + } + return defaultString("", defaultValue) +} + +// AllParams Params is used to get all route parameters. +// Using Params method to get params. +func (c *Ctx) AllParams() map[string]string { + params := make(map[string]string, len(c.route.Params)) + for _, param := range c.route.Params { + params[param] = c.Params(param) + } + + return params +} + +// ParamsParser binds the param string to a struct. +func (c *Ctx) ParamsParser(out interface{}) error { + params := make(map[string][]string, len(c.route.Params)) + for _, param := range c.route.Params { + params[param] = append(params[param], c.Params(param)) + } + return c.parseToStruct(paramsTag, out, params) +} + +// ParamsInt is used to get an integer from the route parameters +// it defaults to zero if the parameter is not found or if the +// parameter cannot be converted to an integer +// If a default value is given, it will return that value in case the param +// doesn't exist or cannot be converted to an integer +func (c *Ctx) ParamsInt(key string, defaultValue ...int) (int, error) { + // Use Atoi to convert the param to an int or return zero and an error + value, err := strconv.Atoi(c.Params(key)) + if err != nil { + if len(defaultValue) > 0 { + return defaultValue[0], nil + } + return 0, fmt.Errorf("failed to convert: %w", err) + } + + return value, nil +} + +// Path returns the path part of the request URL. +// Optionally, you could override the path. +func (c *Ctx) Path(override ...string) string { + if len(override) != 0 && c.path != override[0] { + // Set new path to context + c.pathOriginal = override[0] + + // Set new path to request context + c.fasthttp.Request.URI().SetPath(c.pathOriginal) + // Prettify path + c.configDependentPaths() + } + return c.path +} + +// Protocol contains the request protocol string: http or https for TLS requests. +// Please use Config.EnableTrustedProxyCheck to prevent header spoofing, in case when your app is behind the proxy. +func (c *Ctx) Protocol() string { + if c.fasthttp.IsTLS() { + return schemeHTTPS + } + if !c.IsProxyTrusted() { + return schemeHTTP + } + + scheme := schemeHTTP + const lenXHeaderName = 12 + c.fasthttp.Request.Header.VisitAll(func(key, val []byte) { + if len(key) < lenXHeaderName { + return // Neither "X-Forwarded-" nor "X-Url-Scheme" + } + switch { + case bytes.HasPrefix(key, []byte("X-Forwarded-")): + if bytes.Equal(key, []byte(HeaderXForwardedProto)) || + bytes.Equal(key, []byte(HeaderXForwardedProtocol)) { + v := c.app.getString(val) + commaPos := strings.Index(v, ",") + if commaPos != -1 { + scheme = v[:commaPos] + } else { + scheme = v + } + } else if bytes.Equal(key, []byte(HeaderXForwardedSsl)) && bytes.Equal(val, []byte("on")) { + scheme = schemeHTTPS + } + + case bytes.Equal(key, []byte(HeaderXUrlScheme)): + scheme = c.app.getString(val) + } + }) + return scheme +} + +// Query returns the query string parameter in the url. +// Defaults to empty string "" if the query doesn't exist. +// If a default value is given, it will return that value if the query doesn't exist. +// Returned value is only valid within the handler. Do not store any references. +// Make copies or use the Immutable setting to use the value outside the Handler. +func (c *Ctx) Query(key string, defaultValue ...string) string { + return defaultString(c.app.getString(c.fasthttp.QueryArgs().Peek(key)), defaultValue) +} + +// Queries returns a map of query parameters and their values. +// +// GET /?name=alex&wanna_cake=2&id= +// Queries()["name"] == "alex" +// Queries()["wanna_cake"] == "2" +// Queries()["id"] == "" +// +// GET /?field1=value1&field1=value2&field2=value3 +// Queries()["field1"] == "value2" +// Queries()["field2"] == "value3" +// +// GET /?list_a=1&list_a=2&list_a=3&list_b[]=1&list_b[]=2&list_b[]=3&list_c=1,2,3 +// Queries()["list_a"] == "3" +// Queries()["list_b[]"] == "3" +// Queries()["list_c"] == "1,2,3" +// +// GET /api/search?filters.author.name=John&filters.category.name=Technology&filters[customer][name]=Alice&filters[status]=pending +// Queries()["filters.author.name"] == "John" +// Queries()["filters.category.name"] == "Technology" +// Queries()["filters[customer][name]"] == "Alice" +// Queries()["filters[status]"] == "pending" +func (c *Ctx) Queries() map[string]string { + m := make(map[string]string, c.Context().QueryArgs().Len()) + c.Context().QueryArgs().VisitAll(func(key, value []byte) { + m[c.app.getString(key)] = c.app.getString(value) + }) + return m +} + +// QueryInt returns integer value of key string parameter in the url. +// Default to empty or invalid key is 0. +// +// GET /?name=alex&wanna_cake=2&id= +// QueryInt("wanna_cake", 1) == 2 +// QueryInt("name", 1) == 1 +// QueryInt("id", 1) == 1 +// QueryInt("id") == 0 +func (c *Ctx) QueryInt(key string, defaultValue ...int) int { + // Use Atoi to convert the param to an int or return zero and an error + value, err := strconv.Atoi(c.app.getString(c.fasthttp.QueryArgs().Peek(key))) + if err != nil { + if len(defaultValue) > 0 { + return defaultValue[0] + } + return 0 + } + + return value +} + +// QueryBool returns bool value of key string parameter in the url. +// Default to empty or invalid key is false. +// +// Get /?name=alex&want_pizza=false&id= +// QueryBool("want_pizza") == false +// QueryBool("want_pizza", true) == false +// QueryBool("name") == false +// QueryBool("name", true) == true +// QueryBool("id") == false +// QueryBool("id", true) == true +func (c *Ctx) QueryBool(key string, defaultValue ...bool) bool { + value, err := strconv.ParseBool(c.app.getString(c.fasthttp.QueryArgs().Peek(key))) + if err != nil { + if len(defaultValue) > 0 { + return defaultValue[0] + } + return false + } + return value +} + +// QueryFloat returns float64 value of key string parameter in the url. +// Default to empty or invalid key is 0. +// +// GET /?name=alex&amount=32.23&id= +// QueryFloat("amount") = 32.23 +// QueryFloat("amount", 3) = 32.23 +// QueryFloat("name", 1) = 1 +// QueryFloat("name") = 0 +// QueryFloat("id", 3) = 3 +func (c *Ctx) QueryFloat(key string, defaultValue ...float64) float64 { + // use strconv.ParseFloat to convert the param to a float or return zero and an error. + value, err := strconv.ParseFloat(c.app.getString(c.fasthttp.QueryArgs().Peek(key)), 64) + if err != nil { + if len(defaultValue) > 0 { + return defaultValue[0] + } + return 0 + } + return value +} + +// QueryParser binds the query string to a struct. +func (c *Ctx) QueryParser(out interface{}) error { + data := make(map[string][]string) + var err error + + c.fasthttp.QueryArgs().VisitAll(func(key, val []byte) { + if err != nil { + return + } + + k := c.app.getString(key) + v := c.app.getString(val) + + err = formatParserData(out, data, queryTag, k, v, c.app.config.EnableSplittingOnParsers, true) + }) + + if err != nil { + return err + } + + return c.parseToStruct(queryTag, out, data) +} + +// ReqHeaderParser binds the request header strings to a struct. +func (c *Ctx) ReqHeaderParser(out interface{}) error { + data := make(map[string][]string) + var err error + + c.fasthttp.Request.Header.VisitAll(func(key, val []byte) { + if err != nil { + return + } + + k := c.app.getString(key) + v := c.app.getString(val) + + err = formatParserData(out, data, reqHeaderTag, k, v, c.app.config.EnableSplittingOnParsers, false) + }) + + if err != nil { + return err + } + + return c.parseToStruct(reqHeaderTag, out, data) +} + +func (*Ctx) parseToStruct(aliasTag string, out interface{}, data map[string][]string) error { + // Get decoder from pool + schemaDecoder, ok := decoderPoolMap[aliasTag].Get().(*schema.Decoder) + if !ok { + panic(fmt.Errorf("failed to type-assert to *schema.Decoder")) + } + defer decoderPoolMap[aliasTag].Put(schemaDecoder) + + // Set alias tag + schemaDecoder.SetAliasTag(aliasTag) + + if err := schemaDecoder.Decode(out, data); err != nil { + return fmt.Errorf("failed to decode: %w", err) + } + + return nil +} + +func equalFieldType(out interface{}, kind reflect.Kind, key, tag string) bool { + // Get type of interface + outTyp := reflect.TypeOf(out).Elem() + key = utils.ToLower(key) + // Must be a struct to match a field + if outTyp.Kind() != reflect.Struct { + return false + } + // Copy interface to an value to be used + outVal := reflect.ValueOf(out).Elem() + // Loop over each field + for i := 0; i < outTyp.NumField(); i++ { + // Get field value data + structField := outVal.Field(i) + // Can this field be changed? + if !structField.CanSet() { + continue + } + // Get field key data + typeField := outTyp.Field(i) + // Get type of field key + structFieldKind := structField.Kind() + // Does the field type equals input? + if structFieldKind != kind { + continue + } + // Get tag from field if exist + inputFieldName := typeField.Tag.Get(tag) + if inputFieldName == "" { + inputFieldName = typeField.Name + } else { + inputFieldName = strings.Split(inputFieldName, ",")[0] + } + // Compare field/tag with provided key + if utils.ToLower(inputFieldName) == key { + return true + } + } + return false +} + +var ( + ErrRangeMalformed = errors.New("range: malformed range header string") + ErrRangeUnsatisfiable = errors.New("range: unsatisfiable range") +) + +// Range returns a struct containing the type and a slice of ranges. +func (c *Ctx) Range(size int) (Range, error) { + var ( + rangeData Range + ranges string + ) + rangeStr := c.Get(HeaderRange) + + i := strings.IndexByte(rangeStr, '=') + if i == -1 || strings.Contains(rangeStr[i+1:], "=") { + return rangeData, ErrRangeMalformed + } + rangeData.Type = rangeStr[:i] + ranges = rangeStr[i+1:] + + var ( + singleRange string + moreRanges = ranges + ) + for moreRanges != "" { + singleRange = moreRanges + if i := strings.IndexByte(moreRanges, ','); i >= 0 { + singleRange = moreRanges[:i] + moreRanges = moreRanges[i+1:] + } else { + moreRanges = "" + } + + var ( + startStr, endStr string + i int + ) + if i = strings.IndexByte(singleRange, '-'); i == -1 { + return rangeData, ErrRangeMalformed + } + startStr = singleRange[:i] + endStr = singleRange[i+1:] + + start, startErr := fasthttp.ParseUint(utils.UnsafeBytes(startStr)) + end, endErr := fasthttp.ParseUint(utils.UnsafeBytes(endStr)) + if startErr != nil { // -nnn + start = size - end + end = size - 1 + } else if endErr != nil { // nnn- + end = size - 1 + } + if end > size-1 { // limit last-byte-pos to current length + end = size - 1 + } + if start > end || start < 0 { + continue + } + rangeData.Ranges = append(rangeData.Ranges, struct { + Start int + End int + }{ + start, + end, + }) + } + if len(rangeData.Ranges) < 1 { + return rangeData, ErrRangeUnsatisfiable + } + + return rangeData, nil +} + +// Redirect to the URL derived from the specified path, with specified status. +// If status is not specified, status defaults to 302 Found. +func (c *Ctx) Redirect(location string, status ...int) error { + c.setCanonical(HeaderLocation, location) + if len(status) > 0 { + c.Status(status[0]) + } else { + c.Status(StatusFound) + } + return nil +} + +// Bind Add vars to default view var map binding to template engine. +// Variables are read by the Render method and may be overwritten. +func (c *Ctx) Bind(vars Map) error { + // init viewBindMap - lazy map + for k, v := range vars { + c.viewBindMap.Store(k, v) + } + return nil +} + +// getLocationFromRoute get URL location from route using parameters +func (c *Ctx) getLocationFromRoute(route Route, params Map) (string, error) { + buf := bytebufferpool.Get() + for _, segment := range route.routeParser.segs { + if !segment.IsParam { + _, err := buf.WriteString(segment.Const) + if err != nil { + return "", fmt.Errorf("failed to write string: %w", err) + } + continue + } + + for key, val := range params { + isSame := key == segment.ParamName || (!c.app.config.CaseSensitive && utils.EqualFold(key, segment.ParamName)) + isGreedy := segment.IsGreedy && len(key) == 1 && isInCharset(key[0], greedyParameters) + if isSame || isGreedy { + _, err := buf.WriteString(utils.ToString(val)) + if err != nil { + return "", fmt.Errorf("failed to write string: %w", err) + } + } + } + } + location := buf.String() + // release buffer + bytebufferpool.Put(buf) + return location, nil +} + +// GetRouteURL generates URLs to named routes, with parameters. URLs are relative, for example: "/user/1831" +func (c *Ctx) GetRouteURL(routeName string, params Map) (string, error) { + return c.getLocationFromRoute(c.App().GetRoute(routeName), params) +} + +// RedirectToRoute to the Route registered in the app with appropriate parameters +// If status is not specified, status defaults to 302 Found. +// If you want to send queries to route, you must add "queries" key typed as map[string]string to params. +func (c *Ctx) RedirectToRoute(routeName string, params Map, status ...int) error { + location, err := c.getLocationFromRoute(c.App().GetRoute(routeName), params) + if err != nil { + return err + } + + // Check queries + if queries, ok := params["queries"].(map[string]string); ok { + queryText := bytebufferpool.Get() + defer bytebufferpool.Put(queryText) + + i := 1 + for k, v := range queries { + _, _ = queryText.WriteString(k + "=" + v) //nolint:errcheck // This will never fail + + if i != len(queries) { + _, _ = queryText.WriteString("&") //nolint:errcheck // This will never fail + } + i++ + } + + return c.Redirect(location+"?"+queryText.String(), status...) + } + return c.Redirect(location, status...) +} + +// RedirectBack to the URL to referer +// If status is not specified, status defaults to 302 Found. +func (c *Ctx) RedirectBack(fallback string, status ...int) error { + location := c.Get(HeaderReferer) + if location == "" { + location = fallback + } + return c.Redirect(location, status...) +} + +// Render a template with data and sends a text/html response. +// We support the following engines: html, amber, handlebars, mustache, pug +func (c *Ctx) Render(name string, bind interface{}, layouts ...string) error { + // Get new buffer from pool + buf := bytebufferpool.Get() + defer bytebufferpool.Put(buf) + + // Initialize empty bind map if bind is nil + if bind == nil { + bind = make(Map) + } + + // Pass-locals-to-views, bind, appListKeys + c.renderExtensions(bind) + + var rendered bool + for i := len(c.app.mountFields.appListKeys) - 1; i >= 0; i-- { + prefix := c.app.mountFields.appListKeys[i] + app := c.app.mountFields.appList[prefix] + if prefix == "" || strings.Contains(c.OriginalURL(), prefix) { + if len(layouts) == 0 && app.config.ViewsLayout != "" { + layouts = []string{ + app.config.ViewsLayout, + } + } + + // Render template from Views + if app.config.Views != nil { + if err := app.config.Views.Render(buf, name, bind, layouts...); err != nil { + return fmt.Errorf("failed to render: %w", err) + } + + rendered = true + break + } + } + } + + if !rendered { + // Render raw template using 'name' as filepath if no engine is set + var tmpl *template.Template + if _, err := readContent(buf, name); err != nil { + return err + } + // Parse template + tmpl, err := template.New("").Parse(c.app.getString(buf.Bytes())) + if err != nil { + return fmt.Errorf("failed to parse: %w", err) + } + buf.Reset() + // Render template + if err := tmpl.Execute(buf, bind); err != nil { + return fmt.Errorf("failed to execute: %w", err) + } + } + + // Set Content-Type to text/html + c.fasthttp.Response.Header.SetContentType(MIMETextHTMLCharsetUTF8) + // Set rendered template to body + c.fasthttp.Response.SetBody(buf.Bytes()) + + return nil +} + +func (c *Ctx) renderExtensions(bind interface{}) { + if bindMap, ok := bind.(Map); ok { + // Bind view map + c.viewBindMap.Range(func(key, value interface{}) bool { + keyValue, ok := key.(string) + if !ok { + return true + } + if _, ok := bindMap[keyValue]; !ok { + bindMap[keyValue] = value + } + return true + }) + + // Check if the PassLocalsToViews option is enabled (by default it is disabled) + if c.app.config.PassLocalsToViews { + // Loop through each local and set it in the map + c.fasthttp.VisitUserValues(func(key []byte, val interface{}) { + // check if bindMap doesn't contain the key + if _, ok := bindMap[c.app.getString(key)]; !ok { + // Set the key and value in the bindMap + bindMap[c.app.getString(key)] = val + } + }) + } + } + + if len(c.app.mountFields.appListKeys) == 0 { + c.app.generateAppListKeys() + } +} + +// Route returns the matched Route struct. +func (c *Ctx) Route() *Route { + if c.route == nil { + // Fallback for fasthttp error handler + return &Route{ + path: c.pathOriginal, + Path: c.pathOriginal, + Method: c.method, + Handlers: make([]Handler, 0), + Params: make([]string, 0), + } + } + return c.route +} + +// SaveFile saves any multipart file to disk. +func (*Ctx) SaveFile(fileheader *multipart.FileHeader, path string) error { + return fasthttp.SaveMultipartFile(fileheader, path) +} + +// SaveFileToStorage saves any multipart file to an external storage system. +func (*Ctx) SaveFileToStorage(fileheader *multipart.FileHeader, path string, storage Storage) error { + file, err := fileheader.Open() + if err != nil { + return fmt.Errorf("failed to open: %w", err) + } + + content, err := io.ReadAll(file) + if err != nil { + return fmt.Errorf("failed to read: %w", err) + } + + if err := storage.Set(path, content, 0); err != nil { + return fmt.Errorf("failed to store: %w", err) + } + + return nil +} + +// Secure returns whether a secure connection was established. +func (c *Ctx) Secure() bool { + return c.Protocol() == schemeHTTPS +} + +// Send sets the HTTP response body without copying it. +// From this point onward the body argument must not be changed. +func (c *Ctx) Send(body []byte) error { + // Write response body + c.fasthttp.Response.SetBodyRaw(body) + return nil +} + +var ( + sendFileOnce sync.Once + sendFileFS *fasthttp.FS + sendFileHandler fasthttp.RequestHandler +) + +// SendFile transfers the file from the given path. +// The file is not compressed by default, enable this by passing a 'true' argument +// Sets the Content-Type response HTTP header field based on the filenames extension. +func (c *Ctx) SendFile(file string, compress ...bool) error { + // Save the filename, we will need it in the error message if the file isn't found + filename := file + + // https://github.com/valyala/fasthttp/blob/c7576cc10cabfc9c993317a2d3f8355497bea156/fs.go#L129-L134 + sendFileOnce.Do(func() { + const cacheDuration = 10 * time.Second + sendFileFS = &fasthttp.FS{ + Root: "", + AllowEmptyRoot: true, + GenerateIndexPages: false, + AcceptByteRange: true, + Compress: true, + CompressedFileSuffix: c.app.config.CompressedFileSuffix, + CacheDuration: cacheDuration, + IndexNames: []string{"index.html"}, + PathNotFound: func(ctx *fasthttp.RequestCtx) { + ctx.Response.SetStatusCode(StatusNotFound) + }, + } + sendFileHandler = sendFileFS.NewRequestHandler() + }) + + // Keep original path for mutable params + c.pathOriginal = utils.CopyString(c.pathOriginal) + // Disable compression + if len(compress) == 0 || !compress[0] { + // https://github.com/valyala/fasthttp/blob/7cc6f4c513f9e0d3686142e0a1a5aa2f76b3194a/fs.go#L55 + c.fasthttp.Request.Header.Del(HeaderAcceptEncoding) + } + // copy of https://github.com/valyala/fasthttp/blob/7cc6f4c513f9e0d3686142e0a1a5aa2f76b3194a/fs.go#L103-L121 with small adjustments + if len(file) == 0 || !filepath.IsAbs(file) { + // extend relative path to absolute path + hasTrailingSlash := len(file) > 0 && (file[len(file)-1] == '/' || file[len(file)-1] == '\\') + + var err error + file = filepath.FromSlash(file) + if file, err = filepath.Abs(file); err != nil { + return fmt.Errorf("failed to determine abs file path: %w", err) + } + if hasTrailingSlash { + file += "/" + } + } + // convert the path to forward slashes regardless the OS in order to set the URI properly + // the handler will convert back to OS path separator before opening the file + file = filepath.ToSlash(file) + + // Restore the original requested URL + originalURL := utils.CopyString(c.OriginalURL()) + defer c.fasthttp.Request.SetRequestURI(originalURL) + // Set new URI for fileHandler + c.fasthttp.Request.SetRequestURI(file) + // Save status code + status := c.fasthttp.Response.StatusCode() + // Serve file + sendFileHandler(c.fasthttp) + // Get the status code which is set by fasthttp + fsStatus := c.fasthttp.Response.StatusCode() + // Set the status code set by the user if it is different from the fasthttp status code and 200 + if status != fsStatus && status != StatusOK { + c.Status(status) + } + // Check for error + if status != StatusNotFound && fsStatus == StatusNotFound { + return NewError(StatusNotFound, fmt.Sprintf("sendfile: file %s not found", filename)) + } + return nil +} + +// SendStatus sets the HTTP status code and if the response body is empty, +// it sets the correct status message in the body. +func (c *Ctx) SendStatus(status int) error { + c.Status(status) + + // Only set status body when there is no response body + if len(c.fasthttp.Response.Body()) == 0 { + return c.SendString(utils.StatusMessage(status)) + } + + return nil +} + +// SendString sets the HTTP response body for string types. +// This means no type assertion, recommended for faster performance +func (c *Ctx) SendString(body string) error { + c.fasthttp.Response.SetBodyString(body) + + return nil +} + +// SendStream sets response body stream and optional body size. +func (c *Ctx) SendStream(stream io.Reader, size ...int) error { + if len(size) > 0 && size[0] >= 0 { + c.fasthttp.Response.SetBodyStream(stream, size[0]) + } else { + c.fasthttp.Response.SetBodyStream(stream, -1) + } + + return nil +} + +// Set sets the response's HTTP header field to the specified key, value. +func (c *Ctx) Set(key, val string) { + c.fasthttp.Response.Header.Set(key, val) +} + +func (c *Ctx) setCanonical(key, val string) { + c.fasthttp.Response.Header.SetCanonical(c.app.getBytes(key), c.app.getBytes(val)) +} + +// Subdomains returns a string slice of subdomains in the domain name of the request. +// The subdomain offset, which defaults to 2, is used for determining the beginning of the subdomain segments. +func (c *Ctx) Subdomains(offset ...int) []string { + o := 2 + if len(offset) > 0 { + o = offset[0] + } + subdomains := strings.Split(c.Hostname(), ".") + l := len(subdomains) - o + // Check index to avoid slice bounds out of range panic + if l < 0 { + l = len(subdomains) + } + subdomains = subdomains[:l] + return subdomains +} + +// Stale is not implemented yet, pull requests are welcome! +func (c *Ctx) Stale() bool { + return !c.Fresh() +} + +// Status sets the HTTP status for the response. +// This method is chainable. +func (c *Ctx) Status(status int) *Ctx { + c.fasthttp.Response.SetStatusCode(status) + return c +} + +// String returns unique string representation of the ctx. +// +// The returned value may be useful for logging. +func (c *Ctx) String() string { + return fmt.Sprintf( + "#%016X - %s <-> %s - %s %s", + c.fasthttp.ID(), + c.fasthttp.LocalAddr(), + c.fasthttp.RemoteAddr(), + c.fasthttp.Request.Header.Method(), + c.fasthttp.URI().FullURI(), + ) +} + +// Type sets the Content-Type HTTP header to the MIME type specified by the file extension. +func (c *Ctx) Type(extension string, charset ...string) *Ctx { + if len(charset) > 0 { + c.fasthttp.Response.Header.SetContentType(utils.GetMIME(extension) + "; charset=" + charset[0]) + } else { + c.fasthttp.Response.Header.SetContentType(utils.GetMIME(extension)) + } + return c +} + +// Vary adds the given header field to the Vary response header. +// This will append the header, if not already listed, otherwise leaves it listed in the current location. +func (c *Ctx) Vary(fields ...string) { + c.Append(HeaderVary, fields...) +} + +// Write appends p into response body. +func (c *Ctx) Write(p []byte) (int, error) { + c.fasthttp.Response.AppendBody(p) + return len(p), nil +} + +// Writef appends f & a into response body writer. +func (c *Ctx) Writef(f string, a ...interface{}) (int, error) { + //nolint:wrapcheck // This must not be wrapped + return fmt.Fprintf(c.fasthttp.Response.BodyWriter(), f, a...) +} + +// WriteString appends s to response body. +func (c *Ctx) WriteString(s string) (int, error) { + c.fasthttp.Response.AppendBodyString(s) + return len(s), nil +} + +// XHR returns a Boolean property, that is true, if the request's X-Requested-With header field is XMLHttpRequest, +// indicating that the request was issued by a client library (such as jQuery). +func (c *Ctx) XHR() bool { + return utils.EqualFoldBytes(c.app.getBytes(c.Get(HeaderXRequestedWith)), []byte("xmlhttprequest")) +} + +// configDependentPaths set paths for route recognition and prepared paths for the user, +// here the features for caseSensitive, decoded paths, strict paths are evaluated +func (c *Ctx) configDependentPaths() { + c.pathBuffer = append(c.pathBuffer[0:0], c.pathOriginal...) + // If UnescapePath enabled, we decode the path and save it for the framework user + if c.app.config.UnescapePath { + c.pathBuffer = fasthttp.AppendUnquotedArg(c.pathBuffer[:0], c.pathBuffer) + } + c.path = c.app.getString(c.pathBuffer) + + // another path is specified which is for routing recognition only + // use the path that was changed by the previous configuration flags + c.detectionPathBuffer = append(c.detectionPathBuffer[0:0], c.pathBuffer...) + // If CaseSensitive is disabled, we lowercase the original path + if !c.app.config.CaseSensitive { + c.detectionPathBuffer = utils.ToLowerBytes(c.detectionPathBuffer) + } + // If StrictRouting is disabled, we strip all trailing slashes + if !c.app.config.StrictRouting && len(c.detectionPathBuffer) > 1 && c.detectionPathBuffer[len(c.detectionPathBuffer)-1] == '/' { + c.detectionPathBuffer = utils.TrimRightBytes(c.detectionPathBuffer, '/') + } + c.detectionPath = c.app.getString(c.detectionPathBuffer) + + // Define the path for dividing routes into areas for fast tree detection, so that fewer routes need to be traversed, + // since the first three characters area select a list of routes + c.treePath = c.treePath[0:0] + const maxDetectionPaths = 3 + if len(c.detectionPath) >= maxDetectionPaths { + c.treePath = c.detectionPath[:maxDetectionPaths] + } +} + +func (c *Ctx) IsProxyTrusted() bool { + if !c.app.config.EnableTrustedProxyCheck { + return true + } + + ip := c.fasthttp.RemoteIP() + + if _, trusted := c.app.config.trustedProxiesMap[ip.String()]; trusted { + return true + } + + for _, ipNet := range c.app.config.trustedProxyRanges { + if ipNet.Contains(ip) { + return true + } + } + + return false +} + +var localHosts = [...]string{"127.0.0.1", "::1"} + +// IsLocalHost will return true if address is a localhost address. +func (*Ctx) isLocalHost(address string) bool { + for _, h := range localHosts { + if address == h { + return true + } + } + return false +} + +// IsFromLocal will return true if request came from local. +func (c *Ctx) IsFromLocal() bool { + return c.isLocalHost(c.fasthttp.RemoteIP().String()) +} + +func (c *Ctx) getBody() []byte { + if c.app.config.Immutable { + return utils.CopyBytes(c.fasthttp.Request.Body()) + } + + return c.fasthttp.Request.Body() +} diff --git a/ctx_test.go b/ctx_test.go new file mode 100644 index 0000000..6a3998f --- /dev/null +++ b/ctx_test.go @@ -0,0 +1,5819 @@ +// ⚡️ Fiber is an Express inspired web framework written in Go with ☕️ +// 🤖 Github Repository: https://github.com/gofiber/fiber +// 📌 API Documentation: https://docs.gofiber.io + +//nolint:bodyclose // Much easier to just ignore memory leaks in tests +package fiber + +import ( + "bufio" + "bytes" + "compress/gzip" + "compress/zlib" + "context" + "crypto/tls" + "encoding/xml" + "errors" + "fmt" + "io" + "mime/multipart" + "net" + "net/http/httptest" + "net/url" + "os" + "path/filepath" + "reflect" + "strconv" + "strings" + "testing" + "text/template" + "time" + + "github.com/gofiber/fiber/v2/internal/storage/memory" + "github.com/gofiber/fiber/v2/utils" + + "github.com/valyala/bytebufferpool" + "github.com/valyala/fasthttp" +) + +// go test -run Test_Ctx_Accepts +func Test_Ctx_Accepts(t *testing.T) { + t.Parallel() + app := New() + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + c.Request().Header.Set(HeaderAccept, "text/html,application/xhtml+xml,application/xml;q=0.9") + utils.AssertEqual(t, "", c.Accepts("")) + utils.AssertEqual(t, "", c.Accepts()) + utils.AssertEqual(t, ".xml", c.Accepts(".xml")) + utils.AssertEqual(t, "", c.Accepts(".john")) + utils.AssertEqual(t, "application/xhtml+xml", c.Accepts("application/xml", "application/xml+rss", "application/yaml", "application/xhtml+xml"), "must use client-preferred mime type") + + c.Request().Header.Set(HeaderAccept, "application/json, text/plain, */*;q=0") + utils.AssertEqual(t, "", c.Accepts("html"), "must treat */*;q=0 as not acceptable") + + c.Request().Header.Set(HeaderAccept, "text/*, application/json") + utils.AssertEqual(t, "html", c.Accepts("html")) + utils.AssertEqual(t, "text/html", c.Accepts("text/html")) + utils.AssertEqual(t, "json", c.Accepts("json", "text")) + utils.AssertEqual(t, "application/json", c.Accepts("application/json")) + utils.AssertEqual(t, "", c.Accepts("image/png")) + utils.AssertEqual(t, "", c.Accepts("png")) + + c.Request().Header.Set(HeaderAccept, "text/html, application/json") + utils.AssertEqual(t, "text/*", c.Accepts("text/*")) + + c.Request().Header.Set(HeaderAccept, "*/*") + utils.AssertEqual(t, "html", c.Accepts("html")) +} + +// go test -v -run=^$ -bench=Benchmark_Ctx_Accepts -benchmem -count=4 +func Benchmark_Ctx_Accepts(b *testing.B) { + app := New() + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + acceptHeader := "text/html,application/xhtml+xml,application/xml;q=0.9" + c.Request().Header.Set("Accept", acceptHeader) + acceptValues := [][]string{ + {".xml"}, + {"json", "xml"}, + {"application/json", "application/xml"}, + } + expectedResults := []string{".xml", "xml", "application/xml"} + + for i := 0; i < len(acceptValues); i++ { + b.Run(fmt.Sprintf("run-%#v", acceptValues[i]), func(bb *testing.B) { + var res string + bb.ReportAllocs() + bb.ResetTimer() + + for n := 0; n < bb.N; n++ { + res = c.Accepts(acceptValues[i]...) + } + utils.AssertEqual(bb, expectedResults[i], res) + }) + } +} + +// go test -run Test_Ctx_Accepts_EmptyAccept +func Test_Ctx_Accepts_EmptyAccept(t *testing.T) { + t.Parallel() + app := New() + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + utils.AssertEqual(t, ".forwarded", c.Accepts(".forwarded")) +} + +// go test -run Test_Ctx_Accepts_Wildcard +func Test_Ctx_Accepts_Wildcard(t *testing.T) { + t.Parallel() + app := New() + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + c.Request().Header.Set(HeaderAccept, "*/*;q=0.9") + utils.AssertEqual(t, "html", c.Accepts("html")) + utils.AssertEqual(t, "foo", c.Accepts("foo")) + utils.AssertEqual(t, ".bar", c.Accepts(".bar")) + c.Request().Header.Set(HeaderAccept, "text/html,application/*;q=0.9") + utils.AssertEqual(t, "xml", c.Accepts("xml")) +} + +// go test -run Test_Ctx_AcceptsCharsets +func Test_Ctx_AcceptsCharsets(t *testing.T) { + t.Parallel() + app := New() + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + c.Request().Header.Set(HeaderAcceptCharset, "utf-8, iso-8859-1;q=0.5") + utils.AssertEqual(t, "utf-8", c.AcceptsCharsets("utf-8")) +} + +// go test -v -run=^$ -bench=Benchmark_Ctx_AcceptsCharsets -benchmem -count=4 +func Benchmark_Ctx_AcceptsCharsets(b *testing.B) { + app := New() + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + c.Request().Header.Set("Accept-Charset", "utf-8, iso-8859-1;q=0.5") + var res string + b.ReportAllocs() + b.ResetTimer() + for n := 0; n < b.N; n++ { + res = c.AcceptsCharsets("utf-8") + } + utils.AssertEqual(b, "utf-8", res) +} + +// go test -run Test_Ctx_AcceptsEncodings +func Test_Ctx_AcceptsEncodings(t *testing.T) { + t.Parallel() + app := New() + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + c.Request().Header.Set(HeaderAcceptEncoding, "deflate, gzip;q=1.0, *;q=0.5") + utils.AssertEqual(t, "gzip", c.AcceptsEncodings("gzip")) + utils.AssertEqual(t, "abc", c.AcceptsEncodings("abc")) +} + +// go test -v -run=^$ -bench=Benchmark_Ctx_AcceptsEncodings -benchmem -count=4 +func Benchmark_Ctx_AcceptsEncodings(b *testing.B) { + app := New() + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + c.Request().Header.Set(HeaderAcceptEncoding, "deflate, gzip;q=1.0, *;q=0.5") + var res string + b.ReportAllocs() + b.ResetTimer() + for n := 0; n < b.N; n++ { + res = c.AcceptsEncodings("gzip") + } + utils.AssertEqual(b, "gzip", res) +} + +// go test -run Test_Ctx_AcceptsLanguages +func Test_Ctx_AcceptsLanguages(t *testing.T) { + t.Parallel() + app := New() + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + c.Request().Header.Set(HeaderAcceptLanguage, "fr-CH, fr;q=0.9, en;q=0.8, de;q=0.7, *;q=0.5") + utils.AssertEqual(t, "fr", c.AcceptsLanguages("fr")) +} + +// go test -v -run=^$ -bench=Benchmark_Ctx_AcceptsLanguages -benchmem -count=4 +func Benchmark_Ctx_AcceptsLanguages(b *testing.B) { + app := New() + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + c.Request().Header.Set(HeaderAcceptLanguage, "fr-CH, fr;q=0.9, en;q=0.8, de;q=0.7, *;q=0.5") + var res string + b.ReportAllocs() + b.ResetTimer() + for n := 0; n < b.N; n++ { + res = c.AcceptsLanguages("fr") + } + utils.AssertEqual(b, "fr", res) +} + +// go test -run Test_Ctx_App +func Test_Ctx_App(t *testing.T) { + t.Parallel() + app := New() + app.config.BodyLimit = 1000 + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + utils.AssertEqual(t, 1000, c.App().config.BodyLimit) +} + +// go test -run Test_Ctx_Append +func Test_Ctx_Append(t *testing.T) { + t.Parallel() + app := New() + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + c.Append("X-Test", "Hello") + c.Append("X-Test", "World") + c.Append("X-Test", "Hello", "World") + // similar value in the middle + c.Append("X2-Test", "World") + c.Append("X2-Test", "XHello") + c.Append("X2-Test", "Hello", "World") + // similar value at the start + c.Append("X3-Test", "XHello") + c.Append("X3-Test", "World") + c.Append("X3-Test", "Hello", "World") + // try it with multiple similar values + c.Append("X4-Test", "XHello") + c.Append("X4-Test", "Hello") + c.Append("X4-Test", "HelloZ") + c.Append("X4-Test", "YHello") + c.Append("X4-Test", "Hello") + c.Append("X4-Test", "YHello") + c.Append("X4-Test", "HelloZ") + c.Append("X4-Test", "XHello") + // without append value + c.Append("X-Custom-Header") + + utils.AssertEqual(t, "Hello, World", string(c.Response().Header.Peek("X-Test"))) + utils.AssertEqual(t, "World, XHello, Hello", string(c.Response().Header.Peek("X2-Test"))) + utils.AssertEqual(t, "XHello, World, Hello", string(c.Response().Header.Peek("X3-Test"))) + utils.AssertEqual(t, "XHello, Hello, HelloZ, YHello", string(c.Response().Header.Peek("X4-Test"))) + utils.AssertEqual(t, "", string(c.Response().Header.Peek("x-custom-header"))) +} + +// go test -v -run=^$ -bench=Benchmark_Ctx_Append -benchmem -count=4 +func Benchmark_Ctx_Append(b *testing.B) { + app := New() + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + b.ReportAllocs() + b.ResetTimer() + for n := 0; n < b.N; n++ { + c.Append("X-Custom-Header", "Hello") + c.Append("X-Custom-Header", "World") + c.Append("X-Custom-Header", "Hello") + } + utils.AssertEqual(b, "Hello, World", app.getString(c.Response().Header.Peek("X-Custom-Header"))) +} + +// go test -run Test_Ctx_Attachment +func Test_Ctx_Attachment(t *testing.T) { + t.Parallel() + app := New() + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + // empty + c.Attachment() + utils.AssertEqual(t, `attachment`, string(c.Response().Header.Peek(HeaderContentDisposition))) + // real filename + c.Attachment("./static/img/logo.png") + utils.AssertEqual(t, `attachment; filename="logo.png"`, string(c.Response().Header.Peek(HeaderContentDisposition))) + utils.AssertEqual(t, "image/png", string(c.Response().Header.Peek(HeaderContentType))) + // check quoting + c.Attachment("another document.pdf\"\r\nBla: \"fasel") + utils.AssertEqual(t, `attachment; filename="another+document.pdf%22%0D%0ABla%3A+%22fasel"`, string(c.Response().Header.Peek(HeaderContentDisposition))) +} + +// go test -v -run=^$ -bench=Benchmark_Ctx_Attachment -benchmem -count=4 +func Benchmark_Ctx_Attachment(b *testing.B) { + app := New() + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + b.ReportAllocs() + b.ResetTimer() + for n := 0; n < b.N; n++ { + // example with quote params + c.Attachment("another document.pdf\"\r\nBla: \"fasel") + } + utils.AssertEqual(b, `attachment; filename="another+document.pdf%22%0D%0ABla%3A+%22fasel"`, string(c.Response().Header.Peek(HeaderContentDisposition))) +} + +// go test -run Test_Ctx_BaseURL +func Test_Ctx_BaseURL(t *testing.T) { + t.Parallel() + app := New() + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + c.Request().SetRequestURI("http://google.com/test") + utils.AssertEqual(t, "http://google.com", c.BaseURL()) + // Check cache + utils.AssertEqual(t, "http://google.com", c.BaseURL()) +} + +// go test -v -run=^$ -bench=Benchmark_Ctx_BaseURL -benchmem +func Benchmark_Ctx_BaseURL(b *testing.B) { + app := New() + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + c.Request().SetHost("google.com:1337") + c.Request().URI().SetPath("/haha/oke/lol") + var res string + b.ReportAllocs() + b.ResetTimer() + for n := 0; n < b.N; n++ { + res = c.BaseURL() + } + utils.AssertEqual(b, "http://google.com:1337", res) +} + +// go test -run Test_Ctx_Body +func Test_Ctx_Body(t *testing.T) { + t.Parallel() + app := New() + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + c.Request().SetBody([]byte("john=doe")) + utils.AssertEqual(t, []byte("john=doe"), c.Body()) +} + +func Benchmark_Ctx_Body(b *testing.B) { + const input = "john=doe" + + app := New() + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + + c.Request().SetBody([]byte(input)) + for i := 0; i < b.N; i++ { + _ = c.Body() + } + + utils.AssertEqual(b, []byte(input), c.Body()) +} + +// go test -run Test_Ctx_Body_With_Compression +func Test_Ctx_Body_With_Compression(t *testing.T) { + t.Parallel() + tests := []struct { + name string + contentEncoding string + body []byte + expectedBody []byte + }{ + { + name: "gzip", + contentEncoding: "gzip", + body: []byte("john=doe"), + expectedBody: []byte("john=doe"), + }, + { + name: "unsupported_encoding", + contentEncoding: "undefined", + body: []byte("keeps_ORIGINAL"), + expectedBody: []byte("keeps_ORIGINAL"), + }, + { + name: "gzip then unsupported", + contentEncoding: "gzip, undefined", + body: []byte("Go, be gzipped"), + expectedBody: []byte("Go, be gzipped"), + }, + { + name: "invalid_deflate", + contentEncoding: "gzip,deflate", + body: []byte("I'm not correctly compressed"), + expectedBody: []byte(zlib.ErrHeader.Error()), + }, + } + + for _, testObject := range tests { + tCase := testObject // Duplicate object to ensure it will be unique across all runs + t.Run(tCase.name, func(t *testing.T) { + app := New() + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + c.Request().Header.Set("Content-Encoding", tCase.contentEncoding) + + if strings.Contains(tCase.contentEncoding, "gzip") { + var b bytes.Buffer + gz := gzip.NewWriter(&b) + + _, err := gz.Write(tCase.body) + utils.AssertEqual(t, nil, err) + + err = gz.Flush() + utils.AssertEqual(t, nil, err) + + err = gz.Close() + utils.AssertEqual(t, nil, err) + tCase.body = b.Bytes() + } + + c.Request().SetBody(tCase.body) + body := c.Body() + utils.AssertEqual(t, tCase.expectedBody, body) + + // Check if body raw is the same as previous before decompression + utils.AssertEqual( + t, tCase.body, c.Request().Body(), + "Body raw must be the same as set before", + ) + }) + } +} + +// go test -v -run=^$ -bench=Benchmark_Ctx_Body_With_Compression -benchmem -count=4 +func Benchmark_Ctx_Body_With_Compression(b *testing.B) { + encodingErr := errors.New("failed to encoding data") + + var ( + compressGzip = func(data []byte) ([]byte, error) { + var buf bytes.Buffer + writer := gzip.NewWriter(&buf) + if _, err := writer.Write(data); err != nil { + return nil, encodingErr + } + if err := writer.Flush(); err != nil { + return nil, encodingErr + } + if err := writer.Close(); err != nil { + return nil, encodingErr + } + return buf.Bytes(), nil + } + compressDeflate = func(data []byte) ([]byte, error) { + var buf bytes.Buffer + writer := zlib.NewWriter(&buf) + if _, err := writer.Write(data); err != nil { + return nil, encodingErr + } + if err := writer.Flush(); err != nil { + return nil, encodingErr + } + if err := writer.Close(); err != nil { + return nil, encodingErr + } + return buf.Bytes(), nil + } + ) + compressionTests := []struct { + contentEncoding string + compressWriter func([]byte) ([]byte, error) + }{ + { + contentEncoding: "gzip", + compressWriter: compressGzip, + }, + { + contentEncoding: "gzip,invalid", + compressWriter: compressGzip, + }, + { + contentEncoding: "deflate", + compressWriter: compressDeflate, + }, + { + contentEncoding: "gzip,deflate", + compressWriter: func(data []byte) ([]byte, error) { + var ( + buf bytes.Buffer + writer interface { + io.WriteCloser + Flush() error + } + err error + ) + + // deflate + { + writer = zlib.NewWriter(&buf) + if _, err = writer.Write(data); err != nil { + return nil, encodingErr + } + if err = writer.Flush(); err != nil { + return nil, encodingErr + } + if err = writer.Close(); err != nil { + return nil, encodingErr + } + } + + data = make([]byte, buf.Len()) + copy(data, buf.Bytes()) + buf.Reset() + + // gzip + { + writer = gzip.NewWriter(&buf) + if _, err = writer.Write(data); err != nil { + return nil, encodingErr + } + if err = writer.Flush(); err != nil { + return nil, encodingErr + } + if err = writer.Close(); err != nil { + return nil, encodingErr + } + } + + return buf.Bytes(), nil + }, + }, + } + + for _, ct := range compressionTests { + b.Run(ct.contentEncoding, func(b *testing.B) { + app := New() + const input = "john=doe" + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + + c.Request().Header.Set("Content-Encoding", ct.contentEncoding) + compressedBody, err := ct.compressWriter([]byte(input)) + utils.AssertEqual(b, nil, err) + + c.Request().SetBody(compressedBody) + for i := 0; i < b.N; i++ { + _ = c.Body() + } + + utils.AssertEqual(b, []byte(input), c.Body()) + }) + } +} + +// go test -run Test_Ctx_BodyParser +func Test_Ctx_BodyParser(t *testing.T) { + t.Parallel() + app := New() + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + + type Demo struct { + Name string `json:"name" xml:"name" form:"name" query:"name"` + } + + { + var gzipJSON bytes.Buffer + w := gzip.NewWriter(&gzipJSON) + _, err := w.Write([]byte(`{"name":"john"}`)) + utils.AssertEqual(t, nil, err) + err = w.Close() + utils.AssertEqual(t, nil, err) + + c.Request().Header.SetContentType(MIMEApplicationJSON) + c.Request().Header.Set(HeaderContentEncoding, "gzip") + c.Request().SetBody(gzipJSON.Bytes()) + c.Request().Header.SetContentLength(len(gzipJSON.Bytes())) + d := new(Demo) + utils.AssertEqual(t, nil, c.BodyParser(d)) + utils.AssertEqual(t, "john", d.Name) + c.Request().Header.Del(HeaderContentEncoding) + } + + testDecodeParser := func(contentType, body string) { + c.Request().Header.SetContentType(contentType) + c.Request().SetBody([]byte(body)) + c.Request().Header.SetContentLength(len(body)) + d := new(Demo) + utils.AssertEqual(t, nil, c.BodyParser(d)) + utils.AssertEqual(t, "john", d.Name) + } + + testDecodeParser(MIMEApplicationJSON, `{"name":"john"}`) + testDecodeParser(MIMEApplicationXML, `john`) + testDecodeParser(MIMEApplicationForm, "name=john") + testDecodeParser(MIMEMultipartForm+`;boundary="b"`, "--b\r\nContent-Disposition: form-data; name=\"name\"\r\n\r\njohn\r\n--b--") + + // Ensure JSON extension MIME type gets parsed as JSON + testDecodeParser("application/problem+json", `{"name":"john"}`) + + testDecodeParserError := func(contentType, body string) { + c.Request().Header.SetContentType(contentType) + c.Request().SetBody([]byte(body)) + c.Request().Header.SetContentLength(len(body)) + utils.AssertEqual(t, false, c.BodyParser(nil) == nil) + } + + testDecodeParserError("invalid-content-type", "") + testDecodeParserError(MIMEMultipartForm+`;boundary="b"`, "--b") + + type CollectionQuery struct { + Data []Demo `query:"data"` + } + + c.Request().Reset() + c.Request().Header.SetContentType(MIMEApplicationForm) + c.Request().SetBody([]byte("data[0][name]=john&data[1][name]=doe")) + c.Request().Header.SetContentLength(len(c.Body())) + cq := new(CollectionQuery) + utils.AssertEqual(t, nil, c.BodyParser(cq)) + utils.AssertEqual(t, 2, len(cq.Data)) + utils.AssertEqual(t, "john", cq.Data[0].Name) + utils.AssertEqual(t, "doe", cq.Data[1].Name) + + c.Request().Reset() + c.Request().Header.SetContentType(MIMEApplicationForm) + c.Request().SetBody([]byte("data.0.name=john&data.1.name=doe")) + c.Request().Header.SetContentLength(len(c.Body())) + cq = new(CollectionQuery) + utils.AssertEqual(t, nil, c.BodyParser(cq)) + utils.AssertEqual(t, 2, len(cq.Data)) + utils.AssertEqual(t, "john", cq.Data[0].Name) + utils.AssertEqual(t, "doe", cq.Data[1].Name) + + t.Run("MultipartCollectionQueryDotNotation", func(t *testing.T) { + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + c.Request().Reset() + + buf := &bytes.Buffer{} + writer := multipart.NewWriter(buf) + utils.AssertEqual(t, nil, writer.WriteField("data.0.name", "john")) + utils.AssertEqual(t, nil, writer.WriteField("data.1.name", "doe")) + utils.AssertEqual(t, nil, writer.Close()) + + c.Request().Header.SetContentType(writer.FormDataContentType()) + c.Request().SetBody(buf.Bytes()) + c.Request().Header.SetContentLength(len(c.Body())) + + cq := new(CollectionQuery) + utils.AssertEqual(t, nil, c.BodyParser(cq)) + utils.AssertEqual(t, len(cq.Data), 2) + utils.AssertEqual(t, "john", cq.Data[0].Name) + utils.AssertEqual(t, "doe", cq.Data[1].Name) + }) + + t.Run("MultipartCollectionQuerySquareBrackets", func(t *testing.T) { + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + c.Request().Reset() + + buf := &bytes.Buffer{} + writer := multipart.NewWriter(buf) + utils.AssertEqual(t, nil, writer.WriteField("data[0][name]", "john")) + utils.AssertEqual(t, nil, writer.WriteField("data[1][name]", "doe")) + utils.AssertEqual(t, nil, writer.Close()) + + c.Request().Header.SetContentType(writer.FormDataContentType()) + c.Request().SetBody(buf.Bytes()) + c.Request().Header.SetContentLength(len(c.Body())) + + cq := new(CollectionQuery) + utils.AssertEqual(t, nil, c.BodyParser(cq)) + utils.AssertEqual(t, len(cq.Data), 2) + utils.AssertEqual(t, "john", cq.Data[0].Name) + utils.AssertEqual(t, "doe", cq.Data[1].Name) + }) +} + +func Test_Ctx_ParamParser(t *testing.T) { + t.Parallel() + app := New() + app.Get("/test1/:userId/role/:roleId", func(ctx *Ctx) error { + type Demo struct { + UserID uint `params:"userId"` + RoleID uint `params:"roleId"` + } + d := new(Demo) + + utils.AssertEqual(t, nil, ctx.ParamsParser(d)) + utils.AssertEqual(t, uint(111), d.UserID) + utils.AssertEqual(t, uint(222), d.RoleID) + return nil + }) + _, err := app.Test(httptest.NewRequest(MethodGet, "/test1/111/role/222", nil)) + utils.AssertEqual(t, nil, err) + + _, err = app.Test(httptest.NewRequest(MethodGet, "/test2/111/role/222", nil)) + utils.AssertEqual(t, nil, err) +} + +// go test -run Test_Ctx_BodyParser_WithSetParserDecoder +func Test_Ctx_BodyParser_WithSetParserDecoder(t *testing.T) { + type CustomTime time.Time + + timeConverter := func(value string) reflect.Value { + if v, err := time.Parse("2006-01-02", value); err == nil { + return reflect.ValueOf(v) + } + return reflect.Value{} + } + + customTime := ParserType{ + Customtype: CustomTime{}, + Converter: timeConverter, + } + + SetParserDecoder(ParserConfig{ + IgnoreUnknownKeys: true, + ParserType: []ParserType{customTime}, + ZeroEmpty: true, + SetAliasTag: "form", + }) + + app := New() + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + + type Demo struct { + Date CustomTime `form:"date"` + Title string `form:"title"` + Body string `form:"body"` + } + + testDecodeParser := func(contentType, body string) { + c.Request().Header.SetContentType(contentType) + c.Request().SetBody([]byte(body)) + c.Request().Header.SetContentLength(len(body)) + d := Demo{ + Title: "Existing title", + Body: "Existing Body", + } + utils.AssertEqual(t, nil, c.BodyParser(&d)) + date := fmt.Sprintf("%v", d.Date) + utils.AssertEqual(t, "{0 63743587200 }", date) + utils.AssertEqual(t, "", d.Title) + utils.AssertEqual(t, "New Body", d.Body) + } + + testDecodeParser(MIMEApplicationForm, "date=2020-12-15&title=&body=New Body") + testDecodeParser(MIMEMultipartForm+`; boundary="b"`, "--b\r\nContent-Disposition: form-data; name=\"date\"\r\n\r\n2020-12-15\r\n--b\r\nContent-Disposition: form-data; name=\"title\"\r\n\r\n\r\n--b\r\nContent-Disposition: form-data; name=\"body\"\r\n\r\nNew Body\r\n--b--") +} + +// go test -v -run=^$ -bench=Benchmark_Ctx_BodyParser_JSON -benchmem -count=4 +func Benchmark_Ctx_BodyParser_JSON(b *testing.B) { + app := New() + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + type Demo struct { + Name string `json:"name"` + } + body := []byte(`{"name":"john"}`) + c.Request().SetBody(body) + c.Request().Header.SetContentType(MIMEApplicationJSON) + c.Request().Header.SetContentLength(len(body)) + d := new(Demo) + + b.ReportAllocs() + b.ResetTimer() + + for n := 0; n < b.N; n++ { + _ = c.BodyParser(d) //nolint:errcheck // It is fine to ignore the error here as we check it once further below + } + utils.AssertEqual(b, nil, c.BodyParser(d)) + utils.AssertEqual(b, "john", d.Name) +} + +// go test -v -run=^$ -bench=Benchmark_Ctx_BodyParser_JSON_Extension -benchmem -count=4 +func Benchmark_Ctx_BodyParser_JSON_Extension(b *testing.B) { + app := New() + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + type Demo struct { + Name string `json:"name"` + } + body := []byte(`{"name":"john"}`) + c.Request().SetBody(body) + c.Request().Header.SetContentType("application/problem+json") + c.Request().Header.SetContentLength(len(body)) + d := new(Demo) + + b.ReportAllocs() + b.ResetTimer() + + for n := 0; n < b.N; n++ { + _ = c.BodyParser(d) //nolint:errcheck // It is fine to ignore the error here as we check it once further below + } + utils.AssertEqual(b, nil, c.BodyParser(d)) + utils.AssertEqual(b, "john", d.Name) +} + +// go test -v -run=^$ -bench=Benchmark_Ctx_BodyParser_XML -benchmem -count=4 +func Benchmark_Ctx_BodyParser_XML(b *testing.B) { + app := New() + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + type Demo struct { + Name string `xml:"name"` + } + body := []byte("john") + c.Request().SetBody(body) + c.Request().Header.SetContentType(MIMEApplicationXML) + c.Request().Header.SetContentLength(len(body)) + d := new(Demo) + + b.ReportAllocs() + b.ResetTimer() + + for n := 0; n < b.N; n++ { + _ = c.BodyParser(d) //nolint:errcheck // It is fine to ignore the error here as we check it once further below + } + utils.AssertEqual(b, nil, c.BodyParser(d)) + utils.AssertEqual(b, "john", d.Name) +} + +// go test -v -run=^$ -bench=Benchmark_Ctx_BodyParser_Form -benchmem -count=4 +func Benchmark_Ctx_BodyParser_Form(b *testing.B) { + app := New() + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + type Demo struct { + Name string `form:"name"` + } + body := []byte("name=john") + c.Request().SetBody(body) + c.Request().Header.SetContentType(MIMEApplicationForm) + c.Request().Header.SetContentLength(len(body)) + d := new(Demo) + + b.ReportAllocs() + b.ResetTimer() + + for n := 0; n < b.N; n++ { + _ = c.BodyParser(d) //nolint:errcheck // It is fine to ignore the error here as we check it once further below + } + utils.AssertEqual(b, nil, c.BodyParser(d)) + utils.AssertEqual(b, "john", d.Name) +} + +// go test -v -run=^$ -bench=Benchmark_Ctx_BodyParser_MultipartForm -benchmem -count=4 +func Benchmark_Ctx_BodyParser_MultipartForm(b *testing.B) { + app := New() + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + type Demo struct { + Name string `form:"name"` + } + + body := []byte("--b\r\nContent-Disposition: form-data; name=\"name\"\r\n\r\njohn\r\n--b--") + c.Request().SetBody(body) + c.Request().Header.SetContentType(MIMEMultipartForm + `;boundary="b"`) + c.Request().Header.SetContentLength(len(body)) + d := new(Demo) + + b.ReportAllocs() + b.ResetTimer() + + for n := 0; n < b.N; n++ { + _ = c.BodyParser(d) //nolint:errcheck // It is fine to ignore the error here as we check it once further below + } + utils.AssertEqual(b, nil, c.BodyParser(d)) + utils.AssertEqual(b, "john", d.Name) +} + +// go test -run Test_Ctx_Context +func Test_Ctx_Context(t *testing.T) { + t.Parallel() + app := New() + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + + utils.AssertEqual(t, "*fasthttp.RequestCtx", fmt.Sprintf("%T", c.Context())) +} + +// go test -run Test_Ctx_UserContext +func Test_Ctx_UserContext(t *testing.T) { + app := New() + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + + t.Run("Nil_Context", func(t *testing.T) { + ctx := c.UserContext() + utils.AssertEqual(t, ctx, context.Background()) + }) + t.Run("ValueContext", func(t *testing.T) { + testKey := struct{}{} + testValue := "Test Value" + ctx := context.WithValue(context.Background(), testKey, testValue) + utils.AssertEqual(t, testValue, ctx.Value(testKey)) + }) +} + +// go test -run Test_Ctx_SetUserContext +func Test_Ctx_SetUserContext(t *testing.T) { + t.Parallel() + app := New() + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + + testKey := struct{}{} + testValue := "Test Value" + ctx := context.WithValue(context.Background(), testKey, testValue) + c.SetUserContext(ctx) + utils.AssertEqual(t, testValue, c.UserContext().Value(testKey)) +} + +// go test -run Test_Ctx_UserContext_Multiple_Requests +func Test_Ctx_UserContext_Multiple_Requests(t *testing.T) { + t.Parallel() + testKey := struct{}{} + testValue := "foobar-value" + + app := New() + app.Get("/", func(c *Ctx) error { + ctx := c.UserContext() + + if ctx.Value(testKey) != nil { + return c.SendStatus(StatusInternalServerError) + } + + input := utils.CopyString(c.Query("input", "NO_VALUE")) + ctx = context.WithValue(ctx, testKey, fmt.Sprintf("%s_%s", testValue, input)) + c.SetUserContext(ctx) + + return c.Status(StatusOK).SendString(fmt.Sprintf("resp_%s_returned", input)) + }) + + // Consecutive Requests + for i := 1; i <= 10; i++ { + t.Run(fmt.Sprintf("request_%d", i), func(t *testing.T) { + resp, err := app.Test(httptest.NewRequest(MethodGet, fmt.Sprintf("/?input=%d", i), nil)) + + utils.AssertEqual(t, nil, err, "Unexpected error from response") + utils.AssertEqual(t, StatusOK, resp.StatusCode, "context.Context returned from c.UserContext() is reused") + + b, err := io.ReadAll(resp.Body) + utils.AssertEqual(t, nil, err, "Unexpected error from reading response body") + utils.AssertEqual(t, fmt.Sprintf("resp_%d_returned", i), string(b), "response text incorrect") + }) + } +} + +// go test -run Test_Ctx_Cookie +func Test_Ctx_Cookie(t *testing.T) { + t.Parallel() + app := New() + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + expire := time.Now().Add(24 * time.Hour) + var dst []byte + dst = expire.In(time.UTC).AppendFormat(dst, time.RFC1123) + httpdate := strings.ReplaceAll(string(dst), "UTC", "GMT") + cookie := &Cookie{ + Name: "username", + Value: "john", + Expires: expire, + // SameSite: CookieSameSiteStrictMode, // default is "lax" + } + c.Cookie(cookie) + expect := "username=john; expires=" + httpdate + "; path=/; SameSite=Lax" + utils.AssertEqual(t, expect, string(c.Response().Header.Peek(HeaderSetCookie))) + + expect = "username=john; expires=" + httpdate + "; path=/" + cookie.SameSite = CookieSameSiteDisabled + c.Cookie(cookie) + utils.AssertEqual(t, expect, string(c.Response().Header.Peek(HeaderSetCookie))) + + expect = "username=john; expires=" + httpdate + "; path=/; SameSite=Strict" + cookie.SameSite = CookieSameSiteStrictMode + c.Cookie(cookie) + utils.AssertEqual(t, expect, string(c.Response().Header.Peek(HeaderSetCookie))) + + expect = "username=john; expires=" + httpdate + "; path=/; secure; SameSite=None" + cookie.Secure = true + cookie.SameSite = CookieSameSiteNoneMode + c.Cookie(cookie) + utils.AssertEqual(t, expect, string(c.Response().Header.Peek(HeaderSetCookie))) + + expect = "username=john; path=/; secure; SameSite=None" + // should remove expires and max-age headers + cookie.SessionOnly = true + cookie.Expires = expire + cookie.MaxAge = 10000 + c.Cookie(cookie) + utils.AssertEqual(t, expect, string(c.Response().Header.Peek(HeaderSetCookie))) + + expect = "username=john; path=/; secure; SameSite=None" + // should remove expires and max-age headers when no expire and no MaxAge (default time) + cookie.SessionOnly = false + cookie.Expires = time.Time{} + cookie.MaxAge = 0 + c.Cookie(cookie) + utils.AssertEqual(t, expect, string(c.Response().Header.Peek(HeaderSetCookie))) +} + +// go test -v -run=^$ -bench=Benchmark_Ctx_Cookie -benchmem -count=4 +func Benchmark_Ctx_Cookie(b *testing.B) { + app := New() + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + b.ReportAllocs() + b.ResetTimer() + for n := 0; n < b.N; n++ { + c.Cookie(&Cookie{ + Name: "John", + Value: "Doe", + }) + } + utils.AssertEqual(b, "John=Doe; path=/; SameSite=Lax", app.getString(c.Response().Header.Peek("Set-Cookie"))) +} + +// go test -run Test_Ctx_CookieParser -v +func Test_Ctx_CookieParser(t *testing.T) { + t.Parallel() + app := New(Config{EnableSplittingOnParsers: true}) + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + type Cookie struct { + Name string + Class int + Courses []string + } + c.Request().Header.Set("Cookie", "name=doe") + c.Request().Header.Set("Cookie", "class=100") + c.Request().Header.Set("Cookie", "courses=maths,english") + cookie := new(Cookie) + + // correct test cases + utils.AssertEqual(t, nil, c.CookieParser(cookie)) + utils.AssertEqual(t, "doe", cookie.Name) + utils.AssertEqual(t, 100, cookie.Class) + utils.AssertEqual(t, 2, len(cookie.Courses)) + + // wrong test cases + empty := new(Cookie) + c.Request().Header.Set("Cookie", "name") + c.Request().Header.Set("Cookie", "class") + c.Request().Header.Set("Cookie", "courses") + utils.AssertEqual(t, nil, c.CookieParser(cookie)) + utils.AssertEqual(t, "", empty.Name) + utils.AssertEqual(t, 0, empty.Class) + utils.AssertEqual(t, 0, len(empty.Courses)) +} + +// go test -run Test_Ctx_CookieParserUsingTag -v +func Test_Ctx_CookieParserUsingTag(t *testing.T) { + t.Parallel() + app := New(Config{EnableSplittingOnParsers: true}) + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + type Cook struct { + ID int `cookie:"id"` + Name string `cookie:"name"` + Courses []string `cookie:"courses"` + Enrolled bool `cookie:"student"` + Fees float32 `cookie:"fee"` + Grades []uint8 `cookie:"score"` + } + cookie1 := new(Cook) + cookie1.Name = "Joseph" + utils.AssertEqual(t, "Joseph", cookie1.Name) + + c.Request().Header.Set("Cookie", "id=1") + c.Request().Header.Set("Cookie", "name=Joey") + c.Request().Header.Set("Cookie", "courses=maths,english, chemistry, physics") + c.Request().Header.Set("Cookie", "student=true") + c.Request().Header.Set("Cookie", "fee=45.78") + c.Request().Header.Set("Cookie", "score=7,6,10") + utils.AssertEqual(t, nil, c.CookieParser(cookie1)) + utils.AssertEqual(t, "Joey", cookie1.Name) + utils.AssertEqual(t, true, cookie1.Enrolled) + utils.AssertEqual(t, float32(45.78), cookie1.Fees) + utils.AssertEqual(t, []uint8{7, 6, 10}, cookie1.Grades) + + type RequiredCookie struct { + House string `cookie:"house,required"` + } + rc := new(RequiredCookie) + utils.AssertEqual(t, "failed to decode: house is empty", c.CookieParser(rc).Error()) + + type ArrayCookie struct { + Dates []int + } + + ac := new(ArrayCookie) + c.Request().Header.Set("Cookie", "dates[]=7,6,10") + utils.AssertEqual(t, nil, c.CookieParser(ac)) + utils.AssertEqual(t, 3, len(ac.Dates)) +} + +// go test -run Test_Ctx_CookieParserSchema -v +func Test_Ctx_CookieParser_Schema(t *testing.T) { + t.Parallel() + app := New() + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + type result struct { + Maths int `cookie:"maths"` + English int `cookie:"english"` + } + type resStruct struct { + Name string `cookie:"name"` + Age int `cookie:"age"` + Result result `cookie:"result"` + } + res := &resStruct{ + Name: "Joseph", + Age: 10, + Result: result{ + Maths: 10, + English: 10, + }, + } + + // set cookie + c.Request().Header.Set("Cookie", "name=Joseph") + c.Request().Header.Set("Cookie", "age=10") + c.Request().Header.Set("Cookie", "result.maths=10") + c.Request().Header.Set("Cookie", "result.english=10") + hR := new(resStruct) + r := c.CookieParser(hR) + + utils.AssertEqual(t, nil, r) + utils.AssertEqual(t, *res, *hR) +} + +// go test -run Benchmark_Ctx_CookieParser -v +func Benchmark_Ctx_CookieParser(b *testing.B) { + app := New(Config{EnableSplittingOnParsers: true}) + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + type Cook struct { + ID int `cookie:"id"` + Name string `cookie:"name"` + Courses []string `cookie:"courses"` + Enrolled bool `cookie:"student"` + Fees float32 `cookie:"fee"` + Grades []uint8 `cookie:"score"` + } + cookie1 := new(Cook) + cookie1.Name = "Joseph" + + c.Request().Header.Set("Cookie", "id=1") + c.Request().Header.Set("Cookie", "name=Joey") + c.Request().Header.Set("Cookie", "courses=maths,english, chemistry, physics") + c.Request().Header.Set("Cookie", "student=true") + c.Request().Header.Set("Cookie", "fee=45.78") + c.Request().Header.Set("Cookie", "score=7,6,10") + + var err error + // Run the function b.N times + for i := 0; i < b.N; i++ { + err = c.CookieParser(cookie1) + } + utils.AssertEqual(b, nil, err) +} + +// go test -run Test_Ctx_Cookies +func Test_Ctx_Cookies(t *testing.T) { + t.Parallel() + app := New() + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + c.Request().Header.Set("Cookie", "john=doe") + utils.AssertEqual(t, "doe", c.Cookies("john")) + utils.AssertEqual(t, "default", c.Cookies("unknown", "default")) +} + +// go test -run Test_Ctx_Format +func Test_Ctx_Format(t *testing.T) { + t.Parallel() + app := New() + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + c.Request().Header.Set(HeaderAccept, MIMETextPlain) + err := c.Format([]byte("Hello, World!")) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, "Hello, World!", string(c.Response().Body())) + + c.Request().Header.Set(HeaderAccept, MIMETextHTML) + err = c.Format("Hello, World!") + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, "

Hello, World!

", string(c.Response().Body())) + + c.Request().Header.Set(HeaderAccept, MIMEApplicationJSON) + err = c.Format("Hello, World!") + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, `"Hello, World!"`, string(c.Response().Body())) + + c.Request().Header.Set(HeaderAccept, MIMETextPlain) + err = c.Format(complex(1, 1)) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, "(1+1i)", string(c.Response().Body())) + + c.Request().Header.Set(HeaderAccept, MIMEApplicationXML) + err = c.Format("Hello, World!") + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, `Hello, World!`, string(c.Response().Body())) + + err = c.Format(complex(1, 1)) + utils.AssertEqual(t, true, err != nil) + + c.Request().Header.Set(HeaderAccept, MIMETextPlain) + err = c.Format(Map{}) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, "map[]", string(c.Response().Body())) + + type broken string + c.Request().Header.Set(HeaderAccept, "broken/accept") + err = c.Format(broken("Hello, World!")) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, `Hello, World!`, string(c.Response().Body())) +} + +// go test -v -run=^$ -bench=Benchmark_Ctx_Format -benchmem -count=4 +func Benchmark_Ctx_Format(b *testing.B) { + app := New() + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + c.Request().Header.Set("Accept", "text/plain") + b.ReportAllocs() + b.ResetTimer() + + var err error + for n := 0; n < b.N; n++ { + err = c.Format("Hello, World!") + } + + utils.AssertEqual(b, nil, err) + utils.AssertEqual(b, `Hello, World!`, string(c.Response().Body())) +} + +// go test -v -run=^$ -bench=Benchmark_Ctx_Format_HTML -benchmem -count=4 +func Benchmark_Ctx_Format_HTML(b *testing.B) { + app := New() + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + c.Request().Header.Set("Accept", "text/html") + b.ReportAllocs() + b.ResetTimer() + + var err error + for n := 0; n < b.N; n++ { + err = c.Format("Hello, World!") + } + + utils.AssertEqual(b, nil, err) + utils.AssertEqual(b, "

Hello, World!

", string(c.Response().Body())) +} + +// go test -v -run=^$ -bench=Benchmark_Ctx_Format_JSON -benchmem -count=4 +func Benchmark_Ctx_Format_JSON(b *testing.B) { + app := New() + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + c.Request().Header.Set("Accept", "application/json") + b.ReportAllocs() + b.ResetTimer() + + var err error + for n := 0; n < b.N; n++ { + err = c.Format("Hello, World!") + } + + utils.AssertEqual(b, nil, err) + utils.AssertEqual(b, `"Hello, World!"`, string(c.Response().Body())) +} + +// go test -v -run=^$ -bench=Benchmark_Ctx_Format_XML -benchmem -count=4 +func Benchmark_Ctx_Format_XML(b *testing.B) { + app := New() + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + c.Request().Header.Set("Accept", "application/xml") + b.ReportAllocs() + b.ResetTimer() + + var err error + for n := 0; n < b.N; n++ { + err = c.Format("Hello, World!") + } + + utils.AssertEqual(b, nil, err) + utils.AssertEqual(b, `Hello, World!`, string(c.Response().Body())) +} + +// go test -run Test_Ctx_FormFile +func Test_Ctx_FormFile(t *testing.T) { + // TODO: We should clean this up + t.Parallel() + app := New() + + app.Post("/test", func(c *Ctx) error { + fh, err := c.FormFile("file") + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, "test", fh.Filename) + + f, err := fh.Open() + utils.AssertEqual(t, nil, err) + defer func() { + utils.AssertEqual(t, nil, f.Close()) + }() + + b := new(bytes.Buffer) + _, err = io.Copy(b, f) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, "hello world", b.String()) + return nil + }) + + body := &bytes.Buffer{} + writer := multipart.NewWriter(body) + + ioWriter, err := writer.CreateFormFile("file", "test") + utils.AssertEqual(t, nil, err) + + _, err = ioWriter.Write([]byte("hello world")) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, nil, writer.Close()) + + req := httptest.NewRequest(MethodPost, "/test", body) + req.Header.Set(HeaderContentType, writer.FormDataContentType()) + req.Header.Set(HeaderContentLength, strconv.Itoa(len(body.Bytes()))) + + resp, err := app.Test(req) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, StatusOK, resp.StatusCode, "Status code") +} + +// go test -run Test_Ctx_FormValue +func Test_Ctx_FormValue(t *testing.T) { + t.Parallel() + app := New() + + app.Post("/test", func(c *Ctx) error { + utils.AssertEqual(t, "john", c.FormValue("name")) + return nil + }) + + body := &bytes.Buffer{} + writer := multipart.NewWriter(body) + utils.AssertEqual(t, nil, writer.WriteField("name", "john")) + utils.AssertEqual(t, nil, writer.Close()) + + req := httptest.NewRequest(MethodPost, "/test", body) + req.Header.Set("Content-Type", fmt.Sprintf("multipart/form-data; boundary=%s", writer.Boundary())) + req.Header.Set("Content-Length", strconv.Itoa(len(body.Bytes()))) + + resp, err := app.Test(req) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, StatusOK, resp.StatusCode, "Status code") +} + +// go test -v -run=^$ -bench=Benchmark_Ctx_Fresh_StaleEtag -benchmem -count=4 +func Benchmark_Ctx_Fresh_StaleEtag(b *testing.B) { + app := New() + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + + for n := 0; n < b.N; n++ { + c.Request().Header.Set(HeaderIfNoneMatch, "a, b, c, d") + c.Request().Header.Set(HeaderCacheControl, "c") + c.Fresh() + + c.Request().Header.Set(HeaderIfNoneMatch, "a, b, c, d") + c.Request().Header.Set(HeaderCacheControl, "e") + c.Fresh() + } +} + +// go test -run Test_Ctx_Fresh +func Test_Ctx_Fresh(t *testing.T) { + t.Parallel() + app := New() + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + utils.AssertEqual(t, false, c.Fresh()) + + c.Request().Header.Set(HeaderIfNoneMatch, "*") + c.Request().Header.Set(HeaderCacheControl, "no-cache") + utils.AssertEqual(t, false, c.Fresh()) + + c.Request().Header.Set(HeaderIfNoneMatch, "*") + c.Request().Header.Set(HeaderCacheControl, ",no-cache,") + utils.AssertEqual(t, false, c.Fresh()) + + c.Request().Header.Set(HeaderIfNoneMatch, "*") + c.Request().Header.Set(HeaderCacheControl, "aa,no-cache,") + utils.AssertEqual(t, false, c.Fresh()) + + c.Request().Header.Set(HeaderIfNoneMatch, "*") + c.Request().Header.Set(HeaderCacheControl, ",no-cache,bb") + utils.AssertEqual(t, false, c.Fresh()) + + c.Request().Header.Set(HeaderIfNoneMatch, "675af34563dc-tr34") + c.Request().Header.Set(HeaderCacheControl, "public") + utils.AssertEqual(t, false, c.Fresh()) + + c.Request().Header.Set(HeaderIfNoneMatch, "a, b") + c.Response().Header.Set(HeaderETag, "c") + utils.AssertEqual(t, false, c.Fresh()) + + c.Response().Header.Set(HeaderETag, "a") + utils.AssertEqual(t, true, c.Fresh()) + + c.Request().Header.Set(HeaderIfModifiedSince, "xxWed, 21 Oct 2015 07:28:00 GMT") + c.Response().Header.Set(HeaderLastModified, "xxWed, 21 Oct 2015 07:28:00 GMT") + utils.AssertEqual(t, false, c.Fresh()) + + c.Response().Header.Set(HeaderLastModified, "Wed, 21 Oct 2015 07:28:00 GMT") + utils.AssertEqual(t, false, c.Fresh()) + + c.Request().Header.Set(HeaderIfModifiedSince, "Wed, 21 Oct 2015 07:28:00 GMT") + utils.AssertEqual(t, false, c.Fresh()) +} + +// go test -v -run=^$ -bench=Benchmark_Ctx_Fresh_WithNoCache -benchmem -count=4 +func Benchmark_Ctx_Fresh_WithNoCache(b *testing.B) { + app := New() + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + + c.Request().Header.Set(HeaderIfNoneMatch, "*") + c.Request().Header.Set(HeaderCacheControl, "no-cache") + for n := 0; n < b.N; n++ { + c.Fresh() + } +} + +// go test -run Test_Ctx_Parsers -v +func Test_Ctx_Parsers(t *testing.T) { + t.Parallel() + // setup + app := New() + + type TestStruct struct { + Name string + Class int + NameWithDefault string `json:"name2" xml:"Name2" form:"name2" cookie:"name2" query:"name2" params:"name2" reqHeader:"name2"` + ClassWithDefault int `json:"class2" xml:"Class2" form:"class2" cookie:"class2" query:"class2" params:"class2" reqHeader:"class2"` + } + + withValues := func(t *testing.T, actionFn func(c *Ctx, testStruct *TestStruct) error) { + t.Helper() + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + testStruct := new(TestStruct) + + utils.AssertEqual(t, nil, actionFn(c, testStruct)) + utils.AssertEqual(t, "foo", testStruct.Name) + utils.AssertEqual(t, 111, testStruct.Class) + utils.AssertEqual(t, "bar", testStruct.NameWithDefault) + utils.AssertEqual(t, 222, testStruct.ClassWithDefault) + } + + t.Run("BodyParser:xml", func(t *testing.T) { + t.Parallel() + withValues(t, func(c *Ctx, testStruct *TestStruct) error { + c.Request().Header.SetContentType(MIMEApplicationXML) + c.Request().SetBody([]byte(`foo111bar222`)) + return c.BodyParser(testStruct) + }) + }) + t.Run("BodyParser:form", func(t *testing.T) { + t.Parallel() + withValues(t, func(c *Ctx, testStruct *TestStruct) error { + c.Request().Header.SetContentType(MIMEApplicationForm) + c.Request().SetBody([]byte(`name=foo&class=111&name2=bar&class2=222`)) + return c.BodyParser(testStruct) + }) + }) + t.Run("BodyParser:json", func(t *testing.T) { + t.Parallel() + withValues(t, func(c *Ctx, testStruct *TestStruct) error { + c.Request().Header.SetContentType(MIMEApplicationJSON) + c.Request().SetBody([]byte(`{"name":"foo","class":111,"name2":"bar","class2":222}`)) + return c.BodyParser(testStruct) + }) + }) + t.Run("BodyParser:multiform", func(t *testing.T) { + t.Parallel() + withValues(t, func(c *Ctx, testStruct *TestStruct) error { + body := []byte("--b\r\nContent-Disposition: form-data; name=\"name\"\r\n\r\nfoo\r\n--b\r\nContent-Disposition: form-data; name=\"class\"\r\n\r\n111\r\n--b\r\nContent-Disposition: form-data; name=\"name2\"\r\n\r\nbar\r\n--b\r\nContent-Disposition: form-data; name=\"class2\"\r\n\r\n222\r\n--b--") + c.Request().SetBody(body) + c.Request().Header.SetContentType(MIMEMultipartForm + `;boundary="b"`) + c.Request().Header.SetContentLength(len(body)) + return c.BodyParser(testStruct) + }) + }) + t.Run("CookieParser", func(t *testing.T) { + t.Parallel() + withValues(t, func(c *Ctx, testStruct *TestStruct) error { + c.Request().Header.Set("Cookie", "name=foo;name2=bar;class=111;class2=222") + return c.CookieParser(testStruct) + }) + }) + t.Run("QueryParser", func(t *testing.T) { + t.Parallel() + withValues(t, func(c *Ctx, testStruct *TestStruct) error { + c.Request().URI().SetQueryString("name=foo&name2=bar&class=111&class2=222") + return c.QueryParser(testStruct) + }) + }) + t.Run("ParamsParser", func(t *testing.T) { + t.Parallel() + withValues(t, func(c *Ctx, testStruct *TestStruct) error { + c.route = &Route{Params: []string{"name", "name2", "class", "class2"}} + c.values = [30]string{"foo", "bar", "111", "222"} + return c.ParamsParser(testStruct) + }) + }) + t.Run("ReqHeaderParser", func(t *testing.T) { + t.Parallel() + withValues(t, func(c *Ctx, testStruct *TestStruct) error { + c.Request().Header.Add("name", "foo") + c.Request().Header.Add("name2", "bar") + c.Request().Header.Add("class", "111") + c.Request().Header.Add("class2", "222") + return c.ReqHeaderParser(testStruct) + }) + }) +} + +// go test -run Test_Ctx_Get +func Test_Ctx_Get(t *testing.T) { + t.Parallel() + app := New() + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + c.Request().Header.Set(HeaderAcceptCharset, "utf-8, iso-8859-1;q=0.5") + c.Request().Header.Set(HeaderReferer, "Monster") + utils.AssertEqual(t, "utf-8, iso-8859-1;q=0.5", c.Get(HeaderAcceptCharset)) + utils.AssertEqual(t, "Monster", c.Get(HeaderReferer)) + utils.AssertEqual(t, "default", c.Get("unknown", "default")) +} + +// go test -run Test_Ctx_IsProxyTrusted +func Test_Ctx_IsProxyTrusted(t *testing.T) { + t.Parallel() + + { + app := New() + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + utils.AssertEqual(t, true, c.IsProxyTrusted()) + } + { + app := New(Config{ + EnableTrustedProxyCheck: false, + }) + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + utils.AssertEqual(t, true, c.IsProxyTrusted()) + } + + { + app := New(Config{ + EnableTrustedProxyCheck: true, + }) + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + utils.AssertEqual(t, false, c.IsProxyTrusted()) + } + { + app := New(Config{ + EnableTrustedProxyCheck: true, + + TrustedProxies: []string{}, + }) + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + utils.AssertEqual(t, false, c.IsProxyTrusted()) + } + { + app := New(Config{ + EnableTrustedProxyCheck: true, + + TrustedProxies: []string{ + "127.0.0.1", + }, + }) + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + utils.AssertEqual(t, false, c.IsProxyTrusted()) + } + { + app := New(Config{ + EnableTrustedProxyCheck: true, + + TrustedProxies: []string{ + "127.0.0.1/8", + }, + }) + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + utils.AssertEqual(t, false, c.IsProxyTrusted()) + } + { + app := New(Config{ + EnableTrustedProxyCheck: true, + + TrustedProxies: []string{ + "0.0.0.0", + }, + }) + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + utils.AssertEqual(t, true, c.IsProxyTrusted()) + } + { + app := New(Config{ + EnableTrustedProxyCheck: true, + + TrustedProxies: []string{ + "0.0.0.1/31", + }, + }) + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + utils.AssertEqual(t, true, c.IsProxyTrusted()) + } + { + app := New(Config{ + EnableTrustedProxyCheck: true, + + TrustedProxies: []string{ + "0.0.0.1/31junk", + }, + }) + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + utils.AssertEqual(t, false, c.IsProxyTrusted()) + } +} + +// go test -run Test_Ctx_Hostname +func Test_Ctx_Hostname(t *testing.T) { + t.Parallel() + app := New() + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + c.Request().SetRequestURI("http://google.com/test") + utils.AssertEqual(t, "google.com", c.Hostname()) +} + +// go test -run Test_Ctx_Hostname_Untrusted +func Test_Ctx_Hostname_UntrustedProxy(t *testing.T) { + t.Parallel() + // Don't trust any proxy + { + app := New(Config{EnableTrustedProxyCheck: true, TrustedProxies: []string{}}) + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + c.Request().SetRequestURI("http://google.com/test") + c.Request().Header.Set(HeaderXForwardedHost, "google1.com") + utils.AssertEqual(t, "google.com", c.Hostname()) + app.ReleaseCtx(c) + } + // Trust to specific proxy list + { + app := New(Config{EnableTrustedProxyCheck: true, TrustedProxies: []string{"0.8.0.0", "0.8.0.1"}}) + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + c.Request().SetRequestURI("http://google.com/test") + c.Request().Header.Set(HeaderXForwardedHost, "google1.com") + utils.AssertEqual(t, "google.com", c.Hostname()) + app.ReleaseCtx(c) + } +} + +// go test -run Test_Ctx_Hostname_Trusted +func Test_Ctx_Hostname_TrustedProxy(t *testing.T) { + t.Parallel() + { + app := New(Config{EnableTrustedProxyCheck: true, TrustedProxies: []string{"0.0.0.0", "0.8.0.1"}}) + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + c.Request().SetRequestURI("http://google.com/test") + c.Request().Header.Set(HeaderXForwardedHost, "google1.com") + utils.AssertEqual(t, "google1.com", c.Hostname()) + app.ReleaseCtx(c) + } +} + +// go test -run Test_Ctx_Hostname_Trusted_Multiple +func Test_Ctx_Hostname_TrustedProxy_Multiple(t *testing.T) { + t.Parallel() + { + app := New(Config{EnableTrustedProxyCheck: true, TrustedProxies: []string{"0.0.0.0", "0.8.0.1"}}) + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + c.Request().SetRequestURI("http://google.com/test") + c.Request().Header.Set(HeaderXForwardedHost, "google1.com, google2.com") + utils.AssertEqual(t, "google1.com", c.Hostname()) + app.ReleaseCtx(c) + } +} + +// go test -run Test_Ctx_Hostname_UntrustedProxyRange +func Test_Ctx_Hostname_TrustedProxyRange(t *testing.T) { + t.Parallel() + + app := New(Config{EnableTrustedProxyCheck: true, TrustedProxies: []string{"0.0.0.0/30"}}) + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + c.Request().SetRequestURI("http://google.com/test") + c.Request().Header.Set(HeaderXForwardedHost, "google1.com") + utils.AssertEqual(t, "google1.com", c.Hostname()) + app.ReleaseCtx(c) +} + +// go test -run Test_Ctx_Hostname_UntrustedProxyRange +func Test_Ctx_Hostname_UntrustedProxyRange(t *testing.T) { + t.Parallel() + + app := New(Config{EnableTrustedProxyCheck: true, TrustedProxies: []string{"1.0.0.0/30"}}) + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + c.Request().SetRequestURI("http://google.com/test") + c.Request().Header.Set(HeaderXForwardedHost, "google1.com") + utils.AssertEqual(t, "google.com", c.Hostname()) + app.ReleaseCtx(c) +} + +// go test -run Test_Ctx_Port +func Test_Ctx_Port(t *testing.T) { + t.Parallel() + app := New() + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + utils.AssertEqual(t, "0", c.Port()) +} + +// go test -run Test_Ctx_PortInHandler +func Test_Ctx_PortInHandler(t *testing.T) { + t.Parallel() + app := New() + + app.Get("/port", func(c *Ctx) error { + return c.SendString(c.Port()) + }) + + resp, err := app.Test(httptest.NewRequest(MethodGet, "/port", nil)) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, StatusOK, resp.StatusCode, "Status code") + + body, err := io.ReadAll(resp.Body) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, "0", string(body)) +} + +// go test -run Test_Ctx_IP +func Test_Ctx_IP(t *testing.T) { + t.Parallel() + + app := New() + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + + // default behavior will return the remote IP from the stack + utils.AssertEqual(t, "0.0.0.0", c.IP()) + + // X-Forwarded-For is set, but it is ignored because proxyHeader is not set + c.Request().Header.Set(HeaderXForwardedFor, "0.0.0.1") + utils.AssertEqual(t, "0.0.0.0", c.IP()) +} + +// go test -run Test_Ctx_IP_ProxyHeader +func Test_Ctx_IP_ProxyHeader(t *testing.T) { + t.Parallel() + + // make sure that the same behavior exists for different proxy header names + proxyHeaderNames := []string{"Real-Ip", HeaderXForwardedFor} + + for _, proxyHeaderName := range proxyHeaderNames { + app := New(Config{ProxyHeader: proxyHeaderName}) + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + + c.Request().Header.Set(proxyHeaderName, "0.0.0.1") + utils.AssertEqual(t, "0.0.0.1", c.IP()) + + // without IP validation we return the full string + c.Request().Header.Set(proxyHeaderName, "0.0.0.1, 0.0.0.2") + utils.AssertEqual(t, "0.0.0.1, 0.0.0.2", c.IP()) + + // without IP validation we return invalid IPs + c.Request().Header.Set(proxyHeaderName, "invalid, 0.0.0.2, 0.0.0.3") + utils.AssertEqual(t, "invalid, 0.0.0.2, 0.0.0.3", c.IP()) + + // when proxy header is enabled but the value is empty, without IP validation we return an empty string + c.Request().Header.Set(proxyHeaderName, "") + utils.AssertEqual(t, "", c.IP()) + + // without IP validation we return an invalid IP + c.Request().Header.Set(proxyHeaderName, "not-valid-ip") + utils.AssertEqual(t, "not-valid-ip", c.IP()) + + app.ReleaseCtx(c) + } +} + +// go test -run Test_Ctx_IP_ProxyHeader +func Test_Ctx_IP_ProxyHeader_With_IP_Validation(t *testing.T) { + t.Parallel() + + // make sure that the same behavior exists for different proxy header names + proxyHeaderNames := []string{"Real-Ip", HeaderXForwardedFor} + + for _, proxyHeaderName := range proxyHeaderNames { + app := New(Config{EnableIPValidation: true, ProxyHeader: proxyHeaderName}) + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + + // when proxy header & validation is enabled and the value is a valid IP, we return it + c.Request().Header.Set(proxyHeaderName, "0.0.0.1") + utils.AssertEqual(t, "0.0.0.1", c.IP()) + + // when proxy header & validation is enabled and the value is a list of IPs, we return the first valid IP + c.Request().Header.Set(proxyHeaderName, "0.0.0.1, 0.0.0.2") + utils.AssertEqual(t, "0.0.0.1", c.IP()) + + c.Request().Header.Set(proxyHeaderName, "invalid, 0.0.0.2, 0.0.0.3") + utils.AssertEqual(t, "0.0.0.2", c.IP()) + + // when proxy header & validation is enabled but the value is empty, we will ignore the header + c.Request().Header.Set(proxyHeaderName, "") + utils.AssertEqual(t, "0.0.0.0", c.IP()) + + // when proxy header & validation is enabled but the value is not an IP, we will ignore the header + // and return the IP of the caller + c.Request().Header.Set(proxyHeaderName, "not-valid-ip") + utils.AssertEqual(t, "0.0.0.0", c.IP()) + + app.ReleaseCtx(c) + } +} + +// go test -run Test_Ctx_IP_UntrustedProxy +func Test_Ctx_IP_UntrustedProxy(t *testing.T) { + t.Parallel() + app := New(Config{EnableTrustedProxyCheck: true, TrustedProxies: []string{"0.8.0.1"}, ProxyHeader: HeaderXForwardedFor}) + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + c.Request().Header.Set(HeaderXForwardedFor, "0.0.0.1") + defer app.ReleaseCtx(c) + utils.AssertEqual(t, "0.0.0.0", c.IP()) +} + +// go test -run Test_Ctx_IP_TrustedProxy +func Test_Ctx_IP_TrustedProxy(t *testing.T) { + t.Parallel() + app := New(Config{EnableTrustedProxyCheck: true, TrustedProxies: []string{"0.0.0.0"}, ProxyHeader: HeaderXForwardedFor}) + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + c.Request().Header.Set(HeaderXForwardedFor, "0.0.0.1") + defer app.ReleaseCtx(c) + utils.AssertEqual(t, "0.0.0.1", c.IP()) +} + +// go test -run Test_Ctx_IPs -parallel +func Test_Ctx_IPs(t *testing.T) { + t.Parallel() + app := New() + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + + // normal happy path test case + c.Request().Header.Set(HeaderXForwardedFor, "127.0.0.1, 127.0.0.2, 127.0.0.3") + utils.AssertEqual(t, []string{"127.0.0.1", "127.0.0.2", "127.0.0.3"}, c.IPs()) + + // inconsistent space formatting + c.Request().Header.Set(HeaderXForwardedFor, "127.0.0.1,127.0.0.2 ,127.0.0.3") + utils.AssertEqual(t, []string{"127.0.0.1", "127.0.0.2", "127.0.0.3"}, c.IPs()) + + // invalid IPs are allowed to be returned + c.Request().Header.Set(HeaderXForwardedFor, "invalid, 127.0.0.1, 127.0.0.2") + utils.AssertEqual(t, []string{"invalid", "127.0.0.1", "127.0.0.2"}, c.IPs()) + c.Request().Header.Set(HeaderXForwardedFor, "127.0.0.1, invalid, 127.0.0.2") + utils.AssertEqual(t, []string{"127.0.0.1", "invalid", "127.0.0.2"}, c.IPs()) + + // ensure that the ordering of IPs in the header is maintained + c.Request().Header.Set(HeaderXForwardedFor, "127.0.0.3, 127.0.0.1, 127.0.0.2") + utils.AssertEqual(t, []string{"127.0.0.3", "127.0.0.1", "127.0.0.2"}, c.IPs()) + + // ensure for IPv6 + c.Request().Header.Set(HeaderXForwardedFor, "9396:9549:b4f7:8ed0:4791:1330:8c06:e62d, invalid, 2345:0425:2CA1::0567:5673:23b5") + utils.AssertEqual(t, []string{"9396:9549:b4f7:8ed0:4791:1330:8c06:e62d", "invalid", "2345:0425:2CA1::0567:5673:23b5"}, c.IPs()) + + // empty header + c.Request().Header.Set(HeaderXForwardedFor, "") + utils.AssertEqual(t, 0, len(c.IPs())) + + // missing header + c.Request() + utils.AssertEqual(t, 0, len(c.IPs())) +} + +func Test_Ctx_IPs_With_IP_Validation(t *testing.T) { + t.Parallel() + app := New(Config{EnableIPValidation: true}) + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + + // normal happy path test case + c.Request().Header.Set(HeaderXForwardedFor, "127.0.0.1, 127.0.0.2, 127.0.0.3") + utils.AssertEqual(t, []string{"127.0.0.1", "127.0.0.2", "127.0.0.3"}, c.IPs()) + + // inconsistent space formatting + c.Request().Header.Set(HeaderXForwardedFor, "127.0.0.1,127.0.0.2 ,127.0.0.3") + utils.AssertEqual(t, []string{"127.0.0.1", "127.0.0.2", "127.0.0.3"}, c.IPs()) + + // invalid IPs are in the header + c.Request().Header.Set(HeaderXForwardedFor, "invalid, 127.0.0.1, 127.0.0.2") + utils.AssertEqual(t, []string{"127.0.0.1", "127.0.0.2"}, c.IPs()) + c.Request().Header.Set(HeaderXForwardedFor, "127.0.0.1, invalid, 127.0.0.2") + utils.AssertEqual(t, []string{"127.0.0.1", "127.0.0.2"}, c.IPs()) + + // ensure that the ordering of IPs in the header is maintained + c.Request().Header.Set(HeaderXForwardedFor, "127.0.0.3, 127.0.0.1, 127.0.0.2") + utils.AssertEqual(t, []string{"127.0.0.3", "127.0.0.1", "127.0.0.2"}, c.IPs()) + + // ensure for IPv6 + c.Request().Header.Set(HeaderXForwardedFor, "f037:825e:eadb:1b7b:1667:6f0a:5356:f604, invalid, 9396:9549:b4f7:8ed0:4791:1330:8c06:e62d") + utils.AssertEqual(t, []string{"f037:825e:eadb:1b7b:1667:6f0a:5356:f604", "9396:9549:b4f7:8ed0:4791:1330:8c06:e62d"}, c.IPs()) + + // empty header + c.Request().Header.Set(HeaderXForwardedFor, "") + utils.AssertEqual(t, 0, len(c.IPs())) + + // missing header + c.Request() + utils.AssertEqual(t, 0, len(c.IPs())) +} + +// go test -v -run=^$ -bench=Benchmark_Ctx_IPs -benchmem -count=4 +func Benchmark_Ctx_IPs(b *testing.B) { + app := New() + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + c.Request().Header.Set(HeaderXForwardedFor, "127.0.0.1, invalid, 127.0.0.1") + var res []string + b.ReportAllocs() + b.ResetTimer() + for n := 0; n < b.N; n++ { + res = c.IPs() + } + utils.AssertEqual(b, []string{"127.0.0.1", "invalid", "127.0.0.1"}, res) +} + +func Benchmark_Ctx_IPs_v6(b *testing.B) { + app := New() + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + c.Request().Header.Set(HeaderXForwardedFor, "f037:825e:eadb:1b7b:1667:6f0a:5356:f604, invalid, 2345:0425:2CA1::0567:5673:23b5") + var res []string + b.ReportAllocs() + b.ResetTimer() + for n := 0; n < b.N; n++ { + res = c.IPs() + } + utils.AssertEqual(b, []string{"f037:825e:eadb:1b7b:1667:6f0a:5356:f604", "invalid", "2345:0425:2CA1::0567:5673:23b5"}, res) +} + +func Benchmark_Ctx_IPs_With_IP_Validation(b *testing.B) { + app := New(Config{EnableIPValidation: true}) + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + c.Request().Header.Set(HeaderXForwardedFor, "127.0.0.1, invalid, 127.0.0.1") + var res []string + b.ReportAllocs() + b.ResetTimer() + for n := 0; n < b.N; n++ { + res = c.IPs() + } + utils.AssertEqual(b, []string{"127.0.0.1", "127.0.0.1"}, res) +} + +func Benchmark_Ctx_IPs_v6_With_IP_Validation(b *testing.B) { + app := New(Config{EnableIPValidation: true}) + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + c.Request().Header.Set(HeaderXForwardedFor, "2345:0425:2CA1:0000:0000:0567:5673:23b5, invalid, 2345:0425:2CA1::0567:5673:23b5") + var res []string + b.ReportAllocs() + b.ResetTimer() + for n := 0; n < b.N; n++ { + res = c.IPs() + } + utils.AssertEqual(b, []string{"2345:0425:2CA1:0000:0000:0567:5673:23b5", "2345:0425:2CA1::0567:5673:23b5"}, res) +} + +func Benchmark_Ctx_IP_With_ProxyHeader(b *testing.B) { + app := New(Config{ProxyHeader: HeaderXForwardedFor}) + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + c.Request().Header.Set(HeaderXForwardedFor, "127.0.0.1") + var res string + b.ReportAllocs() + b.ResetTimer() + for n := 0; n < b.N; n++ { + res = c.IP() + } + utils.AssertEqual(b, "127.0.0.1", res) +} + +func Benchmark_Ctx_IP_With_ProxyHeader_and_IP_Validation(b *testing.B) { + app := New(Config{ProxyHeader: HeaderXForwardedFor, EnableIPValidation: true}) + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + c.Request().Header.Set(HeaderXForwardedFor, "127.0.0.1") + var res string + b.ReportAllocs() + b.ResetTimer() + for n := 0; n < b.N; n++ { + res = c.IP() + } + utils.AssertEqual(b, "127.0.0.1", res) +} + +func Benchmark_Ctx_IP(b *testing.B) { + app := New() + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + c.Request() + var res string + b.ReportAllocs() + b.ResetTimer() + for n := 0; n < b.N; n++ { + res = c.IP() + } + utils.AssertEqual(b, "0.0.0.0", res) +} + +// go test -run Test_Ctx_Is +func Test_Ctx_Is(t *testing.T) { + t.Parallel() + app := New() + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + c.Request().Header.Set(HeaderContentType, MIMETextHTML+"; boundary=something") + utils.AssertEqual(t, true, c.Is(".html")) + utils.AssertEqual(t, true, c.Is("html")) + utils.AssertEqual(t, false, c.Is("json")) + utils.AssertEqual(t, false, c.Is(".json")) + utils.AssertEqual(t, false, c.Is("")) + utils.AssertEqual(t, false, c.Is(".foooo")) + + c.Request().Header.Set(HeaderContentType, MIMEApplicationJSONCharsetUTF8) + utils.AssertEqual(t, false, c.Is("html")) + utils.AssertEqual(t, true, c.Is("json")) + utils.AssertEqual(t, true, c.Is(".json")) + + c.Request().Header.Set(HeaderContentType, " application/json;charset=UTF-8") + utils.AssertEqual(t, false, c.Is("html")) + utils.AssertEqual(t, true, c.Is("json")) + utils.AssertEqual(t, true, c.Is(".json")) + + c.Request().Header.Set(HeaderContentType, MIMEApplicationXMLCharsetUTF8) + utils.AssertEqual(t, false, c.Is("html")) + utils.AssertEqual(t, true, c.Is("xml")) + utils.AssertEqual(t, true, c.Is(".xml")) + + c.Request().Header.Set(HeaderContentType, MIMETextPlain) + utils.AssertEqual(t, false, c.Is("html")) + utils.AssertEqual(t, true, c.Is("txt")) + utils.AssertEqual(t, true, c.Is(".txt")) +} + +// go test -v -run=^$ -bench=Benchmark_Ctx_Is -benchmem -count=4 +func Benchmark_Ctx_Is(b *testing.B) { + app := New() + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + c.Request().Header.Set(HeaderContentType, MIMEApplicationJSON) + var res bool + b.ReportAllocs() + b.ResetTimer() + for n := 0; n < b.N; n++ { + _ = c.Is(".json") + res = c.Is("json") + } + utils.AssertEqual(b, true, res) +} + +// go test -run Test_Ctx_Locals +func Test_Ctx_Locals(t *testing.T) { + t.Parallel() + app := New() + app.Use(func(c *Ctx) error { + c.Locals("john", "doe") + return c.Next() + }) + app.Get("/test", func(c *Ctx) error { + utils.AssertEqual(t, "doe", c.Locals("john")) + return nil + }) + resp, err := app.Test(httptest.NewRequest(MethodGet, "/test", nil)) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, StatusOK, resp.StatusCode, "Status code") +} + +// go test -run Test_Ctx_Method +func Test_Ctx_Method(t *testing.T) { + t.Parallel() + fctx := &fasthttp.RequestCtx{} + fctx.Request.Header.SetMethod(MethodGet) + app := New() + c := app.AcquireCtx(fctx) + defer app.ReleaseCtx(c) + utils.AssertEqual(t, MethodGet, c.Method()) + c.Method(MethodPost) + utils.AssertEqual(t, MethodPost, c.Method()) + + c.Method("MethodInvalid") + utils.AssertEqual(t, MethodPost, c.Method()) +} + +// go test -run Test_Ctx_ClientHelloInfo +func Test_Ctx_ClientHelloInfo(t *testing.T) { + t.Parallel() + app := New() + app.Get("/ServerName", func(c *Ctx) error { + result := c.ClientHelloInfo() + if result != nil { + return c.SendString(result.ServerName) + } + + return c.SendString("ClientHelloInfo is nil") + }) + app.Get("/SignatureSchemes", func(c *Ctx) error { + result := c.ClientHelloInfo() + if result != nil { + return c.JSON(result.SignatureSchemes) + } + + return c.SendString("ClientHelloInfo is nil") + }) + app.Get("/SupportedVersions", func(c *Ctx) error { + result := c.ClientHelloInfo() + if result != nil { + return c.JSON(result.SupportedVersions) + } + + return c.SendString("ClientHelloInfo is nil") + }) + + // Test without TLS handler + resp, err := app.Test(httptest.NewRequest(MethodGet, "/ServerName", nil)) + utils.AssertEqual(t, nil, err) + body, err := io.ReadAll(resp.Body) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, []byte("ClientHelloInfo is nil"), body) + + // Test with TLS Handler + const ( + pssWithSHA256 = 0x0804 + versionTLS13 = 0x0304 + ) + app.tlsHandler = &TLSHandler{clientHelloInfo: &tls.ClientHelloInfo{ + ServerName: "example.golang", + SignatureSchemes: []tls.SignatureScheme{pssWithSHA256}, + SupportedVersions: []uint16{versionTLS13}, + }} + + // Test ServerName + resp, err = app.Test(httptest.NewRequest(MethodGet, "/ServerName", nil)) + utils.AssertEqual(t, nil, err) + body, err = io.ReadAll(resp.Body) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, []byte("example.golang"), body) + + // Test SignatureSchemes + resp, err = app.Test(httptest.NewRequest(MethodGet, "/SignatureSchemes", nil)) + utils.AssertEqual(t, nil, err) + body, err = io.ReadAll(resp.Body) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, "["+strconv.Itoa(pssWithSHA256)+"]", string(body)) + + // Test SupportedVersions + resp, err = app.Test(httptest.NewRequest(MethodGet, "/SupportedVersions", nil)) + utils.AssertEqual(t, nil, err) + body, err = io.ReadAll(resp.Body) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, "["+strconv.Itoa(versionTLS13)+"]", string(body)) +} + +// go test -run Test_Ctx_InvalidMethod +func Test_Ctx_InvalidMethod(t *testing.T) { + t.Parallel() + app := New() + app.Get("/", func(c *Ctx) error { + return nil + }) + + fctx := &fasthttp.RequestCtx{} + fctx.Request.Header.SetMethod("InvalidMethod") + fctx.Request.SetRequestURI("/") + + app.Handler()(fctx) + + utils.AssertEqual(t, 400, fctx.Response.StatusCode()) + utils.AssertEqual(t, []byte("Invalid http method"), fctx.Response.Body()) +} + +// go test -run Test_Ctx_MultipartForm +func Test_Ctx_MultipartForm(t *testing.T) { + t.Parallel() + app := New() + + app.Post("/test", func(c *Ctx) error { + result, err := c.MultipartForm() + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, "john", result.Value["name"][0]) + return nil + }) + + body := &bytes.Buffer{} + writer := multipart.NewWriter(body) + utils.AssertEqual(t, nil, writer.WriteField("name", "john")) + utils.AssertEqual(t, nil, writer.Close()) + + req := httptest.NewRequest(MethodPost, "/test", body) + req.Header.Set(HeaderContentType, fmt.Sprintf("multipart/form-data; boundary=%s", writer.Boundary())) + req.Header.Set(HeaderContentLength, strconv.Itoa(len(body.Bytes()))) + + resp, err := app.Test(req) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, StatusOK, resp.StatusCode, "Status code") +} + +// go test -v -run=^$ -bench=Benchmark_Ctx_MultipartForm -benchmem -count=4 +func Benchmark_Ctx_MultipartForm(b *testing.B) { + app := New() + + app.Post("/", func(c *Ctx) error { + _, err := c.MultipartForm() + return err + }) + + c := &fasthttp.RequestCtx{} + + body := []byte("--b\r\nContent-Disposition: form-data; name=\"name\"\r\n\r\njohn\r\n--b--") + c.Request.SetBody(body) + c.Request.Header.SetContentType(MIMEMultipartForm + `;boundary="b"`) + c.Request.Header.SetContentLength(len(body)) + + h := app.Handler() + + b.ReportAllocs() + b.ResetTimer() + + for n := 0; n < b.N; n++ { + h(c) + } +} + +// go test -run Test_Ctx_OriginalURL +func Test_Ctx_OriginalURL(t *testing.T) { + t.Parallel() + app := New() + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + c.Request().Header.SetRequestURI("http://google.com/test?search=demo") + utils.AssertEqual(t, "http://google.com/test?search=demo", c.OriginalURL()) +} + +// go test -race -run Test_Ctx_Params +func Test_Ctx_Params(t *testing.T) { + t.Parallel() + app := New() + app.Get("/test/:user", func(c *Ctx) error { + utils.AssertEqual(t, "john", c.Params("user")) + return nil + }) + app.Get("/test2/*", func(c *Ctx) error { + utils.AssertEqual(t, "im/a/cookie", c.Params("*")) + return nil + }) + app.Get("/test3/*/blafasel/*", func(c *Ctx) error { + utils.AssertEqual(t, "1111", c.Params("*1")) + utils.AssertEqual(t, "2222", c.Params("*2")) + utils.AssertEqual(t, "1111", c.Params("*")) + return nil + }) + app.Get("/test4/:optional?", func(c *Ctx) error { + utils.AssertEqual(t, "", c.Params("optional")) + return nil + }) + app.Get("/test5/:id/:Id", func(c *Ctx) error { + utils.AssertEqual(t, "first", c.Params("id")) + utils.AssertEqual(t, "first", c.Params("Id")) + return nil + }) + resp, err := app.Test(httptest.NewRequest(MethodGet, "/test/john", nil)) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, StatusOK, resp.StatusCode, "Status code") + + resp, err = app.Test(httptest.NewRequest(MethodGet, "/test2/im/a/cookie", nil)) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, StatusOK, resp.StatusCode, "Status code") + + resp, err = app.Test(httptest.NewRequest(MethodGet, "/test3/1111/blafasel/2222", nil)) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, StatusOK, resp.StatusCode, "Status code") + + resp, err = app.Test(httptest.NewRequest(MethodGet, "/test4", nil)) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, StatusOK, resp.StatusCode, "Status code") + + resp, err = app.Test(httptest.NewRequest(MethodGet, "/test5/first/second", nil)) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, StatusOK, resp.StatusCode, "Status code") +} + +func Test_Ctx_Params_Case_Sensitive(t *testing.T) { + t.Parallel() + app := New(Config{CaseSensitive: true}) + app.Get("/test/:User", func(c *Ctx) error { + utils.AssertEqual(t, "john", c.Params("User")) + utils.AssertEqual(t, "", c.Params("user")) + return nil + }) + app.Get("/test2/:id/:Id", func(c *Ctx) error { + utils.AssertEqual(t, "first", c.Params("id")) + utils.AssertEqual(t, "second", c.Params("Id")) + return nil + }) + resp, err := app.Test(httptest.NewRequest(MethodGet, "/test/john", nil)) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, StatusOK, resp.StatusCode, "Status code") + + resp, err = app.Test(httptest.NewRequest(MethodGet, "/test2/first/second", nil)) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, StatusOK, resp.StatusCode, "Status code") +} + +// go test -race -run Test_Ctx_AllParams +func Test_Ctx_AllParams(t *testing.T) { + t.Parallel() + app := New() + app.Get("/test/:user", func(c *Ctx) error { + utils.AssertEqual(t, map[string]string{"user": "john"}, c.AllParams()) + return nil + }) + app.Get("/test2/*", func(c *Ctx) error { + utils.AssertEqual(t, map[string]string{"*1": "im/a/cookie"}, c.AllParams()) + return nil + }) + app.Get("/test3/*/blafasel/*", func(c *Ctx) error { + utils.AssertEqual(t, map[string]string{"*1": "1111", "*2": "2222"}, c.AllParams()) + return nil + }) + app.Get("/test4/:optional?", func(c *Ctx) error { + utils.AssertEqual(t, map[string]string{"optional": ""}, c.AllParams()) + return nil + }) + + resp, err := app.Test(httptest.NewRequest(MethodGet, "/test/john", nil)) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, StatusOK, resp.StatusCode, "Status code") + + resp, err = app.Test(httptest.NewRequest(MethodGet, "/test2/im/a/cookie", nil)) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, StatusOK, resp.StatusCode, "Status code") + + resp, err = app.Test(httptest.NewRequest(MethodGet, "/test3/1111/blafasel/2222", nil)) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, StatusOK, resp.StatusCode, "Status code") + + resp, err = app.Test(httptest.NewRequest(MethodGet, "/test4", nil)) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, StatusOK, resp.StatusCode, "Status code") +} + +// go test -v -run=^$ -bench=Benchmark_Ctx_Params -benchmem -count=4 +func Benchmark_Ctx_Params(b *testing.B) { + app := New() + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + c.route = &Route{ + Params: []string{ + "param1", "param2", "param3", "param4", + }, + } + c.values = [maxParams]string{ + "john", "doe", "is", "awesome", + } + var res string + b.ReportAllocs() + b.ResetTimer() + for n := 0; n < b.N; n++ { + _ = c.Params("param1") + _ = c.Params("param2") + _ = c.Params("param3") + res = c.Params("param4") + } + utils.AssertEqual(b, "awesome", res) +} + +// go test -v -run=^$ -bench=Benchmark_Ctx_AllParams -benchmem -count=4 +func Benchmark_Ctx_AllParams(b *testing.B) { + app := New() + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + c.route = &Route{ + Params: []string{ + "param1", "param2", "param3", "param4", + }, + } + c.values = [maxParams]string{ + "john", "doe", "is", "awesome", + } + var res map[string]string + b.ReportAllocs() + b.ResetTimer() + for n := 0; n < b.N; n++ { + res = c.AllParams() + } + utils.AssertEqual( + b, + map[string]string{ + "param1": "john", + "param2": "doe", + "param3": "is", + "param4": "awesome", + }, + res, + ) +} + +// go test -v -run=^$ -bench=Benchmark_Ctx_ParamsParse -benchmem -count=4 +func Benchmark_Ctx_ParamsParse(b *testing.B) { + app := New() + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + c.route = &Route{ + Params: []string{ + "param1", "param2", "param3", "param4", + }, + } + c.values = [maxParams]string{ + "john", "doe", "is", "awesome", + } + var res struct { + Param1 string `params:"param1"` + Param2 string `params:"param2"` + Param3 string `params:"param3"` + Param4 string `params:"param4"` + } + b.ReportAllocs() + b.ResetTimer() + + var err error + for n := 0; n < b.N; n++ { + err = c.ParamsParser(&res) + } + + utils.AssertEqual(b, nil, err) + utils.AssertEqual(b, "john", res.Param1) + utils.AssertEqual(b, "doe", res.Param2) + utils.AssertEqual(b, "is", res.Param3) + utils.AssertEqual(b, "awesome", res.Param4) +} + +// go test -run Test_Ctx_Path +func Test_Ctx_Path(t *testing.T) { + t.Parallel() + app := New(Config{UnescapePath: true}) + app.Get("/test/:user", func(c *Ctx) error { + utils.AssertEqual(t, "/Test/John", c.Path()) + // not strict && case insensitive + utils.AssertEqual(t, "/ABC/", c.Path("/ABC/")) + utils.AssertEqual(t, "/test/john/", c.Path("/test/john/")) + return nil + }) + + // test with special chars + app.Get("/specialChars/:name", func(c *Ctx) error { + utils.AssertEqual(t, "/specialChars/créer", c.Path()) + // unescape is also working if you set the path afterwards + utils.AssertEqual(t, "/اختبار/", c.Path("/%D8%A7%D8%AE%D8%AA%D8%A8%D8%A7%D8%B1/")) + return nil + }) + resp, err := app.Test(httptest.NewRequest(MethodGet, "/specialChars/cr%C3%A9er", nil)) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, StatusOK, resp.StatusCode, "Status code") +} + +// go test -run Test_Ctx_Protocol +func Test_Ctx_Protocol(t *testing.T) { + t.Parallel() + app := New() + + freq := &fasthttp.RequestCtx{} + freq.Request.Header.Set("X-Forwarded", "invalid") + + c := app.AcquireCtx(freq) + defer app.ReleaseCtx(c) + c.Request().Header.Set(HeaderXForwardedProto, schemeHTTPS) + utils.AssertEqual(t, schemeHTTPS, c.Protocol()) + c.Request().Header.Reset() + + c.Request().Header.Set(HeaderXForwardedProtocol, schemeHTTPS) + utils.AssertEqual(t, schemeHTTPS, c.Protocol()) + c.Request().Header.Reset() + + c.Request().Header.Set(HeaderXForwardedProto, "https, http") + utils.AssertEqual(t, schemeHTTPS, c.Protocol()) + c.Request().Header.Reset() + + c.Request().Header.Set(HeaderXForwardedProtocol, "https, http") + utils.AssertEqual(t, schemeHTTPS, c.Protocol()) + c.Request().Header.Reset() + + c.Request().Header.Set(HeaderXForwardedSsl, "on") + utils.AssertEqual(t, schemeHTTPS, c.Protocol()) + c.Request().Header.Reset() + + c.Request().Header.Set(HeaderXUrlScheme, schemeHTTPS) + utils.AssertEqual(t, schemeHTTPS, c.Protocol()) + c.Request().Header.Reset() + + utils.AssertEqual(t, schemeHTTP, c.Protocol()) +} + +// go test -v -run=^$ -bench=Benchmark_Ctx_Protocol -benchmem -count=4 +func Benchmark_Ctx_Protocol(b *testing.B) { + app := New() + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + var res string + b.ReportAllocs() + b.ResetTimer() + for n := 0; n < b.N; n++ { + res = c.Protocol() + } + utils.AssertEqual(b, schemeHTTP, res) +} + +// go test -run Test_Ctx_Protocol_TrustedProxy +func Test_Ctx_Protocol_TrustedProxy(t *testing.T) { + t.Parallel() + app := New(Config{EnableTrustedProxyCheck: true, TrustedProxies: []string{"0.0.0.0"}}) + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + + c.Request().Header.Set(HeaderXForwardedProto, schemeHTTPS) + utils.AssertEqual(t, schemeHTTPS, c.Protocol()) + c.Request().Header.Reset() + + c.Request().Header.Set(HeaderXForwardedProtocol, schemeHTTPS) + utils.AssertEqual(t, schemeHTTPS, c.Protocol()) + c.Request().Header.Reset() + + c.Request().Header.Set(HeaderXForwardedSsl, "on") + utils.AssertEqual(t, schemeHTTPS, c.Protocol()) + c.Request().Header.Reset() + + c.Request().Header.Set(HeaderXUrlScheme, schemeHTTPS) + utils.AssertEqual(t, schemeHTTPS, c.Protocol()) + c.Request().Header.Reset() + + utils.AssertEqual(t, schemeHTTP, c.Protocol()) +} + +// go test -run Test_Ctx_Protocol_TrustedProxyRange +func Test_Ctx_Protocol_TrustedProxyRange(t *testing.T) { + t.Parallel() + app := New(Config{EnableTrustedProxyCheck: true, TrustedProxies: []string{"0.0.0.0/30"}}) + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + + c.Request().Header.Set(HeaderXForwardedProto, schemeHTTPS) + utils.AssertEqual(t, schemeHTTPS, c.Protocol()) + c.Request().Header.Reset() + + c.Request().Header.Set(HeaderXForwardedProtocol, schemeHTTPS) + utils.AssertEqual(t, schemeHTTPS, c.Protocol()) + c.Request().Header.Reset() + + c.Request().Header.Set(HeaderXForwardedSsl, "on") + utils.AssertEqual(t, schemeHTTPS, c.Protocol()) + c.Request().Header.Reset() + + c.Request().Header.Set(HeaderXUrlScheme, schemeHTTPS) + utils.AssertEqual(t, schemeHTTPS, c.Protocol()) + c.Request().Header.Reset() + + utils.AssertEqual(t, schemeHTTP, c.Protocol()) +} + +// go test -run Test_Ctx_Protocol_UntrustedProxyRange +func Test_Ctx_Protocol_UntrustedProxyRange(t *testing.T) { + t.Parallel() + app := New(Config{EnableTrustedProxyCheck: true, TrustedProxies: []string{"1.1.1.1/30"}}) + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + + c.Request().Header.Set(HeaderXForwardedProto, schemeHTTPS) + utils.AssertEqual(t, schemeHTTP, c.Protocol()) + c.Request().Header.Reset() + + c.Request().Header.Set(HeaderXForwardedProtocol, schemeHTTPS) + utils.AssertEqual(t, schemeHTTP, c.Protocol()) + c.Request().Header.Reset() + + c.Request().Header.Set(HeaderXForwardedSsl, "on") + utils.AssertEqual(t, schemeHTTP, c.Protocol()) + c.Request().Header.Reset() + + c.Request().Header.Set(HeaderXUrlScheme, schemeHTTPS) + utils.AssertEqual(t, schemeHTTP, c.Protocol()) + c.Request().Header.Reset() + + utils.AssertEqual(t, schemeHTTP, c.Protocol()) +} + +// go test -run Test_Ctx_Protocol_UnTrustedProxy +func Test_Ctx_Protocol_UnTrustedProxy(t *testing.T) { + t.Parallel() + app := New(Config{EnableTrustedProxyCheck: true, TrustedProxies: []string{"0.8.0.1"}}) + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + + c.Request().Header.Set(HeaderXForwardedProto, schemeHTTPS) + utils.AssertEqual(t, schemeHTTP, c.Protocol()) + c.Request().Header.Reset() + + c.Request().Header.Set(HeaderXForwardedProtocol, schemeHTTPS) + utils.AssertEqual(t, schemeHTTP, c.Protocol()) + c.Request().Header.Reset() + + c.Request().Header.Set(HeaderXForwardedSsl, "on") + utils.AssertEqual(t, schemeHTTP, c.Protocol()) + c.Request().Header.Reset() + + c.Request().Header.Set(HeaderXUrlScheme, schemeHTTPS) + utils.AssertEqual(t, schemeHTTP, c.Protocol()) + c.Request().Header.Reset() + + utils.AssertEqual(t, schemeHTTP, c.Protocol()) +} + +// go test -run Test_Ctx_Query +func Test_Ctx_Query(t *testing.T) { + t.Parallel() + app := New() + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + c.Request().URI().SetQueryString("search=john&age=20&id=") + utils.AssertEqual(t, "john", c.Query("search")) + utils.AssertEqual(t, "20", c.Query("age")) + utils.AssertEqual(t, "default", c.Query("unknown", "default")) +} + +func Test_Ctx_QueryInt(t *testing.T) { + t.Parallel() + app := New() + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + c.Request().URI().SetQueryString("search=john&age=20&id=") + + utils.AssertEqual(t, 0, c.QueryInt("foo")) + utils.AssertEqual(t, 20, c.QueryInt("age", 12)) + utils.AssertEqual(t, 0, c.QueryInt("search")) + utils.AssertEqual(t, 1, c.QueryInt("search", 1)) + utils.AssertEqual(t, 0, c.QueryInt("id")) + utils.AssertEqual(t, 2, c.QueryInt("id", 2)) +} + +func Test_Ctx_QueryBool(t *testing.T) { + t.Parallel() + app := New() + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + c.Request().URI().SetQueryString("name=alex&want_pizza=false&id=") + + utils.AssertEqual(t, false, c.QueryBool("want_pizza")) + utils.AssertEqual(t, false, c.QueryBool("want_pizza", true)) + utils.AssertEqual(t, false, c.QueryBool("name")) + utils.AssertEqual(t, true, c.QueryBool("name", true)) + utils.AssertEqual(t, false, c.QueryBool("id")) + utils.AssertEqual(t, true, c.QueryBool("id", true)) +} + +func Test_Ctx_QueryFloat(t *testing.T) { + t.Parallel() + app := New() + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + c.Request().URI().SetQueryString("name=alex&amount=32.23&id=") + + utils.AssertEqual(t, 32.23, c.QueryFloat("amount")) + utils.AssertEqual(t, 32.23, c.QueryFloat("amount", 3.123)) + utils.AssertEqual(t, 87.123, c.QueryFloat("name", 87.123)) + utils.AssertEqual(t, float64(0), c.QueryFloat("name")) + utils.AssertEqual(t, 12.87, c.QueryFloat("id", 12.87)) + utils.AssertEqual(t, float64(0), c.QueryFloat("id")) +} + +// go test -run Test_Ctx_Range +func Test_Ctx_Range(t *testing.T) { + t.Parallel() + app := New() + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + + testRange := func(header string, ranges ...RangeSet) { + c.Request().Header.Set(HeaderRange, header) + result, err := c.Range(1000) + if len(ranges) == 0 { + utils.AssertEqual(t, true, err != nil) + } else { + utils.AssertEqual(t, "bytes", result.Type) + utils.AssertEqual(t, true, err == nil) + } + utils.AssertEqual(t, len(ranges), len(result.Ranges)) + for i := range ranges { + utils.AssertEqual(t, ranges[i], result.Ranges[i]) + } + } + + testRange("bytes=500") + testRange("bytes=") + testRange("bytes=500=") + testRange("bytes=500-300") + testRange("bytes=a-700", RangeSet{300, 999}) + testRange("bytes=500-b", RangeSet{500, 999}) + testRange("bytes=500-1000", RangeSet{500, 999}) + testRange("bytes=500-700", RangeSet{500, 700}) + testRange("bytes=0-0,2-1000", RangeSet{0, 0}, RangeSet{2, 999}) + testRange("bytes=0-99,450-549,-100", RangeSet{0, 99}, RangeSet{450, 549}, RangeSet{900, 999}) + testRange("bytes=500-700,601-999", RangeSet{500, 700}, RangeSet{601, 999}) +} + +// go test -v -run=^$ -bench=Benchmark_Ctx_Range -benchmem -count=4 +func Benchmark_Ctx_Range(b *testing.B) { + app := New() + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + + testCases := []struct { + str string + start int + end int + }{ + {"bytes=-700", 300, 999}, + {"bytes=500-", 500, 999}, + {"bytes=500-1000", 500, 999}, + {"bytes=0-700,800-1000", 0, 700}, + } + + for _, tc := range testCases { + b.Run(tc.str, func(b *testing.B) { + c.Request().Header.Set(HeaderRange, tc.str) + var ( + result Range + err error + ) + for n := 0; n < b.N; n++ { + result, err = c.Range(1000) + } + utils.AssertEqual(b, nil, err) + utils.AssertEqual(b, "bytes", result.Type) + utils.AssertEqual(b, tc.start, result.Ranges[0].Start) + utils.AssertEqual(b, tc.end, result.Ranges[0].End) + }) + } +} + +// go test -run Test_Ctx_Route +func Test_Ctx_Route(t *testing.T) { + t.Parallel() + app := New() + app.Get("/test", func(c *Ctx) error { + utils.AssertEqual(t, "/test", c.Route().Path) + return nil + }) + resp, err := app.Test(httptest.NewRequest(MethodGet, "/test", nil)) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, StatusOK, resp.StatusCode, "Status code") + + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + + utils.AssertEqual(t, "/", c.Route().Path) + utils.AssertEqual(t, MethodGet, c.Route().Method) + utils.AssertEqual(t, 0, len(c.Route().Handlers)) +} + +// go test -run Test_Ctx_RouteNormalized +func Test_Ctx_RouteNormalized(t *testing.T) { + t.Parallel() + app := New() + app.Get("/test", func(c *Ctx) error { + utils.AssertEqual(t, "/test", c.Route().Path) + return nil + }) + resp, err := app.Test(httptest.NewRequest(MethodGet, "//test", nil)) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, StatusNotFound, resp.StatusCode, "Status code") +} + +// go test -run Test_Ctx_SaveFile +func Test_Ctx_SaveFile(t *testing.T) { + // TODO We should clean this up + t.Parallel() + app := New() + + app.Post("/test", func(c *Ctx) error { + fh, err := c.FormFile("file") + utils.AssertEqual(t, nil, err) + + tempFile, err := os.CreateTemp(os.TempDir(), "test-") + utils.AssertEqual(t, nil, err) + + defer func(file *os.File) { + err := file.Close() + utils.AssertEqual(t, nil, err) + err = os.Remove(file.Name()) + utils.AssertEqual(t, nil, err) + }(tempFile) + err = c.SaveFile(fh, tempFile.Name()) + utils.AssertEqual(t, nil, err) + + bs, err := os.ReadFile(tempFile.Name()) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, "hello world", string(bs)) + return nil + }) + + body := &bytes.Buffer{} + writer := multipart.NewWriter(body) + + ioWriter, err := writer.CreateFormFile("file", "test") + utils.AssertEqual(t, nil, err) + + _, err = ioWriter.Write([]byte("hello world")) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, nil, writer.Close()) + + req := httptest.NewRequest(MethodPost, "/test", body) + req.Header.Set("Content-Type", writer.FormDataContentType()) + req.Header.Set("Content-Length", strconv.Itoa(len(body.Bytes()))) + + resp, err := app.Test(req) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, StatusOK, resp.StatusCode, "Status code") +} + +// go test -run Test_Ctx_SaveFileToStorage +func Test_Ctx_SaveFileToStorage(t *testing.T) { + t.Parallel() + app := New() + storage := memory.New() + + app.Post("/test", func(c *Ctx) error { + fh, err := c.FormFile("file") + utils.AssertEqual(t, nil, err) + + err = c.SaveFileToStorage(fh, "test", storage) + utils.AssertEqual(t, nil, err) + + file, err := storage.Get("test") + utils.AssertEqual(t, []byte("hello world"), file) + utils.AssertEqual(t, nil, err) + + err = storage.Delete("test") + utils.AssertEqual(t, nil, err) + + return nil + }) + + body := &bytes.Buffer{} + writer := multipart.NewWriter(body) + + ioWriter, err := writer.CreateFormFile("file", "test") + utils.AssertEqual(t, nil, err) + + _, err = ioWriter.Write([]byte("hello world")) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, nil, writer.Close()) + + req := httptest.NewRequest(MethodPost, "/test", body) + req.Header.Set("Content-Type", writer.FormDataContentType()) + req.Header.Set("Content-Length", strconv.Itoa(len(body.Bytes()))) + + resp, err := app.Test(req) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, StatusOK, resp.StatusCode, "Status code") +} + +// go test -run Test_Ctx_Secure +func Test_Ctx_Secure(t *testing.T) { + t.Parallel() + app := New() + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + // TODO Add TLS conn + utils.AssertEqual(t, false, c.Secure()) +} + +// go test -run Test_Ctx_Stale +func Test_Ctx_Stale(t *testing.T) { + t.Parallel() + app := New() + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + utils.AssertEqual(t, true, c.Stale()) +} + +// go test -run Test_Ctx_Subdomains +func Test_Ctx_Subdomains(t *testing.T) { + t.Parallel() + app := New() + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + c.Request().URI().SetHost("john.doe.is.awesome.google.com") + utils.AssertEqual(t, []string{"john", "doe"}, c.Subdomains(4)) + + c.Request().URI().SetHost("localhost:3000") + utils.AssertEqual(t, []string{"localhost:3000"}, c.Subdomains()) +} + +// go test -v -run=^$ -bench=Benchmark_Ctx_Subdomains -benchmem -count=4 +func Benchmark_Ctx_Subdomains(b *testing.B) { + app := New() + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + c.Request().SetRequestURI("http://john.doe.google.com") + var res []string + b.ReportAllocs() + b.ResetTimer() + for n := 0; n < b.N; n++ { + res = c.Subdomains() + } + utils.AssertEqual(b, []string{"john", "doe"}, res) +} + +// go test -run Test_Ctx_ClearCookie +func Test_Ctx_ClearCookie(t *testing.T) { + t.Parallel() + app := New() + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + c.Request().Header.Set(HeaderCookie, "john=doe") + c.ClearCookie("john") + utils.AssertEqual(t, true, strings.HasPrefix(string(c.Response().Header.Peek(HeaderSetCookie)), "john=; expires=")) + + c.Request().Header.Set(HeaderCookie, "test1=dummy") + c.Request().Header.Set(HeaderCookie, "test2=dummy") + c.ClearCookie() + utils.AssertEqual(t, true, strings.Contains(string(c.Response().Header.Peek(HeaderSetCookie)), "test1=; expires=")) + utils.AssertEqual(t, true, strings.Contains(string(c.Response().Header.Peek(HeaderSetCookie)), "test2=; expires=")) +} + +// go test -race -run Test_Ctx_Download +func Test_Ctx_Download(t *testing.T) { + t.Parallel() + app := New() + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + + utils.AssertEqual(t, nil, c.Download("ctx.go", "Awesome File!")) + + f, err := os.Open("./ctx.go") + utils.AssertEqual(t, nil, err) + defer func() { + utils.AssertEqual(t, nil, f.Close()) + }() + + expect, err := io.ReadAll(f) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, expect, c.Response().Body()) + utils.AssertEqual(t, `attachment; filename="Awesome+File%21"`, string(c.Response().Header.Peek(HeaderContentDisposition))) + + utils.AssertEqual(t, nil, c.Download("ctx.go")) + utils.AssertEqual(t, `attachment; filename="ctx.go"`, string(c.Response().Header.Peek(HeaderContentDisposition))) +} + +// go test -race -run Test_Ctx_SendFile +func Test_Ctx_SendFile(t *testing.T) { + t.Parallel() + app := New() + + // fetch file content + f, err := os.Open("./ctx.go") + utils.AssertEqual(t, nil, err) + defer func() { + utils.AssertEqual(t, nil, f.Close()) + }() + expectFileContent, err := io.ReadAll(f) + utils.AssertEqual(t, nil, err) + // fetch file info for the not modified test case + fI, err := os.Stat("./ctx.go") + utils.AssertEqual(t, nil, err) + + // simple test case + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + err = c.SendFile("ctx.go") + // check expectation + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, expectFileContent, c.Response().Body()) + utils.AssertEqual(t, StatusOK, c.Response().StatusCode()) + app.ReleaseCtx(c) + + // test with custom error code + c = app.AcquireCtx(&fasthttp.RequestCtx{}) + err = c.Status(StatusInternalServerError).SendFile("ctx.go") + // check expectation + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, expectFileContent, c.Response().Body()) + utils.AssertEqual(t, StatusInternalServerError, c.Response().StatusCode()) + app.ReleaseCtx(c) + + // test not modified + c = app.AcquireCtx(&fasthttp.RequestCtx{}) + c.Request().Header.Set(HeaderIfModifiedSince, fI.ModTime().Format(time.RFC1123)) + err = c.SendFile("ctx.go") + // check expectation + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, StatusNotModified, c.Response().StatusCode()) + utils.AssertEqual(t, []byte(nil), c.Response().Body()) + app.ReleaseCtx(c) +} + +// go test -race -run Test_Ctx_SendFile_404 +func Test_Ctx_SendFile_404(t *testing.T) { + t.Parallel() + app := New() + app.Get("/", func(c *Ctx) error { + err := c.SendFile(filepath.FromSlash("john_dow.go/")) + utils.AssertEqual(t, false, err == nil) + return err + }) + + resp, err := app.Test(httptest.NewRequest(MethodGet, "/", nil)) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, StatusNotFound, resp.StatusCode) +} + +// go test -race -run Test_Ctx_SendFile_Immutable +func Test_Ctx_SendFile_Immutable(t *testing.T) { + t.Parallel() + app := New() + var endpointsForTest []string + addEndpoint := func(file, endpoint string) { + endpointsForTest = append(endpointsForTest, endpoint) + app.Get(endpoint, func(c *Ctx) error { + if err := c.SendFile(file); err != nil { + utils.AssertEqual(t, nil, err) + return err + } + return c.SendStatus(200) + }) + } + + // relative paths + addEndpoint("./.github/index.html", "/relativeWithDot") + addEndpoint(filepath.FromSlash("./.github/index.html"), "/relativeOSWithDot") + addEndpoint(".github/index.html", "/relative") + addEndpoint(filepath.FromSlash(".github/index.html"), "/relativeOS") + + // absolute paths + if path, err := filepath.Abs(".github/index.html"); err != nil { + utils.AssertEqual(t, nil, err) + } else { + addEndpoint(path, "/absolute") + addEndpoint(filepath.FromSlash(path), "/absoluteOS") // os related + } + + for _, endpoint := range endpointsForTest { + t.Run(endpoint, func(t *testing.T) { + // 1st try + resp, err := app.Test(httptest.NewRequest(MethodGet, endpoint, nil)) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, StatusOK, resp.StatusCode) + // 2nd try + resp, err = app.Test(httptest.NewRequest(MethodGet, endpoint, nil)) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, StatusOK, resp.StatusCode) + }) + } +} + +// go test -race -run Test_Ctx_SendFile_RestoreOriginalURL +func Test_Ctx_SendFile_RestoreOriginalURL(t *testing.T) { + t.Parallel() + app := New() + app.Get("/", func(c *Ctx) error { + originalURL := utils.CopyString(c.OriginalURL()) + err := c.SendFile("ctx.go") + utils.AssertEqual(t, originalURL, c.OriginalURL()) + return err + }) + + _, err1 := app.Test(httptest.NewRequest(MethodGet, "/?test=true", nil)) + // second request required to confirm with zero allocation + _, err2 := app.Test(httptest.NewRequest(MethodGet, "/?test=true", nil)) + + utils.AssertEqual(t, nil, err1) + utils.AssertEqual(t, nil, err2) +} + +// go test -run Test_Ctx_JSON +func Test_Ctx_JSON(t *testing.T) { + t.Parallel() + app := New() + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + + utils.AssertEqual(t, true, c.JSON(complex(1, 1)) != nil) + + // Test without ctype + err := c.JSON(Map{ // map has no order + "Name": "Grame", + "Age": 20, + }) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, `{"Age":20,"Name":"Grame"}`, string(c.Response().Body())) + utils.AssertEqual(t, "application/json", string(c.Response().Header.Peek("content-type"))) + + // Test with ctype + err = c.JSON(Map{ // map has no order + "Name": "Grame", + "Age": 20, + }, "application/problem+json") + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, `{"Age":20,"Name":"Grame"}`, string(c.Response().Body())) + utils.AssertEqual(t, "application/problem+json", string(c.Response().Header.Peek("content-type"))) + + testEmpty := func(v interface{}, r string) { + err := c.JSON(v) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, r, string(c.Response().Body())) + } + + testEmpty(nil, "null") + testEmpty("", `""`) + testEmpty(0, "0") + testEmpty([]int{}, "[]") + + t.Run("custom json encoder", func(t *testing.T) { + t.Parallel() + + app := New(Config{ + JSONEncoder: func(v interface{}) ([]byte, error) { + return []byte(`["custom","json"]`), nil + }, + }) + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + + err := c.JSON(Map{ // map has no order + "Name": "Grame", + "Age": 20, + }) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, `["custom","json"]`, string(c.Response().Body())) + utils.AssertEqual(t, "application/json", string(c.Response().Header.Peek("content-type"))) + }) +} + +// go test -run=^$ -bench=Benchmark_Ctx_JSON -benchmem -count=4 +func Benchmark_Ctx_JSON(b *testing.B) { + app := New() + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + type SomeStruct struct { + Name string + Age uint8 + } + data := SomeStruct{ + Name: "Grame", + Age: 20, + } + var err error + b.ReportAllocs() + b.ResetTimer() + for n := 0; n < b.N; n++ { + err = c.JSON(data) + } + utils.AssertEqual(b, nil, err) + utils.AssertEqual(b, `{"Name":"Grame","Age":20}`, string(c.Response().Body())) +} + +// go test -run=^$ -bench=Benchmark_Ctx_JSON_Ctype -benchmem -count=4 +func Benchmark_Ctx_JSON_Ctype(b *testing.B) { + app := New() + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + type SomeStruct struct { + Name string + Age uint8 + } + data := SomeStruct{ + Name: "Grame", + Age: 20, + } + var err error + b.ReportAllocs() + b.ResetTimer() + for n := 0; n < b.N; n++ { + err = c.JSON(data, "application/problem+json") + } + utils.AssertEqual(b, nil, err) + utils.AssertEqual(b, `{"Name":"Grame","Age":20}`, string(c.Response().Body())) + utils.AssertEqual(b, "application/problem+json", string(c.Response().Header.Peek("content-type"))) +} + +// go test -run Test_Ctx_JSONP +func Test_Ctx_JSONP(t *testing.T) { + t.Parallel() + app := New() + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + + utils.AssertEqual(t, true, c.JSONP(complex(1, 1)) != nil) + + err := c.JSONP(Map{ + "Name": "Grame", + "Age": 20, + }) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, `callback({"Age":20,"Name":"Grame"});`, string(c.Response().Body())) + utils.AssertEqual(t, "text/javascript; charset=utf-8", string(c.Response().Header.Peek("content-type"))) + + err = c.JSONP(Map{ + "Name": "Grame", + "Age": 20, + }, "john") + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, `john({"Age":20,"Name":"Grame"});`, string(c.Response().Body())) + utils.AssertEqual(t, "text/javascript; charset=utf-8", string(c.Response().Header.Peek("content-type"))) + + t.Run("custom json encoder", func(t *testing.T) { + t.Parallel() + + app := New(Config{ + JSONEncoder: func(v interface{}) ([]byte, error) { + return []byte(`["custom","json"]`), nil + }, + }) + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + + err := c.JSONP(Map{ // map has no order + "Name": "Grame", + "Age": 20, + }) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, `callback(["custom","json"]);`, string(c.Response().Body())) + utils.AssertEqual(t, "text/javascript; charset=utf-8", string(c.Response().Header.Peek("content-type"))) + }) +} + +// go test -v -run=^$ -bench=Benchmark_Ctx_JSONP -benchmem -count=4 +func Benchmark_Ctx_JSONP(b *testing.B) { + app := New() + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + type SomeStruct struct { + Name string + Age uint8 + } + data := SomeStruct{ + Name: "Grame", + Age: 20, + } + callback := "emit" + var err error + b.ReportAllocs() + b.ResetTimer() + for n := 0; n < b.N; n++ { + err = c.JSONP(data, callback) + } + utils.AssertEqual(b, nil, err) + utils.AssertEqual(b, `emit({"Name":"Grame","Age":20});`, string(c.Response().Body())) +} + +// go test -run Test_Ctx_XML +func Test_Ctx_XML(t *testing.T) { + t.Parallel() + app := New() + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + + utils.AssertEqual(t, true, c.JSON(complex(1, 1)) != nil) + + type xmlResult struct { + XMLName xml.Name `xml:"Users"` + Names []string `xml:"Names"` + Ages []int `xml:"Ages"` + } + + err := c.XML(xmlResult{ + Names: []string{"Grame", "John"}, + Ages: []int{1, 12, 20}, + }) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, `GrameJohn11220`, string(c.Response().Body())) + utils.AssertEqual(t, "application/xml", string(c.Response().Header.Peek("content-type"))) + + testEmpty := func(v interface{}, r string) { + err := c.XML(v) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, r, string(c.Response().Body())) + } + + testEmpty(nil, "") + testEmpty("", ``) + testEmpty(0, "0") + testEmpty([]int{}, "") + + t.Run("custom xml encoder", func(t *testing.T) { + t.Parallel() + + app := New(Config{ + XMLEncoder: func(v interface{}) ([]byte, error) { + return []byte(`xml`), nil + }, + }) + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + + type xmlResult struct { + XMLName xml.Name `xml:"Users"` + Names []string `xml:"Names"` + Ages []int `xml:"Ages"` + } + + err := c.XML(xmlResult{ + Names: []string{"Grame", "John"}, + Ages: []int{1, 12, 20}, + }) + + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, `xml`, string(c.Response().Body())) + utils.AssertEqual(t, "application/xml", string(c.Response().Header.Peek("content-type"))) + }) +} + +// go test -run=^$ -bench=Benchmark_Ctx_XML -benchmem -count=4 +func Benchmark_Ctx_XML(b *testing.B) { + app := New() + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + type SomeStruct struct { + Name string `xml:"Name"` + Age uint8 `xml:"Age"` + } + data := SomeStruct{ + Name: "Grame", + Age: 20, + } + var err error + b.ReportAllocs() + b.ResetTimer() + for n := 0; n < b.N; n++ { + err = c.XML(data) + } + + utils.AssertEqual(b, nil, err) + utils.AssertEqual(b, `Grame20`, string(c.Response().Body())) +} + +// go test -run Test_Ctx_Links +func Test_Ctx_Links(t *testing.T) { + t.Parallel() + app := New() + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + + c.Links() + utils.AssertEqual(t, "", string(c.Response().Header.Peek(HeaderLink))) + + c.Links( + "http://api.example.com/users?page=2", "next", + "http://api.example.com/users?page=5", "last", + ) + utils.AssertEqual(t, `; rel="next",; rel="last"`, string(c.Response().Header.Peek(HeaderLink))) +} + +// go test -v -run=^$ -bench=Benchmark_Ctx_Links -benchmem -count=4 +func Benchmark_Ctx_Links(b *testing.B) { + app := New() + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + b.ReportAllocs() + b.ResetTimer() + for n := 0; n < b.N; n++ { + c.Links( + "http://api.example.com/users?page=2", "next", + "http://api.example.com/users?page=5", "last", + ) + } +} + +// go test -run Test_Ctx_Location +func Test_Ctx_Location(t *testing.T) { + t.Parallel() + app := New() + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + c.Location("http://example.com") + utils.AssertEqual(t, "http://example.com", string(c.Response().Header.Peek(HeaderLocation))) +} + +// go test -run Test_Ctx_Next +func Test_Ctx_Next(t *testing.T) { + t.Parallel() + app := New() + app.Use("/", func(c *Ctx) error { + return c.Next() + }) + app.Get("/test", func(c *Ctx) error { + c.Set("X-Next-Result", "Works") + return nil + }) + resp, err := app.Test(httptest.NewRequest(MethodGet, "http://example.com/test", nil)) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, StatusOK, resp.StatusCode, "Status code") + utils.AssertEqual(t, "Works", resp.Header.Get("X-Next-Result")) +} + +// go test -run Test_Ctx_Next_Error +func Test_Ctx_Next_Error(t *testing.T) { + t.Parallel() + app := New() + app.Use("/", func(c *Ctx) error { + c.Set("X-Next-Result", "Works") + return ErrNotFound + }) + + resp, err := app.Test(httptest.NewRequest(MethodGet, "http://example.com/test", nil)) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, StatusNotFound, resp.StatusCode, "Status code") + utils.AssertEqual(t, "Works", resp.Header.Get("X-Next-Result")) +} + +// go test -run Test_Ctx_Redirect +func Test_Ctx_Redirect(t *testing.T) { + t.Parallel() + app := New() + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + + err := c.Redirect("http://default.com") + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, 302, c.Response().StatusCode()) + utils.AssertEqual(t, "http://default.com", string(c.Response().Header.Peek(HeaderLocation))) + + err = c.Redirect("http://example.com", 301) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, 301, c.Response().StatusCode()) + utils.AssertEqual(t, "http://example.com", string(c.Response().Header.Peek(HeaderLocation))) +} + +// go test -run Test_Ctx_RedirectToRouteWithParams +func Test_Ctx_RedirectToRouteWithParams(t *testing.T) { + t.Parallel() + app := New() + app.Get("/user/:name", func(c *Ctx) error { + return c.JSON(c.Params("name")) + }).Name("user") + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + + err := c.RedirectToRoute("user", Map{ + "name": "fiber", + }) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, 302, c.Response().StatusCode()) + utils.AssertEqual(t, "/user/fiber", string(c.Response().Header.Peek(HeaderLocation))) +} + +// go test -run Test_Ctx_RedirectToRouteWithParams +func Test_Ctx_RedirectToRouteWithQueries(t *testing.T) { + t.Parallel() + app := New() + app.Get("/user/:name", func(c *Ctx) error { + return c.JSON(c.Params("name")) + }).Name("user") + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + + err := c.RedirectToRoute("user", Map{ + "name": "fiber", + "queries": map[string]string{"data[0][name]": "john", "data[0][age]": "10", "test": "doe"}, + }) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, 302, c.Response().StatusCode()) + // analysis of query parameters with url parsing, since a map pass is always randomly ordered + location, err := url.Parse(string(c.Response().Header.Peek(HeaderLocation))) + utils.AssertEqual(t, nil, err, "url.Parse(location)") + utils.AssertEqual(t, "/user/fiber", location.Path) + utils.AssertEqual(t, url.Values{"data[0][name]": []string{"john"}, "data[0][age]": []string{"10"}, "test": []string{"doe"}}, location.Query()) +} + +// go test -run Test_Ctx_RedirectToRouteWithOptionalParams +func Test_Ctx_RedirectToRouteWithOptionalParams(t *testing.T) { + t.Parallel() + app := New() + app.Get("/user/:name?", func(c *Ctx) error { + return c.JSON(c.Params("name")) + }).Name("user") + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + + err := c.RedirectToRoute("user", Map{ + "name": "fiber", + }) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, 302, c.Response().StatusCode()) + utils.AssertEqual(t, "/user/fiber", string(c.Response().Header.Peek(HeaderLocation))) +} + +// go test -run Test_Ctx_RedirectToRouteWithOptionalParamsWithoutValue +func Test_Ctx_RedirectToRouteWithOptionalParamsWithoutValue(t *testing.T) { + t.Parallel() + app := New() + app.Get("/user/:name?", func(c *Ctx) error { + return c.JSON(c.Params("name")) + }).Name("user") + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + + err := c.RedirectToRoute("user", Map{}) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, 302, c.Response().StatusCode()) + utils.AssertEqual(t, "/user/", string(c.Response().Header.Peek(HeaderLocation))) +} + +// go test -run Test_Ctx_RedirectToRouteWithGreedyParameters +func Test_Ctx_RedirectToRouteWithGreedyParameters(t *testing.T) { + t.Parallel() + app := New() + app.Get("/user/+", func(c *Ctx) error { + return c.JSON(c.Params("+")) + }).Name("user") + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + + err := c.RedirectToRoute("user", Map{ + "+": "test/routes", + }) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, 302, c.Response().StatusCode()) + utils.AssertEqual(t, "/user/test/routes", string(c.Response().Header.Peek(HeaderLocation))) +} + +// go test -run Test_Ctx_RedirectBack +func Test_Ctx_RedirectBack(t *testing.T) { + t.Parallel() + app := New() + app.Get("/", func(c *Ctx) error { + return c.JSON("Home") + }).Name("home") + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + err := c.RedirectBack("/") + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, 302, c.Response().StatusCode()) + utils.AssertEqual(t, "/", string(c.Response().Header.Peek(HeaderLocation))) +} + +// go test -run Test_Ctx_RedirectBackWithReferer +func Test_Ctx_RedirectBackWithReferer(t *testing.T) { + t.Parallel() + app := New() + app.Get("/", func(c *Ctx) error { + return c.JSON("Home") + }).Name("home") + app.Get("/back", func(c *Ctx) error { + return c.JSON("Back") + }).Name("back") + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + c.Request().Header.Set(HeaderReferer, "/back") + err := c.RedirectBack("/") + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, 302, c.Response().StatusCode()) + utils.AssertEqual(t, "/back", c.Get(HeaderReferer)) + utils.AssertEqual(t, "/back", string(c.Response().Header.Peek(HeaderLocation))) +} + +// go test -run Test_Ctx_Render +func Test_Ctx_Render(t *testing.T) { + t.Parallel() + app := New() + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + err := c.Render("./.github/testdata/index.tmpl", Map{ + "Title": "Hello, World!", + }) + utils.AssertEqual(t, nil, err) + + utils.AssertEqual(t, "

Hello, World!

", string(c.Response().Body())) + + err = c.Render("./.github/testdata/template-non-exists.html", nil) + utils.AssertEqual(t, false, err == nil) + + err = c.Render("./.github/testdata/template-invalid.html", nil) + utils.AssertEqual(t, false, err == nil) +} + +func Test_Ctx_RenderWithoutLocals(t *testing.T) { + t.Parallel() + app := New(Config{ + PassLocalsToViews: false, + }) + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + + c.Locals("Title", "Hello, World!") + + err := c.Render("./.github/testdata/index.tmpl", Map{}) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, "

", string(c.Response().Body())) +} + +func Test_Ctx_RenderWithLocals(t *testing.T) { + t.Parallel() + app := New(Config{ + PassLocalsToViews: true, + }) + + t.Run("EmptyBind", func(t *testing.T) { + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + + c.Locals("Title", "Hello, World!") + err := c.Render("./.github/testdata/index.tmpl", Map{}) + + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, "

Hello, World!

", string(c.Response().Body())) + }) + + t.Run("NilBind", func(t *testing.T) { + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + + c.Locals("Title", "Hello, World!") + err := c.Render("./.github/testdata/index.tmpl", nil) + + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, "

Hello, World!

", string(c.Response().Body())) + }) +} + +func Test_Ctx_RenderWithBind(t *testing.T) { + t.Parallel() + + app := New() + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + + err := c.Bind(Map{ + "Title": "Hello, World!", + }) + utils.AssertEqual(t, nil, err) + defer app.ReleaseCtx(c) + err = c.Render("./.github/testdata/index.tmpl", Map{}) + utils.AssertEqual(t, nil, err) + + buf := bytebufferpool.Get() + _, _ = buf.WriteString("overwrite") //nolint:errcheck // This will never fail + defer bytebufferpool.Put(buf) + + utils.AssertEqual(t, "

Hello, World!

", string(c.Response().Body())) +} + +func Test_Ctx_RenderWithOverwrittenBind(t *testing.T) { + t.Parallel() + app := New() + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + + err := c.Bind(Map{ + "Title": "Hello, World!", + }) + utils.AssertEqual(t, nil, err) + defer app.ReleaseCtx(c) + err = c.Render("./.github/testdata/index.tmpl", Map{ + "Title": "Hello from Fiber!", + }) + utils.AssertEqual(t, nil, err) + + buf := bytebufferpool.Get() + _, _ = buf.WriteString("overwrite") //nolint:errcheck // This will never fail + defer bytebufferpool.Put(buf) + + utils.AssertEqual(t, "

Hello from Fiber!

", string(c.Response().Body())) +} + +func Test_Ctx_RenderWithBindLocals(t *testing.T) { + t.Parallel() + app := New(Config{ + PassLocalsToViews: true, + }) + + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + + err := c.Bind(Map{ + "Title": "Hello, World!", + }) + utils.AssertEqual(t, nil, err) + + c.Locals("Summary", "Test") + defer app.ReleaseCtx(c) + + err = c.Render("./.github/testdata/template.tmpl", Map{}) + utils.AssertEqual(t, nil, err) + + utils.AssertEqual(t, "

Hello, World! Test

", string(c.Response().Body())) +} + +func Test_Ctx_RenderWithLocalsAndBinding(t *testing.T) { + t.Parallel() + engine := &testTemplateEngine{} + err := engine.Load() + utils.AssertEqual(t, nil, err) + app := New(Config{ + PassLocalsToViews: true, + Views: engine, + }) + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + + c.Locals("Title", "This is a test.") + defer app.ReleaseCtx(c) + + err = c.Render("index.tmpl", Map{ + "Title": "Hello, World!", + }) + utils.AssertEqual(t, nil, err) + + utils.AssertEqual(t, "

Hello, World!

", string(c.Response().Body())) +} + +func Benchmark_Ctx_RenderWithLocalsAndBinding(b *testing.B) { + engine := &testTemplateEngine{} + err := engine.Load() + utils.AssertEqual(b, nil, err) + utils.AssertEqual(b, nil, err) + app := New(Config{ + PassLocalsToViews: true, + Views: engine, + }) + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + + err = c.Bind(Map{ + "Title": "Hello, World!", + }) + utils.AssertEqual(b, nil, err) + c.Locals("Summary", "Test") + + defer app.ReleaseCtx(c) + + b.ReportAllocs() + b.ResetTimer() + + for n := 0; n < b.N; n++ { + err = c.Render("template.tmpl", Map{}) + } + utils.AssertEqual(b, nil, err) + + utils.AssertEqual(b, "

Hello, World! Test

", string(c.Response().Body())) +} + +func Benchmark_Ctx_RedirectToRoute(b *testing.B) { + app := New() + app.Get("/user/:name", func(c *Ctx) error { + return c.JSON(c.Params("name")) + }).Name("user") + + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + + b.ReportAllocs() + b.ResetTimer() + + var err error + for n := 0; n < b.N; n++ { + err = c.RedirectToRoute("user", Map{ + "name": "fiber", + }) + } + utils.AssertEqual(b, nil, err) + + utils.AssertEqual(b, 302, c.Response().StatusCode()) + utils.AssertEqual(b, "/user/fiber", string(c.Response().Header.Peek(HeaderLocation))) +} + +func Benchmark_Ctx_RedirectToRouteWithQueries(b *testing.B) { + app := New() + app.Get("/user/:name", func(c *Ctx) error { + return c.JSON(c.Params("name")) + }).Name("user") + + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + + b.ReportAllocs() + b.ResetTimer() + + var err error + for n := 0; n < b.N; n++ { + err = c.RedirectToRoute("user", Map{ + "name": "fiber", + "queries": map[string]string{"a": "a", "b": "b"}, + }) + } + utils.AssertEqual(b, nil, err) + + utils.AssertEqual(b, 302, c.Response().StatusCode()) + // analysis of query parameters with url parsing, since a map pass is always randomly ordered + location, err := url.Parse(string(c.Response().Header.Peek(HeaderLocation))) + utils.AssertEqual(b, nil, err, "url.Parse(location)") + utils.AssertEqual(b, "/user/fiber", location.Path) + utils.AssertEqual(b, url.Values{"a": []string{"a"}, "b": []string{"b"}}, location.Query()) +} + +func Benchmark_Ctx_RenderLocals(b *testing.B) { + engine := &testTemplateEngine{} + err := engine.Load() + utils.AssertEqual(b, nil, err) + app := New(Config{ + PassLocalsToViews: true, + }) + app.config.Views = engine + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + + c.Locals("Title", "Hello, World!") + + defer app.ReleaseCtx(c) + + b.ReportAllocs() + b.ResetTimer() + + for n := 0; n < b.N; n++ { + err = c.Render("index.tmpl", Map{}) + } + utils.AssertEqual(b, nil, err) + + utils.AssertEqual(b, "

Hello, World!

", string(c.Response().Body())) +} + +func Benchmark_Ctx_RenderBind(b *testing.B) { + engine := &testTemplateEngine{} + err := engine.Load() + utils.AssertEqual(b, nil, err) + app := New() + app.config.Views = engine + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + + err = c.Bind(Map{ + "Title": "Hello, World!", + }) + utils.AssertEqual(b, nil, err) + + defer app.ReleaseCtx(c) + + b.ReportAllocs() + b.ResetTimer() + + for n := 0; n < b.N; n++ { + err = c.Render("index.tmpl", Map{}) + } + utils.AssertEqual(b, nil, err) + + utils.AssertEqual(b, "

Hello, World!

", string(c.Response().Body())) +} + +// go test -run Test_Ctx_RestartRouting +func Test_Ctx_RestartRouting(t *testing.T) { + t.Parallel() + app := New() + calls := 0 + app.Get("/", func(c *Ctx) error { + calls++ + if calls < 3 { + return c.RestartRouting() + } + return nil + }) + resp, err := app.Test(httptest.NewRequest(MethodGet, "http://example.com/", nil)) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, StatusOK, resp.StatusCode, "Status code") + utils.AssertEqual(t, 3, calls, "Number of calls") +} + +// go test -run Test_Ctx_RestartRoutingWithChangedPath +func Test_Ctx_RestartRoutingWithChangedPath(t *testing.T) { + t.Parallel() + app := New() + var executedOldHandler, executedNewHandler bool + + app.Get("/old", func(c *Ctx) error { + c.Path("/new") + return c.RestartRouting() + }) + app.Get("/old", func(c *Ctx) error { + executedOldHandler = true + return nil + }) + app.Get("/new", func(c *Ctx) error { + executedNewHandler = true + return nil + }) + + resp, err := app.Test(httptest.NewRequest(MethodGet, "http://example.com/old", nil)) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, StatusOK, resp.StatusCode, "Status code") + utils.AssertEqual(t, false, executedOldHandler, "Executed old handler") + utils.AssertEqual(t, true, executedNewHandler, "Executed new handler") +} + +// go test -run Test_Ctx_RestartRoutingWithChangedPathAnd404 +func Test_Ctx_RestartRoutingWithChangedPathAndCatchAll(t *testing.T) { + t.Parallel() + app := New() + app.Get("/new", func(c *Ctx) error { + return nil + }) + app.Use(func(c *Ctx) error { + c.Path("/new") + // c.Next() would fail this test as a 404 is returned from the next handler + return c.RestartRouting() + }) + app.Use(func(c *Ctx) error { + return ErrNotFound + }) + + resp, err := app.Test(httptest.NewRequest(MethodGet, "http://example.com/old", nil)) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, StatusOK, resp.StatusCode, "Status code") +} + +type testTemplateEngine struct { + templates *template.Template + path string +} + +func (t *testTemplateEngine) Render(w io.Writer, name string, bind interface{}, layout ...string) error { + if len(layout) == 0 { + if err := t.templates.ExecuteTemplate(w, name, bind); err != nil { + return fmt.Errorf("failed to execute template without layout: %w", err) + } + return nil + } + if err := t.templates.ExecuteTemplate(w, name, bind); err != nil { + return fmt.Errorf("failed to execute template: %w", err) + } + if err := t.templates.ExecuteTemplate(w, layout[0], bind); err != nil { + return fmt.Errorf("failed to execute template with layout: %w", err) + } + return nil +} + +func (t *testTemplateEngine) Load() error { + if t.path == "" { + t.path = "testdata" + } + t.templates = template.Must(template.ParseGlob("./.github/" + t.path + "/*.tmpl")) + return nil +} + +// go test -run Test_Ctx_Render_Engine +func Test_Ctx_Render_Engine(t *testing.T) { + t.Parallel() + engine := &testTemplateEngine{} + utils.AssertEqual(t, nil, engine.Load()) + app := New() + app.config.Views = engine + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + err := c.Render("index.tmpl", Map{ + "Title": "Hello, World!", + }) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, "

Hello, World!

", string(c.Response().Body())) +} + +// go test -run Test_Ctx_Render_Engine_With_View_Layout +func Test_Ctx_Render_Engine_With_View_Layout(t *testing.T) { + t.Parallel() + engine := &testTemplateEngine{} + utils.AssertEqual(t, nil, engine.Load()) + app := New(Config{ViewsLayout: "main.tmpl"}) + app.config.Views = engine + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + err := c.Render("index.tmpl", Map{ + "Title": "Hello, World!", + }) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, "

Hello, World!

I'm main

", string(c.Response().Body())) +} + +// go test -v -run=^$ -bench=Benchmark_Ctx_Render_Engine -benchmem -count=4 +func Benchmark_Ctx_Render_Engine(b *testing.B) { + engine := &testTemplateEngine{} + err := engine.Load() + utils.AssertEqual(b, nil, err) + app := New() + app.config.Views = engine + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + b.ReportAllocs() + b.ResetTimer() + for n := 0; n < b.N; n++ { + err = c.Render("index.tmpl", Map{ + "Title": "Hello, World!", + }) + } + utils.AssertEqual(b, nil, err) + utils.AssertEqual(b, "

Hello, World!

", string(c.Response().Body())) +} + +// go test -v -run=^$ -bench=Benchmark_Ctx_Get_Location_From_Route -benchmem -count=4 +func Benchmark_Ctx_Get_Location_From_Route(b *testing.B) { + app := New() + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + app.Get("/user/:name", func(c *Ctx) error { + return c.SendString(c.Params("name")) + }).Name("User") + + var err error + var location string + for n := 0; n < b.N; n++ { + location, err = c.getLocationFromRoute(app.GetRoute("User"), Map{"name": "fiber"}) + } + utils.AssertEqual(b, "/user/fiber", location) + utils.AssertEqual(b, nil, err) +} + +// go test -run Test_Ctx_Get_Location_From_Route_name +func Test_Ctx_Get_Location_From_Route_name(t *testing.T) { + t.Parallel() + + t.Run("case insensitive", func(t *testing.T) { + t.Parallel() + app := New() + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + app.Get("/user/:name", func(c *Ctx) error { + return c.SendString(c.Params("name")) + }).Name("User") + + location, err := c.GetRouteURL("User", Map{"name": "fiber"}) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, "/user/fiber", location) + + location, err = c.GetRouteURL("User", Map{"Name": "fiber"}) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, "/user/fiber", location) + }) + + t.Run("case sensitive", func(t *testing.T) { + t.Parallel() + app := New(Config{CaseSensitive: true}) + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + app.Get("/user/:name", func(c *Ctx) error { + return c.SendString(c.Params("name")) + }).Name("User") + + location, err := c.GetRouteURL("User", Map{"name": "fiber"}) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, "/user/fiber", location) + + location, err = c.GetRouteURL("User", Map{"Name": "fiber"}) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, "/user/", location) + }) +} + +// go test -run Test_Ctx_Get_Location_From_Route_name_Optional_greedy +func Test_Ctx_Get_Location_From_Route_name_Optional_greedy(t *testing.T) { + t.Parallel() + app := New() + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + app.Get("/:phone/*/send/*", func(c *Ctx) error { + return c.SendString("Phone: " + c.Params("phone") + "\nFirst Param: " + c.Params("*1") + "\nSecond Param: " + c.Params("*2")) + }).Name("SendSms") + + location, err := c.GetRouteURL("SendSms", Map{ + "phone": "23456789", + "*1": "sms", + "*2": "test-msg", + }) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, "/23456789/sms/send/test-msg", location) +} + +// go test -run Test_Ctx_Get_Location_From_Route_name_Optional_greedy_one_param +func Test_Ctx_Get_Location_From_Route_name_Optional_greedy_one_param(t *testing.T) { + t.Parallel() + app := New() + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + app.Get("/:phone/*/send", func(c *Ctx) error { + return c.SendString("Phone: " + c.Params("phone") + "\nFirst Param: " + c.Params("*1")) + }).Name("SendSms") + + location, err := c.GetRouteURL("SendSms", Map{ + "phone": "23456789", + "*": "sms", + }) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, "/23456789/sms/send", location) +} + +type errorTemplateEngine struct{} + +func (errorTemplateEngine) Render(_ io.Writer, _ string, _ interface{}, _ ...string) error { + return errors.New("errorTemplateEngine") +} + +func (errorTemplateEngine) Load() error { return nil } + +// go test -run Test_Ctx_Render_Engine_Error +func Test_Ctx_Render_Engine_Error(t *testing.T) { + t.Parallel() + app := New() + app.config.Views = errorTemplateEngine{} + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + err := c.Render("index.tmpl", nil) + utils.AssertEqual(t, false, err == nil) +} + +// go test -run Test_Ctx_Render_Go_Template +func Test_Ctx_Render_Go_Template(t *testing.T) { + t.Parallel() + file, err := os.CreateTemp(os.TempDir(), "fiber") + utils.AssertEqual(t, nil, err) + defer func() { + err := os.Remove(file.Name()) + utils.AssertEqual(t, nil, err) + }() + + _, err = file.Write([]byte("template")) + utils.AssertEqual(t, nil, err) + + err = file.Close() + utils.AssertEqual(t, nil, err) + + app := New() + + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + + err = c.Render(file.Name(), nil) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, "template", string(c.Response().Body())) +} + +// go test -run Test_Ctx_Send +func Test_Ctx_Send(t *testing.T) { + t.Parallel() + app := New() + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + utils.AssertEqual(t, nil, c.Send([]byte("Hello, World"))) + utils.AssertEqual(t, nil, c.Send([]byte("Don't crash please"))) + utils.AssertEqual(t, nil, c.Send([]byte("1337"))) + utils.AssertEqual(t, "1337", string(c.Response().Body())) +} + +// go test -v -run=^$ -bench=Benchmark_Ctx_Send -benchmem -count=4 +func Benchmark_Ctx_Send(b *testing.B) { + app := New() + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + byt := []byte("Hello, World!") + b.ReportAllocs() + b.ResetTimer() + + var err error + for n := 0; n < b.N; n++ { + err = c.Send(byt) + } + utils.AssertEqual(b, nil, err) + utils.AssertEqual(b, "Hello, World!", string(c.Response().Body())) +} + +// go test -run Test_Ctx_SendStatus +func Test_Ctx_SendStatus(t *testing.T) { + t.Parallel() + app := New() + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + err := c.SendStatus(415) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, 415, c.Response().StatusCode()) + utils.AssertEqual(t, "Unsupported Media Type", string(c.Response().Body())) +} + +// go test -run Test_Ctx_SendString +func Test_Ctx_SendString(t *testing.T) { + t.Parallel() + app := New() + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + err := c.SendString("Don't crash please") + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, "Don't crash please", string(c.Response().Body())) +} + +// go test -run Test_Ctx_SendStream +func Test_Ctx_SendStream(t *testing.T) { + t.Parallel() + app := New() + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + + err := c.SendStream(bytes.NewReader([]byte("Don't crash please"))) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, "Don't crash please", string(c.Response().Body())) + + err = c.SendStream(bytes.NewReader([]byte("Don't crash please")), len([]byte("Don't crash please"))) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, "Don't crash please", string(c.Response().Body())) + + err = c.SendStream(bufio.NewReader(bytes.NewReader([]byte("Hello bufio")))) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, "Hello bufio", string(c.Response().Body())) +} + +// go test -run Test_Ctx_Set +func Test_Ctx_Set(t *testing.T) { + t.Parallel() + app := New() + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + c.Set("X-1", "1") + c.Set("X-2", "2") + c.Set("X-3", "3") + c.Set("X-3", "1337") + utils.AssertEqual(t, "1", string(c.Response().Header.Peek("x-1"))) + utils.AssertEqual(t, "2", string(c.Response().Header.Peek("x-2"))) + utils.AssertEqual(t, "1337", string(c.Response().Header.Peek("x-3"))) +} + +// go test -run Test_Ctx_Set_Splitter +func Test_Ctx_Set_Splitter(t *testing.T) { + t.Parallel() + app := New() + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + + c.Set("Location", "foo\r\nSet-Cookie:%20SESSIONID=MaliciousValue\r\n") + h := string(c.Response().Header.Peek("Location")) + utils.AssertEqual(t, false, strings.Contains(h, "\r\n"), h) + + c.Set("Location", "foo\nSet-Cookie:%20SESSIONID=MaliciousValue\n") + h = string(c.Response().Header.Peek("Location")) + utils.AssertEqual(t, false, strings.Contains(h, "\n"), h) +} + +// go test -v -run=^$ -bench=Benchmark_Ctx_Set -benchmem -count=4 +func Benchmark_Ctx_Set(b *testing.B) { + app := New() + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + val := "1431-15132-3423" + b.ReportAllocs() + b.ResetTimer() + for n := 0; n < b.N; n++ { + c.Set(HeaderXRequestID, val) + } +} + +// go test -run Test_Ctx_Status +func Test_Ctx_Status(t *testing.T) { + t.Parallel() + app := New() + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + c.Status(400) + utils.AssertEqual(t, 400, c.Response().StatusCode()) + err := c.Status(415).Send([]byte("Hello, World")) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, 415, c.Response().StatusCode()) + utils.AssertEqual(t, "Hello, World", string(c.Response().Body())) +} + +// go test -run Test_Ctx_Type +func Test_Ctx_Type(t *testing.T) { + t.Parallel() + app := New() + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + c.Type(".json") + utils.AssertEqual(t, "application/json", string(c.Response().Header.Peek("Content-Type"))) + + c.Type("json", "utf-8") + utils.AssertEqual(t, "application/json; charset=utf-8", string(c.Response().Header.Peek("Content-Type"))) + + c.Type(".html") + utils.AssertEqual(t, "text/html", string(c.Response().Header.Peek("Content-Type"))) + + c.Type("html", "utf-8") + utils.AssertEqual(t, "text/html; charset=utf-8", string(c.Response().Header.Peek("Content-Type"))) +} + +// go test -v -run=^$ -bench=Benchmark_Ctx_Type -benchmem -count=4 +func Benchmark_Ctx_Type(b *testing.B) { + app := New() + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + b.ReportAllocs() + b.ResetTimer() + for n := 0; n < b.N; n++ { + c.Type(".json") + c.Type("json") + } +} + +// go test -v -run=^$ -bench=Benchmark_Ctx_Type_Charset -benchmem -count=4 +func Benchmark_Ctx_Type_Charset(b *testing.B) { + app := New() + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + b.ReportAllocs() + b.ResetTimer() + for n := 0; n < b.N; n++ { + c.Type(".json", "utf-8") + c.Type("json", "utf-8") + } +} + +// go test -run Test_Ctx_Vary +func Test_Ctx_Vary(t *testing.T) { + t.Parallel() + app := New() + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + c.Vary("Origin") + c.Vary("User-Agent") + c.Vary("Accept-Encoding", "Accept") + utils.AssertEqual(t, "Origin, User-Agent, Accept-Encoding, Accept", string(c.Response().Header.Peek("Vary"))) +} + +// go test -v -run=^$ -bench=Benchmark_Ctx_Vary -benchmem -count=4 +func Benchmark_Ctx_Vary(b *testing.B) { + app := New() + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + b.ReportAllocs() + b.ResetTimer() + for n := 0; n < b.N; n++ { + c.Vary("Origin", "User-Agent") + } +} + +// go test -run Test_Ctx_Write +func Test_Ctx_Write(t *testing.T) { + t.Parallel() + app := New() + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + _, err := c.Write([]byte("Hello, ")) + utils.AssertEqual(t, nil, err) + _, err = c.Write([]byte("World!")) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, "Hello, World!", string(c.Response().Body())) +} + +// go test -v -run=^$ -bench=Benchmark_Ctx_Write -benchmem -count=4 +func Benchmark_Ctx_Write(b *testing.B) { + app := New() + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + byt := []byte("Hello, World!") + b.ReportAllocs() + b.ResetTimer() + + var err error + for n := 0; n < b.N; n++ { + _, err = c.Write(byt) + } + utils.AssertEqual(b, nil, err) +} + +// go test -run Test_Ctx_Writef +func Test_Ctx_Writef(t *testing.T) { + t.Parallel() + app := New() + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + world := "World!" + _, err := c.Writef("Hello, %s", world) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, "Hello, World!", string(c.Response().Body())) +} + +// go test -v -run=^$ -bench=Benchmark_Ctx_Writef -benchmem -count=4 +func Benchmark_Ctx_Writef(b *testing.B) { + app := New() + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + world := "World!" + b.ReportAllocs() + b.ResetTimer() + + var err error + for n := 0; n < b.N; n++ { + _, err = c.Writef("Hello, %s", world) + } + utils.AssertEqual(b, nil, err) +} + +// go test -run Test_Ctx_WriteString +func Test_Ctx_WriteString(t *testing.T) { + t.Parallel() + app := New() + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + _, err := c.WriteString("Hello, ") + utils.AssertEqual(t, nil, err) + _, err = c.WriteString("World!") + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, "Hello, World!", string(c.Response().Body())) +} + +// go test -run Test_Ctx_XHR +func Test_Ctx_XHR(t *testing.T) { + t.Parallel() + app := New() + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + c.Request().Header.Set(HeaderXRequestedWith, "XMLHttpRequest") + utils.AssertEqual(t, true, c.XHR()) +} + +// go test -run=^$ -bench=Benchmark_Ctx_XHR -benchmem -count=4 +func Benchmark_Ctx_XHR(b *testing.B) { + app := New() + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + c.Request().Header.Set(HeaderXRequestedWith, "XMLHttpRequest") + var equal bool + b.ReportAllocs() + b.ResetTimer() + for n := 0; n < b.N; n++ { + equal = c.XHR() + } + utils.AssertEqual(b, true, equal) +} + +// go test -v -run=^$ -bench=Benchmark_Ctx_SendString_B -benchmem -count=4 +func Benchmark_Ctx_SendString_B(b *testing.B) { + app := New() + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + body := "Hello, world!" + b.ReportAllocs() + b.ResetTimer() + + var err error + for n := 0; n < b.N; n++ { + err = c.SendString(body) + } + utils.AssertEqual(b, nil, err) + utils.AssertEqual(b, []byte("Hello, world!"), c.Response().Body()) +} + +// go test -run Test_Ctx_Queries -v +func Test_Ctx_Queries(t *testing.T) { + t.Parallel() + app := New() + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + + c.Request().SetBody([]byte(``)) + c.Request().Header.SetContentType("") + c.Request().URI().SetQueryString("id=1&name=tom&hobby=basketball,football&favouriteDrinks=milo,coke,pepsi&alloc=&no=1&field1=value1&field1=value2&field2=value3&list_a=1&list_a=2&list_a=3&list_b[]=1&list_b[]=2&list_b[]=3&list_c=1,2,3") + + queries := c.Queries() + utils.AssertEqual(t, "1", queries["id"]) + utils.AssertEqual(t, "tom", queries["name"]) + utils.AssertEqual(t, "basketball,football", queries["hobby"]) + utils.AssertEqual(t, "milo,coke,pepsi", queries["favouriteDrinks"]) + utils.AssertEqual(t, "", queries["alloc"]) + utils.AssertEqual(t, "1", queries["no"]) + utils.AssertEqual(t, "value2", queries["field1"]) + utils.AssertEqual(t, "value3", queries["field2"]) + utils.AssertEqual(t, "3", queries["list_a"]) + utils.AssertEqual(t, "3", queries["list_b[]"]) + utils.AssertEqual(t, "1,2,3", queries["list_c"]) + + c.Request().URI().SetQueryString("filters.author.name=John&filters.category.name=Technology&filters[customer][name]=Alice&filters[status]=pending") + + queries = c.Queries() + utils.AssertEqual(t, "John", queries["filters.author.name"]) + utils.AssertEqual(t, "Technology", queries["filters.category.name"]) + utils.AssertEqual(t, "Alice", queries["filters[customer][name]"]) + utils.AssertEqual(t, "pending", queries["filters[status]"]) + + c.Request().URI().SetQueryString("tags=apple,orange,banana&filters[tags]=apple,orange,banana&filters[category][name]=fruits&filters.tags=apple,orange,banana&filters.category.name=fruits") + + queries = c.Queries() + utils.AssertEqual(t, "apple,orange,banana", queries["tags"]) + utils.AssertEqual(t, "apple,orange,banana", queries["filters[tags]"]) + utils.AssertEqual(t, "fruits", queries["filters[category][name]"]) + utils.AssertEqual(t, "apple,orange,banana", queries["filters.tags"]) + utils.AssertEqual(t, "fruits", queries["filters.category.name"]) + + c.Request().URI().SetQueryString("filters[tags][0]=apple&filters[tags][1]=orange&filters[tags][2]=banana&filters[category][name]=fruits") + + queries = c.Queries() + utils.AssertEqual(t, "apple", queries["filters[tags][0]"]) + utils.AssertEqual(t, "orange", queries["filters[tags][1]"]) + utils.AssertEqual(t, "banana", queries["filters[tags][2]"]) + utils.AssertEqual(t, "fruits", queries["filters[category][name]"]) +} + +// go test -v -run=^$ -bench=Benchmark_Ctx_Queries -benchmem -count=4 +func Benchmark_Ctx_Queries(b *testing.B) { + app := New() + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + b.ReportAllocs() + b.ResetTimer() + c.Request().URI().SetQueryString("id=1&name=tom&hobby=basketball,football&favouriteDrinks=milo,coke,pepsi&alloc=&no=1") + + var queries map[string]string + for n := 0; n < b.N; n++ { + queries = c.Queries() + } + + utils.AssertEqual(b, "1", queries["id"]) + utils.AssertEqual(b, "tom", queries["name"]) + utils.AssertEqual(b, "basketball,football", queries["hobby"]) + utils.AssertEqual(b, "milo,coke,pepsi", queries["favouriteDrinks"]) + utils.AssertEqual(b, "", queries["alloc"]) + utils.AssertEqual(b, "1", queries["no"]) +} + +// go test -run Test_Ctx_QueryParser -v +func Test_Ctx_QueryParser(t *testing.T) { + t.Parallel() + app := New(Config{EnableSplittingOnParsers: true}) + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + type Query struct { + ID int + Name string + Hobby []string + } + c.Request().SetBody([]byte(``)) + c.Request().Header.SetContentType("") + c.Request().URI().SetQueryString("id=1&name=tom&hobby=basketball&hobby=football") + q := new(Query) + utils.AssertEqual(t, nil, c.QueryParser(q)) + utils.AssertEqual(t, 2, len(q.Hobby)) + + c.Request().URI().SetQueryString("id=1&name=tom&hobby=basketball,football") + q = new(Query) + utils.AssertEqual(t, nil, c.QueryParser(q)) + utils.AssertEqual(t, 2, len(q.Hobby)) + + c.Request().URI().SetQueryString("id=1&name=tom&hobby=scoccer&hobby=basketball,football") + q = new(Query) + utils.AssertEqual(t, nil, c.QueryParser(q)) + utils.AssertEqual(t, 3, len(q.Hobby)) + + empty := new(Query) + c.Request().URI().SetQueryString("") + utils.AssertEqual(t, nil, c.QueryParser(empty)) + utils.AssertEqual(t, 0, len(empty.Hobby)) + + c.Request().URI().SetQueryString("id=1&name[=tom") + q = new(Query) + utils.AssertEqual(t, "unmatched brackets", c.QueryParser(q).Error()) + + type Query2 struct { + Bool bool + ID int + Name string + Hobby string + FavouriteDrinks []string + Empty []string + Alloc []string + No []int64 + } + + c.Request().URI().SetQueryString("id=1&name=tom&hobby=basketball,football&favouriteDrinks=milo,coke,pepsi&alloc=&no=1") + q2 := new(Query2) + q2.Bool = true + q2.Name = "hello world 1" + utils.AssertEqual(t, nil, c.QueryParser(q2)) + utils.AssertEqual(t, "basketball,football", q2.Hobby) + utils.AssertEqual(t, true, q2.Bool) + utils.AssertEqual(t, "tom", q2.Name) // check value get overwritten + utils.AssertEqual(t, []string{"milo", "coke", "pepsi"}, q2.FavouriteDrinks) + var nilSlice []string + utils.AssertEqual(t, nilSlice, q2.Empty) + utils.AssertEqual(t, []string{""}, q2.Alloc) + utils.AssertEqual(t, []int64{1}, q2.No) + + type RequiredQuery struct { + Name string `query:"name,required"` + } + rq := new(RequiredQuery) + c.Request().URI().SetQueryString("") + utils.AssertEqual(t, "failed to decode: name is empty", c.QueryParser(rq).Error()) + + type ArrayQuery struct { + Data []string + } + aq := new(ArrayQuery) + c.Request().URI().SetQueryString("data[]=john&data[]=doe") + utils.AssertEqual(t, nil, c.QueryParser(aq)) + utils.AssertEqual(t, 2, len(aq.Data)) +} + +// go test -run Test_Ctx_QueryParserUsingTag -v +func Test_Ctx_QueryParserUsingTag(t *testing.T) { + t.Parallel() + app := New(Config{EnableSplittingOnParsers: true}) + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + type Query struct { + ID int `query:"id"` + Name string `query:"name"` + Hobby []string `query:"hobby"` + } + c.Request().SetBody([]byte(``)) + c.Request().Header.SetContentType("") + c.Request().URI().SetQueryString("id=1&name=tom&hobby=basketball&hobby=football") + q := new(Query) + utils.AssertEqual(t, nil, c.QueryParser(q)) + utils.AssertEqual(t, 2, len(q.Hobby)) + + c.Request().URI().SetQueryString("id=1&name=tom&hobby=basketball,football") + q = new(Query) + utils.AssertEqual(t, nil, c.QueryParser(q)) + utils.AssertEqual(t, 2, len(q.Hobby)) + + c.Request().URI().SetQueryString("id=1&name=tom&hobby=scoccer&hobby=basketball,football") + q = new(Query) + utils.AssertEqual(t, nil, c.QueryParser(q)) + utils.AssertEqual(t, 3, len(q.Hobby)) + + empty := new(Query) + c.Request().URI().SetQueryString("") + utils.AssertEqual(t, nil, c.QueryParser(empty)) + utils.AssertEqual(t, 0, len(empty.Hobby)) + + type Query2 struct { + Bool bool `query:"bool"` + ID int `query:"id"` + Name string `query:"name"` + Hobby string `query:"hobby"` + FavouriteDrinks []string `query:"favouriteDrinks"` + Empty []string `query:"empty"` + Alloc []string `query:"alloc"` + No []int64 `query:"no"` + } + + c.Request().URI().SetQueryString("id=1&name=tom&hobby=basketball,football&favouriteDrinks=milo,coke,pepsi&alloc=&no=1") + q2 := new(Query2) + q2.Bool = true + q2.Name = "hello world 2" + utils.AssertEqual(t, nil, c.QueryParser(q2)) + utils.AssertEqual(t, "basketball,football", q2.Hobby) + utils.AssertEqual(t, true, q2.Bool) + utils.AssertEqual(t, "tom", q2.Name) // check value get overwritten + utils.AssertEqual(t, []string{"milo", "coke", "pepsi"}, q2.FavouriteDrinks) + var nilSlice []string + utils.AssertEqual(t, nilSlice, q2.Empty) + utils.AssertEqual(t, []string{""}, q2.Alloc) + utils.AssertEqual(t, []int64{1}, q2.No) + + type RequiredQuery struct { + Name string `query:"name,required"` + } + rq := new(RequiredQuery) + c.Request().URI().SetQueryString("") + utils.AssertEqual(t, "failed to decode: name is empty", c.QueryParser(rq).Error()) + + type ArrayQuery struct { + Data []string + } + aq := new(ArrayQuery) + c.Request().URI().SetQueryString("data[]=john&data[]=doe") + utils.AssertEqual(t, nil, c.QueryParser(aq)) + utils.AssertEqual(t, 2, len(aq.Data)) +} + +// go test -run Test_Ctx_QueryParser -v +func Test_Ctx_QueryParser_WithoutSplitting(t *testing.T) { + t.Parallel() + app := New() + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + type Query struct { + ID int + Name string + Hobby []string + } + + c.Request().URI().SetQueryString("id=1&name=tom&hobby=basketball,football") + q := new(Query) + utils.AssertEqual(t, nil, c.QueryParser(q)) + utils.AssertEqual(t, 1, len(q.Hobby)) + + c.Request().URI().SetQueryString("id=1&name=tom&hobby=scoccer&hobby=basketball,football") + q = new(Query) + utils.AssertEqual(t, nil, c.QueryParser(q)) + utils.AssertEqual(t, 2, len(q.Hobby)) +} + +// go test -run Test_Ctx_QueryParser_WithSetParserDecoder -v +func Test_Ctx_QueryParser_WithSetParserDecoder(t *testing.T) { + type NonRFCTime time.Time + + nonRFCConverter := func(value string) reflect.Value { + if v, err := time.Parse("2006-01-02", value); err == nil { + return reflect.ValueOf(v) + } + return reflect.Value{} + } + + nonRFCTime := ParserType{ + Customtype: NonRFCTime{}, + Converter: nonRFCConverter, + } + + SetParserDecoder(ParserConfig{ + IgnoreUnknownKeys: true, + ParserType: []ParserType{nonRFCTime}, + ZeroEmpty: true, + SetAliasTag: "query", + }) + + app := New() + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + + type NonRFCTimeInput struct { + Date NonRFCTime `query:"date"` + Title string `query:"title"` + Body string `query:"body"` + } + + c.Request().SetBody([]byte(``)) + c.Request().Header.SetContentType("") + q := new(NonRFCTimeInput) + + c.Request().URI().SetQueryString("date=2021-04-10&title=CustomDateTest&Body=October") + utils.AssertEqual(t, nil, c.QueryParser(q)) + utils.AssertEqual(t, "CustomDateTest", q.Title) + date := fmt.Sprintf("%v", q.Date) + utils.AssertEqual(t, "{0 63753609600 }", date) + utils.AssertEqual(t, "October", q.Body) + + c.Request().URI().SetQueryString("date=2021-04-10&title&Body=October") + q = &NonRFCTimeInput{ + Title: "Existing title", + Body: "Existing Body", + } + utils.AssertEqual(t, nil, c.QueryParser(q)) + utils.AssertEqual(t, "", q.Title) +} + +// go test -run Test_Ctx_QueryParser_Schema -v +func Test_Ctx_QueryParser_Schema(t *testing.T) { + t.Parallel() + app := New() + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + type Query1 struct { + Name string `query:"name,required"` + Nested struct { + Age int `query:"age"` + } `query:"nested,required"` + } + c.Request().SetBody([]byte(``)) + c.Request().Header.SetContentType("") + c.Request().URI().SetQueryString("name=tom&nested.age=10") + q := new(Query1) + utils.AssertEqual(t, nil, c.QueryParser(q)) + + c.Request().URI().SetQueryString("namex=tom&nested.age=10") + q = new(Query1) + utils.AssertEqual(t, "failed to decode: name is empty", c.QueryParser(q).Error()) + + c.Request().URI().SetQueryString("name=tom&nested.agex=10") + q = new(Query1) + utils.AssertEqual(t, nil, c.QueryParser(q)) + + c.Request().URI().SetQueryString("name=tom&test.age=10") + q = new(Query1) + utils.AssertEqual(t, "failed to decode: nested is empty", c.QueryParser(q).Error()) + + type Query2 struct { + Name string `query:"name"` + Nested struct { + Age int `query:"age,required"` + } `query:"nested"` + } + c.Request().URI().SetQueryString("name=tom&nested.age=10") + q2 := new(Query2) + utils.AssertEqual(t, nil, c.QueryParser(q2)) + + c.Request().URI().SetQueryString("nested.age=10") + q2 = new(Query2) + utils.AssertEqual(t, nil, c.QueryParser(q2)) + + c.Request().URI().SetQueryString("nested.agex=10") + q2 = new(Query2) + utils.AssertEqual(t, "failed to decode: nested.age is empty", c.QueryParser(q2).Error()) + + c.Request().URI().SetQueryString("nested.agex=10") + q2 = new(Query2) + utils.AssertEqual(t, "failed to decode: nested.age is empty", c.QueryParser(q2).Error()) + + type Node struct { + Value int `query:"val,required"` + Next *Node `query:"next,required"` + } + c.Request().URI().SetQueryString("val=1&next.val=3") + n := new(Node) + utils.AssertEqual(t, nil, c.QueryParser(n)) + utils.AssertEqual(t, 1, n.Value) + utils.AssertEqual(t, 3, n.Next.Value) + + c.Request().URI().SetQueryString("next.val=2") + n = new(Node) + utils.AssertEqual(t, "failed to decode: val is empty", c.QueryParser(n).Error()) + + c.Request().URI().SetQueryString("val=3&next.value=2") + n = new(Node) + n.Next = new(Node) + utils.AssertEqual(t, nil, c.QueryParser(n)) + utils.AssertEqual(t, 3, n.Value) + utils.AssertEqual(t, 0, n.Next.Value) + + type Person struct { + Name string `query:"name"` + Age int `query:"age"` + } + + type CollectionQuery struct { + Data []Person `query:"data"` + } + + c.Request().URI().SetQueryString("data[0][name]=john&data[0][age]=10&data[1][name]=doe&data[1][age]=12") + cq := new(CollectionQuery) + utils.AssertEqual(t, nil, c.QueryParser(cq)) + utils.AssertEqual(t, 2, len(cq.Data)) + utils.AssertEqual(t, "john", cq.Data[0].Name) + utils.AssertEqual(t, 10, cq.Data[0].Age) + utils.AssertEqual(t, "doe", cq.Data[1].Name) + utils.AssertEqual(t, 12, cq.Data[1].Age) + + c.Request().URI().SetQueryString("data[0][name]=john&data[0][age]=10&data[1]name]=doe&data[1][age]=12") + cq = new(CollectionQuery) + utils.AssertEqual(t, "unmatched brackets", c.QueryParser(cq).Error()) + + c.Request().URI().SetQueryString("data.0.name=john&data.0.age=10&data.1.name=doe&data.1.age=12") + cq = new(CollectionQuery) + utils.AssertEqual(t, nil, c.QueryParser(cq)) + utils.AssertEqual(t, 2, len(cq.Data)) + utils.AssertEqual(t, "john", cq.Data[0].Name) + utils.AssertEqual(t, 10, cq.Data[0].Age) + utils.AssertEqual(t, "doe", cq.Data[1].Name) + utils.AssertEqual(t, 12, cq.Data[1].Age) +} + +// go test -run Test_Ctx_ReqHeaderParser -v +func Test_Ctx_ReqHeaderParser(t *testing.T) { + t.Parallel() + app := New(Config{EnableSplittingOnParsers: true}) + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + type Header struct { + ID int + Name string + Hobby []string + } + c.Request().SetBody([]byte(``)) + c.Request().Header.SetContentType("") + + c.Request().Header.Add("id", "1") + c.Request().Header.Add("Name", "John Doe") + c.Request().Header.Add("Hobby", "golang,fiber") + q := new(Header) + utils.AssertEqual(t, nil, c.ReqHeaderParser(q)) + utils.AssertEqual(t, 2, len(q.Hobby)) + + c.Request().Header.Del("hobby") + c.Request().Header.Add("Hobby", "golang,fiber,go") + q = new(Header) + utils.AssertEqual(t, nil, c.ReqHeaderParser(q)) + utils.AssertEqual(t, 3, len(q.Hobby)) + + empty := new(Header) + c.Request().Header.Del("hobby") + utils.AssertEqual(t, nil, c.QueryParser(empty)) + utils.AssertEqual(t, 0, len(empty.Hobby)) + + type Header2 struct { + Bool bool + ID int + Name string + Hobby string + FavouriteDrinks []string + Empty []string + Alloc []string + No []int64 + } + + c.Request().Header.Add("id", "2") + c.Request().Header.Add("Name", "Jane Doe") + c.Request().Header.Del("hobby") + c.Request().Header.Add("Hobby", "go,fiber") + c.Request().Header.Add("favouriteDrinks", "milo,coke,pepsi") + c.Request().Header.Add("alloc", "") + c.Request().Header.Add("no", "1") + + h2 := new(Header2) + h2.Bool = true + h2.Name = "hello world 3" + utils.AssertEqual(t, nil, c.ReqHeaderParser(h2)) + utils.AssertEqual(t, "go,fiber", h2.Hobby) + utils.AssertEqual(t, true, h2.Bool) + utils.AssertEqual(t, "Jane Doe", h2.Name) // check value get overwritten + utils.AssertEqual(t, []string{"milo", "coke", "pepsi"}, h2.FavouriteDrinks) + var nilSlice []string + utils.AssertEqual(t, nilSlice, h2.Empty) + utils.AssertEqual(t, []string{""}, h2.Alloc) + utils.AssertEqual(t, []int64{1}, h2.No) + + type RequiredHeader struct { + Name string `reqHeader:"name,required"` + } + rh := new(RequiredHeader) + c.Request().Header.Del("name") + utils.AssertEqual(t, "failed to decode: name is empty", c.ReqHeaderParser(rh).Error()) +} + +// go test -run Test_Ctx_ReqHeaderParserUsingTag -v +func Test_Ctx_ReqHeaderParserUsingTag(t *testing.T) { + t.Parallel() + app := New(Config{EnableSplittingOnParsers: true}) + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + type Header struct { + ID int `reqHeader:"id"` + Name string `reqHeader:"name"` + Hobby []string `reqHeader:"hobby"` + Address []string `reqHeader:"x-secure-address"` + } + c.Request().SetBody([]byte(``)) + c.Request().Header.SetContentType("") + + c.Request().Header.Add("id", "1") + c.Request().Header.Add("Name", "John Doe") + c.Request().Header.Add("Hobby", "golang,fiber") + c.Request().Header.Add("x-secure-address", "1st,2st") + q := new(Header) + utils.AssertEqual(t, nil, c.ReqHeaderParser(q)) + utils.AssertEqual(t, 2, len(q.Hobby)) + utils.AssertEqual(t, 2, len(q.Address)) + + c.Request().Header.Del("hobby") + c.Request().Header.Add("Hobby", "golang,fiber,go") + q = new(Header) + utils.AssertEqual(t, nil, c.ReqHeaderParser(q)) + utils.AssertEqual(t, 3, len(q.Hobby)) + + empty := new(Header) + c.Request().Header.Del("hobby") + utils.AssertEqual(t, nil, c.QueryParser(empty)) + utils.AssertEqual(t, 0, len(empty.Hobby)) + + type Header2 struct { + Bool bool `reqHeader:"bool"` + ID int `reqHeader:"id"` + Name string `reqHeader:"name"` + Hobby string `reqHeader:"hobby"` + FavouriteDrinks []string `reqHeader:"favouriteDrinks"` + Empty []string `reqHeader:"empty"` + Alloc []string `reqHeader:"alloc"` + No []int64 `reqHeader:"no"` + } + + c.Request().Header.Add("id", "2") + c.Request().Header.Add("Name", "Jane Doe") + c.Request().Header.Del("hobby") + c.Request().Header.Add("Hobby", "go,fiber") + c.Request().Header.Add("favouriteDrinks", "milo,coke,pepsi") + c.Request().Header.Add("alloc", "") + c.Request().Header.Add("no", "1") + + h2 := new(Header2) + h2.Bool = true + h2.Name = "hello world 4" + utils.AssertEqual(t, nil, c.ReqHeaderParser(h2)) + utils.AssertEqual(t, "go,fiber", h2.Hobby) + utils.AssertEqual(t, true, h2.Bool) + utils.AssertEqual(t, "Jane Doe", h2.Name) // check value get overwritten + utils.AssertEqual(t, []string{"milo", "coke", "pepsi"}, h2.FavouriteDrinks) + var nilSlice []string + utils.AssertEqual(t, nilSlice, h2.Empty) + utils.AssertEqual(t, []string{""}, h2.Alloc) + utils.AssertEqual(t, []int64{1}, h2.No) + + type RequiredHeader struct { + Name string `reqHeader:"name,required"` + } + rh := new(RequiredHeader) + c.Request().Header.Del("name") + utils.AssertEqual(t, "failed to decode: name is empty", c.ReqHeaderParser(rh).Error()) +} + +// go test -run Test_Ctx_ReqHeaderParser -v +func Test_Ctx_ReqHeaderParser_WithoutSplitting(t *testing.T) { + t.Parallel() + app := New() + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + type Header struct { + ID int + Name string + Hobby []string + } + c.Request().SetBody([]byte(``)) + c.Request().Header.SetContentType("") + + c.Request().Header.Add("id", "1") + c.Request().Header.Add("Name", "John Doe") + c.Request().Header.Add("Hobby", "golang,fiber") + q := new(Header) + utils.AssertEqual(t, nil, c.ReqHeaderParser(q)) + utils.AssertEqual(t, 1, len(q.Hobby)) + + c.Request().Header.Del("hobby") + c.Request().Header.Add("Hobby", "golang,fiber,go") + q = new(Header) + utils.AssertEqual(t, nil, c.ReqHeaderParser(q)) + utils.AssertEqual(t, 1, len(q.Hobby)) +} + +// go test -run Test_Ctx_ReqHeaderParser_WithSetParserDecoder -v +func Test_Ctx_ReqHeaderParser_WithSetParserDecoder(t *testing.T) { + type NonRFCTime time.Time + + nonRFCConverter := func(value string) reflect.Value { + if v, err := time.Parse("2006-01-02", value); err == nil { + return reflect.ValueOf(v) + } + return reflect.Value{} + } + + nonRFCTime := ParserType{ + Customtype: NonRFCTime{}, + Converter: nonRFCConverter, + } + + SetParserDecoder(ParserConfig{ + IgnoreUnknownKeys: true, + ParserType: []ParserType{nonRFCTime}, + ZeroEmpty: true, + SetAliasTag: "req", + }) + + app := New() + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + + type NonRFCTimeInput struct { + Date NonRFCTime `req:"date"` + Title string `req:"title"` + Body string `req:"body"` + } + + c.Request().SetBody([]byte(``)) + c.Request().Header.SetContentType("") + r := new(NonRFCTimeInput) + + c.Request().Header.Add("Date", "2021-04-10") + c.Request().Header.Add("Title", "CustomDateTest") + c.Request().Header.Add("Body", "October") + + utils.AssertEqual(t, nil, c.ReqHeaderParser(r)) + utils.AssertEqual(t, "CustomDateTest", r.Title) + date := fmt.Sprintf("%v", r.Date) + utils.AssertEqual(t, "{0 63753609600 }", date) + utils.AssertEqual(t, "October", r.Body) + + c.Request().Header.Add("Title", "") + r = &NonRFCTimeInput{ + Title: "Existing title", + Body: "Existing Body", + } + utils.AssertEqual(t, nil, c.ReqHeaderParser(r)) + utils.AssertEqual(t, "", r.Title) +} + +// go test -run Test_Ctx_ReqHeaderParser_Schema -v +func Test_Ctx_ReqHeaderParser_Schema(t *testing.T) { + t.Parallel() + app := New() + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + type Header1 struct { + Name string `reqHeader:"Name,required"` + Nested struct { + Age int `reqHeader:"Age"` + } `reqHeader:"Nested,required"` + } + c.Request().SetBody([]byte(``)) + c.Request().Header.SetContentType("") + + c.Request().Header.Add("Name", "tom") + c.Request().Header.Add("Nested.Age", "10") + q := new(Header1) + utils.AssertEqual(t, nil, c.ReqHeaderParser(q)) + + c.Request().Header.Del("Name") + q = new(Header1) + utils.AssertEqual(t, "failed to decode: Name is empty", c.ReqHeaderParser(q).Error()) + + c.Request().Header.Add("Name", "tom") + c.Request().Header.Del("Nested.Age") + c.Request().Header.Add("Nested.Agex", "10") + q = new(Header1) + utils.AssertEqual(t, nil, c.ReqHeaderParser(q)) + + c.Request().Header.Del("Nested.Agex") + q = new(Header1) + utils.AssertEqual(t, "failed to decode: Nested is empty", c.ReqHeaderParser(q).Error()) + + c.Request().Header.Del("Nested.Agex") + c.Request().Header.Del("Name") + + type Header2 struct { + Name string `reqHeader:"Name"` + Nested struct { + Age int `reqHeader:"age,required"` + } `reqHeader:"Nested"` + } + + c.Request().Header.Add("Name", "tom") + c.Request().Header.Add("Nested.Age", "10") + + h2 := new(Header2) + utils.AssertEqual(t, nil, c.ReqHeaderParser(h2)) + + c.Request().Header.Del("Name") + h2 = new(Header2) + utils.AssertEqual(t, nil, c.ReqHeaderParser(h2)) + + c.Request().Header.Del("Name") + c.Request().Header.Del("Nested.Age") + c.Request().Header.Add("Nested.Agex", "10") + h2 = new(Header2) + utils.AssertEqual(t, "failed to decode: Nested.age is empty", c.ReqHeaderParser(h2).Error()) + + type Node struct { + Value int `reqHeader:"Val,required"` + Next *Node `reqHeader:"Next,required"` + } + c.Request().Header.Add("Val", "1") + c.Request().Header.Add("Next.Val", "3") + n := new(Node) + utils.AssertEqual(t, nil, c.ReqHeaderParser(n)) + utils.AssertEqual(t, 1, n.Value) + utils.AssertEqual(t, 3, n.Next.Value) + + c.Request().Header.Del("Val") + n = new(Node) + utils.AssertEqual(t, "failed to decode: Val is empty", c.ReqHeaderParser(n).Error()) + + c.Request().Header.Add("Val", "3") + c.Request().Header.Del("Next.Val") + c.Request().Header.Add("Next.Value", "2") + n = new(Node) + n.Next = new(Node) + utils.AssertEqual(t, nil, c.ReqHeaderParser(n)) + utils.AssertEqual(t, 3, n.Value) + utils.AssertEqual(t, 0, n.Next.Value) +} + +// go test -run Test_Ctx_EqualFieldTypeOfRequestQuery +func Test_Ctx_EqualFieldTypeOfRequestQuery(t *testing.T) { + t.Parallel() + var out int + utils.AssertEqual(t, false, equalFieldType(&out, reflect.Int, "key", queryTag)) + + var dummy struct{ f string } + utils.AssertEqual(t, false, equalFieldType(&dummy, reflect.String, "key", queryTag)) + + var dummy2 struct{ f string } + utils.AssertEqual(t, false, equalFieldType(&dummy2, reflect.String, "f", queryTag)) + + var user struct { + Name string + Address string `query:"address"` + Age int `query:"AGE"` + } + utils.AssertEqual(t, true, equalFieldType(&user, reflect.String, "name", queryTag)) + utils.AssertEqual(t, true, equalFieldType(&user, reflect.String, "Name", queryTag)) + utils.AssertEqual(t, true, equalFieldType(&user, reflect.String, "address", queryTag)) + utils.AssertEqual(t, true, equalFieldType(&user, reflect.String, "Address", queryTag)) + utils.AssertEqual(t, true, equalFieldType(&user, reflect.Int, "AGE", queryTag)) + utils.AssertEqual(t, true, equalFieldType(&user, reflect.Int, "age", queryTag)) +} + +// go test -run Test_Ctx_EqualFieldTypeOfRequestHeader +func Test_Ctx_EqualFieldTypeOfRequestHeader(t *testing.T) { + t.Parallel() + var out int + utils.AssertEqual(t, false, equalFieldType(&out, reflect.Int, "key", reqHeaderTag)) + + var dummy struct{ f string } + utils.AssertEqual(t, false, equalFieldType(&dummy, reflect.String, "key", reqHeaderTag)) + + var dummy2 struct{ f string } + utils.AssertEqual(t, false, equalFieldType(&dummy2, reflect.String, "f", reqHeaderTag)) + + var user struct { + Name string + Address string `reqHeader:"address"` + Age int `reqHeader:"AGE"` + } + utils.AssertEqual(t, true, equalFieldType(&user, reflect.String, "name", reqHeaderTag)) + utils.AssertEqual(t, true, equalFieldType(&user, reflect.String, "Name", reqHeaderTag)) + utils.AssertEqual(t, true, equalFieldType(&user, reflect.String, "address", reqHeaderTag)) + utils.AssertEqual(t, true, equalFieldType(&user, reflect.String, "Address", reqHeaderTag)) + utils.AssertEqual(t, true, equalFieldType(&user, reflect.Int, "AGE", reqHeaderTag)) + utils.AssertEqual(t, true, equalFieldType(&user, reflect.Int, "age", reqHeaderTag)) +} + +// go test -run Test_Ctx_EqualFieldTypeOfRequestBody +func Test_Ctx_EqualFieldTypeOfRequestBody(t *testing.T) { + t.Parallel() + var out int + utils.AssertEqual(t, false, equalFieldType(&out, reflect.Int, "key", bodyTag)) + + var dummy struct{ f string } + utils.AssertEqual(t, false, equalFieldType(&dummy, reflect.String, "key", bodyTag)) + + var dummy2 struct{ f string } + utils.AssertEqual(t, false, equalFieldType(&dummy2, reflect.String, "f", bodyTag)) + + var user struct { + Name string + Address string `form:"address"` + Age int `form:"AGE"` + } + utils.AssertEqual(t, true, equalFieldType(&user, reflect.String, "name", bodyTag)) + utils.AssertEqual(t, true, equalFieldType(&user, reflect.String, "Name", bodyTag)) + utils.AssertEqual(t, true, equalFieldType(&user, reflect.String, "address", bodyTag)) + utils.AssertEqual(t, true, equalFieldType(&user, reflect.String, "Address", bodyTag)) + utils.AssertEqual(t, true, equalFieldType(&user, reflect.Int, "AGE", bodyTag)) + utils.AssertEqual(t, true, equalFieldType(&user, reflect.Int, "age", bodyTag)) +} + +// go test -run Test_Ctx_EqualFieldTypeOfRequestParams +func Test_Ctx_EqualFieldTypeOfRequestParams(t *testing.T) { + t.Parallel() + var out int + utils.AssertEqual(t, false, equalFieldType(&out, reflect.Int, "key", paramsTag)) + + var dummy struct{ f string } + utils.AssertEqual(t, false, equalFieldType(&dummy, reflect.String, "key", paramsTag)) + + var dummy2 struct{ f string } + utils.AssertEqual(t, false, equalFieldType(&dummy2, reflect.String, "f", paramsTag)) + + var user struct { + Name string + Address string `params:"address"` + Age int `params:"AGE"` + } + utils.AssertEqual(t, true, equalFieldType(&user, reflect.String, "name", paramsTag)) + utils.AssertEqual(t, true, equalFieldType(&user, reflect.String, "Name", paramsTag)) + utils.AssertEqual(t, true, equalFieldType(&user, reflect.String, "address", paramsTag)) + utils.AssertEqual(t, true, equalFieldType(&user, reflect.String, "Address", paramsTag)) + utils.AssertEqual(t, true, equalFieldType(&user, reflect.Int, "AGE", paramsTag)) + utils.AssertEqual(t, true, equalFieldType(&user, reflect.Int, "age", paramsTag)) +} + +// go test -v -run=^$ -bench=Benchmark_Ctx_QueryParser -benchmem -count=4 +func Benchmark_Ctx_QueryParser(b *testing.B) { + app := New() + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + type Query struct { + ID int + Name string + Hobby []string + } + c.Request().SetBody([]byte(``)) + c.Request().Header.SetContentType("") + c.Request().URI().SetQueryString("id=1&name=tom&hobby=basketball&hobby=football") + q := new(Query) + b.ReportAllocs() + b.ResetTimer() + + var err error + for n := 0; n < b.N; n++ { + err = c.QueryParser(q) + } + utils.AssertEqual(b, nil, err) + utils.AssertEqual(b, nil, c.QueryParser(q)) +} + +// go test -v -run=^$ -bench=Benchmark_Ctx_parseQuery -benchmem -count=4 +func Benchmark_Ctx_parseQuery(b *testing.B) { + app := New() + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + type Person struct { + Name string `query:"name"` + Age int `query:"age"` + } + + type CollectionQuery struct { + Data []Person `query:"data"` + } + + c.Request().SetBody([]byte(``)) + c.Request().Header.SetContentType("") + c.Request().URI().SetQueryString("data[0][name]=john&data[0][age]=10") + cq := new(CollectionQuery) + + b.ReportAllocs() + b.ResetTimer() + + var err error + for n := 0; n < b.N; n++ { + err = c.QueryParser(cq) + } + + utils.AssertEqual(b, nil, err) + utils.AssertEqual(b, nil, c.QueryParser(cq)) +} + +// go test -v -run=^$ -bench=Benchmark_Ctx_QueryParser_Comma -benchmem -count=4 +func Benchmark_Ctx_QueryParser_Comma(b *testing.B) { + app := New(Config{EnableSplittingOnParsers: true}) + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + type Query struct { + ID int + Name string + Hobby []string + } + c.Request().SetBody([]byte(``)) + c.Request().Header.SetContentType("") + // c.Request().URI().SetQueryString("id=1&name=tom&hobby=basketball&hobby=football") + c.Request().URI().SetQueryString("id=1&name=tom&hobby=basketball,football") + q := new(Query) + b.ReportAllocs() + b.ResetTimer() + + var err error + for n := 0; n < b.N; n++ { + err = c.QueryParser(q) + } + utils.AssertEqual(b, nil, err) + utils.AssertEqual(b, nil, c.QueryParser(q)) +} + +// go test -v -run=^$ -bench=Benchmark_Ctx_ReqHeaderParser -benchmem -count=4 +func Benchmark_Ctx_ReqHeaderParser(b *testing.B) { + app := New(Config{EnableSplittingOnParsers: true}) + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + type ReqHeader struct { + ID int + Name string + Hobby []string + } + c.Request().SetBody([]byte(``)) + c.Request().Header.SetContentType("") + + c.Request().Header.Add("id", "1") + c.Request().Header.Add("Name", "John Doe") + c.Request().Header.Add("Hobby", "golang,fiber") + + q := new(ReqHeader) + b.ReportAllocs() + b.ResetTimer() + + var err error + for n := 0; n < b.N; n++ { + err = c.ReqHeaderParser(q) + } + utils.AssertEqual(b, nil, err) + utils.AssertEqual(b, nil, c.ReqHeaderParser(q)) +} + +// go test -run Test_Ctx_BodyStreamWriter +func Test_Ctx_BodyStreamWriter(t *testing.T) { + t.Parallel() + ctx := &fasthttp.RequestCtx{} + + ctx.SetBodyStreamWriter(func(w *bufio.Writer) { + fmt.Fprintf(w, "body writer line 1\n") + if err := w.Flush(); err != nil { + t.Errorf("unexpected error: %s", err) + } + fmt.Fprintf(w, "body writer line 2\n") + }) + + utils.AssertEqual(t, true, ctx.IsBodyStream()) + + s := ctx.Response.String() + br := bufio.NewReader(bytes.NewBufferString(s)) + var resp fasthttp.Response + utils.AssertEqual(t, nil, resp.Read(br)) + + body := string(resp.Body()) + expectedBody := "body writer line 1\nbody writer line 2\n" + utils.AssertEqual(t, expectedBody, body) +} + +// go test -v -run=^$ -bench=Benchmark_Ctx_BodyStreamWriter -benchmem -count=4 +func Benchmark_Ctx_BodyStreamWriter(b *testing.B) { + ctx := &fasthttp.RequestCtx{} + user := []byte(`{"name":"john"}`) + b.ReportAllocs() + b.ResetTimer() + + var err error + for n := 0; n < b.N; n++ { + ctx.ResetBody() + ctx.SetBodyStreamWriter(func(w *bufio.Writer) { + for i := 0; i < 10; i++ { + _, err = w.Write(user) + if err := w.Flush(); err != nil { + return + } + } + }) + } + utils.AssertEqual(b, nil, err) +} + +func Test_Ctx_String(t *testing.T) { + t.Parallel() + app := New() + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + + utils.AssertEqual(t, "#0000000000000000 - 0.0.0.0:0 <-> 0.0.0.0:0 - GET http:///", c.String()) +} + +func TestCtx_ParamsInt(t *testing.T) { + // Create a test context and set some strings (or params) + // create a fake app to be used within this test + t.Parallel() + app := New() + + // Create some test endpoints + + // For the user id I will use the number 1111, so I should be able to get the number + // 1111 from the Ctx + app.Get("/test/:user", func(c *Ctx) error { + // utils.AssertEqual(t, "john", c.Params("user")) + + num, err := c.ParamsInt("user") + + // Check the number matches + utils.AssertEqual(t, 1111, num) + + // Check no errors are returned, because we want NO errors in this one + utils.AssertEqual(t, nil, err) + + return nil + }) + + // In this test case, there will be a bad request where the expected number is NOT + // a number in the path + app.Get("/testnoint/:user", func(c *Ctx) error { + // utils.AssertEqual(t, "john", c.Params("user")) + + num, err := c.ParamsInt("user") + + // Check the number matches + utils.AssertEqual(t, 0, num) + + // Check an error is returned, because we want NO errors in this one + utils.AssertEqual(t, true, err != nil) + + return nil + }) + + // For the user id I will use the number 2222, so I should be able to get the number + // 2222 from the Ctx even when the default value is specified + app.Get("/testignoredefault/:user", func(c *Ctx) error { + // utils.AssertEqual(t, "john", c.Params("user")) + + num, err := c.ParamsInt("user", 1111) + + // Check the number matches + utils.AssertEqual(t, 2222, num) + + // Check no errors are returned, because we want NO errors in this one + utils.AssertEqual(t, nil, err) + + return nil + }) + + // In this test case, there will be a bad request where the expected number is NOT + // a number in the path, default value of 1111 should be used instead + app.Get("/testdefault/:user", func(c *Ctx) error { + // utils.AssertEqual(t, "john", c.Params("user")) + + num, err := c.ParamsInt("user", 1111) + + // Check the number matches + utils.AssertEqual(t, 1111, num) + + // Check an error is returned, because we want NO errors in this one + utils.AssertEqual(t, nil, err) + + return nil + }) + + _, err := app.Test(httptest.NewRequest(MethodGet, "/test/1111", nil)) + utils.AssertEqual(t, nil, err) + + _, err = app.Test(httptest.NewRequest(MethodGet, "/testnoint/xd", nil)) + utils.AssertEqual(t, nil, err) + + _, err = app.Test(httptest.NewRequest(MethodGet, "/testignoredefault/2222", nil)) + utils.AssertEqual(t, nil, err) + + _, err = app.Test(httptest.NewRequest(MethodGet, "/testdefault/xd", nil)) + utils.AssertEqual(t, nil, err) +} + +// go test -run Test_Ctx_GetRespHeader +func Test_Ctx_GetRespHeader(t *testing.T) { + t.Parallel() + app := New() + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + + c.Set("test", "Hello, World 👋!") + c.Response().Header.Set(HeaderContentType, "application/json") + utils.AssertEqual(t, c.GetRespHeader("test"), "Hello, World 👋!") + utils.AssertEqual(t, c.GetRespHeader(HeaderContentType), "application/json") +} + +// go test -run Test_Ctx_GetRespHeaders +func Test_Ctx_GetRespHeaders(t *testing.T) { + t.Parallel() + app := New() + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + + c.Set("test", "Hello, World 👋!") + c.Set("foo", "bar") + c.Response().Header.Set("multi", "one") + c.Response().Header.Add("multi", "two") + c.Response().Header.Set(HeaderContentType, "application/json") + + utils.AssertEqual(t, c.GetRespHeaders(), map[string][]string{ + "Content-Type": {"application/json"}, + "Foo": {"bar"}, + "Multi": {"one", "two"}, + "Test": {"Hello, World 👋!"}, + }) +} + +func Benchmark_Ctx_GetRespHeaders(b *testing.B) { + app := New() + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + + c.Response().Header.Set("test", "Hello, World 👋!") + c.Response().Header.Set("foo", "bar") + c.Response().Header.Set(HeaderContentType, "application/json") + + b.ReportAllocs() + b.ResetTimer() + + var headers map[string][]string + for n := 0; n < b.N; n++ { + headers = c.GetRespHeaders() + } + + utils.AssertEqual(b, headers, map[string][]string{ + "Content-Type": {"application/json"}, + "Foo": {"bar"}, + "Test": {"Hello, World 👋!"}, + }) +} + +// go test -run Test_Ctx_GetReqHeaders +func Test_Ctx_GetReqHeaders(t *testing.T) { + t.Parallel() + app := New() + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + + c.Request().Header.Set("test", "Hello, World 👋!") + c.Request().Header.Set("foo", "bar") + c.Request().Header.Set("multi", "one") + c.Request().Header.Add("multi", "two") + c.Request().Header.Set(HeaderContentType, "application/json") + + utils.AssertEqual(t, c.GetReqHeaders(), map[string][]string{ + "Content-Type": {"application/json"}, + "Foo": {"bar"}, + "Test": {"Hello, World 👋!"}, + "Multi": {"one", "two"}, + }) +} + +func Benchmark_Ctx_GetReqHeaders(b *testing.B) { + app := New() + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + + c.Request().Header.Set("test", "Hello, World 👋!") + c.Request().Header.Set("foo", "bar") + c.Request().Header.Set(HeaderContentType, "application/json") + + b.ReportAllocs() + b.ResetTimer() + + var headers map[string][]string + for n := 0; n < b.N; n++ { + headers = c.GetReqHeaders() + } + + utils.AssertEqual(b, headers, map[string][]string{ + "Content-Type": {"application/json"}, + "Foo": {"bar"}, + "Test": {"Hello, World 👋!"}, + }) +} + +// go test -run Test_Ctx_IsFromLocal_X_Forwarded +func Test_Ctx_IsFromLocal_X_Forwarded(t *testing.T) { + t.Parallel() + // Test unset X-Forwarded-For header. + { + app := New() + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + // fasthttp returns "0.0.0.0" as IP as there is no remote address. + utils.AssertEqual(t, "0.0.0.0", c.IP()) + utils.AssertEqual(t, false, c.IsFromLocal()) + } + // Test when setting X-Forwarded-For header to localhost "127.0.0.1" + { + app := New() + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + c.Request().Header.Set(HeaderXForwardedFor, "127.0.0.1") + defer app.ReleaseCtx(c) + utils.AssertEqual(t, false, c.IsFromLocal()) + } + // Test when setting X-Forwarded-For header to localhost "::1" + { + app := New() + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + c.Request().Header.Set(HeaderXForwardedFor, "::1") + defer app.ReleaseCtx(c) + utils.AssertEqual(t, false, c.IsFromLocal()) + } + // Test when setting X-Forwarded-For to full localhost IPv6 address "0:0:0:0:0:0:0:1" + { + app := New() + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + c.Request().Header.Set(HeaderXForwardedFor, "0:0:0:0:0:0:0:1") + defer app.ReleaseCtx(c) + utils.AssertEqual(t, false, c.IsFromLocal()) + } + // Test for a random IP address. + { + app := New() + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + c.Request().Header.Set(HeaderXForwardedFor, "93.46.8.90") + defer app.ReleaseCtx(c) + utils.AssertEqual(t, false, c.IsFromLocal()) + } +} + +// go test -run Test_Ctx_IsFromLocal_RemoteAddr +func Test_Ctx_IsFromLocal_RemoteAddr(t *testing.T) { + t.Parallel() + + localIPv4 := net.Addr(&net.TCPAddr{IP: net.ParseIP("127.0.0.1")}) + localIPv6 := net.Addr(&net.TCPAddr{IP: net.ParseIP("::1")}) + localIPv6long := net.Addr(&net.TCPAddr{IP: net.ParseIP("0:0:0:0:0:0:0:1")}) + + zeroIPv4 := net.Addr(&net.TCPAddr{IP: net.IPv4zero}) + + someIPv4 := net.Addr(&net.TCPAddr{IP: net.ParseIP("93.46.8.90")}) + someIPv6 := net.Addr(&net.TCPAddr{IP: net.ParseIP("2001:0db8:85a3:0000:0000:8a2e:0370:7334")}) + + // Test for the case fasthttp remoteAddr is set to "127.0.0.1". + { + app := New() + fastCtx := &fasthttp.RequestCtx{} + fastCtx.SetRemoteAddr(localIPv4) + c := app.AcquireCtx(fastCtx) + defer app.ReleaseCtx(c) + + utils.AssertEqual(t, "127.0.0.1", c.IP()) + utils.AssertEqual(t, true, c.IsFromLocal()) + } + // Test for the case fasthttp remoteAddr is set to "::1". + { + app := New() + fastCtx := &fasthttp.RequestCtx{} + fastCtx.SetRemoteAddr(localIPv6) + c := app.AcquireCtx(fastCtx) + defer app.ReleaseCtx(c) + utils.AssertEqual(t, "::1", c.IP()) + utils.AssertEqual(t, true, c.IsFromLocal()) + } + // Test for the case fasthttp remoteAddr is set to "0:0:0:0:0:0:0:1". + { + app := New() + fastCtx := &fasthttp.RequestCtx{} + fastCtx.SetRemoteAddr(localIPv6long) + c := app.AcquireCtx(fastCtx) + defer app.ReleaseCtx(c) + // fasthttp should return "::1" for "0:0:0:0:0:0:0:1". + // otherwise IsFromLocal() will break. + utils.AssertEqual(t, "::1", c.IP()) + utils.AssertEqual(t, true, c.IsFromLocal()) + } + // Test for the case fasthttp remoteAddr is set to "0.0.0.0". + { + app := New() + fastCtx := &fasthttp.RequestCtx{} + fastCtx.SetRemoteAddr(zeroIPv4) + c := app.AcquireCtx(fastCtx) + defer app.ReleaseCtx(c) + utils.AssertEqual(t, "0.0.0.0", c.IP()) + utils.AssertEqual(t, false, c.IsFromLocal()) + } + // Test for the case fasthttp remoteAddr is set to "93.46.8.90". + { + app := New() + fastCtx := &fasthttp.RequestCtx{} + fastCtx.SetRemoteAddr(someIPv4) + c := app.AcquireCtx(fastCtx) + defer app.ReleaseCtx(c) + utils.AssertEqual(t, "93.46.8.90", c.IP()) + utils.AssertEqual(t, false, c.IsFromLocal()) + } + // Test for the case fasthttp remoteAddr is set to "2001:0db8:85a3:0000:0000:8a2e:0370:7334". + { + app := New() + fastCtx := &fasthttp.RequestCtx{} + fastCtx.SetRemoteAddr(someIPv6) + c := app.AcquireCtx(fastCtx) + defer app.ReleaseCtx(c) + utils.AssertEqual(t, "2001:db8:85a3::8a2e:370:7334", c.IP()) + utils.AssertEqual(t, false, c.IsFromLocal()) + } +} + +// go test -run Test_Ctx_RepeatParserWithSameStruct -v +func Test_Ctx_RepeatParserWithSameStruct(t *testing.T) { + t.Parallel() + app := New() + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + + type Request struct { + QueryParam string `query:"query_param"` + HeaderParam string `reqHeader:"header_param"` + BodyParam string `json:"body_param" xml:"body_param" form:"body_param"` + } + + r := new(Request) + + c.Request().URI().SetQueryString("query_param=query_param") + utils.AssertEqual(t, nil, c.QueryParser(r)) + utils.AssertEqual(t, "query_param", r.QueryParam) + + c.Request().Header.Add("header_param", "header_param") + utils.AssertEqual(t, nil, c.ReqHeaderParser(r)) + utils.AssertEqual(t, "header_param", r.HeaderParam) + + var gzipJSON bytes.Buffer + w := gzip.NewWriter(&gzipJSON) + _, _ = w.Write([]byte(`{"body_param":"body_param"}`)) //nolint:errcheck // This will never fail + err := w.Close() + utils.AssertEqual(t, nil, err) + c.Request().Header.SetContentType(MIMEApplicationJSON) + c.Request().Header.Set(HeaderContentEncoding, "gzip") + c.Request().SetBody(gzipJSON.Bytes()) + c.Request().Header.SetContentLength(len(gzipJSON.Bytes())) + utils.AssertEqual(t, nil, c.BodyParser(r)) + utils.AssertEqual(t, "body_param", r.BodyParam) + c.Request().Header.Del(HeaderContentEncoding) + + testDecodeParser := func(contentType, body string) { + c.Request().Header.SetContentType(contentType) + c.Request().SetBody([]byte(body)) + c.Request().Header.SetContentLength(len(body)) + utils.AssertEqual(t, nil, c.BodyParser(r)) + utils.AssertEqual(t, "body_param", r.BodyParam) + } + + testDecodeParser(MIMEApplicationJSON, `{"body_param":"body_param"}`) + testDecodeParser(MIMEApplicationXML, `body_param`) + testDecodeParser(MIMEApplicationForm, "body_param=body_param") + testDecodeParser(MIMEMultipartForm+`;boundary="b"`, "--b\r\nContent-Disposition: form-data; name=\"body_param\"\r\n\r\nbody_param\r\n--b--") +} + +// go test -run Test_Ctx_extractIPsFromHeader -v +func Test_Ctx_extractIPsFromHeader(t *testing.T) { + app := New() + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + c.Request().Header.Set("x-forwarded-for", "1.1.1.1,8.8.8.8 , /n, \n,1.1, a.c, 6.,6., , a,,42.118.81.169,10.0.137.108") + ips := c.IPs() + res := ips[len(ips)-2] + utils.AssertEqual(t, "42.118.81.169", res) +} + +// go test -run Test_Ctx_extractIPsFromHeader -v +func Test_Ctx_extractIPsFromHeader_EnableValidateIp(t *testing.T) { + app := New() + app.config.EnableIPValidation = true + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + c.Request().Header.Set("x-forwarded-for", "1.1.1.1,8.8.8.8 , /n, \n,1.1, a.c, 6.,6., , a,,42.118.81.169,10.0.137.108") + ips := c.IPs() + res := ips[len(ips)-2] + utils.AssertEqual(t, "42.118.81.169", res) +} diff --git a/docs/api/_category_.json b/docs/api/_category_.json new file mode 100644 index 0000000..c0fc663 --- /dev/null +++ b/docs/api/_category_.json @@ -0,0 +1,8 @@ +{ + "label": "API", + "position": 2, + "link": { + "type": "generated-index", + "description": "API documentation for Fiber." + } +} diff --git a/docs/api/app.md b/docs/api/app.md new file mode 100644 index 0000000..cb38f70 --- /dev/null +++ b/docs/api/app.md @@ -0,0 +1,657 @@ +--- +id: app +title: 🚀 App +description: The app instance conventionally denotes the Fiber application. +sidebar_position: 2 +--- + +import RoutingHandler from './../partials/routing/handler.md'; + +## Static + +Use the **Static** method to serve static files such as **images**, **CSS,** and **JavaScript**. + +:::info +By default, **Static** will serve `index.html` files in response to a request on a directory. +::: + +```go title="Signature" +func (app *App) Static(prefix, root string, config ...Static) Router +``` + +Use the following code to serve files in a directory named `./public` + +```go +app.Static("/", "./public") + +// => http://localhost:3000/hello.html +// => http://localhost:3000/js/jquery.js +// => http://localhost:3000/css/style.css +``` + +```go title="Examples" +// Serve files from multiple directories +app.Static("/", "./public") + +// Serve files from "./files" directory: +app.Static("/", "./files") +``` + +You can use any virtual path prefix \(_where the path does not actually exist in the file system_\) for files that are served by the **Static** method, specify a prefix path for the static directory, as shown below: + +```go title="Examples" +app.Static("/static", "./public") + +// => http://localhost:3000/static/hello.html +// => http://localhost:3000/static/js/jquery.js +// => http://localhost:3000/static/css/style.css +``` + +If you want to have a little bit more control regarding the settings for serving static files. You could use the `fiber.Static` struct to enable specific settings. + +```go title="fiber.Static{}" +// Static defines configuration options when defining static assets. +type Static struct { + // When set to true, the server tries minimizing CPU usage by caching compressed files. + // This works differently than the github.com/gofiber/compression middleware. + // Optional. Default value false + Compress bool `json:"compress"` + + // When set to true, enables byte range requests. + // Optional. Default value false + ByteRange bool `json:"byte_range"` + + // When set to true, enables directory browsing. + // Optional. Default value false. + Browse bool `json:"browse"` + + // When set to true, enables direct download. + // Optional. Default value false. + Download bool `json:"download"` + + // The name of the index file for serving a directory. + // Optional. Default value "index.html". + Index string `json:"index"` + + // Expiration duration for inactive file handlers. + // Use a negative time.Duration to disable it. + // + // Optional. Default value 10 * time.Second. + CacheDuration time.Duration `json:"cache_duration"` + + // The value for the Cache-Control HTTP-header + // that is set on the file response. MaxAge is defined in seconds. + // + // Optional. Default value 0. + MaxAge int `json:"max_age"` + + // ModifyResponse defines a function that allows you to alter the response. + // + // Optional. Default: nil + ModifyResponse Handler + + // Next defines a function to skip this middleware when returned true. + // + // Optional. Default: nil + Next func(c *Ctx) bool +} +``` + +```go title="Example" +// Custom config +app.Static("/", "./public", fiber.Static{ + Compress: true, + ByteRange: true, + Browse: true, + Index: "john.html", + CacheDuration: 10 * time.Second, + MaxAge: 3600, +}) +``` + +## Route Handlers + + + +## Mount + +You can Mount Fiber instance by creating a `*Mount` + +```go title="Signature" +func (a *App) Mount(prefix string, app *App) Router +``` + +```go title="Examples" +func main() { + app := fiber.New() + micro := fiber.New() + app.Mount("/john", micro) // GET /john/doe -> 200 OK + + micro.Get("/doe", func(c *fiber.Ctx) error { + return c.SendStatus(fiber.StatusOK) + }) + + log.Fatal(app.Listen(":3000")) +} +``` + +## MountPath + +The `MountPath` property contains one or more path patterns on which a sub-app was mounted. + +```go title="Signature" +func (app *App) MountPath() string +``` + +```go title="Examples" +func main() { + app := fiber.New() + one := fiber.New() + two := fiber.New() + three := fiber.New() + + two.Mount("/three", three) + one.Mount("/two", two) + app.Mount("/one", one) + + one.MountPath() // "/one" + two.MountPath() // "/one/two" + three.MountPath() // "/one/two/three" + app.MountPath() // "" +} +``` + +:::caution +Mounting order is important for MountPath. If you want to get mount paths properly, you should start mounting from the deepest app. +::: + +## Group + +You can group routes by creating a `*Group` struct. + +```go title="Signature" +func (app *App) Group(prefix string, handlers ...Handler) Router +``` + +```go title="Examples" +func main() { + app := fiber.New() + + api := app.Group("/api", handler) // /api + + v1 := api.Group("/v1", handler) // /api/v1 + v1.Get("/list", handler) // /api/v1/list + v1.Get("/user", handler) // /api/v1/user + + v2 := api.Group("/v2", handler) // /api/v2 + v2.Get("/list", handler) // /api/v2/list + v2.Get("/user", handler) // /api/v2/user + + log.Fatal(app.Listen(":3000")) +} +``` + +## Route + +You can define routes with a common prefix inside the common function. + +```go title="Signature" +func (app *App) Route(prefix string, fn func(router Router), name ...string) Router +``` + +```go title="Examples" +func main() { + app := fiber.New() + + app.Route("/test", func(api fiber.Router) { + api.Get("/foo", handler).Name("foo") // /test/foo (name: test.foo) + api.Get("/bar", handler).Name("bar") // /test/bar (name: test.bar) + }, "test.") + + log.Fatal(app.Listen(":3000")) +} +``` + +## Server + +Server returns the underlying [fasthttp server](https://godoc.org/github.com/valyala/fasthttp#Server) + +```go title="Signature" +func (app *App) Server() *fasthttp.Server +``` + +```go title="Examples" +func main() { + app := fiber.New() + + app.Server().MaxConnsPerIP = 1 + + // ... +} +``` + +## Server Shutdown + +Shutdown gracefully shuts down the server without interrupting any active connections. Shutdown works by first closing all open listeners and then waits indefinitely for all connections to return to idle before shutting down. + +ShutdownWithTimeout will forcefully close any active connections after the timeout expires. + +ShutdownWithContext shuts down the server including by force if the context's deadline is exceeded. + +```go +func (app *App) Shutdown() error +func (app *App) ShutdownWithTimeout(timeout time.Duration) error +func (app *App) ShutdownWithContext(ctx context.Context) error +``` + +## HandlersCount + +This method returns the amount of registered handlers. + +```go title="Signature" +func (app *App) HandlersCount() uint32 +``` + +## Stack + +This method returns the original router stack + +```go title="Signature" +func (app *App) Stack() [][]*Route +``` + +```go title="Examples" +var handler = func(c *fiber.Ctx) error { return nil } + +func main() { + app := fiber.New() + + app.Get("/john/:age", handler) + app.Post("/register", handler) + + data, _ := json.MarshalIndent(app.Stack(), "", " ") + fmt.Println(string(data)) + + app.Listen(":3000") +} +``` + +```javascript title="Result" +[ + [ + { + "method": "GET", + "path": "/john/:age", + "params": [ + "age" + ] + } + ], + [ + { + "method": "HEAD", + "path": "/john/:age", + "params": [ + "age" + ] + } + ], + [ + { + "method": "POST", + "path": "/register", + "params": null + } + ] +] +``` + +## Name + +This method assigns the name of latest created route. + +```go title="Signature" +func (app *App) Name(name string) Router +``` + +```go title="Examples" +var handler = func(c *fiber.Ctx) error { return nil } + +func main() { + app := fiber.New() + + app.Get("/", handler) + app.Name("index") + + app.Get("/doe", handler).Name("home") + + app.Trace("/tracer", handler).Name("tracert") + + app.Delete("/delete", handler).Name("delete") + + a := app.Group("/a") + a.Name("fd.") + + a.Get("/test", handler).Name("test") + + data, _ := json.MarshalIndent(app.Stack(), "", " ") + fmt.Print(string(data)) + + app.Listen(":3000") + +} +``` + +```javascript title="Result" +[ + [ + { + "method": "GET", + "name": "index", + "path": "/", + "params": null + }, + { + "method": "GET", + "name": "home", + "path": "/doe", + "params": null + }, + { + "method": "GET", + "name": "fd.test", + "path": "/a/test", + "params": null + } + ], + [ + { + "method": "HEAD", + "name": "", + "path": "/", + "params": null + }, + { + "method": "HEAD", + "name": "", + "path": "/doe", + "params": null + }, + { + "method": "HEAD", + "name": "", + "path": "/a/test", + "params": null + } + ], + null, + null, + [ + { + "method": "DELETE", + "name": "delete", + "path": "/delete", + "params": null + } + ], + null, + null, + [ + { + "method": "TRACE", + "name": "tracert", + "path": "/tracer", + "params": null + } + ], + null +] +``` + +## GetRoute + +This method gets the route by name. + +```go title="Signature" +func (app *App) GetRoute(name string) Route +``` + +```go title="Examples" +var handler = func(c *fiber.Ctx) error { return nil } + +func main() { + app := fiber.New() + + app.Get("/", handler).Name("index") + + data, _ := json.MarshalIndent(app.GetRoute("index"), "", " ") + fmt.Print(string(data)) + + + app.Listen(":3000") + +} +``` + +```javascript title="Result" +{ + "method": "GET", + "name": "index", + "path": "/", + "params": null +} +``` + +## GetRoutes + +This method gets all routes. + +```go title="Signature" +func (app *App) GetRoutes(filterUseOption ...bool) []Route +``` + +When filterUseOption equal to true, it will filter the routes registered by the middleware. +```go title="Examples" +func main() { + app := fiber.New() + app.Post("/", func (c *fiber.Ctx) error { + return c.SendString("Hello, World!") + }).Name("index") + data, _ := json.MarshalIndent(app.GetRoutes(true), "", " ") + fmt.Print(string(data)) +} +``` + +```javascript title="Result" +[ + { + "method": "POST", + "name": "index", + "path": "/", + "params": null + } +] +``` + +## Config + +Config returns the app config as value \( read-only \). + +```go title="Signature" +func (app *App) Config() Config +``` + +## Handler + +Handler returns the server handler that can be used to serve custom \*fasthttp.RequestCtx requests. + +```go title="Signature" +func (app *App) Handler() fasthttp.RequestHandler +``` + +## Listen + +Listen serves HTTP requests from the given address. + +```go title="Signature" +func (app *App) Listen(addr string) error +``` + +```go title="Examples" +// Listen on port :8080 +app.Listen(":8080") + +// Custom host +app.Listen("127.0.0.1:8080") +``` + +## ListenTLS + +ListenTLS serves HTTPs requests from the given address using certFile and keyFile paths to as TLS certificate and key file. + +```go title="Signature" +func (app *App) ListenTLS(addr, certFile, keyFile string) error +``` + +```go title="Examples" +app.ListenTLS(":443", "./cert.pem", "./cert.key"); +``` + +Using `ListenTLS` defaults to the following config \( use `Listener` to provide your own config \) + +```go title="Default \*tls.Config" +&tls.Config{ + MinVersion: tls.VersionTLS12, + Certificates: []tls.Certificate{ + cert, + }, +} +``` + +## ListenTLSWithCertificate + +```go title="Signature" +func (app *App) ListenTLS(addr string, cert tls.Certificate) error +``` + +```go title="Examples" +app.ListenTLSWithCertificate(":443", cert); +``` + +Using `ListenTLSWithCertificate` defaults to the following config \( use `Listener` to provide your own config \) + +```go title="Default \*tls.Config" +&tls.Config{ + MinVersion: tls.VersionTLS12, + Certificates: []tls.Certificate{ + cert, + }, +} +``` + +## ListenMutualTLS + +ListenMutualTLS serves HTTPs requests from the given address using certFile, keyFile and clientCertFile are the paths to TLS certificate and key file + +```go title="Signature" +func (app *App) ListenMutualTLS(addr, certFile, keyFile, clientCertFile string) error +``` + +```go title="Examples" +app.ListenMutualTLS(":443", "./cert.pem", "./cert.key", "./ca-chain-cert.pem"); +``` + +Using `ListenMutualTLS` defaults to the following config \( use `Listener` to provide your own config \) + +```go title="Default \*tls.Config" +&tls.Config{ + MinVersion: tls.VersionTLS12, + ClientAuth: tls.RequireAndVerifyClientCert, + ClientCAs: clientCertPool, + Certificates: []tls.Certificate{ + cert, + }, +} +``` + +## ListenMutualTLSWithCertificate + +ListenMutualTLSWithCertificate serves HTTPs requests from the given address using certFile, keyFile and clientCertFile are the paths to TLS certificate and key file + +```go title="Signature" +func (app *App) ListenMutualTLSWithCertificate(addr string, cert tls.Certificate, clientCertPool *x509.CertPool) error +``` + +```go title="Examples" +app.ListenMutualTLSWithCertificate(":443", cert, clientCertPool); +``` + +Using `ListenMutualTLSWithCertificate` defaults to the following config \( use `Listener` to provide your own config \) + +```go title="Default \*tls.Config" +&tls.Config{ + MinVersion: tls.VersionTLS12, + ClientAuth: tls.RequireAndVerifyClientCert, + ClientCAs: clientCertPool, + Certificates: []tls.Certificate{ + cert, + }, +} +``` + +## Listener + +You can pass your own [`net.Listener`](https://pkg.go.dev/net/#Listener) using the `Listener` method. This method can be used to enable **TLS/HTTPS** with a custom tls.Config. + +```go title="Signature" +func (app *App) Listener(ln net.Listener) error +``` + +```go title="Examples" +ln, _ := net.Listen("tcp", ":3000") + +cer, _:= tls.LoadX509KeyPair("server.crt", "server.key") + +ln = tls.NewListener(ln, &tls.Config{Certificates: []tls.Certificate{cer}}) + +app.Listener(ln) +``` + +## Test + +Testing your application is done with the **Test** method. Use this method for creating `_test.go` files or when you need to debug your routing logic. The default timeout is `1s` if you want to disable a timeout altogether, pass `-1` as a second argument. + +```go title="Signature" +func (app *App) Test(req *http.Request, msTimeout ...int) (*http.Response, error) +``` + +```go title="Examples" +// Create route with GET method for test: +app.Get("/", func(c *fiber.Ctx) error { + fmt.Println(c.BaseURL()) // => http://google.com + fmt.Println(c.Get("X-Custom-Header")) // => hi + + return c.SendString("hello, World!") +}) + +// http.Request +req := httptest.NewRequest("GET", "http://google.com", nil) +req.Header.Set("X-Custom-Header", "hi") + +// http.Response +resp, _ := app.Test(req) + +// Do something with results: +if resp.StatusCode == fiber.StatusOK { + body, _ := io.ReadAll(resp.Body) + fmt.Println(string(body)) // => Hello, World! +} +``` + +## Hooks + +Hooks is a method to return [hooks](../guide/hooks.md) property. + +```go title="Signature" +func (app *App) Hooks() *Hooks +``` \ No newline at end of file diff --git a/docs/api/client.md b/docs/api/client.md new file mode 100644 index 0000000..4b52d72 --- /dev/null +++ b/docs/api/client.md @@ -0,0 +1,663 @@ +--- +id: client +title: 🌎 Client +description: The Client struct represents the Fiber HTTP Client. +sidebar_position: 5 +--- + +## Start request + +Start a http request with http method and url. + +```go title="Signatures" +// Client http methods +func (c *Client) Get(url string) *Agent +func (c *Client) Head(url string) *Agent +func (c *Client) Post(url string) *Agent +func (c *Client) Put(url string) *Agent +func (c *Client) Patch(url string) *Agent +func (c *Client) Delete(url string) *Agent +``` + +Here we present a brief example demonstrating the simulation of a proxy using our `*fiber.Agent` methods. +```go +// Get something +func getSomething(c *fiber.Ctx) (err error) { + agent := fiber.Get("") + statusCode, body, errs := agent.Bytes() + if len(errs) > 0 { + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ + "errs": errs, + }) + } + + var something fiber.Map + err = json.Unmarshal(body, &something) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ + "err": err, + }) + } + + return c.Status(statusCode).JSON(something) +} + +// Post something +func createSomething(c *fiber.Ctx) (err error) { + agent := fiber.Post("") + agent.Body(c.Body()) // set body received by request + statusCode, body, errs := agent.Bytes() + if len(errs) > 0 { + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ + "errs": errs, + }) + } + + // pass status code and body received by the proxy + return c.Status(statusCode).Send(body) +} +``` +Based on this short example, we can perceive that using the `*fiber.Client` is very straightforward and intuitive. + + +## ✨ Agent +`Agent` is built on top of FastHTTP's [`HostClient`](https://github.com/valyala/fasthttp/blob/master/client.go#L603) which has lots of convenient helper methods such as dedicated methods for request methods. + +### Parse + +Parse initializes a HostClient. + +```go title="Parse" +a := AcquireAgent() +req := a.Request() +req.Header.SetMethod(MethodGet) +req.SetRequestURI("http://example.com") + +if err := a.Parse(); err != nil { + panic(err) +} + +code, body, errs := a.Bytes() // ... +``` + +### Set + +Set sets the given `key: value` header. + +```go title="Signature" +func (a *Agent) Set(k, v string) *Agent +func (a *Agent) SetBytesK(k []byte, v string) *Agent +func (a *Agent) SetBytesV(k string, v []byte) *Agent +func (a *Agent) SetBytesKV(k []byte, v []byte) *Agent +``` + +```go title="Example" +agent.Set("k1", "v1"). + SetBytesK([]byte("k1"), "v1"). + SetBytesV("k1", []byte("v1")). + SetBytesKV([]byte("k2"), []byte("v2")) +// ... +``` + +### Add + +Add adds the given `key: value` header. Multiple headers with the same key may be added with this function. + +```go title="Signature" +func (a *Agent) Add(k, v string) *Agent +func (a *Agent) AddBytesK(k []byte, v string) *Agent +func (a *Agent) AddBytesV(k string, v []byte) *Agent +func (a *Agent) AddBytesKV(k []byte, v []byte) *Agent +``` + +```go title="Example" +agent.Add("k1", "v1"). + AddBytesK([]byte("k1"), "v1"). + AddBytesV("k1", []byte("v1")). + AddBytesKV([]byte("k2"), []byte("v2")) +// Headers: +// K1: v1 +// K1: v1 +// K1: v1 +// K2: v2 +``` + +### ConnectionClose + +ConnectionClose adds the `Connection: close` header. + +```go title="Signature" +func (a *Agent) ConnectionClose() *Agent +``` + +```go title="Example" +agent.ConnectionClose() +// ... +``` + +### UserAgent + +UserAgent sets `User-Agent` header value. + +```go title="Signature" +func (a *Agent) UserAgent(userAgent string) *Agent +func (a *Agent) UserAgentBytes(userAgent []byte) *Agent +``` + +```go title="Example" +agent.UserAgent("fiber") +// ... +``` + +### Cookie + +Cookie sets a cookie in `key: value` form. `Cookies` can be used to set multiple cookies. + +```go title="Signature" +func (a *Agent) Cookie(key, value string) *Agent +func (a *Agent) CookieBytesK(key []byte, value string) *Agent +func (a *Agent) CookieBytesKV(key, value []byte) *Agent +func (a *Agent) Cookies(kv ...string) *Agent +func (a *Agent) CookiesBytesKV(kv ...[]byte) *Agent +``` + +```go title="Example" +agent.Cookie("k", "v") +agent.Cookies("k1", "v1", "k2", "v2") +// ... +``` + +### Referer + +Referer sets the Referer header value. + +```go title="Signature" +func (a *Agent) Referer(referer string) *Agent +func (a *Agent) RefererBytes(referer []byte) *Agent +``` + +```go title="Example" +agent.Referer("https://docs.gofiber.io") +// ... +``` + +### ContentType + +ContentType sets Content-Type header value. + +```go title="Signature" +func (a *Agent) ContentType(contentType string) *Agent +func (a *Agent) ContentTypeBytes(contentType []byte) *Agent +``` + +```go title="Example" +agent.ContentType("custom-type") +// ... +``` + +### Host + +Host sets the Host header. + +```go title="Signature" +func (a *Agent) Host(host string) *Agent +func (a *Agent) HostBytes(host []byte) *Agent +``` + +```go title="Example" +agent.Host("example.com") +// ... +``` + +### QueryString + +QueryString sets the URI query string. + +```go title="Signature" +func (a *Agent) QueryString(queryString string) *Agent +func (a *Agent) QueryStringBytes(queryString []byte) *Agent +``` + +```go title="Example" +agent.QueryString("foo=bar") +// ... +``` + +### BasicAuth + +BasicAuth sets the URI username and password using HTTP Basic Auth. + +```go title="Signature" +func (a *Agent) BasicAuth(username, password string) *Agent +func (a *Agent) BasicAuthBytes(username, password []byte) *Agent +``` + +```go title="Example" +agent.BasicAuth("foo", "bar") +// ... +``` + +### Body + +There are several ways to set request body. + +```go title="Signature" +func (a *Agent) BodyString(bodyString string) *Agent +func (a *Agent) Body(body []byte) *Agent + +// BodyStream sets request body stream and, optionally body size. +// +// If bodySize is >= 0, then the bodyStream must provide exactly bodySize bytes +// before returning io.EOF. +// +// If bodySize < 0, then bodyStream is read until io.EOF. +// +// bodyStream.Close() is called after finishing reading all body data +// if it implements io.Closer. +// +// Note that GET and HEAD requests cannot have body. +func (a *Agent) BodyStream(bodyStream io.Reader, bodySize int) *Agent +``` + +```go title="Example" +agent.BodyString("foo=bar") +agent.Body([]byte("bar=baz")) +agent.BodyStream(strings.NewReader("body=stream"), -1) +// ... +``` + +### JSON + +JSON sends a JSON request by setting the Content-Type header to the `ctype` parameter. If no `ctype` is passed in, the header is set to `application/json`. + +```go title="Signature" +func (a *Agent) JSON(v interface{}, ctype ...string) *Agent +``` + +```go title="Example" +agent.JSON(fiber.Map{"success": true}) +// ... +``` + +### XML + +XML sends an XML request by setting the Content-Type header to `application/xml`. + +```go title="Signature" +func (a *Agent) XML(v interface{}) *Agent +``` + +```go title="Example" +agent.XML(fiber.Map{"success": true}) +// ... +``` + +### Form + +Form sends a form request by setting the Content-Type header to `application/x-www-form-urlencoded`. + +```go title="Signature" +// Form sends form request with body if args is non-nil. +// +// It is recommended obtaining args via AcquireArgs and release it +// manually in performance-critical code. +func (a *Agent) Form(args *Args) *Agent +``` + +```go title="Example" +args := AcquireArgs() +args.Set("foo", "bar") + +agent.Form(args) +// ... +ReleaseArgs(args) +``` + +### MultipartForm + +MultipartForm sends multipart form request by setting the Content-Type header to `multipart/form-data`. These requests can include key-value's and files. + +```go title="Signature" +// MultipartForm sends multipart form request with k-v and files. +// +// It is recommended to obtain args via AcquireArgs and release it +// manually in performance-critical code. +func (a *Agent) MultipartForm(args *Args) *Agent +``` + +```go title="Example" +args := AcquireArgs() +args.Set("foo", "bar") + +agent.MultipartForm(args) +// ... +ReleaseArgs(args) +``` + +Fiber provides several methods for sending files. Note that they must be called before `MultipartForm`. + +#### Boundary + +Boundary sets boundary for multipart form request. + +```go title="Signature" +func (a *Agent) Boundary(boundary string) *Agent +``` + +```go title="Example" +agent.Boundary("myBoundary") + .MultipartForm(nil) +// ... +``` + +#### SendFile\(s\) + +SendFile read a file and appends it to a multipart form request. Sendfiles can be used to append multiple files. + +```go title="Signature" +func (a *Agent) SendFile(filename string, fieldname ...string) *Agent +func (a *Agent) SendFiles(filenamesAndFieldnames ...string) *Agent +``` + +```go title="Example" +agent.SendFile("f", "field name") + .SendFiles("f1", "field name1", "f2"). + .MultipartForm(nil) +// ... +``` + +#### FileData + +FileData appends file data for multipart form request. + +```go +// FormFile represents multipart form file +type FormFile struct { + // Fieldname is form file's field name + Fieldname string + // Name is form file's name + Name string + // Content is form file's content + Content []byte +} +``` + +```go title="Signature" +// FileData appends files for multipart form request. +// +// It is recommended obtaining formFile via AcquireFormFile and release it +// manually in performance-critical code. +func (a *Agent) FileData(formFiles ...*FormFile) *Agent +``` + +```go title="Example" +ff1 := &FormFile{"filename1", "field name1", []byte("content")} +ff2 := &FormFile{"filename2", "field name2", []byte("content")} +agent.FileData(ff1, ff2). + MultipartForm(nil) +// ... +``` + +### Debug + +Debug mode enables logging request and response detail to `io.writer`\(default is `os.Stdout`\). + +```go title="Signature" +func (a *Agent) Debug(w ...io.Writer) *Agent +``` + +```go title="Example" +agent.Debug() +// ... +``` + +### Timeout + +Timeout sets request timeout duration. + +```go title="Signature" +func (a *Agent) Timeout(timeout time.Duration) *Agent +``` + +```go title="Example" +agent.Timeout(time.Second) +// ... +``` + +### Reuse + +Reuse enables the Agent instance to be used again after one request. If agent is reusable, then it should be released manually when it is no longer used. + +```go title="Signature" +func (a *Agent) Reuse() *Agent +``` + +```go title="Example" +agent.Reuse() +// ... +``` + +### InsecureSkipVerify + +InsecureSkipVerify controls whether the Agent verifies the server certificate chain and host name. + +```go title="Signature" +func (a *Agent) InsecureSkipVerify() *Agent +``` + +```go title="Example" +agent.InsecureSkipVerify() +// ... +``` + +### TLSConfig + +TLSConfig sets tls config. + +```go title="Signature" +func (a *Agent) TLSConfig(config *tls.Config) *Agent +``` + +```go title="Example" +// Create tls certificate +cer, _ := tls.LoadX509KeyPair("pem", "key") + +config := &tls.Config{ + Certificates: []tls.Certificate{cer}, +} + +agent.TLSConfig(config) +// ... +``` + +### MaxRedirectsCount + +MaxRedirectsCount sets max redirect count for GET and HEAD. + +```go title="Signature" +func (a *Agent) MaxRedirectsCount(count int) *Agent +``` + +```go title="Example" +agent.MaxRedirectsCount(7) +// ... +``` + +### JSONEncoder + +JSONEncoder sets custom json encoder. + +```go title="Signature" +func (a *Agent) JSONEncoder(jsonEncoder utils.JSONMarshal) *Agent +``` + +```go title="Example" +agent.JSONEncoder(json.Marshal) +// ... +``` + +### JSONDecoder + +JSONDecoder sets custom json decoder. + +```go title="Signature" +func (a *Agent) JSONDecoder(jsonDecoder utils.JSONUnmarshal) *Agent +``` + +```go title="Example" +agent.JSONDecoder(json.Unmarshal) +// ... +``` + +### Request + +Request returns Agent request instance. + +```go title="Signature" +func (a *Agent) Request() *Request +``` + +```go title="Example" +req := agent.Request() +// ... +``` + +### SetResponse + +SetResponse sets custom response for the Agent instance. It is recommended obtaining custom response via AcquireResponse and release it manually in performance-critical code. + +```go title="Signature" +func (a *Agent) SetResponse(customResp *Response) *Agent +``` + +```go title="Example" +resp := AcquireResponse() +agent.SetResponse(resp) +// ... +ReleaseResponse(resp) +``` + +
+Example handling for response values + +```go title="Example handling response" +// Create a Fiber HTTP client agent +agent := fiber.Get("https://httpbin.org/get") + +// Acquire a response object to store the result +resp := fiber.AcquireResponse() +agent.SetResponse(resp) + +// Perform the HTTP GET request +code, body, errs := agent.String() +if errs != nil { + // Handle any errors that occur during the request + panic(errs) +} + +// Print the HTTP response code and body +fmt.Println("Response Code:", code) +fmt.Println("Response Body:", body) + +// Visit and print all the headers in the response +resp.Header.VisitAll(func(key, value []byte) { + fmt.Println("Header", string(key), "value", string(value)) +}) + +// Release the response to free up resources +fiber.ReleaseResponse(resp) +``` + +Output: +```txt title="Output" +Response Code: 200 +Response Body: { + "args": {}, + "headers": { + "Host": "httpbin.org", + "User-Agent": "fiber", + "X-Amzn-Trace-Id": "Root=1-653763d0-2555d5ba3838f1e9092f9f72" + }, + "origin": "83.137.191.1", + "url": "https://httpbin.org/get" +} + +Header Content-Length value 226 +Header Content-Type value application/json +Header Server value gunicorn/19.9.0 +Header Date value Tue, 24 Oct 2023 06:27:28 GMT +Header Connection value keep-alive +Header Access-Control-Allow-Origin value * +Header Access-Control-Allow-Credentials value true +``` + +
+ +### Dest + +Dest sets custom dest. The contents of dest will be replaced by the response body, if the dest is too small a new slice will be allocated. + +```go title="Signature" +func (a *Agent) Dest(dest []byte) *Agent { +``` + +```go title="Example" +agent.Dest(nil) +// ... +``` + +### Bytes + +Bytes returns the status code, bytes body and errors of url. + +```go title="Signature" +func (a *Agent) Bytes() (code int, body []byte, errs []error) +``` + +```go title="Example" +code, body, errs := agent.Bytes() +// ... +``` + +### String + +String returns the status code, string body and errors of url. + +```go title="Signature" +func (a *Agent) String() (int, string, []error) +``` + +```go title="Example" +code, body, errs := agent.String() +// ... +``` + +### Struct + +Struct returns the status code, bytes body and errors of url. And bytes body will be unmarshalled to given v. + +```go title="Signature" +func (a *Agent) Struct(v interface{}) (code int, body []byte, errs []error) +``` + +```go title="Example" +var d data +code, body, errs := agent.Struct(&d) +// ... +``` + +### RetryIf + +RetryIf controls whether a retry should be attempted after an error. +By default, will use isIdempotent function from fasthttp + +```go title="Signature" +func (a *Agent) RetryIf(retryIf RetryIfFunc) *Agent +``` + +```go title="Example" +agent.Get("https://example.com").RetryIf(func (req *fiber.Request) bool { + return req.URI() == "https://example.com" +}) +// ... +``` diff --git a/docs/api/constants.md b/docs/api/constants.md new file mode 100644 index 0000000..8a436a9 --- /dev/null +++ b/docs/api/constants.md @@ -0,0 +1,291 @@ +--- +id: constants +title: 📋 Constants +description: Some constants for Fiber. +sidebar_position: 4 +--- + +HTTP methods were copied from net/http. + +```go +const ( + MethodGet = "GET" // RFC 7231, 4.3.1 + MethodHead = "HEAD" // RFC 7231, 4.3.2 + MethodPost = "POST" // RFC 7231, 4.3.3 + MethodPut = "PUT" // RFC 7231, 4.3.4 + MethodPatch = "PATCH" // RFC 5789 + MethodDelete = "DELETE" // RFC 7231, 4.3.5 + MethodConnect = "CONNECT" // RFC 7231, 4.3.6 + MethodOptions = "OPTIONS" // RFC 7231, 4.3.7 + MethodTrace = "TRACE" // RFC 7231, 4.3.8 + methodUse = "USE" +) +``` + +MIME types that are commonly used + +```go +const ( + MIMETextXML = "text/xml" + MIMETextHTML = "text/html" + MIMETextPlain = "text/plain" + MIMEApplicationXML = "application/xml" + MIMEApplicationJSON = "application/json" + MIMEApplicationJavaScript = "application/javascript" + MIMEApplicationForm = "application/x-www-form-urlencoded" + MIMEOctetStream = "application/octet-stream" + MIMEMultipartForm = "multipart/form-data" + + MIMETextXMLCharsetUTF8 = "text/xml; charset=utf-8" + MIMETextHTMLCharsetUTF8 = "text/html; charset=utf-8" + MIMETextPlainCharsetUTF8 = "text/plain; charset=utf-8" + MIMEApplicationXMLCharsetUTF8 = "application/xml; charset=utf-8" + MIMEApplicationJSONCharsetUTF8 = "application/json; charset=utf-8" + MIMEApplicationJavaScriptCharsetUTF8 = "application/javascript; charset=utf-8" +) +``` + +HTTP status codes were copied from net/http. + +```go +const ( + StatusContinue = 100 // RFC 7231, 6.2.1 + StatusSwitchingProtocols = 101 // RFC 7231, 6.2.2 + StatusProcessing = 102 // RFC 2518, 10.1 + StatusEarlyHints = 103 // RFC 8297 + StatusOK = 200 // RFC 7231, 6.3.1 + StatusCreated = 201 // RFC 7231, 6.3.2 + StatusAccepted = 202 // RFC 7231, 6.3.3 + StatusNonAuthoritativeInformation = 203 // RFC 7231, 6.3.4 + StatusNoContent = 204 // RFC 7231, 6.3.5 + StatusResetContent = 205 // RFC 7231, 6.3.6 + StatusPartialContent = 206 // RFC 7233, 4.1 + StatusMultiStatus = 207 // RFC 4918, 11.1 + StatusAlreadyReported = 208 // RFC 5842, 7.1 + StatusIMUsed = 226 // RFC 3229, 10.4.1 + StatusMultipleChoices = 300 // RFC 7231, 6.4.1 + StatusMovedPermanently = 301 // RFC 7231, 6.4.2 + StatusFound = 302 // RFC 7231, 6.4.3 + StatusSeeOther = 303 // RFC 7231, 6.4.4 + StatusNotModified = 304 // RFC 7232, 4.1 + StatusUseProxy = 305 // RFC 7231, 6.4.5 + StatusTemporaryRedirect = 307 // RFC 7231, 6.4.7 + StatusPermanentRedirect = 308 // RFC 7538, 3 + StatusBadRequest = 400 // RFC 7231, 6.5.1 + StatusUnauthorized = 401 // RFC 7235, 3.1 + StatusPaymentRequired = 402 // RFC 7231, 6.5.2 + StatusForbidden = 403 // RFC 7231, 6.5.3 + StatusNotFound = 404 // RFC 7231, 6.5.4 + StatusMethodNotAllowed = 405 // RFC 7231, 6.5.5 + StatusNotAcceptable = 406 // RFC 7231, 6.5.6 + StatusProxyAuthRequired = 407 // RFC 7235, 3.2 + StatusRequestTimeout = 408 // RFC 7231, 6.5.7 + StatusConflict = 409 // RFC 7231, 6.5.8 + StatusGone = 410 // RFC 7231, 6.5.9 + StatusLengthRequired = 411 // RFC 7231, 6.5.10 + StatusPreconditionFailed = 412 // RFC 7232, 4.2 + StatusRequestEntityTooLarge = 413 // RFC 7231, 6.5.11 + StatusRequestURITooLong = 414 // RFC 7231, 6.5.12 + StatusUnsupportedMediaType = 415 // RFC 7231, 6.5.13 + StatusRequestedRangeNotSatisfiable = 416 // RFC 7233, 4.4 + StatusExpectationFailed = 417 // RFC 7231, 6.5.14 + StatusTeapot = 418 // RFC 7168, 2.3.3 + StatusMisdirectedRequest = 421 // RFC 7540, 9.1.2 + StatusUnprocessableEntity = 422 // RFC 4918, 11.2 + StatusLocked = 423 // RFC 4918, 11.3 + StatusFailedDependency = 424 // RFC 4918, 11.4 + StatusTooEarly = 425 // RFC 8470, 5.2. + StatusUpgradeRequired = 426 // RFC 7231, 6.5.15 + StatusPreconditionRequired = 428 // RFC 6585, 3 + StatusTooManyRequests = 429 // RFC 6585, 4 + StatusRequestHeaderFieldsTooLarge = 431 // RFC 6585, 5 + StatusUnavailableForLegalReasons = 451 // RFC 7725, 3 + StatusInternalServerError = 500 // RFC 7231, 6.6.1 + StatusNotImplemented = 501 // RFC 7231, 6.6.2 + StatusBadGateway = 502 // RFC 7231, 6.6.3 + StatusServiceUnavailable = 503 // RFC 7231, 6.6.4 + StatusGatewayTimeout = 504 // RFC 7231, 6.6.5 + StatusHTTPVersionNotSupported = 505 // RFC 7231, 6.6.6 + StatusVariantAlsoNegotiates = 506 // RFC 2295, 8.1 + StatusInsufficientStorage = 507 // RFC 4918, 11.5 + StatusLoopDetected = 508 // RFC 5842, 7.2 + StatusNotExtended = 510 // RFC 2774, 7 + StatusNetworkAuthenticationRequired = 511 // RFC 6585, 6 +) +``` + +Errors + +```go +var ( + ErrBadRequest = NewError(StatusBadRequest) // RFC 7231, 6.5.1 + ErrUnauthorized = NewError(StatusUnauthorized) // RFC 7235, 3.1 + ErrPaymentRequired = NewError(StatusPaymentRequired) // RFC 7231, 6.5.2 + ErrForbidden = NewError(StatusForbidden) // RFC 7231, 6.5.3 + ErrNotFound = NewError(StatusNotFound) // RFC 7231, 6.5.4 + ErrMethodNotAllowed = NewError(StatusMethodNotAllowed) // RFC 7231, 6.5.5 + ErrNotAcceptable = NewError(StatusNotAcceptable) // RFC 7231, 6.5.6 + ErrProxyAuthRequired = NewError(StatusProxyAuthRequired) // RFC 7235, 3.2 + ErrRequestTimeout = NewError(StatusRequestTimeout) // RFC 7231, 6.5.7 + ErrConflict = NewError(StatusConflict) // RFC 7231, 6.5.8 + ErrGone = NewError(StatusGone) // RFC 7231, 6.5.9 + ErrLengthRequired = NewError(StatusLengthRequired) // RFC 7231, 6.5.10 + ErrPreconditionFailed = NewError(StatusPreconditionFailed) // RFC 7232, 4.2 + ErrRequestEntityTooLarge = NewError(StatusRequestEntityTooLarge) // RFC 7231, 6.5.11 + ErrRequestURITooLong = NewError(StatusRequestURITooLong) // RFC 7231, 6.5.12 + ErrUnsupportedMediaType = NewError(StatusUnsupportedMediaType) // RFC 7231, 6.5.13 + ErrRequestedRangeNotSatisfiable = NewError(StatusRequestedRangeNotSatisfiable) // RFC 7233, 4.4 + ErrExpectationFailed = NewError(StatusExpectationFailed) // RFC 7231, 6.5.14 + ErrTeapot = NewError(StatusTeapot) // RFC 7168, 2.3.3 + ErrMisdirectedRequest = NewError(StatusMisdirectedRequest) // RFC 7540, 9.1.2 + ErrUnprocessableEntity = NewError(StatusUnprocessableEntity) // RFC 4918, 11.2 + ErrLocked = NewError(StatusLocked) // RFC 4918, 11.3 + ErrFailedDependency = NewError(StatusFailedDependency) // RFC 4918, 11.4 + ErrTooEarly = NewError(StatusTooEarly) // RFC 8470, 5.2. + ErrUpgradeRequired = NewError(StatusUpgradeRequired) // RFC 7231, 6.5.15 + ErrPreconditionRequired = NewError(StatusPreconditionRequired) // RFC 6585, 3 + ErrTooManyRequests = NewError(StatusTooManyRequests) // RFC 6585, 4 + ErrRequestHeaderFieldsTooLarge = NewError(StatusRequestHeaderFieldsTooLarge) // RFC 6585, 5 + ErrUnavailableForLegalReasons = NewError(StatusUnavailableForLegalReasons) // RFC 7725, 3 + ErrInternalServerError = NewError(StatusInternalServerError) // RFC 7231, 6.6.1 + ErrNotImplemented = NewError(StatusNotImplemented) // RFC 7231, 6.6.2 + ErrBadGateway = NewError(StatusBadGateway) // RFC 7231, 6.6.3 + ErrServiceUnavailable = NewError(StatusServiceUnavailable) // RFC 7231, 6.6.4 + ErrGatewayTimeout = NewError(StatusGatewayTimeout) // RFC 7231, 6.6.5 + ErrHTTPVersionNotSupported = NewError(StatusHTTPVersionNotSupported) // RFC 7231, 6.6.6 + ErrVariantAlsoNegotiates = NewError(StatusVariantAlsoNegotiates) // RFC 2295, 8.1 + ErrInsufficientStorage = NewError(StatusInsufficientStorage) // RFC 4918, 11.5 + ErrLoopDetected = NewError(StatusLoopDetected) // RFC 5842, 7.2 + ErrNotExtended = NewError(StatusNotExtended) // RFC 2774, 7 + ErrNetworkAuthenticationRequired = NewError(StatusNetworkAuthenticationRequired) // RFC 6585, 6 +) +``` + +HTTP Headers were copied from net/http. + +```go +const ( + HeaderAuthorization = "Authorization" + HeaderProxyAuthenticate = "Proxy-Authenticate" + HeaderProxyAuthorization = "Proxy-Authorization" + HeaderWWWAuthenticate = "WWW-Authenticate" + HeaderAge = "Age" + HeaderCacheControl = "Cache-Control" + HeaderClearSiteData = "Clear-Site-Data" + HeaderExpires = "Expires" + HeaderPragma = "Pragma" + HeaderWarning = "Warning" + HeaderAcceptCH = "Accept-CH" + HeaderAcceptCHLifetime = "Accept-CH-Lifetime" + HeaderContentDPR = "Content-DPR" + HeaderDPR = "DPR" + HeaderEarlyData = "Early-Data" + HeaderSaveData = "Save-Data" + HeaderViewportWidth = "Viewport-Width" + HeaderWidth = "Width" + HeaderETag = "ETag" + HeaderIfMatch = "If-Match" + HeaderIfModifiedSince = "If-Modified-Since" + HeaderIfNoneMatch = "If-None-Match" + HeaderIfUnmodifiedSince = "If-Unmodified-Since" + HeaderLastModified = "Last-Modified" + HeaderVary = "Vary" + HeaderConnection = "Connection" + HeaderKeepAlive = "Keep-Alive" + HeaderAccept = "Accept" + HeaderAcceptCharset = "Accept-Charset" + HeaderAcceptEncoding = "Accept-Encoding" + HeaderAcceptLanguage = "Accept-Language" + HeaderCookie = "Cookie" + HeaderExpect = "Expect" + HeaderMaxForwards = "Max-Forwards" + HeaderSetCookie = "Set-Cookie" + HeaderAccessControlAllowCredentials = "Access-Control-Allow-Credentials" + HeaderAccessControlAllowHeaders = "Access-Control-Allow-Headers" + HeaderAccessControlAllowMethods = "Access-Control-Allow-Methods" + HeaderAccessControlAllowOrigin = "Access-Control-Allow-Origin" + HeaderAccessControlExposeHeaders = "Access-Control-Expose-Headers" + HeaderAccessControlMaxAge = "Access-Control-Max-Age" + HeaderAccessControlRequestHeaders = "Access-Control-Request-Headers" + HeaderAccessControlRequestMethod = "Access-Control-Request-Method" + HeaderOrigin = "Origin" + HeaderTimingAllowOrigin = "Timing-Allow-Origin" + HeaderXPermittedCrossDomainPolicies = "X-Permitted-Cross-Domain-Policies" + HeaderDNT = "DNT" + HeaderTk = "Tk" + HeaderContentDisposition = "Content-Disposition" + HeaderContentEncoding = "Content-Encoding" + HeaderContentLanguage = "Content-Language" + HeaderContentLength = "Content-Length" + HeaderContentLocation = "Content-Location" + HeaderContentType = "Content-Type" + HeaderForwarded = "Forwarded" + HeaderVia = "Via" + HeaderXForwardedFor = "X-Forwarded-For" + HeaderXForwardedHost = "X-Forwarded-Host" + HeaderXForwardedProto = "X-Forwarded-Proto" + HeaderXForwardedProtocol = "X-Forwarded-Protocol" + HeaderXForwardedSsl = "X-Forwarded-Ssl" + HeaderXUrlScheme = "X-Url-Scheme" + HeaderLocation = "Location" + HeaderFrom = "From" + HeaderHost = "Host" + HeaderReferer = "Referer" + HeaderReferrerPolicy = "Referrer-Policy" + HeaderUserAgent = "User-Agent" + HeaderAllow = "Allow" + HeaderServer = "Server" + HeaderAcceptRanges = "Accept-Ranges" + HeaderContentRange = "Content-Range" + HeaderIfRange = "If-Range" + HeaderRange = "Range" + HeaderContentSecurityPolicy = "Content-Security-Policy" + HeaderContentSecurityPolicyReportOnly = "Content-Security-Policy-Report-Only" + HeaderCrossOriginResourcePolicy = "Cross-Origin-Resource-Policy" + HeaderExpectCT = "Expect-CT" + HeaderFeaturePolicy = "Feature-Policy" + HeaderPublicKeyPins = "Public-Key-Pins" + HeaderPublicKeyPinsReportOnly = "Public-Key-Pins-Report-Only" + HeaderStrictTransportSecurity = "Strict-Transport-Security" + HeaderUpgradeInsecureRequests = "Upgrade-Insecure-Requests" + HeaderXContentTypeOptions = "X-Content-Type-Options" + HeaderXDownloadOptions = "X-Download-Options" + HeaderXFrameOptions = "X-Frame-Options" + HeaderXPoweredBy = "X-Powered-By" + HeaderXXSSProtection = "X-XSS-Protection" + HeaderLastEventID = "Last-Event-ID" + HeaderNEL = "NEL" + HeaderPingFrom = "Ping-From" + HeaderPingTo = "Ping-To" + HeaderReportTo = "Report-To" + HeaderTE = "TE" + HeaderTrailer = "Trailer" + HeaderTransferEncoding = "Transfer-Encoding" + HeaderSecWebSocketAccept = "Sec-WebSocket-Accept" + HeaderSecWebSocketExtensions = "Sec-WebSocket-Extensions" + HeaderSecWebSocketKey = "Sec-WebSocket-Key" + HeaderSecWebSocketProtocol = "Sec-WebSocket-Protocol" + HeaderSecWebSocketVersion = "Sec-WebSocket-Version" + HeaderAcceptPatch = "Accept-Patch" + HeaderAcceptPushPolicy = "Accept-Push-Policy" + HeaderAcceptSignature = "Accept-Signature" + HeaderAltSvc = "Alt-Svc" + HeaderDate = "Date" + HeaderIndex = "Index" + HeaderLargeAllocation = "Large-Allocation" + HeaderLink = "Link" + HeaderPushPolicy = "Push-Policy" + HeaderRetryAfter = "Retry-After" + HeaderServerTiming = "Server-Timing" + HeaderSignature = "Signature" + HeaderSignedHeaders = "Signed-Headers" + HeaderSourceMap = "SourceMap" + HeaderUpgrade = "Upgrade" + HeaderXDNSPrefetchControl = "X-DNS-Prefetch-Control" + HeaderXPingback = "X-Pingback" + HeaderXRequestID = "X-Request-ID" + HeaderXRequestedWith = "X-Requested-With" + HeaderXRobotsTag = "X-Robots-Tag" + HeaderXUACompatible = "X-UA-Compatible" +) +``` \ No newline at end of file diff --git a/docs/api/ctx.md b/docs/api/ctx.md new file mode 100644 index 0000000..fdb2adc --- /dev/null +++ b/docs/api/ctx.md @@ -0,0 +1,2159 @@ +--- +id: ctx +title: 🧠 Ctx +description: >- + The Ctx struct represents the Context which hold the HTTP request and + response. It has methods for the request query string, parameters, body, HTTP + headers, and so on. +sidebar_position: 3 +--- + +## Accepts + +Checks, if the specified **extensions** or **content** **types** are acceptable. + +:::info +Based on the request’s [Accept](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept) HTTP header. +::: + +```go title="Signature" +func (c *Ctx) Accepts(offers ...string) string +func (c *Ctx) AcceptsCharsets(offers ...string) string +func (c *Ctx) AcceptsEncodings(offers ...string) string +func (c *Ctx) AcceptsLanguages(offers ...string) string +``` + +```go title="Example" +// Accept: text/html, application/json; q=0.8, text/plain; q=0.5; charset="utf-8" + +app.Get("/", func(c *fiber.Ctx) error { + c.Accepts("html") // "html" + c.Accepts("text/html") // "text/html" + c.Accepts("json", "text") // "json" + c.Accepts("application/json") // "application/json" + c.Accepts("text/plain", "application/json") // "application/json", due to quality + c.Accepts("image/png") // "" + c.Accepts("png") // "" + // ... +}) +``` + +```go title="Example 2" +// Accept: text/html, text/*, application/json, */*; q=0 + +app.Get("/", func(c *fiber.Ctx) error { + c.Accepts("text/plain", "application/json") // "application/json", due to specificity + c.Accepts("application/json", "text/html") // "text/html", due to first match + c.Accepts("image/png") // "", due to */* without q factor 0 is Not Acceptable + // ... +}) +``` + +Media-Type parameters are supported. + +```go title="Example 3" +// Accept: text/plain, application/json; version=1; foo=bar + +app.Get("/", func(c *fiber.Ctx) error { + // Extra parameters in the accept are ignored + c.Accepts("text/plain;format=flowed") // "text/plain;format=flowed" + + // An offer must contain all parameters present in the Accept type + c.Accepts("application/json") // "" + + // Parameter order and capitalization does not matter. Quotes on values are stripped. + c.Accepts(`application/json;foo="bar";VERSION=1`) // "application/json;foo="bar";VERSION=1" +}) +``` + +```go title="Example 4" +// Accept: text/plain;format=flowed;q=0.9, text/plain +// i.e., "I prefer text/plain;format=flowed less than other forms of text/plain" +app.Get("/", func(c *fiber.Ctx) error { + // Beware: the order in which offers are listed matters. + // Although the client specified they prefer not to receive format=flowed, + // the text/plain Accept matches with "text/plain;format=flowed" first, so it is returned. + c.Accepts("text/plain;format=flowed", "text/plain") // "text/plain;format=flowed" + + // Here, things behave as expected: + c.Accepts("text/plain", "text/plain;format=flowed") // "text/plain" +}) +``` + +Fiber provides similar functions for the other accept headers. + +```go +// Accept-Charset: utf-8, iso-8859-1;q=0.2 +// Accept-Encoding: gzip, compress;q=0.2 +// Accept-Language: en;q=0.8, nl, ru + +app.Get("/", func(c *fiber.Ctx) error { + c.AcceptsCharsets("utf-16", "iso-8859-1") + // "iso-8859-1" + + c.AcceptsEncodings("compress", "br") + // "compress" + + c.AcceptsLanguages("pt", "nl", "ru") + // "nl" + // ... +}) +``` + +## AllParams + +Params is used to get all route parameters. +Using Params method to get params. + +```go title="Signature" +func (c *Ctx) AllParams() map[string]string +``` + +```go title="Example" +// GET http://example.com/user/fenny +app.Get("/user/:name", func(c *fiber.Ctx) error { + c.AllParams() // "{"name": "fenny"}" + + // ... +}) + +// GET http://example.com/user/fenny/123 +app.Get("/user/*", func(c *fiber.Ctx) error { + c.AllParams() // "{"*1": "fenny/123"}" + + // ... +}) +``` + +## App + +Returns the [\*App](ctx.md) reference so you could easily access all application settings. + +```go title="Signature" +func (c *Ctx) App() *App +``` + +```go title="Example" +app.Get("/stack", func(c *fiber.Ctx) error { + return c.JSON(c.App().Stack()) +}) +``` + +## Append + +Appends the specified **value** to the HTTP response header field. + +:::caution +If the header is **not** already set, it creates the header with the specified value. +::: + +```go title="Signature" +func (c *Ctx) Append(field string, values ...string) +``` + +```go title="Example" +app.Get("/", func(c *fiber.Ctx) error { + c.Append("Link", "http://google.com", "http://localhost") + // => Link: http://localhost, http://google.com + + c.Append("Link", "Test") + // => Link: http://localhost, http://google.com, Test + + // ... +}) +``` + +## Attachment + +Sets the HTTP response [Content-Disposition](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Disposition) header field to `attachment`. + +```go title="Signature" +func (c *Ctx) Attachment(filename ...string) +``` + +```go title="Example" +app.Get("/", func(c *fiber.Ctx) error { + c.Attachment() + // => Content-Disposition: attachment + + c.Attachment("./upload/images/logo.png") + // => Content-Disposition: attachment; filename="logo.png" + // => Content-Type: image/png + + // ... +}) +``` + +## BaseURL + +Returns the base URL \(**protocol** + **host**\) as a `string`. + +```go title="Signature" +func (c *Ctx) BaseURL() string +``` + +```go title="Example" +// GET https://example.com/page#chapter-1 + +app.Get("/", func(c *fiber.Ctx) error { + c.BaseURL() // https://example.com + // ... +}) +``` + +## Bind + +Add vars to default view var map binding to template engine. +Variables are read by the Render method and may be overwritten. + +```go title="Signature" +func (c *Ctx) Bind(vars Map) error +``` + +```go title="Example" +app.Use(func(c *fiber.Ctx) error { + c.Bind(fiber.Map{ + "Title": "Hello, World!", + }) +}) + +app.Get("/", func(c *fiber.Ctx) error { + return c.Render("xxx.tmpl", fiber.Map{}) // Render will use Title variable +}) +``` + +## BodyRaw + +Returns the raw request **body**. + +```go title="Signature" +func (c *Ctx) BodyRaw() []byte +``` + +```go title="Example" +// curl -X POST http://localhost:8080 -d user=john + +app.Post("/", func(c *fiber.Ctx) error { + // Get raw body from POST request: + return c.Send(c.BodyRaw()) // []byte("user=john") +}) +``` + +> _Returned value is only valid within the handler. Do not store any references. +> Make copies or use the_ [_**`Immutable`**_](ctx.md) _setting instead._ [_Read more..._](../#zero-allocation) + +## Body + +As per the header `Content-Encoding`, this method will try to perform a file decompression from the **body** bytes. In case no `Content-Encoding` header is sent, it will perform as [BodyRaw](#bodyraw). + +```go title="Signature" +func (c *Ctx) Body() []byte +``` + +```go title="Example" +// echo 'user=john' | gzip | curl -v -i --data-binary @- -H "Content-Encoding: gzip" http://localhost:8080 + +app.Post("/", func(c *fiber.Ctx) error { + // Decompress body from POST request based on the Content-Encoding and return the raw content: + return c.Send(c.Body()) // []byte("user=john") +}) +``` + +> _Returned value is only valid within the handler. Do not store any references. +> Make copies or use the_ [_**`Immutable`**_](ctx.md) _setting instead._ [_Read more..._](../#zero-allocation) + +## BodyParser + +Binds the request body to a struct. + +It is important to specify the correct struct tag based on the content type to be parsed. For example, if you want to parse a JSON body with a field called Pass, you would use a struct field of `json:"pass"`. + +| content-type | struct tag | +| ----------------------------------- | ---------- | +| `application/x-www-form-urlencoded` | form | +| `multipart/form-data` | form | +| `application/json` | json | +| `application/xml` | xml | +| `text/xml` | xml | + +```go title="Signature" +func (c *Ctx) BodyParser(out interface{}) error +``` + +```go title="Example" +// Field names should start with an uppercase letter +type Person struct { + Name string `json:"name" xml:"name" form:"name"` + Pass string `json:"pass" xml:"pass" form:"pass"` +} + +app.Post("/", func(c *fiber.Ctx) error { + p := new(Person) + + if err := c.BodyParser(p); err != nil { + return err + } + + log.Println(p.Name) // john + log.Println(p.Pass) // doe + + // ... +}) + +// Run tests with the following curl commands + +// curl -X POST -H "Content-Type: application/json" --data "{\"name\":\"john\",\"pass\":\"doe\"}" localhost:3000 + +// curl -X POST -H "Content-Type: application/xml" --data "johndoe" localhost:3000 + +// curl -X POST -H "Content-Type: application/x-www-form-urlencoded" --data "name=john&pass=doe" localhost:3000 + +// curl -X POST -F name=john -F pass=doe http://localhost:3000 + +// curl -X POST "http://localhost:3000/?name=john&pass=doe" +``` + +> _Returned value is only valid within the handler. Do not store any references. +> Make copies or use the_ [_**`Immutable`**_](ctx.md) _setting instead._ [_Read more..._](../#zero-allocation) + +## ClearCookie + +Expire a client cookie \(_or all cookies if left empty\)_ + +```go title="Signature" +func (c *Ctx) ClearCookie(key ...string) +``` + +```go title="Example" +app.Get("/", func(c *fiber.Ctx) error { + // Clears all cookies: + c.ClearCookie() + + // Expire specific cookie by name: + c.ClearCookie("user") + + // Expire multiple cookies by names: + c.ClearCookie("token", "session", "track_id", "version") + // ... +}) +``` + +:::caution +Web browsers and other compliant clients will only clear the cookie if the given options are identical to those when creating the cookie, excluding expires and maxAge. ClearCookie will not set these values for you - a technique similar to the one shown below should be used to ensure your cookie is deleted. +::: + +```go title="Example" +app.Get("/set", func(c *fiber.Ctx) error { + c.Cookie(&fiber.Cookie{ + Name: "token", + Value: "randomvalue", + Expires: time.Now().Add(24 * time.Hour), + HTTPOnly: true, + SameSite: "lax", + }) + + // ... +}) + +app.Get("/delete", func(c *fiber.Ctx) error { + c.Cookie(&fiber.Cookie{ + Name: "token", + // Set expiry date to the past + Expires: time.Now().Add(-(time.Hour * 2)), + HTTPOnly: true, + SameSite: "lax", + }) + + // ... +}) +``` + +## ClientHelloInfo + +ClientHelloInfo contains information from a ClientHello message in order to guide application logic in the GetCertificate and GetConfigForClient callbacks. +You can refer to the [ClientHelloInfo](https://golang.org/pkg/crypto/tls/#ClientHelloInfo) struct documentation for more information on the returned struct. + +```go title="Signature" +func (c *Ctx) ClientHelloInfo() *tls.ClientHelloInfo +``` + +```go title="Example" +// GET http://example.com/hello +app.Get("/hello", func(c *fiber.Ctx) error { + chi := c.ClientHelloInfo() + // ... +}) +``` + +## Context + +Returns [\*fasthttp.RequestCtx](https://godoc.org/github.com/valyala/fasthttp#RequestCtx) that is compatible with the context.Context interface that requires a deadline, a cancellation signal, and other values across API boundaries. + +```go title="Signature" +func (c *Ctx) Context() *fasthttp.RequestCtx +``` + +:::info +Please read the [Fasthttp Documentation](https://pkg.go.dev/github.com/valyala/fasthttp?tab=doc) for more information. +::: + +## Cookie + +Set cookie + +```go title="Signature" +func (c *Ctx) Cookie(cookie *Cookie) +``` + +```go +type Cookie struct { + Name string `json:"name"` + Value string `json:"value"` + Path string `json:"path"` + Domain string `json:"domain"` + MaxAge int `json:"max_age"` + Expires time.Time `json:"expires"` + Secure bool `json:"secure"` + HTTPOnly bool `json:"http_only"` + SameSite string `json:"same_site"` + SessionOnly bool `json:"session_only"` +} +``` + +```go title="Example" +app.Get("/", func(c *fiber.Ctx) error { + // Create cookie + cookie := new(fiber.Cookie) + cookie.Name = "john" + cookie.Value = "doe" + cookie.Expires = time.Now().Add(24 * time.Hour) + + // Set cookie + c.Cookie(cookie) + // ... +}) +``` + +## CookieParser + +This method is similar to [BodyParser](ctx.md#bodyparser), but for cookie parameters. +It is important to use the struct tag "cookie". For example, if you want to parse a cookie with a field called Age, you would use a struct field of `cookie:"age"`. + +```go title="Signature" +func (c *Ctx) CookieParser(out interface{}) error +``` + +```go title="Example" +// Field names should start with an uppercase letter +type Person struct { + Name string `cookie:"name"` + Age int `cookie:"age"` + Job bool `cookie:"job"` +} + +app.Get("/", func(c *fiber.Ctx) error { + p := new(Person) + + if err := c.CookieParser(p); err != nil { + return err + } + + log.Println(p.Name) // Joseph + log.Println(p.Age) // 23 + log.Println(p.Job) // true +}) +// Run tests with the following curl command +// curl.exe --cookie "name=Joseph; age=23; job=true" http://localhost:8000/ +``` + +## Cookies + +Get cookie value by key, you could pass an optional default value that will be returned if the cookie key does not exist. + +```go title="Signature" +func (c *Ctx) Cookies(key string, defaultValue ...string) string +``` + +```go title="Example" +app.Get("/", func(c *fiber.Ctx) error { + // Get cookie by key: + c.Cookies("name") // "john" + c.Cookies("empty", "doe") // "doe" + // ... +}) +``` + +> _Returned value is only valid within the handler. Do not store any references. +> Make copies or use the_ [_**`Immutable`**_](ctx.md) _setting instead._ [_Read more..._](../#zero-allocation) + +## Download + +Transfers the file from path as an `attachment`. + +Typically, browsers will prompt the user to download. By default, the [Content-Disposition](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Disposition) header `filename=` parameter is the file path \(_this typically appears in the browser dialog_\). + +Override this default with the **filename** parameter. + +```go title="Signature" +func (c *Ctx) Download(file string, filename ...string) error +``` + +```go title="Example" +app.Get("/", func(c *fiber.Ctx) error { + return c.Download("./files/report-12345.pdf"); + // => Download report-12345.pdf + + return c.Download("./files/report-12345.pdf", "report.pdf"); + // => Download report.pdf +}) +``` + +## Format + +Performs content-negotiation on the [Accept](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept) HTTP header. It uses [Accepts](ctx.md#accepts) to select a proper format. + +:::info +If the header is **not** specified or there is **no** proper format, **text/plain** is used. +::: + +```go title="Signature" +func (c *Ctx) Format(body interface{}) error +``` + +```go title="Example" +app.Get("/", func(c *fiber.Ctx) error { + // Accept: text/plain + c.Format("Hello, World!") + // => Hello, World! + + // Accept: text/html + c.Format("Hello, World!") + // =>

Hello, World!

+ + // Accept: application/json + c.Format("Hello, World!") + // => "Hello, World!" + // .. +}) +``` + +## FormFile + +MultipartForm files can be retrieved by name, the **first** file from the given key is returned. + +```go title="Signature" +func (c *Ctx) FormFile(key string) (*multipart.FileHeader, error) +``` + +```go title="Example" +app.Post("/", func(c *fiber.Ctx) error { + // Get first file from form field "document": + file, err := c.FormFile("document") + + // Save file to root directory: + return c.SaveFile(file, fmt.Sprintf("./%s", file.Filename)) +}) +``` + +## FormValue + +Any form values can be retrieved by name, the **first** value from the given key is returned. + +```go title="Signature" +func (c *Ctx) FormValue(key string, defaultValue ...string) string +``` + +```go title="Example" +app.Post("/", func(c *fiber.Ctx) error { + // Get first value from form field "name": + c.FormValue("name") + // => "john" or "" if not exist + + // .. +}) +``` + +> _Returned value is only valid within the handler. Do not store any references. +> Make copies or use the_ [_**`Immutable`**_](ctx.md) _setting instead._ [_Read more..._](../#zero-allocation) + +## Fresh + +When the response is still **fresh** in the client's cache **true** is returned, otherwise **false** is returned to indicate that the client cache is now stale and the full response should be sent. + +When a client sends the Cache-Control: no-cache request header to indicate an end-to-end reload request, `Fresh` will return false to make handling these requests transparent. + +Read more on [https://expressjs.com/en/4x/api.html\#req.fresh](https://expressjs.com/en/4x/api.html#req.fresh) + +```go title="Signature" +func (c *Ctx) Fresh() bool +``` + +## Get + +Returns the HTTP request header specified by the field. + +:::tip +The match is **case-insensitive**. +::: + +```go title="Signature" +func (c *Ctx) Get(key string, defaultValue ...string) string +``` + +```go title="Example" +app.Get("/", func(c *fiber.Ctx) error { + c.Get("Content-Type") // "text/plain" + c.Get("CoNtEnT-TypE") // "text/plain" + c.Get("something", "john") // "john" + // .. +}) +``` + +> _Returned value is only valid within the handler. Do not store any references. +> Make copies or use the_ [_**`Immutable`**_](ctx.md) _setting instead._ [_Read more..._](../#zero-allocation) + +## GetReqHeaders + +Returns the HTTP request headers as a map. Since a header can be set multiple times in a single request, the values of the map are slices of strings containing all the different values of the header. + +```go title="Signature" +func (c *Ctx) GetReqHeaders() map[string][]string +``` + +> _Returned value is only valid within the handler. Do not store any references. +> Make copies or use the_ [_**`Immutable`**_](ctx.md) _setting instead._ [_Read more..._](../#zero-allocation) + +## GetRespHeader + +Returns the HTTP response header specified by the field. + +:::tip +The match is **case-insensitive**. +::: + +```go title="Signature" +func (c *Ctx) GetRespHeader(key string, defaultValue ...string) string +``` + +```go title="Example" +app.Get("/", func(c *fiber.Ctx) error { + c.GetRespHeader("X-Request-Id") // "8d7ad5e3-aaf3-450b-a241-2beb887efd54" + c.GetRespHeader("Content-Type") // "text/plain" + c.GetRespHeader("something", "john") // "john" + // .. +}) +``` + +> _Returned value is only valid within the handler. Do not store any references. +> Make copies or use the_ [_**`Immutable`**_](ctx.md) _setting instead._ [_Read more..._](../#zero-allocation) + +## GetRespHeaders + +Returns the HTTP response headers as a map. Since a header can be set multiple times in a single request, the values of the map are slices of strings containing all the different values of the header. + +```go title="Signature" +func (c *Ctx) GetRespHeaders() map[string][]string +``` + +> _Returned value is only valid within the handler. Do not store any references. +> Make copies or use the_ [_**`Immutable`**_](ctx.md) _setting instead._ [_Read more..._](../#zero-allocation) + +## GetRouteURL + +Generates URLs to named routes, with parameters. URLs are relative, for example: "/user/1831" + +```go title="Signature" +func (c *Ctx) GetRouteURL(routeName string, params Map) (string, error) +``` + +```go title="Example" +app.Get("/", func(c *fiber.Ctx) error { + return c.SendString("Home page") +}).Name("home") + +app.Get("/user/:id", func(c *fiber.Ctx) error { + return c.SendString(c.Params("id")) +}).Name("user.show") + +app.Get("/test", func(c *fiber.Ctx) error { + location, _ := c.GetRouteURL("user.show", fiber.Map{"id": 1}) + return c.SendString(location) +}) + +// /test returns "/user/1" +``` + +## Hostname + +Returns the hostname derived from the [Host](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Host) HTTP header. + +```go title="Signature" +func (c *Ctx) Hostname() string +``` + +```go title="Example" +// GET http://google.com/search + +app.Get("/", func(c *fiber.Ctx) error { + c.Hostname() // "google.com" + + // ... +}) +``` + +> _Returned value is only valid within the handler. Do not store any references. +> Make copies or use the_ [_**`Immutable`**_](ctx.md) _setting instead._ [_Read more..._](../#zero-allocation) + +## IP + +Returns the remote IP address of the request. + +```go title="Signature" +func (c *Ctx) IP() string +``` + +```go title="Example" +app.Get("/", func(c *fiber.Ctx) error { + c.IP() // "127.0.0.1" + + // ... +}) +``` + +When registering the proxy request header in the fiber app, the ip address of the header is returned [(Fiber configuration)](fiber.md#config) + +```go +app := fiber.New(fiber.Config{ + ProxyHeader: fiber.HeaderXForwardedFor, +}) +``` + +## IPs + +Returns an array of IP addresses specified in the [X-Forwarded-For](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-For) request header. + +```go title="Signature" +func (c *Ctx) IPs() []string +``` + +```go title="Example" +// X-Forwarded-For: proxy1, 127.0.0.1, proxy3 + +app.Get("/", func(c *fiber.Ctx) error { + c.IPs() // ["proxy1", "127.0.0.1", "proxy3"] + + // ... +}) +``` + +:::caution +Improper use of the X-Forwarded-For header can be a security risk. For details, see the [Security and privacy concerns](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-For#security_and_privacy_concerns) section. +::: + +## Is + +Returns the matching **content type**, if the incoming request’s [Content-Type](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Type) HTTP header field matches the [MIME type](https://developer.mozilla.org/ru/docs/Web/HTTP/Basics_of_HTTP/MIME_types) specified by the type parameter. + +:::info +If the request has **no** body, it returns **false**. +::: + +```go title="Signature" +func (c *Ctx) Is(extension string) bool +``` + +```go title="Example" +// Content-Type: text/html; charset=utf-8 + +app.Get("/", func(c *fiber.Ctx) error { + c.Is("html") // true + c.Is(".html") // true + c.Is("json") // false + + // ... +}) +``` + +## IsFromLocal + +Returns true if request came from localhost + +```go title="Signature" +func (c *Ctx) IsFromLocal() bool { +``` + +```go title="Example" + +app.Get("/", func(c *fiber.Ctx) error { + // If request came from localhost, return true else return false + c.IsFromLocal() + + // ... +}) +``` + +## JSON + +Converts any **interface** or **string** to JSON using the [encoding/json](https://pkg.go.dev/encoding/json) package. + +:::info +JSON also sets the content header to the `ctype` parameter. If no `ctype` is passed in, the header is set to `application/json`. +::: + +```go title="Signature" +func (c *Ctx) JSON(data interface{}, ctype ...string) error +``` + +```go title="Example" +type SomeStruct struct { + Name string + Age uint8 +} + +app.Get("/json", func(c *fiber.Ctx) error { + // Create data struct: + data := SomeStruct{ + Name: "Grame", + Age: 20, + } + + return c.JSON(data) + // => Content-Type: application/json + // => "{"Name": "Grame", "Age": 20}" + + return c.JSON(fiber.Map{ + "name": "Grame", + "age": 20, + }) + // => Content-Type: application/json + // => "{"name": "Grame", "age": 20}" + + return c.JSON(fiber.Map{ + "type": "https://example.com/probs/out-of-credit", + "title": "You do not have enough credit.", + "status": 403, + "detail": "Your current balance is 30, but that costs 50.", + "instance": "/account/12345/msgs/abc", + }, "application/problem+json") + // => Content-Type: application/problem+json + // => "{ + // => "type": "https://example.com/probs/out-of-credit", + // => "title": "You do not have enough credit.", + // => "status": 403, + // => "detail": "Your current balance is 30, but that costs 50.", + // => "instance": "/account/12345/msgs/abc", + // => }" +}) +``` + +## JSONP + +Sends a JSON response with JSONP support. This method is identical to [JSON](ctx.md#json), except that it opts-in to JSONP callback support. By default, the callback name is simply callback. + +Override this by passing a **named string** in the method. + +```go title="Signature" +func (c *Ctx) JSONP(data interface{}, callback ...string) error +``` + +```go title="Example" +type SomeStruct struct { + name string + age uint8 +} + +app.Get("/", func(c *fiber.Ctx) error { + // Create data struct: + data := SomeStruct{ + name: "Grame", + age: 20, + } + + return c.JSONP(data) + // => callback({"name": "Grame", "age": 20}) + + return c.JSONP(data, "customFunc") + // => customFunc({"name": "Grame", "age": 20}) +}) +``` + +## Links + +Joins the links followed by the property to populate the response’s [Link](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Link) HTTP header field. + +```go title="Signature" +func (c *Ctx) Links(link ...string) +``` + +```go title="Example" +app.Get("/", func(c *fiber.Ctx) error { + c.Links( + "http://api.example.com/users?page=2", "next", + "http://api.example.com/users?page=5", "last", + ) + // Link: ; rel="next", + // ; rel="last" + + // ... +}) +``` + +## Locals + +A method that stores variables scoped to the request and, therefore, are available only to the routes that match the request. The stored variables are removed after the request is handled. If any of the stored data implements the `io.Closer` interface, its `Close` method will be called before it's removed. + +:::tip +This is useful if you want to pass some **specific** data to the next middleware. Remember to perform type assertions when retrieving the data to ensure it is of the expected type. You can also use a non-exported type as a key to avoid collisions. +::: + +```go title="Signature" +func (c *Ctx) Locals(key interface{}, value ...interface{}) interface{} +``` + +```go title="Example" +type keyType struct{} +var userKey keyType + +app.Use(func(c *fiber.Ctx) error { + c.Locals(userKey, "admin") // Stores the string "admin" under a non-exported type key + return c.Next() +}) + +app.Get("/admin", func(c *fiber.Ctx) error { + user, ok := c.Locals(userKey).(string) // Retrieves the data stored under the key and performs a type assertion + if ok && user == "admin" { + return c.Status(fiber.StatusOK).SendString("Welcome, admin!") + } + return c.SendStatus(fiber.StatusForbidden) +}) +``` + +## Location + +Sets the response [Location](https://developer.mozilla.org/ru/docs/Web/HTTP/Headers/Location) HTTP header to the specified path parameter. + +```go title="Signature" +func (c *Ctx) Location(path string) +``` + +```go title="Example" +app.Post("/", func(c *fiber.Ctx) error { + c.Location("http://example.com") + + c.Location("/foo/bar") + + return nil +}) +``` + +## Method + +Returns a string corresponding to the HTTP method of the request: `GET`, `POST`, `PUT`, and so on. +Optionally, you could override the method by passing a string. + +```go title="Signature" +func (c *Ctx) Method(override ...string) string +``` + +```go title="Example" +app.Post("/", func(c *fiber.Ctx) error { + c.Method() // "POST" + + c.Method("GET") + c.Method() // GET + + // ... +}) +``` + +## MultipartForm + +To access multipart form entries, you can parse the binary with `MultipartForm()`. This returns a `map[string][]string`, so given a key, the value will be a string slice. + +```go title="Signature" +func (c *Ctx) MultipartForm() (*multipart.Form, error) +``` + +```go title="Example" +app.Post("/", func(c *fiber.Ctx) error { + // Parse the multipart form: + if form, err := c.MultipartForm(); err == nil { + // => *multipart.Form + + if token := form.Value["token"]; len(token) > 0 { + // Get key value: + fmt.Println(token[0]) + } + + // Get all files from "documents" key: + files := form.File["documents"] + // => []*multipart.FileHeader + + // Loop through files: + for _, file := range files { + fmt.Println(file.Filename, file.Size, file.Header["Content-Type"][0]) + // => "tutorial.pdf" 360641 "application/pdf" + + // Save the files to disk: + if err := c.SaveFile(file, fmt.Sprintf("./%s", file.Filename)); err != nil { + return err + } + } + } + + return err +}) +``` + +## Next + +When **Next** is called, it executes the next method in the stack that matches the current route. You can pass an error struct within the method that will end the chaining and call the [error handler](https://docs.gofiber.io/guide/error-handling). + +```go title="Signature" +func (c *Ctx) Next() error +``` + +```go title="Example" +app.Get("/", func(c *fiber.Ctx) error { + fmt.Println("1st route!") + return c.Next() +}) + +app.Get("*", func(c *fiber.Ctx) error { + fmt.Println("2nd route!") + return c.Next() +}) + +app.Get("/", func(c *fiber.Ctx) error { + fmt.Println("3rd route!") + return c.SendString("Hello, World!") +}) +``` + +## OriginalURL + +Returns the original request URL. + +```go title="Signature" +func (c *Ctx) OriginalURL() string +``` + +```go title="Example" +// GET http://example.com/search?q=something + +app.Get("/", func(c *fiber.Ctx) error { + c.OriginalURL() // "/search?q=something" + + // ... +}) +``` + +> _Returned value is only valid within the handler. Do not store any references. +> Make copies or use the_ [_**`Immutable`**_](ctx.md) _setting instead._ [_Read more..._](../#zero-allocation) + +## Params + +Method can be used to get the route parameters, you could pass an optional default value that will be returned if the param key does not exist. + +:::info +Defaults to empty string \(`""`\), if the param **doesn't** exist. +::: + +```go title="Signature" +func (c *Ctx) Params(key string, defaultValue ...string) string +``` + +```go title="Example" +// GET http://example.com/user/fenny +app.Get("/user/:name", func(c *fiber.Ctx) error { + c.Params("name") // "fenny" + + // ... +}) + +// GET http://example.com/user/fenny/123 +app.Get("/user/*", func(c *fiber.Ctx) error { + c.Params("*") // "fenny/123" + c.Params("*1") // "fenny/123" + + // ... +}) +``` + +Unnamed route parameters\(\*, +\) can be fetched by the **character** and the **counter** in the route. + +```go title="Example" +// ROUTE: /v1/*/shop/* +// GET: /v1/brand/4/shop/blue/xs +c.Params("*1") // "brand/4" +c.Params("*2") // "blue/xs" +``` + +For reasons of **downward compatibility**, the first parameter segment for the parameter character can also be accessed without the counter. + +```go title="Example" +app.Get("/v1/*/shop/*", func(c *fiber.Ctx) error { + c.Params("*") // outputs the values of the first wildcard segment +}) +``` + +> _Returned value is only valid within the handler. Do not store any references. +> Make copies or use the_ [_**`Immutable`**_](ctx.md) _setting instead._ [_Read more..._](../#zero-allocation) + +## ParamsInt + +Method can be used to get an integer from the route parameters. +Please note if that parameter is not in the request, zero +will be returned. If the parameter is NOT a number, zero and an error +will be returned + +:::info +Defaults to the integer zero \(`0`\), if the param **doesn't** exist. +::: + +```go title="Signature" +func (c *Ctx) ParamsInt(key string) (int, error) +``` + +```go title="Example" +// GET http://example.com/user/123 +app.Get("/user/:id", func(c *fiber.Ctx) error { + id, err := c.ParamsInt("id") // int 123 and no error + + // ... +}) + +``` + +This method is equivalent of using `atoi` with ctx.Params + +## ParamsParser + +This method is similar to BodyParser, but for path parameters. It is important to use the struct tag "params". For example, if you want to parse a path parameter with a field called Pass, you would use a struct field of params:"pass" + +```go title="Signature" +func (c *Ctx) ParamsParser(out interface{}) error +``` + +```go title="Example" +// GET http://example.com/user/111 +app.Get("/user/:id", func(c *fiber.Ctx) error { + param := struct {ID uint `params:"id"`}{} + + c.ParamsParser(¶m) // "{"id": 111}" + + // ... +}) + +``` + +## Path + +Contains the path part of the request URL. Optionally, you could override the path by passing a string. For internal redirects, you might want to call [RestartRouting](ctx.md#restartrouting) instead of [Next](ctx.md#next). + +```go title="Signature" +func (c *Ctx) Path(override ...string) string +``` + +```go title="Example" +// GET http://example.com/users?sort=desc + +app.Get("/users", func(c *fiber.Ctx) error { + c.Path() // "/users" + + c.Path("/john") + c.Path() // "/john" + + // ... +}) +``` + +## Protocol + +Contains the request protocol string: `http` or `https` for **TLS** requests. + +```go title="Signature" +func (c *Ctx) Protocol() string +``` + +```go title="Example" +// GET http://example.com + +app.Get("/", func(c *fiber.Ctx) error { + c.Protocol() // "http" + + // ... +}) +``` + +## Queries + +Queries is a function that returns an object containing a property for each query string parameter in the route. + +```go title="Signature" +func (c *Ctx) Queries() map[string]string +``` + +```go title="Example" +// GET http://example.com/?name=alex&want_pizza=false&id= + +app.Get("/", func(c *fiber.Ctx) error { + m := c.Queries() + m["name"] // "alex" + m["want_pizza"] // "false" + m["id"] // "" + // ... +}) +``` + +```go title="Example" +// GET http://example.com/?field1=value1&field1=value2&field2=value3 + +app.Get("/", func (c *fiber.Ctx) error { + m := c.Queries() + m["field1"] // "value2" + m["field2"] // value3 +}) +``` + +```go title="Example" +// GET http://example.com/?list_a=1&list_a=2&list_a=3&list_b[]=1&list_b[]=2&list_b[]=3&list_c=1,2,3 + +app.Get("/", func(c *fiber.Ctx) error { + m := c.Queries() + m["list_a"] // "3" + m["list_b[]"] // "3" + m["list_c"] // "1,2,3" +}) +``` + +```go title="Example" +// GET /api/posts?filters.author.name=John&filters.category.name=Technology + +app.Get("/", func(c *fiber.Ctx) error { + m := c.Queries() + m["filters.author.name"] // John + m["filters.category.name"] // Technology +}) +``` + +```go title="Example" +// GET /api/posts?tags=apple,orange,banana&filters[tags]=apple,orange,banana&filters[category][name]=fruits&filters.tags=apple,orange,banana&filters.category.name=fruits + +app.Get("/", func(c *fiber.Ctx) error { + m := c.Queries() + m["tags"] // apple,orange,banana + m["filters[tags]"] // apple,orange,banana + m["filters[category][name]"] // fruits + m["filters.tags"] // apple,orange,banana + m["filters.category.name"] // fruits +}) +``` + +## Query + +This property is an object containing a property for each query string parameter in the route, you could pass an optional default value that will be returned if the query key does not exist. + +:::info +If there is **no** query string, it returns an **empty string**. +::: + +```go title="Signature" +func (c *Ctx) Query(key string, defaultValue ...string) string +``` + +```go title="Example" +// GET http://example.com/?order=desc&brand=nike + +app.Get("/", func(c *fiber.Ctx) error { + c.Query("order") // "desc" + c.Query("brand") // "nike" + c.Query("empty", "nike") // "nike" + + // ... +}) +``` + +> _Returned value is only valid within the handler. Do not store any references. +> Make copies or use the_ [_**`Immutable`**_](ctx.md) _setting instead._ [_Read more..._](../#zero-allocation) + +## QueryBool + +This property is an object containing a property for each query boolean parameter in the route, you could pass an optional default value that will be returned if the query key does not exist. + +:::caution +Please note if that parameter is not in the request, false will be returned. +If the parameter is not a boolean, it is still tried to be converted and usually returned as false. +::: + +```go title="Signature" +func (c *Ctx) QueryBool(key string, defaultValue ...bool) bool +``` + +```go title="Example" +// GET http://example.com/?name=alex&want_pizza=false&id= + +app.Get("/", func(c *fiber.Ctx) error { + c.QueryBool("want_pizza") // false + c.QueryBool("want_pizza", true) // false + c.QueryBool("name") // false + c.QueryBool("name", true) // true + c.QueryBool("id") // false + c.QueryBool("id", true) // true + + // ... +}) +``` + +## QueryFloat + +This property is an object containing a property for each query float64 parameter in the route, you could pass an optional default value that will be returned if the query key does not exist. + +:::caution +Please note if that parameter is not in the request, zero will be returned. +If the parameter is not a number, it is still tried to be converted and usually returned as 1. +::: + +:::info +Defaults to the float64 zero \(`0`\), if the param **doesn't** exist. +::: + +```go title="Signature" +func (c *Ctx) QueryFloat(key string, defaultValue ...float64) float64 +``` + +```go title="Example" +// GET http://example.com/?name=alex&amount=32.23&id= + +app.Get("/", func(c *fiber.Ctx) error { + c.QueryFloat("amount") // 32.23 + c.QueryFloat("amount", 3) // 32.23 + c.QueryFloat("name", 1) // 1 + c.QueryFloat("name") // 0 + c.QueryFloat("id", 3) // 3 + + // ... +}) +``` + +## QueryInt + +This property is an object containing a property for each query integer parameter in the route, you could pass an optional default value that will be returned if the query key does not exist. + +:::caution +Please note if that parameter is not in the request, zero will be returned. +If the parameter is not a number, it is still tried to be converted and usually returned as 1. +::: + +:::info +Defaults to the integer zero \(`0`\), if the param **doesn't** exist. +::: + +```go title="Signature" +func (c *Ctx) QueryInt(key string, defaultValue ...int) int +``` + +```go title="Example" +// GET http://example.com/?name=alex&wanna_cake=2&id= + +app.Get("/", func(c *fiber.Ctx) error { + c.QueryInt("wanna_cake", 1) // 2 + c.QueryInt("name", 1) // 1 + c.QueryInt("id", 1) // 1 + c.QueryInt("id") // 0 + + // ... +}) +``` + +## QueryParser + +This method is similar to [BodyParser](ctx.md#bodyparser), but for query parameters. +It is important to use the struct tag "query". For example, if you want to parse a query parameter with a field called Pass, you would use a struct field of `query:"pass"`. + +```go title="Signature" +func (c *Ctx) QueryParser(out interface{}) error +``` + +```go title="Example" +// Field names should start with an uppercase letter +type Person struct { + Name string `query:"name"` + Pass string `query:"pass"` + Products []string `query:"products"` +} + +app.Get("/", func(c *fiber.Ctx) error { + p := new(Person) + + if err := c.QueryParser(p); err != nil { + return err + } + + log.Println(p.Name) // john + log.Println(p.Pass) // doe + // fiber.Config{EnableSplittingOnParsers: false} - default + log.Println(p.Products) // ["shoe,hat"] + // fiber.Config{EnableSplittingOnParsers: true} + // log.Println(p.Products) // ["shoe", "hat"] + + + // ... +}) +// Run tests with the following curl command + +// curl "http://localhost:3000/?name=john&pass=doe&products=shoe,hat" +``` + +:::info +For more parser settings please look here [Config](fiber.md#config) +::: + +## Range + +A struct containing the type and a slice of ranges will be returned. + +```go title="Signature" +func (c *Ctx) Range(size int) (Range, error) +``` + +```go title="Example" +// Range: bytes=500-700, 700-900 +app.Get("/", func(c *fiber.Ctx) error { + b := c.Range(1000) + if b.Type == "bytes" { + for r := range r.Ranges { + fmt.Println(r) + // [500, 700] + } + } +}) +``` + +## Redirect + +Redirects to the URL derived from the specified path, with specified status, a positive integer that corresponds to an HTTP status code. + +:::info +If **not** specified, status defaults to **302 Found**. +::: + +```go title="Signature" +func (c *Ctx) Redirect(location string, status ...int) error +``` + +```go title="Example" +app.Get("/coffee", func(c *fiber.Ctx) error { + return c.Redirect("/teapot") +}) + +app.Get("/teapot", func(c *fiber.Ctx) error { + return c.Status(fiber.StatusTeapot).Send("🍵 short and stout 🍵") +}) +``` + +```go title="More examples" +app.Get("/", func(c *fiber.Ctx) error { + return c.Redirect("/foo/bar") + return c.Redirect("../login") + return c.Redirect("http://example.com") + return c.Redirect("http://example.com", 301) +}) +``` + +## RedirectToRoute + +Redirects to the specific route along with the parameters and with specified status, a positive integer that corresponds to an HTTP status code. + +:::info +If **not** specified, status defaults to **302 Found**. +::: + +:::info +If you want to send queries to route, you must add **"queries"** key typed as **map[string]string** to params. +::: + +```go title="Signature" +func (c *Ctx) RedirectToRoute(routeName string, params fiber.Map, status ...int) error +``` + +```go title="Example" +app.Get("/", func(c *fiber.Ctx) error { + // /user/fiber + return c.RedirectToRoute("user", fiber.Map{ + "name": "fiber" + }) +}) + +app.Get("/with-queries", func(c *fiber.Ctx) error { + // /user/fiber?data[0][name]=john&data[0][age]=10&test=doe + return c.RedirectToRoute("user", fiber.Map{ + "name": "fiber", + "queries": map[string]string{"data[0][name]": "john", "data[0][age]": "10", "test": "doe"}, + }) +}) + +app.Get("/user/:name", func(c *fiber.Ctx) error { + return c.SendString(c.Params("name")) +}).Name("user") +``` + +## RedirectBack + +Redirects back to refer URL. It redirects to fallback URL if refer header doesn't exists, with specified status, a positive integer that corresponds to an HTTP status code. + +:::info +If **not** specified, status defaults to **302 Found**. +::: + +```go title="Signature" +func (c *Ctx) RedirectBack(fallback string, status ...int) error +``` + +```go title="Example" +app.Get("/", func(c *fiber.Ctx) error { + return c.SendString("Home page") +}) +app.Get("/test", func(c *fiber.Ctx) error { + c.Set("Content-Type", "text/html") + return c.SendString(`Back`) +}) + +app.Get("/back", func(c *fiber.Ctx) error { + return c.RedirectBack("/") +}) +``` + +## Render + +Renders a view with data and sends a `text/html` response. By default `Render` uses the default [**Go Template engine**](https://pkg.go.dev/html/template/). If you want to use another View engine, please take a look at our [**Template middleware**](https://docs.gofiber.io/template). + +```go title="Signature" +func (c *Ctx) Render(name string, bind interface{}, layouts ...string) error +``` + +## Request + +Request return the [\*fasthttp.Request](https://godoc.org/github.com/valyala/fasthttp#Request) pointer + +```go title="Signature" +func (c *Ctx) Request() *fasthttp.Request +``` + +```go title="Example" +app.Get("/", func(c *fiber.Ctx) error { + c.Request().Header.Method() + // => []byte("GET") +}) +``` + +## ReqHeaderParser + +This method is similar to [BodyParser](ctx.md#bodyparser), but for request headers. +It is important to use the struct tag "reqHeader". For example, if you want to parse a request header with a field called Pass, you would use a struct field of `reqHeader:"pass"`. + +```go title="Signature" +func (c *Ctx) ReqHeaderParser(out interface{}) error +``` + +```go title="Example" +// Field names should start with an uppercase letter +type Person struct { + Name string `reqHeader:"name"` + Pass string `reqHeader:"pass"` + Products []string `reqHeader:"products"` +} + +app.Get("/", func(c *fiber.Ctx) error { + p := new(Person) + + if err := c.ReqHeaderParser(p); err != nil { + return err + } + + log.Println(p.Name) // john + log.Println(p.Pass) // doe + log.Println(p.Products) // [shoe, hat] + + // ... +}) +// Run tests with the following curl command + +// curl "http://localhost:3000/" -H "name: john" -H "pass: doe" -H "products: shoe,hat" +``` + +## Response + +Response return the [\*fasthttp.Response](https://godoc.org/github.com/valyala/fasthttp#Response) pointer + +```go title="Signature" +func (c *Ctx) Response() *fasthttp.Response +``` + +```go title="Example" +app.Get("/", func(c *fiber.Ctx) error { + c.Response().BodyWriter().Write([]byte("Hello, World!")) + // => "Hello, World!" + return nil +}) +``` + +## RestartRouting + +Instead of executing the next method when calling [Next](ctx.md#next), **RestartRouting** restarts execution from the first method that matches the current route. This may be helpful after overriding the path, i. e. an internal redirect. Note that handlers might be executed again which could result in an infinite loop. + +```go title="Signature" +func (c *Ctx) RestartRouting() error +``` + +```go title="Example" +app.Get("/new", func(c *fiber.Ctx) error { + return c.SendString("From /new") +}) + +app.Get("/old", func(c *fiber.Ctx) error { + c.Path("/new") + return c.RestartRouting() +}) +``` + +## Route + +Returns the matched [Route](https://pkg.go.dev/github.com/gofiber/fiber?tab=doc#Route) struct. + +```go title="Signature" +func (c *Ctx) Route() *Route +``` + +```go title="Example" +// http://localhost:8080/hello + + +app.Get("/hello/:name", func(c *fiber.Ctx) error { + r := c.Route() + fmt.Println(r.Method, r.Path, r.Params, r.Handlers) + // GET /hello/:name handler [name] + + // ... +}) +``` + +:::caution +Do not rely on `c.Route()` in middlewares **before** calling `c.Next()` - `c.Route()` returns the **last executed route**. +::: + +```go title="Example" +func MyMiddleware() fiber.Handler { + return func(c *fiber.Ctx) error { + beforeNext := c.Route().Path // Will be '/' + err := c.Next() + afterNext := c.Route().Path // Will be '/hello/:name' + return err + } +} +``` + +## SaveFile + +Method is used to save **any** multipart file to disk. + +```go title="Signature" +func (c *Ctx) SaveFile(fh *multipart.FileHeader, path string) error +``` + +```go title="Example" +app.Post("/", func(c *fiber.Ctx) error { + // Parse the multipart form: + if form, err := c.MultipartForm(); err == nil { + // => *multipart.Form + + // Get all files from "documents" key: + files := form.File["documents"] + // => []*multipart.FileHeader + + // Loop through files: + for _, file := range files { + fmt.Println(file.Filename, file.Size, file.Header["Content-Type"][0]) + // => "tutorial.pdf" 360641 "application/pdf" + + // Save the files to disk: + if err := c.SaveFile(file, fmt.Sprintf("./%s", file.Filename)); err != nil { + return err + } + } + return err + } +}) +``` + +## SaveFileToStorage + +Method is used to save **any** multipart file to an external storage system. + +```go title="Signature" +func (c *Ctx) SaveFileToStorage(fileheader *multipart.FileHeader, path string, storage Storage) error +``` + +```go title="Example" +storage := memory.New() + +app.Post("/", func(c *fiber.Ctx) error { + // Parse the multipart form: + if form, err := c.MultipartForm(); err == nil { + // => *multipart.Form + + // Get all files from "documents" key: + files := form.File["documents"] + // => []*multipart.FileHeader + + // Loop through files: + for _, file := range files { + fmt.Println(file.Filename, file.Size, file.Header["Content-Type"][0]) + // => "tutorial.pdf" 360641 "application/pdf" + + // Save the files to storage: + if err := c.SaveFileToStorage(file, fmt.Sprintf("./%s", file.Filename), storage); err != nil { + return err + } + } + return err + } +}) +``` + +## Secure + +A boolean property that is `true` , if a **TLS** connection is established. + +```go title="Signature" +func (c *Ctx) Secure() bool +``` + +```go title="Example" +// Secure() method is equivalent to: +c.Protocol() == "https" +``` + +## Send + +Sets the HTTP response body. + +```go title="Signature" +func (c *Ctx) Send(body []byte) error +``` + +```go title="Example" +app.Get("/", func(c *fiber.Ctx) error { + return c.Send([]byte("Hello, World!")) // => "Hello, World!" +}) +``` + +Fiber also provides `SendString` and `SendStream` methods for raw inputs. + +:::tip +Use this if you **don't need** type assertion, recommended for **faster** performance. +::: + +```go title="Signature" +func (c *Ctx) SendString(body string) error +func (c *Ctx) SendStream(stream io.Reader, size ...int) error +``` + +```go title="Example" +app.Get("/", func(c *fiber.Ctx) error { + return c.SendString("Hello, World!") + // => "Hello, World!" + + return c.SendStream(bytes.NewReader([]byte("Hello, World!"))) + // => "Hello, World!" +}) +``` + +## SendFile + +Transfers the file from the given path. Sets the [Content-Type](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Type) response HTTP header field based on the **filenames** extension. + +:::caution +Method doesn´t use **gzipping** by default, set it to **true** to enable. +::: + +```go title="Signature" title="Signature" +func (c *Ctx) SendFile(file string, compress ...bool) error +``` + +```go title="Example" +app.Get("/not-found", func(c *fiber.Ctx) error { + return c.SendFile("./public/404.html"); + + // Disable compression + return c.SendFile("./static/index.html", false); +}) +``` + +:::info +If the file contains an url specific character you have to escape it before passing the file path into the `sendFile` function. +::: + +```go title="Example" +app.Get("/file-with-url-chars", func(c *fiber.Ctx) error { + return c.SendFile(url.PathEscape("hash_sign_#.txt")) +}) +``` + +:::info +For sending files from embedded file system [this functionality](./middleware/filesystem.md#sendfile) can be used +::: + +## SendStatus + +Sets the status code and the correct status message in the body, if the response body is **empty**. + +:::tip +You can find all used status codes and messages [here](https://github.com/gofiber/fiber/blob/dffab20bcdf4f3597d2c74633a7705a517d2c8c2/utils.go#L183-L244). +::: + +```go title="Signature" +func (c *Ctx) SendStatus(status int) error +``` + +```go title="Example" +app.Get("/not-found", func(c *fiber.Ctx) error { + return c.SendStatus(415) + // => 415 "Unsupported Media Type" + + c.SendString("Hello, World!") + return c.SendStatus(415) + // => 415 "Hello, World!" +}) +``` + +## Set + +Sets the response’s HTTP header field to the specified `key`, `value`. + +```go title="Signature" +func (c *Ctx) Set(key string, val string) +``` + +```go title="Example" +app.Get("/", func(c *fiber.Ctx) error { + c.Set("Content-Type", "text/plain") + // => "Content-type: text/plain" + + // ... +}) +``` + +## SetParserDecoder + +Allow you to config BodyParser/QueryParser decoder, base on schema's options, providing possibility to add custom type for parsing. + +```go title="Signature" +func SetParserDecoder(parserConfig fiber.ParserConfig{ + IgnoreUnknownKeys bool, + ParserType []fiber.ParserType{ + Customtype interface{}, + Converter func(string) reflect.Value, + }, + ZeroEmpty bool, + SetAliasTag string, +}) +``` + +```go title="Example" + +type CustomTime time.Time + +// String() returns the time in string +func (ct *CustomTime) String() string { + t := time.Time(*ct).String() + return t +} + +// Register the converter for CustomTime type format as 2006-01-02 +var timeConverter = func(value string) reflect.Value { + fmt.Println("timeConverter", value) + if v, err := time.Parse("2006-01-02", value); err == nil { + return reflect.ValueOf(v) + } + return reflect.Value{} +} + +customTime := fiber.ParserType{ + Customtype: CustomTime{}, + Converter: timeConverter, +} + +// Add setting to the Decoder +fiber.SetParserDecoder(fiber.ParserConfig{ + IgnoreUnknownKeys: true, + ParserType: []fiber.ParserType{customTime}, + ZeroEmpty: true, +}) + +// Example to use CustomType, you pause custom time format not in RFC3339 +type Demo struct { + Date CustomTime `form:"date" query:"date"` + Title string `form:"title" query:"title"` + Body string `form:"body" query:"body"` +} + +app.Post("/body", func(c *fiber.Ctx) error { + var d Demo + c.BodyParser(&d) + fmt.Println("d.Date", d.Date.String()) + return c.JSON(d) +}) + +app.Get("/query", func(c *fiber.Ctx) error { + var d Demo + c.QueryParser(&d) + fmt.Println("d.Date", d.Date.String()) + return c.JSON(d) +}) + +// curl -X POST -F title=title -F body=body -F date=2021-10-20 http://localhost:3000/body + +// curl -X GET "http://localhost:3000/query?title=title&body=body&date=2021-10-20" + +``` + +## SetUserContext + +Sets the user specified implementation for context interface. + +```go title="Signature" +func (c *Ctx) SetUserContext(ctx context.Context) +``` + +```go title="Example" +app.Get("/", func(c *fiber.Ctx) error { + ctx := context.Background() + c.SetUserContext(ctx) + // Here ctx could be any context implementation + + // ... +}) +``` + +## Stale + +[https://expressjs.com/en/4x/api.html\#req.stale](https://expressjs.com/en/4x/api.html#req.stale) + +```go title="Signature" +func (c *Ctx) Stale() bool +``` + +## Status + +Sets the HTTP status for the response. + +:::info +Method is a **chainable**. +::: + +```go title="Signature" +func (c *Ctx) Status(status int) *Ctx +``` + +```go title="Example" +app.Get("/fiber", func(c *fiber.Ctx) error { + c.Status(fiber.StatusOK) + return nil +} + +app.Get("/hello", func(c *fiber.Ctx) error { + return c.Status(fiber.StatusBadRequest).SendString("Bad Request") +} + +app.Get("/world", func(c *fiber.Ctx) error { + return c.Status(fiber.StatusNotFound).SendFile("./public/gopher.png") +}) +``` + +## Subdomains + +Returns a string slice of subdomains in the domain name of the request. + +The application property subdomain offset, which defaults to `2`, is used for determining the beginning of the subdomain segments. + +```go title="Signature" +func (c *Ctx) Subdomains(offset ...int) []string +``` + +```go title="Example" +// Host: "tobi.ferrets.example.com" + +app.Get("/", func(c *fiber.Ctx) error { + c.Subdomains() // ["ferrets", "tobi"] + c.Subdomains(1) // ["tobi"] + + // ... +}) +``` + +## Type + +Sets the [Content-Type](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Type) HTTP header to the MIME type listed [here](https://github.com/nginx/nginx/blob/master/conf/mime.types) specified by the file **extension**. + +```go title="Signature" +func (c *Ctx) Type(ext string, charset ...string) *Ctx +``` + +```go title="Example" +app.Get("/", func(c *fiber.Ctx) error { + c.Type(".html") // => "text/html" + c.Type("html") // => "text/html" + c.Type("png") // => "image/png" + + c.Type("json", "utf-8") // => "application/json; charset=utf-8" + + // ... +}) +``` + +## UserContext + +UserContext returns a context implementation that was set by user earlier +or returns a non-nil, empty context, if it was not set earlier. + +```go title="Signature" +func (c *Ctx) UserContext() context.Context +``` + +```go title="Example" +app.Get("/", func(c *fiber.Ctx) error { + ctx := c.UserContext() + // ctx is context implementation set by user + + // ... +}) +``` + +## Vary + +Adds the given header field to the [Vary](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Vary) response header. This will append the header, if not already listed, otherwise leaves it listed in the current location. + +:::info +Multiple fields are **allowed**. +::: + +```go title="Signature" +func (c *Ctx) Vary(fields ...string) +``` + +```go title="Example" +app.Get("/", func(c *fiber.Ctx) error { + c.Vary("Origin") // => Vary: Origin + c.Vary("User-Agent") // => Vary: Origin, User-Agent + + // No duplicates + c.Vary("Origin") // => Vary: Origin, User-Agent + + c.Vary("Accept-Encoding", "Accept") + // => Vary: Origin, User-Agent, Accept-Encoding, Accept + + // ... +}) +``` + +## Write + +Write adopts the Writer interface + +```go title="Signature" +func (c *Ctx) Write(p []byte) (n int, err error) +``` + +```go title="Example" +app.Get("/", func(c *fiber.Ctx) error { + c.Write([]byte("Hello, World!")) // => "Hello, World!" + + fmt.Fprintf(c, "%s\n", "Hello, World!") // "Hello, World!Hello, World!" +}) +``` + +## Writef + +Writef adopts the string with variables + +```go title="Signature" +func (c *Ctx) Writef(f string, a ...interface{}) (n int, err error) +``` + +```go title="Example" +app.Get("/", func(c *fiber.Ctx) error { + world := "World!" + c.Writef("Hello, %s", world) // => "Hello, World!" + + fmt.Fprintf(c, "%s\n", "Hello, World!") // "Hello, World!Hello, World!" +}) +``` + +## WriteString + +WriteString adopts the string + +```go title="Signature" +func (c *Ctx) WriteString(s string) (n int, err error) +``` + +```go title="Example" +app.Get("/", func(c *fiber.Ctx) error { + c.WriteString("Hello, World!") // => "Hello, World!" + + fmt.Fprintf(c, "%s\n", "Hello, World!") // "Hello, World!Hello, World!" +}) +``` + +## XHR + +A Boolean property, that is `true`, if the request’s [X-Requested-With](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers) header field is [XMLHttpRequest](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest), indicating that the request was issued by a client library \(such as [jQuery](https://api.jquery.com/jQuery.ajax/)\). + +```go title="Signature" +func (c *Ctx) XHR() bool +``` + +```go title="Example" +// X-Requested-With: XMLHttpRequest + +app.Get("/", func(c *fiber.Ctx) error { + c.XHR() // true + + // ... +}) +``` + +## XML + +Converts any **interface** or **string** to XML using the standard `encoding/xml` package. + +:::info +XML also sets the content header to **application/xml**. +::: + +```go title="Signature" +func (c *Ctx) XML(data interface{}) error +``` + +```go title="Example" +type SomeStruct struct { + XMLName xml.Name `xml:"Fiber"` + Name string `xml:"Name"` + Age uint8 `xml:"Age"` +} + +app.Get("/", func(c *fiber.Ctx) error { + // Create data struct: + data := SomeStruct{ + Name: "Grame", + Age: 20, + } + + return c.XML(data) + // + // Grame + // 20 + // +}) +``` diff --git a/docs/api/fiber.md b/docs/api/fiber.md new file mode 100644 index 0000000..1f9a91b --- /dev/null +++ b/docs/api/fiber.md @@ -0,0 +1,120 @@ +--- +id: fiber +title: 📦 Fiber +description: Fiber represents the fiber package where you start to create an instance. +sidebar_position: 1 +--- + +## New + +This method creates a new **App** named instance. You can pass optional [config ](#config)when creating a new instance. + +```go title="Signature" +func New(config ...Config) *App +``` + +```go title="Example" +// Default config +app := fiber.New() + +// ... +``` + +## Config + +You can pass an optional Config when creating a new Fiber instance. + +```go title="Example" +// Custom config +app := fiber.New(fiber.Config{ + Prefork: true, + CaseSensitive: true, + StrictRouting: true, + ServerHeader: "Fiber", + AppName: "Test App v1.0.1", +}) + +// ... +``` + +**Config fields** + +| Property | Type | Description | Default | +| ---------------------------- | --------------------- |--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| --------------------- | +| AppName | `string` | This allows to setup app name for the app | `""` | +| BodyLimit | `int` | Sets the maximum allowed size for a request body, if the size exceeds the configured limit, it sends `413 - Request Entity Too Large` response. | `4 * 1024 * 1024` | +| CaseSensitive | `bool` | When enabled, `/Foo` and `/foo` are different routes. When disabled, `/Foo`and `/foo` are treated the same. | `false` | +| ColorScheme | [`Colors`](https://github.com/gofiber/fiber/blob/master/color.go) | You can define custom color scheme. They'll be used for startup message, route list and some middlewares. | [`DefaultColors`](https://github.com/gofiber/fiber/blob/master/color.go) | +| CompressedFileSuffix | `string` | Adds a suffix to the original file name and tries saving the resulting compressed file under the new file name. | `".fiber.gz"` | +| Concurrency | `int` | Maximum number of concurrent connections. | `256 * 1024` | +| DisableDefaultContentType | `bool` | When set to true, causes the default Content-Type header to be excluded from the Response. | `false` | +| DisableDefaultDate | `bool` | When set to true causes the default date header to be excluded from the response. | `false` | +| DisableHeaderNormalizing | `bool` | By default all header names are normalized: conteNT-tYPE -> Content-Type | `false` | +| DisableKeepalive | `bool` | Disable keep-alive connections, the server will close incoming connections after sending the first response to the client | `false` | +| DisablePreParseMultipartForm | `bool` | Will not pre parse Multipart Form data if set to true. This option is useful for servers that desire to treat multipart form data as a binary blob, or choose when to parse the data. | `false` | +| DisableStartupMessage | `bool` | When set to true, it will not print out debug information | `false` | +| ETag | `bool` | Enable or disable ETag header generation, since both weak and strong etags are generated using the same hashing method \(CRC-32\). Weak ETags are the default when enabled. | `false` | +| EnableIPValidation | `bool` | If set to true, `c.IP()` and `c.IPs()` will validate IP addresses before returning them. Also, `c.IP()` will return only the first valid IP rather than just the raw header value that may be a comma seperated string.

**WARNING:** There is a small performance cost to doing this validation. Keep disabled if speed is your only concern and your application is behind a trusted proxy that already validates this header. | `false` | +| EnablePrintRoutes | `bool` | EnablePrintRoutes enables print all routes with their method, path, name and handler.. | `false` | +| EnableSplittingOnParsers | `bool` | EnableSplittingOnParsers splits the query/body/header parameters by comma when it's true.

For example, you can use it to parse multiple values from a query parameter like this: `/api?foo=bar,baz == foo[]=bar&foo[]=baz` | `false` | +| EnableTrustedProxyCheck | `bool` | When set to true, fiber will check whether proxy is trusted, using TrustedProxies list.

By default `c.Protocol()` will get value from X-Forwarded-Proto, X-Forwarded-Protocol, X-Forwarded-Ssl or X-Url-Scheme header, `c.IP()` will get value from `ProxyHeader` header, `c.Hostname()` will get value from X-Forwarded-Host header.
If `EnableTrustedProxyCheck` is true, and `RemoteIP` is in the list of `TrustedProxies` `c.Protocol()`, `c.IP()`, and `c.Hostname()` will have the same behaviour when `EnableTrustedProxyCheck` disabled, if `RemoteIP` isn't in the list, `c.Protocol()` will return https in case when tls connection is handled by the app, or http otherwise, `c.IP()` will return RemoteIP() from fasthttp context, `c.Hostname()` will return `fasthttp.Request.URI().Host()` | `false` | +| ErrorHandler | `ErrorHandler` | ErrorHandler is executed when an error is returned from fiber.Handler. Mounted fiber error handlers are retained by the top-level app and applied on prefix associated requests. | `DefaultErrorHandler` | +| GETOnly | `bool` | Rejects all non-GET requests if set to true. This option is useful as anti-DoS protection for servers accepting only GET requests. The request size is limited by ReadBufferSize if GETOnly is set. | `false` | +| IdleTimeout | `time.Duration` | The maximum amount of time to wait for the next request when keep-alive is enabled. If IdleTimeout is zero, the value of ReadTimeout is used. | `nil` | +| Immutable | `bool` | When enabled, all values returned by context methods are immutable. By default, they are valid until you return from the handler; see issue [\#185](https://github.com/gofiber/fiber/issues/185). | `false` | +| JSONDecoder | `utils.JSONUnmarshal` | Allowing for flexibility in using another json library for decoding. | `json.Unmarshal` | +| JSONEncoder | `utils.JSONMarshal` | Allowing for flexibility in using another json library for encoding. | `json.Marshal` | +| Network | `string` | Known networks are "tcp", "tcp4" (IPv4-only), "tcp6" (IPv6-only)

**WARNING:** When prefork is set to true, only "tcp4" and "tcp6" can be chosen. | `NetworkTCP4` | +| PassLocalsToViews | `bool` | PassLocalsToViews Enables passing of the locals set on a fiber.Ctx to the template engine. See our **Template Middleware** for supported engines. | `false` | +| Prefork | `bool` | Enables use of the[`SO_REUSEPORT`](https://lwn.net/Articles/542629/)socket option. This will spawn multiple Go processes listening on the same port. learn more about [socket sharding](https://www.nginx.com/blog/socket-sharding-nginx-release-1-9-1/). **NOTE: if enabled, the application will need to be ran through a shell because prefork mode sets environment variables. If you're using Docker, make sure the app is ran with `CMD ./app` or `CMD ["sh", "-c", "/app"]`. For more info, see** [**this**](https://github.com/gofiber/fiber/issues/1021#issuecomment-730537971) **issue comment.** | `false` | +| ProxyHeader | `string` | This will enable `c.IP()` to return the value of the given header key. By default `c.IP()`will return the Remote IP from the TCP connection, this property can be useful if you are behind a load balancer e.g. _X-Forwarded-\*_. | `""` | +| ReadBufferSize | `int` | per-connection buffer size for requests' reading. This also limits the maximum header size. Increase this buffer if your clients send multi-KB RequestURIs and/or multi-KB headers \(for example, BIG cookies\). | `4096` | +| ReadTimeout | `time.Duration` | The amount of time allowed to read the full request, including the body. The default timeout is unlimited. | `nil` | +| RequestMethods | `[]string` | RequestMethods provides customizibility for HTTP methods. You can add/remove methods as you wish. | `DefaultMethods` | +| ServerHeader | `string` | Enables the `Server` HTTP header with the given value. | `""` | +| StreamRequestBody | `bool` | StreamRequestBody enables request body streaming, and calls the handler sooner when given body is larger then the current limit. | `false` | +| StrictRouting | `bool` | When enabled, the router treats `/foo` and `/foo/` as different. Otherwise, the router treats `/foo` and `/foo/` as the same. | `false` | +| TrustedProxies | `[]string` | Contains the list of trusted proxy IP's. Look at `EnableTrustedProxyCheck` doc.

It can take IP or IP range addresses. If it gets IP range, it iterates all possible addresses. | `[]string*__*` | +| UnescapePath | `bool` | Converts all encoded characters in the route back before setting the path for the context, so that the routing can also work with URL encoded special characters | `false` | +| Views | `Views` | Views is the interface that wraps the Render function. See our **Template Middleware** for supported engines. | `nil` | +| ViewsLayout | `string` | Views Layout is the global layout for all template render until override on Render function. See our **Template Middleware** for supported engines. | `""` | +| WriteBufferSize | `int` | Per-connection buffer size for responses' writing. | `4096` | +| WriteTimeout | `time.Duration` | The maximum duration before timing out writes of the response. The default timeout is unlimited. | `nil` | +| XMLEncoder | `utils.XMLMarshal` | Allowing for flexibility in using another XML library for encoding. | `xml.Marshal` | + +## NewError + +NewError creates a new HTTPError instance with an optional message. + +```go title="Signature" +func NewError(code int, message ...string) *Error +``` + +```go title="Example" +app.Get("/", func(c *fiber.Ctx) error { + return fiber.NewError(782, "Custom error message") +}) +``` + +## IsChild + +IsChild determines if the current process is a result of Prefork. + +```go title="Signature" +func IsChild() bool +``` + +```go title="Example" +// Prefork will spawn child processes +app := fiber.New(fiber.Config{ + Prefork: true, +}) + +if !fiber.IsChild() { + fmt.Println("I'm the parent process") +} else { + fmt.Println("I'm a child process") +} + +// ... +``` diff --git a/docs/api/log.md b/docs/api/log.md new file mode 100644 index 0000000..9b741b1 --- /dev/null +++ b/docs/api/log.md @@ -0,0 +1,155 @@ +--- +id: log +title: 📃 Log +description: Fiber's built-in log package +sidebar_position: 6 +--- + +We can use logs to observe program behavior, diagnose problems, or configure corresponding alarms. +And defining a well structured log can improve search efficiency and facilitate handling of problems. + +Fiber provides a default way to print logs in the standard output. +It also provides several global functions, such as `log.Info`, `log.Errorf`, `log.Warnw`, etc. + +## Log levels + +```go +const ( + LevelTrace Level = iota + LevelDebug + LevelInfo + LevelWarn + LevelError + LevelFatal + LevelPanic +) +``` + +## Custom log + +Fiber provides the `AllLogger` interface for adapting the various log libraries. + +```go +type CommonLogger interface { + Logger + FormatLogger + WithLogger +} + +type AllLogger interface { + CommonLogger + ControlLogger + WithLogger +} +``` + +## Print log +Note: The method of calling the Fatal level will interrupt the program running after printing the log, please use it with caution. +Directly print logs of different levels, which will be entered into messageKey, the default is msg. + +```go +log.Info("Hello, World!") +log.Debug("Are you OK?") +log.Info("42 is the answer to life, the universe, and everything") +log.Warn("We are under attack!") +log.Error("Houston, we have a problem.") +log.Fatal("So Long, and Thanks for All the Fislog.") +log.Panic("The system is down.") +``` +Format and print logs of different levels, all methods end with f + +```go +log.Debugf("Hello %s", "boy") +log.Infof("%d is the answer to life, the universe, and everything", 233) +log.Warnf("We are under attack %s!", "boss") +log.Errorf("%s, we have a problem.", "Master Shifu") +log.Fatalf("So Long, and Thanks for All the %s.", "banana") +``` + +Print a message with the key and value, or `KEYVALS UNPAIRED` if the key and value are not a pair. + +```go +log.Debugw("", "Hello", "boy") +log.Infow("", "number", 233) +log.Warnw("", "job", "boss") +log.Errorw("", "name", "Master Shifu") +log.Fatalw("", "fruit", "banana") +``` + +## Global log +If you are in a project and just want to use a simple log function that can be printed at any time in the global, we provide a global log. + +```go +import "github.com/gofiber/fiber/v2/log" + +log.Info("info") +log.Warn("warn") +``` + +The above is using the default `log.DefaultLogger` standard output. +You can also find an already implemented adaptation under contrib, or use your own implemented Logger and use `log.SetLogger` to set the global log logger. + +```go +import ( + "log" + fiberlog "github.com/gofiber/fiber/v2/log" +) + +var _ log.AllLogger = (*customLogger)(nil) + +type customLogger struct { + stdlog *log.Logger +} + +// ... +// inject your custom logger +fiberlog.SetLogger(customLogger) +``` + +## Set Level +`log.SetLevel` sets the level of logs below which logs will not be output. +The default logger is LevelTrace. + +Note that this method is not **concurrent-safe**. + +```go +import "github.com/gofiber/fiber/v2/log" + +log.SetLevel(log.LevelInfo) +``` +## Set output + +`log.SetOutput` sets the output destination of the logger. The default logger types the log in the console. + +```go +var logger AllLogger = &defaultLogger{ + stdlog: log.New(os.Stderr, "", log.LstdFlags|log.Lshortfile|log.Lmicroseconds), + depth: 4, +} +``` + +Set the output destination to the file. + +```go +// Output to ./test.log file +f, err := os.OpenFile("test.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666) +if err != nil { + return +} +log.SetOutput(f) +``` +Set the output destination to the console and file. + +```go +// Output to ./test.log file +file, _ := os.OpenFile("test.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666) +iw := io.MultiWriter(os.Stdout, file) +log.SetOutput(iw) +``` +## Bind context +Set the context, using the following method will return a `CommonLogger` instance bound to the specified context +```go +commonLogger := log.WithContext(ctx) +commonLogger.Info("info") +``` + diff --git a/docs/api/middleware/_category_.json b/docs/api/middleware/_category_.json new file mode 100644 index 0000000..133ac51 --- /dev/null +++ b/docs/api/middleware/_category_.json @@ -0,0 +1,9 @@ +{ + "label": "🧬 Middleware", + "position": 7, + "collapsed": true, + "link": { + "type": "generated-index", + "description": "Middleware is a function chained in the HTTP request cycle with access to the Context which it uses to perform a specific action, for example, logging every request or enabling CORS." + } +} \ No newline at end of file diff --git a/docs/api/middleware/adaptor.md b/docs/api/middleware/adaptor.md new file mode 100644 index 0000000..64df229 --- /dev/null +++ b/docs/api/middleware/adaptor.md @@ -0,0 +1,169 @@ +--- +id: adaptor +--- + +# Adaptor + +Converter for net/http handlers to/from Fiber request handlers, special thanks to [@arsmn](https://github.com/arsmn)! + +## Signatures +| Name | Signature | Description +| :--- | :--- | :--- +| HTTPHandler | `HTTPHandler(h http.Handler) fiber.Handler` | http.Handler -> fiber.Handler +| HTTPHandlerFunc | `HTTPHandlerFunc(h http.HandlerFunc) fiber.Handler` | http.HandlerFunc -> fiber.Handler +| HTTPMiddleware | `HTTPHandlerFunc(mw func(http.Handler) http.Handler) fiber.Handler` | func(http.Handler) http.Handler -> fiber.Handler +| FiberHandler | `FiberHandler(h fiber.Handler) http.Handler` | fiber.Handler -> http.Handler +| FiberHandlerFunc | `FiberHandlerFunc(h fiber.Handler) http.HandlerFunc` | fiber.Handler -> http.HandlerFunc +| FiberApp | `FiberApp(app *fiber.App) http.HandlerFunc` | Fiber app -> http.HandlerFunc +| ConvertRequest | `ConvertRequest(c *fiber.Ctx, forServer bool) (*http.Request, error)` | fiber.Ctx -> http.Request +| CopyContextToFiberContext | `CopyContextToFiberContext(context interface{}, requestContext *fasthttp.RequestCtx)` | context.Context -> fasthttp.RequestCtx + +## Examples + +### net/http to Fiber +```go +package main + +import ( + "fmt" + "net/http" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/adaptor" +) + +func main() { + // New fiber app + app := fiber.New() + + // http.Handler -> fiber.Handler + app.Get("/", adaptor.HTTPHandler(handler(greet))) + + // http.HandlerFunc -> fiber.Handler + app.Get("/func", adaptor.HTTPHandlerFunc(greet)) + + // Listen on port 3000 + app.Listen(":3000") +} + +func handler(f http.HandlerFunc) http.Handler { + return http.HandlerFunc(f) +} + +func greet(w http.ResponseWriter, r *http.Request) { + fmt.Fprint(w, "Hello World!") +} +``` + +### net/http middleware to Fiber +```go +package main + +import ( + "log" + "net/http" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/adaptor" +) + +func main() { + // New fiber app + app := fiber.New() + + // http middleware -> fiber.Handler + app.Use(adaptor.HTTPMiddleware(logMiddleware)) + + // Listen on port 3000 + app.Listen(":3000") +} + +func logMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + log.Println("log middleware") + next.ServeHTTP(w, r) + }) +} +``` + +### Fiber Handler to net/http +```go +package main + +import ( + "net/http" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/adaptor" +) + +func main() { + // fiber.Handler -> http.Handler + http.Handle("/", adaptor.FiberHandler(greet)) + + // fiber.Handler -> http.HandlerFunc + http.HandleFunc("/func", adaptor.FiberHandlerFunc(greet)) + + // Listen on port 3000 + http.ListenAndServe(":3000", nil) +} + +func greet(c *fiber.Ctx) error { + return c.SendString("Hello World!") +} +``` + +### Fiber App to net/http +```go +package main + +import ( + "net/http" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/adaptor" +) + +func main() { + app := fiber.New() + + app.Get("/greet", greet) + + // Listen on port 3000 + http.ListenAndServe(":3000", adaptor.FiberApp(app)) +} + +func greet(c *fiber.Ctx) error { + return c.SendString("Hello World!") +} +``` + +### Fiber Context to (net/http).Request +```go +package main + +import ( + "net/http" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/adaptor" +) + +func main() { + app := fiber.New() + + app.Get("/greet", greetWithHTTPReq) + + // Listen on port 3000 + http.ListenAndServe(":3000", adaptor.FiberApp(app)) +} + +func greetWithHTTPReq(c *fiber.Ctx) error { + httpReq, err := adaptor.ConvertRequest(c, false) + if err != nil { + return err + } + + return c.SendString("Request URL: " + httpReq.URL.String()) +} +``` diff --git a/docs/api/middleware/basicauth.md b/docs/api/middleware/basicauth.md new file mode 100644 index 0000000..d0f3609 --- /dev/null +++ b/docs/api/middleware/basicauth.md @@ -0,0 +1,85 @@ +--- +id: basicauth +--- + +# BasicAuth + +Basic Authentication middleware for [Fiber](https://github.com/gofiber/fiber) that provides an HTTP basic authentication. It calls the next handler for valid credentials and [401 Unauthorized](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/401) or a custom response for missing or invalid credentials. + +## Signatures + +```go +func New(config Config) fiber.Handler +``` + +## Examples + +Import the middleware package that is part of the Fiber web framework + +```go +import ( + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/basicauth" +) +``` + +After you initiate your Fiber app, you can use the following possibilities: + +```go +// Provide a minimal config +app.Use(basicauth.New(basicauth.Config{ + Users: map[string]string{ + "john": "doe", + "admin": "123456", + }, +})) + +// Or extend your config for customization +app.Use(basicauth.New(basicauth.Config{ + Users: map[string]string{ + "john": "doe", + "admin": "123456", + }, + Realm: "Forbidden", + Authorizer: func(user, pass string) bool { + if user == "john" && pass == "doe" { + return true + } + if user == "admin" && pass == "123456" { + return true + } + return false + }, + Unauthorized: func(c *fiber.Ctx) error { + return c.SendFile("./unauthorized.html") + }, + ContextUsername: "_user", + ContextPassword: "_pass", +})) +``` + +## Config + +| Property | Type | Description | Default | +|:----------------|:----------------------------|:----------------------------------------------------------------------------------------------------------------------------------------------------------------------|:----------------------| +| Next | `func(*fiber.Ctx) bool` | Next defines a function to skip this middleware when returned true. | `nil` | +| Users | `map[string]string` | Users defines the allowed credentials. | `map[string]string{}` | +| Realm | `string` | Realm is a string to define the realm attribute of BasicAuth. The realm identifies the system to authenticate against and can be used by clients to save credentials. | `"Restricted"` | +| Authorizer | `func(string, string) bool` | Authorizer defines a function to check the credentials. It will be called with a username and password and is expected to return true or false to indicate approval. | `nil` | +| Unauthorized | `fiber.Handler` | Unauthorized defines the response body for unauthorized responses. | `nil` | +| ContextUsername | `interface{}` | ContextUsername is the key to store the username in Locals. | `"username"` | +| ContextPassword | `interface{}` | ContextPassword is the key to store the password in Locals. | `"password"` | + +## Default Config + +```go +var ConfigDefault = Config{ + Next: nil, + Users: map[string]string{}, + Realm: "Restricted", + Authorizer: nil, + Unauthorized: nil, + ContextUsername: "username", + ContextPassword: "password", +} +``` diff --git a/docs/api/middleware/cache.md b/docs/api/middleware/cache.md new file mode 100644 index 0000000..e014694 --- /dev/null +++ b/docs/api/middleware/cache.md @@ -0,0 +1,99 @@ +--- +id: cache +--- + +# Cache + +Cache middleware for [Fiber](https://github.com/gofiber/fiber) designed to intercept responses and cache them. This middleware will cache the `Body`, `Content-Type` and `StatusCode` using the `c.Path()` as unique identifier. Special thanks to [@codemicro](https://github.com/codemicro/fiber-cache) for creating this middleware for Fiber core! + +Request Directives
+`Cache-Control: no-cache` will return the up-to-date response but still caches it. You will always get a `miss` cache status.
+`Cache-Control: no-store` will refrain from caching. You will always get the up-to-date response. + +## Signatures + +```go +func New(config ...Config) fiber.Handler +``` + +## Examples + +Import the middleware package that is part of the Fiber web framework + +```go +import ( + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/cache" +) +``` + +After you initiate your Fiber app, you can use the following possibilities: + +```go +// Initialize default config +app.Use(cache.New()) + +// Or extend your config for customization +app.Use(cache.New(cache.Config{ + Next: func(c *fiber.Ctx) bool { + return c.Query("noCache") == "true" + }, + Expiration: 30 * time.Minute, + CacheControl: true, +})) +``` + +Or you can custom key and expire time like this: + +```go +app.Use(cache.New(cache.Config{ + ExpirationGenerator: func(c *fiber.Ctx, cfg *cache.Config) time.Duration { + newCacheTime, _ := strconv.Atoi(c.GetRespHeader("Cache-Time", "600")) + return time.Second * time.Duration(newCacheTime) + }, + KeyGenerator: func(c *fiber.Ctx) string { + return utils.CopyString(c.Path()) + }, +})) + +app.Get("/", func(c *fiber.Ctx) error { + c.Response().Header.Add("Cache-Time", "6000") + return c.SendString("hi") +}) +``` + +## Config + +| Property | Type | Description | Default | +|:---------------------|:------------------------------------------------|:-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:------------------------------------------------------------------| +| Next | `func(*fiber.Ctx) bool` | Next defines a function that is executed before creating the cache entry and can be used to execute the request without cache creation. If an entry already exists, it will be used. If you want to completely bypass the cache functionality in certain cases, you should use the [skip middleware](./skip.md). | `nil` | +| Expiration | `time.Duration` | Expiration is the time that a cached response will live. | `1 * time.Minute` | +| CacheHeader | `string` | CacheHeader is the header on the response header that indicates the cache status, with the possible return values "hit," "miss," or "unreachable." | `X-Cache` | +| CacheControl | `bool` | CacheControl enables client-side caching if set to true. | `false` | +| KeyGenerator | `func(*fiber.Ctx) string` | Key allows you to generate custom keys. | `func(c *fiber.Ctx) string { return utils.CopyString(c.Path()) }` | +| ExpirationGenerator | `func(*fiber.Ctx, *cache.Config) time.Duration` | ExpirationGenerator allows you to generate custom expiration keys based on the request. | `nil` | +| Storage | `fiber.Storage` | Store is used to store the state of the middleware. | In-memory store | +| Store (Deprecated) | `fiber.Storage` | Deprecated: Use Storage instead. | In-memory store | +| Key (Deprecated) | `func(*fiber.Ctx) string` | Deprecated: Use KeyGenerator instead. | `nil` | +| StoreResponseHeaders | `bool` | StoreResponseHeaders allows you to store additional headers generated by next middlewares & handler. | `false` | +| MaxBytes | `uint` | MaxBytes is the maximum number of bytes of response bodies simultaneously stored in cache. | `0` (No limit) | +| Methods | `[]string` | Methods specifies the HTTP methods to cache. | `[]string{fiber.MethodGet, fiber.MethodHead}` | + +## Default Config + +```go +var ConfigDefault = Config{ + Next: nil, + Expiration: 1 * time.Minute, + CacheHeader: "X-Cache", + CacheControl: false, + KeyGenerator: func(c *fiber.Ctx) string { + return utils.CopyString(c.Path()) + }, + ExpirationGenerator: nil, + StoreResponseHeaders: false, + Storage: nil, + MaxBytes: 0, + Methods: []string{fiber.MethodGet, fiber.MethodHead}, +} +``` diff --git a/docs/api/middleware/compress.md b/docs/api/middleware/compress.md new file mode 100644 index 0000000..472f9d9 --- /dev/null +++ b/docs/api/middleware/compress.md @@ -0,0 +1,85 @@ +--- +id: compress +--- + +# Compress + +Compression middleware for [Fiber](https://github.com/gofiber/fiber) that will compress the response using `gzip`, `deflate` and `brotli` compression depending on the [Accept-Encoding](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Encoding) header. + +:::note +The compression middleware refrains from compressing bodies that are smaller than 200 bytes. This decision is based on the observation that, in such cases, the compressed size is likely to exceed the original size, making compression inefficient. [more](https://github.com/valyala/fasthttp/blob/497922a21ef4b314f393887e9c6147b8c3e3eda4/http.go#L1713-L1715) +::: + +## Signatures + +```go +func New(config ...Config) fiber.Handler +``` + +## Examples + +Import the middleware package that is part of the Fiber web framework + +```go +import ( + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/compress" +) +``` + +After you initiate your Fiber app, you can use the following possibilities: + +```go +// Initialize default config +app.Use(compress.New()) + +// Or extend your config for customization +app.Use(compress.New(compress.Config{ + Level: compress.LevelBestSpeed, // 1 +})) + +// Skip middleware for specific routes +app.Use(compress.New(compress.Config{ + Next: func(c *fiber.Ctx) bool { + return c.Path() == "/dont_compress" + }, + Level: compress.LevelBestSpeed, // 1 +})) +``` + +## Config + +### Config + +| Property | Type | Description | Default | +|:---------|:------------------------|:--------------------------------------------------------------------|:-------------------| +| Next | `func(*fiber.Ctx) bool` | Next defines a function to skip this middleware when returned true. | `nil` | +| Level | `Level` | Level determines the compression algorithm. | `LevelDefault (0)` | + +Possible values for the "Level" field are: + +- `LevelDisabled (-1)`: Compression is disabled. +- `LevelDefault (0)`: Default compression level. +- `LevelBestSpeed (1)`: Best compression speed. +- `LevelBestCompression (2)`: Best compression. + +## Default Config + +```go +var ConfigDefault = Config{ + Next: nil, + Level: LevelDefault, +} +``` + +## Constants + +```go +// Compression levels +const ( + LevelDisabled = -1 + LevelDefault = 0 + LevelBestSpeed = 1 + LevelBestCompression = 2 +) +``` diff --git a/docs/api/middleware/cors.md b/docs/api/middleware/cors.md new file mode 100644 index 0000000..882a748 --- /dev/null +++ b/docs/api/middleware/cors.md @@ -0,0 +1,214 @@ +--- +id: cors +--- + +# CORS + +CORS (Cross-Origin Resource Sharing) is a middleware for [Fiber](https://github.com/gofiber/fiber) that allows servers to specify who can access its resources and how. It's not a security feature, but a way to relax the security model of web browsers for cross-origin requests. You can learn more about CORS on [Mozilla Developer Network](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS). + +This middleware works by adding CORS headers to responses from your Fiber application. These headers specify which origins, methods, and headers are allowed for cross-origin requests. It also handles preflight requests, which are a CORS mechanism to check if the actual request is safe to send. + +The middleware uses the `AllowOrigins` option to control which origins can make cross-origin requests. It supports single origin, multiple origins, subdomain matching, and wildcard origin. It also allows programmatic origin validation with the `AllowOriginsFunc` option. + +To ensure that the provided `AllowOrigins` origins are correctly formatted, this middleware validates and normalizes them. It checks for valid schemes, i.e., HTTP or HTTPS, and it will automatically remove trailing slashes. If the provided origin is invalid, the middleware will panic. + +When configuring CORS, it's important to avoid [common pitfalls](#common-pitfalls) like using a wildcard origin with credentials, being overly permissive with origins, and inadequate validation with `AllowOriginsFunc`. Misconfiguration can expose your application to various security risks. + +## Signatures + +```go +func New(config ...Config) fiber.Handler +``` + +## Examples + +Import the middleware package that is part of the Fiber web framework + +```go +import ( + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/cors" +) +``` + +After you initiate your Fiber app, you can use the following possibilities: + +### Basic usage + +To use the default configuration, simply use `cors.New()`. This will allow wildcard origins '*', all methods, no credentials, and no headers or exposed headers. + +```go +app.Use(cors.New()) +``` + +### Custom configuration (specific origins, headers, etc.) + +```go +// Initialize default config +app.Use(cors.New()) + +// Or extend your config for customization +app.Use(cors.New(cors.Config{ + AllowOrigins: "https://gofiber.io, https://gofiber.net", + AllowHeaders: "Origin, Content-Type, Accept", +})) +``` + +### Dynamic origin validation + +You can use `AllowOriginsFunc` to programmatically determine whether to allow a request based on its origin. This is useful when you need to validate origins against a database or other dynamic sources. The function should return `true` if the origin is allowed, and `false` otherwise. + +Be sure to review the [security considerations](#security-considerations) when using `AllowOriginsFunc`. + +:::caution +Never allow `AllowOriginsFunc` to return `true` for all origins. This is particularly crucial when `AllowCredentials` is set to `true`. Doing so can bypass the restriction of using a wildcard origin with credentials, exposing your application to serious security threats. + +If you need to allow wildcard origins, use `AllowOrigins` with a wildcard `"*"` instead of `AllowOriginsFunc`. +::: + +```go +// dbCheckOrigin checks if the origin is in the list of allowed origins in the database. +func dbCheckOrigin(db *sql.DB, origin string) bool { + // Placeholder query - adjust according to your database schema and query needs + query := "SELECT COUNT(*) FROM allowed_origins WHERE origin = $1" + + var count int + err := db.QueryRow(query, origin).Scan(&count) + if err != nil { + // Handle error (e.g., log it); for simplicity, we return false here + return false + } + + return count > 0 +} + +// ... + +app.Use(cors.New(cors.Config{ + AllowOriginsFunc: func(origin string) bool { + return dbCheckOrigin(db, origin) + }, +})) +``` + +### Prohibited usage + +The following example is prohibited because it can expose your application to security risks. It sets `AllowOrigins` to `"*"` (a wildcard) and `AllowCredentials` to `true`. + +```go +app.Use(cors.New(cors.Config{ + AllowOrigins: "*", + AllowCredentials: true, +})) +``` + +This will result in the following panic: + +``` +panic: [CORS] 'AllowCredentials' is true, but 'AllowOrigins' cannot be set to `"*"`. +``` + +## Config + +| Property | Type | Description | Default | +|:-----------------|:---------------------------|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:-----------------------------------| +| Next | `func(*fiber.Ctx) bool` | Next defines a function to skip this middleware when returned true. | `nil` | +| AllowOriginsFunc | `func(origin string) bool` | `AllowOriginsFunc` is a function that dynamically determines whether to allow a request based on its origin. If this function returns `true`, the 'Access-Control-Allow-Origin' response header will be set to the request's 'origin' header. This function is only used if the request's origin doesn't match any origin in `AllowOrigins`. | `nil` | +| AllowOrigins | `string` | AllowOrigins defines a comma separated list of origins that may access the resource. This supports subdomain matching, so you can use a value like "https://*.example.com" to allow any subdomain of example.com to submit requests. | `"*"` | +| AllowMethods | `string` | AllowMethods defines a list of methods allowed when accessing the resource. This is used in response to a preflight request. | `"GET,POST,HEAD,PUT,DELETE,PATCH"` | +| AllowHeaders | `string` | AllowHeaders defines a list of request headers that can be used when making the actual request. This is in response to a preflight request. | `""` | +| AllowCredentials | `bool` | AllowCredentials indicates whether or not the response to the request can be exposed when the credentials flag is true. When used as part of a response to a preflight request, this indicates whether or not the actual request can be made using credentials. Note: If true, AllowOrigins cannot be set to a wildcard (`"*"`) to prevent security vulnerabilities. | `false` | +| ExposeHeaders | `string` | ExposeHeaders defines whitelist headers that clients are allowed to access. | `""` | +| MaxAge | `int` | MaxAge indicates how long (in seconds) the results of a preflight request can be cached. If you pass MaxAge 0, the Access-Control-Max-Age header will not be added and the browser will use 5 seconds by default. To disable caching completely, pass MaxAge value negative. It will set the Access-Control-Max-Age header to 0. | `0` | + +## Default Config + +```go +var ConfigDefault = Config{ + Next: nil, + AllowOriginsFunc: nil, + AllowOrigins: "*", + AllowMethods: strings.Join([]string{ + fiber.MethodGet, + fiber.MethodPost, + fiber.MethodHead, + fiber.MethodPut, + fiber.MethodDelete, + fiber.MethodPatch, + }, ","), + AllowHeaders: "", + AllowCredentials: false, + ExposeHeaders: "", + MaxAge: 0, +} +``` + +## Subdomain Matching + +The `AllowOrigins` configuration supports matching subdomains at any level. This means you can use a value like `"https://*.example.com"` to allow any subdomain of `example.com` to submit requests, including multiple subdomain levels such as `"https://sub.sub.example.com"`. + +### Example + +If you want to allow CORS requests from any subdomain of `example.com`, including nested subdomains, you can configure the `AllowOrigins` like so: + +```go +app.Use(cors.New(cors.Config{ + AllowOrigins: "https://*.example.com", +})) +``` + +# How It Works + +The CORS middleware works by adding the necessary CORS headers to responses from your Fiber application. These headers tell browsers what origins, methods, and headers are allowed for cross-origin requests. + +When a request comes in, the middleware first checks if it's a preflight request, which is a CORS mechanism to determine whether the actual request is safe to send. Preflight requests are HTTP OPTIONS requests with specific CORS headers. If it's a preflight request, the middleware responds with the appropriate CORS headers and ends the request. + +If it's not a preflight request, the middleware adds the CORS headers to the response and passes the request to the next handler. The actual CORS headers added depend on the configuration of the middleware. + +The `AllowOrigins` option controls which origins can make cross-origin requests. The middleware handles different `AllowOrigins` configurations as follows: + +- **Single origin:** If `AllowOrigins` is set to a single origin like `"http://www.example.com"`, and that origin matches the origin of the incoming request, the middleware adds the header `Access-Control-Allow-Origin: http://www.example.com` to the response. + +- **Multiple origins:** If `AllowOrigins` is set to multiple origins like `"https://example.com, https://www.example.com"`, the middleware picks the origin that matches the origin of the incoming request. + +- **Subdomain matching:** If `AllowOrigins` includes `"https://*.example.com"`, a subdomain like `https://sub.example.com` will be matched and `"https://sub.example.com"` will be the header. This will also match `https://sub.sub.example.com` and so on, but not `https://example.com`. + +- **Wildcard origin:** If `AllowOrigins` is set to `"*"`, the middleware uses that and adds the header `Access-Control-Allow-Origin: *` to the response. + +In all cases above, except the **Wildcard origin**, the middleware will either add the `Access-Control-Allow-Origin` header to the response matching the origin of the incoming request, or it will not add the header at all if the origin is not allowed. + +- **Programmatic origin validation:**: The middleware also handles the `AllowOriginsFunc` option, which allows you to programmatically determine if an origin is allowed. If `AllowOriginsFunc` returns `true` for an origin, the middleware sets the `Access-Control-Allow-Origin` header to that origin. + +The `AllowMethods` option controls which HTTP methods are allowed. For example, if `AllowMethods` is set to `"GET, POST"`, the middleware adds the header `Access-Control-Allow-Methods: GET, POST` to the response. + +The `AllowHeaders` option specifies which headers are allowed in the actual request. The middleware sets the Access-Control-Allow-Headers response header to the value of `AllowHeaders`. This informs the client which headers it can use in the actual request. + +The `AllowCredentials` option indicates whether the response to the request can be exposed when the credentials flag is true. If `AllowCredentials` is set to `true`, the middleware adds the header `Access-Control-Allow-Credentials: true` to the response. To prevent security vulnerabilities, `AllowCredentials` cannot be set to `true` if `AllowOrigins` is set to a wildcard (`*`). + +The `ExposeHeaders` option defines a whitelist of headers that clients are allowed to access. If `ExposeHeaders` is set to `"X-Custom-Header"`, the middleware adds the header `Access-Control-Expose-Headers: X-Custom-Header` to the response. + +The `MaxAge` option indicates how long the results of a preflight request can be cached. If `MaxAge` is set to `3600`, the middleware adds the header `Access-Control-Max-Age: 3600` to the response. + +The `Vary` header is used in this middleware to inform the client that the server's response to a request. For or both preflight and actual requests, the Vary header is set to `Access-Control-Request-Method` and `Access-Control-Request-Headers`. For preflight requests, the Vary header is also set to `Origin`. The `Vary` header is important for caching. It helps caches (like a web browser's cache or a CDN) determine when a cached response can be used in response to a future request, and when the server needs to be queried for a new response. + +## Security Considerations + +When configuring CORS, misconfiguration can potentially expose your application to various security risks. Here are some secure configurations and common pitfalls to avoid: + +### Secure Configurations + +- **Specify Allowed Origins**: Instead of using a wildcard (`"*"`), specify the exact domains allowed to make requests. For example, `AllowOrigins: "https://www.example.com, https://api.example.com"` ensures only these domains can make cross-origin requests to your application. + +- **Use Credentials Carefully**: If your application needs to support credentials in cross-origin requests, ensure `AllowCredentials` is set to `true` and specify exact origins in `AllowOrigins`. Do not use a wildcard origin in this case. + +- **Limit Exposed Headers**: Only whitelist headers that are necessary for the client-side application by setting `ExposeHeaders` appropriately. This minimizes the risk of exposing sensitive information. + +### Common Pitfalls + +- **Wildcard Origin with Credentials**: Setting `AllowOrigins` to `"*"` (a wildcard) and `AllowCredentials` to `true` is a common misconfiguration. This combination is prohibited because it can expose your application to security risks. + +- **Overly Permissive Origins**: Specifying too many origins or using overly broad patterns (e.g., `https://*.example.com`) can inadvertently allow malicious sites to interact with your application. Be as specific as possible with allowed origins. + +- **Inadequate `AllowOriginsFunc` Validation**: When using `AllowOriginsFunc` for dynamic origin validation, ensure the function includes robust checks to prevent unauthorized origins from being accepted. Overly permissive validation can lead to security vulnerabilities. Never allow `AllowOriginsFunc` to return `true` for all origins. This is particularly crucial when `AllowCredentials` is set to `true`. Doing so can bypass the restriction of using a wildcard origin with credentials, exposing your application to serious security threats. If you need to allow wildcard origins, use `AllowOrigins` with a wildcard `"*"` instead of `AllowOriginsFunc`. + +Remember, the key to secure CORS configuration is specificity and caution. By carefully selecting which origins, methods, and headers are allowed, you can help protect your application from cross-origin attacks. \ No newline at end of file diff --git a/docs/api/middleware/csrf.md b/docs/api/middleware/csrf.md new file mode 100644 index 0000000..1ce4800 --- /dev/null +++ b/docs/api/middleware/csrf.md @@ -0,0 +1,256 @@ +--- +id: csrf +--- + +# CSRF + +The CSRF middleware for [Fiber](https://github.com/gofiber/fiber) provides protection against [Cross-Site Request Forgery](https://en.wikipedia.org/wiki/Cross-site_request_forgery) (CSRF) attacks. Requests made using methods other than those defined as 'safe' by [RFC9110#section-9.2.1](https://datatracker.ietf.org/doc/html/rfc9110.html#section-9.2.1) (GET, HEAD, OPTIONS, and TRACE) are validated using tokens. If a potential attack is detected, the middleware will return a default 403 Forbidden error. + +This middleware offers two [Token Validation Patterns](#token-validation-patterns): the [Double Submit Cookie Pattern (default)](#double-submit-cookie-pattern-default), and the [Synchronizer Token Pattern (with Session)](#synchronizer-token-pattern-with-session). + +As a [Defense In Depth](#defense-in-depth) measure, this middleware performs [Referer Checking](#referer-checking) for HTTPS requests. + +## Token Generation + +CSRF tokens are generated on 'safe' requests and when the existing token has expired or hasn't been set yet. If `SingleUseToken` is `true`, a new token is generated after each use. Retrieve the CSRF token using `c.Locals(contextKey)`, where `contextKey` is defined within the configuration. + +## Security Considerations + +This middleware is designed to protect against CSRF attacks but does not protect against other attack vectors, such as XSS. It should be used in combination with other security measures. + +:::danger +Never use 'safe' methods to mutate data, for example, never use a GET request to modify a resource. This middleware will not protect against CSRF attacks on 'safe' methods. +::: + +### Token Validation Patterns + +#### Double Submit Cookie Pattern (Default) + +By default, the middleware generates and stores tokens using the `fiber.Storage` interface. These tokens are not linked to any particular user session, and they are validated using the Double Submit Cookie pattern. The token is stored in a cookie, and then sent as a header on requests. The middleware compares the cookie value with the header value to validate the token. This is a secure pattern that does not require a user session. + +When the authorization status changes, the previously issued token MUST be deleted, and a new one generated. See [Token Lifecycle](#token-lifecycle) [Deleting Tokens](#deleting-tokens) for more information. + +:::caution +When using this pattern, it's important to set the `CookieSameSite` option to `Lax` or `Strict` and ensure that the Extractor is not `CsrfFromCookie`, and KeyLookup is not `cookie:`. +::: + +:::note +When using this pattern, this middleware uses our [Storage](https://github.com/gofiber/storage) package to support various databases through a single interface. The default configuration for Storage saves data to memory. See [Custom Storage/Database](#custom-storagedatabase) for customizing the storage. +::: + +#### Synchronizer Token Pattern (with Session) + +When using this middleware with a user session, the middleware can be configured to store the token within the session. This method is recommended when using a user session, as it is generally more secure than the Double Submit Cookie Pattern. + +When using this pattern it's important to regenerate the session when the authorization status changes, this will also delete the token. See: [Token Lifecycle](#token-lifecycle) for more information. + +:::caution +Pre-sessions are required and will be created automatically if not present. Use a session value to indicate authentication instead of relying on presence of a session. +::: + +### Defense In Depth + +When using this middleware, it's recommended to serve your pages over HTTPS, set the `CookieSecure` option to `true`, and set the `CookieSameSite` option to `Lax` or `Strict`. This ensures that the cookie is only sent over HTTPS and not on requests from external sites. + +:::note +Cookie prefixes `__Host-` and `__Secure-` can be used to further secure the cookie. Note that these prefixes are not supported by all browsers and there are other limitations. See [MDN#Set-Cookie#cookie_prefixes](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#cookie_prefixes) for more information. + +To use these prefixes, set the `CookieName` option to `__Host-csrf_` or `__Secure-csrf_`. +::: + +### Referer Checking + +For HTTPS requests, this middleware performs strict referer checking. Even if a subdomain can set or modify cookies on your domain, it can't force a user to post to your application since that request won't come from your own exact domain. + +:::caution +When HTTPS requests are protected by CSRF, referer checking is always carried out. + +The Referer header is automatically included in requests by all modern browsers, including those made using the JS Fetch API. However, if you're making use of this middleware with a custom client, it's important to ensure that the client sends a valid Referer header. +::: + + +### Token Lifecycle + +Tokens are valid until they expire or until they are deleted. By default, tokens are valid for 1 hour, and each subsequent request extends the expiration by 1 hour. The token only expires if the user doesn't make a request for the duration of the expiration time. + +#### Token Reuse + +By default, tokens may be used multiple times. If you want to delete the token after it has been used, you can set the `SingleUseToken` option to `true`. This will delete the token after it has been used, and a new token will be generated on the next request. + +:::info +Using `SingleUseToken` comes with usability trade-offs and is not enabled by default. For example, it can interfere with the user experience if the user has multiple tabs open or uses the back button. +::: + +#### Deleting Tokens + +When the authorization status changes, the CSRF token MUST be deleted, and a new one generated. This can be done by calling `handler.DeleteToken(c)`. + +```go +if handler, ok := app.AcquireCtx(ctx).Locals(csrf.ConfigDefault.HandlerContextKey).(*CSRFHandler); ok { + if err := handler.DeleteToken(app.AcquireCtx(ctx)); err != nil { + // handle error + } +} +``` + +:::tip +If you are using this middleware with the fiber session middleware, then you can simply call `session.Destroy()`, `session.Regenerate()`, or `session.Reset()` to delete session and the token stored therein. +::: + +### BREACH + +It's important to note that the token is sent as a header on every request. If you include the token in a page that is vulnerable to [BREACH](https://en.wikipedia.org/wiki/BREACH), an attacker may be able to extract the token. To mitigate this, ensure your pages are served over HTTPS, disable HTTP compression, and implement rate limiting for requests. + +## Signatures + +```go +func New(config ...Config) fiber.Handler +``` + +## Examples + +Import the middleware package that is part of the Fiber web framework: + +```go +import ( + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/csrf" +) +``` + +After initializing your Fiber app, you can use the following code to initialize the middleware: + +```go +// Initialize default config +app.Use(csrf.New()) + +// Or extend your config for customization +app.Use(csrf.New(csrf.Config{ + KeyLookup: "header:X-Csrf-Token", + CookieName: "csrf_", + CookieSameSite: "Lax", + Expiration: 1 * time.Hour, + KeyGenerator: utils.UUIDv4, +})) +``` + +## Config + +| Property | Type | Description | Default | +|:------------------|:-----------------------------------|:---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:-----------------------------| +| Next | `func(*fiber.Ctx) bool` | Next defines a function to skip this middleware when returned true. | `nil` | +| KeyLookup | `string` | KeyLookup is a string in the form of "`:`" that is used to create an Extractor that extracts the token from the request. Possible values: "`header:`", "`query:`", "`param:`", "`form:`", "`cookie:`". Ignored if an Extractor is explicitly set. | "header:X-Csrf-Token" | +| CookieName | `string` | Name of the csrf cookie. This cookie will store the csrf key. | "csrf_" | +| CookieDomain | `string` | Domain of the CSRF cookie. | "" | +| CookiePath | `string` | Path of the CSRF cookie. | "" | +| CookieSecure | `bool` | Indicates if the CSRF cookie is secure. | false | +| CookieHTTPOnly | `bool` | Indicates if the CSRF cookie is HTTP-only. | false | +| CookieSameSite | `string` | Value of SameSite cookie. | "Lax" | +| CookieSessionOnly | `bool` | Decides whether the cookie should last for only the browser session. Ignores Expiration if set to true. | false | +| Expiration | `time.Duration` | Expiration is the duration before the CSRF token will expire. | 1 * time.Hour | +| SingleUseToken | `bool` | SingleUseToken indicates if the CSRF token be destroyed and a new one generated on each use. (See TokenLifecycle) | false | +| Storage | `fiber.Storage` | Store is used to store the state of the middleware. | `nil` | +| Session | `*session.Store` | Session is used to store the state of the middleware. Overrides Storage if set. | `nil` | +| SessionKey | `string` | SessionKey is the key used to store the token within the session. | "fiber.csrf.token" | +| ContextKey | `inteface{}` | Context key to store the generated CSRF token into the context. If left empty, the token will not be stored within the context. | "" | +| KeyGenerator | `func() string` | KeyGenerator creates a new CSRF token. | utils.UUID | +| CookieExpires | `time.Duration` (Deprecated) | Deprecated: Please use Expiration. | 0 | +| Cookie | `*fiber.Cookie` (Deprecated) | Deprecated: Please use Cookie* related fields. | `nil` | +| TokenLookup | `string` (Deprecated) | Deprecated: Please use KeyLookup. | "" | +| ErrorHandler | `fiber.ErrorHandler` | ErrorHandler is executed when an error is returned from fiber.Handler. | DefaultErrorHandler | +| Extractor | `func(*fiber.Ctx) (string, error)` | Extractor returns the CSRF token. If set, this will be used in place of an Extractor based on KeyLookup. | Extractor based on KeyLookup | +| HandlerContextKey | `interface{}` | HandlerContextKey is used to store the CSRF Handler into context. | "fiber.csrf.handler" | + +### Default Config + +```go +var ConfigDefault = Config{ + KeyLookup: "header:" + HeaderName, + CookieName: "csrf_", + CookieSameSite: "Lax", + Expiration: 1 * time.Hour, + KeyGenerator: utils.UUIDv4, + ErrorHandler: defaultErrorHandler, + Extractor: CsrfFromHeader(HeaderName), + SessionKey: "fiber.csrf.token", + HandlerContextKey: "fiber.csrf.handler", +} +``` + +### Recommended Config (with session) + +It's recommended to use this middleware with [fiber/middleware/session](https://docs.gofiber.io/api/middleware/session) to store the CSRF token within the session. This is generally more secure than the default configuration. + +```go +var ConfigDefault = Config{ + KeyLookup: "header:" + HeaderName, + CookieName: "__Host-csrf_", + CookieSameSite: "Lax", + CookieSecure: true, + CookieSessionOnly: true, + CookieHTTPOnly: true, + Expiration: 1 * time.Hour, + KeyGenerator: utils.UUIDv4, + ErrorHandler: defaultErrorHandler, + Extractor: CsrfFromHeader(HeaderName), + Session: session.Store, + SessionKey: "fiber.csrf.token", + HandlerContextKey: "fiber.csrf.handler", +} +``` + +## Constants + +```go +const ( + HeaderName = "X-Csrf-Token" +) +``` + +## Sentinel Errors + +The CSRF middleware utilizes a set of sentinel errors to handle various scenarios and communicate errors effectively. These can be used within a [custom error handler](#custom-error-handler) to handle errors returned by the middleware. + +### Errors Returned to Error Handler + +- `ErrTokenNotFound`: Indicates that the CSRF token was not found. +- `ErrTokenInvalid`: Indicates that the CSRF token is invalid. +- `ErrNoReferer`: Indicates that the referer was not supplied. +- `ErrBadReferer`: Indicates that the referer is invalid. + +If you use the default error handler, the client will receive a 403 Forbidden error without any additional information. + +## Custom Error Handler + +You can use a custom error handler to handle errors returned by the CSRF middleware. The error handler is executed when an error is returned from the middleware. The error handler is passed the error returned from the middleware and the fiber.Ctx. + +Example, returning a JSON response for API requests and rendering an error page for other requests: + +```go +app.Use(csrf.New(csrf.Config{ + ErrorHandler: func(c *fiber.Ctx, err error) error { + accepts := c.Accepts("html", "json") + path := c.Path() + if accepts == "json" || strings.HasPrefix(path, "/api/") { + return c.Status(fiber.StatusForbidden).JSON(fiber.Map{ + "error": "Forbidden", + }) + } + return c.Status(fiber.StatusForbidden).Render("error", fiber.Map{ + "Title": "Forbidden", + "Status": fiber.StatusForbidden, + }, "layouts/main") + }, +})) +``` + +## Custom Storage/Database + +You can use any storage from our [storage](https://github.com/gofiber/storage/) package. + +```go +storage := sqlite3.New() // From github.com/gofiber/storage/sqlite3 +app.Use(csrf.New(csrf.Config{ + Storage: storage, +})) +``` diff --git a/docs/api/middleware/earlydata.md b/docs/api/middleware/earlydata.md new file mode 100644 index 0000000..50e5bb1 --- /dev/null +++ b/docs/api/middleware/earlydata.md @@ -0,0 +1,82 @@ +--- +id: earlydata +--- + +# EarlyData + +The Early Data middleware for [Fiber](https://github.com/gofiber/fiber) adds support for TLS 1.3's early data ("0-RTT") feature. +Citing [RFC 8446](https://datatracker.ietf.org/doc/html/rfc8446#section-2-3), when a client and server share a PSK, TLS 1.3 allows clients to send data on the first flight ("early data") to speed up the request, effectively reducing the regular 1-RTT request to a 0-RTT request. + +Make sure to enable fiber's `EnableTrustedProxyCheck` config option before using this middleware in order to not trust bogus HTTP request headers of the client. + +Also be aware that enabling support for early data in your reverse proxy (e.g. nginx, as done with a simple `ssl_early_data on;`) makes requests replayable. Refer to the following documents before continuing: + +- https://datatracker.ietf.org/doc/html/rfc8446#section-8 +- https://blog.trailofbits.com/2019/03/25/what-application-developers-need-to-know-about-tls-early-data-0rtt/ + +By default, this middleware allows early data requests on safe HTTP request methods only and rejects the request otherwise, i.e. aborts the request before executing your handler. This behavior can be controlled by the `AllowEarlyData` config option. +Safe HTTP methods — `GET`, `HEAD`, `OPTIONS` and `TRACE` — should not modify a state on the server. + +## Signatures + +```go +func New(config ...Config) fiber.Handler +``` + +## Examples + +Import the middleware package that is part of the Fiber web framework + +```go +import ( + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/earlydata" +) +``` + +After you initiate your Fiber app, you can use the following possibilities: + +```go +// Initialize default config +app.Use(earlydata.New()) + +// Or extend your config for customization +app.Use(earlydata.New(earlydata.Config{ + Error: fiber.ErrTooEarly, + // ... +})) +``` + +## Config + +| Property | Type | Description | Default | +|:---------------|:------------------------|:-------------------------------------------------------------------------------------|:-------------------------------------------------------| +| Next | `func(*fiber.Ctx) bool` | Next defines a function to skip this middleware when returned true. | `nil` | +| IsEarlyData | `func(*fiber.Ctx) bool` | IsEarlyData returns whether the request is an early-data request. | Function checking if "Early-Data" header equals "1" | +| AllowEarlyData | `func(*fiber.Ctx) bool` | AllowEarlyData returns whether the early-data request should be allowed or rejected. | Function rejecting on unsafe and allowing safe methods | +| Error | `error` | Error is returned in case an early-data request is rejected. | `fiber.ErrTooEarly` | + +## Default Config + +```go +var ConfigDefault = Config{ + IsEarlyData: func(c *fiber.Ctx) bool { + return c.Get(DefaultHeaderName) == DefaultHeaderTrueValue + }, + + AllowEarlyData: func(c *fiber.Ctx) bool { + return fiber.IsMethodSafe(c.Method()) + }, + + Error: fiber.ErrTooEarly, +} +``` + +## Constants + +```go +const ( + DefaultHeaderName = "Early-Data" + DefaultHeaderTrueValue = "1" +) +``` diff --git a/docs/api/middleware/encryptcookie.md b/docs/api/middleware/encryptcookie.md new file mode 100644 index 0000000..c6e689e --- /dev/null +++ b/docs/api/middleware/encryptcookie.md @@ -0,0 +1,101 @@ +--- +id: encryptcookie +--- + +# Encrypt Cookie + +Encrypt Cookie is a middleware for [Fiber](https://github.com/gofiber/fiber) that secures your cookie values through encryption. + +:::note +This middleware encrypts cookie values and not the cookie names. +::: + +## Signatures + +```go +// Intitializes the middleware +func New(config ...Config) fiber.Handler + +// Returns a random 32 character long string +func GenerateKey() string +``` + +## Examples + +To use the Encrypt Cookie middleware, first, import the middleware package as part of the Fiber web framework: + +```go +import ( + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/encryptcookie" +) +``` + +Once you've imported the middleware package, you can use it inside your Fiber app: + +```go +// Provide a minimal configuration +app.Use(encryptcookie.New(encryptcookie.Config{ + Key: "secret-thirty-2-character-string", +})) + +// Retrieve the encrypted cookie value +app.Get("/", func(c *fiber.Ctx) error { + return c.SendString("value=" + c.Cookies("test")) +}) + +// Create an encrypted cookie +app.Post("/", func(c *fiber.Ctx) error { + c.Cookie(&fiber.Cookie{ + Name: "test", + Value: "SomeThing", + }) + return nil +}) +``` + +:::note +`Key` must be a 32 character string. It's used to encrypt the values, so make sure it is random and keep it secret. +You can run `openssl rand -base64 32` or call `encryptcookie.GenerateKey()` to create a random key for you. +Make sure not to set `Key` to `encryptcookie.GenerateKey()` because that will create a new key every run. +::: + +## Config + +| Property | Type | Description | Default | +|:----------|:----------------------------------------------------|:------------------------------------------------------------------------------------------------------|:-----------------------------| +| Next | `func(*fiber.Ctx) bool` | A function to skip this middleware when returned true. | `nil` | +| Except | `[]string` | Array of cookie keys that should not be encrypted. | `[]` | +| Key | `string` | A base64-encoded unique key to encode & decode cookies. Required. Key length should be 32 characters. | (No default, required field) | +| Encryptor | `func(decryptedString, key string) (string, error)` | A custom function to encrypt cookies. | `EncryptCookie` | +| Decryptor | `func(encryptedString, key string) (string, error)` | A custom function to decrypt cookies. | `DecryptCookie` | + +## Default Config + +```go +var ConfigDefault = Config{ + Next: nil, + Except: []string{}, + Key: "", + Encryptor: EncryptCookie, + Decryptor: DecryptCookie, +} +``` + +## Usage With Other Middlewares That Reads Or Modify Cookies +Place the encryptcookie middleware before any other middleware that reads or modifies cookies. For example, if you are using the CSRF middleware, ensure that the encryptcookie middleware is placed before it. Failure to do so may prevent the CSRF middleware from reading the encrypted cookie. + +You may also choose to exclude certain cookies from encryption. For instance, if you are using the CSRF middleware with a frontend framework like Angular, and the framework reads the token from a cookie, you should exclude that cookie from encryption. This can be achieved by adding the cookie name to the Except array in the configuration: + +```go +app.Use(encryptcookie.New(encryptcookie.Config{ + Key: "secret-thirty-2-character-string", + Except: []string{csrf.ConfigDefault.CookieName}, // exclude CSRF cookie +})) +app.Use(csrf.New(csrf.Config{ + KeyLookup: "header:" + csrf.HeaderName, + CookieSameSite: "Lax", + CookieSecure: true, + CookieHTTPOnly: false, +})) +``` diff --git a/docs/api/middleware/envvar.md b/docs/api/middleware/envvar.md new file mode 100644 index 0000000..1d9f474 --- /dev/null +++ b/docs/api/middleware/envvar.md @@ -0,0 +1,69 @@ +--- +id: envvar +--- + +# EnvVar + +EnvVar middleware for [Fiber](https://github.com/gofiber/fiber) that can be used to expose environment variables with various options. + +## Signatures + +```go +func New(config ...Config) fiber.Handler +``` + +## Examples + +Import the middleware package that is part of the Fiber web framework + +```go +import ( + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/envvar" +) +``` + +After you initiate your Fiber app, you can use the following possibilities: + +```go +// Initialize default config +app.Use("/expose/envvars", envvar.New()) + +// Or extend your config for customization +app.Use("/expose/envvars", envvar.New( + envvar.Config{ + ExportVars: map[string]string{"testKey": "", "testDefaultKey": "testDefaultVal"}, + ExcludeVars: map[string]string{"excludeKey": ""}, + }), +) +``` + +:::note +You will need to provide a path to use the envvar middleware. +::: + +## Response + +Http response contract: +``` +{ + "vars": { + "someEnvVariable": "someValue", + "anotherEnvVariable": "anotherValue", + } +} + +``` + +## Config + +| Property | Type | Description | Default | +|:------------|:--------------------|:-----------------------------------------------------------------------------|:--------| +| ExportVars | `map[string]string` | ExportVars specifies the environment variables that should be exported. | `nil` | +| ExcludeVars | `map[string]string` | ExcludeVars specifies the environment variables that should not be exported. | `nil` | + +## Default Config + +```go +Config{} +``` diff --git a/docs/api/middleware/etag.md b/docs/api/middleware/etag.md new file mode 100644 index 0000000..24be273 --- /dev/null +++ b/docs/api/middleware/etag.md @@ -0,0 +1,62 @@ +--- +id: etag +--- + +# ETag + +ETag middleware for [Fiber](https://github.com/gofiber/fiber) that lets caches be more efficient and save bandwidth, as a web server does not need to resend a full response if the content has not changed. + +## Signatures + +```go +func New(config ...Config) fiber.Handler +``` + +## Examples + +Import the middleware package that is part of the Fiber web framework + +```go +import ( + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/etag" +) +``` + +After you initiate your Fiber app, you can use the following possibilities: + +```go +// Initialize default config +app.Use(etag.New()) + +// Get / receives Etag: "13-1831710635" in response header +app.Get("/", func(c *fiber.Ctx) error { + return c.SendString("Hello, World!") +}) + +// Or extend your config for customization +app.Use(etag.New(etag.Config{ + Weak: true, +})) + +// Get / receives Etag: "W/"13-1831710635" in response header +app.Get("/", func(c *fiber.Ctx) error { + return c.SendString("Hello, World!") +}) +``` + +## Config + +| Property | Type | Description | Default | +|:---------|:------------------------|:-------------------------------------------------------------------------------------------------------------------|:--------| +| Weak | `bool` | Weak indicates that a weak validator is used. Weak etags are easy to generate but are less useful for comparisons. | `false` | +| Next | `func(*fiber.Ctx) bool` | Next defines a function to skip this middleware when returned true. | `nil` | + +## Default Config + +```go +var ConfigDefault = Config{ + Next: nil, + Weak: false, +} +``` diff --git a/docs/api/middleware/expvar.md b/docs/api/middleware/expvar.md new file mode 100644 index 0000000..900850e --- /dev/null +++ b/docs/api/middleware/expvar.md @@ -0,0 +1,72 @@ +--- +id: expvar +--- + +# ExpVar + +Expvar middleware for [Fiber](https://github.com/gofiber/fiber) that serves via its HTTP server runtime exposed variants in the JSON format. The package is typically only imported for the side effect of registering its HTTP handlers. The handled path is `/debug/vars`. + +## Signatures + +```go +func New() fiber.Handler +``` + +## Examples + +Import the middleware package that is part of the Fiber web framework + +```go +import ( + "github.com/gofiber/fiber/v2" + expvarmw "github.com/gofiber/fiber/v2/middleware/expvar" +) +``` + +After you initiate your Fiber app, you can use the following possibilities: +```go +var count = expvar.NewInt("count") + +app.Use(expvarmw.New()) +app.Get("/", func(c *fiber.Ctx) error { + count.Add(1) + + return c.SendString(fmt.Sprintf("hello expvar count %d", count.Value())) +}) +``` + +Visit path `/debug/vars` to see all vars and use query `r=key` to filter exposed variables. + +```bash +curl 127.0.0.1:3000 +hello expvar count 1 + +curl 127.0.0.1:3000/debug/vars +{ + "cmdline": ["xxx"], + "count": 1, + "expvarHandlerCalls": 33, + "expvarRegexpErrors": 0, + "memstats": {...} +} + +curl 127.0.0.1:3000/debug/vars?r=c +{ + "cmdline": ["xxx"], + "count": 1 +} +``` + +## Config + +| Property | Type | Description | Default | +|:---------|:------------------------|:--------------------------------------------------------------------|:--------| +| Next | `func(*fiber.Ctx) bool` | Next defines a function to skip this middleware when returned true. | `nil` | + +## Default Config + +```go +var ConfigDefault = Config{ + Next: nil, +} +``` diff --git a/docs/api/middleware/favicon.md b/docs/api/middleware/favicon.md new file mode 100644 index 0000000..b1a9b0d --- /dev/null +++ b/docs/api/middleware/favicon.md @@ -0,0 +1,63 @@ +--- +id: favicon +--- + +# Favicon + +Favicon middleware for [Fiber](https://github.com/gofiber/fiber) that ignores favicon requests or caches a provided icon in memory to improve performance by skipping disk access. User agents request favicon.ico frequently and indiscriminately, so you may wish to exclude these requests from your logs by using this middleware before your logger middleware. + +:::note +This middleware is exclusively for serving the default, implicit favicon, which is GET /favicon.ico or [custom favicon URL](#config). +::: + +## Signatures + +```go +func New(config ...Config) fiber.Handler +``` + +## Examples + +Import the middleware package that is part of the Fiber web framework + +```go +import ( + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/favicon" +) +``` + +After you initiate your Fiber app, you can use the following possibilities: + +```go +// Initialize default config +app.Use(favicon.New()) + +// Or extend your config for customization +app.Use(favicon.New(favicon.Config{ + File: "./favicon.ico", + URL: "/favicon.ico", +})) +``` + +## Config + +| Property | Type | Description | Default | +|:-------------|:------------------------|:---------------------------------------------------------------------------------|:---------------------------| +| Next | `func(*fiber.Ctx) bool` | Next defines a function to skip this middleware when returned true. | `nil` | +| Data | `[]byte` | Raw data of the favicon file. This can be used instead of `File`. | `nil` | +| File | `string` | File holds the path to an actual favicon that will be cached. | "" | +| URL | `string` | URL for favicon handler. | "/favicon.ico" | +| FileSystem | `http.FileSystem` | FileSystem is an optional alternate filesystem to search for the favicon in. | `nil` | +| CacheControl | `string` | CacheControl defines how the Cache-Control header in the response should be set. | "public, max-age=31536000" | + +## Default Config + +```go +var ConfigDefault = Config{ + Next: nil, + File: "", + URL: fPath, + CacheControl: "public, max-age=31536000", +} +``` diff --git a/docs/api/middleware/filesystem.md b/docs/api/middleware/filesystem.md new file mode 100644 index 0000000..bbeff14 --- /dev/null +++ b/docs/api/middleware/filesystem.md @@ -0,0 +1,300 @@ +--- +id: filesystem +--- + +# FileSystem + +Filesystem middleware for [Fiber](https://github.com/gofiber/fiber) that enables you to serve files from a directory. + +:::caution +**`:params` & `:optionals?` within the prefix path are not supported!** + +**To handle paths with spaces (or other url encoded values) make sure to set `fiber.Config{ UnescapePath: true }`** +::: + +## Signatures + +```go +func New(config Config) fiber.Handler +``` + +## Examples + +Import the middleware package that is part of the Fiber web framework + +```go +import ( + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/filesystem" +) +``` + +After you initiate your Fiber app, you can use the following possibilities: + +```go +// Provide a minimal config +app.Use(filesystem.New(filesystem.Config{ + Root: http.Dir("./assets"), +})) + +// Or extend your config for customization +app.Use(filesystem.New(filesystem.Config{ + Root: http.Dir("./assets"), + Browse: true, + Index: "index.html", + NotFoundFile: "404.html", + MaxAge: 3600, +})) +``` + + +> If your environment (Go 1.16+) supports it, we recommend using Go Embed instead of the other solutions listed as this one is native to Go and the easiest to use. + +## embed + +[Embed](https://golang.org/pkg/embed/) is the native method to embed files in a Golang excecutable. Introduced in Go 1.16. + +```go +package main + +import ( + "embed" + "io/fs" + "log" + "net/http" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/filesystem" +) + +// Embed a single file +//go:embed index.html +var f embed.FS + +// Embed a directory +//go:embed static/* +var embedDirStatic embed.FS + +func main() { + app := fiber.New() + + app.Use("/", filesystem.New(filesystem.Config{ + Root: http.FS(f), + })) + + // Access file "image.png" under `static/` directory via URL: `http:///static/image.png`. + // Without `PathPrefix`, you have to access it via URL: + // `http:///static/static/image.png`. + app.Use("/static", filesystem.New(filesystem.Config{ + Root: http.FS(embedDirStatic), + PathPrefix: "static", + Browse: true, + })) + + log.Fatal(app.Listen(":3000")) +} +``` + +## pkger + +[https://github.com/markbates/pkger](https://github.com/markbates/pkger) + +```go +package main + +import ( + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/filesystem" + + "github.com/markbates/pkger" +) + +func main() { + app := fiber.New() + + app.Use("/assets", filesystem.New(filesystem.Config{ + Root: pkger.Dir("/assets"), + })) + + log.Fatal(app.Listen(":3000")) +} +``` + +## packr + +[https://github.com/gobuffalo/packr](https://github.com/gobuffalo/packr) + +```go +package main + +import ( + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/filesystem" + + "github.com/gobuffalo/packr/v2" +) + +func main() { + app := fiber.New() + + app.Use("/assets", filesystem.New(filesystem.Config{ + Root: packr.New("Assets Box", "/assets"), + })) + + log.Fatal(app.Listen(":3000")) +} +``` + +## go.rice + +[https://github.com/GeertJohan/go.rice](https://github.com/GeertJohan/go.rice) + +```go +package main + +import ( + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/filesystem" + + "github.com/GeertJohan/go.rice" +) + +func main() { + app := fiber.New() + + app.Use("/assets", filesystem.New(filesystem.Config{ + Root: rice.MustFindBox("assets").HTTPBox(), + })) + + log.Fatal(app.Listen(":3000")) +} +``` + +## fileb0x + +[https://github.com/UnnoTed/fileb0x](https://github.com/UnnoTed/fileb0x) + +```go +package main + +import ( + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/filesystem" + + "/myEmbeddedFiles" +) + +func main() { + app := fiber.New() + + app.Use("/assets", filesystem.New(filesystem.Config{ + Root: myEmbeddedFiles.HTTP, + })) + + log.Fatal(app.Listen(":3000")) +} +``` + +## statik + +[https://github.com/rakyll/statik](https://github.com/rakyll/statik) + +```go +package main + +import ( + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/filesystem" + + // Use blank to invoke init function and register data to statik + _ "/statik" + "github.com/rakyll/statik/fs" +) + +func main() { + statikFS, err := fs.New() + if err != nil { + panic(err) + } + + app := fiber.New() + + app.Use("/", filesystem.New(filesystem.Config{ + Root: statikFS, + })) + + log.Fatal(app.Listen(":3000")) +} +``` + +## Config + +| Property | Type | Description | Default | +|:-------------------|:------------------------|:------------------------------------------------------------------------------------------------------------|:-------------| +| Next | `func(*fiber.Ctx) bool` | Next defines a function to skip this middleware when returned true. | `nil` | +| Root | `http.FileSystem` | Root is a FileSystem that provides access to a collection of files and directories. | `nil` | +| PathPrefix | `string` | PathPrefix defines a prefix to be added to a filepath when reading a file from the FileSystem. | "" | +| Browse | `bool` | Enable directory browsing. | `false` | +| Index | `string` | Index file for serving a directory. | "index.html" | +| MaxAge | `int` | The value for the Cache-Control HTTP-header that is set on the file response. MaxAge is defined in seconds. | 0 | +| NotFoundFile | `string` | File to return if the path is not found. Useful for SPA's. | "" | +| ContentTypeCharset | `string` | The value for the Content-Type HTTP-header that is set on the file response. | "" | + +## Default Config + +```go +var ConfigDefault = Config{ + Next: nil, + Root: nil, + PathPrefix: "", + Browse: false, + Index: "/index.html", + MaxAge: 0, + ContentTypeCharset: "", +} +``` + +## Utils + +### SendFile + +Serves a file from an [HTTP file system](https://pkg.go.dev/net/http#FileSystem) at the specified path. + +```go title="Signature" title="Signature" +func SendFile(c *fiber.Ctx, filesystem http.FileSystem, path string) error +``` +Import the middleware package that is part of the Fiber web framework + +```go +import ( + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/filesystem" +) +``` + +```go title="Example" +// Define a route to serve a specific file +app.Get("/download", func(c *fiber.Ctx) error { + // Serve the file using SendFile function + err := filesystem.SendFile(c, http.Dir("your/filesystem/root"), "path/to/your/file.txt") + if err != nil { + // Handle the error, e.g., return a 404 Not Found response + return c.Status(fiber.StatusNotFound).SendString("File not found") + } + + return nil +}) +``` + +```go title="Example" +// Serve static files from the "build" directory using Fiber's built-in middleware. +app.Use("/", filesystem.New(filesystem.Config{ + Root: http.FS(f), // Specify the root directory for static files. + PathPrefix: "build", // Define the path prefix where static files are served. +})) + +// For all other routes (wildcard "*"), serve the "index.html" file from the "build" directory. +app.Use("*", func(ctx *fiber.Ctx) error { + return filesystem.SendFile(ctx, http.FS(f), "build/index.html") +}) +``` diff --git a/docs/api/middleware/healthcheck.md b/docs/api/middleware/healthcheck.md new file mode 100644 index 0000000..641c236 --- /dev/null +++ b/docs/api/middleware/healthcheck.md @@ -0,0 +1,106 @@ +--- +id: healthcheck +--- + +# Health Check + +Liveness and readiness probes middleware for [Fiber](https://github.com/gofiber/fiber) that provides two endpoints for checking the liveness and readiness state of HTTP applications. + +## Overview + +- **Liveness Probe**: Checks if the server is up and running. + - **Default Endpoint**: `/livez` + - **Behavior**: By default returns `true` immediately when the server is operational. + +- **Readiness Probe**: Assesses if the application is ready to handle requests. + - **Default Endpoint**: `/readyz` + - **Behavior**: By default returns `true` immediately when the server is operational. + +- **HTTP Status Codes**: + - `200 OK`: Returned when the checker function evaluates to `true`. + - `503 Service Unavailable`: Returned when the checker function evaluates to `false`. + +## Signatures + +```go +func New(config Config) fiber.Handler +``` + +## Examples + +Import the middleware package that is part of the [Fiber](https://github.com/gofiber/fiber) web framework +```go +import ( + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/healthcheck" +) +``` + +After you initiate your [Fiber](https://github.com/gofiber/fiber) app, you can use the following possibilities: + +```go +// Provide a minimal config +app.Use(healthcheck.New()) + +// Or extend your config for customization +app.Use(healthcheck.New(healthcheck.Config{ + LivenessProbe: func(c *fiber.Ctx) bool { + return true + }, + LivenessEndpoint: "/live", + ReadinessProbe: func(c *fiber.Ctx) bool { + return serviceA.Ready() && serviceB.Ready() && ... + }, + ReadinessEndpoint: "/ready", +})) +``` + +## Config + +```go +type Config struct { + // Next defines a function to skip this middleware when returned true. + // + // Optional. Default: nil + Next func(c *fiber.Ctx) bool + + // Function used for checking the liveness of the application. Returns true if the application + // is running and false if it is not. The liveness probe is typically used to indicate if + // the application is in a state where it can handle requests (e.g., the server is up and running). + // + // Optional. Default: func(c *fiber.Ctx) bool { return true } + LivenessProbe HealthChecker + + // HTTP endpoint at which the liveness probe will be available. + // + // Optional. Default: "/livez" + LivenessEndpoint string + + // Function used for checking the readiness of the application. Returns true if the application + // is ready to process requests and false otherwise. The readiness probe typically checks if all necessary + // services, databases, and other dependencies are available for the application to function correctly. + // + // Optional. Default: func(c *fiber.Ctx) bool { return true } + ReadinessProbe HealthChecker + + // HTTP endpoint at which the readiness probe will be available. + // Optional. Default: "/readyz" + ReadinessEndpoint string +} +``` + +## Default Config + +The default configuration used by this middleware is defined as follows: +```go +func defaultLivenessProbe(*fiber.Ctx) bool { return true } + +func defaultReadinessProbe(*fiber.Ctx) bool { return true } + +var ConfigDefault = Config{ + LivenessProbe: defaultLivenessProbe, + ReadinessProbe: defaultReadinessProbe, + LivenessEndpoint: "/livez", + ReadinessEndpoint: "/readyz", +} +``` diff --git a/docs/api/middleware/helmet.md b/docs/api/middleware/helmet.md new file mode 100644 index 0000000..0835f31 --- /dev/null +++ b/docs/api/middleware/helmet.md @@ -0,0 +1,82 @@ +--- +id: helmet +--- + +# Helmet + +Helmet middleware helps secure your apps by setting various HTTP headers. + +## Signatures + +```go +func New(config ...Config) fiber.Handler +``` + +## Examples +```go +package main + +import ( + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/helmet" +) + +func main() { + app := fiber.New() + + app.Use(helmet.New()) + + app.Get("/", func(c *fiber.Ctx) error { + return c.SendString("Welcome!") + }) + + app.Listen(":3000") +} +``` + +**Test:** + +```curl +curl -I http://localhost:3000 +``` + +## Config + +| Property | Type | Description | Default | +|:--------------------------|:------------------------|:--------------------------------------------|:-----------------| +| Next | `func(*fiber.Ctx) bool` | Next defines a function to skip middleware. | `nil` | +| XSSProtection | `string` | XSSProtection | "0" | +| ContentTypeNosniff | `string` | ContentTypeNosniff | "nosniff" | +| XFrameOptions | `string` | XFrameOptions | "SAMEORIGIN" | +| HSTSMaxAge | `int` | HSTSMaxAge | 0 | +| HSTSExcludeSubdomains | `bool` | HSTSExcludeSubdomains | false | +| ContentSecurityPolicy | `string` | ContentSecurityPolicy | "" | +| CSPReportOnly | `bool` | CSPReportOnly | false | +| HSTSPreloadEnabled | `bool` | HSTSPreloadEnabled | false | +| ReferrerPolicy | `string` | ReferrerPolicy | "ReferrerPolicy" | +| PermissionPolicy | `string` | Permissions-Policy | "" | +| CrossOriginEmbedderPolicy | `string` | Cross-Origin-Embedder-Policy | "require-corp" | +| CrossOriginOpenerPolicy | `string` | Cross-Origin-Opener-Policy | "same-origin" | +| CrossOriginResourcePolicy | `string` | Cross-Origin-Resource-Policy | "same-origin" | +| OriginAgentCluster | `string` | Origin-Agent-Cluster | "?1" | +| XDNSPrefetchControl | `string` | X-DNS-Prefetch-Control | "off" | +| XDownloadOptions | `string` | X-Download-Options | "noopen" | +| XPermittedCrossDomain | `string` | X-Permitted-Cross-Domain-Policies | "none" | + +## Default Config + +```go +var ConfigDefault = Config{ + XSSProtection: "0", + ContentTypeNosniff: "nosniff", + XFrameOptions: "SAMEORIGIN", + ReferrerPolicy: "no-referrer", + CrossOriginEmbedderPolicy: "require-corp", + CrossOriginOpenerPolicy: "same-origin", + CrossOriginResourcePolicy: "same-origin", + OriginAgentCluster: "?1", + XDNSPrefetchControl: "off", + XDownloadOptions: "noopen", + XPermittedCrossDomain: "none", +} +``` diff --git a/docs/api/middleware/idempotency.md b/docs/api/middleware/idempotency.md new file mode 100644 index 0000000..bab7c0e --- /dev/null +++ b/docs/api/middleware/idempotency.md @@ -0,0 +1,83 @@ +--- +id: idempotency +--- + +# Idempotency + +Idempotency middleware for [Fiber](https://github.com/gofiber/fiber) allows for fault-tolerant APIs where duplicate requests — for example due to networking issues on the client-side — do not erroneously cause the same action performed multiple times on the server-side. + +Refer to https://datatracker.ietf.org/doc/html/draft-ietf-httpapi-idempotency-key-header-02 for a better understanding. + +## Signatures + +```go +func New(config ...Config) fiber.Handler +``` + +## Examples + +Import the middleware package that is part of the Fiber web framework + +```go +import ( + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/idempotency" +) +``` + +After you initiate your Fiber app, you can use the following possibilities: + +### Default Config + +```go +app.Use(idempotency.New()) +``` + +### Custom Config + +```go +app.Use(idempotency.New(idempotency.Config{ + Lifetime: 42 * time.Minute, + // ... +})) +``` + +### Config + +| Property | Type | Description | Default | +|:--------------------|:------------------------|:-----------------------------------------------------------------------------------------|:-------------------------------| +| Next | `func(*fiber.Ctx) bool` | Next defines a function to skip this middleware when returned true. | A function for safe methods | +| Lifetime | `time.Duration` | Lifetime is the maximum lifetime of an idempotency key. | 30 * time.Minute | +| KeyHeader | `string` | KeyHeader is the name of the header that contains the idempotency key. | "X-Idempotency-Key" | +| KeyHeaderValidate | `func(string) error` | KeyHeaderValidate defines a function to validate the syntax of the idempotency header. | A function for UUID validation | +| KeepResponseHeaders | `[]string` | KeepResponseHeaders is a list of headers that should be kept from the original response. | nil (keep all headers) | +| Lock | `Locker` | Lock locks an idempotency key. | An in-memory locker | +| Storage | `fiber.Storage` | Storage stores response data by idempotency key. | An in-memory storage | + +## Default Config + +```go +var ConfigDefault = Config{ + Next: func(c *fiber.Ctx) bool { + // Skip middleware if the request was done using a safe HTTP method + return fiber.IsMethodSafe(c.Method()) + }, + + Lifetime: 30 * time.Minute, + + KeyHeader: "X-Idempotency-Key", + KeyHeaderValidate: func(k string) error { + if l, wl := len(k), 36; l != wl { // UUID length is 36 chars + return fmt.Errorf("%w: invalid length: %d != %d", ErrInvalidIdempotencyKey, l, wl) + } + + return nil + }, + + KeepResponseHeaders: nil, + + Lock: nil, // Set in configDefault so we don't allocate data here. + + Storage: nil, // Set in configDefault so we don't allocate data here. +} +``` diff --git a/docs/api/middleware/keyauth.md b/docs/api/middleware/keyauth.md new file mode 100644 index 0000000..4705c2e --- /dev/null +++ b/docs/api/middleware/keyauth.md @@ -0,0 +1,243 @@ +--- +id: keyauth +--- + +# Keyauth + +Key auth middleware provides a key based authentication. + +## Signatures + +```go +func New(config ...Config) fiber.Handler +``` + +## Examples + +```go +package main + +import ( + "crypto/sha256" + "crypto/subtle" + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/keyauth" +) + +var ( + apiKey = "correct horse battery staple" +) + +func validateAPIKey(c *fiber.Ctx, key string) (bool, error) { + hashedAPIKey := sha256.Sum256([]byte(apiKey)) + hashedKey := sha256.Sum256([]byte(key)) + + if subtle.ConstantTimeCompare(hashedAPIKey[:], hashedKey[:]) == 1 { + return true, nil + } + return false, keyauth.ErrMissingOrMalformedAPIKey +} + +func main() { + app := fiber.New() + + // note that the keyauth middleware needs to be defined before the routes are defined! + app.Use(keyauth.New(keyauth.Config{ + KeyLookup: "cookie:access_token", + Validator: validateAPIKey, + })) + + app.Get("/", func(c *fiber.Ctx) error { + return c.SendString("Successfully authenticated!") + }) + + app.Listen(":3000") +} +``` + +**Test:** + +```bash +# No api-key specified -> 400 missing +curl http://localhost:3000 +#> missing or malformed API Key + +curl --cookie "access_token=correct horse battery staple" http://localhost:3000 +#> Successfully authenticated! + +curl --cookie "access_token=Clearly A Wrong Key" http://localhost:3000 +#> missing or malformed API Key +``` + +For a more detailed example, see also the [`github.com/gofiber/recipes`](https://github.com/gofiber/recipes) repository and specifically the `fiber-envoy-extauthz` repository and the [`keyauth example`](https://github.com/gofiber/recipes/blob/master/fiber-envoy-extauthz/authz/main.go) code. + + +### Authenticate only certain endpoints + +If you want to authenticate only certain endpoints, you can use the `Config` of keyauth and apply a filter function (eg. `authFilter`) like so + +```go +package main + +import ( + "crypto/sha256" + "crypto/subtle" + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/keyauth" + "regexp" + "strings" +) + +var ( + apiKey = "correct horse battery staple" + protectedURLs = []*regexp.Regexp{ + regexp.MustCompile("^/authenticated$"), + regexp.MustCompile("^/auth2$"), + } +) + +func validateAPIKey(c *fiber.Ctx, key string) (bool, error) { + hashedAPIKey := sha256.Sum256([]byte(apiKey)) + hashedKey := sha256.Sum256([]byte(key)) + + if subtle.ConstantTimeCompare(hashedAPIKey[:], hashedKey[:]) == 1 { + return true, nil + } + return false, keyauth.ErrMissingOrMalformedAPIKey +} + +func authFilter(c *fiber.Ctx) bool { + originalURL := strings.ToLower(c.OriginalURL()) + + for _, pattern := range protectedURLs { + if pattern.MatchString(originalURL) { + return false + } + } + return true +} + +func main() { + app := fiber.New() + + app.Use(keyauth.New(keyauth.Config{ + Next: authFilter, + KeyLookup: "cookie:access_token", + Validator: validateAPIKey, + })) + + app.Get("/", func(c *fiber.Ctx) error { + return c.SendString("Welcome") + }) + app.Get("/authenticated", func(c *fiber.Ctx) error { + return c.SendString("Successfully authenticated!") + }) + app.Get("/auth2", func(c *fiber.Ctx) error { + return c.SendString("Successfully authenticated 2!") + }) + + app.Listen(":3000") +} +``` + +Which results in this + +```bash +# / does not need to be authenticated +curl http://localhost:3000 +#> Welcome + +# /authenticated needs to be authenticated +curl --cookie "access_token=correct horse battery staple" http://localhost:3000/authenticated +#> Successfully authenticated! + +# /auth2 needs to be authenticated too +curl --cookie "access_token=correct horse battery staple" http://localhost:3000/auth2 +#> Successfully authenticated 2! +``` + +### Specifying middleware in the handler + +```go +package main + +import ( + "crypto/sha256" + "crypto/subtle" + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/keyauth" +) + +const ( + apiKey = "my-super-secret-key" +) + +func main() { + app := fiber.New() + + authMiddleware := keyauth.New(keyauth.Config{ + Validator: func(c *fiber.Ctx, key string) (bool, error) { + hashedAPIKey := sha256.Sum256([]byte(apiKey)) + hashedKey := sha256.Sum256([]byte(key)) + + if subtle.ConstantTimeCompare(hashedAPIKey[:], hashedKey[:]) == 1 { + return true, nil + } + return false, keyauth.ErrMissingOrMalformedAPIKey + }, + }) + + app.Get("/", func(c *fiber.Ctx) error { + return c.SendString("Welcome") + }) + + app.Get("/allowed", authMiddleware, func(c *fiber.Ctx) error { + return c.SendString("Successfully authenticated!") + }) + + app.Listen(":3000") +} +``` + +Which results in this + +```bash +# / does not need to be authenticated +curl http://localhost:3000 +#> Welcome + +# /allowed needs to be authenticated too +curl --header "Authorization: Bearer my-super-secret-key" http://localhost:3000/allowed +#> Successfully authenticated! +``` + +## Config + +| Property | Type | Description | Default | +|:---------------|:-----------------------------------------|:-----------------------------------------------------------------------------------------------------|:------------------------------| +| Next | `func(*fiber.Ctx) bool` | Next defines a function to skip this middleware when returned true. | `nil` | +| SuccessHandler | `fiber.Handler` | SuccessHandler defines a function which is executed for a valid key. | `nil` | +| ErrorHandler | `fiber.ErrorHandler` | ErrorHandler defines a function which is executed for an invalid key. | `401 Invalid or expired key` | +| KeyLookup | `string` | KeyLookup is a string in the form of "`:`" that is used to extract key from the request. | "header:Authorization" | +| AuthScheme | `string` | AuthScheme to be used in the Authorization header. | "Bearer" | +| Validator | `func(*fiber.Ctx, string) (bool, error)` | Validator is a function to validate the key. | A function for key validation | +| ContextKey | `interface{}` | Context key to store the bearer token from the token into context. | "token" | + +## Default Config + +```go +var ConfigDefault = Config{ + SuccessHandler: func(c *fiber.Ctx) error { + return c.Next() + }, + ErrorHandler: func(c *fiber.Ctx, err error) error { + if err == ErrMissingOrMalformedAPIKey { + return c.Status(fiber.StatusUnauthorized).SendString(err.Error()) + } + return c.Status(fiber.StatusUnauthorized).SendString("Invalid or expired API Key") + }, + KeyLookup: "header:" + fiber.HeaderAuthorization, + AuthScheme: "Bearer", + ContextKey: "token", +} +``` diff --git a/docs/api/middleware/limiter.md b/docs/api/middleware/limiter.md new file mode 100644 index 0000000..8a48cbd --- /dev/null +++ b/docs/api/middleware/limiter.md @@ -0,0 +1,125 @@ +--- +id: limiter +--- + +# Limiter + +Limiter middleware for [Fiber](https://github.com/gofiber/fiber) that is used to limit repeat requests to public APIs and/or endpoints such as password reset. It is also useful for API clients, web crawling, or other tasks that need to be throttled. + +:::note +This middleware uses our [Storage](https://github.com/gofiber/storage) package to support various databases through a single interface. The default configuration for this middleware saves data to memory, see the examples below for other databases. +::: + +:::note +This module does not share state with other processes/servers by default. +::: + +## Signatures + +```go +func New(config ...Config) fiber.Handler +``` + +## Examples + +Import the middleware package that is part of the Fiber web framework + +```go +import ( + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/limiter" +) +``` + +After you initiate your Fiber app, you can use the following possibilities: + +```go +// Initialize default config +app.Use(limiter.New()) + +// Or extend your config for customization +app.Use(limiter.New(limiter.Config{ + Next: func(c *fiber.Ctx) bool { + return c.IP() == "127.0.0.1" + }, + Max: 20, + Expiration: 30 * time.Second, + KeyGenerator: func(c *fiber.Ctx) string { + return c.Get("x-forwarded-for") + }, + LimitReached: func(c *fiber.Ctx) error { + return c.SendFile("./toofast.html") + }, + Storage: myCustomStorage{}, +})) +``` + +## Sliding window + +Instead of using the standard fixed window algorithm, you can enable the [sliding window](https://en.wikipedia.org/wiki/Sliding_window_protocol) algorithm. + +A example of such configuration is: + +```go +app.Use(limiter.New(limiter.Config{ + Max: 20, + Expiration: 30 * time.Second, + LimiterMiddleware: limiter.SlidingWindow{}, +})) +``` + +This means that every window will take into account the previous window(if there was any). The given formula for the rate is: +``` +weightOfPreviousWindpw = previous window's amount request * (whenNewWindow / Expiration) +rate = weightOfPreviousWindpw + current window's amount request. +``` + +## Config + +| Property | Type | Description | Default | +|:-----------------------|:--------------------------|:--------------------------------------------------------------------------------------------|:-----------------------------------------| +| Next | `func(*fiber.Ctx) bool` | Next defines a function to skip this middleware when returned true. | `nil` | +| Max | `int` | Max number of recent connections during `Expiration` seconds before sending a 429 response. | 5 | +| KeyGenerator | `func(*fiber.Ctx) string` | KeyGenerator allows you to generate custom keys, by default c.IP() is used. | A function using c.IP() as the default | +| Expiration | `time.Duration` | Expiration is the time on how long to keep records of requests in memory. | 1 * time.Minute | +| LimitReached | `fiber.Handler` | LimitReached is called when a request hits the limit. | A function sending 429 response | +| SkipFailedRequests | `bool` | When set to true, requests with StatusCode >= 400 won't be counted. | false | +| SkipSuccessfulRequests | `bool` | When set to true, requests with StatusCode < 400 won't be counted. | false | +| Storage | `fiber.Storage` | Store is used to store the state of the middleware. | An in-memory store for this process only | +| LimiterMiddleware | `LimiterHandler` | LimiterMiddleware is the struct that implements a limiter middleware. | A new Fixed Window Rate Limiter | +| Duration (Deprecated) | `time.Duration` | Deprecated: Use Expiration instead | - | +| Store (Deprecated) | `fiber.Storage` | Deprecated: Use Storage instead | - | +| Key (Deprecated) | `func(*fiber.Ctx) string` | Deprecated: Use KeyGenerator instead | - | + +:::note +A custom store can be used if it implements the `Storage` interface - more details and an example can be found in `store.go`. +::: + +## Default Config + +```go +var ConfigDefault = Config{ + Max: 5, + Expiration: 1 * time.Minute, + KeyGenerator: func(c *fiber.Ctx) string { + return c.IP() + }, + LimitReached: func(c *fiber.Ctx) error { + return c.SendStatus(fiber.StatusTooManyRequests) + }, + SkipFailedRequests: false, + SkipSuccessfulRequests: false, + LimiterMiddleware: FixedWindow{}, +} +``` + +### Custom Storage/Database + +You can use any storage from our [storage](https://github.com/gofiber/storage/) package. + +```go +storage := sqlite3.New() // From github.com/gofiber/storage/sqlite3 +app.Use(limiter.New(limiter.Config{ + Storage: storage, +})) +``` diff --git a/docs/api/middleware/logger.md b/docs/api/middleware/logger.md new file mode 100644 index 0000000..596ace3 --- /dev/null +++ b/docs/api/middleware/logger.md @@ -0,0 +1,168 @@ +--- +id: logger +--- + +# Logger + +Logger middleware for [Fiber](https://github.com/gofiber/fiber) that logs HTTP request/response details. + +## Signatures +```go +func New(config ...Config) fiber.Handler +``` +## Examples + +Import the middleware package that is part of the Fiber web framework + +```go +import ( + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/logger" +) +``` + +:::tip +The order of registration plays a role. Only all routes that are registered after this one will be logged. +The middleware should therefore be one of the first to be registered. +::: + +After you initiate your Fiber app, you can use the following possibilities: + +```go +// Initialize default config +app.Use(logger.New()) + +// Or extend your config for customization +// Logging remote IP and Port +app.Use(logger.New(logger.Config{ + Format: "[${ip}]:${port} ${status} - ${method} ${path}\n", +})) + +// Logging Request ID +app.Use(requestid.New()) +app.Use(logger.New(logger.Config{ + // For more options, see the Config section + Format: "${pid} ${locals:requestid} ${status} - ${method} ${path}​\n", +})) + +// Changing TimeZone & TimeFormat +app.Use(logger.New(logger.Config{ + Format: "${pid} ${status} - ${method} ${path}\n", + TimeFormat: "02-Jan-2006", + TimeZone: "America/New_York", +})) + +// Custom File Writer +file, err := os.OpenFile("./123.log", os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666) +if err != nil { + log.Fatalf("error opening file: %v", err) +} +defer file.Close() +app.Use(logger.New(logger.Config{ + Output: file, +})) + +// Add Custom Tags +app.Use(logger.New(logger.Config{ + CustomTags: map[string]logger.LogFunc{ + "custom_tag": func(output logger.Buffer, c *fiber.Ctx, data *logger.Data, extraParam string) (int, error) { + return output.WriteString("it is a custom tag") + }, + }, +})) + +// Callback after log is written +app.Use(logger.New(logger.Config{ + TimeFormat: time.RFC3339Nano, + TimeZone: "Asia/Shanghai", + Done: func(c *fiber.Ctx, logString []byte) { + if c.Response().StatusCode() != fiber.StatusOK { + reporter.SendToSlack(logString) + } + }, +})) + +// Disable colors when outputting to default format +app.Use(logger.New(logger.Config{ + DisableColors: true, +})) +``` + +## Config + +### Config + +| Property | Type | Description | Default | +|:-----------------|:---------------------------|:---------------------------------------------------------------------------------------------------------------------------------|:------------------------------------------------------------------------------------| +| Next | `func(*fiber.Ctx) bool` | Next defines a function to skip this middleware when returned true. | `nil` | +| Done | `func(*fiber.Ctx, []byte)` | Done is a function that is called after the log string for a request is written to Output, and pass the log string as parameter. | `nil` | +| CustomTags | `map[string]LogFunc` | tagFunctions defines the custom tag action. | `map[string]LogFunc` | +| Format | `string` | Format defines the logging tags. | `${time} \| ${status} \| ${latency} \| ${ip} \| ${method} \| ${path} \| ${error}\n` || TimeFormat | `string` | TimeFormat defines the time format for log timestamps. | `15:04:05` | +| TimeZone | `string` | TimeZone can be specified, such as "UTC" and "America/New_York" and "Asia/Chongqing", etc | `"Local"` | +| TimeInterval | `time.Duration` | TimeInterval is the delay before the timestamp is updated. | `500 * time.Millisecond` | +| Output | `io.Writer` | Output is a writer where logs are written. | `os.Stdout` | +| DisableColors | `bool` | DisableColors defines if the logs output should be colorized. | `false` | +| enableColors | `bool` | Internal field for enabling colors in the log output. (This is not a user-configurable field) | - | +| enableLatency | `bool` | Internal field for enabling latency measurement in logs. (This is not a user-configurable field) | - | +| timeZoneLocation | `*time.Location` | Internal field for the time zone location. (This is not a user-configurable field) | - | + +## Default Config +```go +var ConfigDefault = Config{ + Next: nil, + Done: nil, + Format: "${time} | ${status} | ${latency} | ${ip} | ${method} | ${path} | ${error}\n", + TimeFormat: "15:04:05", + TimeZone: "Local", + TimeInterval: 500 * time.Millisecond, + Output: os.Stdout, + DisableColors: false, +} +``` + +## Constants +```go +// Logger variables +const ( + TagPid = "pid" + TagTime = "time" + TagReferer = "referer" + TagProtocol = "protocol" + TagPort = "port" + TagIP = "ip" + TagIPs = "ips" + TagHost = "host" + TagMethod = "method" + TagPath = "path" + TagURL = "url" + TagUA = "ua" + TagLatency = "latency" + TagStatus = "status" // response status + TagResBody = "resBody" // response body + TagReqHeaders = "reqHeaders" + TagQueryStringParams = "queryParams" // request query parameters + TagBody = "body" // request body + TagBytesSent = "bytesSent" + TagBytesReceived = "bytesReceived" + TagRoute = "route" + TagError = "error" + // DEPRECATED: Use TagReqHeader instead + TagHeader = "header:" // request header + TagReqHeader = "reqHeader:" // request header + TagRespHeader = "respHeader:" // response header + TagQuery = "query:" // request query + TagForm = "form:" // request form + TagCookie = "cookie:" // request cookie + TagLocals = "locals:" + // colors + TagBlack = "black" + TagRed = "red" + TagGreen = "green" + TagYellow = "yellow" + TagBlue = "blue" + TagMagenta = "magenta" + TagCyan = "cyan" + TagWhite = "white" + TagReset = "reset" +) +``` diff --git a/docs/api/middleware/monitor.md b/docs/api/middleware/monitor.md new file mode 100644 index 0000000..cbac367 --- /dev/null +++ b/docs/api/middleware/monitor.md @@ -0,0 +1,81 @@ +--- +id: monitor +--- + +# Monitor + +Monitor middleware for [Fiber](https://github.com/gofiber/fiber) that reports server metrics, inspired by [express-status-monitor](https://github.com/RafalWilinski/express-status-monitor) + +:::caution + +Monitor is still in beta, API might change in the future! + +::: + +![](https://i.imgur.com/nHAtBpJ.gif) + +### Signatures +```go +func New() fiber.Handler +``` + +### Examples +Import the middleware package that is part of the Fiber web framework + +```go +import ( + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/monitor" +) +``` + +After you initiate your Fiber app, you can use the following possibilities: +```go +// Initialize default config (Assign the middleware to /metrics) +app.Get("/metrics", monitor.New()) + +// Or extend your config for customization +// Assign the middleware to /metrics +// and change the Title to `MyService Metrics Page` +app.Get("/metrics", monitor.New(monitor.Config{Title: "MyService Metrics Page"})) +``` +You can also access the API endpoint with +`curl -X GET -H "Accept: application/json" http://localhost:3000/metrics` which returns: +```json +{"pid":{ "cpu":0.4568381746582226, "ram":20516864, "conns":3 }, + "os": { "cpu":8.759124087593099, "ram":3997155328, "conns":44, + "total_ram":8245489664, "load_avg":0.51 }} +``` + +## Config + +| Property | Type | Description | Default | +|:-----------|:------------------------|:--------------------------------------------------------------------|:----------------------------------------------------------------------------| +| Title | `string` | Metrics page title | "Fiber Monitor" | +| Refresh | `time.Duration` | Refresh period | 3 seconds | +| APIOnly | `bool` | Whether the service should expose only the monitoring API | false | +| Next | `func(*fiber.Ctx) bool` | Next defines a function to skip this middleware when returned true. | `nil` | +| CustomHead | `string` | Custom HTML Code to Head Section(Before End) | empty | +| FontURL | `string` | FontURL for specify font resource path or URL | "https://fonts.googleapis.com/css2?family=Roboto:wght@400;900&display=swap" | +| ChartJsURL | `string` | ChartJsURL for specify ChartJS library path or URL | "https://cdn.jsdelivr.net/npm/chart.js@2.9/dist/Chart.bundle.min.js" | + +## Default Config + +```go +var ConfigDefault = Config{ + Title: defaultTitle, + Refresh: defaultRefresh, + FontURL: defaultFontURL, + ChartJsURL: defaultChartJSURL, + CustomHead: defaultCustomHead, + APIOnly: false, + Next: nil, + index: newIndex(viewBag{ + defaultTitle, + defaultRefresh, + defaultFontURL, + defaultChartJSURL, + defaultCustomHead, + }), +} +``` diff --git a/docs/api/middleware/pprof.md b/docs/api/middleware/pprof.md new file mode 100644 index 0000000..c4808f2 --- /dev/null +++ b/docs/api/middleware/pprof.md @@ -0,0 +1,53 @@ +--- +id: pprof +--- + +# Pprof + +Pprof middleware for [Fiber](https://github.com/gofiber/fiber) that serves via its HTTP server runtime profiling data in the format expected by the pprof visualization tool. The package is typically only imported for the side effect of registering its HTTP handlers. The handled paths all begin with /debug/pprof/. + +## Signatures + +```go +func New() fiber.Handler +``` + +## Examples + +Import the middleware package that is part of the Fiber web framework + +```go +import ( + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/pprof" +) +``` + +After you initiate your Fiber app, you can use the following possibilities: + +```go +// Initialize default config +app.Use(pprof.New()) + +// Or extend your config for customization + +// For example, in systems where you have multiple ingress endpoints, it is common to add a URL prefix, like so: +app.Use(pprof.New(pprof.Config{Prefix: "/endpoint-prefix"})) + +// This prefix will be added to the default path of "/debug/pprof/", for a resulting URL of: "/endpoint-prefix/debug/pprof/". +``` + +## Config + +| Property | Type | Description | Default | +|:---------|:------------------------|:------------------------------------------------------------------------------------------------------------------------------------------------|:--------| +| Next | `func(*fiber.Ctx) bool` | Next defines a function to skip this middleware when returned true. | `nil` | +| Prefix | `string` | Prefix defines a URL prefix added before "/debug/pprof". Note that it should start with (but not end with) a slash. Example: "/federated-fiber" | "" | + +## Default Config + +```go +var ConfigDefault = Config{ + Next: nil, +} +``` diff --git a/docs/api/middleware/proxy.md b/docs/api/middleware/proxy.md new file mode 100644 index 0000000..e36654f --- /dev/null +++ b/docs/api/middleware/proxy.md @@ -0,0 +1,165 @@ +--- +id: proxy +--- + +# Proxy + +Proxy middleware for [Fiber](https://github.com/gofiber/fiber) that allows you to proxy requests to multiple servers. + +## Signatures + +```go +// Balancer create a load balancer among multiple upstrem servers. +func Balancer(config Config) fiber.Handler +// Forward performs the given http request and fills the given http response. +func Forward(addr string, clients ...*fasthttp.Client) fiber.Handler +// Do performs the given http request and fills the given http response. +func Do(c *fiber.Ctx, addr string, clients ...*fasthttp.Client) error +// DoRedirects performs the given http request and fills the given http response while following up to maxRedirectsCount redirects. +func DoRedirects(c *fiber.Ctx, addr string, maxRedirectsCount int, clients ...*fasthttp.Client) error +// DoDeadline performs the given request and waits for response until the given deadline. +func DoDeadline(c *fiber.Ctx, addr string, deadline time.Time, clients ...*fasthttp.Client) error +// DoTimeout performs the given request and waits for response during the given timeout duration. +func DoTimeout(c *fiber.Ctx, addr string, timeout time.Duration, clients ...*fasthttp.Client) error +// DomainForward the given http request based on the given domain and fills the given http response +func DomainForward(hostname string, addr string, clients ...*fasthttp.Client) fiber.Handler +// BalancerForward performs the given http request based round robin balancer and fills the given http response +func BalancerForward(servers []string, clients ...*fasthttp.Client) fiber.Handler +``` + +## Examples + +Import the middleware package that is part of the Fiber web framework + +```go +import ( + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/proxy" +) +``` + +After you initiate your Fiber app, you can use the following possibilities: + +```go +// if target https site uses a self-signed certificate, you should +// call WithTlsConfig before Do and Forward +proxy.WithTlsConfig(&tls.Config{ + InsecureSkipVerify: true, +}) +// if you need to use global self-custom client, you should use proxy.WithClient. +proxy.WithClient(&fasthttp.Client{ + NoDefaultUserAgentHeader: true, + DisablePathNormalizing: true, +}) + +// Forward to url +app.Get("/gif", proxy.Forward("https://i.imgur.com/IWaBepg.gif")) + +// If you want to forward with a specific domain. You have to use proxy.DomainForward. +app.Get("/payments", proxy.DomainForward("docs.gofiber.io", "http://localhost:8000")) + +// Forward to url with local custom client +app.Get("/gif", proxy.Forward("https://i.imgur.com/IWaBepg.gif", &fasthttp.Client{ + NoDefaultUserAgentHeader: true, + DisablePathNormalizing: true, +})) + +// Make request within handler +app.Get("/:id", func(c *fiber.Ctx) error { + url := "https://i.imgur.com/"+c.Params("id")+".gif" + if err := proxy.Do(c, url); err != nil { + return err + } + // Remove Server header from response + c.Response().Header.Del(fiber.HeaderServer) + return nil +}) + +// Make proxy requests while following redirects +app.Get("/proxy", func(c *fiber.Ctx) error { + if err := proxy.DoRedirects(c, "http://google.com", 3); err != nil { + return err + } + // Remove Server header from response + c.Response().Header.Del(fiber.HeaderServer) + return nil +}) + +// Make proxy requests and wait up to 5 seconds before timing out +app.Get("/proxy", func(c *fiber.Ctx) error { + if err := proxy.DoTimeout(c, "http://localhost:3000", time.Second * 5); err != nil { + return err + } + // Remove Server header from response + c.Response().Header.Del(fiber.HeaderServer) + return nil +}) + +// Make proxy requests, timeout a minute from now +app.Get("/proxy", func(c *fiber.Ctx) error { + if err := proxy.DoDeadline(c, "http://localhost", time.Now().Add(time.Minute)); err != nil { + return err + } + // Remove Server header from response + c.Response().Header.Del(fiber.HeaderServer) + return nil +}) + +// Minimal round robin balancer +app.Use(proxy.Balancer(proxy.Config{ + Servers: []string{ + "http://localhost:3001", + "http://localhost:3002", + "http://localhost:3003", + }, +})) + +// Or extend your balancer for customization +app.Use(proxy.Balancer(proxy.Config{ + Servers: []string{ + "http://localhost:3001", + "http://localhost:3002", + "http://localhost:3003", + }, + ModifyRequest: func(c *fiber.Ctx) error { + c.Request().Header.Add("X-Real-IP", c.IP()) + return nil + }, + ModifyResponse: func(c *fiber.Ctx) error { + c.Response().Header.Del(fiber.HeaderServer) + return nil + }, +})) + +// Or this way if the balancer is using https and the destination server is only using http. +app.Use(proxy.BalancerForward([]string{ + "http://localhost:3001", + "http://localhost:3002", + "http://localhost:3003", +})) +``` + +## Config + +| Property | Type | Description | Default | +|:----------------|:-----------------------------------------------|:---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:----------------| +| Next | `func(*fiber.Ctx) bool` | Next defines a function to skip this middleware when returned true. | `nil` | +| Servers | `[]string` | Servers defines a list of `://` HTTP servers, which are used in a round-robin manner. i.e.: "https://foobar.com, http://www.foobar.com" | (Required) | +| ModifyRequest | `fiber.Handler` | ModifyRequest allows you to alter the request. | `nil` | +| ModifyResponse | `fiber.Handler` | ModifyResponse allows you to alter the response. | `nil` | +| Timeout | `time.Duration` | Timeout is the request timeout used when calling the proxy client. | 1 second | +| ReadBufferSize | `int` | Per-connection buffer size for requests' reading. This also limits the maximum header size. Increase this buffer if your clients send multi-KB RequestURIs and/or multi-KB headers (for example, BIG cookies). | (Not specified) | +| WriteBufferSize | `int` | Per-connection buffer size for responses' writing. | (Not specified) | +| TlsConfig | `*tls.Config` (or `*fasthttp.TLSConfig` in v3) | TLS config for the HTTP client. | `nil` | +| Client | `*fasthttp.LBClient` | Client is a custom client when client config is complex. | `nil` | + +## Default Config + +```go +var ConfigDefault = Config{ + Next: nil, + ModifyRequest: nil, + ModifyResponse: nil, + Timeout: fasthttp.DefaultLBClientTimeout, +} +``` diff --git a/docs/api/middleware/recover.md b/docs/api/middleware/recover.md new file mode 100644 index 0000000..81f67fd --- /dev/null +++ b/docs/api/middleware/recover.md @@ -0,0 +1,54 @@ +--- +id: recover +--- + +# Recover + +Recover middleware for [Fiber](https://github.com/gofiber/fiber) that recovers from panics anywhere in the stack chain and handles the control to the centralized [ErrorHandler](https://docs.gofiber.io/guide/error-handling). + +## Signatures + +```go +func New(config ...Config) fiber.Handler +``` + +## Examples + +Import the middleware package that is part of the Fiber web framework + +```go +import ( + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/recover" +) +``` + +After you initiate your Fiber app, you can use the following possibilities: + +```go +// Initialize default config +app.Use(recover.New()) + +// This panic will be caught by the middleware +app.Get("/", func(c *fiber.Ctx) error { + panic("I'm an error") +}) +``` + +## Config + +| Property | Type | Description | Default | +|:------------------|:--------------------------------|:--------------------------------------------------------------------|:-------------------------| +| Next | `func(*fiber.Ctx) bool` | Next defines a function to skip this middleware when returned true. | `nil` | +| EnableStackTrace | `bool` | EnableStackTrace enables handling stack trace. | `false` | +| StackTraceHandler | `func(*fiber.Ctx, interface{})` | StackTraceHandler defines a function to handle stack trace. | defaultStackTraceHandler | + +## Default Config + +```go +var ConfigDefault = Config{ + Next: nil, + EnableStackTrace: false, + StackTraceHandler: defaultStackTraceHandler, +} +``` diff --git a/docs/api/middleware/redirect.md b/docs/api/middleware/redirect.md new file mode 100644 index 0000000..762aa0b --- /dev/null +++ b/docs/api/middleware/redirect.md @@ -0,0 +1,68 @@ +--- +id: redirect +--- + +# Redirect + +Redirection middleware for Fiber. + +## Signatures + +```go +func New(config ...Config) fiber.Handler +``` + +## Examples + +```go +package main + +import ( + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/redirect" +) + +func main() { + app := fiber.New() + + app.Use(redirect.New(redirect.Config{ + Rules: map[string]string{ + "/old": "/new", + "/old/*": "/new/$1", + }, + StatusCode: 301, + })) + + app.Get("/new", func(c *fiber.Ctx) error { + return c.SendString("Hello, World!") + }) + app.Get("/new/*", func(c *fiber.Ctx) error { + return c.SendString("Wildcard: " + c.Params("*")) + }) + + app.Listen(":3000") +} +``` + +**Test:** + +```curl +curl http://localhost:3000/old +curl http://localhost:3000/old/hello +``` + +## Config + +| Property | Type | Description | Default | +|:-----------|:------------------------|:---------------------------------------------------------------------------------------------------------------------------|:-----------------------| +| Next | `func(*fiber.Ctx) bool` | Filter defines a function to skip middleware. | `nil` | +| Rules | `map[string]string` | Rules defines the URL path rewrite rules. The values captured in asterisk can be retrieved by index e.g. $1, $2 and so on. | Required | +| StatusCode | `int` | The status code when redirecting. This is ignored if Redirect is disabled. | 302 Temporary Redirect | + +## Default Config + +```go +var ConfigDefault = Config{ + StatusCode: fiber.StatusFound, +} +``` diff --git a/docs/api/middleware/requestid.md b/docs/api/middleware/requestid.md new file mode 100644 index 0000000..c8ca8d5 --- /dev/null +++ b/docs/api/middleware/requestid.md @@ -0,0 +1,62 @@ +--- +id: requestid +--- + +# RequestID + +RequestID middleware for [Fiber](https://github.com/gofiber/fiber) that adds an identifier to the response. + +## Signatures + +```go +func New(config ...Config) fiber.Handler +``` + +## Examples + +Import the middleware package that is part of the Fiber web framework + +```go +import ( + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/requestid" +) +``` + +After you initiate your Fiber app, you can use the following possibilities: + +```go +// Initialize default config +app.Use(requestid.New()) + +// Or extend your config for customization +app.Use(requestid.New(requestid.Config{ + Header: "X-Custom-Header", + Generator: func() string { + return "static-id" + }, +})) +``` + +## Config + +| Property | Type | Description | Default | +|:-----------|:------------------------|:--------------------------------------------------------------------------------------------------|:---------------| +| Next | `func(*fiber.Ctx) bool` | Next defines a function to skip this middleware when returned true. | `nil` | +| Header | `string` | Header is the header key where to get/set the unique request ID. | "X-Request-ID" | +| Generator | `func() string` | Generator defines a function to generate the unique identifier. | utils.UUID | +| ContextKey | `interface{}` | ContextKey defines the key used when storing the request ID in the locals for a specific request. | "requestid" | + +## Default Config +The default config uses a fast UUID generator which will expose the number of +requests made to the server. To conceal this value for better privacy, use the +`utils.UUIDv4` generator. + +```go +var ConfigDefault = Config{ + Next: nil, + Header: fiber.HeaderXRequestID, + Generator: utils.UUID, + ContextKey: "requestid", +} +``` diff --git a/docs/api/middleware/rewrite.md b/docs/api/middleware/rewrite.md new file mode 100644 index 0000000..fd59595 --- /dev/null +++ b/docs/api/middleware/rewrite.md @@ -0,0 +1,58 @@ +--- +id: rewrite +--- + +# Rewrite + +Rewrite middleware rewrites the URL path based on provided rules. It can be helpful for backward compatibility or just creating cleaner and more descriptive links. + +## Signatures + +```go +func New(config ...Config) fiber.Handler +``` + +## Config + +| Property | Type | Description | Default | +|:---------|:------------------------|:-----------------------------------------------------------------------------------------------------|:-----------| +| Next | `func(*fiber.Ctx) bool` | Next defines a function to skip middleware. | `nil` | +| Rules | `map[string]string` | Rules defines the URL path rewrite rules. The values captured in asterisk can be retrieved by index. | (Required) | + +### Examples +```go +package main + +import ( + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/rewrite" +) + +func main() { + app := fiber.New() + + app.Use(rewrite.New(rewrite.Config{ + Rules: map[string]string{ + "/old": "/new", + "/old/*": "/new/$1", + }, + })) + + app.Get("/new", func(c *fiber.Ctx) error { + return c.SendString("Hello, World!") + }) + app.Get("/new/*", func(c *fiber.Ctx) error { + return c.SendString("Wildcard: " + c.Params("*")) + }) + + app.Listen(":3000") +} + +``` + +**Test:** + +```curl +curl http://localhost:3000/old +curl http://localhost:3000/old/hello +``` diff --git a/docs/api/middleware/session.md b/docs/api/middleware/session.md new file mode 100644 index 0000000..6315995 --- /dev/null +++ b/docs/api/middleware/session.md @@ -0,0 +1,139 @@ +--- +id: session +--- + +# Session + +Session middleware for [Fiber](https://github.com/gofiber/fiber). + +:::note +This middleware uses our [Storage](https://github.com/gofiber/storage) package to support various databases through a single interface. The default configuration for this middleware saves data to memory, see the examples below for other databases. +::: + +## Signatures + +```go +func New(config ...Config) *Store +func (s *Store) RegisterType(i interface{}) +func (s *Store) Get(c *fiber.Ctx) (*Session, error) +func (s *Store) Delete(id string) error +func (s *Store) Reset() error + +func (s *Session) Get(key string) interface{} +func (s *Session) Set(key string, val interface{}) +func (s *Session) Delete(key string) +func (s *Session) Destroy() error +func (s *Session) Reset() error +func (s *Session) Regenerate() error +func (s *Session) Save() error +func (s *Session) Fresh() bool +func (s *Session) ID() string +func (s *Session) Keys() []string +``` + +:::caution +Storing `interface{}` values are limited to built-ins Go types. +::: + +## Examples +Import the middleware package that is part of the Fiber web framework +```go +import ( + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/session" +) +``` + +After you initiate your Fiber app, you can use the following possibilities: + +```go +// Initialize default config +// This stores all of your app's sessions +store := session.New() + +app.Get("/", func(c *fiber.Ctx) error { + // Get session from storage + sess, err := store.Get(c) + if err != nil { + panic(err) + } + + // Get value + name := sess.Get("name") + + // Set key/value + sess.Set("name", "john") + + // Get all Keys + keys := sess.Keys() + + // Delete key + sess.Delete("name") + + // Destroy session + if err := sess.Destroy(); err != nil { + panic(err) + } + + // Sets a specific expiration for this session + sess.SetExpiry(time.Second * 2) + + // Save session + if err := sess.Save(); err != nil { + panic(err) + } + + return c.SendString(fmt.Sprintf("Welcome %v", name)) +}) +``` + +## Config + +| Property | Type | Description | Default | +|:------------------------|:----------------|:------------------------------------------------------------------------------------------------------------|:----------------------| +| Expiration | `time.Duration` | Allowed session duration. | `24 * time.Hour` | +| Storage | `fiber.Storage` | Storage interface to store the session data. | `memory.New()` | +| KeyLookup | `string` | KeyLookup is a string in the form of "`:`" that is used to extract session id from the request. | `"cookie:session_id"` | +| CookieDomain | `string` | Domain of the cookie. | `""` | +| CookiePath | `string` | Path of the cookie. | `""` | +| CookieSecure | `bool` | Indicates if cookie is secure. | `false` | +| CookieHTTPOnly | `bool` | Indicates if cookie is HTTP only. | `false` | +| CookieSameSite | `string` | Value of SameSite cookie. | `"Lax"` | +| CookieSessionOnly | `bool` | Decides whether cookie should last for only the browser session. Ignores Expiration if set to true. | `false` | +| KeyGenerator | `func() string` | KeyGenerator generates the session key. | `utils.UUIDv4` | +| CookieName (Deprecated) | `string` | Deprecated: Please use KeyLookup. The session name. | `""` | + +## Default Config + +```go +var ConfigDefault = Config{ + Expiration: 24 * time.Hour, + KeyLookup: "cookie:session_id", + KeyGenerator: utils.UUIDv4, + source: "cookie", + sessionName: "session_id", +} +``` + +## Constants + +```go +const ( + SourceCookie Source = "cookie" + SourceHeader Source = "header" + SourceURLQuery Source = "query" +) +``` + +### Custom Storage/Database + +You can use any storage from our [storage](https://github.com/gofiber/storage/) package. + +```go +storage := sqlite3.New() // From github.com/gofiber/storage/sqlite3 +store := session.New(session.Config{ + Storage: storage, +}) +``` + +To use the store, see the [Examples](#examples). diff --git a/docs/api/middleware/skip.md b/docs/api/middleware/skip.md new file mode 100644 index 0000000..0923bd0 --- /dev/null +++ b/docs/api/middleware/skip.md @@ -0,0 +1,47 @@ +--- +id: skip +--- + +# Skip + +Skip middleware for [Fiber](https://github.com/gofiber/fiber) that skips a wrapped handler if a predicate is true. + +## Signatures +```go +func New(handler fiber.Handler, exclude func(c *fiber.Ctx) bool) fiber.Handler +``` + +## Examples +Import the middleware package that is part of the Fiber web framework +```go +import ( + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/skip" +) +``` + +After you initiate your Fiber app, you can use the following possibilities: + +```go +func main() { + app := fiber.New() + + app.Use(skip.New(BasicHandler, func(ctx *fiber.Ctx) bool { + return ctx.Method() == fiber.MethodGet + })) + + app.Get("/", func(ctx *fiber.Ctx) error { + return ctx.SendString("It was a GET request!") + }) + + log.Fatal(app.Listen(":3000")) +} + +func BasicHandler(ctx *fiber.Ctx) error { + return ctx.SendString("It was not a GET request!") +} +``` + +:::tip +app.Use will handle requests from any route, and any method. In the example above, it will only skip if the method is GET. +::: diff --git a/docs/api/middleware/timeout.md b/docs/api/middleware/timeout.md new file mode 100644 index 0000000..5fdf18d --- /dev/null +++ b/docs/api/middleware/timeout.md @@ -0,0 +1,146 @@ +--- +id: timeout +--- + +# Timeout + +There exist two distinct implementations of timeout middleware [Fiber](https://github.com/gofiber/fiber). + +**New** + +Wraps a `fiber.Handler` with a timeout. If the handler takes longer than the given duration to return, the timeout error is set and forwarded to the centralized [ErrorHandler](https://docs.gofiber.io/error-handling). + +:::caution +This has been deprecated since it raises race conditions. +::: + +**NewWithContext** + +As a `fiber.Handler` wrapper, it creates a context with `context.WithTimeout` and pass it in `UserContext`. + +If the context passed executions (eg. DB ops, Http calls) takes longer than the given duration to return, the timeout error is set and forwarded to the centralized `ErrorHandler`. + + +It does not cancel long running executions. Underlying executions must handle timeout by using `context.Context` parameter. + +## Signatures + +```go +func New(handler fiber.Handler, timeout time.Duration, timeoutErrors ...error) fiber.Handler +func NewWithContext(handler fiber.Handler, timeout time.Duration, timeoutErrors ...error) fiber.Handler +``` + +## Examples + +Import the middleware package that is part of the Fiber web framework + +```go +import ( + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/timeout" +) +``` + +After you initiate your Fiber app, you can use the following possibilities: + +```go +func main() { + app := fiber.New() + + h := func(c *fiber.Ctx) error { + sleepTime, _ := time.ParseDuration(c.Params("sleepTime") + "ms") + if err := sleepWithContext(c.UserContext(), sleepTime); err != nil { + return fmt.Errorf("%w: execution error", err) + } + return nil + } + + app.Get("/foo/:sleepTime", timeout.New(h, 2*time.Second)) + log.Fatal(app.Listen(":3000")) +} + +func sleepWithContext(ctx context.Context, d time.Duration) error { + timer := time.NewTimer(d) + + select { + case <-ctx.Done(): + if !timer.Stop() { + <-timer.C + } + return context.DeadlineExceeded + case <-timer.C: + } + return nil +} +``` + +Test http 200 with curl: + +```bash +curl --location -I --request GET 'http://localhost:3000/foo/1000' +``` + +Test http 408 with curl: + +```bash +curl --location -I --request GET 'http://localhost:3000/foo/3000' +``` + +Use with custom error: + +```go +var ErrFooTimeOut = errors.New("foo context canceled") + +func main() { + app := fiber.New() + h := func(c *fiber.Ctx) error { + sleepTime, _ := time.ParseDuration(c.Params("sleepTime") + "ms") + if err := sleepWithContextWithCustomError(c.UserContext(), sleepTime); err != nil { + return fmt.Errorf("%w: execution error", err) + } + return nil + } + + app.Get("/foo/:sleepTime", timeout.NewWithContext(h, 2*time.Second, ErrFooTimeOut)) + log.Fatal(app.Listen(":3000")) +} + +func sleepWithContextWithCustomError(ctx context.Context, d time.Duration) error { + timer := time.NewTimer(d) + select { + case <-ctx.Done(): + if !timer.Stop() { + <-timer.C + } + return ErrFooTimeOut + case <-timer.C: + } + return nil +} +``` + +Sample usage with a DB call: + +```go +func main() { + app := fiber.New() + db, _ := gorm.Open(postgres.Open("postgres://localhost/foodb"), &gorm.Config{}) + + handler := func(ctx *fiber.Ctx) error { + tran := db.WithContext(ctx.UserContext()).Begin() + + if tran = tran.Exec("SELECT pg_sleep(50)"); tran.Error != nil { + return tran.Error + } + + if tran = tran.Commit(); tran.Error != nil { + return tran.Error + } + + return nil + } + + app.Get("/foo", timeout.NewWithContext(handler, 10*time.Second)) + log.Fatal(app.Listen(":3000")) +} +``` diff --git a/docs/extra/_category_.json b/docs/extra/_category_.json new file mode 100644 index 0000000..f17f137 --- /dev/null +++ b/docs/extra/_category_.json @@ -0,0 +1,8 @@ +{ + "label": "Extra", + "position": 4, + "link": { + "type": "generated-index", + "description": "Extra contents for Fiber." + } +} \ No newline at end of file diff --git a/docs/extra/benchmarks.md b/docs/extra/benchmarks.md new file mode 100644 index 0000000..3c2a820 --- /dev/null +++ b/docs/extra/benchmarks.md @@ -0,0 +1,112 @@ +--- +id: benchmarks +title: 📊 Benchmarks +description: >- + These benchmarks aim to compare the performance of Fiber and other web + frameworks. +sidebar_position: 2 +--- + +## TechEmpower + +[TechEmpower](https://www.techempower.com/benchmarks/#section=data-r19&hw=ph&test=composite) provides a performance comparison of many web application frameworks executing fundamental tasks such as JSON serialization, database access, and server-side template composition. + +Each framework is operating in a realistic production configuration. Results are captured on cloud instances and on physical hardware. The test implementations are largely community-contributed and all source is available at the [GitHub repository](https://github.com/TechEmpower/FrameworkBenchmarks). + +* Fiber `v1.10.0` +* 28 HT Cores Intel\(R\) Xeon\(R\) Gold 5120 CPU @ 2.20GHz +* 32GB RAM +* Ubuntu 18.04.3 4.15.0-88-generic +* Dedicated Cisco 10-Gbit Ethernet switch. + +### Plaintext + +The Plaintext test is an exercise of the request-routing fundamentals only, designed to demonstrate the capacity of high-performance platforms in particular. Requests will be sent using HTTP pipelining. The response payload is still small, meaning good performance is still necessary in order to saturate the gigabit Ethernet of the test environment. + +See [Plaintext requirements](https://github.com/TechEmpower/FrameworkBenchmarks/wiki/Project-Information-Framework-Tests-Overview#single-database-query) + +**Fiber** - **6,162,556** responses per second with an average latency of **2.0** ms. +**Express** - **367,069** responses per second with an average latency of **354.1** ms. + +![](/img/plaintext.png) + +![Fiber vs Express](/img/plaintext_express.png) + +### Data Updates + +**Fiber** handled **11,846** responses per second with an average latency of **42.8** ms. +**Express** handled **2,066** responses per second with an average latency of **390.44** ms. + +![](/img/data_updates.png) + +![Fiber vs Express](/img/data_updates_express.png) + +### Multiple Queries + +**Fiber** handled **19,664** responses per second with an average latency of **25.7** ms. +**Express** handled **4,302** responses per second with an average latency of **117.2** ms. + +![](/img/multiple_queries.png) + +![Fiber vs Express](/img/multiple_queries_express.png) + +### Single Query + +**Fiber** handled **368,647** responses per second with an average latency of **0.7** ms. +**Express** handled **57,880** responses per second with an average latency of **4.4** ms. + +![](/img/single_query.png) + +![Fiber vs Express](/img/single_query_express.png) + +### JSON Serialization + +**Fiber** handled **1,146,667** responses per second with an average latency of **0.4** ms. +**Express** handled **244,847** responses per second with an average latency of **1.1** ms. + +![](/img/json.png) + +![Fiber vs Express](/img/json_express.png) + +## Go web framework benchmark + +🔗 [https://github.com/smallnest/go-web-framework-benchmark](https://github.com/smallnest/go-web-framework-benchmark) + +* **CPU** Intel\(R\) Xeon\(R\) Gold 6140 CPU @ 2.30GHz +* **MEM** 4GB +* **GO** go1.13.6 linux/amd64 +* **OS** Linux + +The first test case is to mock **0 ms**, **10 ms**, **100 ms**, **500 ms** processing time in handlers. + +![](/img/benchmark.png) + +The concurrency clients are **5000**. + +![](/img/benchmark_latency.png) + +Latency is the time of real processing time by web servers. _The smaller is the better._ + +![](/img/benchmark_alloc.png) + +Allocs is the heap allocations by web servers when test is running. The unit is MB. _The smaller is the better._ + +If we enable **http pipelining**, test result as below: + +![](/img/benchmark-pipeline.png) + +Concurrency test in **30 ms** processing time, the test result for **100**, **1000**, **5000** clients is: + +![](/img/concurrency.png) + +![](/img/concurrency_latency.png) + +![](/img/concurrency_alloc.png) + +If we enable **http pipelining**, test result as below: + +![](/img/concurrency-pipeline.png) + +Dependency graph for `v1.9.0` + +![](/img/graph.svg) diff --git a/docs/extra/faq.md b/docs/extra/faq.md new file mode 100644 index 0000000..54dbca5 --- /dev/null +++ b/docs/extra/faq.md @@ -0,0 +1,169 @@ +--- +id: faq +title: 🤔 FAQ +description: >- + List of frequently asked questions. Feel free to open an issue to add your + question to this page. +sidebar_position: 1 +--- + +## How should I structure my application? + +There is no definitive answer to this question. The answer depends on the scale of your application and the team that is involved. To be as flexible as possible, Fiber makes no assumptions in terms of structure. + +Routes and other application-specific logic can live in as many files as you wish, in any directory structure you prefer. View the following examples for inspiration: + +* [gofiber/boilerplate](https://github.com/gofiber/boilerplate) +* [thomasvvugt/fiber-boilerplate](https://github.com/thomasvvugt/fiber-boilerplate) +* [Youtube - Building a REST API using Gorm and Fiber](https://www.youtube.com/watch?v=Iq2qT0fRhAA) +* [embedmode/fiberseed](https://github.com/embedmode/fiberseed) + +## How do I handle custom 404 responses? + +If you're using v2.32.0 or later, all you need to do is to implement a custom error handler. See below, or see a more detailed explanation at [Error Handling](../guide/error-handling.md#custom-error-handler). + +If you're using v2.31.0 or earlier, the error handler will not capture 404 errors. Instead, you need to add a middleware function at the very bottom of the stack \(below all other functions\) to handle a 404 response: + +```go title="Example" +app.Use(func(c *fiber.Ctx) error { + return c.Status(fiber.StatusNotFound).SendString("Sorry can't find that!") +}) +``` + +## How can i use live reload ? + +[Air](https://github.com/cosmtrek/air) is a handy tool that automatically restarts your Go applications whenever the source code changes, making your development process faster and more efficient. + +To use Air in a Fiber project, follow these steps: + +1. Install Air by downloading the appropriate binary for your operating system from the GitHub release page or by building the tool directly from source. +2. Create a configuration file for Air in your project directory. This file can be named, for example, .air.toml or air.conf. Here's a sample configuration file that works with Fiber: +```toml +# .air.toml +root = "." +tmp_dir = "tmp" +[build] + cmd = "go build -o ./tmp/main ." + bin = "./tmp/main" + delay = 1000 # ms + exclude_dir = ["assets", "tmp", "vendor"] + include_ext = ["go", "tpl", "tmpl", "html"] + exclude_regex = ["_test\\.go"] +``` +3. Start your Fiber application using Air by running the following command in the terminal: +```sh +air +``` + +As you make changes to your source code, Air will detect them and automatically restart the application. + +A complete example demonstrating the use of Air with Fiber can be found in the [Fiber Recipes repository](https://github.com/gofiber/recipes/tree/master/air). This example shows how to configure and use Air in a Fiber project to create an efficient development environment. + + +## How do I set up an error handler? + +To override the default error handler, you can override the default when providing a [Config](../api/fiber.md#config) when initiating a new [Fiber instance](../api/fiber.md#new). + +```go title="Example" +app := fiber.New(fiber.Config{ + ErrorHandler: func(c *fiber.Ctx, err error) error { + return c.Status(fiber.StatusInternalServerError).SendString(err.Error()) + }, +}) +``` + +We have a dedicated page explaining how error handling works in Fiber, see [Error Handling](../guide/error-handling.md). + +## Which template engines does Fiber support? + +Fiber currently supports 9 template engines in our [gofiber/template](https://docs.gofiber.io/template/) middleware: + +* [ace](https://docs.gofiber.io/template/ace/) +* [amber](https://docs.gofiber.io/template/amber/) +* [django](https://docs.gofiber.io/template/django/) +* [handlebars](https://docs.gofiber.io/template/handlebars) +* [html](https://docs.gofiber.io/template/html) +* [jet](https://docs.gofiber.io/template/jet) +* [mustache](https://docs.gofiber.io/template/mustache) +* [pug](https://docs.gofiber.io/template/pug) +* [slim](https://docs.gofiber.io/template/pug) + +To learn more about using Templates in Fiber, see [Templates](../guide/templates.md). + +## Does Fiber have a community chat? + +Yes, we have our own [Discord ](https://gofiber.io/discord)server, where we hang out. We have different rooms for every subject. +If you have questions or just want to have a chat, feel free to join us via this **>** [**invite link**](https://gofiber.io/discord) **<**. + +![](/img/support-discord.png) + +## Does fiber support sub domain routing ? + +Yes we do, here are some examples: +This example works v2 +```go +package main + +import ( + "log" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/logger" +) + +type Host struct { + Fiber *fiber.App +} + +func main() { + // Hosts + hosts := map[string]*Host{} + //----- + // API + //----- + api := fiber.New() + api.Use(logger.New(logger.Config{ + Format: "[${ip}]:${port} ${status} - ${method} ${path}\n", + })) + hosts["api.localhost:3000"] = &Host{api} + api.Get("/", func(c *fiber.Ctx) error { + return c.SendString("API") + }) + //------ + // Blog + //------ + blog := fiber.New() + blog.Use(logger.New(logger.Config{ + Format: "[${ip}]:${port} ${status} - ${method} ${path}\n", + })) + hosts["blog.localhost:3000"] = &Host{blog} + blog.Get("/", func(c *fiber.Ctx) error { + return c.SendString("Blog") + }) + //--------- + // Website + //--------- + site := fiber.New() + site.Use(logger.New(logger.Config{ + Format: "[${ip}]:${port} ${status} - ${method} ${path}\n", + })) + + hosts["localhost:3000"] = &Host{site} + site.Get("/", func(c *fiber.Ctx) error { + return c.SendString("Website") + }) + // Server + app := fiber.New() + app.Use(func(c *fiber.Ctx) error { + host := hosts[c.Hostname()] + if host == nil { + return c.SendStatus(fiber.StatusNotFound) + } else { + host.Fiber.Handler()(c.Context()) + return nil + } + }) + log.Fatal(app.Listen(":3000")) +} +``` +If more information is needed, please refer to this issue [#750](https://github.com/gofiber/fiber/issues/750) diff --git a/docs/guide/_category_.json b/docs/guide/_category_.json new file mode 100644 index 0000000..b0e157a --- /dev/null +++ b/docs/guide/_category_.json @@ -0,0 +1,8 @@ +{ + "label": "Guide", + "position": 3, + "link": { + "type": "generated-index", + "description": "Guides for Fiber." + } +} diff --git a/docs/guide/error-handling.md b/docs/guide/error-handling.md new file mode 100644 index 0000000..7d3aa36 --- /dev/null +++ b/docs/guide/error-handling.md @@ -0,0 +1,128 @@ +--- +id: error-handling +title: 🐛 Error Handling +description: >- + Fiber supports centralized error handling by returning an error to the handler + which allows you to log errors to external services or send a customized HTTP + response to the client. +sidebar_position: 4 +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +## Catching Errors + +It’s essential to ensure that Fiber catches all errors that occur while running route handlers and middleware. You must return them to the handler function, where Fiber will catch and process them. + + + + +```go +app.Get("/", func(c *fiber.Ctx) error { + // Pass error to Fiber + return c.SendFile("file-does-not-exist") +}) +``` + + + +Fiber does not handle [panics](https://go.dev/blog/defer-panic-and-recover) by default. To recover from a panic thrown by any handler in the stack, you need to include the `Recover` middleware below: + +```go title="Example" +package main + +import ( + "log" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/recover" +) + +func main() { + app := fiber.New() + + app.Use(recover.New()) + + app.Get("/", func(c *fiber.Ctx) error { + panic("This panic is caught by fiber") + }) + + log.Fatal(app.Listen(":3000")) +} +``` + +You could use Fiber's custom error struct to pass an additional `status code` using `fiber.NewError()`. It's optional to pass a message; if this is left empty, it will default to the status code message \(`404` equals `Not Found`\). + +```go title="Example" +app.Get("/", func(c *fiber.Ctx) error { + // 503 Service Unavailable + return fiber.ErrServiceUnavailable + + // 503 On vacation! + return fiber.NewError(fiber.StatusServiceUnavailable, "On vacation!") +}) +``` + +## Default Error Handler + +Fiber provides an error handler by default. For a standard error, the response is sent as **500 Internal Server Error**. If the error is of type [fiber.Error](https://godoc.org/github.com/gofiber/fiber#Error), the response is sent with the provided status code and message. + +```go title="Example" +// Default error handler +var DefaultErrorHandler = func(c *fiber.Ctx, err error) error { + // Status code defaults to 500 + code := fiber.StatusInternalServerError + + // Retrieve the custom status code if it's a *fiber.Error + var e *fiber.Error + if errors.As(err, &e) { + code = e.Code + } + + // Set Content-Type: text/plain; charset=utf-8 + c.Set(fiber.HeaderContentType, fiber.MIMETextPlainCharsetUTF8) + + // Return status code with error message + return c.Status(code).SendString(err.Error()) +} +``` + +## Custom Error Handler + +A custom error handler can be set using a [Config ](../api/fiber.md#config)when initializing a [Fiber instance](../api/fiber.md#new). + +In most cases, the default error handler should be sufficient. However, a custom error handler can come in handy if you want to capture different types of errors and take action accordingly e.g., send a notification email or log an error to the centralized system. You can also send customized responses to the client e.g., error page or just a JSON response. + +The following example shows how to display error pages for different types of errors. + +```go title="Example" +// Create a new fiber instance with custom config +app := fiber.New(fiber.Config{ + // Override default error handler + ErrorHandler: func(ctx *fiber.Ctx, err error) error { + // Status code defaults to 500 + code := fiber.StatusInternalServerError + + // Retrieve the custom status code if it's a *fiber.Error + var e *fiber.Error + if errors.As(err, &e) { + code = e.Code + } + + // Send custom error page + err = ctx.Status(code).SendFile(fmt.Sprintf("./%d.html", code)) + if err != nil { + // In case the SendFile fails + return ctx.Status(fiber.StatusInternalServerError).SendString("Internal Server Error") + } + + // Return from handler + return nil + }, +}) + +// ... +``` + +> Special thanks to the [Echo](https://echo.labstack.com/) & [Express](https://expressjs.com/) framework for inspiration regarding error handling. diff --git a/docs/guide/faster-fiber.md b/docs/guide/faster-fiber.md new file mode 100644 index 0000000..b0b7bcb --- /dev/null +++ b/docs/guide/faster-fiber.md @@ -0,0 +1,36 @@ +--- +id: faster-fiber +title: ⚡ Make Fiber Faster +sidebar_position: 7 +--- + +## Custom JSON Encoder/Decoder +Since Fiber v2.32.0, we use **encoding/json** as default json library due to stability and producibility. However, the standard library is a bit slow compared to 3rd party libraries. If you're not happy with the performance of **encoding/json**, we recommend you to use these libraries: +- [goccy/go-json](https://github.com/goccy/go-json) +- [bytedance/sonic](https://github.com/bytedance/sonic) +- [segmentio/encoding](https://github.com/segmentio/encoding) +- [mailru/easyjson](https://github.com/mailru/easyjson) +- [minio/simdjson-go](https://github.com/minio/simdjson-go) +- [wI2L/jettison](https://github.com/wI2L/jettison) + +```go title="Example" +package main + +import "github.com/gofiber/fiber/v2" +import "github.com/goccy/go-json" + +func main() { + app := fiber.New(fiber.Config{ + JSONEncoder: json.Marshal, + JSONDecoder: json.Unmarshal, + }) + + # ... +} +``` + +### References +- [Set custom JSON encoder for client](../api/client.md#jsonencoder) +- [Set custom JSON decoder for client](../api/client.md#jsondecoder) +- [Set custom JSON encoder for application](../api/fiber.md#config) +- [Set custom JSON decoder for application](../api/fiber.md#config) \ No newline at end of file diff --git a/docs/guide/grouping.md b/docs/guide/grouping.md new file mode 100644 index 0000000..429e170 --- /dev/null +++ b/docs/guide/grouping.md @@ -0,0 +1,79 @@ +--- +id: grouping +title: 🎭 Grouping +sidebar_position: 2 +--- + +:::info +In general, the Group functionality in Fiber behaves similarly to ExpressJS. Groups are declared virtually and all routes declared within the group are flattened into a single list with a prefix, which is then checked by the framework in the order it was declared. This means that the behavior of Group in Fiber is identical to that of ExpressJS. +::: + +## Paths + +Like **Routing**, groups can also have paths that belong to a cluster. + +```go +func main() { + app := fiber.New() + + api := app.Group("/api", middleware) // /api + + v1 := api.Group("/v1", middleware) // /api/v1 + v1.Get("/list", handler) // /api/v1/list + v1.Get("/user", handler) // /api/v1/user + + v2 := api.Group("/v2", middleware) // /api/v2 + v2.Get("/list", handler) // /api/v2/list + v2.Get("/user", handler) // /api/v2/user + + log.Fatal(app.Listen(":3000")) +} +``` + +A **Group** of paths can have an optional handler. + +```go +func main() { + app := fiber.New() + + api := app.Group("/api") // /api + + v1 := api.Group("/v1") // /api/v1 + v1.Get("/list", handler) // /api/v1/list + v1.Get("/user", handler) // /api/v1/user + + v2 := api.Group("/v2") // /api/v2 + v2.Get("/list", handler) // /api/v2/list + v2.Get("/user", handler) // /api/v2/user + + log.Fatal(app.Listen(":3000")) +} +``` + +:::caution +Running **/api**, **/v1** or **/v2** will result in **404** error, make sure you have the errors set. +::: + +## Group Handlers + +Group handlers can also be used as a routing path but they must have **Next** added to them so that the flow can continue. + +```go +func main() { + app := fiber.New() + + handler := func(c *fiber.Ctx) error { + return c.SendStatus(fiber.StatusOK) + } + api := app.Group("/api") // /api + + v1 := api.Group("/v1", func(c *fiber.Ctx) error { // middleware for /api/v1 + c.Set("Version", "v1") + return c.Next() + }) + v1.Get("/list", handler) // /api/v1/list + v1.Get("/user", handler) // /api/v1/user + + log.Fatal(app.Listen(":3000")) +} +``` diff --git a/docs/guide/hooks.md b/docs/guide/hooks.md new file mode 100644 index 0000000..357d59d --- /dev/null +++ b/docs/guide/hooks.md @@ -0,0 +1,218 @@ +--- +id: hooks +title: 🎣 Hooks +sidebar_position: 6 +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +With Fiber v2.30.0, you can execute custom user functions when to run some methods. Here is a list of this hooks: +- [OnRoute](#onroute) +- [OnName](#onname) +- [OnGroup](#ongroup) +- [OnGroupName](#ongroupname) +- [OnListen](#onlisten) +- [OnFork](#onfork) +- [OnShutdown](#onshutdown) +- [OnMount](#onmount) + +## Constants +```go +// Handlers define a function to create hooks for Fiber. +type OnRouteHandler = func(Route) error +type OnNameHandler = OnRouteHandler +type OnGroupHandler = func(Group) error +type OnGroupNameHandler = OnGroupHandler +type OnListenHandler = func(ListenData) error +type OnForkHandler = func(int) error +type OnShutdownHandler = func() error +type OnMountHandler = func(*App) error +``` + +## OnRoute + +OnRoute is a hook to execute user functions on each route registeration. Also you can get route properties by **route** parameter. + +```go title="Signature" +func (h *Hooks) OnRoute(handler ...OnRouteHandler) +``` + +## OnName + +OnName is a hook to execute user functions on each route naming. Also you can get route properties by **route** parameter. + +:::caution +OnName only works with naming routes, not groups. +::: + +```go title="Signature" +func (h *Hooks) OnName(handler ...OnNameHandler) +``` + + + + +```go +package main + +import ( + "fmt" + + "github.com/gofiber/fiber/v2" +) + +func main() { + app := fiber.New() + + app.Get("/", func(c *fiber.Ctx) error { + return c.SendString(c.Route().Name) + }).Name("index") + + app.Hooks().OnName(func(r fiber.Route) error { + fmt.Print("Name: " + r.Name + ", ") + + return nil + }) + + app.Hooks().OnName(func(r fiber.Route) error { + fmt.Print("Method: " + r.Method + "\n") + + return nil + }) + + app.Get("/add/user", func(c *fiber.Ctx) error { + return c.SendString(c.Route().Name) + }).Name("addUser") + + app.Delete("/destroy/user", func(c *fiber.Ctx) error { + return c.SendString(c.Route().Name) + }).Name("destroyUser") + + app.Listen(":5000") +} + +// Results: +// Name: addUser, Method: GET +// Name: destroyUser, Method: DELETE +``` + + + +## OnGroup + +OnGroup is a hook to execute user functions on each group registeration. Also you can get group properties by **group** parameter. + +```go title="Signature" +func (h *Hooks) OnGroup(handler ...OnGroupHandler) +``` + +## OnGroupName + +OnGroupName is a hook to execute user functions on each group naming. Also you can get group properties by **group** parameter. + +:::caution +OnGroupName only works with naming groups, not routes. +::: + +```go title="Signature" +func (h *Hooks) OnGroupName(handler ...OnGroupNameHandler) +``` + +## OnListen + +OnListen is a hook to execute user functions on Listen, ListenTLS, Listener. + +```go title="Signature" +func (h *Hooks) OnListen(handler ...OnListenHandler) +``` + + + + +```go +app := fiber.New(fiber.Config{ + DisableStartupMessage: true, +}) + +app.Hooks().OnListen(func(listenData fiber.ListenData) error { + if fiber.IsChild() { + return nil + } + scheme := "http" + if data.TLS { + scheme = "https" + } + log.Println(scheme + "://" + listenData.Host + ":" + listenData.Port) + return nil +}) + +app.Listen(":5000") +``` + + + + +## OnFork + +OnFork is a hook to execute user functions on Fork. + +```go title="Signature" +func (h *Hooks) OnFork(handler ...OnForkHandler) +``` + +## OnShutdown + +OnShutdown is a hook to execute user functions after Shutdown. + +```go title="Signature" +func (h *Hooks) OnShutdown(handler ...OnShutdownHandler) +``` + +## OnMount + +OnMount is a hook to execute user function after mounting process. The mount event is fired when sub-app is mounted on a parent app. The parent app is passed as a parameter. It works for app and group mounting. + +```go title="Signature" +func (h *Hooks) OnMount(handler ...OnMountHandler) +``` + + + + +```go +package main + +import ( + "fmt" + + "github.com/gofiber/fiber/v2" +) + +func main() { + app := New() + app.Get("/", testSimpleHandler).Name("x") + + subApp := New() + subApp.Get("/test", testSimpleHandler) + + subApp.Hooks().OnMount(func(parent *fiber.App) error { + fmt.Print("Mount path of parent app: "+parent.MountPath()) + // ... + + return nil + }) + + app.Mount("/sub", subApp) +} + +// Result: +// Mount path of parent app: +``` + + + + + +:::caution +OnName/OnRoute/OnGroup/OnGroupName hooks are mount-sensitive. If you use one of these routes on sub app and you mount it; paths of routes and groups will start with mount prefix. diff --git a/docs/guide/routing.md b/docs/guide/routing.md new file mode 100644 index 0000000..dbb92dc --- /dev/null +++ b/docs/guide/routing.md @@ -0,0 +1,294 @@ +--- +id: routing +title: 🔌 Routing +description: >- + Routing refers to how an application's endpoints (URIs) respond to client + requests. +sidebar_position: 1 +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; +import RoutingHandler from './../partials/routing/handler.md'; + +## Handlers + + + +## Paths + +Route paths, combined with a request method, define the endpoints at which requests can be made. Route paths can be **strings** or **string patterns**. + +**Examples of route paths based on strings** + +```go +// This route path will match requests to the root route, "/": +app.Get("/", func(c *fiber.Ctx) error { + return c.SendString("root") +}) + +// This route path will match requests to "/about": +app.Get("/about", func(c *fiber.Ctx) error { + return c.SendString("about") +}) + +// This route path will match requests to "/random.txt": +app.Get("/random.txt", func(c *fiber.Ctx) error { + return c.SendString("random.txt") +}) +``` + +As with the expressJs framework, the order of the route declaration plays a role. +When a request is received, the routes are checked in the order in which they are declared. + +:::info +So please be careful to write routes with variable parameters after the routes that contain fixed parts, so that these variable parts do not match instead and unexpected behavior occurs. +::: + +## Parameters + +Route parameters are dynamic elements in the route, which are **named** or **not named segments**. This segments that are used to capture the values specified at their position in the URL. The obtained values can be retrieved using the [Params](https://fiber.wiki/context#params) function, with the name of the route parameter specified in the path as their respective keys or for unnamed parameters the character\(\*, +\) and the counter of this. + +The characters :, +, and \* are characters that introduce a parameter. + +Greedy parameters are indicated by wildcard\(\*\) or plus\(+\) signs. + +The routing also offers the possibility to use optional parameters, for the named parameters these are marked with a final "?", unlike the plus sign which is not optional, you can use the wildcard character for a parameter range which is optional and greedy. + +**Example of define routes with route parameters** + +```go +// Parameters +app.Get("/user/:name/books/:title", func(c *fiber.Ctx) error { + fmt.Fprintf(c, "%s\n", c.Params("name")) + fmt.Fprintf(c, "%s\n", c.Params("title")) + return nil +}) +// Plus - greedy - not optional +app.Get("/user/+", func(c *fiber.Ctx) error { + return c.SendString(c.Params("+")) +}) + +// Optional parameter +app.Get("/user/:name?", func(c *fiber.Ctx) error { + return c.SendString(c.Params("name")) +}) + +// Wildcard - greedy - optional +app.Get("/user/*", func(c *fiber.Ctx) error { + return c.SendString(c.Params("*")) +}) + +// This route path will match requests to "/v1/some/resource/name:customVerb", since the parameter character is escaped +app.Get(`/v1/some/resource/name\:customVerb`, func(c *fiber.Ctx) error { + return c.SendString("Hello, Community") +}) +``` + +:::info +Since the hyphen \(`-`\) and the dot \(`.`\) are interpreted literally, they can be used along with route parameters for useful purposes. +::: + +:::info +All special parameter characters can also be escaped with `"\\"` and lose their value, so you can use them in the route if you want, like in the custom methods of the [google api design guide](https://cloud.google.com/apis/design/custom_methods). It's recommended to use backticks `` ` `` because in go's regex documentation, they always use backticks to make sure it is unambiguous and the escape character doesn't interfere with regex patterns in an unexpected way. +::: + +```go +// http://localhost:3000/plantae/prunus.persica +app.Get("/plantae/:genus.:species", func(c *fiber.Ctx) error { + fmt.Fprintf(c, "%s.%s\n", c.Params("genus"), c.Params("species")) + return nil // prunus.persica +}) +``` + +```go +// http://localhost:3000/flights/LAX-SFO +app.Get("/flights/:from-:to", func(c *fiber.Ctx) error { + fmt.Fprintf(c, "%s-%s\n", c.Params("from"), c.Params("to")) + return nil // LAX-SFO +}) +``` + +Our intelligent router recognizes that the introductory parameter characters should be part of the request route in this case and can process them as such. + +```go +// http://localhost:3000/shop/product/color:blue/size:xs +app.Get("/shop/product/color::color/size::size", func(c *fiber.Ctx) error { + fmt.Fprintf(c, "%s:%s\n", c.Params("color"), c.Params("size")) + return nil // blue:xs +}) +``` + +In addition, several parameters in a row and several unnamed parameter characters in the route, such as the wildcard or plus character, are possible, which greatly expands the possibilities of the router for the user. + +```go +// GET /@v1 +// Params: "sign" -> "@", "param" -> "v1" +app.Get("/:sign:param", handler) + +// GET /api-v1 +// Params: "name" -> "v1" +app.Get("/api-:name", handler) + +// GET /customer/v1/cart/proxy +// Params: "*1" -> "customer/", "*2" -> "/cart" +app.Get("/*v1*/proxy", handler) + +// GET /v1/brand/4/shop/blue/xs +// Params: "*1" -> "brand/4", "*2" -> "blue/xs" +app.Get("/v1/*/shop/*", handler) +``` + +We have adapted the routing strongly to the express routing, but currently without the possibility of the regular expressions, because they are quite slow. The possibilities can be tested with version 0.1.7 \(express 4\) in the online [Express route tester](http://forbeslindesay.github.io/express-route-tester/). + +### Constraints +Route constraints execute when a match has occurred to the incoming URL and the URL path is tokenized into route values by parameters. The feature was intorduced in `v2.37.0` and inspired by [.NET Core](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/routing?view=aspnetcore-6.0#route-constraints). + +:::caution +Constraints aren't validation for parameters. If constraints aren't valid for a parameter value, Fiber returns **404 handler**. +::: + +| Constraint | Example | Example matches | +| ----------------- | ------------------------------------ | ------------------------------------------------------------------------------------------- | +| int | `:id` | 123456789, -123456789 | +| bool | `:active` | true,false | +| guid | `:id` | CD2C1638-1638-72D5-1638-DEADBEEF1638 | +| float | `:weight` | 1.234, -1,001.01e8 | +| minLen(value) | `:username` | Test (must be at least 4 characters) | +| maxLen(value) | `:filename` | MyFile (must be no more than 8 characters | +| len(length) | `:filename` | somefile.txt (exactly 12 characters) | +| min(value) | `:age` | 19 (Integer value must be at least 18) | +| max(value) | `:age` | 91 (Integer value must be no more than 120) | +| range(min,max) | `:age` | 91 (Integer value must be at least 18 but no more than 120) | +| alpha | `:name` | Rick (String must consist of one or more alphabetical characters, a-z and case-insensitive) | +| datetime | `:dob` | 2005-11-01 | +| regex(expression) | `:date` | 2022-08-27 (Must match regular expression) | + +**Examples** + + + + +```go +app.Get("/:test", func(c *fiber.Ctx) error { + return c.SendString(c.Params("test")) +}) + +// curl -X GET http://localhost:3000/12 +// 12 + +// curl -X GET http://localhost:3000/1 +// Cannot GET /1 +``` + + + +You can use `;` for multiple constraints. +```go +app.Get("/:test", func(c *fiber.Ctx) error { + return c.SendString(c.Params("test")) +}) + +// curl -X GET http://localhost:3000/120000 +// Cannot GET /120000 + +// curl -X GET http://localhost:3000/1 +// Cannot GET /1 + +// curl -X GET http://localhost:3000/250 +// 250 +``` + + + +Fiber precompiles regex query when to register routes. So there're no performance overhead for regex constraint. +```go +app.Get(`/:date`, func(c *fiber.Ctx) error { + return c.SendString(c.Params("date")) +}) + +// curl -X GET http://localhost:3000/125 +// Cannot GET /125 + +// curl -X GET http://localhost:3000/test +// Cannot GET /test + +// curl -X GET http://localhost:3000/2022-08-27 +// 2022-08-27 +``` + + + + +:::caution +You should use `\\` before routing-specific characters when to use datetime constraint (`*`, `+`, `?`, `:`, `/`, `<`, `>`, `;`, `(`, `)`), to avoid wrong parsing. +::: + +**Optional Parameter Example** + +You can impose constraints on optional parameters as well. + +```go +app.Get("/:test?", func(c *fiber.Ctx) error { + return c.SendString(c.Params("test")) +}) +// curl -X GET http://localhost:3000/42 +// 42 +// curl -X GET http://localhost:3000/ +// +// curl -X GET http://localhost:3000/7.0 +// Cannot GET /7.0 +``` + +## Middleware + +Functions that are designed to make changes to the request or response are called **middleware functions**. The [Next](../api/ctx.md#next) is a **Fiber** router function, when called, executes the **next** function that **matches** the current route. + +**Example of a middleware function** + +```go +app.Use(func(c *fiber.Ctx) error { + // Set a custom header on all responses: + c.Set("X-Custom-Header", "Hello, World") + + // Go to next middleware: + return c.Next() +}) + +app.Get("/", func(c *fiber.Ctx) error { + return c.SendString("Hello, World!") +}) +``` + +`Use` method path is a **mount**, or **prefix** path, and limits middleware to only apply to any paths requested that begin with it. + +### Constraints on Adding Routes Dynamically + +:::caution +Adding routes dynamically after the application has started is not supported due to design and performance considerations. Make sure to define all your routes before the application starts. +::: + + +## Grouping + +If you have many endpoints, you can organize your routes using `Group`. + +```go +func main() { + app := fiber.New() + + api := app.Group("/api", middleware) // /api + + v1 := api.Group("/v1", middleware) // /api/v1 + v1.Get("/list", handler) // /api/v1/list + v1.Get("/user", handler) // /api/v1/user + + v2 := api.Group("/v2", middleware) // /api/v2 + v2.Get("/list", handler) // /api/v2/list + v2.Get("/user", handler) // /api/v2/user + + log.Fatal(app.Listen(":3000")) +} +``` + +More information about this in our [Grouping Guide](./grouping.md) diff --git a/docs/guide/templates.md b/docs/guide/templates.md new file mode 100644 index 0000000..18dd0e3 --- /dev/null +++ b/docs/guide/templates.md @@ -0,0 +1,267 @@ +--- +id: templates +title: 📝 Templates +description: Fiber supports server-side template engines. +sidebar_position: 3 +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +Templates are a great tool to render dynamic content without using a separate frontend framework. + +## Template Engines + +Fiber allows you to provide a custom template engine at app initialization. + +```go +app := fiber.New(fiber.Config{ + // Pass in Views Template Engine + Views: engine, + + // Default global path to search for views (can be overriden when calling Render()) + ViewsLayout: "layouts/main", + + // Enables/Disables access to `ctx.Locals()` entries in rendered views + // (defaults to false) + PassLocalsToViews: false, +}) +``` + +### Supported Engines + +The Fiber team maintains a [templates](https://docs.gofiber.io/template) package that provides wrappers for multiple template engines: + +* [ace](https://docs.gofiber.io/template/ace/) +* [amber](https://docs.gofiber.io/template/amber/) +* [django](https://docs.gofiber.io/template/django/) +* [handlebars](https://docs.gofiber.io/template/handlebars) +* [html](https://docs.gofiber.io/template/html) +* [jet](https://docs.gofiber.io/template/jet) +* [mustache](https://docs.gofiber.io/template/mustache) +* [pug](https://docs.gofiber.io/template/pug) +* [slim](https://docs.gofiber.io/template/slim) + +:::info +Custom template engines can implement the `Views` interface to be supported in Fiber. +::: + +```go title="Views interface" +type Views interface { + // Fiber executes Load() on app initialization to load/parse the templates + Load() error + + // Outputs a template to the provided buffer using the provided template, + // template name, and binded data + Render(io.Writer, string, interface{}, ...string) error +} +``` + +:::note +The `Render` method is linked to the [**ctx.Render\(\)**](../api/ctx.md#render) function that accepts a template name and binding data. +::: + +## Rendering Templates + +Once an engine is set up, a route handler can call the [**ctx.Render\(\)**](../api/ctx.md#render) function with a template name and binded data to send the rendered template. + +```go title="Signature" +func (c *Ctx) Render(name string, bind Map, layouts ...string) error +``` + +:::info +By default, [**ctx.Render\(\)**](../api/ctx.md#render) searches for the template name in the `ViewsLayout` path. To override this setting, provide the path(s) in the `layouts` argument. +::: + + + + +```go +app.Get("/", func(c *fiber.Ctx) error { + return c.Render("index", fiber.Map{ + "Title": "Hello, World!", + }) + +}) +``` + + + + + +```html + + + +

{{.Title}}

+ + +``` + +
+ +
+ +:::caution +If the Fiber config option `PassLocalsToViews` is enabled, then all locals set using `ctx.Locals(key, value)` will be passed to the template. It is important to avoid clashing keys when using this setting. +::: + +## Advanced Templating + +### Custom Functions + +Fiber supports adding custom functions to templates. + +#### AddFunc + +Adds a global function to all templates. + +```go title="Signature" +func (e *Engine) AddFunc(name string, fn interface{}) IEngineCore +``` + + + + +```go +// Add `ToUpper` to engine +engine := html.New("./views", ".html") +engine.AddFunc("ToUpper", func(s string) string { + return strings.ToUpper(s) +} + +// Initialize Fiber App +app := fiber.New(fiber.Config{ + Views: engine, +}) + +app.Get("/", func (c *fiber.Ctx) error { + return c.Render("index", fiber.Map{ + "Content": "hello, world!" + }) +}) +``` + + + + +```html + + + +

This will be in {{ToUpper "all caps"}}:

+

{{ToUpper .Content}}

+ + +``` + +
+
+ +#### AddFuncMap + +Adds a Map of functions (keyed by name) to all templates. + +```go title="Signature" +func (e *Engine) AddFuncMap(m map[string]interface{}) IEngineCore +``` + + + + +```go +// Add `ToUpper` to engine +engine := html.New("./views", ".html") +engine.AddFuncMap(map[string]interface{}{ + "ToUpper": func(s string) string { + return strings.ToUpper(s) + }, +}) + +// Initialize Fiber App +app := fiber.New(fiber.Config{ + Views: engine, +}) + +app.Get("/", func (c *fiber.Ctx) error { + return c.Render("index", fiber.Map{ + "Content": "hello, world!" + }) +}) +``` + + + + +```html + + + +

This will be in {{ToUpper "all caps"}}:

+

{{ToUpper .Content}}

+ + +``` + +
+
+ +* For more advanced template documentation, please visit the [gofiber/template GitHub Repository](https://github.com/gofiber/template). + +## Full Example + + + + +```go +package main + +import ( + "log" + "github.com/gofiber/fiber/v2" + "github.com/gofiber/template/html/v2" +) + +func main() { + // Initialize standard Go html template engine + engine := html.New("./views", ".html") + // If you want to use another engine, + // just replace with following: + // Create a new engine with django + // engine := django.New("./views", ".django") + + app := fiber.New(fiber.Config{ + Views: engine, + }) + app.Get("/", func(c *fiber.Ctx) error { + // Render index template + return c.Render("index", fiber.Map{ + "Title": "Go Fiber Template Example", + "Description": "An example template", + "Greeting": "Hello, world!", + }); + }) + + log.Fatal(app.Listen(":3000")) +} +``` + + + + +```html + + + + {{.Title}} + + + +

{{.Title}}

+

{{.Greeting}}

+ + +``` + +
+
diff --git a/docs/guide/validation.md b/docs/guide/validation.md new file mode 100644 index 0000000..417298a --- /dev/null +++ b/docs/guide/validation.md @@ -0,0 +1,168 @@ +--- +id: validation +title: 🔎 Validation +sidebar_position: 5 +--- + +## Validator package + +Fiber can make _great_ use of the validator package to ensure correct validation of data to store. + +- [Official validator Github page \(Installation, use, examples..\).](https://github.com/go-playground/validator) + +You can find the detailed descriptions of the _validations_ used in the fields contained on the structs below: + +- [Detailed docs](https://pkg.go.dev/github.com/go-playground/validator?tab=doc) + +```go title="Validation Example" +package main + +import ( + "fmt" + "log" + "strings" + + "github.com/go-playground/validator/v10" + "github.com/gofiber/fiber/v2" +) + +type ( + User struct { + Name string `validate:"required,min=5,max=20"` // Required field, min 5 char long max 20 + Age int `validate:"required,teener"` // Required field, and client needs to implement our 'teener' tag format which we'll see later + } + + ErrorResponse struct { + Error bool + FailedField string + Tag string + Value interface{} + } + + XValidator struct { + validator *validator.Validate + } + + GlobalErrorHandlerResp struct { + Success bool `json:"success"` + Message string `json:"message"` + } +) + +// This is the validator instance +// for more information see: https://github.com/go-playground/validator +var validate = validator.New() + +func (v XValidator) Validate(data interface{}) []ErrorResponse { + validationErrors := []ErrorResponse{} + + errs := validate.Struct(data) + if errs != nil { + for _, err := range errs.(validator.ValidationErrors) { + // In this case data object is actually holding the User struct + var elem ErrorResponse + + elem.FailedField = err.Field() // Export struct field name + elem.Tag = err.Tag() // Export struct tag + elem.Value = err.Value() // Export field value + elem.Error = true + + validationErrors = append(validationErrors, elem) + } + } + + return validationErrors +} + +func main() { + myValidator := &XValidator{ + validator: validate, + } + + app := fiber.New(fiber.Config{ + // Global custom error handler + ErrorHandler: func(c *fiber.Ctx, err error) error { + return c.Status(fiber.StatusBadRequest).JSON(GlobalErrorHandlerResp{ + Success: false, + Message: err.Error(), + }) + }, + }) + + // Custom struct validation tag format + myValidator.validator.RegisterValidation("teener", func(fl validator.FieldLevel) bool { + // User.Age needs to fit our needs, 12-18 years old. + return fl.Field().Int() >= 12 && fl.Field().Int() <= 18 + }) + + app.Get("/", func(c *fiber.Ctx) error { + user := &User{ + Name: c.Query("name"), + Age: c.QueryInt("age"), + } + + // Validation + if errs := myValidator.Validate(user); len(errs) > 0 && errs[0].Error { + errMsgs := make([]string, 0) + + for _, err := range errs { + errMsgs = append(errMsgs, fmt.Sprintf( + "[%s]: '%v' | Needs to implement '%s'", + err.FailedField, + err.Value, + err.Tag, + )) + } + + return &fiber.Error{ + Code: fiber.ErrBadRequest.Code, + Message: strings.Join(errMsgs, " and "), + } + } + + // Logic, validated with success + return c.SendString("Hello, World!") + }) + + log.Fatal(app.Listen(":3000")) +} + +/** +OUTPUT + +[1] +Request: + +GET http://127.0.0.1:3000/ + +Response: + +{"success":false,"message":"[Name]: '' | Needs to implement 'required' and [Age]: '0' | Needs to implement 'required'"} + +[2] +Request: + +GET http://127.0.0.1:3000/?name=efdal&age=9 + +Response: +{"success":false,"message":"[Age]: '9' | Needs to implement 'teener'"} + +[3] +Request: + +GET http://127.0.0.1:3000/?name=efdal&age= + +Response: +{"success":false,"message":"[Age]: '0' | Needs to implement 'required'"} + +[4] +Request: + +GET http://127.0.0.1:3000/?name=efdal&age=18 + +Response: +Hello, World! + +**/ + +``` diff --git a/docs/intro.md b/docs/intro.md new file mode 100644 index 0000000..456035d --- /dev/null +++ b/docs/intro.md @@ -0,0 +1,195 @@ +--- +slug: / +id: welcome +title: 👋 Welcome +sidebar_position: 1 +--- +An online API documentation with examples so you can start building web apps with Fiber right away! + +**Fiber** is an [Express](https://github.com/expressjs/express) inspired **web framework** built on top of [Fasthttp](https://github.com/valyala/fasthttp), the **fastest** HTTP engine for [Go](https://go.dev/doc/). Designed to **ease** things up for **fast** development with **zero memory allocation** and **performance** in mind. + +These docs are for **Fiber v2**, which was released on **September 15th, 2020**. + +### Installation + +First of all, [download](https://go.dev/dl/) and install Go. `1.17` or higher is required. + +Installation is done using the [`go get`](https://pkg.go.dev/cmd/go/#hdr-Add_dependencies_to_current_module_and_install_them) command: + +```bash +go get github.com/gofiber/fiber/v2 +``` + +### Zero Allocation +Some values returned from \***fiber.Ctx** are **not** immutable by default. + +Because fiber is optimized for **high-performance**, values returned from **fiber.Ctx** are **not** immutable by default and **will** be re-used across requests. As a rule of thumb, you **must** only use context values within the handler, and you **must not** keep any references. As soon as you return from the handler, any values you have obtained from the context will be re-used in future requests and will change below your feet. Here is an example: + +```go +func handler(c *fiber.Ctx) error { + // Variable is only valid within this handler + result := c.Params("foo") + + // ... +} +``` + +If you need to persist such values outside the handler, make copies of their **underlying buffer** using the [copy](https://pkg.go.dev/builtin/#copy) builtin. Here is an example for persisting a string: + +```go +func handler(c *fiber.Ctx) error { + // Variable is only valid within this handler + result := c.Params("foo") + + // Make a copy + buffer := make([]byte, len(result)) + copy(buffer, result) + resultCopy := string(buffer) + // Variable is now valid forever + + // ... +} +``` + +We created a custom `CopyString` function that does the above and is available under [gofiber/utils](https://github.com/gofiber/fiber/tree/master/utils). + +```go +app.Get("/:foo", func(c *fiber.Ctx) error { + // Variable is now immutable + result := utils.CopyString(c.Params("foo")) + + // ... +}) +``` + +Alternatively, you can also use the `Immutable` setting. It will make all values returned from the context immutable, allowing you to persist them anywhere. Of course, this comes at the cost of performance. + +```go +app := fiber.New(fiber.Config{ + Immutable: true, +}) +``` + +For more information, please check [**\#426**](https://github.com/gofiber/fiber/issues/426) and [**\#185**](https://github.com/gofiber/fiber/issues/185). + +### Hello, World! + +Embedded below is essentially the most straightforward **Fiber** app you can create: + +```go +package main + +import "github.com/gofiber/fiber/v2" + +func main() { + app := fiber.New() + + app.Get("/", func(c *fiber.Ctx) error { + return c.SendString("Hello, World!") + }) + + app.Listen(":3000") +} +``` + +```text +go run server.go +``` + +Browse to `http://localhost:3000` and you should see `Hello, World!` on the page. + +### Basic routing + +Routing refers to determining how an application responds to a client request to a particular endpoint, which is a URI (or path) and a specific HTTP request method (`GET`, `PUT`, `POST`, etc.). + +Each route can have **multiple handler functions** that are executed when the route is matched. + +Route definition takes the following structures: + +```go +// Function signature +app.Method(path string, ...func(*fiber.Ctx) error) +``` + +- `app` is an instance of **Fiber** +- `Method` is an [HTTP request method](https://docs.gofiber.io/api/app#route-handlers): `GET`, `PUT`, `POST`, etc. +- `path` is a virtual path on the server +- `func(*fiber.Ctx) error` is a callback function containing the [Context](https://docs.gofiber.io/api/ctx) executed when the route is matched + +**Simple route** + +```go +// Respond with "Hello, World!" on root path, "/" +app.Get("/", func(c *fiber.Ctx) error { + return c.SendString("Hello, World!") +}) +``` + +**Parameters** + +```go +// GET http://localhost:8080/hello%20world + +app.Get("/:value", func(c *fiber.Ctx) error { + return c.SendString("value: " + c.Params("value")) + // => Get request with value: hello world +}) +``` + +**Optional parameter** + +```go +// GET http://localhost:3000/john + +app.Get("/:name?", func(c *fiber.Ctx) error { + if c.Params("name") != "" { + return c.SendString("Hello " + c.Params("name")) + // => Hello john + } + return c.SendString("Where is john?") +}) +``` + +**Wildcards** + +```go +// GET http://localhost:3000/api/user/john + +app.Get("/api/*", func(c *fiber.Ctx) error { + return c.SendString("API path: " + c.Params("*")) + // => API path: user/john +}) +``` + +### Static files + +To serve static files such as **images**, **CSS**, and **JavaScript** files, replace your function handler with a file or directory string. + +Function signature: + +```go +app.Static(prefix, root string, config ...Static) +``` + +Use the following code to serve files in a directory named `./public`: + +```go +app := fiber.New() + +app.Static("/", "./public") + +app.Listen(":3000") +``` + +Now, you can load the files that are in the `./public` directory: + +```bash +http://localhost:3000/hello.html +http://localhost:3000/js/jquery.js +http://localhost:3000/css/style.css +``` + +### Note + +For more information on how to build APIs in Go with Fiber, please check out this excellent article +[on building an express-style API in Go with Fiber](https://blog.logrocket.com/express-style-api-go-fiber/). diff --git a/docs/partials/routing/handler.md b/docs/partials/routing/handler.md new file mode 100644 index 0000000..2d198f0 --- /dev/null +++ b/docs/partials/routing/handler.md @@ -0,0 +1,69 @@ +--- +id: route-handlers +title: Route Handlers +--- + +Registers a route bound to a specific [HTTP method](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods). + +```go title="Signatures" +// HTTP methods +func (app *App) Get(path string, handlers ...Handler) Router +func (app *App) Head(path string, handlers ...Handler) Router +func (app *App) Post(path string, handlers ...Handler) Router +func (app *App) Put(path string, handlers ...Handler) Router +func (app *App) Delete(path string, handlers ...Handler) Router +func (app *App) Connect(path string, handlers ...Handler) Router +func (app *App) Options(path string, handlers ...Handler) Router +func (app *App) Trace(path string, handlers ...Handler) Router +func (app *App) Patch(path string, handlers ...Handler) Router + +// Add allows you to specifiy a method as value +func (app *App) Add(method, path string, handlers ...Handler) Router + +// All will register the route on all HTTP methods +// Almost the same as app.Use but not bound to prefixes +func (app *App) All(path string, handlers ...Handler) Router +``` + +```go title="Examples" +// Simple GET handler +app.Get("/api/list", func(c *fiber.Ctx) error { + return c.SendString("I'm a GET request!") +}) + +// Simple POST handler +app.Post("/api/register", func(c *fiber.Ctx) error { + return c.SendString("I'm a POST request!") +}) +``` + +**Use** can be used for middleware packages and prefix catchers. These routes will only match the beginning of each path i.e. `/john` will match `/john/doe`, `/johnnnnn` etc + +```go title="Signature" +func (app *App) Use(args ...interface{}) Router +``` + +```go title="Examples" +// Match any request +app.Use(func(c *fiber.Ctx) error { + return c.Next() +}) + +// Match request starting with /api +app.Use("/api", func(c *fiber.Ctx) error { + return c.Next() +}) + +// Match requests starting with /api or /home (multiple-prefix support) +app.Use([]string{"/api", "/home"}, func(c *fiber.Ctx) error { + return c.Next() +}) + +// Attach multiple handlers +app.Use("/api", func(c *fiber.Ctx) error { + c.Set("X-Custom-Header", random.String(32)) + return c.Next() +}, func(c *fiber.Ctx) error { + return c.Next() +}) +``` diff --git a/error.go b/error.go new file mode 100644 index 0000000..e520420 --- /dev/null +++ b/error.go @@ -0,0 +1,40 @@ +package fiber + +import ( + errors "encoding/json" + + "github.com/gofiber/fiber/v2/internal/schema" +) + +type ( + // ConversionError Conversion error exposes the internal schema.ConversionError for public use. + ConversionError = schema.ConversionError + // UnknownKeyError error exposes the internal schema.UnknownKeyError for public use. + UnknownKeyError = schema.UnknownKeyError + // EmptyFieldError error exposes the internal schema.EmptyFieldError for public use. + EmptyFieldError = schema.EmptyFieldError + // MultiError error exposes the internal schema.MultiError for public use. + MultiError = schema.MultiError +) + +type ( + // An InvalidUnmarshalError describes an invalid argument passed to Unmarshal. + // (The argument to Unmarshal must be a non-nil pointer.) + InvalidUnmarshalError = errors.InvalidUnmarshalError + + // A MarshalerError represents an error from calling a MarshalJSON or MarshalText method. + MarshalerError = errors.MarshalerError + + // A SyntaxError is a description of a JSON syntax error. + SyntaxError = errors.SyntaxError + + // An UnmarshalTypeError describes a JSON value that was + // not appropriate for a value of a specific Go type. + UnmarshalTypeError = errors.UnmarshalTypeError + + // An UnsupportedTypeError is returned by Marshal when attempting + // to encode an unsupported value type. + UnsupportedTypeError = errors.UnsupportedTypeError + + UnsupportedValueError = errors.UnsupportedValueError +) diff --git a/error_test.go b/error_test.go new file mode 100644 index 0000000..5cd47e3 --- /dev/null +++ b/error_test.go @@ -0,0 +1,76 @@ +package fiber + +import ( + "encoding/json" + "errors" + "testing" + + "github.com/gofiber/fiber/v2/internal/schema" + "github.com/gofiber/fiber/v2/utils" +) + +func TestConversionError(t *testing.T) { + t.Parallel() + ok := errors.As(ConversionError{}, &schema.ConversionError{}) + utils.AssertEqual(t, true, ok) +} + +func TestUnknownKeyError(t *testing.T) { + t.Parallel() + ok := errors.As(UnknownKeyError{}, &schema.UnknownKeyError{}) + utils.AssertEqual(t, true, ok) +} + +func TestEmptyFieldError(t *testing.T) { + t.Parallel() + ok := errors.As(EmptyFieldError{}, &schema.EmptyFieldError{}) + utils.AssertEqual(t, true, ok) +} + +func TestMultiError(t *testing.T) { + t.Parallel() + ok := errors.As(MultiError{}, &schema.MultiError{}) + utils.AssertEqual(t, true, ok) +} + +func TestInvalidUnmarshalError(t *testing.T) { + t.Parallel() + var e *json.InvalidUnmarshalError + ok := errors.As(&InvalidUnmarshalError{}, &e) + utils.AssertEqual(t, true, ok) +} + +func TestMarshalerError(t *testing.T) { + t.Parallel() + var e *json.MarshalerError + ok := errors.As(&MarshalerError{}, &e) + utils.AssertEqual(t, true, ok) +} + +func TestSyntaxError(t *testing.T) { + t.Parallel() + var e *json.SyntaxError + ok := errors.As(&SyntaxError{}, &e) + utils.AssertEqual(t, true, ok) +} + +func TestUnmarshalTypeError(t *testing.T) { + t.Parallel() + var e *json.UnmarshalTypeError + ok := errors.As(&UnmarshalTypeError{}, &e) + utils.AssertEqual(t, true, ok) +} + +func TestUnsupportedTypeError(t *testing.T) { + t.Parallel() + var e *json.UnsupportedTypeError + ok := errors.As(&UnsupportedTypeError{}, &e) + utils.AssertEqual(t, true, ok) +} + +func TestUnsupportedValeError(t *testing.T) { + t.Parallel() + var e *json.UnsupportedValueError + ok := errors.As(&UnsupportedValueError{}, &e) + utils.AssertEqual(t, true, ok) +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..4a3b38d --- /dev/null +++ b/go.mod @@ -0,0 +1,22 @@ +module github.com/gofiber/fiber/v2 + +go 1.20 + +require ( + github.com/google/uuid v1.6.0 + github.com/mattn/go-colorable v0.1.13 + github.com/mattn/go-isatty v0.0.20 + github.com/mattn/go-runewidth v0.0.16 + github.com/tinylib/msgp v1.2.5 + github.com/valyala/bytebufferpool v1.0.0 + github.com/valyala/fasthttp v1.51.0 + golang.org/x/sys v0.28.0 +) + +require ( + github.com/andybalholm/brotli v1.1.0 // indirect + github.com/klauspost/compress v1.17.9 // indirect + github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c // indirect + github.com/rivo/uniseg v0.2.0 // indirect + github.com/valyala/tcplisten v1.0.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..67a47b0 --- /dev/null +++ b/go.sum @@ -0,0 +1,29 @@ +github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M= +github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= +github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= +github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c h1:dAMKvw0MlJT1GshSTtih8C2gDs04w8dReiOGXrGLNoY= +github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM= +github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/tinylib/msgp v1.2.5 h1:WeQg1whrXRFiZusidTQqzETkRpGjFjcIhW6uqWH09po= +github.com/tinylib/msgp v1.2.5/go.mod h1:ykjzy2wzgrlvpDCRc4LA8UXy6D8bzMSuAF3WD57Gok0= +github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasthttp v1.51.0 h1:8b30A5JlZ6C7AS81RsWjYMQmrZG6feChmgAolCl1SqA= +github.com/valyala/fasthttp v1.51.0/go.mod h1:oI2XroL+lI7vdXyYoQk03bXBThfFl2cVdIA3Xl7cH8g= +github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8= +github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= +golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= diff --git a/group.go b/group.go new file mode 100644 index 0000000..0e546a3 --- /dev/null +++ b/group.go @@ -0,0 +1,209 @@ +// ⚡️ Fiber is an Express inspired web framework written in Go with ☕️ +// 🤖 Github Repository: https://github.com/gofiber/fiber +// 📌 API Documentation: https://docs.gofiber.io + +package fiber + +import ( + "fmt" + "reflect" +) + +// Group struct +type Group struct { + app *App + parentGroup *Group + name string + anyRouteDefined bool + + Prefix string +} + +// Name Assign name to specific route or group itself. +// +// If this method is used before any route added to group, it'll set group name and OnGroupNameHook will be used. +// Otherwise, it'll set route name and OnName hook will be used. +func (grp *Group) Name(name string) Router { + if grp.anyRouteDefined { + grp.app.Name(name) + + return grp + } + + grp.app.mutex.Lock() + if grp.parentGroup != nil { + grp.name = grp.parentGroup.name + name + } else { + grp.name = name + } + + if err := grp.app.hooks.executeOnGroupNameHooks(*grp); err != nil { + panic(err) + } + grp.app.mutex.Unlock() + + return grp +} + +// Use registers a middleware route that will match requests +// with the provided prefix (which is optional and defaults to "/"). +// +// app.Use(func(c *fiber.Ctx) error { +// return c.Next() +// }) +// app.Use("/api", func(c *fiber.Ctx) error { +// return c.Next() +// }) +// app.Use("/api", handler, func(c *fiber.Ctx) error { +// return c.Next() +// }) +// +// This method will match all HTTP verbs: GET, POST, PUT, HEAD etc... +func (grp *Group) Use(args ...interface{}) Router { + var prefix string + var prefixes []string + var handlers []Handler + + for i := 0; i < len(args); i++ { + switch arg := args[i].(type) { + case string: + prefix = arg + case []string: + prefixes = arg + case Handler: + handlers = append(handlers, arg) + default: + panic(fmt.Sprintf("use: invalid handler %v\n", reflect.TypeOf(arg))) + } + } + + if len(prefixes) == 0 { + prefixes = append(prefixes, prefix) + } + + for _, prefix := range prefixes { + grp.app.register(methodUse, getGroupPath(grp.Prefix, prefix), grp, handlers...) + } + + if !grp.anyRouteDefined { + grp.anyRouteDefined = true + } + + return grp +} + +// Get registers a route for GET methods that requests a representation +// of the specified resource. Requests using GET should only retrieve data. +func (grp *Group) Get(path string, handlers ...Handler) Router { + grp.Add(MethodHead, path, handlers...) + return grp.Add(MethodGet, path, handlers...) +} + +// Head registers a route for HEAD methods that asks for a response identical +// to that of a GET request, but without the response body. +func (grp *Group) Head(path string, handlers ...Handler) Router { + return grp.Add(MethodHead, path, handlers...) +} + +// Post registers a route for POST methods that is used to submit an entity to the +// specified resource, often causing a change in state or side effects on the server. +func (grp *Group) Post(path string, handlers ...Handler) Router { + return grp.Add(MethodPost, path, handlers...) +} + +// Put registers a route for PUT methods that replaces all current representations +// of the target resource with the request payload. +func (grp *Group) Put(path string, handlers ...Handler) Router { + return grp.Add(MethodPut, path, handlers...) +} + +// Delete registers a route for DELETE methods that deletes the specified resource. +func (grp *Group) Delete(path string, handlers ...Handler) Router { + return grp.Add(MethodDelete, path, handlers...) +} + +// Connect registers a route for CONNECT methods that establishes a tunnel to the +// server identified by the target resource. +func (grp *Group) Connect(path string, handlers ...Handler) Router { + return grp.Add(MethodConnect, path, handlers...) +} + +// Options registers a route for OPTIONS methods that is used to describe the +// communication options for the target resource. +func (grp *Group) Options(path string, handlers ...Handler) Router { + return grp.Add(MethodOptions, path, handlers...) +} + +// Trace registers a route for TRACE methods that performs a message loop-back +// test along the path to the target resource. +func (grp *Group) Trace(path string, handlers ...Handler) Router { + return grp.Add(MethodTrace, path, handlers...) +} + +// Patch registers a route for PATCH methods that is used to apply partial +// modifications to a resource. +func (grp *Group) Patch(path string, handlers ...Handler) Router { + return grp.Add(MethodPatch, path, handlers...) +} + +// Add allows you to specify a HTTP method to register a route +func (grp *Group) Add(method, path string, handlers ...Handler) Router { + grp.app.register(method, getGroupPath(grp.Prefix, path), grp, handlers...) + if !grp.anyRouteDefined { + grp.anyRouteDefined = true + } + + return grp +} + +// Static will create a file server serving static files +func (grp *Group) Static(prefix, root string, config ...Static) Router { + grp.app.registerStatic(getGroupPath(grp.Prefix, prefix), root, config...) + if !grp.anyRouteDefined { + grp.anyRouteDefined = true + } + + return grp +} + +// All will register the handler on all HTTP methods +func (grp *Group) All(path string, handlers ...Handler) Router { + for _, method := range grp.app.config.RequestMethods { + _ = grp.Add(method, path, handlers...) + } + return grp +} + +// Group is used for Routes with common prefix to define a new sub-router with optional middleware. +// +// api := app.Group("/api") +// api.Get("/users", handler) +func (grp *Group) Group(prefix string, handlers ...Handler) Router { + prefix = getGroupPath(grp.Prefix, prefix) + if len(handlers) > 0 { + grp.app.register(methodUse, prefix, grp, handlers...) + } + + // Create new group + newGrp := &Group{Prefix: prefix, app: grp.app, parentGroup: grp} + if err := grp.app.hooks.executeOnGroupHooks(*newGrp); err != nil { + panic(err) + } + + return newGrp +} + +// Route is used to define routes with a common prefix inside the common function. +// Uses Group method to define new sub-router. +func (grp *Group) Route(prefix string, fn func(router Router), name ...string) Router { + // Create new group + group := grp.Group(prefix) + if len(name) > 0 { + group.Name(name[0]) + } + + // Define routes + fn(group) + + return group +} diff --git a/helpers.go b/helpers.go new file mode 100644 index 0000000..2896c2a --- /dev/null +++ b/helpers.go @@ -0,0 +1,1226 @@ +// ⚡️ Fiber is an Express inspired web framework written in Go with ☕️ +// 🤖 Github Repository: https://github.com/gofiber/fiber +// 📌 API Documentation: https://docs.gofiber.io + +package fiber + +import ( + "bytes" + "crypto/tls" + "errors" + "fmt" + "hash/crc32" + "io" + "net" + "os" + "path/filepath" + "reflect" + "strings" + "time" + "unsafe" + + "github.com/gofiber/fiber/v2/log" + "github.com/gofiber/fiber/v2/utils" + + "github.com/valyala/bytebufferpool" + "github.com/valyala/fasthttp" +) + +// acceptType is a struct that holds the parsed value of an Accept header +// along with quality, specificity, parameters, and order. +// Used for sorting accept headers. +type acceptedType struct { + spec string + quality float64 + specificity int + order int + params string +} + +// getTLSConfig returns a net listener's tls config +func getTLSConfig(ln net.Listener) *tls.Config { + // Get listener type + pointer := reflect.ValueOf(ln) + + // Is it a tls.listener? + if pointer.String() == "<*tls.listener Value>" { + // Copy value from pointer + if val := reflect.Indirect(pointer); val.Type() != nil { + // Get private field from value + if field := val.FieldByName("config"); field.Type() != nil { + // Copy value from pointer field (unsafe) + newval := reflect.NewAt(field.Type(), unsafe.Pointer(field.UnsafeAddr())) //nolint:gosec // Probably the only way to extract the *tls.Config from a net.Listener. TODO: Verify there really is no easier way without using unsafe. + if newval.Type() != nil { + // Get element from pointer + if elem := newval.Elem(); elem.Type() != nil { + // Cast value to *tls.Config + c, ok := elem.Interface().(*tls.Config) + if !ok { + panic(fmt.Errorf("failed to type-assert to *tls.Config")) + } + return c + } + } + } + } + } + + return nil +} + +// readContent opens a named file and read content from it +func readContent(rf io.ReaderFrom, name string) (int64, error) { + // Read file + f, err := os.Open(filepath.Clean(name)) + if err != nil { + return 0, fmt.Errorf("failed to open: %w", err) + } + defer func() { + if err = f.Close(); err != nil { + log.Errorf("Error closing file: %s", err) + } + }() + if n, err := rf.ReadFrom(f); err != nil { + return n, fmt.Errorf("failed to read: %w", err) + } + return 0, nil +} + +// quoteString escape special characters in a given string +func (app *App) quoteString(raw string) string { + bb := bytebufferpool.Get() + // quoted := string(fasthttp.AppendQuotedArg(bb.B, getBytes(raw))) + quoted := app.getString(fasthttp.AppendQuotedArg(bb.B, app.getBytes(raw))) + bytebufferpool.Put(bb) + return quoted +} + +// Scan stack if other methods match the request +func (app *App) methodExist(ctx *Ctx) bool { + var exists bool + methods := app.config.RequestMethods + for i := 0; i < len(methods); i++ { + // Skip original method + if ctx.methodINT == i { + continue + } + // Reset stack index + indexRoute := -1 + tree, ok := ctx.app.treeStack[i][ctx.treePath] + if !ok { + tree = ctx.app.treeStack[i][""] + } + // Get stack length + lenr := len(tree) - 1 + // Loop over the route stack starting from previous index + for indexRoute < lenr { + // Increment route index + indexRoute++ + // Get *Route + route := tree[indexRoute] + // Skip use routes + if route.use { + continue + } + // Check if it matches the request path + match := route.match(ctx.detectionPath, ctx.path, &ctx.values) + // No match, next route + if match { + // We matched + exists = true + // Add method to Allow header + ctx.Append(HeaderAllow, methods[i]) + // Break stack loop + break + } + } + } + return exists +} + +// uniqueRouteStack drop all not unique routes from the slice +func uniqueRouteStack(stack []*Route) []*Route { + var unique []*Route + m := make(map[*Route]int) + for _, v := range stack { + if _, ok := m[v]; !ok { + // Unique key found. Record position and collect + // in result. + m[v] = len(unique) + unique = append(unique, v) + } + } + + return unique +} + +// defaultString returns the value or a default value if it is set +func defaultString(value string, defaultValue []string) string { + if len(value) == 0 && len(defaultValue) > 0 { + return defaultValue[0] + } + return value +} + +const normalizedHeaderETag = "Etag" + +// Generate and set ETag header to response +func setETag(c *Ctx, weak bool) { //nolint: revive // Accepting a bool param is fine here + // Don't generate ETags for invalid responses + if c.fasthttp.Response.StatusCode() != StatusOK { + return + } + body := c.fasthttp.Response.Body() + // Skips ETag if no response body is present + if len(body) == 0 { + return + } + // Get ETag header from request + clientEtag := c.Get(HeaderIfNoneMatch) + + // Generate ETag for response + const pol = 0xD5828281 + crc32q := crc32.MakeTable(pol) + etag := fmt.Sprintf("\"%d-%v\"", len(body), crc32.Checksum(body, crc32q)) + + // Enable weak tag + if weak { + etag = "W/" + etag + } + + // Check if client's ETag is weak + if strings.HasPrefix(clientEtag, "W/") { + // Check if server's ETag is weak + if clientEtag[2:] == etag || clientEtag[2:] == etag[2:] { + // W/1 == 1 || W/1 == W/1 + if err := c.SendStatus(StatusNotModified); err != nil { + log.Errorf("setETag: failed to SendStatus: %v", err) + } + c.fasthttp.ResetBody() + return + } + // W/1 != W/2 || W/1 != 2 + c.setCanonical(normalizedHeaderETag, etag) + return + } + if strings.Contains(clientEtag, etag) { + // 1 == 1 + if err := c.SendStatus(StatusNotModified); err != nil { + log.Errorf("setETag: failed to SendStatus: %v", err) + } + c.fasthttp.ResetBody() + return + } + // 1 != 2 + c.setCanonical(normalizedHeaderETag, etag) +} + +func getGroupPath(prefix, path string) string { + if len(path) == 0 { + return prefix + } + + if path[0] != '/' { + path = "/" + path + } + + return utils.TrimRight(prefix, '/') + path +} + +// acceptsOffer This function determines if an offer matches a given specification. +// It checks if the specification ends with a '*' or if the offer has the prefix of the specification. +// Returns true if the offer matches the specification, false otherwise. +func acceptsOffer(spec, offer, _ string) bool { + if len(spec) >= 1 && spec[len(spec)-1] == '*' { + return true + } else if strings.HasPrefix(spec, offer) { + return true + } + return false +} + +// acceptsOfferType This function determines if an offer type matches a given specification. +// It checks if the specification is equal to */* (i.e., all types are accepted). +// It gets the MIME type of the offer (either from the offer itself or by its file extension). +// It checks if the offer MIME type matches the specification MIME type or if the specification is of the form /* and the offer MIME type has the same MIME type. +// It checks if the offer contains every parameter present in the specification. +// Returns true if the offer type matches the specification, false otherwise. +func acceptsOfferType(spec, offerType, specParams string) bool { + var offerMime, offerParams string + + if i := strings.IndexByte(offerType, ';'); i == -1 { + offerMime = offerType + } else { + offerMime = offerType[:i] + offerParams = offerType[i:] + } + + // Accept: */* + if spec == "*/*" { + return paramsMatch(specParams, offerParams) + } + + var mimetype string + if strings.IndexByte(offerMime, '/') != -1 { + mimetype = offerMime // MIME type + } else { + mimetype = utils.GetMIME(offerMime) // extension + } + + if spec == mimetype { + // Accept: / + return paramsMatch(specParams, offerParams) + } + + s := strings.IndexByte(mimetype, '/') + // Accept: /* + if strings.HasPrefix(spec, mimetype[:s]) && (spec[s:] == "/*" || mimetype[s:] == "/*") { + return paramsMatch(specParams, offerParams) + } + + return false +} + +// paramsMatch returns whether offerParams contains all parameters present in specParams. +// Matching is case insensitive, and surrounding quotes are stripped. +// To align with the behavior of res.format from Express, the order of parameters is +// ignored, and if a parameter is specified twice in the incoming Accept, the last +// provided value is given precedence. +// In the case of quoted values, RFC 9110 says that we must treat any character escaped +// by a backslash as equivalent to the character itself (e.g., "a\aa" is equivalent to "aaa"). +// For the sake of simplicity, we forgo this and compare the value as-is. Besides, it would +// be highly unusual for a client to escape something other than a double quote or backslash. +// See https://www.rfc-editor.org/rfc/rfc9110#name-parameters +func paramsMatch(specParamStr, offerParams string) bool { + if specParamStr == "" { + return true + } + + // Preprocess the spec params to more easily test + // for out-of-order parameters + specParams := make([][2]string, 0, 2) + forEachParameter(specParamStr, func(s1, s2 string) bool { + if s1 == "q" || s1 == "Q" { + return false + } + for i := range specParams { + if utils.EqualFold(s1, specParams[i][0]) { + specParams[i][1] = s2 + return false + } + } + specParams = append(specParams, [2]string{s1, s2}) + return true + }) + + allSpecParamsMatch := true + for i := range specParams { + foundParam := false + forEachParameter(offerParams, func(offerParam, offerVal string) bool { + if utils.EqualFold(specParams[i][0], offerParam) { + foundParam = true + allSpecParamsMatch = utils.EqualFold(specParams[i][1], offerVal) + return false + } + return true + }) + if !foundParam || !allSpecParamsMatch { + return false + } + } + return allSpecParamsMatch +} + +// getSplicedStrList function takes a string and a string slice as an argument, divides the string into different +// elements divided by ',' and stores these elements in the string slice. +// It returns the populated string slice as an output. +// +// If the given slice hasn't enough space, it will allocate more and return. +func getSplicedStrList(headerValue string, dst []string) []string { + if headerValue == "" { + return nil + } + + var ( + index int + character rune + lastElementEndsAt uint8 + insertIndex int + ) + for index, character = range headerValue + "$" { + if character == ',' || index == len(headerValue) { + if insertIndex >= len(dst) { + oldSlice := dst + dst = make([]string, len(dst)+(len(dst)>>1)+2) + copy(dst, oldSlice) + } + dst[insertIndex] = utils.TrimLeft(headerValue[lastElementEndsAt:index], ' ') + lastElementEndsAt = uint8(index + 1) + insertIndex++ + } + } + + if len(dst) > insertIndex { + dst = dst[:insertIndex] + } + return dst +} + +// forEachMediaRange parses an Accept or Content-Type header, calling functor +// on each media range. +// See: https://www.rfc-editor.org/rfc/rfc9110#name-content-negotiation-fields +func forEachMediaRange(header string, functor func(string)) { + hasDQuote := strings.IndexByte(header, '"') != -1 + + for len(header) > 0 { + n := 0 + header = utils.TrimLeft(header, ' ') + quotes := 0 + escaping := false + + if hasDQuote { + // Complex case. We need to keep track of quotes and quoted-pairs (i.e., characters escaped with \ ) + loop: + for n < len(header) { + switch header[n] { + case ',': + if quotes%2 == 0 { + break loop + } + case '"': + if !escaping { + quotes++ + } + case '\\': + if quotes%2 == 1 { + escaping = !escaping + } + } + n++ + } + } else { + // Simple case. Just look for the next comma. + if n = strings.IndexByte(header, ','); n == -1 { + n = len(header) + } + } + + functor(header[:n]) + + if n >= len(header) { + return + } + header = header[n+1:] + } +} + +// forEachParamter parses a given parameter list, calling functor +// on each valid parameter. If functor returns false, we stop processing. +// It expects a leading ';'. +// See: https://www.rfc-editor.org/rfc/rfc9110#section-5.6.6 +// According to RFC-9110 2.4, it is up to our discretion whether +// to attempt to recover from errors in HTTP semantics. Therefor, +// we take the simple approach and exit early when a semantic error +// is detected in the header. +// +// parameter = parameter-name "=" parameter-value +// parameter-name = token +// parameter-value = ( token / quoted-string ) +// parameters = *( OWS ";" OWS [ parameter ] ) +func forEachParameter(params string, functor func(string, string) bool) { + for len(params) > 0 { + // eat OWS ";" OWS + params = utils.TrimLeft(params, ' ') + if len(params) == 0 || params[0] != ';' { + return + } + params = utils.TrimLeft(params[1:], ' ') + + n := 0 + + // make sure the parameter is at least one character long + if len(params) == 0 || !validHeaderFieldByte(params[n]) { + return + } + n++ + for n < len(params) && validHeaderFieldByte(params[n]) { + n++ + } + + // We should hit a '=' (that has more characters after it) + // If not, the parameter is invalid. + // param=foo + // ~~~~~^ + if n >= len(params)-1 || params[n] != '=' { + return + } + param := params[:n] + n++ + + if params[n] == '"' { + // Handle quoted strings and quoted-pairs (i.e., characters escaped with \ ) + // See: https://www.rfc-editor.org/rfc/rfc9110#section-5.6.4 + foundEndQuote := false + escaping := false + n++ + m := n + for ; n < len(params); n++ { + if params[n] == '"' && !escaping { + foundEndQuote = true + break + } + // Recipients that process the value of a quoted-string MUST handle + // a quoted-pair as if it were replaced by the octet following the backslash + escaping = params[n] == '\\' && !escaping + } + if !foundEndQuote { + // Not a valid parameter + return + } + if !functor(param, params[m:n]) { + return + } + n++ + } else if validHeaderFieldByte(params[n]) { + // Parse a normal value, which should just be a token. + m := n + n++ + for n < len(params) && validHeaderFieldByte(params[n]) { + n++ + } + if !functor(param, params[m:n]) { + return + } + } else { + // Value was invalid + return + } + params = params[n:] + } +} + +// validHeaderFieldByte returns true if a valid tchar +// +// tchar = "!" / "#" / "$" / "%" / "&" / "'" / "*" / "+" / "-" / "." / +// "^" / "_" / "`" / "|" / "~" / DIGIT / ALPHA +// +// See: https://www.rfc-editor.org/rfc/rfc9110#section-5.6.2 +// Function copied from net/textproto: +// https://github.com/golang/go/blob/master/src/net/textproto/reader.go#L663 +func validHeaderFieldByte(c byte) bool { + // mask is a 128-bit bitmap with 1s for allowed bytes, + // so that the byte c can be tested with a shift and an and. + // If c >= 128, then 1<>64)) != 0 +} + +// getOffer return valid offer for header negotiation +func getOffer(header string, isAccepted func(spec, offer, specParams string) bool, offers ...string) string { + if len(offers) == 0 { + return "" + } + if header == "" { + return offers[0] + } + + acceptedTypes := make([]acceptedType, 0, 8) + order := 0 + + // Parse header and get accepted types with their quality and specificity + // See: https://www.rfc-editor.org/rfc/rfc9110#name-content-negotiation-fields + forEachMediaRange(header, func(accept string) { + order++ + spec, quality, params := accept, 1.0, "" + + if i := strings.IndexByte(accept, ';'); i != -1 { + spec = accept[:i] + + // The vast majority of requests will have only the q parameter with + // no whitespace. Check this first to see if we can skip + // the more involved parsing. + if strings.HasPrefix(accept[i:], ";q=") && strings.IndexByte(accept[i+3:], ';') == -1 { + if q, err := fasthttp.ParseUfloat([]byte(utils.TrimRight(accept[i+3:], ' '))); err == nil { + quality = q + } + } else { + hasParams := false + forEachParameter(accept[i:], func(param, val string) bool { + if param == "q" || param == "Q" { + if q, err := fasthttp.ParseUfloat([]byte(val)); err == nil { + quality = q + } + return false + } + hasParams = true + return true + }) + if hasParams { + params = accept[i:] + } + } + // Skip this accept type if quality is 0.0 + // See: https://www.rfc-editor.org/rfc/rfc9110#quality.values + if quality == 0.0 { + return + } + } + + spec = utils.TrimRight(spec, ' ') + + // Get specificity + var specificity int + // check for wildcard this could be a mime */* or a wildcard character * + if spec == "*/*" || spec == "*" { + specificity = 1 + } else if strings.HasSuffix(spec, "/*") { + specificity = 2 + } else if strings.IndexByte(spec, '/') != -1 { + specificity = 3 + } else { + specificity = 4 + } + + // Add to accepted types + acceptedTypes = append(acceptedTypes, acceptedType{spec, quality, specificity, order, params}) + }) + + if len(acceptedTypes) > 1 { + // Sort accepted types by quality and specificity, preserving order of equal elements + sortAcceptedTypes(&acceptedTypes) + } + + // Find the first offer that matches the accepted types + for _, acceptedType := range acceptedTypes { + for _, offer := range offers { + if len(offer) == 0 { + continue + } + if isAccepted(acceptedType.spec, offer, acceptedType.params) { + return offer + } + } + } + + return "" +} + +// sortAcceptedTypes sorts accepted types by quality and specificity, preserving order of equal elements +// A type with parameters has higher priority than an equivalent one without parameters. +// e.g., text/html;a=1;b=2 comes before text/html;a=1 +// See: https://www.rfc-editor.org/rfc/rfc9110#name-content-negotiation-fields +func sortAcceptedTypes(acceptedTypes *[]acceptedType) { + if acceptedTypes == nil || len(*acceptedTypes) < 2 { + return + } + at := *acceptedTypes + + for i := 1; i < len(at); i++ { + lo, hi := 0, i-1 + for lo <= hi { + mid := (lo + hi) / 2 + if at[i].quality < at[mid].quality || + (at[i].quality == at[mid].quality && at[i].specificity < at[mid].specificity) || + (at[i].quality == at[mid].quality && at[i].specificity < at[mid].specificity && len(at[i].params) < len(at[mid].params)) || + (at[i].quality == at[mid].quality && at[i].specificity == at[mid].specificity && len(at[i].params) == len(at[mid].params) && at[i].order > at[mid].order) { + lo = mid + 1 + } else { + hi = mid - 1 + } + } + for j := i; j > lo; j-- { + at[j-1], at[j] = at[j], at[j-1] + } + } +} + +func matchEtag(s, etag string) bool { + if s == etag || s == "W/"+etag || "W/"+s == etag { + return true + } + + return false +} + +func (app *App) isEtagStale(etag string, noneMatchBytes []byte) bool { + var start, end int + + // Adapted from: + // https://github.com/jshttp/fresh/blob/10e0471669dbbfbfd8de65bc6efac2ddd0bfa057/index.js#L110 + for i := range noneMatchBytes { + switch noneMatchBytes[i] { + case 0x20: + if start == end { + start = i + 1 + end = i + 1 + } + case 0x2c: + if matchEtag(app.getString(noneMatchBytes[start:end]), etag) { + return false + } + start = i + 1 + end = i + 1 + default: + end = i + 1 + } + } + + return !matchEtag(app.getString(noneMatchBytes[start:end]), etag) +} + +func parseAddr(raw string) (string, string) { //nolint:revive // Returns (host, port) + if i := strings.LastIndex(raw, ":"); i != -1 { + return raw[:i], raw[i+1:] + } + return raw, "" +} + +const noCacheValue = "no-cache" + +// isNoCache checks if the cacheControl header value is a `no-cache`. +func isNoCache(cacheControl string) bool { + i := strings.Index(cacheControl, noCacheValue) + if i == -1 { + return false + } + + // Xno-cache + if i > 0 && !(cacheControl[i-1] == ' ' || cacheControl[i-1] == ',') { + return false + } + + // bla bla, no-cache + if i+len(noCacheValue) == len(cacheControl) { + return true + } + + // bla bla, no-cacheX + if cacheControl[i+len(noCacheValue)] != ',' { + return false + } + + // OK + return true +} + +type testConn struct { + r bytes.Buffer + w bytes.Buffer +} + +func (c *testConn) Read(b []byte) (int, error) { return c.r.Read(b) } //nolint:wrapcheck // This must not be wrapped +func (c *testConn) Write(b []byte) (int, error) { return c.w.Write(b) } //nolint:wrapcheck // This must not be wrapped +func (*testConn) Close() error { return nil } + +func (*testConn) LocalAddr() net.Addr { return &net.TCPAddr{Port: 0, Zone: "", IP: net.IPv4zero} } +func (*testConn) RemoteAddr() net.Addr { return &net.TCPAddr{Port: 0, Zone: "", IP: net.IPv4zero} } +func (*testConn) SetDeadline(_ time.Time) error { return nil } +func (*testConn) SetReadDeadline(_ time.Time) error { return nil } +func (*testConn) SetWriteDeadline(_ time.Time) error { return nil } + +func getStringImmutable(b []byte) string { + return string(b) +} + +func getBytesImmutable(s string) []byte { + return []byte(s) +} + +// HTTP methods and their unique INTs +func (app *App) methodInt(s string) int { + // For better performance + if len(app.configured.RequestMethods) == 0 { + // TODO: Use iota instead + switch s { + case MethodGet: + return 0 + case MethodHead: + return 1 + case MethodPost: + return 2 + case MethodPut: + return 3 + case MethodDelete: + return 4 + case MethodConnect: + return 5 + case MethodOptions: + return 6 + case MethodTrace: + return 7 + case MethodPatch: + return 8 + default: + return -1 + } + } + + // For method customization + for i, v := range app.config.RequestMethods { + if s == v { + return i + } + } + + return -1 +} + +// IsMethodSafe reports whether the HTTP method is considered safe. +// See https://datatracker.ietf.org/doc/html/rfc9110#section-9.2.1 +func IsMethodSafe(m string) bool { + switch m { + case MethodGet, + MethodHead, + MethodOptions, + MethodTrace: + return true + default: + return false + } +} + +// IsMethodIdempotent reports whether the HTTP method is considered idempotent. +// See https://datatracker.ietf.org/doc/html/rfc9110#section-9.2.2 +func IsMethodIdempotent(m string) bool { + if IsMethodSafe(m) { + return true + } + + switch m { + case MethodPut, MethodDelete: + return true + default: + return false + } +} + +// HTTP methods were copied from net/http. +const ( + MethodGet = "GET" // RFC 7231, 4.3.1 + MethodHead = "HEAD" // RFC 7231, 4.3.2 + MethodPost = "POST" // RFC 7231, 4.3.3 + MethodPut = "PUT" // RFC 7231, 4.3.4 + MethodPatch = "PATCH" // RFC 5789 + MethodDelete = "DELETE" // RFC 7231, 4.3.5 + MethodConnect = "CONNECT" // RFC 7231, 4.3.6 + MethodOptions = "OPTIONS" // RFC 7231, 4.3.7 + MethodTrace = "TRACE" // RFC 7231, 4.3.8 + methodUse = "USE" +) + +// MIME types that are commonly used +const ( + MIMETextXML = "text/xml" + MIMETextHTML = "text/html" + MIMETextPlain = "text/plain" + MIMETextJavaScript = "text/javascript" + MIMEApplicationXML = "application/xml" + MIMEApplicationJSON = "application/json" + // Deprecated: use MIMETextJavaScript instead + MIMEApplicationJavaScript = "application/javascript" + MIMEApplicationForm = "application/x-www-form-urlencoded" + MIMEOctetStream = "application/octet-stream" + MIMEMultipartForm = "multipart/form-data" + + MIMETextXMLCharsetUTF8 = "text/xml; charset=utf-8" + MIMETextHTMLCharsetUTF8 = "text/html; charset=utf-8" + MIMETextPlainCharsetUTF8 = "text/plain; charset=utf-8" + MIMETextJavaScriptCharsetUTF8 = "text/javascript; charset=utf-8" + MIMEApplicationXMLCharsetUTF8 = "application/xml; charset=utf-8" + MIMEApplicationJSONCharsetUTF8 = "application/json; charset=utf-8" + // Deprecated: use MIMETextJavaScriptCharsetUTF8 instead + MIMEApplicationJavaScriptCharsetUTF8 = "application/javascript; charset=utf-8" +) + +// HTTP status codes were copied from net/http with the following updates: +// - Rename StatusNonAuthoritativeInfo to StatusNonAuthoritativeInformation +// - Add StatusSwitchProxy (306) +// NOTE: Keep this list in sync with statusMessage +const ( + StatusContinue = 100 // RFC 9110, 15.2.1 + StatusSwitchingProtocols = 101 // RFC 9110, 15.2.2 + StatusProcessing = 102 // RFC 2518, 10.1 + StatusEarlyHints = 103 // RFC 8297 + + StatusOK = 200 // RFC 9110, 15.3.1 + StatusCreated = 201 // RFC 9110, 15.3.2 + StatusAccepted = 202 // RFC 9110, 15.3.3 + StatusNonAuthoritativeInformation = 203 // RFC 9110, 15.3.4 + StatusNoContent = 204 // RFC 9110, 15.3.5 + StatusResetContent = 205 // RFC 9110, 15.3.6 + StatusPartialContent = 206 // RFC 9110, 15.3.7 + StatusMultiStatus = 207 // RFC 4918, 11.1 + StatusAlreadyReported = 208 // RFC 5842, 7.1 + StatusIMUsed = 226 // RFC 3229, 10.4.1 + + StatusMultipleChoices = 300 // RFC 9110, 15.4.1 + StatusMovedPermanently = 301 // RFC 9110, 15.4.2 + StatusFound = 302 // RFC 9110, 15.4.3 + StatusSeeOther = 303 // RFC 9110, 15.4.4 + StatusNotModified = 304 // RFC 9110, 15.4.5 + StatusUseProxy = 305 // RFC 9110, 15.4.6 + StatusSwitchProxy = 306 // RFC 9110, 15.4.7 (Unused) + StatusTemporaryRedirect = 307 // RFC 9110, 15.4.8 + StatusPermanentRedirect = 308 // RFC 9110, 15.4.9 + + StatusBadRequest = 400 // RFC 9110, 15.5.1 + StatusUnauthorized = 401 // RFC 9110, 15.5.2 + StatusPaymentRequired = 402 // RFC 9110, 15.5.3 + StatusForbidden = 403 // RFC 9110, 15.5.4 + StatusNotFound = 404 // RFC 9110, 15.5.5 + StatusMethodNotAllowed = 405 // RFC 9110, 15.5.6 + StatusNotAcceptable = 406 // RFC 9110, 15.5.7 + StatusProxyAuthRequired = 407 // RFC 9110, 15.5.8 + StatusRequestTimeout = 408 // RFC 9110, 15.5.9 + StatusConflict = 409 // RFC 9110, 15.5.10 + StatusGone = 410 // RFC 9110, 15.5.11 + StatusLengthRequired = 411 // RFC 9110, 15.5.12 + StatusPreconditionFailed = 412 // RFC 9110, 15.5.13 + StatusRequestEntityTooLarge = 413 // RFC 9110, 15.5.14 + StatusRequestURITooLong = 414 // RFC 9110, 15.5.15 + StatusUnsupportedMediaType = 415 // RFC 9110, 15.5.16 + StatusRequestedRangeNotSatisfiable = 416 // RFC 9110, 15.5.17 + StatusExpectationFailed = 417 // RFC 9110, 15.5.18 + StatusTeapot = 418 // RFC 9110, 15.5.19 (Unused) + StatusMisdirectedRequest = 421 // RFC 9110, 15.5.20 + StatusUnprocessableEntity = 422 // RFC 9110, 15.5.21 + StatusLocked = 423 // RFC 4918, 11.3 + StatusFailedDependency = 424 // RFC 4918, 11.4 + StatusTooEarly = 425 // RFC 8470, 5.2. + StatusUpgradeRequired = 426 // RFC 9110, 15.5.22 + StatusPreconditionRequired = 428 // RFC 6585, 3 + StatusTooManyRequests = 429 // RFC 6585, 4 + StatusRequestHeaderFieldsTooLarge = 431 // RFC 6585, 5 + StatusUnavailableForLegalReasons = 451 // RFC 7725, 3 + + StatusInternalServerError = 500 // RFC 9110, 15.6.1 + StatusNotImplemented = 501 // RFC 9110, 15.6.2 + StatusBadGateway = 502 // RFC 9110, 15.6.3 + StatusServiceUnavailable = 503 // RFC 9110, 15.6.4 + StatusGatewayTimeout = 504 // RFC 9110, 15.6.5 + StatusHTTPVersionNotSupported = 505 // RFC 9110, 15.6.6 + StatusVariantAlsoNegotiates = 506 // RFC 2295, 8.1 + StatusInsufficientStorage = 507 // RFC 4918, 11.5 + StatusLoopDetected = 508 // RFC 5842, 7.2 + StatusNotExtended = 510 // RFC 2774, 7 + StatusNetworkAuthenticationRequired = 511 // RFC 6585, 6 +) + +// Errors +var ( + ErrBadRequest = NewError(StatusBadRequest) // 400 + ErrUnauthorized = NewError(StatusUnauthorized) // 401 + ErrPaymentRequired = NewError(StatusPaymentRequired) // 402 + ErrForbidden = NewError(StatusForbidden) // 403 + ErrNotFound = NewError(StatusNotFound) // 404 + ErrMethodNotAllowed = NewError(StatusMethodNotAllowed) // 405 + ErrNotAcceptable = NewError(StatusNotAcceptable) // 406 + ErrProxyAuthRequired = NewError(StatusProxyAuthRequired) // 407 + ErrRequestTimeout = NewError(StatusRequestTimeout) // 408 + ErrConflict = NewError(StatusConflict) // 409 + ErrGone = NewError(StatusGone) // 410 + ErrLengthRequired = NewError(StatusLengthRequired) // 411 + ErrPreconditionFailed = NewError(StatusPreconditionFailed) // 412 + ErrRequestEntityTooLarge = NewError(StatusRequestEntityTooLarge) // 413 + ErrRequestURITooLong = NewError(StatusRequestURITooLong) // 414 + ErrUnsupportedMediaType = NewError(StatusUnsupportedMediaType) // 415 + ErrRequestedRangeNotSatisfiable = NewError(StatusRequestedRangeNotSatisfiable) // 416 + ErrExpectationFailed = NewError(StatusExpectationFailed) // 417 + ErrTeapot = NewError(StatusTeapot) // 418 + ErrMisdirectedRequest = NewError(StatusMisdirectedRequest) // 421 + ErrUnprocessableEntity = NewError(StatusUnprocessableEntity) // 422 + ErrLocked = NewError(StatusLocked) // 423 + ErrFailedDependency = NewError(StatusFailedDependency) // 424 + ErrTooEarly = NewError(StatusTooEarly) // 425 + ErrUpgradeRequired = NewError(StatusUpgradeRequired) // 426 + ErrPreconditionRequired = NewError(StatusPreconditionRequired) // 428 + ErrTooManyRequests = NewError(StatusTooManyRequests) // 429 + ErrRequestHeaderFieldsTooLarge = NewError(StatusRequestHeaderFieldsTooLarge) // 431 + ErrUnavailableForLegalReasons = NewError(StatusUnavailableForLegalReasons) // 451 + + ErrInternalServerError = NewError(StatusInternalServerError) // 500 + ErrNotImplemented = NewError(StatusNotImplemented) // 501 + ErrBadGateway = NewError(StatusBadGateway) // 502 + ErrServiceUnavailable = NewError(StatusServiceUnavailable) // 503 + ErrGatewayTimeout = NewError(StatusGatewayTimeout) // 504 + ErrHTTPVersionNotSupported = NewError(StatusHTTPVersionNotSupported) // 505 + ErrVariantAlsoNegotiates = NewError(StatusVariantAlsoNegotiates) // 506 + ErrInsufficientStorage = NewError(StatusInsufficientStorage) // 507 + ErrLoopDetected = NewError(StatusLoopDetected) // 508 + ErrNotExtended = NewError(StatusNotExtended) // 510 + ErrNetworkAuthenticationRequired = NewError(StatusNetworkAuthenticationRequired) // 511 +) + +// HTTP Headers were copied from net/http. +const ( + HeaderAuthorization = "Authorization" + HeaderProxyAuthenticate = "Proxy-Authenticate" + HeaderProxyAuthorization = "Proxy-Authorization" + HeaderWWWAuthenticate = "WWW-Authenticate" + HeaderAge = "Age" + HeaderCacheControl = "Cache-Control" + HeaderClearSiteData = "Clear-Site-Data" + HeaderExpires = "Expires" + HeaderPragma = "Pragma" + HeaderWarning = "Warning" + HeaderAcceptCH = "Accept-CH" + HeaderAcceptCHLifetime = "Accept-CH-Lifetime" + HeaderContentDPR = "Content-DPR" + HeaderDPR = "DPR" + HeaderEarlyData = "Early-Data" + HeaderSaveData = "Save-Data" + HeaderViewportWidth = "Viewport-Width" + HeaderWidth = "Width" + HeaderETag = "ETag" + HeaderIfMatch = "If-Match" + HeaderIfModifiedSince = "If-Modified-Since" + HeaderIfNoneMatch = "If-None-Match" + HeaderIfUnmodifiedSince = "If-Unmodified-Since" + HeaderLastModified = "Last-Modified" + HeaderVary = "Vary" + HeaderConnection = "Connection" + HeaderKeepAlive = "Keep-Alive" + HeaderAccept = "Accept" + HeaderAcceptCharset = "Accept-Charset" + HeaderAcceptEncoding = "Accept-Encoding" + HeaderAcceptLanguage = "Accept-Language" + HeaderCookie = "Cookie" + HeaderExpect = "Expect" + HeaderMaxForwards = "Max-Forwards" + HeaderSetCookie = "Set-Cookie" + HeaderAccessControlAllowCredentials = "Access-Control-Allow-Credentials" + HeaderAccessControlAllowHeaders = "Access-Control-Allow-Headers" + HeaderAccessControlAllowMethods = "Access-Control-Allow-Methods" + HeaderAccessControlAllowOrigin = "Access-Control-Allow-Origin" + HeaderAccessControlExposeHeaders = "Access-Control-Expose-Headers" + HeaderAccessControlMaxAge = "Access-Control-Max-Age" + HeaderAccessControlRequestHeaders = "Access-Control-Request-Headers" + HeaderAccessControlRequestMethod = "Access-Control-Request-Method" + HeaderOrigin = "Origin" + HeaderTimingAllowOrigin = "Timing-Allow-Origin" + HeaderXPermittedCrossDomainPolicies = "X-Permitted-Cross-Domain-Policies" + HeaderDNT = "DNT" + HeaderTk = "Tk" + HeaderContentDisposition = "Content-Disposition" + HeaderContentEncoding = "Content-Encoding" + HeaderContentLanguage = "Content-Language" + HeaderContentLength = "Content-Length" + HeaderContentLocation = "Content-Location" + HeaderContentType = "Content-Type" + HeaderForwarded = "Forwarded" + HeaderVia = "Via" + HeaderXForwardedFor = "X-Forwarded-For" + HeaderXForwardedHost = "X-Forwarded-Host" + HeaderXForwardedProto = "X-Forwarded-Proto" + HeaderXForwardedProtocol = "X-Forwarded-Protocol" + HeaderXForwardedSsl = "X-Forwarded-Ssl" + HeaderXUrlScheme = "X-Url-Scheme" + HeaderLocation = "Location" + HeaderFrom = "From" + HeaderHost = "Host" + HeaderReferer = "Referer" + HeaderReferrerPolicy = "Referrer-Policy" + HeaderUserAgent = "User-Agent" + HeaderAllow = "Allow" + HeaderServer = "Server" + HeaderAcceptRanges = "Accept-Ranges" + HeaderContentRange = "Content-Range" + HeaderIfRange = "If-Range" + HeaderRange = "Range" + HeaderContentSecurityPolicy = "Content-Security-Policy" + HeaderContentSecurityPolicyReportOnly = "Content-Security-Policy-Report-Only" + HeaderCrossOriginResourcePolicy = "Cross-Origin-Resource-Policy" + HeaderExpectCT = "Expect-CT" + // Deprecated: use HeaderPermissionsPolicy instead + HeaderFeaturePolicy = "Feature-Policy" + HeaderPermissionsPolicy = "Permissions-Policy" + HeaderPublicKeyPins = "Public-Key-Pins" + HeaderPublicKeyPinsReportOnly = "Public-Key-Pins-Report-Only" + HeaderStrictTransportSecurity = "Strict-Transport-Security" + HeaderUpgradeInsecureRequests = "Upgrade-Insecure-Requests" + HeaderXContentTypeOptions = "X-Content-Type-Options" + HeaderXDownloadOptions = "X-Download-Options" + HeaderXFrameOptions = "X-Frame-Options" + HeaderXPoweredBy = "X-Powered-By" + HeaderXXSSProtection = "X-XSS-Protection" + HeaderLastEventID = "Last-Event-ID" + HeaderNEL = "NEL" + HeaderPingFrom = "Ping-From" + HeaderPingTo = "Ping-To" + HeaderReportTo = "Report-To" + HeaderTE = "TE" + HeaderTrailer = "Trailer" + HeaderTransferEncoding = "Transfer-Encoding" + HeaderSecWebSocketAccept = "Sec-WebSocket-Accept" + HeaderSecWebSocketExtensions = "Sec-WebSocket-Extensions" + HeaderSecWebSocketKey = "Sec-WebSocket-Key" + HeaderSecWebSocketProtocol = "Sec-WebSocket-Protocol" + HeaderSecWebSocketVersion = "Sec-WebSocket-Version" + HeaderAcceptPatch = "Accept-Patch" + HeaderAcceptPushPolicy = "Accept-Push-Policy" + HeaderAcceptSignature = "Accept-Signature" + HeaderAltSvc = "Alt-Svc" + HeaderDate = "Date" + HeaderIndex = "Index" + HeaderLargeAllocation = "Large-Allocation" + HeaderLink = "Link" + HeaderPushPolicy = "Push-Policy" + HeaderRetryAfter = "Retry-After" + HeaderServerTiming = "Server-Timing" + HeaderSignature = "Signature" + HeaderSignedHeaders = "Signed-Headers" + HeaderSourceMap = "SourceMap" + HeaderUpgrade = "Upgrade" + HeaderXDNSPrefetchControl = "X-DNS-Prefetch-Control" + HeaderXPingback = "X-Pingback" + HeaderXRequestID = "X-Request-ID" + HeaderXRequestedWith = "X-Requested-With" + HeaderXRobotsTag = "X-Robots-Tag" + HeaderXUACompatible = "X-UA-Compatible" +) + +// Network types that are commonly used +const ( + NetworkTCP = "tcp" + NetworkTCP4 = "tcp4" + NetworkTCP6 = "tcp6" +) + +// Compression types +const ( + StrGzip = "gzip" + StrBr = "br" + StrDeflate = "deflate" + StrBrotli = "brotli" +) + +// Cookie SameSite +// https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis-03#section-4.1.2.7 +const ( + CookieSameSiteDisabled = "disabled" // not in RFC, just control "SameSite" attribute will not be set. + CookieSameSiteLaxMode = "lax" + CookieSameSiteStrictMode = "strict" + CookieSameSiteNoneMode = "none" +) + +// Route Constraints +const ( + ConstraintInt = "int" + ConstraintBool = "bool" + ConstraintFloat = "float" + ConstraintAlpha = "alpha" + ConstraintGuid = "guid" //nolint:revive,stylecheck // TODO: Rename to "ConstraintGUID" in v3 + ConstraintMinLen = "minLen" + ConstraintMaxLen = "maxLen" + ConstraintLen = "len" + ConstraintBetweenLen = "betweenLen" + ConstraintMinLenLower = "minlen" + ConstraintMaxLenLower = "maxlen" + ConstraintBetweenLenLower = "betweenlen" + ConstraintMin = "min" + ConstraintMax = "max" + ConstraintRange = "range" + ConstraintDatetime = "datetime" + ConstraintRegex = "regex" +) + +func IndexRune(str string, needle int32) bool { + for _, b := range str { + if b == needle { + return true + } + } + return false +} + +func parseParamSquareBrackets(k string) (string, error) { + bb := bytebufferpool.Get() + defer bytebufferpool.Put(bb) + + kbytes := []byte(k) + openBracketsCount := 0 + + for i, b := range kbytes { + if b == '[' { + openBracketsCount++ + if i+1 < len(kbytes) && kbytes[i+1] != ']' { + if err := bb.WriteByte('.'); err != nil { + return "", fmt.Errorf("failed to write: %w", err) + } + } + continue + } + + if b == ']' { + openBracketsCount-- + if openBracketsCount < 0 { + return "", errors.New("unmatched brackets") + } + continue + } + + if err := bb.WriteByte(b); err != nil { + return "", fmt.Errorf("failed to write: %w", err) + } + } + + if openBracketsCount > 0 { + return "", errors.New("unmatched brackets") + } + + return bb.String(), nil +} + +func formatParserData(out interface{}, data map[string][]string, aliasTag, key string, value interface{}, enableSplitting, supportBracketNotation bool) error { //nolint:revive // it's okay + var err error + if supportBracketNotation && strings.Contains(key, "[") { + key, err = parseParamSquareBrackets(key) + if err != nil { + return err + } + } + + switch v := value.(type) { + case string: + assignBindData(out, data, aliasTag, key, v, enableSplitting) + case []string: + for _, val := range v { + assignBindData(out, data, aliasTag, key, val, enableSplitting) + } + default: + return fmt.Errorf("unsupported value type: %T", value) + } + + return err +} + +func assignBindData(out interface{}, data map[string][]string, aliasTag, key, value string, enableSplitting bool) { //nolint:revive // it's okay + if enableSplitting && strings.Contains(value, ",") && equalFieldType(out, reflect.Slice, key, aliasTag) { + values := strings.Split(value, ",") + for i := 0; i < len(values); i++ { + data[key] = append(data[key], values[i]) + } + } else { + data[key] = append(data[key], value) + } +} diff --git a/helpers_fuzz_test.go b/helpers_fuzz_test.go new file mode 100644 index 0000000..2fce647 --- /dev/null +++ b/helpers_fuzz_test.go @@ -0,0 +1,23 @@ +//go:build go1.18 + +package fiber + +import ( + "testing" +) + +// go test -v -run=^$ -fuzz=FuzzUtilsGetOffer +func FuzzUtilsGetOffer(f *testing.F) { + inputs := []string{ + `application/json; v=1; foo=bar; q=0.938; extra=param, text/plain;param="big fox"; q=0.43`, + `text/html, application/xhtml+xml, application/xml;q=0.9, */*;q=0.8`, + `*/*`, + `text/plain; q=0.5, text/html, text/x-dvi; q=0.8, text/x-c`, + } + for _, input := range inputs { + f.Add(input) + } + f.Fuzz(func(_ *testing.T, spec string) { + getOffer(spec, acceptsOfferType, `application/json;version=1;v=1;foo=bar`, `text/plain;param="big fox"`) + }) +} diff --git a/helpers_test.go b/helpers_test.go new file mode 100644 index 0000000..9f7f88c --- /dev/null +++ b/helpers_test.go @@ -0,0 +1,801 @@ +// ⚡️ Fiber is an Express inspired web framework written in Go with ☕️ +// 📝 Github Repository: https://github.com/gofiber/fiber +// 📌 API Documentation: https://docs.gofiber.io + +package fiber + +import ( + "fmt" + "strings" + "testing" + "time" + + "github.com/gofiber/fiber/v2/utils" + + "github.com/valyala/fasthttp" +) + +// go test -v -run=Test_Utils_ -count=3 +func Test_Utils_ETag(t *testing.T) { + t.Parallel() + app := New() + t.Run("Not Status OK", func(t *testing.T) { + t.Parallel() + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + err := c.SendString("Hello, World!") + utils.AssertEqual(t, nil, err) + c.Status(201) + setETag(c, false) + utils.AssertEqual(t, "", string(c.Response().Header.Peek(HeaderETag))) + }) + + t.Run("No Body", func(t *testing.T) { + t.Parallel() + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + setETag(c, false) + utils.AssertEqual(t, "", string(c.Response().Header.Peek(HeaderETag))) + }) + + t.Run("Has HeaderIfNoneMatch", func(t *testing.T) { + t.Parallel() + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + err := c.SendString("Hello, World!") + utils.AssertEqual(t, nil, err) + c.Request().Header.Set(HeaderIfNoneMatch, `"13-1831710635"`) + setETag(c, false) + utils.AssertEqual(t, 304, c.Response().StatusCode()) + utils.AssertEqual(t, "", string(c.Response().Header.Peek(HeaderETag))) + utils.AssertEqual(t, "", string(c.Response().Body())) + }) + + t.Run("No HeaderIfNoneMatch", func(t *testing.T) { + t.Parallel() + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + err := c.SendString("Hello, World!") + utils.AssertEqual(t, nil, err) + setETag(c, false) + utils.AssertEqual(t, `"13-1831710635"`, string(c.Response().Header.Peek(HeaderETag))) + }) +} + +func Test_Utils_GetOffer(t *testing.T) { + t.Parallel() + utils.AssertEqual(t, "", getOffer("hello", acceptsOffer)) + utils.AssertEqual(t, "1", getOffer("", acceptsOffer, "1")) + utils.AssertEqual(t, "", getOffer("2", acceptsOffer, "1")) + + utils.AssertEqual(t, "", getOffer("", acceptsOfferType)) + utils.AssertEqual(t, "", getOffer("text/html", acceptsOfferType)) + utils.AssertEqual(t, "", getOffer("text/html", acceptsOfferType, "application/json")) + utils.AssertEqual(t, "", getOffer("text/html;q=0", acceptsOfferType, "text/html")) + utils.AssertEqual(t, "", getOffer("application/json, */*; q=0", acceptsOfferType, "image/png")) + utils.AssertEqual(t, "application/xml", getOffer("text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", acceptsOfferType, "application/xml", "application/json")) + utils.AssertEqual(t, "text/html", getOffer("text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", acceptsOfferType, "text/html")) + utils.AssertEqual(t, "application/pdf", getOffer("text/plain;q=0,application/pdf;q=0.9,*/*;q=0.000", acceptsOfferType, "application/pdf", "application/json")) + utils.AssertEqual(t, "application/pdf", getOffer("text/plain;q=0,application/pdf;q=0.9,*/*;q=0.000", acceptsOfferType, "application/pdf", "application/json")) + utils.AssertEqual(t, "text/plain;a=1", getOffer("text/plain;a=1", acceptsOfferType, "text/plain;a=1")) + utils.AssertEqual(t, "", getOffer("text/plain;a=1;b=2", acceptsOfferType, "text/plain;b=2")) + + // Spaces, quotes, out of order params, and case insensitivity + utils.AssertEqual(t, "text/plain", getOffer("text/plain ", acceptsOfferType, "text/plain")) + utils.AssertEqual(t, "text/plain", getOffer("text/plain;q=0.4 ", acceptsOfferType, "text/plain")) + utils.AssertEqual(t, "text/plain", getOffer("text/plain;q=0.4 ;", acceptsOfferType, "text/plain")) + utils.AssertEqual(t, "text/plain", getOffer("text/plain;q=0.4 ; p=foo", acceptsOfferType, "text/plain")) + utils.AssertEqual(t, "text/plain;b=2;a=1", getOffer("text/plain ;a=1;b=2", acceptsOfferType, "text/plain;b=2;a=1")) + utils.AssertEqual(t, "text/plain;a=1", getOffer("text/plain; a=1 ", acceptsOfferType, "text/plain;a=1")) + utils.AssertEqual(t, `text/plain;a="1;b=2\",text/plain"`, getOffer(`text/plain;a="1;b=2\",text/plain";q=0.9`, acceptsOfferType, `text/plain;a=1;b=2`, `text/plain;a="1;b=2\",text/plain"`)) + utils.AssertEqual(t, "text/plain;A=CAPS", getOffer(`text/plain;a="caPs"`, acceptsOfferType, "text/plain;A=CAPS")) + + // Priority + utils.AssertEqual(t, "text/plain", getOffer("text/plain", acceptsOfferType, "text/plain", "text/plain;a=1")) + utils.AssertEqual(t, "text/plain;a=1", getOffer("text/plain", acceptsOfferType, "text/plain;a=1", "text/plain")) + utils.AssertEqual(t, "text/plain;a=1", getOffer("text/plain,text/plain;a=1", acceptsOfferType, "text/plain", "text/plain;a=1")) + utils.AssertEqual(t, "text/plain", getOffer("text/plain;q=0.899,text/plain;a=1;q=0.898", acceptsOfferType, "text/plain", "text/plain;a=1")) + utils.AssertEqual(t, "text/plain;a=1;b=2", getOffer("text/plain,text/plain;a=1,text/plain;a=1;b=2", acceptsOfferType, "text/plain", "text/plain;a=1", "text/plain;a=1;b=2")) + + // Takes the last value specified + utils.AssertEqual(t, "text/plain;a=1;b=2", getOffer("text/plain;a=1;b=1;B=2", acceptsOfferType, "text/plain;a=1;b=1", "text/plain;a=1;b=2")) + + utils.AssertEqual(t, "", getOffer("utf-8, iso-8859-1;q=0.5", acceptsOffer)) + utils.AssertEqual(t, "", getOffer("utf-8, iso-8859-1;q=0.5", acceptsOffer, "ascii")) + utils.AssertEqual(t, "utf-8", getOffer("utf-8, iso-8859-1;q=0.5", acceptsOffer, "utf-8")) + utils.AssertEqual(t, "iso-8859-1", getOffer("utf-8;q=0, iso-8859-1;q=0.5", acceptsOffer, "utf-8", "iso-8859-1")) + + utils.AssertEqual(t, "deflate", getOffer("gzip, deflate", acceptsOffer, "deflate")) + utils.AssertEqual(t, "", getOffer("gzip, deflate;q=0", acceptsOffer, "deflate")) +} + +// go test -v -run=^$ -bench=Benchmark_Utils_GetOffer -benchmem -count=4 +func Benchmark_Utils_GetOffer(b *testing.B) { + testCases := []struct { + description string + accept string + offers []string + }{ + { + description: "simple", + accept: "application/json", + offers: []string{"application/json"}, + }, + { + description: "6 offers", + accept: "text/plain", + offers: []string{"junk/a", "junk/b", "junk/c", "junk/d", "junk/e", "text/plain"}, + }, + { + description: "1 parameter", + accept: "application/json; version=1", + offers: []string{"application/json;version=1"}, + }, + { + description: "2 parameters", + accept: "application/json; version=1; foo=bar", + offers: []string{"application/json;version=1;foo=bar"}, + }, + { + // 1 alloc: + // The implementation uses a slice of length 2 allocated on the stack, + // so a third parameters causes a heap allocation. + description: "3 parameters", + accept: "application/json; version=1; foo=bar; charset=utf-8", + offers: []string{"application/json;version=1;foo=bar;charset=utf-8"}, + }, + { + description: "10 parameters", + accept: "text/plain;a=1;b=2;c=3;d=4;e=5;f=6;g=7;h=8;i=9;j=10", + offers: []string{"text/plain;a=1;b=2;c=3;d=4;e=5;f=6;g=7;h=8;i=9;j=10"}, + }, + { + description: "6 offers w/params", + accept: "text/plain; format=flowed", + offers: []string{ + "junk/a;a=b", + "junk/b;b=c", + "junk/c;c=d", + "text/plain; format=justified", + "text/plain; format=flat", + "text/plain; format=flowed", + }, + }, + { + description: "mime extension", + accept: "utf-8, iso-8859-1;q=0.5", + offers: []string{"utf-8"}, + }, + { + description: "mime extension", + accept: "utf-8, iso-8859-1;q=0.5", + offers: []string{"iso-8859-1"}, + }, + { + description: "mime extension", + accept: "utf-8, iso-8859-1;q=0.5", + offers: []string{"iso-8859-1", "utf-8"}, + }, + { + description: "mime extension", + accept: "gzip, deflate", + offers: []string{"deflate"}, + }, + { + description: "web browser", + accept: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", + offers: []string{"text/html", "application/xml", "application/xml+xhtml"}, + }, + } + + for _, tc := range testCases { + b.Run(tc.description, func(b *testing.B) { + for n := 0; n < b.N; n++ { + getOffer(tc.accept, acceptsOfferType, tc.offers...) + } + }) + } +} + +func Test_Utils_ForEachParameter(t *testing.T) { + testCases := []struct { + description string + paramStr string + expectedParams [][]string + }{ + { + description: "empty input", + paramStr: ``, + }, + { + description: "no parameters", + paramStr: `; `, + }, + { + description: "naked equals", + paramStr: `; = `, + }, + { + description: "no value", + paramStr: `;s=`, + }, + { + description: "no name", + paramStr: `;=bar`, + }, + { + description: "illegal characters in name", + paramStr: `; foo@bar=baz`, + }, + { + description: "value starts with illegal characters", + paramStr: `; foo=@baz; param=val`, + }, + { + description: "unterminated quoted value", + paramStr: `; foo="bar`, + }, + { + description: "illegal character after value terminates parsing", + paramStr: `; foo=bar@baz; param=val`, + expectedParams: [][]string{ + {"foo", "bar"}, + }, + }, + { + description: "parses parameters", + paramStr: `; foo=bar; PARAM=BAZ`, + expectedParams: [][]string{ + {"foo", "bar"}, + {"PARAM", "BAZ"}, + }, + }, + { + description: "stops parsing when functor returns false", + paramStr: `; foo=bar; end=baz; extra=unparsed`, + expectedParams: [][]string{ + {"foo", "bar"}, + {"end", "baz"}, + }, + }, + { + description: "stops parsing when encountering a non-parameter string", + paramStr: `; foo=bar; gzip; param=baz`, + expectedParams: [][]string{ + {"foo", "bar"}, + }, + }, + { + description: "quoted string with escapes and special characters", + // Note: the sequence \\\" is effectively an escaped backslash \\ and + // an escaped double quote \" + paramStr: `;foo="20t\w,b\\\"b;s=k o"`, + expectedParams: [][]string{ + {"foo", `20t\w,b\\\"b;s=k o`}, + }, + }, + { + description: "complex", + paramStr: ` ; foo=1 ; bar="\"value\""; end="20tw,b\\\"b;s=k o" ; action=skip `, + expectedParams: [][]string{ + {"foo", "1"}, + {"bar", `\"value\"`}, + {"end", `20tw,b\\\"b;s=k o`}, + }, + }, + } + for _, tc := range testCases { + n := 0 + forEachParameter(tc.paramStr, func(p, v string) bool { + utils.AssertEqual(t, true, n < len(tc.expectedParams), "Received more parameters than expected: "+p+"="+v) + utils.AssertEqual(t, tc.expectedParams[n][0], p, tc.description) + utils.AssertEqual(t, tc.expectedParams[n][1], v, tc.description) + n++ + + // Stop parsing at the first parameter called "end" + return p != "end" + }) + utils.AssertEqual(t, len(tc.expectedParams), n, tc.description+": number of parameters differs") + } + // Check that we exited on the second parameter (bar) +} + +// go test -v -run=^$ -bench=Benchmark_Utils_ForEachParameter -benchmem -count=4 +func Benchmark_Utils_ForEachParameter(b *testing.B) { + for n := 0; n < b.N; n++ { + forEachParameter(` ; josua=1 ; vermant="20tw\",bob;sack o" ; version=1; foo=bar; `, func(s1, s2 string) bool { + return true + }) + } +} + +func Test_Utils_ParamsMatch(t *testing.T) { + testCases := []struct { + description string + accept string + offer string + match bool + }{ + { + description: "empty accept and offer", + accept: "", + offer: "", + match: true, + }, + { + description: "accept is empty, offer has params", + accept: "", + offer: ";foo=bar", + match: true, + }, + { + description: "offer is empty, accept has params", + accept: ";foo=bar", + offer: "", + match: false, + }, + { + description: "accept has extra parameters", + accept: ";foo=bar;a=1", + offer: ";foo=bar", + match: false, + }, + { + description: "matches regardless of order", + accept: "; a=1; b=2", + offer: ";b=2;a=1", + match: true, + }, + { + description: "case insensitive", + accept: ";ParaM=FoO", + offer: ";pAram=foO", + match: true, + }, + { + description: "ignores q", + accept: ";q=0.42", + offer: "", + match: true, + }, + } + + for _, tc := range testCases { + utils.AssertEqual(t, tc.match, paramsMatch(tc.accept, tc.offer), tc.description) + } +} + +func Benchmark_Utils_ParamsMatch(b *testing.B) { + var match bool + for n := 0; n < b.N; n++ { + match = paramsMatch(`; appLe=orange; param="foo"`, `;param=foo; apple=orange`) + } + utils.AssertEqual(b, true, match) +} + +func Test_Utils_AcceptsOfferType(t *testing.T) { + testCases := []struct { + description string + spec string + specParams string + offerType string + accepts bool + }{ + { + description: "no params, matching", + spec: "application/json", + offerType: "application/json", + accepts: true, + }, + { + description: "no params, mismatch", + spec: "application/json", + offerType: "application/xml", + accepts: false, + }, + { + description: "params match", + spec: "application/json", + specParams: `; format=foo; version=1`, + offerType: "application/json;version=1;format=foo;q=0.1", + accepts: true, + }, + { + description: "spec has extra params", + spec: "text/html", + specParams: "; charset=utf-8", + offerType: "text/html", + accepts: false, + }, + { + description: "offer has extra params", + spec: "text/html", + offerType: "text/html;charset=utf-8", + accepts: true, + }, + { + description: "ignores optional whitespace", + spec: "application/json", + specParams: `;format=foo; version=1`, + offerType: "application/json; version=1 ; format=foo ", + accepts: true, + }, + { + description: "ignores optional whitespace", + spec: "application/json", + specParams: `;format="foo bar"; version=1`, + offerType: `application/json;version="1";format="foo bar"`, + accepts: true, + }, + } + for _, tc := range testCases { + accepts := acceptsOfferType(tc.spec, tc.offerType, tc.specParams) + utils.AssertEqual(t, tc.accepts, accepts, tc.description) + } +} + +func Test_Utils_GetSplicedStrList(t *testing.T) { + testCases := []struct { + description string + headerValue string + expectedList []string + }{ + { + description: "normal case", + headerValue: "gzip, deflate,br", + expectedList: []string{"gzip", "deflate", "br"}, + }, + { + description: "no matter the value", + headerValue: " gzip,deflate, br, zip", + expectedList: []string{"gzip", "deflate", "br", "zip"}, + }, + { + description: "headerValue is empty", + headerValue: "", + expectedList: nil, + }, + { + description: "has a comma without element", + headerValue: "gzip,", + expectedList: []string{"gzip", ""}, + }, + } + + for _, tc := range testCases { + t.Run(tc.description, func(t *testing.T) { + dst := make([]string, 10) + result := getSplicedStrList(tc.headerValue, dst) + utils.AssertEqual(t, tc.expectedList, result) + }) + } +} + +func Benchmark_Utils_GetSplicedStrList(b *testing.B) { + destination := make([]string, 5) + result := destination + const input = `deflate, gzip,br,brotli` + for n := 0; n < b.N; n++ { + result = getSplicedStrList(input, destination) + } + utils.AssertEqual(b, []string{"deflate", "gzip", "br", "brotli"}, result) +} + +func Test_Utils_SortAcceptedTypes(t *testing.T) { + t.Parallel() + acceptedTypes := []acceptedType{ + {spec: "text/html", quality: 1, specificity: 3, order: 0}, + {spec: "text/*", quality: 0.5, specificity: 2, order: 1}, + {spec: "*/*", quality: 0.1, specificity: 1, order: 2}, + {spec: "application/json", quality: 0.999, specificity: 3, order: 3}, + {spec: "application/xml", quality: 1, specificity: 3, order: 4}, + {spec: "application/pdf", quality: 1, specificity: 3, order: 5}, + {spec: "image/png", quality: 1, specificity: 3, order: 6}, + {spec: "image/jpeg", quality: 1, specificity: 3, order: 7}, + {spec: "image/*", quality: 1, specificity: 2, order: 8}, + {spec: "image/gif", quality: 1, specificity: 3, order: 9}, + {spec: "text/plain", quality: 1, specificity: 3, order: 10}, + {spec: "application/json", quality: 0.999, specificity: 3, params: ";a=1", order: 11}, + } + sortAcceptedTypes(&acceptedTypes) + utils.AssertEqual(t, acceptedTypes, []acceptedType{ + {spec: "text/html", quality: 1, specificity: 3, order: 0}, + {spec: "application/xml", quality: 1, specificity: 3, order: 4}, + {spec: "application/pdf", quality: 1, specificity: 3, order: 5}, + {spec: "image/png", quality: 1, specificity: 3, order: 6}, + {spec: "image/jpeg", quality: 1, specificity: 3, order: 7}, + {spec: "image/gif", quality: 1, specificity: 3, order: 9}, + {spec: "text/plain", quality: 1, specificity: 3, order: 10}, + {spec: "image/*", quality: 1, specificity: 2, order: 8}, + {spec: "application/json", quality: 0.999, specificity: 3, params: ";a=1", order: 11}, + {spec: "application/json", quality: 0.999, specificity: 3, order: 3}, + {spec: "text/*", quality: 0.5, specificity: 2, order: 1}, + {spec: "*/*", quality: 0.1, specificity: 1, order: 2}, + }) +} + +// go test -v -run=^$ -bench=Benchmark_Utils_SortAcceptedTypes_Sorted -benchmem -count=4 +func Benchmark_Utils_SortAcceptedTypes_Sorted(b *testing.B) { + acceptedTypes := make([]acceptedType, 3) + for n := 0; n < b.N; n++ { + acceptedTypes[0] = acceptedType{spec: "text/html", quality: 1, specificity: 1, order: 0} + acceptedTypes[1] = acceptedType{spec: "text/*", quality: 0.5, specificity: 1, order: 1} + acceptedTypes[2] = acceptedType{spec: "*/*", quality: 0.1, specificity: 1, order: 2} + sortAcceptedTypes(&acceptedTypes) + } + utils.AssertEqual(b, "text/html", acceptedTypes[0].spec) + utils.AssertEqual(b, "text/*", acceptedTypes[1].spec) + utils.AssertEqual(b, "*/*", acceptedTypes[2].spec) +} + +// go test -v -run=^$ -bench=Benchmark_Utils_SortAcceptedTypes_Unsorted -benchmem -count=4 +func Benchmark_Utils_SortAcceptedTypes_Unsorted(b *testing.B) { + acceptedTypes := make([]acceptedType, 11) + for n := 0; n < b.N; n++ { + acceptedTypes[0] = acceptedType{spec: "text/html", quality: 1, specificity: 3, order: 0} + acceptedTypes[1] = acceptedType{spec: "text/*", quality: 0.5, specificity: 2, order: 1} + acceptedTypes[2] = acceptedType{spec: "*/*", quality: 0.1, specificity: 1, order: 2} + acceptedTypes[3] = acceptedType{spec: "application/json", quality: 0.999, specificity: 3, order: 3} + acceptedTypes[4] = acceptedType{spec: "application/xml", quality: 1, specificity: 3, order: 4} + acceptedTypes[5] = acceptedType{spec: "application/pdf", quality: 1, specificity: 3, order: 5} + acceptedTypes[6] = acceptedType{spec: "image/png", quality: 1, specificity: 3, order: 6} + acceptedTypes[7] = acceptedType{spec: "image/jpeg", quality: 1, specificity: 3, order: 7} + acceptedTypes[8] = acceptedType{spec: "image/*", quality: 1, specificity: 2, order: 8} + acceptedTypes[9] = acceptedType{spec: "image/gif", quality: 1, specificity: 3, order: 9} + acceptedTypes[10] = acceptedType{spec: "text/plain", quality: 1, specificity: 3, order: 10} + sortAcceptedTypes(&acceptedTypes) + } + utils.AssertEqual(b, acceptedTypes, []acceptedType{ + {spec: "text/html", quality: 1, specificity: 3, order: 0}, + {spec: "application/xml", quality: 1, specificity: 3, order: 4}, + {spec: "application/pdf", quality: 1, specificity: 3, order: 5}, + {spec: "image/png", quality: 1, specificity: 3, order: 6}, + {spec: "image/jpeg", quality: 1, specificity: 3, order: 7}, + {spec: "image/gif", quality: 1, specificity: 3, order: 9}, + {spec: "text/plain", quality: 1, specificity: 3, order: 10}, + {spec: "image/*", quality: 1, specificity: 2, order: 8}, + {spec: "application/json", quality: 0.999, specificity: 3, order: 3}, + {spec: "text/*", quality: 0.5, specificity: 2, order: 1}, + {spec: "*/*", quality: 0.1, specificity: 1, order: 2}, + }) +} + +// go test -v -run=^$ -bench=Benchmark_App_ETag -benchmem -count=4 +func Benchmark_Utils_ETag(b *testing.B) { + app := New() + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + err := c.SendString("Hello, World!") + utils.AssertEqual(b, nil, err) + for n := 0; n < b.N; n++ { + setETag(c, false) + } + utils.AssertEqual(b, `"13-1831710635"`, string(c.Response().Header.Peek(HeaderETag))) +} + +// go test -v -run=Test_Utils_ETag_Weak -count=1 +func Test_Utils_ETag_Weak(t *testing.T) { + t.Parallel() + app := New() + t.Run("Set Weak", func(t *testing.T) { + t.Parallel() + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + err := c.SendString("Hello, World!") + utils.AssertEqual(t, nil, err) + setETag(c, true) + utils.AssertEqual(t, `W/"13-1831710635"`, string(c.Response().Header.Peek(HeaderETag))) + }) + + t.Run("Match Weak ETag", func(t *testing.T) { + t.Parallel() + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + err := c.SendString("Hello, World!") + utils.AssertEqual(t, nil, err) + c.Request().Header.Set(HeaderIfNoneMatch, `W/"13-1831710635"`) + setETag(c, true) + utils.AssertEqual(t, 304, c.Response().StatusCode()) + utils.AssertEqual(t, "", string(c.Response().Header.Peek(HeaderETag))) + utils.AssertEqual(t, "", string(c.Response().Body())) + }) + + t.Run("Not Match Weak ETag", func(t *testing.T) { + t.Parallel() + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + err := c.SendString("Hello, World!") + utils.AssertEqual(t, nil, err) + c.Request().Header.Set(HeaderIfNoneMatch, `W/"13-1831710635xx"`) + setETag(c, true) + utils.AssertEqual(t, `W/"13-1831710635"`, string(c.Response().Header.Peek(HeaderETag))) + }) +} + +func Test_Utils_UniqueRouteStack(t *testing.T) { + t.Parallel() + route1 := &Route{} + route2 := &Route{} + route3 := &Route{} + utils.AssertEqual( + t, + []*Route{ + route1, + route2, + route3, + }, + uniqueRouteStack([]*Route{ + route1, + route1, + route1, + route2, + route2, + route2, + route3, + route3, + route3, + route1, + route2, + route3, + }), + ) +} + +// go test -v -run=^$ -bench=Benchmark_App_ETag_Weak -benchmem -count=4 +func Benchmark_Utils_ETag_Weak(b *testing.B) { + app := New() + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + err := c.SendString("Hello, World!") + utils.AssertEqual(b, nil, err) + for n := 0; n < b.N; n++ { + setETag(c, true) + } + utils.AssertEqual(b, `W/"13-1831710635"`, string(c.Response().Header.Peek(HeaderETag))) +} + +func Test_Utils_getGroupPath(t *testing.T) { + t.Parallel() + res := getGroupPath("/v1", "/") + utils.AssertEqual(t, "/v1/", res) + + res = getGroupPath("/v1/", "/") + utils.AssertEqual(t, "/v1/", res) + + res = getGroupPath("/", "/") + utils.AssertEqual(t, "/", res) + + res = getGroupPath("/v1/api/", "/") + utils.AssertEqual(t, "/v1/api/", res) + + res = getGroupPath("/v1/api", "group") + utils.AssertEqual(t, "/v1/api/group", res) + + res = getGroupPath("/v1/api", "") + utils.AssertEqual(t, "/v1/api", res) +} + +// go test -v -run=^$ -bench=Benchmark_Utils_ -benchmem -count=3 + +func Benchmark_Utils_getGroupPath(b *testing.B) { + var res string + for n := 0; n < b.N; n++ { + _ = getGroupPath("/v1/long/path/john/doe", "/why/this/name/is/so/awesome") + _ = getGroupPath("/v1", "/") + _ = getGroupPath("/v1", "/api") + res = getGroupPath("/v1", "/api/register/:project") + } + utils.AssertEqual(b, "/v1/api/register/:project", res) +} + +func Benchmark_Utils_Unescape(b *testing.B) { + unescaped := "" + dst := make([]byte, 0) + + for n := 0; n < b.N; n++ { + source := "/cr%C3%A9er" + pathBytes := utils.UnsafeBytes(source) + pathBytes = fasthttp.AppendUnquotedArg(dst[:0], pathBytes) + unescaped = utils.UnsafeString(pathBytes) + } + + utils.AssertEqual(b, "/créer", unescaped) +} + +func Test_Utils_Parse_Address(t *testing.T) { + t.Parallel() + testCases := []struct { + addr, host, port string + }{ + {"[::1]:3000", "[::1]", "3000"}, + {"127.0.0.1:3000", "127.0.0.1", "3000"}, + {"/path/to/unix/socket", "/path/to/unix/socket", ""}, + } + + for _, c := range testCases { + host, port := parseAddr(c.addr) + utils.AssertEqual(t, c.host, host, "addr host") + utils.AssertEqual(t, c.port, port, "addr port") + } +} + +func Test_Utils_TestConn_Deadline(t *testing.T) { + t.Parallel() + conn := &testConn{} + utils.AssertEqual(t, nil, conn.SetDeadline(time.Time{})) + utils.AssertEqual(t, nil, conn.SetReadDeadline(time.Time{})) + utils.AssertEqual(t, nil, conn.SetWriteDeadline(time.Time{})) +} + +func Test_Utils_IsNoCache(t *testing.T) { + t.Parallel() + testCases := []struct { + string + bool + }{ + {"public", false}, + {"no-cache", true}, + {"public, no-cache, max-age=30", true}, + {"public,no-cache", true}, + {"public,no-cacheX", false}, + {"no-cache, public", true}, + {"Xno-cache, public", false}, + {"max-age=30, no-cache,public", true}, + } + + for _, c := range testCases { + ok := isNoCache(c.string) + utils.AssertEqual(t, c.bool, ok, + fmt.Sprintf("want %t, got isNoCache(%s)=%t", c.bool, c.string, ok)) + } +} + +// go test -v -run=^$ -bench=Benchmark_Utils_IsNoCache -benchmem -count=4 +func Benchmark_Utils_IsNoCache(b *testing.B) { + var ok bool + for i := 0; i < b.N; i++ { + _ = isNoCache("public") + _ = isNoCache("no-cache") + _ = isNoCache("public, no-cache, max-age=30") + _ = isNoCache("public,no-cache") + _ = isNoCache("no-cache, public") + ok = isNoCache("max-age=30, no-cache,public") + } + utils.AssertEqual(b, true, ok) +} + +// go test -v -run=^$ -bench=Benchmark_SlashRecognition -benchmem -count=4 +func Benchmark_SlashRecognition(b *testing.B) { + search := "wtf/1234" + var result bool + b.Run("indexBytes", func(b *testing.B) { + result = false + for i := 0; i < b.N; i++ { + if strings.IndexByte(search, slashDelimiter) != -1 { + result = true + } + } + utils.AssertEqual(b, true, result) + }) + b.Run("forEach", func(b *testing.B) { + result = false + c := int32(slashDelimiter) + for i := 0; i < b.N; i++ { + for _, b := range search { + if b == c { + result = true + break + } + } + } + utils.AssertEqual(b, true, result) + }) + b.Run("IndexRune", func(b *testing.B) { + result = false + c := int32(slashDelimiter) + for i := 0; i < b.N; i++ { + result = IndexRune(search, c) + } + utils.AssertEqual(b, true, result) + }) +} diff --git a/hooks.go b/hooks.go new file mode 100644 index 0000000..6b0b860 --- /dev/null +++ b/hooks.go @@ -0,0 +1,218 @@ +package fiber + +import ( + "github.com/gofiber/fiber/v2/log" +) + +// OnRouteHandler Handlers define a function to create hooks for Fiber. +type ( + OnRouteHandler = func(Route) error + OnNameHandler = OnRouteHandler + OnGroupHandler = func(Group) error + OnGroupNameHandler = OnGroupHandler + OnListenHandler = func(ListenData) error + OnShutdownHandler = func() error + OnForkHandler = func(int) error + OnMountHandler = func(*App) error +) + +// Hooks is a struct to use it with App. +type Hooks struct { + // Embed app + app *App + + // Hooks + onRoute []OnRouteHandler + onName []OnNameHandler + onGroup []OnGroupHandler + onGroupName []OnGroupNameHandler + onListen []OnListenHandler + onShutdown []OnShutdownHandler + onFork []OnForkHandler + onMount []OnMountHandler +} + +// ListenData is a struct to use it with OnListenHandler +type ListenData struct { + Host string + Port string + TLS bool +} + +func newHooks(app *App) *Hooks { + return &Hooks{ + app: app, + onRoute: make([]OnRouteHandler, 0), + onGroup: make([]OnGroupHandler, 0), + onGroupName: make([]OnGroupNameHandler, 0), + onName: make([]OnNameHandler, 0), + onListen: make([]OnListenHandler, 0), + onShutdown: make([]OnShutdownHandler, 0), + onFork: make([]OnForkHandler, 0), + onMount: make([]OnMountHandler, 0), + } +} + +// OnRoute is a hook to execute user functions on each route registeration. +// Also you can get route properties by route parameter. +func (h *Hooks) OnRoute(handler ...OnRouteHandler) { + h.app.mutex.Lock() + h.onRoute = append(h.onRoute, handler...) + h.app.mutex.Unlock() +} + +// OnName is a hook to execute user functions on each route naming. +// Also you can get route properties by route parameter. +// +// WARN: OnName only works with naming routes, not groups. +func (h *Hooks) OnName(handler ...OnNameHandler) { + h.app.mutex.Lock() + h.onName = append(h.onName, handler...) + h.app.mutex.Unlock() +} + +// OnGroup is a hook to execute user functions on each group registeration. +// Also you can get group properties by group parameter. +func (h *Hooks) OnGroup(handler ...OnGroupHandler) { + h.app.mutex.Lock() + h.onGroup = append(h.onGroup, handler...) + h.app.mutex.Unlock() +} + +// OnGroupName is a hook to execute user functions on each group naming. +// Also you can get group properties by group parameter. +// +// WARN: OnGroupName only works with naming groups, not routes. +func (h *Hooks) OnGroupName(handler ...OnGroupNameHandler) { + h.app.mutex.Lock() + h.onGroupName = append(h.onGroupName, handler...) + h.app.mutex.Unlock() +} + +// OnListen is a hook to execute user functions on Listen, ListenTLS, Listener. +func (h *Hooks) OnListen(handler ...OnListenHandler) { + h.app.mutex.Lock() + h.onListen = append(h.onListen, handler...) + h.app.mutex.Unlock() +} + +// OnShutdown is a hook to execute user functions after Shutdown. +func (h *Hooks) OnShutdown(handler ...OnShutdownHandler) { + h.app.mutex.Lock() + h.onShutdown = append(h.onShutdown, handler...) + h.app.mutex.Unlock() +} + +// OnFork is a hook to execute user function after fork process. +func (h *Hooks) OnFork(handler ...OnForkHandler) { + h.app.mutex.Lock() + h.onFork = append(h.onFork, handler...) + h.app.mutex.Unlock() +} + +// OnMount is a hook to execute user function after mounting process. +// The mount event is fired when sub-app is mounted on a parent app. The parent app is passed as a parameter. +// It works for app and group mounting. +func (h *Hooks) OnMount(handler ...OnMountHandler) { + h.app.mutex.Lock() + h.onMount = append(h.onMount, handler...) + h.app.mutex.Unlock() +} + +func (h *Hooks) executeOnRouteHooks(route Route) error { + // Check mounting + if h.app.mountFields.mountPath != "" { + route.path = h.app.mountFields.mountPath + route.path + route.Path = route.path + } + + for _, v := range h.onRoute { + if err := v(route); err != nil { + return err + } + } + + return nil +} + +func (h *Hooks) executeOnNameHooks(route Route) error { + // Check mounting + if h.app.mountFields.mountPath != "" { + route.path = h.app.mountFields.mountPath + route.path + route.Path = route.path + } + + for _, v := range h.onName { + if err := v(route); err != nil { + return err + } + } + + return nil +} + +func (h *Hooks) executeOnGroupHooks(group Group) error { + // Check mounting + if h.app.mountFields.mountPath != "" { + group.Prefix = h.app.mountFields.mountPath + group.Prefix + } + + for _, v := range h.onGroup { + if err := v(group); err != nil { + return err + } + } + + return nil +} + +func (h *Hooks) executeOnGroupNameHooks(group Group) error { + // Check mounting + if h.app.mountFields.mountPath != "" { + group.Prefix = h.app.mountFields.mountPath + group.Prefix + } + + for _, v := range h.onGroupName { + if err := v(group); err != nil { + return err + } + } + + return nil +} + +func (h *Hooks) executeOnListenHooks(listenData ListenData) error { + for _, v := range h.onListen { + if err := v(listenData); err != nil { + return err + } + } + + return nil +} + +func (h *Hooks) executeOnShutdownHooks() { + for _, v := range h.onShutdown { + if err := v(); err != nil { + log.Errorf("failed to call shutdown hook: %v", err) + } + } +} + +func (h *Hooks) executeOnForkHooks(pid int) { + for _, v := range h.onFork { + if err := v(pid); err != nil { + log.Errorf("failed to call fork hook: %v", err) + } + } +} + +func (h *Hooks) executeOnMountHooks(app *App) error { + for _, v := range h.onMount { + if err := v(app); err != nil { + return err + } + } + + return nil +} diff --git a/hooks_test.go b/hooks_test.go new file mode 100644 index 0000000..fb3cad2 --- /dev/null +++ b/hooks_test.go @@ -0,0 +1,288 @@ +package fiber + +import ( + "errors" + "fmt" + "testing" + "time" + + "github.com/gofiber/fiber/v2/utils" + + "github.com/valyala/bytebufferpool" +) + +func testSimpleHandler(c *Ctx) error { + return c.SendString("simple") +} + +func Test_Hook_OnRoute(t *testing.T) { + t.Parallel() + app := New() + + app.Hooks().OnRoute(func(r Route) error { + utils.AssertEqual(t, "", r.Name) + + return nil + }) + + app.Get("/", testSimpleHandler).Name("x") + + subApp := New() + subApp.Get("/test", testSimpleHandler) + + app.Mount("/sub", subApp) +} + +func Test_Hook_OnRoute_Mount(t *testing.T) { + t.Parallel() + app := New() + subApp := New() + app.Mount("/sub", subApp) + + subApp.Hooks().OnRoute(func(r Route) error { + utils.AssertEqual(t, "/sub/test", r.Path) + + return nil + }) + + app.Hooks().OnRoute(func(r Route) error { + utils.AssertEqual(t, "/", r.Path) + + return nil + }) + + app.Get("/", testSimpleHandler).Name("x") + subApp.Get("/test", testSimpleHandler) +} + +func Test_Hook_OnName(t *testing.T) { + t.Parallel() + app := New() + + buf := bytebufferpool.Get() + defer bytebufferpool.Put(buf) + + app.Hooks().OnName(func(r Route) error { + _, err := buf.WriteString(r.Name) + utils.AssertEqual(t, nil, err) + + return nil + }) + + app.Get("/", testSimpleHandler).Name("index") + + subApp := New() + subApp.Get("/test", testSimpleHandler) + subApp.Get("/test2", testSimpleHandler) + + app.Mount("/sub", subApp) + + utils.AssertEqual(t, "index", buf.String()) +} + +func Test_Hook_OnName_Error(t *testing.T) { + t.Parallel() + app := New() + defer func() { + if err := recover(); err != nil { + utils.AssertEqual(t, "unknown error", fmt.Sprintf("%v", err)) + } + }() + + app.Hooks().OnName(func(r Route) error { + return errors.New("unknown error") + }) + + app.Get("/", testSimpleHandler).Name("index") +} + +func Test_Hook_OnGroup(t *testing.T) { + t.Parallel() + app := New() + + buf := bytebufferpool.Get() + defer bytebufferpool.Put(buf) + + app.Hooks().OnGroup(func(g Group) error { + _, err := buf.WriteString(g.Prefix) + utils.AssertEqual(t, nil, err) + return nil + }) + + grp := app.Group("/x").Name("x.") + grp.Group("/a") + + utils.AssertEqual(t, "/x/x/a", buf.String()) +} + +func Test_Hook_OnGroup_Mount(t *testing.T) { + t.Parallel() + app := New() + micro := New() + micro.Mount("/john", app) + + app.Hooks().OnGroup(func(g Group) error { + utils.AssertEqual(t, "/john/v1", g.Prefix) + return nil + }) + + v1 := app.Group("/v1") + v1.Get("/doe", func(c *Ctx) error { + return c.SendStatus(StatusOK) + }) +} + +func Test_Hook_OnGroupName(t *testing.T) { + t.Parallel() + app := New() + + buf := bytebufferpool.Get() + defer bytebufferpool.Put(buf) + + buf2 := bytebufferpool.Get() + defer bytebufferpool.Put(buf2) + + app.Hooks().OnGroupName(func(g Group) error { + _, err := buf.WriteString(g.name) + utils.AssertEqual(t, nil, err) + + return nil + }) + + app.Hooks().OnName(func(r Route) error { + _, err := buf2.WriteString(r.Name) + utils.AssertEqual(t, nil, err) + + return nil + }) + + grp := app.Group("/x").Name("x.") + grp.Get("/test", testSimpleHandler).Name("test") + grp.Get("/test2", testSimpleHandler) + + utils.AssertEqual(t, "x.", buf.String()) + utils.AssertEqual(t, "x.test", buf2.String()) +} + +func Test_Hook_OnGroupName_Error(t *testing.T) { + t.Parallel() + app := New() + defer func() { + if err := recover(); err != nil { + utils.AssertEqual(t, "unknown error", fmt.Sprintf("%v", err)) + } + }() + + app.Hooks().OnGroupName(func(g Group) error { + return errors.New("unknown error") + }) + + grp := app.Group("/x").Name("x.") + grp.Get("/test", testSimpleHandler) +} + +func Test_Hook_OnShutdown(t *testing.T) { + t.Parallel() + app := New() + + buf := bytebufferpool.Get() + defer bytebufferpool.Put(buf) + + app.Hooks().OnShutdown(func() error { + _, err := buf.WriteString("shutdowning") + utils.AssertEqual(t, nil, err) + + return nil + }) + + utils.AssertEqual(t, nil, app.Shutdown()) + utils.AssertEqual(t, "shutdowning", buf.String()) +} + +func Test_Hook_OnListen(t *testing.T) { + t.Parallel() + app := New(Config{ + DisableStartupMessage: true, + }) + + buf := bytebufferpool.Get() + defer bytebufferpool.Put(buf) + + app.Hooks().OnListen(func(listenData ListenData) error { + _, err := buf.WriteString("ready") + utils.AssertEqual(t, nil, err) + + return nil + }) + + go func() { + time.Sleep(1000 * time.Millisecond) + utils.AssertEqual(t, nil, app.Shutdown()) + }() + utils.AssertEqual(t, nil, app.Listen(":9000")) + + utils.AssertEqual(t, "ready", buf.String()) +} + +func Test_Hook_OnListenPrefork(t *testing.T) { + t.Parallel() + app := New(Config{ + DisableStartupMessage: true, + Prefork: true, + }) + + buf := bytebufferpool.Get() + defer bytebufferpool.Put(buf) + + app.Hooks().OnListen(func(listenData ListenData) error { + _, err := buf.WriteString("ready") + utils.AssertEqual(t, nil, err) + + return nil + }) + + go func() { + time.Sleep(1000 * time.Millisecond) + utils.AssertEqual(t, nil, app.Shutdown()) + }() + utils.AssertEqual(t, nil, app.Listen(":9000")) + + utils.AssertEqual(t, "ready", buf.String()) +} + +func Test_Hook_OnHook(t *testing.T) { + app := New() + + // Reset test var + testPreforkMaster = true + testOnPrefork = true + + go func() { + time.Sleep(1000 * time.Millisecond) + utils.AssertEqual(t, nil, app.Shutdown()) + }() + + app.Hooks().OnFork(func(pid int) error { + utils.AssertEqual(t, 1, pid) + return nil + }) + + utils.AssertEqual(t, nil, app.prefork(NetworkTCP4, ":3000", nil)) +} + +func Test_Hook_OnMount(t *testing.T) { + t.Parallel() + app := New() + app.Get("/", testSimpleHandler).Name("x") + + subApp := New() + subApp.Get("/test", testSimpleHandler) + + subApp.Hooks().OnMount(func(parent *App) error { + utils.AssertEqual(t, parent.mountFields.mountPath, "") + + return nil + }) + + app.Mount("/sub", subApp) +} diff --git a/internal/go-ole/LICENSE b/internal/go-ole/LICENSE new file mode 100644 index 0000000..623ec06 --- /dev/null +++ b/internal/go-ole/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright © 2013-2017 Yasuhiro Matsumoto, + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the “Software”), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/internal/go-ole/com.go b/internal/go-ole/com.go new file mode 100644 index 0000000..afc58cf --- /dev/null +++ b/internal/go-ole/com.go @@ -0,0 +1,344 @@ +//go:build windows + +package ole + +import ( + "syscall" + "unicode/utf16" + "unsafe" +) + +var ( + procCoInitialize = modole32.NewProc("CoInitialize") + procCoInitializeEx = modole32.NewProc("CoInitializeEx") + procCoUninitialize = modole32.NewProc("CoUninitialize") + procCoCreateInstance = modole32.NewProc("CoCreateInstance") + procCoTaskMemFree = modole32.NewProc("CoTaskMemFree") + procCLSIDFromProgID = modole32.NewProc("CLSIDFromProgID") + procCLSIDFromString = modole32.NewProc("CLSIDFromString") + procStringFromCLSID = modole32.NewProc("StringFromCLSID") + procStringFromIID = modole32.NewProc("StringFromIID") + procIIDFromString = modole32.NewProc("IIDFromString") + procCoGetObject = modole32.NewProc("CoGetObject") + procGetUserDefaultLCID = modkernel32.NewProc("GetUserDefaultLCID") + procCopyMemory = modkernel32.NewProc("RtlMoveMemory") + procVariantInit = modoleaut32.NewProc("VariantInit") + procVariantClear = modoleaut32.NewProc("VariantClear") + procVariantTimeToSystemTime = modoleaut32.NewProc("VariantTimeToSystemTime") + procSysAllocString = modoleaut32.NewProc("SysAllocString") + procSysAllocStringLen = modoleaut32.NewProc("SysAllocStringLen") + procSysFreeString = modoleaut32.NewProc("SysFreeString") + procSysStringLen = modoleaut32.NewProc("SysStringLen") + procCreateDispTypeInfo = modoleaut32.NewProc("CreateDispTypeInfo") + procCreateStdDispatch = modoleaut32.NewProc("CreateStdDispatch") + procGetActiveObject = modoleaut32.NewProc("GetActiveObject") + + procGetMessageW = moduser32.NewProc("GetMessageW") + procDispatchMessageW = moduser32.NewProc("DispatchMessageW") +) + +// coInitialize initializes COM library on current thread. +// +// MSDN documentation suggests that this function should not be called. Call +// CoInitializeEx() instead. The reason has to do with threading and this +// function is only for single-threaded apartments. +// +// That said, most users of the library have gotten away with just this +// function. If you are experiencing threading issues, then use +// CoInitializeEx(). +func coInitialize() (err error) { + // http://msdn.microsoft.com/en-us/library/windows/desktop/ms678543(v=vs.85).aspx + // Suggests that no value should be passed to CoInitialized. + // Could just be Call() since the parameter is optional. <-- Needs testing to be sure. + hr, _, _ := procCoInitialize.Call(uintptr(0)) + if hr != 0 { + err = NewError(hr) + } + return +} + +// coInitializeEx initializes COM library with concurrency model. +func coInitializeEx(coinit uint32) (err error) { + // http://msdn.microsoft.com/en-us/library/windows/desktop/ms695279(v=vs.85).aspx + // Suggests that the first parameter is not only optional but should always be NULL. + hr, _, _ := procCoInitializeEx.Call(uintptr(0), uintptr(coinit)) + if hr != 0 { + err = NewError(hr) + } + return +} + +// CoInitialize initializes COM library on current thread. +// +// MSDN documentation suggests that this function should not be called. Call +// CoInitializeEx() instead. The reason has to do with threading and this +// function is only for single-threaded apartments. +// +// That said, most users of the library have gotten away with just this +// function. If you are experiencing threading issues, then use +// CoInitializeEx(). +func CoInitialize(p uintptr) (err error) { + // p is ignored and won't be used. + // Avoid any variable not used errors. + p = uintptr(0) + return coInitialize() +} + +// CoInitializeEx initializes COM library with concurrency model. +func CoInitializeEx(p uintptr, coinit uint32) (err error) { + // Avoid any variable not used errors. + p = uintptr(0) + return coInitializeEx(coinit) +} + +// CoUninitialize uninitializes COM Library. +func CoUninitialize() { + procCoUninitialize.Call() +} + +// CoTaskMemFree frees memory pointer. +func CoTaskMemFree(memptr uintptr) { + procCoTaskMemFree.Call(memptr) +} + +// CLSIDFromProgID retrieves Class Identifier with the given Program Identifier. +// +// The Programmatic Identifier must be registered, because it will be looked up +// in the Windows Registry. The registry entry has the following keys: CLSID, +// Insertable, Protocol and Shell +// (https://msdn.microsoft.com/en-us/library/dd542719(v=vs.85).aspx). +// +// programID identifies the class id with less precision and is not guaranteed +// to be unique. These are usually found in the registry under +// HKEY_LOCAL_MACHINE\SOFTWARE\Classes, usually with the format of +// "Program.Component.Version" with version being optional. +// +// CLSIDFromProgID in Windows API. +func CLSIDFromProgID(progId string) (clsid *GUID, err error) { + var guid GUID + lpszProgID := uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr(progId))) + hr, _, _ := procCLSIDFromProgID.Call(lpszProgID, uintptr(unsafe.Pointer(&guid))) + if hr != 0 { + err = NewError(hr) + } + clsid = &guid + return +} + +// CLSIDFromString retrieves Class ID from string representation. +// +// This is technically the string version of the GUID and will convert the +// string to object. +// +// CLSIDFromString in Windows API. +func CLSIDFromString(str string) (clsid *GUID, err error) { + var guid GUID + lpsz := uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr(str))) + hr, _, _ := procCLSIDFromString.Call(lpsz, uintptr(unsafe.Pointer(&guid))) + if hr != 0 { + err = NewError(hr) + } + clsid = &guid + return +} + +// StringFromCLSID returns GUID formated string from GUID object. +func StringFromCLSID(clsid *GUID) (str string, err error) { + var p *uint16 + hr, _, _ := procStringFromCLSID.Call(uintptr(unsafe.Pointer(clsid)), uintptr(unsafe.Pointer(&p))) + if hr != 0 { + err = NewError(hr) + } + str = LpOleStrToString(p) + return +} + +// IIDFromString returns GUID from program ID. +func IIDFromString(progId string) (clsid *GUID, err error) { + var guid GUID + lpsz := uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr(progId))) + hr, _, _ := procIIDFromString.Call(lpsz, uintptr(unsafe.Pointer(&guid))) + if hr != 0 { + err = NewError(hr) + } + clsid = &guid + return +} + +// StringFromIID returns GUID formatted string from GUID object. +func StringFromIID(iid *GUID) (str string, err error) { + var p *uint16 + hr, _, _ := procStringFromIID.Call(uintptr(unsafe.Pointer(iid)), uintptr(unsafe.Pointer(&p))) + if hr != 0 { + err = NewError(hr) + } + str = LpOleStrToString(p) + return +} + +// CreateInstance of single uninitialized object with GUID. +func CreateInstance(clsid, iid *GUID) (unk *IUnknown, err error) { + if iid == nil { + iid = IID_IUnknown + } + hr, _, _ := procCoCreateInstance.Call( + uintptr(unsafe.Pointer(clsid)), + 0, + CLSCTX_SERVER, + uintptr(unsafe.Pointer(iid)), + uintptr(unsafe.Pointer(&unk))) + if hr != 0 { + err = NewError(hr) + } + return +} + +// GetActiveObject retrieves pointer to active object. +func GetActiveObject(clsid, iid *GUID) (unk *IUnknown, err error) { + if iid == nil { + iid = IID_IUnknown + } + hr, _, _ := procGetActiveObject.Call( + uintptr(unsafe.Pointer(clsid)), + uintptr(unsafe.Pointer(iid)), + uintptr(unsafe.Pointer(&unk))) + if hr != 0 { + err = NewError(hr) + } + return +} + +type BindOpts struct { + CbStruct uint32 + GrfFlags uint32 + GrfMode uint32 + TickCountDeadline uint32 +} + +// GetObject retrieves pointer to active object. +func GetObject(programID string, bindOpts *BindOpts, iid *GUID) (unk *IUnknown, err error) { + if bindOpts != nil { + bindOpts.CbStruct = uint32(unsafe.Sizeof(BindOpts{})) + } + if iid == nil { + iid = IID_IUnknown + } + hr, _, _ := procCoGetObject.Call( + uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr(programID))), + uintptr(unsafe.Pointer(bindOpts)), + uintptr(unsafe.Pointer(iid)), + uintptr(unsafe.Pointer(&unk))) + if hr != 0 { + err = NewError(hr) + } + return +} + +// VariantInit initializes variant. +func VariantInit(v *VARIANT) (err error) { + hr, _, _ := procVariantInit.Call(uintptr(unsafe.Pointer(v))) + if hr != 0 { + err = NewError(hr) + } + return +} + +// VariantClear clears value in Variant settings to VT_EMPTY. +func VariantClear(v *VARIANT) (err error) { + hr, _, _ := procVariantClear.Call(uintptr(unsafe.Pointer(v))) + if hr != 0 { + err = NewError(hr) + } + return +} + +// SysAllocString allocates memory for string and copies string into memory. +func SysAllocString(v string) (ss *int16) { + pss, _, _ := procSysAllocString.Call(uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr(v)))) + ss = (*int16)(unsafe.Pointer(pss)) + return +} + +// SysAllocStringLen copies up to length of given string returning pointer. +func SysAllocStringLen(v string) (ss *int16) { + utf16 := utf16.Encode([]rune(v + "\x00")) + ptr := &utf16[0] + + pss, _, _ := procSysAllocStringLen.Call(uintptr(unsafe.Pointer(ptr)), uintptr(len(utf16)-1)) + ss = (*int16)(unsafe.Pointer(pss)) + return +} + +// SysFreeString frees string system memory. This must be called with SysAllocString. +func SysFreeString(v *int16) (err error) { + hr, _, _ := procSysFreeString.Call(uintptr(unsafe.Pointer(v))) + if hr != 0 { + err = NewError(hr) + } + return +} + +// SysStringLen is the length of the system allocated string. +func SysStringLen(v *int16) uint32 { + l, _, _ := procSysStringLen.Call(uintptr(unsafe.Pointer(v))) + return uint32(l) +} + +// CreateStdDispatch provides default IDispatch implementation for IUnknown. +// +// This handles default IDispatch implementation for objects. It haves a few +// limitations with only supporting one language. It will also only return +// default exception codes. +func CreateStdDispatch(unk *IUnknown, v uintptr, ptinfo *IUnknown) (disp *IDispatch, err error) { + hr, _, _ := procCreateStdDispatch.Call( + uintptr(unsafe.Pointer(unk)), + v, + uintptr(unsafe.Pointer(ptinfo)), + uintptr(unsafe.Pointer(&disp))) + if hr != 0 { + err = NewError(hr) + } + return +} + +// CreateDispTypeInfo provides default ITypeInfo implementation for IDispatch. +// +// This will not handle the full implementation of the interface. +func CreateDispTypeInfo(idata *INTERFACEDATA) (pptinfo *IUnknown, err error) { + hr, _, _ := procCreateDispTypeInfo.Call( + uintptr(unsafe.Pointer(idata)), + uintptr(GetUserDefaultLCID()), + uintptr(unsafe.Pointer(&pptinfo))) + if hr != 0 { + err = NewError(hr) + } + return +} + +// copyMemory moves location of a block of memory. +func copyMemory(dest, src unsafe.Pointer, length uint32) { + procCopyMemory.Call(uintptr(dest), uintptr(src), uintptr(length)) +} + +// GetUserDefaultLCID retrieves current user default locale. +func GetUserDefaultLCID() (lcid uint32) { + ret, _, _ := procGetUserDefaultLCID.Call() + lcid = uint32(ret) + return +} + +// GetMessage in message queue from runtime. +// +// This function appears to block. PeekMessage does not block. +func GetMessage(msg *Msg, hwnd, MsgFilterMin, MsgFilterMax uint32) (ret int32, err error) { + r0, _, err := procGetMessageW.Call(uintptr(unsafe.Pointer(msg)), uintptr(hwnd), uintptr(MsgFilterMin), uintptr(MsgFilterMax)) + ret = int32(r0) + return +} + +// DispatchMessage to window procedure. +func DispatchMessage(msg *Msg) (ret int32) { + r0, _, _ := procDispatchMessageW.Call(uintptr(unsafe.Pointer(msg))) + ret = int32(r0) + return +} diff --git a/internal/go-ole/com_func.go b/internal/go-ole/com_func.go new file mode 100644 index 0000000..cc5f7b8 --- /dev/null +++ b/internal/go-ole/com_func.go @@ -0,0 +1,174 @@ +//go:build !windows + +package ole + +import ( + "time" + "unsafe" +) + +// coInitialize initializes COM library on current thread. +// +// MSDN documentation suggests that this function should not be called. Call +// CoInitializeEx() instead. The reason has to do with threading and this +// function is only for single-threaded apartments. +// +// That said, most users of the library have gotten away with just this +// function. If you are experiencing threading issues, then use +// CoInitializeEx(). +func coInitialize() error { + return NewError(E_NOTIMPL) +} + +// coInitializeEx initializes COM library with concurrency model. +func coInitializeEx(coinit uint32) error { + return NewError(E_NOTIMPL) +} + +// CoInitialize initializes COM library on current thread. +// +// MSDN documentation suggests that this function should not be called. Call +// CoInitializeEx() instead. The reason has to do with threading and this +// function is only for single-threaded apartments. +// +// That said, most users of the library have gotten away with just this +// function. If you are experiencing threading issues, then use +// CoInitializeEx(). +func CoInitialize(p uintptr) error { + return NewError(E_NOTIMPL) +} + +// CoInitializeEx initializes COM library with concurrency model. +func CoInitializeEx(p uintptr, coinit uint32) error { + return NewError(E_NOTIMPL) +} + +// CoUninitialize uninitializes COM Library. +func CoUninitialize() {} + +// CoTaskMemFree frees memory pointer. +func CoTaskMemFree(memptr uintptr) {} + +// CLSIDFromProgID retrieves Class Identifier with the given Program Identifier. +// +// The Programmatic Identifier must be registered, because it will be looked up +// in the Windows Registry. The registry entry has the following keys: CLSID, +// Insertable, Protocol and Shell +// (https://msdn.microsoft.com/en-us/library/dd542719(v=vs.85).aspx). +// +// programID identifies the class id with less precision and is not guaranteed +// to be unique. These are usually found in the registry under +// HKEY_LOCAL_MACHINE\SOFTWARE\Classes, usually with the format of +// "Program.Component.Version" with version being optional. +// +// CLSIDFromProgID in Windows API. +func CLSIDFromProgID(progId string) (*GUID, error) { + return nil, NewError(E_NOTIMPL) +} + +// CLSIDFromString retrieves Class ID from string representation. +// +// This is technically the string version of the GUID and will convert the +// string to object. +// +// CLSIDFromString in Windows API. +func CLSIDFromString(str string) (*GUID, error) { + return nil, NewError(E_NOTIMPL) +} + +// StringFromCLSID returns GUID formated string from GUID object. +func StringFromCLSID(clsid *GUID) (string, error) { + return "", NewError(E_NOTIMPL) +} + +// IIDFromString returns GUID from program ID. +func IIDFromString(progId string) (*GUID, error) { + return nil, NewError(E_NOTIMPL) +} + +// StringFromIID returns GUID formatted string from GUID object. +func StringFromIID(iid *GUID) (string, error) { + return "", NewError(E_NOTIMPL) +} + +// CreateInstance of single uninitialized object with GUID. +func CreateInstance(clsid, iid *GUID) (*IUnknown, error) { + return nil, NewError(E_NOTIMPL) +} + +// GetActiveObject retrieves pointer to active object. +func GetActiveObject(clsid, iid *GUID) (*IUnknown, error) { + return nil, NewError(E_NOTIMPL) +} + +// VariantInit initializes variant. +func VariantInit(v *VARIANT) error { + return NewError(E_NOTIMPL) +} + +// VariantClear clears value in Variant settings to VT_EMPTY. +func VariantClear(v *VARIANT) error { + return NewError(E_NOTIMPL) +} + +// SysAllocString allocates memory for string and copies string into memory. +func SysAllocString(v string) *int16 { + u := int16(0) + return &u +} + +// SysAllocStringLen copies up to length of given string returning pointer. +func SysAllocStringLen(v string) *int16 { + u := int16(0) + return &u +} + +// SysFreeString frees string system memory. This must be called with SysAllocString. +func SysFreeString(v *int16) error { + return NewError(E_NOTIMPL) +} + +// SysStringLen is the length of the system allocated string. +func SysStringLen(v *int16) uint32 { + return uint32(0) +} + +// CreateStdDispatch provides default IDispatch implementation for IUnknown. +// +// This handles default IDispatch implementation for objects. It haves a few +// limitations with only supporting one language. It will also only return +// default exception codes. +func CreateStdDispatch(unk *IUnknown, v uintptr, ptinfo *IUnknown) (*IDispatch, error) { + return nil, NewError(E_NOTIMPL) +} + +// CreateDispTypeInfo provides default ITypeInfo implementation for IDispatch. +// +// This will not handle the full implementation of the interface. +func CreateDispTypeInfo(idata *INTERFACEDATA) (*IUnknown, error) { + return nil, NewError(E_NOTIMPL) +} + +// copyMemory moves location of a block of memory. +func copyMemory(dest, src unsafe.Pointer, length uint32) {} + +// GetUserDefaultLCID retrieves current user default locale. +func GetUserDefaultLCID() uint32 { + return uint32(0) +} + +// GetMessage in message queue from runtime. +// +// This function appears to block. PeekMessage does not block. +func GetMessage(msg *Msg, hwnd, MsgFilterMin, MsgFilterMax uint32) (int32, error) { + return int32(0), NewError(E_NOTIMPL) +} + +// DispatchMessage to window procedure. +func DispatchMessage(msg *Msg) int32 { + return int32(0) +} + +func GetVariantDate(value uint64) (time.Time, error) { + return time.Now(), NewError(E_NOTIMPL) +} diff --git a/internal/go-ole/connect.go b/internal/go-ole/connect.go new file mode 100644 index 0000000..bca3a65 --- /dev/null +++ b/internal/go-ole/connect.go @@ -0,0 +1,192 @@ +package ole + +// Connection contains IUnknown for fluent interface interaction. +// +// Deprecated. Use oleutil package instead. +type Connection struct { + Object *IUnknown // Access COM +} + +// Initialize COM. +func (*Connection) Initialize() (err error) { + return coInitialize() +} + +// Uninitialize COM. +func (*Connection) Uninitialize() { + CoUninitialize() +} + +// Create IUnknown object based first on ProgId and then from String. +func (c *Connection) Create(progId string) (err error) { + var clsid *GUID + clsid, err = CLSIDFromProgID(progId) + if err != nil { + clsid, err = CLSIDFromString(progId) + if err != nil { + return + } + } + + unknown, err := CreateInstance(clsid, IID_IUnknown) + if err != nil { + return + } + c.Object = unknown + + return +} + +// Release IUnknown object. +func (c *Connection) Release() { + c.Object.Release() +} + +// Load COM object from list of programIDs or strings. +func (c *Connection) Load(names ...string) (errors []error) { + tempErrors := make([]error, len(names)) + numErrors := 0 + for _, name := range names { + err := c.Create(name) + if err != nil { + tempErrors = append(tempErrors, err) + numErrors += 1 + continue + } + break + } + + copy(errors, tempErrors[0:numErrors]) + return +} + +// Dispatch returns Dispatch object. +func (c *Connection) Dispatch() (object *Dispatch, err error) { + dispatch, err := c.Object.QueryInterface(IID_IDispatch) + if err != nil { + return + } + object = &Dispatch{dispatch} + return +} + +// Dispatch stores IDispatch object. +type Dispatch struct { + Object *IDispatch // Dispatch object. +} + +// Call method on IDispatch with parameters. +func (d *Dispatch) Call(method string, params ...interface{}) (result *VARIANT, err error) { + id, err := d.GetId(method) + if err != nil { + return + } + + result, err = d.Invoke(id, DISPATCH_METHOD, params) + return +} + +// MustCall method on IDispatch with parameters. +func (d *Dispatch) MustCall(method string, params ...interface{}) (result *VARIANT) { + id, err := d.GetId(method) + if err != nil { + panic(err) + } + + result, err = d.Invoke(id, DISPATCH_METHOD, params) + if err != nil { + panic(err) + } + + return +} + +// Get property on IDispatch with parameters. +func (d *Dispatch) Get(name string, params ...interface{}) (result *VARIANT, err error) { + id, err := d.GetId(name) + if err != nil { + return + } + result, err = d.Invoke(id, DISPATCH_PROPERTYGET, params) + return +} + +// MustGet property on IDispatch with parameters. +func (d *Dispatch) MustGet(name string, params ...interface{}) (result *VARIANT) { + id, err := d.GetId(name) + if err != nil { + panic(err) + } + + result, err = d.Invoke(id, DISPATCH_PROPERTYGET, params) + if err != nil { + panic(err) + } + return +} + +// Set property on IDispatch with parameters. +func (d *Dispatch) Set(name string, params ...interface{}) (result *VARIANT, err error) { + id, err := d.GetId(name) + if err != nil { + return + } + result, err = d.Invoke(id, DISPATCH_PROPERTYPUT, params) + return +} + +// MustSet property on IDispatch with parameters. +func (d *Dispatch) MustSet(name string, params ...interface{}) (result *VARIANT) { + id, err := d.GetId(name) + if err != nil { + panic(err) + } + + result, err = d.Invoke(id, DISPATCH_PROPERTYPUT, params) + if err != nil { + panic(err) + } + return +} + +// GetId retrieves ID of name on IDispatch. +func (d *Dispatch) GetId(name string) (id int32, err error) { + var dispid []int32 + dispid, err = d.Object.GetIDsOfName([]string{name}) + if err != nil { + return + } + id = dispid[0] + return +} + +// GetIds retrieves all IDs of names on IDispatch. +func (d *Dispatch) GetIds(names ...string) (dispid []int32, err error) { + dispid, err = d.Object.GetIDsOfName(names) + return +} + +// Invoke IDispatch on DisplayID of dispatch type with parameters. +// +// There have been problems where if send cascading params..., it would error +// out because the parameters would be empty. +func (d *Dispatch) Invoke(id int32, dispatch int16, params []interface{}) (result *VARIANT, err error) { + if len(params) < 1 { + result, err = d.Object.Invoke(id, dispatch) + } else { + result, err = d.Object.Invoke(id, dispatch, params...) + } + return +} + +// Release IDispatch object. +func (d *Dispatch) Release() { + d.Object.Release() +} + +// Connect initializes COM and attempts to load IUnknown based on given names. +func Connect(names ...string) (connection *Connection) { + connection.Initialize() + connection.Load(names...) + return +} diff --git a/internal/go-ole/constants.go b/internal/go-ole/constants.go new file mode 100644 index 0000000..fd0c6d7 --- /dev/null +++ b/internal/go-ole/constants.go @@ -0,0 +1,153 @@ +package ole + +const ( + CLSCTX_INPROC_SERVER = 1 + CLSCTX_INPROC_HANDLER = 2 + CLSCTX_LOCAL_SERVER = 4 + CLSCTX_INPROC_SERVER16 = 8 + CLSCTX_REMOTE_SERVER = 16 + CLSCTX_ALL = CLSCTX_INPROC_SERVER | CLSCTX_INPROC_HANDLER | CLSCTX_LOCAL_SERVER + CLSCTX_INPROC = CLSCTX_INPROC_SERVER | CLSCTX_INPROC_HANDLER + CLSCTX_SERVER = CLSCTX_INPROC_SERVER | CLSCTX_LOCAL_SERVER | CLSCTX_REMOTE_SERVER +) + +const ( + COINIT_APARTMENTTHREADED = 0x2 + COINIT_MULTITHREADED = 0x0 + COINIT_DISABLE_OLE1DDE = 0x4 + COINIT_SPEED_OVER_MEMORY = 0x8 +) + +const ( + DISPATCH_METHOD = 1 + DISPATCH_PROPERTYGET = 2 + DISPATCH_PROPERTYPUT = 4 + DISPATCH_PROPERTYPUTREF = 8 +) + +const ( + S_OK = 0x00000000 + E_UNEXPECTED = 0x8000FFFF + E_NOTIMPL = 0x80004001 + E_OUTOFMEMORY = 0x8007000E + E_INVALIDARG = 0x80070057 + E_NOINTERFACE = 0x80004002 + E_POINTER = 0x80004003 + E_HANDLE = 0x80070006 + E_ABORT = 0x80004004 + E_FAIL = 0x80004005 + E_ACCESSDENIED = 0x80070005 + E_PENDING = 0x8000000A + + CO_E_CLASSSTRING = 0x800401F3 +) + +const ( + CC_FASTCALL = iota + CC_CDECL + CC_MSCPASCAL + CC_PASCAL = CC_MSCPASCAL + CC_MACPASCAL + CC_STDCALL + CC_FPFASTCALL + CC_SYSCALL + CC_MPWCDECL + CC_MPWPASCAL + CC_MAX = CC_MPWPASCAL +) + +type VT uint16 + +const ( + VT_EMPTY VT = 0x0 + VT_NULL VT = 0x1 + VT_I2 VT = 0x2 + VT_I4 VT = 0x3 + VT_R4 VT = 0x4 + VT_R8 VT = 0x5 + VT_CY VT = 0x6 + VT_DATE VT = 0x7 + VT_BSTR VT = 0x8 + VT_DISPATCH VT = 0x9 + VT_ERROR VT = 0xa + VT_BOOL VT = 0xb + VT_VARIANT VT = 0xc + VT_UNKNOWN VT = 0xd + VT_DECIMAL VT = 0xe + VT_I1 VT = 0x10 + VT_UI1 VT = 0x11 + VT_UI2 VT = 0x12 + VT_UI4 VT = 0x13 + VT_I8 VT = 0x14 + VT_UI8 VT = 0x15 + VT_INT VT = 0x16 + VT_UINT VT = 0x17 + VT_VOID VT = 0x18 + VT_HRESULT VT = 0x19 + VT_PTR VT = 0x1a + VT_SAFEARRAY VT = 0x1b + VT_CARRAY VT = 0x1c + VT_USERDEFINED VT = 0x1d + VT_LPSTR VT = 0x1e + VT_LPWSTR VT = 0x1f + VT_RECORD VT = 0x24 + VT_INT_PTR VT = 0x25 + VT_UINT_PTR VT = 0x26 + VT_FILETIME VT = 0x40 + VT_BLOB VT = 0x41 + VT_STREAM VT = 0x42 + VT_STORAGE VT = 0x43 + VT_STREAMED_OBJECT VT = 0x44 + VT_STORED_OBJECT VT = 0x45 + VT_BLOB_OBJECT VT = 0x46 + VT_CF VT = 0x47 + VT_CLSID VT = 0x48 + VT_BSTR_BLOB VT = 0xfff + VT_VECTOR VT = 0x1000 + VT_ARRAY VT = 0x2000 + VT_BYREF VT = 0x4000 + VT_RESERVED VT = 0x8000 + VT_ILLEGAL VT = 0xffff + VT_ILLEGALMASKED VT = 0xfff + VT_TYPEMASK VT = 0xfff +) + +const ( + DISPID_UNKNOWN = -1 + DISPID_VALUE = 0 + DISPID_PROPERTYPUT = -3 + DISPID_NEWENUM = -4 + DISPID_EVALUATE = -5 + DISPID_CONSTRUCTOR = -6 + DISPID_DESTRUCTOR = -7 + DISPID_COLLECT = -8 +) + +const ( + TKIND_ENUM = 1 + TKIND_RECORD = 2 + TKIND_MODULE = 3 + TKIND_INTERFACE = 4 + TKIND_DISPATCH = 5 + TKIND_COCLASS = 6 + TKIND_ALIAS = 7 + TKIND_UNION = 8 + TKIND_MAX = 9 +) + +// Safe Array Feature Flags + +const ( + FADF_AUTO = 0x0001 + FADF_STATIC = 0x0002 + FADF_EMBEDDED = 0x0004 + FADF_FIXEDSIZE = 0x0010 + FADF_RECORD = 0x0020 + FADF_HAVEIID = 0x0040 + FADF_HAVEVARTYPE = 0x0080 + FADF_BSTR = 0x0100 + FADF_UNKNOWN = 0x0200 + FADF_DISPATCH = 0x0400 + FADF_VARIANT = 0x0800 + FADF_RESERVED = 0xF008 +) diff --git a/internal/go-ole/error.go b/internal/go-ole/error.go new file mode 100644 index 0000000..096b456 --- /dev/null +++ b/internal/go-ole/error.go @@ -0,0 +1,51 @@ +package ole + +// OleError stores COM errors. +type OleError struct { + hr uintptr + description string + subError error +} + +// NewError creates new error with HResult. +func NewError(hr uintptr) *OleError { + return &OleError{hr: hr} +} + +// NewErrorWithDescription creates new COM error with HResult and description. +func NewErrorWithDescription(hr uintptr, description string) *OleError { + return &OleError{hr: hr, description: description} +} + +// NewErrorWithSubError creates new COM error with parent error. +func NewErrorWithSubError(hr uintptr, description string, err error) *OleError { + return &OleError{hr: hr, description: description, subError: err} +} + +// Code is the HResult. +func (v *OleError) Code() uintptr { + return uintptr(v.hr) +} + +// String description, either manually set or format message with error code. +func (v *OleError) String() string { + if v.description != "" { + return errstr(int(v.hr)) + " (" + v.description + ")" + } + return errstr(int(v.hr)) +} + +// Error implements error interface. +func (v *OleError) Error() string { + return v.String() +} + +// Description retrieves error summary, if there is one. +func (v *OleError) Description() string { + return v.description +} + +// SubError returns parent error, if there is one. +func (v *OleError) SubError() error { + return v.subError +} diff --git a/internal/go-ole/error_func.go b/internal/go-ole/error_func.go new file mode 100644 index 0000000..f7ef6a1 --- /dev/null +++ b/internal/go-ole/error_func.go @@ -0,0 +1,8 @@ +//go:build !windows + +package ole + +// errstr converts error code to string. +func errstr(errno int) string { + return "" +} diff --git a/internal/go-ole/error_windows.go b/internal/go-ole/error_windows.go new file mode 100644 index 0000000..19a9dbe --- /dev/null +++ b/internal/go-ole/error_windows.go @@ -0,0 +1,24 @@ +//go:build windows + +package ole + +import ( + "fmt" + "syscall" + "unicode/utf16" +) + +// errstr converts error code to string. +func errstr(errno int) string { + // ask windows for the remaining errors + var flags uint32 = syscall.FORMAT_MESSAGE_FROM_SYSTEM | syscall.FORMAT_MESSAGE_ARGUMENT_ARRAY | syscall.FORMAT_MESSAGE_IGNORE_INSERTS + b := make([]uint16, 300) + n, err := syscall.FormatMessage(flags, 0, uint32(errno), 0, b, nil) + if err != nil { + return fmt.Sprintf("error %d (FormatMessage failed with: %v)", errno, err) + } + // trim terminating \r and \n + for ; n > 0 && (b[n-1] == '\n' || b[n-1] == '\r'); n-- { + } + return string(utf16.Decode(b[:n])) +} diff --git a/internal/go-ole/guid.go b/internal/go-ole/guid.go new file mode 100644 index 0000000..a3921cb --- /dev/null +++ b/internal/go-ole/guid.go @@ -0,0 +1,286 @@ +package ole + +var ( + // IID_NULL is null Interface ID, used when no other Interface ID is known. + IID_NULL = NewGUID("{00000000-0000-0000-0000-000000000000}") + + // IID_IUnknown is for IUnknown interfaces. + IID_IUnknown = NewGUID("{00000000-0000-0000-C000-000000000046}") + + // IID_IDispatch is for IDispatch interfaces. + IID_IDispatch = NewGUID("{00020400-0000-0000-C000-000000000046}") + + // IID_IEnumVariant is for IEnumVariant interfaces + IID_IEnumVariant = NewGUID("{00020404-0000-0000-C000-000000000046}") + + // IID_IConnectionPointContainer is for IConnectionPointContainer interfaces. + IID_IConnectionPointContainer = NewGUID("{B196B284-BAB4-101A-B69C-00AA00341D07}") + + // IID_IConnectionPoint is for IConnectionPoint interfaces. + IID_IConnectionPoint = NewGUID("{B196B286-BAB4-101A-B69C-00AA00341D07}") + + // IID_IInspectable is for IInspectable interfaces. + IID_IInspectable = NewGUID("{AF86E2E0-B12D-4C6A-9C5A-D7AA65101E90}") + + // IID_IProvideClassInfo is for IProvideClassInfo interfaces. + IID_IProvideClassInfo = NewGUID("{B196B283-BAB4-101A-B69C-00AA00341D07}") +) + +// These are for testing and not part of any library. +var ( + // IID_ICOMTestString is for ICOMTestString interfaces. + // + // {E0133EB4-C36F-469A-9D3D-C66B84BE19ED} + IID_ICOMTestString = NewGUID("{E0133EB4-C36F-469A-9D3D-C66B84BE19ED}") + + // IID_ICOMTestInt8 is for ICOMTestInt8 interfaces. + // + // {BEB06610-EB84-4155-AF58-E2BFF53680B4} + IID_ICOMTestInt8 = NewGUID("{BEB06610-EB84-4155-AF58-E2BFF53680B4}") + + // IID_ICOMTestInt16 is for ICOMTestInt16 interfaces. + // + // {DAA3F9FA-761E-4976-A860-8364CE55F6FC} + IID_ICOMTestInt16 = NewGUID("{DAA3F9FA-761E-4976-A860-8364CE55F6FC}") + + // IID_ICOMTestInt32 is for ICOMTestInt32 interfaces. + // + // {E3DEDEE7-38A2-4540-91D1-2EEF1D8891B0} + IID_ICOMTestInt32 = NewGUID("{E3DEDEE7-38A2-4540-91D1-2EEF1D8891B0}") + + // IID_ICOMTestInt64 is for ICOMTestInt64 interfaces. + // + // {8D437CBC-B3ED-485C-BC32-C336432A1623} + IID_ICOMTestInt64 = NewGUID("{8D437CBC-B3ED-485C-BC32-C336432A1623}") + + // IID_ICOMTestFloat is for ICOMTestFloat interfaces. + // + // {BF1ED004-EA02-456A-AA55-2AC8AC6B054C} + IID_ICOMTestFloat = NewGUID("{BF1ED004-EA02-456A-AA55-2AC8AC6B054C}") + + // IID_ICOMTestDouble is for ICOMTestDouble interfaces. + // + // {BF908A81-8687-4E93-999F-D86FAB284BA0} + IID_ICOMTestDouble = NewGUID("{BF908A81-8687-4E93-999F-D86FAB284BA0}") + + // IID_ICOMTestBoolean is for ICOMTestBoolean interfaces. + // + // {D530E7A6-4EE8-40D1-8931-3D63B8605010} + IID_ICOMTestBoolean = NewGUID("{D530E7A6-4EE8-40D1-8931-3D63B8605010}") + + // IID_ICOMEchoTestObject is for ICOMEchoTestObject interfaces. + // + // {6485B1EF-D780-4834-A4FE-1EBB51746CA3} + IID_ICOMEchoTestObject = NewGUID("{6485B1EF-D780-4834-A4FE-1EBB51746CA3}") + + // IID_ICOMTestTypes is for ICOMTestTypes interfaces. + // + // {CCA8D7AE-91C0-4277-A8B3-FF4EDF28D3C0} + IID_ICOMTestTypes = NewGUID("{CCA8D7AE-91C0-4277-A8B3-FF4EDF28D3C0}") + + // CLSID_COMEchoTestObject is for COMEchoTestObject class. + // + // {3C24506A-AE9E-4D50-9157-EF317281F1B0} + CLSID_COMEchoTestObject = NewGUID("{3C24506A-AE9E-4D50-9157-EF317281F1B0}") + + // CLSID_COMTestScalarClass is for COMTestScalarClass class. + // + // {865B85C5-0334-4AC6-9EF6-AACEC8FC5E86} + CLSID_COMTestScalarClass = NewGUID("{865B85C5-0334-4AC6-9EF6-AACEC8FC5E86}") +) + +const ( + hextable = "0123456789ABCDEF" + emptyGUID = "{00000000-0000-0000-0000-000000000000}" +) + +// GUID is Windows API specific GUID type. +// +// This exists to match Windows GUID type for direct passing for COM. +// Format is in xxxxxxxx-xxxx-xxxx-xxxxxxxxxxxxxxxx. +type GUID struct { + Data1 uint32 + Data2 uint16 + Data3 uint16 + Data4 [8]byte +} + +// NewGUID converts the given string into a globally unique identifier that is +// compliant with the Windows API. +// +// The supplied string may be in any of these formats: +// +// XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX +// XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX +// {XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX} +// +// The conversion of the supplied string is not case-sensitive. +func NewGUID(guid string) *GUID { + d := []byte(guid) + var d1, d2, d3, d4a, d4b []byte + + switch len(d) { + case 38: + if d[0] != '{' || d[37] != '}' { + return nil + } + d = d[1:37] + fallthrough + case 36: + if d[8] != '-' || d[13] != '-' || d[18] != '-' || d[23] != '-' { + return nil + } + d1 = d[0:8] + d2 = d[9:13] + d3 = d[14:18] + d4a = d[19:23] + d4b = d[24:36] + case 32: + d1 = d[0:8] + d2 = d[8:12] + d3 = d[12:16] + d4a = d[16:20] + d4b = d[20:32] + default: + return nil + } + + var g GUID + var ok1, ok2, ok3, ok4 bool + g.Data1, ok1 = decodeHexUint32(d1) + g.Data2, ok2 = decodeHexUint16(d2) + g.Data3, ok3 = decodeHexUint16(d3) + g.Data4, ok4 = decodeHexByte64(d4a, d4b) + if ok1 && ok2 && ok3 && ok4 { + return &g + } + return nil +} + +func decodeHexUint32(src []byte) (value uint32, ok bool) { + var b1, b2, b3, b4 byte + var ok1, ok2, ok3, ok4 bool + b1, ok1 = decodeHexByte(src[0], src[1]) + b2, ok2 = decodeHexByte(src[2], src[3]) + b3, ok3 = decodeHexByte(src[4], src[5]) + b4, ok4 = decodeHexByte(src[6], src[7]) + value = (uint32(b1) << 24) | (uint32(b2) << 16) | (uint32(b3) << 8) | uint32(b4) + ok = ok1 && ok2 && ok3 && ok4 + return +} + +func decodeHexUint16(src []byte) (value uint16, ok bool) { + var b1, b2 byte + var ok1, ok2 bool + b1, ok1 = decodeHexByte(src[0], src[1]) + b2, ok2 = decodeHexByte(src[2], src[3]) + value = (uint16(b1) << 8) | uint16(b2) + ok = ok1 && ok2 + return +} + +func decodeHexByte64(s1, s2 []byte) (value [8]byte, ok bool) { + var ok1, ok2, ok3, ok4, ok5, ok6, ok7, ok8 bool + value[0], ok1 = decodeHexByte(s1[0], s1[1]) + value[1], ok2 = decodeHexByte(s1[2], s1[3]) + value[2], ok3 = decodeHexByte(s2[0], s2[1]) + value[3], ok4 = decodeHexByte(s2[2], s2[3]) + value[4], ok5 = decodeHexByte(s2[4], s2[5]) + value[5], ok6 = decodeHexByte(s2[6], s2[7]) + value[6], ok7 = decodeHexByte(s2[8], s2[9]) + value[7], ok8 = decodeHexByte(s2[10], s2[11]) + ok = ok1 && ok2 && ok3 && ok4 && ok5 && ok6 && ok7 && ok8 + return +} + +func decodeHexByte(c1, c2 byte) (value byte, ok bool) { + var n1, n2 byte + var ok1, ok2 bool + n1, ok1 = decodeHexChar(c1) + n2, ok2 = decodeHexChar(c2) + value = (n1 << 4) | n2 + ok = ok1 && ok2 + return +} + +func decodeHexChar(c byte) (byte, bool) { + switch { + case '0' <= c && c <= '9': + return c - '0', true + case 'a' <= c && c <= 'f': + return c - 'a' + 10, true + case 'A' <= c && c <= 'F': + return c - 'A' + 10, true + } + + return 0, false +} + +// String converts the GUID to string form. It will adhere to this pattern: +// +// {XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX} +// +// If the GUID is nil, the string representation of an empty GUID is returned: +// +// {00000000-0000-0000-0000-000000000000} +func (guid *GUID) String() string { + if guid == nil { + return emptyGUID + } + + var c [38]byte + c[0] = '{' + putUint32Hex(c[1:9], guid.Data1) + c[9] = '-' + putUint16Hex(c[10:14], guid.Data2) + c[14] = '-' + putUint16Hex(c[15:19], guid.Data3) + c[19] = '-' + putByteHex(c[20:24], guid.Data4[0:2]) + c[24] = '-' + putByteHex(c[25:37], guid.Data4[2:8]) + c[37] = '}' + return string(c[:]) +} + +func putUint32Hex(b []byte, v uint32) { + b[0] = hextable[byte(v>>24)>>4] + b[1] = hextable[byte(v>>24)&0x0f] + b[2] = hextable[byte(v>>16)>>4] + b[3] = hextable[byte(v>>16)&0x0f] + b[4] = hextable[byte(v>>8)>>4] + b[5] = hextable[byte(v>>8)&0x0f] + b[6] = hextable[byte(v)>>4] + b[7] = hextable[byte(v)&0x0f] +} + +func putUint16Hex(b []byte, v uint16) { + b[0] = hextable[byte(v>>8)>>4] + b[1] = hextable[byte(v>>8)&0x0f] + b[2] = hextable[byte(v)>>4] + b[3] = hextable[byte(v)&0x0f] +} + +func putByteHex(dst, src []byte) { + for i := 0; i < len(src); i++ { + dst[i*2] = hextable[src[i]>>4] + dst[i*2+1] = hextable[src[i]&0x0f] + } +} + +// IsEqualGUID compares two GUID. +// +// Not constant time comparison. +func IsEqualGUID(guid1, guid2 *GUID) bool { + return guid1.Data1 == guid2.Data1 && + guid1.Data2 == guid2.Data2 && + guid1.Data3 == guid2.Data3 && + guid1.Data4[0] == guid2.Data4[0] && + guid1.Data4[1] == guid2.Data4[1] && + guid1.Data4[2] == guid2.Data4[2] && + guid1.Data4[3] == guid2.Data4[3] && + guid1.Data4[4] == guid2.Data4[4] && + guid1.Data4[5] == guid2.Data4[5] && + guid1.Data4[6] == guid2.Data4[6] && + guid1.Data4[7] == guid2.Data4[7] +} diff --git a/internal/go-ole/iconnectionpoint.go b/internal/go-ole/iconnectionpoint.go new file mode 100644 index 0000000..9e6c49f --- /dev/null +++ b/internal/go-ole/iconnectionpoint.go @@ -0,0 +1,20 @@ +package ole + +import "unsafe" + +type IConnectionPoint struct { + IUnknown +} + +type IConnectionPointVtbl struct { + IUnknownVtbl + GetConnectionInterface uintptr + GetConnectionPointContainer uintptr + Advise uintptr + Unadvise uintptr + EnumConnections uintptr +} + +func (v *IConnectionPoint) VTable() *IConnectionPointVtbl { + return (*IConnectionPointVtbl)(unsafe.Pointer(v.RawVTable)) +} diff --git a/internal/go-ole/iconnectionpoint_func.go b/internal/go-ole/iconnectionpoint_func.go new file mode 100644 index 0000000..f71b905 --- /dev/null +++ b/internal/go-ole/iconnectionpoint_func.go @@ -0,0 +1,21 @@ +//go:build !windows + +package ole + +import "unsafe" + +func (v *IConnectionPoint) GetConnectionInterface(piid **GUID) int32 { + return int32(0) +} + +func (v *IConnectionPoint) Advise(unknown *IUnknown) (uint32, error) { + return uint32(0), NewError(E_NOTIMPL) +} + +func (v *IConnectionPoint) Unadvise(cookie uint32) error { + return NewError(E_NOTIMPL) +} + +func (v *IConnectionPoint) EnumConnections(p *unsafe.Pointer) (err error) { + return NewError(E_NOTIMPL) +} diff --git a/internal/go-ole/iconnectionpoint_windows.go b/internal/go-ole/iconnectionpoint_windows.go new file mode 100644 index 0000000..5e0a01c --- /dev/null +++ b/internal/go-ole/iconnectionpoint_windows.go @@ -0,0 +1,43 @@ +//go:build windows + +package ole + +import ( + "syscall" + "unsafe" +) + +func (v *IConnectionPoint) GetConnectionInterface(piid **GUID) int32 { + // XXX: This doesn't look like it does what it's supposed to + return release((*IUnknown)(unsafe.Pointer(v))) +} + +func (v *IConnectionPoint) Advise(unknown *IUnknown) (cookie uint32, err error) { + hr, _, _ := syscall.Syscall( + v.VTable().Advise, + 3, + uintptr(unsafe.Pointer(v)), + uintptr(unsafe.Pointer(unknown)), + uintptr(unsafe.Pointer(&cookie))) + if hr != 0 { + err = NewError(hr) + } + return +} + +func (v *IConnectionPoint) Unadvise(cookie uint32) (err error) { + hr, _, _ := syscall.Syscall( + v.VTable().Unadvise, + 2, + uintptr(unsafe.Pointer(v)), + uintptr(cookie), + 0) + if hr != 0 { + err = NewError(hr) + } + return +} + +func (v *IConnectionPoint) EnumConnections(p *unsafe.Pointer) error { + return NewError(E_NOTIMPL) +} diff --git a/internal/go-ole/iconnectionpointcontainer.go b/internal/go-ole/iconnectionpointcontainer.go new file mode 100644 index 0000000..165860d --- /dev/null +++ b/internal/go-ole/iconnectionpointcontainer.go @@ -0,0 +1,17 @@ +package ole + +import "unsafe" + +type IConnectionPointContainer struct { + IUnknown +} + +type IConnectionPointContainerVtbl struct { + IUnknownVtbl + EnumConnectionPoints uintptr + FindConnectionPoint uintptr +} + +func (v *IConnectionPointContainer) VTable() *IConnectionPointContainerVtbl { + return (*IConnectionPointContainerVtbl)(unsafe.Pointer(v.RawVTable)) +} diff --git a/internal/go-ole/iconnectionpointcontainer_func.go b/internal/go-ole/iconnectionpointcontainer_func.go new file mode 100644 index 0000000..83f0864 --- /dev/null +++ b/internal/go-ole/iconnectionpointcontainer_func.go @@ -0,0 +1,11 @@ +//go:build !windows + +package ole + +func (v *IConnectionPointContainer) EnumConnectionPoints(points interface{}) error { + return NewError(E_NOTIMPL) +} + +func (v *IConnectionPointContainer) FindConnectionPoint(iid *GUID, point **IConnectionPoint) error { + return NewError(E_NOTIMPL) +} diff --git a/internal/go-ole/iconnectionpointcontainer_windows.go b/internal/go-ole/iconnectionpointcontainer_windows.go new file mode 100644 index 0000000..9e21dbe --- /dev/null +++ b/internal/go-ole/iconnectionpointcontainer_windows.go @@ -0,0 +1,25 @@ +//go:build windows + +package ole + +import ( + "syscall" + "unsafe" +) + +func (v *IConnectionPointContainer) EnumConnectionPoints(points interface{}) error { + return NewError(E_NOTIMPL) +} + +func (v *IConnectionPointContainer) FindConnectionPoint(iid *GUID, point **IConnectionPoint) (err error) { + hr, _, _ := syscall.Syscall( + v.VTable().FindConnectionPoint, + 3, + uintptr(unsafe.Pointer(v)), + uintptr(unsafe.Pointer(iid)), + uintptr(unsafe.Pointer(point))) + if hr != 0 { + err = NewError(hr) + } + return +} diff --git a/internal/go-ole/idispatch.go b/internal/go-ole/idispatch.go new file mode 100644 index 0000000..d4af124 --- /dev/null +++ b/internal/go-ole/idispatch.go @@ -0,0 +1,94 @@ +package ole + +import "unsafe" + +type IDispatch struct { + IUnknown +} + +type IDispatchVtbl struct { + IUnknownVtbl + GetTypeInfoCount uintptr + GetTypeInfo uintptr + GetIDsOfNames uintptr + Invoke uintptr +} + +func (v *IDispatch) VTable() *IDispatchVtbl { + return (*IDispatchVtbl)(unsafe.Pointer(v.RawVTable)) +} + +func (v *IDispatch) GetIDsOfName(names []string) (dispid []int32, err error) { + dispid, err = getIDsOfName(v, names) + return +} + +func (v *IDispatch) Invoke(dispid int32, dispatch int16, params ...interface{}) (result *VARIANT, err error) { + result, err = invoke(v, dispid, dispatch, params...) + return +} + +func (v *IDispatch) GetTypeInfoCount() (c uint32, err error) { + c, err = getTypeInfoCount(v) + return +} + +func (v *IDispatch) GetTypeInfo() (tinfo *ITypeInfo, err error) { + tinfo, err = getTypeInfo(v) + return +} + +// GetSingleIDOfName is a helper that returns single display ID for IDispatch name. +// +// This replaces the common pattern of attempting to get a single name from the list of available +// IDs. It gives the first ID, if it is available. +func (v *IDispatch) GetSingleIDOfName(name string) (displayID int32, err error) { + var displayIDs []int32 + displayIDs, err = v.GetIDsOfName([]string{name}) + if err != nil { + return + } + displayID = displayIDs[0] + return +} + +// InvokeWithOptionalArgs accepts arguments as an array, works like Invoke. +// +// Accepts name and will attempt to retrieve Display ID to pass to Invoke. +// +// Passing params as an array is a workaround that could be fixed in later versions of Go that +// prevent passing empty params. During testing it was discovered that this is an acceptable way of +// getting around not being able to pass params normally. +func (v *IDispatch) InvokeWithOptionalArgs(name string, dispatch int16, params []interface{}) (result *VARIANT, err error) { + displayID, err := v.GetSingleIDOfName(name) + if err != nil { + return + } + + if len(params) < 1 { + result, err = v.Invoke(displayID, dispatch) + } else { + result, err = v.Invoke(displayID, dispatch, params...) + } + + return +} + +// CallMethod invokes named function with arguments on object. +func (v *IDispatch) CallMethod(name string, params ...interface{}) (*VARIANT, error) { + return v.InvokeWithOptionalArgs(name, DISPATCH_METHOD, params) +} + +// GetProperty retrieves the property with the name with the ability to pass arguments. +// +// Most of the time you will not need to pass arguments as most objects do not allow for this +// feature. Or at least, should not allow for this feature. Some servers don't follow best practices +// and this is provided for those edge cases. +func (v *IDispatch) GetProperty(name string, params ...interface{}) (*VARIANT, error) { + return v.InvokeWithOptionalArgs(name, DISPATCH_PROPERTYGET, params) +} + +// PutProperty attempts to mutate a property in the object. +func (v *IDispatch) PutProperty(name string, params ...interface{}) (*VARIANT, error) { + return v.InvokeWithOptionalArgs(name, DISPATCH_PROPERTYPUT, params) +} diff --git a/internal/go-ole/idispatch_func.go b/internal/go-ole/idispatch_func.go new file mode 100644 index 0000000..c082074 --- /dev/null +++ b/internal/go-ole/idispatch_func.go @@ -0,0 +1,19 @@ +//go:build !windows + +package ole + +func getIDsOfName(disp *IDispatch, names []string) ([]int32, error) { + return []int32{}, NewError(E_NOTIMPL) +} + +func getTypeInfoCount(disp *IDispatch) (uint32, error) { + return uint32(0), NewError(E_NOTIMPL) +} + +func getTypeInfo(disp *IDispatch) (*ITypeInfo, error) { + return nil, NewError(E_NOTIMPL) +} + +func invoke(disp *IDispatch, dispid int32, dispatch int16, params ...interface{}) (*VARIANT, error) { + return nil, NewError(E_NOTIMPL) +} diff --git a/internal/go-ole/idispatch_windows.go b/internal/go-ole/idispatch_windows.go new file mode 100644 index 0000000..98a555b --- /dev/null +++ b/internal/go-ole/idispatch_windows.go @@ -0,0 +1,200 @@ +//go:build windows + +package ole + +import ( + "math/big" + "syscall" + "time" + "unsafe" +) + +func getIDsOfName(disp *IDispatch, names []string) (dispid []int32, err error) { + wnames := make([]*uint16, len(names)) + for i := 0; i < len(names); i++ { + wnames[i] = syscall.StringToUTF16Ptr(names[i]) + } + dispid = make([]int32, len(names)) + namelen := uint32(len(names)) + hr, _, _ := syscall.Syscall6( + disp.VTable().GetIDsOfNames, + 6, + uintptr(unsafe.Pointer(disp)), + uintptr(unsafe.Pointer(IID_NULL)), + uintptr(unsafe.Pointer(&wnames[0])), + uintptr(namelen), + uintptr(GetUserDefaultLCID()), + uintptr(unsafe.Pointer(&dispid[0]))) + if hr != 0 { + err = NewError(hr) + } + return +} + +func getTypeInfoCount(disp *IDispatch) (c uint32, err error) { + hr, _, _ := syscall.Syscall( + disp.VTable().GetTypeInfoCount, + 2, + uintptr(unsafe.Pointer(disp)), + uintptr(unsafe.Pointer(&c)), + 0) + if hr != 0 { + err = NewError(hr) + } + return +} + +func getTypeInfo(disp *IDispatch) (tinfo *ITypeInfo, err error) { + hr, _, _ := syscall.Syscall( + disp.VTable().GetTypeInfo, + 3, + uintptr(unsafe.Pointer(disp)), + uintptr(GetUserDefaultLCID()), + uintptr(unsafe.Pointer(&tinfo))) + if hr != 0 { + err = NewError(hr) + } + return +} + +func invoke(disp *IDispatch, dispid int32, dispatch int16, params ...interface{}) (result *VARIANT, err error) { + var dispparams DISPPARAMS + + if dispatch&DISPATCH_PROPERTYPUT != 0 { + dispnames := [1]int32{DISPID_PROPERTYPUT} + dispparams.rgdispidNamedArgs = uintptr(unsafe.Pointer(&dispnames[0])) + dispparams.cNamedArgs = 1 + } else if dispatch&DISPATCH_PROPERTYPUTREF != 0 { + dispnames := [1]int32{DISPID_PROPERTYPUT} + dispparams.rgdispidNamedArgs = uintptr(unsafe.Pointer(&dispnames[0])) + dispparams.cNamedArgs = 1 + } + var vargs []VARIANT + if len(params) > 0 { + vargs = make([]VARIANT, len(params)) + for i, v := range params { + // n := len(params)-i-1 + n := len(params) - i - 1 + VariantInit(&vargs[n]) + switch vv := v.(type) { + case bool: + if vv { + vargs[n] = NewVariant(VT_BOOL, 0xffff) + } else { + vargs[n] = NewVariant(VT_BOOL, 0) + } + case *bool: + vargs[n] = NewVariant(VT_BOOL|VT_BYREF, int64(uintptr(unsafe.Pointer(v.(*bool))))) + case uint8: + vargs[n] = NewVariant(VT_I1, int64(v.(uint8))) + case *uint8: + vargs[n] = NewVariant(VT_I1|VT_BYREF, int64(uintptr(unsafe.Pointer(v.(*uint8))))) + case int8: + vargs[n] = NewVariant(VT_I1, int64(v.(int8))) + case *int8: + vargs[n] = NewVariant(VT_I1|VT_BYREF, int64(uintptr(unsafe.Pointer(v.(*uint8))))) + case int16: + vargs[n] = NewVariant(VT_I2, int64(v.(int16))) + case *int16: + vargs[n] = NewVariant(VT_I2|VT_BYREF, int64(uintptr(unsafe.Pointer(v.(*int16))))) + case uint16: + vargs[n] = NewVariant(VT_UI2, int64(v.(uint16))) + case *uint16: + vargs[n] = NewVariant(VT_UI2|VT_BYREF, int64(uintptr(unsafe.Pointer(v.(*uint16))))) + case int32: + vargs[n] = NewVariant(VT_I4, int64(v.(int32))) + case *int32: + vargs[n] = NewVariant(VT_I4|VT_BYREF, int64(uintptr(unsafe.Pointer(v.(*int32))))) + case uint32: + vargs[n] = NewVariant(VT_UI4, int64(v.(uint32))) + case *uint32: + vargs[n] = NewVariant(VT_UI4|VT_BYREF, int64(uintptr(unsafe.Pointer(v.(*uint32))))) + case int64: + vargs[n] = NewVariant(VT_I8, int64(v.(int64))) + case *int64: + vargs[n] = NewVariant(VT_I8|VT_BYREF, int64(uintptr(unsafe.Pointer(v.(*int64))))) + case uint64: + vargs[n] = NewVariant(VT_UI8, int64(uintptr(v.(uint64)))) + case *uint64: + vargs[n] = NewVariant(VT_UI8|VT_BYREF, int64(uintptr(unsafe.Pointer(v.(*uint64))))) + case int: + vargs[n] = NewVariant(VT_I4, int64(v.(int))) + case *int: + vargs[n] = NewVariant(VT_I4|VT_BYREF, int64(uintptr(unsafe.Pointer(v.(*int))))) + case uint: + vargs[n] = NewVariant(VT_UI4, int64(v.(uint))) + case *uint: + vargs[n] = NewVariant(VT_UI4|VT_BYREF, int64(uintptr(unsafe.Pointer(v.(*uint))))) + case float32: + vargs[n] = NewVariant(VT_R4, *(*int64)(unsafe.Pointer(&vv))) + case *float32: + vargs[n] = NewVariant(VT_R4|VT_BYREF, int64(uintptr(unsafe.Pointer(v.(*float32))))) + case float64: + vargs[n] = NewVariant(VT_R8, *(*int64)(unsafe.Pointer(&vv))) + case *float64: + vargs[n] = NewVariant(VT_R8|VT_BYREF, int64(uintptr(unsafe.Pointer(v.(*float64))))) + case *big.Int: + vargs[n] = NewVariant(VT_DECIMAL, v.(*big.Int).Int64()) + case string: + vargs[n] = NewVariant(VT_BSTR, int64(uintptr(unsafe.Pointer(SysAllocStringLen(v.(string)))))) + case *string: + vargs[n] = NewVariant(VT_BSTR|VT_BYREF, int64(uintptr(unsafe.Pointer(v.(*string))))) + case time.Time: + s := vv.Format("2006-01-02 15:04:05") + vargs[n] = NewVariant(VT_BSTR, int64(uintptr(unsafe.Pointer(SysAllocStringLen(s))))) + case *time.Time: + s := vv.Format("2006-01-02 15:04:05") + vargs[n] = NewVariant(VT_BSTR|VT_BYREF, int64(uintptr(unsafe.Pointer(&s)))) + case *IDispatch: + vargs[n] = NewVariant(VT_DISPATCH, int64(uintptr(unsafe.Pointer(v.(*IDispatch))))) + case **IDispatch: + vargs[n] = NewVariant(VT_DISPATCH|VT_BYREF, int64(uintptr(unsafe.Pointer(v.(**IDispatch))))) + case nil: + vargs[n] = NewVariant(VT_NULL, 0) + case *VARIANT: + vargs[n] = NewVariant(VT_VARIANT|VT_BYREF, int64(uintptr(unsafe.Pointer(v.(*VARIANT))))) + case []byte: + safeByteArray := safeArrayFromByteSlice(v.([]byte)) + vargs[n] = NewVariant(VT_ARRAY|VT_UI1, int64(uintptr(unsafe.Pointer(safeByteArray)))) + defer VariantClear(&vargs[n]) + case []string: + safeByteArray := safeArrayFromStringSlice(v.([]string)) + vargs[n] = NewVariant(VT_ARRAY|VT_BSTR, int64(uintptr(unsafe.Pointer(safeByteArray)))) + defer VariantClear(&vargs[n]) + default: + panic("unknown type") + } + } + dispparams.rgvarg = uintptr(unsafe.Pointer(&vargs[0])) + dispparams.cArgs = uint32(len(params)) + } + + result = new(VARIANT) + var excepInfo EXCEPINFO + VariantInit(result) + hr, _, _ := syscall.Syscall9( + disp.VTable().Invoke, + 9, + uintptr(unsafe.Pointer(disp)), + uintptr(dispid), + uintptr(unsafe.Pointer(IID_NULL)), + uintptr(GetUserDefaultLCID()), + uintptr(dispatch), + uintptr(unsafe.Pointer(&dispparams)), + uintptr(unsafe.Pointer(result)), + uintptr(unsafe.Pointer(&excepInfo)), + 0) + if hr != 0 { + err = NewErrorWithSubError(hr, BstrToString(excepInfo.bstrDescription), excepInfo) + } + for i, varg := range vargs { + n := len(params) - i - 1 + if varg.VT == VT_BSTR && varg.Val != 0 { + SysFreeString(((*int16)(unsafe.Pointer(uintptr(varg.Val))))) + } + if varg.VT == (VT_BSTR|VT_BYREF) && varg.Val != 0 { + *(params[n].(*string)) = LpOleStrToString(*(**uint16)(unsafe.Pointer(uintptr(varg.Val)))) + } + } + return +} diff --git a/internal/go-ole/ienumvariant.go b/internal/go-ole/ienumvariant.go new file mode 100644 index 0000000..2433897 --- /dev/null +++ b/internal/go-ole/ienumvariant.go @@ -0,0 +1,19 @@ +package ole + +import "unsafe" + +type IEnumVARIANT struct { + IUnknown +} + +type IEnumVARIANTVtbl struct { + IUnknownVtbl + Next uintptr + Skip uintptr + Reset uintptr + Clone uintptr +} + +func (v *IEnumVARIANT) VTable() *IEnumVARIANTVtbl { + return (*IEnumVARIANTVtbl)(unsafe.Pointer(v.RawVTable)) +} diff --git a/internal/go-ole/ienumvariant_func.go b/internal/go-ole/ienumvariant_func.go new file mode 100644 index 0000000..b487d0f --- /dev/null +++ b/internal/go-ole/ienumvariant_func.go @@ -0,0 +1,19 @@ +//go:build !windows + +package ole + +func (enum *IEnumVARIANT) Clone() (*IEnumVARIANT, error) { + return nil, NewError(E_NOTIMPL) +} + +func (enum *IEnumVARIANT) Reset() error { + return NewError(E_NOTIMPL) +} + +func (enum *IEnumVARIANT) Skip(celt uint) error { + return NewError(E_NOTIMPL) +} + +func (enum *IEnumVARIANT) Next(celt uint) (VARIANT, uint, error) { + return NewVariant(VT_NULL, int64(0)), 0, NewError(E_NOTIMPL) +} diff --git a/internal/go-ole/ienumvariant_windows.go b/internal/go-ole/ienumvariant_windows.go new file mode 100644 index 0000000..3dc273e --- /dev/null +++ b/internal/go-ole/ienumvariant_windows.go @@ -0,0 +1,63 @@ +//go:build windows + +package ole + +import ( + "syscall" + "unsafe" +) + +func (enum *IEnumVARIANT) Clone() (cloned *IEnumVARIANT, err error) { + hr, _, _ := syscall.Syscall( + enum.VTable().Clone, + 2, + uintptr(unsafe.Pointer(enum)), + uintptr(unsafe.Pointer(&cloned)), + 0) + if hr != 0 { + err = NewError(hr) + } + return +} + +func (enum *IEnumVARIANT) Reset() (err error) { + hr, _, _ := syscall.Syscall( + enum.VTable().Reset, + 1, + uintptr(unsafe.Pointer(enum)), + 0, + 0) + if hr != 0 { + err = NewError(hr) + } + return +} + +func (enum *IEnumVARIANT) Skip(celt uint) (err error) { + hr, _, _ := syscall.Syscall( + enum.VTable().Skip, + 2, + uintptr(unsafe.Pointer(enum)), + uintptr(celt), + 0) + if hr != 0 { + err = NewError(hr) + } + return +} + +func (enum *IEnumVARIANT) Next(celt uint) (array VARIANT, length uint, err error) { + hr, _, _ := syscall.Syscall6( + enum.VTable().Next, + 4, + uintptr(unsafe.Pointer(enum)), + uintptr(celt), + uintptr(unsafe.Pointer(&array)), + uintptr(unsafe.Pointer(&length)), + 0, + 0) + if hr != 0 { + err = NewError(hr) + } + return +} diff --git a/internal/go-ole/iinspectable.go b/internal/go-ole/iinspectable.go new file mode 100644 index 0000000..f4a19e2 --- /dev/null +++ b/internal/go-ole/iinspectable.go @@ -0,0 +1,18 @@ +package ole + +import "unsafe" + +type IInspectable struct { + IUnknown +} + +type IInspectableVtbl struct { + IUnknownVtbl + GetIIds uintptr + GetRuntimeClassName uintptr + GetTrustLevel uintptr +} + +func (v *IInspectable) VTable() *IInspectableVtbl { + return (*IInspectableVtbl)(unsafe.Pointer(v.RawVTable)) +} diff --git a/internal/go-ole/iinspectable_func.go b/internal/go-ole/iinspectable_func.go new file mode 100644 index 0000000..49718ac --- /dev/null +++ b/internal/go-ole/iinspectable_func.go @@ -0,0 +1,15 @@ +//go:build !windows + +package ole + +func (v *IInspectable) GetIids() ([]*GUID, error) { + return []*GUID{}, NewError(E_NOTIMPL) +} + +func (v *IInspectable) GetRuntimeClassName() (string, error) { + return "", NewError(E_NOTIMPL) +} + +func (v *IInspectable) GetTrustLevel() (uint32, error) { + return uint32(0), NewError(E_NOTIMPL) +} diff --git a/internal/go-ole/iinspectable_windows.go b/internal/go-ole/iinspectable_windows.go new file mode 100644 index 0000000..bf98a36 --- /dev/null +++ b/internal/go-ole/iinspectable_windows.go @@ -0,0 +1,72 @@ +//go:build windows + +package ole + +import ( + "bytes" + "encoding/binary" + "reflect" + "syscall" + "unsafe" +) + +func (v *IInspectable) GetIids() (iids []*GUID, err error) { + var count uint32 + var array uintptr + hr, _, _ := syscall.Syscall( + v.VTable().GetIIds, + 3, + uintptr(unsafe.Pointer(v)), + uintptr(unsafe.Pointer(&count)), + uintptr(unsafe.Pointer(&array))) + if hr != 0 { + err = NewError(hr) + return + } + defer CoTaskMemFree(array) + + iids = make([]*GUID, count) + byteCount := count * uint32(unsafe.Sizeof(GUID{})) + slicehdr := reflect.SliceHeader{Data: array, Len: int(byteCount), Cap: int(byteCount)} + byteSlice := *(*[]byte)(unsafe.Pointer(&slicehdr)) + reader := bytes.NewReader(byteSlice) + for i := range iids { + guid := GUID{} + err = binary.Read(reader, binary.LittleEndian, &guid) + if err != nil { + return + } + iids[i] = &guid + } + return +} + +func (v *IInspectable) GetRuntimeClassName() (s string, err error) { + var hstring HString + hr, _, _ := syscall.Syscall( + v.VTable().GetRuntimeClassName, + 2, + uintptr(unsafe.Pointer(v)), + uintptr(unsafe.Pointer(&hstring)), + 0) + if hr != 0 { + err = NewError(hr) + return + } + s = hstring.String() + DeleteHString(hstring) + return +} + +func (v *IInspectable) GetTrustLevel() (level uint32, err error) { + hr, _, _ := syscall.Syscall( + v.VTable().GetTrustLevel, + 2, + uintptr(unsafe.Pointer(v)), + uintptr(unsafe.Pointer(&level)), + 0) + if hr != 0 { + err = NewError(hr) + } + return +} diff --git a/internal/go-ole/iprovideclassinfo.go b/internal/go-ole/iprovideclassinfo.go new file mode 100644 index 0000000..25f3a6f --- /dev/null +++ b/internal/go-ole/iprovideclassinfo.go @@ -0,0 +1,21 @@ +package ole + +import "unsafe" + +type IProvideClassInfo struct { + IUnknown +} + +type IProvideClassInfoVtbl struct { + IUnknownVtbl + GetClassInfo uintptr +} + +func (v *IProvideClassInfo) VTable() *IProvideClassInfoVtbl { + return (*IProvideClassInfoVtbl)(unsafe.Pointer(v.RawVTable)) +} + +func (v *IProvideClassInfo) GetClassInfo() (cinfo *ITypeInfo, err error) { + cinfo, err = getClassInfo(v) + return +} diff --git a/internal/go-ole/iprovideclassinfo_func.go b/internal/go-ole/iprovideclassinfo_func.go new file mode 100644 index 0000000..6f94dd8 --- /dev/null +++ b/internal/go-ole/iprovideclassinfo_func.go @@ -0,0 +1,7 @@ +//go:build !windows + +package ole + +func getClassInfo(disp *IProvideClassInfo) (tinfo *ITypeInfo, err error) { + return nil, NewError(E_NOTIMPL) +} diff --git a/internal/go-ole/iprovideclassinfo_windows.go b/internal/go-ole/iprovideclassinfo_windows.go new file mode 100644 index 0000000..aab9c5f --- /dev/null +++ b/internal/go-ole/iprovideclassinfo_windows.go @@ -0,0 +1,21 @@ +//go:build windows + +package ole + +import ( + "syscall" + "unsafe" +) + +func getClassInfo(disp *IProvideClassInfo) (tinfo *ITypeInfo, err error) { + hr, _, _ := syscall.Syscall( + disp.VTable().GetClassInfo, + 2, + uintptr(unsafe.Pointer(disp)), + uintptr(unsafe.Pointer(&tinfo)), + 0) + if hr != 0 { + err = NewError(hr) + } + return +} diff --git a/internal/go-ole/itypeinfo.go b/internal/go-ole/itypeinfo.go new file mode 100644 index 0000000..dd3c5e2 --- /dev/null +++ b/internal/go-ole/itypeinfo.go @@ -0,0 +1,34 @@ +package ole + +import "unsafe" + +type ITypeInfo struct { + IUnknown +} + +type ITypeInfoVtbl struct { + IUnknownVtbl + GetTypeAttr uintptr + GetTypeComp uintptr + GetFuncDesc uintptr + GetVarDesc uintptr + GetNames uintptr + GetRefTypeOfImplType uintptr + GetImplTypeFlags uintptr + GetIDsOfNames uintptr + Invoke uintptr + GetDocumentation uintptr + GetDllEntry uintptr + GetRefTypeInfo uintptr + AddressOfMember uintptr + CreateInstance uintptr + GetMops uintptr + GetContainingTypeLib uintptr + ReleaseTypeAttr uintptr + ReleaseFuncDesc uintptr + ReleaseVarDesc uintptr +} + +func (v *ITypeInfo) VTable() *ITypeInfoVtbl { + return (*ITypeInfoVtbl)(unsafe.Pointer(v.RawVTable)) +} diff --git a/internal/go-ole/itypeinfo_func.go b/internal/go-ole/itypeinfo_func.go new file mode 100644 index 0000000..c82cdb9 --- /dev/null +++ b/internal/go-ole/itypeinfo_func.go @@ -0,0 +1,7 @@ +//go:build !windows + +package ole + +func (v *ITypeInfo) GetTypeAttr() (*TYPEATTR, error) { + return nil, NewError(E_NOTIMPL) +} diff --git a/internal/go-ole/itypeinfo_windows.go b/internal/go-ole/itypeinfo_windows.go new file mode 100644 index 0000000..b7ac772 --- /dev/null +++ b/internal/go-ole/itypeinfo_windows.go @@ -0,0 +1,21 @@ +//go:build windows + +package ole + +import ( + "syscall" + "unsafe" +) + +func (v *ITypeInfo) GetTypeAttr() (tattr *TYPEATTR, err error) { + hr, _, _ := syscall.Syscall( + uintptr(v.VTable().GetTypeAttr), + 2, + uintptr(unsafe.Pointer(v)), + uintptr(unsafe.Pointer(&tattr)), + 0) + if hr != 0 { + err = NewError(hr) + } + return +} diff --git a/internal/go-ole/iunknown.go b/internal/go-ole/iunknown.go new file mode 100644 index 0000000..108f28e --- /dev/null +++ b/internal/go-ole/iunknown.go @@ -0,0 +1,57 @@ +package ole + +import "unsafe" + +type IUnknown struct { + RawVTable *interface{} +} + +type IUnknownVtbl struct { + QueryInterface uintptr + AddRef uintptr + Release uintptr +} + +type UnknownLike interface { + QueryInterface(iid *GUID) (disp *IDispatch, err error) + AddRef() int32 + Release() int32 +} + +func (v *IUnknown) VTable() *IUnknownVtbl { + return (*IUnknownVtbl)(unsafe.Pointer(v.RawVTable)) +} + +func (v *IUnknown) PutQueryInterface(interfaceID *GUID, obj interface{}) error { + return reflectQueryInterface(v, v.VTable().QueryInterface, interfaceID, obj) +} + +func (v *IUnknown) IDispatch(interfaceID *GUID) (dispatch *IDispatch, err error) { + err = v.PutQueryInterface(interfaceID, &dispatch) + return +} + +func (v *IUnknown) IEnumVARIANT(interfaceID *GUID) (enum *IEnumVARIANT, err error) { + err = v.PutQueryInterface(interfaceID, &enum) + return +} + +func (v *IUnknown) QueryInterface(iid *GUID) (*IDispatch, error) { + return queryInterface(v, iid) +} + +func (v *IUnknown) MustQueryInterface(iid *GUID) (disp *IDispatch) { + unk, err := queryInterface(v, iid) + if err != nil { + panic(err) + } + return unk +} + +func (v *IUnknown) AddRef() int32 { + return addRef(v) +} + +func (v *IUnknown) Release() int32 { + return release(v) +} diff --git a/internal/go-ole/iunknown_func.go b/internal/go-ole/iunknown_func.go new file mode 100644 index 0000000..8752630 --- /dev/null +++ b/internal/go-ole/iunknown_func.go @@ -0,0 +1,19 @@ +//go:build !windows + +package ole + +func reflectQueryInterface(self interface{}, method uintptr, interfaceID *GUID, obj interface{}) (err error) { + return NewError(E_NOTIMPL) +} + +func queryInterface(unk *IUnknown, iid *GUID) (disp *IDispatch, err error) { + return nil, NewError(E_NOTIMPL) +} + +func addRef(unk *IUnknown) int32 { + return 0 +} + +func release(unk *IUnknown) int32 { + return 0 +} diff --git a/internal/go-ole/iunknown_windows.go b/internal/go-ole/iunknown_windows.go new file mode 100644 index 0000000..e948d60 --- /dev/null +++ b/internal/go-ole/iunknown_windows.go @@ -0,0 +1,58 @@ +//go:build windows + +package ole + +import ( + "reflect" + "syscall" + "unsafe" +) + +func reflectQueryInterface(self interface{}, method uintptr, interfaceID *GUID, obj interface{}) (err error) { + selfValue := reflect.ValueOf(self).Elem() + objValue := reflect.ValueOf(obj).Elem() + + hr, _, _ := syscall.Syscall( + method, + 3, + selfValue.UnsafeAddr(), + uintptr(unsafe.Pointer(interfaceID)), + objValue.Addr().Pointer()) + if hr != 0 { + err = NewError(hr) + } + return +} + +func queryInterface(unk *IUnknown, iid *GUID) (disp *IDispatch, err error) { + hr, _, _ := syscall.Syscall( + unk.VTable().QueryInterface, + 3, + uintptr(unsafe.Pointer(unk)), + uintptr(unsafe.Pointer(iid)), + uintptr(unsafe.Pointer(&disp))) + if hr != 0 { + err = NewError(hr) + } + return +} + +func addRef(unk *IUnknown) int32 { + ret, _, _ := syscall.Syscall( + unk.VTable().AddRef, + 1, + uintptr(unsafe.Pointer(unk)), + 0, + 0) + return int32(ret) +} + +func release(unk *IUnknown) int32 { + ret, _, _ := syscall.Syscall( + unk.VTable().Release, + 1, + uintptr(unsafe.Pointer(unk)), + 0, + 0) + return int32(ret) +} diff --git a/internal/go-ole/ole.go b/internal/go-ole/ole.go new file mode 100644 index 0000000..e2ae4f4 --- /dev/null +++ b/internal/go-ole/ole.go @@ -0,0 +1,157 @@ +package ole + +import ( + "fmt" + "strings" +) + +// DISPPARAMS are the arguments that passed to methods or property. +type DISPPARAMS struct { + rgvarg uintptr + rgdispidNamedArgs uintptr + cArgs uint32 + cNamedArgs uint32 +} + +// EXCEPINFO defines exception info. +type EXCEPINFO struct { + wCode uint16 + wReserved uint16 + bstrSource *uint16 + bstrDescription *uint16 + bstrHelpFile *uint16 + dwHelpContext uint32 + pvReserved uintptr + pfnDeferredFillIn uintptr + scode uint32 +} + +// WCode return wCode in EXCEPINFO. +func (e EXCEPINFO) WCode() uint16 { + return e.wCode +} + +// SCODE return scode in EXCEPINFO. +func (e EXCEPINFO) SCODE() uint32 { + return e.scode +} + +// String convert EXCEPINFO to string. +func (e EXCEPINFO) String() string { + var src, desc, hlp string + if e.bstrSource == nil { + src = "" + } else { + src = BstrToString(e.bstrSource) + } + + if e.bstrDescription == nil { + desc = "" + } else { + desc = BstrToString(e.bstrDescription) + } + + if e.bstrHelpFile == nil { + hlp = "" + } else { + hlp = BstrToString(e.bstrHelpFile) + } + + return fmt.Sprintf( + "wCode: %#x, bstrSource: %v, bstrDescription: %v, bstrHelpFile: %v, dwHelpContext: %#x, scode: %#x", + e.wCode, src, desc, hlp, e.dwHelpContext, e.scode, + ) +} + +// Error implements error interface and returns error string. +func (e EXCEPINFO) Error() string { + if e.bstrDescription != nil { + return strings.TrimSpace(BstrToString(e.bstrDescription)) + } + + src := "Unknown" + if e.bstrSource != nil { + src = BstrToString(e.bstrSource) + } + + code := e.scode + if e.wCode != 0 { + code = uint32(e.wCode) + } + + return fmt.Sprintf("%v: %#x", src, code) +} + +// PARAMDATA defines parameter data type. +type PARAMDATA struct { + Name *int16 + Vt uint16 +} + +// METHODDATA defines method info. +type METHODDATA struct { + Name *uint16 + Data *PARAMDATA + Dispid int32 + Meth uint32 + CC int32 + CArgs uint32 + Flags uint16 + VtReturn uint32 +} + +// INTERFACEDATA defines interface info. +type INTERFACEDATA struct { + MethodData *METHODDATA + CMembers uint32 +} + +// Point is 2D vector type. +type Point struct { + X int32 + Y int32 +} + +// Msg is message between processes. +type Msg struct { + Hwnd uint32 + Message uint32 + Wparam int32 + Lparam int32 + Time uint32 + Pt Point +} + +// TYPEDESC defines data type. +type TYPEDESC struct { + Hreftype uint32 + VT uint16 +} + +// IDLDESC defines IDL info. +type IDLDESC struct { + DwReserved uint32 + WIDLFlags uint16 +} + +// TYPEATTR defines type info. +type TYPEATTR struct { + Guid GUID + Lcid uint32 + dwReserved uint32 + MemidConstructor int32 + MemidDestructor int32 + LpstrSchema *uint16 + CbSizeInstance uint32 + Typekind int32 + CFuncs uint16 + CVars uint16 + CImplTypes uint16 + CbSizeVft uint16 + CbAlignment uint16 + WTypeFlags uint16 + WMajorVerNum uint16 + WMinorVerNum uint16 + TdescAlias TYPEDESC + IdldescType IDLDESC +} diff --git a/internal/go-ole/oleutil/connection.go b/internal/go-ole/oleutil/connection.go new file mode 100644 index 0000000..c3e7d48 --- /dev/null +++ b/internal/go-ole/oleutil/connection.go @@ -0,0 +1,100 @@ +//go:build windows + +package oleutil + +import ( + "reflect" + "unsafe" + + ole "github.com/gofiber/fiber/v2/internal/go-ole" +) + +type stdDispatch struct { + lpVtbl *stdDispatchVtbl + ref int32 + iid *ole.GUID + iface interface{} + funcMap map[string]int32 +} + +type stdDispatchVtbl struct { + pQueryInterface uintptr + pAddRef uintptr + pRelease uintptr + pGetTypeInfoCount uintptr + pGetTypeInfo uintptr + pGetIDsOfNames uintptr + pInvoke uintptr +} + +func dispQueryInterface(this *ole.IUnknown, iid *ole.GUID, punk **ole.IUnknown) uint32 { + pthis := (*stdDispatch)(unsafe.Pointer(this)) + *punk = nil + if ole.IsEqualGUID(iid, ole.IID_IUnknown) || + ole.IsEqualGUID(iid, ole.IID_IDispatch) { + dispAddRef(this) + *punk = this + return ole.S_OK + } + if ole.IsEqualGUID(iid, pthis.iid) { + dispAddRef(this) + *punk = this + return ole.S_OK + } + return ole.E_NOINTERFACE +} + +func dispAddRef(this *ole.IUnknown) int32 { + pthis := (*stdDispatch)(unsafe.Pointer(this)) + pthis.ref++ + return pthis.ref +} + +func dispRelease(this *ole.IUnknown) int32 { + pthis := (*stdDispatch)(unsafe.Pointer(this)) + pthis.ref-- + return pthis.ref +} + +func dispGetIDsOfNames(this *ole.IUnknown, iid *ole.GUID, wnames []*uint16, namelen, lcid int, pdisp []int32) uintptr { + pthis := (*stdDispatch)(unsafe.Pointer(this)) + names := make([]string, len(wnames)) + for i := 0; i < len(names); i++ { + names[i] = ole.LpOleStrToString(wnames[i]) + } + for n := 0; n < namelen; n++ { + if id, ok := pthis.funcMap[names[n]]; ok { + pdisp[n] = id + } + } + return ole.S_OK +} + +func dispGetTypeInfoCount(pcount *int) uintptr { + if pcount != nil { + *pcount = 0 + } + return ole.S_OK +} + +func dispGetTypeInfo(ptypeif *uintptr) uintptr { + return ole.E_NOTIMPL +} + +func dispInvoke(this *ole.IDispatch, dispid int32, riid *ole.GUID, lcid int, flags int16, dispparams *ole.DISPPARAMS, result *ole.VARIANT, pexcepinfo *ole.EXCEPINFO, nerr *uint) uintptr { + pthis := (*stdDispatch)(unsafe.Pointer(this)) + found := "" + for name, id := range pthis.funcMap { + if id == dispid { + found = name + } + } + if found != "" { + rv := reflect.ValueOf(pthis.iface).Elem() + rm := rv.MethodByName(found) + rr := rm.Call([]reflect.Value{}) + println(len(rr)) + return ole.S_OK + } + return ole.E_NOTIMPL +} diff --git a/internal/go-ole/oleutil/connection_func.go b/internal/go-ole/oleutil/connection_func.go new file mode 100644 index 0000000..612a583 --- /dev/null +++ b/internal/go-ole/oleutil/connection_func.go @@ -0,0 +1,10 @@ +//go:build !windows + +package oleutil + +import ole "github.com/gofiber/fiber/v2/internal/go-ole" + +// ConnectObject creates a connection point between two services for communication. +func ConnectObject(disp *ole.IDispatch, iid *ole.GUID, idisp interface{}) (uint32, error) { + return 0, ole.NewError(ole.E_NOTIMPL) +} diff --git a/internal/go-ole/oleutil/connection_windows.go b/internal/go-ole/oleutil/connection_windows.go new file mode 100644 index 0000000..0f93ba3 --- /dev/null +++ b/internal/go-ole/oleutil/connection_windows.go @@ -0,0 +1,58 @@ +//go:build windows + +package oleutil + +import ( + "reflect" + "syscall" + "unsafe" + + ole "github.com/gofiber/fiber/v2/internal/go-ole" +) + +// ConnectObject creates a connection point between two services for communication. +func ConnectObject(disp *ole.IDispatch, iid *ole.GUID, idisp interface{}) (cookie uint32, err error) { + unknown, err := disp.QueryInterface(ole.IID_IConnectionPointContainer) + if err != nil { + return + } + + container := (*ole.IConnectionPointContainer)(unsafe.Pointer(unknown)) + var point *ole.IConnectionPoint + err = container.FindConnectionPoint(iid, &point) + if err != nil { + return + } + if edisp, ok := idisp.(*ole.IUnknown); ok { + cookie, err = point.Advise(edisp) + container.Release() + if err != nil { + return + } + } + rv := reflect.ValueOf(disp).Elem() + if rv.Type().Kind() == reflect.Struct { + dest := &stdDispatch{} + dest.lpVtbl = &stdDispatchVtbl{} + dest.lpVtbl.pQueryInterface = syscall.NewCallback(dispQueryInterface) + dest.lpVtbl.pAddRef = syscall.NewCallback(dispAddRef) + dest.lpVtbl.pRelease = syscall.NewCallback(dispRelease) + dest.lpVtbl.pGetTypeInfoCount = syscall.NewCallback(dispGetTypeInfoCount) + dest.lpVtbl.pGetTypeInfo = syscall.NewCallback(dispGetTypeInfo) + dest.lpVtbl.pGetIDsOfNames = syscall.NewCallback(dispGetIDsOfNames) + dest.lpVtbl.pInvoke = syscall.NewCallback(dispInvoke) + dest.iface = disp + dest.iid = iid + cookie, err = point.Advise((*ole.IUnknown)(unsafe.Pointer(dest))) + container.Release() + if err != nil { + point.Release() + return + } + return + } + + container.Release() + + return 0, ole.NewError(ole.E_INVALIDARG) +} diff --git a/internal/go-ole/oleutil/go-get.go b/internal/go-ole/oleutil/go-get.go new file mode 100644 index 0000000..af6a903 --- /dev/null +++ b/internal/go-ole/oleutil/go-get.go @@ -0,0 +1,6 @@ +// This file is here so go get succeeds as without it errors with: +// no buildable Go source files in ... +// +//go:build !windows + +package oleutil diff --git a/internal/go-ole/oleutil/oleutil.go b/internal/go-ole/oleutil/oleutil.go new file mode 100644 index 0000000..42dcad4 --- /dev/null +++ b/internal/go-ole/oleutil/oleutil.go @@ -0,0 +1,127 @@ +package oleutil + +import ole "github.com/gofiber/fiber/v2/internal/go-ole" + +// ClassIDFrom retrieves class ID whether given is program ID or application string. +func ClassIDFrom(programID string) (classID *ole.GUID, err error) { + return ole.ClassIDFrom(programID) +} + +// CreateObject creates object from programID based on interface type. +// +// Only supports IUnknown. +// +// Program ID can be either program ID or application string. +func CreateObject(programID string) (unknown *ole.IUnknown, err error) { + classID, err := ole.ClassIDFrom(programID) + if err != nil { + return + } + + unknown, err = ole.CreateInstance(classID, ole.IID_IUnknown) + if err != nil { + return + } + + return +} + +// GetActiveObject retrieves active object for program ID and interface ID based +// on interface type. +// +// Only supports IUnknown. +// +// Program ID can be either program ID or application string. +func GetActiveObject(programID string) (unknown *ole.IUnknown, err error) { + classID, err := ole.ClassIDFrom(programID) + if err != nil { + return + } + + unknown, err = ole.GetActiveObject(classID, ole.IID_IUnknown) + if err != nil { + return + } + + return +} + +// CallMethod calls method on IDispatch with parameters. +func CallMethod(disp *ole.IDispatch, name string, params ...interface{}) (result *ole.VARIANT, err error) { + return disp.InvokeWithOptionalArgs(name, ole.DISPATCH_METHOD, params) +} + +// MustCallMethod calls method on IDispatch with parameters or panics. +func MustCallMethod(disp *ole.IDispatch, name string, params ...interface{}) (result *ole.VARIANT) { + r, err := CallMethod(disp, name, params...) + if err != nil { + panic(err.Error()) + } + return r +} + +// GetProperty retrieves property from IDispatch. +func GetProperty(disp *ole.IDispatch, name string, params ...interface{}) (result *ole.VARIANT, err error) { + return disp.InvokeWithOptionalArgs(name, ole.DISPATCH_PROPERTYGET, params) +} + +// MustGetProperty retrieves property from IDispatch or panics. +func MustGetProperty(disp *ole.IDispatch, name string, params ...interface{}) (result *ole.VARIANT) { + r, err := GetProperty(disp, name, params...) + if err != nil { + panic(err.Error()) + } + return r +} + +// PutProperty mutates property. +func PutProperty(disp *ole.IDispatch, name string, params ...interface{}) (result *ole.VARIANT, err error) { + return disp.InvokeWithOptionalArgs(name, ole.DISPATCH_PROPERTYPUT, params) +} + +// MustPutProperty mutates property or panics. +func MustPutProperty(disp *ole.IDispatch, name string, params ...interface{}) (result *ole.VARIANT) { + r, err := PutProperty(disp, name, params...) + if err != nil { + panic(err.Error()) + } + return r +} + +// PutPropertyRef mutates property reference. +func PutPropertyRef(disp *ole.IDispatch, name string, params ...interface{}) (result *ole.VARIANT, err error) { + return disp.InvokeWithOptionalArgs(name, ole.DISPATCH_PROPERTYPUTREF, params) +} + +// MustPutPropertyRef mutates property reference or panics. +func MustPutPropertyRef(disp *ole.IDispatch, name string, params ...interface{}) (result *ole.VARIANT) { + r, err := PutPropertyRef(disp, name, params...) + if err != nil { + panic(err.Error()) + } + return r +} + +func ForEach(disp *ole.IDispatch, f func(v *ole.VARIANT) error) error { + newEnum, err := disp.GetProperty("_NewEnum") + if err != nil { + return err + } + defer newEnum.Clear() + + enum, err := newEnum.ToIUnknown().IEnumVARIANT(ole.IID_IEnumVariant) + if err != nil { + return err + } + defer enum.Release() + + for item, length, err := enum.Next(1); length > 0; item, length, err = enum.Next(1) { + if err != nil { + return err + } + if ferr := f(&item); ferr != nil { + return ferr + } + } + return nil +} diff --git a/internal/go-ole/safearray.go b/internal/go-ole/safearray.go new file mode 100644 index 0000000..a5201b5 --- /dev/null +++ b/internal/go-ole/safearray.go @@ -0,0 +1,27 @@ +// Package is meant to retrieve and process safe array data returned from COM. + +package ole + +// SafeArrayBound defines the SafeArray boundaries. +type SafeArrayBound struct { + Elements uint32 + LowerBound int32 +} + +// SafeArray is how COM handles arrays. +type SafeArray struct { + Dimensions uint16 + FeaturesFlag uint16 + ElementsSize uint32 + LocksAmount uint32 + Data uint32 + Bounds [16]byte +} + +// SAFEARRAY is obsolete, exists for backwards compatibility. +// Use SafeArray +type SAFEARRAY SafeArray + +// SAFEARRAYBOUND is obsolete, exists for backwards compatibility. +// Use SafeArrayBound +type SAFEARRAYBOUND SafeArrayBound diff --git a/internal/go-ole/safearray_func.go b/internal/go-ole/safearray_func.go new file mode 100644 index 0000000..c5bd363 --- /dev/null +++ b/internal/go-ole/safearray_func.go @@ -0,0 +1,211 @@ +//go:build !windows + +package ole + +import ( + "unsafe" +) + +// safeArrayAccessData returns raw array pointer. +// +// AKA: SafeArrayAccessData in Windows API. +func safeArrayAccessData(safearray *SafeArray) (uintptr, error) { + return uintptr(0), NewError(E_NOTIMPL) +} + +// safeArrayUnaccessData releases raw array. +// +// AKA: SafeArrayUnaccessData in Windows API. +func safeArrayUnaccessData(safearray *SafeArray) error { + return NewError(E_NOTIMPL) +} + +// safeArrayAllocData allocates SafeArray. +// +// AKA: SafeArrayAllocData in Windows API. +func safeArrayAllocData(safearray *SafeArray) error { + return NewError(E_NOTIMPL) +} + +// safeArrayAllocDescriptor allocates SafeArray. +// +// AKA: SafeArrayAllocDescriptor in Windows API. +func safeArrayAllocDescriptor(dimensions uint32) (*SafeArray, error) { + return nil, NewError(E_NOTIMPL) +} + +// safeArrayAllocDescriptorEx allocates SafeArray. +// +// AKA: SafeArrayAllocDescriptorEx in Windows API. +func safeArrayAllocDescriptorEx(variantType VT, dimensions uint32) (*SafeArray, error) { + return nil, NewError(E_NOTIMPL) +} + +// safeArrayCopy returns copy of SafeArray. +// +// AKA: SafeArrayCopy in Windows API. +func safeArrayCopy(original *SafeArray) (*SafeArray, error) { + return nil, NewError(E_NOTIMPL) +} + +// safeArrayCopyData duplicates SafeArray into another SafeArray object. +// +// AKA: SafeArrayCopyData in Windows API. +func safeArrayCopyData(original, duplicate *SafeArray) error { + return NewError(E_NOTIMPL) +} + +// safeArrayCreate creates SafeArray. +// +// AKA: SafeArrayCreate in Windows API. +func safeArrayCreate(variantType VT, dimensions uint32, bounds *SafeArrayBound) (*SafeArray, error) { + return nil, NewError(E_NOTIMPL) +} + +// safeArrayCreateEx creates SafeArray. +// +// AKA: SafeArrayCreateEx in Windows API. +func safeArrayCreateEx(variantType VT, dimensions uint32, bounds *SafeArrayBound, extra uintptr) (*SafeArray, error) { + return nil, NewError(E_NOTIMPL) +} + +// safeArrayCreateVector creates SafeArray. +// +// AKA: SafeArrayCreateVector in Windows API. +func safeArrayCreateVector(variantType VT, lowerBound int32, length uint32) (*SafeArray, error) { + return nil, NewError(E_NOTIMPL) +} + +// safeArrayCreateVectorEx creates SafeArray. +// +// AKA: SafeArrayCreateVectorEx in Windows API. +func safeArrayCreateVectorEx(variantType VT, lowerBound int32, length uint32, extra uintptr) (*SafeArray, error) { + return nil, NewError(E_NOTIMPL) +} + +// safeArrayDestroy destroys SafeArray object. +// +// AKA: SafeArrayDestroy in Windows API. +func safeArrayDestroy(safearray *SafeArray) error { + return NewError(E_NOTIMPL) +} + +// safeArrayDestroyData destroys SafeArray object. +// +// AKA: SafeArrayDestroyData in Windows API. +func safeArrayDestroyData(safearray *SafeArray) error { + return NewError(E_NOTIMPL) +} + +// safeArrayDestroyDescriptor destroys SafeArray object. +// +// AKA: SafeArrayDestroyDescriptor in Windows API. +func safeArrayDestroyDescriptor(safearray *SafeArray) error { + return NewError(E_NOTIMPL) +} + +// safeArrayGetDim is the amount of dimensions in the SafeArray. +// +// SafeArrays may have multiple dimensions. Meaning, it could be +// multidimensional array. +// +// AKA: SafeArrayGetDim in Windows API. +func safeArrayGetDim(safearray *SafeArray) (*uint32, error) { + u := uint32(0) + return &u, NewError(E_NOTIMPL) +} + +// safeArrayGetElementSize is the element size in bytes. +// +// AKA: SafeArrayGetElemsize in Windows API. +func safeArrayGetElementSize(safearray *SafeArray) (*uint32, error) { + u := uint32(0) + return &u, NewError(E_NOTIMPL) +} + +// safeArrayGetElement retrieves element at given index. +func safeArrayGetElement(safearray *SafeArray, index int32, pv unsafe.Pointer) error { + return NewError(E_NOTIMPL) +} + +// safeArrayGetElement retrieves element at given index and converts to string. +func safeArrayGetElementString(safearray *SafeArray, index int32) (string, error) { + return "", NewError(E_NOTIMPL) +} + +// safeArrayGetIID is the InterfaceID of the elements in the SafeArray. +// +// AKA: SafeArrayGetIID in Windows API. +func safeArrayGetIID(safearray *SafeArray) (*GUID, error) { + return nil, NewError(E_NOTIMPL) +} + +// safeArrayGetLBound returns lower bounds of SafeArray. +// +// SafeArrays may have multiple dimensions. Meaning, it could be +// multidimensional array. +// +// AKA: SafeArrayGetLBound in Windows API. +func safeArrayGetLBound(safearray *SafeArray, dimension uint32) (int32, error) { + return int32(0), NewError(E_NOTIMPL) +} + +// safeArrayGetUBound returns upper bounds of SafeArray. +// +// SafeArrays may have multiple dimensions. Meaning, it could be +// multidimensional array. +// +// AKA: SafeArrayGetUBound in Windows API. +func safeArrayGetUBound(safearray *SafeArray, dimension uint32) (int32, error) { + return int32(0), NewError(E_NOTIMPL) +} + +// safeArrayGetVartype returns data type of SafeArray. +// +// AKA: SafeArrayGetVartype in Windows API. +func safeArrayGetVartype(safearray *SafeArray) (uint16, error) { + return uint16(0), NewError(E_NOTIMPL) +} + +// safeArrayLock locks SafeArray for reading to modify SafeArray. +// +// This must be called during some calls to ensure that another process does not +// read or write to the SafeArray during editing. +// +// AKA: SafeArrayLock in Windows API. +func safeArrayLock(safearray *SafeArray) error { + return NewError(E_NOTIMPL) +} + +// safeArrayUnlock unlocks SafeArray for reading. +// +// AKA: SafeArrayUnlock in Windows API. +func safeArrayUnlock(safearray *SafeArray) error { + return NewError(E_NOTIMPL) +} + +// safeArrayPutElement stores the data element at the specified location in the +// array. +// +// AKA: SafeArrayPutElement in Windows API. +func safeArrayPutElement(safearray *SafeArray, index int64, element uintptr) error { + return NewError(E_NOTIMPL) +} + +// safeArrayGetRecordInfo accesses IRecordInfo info for custom types. +// +// AKA: SafeArrayGetRecordInfo in Windows API. +// +// XXX: Must implement IRecordInfo interface for this to return. +func safeArrayGetRecordInfo(safearray *SafeArray) (interface{}, error) { + return nil, NewError(E_NOTIMPL) +} + +// safeArraySetRecordInfo mutates IRecordInfo info for custom types. +// +// AKA: SafeArraySetRecordInfo in Windows API. +// +// XXX: Must implement IRecordInfo interface for this to return. +func safeArraySetRecordInfo(safearray *SafeArray, recordInfo interface{}) error { + return NewError(E_NOTIMPL) +} diff --git a/internal/go-ole/safearray_windows.go b/internal/go-ole/safearray_windows.go new file mode 100644 index 0000000..67f98e5 --- /dev/null +++ b/internal/go-ole/safearray_windows.go @@ -0,0 +1,337 @@ +//go:build windows + +package ole + +import ( + "unsafe" +) + +var ( + procSafeArrayAccessData = modoleaut32.NewProc("SafeArrayAccessData") + procSafeArrayAllocData = modoleaut32.NewProc("SafeArrayAllocData") + procSafeArrayAllocDescriptor = modoleaut32.NewProc("SafeArrayAllocDescriptor") + procSafeArrayAllocDescriptorEx = modoleaut32.NewProc("SafeArrayAllocDescriptorEx") + procSafeArrayCopy = modoleaut32.NewProc("SafeArrayCopy") + procSafeArrayCopyData = modoleaut32.NewProc("SafeArrayCopyData") + procSafeArrayCreate = modoleaut32.NewProc("SafeArrayCreate") + procSafeArrayCreateEx = modoleaut32.NewProc("SafeArrayCreateEx") + procSafeArrayCreateVector = modoleaut32.NewProc("SafeArrayCreateVector") + procSafeArrayCreateVectorEx = modoleaut32.NewProc("SafeArrayCreateVectorEx") + procSafeArrayDestroy = modoleaut32.NewProc("SafeArrayDestroy") + procSafeArrayDestroyData = modoleaut32.NewProc("SafeArrayDestroyData") + procSafeArrayDestroyDescriptor = modoleaut32.NewProc("SafeArrayDestroyDescriptor") + procSafeArrayGetDim = modoleaut32.NewProc("SafeArrayGetDim") + procSafeArrayGetElement = modoleaut32.NewProc("SafeArrayGetElement") + procSafeArrayGetElemsize = modoleaut32.NewProc("SafeArrayGetElemsize") + procSafeArrayGetIID = modoleaut32.NewProc("SafeArrayGetIID") + procSafeArrayGetLBound = modoleaut32.NewProc("SafeArrayGetLBound") + procSafeArrayGetUBound = modoleaut32.NewProc("SafeArrayGetUBound") + procSafeArrayGetVartype = modoleaut32.NewProc("SafeArrayGetVartype") + procSafeArrayLock = modoleaut32.NewProc("SafeArrayLock") + procSafeArrayPtrOfIndex = modoleaut32.NewProc("SafeArrayPtrOfIndex") + procSafeArrayUnaccessData = modoleaut32.NewProc("SafeArrayUnaccessData") + procSafeArrayUnlock = modoleaut32.NewProc("SafeArrayUnlock") + procSafeArrayPutElement = modoleaut32.NewProc("SafeArrayPutElement") + // procSafeArrayRedim = modoleaut32.NewProc("SafeArrayRedim") // TODO + // procSafeArraySetIID = modoleaut32.NewProc("SafeArraySetIID") // TODO + procSafeArrayGetRecordInfo = modoleaut32.NewProc("SafeArrayGetRecordInfo") + procSafeArraySetRecordInfo = modoleaut32.NewProc("SafeArraySetRecordInfo") +) + +// safeArrayAccessData returns raw array pointer. +// +// AKA: SafeArrayAccessData in Windows API. +// Todo: Test +func safeArrayAccessData(safearray *SafeArray) (element uintptr, err error) { + err = convertHresultToError( + procSafeArrayAccessData.Call( + uintptr(unsafe.Pointer(safearray)), + uintptr(unsafe.Pointer(&element)))) + return +} + +// safeArrayUnaccessData releases raw array. +// +// AKA: SafeArrayUnaccessData in Windows API. +func safeArrayUnaccessData(safearray *SafeArray) (err error) { + err = convertHresultToError(procSafeArrayUnaccessData.Call(uintptr(unsafe.Pointer(safearray)))) + return +} + +// safeArrayAllocData allocates SafeArray. +// +// AKA: SafeArrayAllocData in Windows API. +func safeArrayAllocData(safearray *SafeArray) (err error) { + err = convertHresultToError(procSafeArrayAllocData.Call(uintptr(unsafe.Pointer(safearray)))) + return +} + +// safeArrayAllocDescriptor allocates SafeArray. +// +// AKA: SafeArrayAllocDescriptor in Windows API. +func safeArrayAllocDescriptor(dimensions uint32) (safearray *SafeArray, err error) { + err = convertHresultToError( + procSafeArrayAllocDescriptor.Call(uintptr(dimensions), uintptr(unsafe.Pointer(&safearray)))) + return +} + +// safeArrayAllocDescriptorEx allocates SafeArray. +// +// AKA: SafeArrayAllocDescriptorEx in Windows API. +func safeArrayAllocDescriptorEx(variantType VT, dimensions uint32) (safearray *SafeArray, err error) { + err = convertHresultToError( + procSafeArrayAllocDescriptorEx.Call( + uintptr(variantType), + uintptr(dimensions), + uintptr(unsafe.Pointer(&safearray)))) + return +} + +// safeArrayCopy returns copy of SafeArray. +// +// AKA: SafeArrayCopy in Windows API. +func safeArrayCopy(original *SafeArray) (safearray *SafeArray, err error) { + err = convertHresultToError( + procSafeArrayCopy.Call( + uintptr(unsafe.Pointer(original)), + uintptr(unsafe.Pointer(&safearray)))) + return +} + +// safeArrayCopyData duplicates SafeArray into another SafeArray object. +// +// AKA: SafeArrayCopyData in Windows API. +func safeArrayCopyData(original, duplicate *SafeArray) (err error) { + err = convertHresultToError( + procSafeArrayCopyData.Call( + uintptr(unsafe.Pointer(original)), + uintptr(unsafe.Pointer(duplicate)))) + return +} + +// safeArrayCreate creates SafeArray. +// +// AKA: SafeArrayCreate in Windows API. +func safeArrayCreate(variantType VT, dimensions uint32, bounds *SafeArrayBound) (safearray *SafeArray, err error) { + sa, _, err := procSafeArrayCreate.Call( + uintptr(variantType), + uintptr(dimensions), + uintptr(unsafe.Pointer(bounds))) + safearray = (*SafeArray)(unsafe.Pointer(&sa)) + return +} + +// safeArrayCreateEx creates SafeArray. +// +// AKA: SafeArrayCreateEx in Windows API. +func safeArrayCreateEx(variantType VT, dimensions uint32, bounds *SafeArrayBound, extra uintptr) (safearray *SafeArray, err error) { + sa, _, err := procSafeArrayCreateEx.Call( + uintptr(variantType), + uintptr(dimensions), + uintptr(unsafe.Pointer(bounds)), + extra) + safearray = (*SafeArray)(unsafe.Pointer(sa)) + return +} + +// safeArrayCreateVector creates SafeArray. +// +// AKA: SafeArrayCreateVector in Windows API. +func safeArrayCreateVector(variantType VT, lowerBound int32, length uint32) (safearray *SafeArray, err error) { + sa, _, err := procSafeArrayCreateVector.Call( + uintptr(variantType), + uintptr(lowerBound), + uintptr(length)) + safearray = (*SafeArray)(unsafe.Pointer(sa)) + return +} + +// safeArrayCreateVectorEx creates SafeArray. +// +// AKA: SafeArrayCreateVectorEx in Windows API. +func safeArrayCreateVectorEx(variantType VT, lowerBound int32, length uint32, extra uintptr) (safearray *SafeArray, err error) { + sa, _, err := procSafeArrayCreateVectorEx.Call( + uintptr(variantType), + uintptr(lowerBound), + uintptr(length), + extra) + safearray = (*SafeArray)(unsafe.Pointer(sa)) + return +} + +// safeArrayDestroy destroys SafeArray object. +// +// AKA: SafeArrayDestroy in Windows API. +func safeArrayDestroy(safearray *SafeArray) (err error) { + err = convertHresultToError(procSafeArrayDestroy.Call(uintptr(unsafe.Pointer(safearray)))) + return +} + +// safeArrayDestroyData destroys SafeArray object. +// +// AKA: SafeArrayDestroyData in Windows API. +func safeArrayDestroyData(safearray *SafeArray) (err error) { + err = convertHresultToError(procSafeArrayDestroyData.Call(uintptr(unsafe.Pointer(safearray)))) + return +} + +// safeArrayDestroyDescriptor destroys SafeArray object. +// +// AKA: SafeArrayDestroyDescriptor in Windows API. +func safeArrayDestroyDescriptor(safearray *SafeArray) (err error) { + err = convertHresultToError(procSafeArrayDestroyDescriptor.Call(uintptr(unsafe.Pointer(safearray)))) + return +} + +// safeArrayGetDim is the amount of dimensions in the SafeArray. +// +// SafeArrays may have multiple dimensions. Meaning, it could be +// multidimensional array. +// +// AKA: SafeArrayGetDim in Windows API. +func safeArrayGetDim(safearray *SafeArray) (dimensions *uint32, err error) { + l, _, err := procSafeArrayGetDim.Call(uintptr(unsafe.Pointer(safearray))) + dimensions = (*uint32)(unsafe.Pointer(l)) + return +} + +// safeArrayGetElementSize is the element size in bytes. +// +// AKA: SafeArrayGetElemsize in Windows API. +func safeArrayGetElementSize(safearray *SafeArray) (length *uint32, err error) { + l, _, err := procSafeArrayGetElemsize.Call(uintptr(unsafe.Pointer(safearray))) + length = (*uint32)(unsafe.Pointer(l)) + return +} + +// safeArrayGetElement retrieves element at given index. +func safeArrayGetElement(safearray *SafeArray, index int32, pv unsafe.Pointer) error { + return convertHresultToError( + procSafeArrayGetElement.Call( + uintptr(unsafe.Pointer(safearray)), + uintptr(unsafe.Pointer(&index)), + uintptr(pv))) +} + +// safeArrayGetElementString retrieves element at given index and converts to string. +func safeArrayGetElementString(safearray *SafeArray, index int32) (str string, err error) { + var element *int16 + err = convertHresultToError( + procSafeArrayGetElement.Call( + uintptr(unsafe.Pointer(safearray)), + uintptr(unsafe.Pointer(&index)), + uintptr(unsafe.Pointer(&element)))) + str = BstrToString(*(**uint16)(unsafe.Pointer(&element))) + SysFreeString(element) + return +} + +// safeArrayGetIID is the InterfaceID of the elements in the SafeArray. +// +// AKA: SafeArrayGetIID in Windows API. +func safeArrayGetIID(safearray *SafeArray) (guid *GUID, err error) { + err = convertHresultToError( + procSafeArrayGetIID.Call( + uintptr(unsafe.Pointer(safearray)), + uintptr(unsafe.Pointer(&guid)))) + return +} + +// safeArrayGetLBound returns lower bounds of SafeArray. +// +// SafeArrays may have multiple dimensions. Meaning, it could be +// multidimensional array. +// +// AKA: SafeArrayGetLBound in Windows API. +func safeArrayGetLBound(safearray *SafeArray, dimension uint32) (lowerBound int32, err error) { + err = convertHresultToError( + procSafeArrayGetLBound.Call( + uintptr(unsafe.Pointer(safearray)), + uintptr(dimension), + uintptr(unsafe.Pointer(&lowerBound)))) + return +} + +// safeArrayGetUBound returns upper bounds of SafeArray. +// +// SafeArrays may have multiple dimensions. Meaning, it could be +// multidimensional array. +// +// AKA: SafeArrayGetUBound in Windows API. +func safeArrayGetUBound(safearray *SafeArray, dimension uint32) (upperBound int32, err error) { + err = convertHresultToError( + procSafeArrayGetUBound.Call( + uintptr(unsafe.Pointer(safearray)), + uintptr(dimension), + uintptr(unsafe.Pointer(&upperBound)))) + return +} + +// safeArrayGetVartype returns data type of SafeArray. +// +// AKA: SafeArrayGetVartype in Windows API. +func safeArrayGetVartype(safearray *SafeArray) (varType uint16, err error) { + err = convertHresultToError( + procSafeArrayGetVartype.Call( + uintptr(unsafe.Pointer(safearray)), + uintptr(unsafe.Pointer(&varType)))) + return +} + +// safeArrayLock locks SafeArray for reading to modify SafeArray. +// +// This must be called during some calls to ensure that another process does not +// read or write to the SafeArray during editing. +// +// AKA: SafeArrayLock in Windows API. +func safeArrayLock(safearray *SafeArray) (err error) { + err = convertHresultToError(procSafeArrayLock.Call(uintptr(unsafe.Pointer(safearray)))) + return +} + +// safeArrayUnlock unlocks SafeArray for reading. +// +// AKA: SafeArrayUnlock in Windows API. +func safeArrayUnlock(safearray *SafeArray) (err error) { + err = convertHresultToError(procSafeArrayUnlock.Call(uintptr(unsafe.Pointer(safearray)))) + return +} + +// safeArrayPutElement stores the data element at the specified location in the +// array. +// +// AKA: SafeArrayPutElement in Windows API. +func safeArrayPutElement(safearray *SafeArray, index int64, element uintptr) (err error) { + err = convertHresultToError( + procSafeArrayPutElement.Call( + uintptr(unsafe.Pointer(safearray)), + uintptr(unsafe.Pointer(&index)), + uintptr(unsafe.Pointer(element)))) + return +} + +// safeArrayGetRecordInfo accesses IRecordInfo info for custom types. +// +// AKA: SafeArrayGetRecordInfo in Windows API. +// +// XXX: Must implement IRecordInfo interface for this to return. +func safeArrayGetRecordInfo(safearray *SafeArray) (recordInfo interface{}, err error) { + err = convertHresultToError( + procSafeArrayGetRecordInfo.Call( + uintptr(unsafe.Pointer(safearray)), + uintptr(unsafe.Pointer(&recordInfo)))) + return +} + +// safeArraySetRecordInfo mutates IRecordInfo info for custom types. +// +// AKA: SafeArraySetRecordInfo in Windows API. +// +// XXX: Must implement IRecordInfo interface for this to return. +func safeArraySetRecordInfo(safearray *SafeArray, recordInfo interface{}) (err error) { + err = convertHresultToError( + procSafeArraySetRecordInfo.Call( + uintptr(unsafe.Pointer(safearray)), + uintptr(unsafe.Pointer(&recordInfo)))) + return +} diff --git a/internal/go-ole/safearrayconversion.go b/internal/go-ole/safearrayconversion.go new file mode 100644 index 0000000..259f488 --- /dev/null +++ b/internal/go-ole/safearrayconversion.go @@ -0,0 +1,140 @@ +// Helper for converting SafeArray to array of objects. + +package ole + +import ( + "unsafe" +) + +type SafeArrayConversion struct { + Array *SafeArray +} + +func (sac *SafeArrayConversion) ToStringArray() (strings []string) { + totalElements, _ := sac.TotalElements(0) + strings = make([]string, totalElements) + + for i := int32(0); i < totalElements; i++ { + strings[int32(i)], _ = safeArrayGetElementString(sac.Array, i) + } + + return +} + +func (sac *SafeArrayConversion) ToByteArray() (bytes []byte) { + totalElements, _ := sac.TotalElements(0) + bytes = make([]byte, totalElements) + + for i := int32(0); i < totalElements; i++ { + safeArrayGetElement(sac.Array, i, unsafe.Pointer(&bytes[int32(i)])) + } + + return +} + +func (sac *SafeArrayConversion) ToValueArray() (values []interface{}) { + totalElements, _ := sac.TotalElements(0) + values = make([]interface{}, totalElements) + vt, _ := safeArrayGetVartype(sac.Array) + + for i := int32(0); i < totalElements; i++ { + switch VT(vt) { + case VT_BOOL: + var v bool + safeArrayGetElement(sac.Array, i, unsafe.Pointer(&v)) + values[i] = v + case VT_I1: + var v int8 + safeArrayGetElement(sac.Array, i, unsafe.Pointer(&v)) + values[i] = v + case VT_I2: + var v int16 + safeArrayGetElement(sac.Array, i, unsafe.Pointer(&v)) + values[i] = v + case VT_I4: + var v int32 + safeArrayGetElement(sac.Array, i, unsafe.Pointer(&v)) + values[i] = v + case VT_I8: + var v int64 + safeArrayGetElement(sac.Array, i, unsafe.Pointer(&v)) + values[i] = v + case VT_UI1: + var v uint8 + safeArrayGetElement(sac.Array, i, unsafe.Pointer(&v)) + values[i] = v + case VT_UI2: + var v uint16 + safeArrayGetElement(sac.Array, i, unsafe.Pointer(&v)) + values[i] = v + case VT_UI4: + var v uint32 + safeArrayGetElement(sac.Array, i, unsafe.Pointer(&v)) + values[i] = v + case VT_UI8: + var v uint64 + safeArrayGetElement(sac.Array, i, unsafe.Pointer(&v)) + values[i] = v + case VT_R4: + var v float32 + safeArrayGetElement(sac.Array, i, unsafe.Pointer(&v)) + values[i] = v + case VT_R8: + var v float64 + safeArrayGetElement(sac.Array, i, unsafe.Pointer(&v)) + values[i] = v + case VT_BSTR: + var v string + safeArrayGetElement(sac.Array, i, unsafe.Pointer(&v)) + values[i] = v + case VT_VARIANT: + var v VARIANT + safeArrayGetElement(sac.Array, i, unsafe.Pointer(&v)) + values[i] = v.Value() + default: + // TODO + } + } + + return +} + +func (sac *SafeArrayConversion) GetType() (varType uint16, err error) { + return safeArrayGetVartype(sac.Array) +} + +func (sac *SafeArrayConversion) GetDimensions() (dimensions *uint32, err error) { + return safeArrayGetDim(sac.Array) +} + +func (sac *SafeArrayConversion) GetSize() (length *uint32, err error) { + return safeArrayGetElementSize(sac.Array) +} + +func (sac *SafeArrayConversion) TotalElements(index uint32) (totalElements int32, err error) { + if index < 1 { + index = 1 + } + + // Get array bounds + var LowerBounds int32 + var UpperBounds int32 + + LowerBounds, err = safeArrayGetLBound(sac.Array, index) + if err != nil { + return + } + + UpperBounds, err = safeArrayGetUBound(sac.Array, index) + if err != nil { + return + } + + totalElements = UpperBounds - LowerBounds + 1 + return +} + +// Release Safe Array memory +func (sac *SafeArrayConversion) Release() { + safeArrayDestroy(sac.Array) +} diff --git a/internal/go-ole/safearrayslices.go b/internal/go-ole/safearrayslices.go new file mode 100644 index 0000000..0307587 --- /dev/null +++ b/internal/go-ole/safearrayslices.go @@ -0,0 +1,33 @@ +//go:build windows + +package ole + +import ( + "unsafe" +) + +func safeArrayFromByteSlice(slice []byte) *SafeArray { + array, _ := safeArrayCreateVector(VT_UI1, 0, uint32(len(slice))) + + if array == nil { + panic("Could not convert []byte to SAFEARRAY") + } + + for i, v := range slice { + safeArrayPutElement(array, int64(i), uintptr(unsafe.Pointer(&v))) + } + return array +} + +func safeArrayFromStringSlice(slice []string) *SafeArray { + array, _ := safeArrayCreateVector(VT_BSTR, 0, uint32(len(slice))) + + if array == nil { + panic("Could not convert []string to SAFEARRAY") + } + // SysAllocStringLen(s) + for i, v := range slice { + safeArrayPutElement(array, int64(i), uintptr(unsafe.Pointer(SysAllocStringLen(v)))) + } + return array +} diff --git a/internal/go-ole/utility.go b/internal/go-ole/utility.go new file mode 100644 index 0000000..1ba6628 --- /dev/null +++ b/internal/go-ole/utility.go @@ -0,0 +1,101 @@ +package ole + +import ( + "unicode/utf16" + "unsafe" +) + +// ClassIDFrom retrieves class ID whether given is program ID or application string. +// +// Helper that provides check against both Class ID from Program ID and Class ID from string. It is +// faster, if you know which you are using, to use the individual functions, but this will check +// against available functions for you. +func ClassIDFrom(programID string) (classID *GUID, err error) { + classID, err = CLSIDFromProgID(programID) + if err != nil { + classID, err = CLSIDFromString(programID) + if err != nil { + return + } + } + return +} + +// BytePtrToString converts byte pointer to a Go string. +func BytePtrToString(p *byte) string { + a := (*[10000]uint8)(unsafe.Pointer(p)) + i := 0 + for a[i] != 0 { + i++ + } + return string(a[:i]) +} + +// UTF16PtrToString is alias for LpOleStrToString. +// +// Kept for compatibility reasons. +func UTF16PtrToString(p *uint16) string { + return LpOleStrToString(p) +} + +// LpOleStrToString converts COM Unicode to Go string. +func LpOleStrToString(p *uint16) string { + if p == nil { + return "" + } + + length := lpOleStrLen(p) + a := make([]uint16, length) + + ptr := unsafe.Pointer(p) + + for i := 0; i < int(length); i++ { + a[i] = *(*uint16)(ptr) + ptr = unsafe.Pointer(uintptr(ptr) + 2) + } + + return string(utf16.Decode(a)) +} + +// BstrToString converts COM binary string to Go string. +func BstrToString(p *uint16) string { + if p == nil { + return "" + } + length := SysStringLen((*int16)(unsafe.Pointer(p))) + a := make([]uint16, length) + + ptr := unsafe.Pointer(p) + + for i := 0; i < int(length); i++ { + a[i] = *(*uint16)(ptr) + ptr = unsafe.Pointer(uintptr(ptr) + 2) + } + return string(utf16.Decode(a)) +} + +// lpOleStrLen returns the length of Unicode string. +func lpOleStrLen(p *uint16) (length int64) { + if p == nil { + return 0 + } + + ptr := unsafe.Pointer(p) + + for i := 0; ; i++ { + if 0 == *(*uint16)(ptr) { + length = int64(i) + break + } + ptr = unsafe.Pointer(uintptr(ptr) + 2) + } + return +} + +// convertHresultToError converts syscall to error, if call is unsuccessful. +func convertHresultToError(hr, r2 uintptr, ignore error) (err error) { + if hr != 0 { + err = NewError(hr) + } + return +} diff --git a/internal/go-ole/variables.go b/internal/go-ole/variables.go new file mode 100644 index 0000000..fe7a9a2 --- /dev/null +++ b/internal/go-ole/variables.go @@ -0,0 +1,15 @@ +//go:build windows + +package ole + +import ( + "golang.org/x/sys/windows" +) + +var ( + modcombase = windows.NewLazySystemDLL("combase.dll") + modkernel32 = windows.NewLazySystemDLL("kernel32.dll") + modole32 = windows.NewLazySystemDLL("ole32.dll") + modoleaut32 = windows.NewLazySystemDLL("oleaut32.dll") + moduser32 = windows.NewLazySystemDLL("user32.dll") +) diff --git a/internal/go-ole/variant.go b/internal/go-ole/variant.go new file mode 100644 index 0000000..967a23f --- /dev/null +++ b/internal/go-ole/variant.go @@ -0,0 +1,105 @@ +package ole + +import "unsafe" + +// NewVariant returns new variant based on type and value. +func NewVariant(vt VT, val int64) VARIANT { + return VARIANT{VT: vt, Val: val} +} + +// ToIUnknown converts Variant to Unknown object. +func (v *VARIANT) ToIUnknown() *IUnknown { + if v.VT != VT_UNKNOWN { + return nil + } + return (*IUnknown)(unsafe.Pointer(uintptr(v.Val))) +} + +// ToIDispatch converts variant to dispatch object. +func (v *VARIANT) ToIDispatch() *IDispatch { + if v.VT != VT_DISPATCH { + return nil + } + return (*IDispatch)(unsafe.Pointer(uintptr(v.Val))) +} + +// ToArray converts variant to SafeArray helper. +func (v *VARIANT) ToArray() *SafeArrayConversion { + if v.VT != VT_SAFEARRAY { + if v.VT&VT_ARRAY == 0 { + return nil + } + } + var safeArray *SafeArray = (*SafeArray)(unsafe.Pointer(uintptr(v.Val))) + return &SafeArrayConversion{safeArray} +} + +// ToString converts variant to Go string. +func (v *VARIANT) ToString() string { + if v.VT != VT_BSTR { + return "" + } + return BstrToString(*(**uint16)(unsafe.Pointer(&v.Val))) +} + +// Clear the memory of variant object. +func (v *VARIANT) Clear() error { + return VariantClear(v) +} + +// Value returns variant value based on its type. +// +// Currently supported types: 2- and 4-byte integers, strings, bools. +// Note that 64-bit integers, datetimes, and other types are stored as strings +// and will be returned as strings. +// +// Needs to be further converted, because this returns an interface{}. +func (v *VARIANT) Value() interface{} { + switch v.VT { + case VT_I1: + return int8(v.Val) + case VT_UI1: + return uint8(v.Val) + case VT_I2: + return int16(v.Val) + case VT_UI2: + return uint16(v.Val) + case VT_I4: + return int32(v.Val) + case VT_UI4: + return uint32(v.Val) + case VT_I8: + return int64(v.Val) + case VT_UI8: + return uint64(v.Val) + case VT_INT: + return int(v.Val) + case VT_UINT: + return uint(v.Val) + case VT_INT_PTR: + return uintptr(v.Val) // TODO + case VT_UINT_PTR: + return uintptr(v.Val) + case VT_R4: + return *(*float32)(unsafe.Pointer(&v.Val)) + case VT_R8: + return *(*float64)(unsafe.Pointer(&v.Val)) + case VT_BSTR: + return v.ToString() + case VT_DATE: + // VT_DATE type will either return float64 or time.Time. + d := uint64(v.Val) + date, err := GetVariantDate(d) + if err != nil { + return float64(v.Val) + } + return date + case VT_UNKNOWN: + return v.ToIUnknown() + case VT_DISPATCH: + return v.ToIDispatch() + case VT_BOOL: + return v.Val != 0 + } + return nil +} diff --git a/internal/go-ole/variant32.go b/internal/go-ole/variant32.go new file mode 100644 index 0000000..2628428 --- /dev/null +++ b/internal/go-ole/variant32.go @@ -0,0 +1,11 @@ +//go:build 386 || arm + +package ole + +type VARIANT struct { + VT VT // 2 + wReserved1 uint16 // 4 + wReserved2 uint16 // 6 + wReserved3 uint16 // 8 + Val int64 // 16 +} diff --git a/internal/go-ole/variant64.go b/internal/go-ole/variant64.go new file mode 100644 index 0000000..9a0c248 --- /dev/null +++ b/internal/go-ole/variant64.go @@ -0,0 +1,12 @@ +//go:build amd64 || arm64 || ppc64le || s390x + +package ole + +type VARIANT struct { + VT VT // 2 + wReserved1 uint16 // 4 + wReserved2 uint16 // 6 + wReserved3 uint16 // 8 + Val int64 // 16 + _ [8]byte // 24 +} diff --git a/internal/go-ole/variant_date_386.go b/internal/go-ole/variant_date_386.go new file mode 100644 index 0000000..e5d1004 --- /dev/null +++ b/internal/go-ole/variant_date_386.go @@ -0,0 +1,22 @@ +//go:build windows && 386 + +package ole + +import ( + "errors" + "syscall" + "time" + "unsafe" +) + +// GetVariantDate converts COM Variant Time value to Go time.Time. +func GetVariantDate(value uint64) (time.Time, error) { + var st syscall.Systemtime + v1 := uint32(value) + v2 := uint32(value >> 32) + r, _, _ := procVariantTimeToSystemTime.Call(uintptr(v1), uintptr(v2), uintptr(unsafe.Pointer(&st))) + if r != 0 { + return time.Date(int(st.Year), time.Month(st.Month), int(st.Day), int(st.Hour), int(st.Minute), int(st.Second), int(st.Milliseconds/1000), time.UTC), nil + } + return time.Now(), errors.New("Could not convert to time, passing current time.") +} diff --git a/internal/go-ole/variant_date_amd64.go b/internal/go-ole/variant_date_amd64.go new file mode 100644 index 0000000..a6c279b --- /dev/null +++ b/internal/go-ole/variant_date_amd64.go @@ -0,0 +1,20 @@ +//go:build windows && amd64 + +package ole + +import ( + "errors" + "syscall" + "time" + "unsafe" +) + +// GetVariantDate converts COM Variant Time value to Go time.Time. +func GetVariantDate(value uint64) (time.Time, error) { + var st syscall.Systemtime + r, _, _ := procVariantTimeToSystemTime.Call(uintptr(value), uintptr(unsafe.Pointer(&st))) + if r != 0 { + return time.Date(int(st.Year), time.Month(st.Month), int(st.Day), int(st.Hour), int(st.Minute), int(st.Second), int(st.Milliseconds/1000), time.UTC), nil + } + return time.Now(), errors.New("Could not convert to time, passing current time.") +} diff --git a/internal/go-ole/vt_string.go b/internal/go-ole/vt_string.go new file mode 100644 index 0000000..729b4a0 --- /dev/null +++ b/internal/go-ole/vt_string.go @@ -0,0 +1,58 @@ +// generated by stringer -output vt_string.go -type VT; DO NOT EDIT + +package ole + +import "fmt" + +const ( + _VT_name_0 = "VT_EMPTYVT_NULLVT_I2VT_I4VT_R4VT_R8VT_CYVT_DATEVT_BSTRVT_DISPATCHVT_ERRORVT_BOOLVT_VARIANTVT_UNKNOWNVT_DECIMAL" + _VT_name_1 = "VT_I1VT_UI1VT_UI2VT_UI4VT_I8VT_UI8VT_INTVT_UINTVT_VOIDVT_HRESULTVT_PTRVT_SAFEARRAYVT_CARRAYVT_USERDEFINEDVT_LPSTRVT_LPWSTR" + _VT_name_2 = "VT_RECORDVT_INT_PTRVT_UINT_PTR" + _VT_name_3 = "VT_FILETIMEVT_BLOBVT_STREAMVT_STORAGEVT_STREAMED_OBJECTVT_STORED_OBJECTVT_BLOB_OBJECTVT_CFVT_CLSID" + _VT_name_4 = "VT_BSTR_BLOBVT_VECTOR" + _VT_name_5 = "VT_ARRAY" + _VT_name_6 = "VT_BYREF" + _VT_name_7 = "VT_RESERVED" + _VT_name_8 = "VT_ILLEGAL" +) + +var ( + _VT_index_0 = [...]uint8{0, 8, 15, 20, 25, 30, 35, 40, 47, 54, 65, 73, 80, 90, 100, 110} + _VT_index_1 = [...]uint8{0, 5, 11, 17, 23, 28, 34, 40, 47, 54, 64, 70, 82, 91, 105, 113, 122} + _VT_index_2 = [...]uint8{0, 9, 19, 30} + _VT_index_3 = [...]uint8{0, 11, 18, 27, 37, 55, 71, 85, 90, 98} + _VT_index_4 = [...]uint8{0, 12, 21} + _VT_index_5 = [...]uint8{0, 8} + _VT_index_6 = [...]uint8{0, 8} + _VT_index_7 = [...]uint8{0, 11} + _VT_index_8 = [...]uint8{0, 10} +) + +func (i VT) String() string { + switch { + case 0 <= i && i <= 14: + return _VT_name_0[_VT_index_0[i]:_VT_index_0[i+1]] + case 16 <= i && i <= 31: + i -= 16 + return _VT_name_1[_VT_index_1[i]:_VT_index_1[i+1]] + case 36 <= i && i <= 38: + i -= 36 + return _VT_name_2[_VT_index_2[i]:_VT_index_2[i+1]] + case 64 <= i && i <= 72: + i -= 64 + return _VT_name_3[_VT_index_3[i]:_VT_index_3[i+1]] + case 4095 <= i && i <= 4096: + i -= 4095 + return _VT_name_4[_VT_index_4[i]:_VT_index_4[i+1]] + case i == 8192: + return _VT_name_5 + case i == 16384: + return _VT_name_6 + case i == 32768: + return _VT_name_7 + case i == 65535: + return _VT_name_8 + default: + return fmt.Sprintf("VT(%d)", i) + } +} diff --git a/internal/go-ole/winrt.go b/internal/go-ole/winrt.go new file mode 100644 index 0000000..5aa33c2 --- /dev/null +++ b/internal/go-ole/winrt.go @@ -0,0 +1,99 @@ +//go:build windows + +package ole + +import ( + "reflect" + "syscall" + "unicode/utf8" + "unsafe" +) + +var ( + procRoInitialize = modcombase.NewProc("RoInitialize") + procRoActivateInstance = modcombase.NewProc("RoActivateInstance") + procRoGetActivationFactory = modcombase.NewProc("RoGetActivationFactory") + procWindowsCreateString = modcombase.NewProc("WindowsCreateString") + procWindowsDeleteString = modcombase.NewProc("WindowsDeleteString") + procWindowsGetStringRawBuffer = modcombase.NewProc("WindowsGetStringRawBuffer") +) + +func RoInitialize(thread_type uint32) (err error) { + hr, _, _ := procRoInitialize.Call(uintptr(thread_type)) + if hr != 0 { + err = NewError(hr) + } + return +} + +func RoActivateInstance(clsid string) (ins *IInspectable, err error) { + hClsid, err := NewHString(clsid) + if err != nil { + return nil, err + } + defer DeleteHString(hClsid) + + hr, _, _ := procRoActivateInstance.Call( + uintptr(unsafe.Pointer(hClsid)), + uintptr(unsafe.Pointer(&ins))) + if hr != 0 { + err = NewError(hr) + } + return +} + +func RoGetActivationFactory(clsid string, iid *GUID) (ins *IInspectable, err error) { + hClsid, err := NewHString(clsid) + if err != nil { + return nil, err + } + defer DeleteHString(hClsid) + + hr, _, _ := procRoGetActivationFactory.Call( + uintptr(unsafe.Pointer(hClsid)), + uintptr(unsafe.Pointer(iid)), + uintptr(unsafe.Pointer(&ins))) + if hr != 0 { + err = NewError(hr) + } + return +} + +// HString is handle string for pointers. +type HString uintptr + +// NewHString returns a new HString for Go string. +func NewHString(s string) (hstring HString, err error) { + u16 := syscall.StringToUTF16Ptr(s) + len := uint32(utf8.RuneCountInString(s)) + hr, _, _ := procWindowsCreateString.Call( + uintptr(unsafe.Pointer(u16)), + uintptr(len), + uintptr(unsafe.Pointer(&hstring))) + if hr != 0 { + err = NewError(hr) + } + return +} + +// DeleteHString deletes HString. +func DeleteHString(hstring HString) (err error) { + hr, _, _ := procWindowsDeleteString.Call(uintptr(hstring)) + if hr != 0 { + err = NewError(hr) + } + return +} + +// String returns Go string value of HString. +func (h HString) String() string { + var u16buf uintptr + var u16len uint32 + u16buf, _, _ = procWindowsGetStringRawBuffer.Call( + uintptr(h), + uintptr(unsafe.Pointer(&u16len))) + + u16hdr := reflect.SliceHeader{Data: u16buf, Len: int(u16len), Cap: int(u16len)} + u16 := *(*[]uint16)(unsafe.Pointer(&u16hdr)) + return syscall.UTF16ToString(u16) +} diff --git a/internal/go-ole/winrt_doc.go b/internal/go-ole/winrt_doc.go new file mode 100644 index 0000000..ec74e75 --- /dev/null +++ b/internal/go-ole/winrt_doc.go @@ -0,0 +1,36 @@ +//go:build !windows + +package ole + +// RoInitialize +func RoInitialize(thread_type uint32) (err error) { + return NewError(E_NOTIMPL) +} + +// RoActivateInstance +func RoActivateInstance(clsid string) (ins *IInspectable, err error) { + return nil, NewError(E_NOTIMPL) +} + +// RoGetActivationFactory +func RoGetActivationFactory(clsid string, iid *GUID) (ins *IInspectable, err error) { + return nil, NewError(E_NOTIMPL) +} + +// HString is handle string for pointers. +type HString uintptr + +// NewHString returns a new HString for Go string. +func NewHString(s string) (hstring HString, err error) { + return HString(uintptr(0)), NewError(E_NOTIMPL) +} + +// DeleteHString deletes HString. +func DeleteHString(hstring HString) (err error) { + return NewError(E_NOTIMPL) +} + +// String returns Go string value of HString. +func (h HString) String() string { + return "" +} diff --git a/internal/gopsutil/LICENSE b/internal/gopsutil/LICENSE new file mode 100644 index 0000000..da71a5e --- /dev/null +++ b/internal/gopsutil/LICENSE @@ -0,0 +1,61 @@ +gopsutil is distributed under BSD license reproduced below. + +Copyright (c) 2014, WAKAYAMA Shirou +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + * Neither the name of the gopsutil authors nor the names of its contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +------- +internal/common/binary.go in the gopsutil is copied and modifid from golang/encoding/binary.go. + + + +Copyright (c) 2009 The Go Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/internal/gopsutil/common/binary.go b/internal/gopsutil/common/binary.go new file mode 100644 index 0000000..446c359 --- /dev/null +++ b/internal/gopsutil/common/binary.go @@ -0,0 +1,636 @@ +package common + +// Copyright 2009 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package binary implements simple translation between numbers and byte +// sequences and encoding and decoding of varints. +// +// Numbers are translated by reading and writing fixed-size values. +// A fixed-size value is either a fixed-size arithmetic +// type (int8, uint8, int16, float32, complex64, ...) +// or an array or struct containing only fixed-size values. +// +// The varint functions encode and decode single integer values using +// a variable-length encoding; smaller values require fewer bytes. +// For a specification, see +// http://code.google.com/apis/protocolbuffers/docs/encoding.html. +// +// This package favors simplicity over efficiency. Clients that require +// high-performance serialization, especially for large data structures, +// should look at more advanced solutions such as the encoding/gob +// package or protocol buffers. +import ( + "errors" + "io" + "math" + "reflect" +) + +// A ByteOrder specifies how to convert byte sequences into +// 16-, 32-, or 64-bit unsigned integers. +type ByteOrder interface { + Uint16([]byte) uint16 + Uint32([]byte) uint32 + Uint64([]byte) uint64 + PutUint16([]byte, uint16) + PutUint32([]byte, uint32) + PutUint64([]byte, uint64) + String() string +} + +// LittleEndian is the little-endian implementation of ByteOrder. +var LittleEndian littleEndian + +// BigEndian is the big-endian implementation of ByteOrder. +var BigEndian bigEndian + +type littleEndian struct{} + +func (littleEndian) Uint16(b []byte) uint16 { return uint16(b[0]) | uint16(b[1])<<8 } + +func (littleEndian) PutUint16(b []byte, v uint16) { + b[0] = byte(v) + b[1] = byte(v >> 8) +} + +func (littleEndian) Uint32(b []byte) uint32 { + return uint32(b[0]) | uint32(b[1])<<8 | uint32(b[2])<<16 | uint32(b[3])<<24 +} + +func (littleEndian) PutUint32(b []byte, v uint32) { + b[0] = byte(v) + b[1] = byte(v >> 8) + b[2] = byte(v >> 16) + b[3] = byte(v >> 24) +} + +func (littleEndian) Uint64(b []byte) uint64 { + return uint64(b[0]) | uint64(b[1])<<8 | uint64(b[2])<<16 | uint64(b[3])<<24 | + uint64(b[4])<<32 | uint64(b[5])<<40 | uint64(b[6])<<48 | uint64(b[7])<<56 +} + +func (littleEndian) PutUint64(b []byte, v uint64) { + b[0] = byte(v) + b[1] = byte(v >> 8) + b[2] = byte(v >> 16) + b[3] = byte(v >> 24) + b[4] = byte(v >> 32) + b[5] = byte(v >> 40) + b[6] = byte(v >> 48) + b[7] = byte(v >> 56) +} + +func (littleEndian) String() string { return "LittleEndian" } + +func (littleEndian) GoString() string { return "binary.LittleEndian" } + +type bigEndian struct{} + +func (bigEndian) Uint16(b []byte) uint16 { return uint16(b[1]) | uint16(b[0])<<8 } + +func (bigEndian) PutUint16(b []byte, v uint16) { + b[0] = byte(v >> 8) + b[1] = byte(v) +} + +func (bigEndian) Uint32(b []byte) uint32 { + return uint32(b[3]) | uint32(b[2])<<8 | uint32(b[1])<<16 | uint32(b[0])<<24 +} + +func (bigEndian) PutUint32(b []byte, v uint32) { + b[0] = byte(v >> 24) + b[1] = byte(v >> 16) + b[2] = byte(v >> 8) + b[3] = byte(v) +} + +func (bigEndian) Uint64(b []byte) uint64 { + return uint64(b[7]) | uint64(b[6])<<8 | uint64(b[5])<<16 | uint64(b[4])<<24 | + uint64(b[3])<<32 | uint64(b[2])<<40 | uint64(b[1])<<48 | uint64(b[0])<<56 +} + +func (bigEndian) PutUint64(b []byte, v uint64) { + b[0] = byte(v >> 56) + b[1] = byte(v >> 48) + b[2] = byte(v >> 40) + b[3] = byte(v >> 32) + b[4] = byte(v >> 24) + b[5] = byte(v >> 16) + b[6] = byte(v >> 8) + b[7] = byte(v) +} + +func (bigEndian) String() string { return "BigEndian" } + +func (bigEndian) GoString() string { return "binary.BigEndian" } + +// Read reads structured binary data from r into data. +// Data must be a pointer to a fixed-size value or a slice +// of fixed-size values. +// Bytes read from r are decoded using the specified byte order +// and written to successive fields of the data. +// When reading into structs, the field data for fields with +// blank (_) field names is skipped; i.e., blank field names +// may be used for padding. +// When reading into a struct, all non-blank fields must be exported. +func Read(r io.Reader, order ByteOrder, data interface{}) error { + // Fast path for basic types and slices. + if n := intDataSize(data); n != 0 { + var b [8]byte + var bs []byte + if n > len(b) { + bs = make([]byte, n) + } else { + bs = b[:n] + } + if _, err := io.ReadFull(r, bs); err != nil { + return err + } + switch data := data.(type) { + case *int8: + *data = int8(b[0]) + case *uint8: + *data = b[0] + case *int16: + *data = int16(order.Uint16(bs)) + case *uint16: + *data = order.Uint16(bs) + case *int32: + *data = int32(order.Uint32(bs)) + case *uint32: + *data = order.Uint32(bs) + case *int64: + *data = int64(order.Uint64(bs)) + case *uint64: + *data = order.Uint64(bs) + case []int8: + for i, x := range bs { // Easier to loop over the input for 8-bit values. + data[i] = int8(x) + } + case []uint8: + copy(data, bs) + case []int16: + for i := range data { + data[i] = int16(order.Uint16(bs[2*i:])) + } + case []uint16: + for i := range data { + data[i] = order.Uint16(bs[2*i:]) + } + case []int32: + for i := range data { + data[i] = int32(order.Uint32(bs[4*i:])) + } + case []uint32: + for i := range data { + data[i] = order.Uint32(bs[4*i:]) + } + case []int64: + for i := range data { + data[i] = int64(order.Uint64(bs[8*i:])) + } + case []uint64: + for i := range data { + data[i] = order.Uint64(bs[8*i:]) + } + } + return nil + } + + // Fallback to reflect-based decoding. + v := reflect.ValueOf(data) + size := -1 + switch v.Kind() { + case reflect.Ptr: + v = v.Elem() + size = dataSize(v) + case reflect.Slice: + size = dataSize(v) + } + if size < 0 { + return errors.New("binary.Read: invalid type " + reflect.TypeOf(data).String()) + } + d := &decoder{order: order, buf: make([]byte, size)} + if _, err := io.ReadFull(r, d.buf); err != nil { + return err + } + d.value(v) + return nil +} + +// Write writes the binary representation of data into w. +// Data must be a fixed-size value or a slice of fixed-size +// values, or a pointer to such data. +// Bytes written to w are encoded using the specified byte order +// and read from successive fields of the data. +// When writing structs, zero values are written for fields +// with blank (_) field names. +func Write(w io.Writer, order ByteOrder, data interface{}) error { + // Fast path for basic types and slices. + if n := intDataSize(data); n != 0 { + var b [8]byte + var bs []byte + if n > len(b) { + bs = make([]byte, n) + } else { + bs = b[:n] + } + switch v := data.(type) { + case *int8: + bs = b[:1] + b[0] = byte(*v) + case int8: + bs = b[:1] + b[0] = byte(v) + case []int8: + for i, x := range v { + bs[i] = byte(x) + } + case *uint8: + bs = b[:1] + b[0] = *v + case uint8: + bs = b[:1] + b[0] = byte(v) + case []uint8: + bs = v + case *int16: + bs = b[:2] + order.PutUint16(bs, uint16(*v)) + case int16: + bs = b[:2] + order.PutUint16(bs, uint16(v)) + case []int16: + for i, x := range v { + order.PutUint16(bs[2*i:], uint16(x)) + } + case *uint16: + bs = b[:2] + order.PutUint16(bs, *v) + case uint16: + bs = b[:2] + order.PutUint16(bs, v) + case []uint16: + for i, x := range v { + order.PutUint16(bs[2*i:], x) + } + case *int32: + bs = b[:4] + order.PutUint32(bs, uint32(*v)) + case int32: + bs = b[:4] + order.PutUint32(bs, uint32(v)) + case []int32: + for i, x := range v { + order.PutUint32(bs[4*i:], uint32(x)) + } + case *uint32: + bs = b[:4] + order.PutUint32(bs, *v) + case uint32: + bs = b[:4] + order.PutUint32(bs, v) + case []uint32: + for i, x := range v { + order.PutUint32(bs[4*i:], x) + } + case *int64: + bs = b[:8] + order.PutUint64(bs, uint64(*v)) + case int64: + bs = b[:8] + order.PutUint64(bs, uint64(v)) + case []int64: + for i, x := range v { + order.PutUint64(bs[8*i:], uint64(x)) + } + case *uint64: + bs = b[:8] + order.PutUint64(bs, *v) + case uint64: + bs = b[:8] + order.PutUint64(bs, v) + case []uint64: + for i, x := range v { + order.PutUint64(bs[8*i:], x) + } + } + _, err := w.Write(bs) + return err + } + + // Fallback to reflect-based encoding. + v := reflect.Indirect(reflect.ValueOf(data)) + size := dataSize(v) + if size < 0 { + return errors.New("binary.Write: invalid type " + reflect.TypeOf(data).String()) + } + buf := make([]byte, size) + e := &encoder{order: order, buf: buf} + e.value(v) + _, err := w.Write(buf) + return err +} + +// Size returns how many bytes Write would generate to encode the value v, which +// must be a fixed-size value or a slice of fixed-size values, or a pointer to such data. +// If v is neither of these, Size returns -1. +func Size(v interface{}) int { + return dataSize(reflect.Indirect(reflect.ValueOf(v))) +} + +// dataSize returns the number of bytes the actual data represented by v occupies in memory. +// For compound structures, it sums the sizes of the elements. Thus, for instance, for a slice +// it returns the length of the slice times the element size and does not count the memory +// occupied by the header. If the type of v is not acceptable, dataSize returns -1. +func dataSize(v reflect.Value) int { + if v.Kind() == reflect.Slice { + if s := sizeof(v.Type().Elem()); s >= 0 { + return s * v.Len() + } + return -1 + } + return sizeof(v.Type()) +} + +// sizeof returns the size >= 0 of variables for the given type or -1 if the type is not acceptable. +func sizeof(t reflect.Type) int { + switch t.Kind() { + case reflect.Array: + if s := sizeof(t.Elem()); s >= 0 { + return s * t.Len() + } + + case reflect.Struct: + sum := 0 + for i, n := 0, t.NumField(); i < n; i++ { + s := sizeof(t.Field(i).Type) + if s < 0 { + return -1 + } + sum += s + } + return sum + + case reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, + reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, + reflect.Float32, reflect.Float64, reflect.Complex64, reflect.Complex128, reflect.Ptr: + return int(t.Size()) + } + + return -1 +} + +type coder struct { + order ByteOrder + buf []byte +} + +type ( + decoder coder + encoder coder +) + +func (d *decoder) uint8() uint8 { + x := d.buf[0] + d.buf = d.buf[1:] + return x +} + +func (e *encoder) uint8(x uint8) { + e.buf[0] = x + e.buf = e.buf[1:] +} + +func (d *decoder) uint16() uint16 { + x := d.order.Uint16(d.buf[0:2]) + d.buf = d.buf[2:] + return x +} + +func (e *encoder) uint16(x uint16) { + e.order.PutUint16(e.buf[0:2], x) + e.buf = e.buf[2:] +} + +func (d *decoder) uint32() uint32 { + x := d.order.Uint32(d.buf[0:4]) + d.buf = d.buf[4:] + return x +} + +func (e *encoder) uint32(x uint32) { + e.order.PutUint32(e.buf[0:4], x) + e.buf = e.buf[4:] +} + +func (d *decoder) uint64() uint64 { + x := d.order.Uint64(d.buf[0:8]) + d.buf = d.buf[8:] + return x +} + +func (e *encoder) uint64(x uint64) { + e.order.PutUint64(e.buf[0:8], x) + e.buf = e.buf[8:] +} + +func (d *decoder) int8() int8 { return int8(d.uint8()) } + +func (e *encoder) int8(x int8) { e.uint8(uint8(x)) } + +func (d *decoder) int16() int16 { return int16(d.uint16()) } + +func (e *encoder) int16(x int16) { e.uint16(uint16(x)) } + +func (d *decoder) int32() int32 { return int32(d.uint32()) } + +func (e *encoder) int32(x int32) { e.uint32(uint32(x)) } + +func (d *decoder) int64() int64 { return int64(d.uint64()) } + +func (e *encoder) int64(x int64) { e.uint64(uint64(x)) } + +func (d *decoder) value(v reflect.Value) { + switch v.Kind() { + case reflect.Array: + l := v.Len() + for i := 0; i < l; i++ { + d.value(v.Index(i)) + } + + case reflect.Struct: + t := v.Type() + l := v.NumField() + for i := 0; i < l; i++ { + // Note: Calling v.CanSet() below is an optimization. + // It would be sufficient to check the field name, + // but creating the StructField info for each field is + // costly (run "go test -bench=ReadStruct" and compare + // results when making changes to this code). + if v := v.Field(i); v.CanSet() || t.Field(i).Name != "_" { + d.value(v) + } else { + d.skip(v) + } + } + + case reflect.Slice: + l := v.Len() + for i := 0; i < l; i++ { + d.value(v.Index(i)) + } + + case reflect.Int8: + v.SetInt(int64(d.int8())) + case reflect.Int16: + v.SetInt(int64(d.int16())) + case reflect.Int32: + v.SetInt(int64(d.int32())) + case reflect.Int64: + v.SetInt(d.int64()) + + case reflect.Uint8: + v.SetUint(uint64(d.uint8())) + case reflect.Uint16: + v.SetUint(uint64(d.uint16())) + case reflect.Uint32: + v.SetUint(uint64(d.uint32())) + case reflect.Uint64: + v.SetUint(d.uint64()) + + case reflect.Float32: + v.SetFloat(float64(math.Float32frombits(d.uint32()))) + case reflect.Float64: + v.SetFloat(math.Float64frombits(d.uint64())) + + case reflect.Complex64: + v.SetComplex(complex( + float64(math.Float32frombits(d.uint32())), + float64(math.Float32frombits(d.uint32())), + )) + case reflect.Complex128: + v.SetComplex(complex( + math.Float64frombits(d.uint64()), + math.Float64frombits(d.uint64()), + )) + } +} + +func (e *encoder) value(v reflect.Value) { + switch v.Kind() { + case reflect.Array: + l := v.Len() + for i := 0; i < l; i++ { + e.value(v.Index(i)) + } + + case reflect.Struct: + t := v.Type() + l := v.NumField() + for i := 0; i < l; i++ { + // see comment for corresponding code in decoder.value() + if v := v.Field(i); v.CanSet() || t.Field(i).Name != "_" { + e.value(v) + } else { + e.skip(v) + } + } + + case reflect.Slice: + l := v.Len() + for i := 0; i < l; i++ { + e.value(v.Index(i)) + } + + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + switch v.Type().Kind() { + case reflect.Int8: + e.int8(int8(v.Int())) + case reflect.Int16: + e.int16(int16(v.Int())) + case reflect.Int32: + e.int32(int32(v.Int())) + case reflect.Int64: + e.int64(v.Int()) + } + + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + switch v.Type().Kind() { + case reflect.Uint8: + e.uint8(uint8(v.Uint())) + case reflect.Uint16: + e.uint16(uint16(v.Uint())) + case reflect.Uint32: + e.uint32(uint32(v.Uint())) + case reflect.Uint64: + e.uint64(v.Uint()) + } + + case reflect.Float32, reflect.Float64: + switch v.Type().Kind() { + case reflect.Float32: + e.uint32(math.Float32bits(float32(v.Float()))) + case reflect.Float64: + e.uint64(math.Float64bits(v.Float())) + } + + case reflect.Complex64, reflect.Complex128: + switch v.Type().Kind() { + case reflect.Complex64: + x := v.Complex() + e.uint32(math.Float32bits(float32(real(x)))) + e.uint32(math.Float32bits(float32(imag(x)))) + case reflect.Complex128: + x := v.Complex() + e.uint64(math.Float64bits(real(x))) + e.uint64(math.Float64bits(imag(x))) + } + } +} + +func (d *decoder) skip(v reflect.Value) { + d.buf = d.buf[dataSize(v):] +} + +func (e *encoder) skip(v reflect.Value) { + n := dataSize(v) + for i := range e.buf[0:n] { + e.buf[i] = 0 + } + e.buf = e.buf[n:] +} + +// intDataSize returns the size of the data required to represent the data when encoded. +// It returns zero if the type cannot be implemented by the fast path in Read or Write. +func intDataSize(data interface{}) int { + switch data := data.(type) { + case int8, *int8, *uint8: + return 1 + case []int8: + return len(data) + case []uint8: + return len(data) + case int16, *int16, *uint16: + return 2 + case []int16: + return 2 * len(data) + case []uint16: + return 2 * len(data) + case int32, *int32, *uint32: + return 4 + case []int32: + return 4 * len(data) + case []uint32: + return 4 * len(data) + case int64, *int64, *uint64: + return 8 + case []int64: + return 8 * len(data) + case []uint64: + return 8 * len(data) + } + return 0 +} diff --git a/internal/gopsutil/common/common.go b/internal/gopsutil/common/common.go new file mode 100644 index 0000000..7181ad0 --- /dev/null +++ b/internal/gopsutil/common/common.go @@ -0,0 +1,379 @@ +package common + +// +// gopsutil is a port of psutil(http://pythonhosted.org/psutil/). +// This covers these architectures. +// - linux (amd64, arm) +// - freebsd (amd64) +// - windows (amd64) +import ( + "bufio" + "bytes" + "context" + "errors" + "fmt" + "log" + "net/url" + "os" + "os/exec" + "path" + "path/filepath" + "reflect" + "runtime" + "strconv" + "strings" + "time" +) + +var ( + Timeout = 3 * time.Second + ErrTimeout = errors.New("command timed out") +) + +type Invoker interface { + Command(string, ...string) ([]byte, error) + CommandWithContext(context.Context, string, ...string) ([]byte, error) +} + +type Invoke struct{} + +func (i Invoke) Command(name string, arg ...string) ([]byte, error) { + ctx, cancel := context.WithTimeout(context.Background(), Timeout) + defer cancel() + return i.CommandWithContext(ctx, name, arg...) +} + +func (i Invoke) CommandWithContext(ctx context.Context, name string, arg ...string) ([]byte, error) { + cmd := exec.CommandContext(ctx, name, arg...) + + var buf bytes.Buffer + cmd.Stdout = &buf + cmd.Stderr = &buf + + if err := cmd.Start(); err != nil { + return buf.Bytes(), err + } + + if err := cmd.Wait(); err != nil { + return buf.Bytes(), err + } + + return buf.Bytes(), nil +} + +type FakeInvoke struct { + Suffix string // Suffix species expected file name suffix such as "fail" + Error error // If Error specfied, return the error. +} + +// Command in FakeInvoke returns from expected file if exists. +func (i FakeInvoke) Command(name string, arg ...string) ([]byte, error) { + if i.Error != nil { + return []byte{}, i.Error + } + + arch := runtime.GOOS + + commandName := filepath.Base(name) + + fname := strings.Join(append([]string{commandName}, arg...), "") + fname = url.QueryEscape(fname) + fpath := path.Join("testdata", arch, fname) + if i.Suffix != "" { + fpath += "_" + i.Suffix + } + if PathExists(fpath) { + return os.ReadFile(fpath) + } + return []byte{}, fmt.Errorf("could not find testdata: %s", fpath) +} + +func (i FakeInvoke) CommandWithContext(ctx context.Context, name string, arg ...string) ([]byte, error) { + return i.Command(name, arg...) +} + +var ErrNotImplementedError = errors.New("not implemented yet") + +// ReadFile reads contents from a file +func ReadFile(filename string) (string, error) { + content, err := os.ReadFile(filename) + if err != nil { + return "", err + } + + return string(content), nil +} + +// ReadLines reads contents from a file and splits them by new lines. +// A convenience wrapper to ReadLinesOffsetN(filename, 0, -1). +func ReadLines(filename string) ([]string, error) { + return ReadLinesOffsetN(filename, 0, -1) +} + +// ReadLines reads contents from file and splits them by new line. +// The offset tells at which line number to start. +// The count determines the number of lines to read (starting from offset): +// +// n >= 0: at most n lines +// n < 0: whole file +func ReadLinesOffsetN(filename string, offset uint, n int) ([]string, error) { + f, err := os.Open(filename) + if err != nil { + return []string{""}, err + } + defer func(f *os.File) { + err := f.Close() + if err != nil { + log.Fatalln(err) + } + }(f) + + var ret []string + + r := bufio.NewReader(f) + for i := 0; i < n+int(offset) || n < 0; i++ { + line, err := r.ReadString('\n') + if err != nil { + break + } + if i < int(offset) { + continue + } + ret = append(ret, strings.Trim(line, "\n")) + } + + return ret, nil +} + +func IntToString(orig []int8) string { + ret := make([]byte, len(orig)) + size := -1 + for i, o := range orig { + if o == 0 { + size = i + break + } + ret[i] = byte(o) + } + if size == -1 { + size = len(orig) + } + + return string(ret[0:size]) +} + +func UintToString(orig []uint8) string { + ret := make([]byte, len(orig)) + size := -1 + for i, o := range orig { + if o == 0 { + size = i + break + } + ret[i] = byte(o) + } + if size == -1 { + size = len(orig) + } + + return string(ret[0:size]) +} + +func ByteToString(orig []byte) string { + n := -1 + l := -1 + for i, b := range orig { + // skip left side null + if l == -1 && b == 0 { + continue + } + if l == -1 { + l = i + } + + if b == 0 { + break + } + n = i + 1 + } + if n == -1 { + return string(orig) + } + return string(orig[l:n]) +} + +// ReadInts reads contents from single line file and returns them as []int32. +func ReadInts(filename string) ([]int64, error) { + f, err := os.Open(filename) + if err != nil { + return []int64{}, err + } + defer func(f *os.File) { + err := f.Close() + if err != nil { + log.Fatalln(err) + } + }(f) + + var ret []int64 + + r := bufio.NewReader(f) + + // The int files that this is concerned with should only be one liners. + line, err := r.ReadString('\n') + if err != nil { + return []int64{}, err + } + + i, err := strconv.ParseInt(strings.Trim(line, "\n"), 10, 32) + if err != nil { + return []int64{}, err + } + ret = append(ret, i) + + return ret, nil +} + +// Parse Hex to uint32 without error +func HexToUint32(hex string) uint32 { + vv, _ := strconv.ParseUint(hex, 16, 32) + return uint32(vv) +} + +// Parse to int32 without error +func mustParseInt32(val string) int32 { + vv, _ := strconv.ParseInt(val, 10, 32) + return int32(vv) +} + +// Parse to uint64 without error +func mustParseUint64(val string) uint64 { + vv, _ := strconv.ParseInt(val, 10, 64) + return uint64(vv) +} + +// Parse to Float64 without error +func mustParseFloat64(val string) float64 { + vv, _ := strconv.ParseFloat(val, 64) + return vv +} + +// StringsHas checks the target string slice contains src or not +func StringsHas(target []string, src string) bool { + for _, t := range target { + if strings.TrimSpace(t) == src { + return true + } + } + return false +} + +// StringsContains checks the src in any string of the target string slice +func StringsContains(target []string, src string) bool { + for _, t := range target { + if strings.Contains(t, src) { + return true + } + } + return false +} + +// IntContains checks the src in any int of the target int slice. +func IntContains(target []int, src int) bool { + for _, t := range target { + if src == t { + return true + } + } + return false +} + +// get struct attributes. +// This method is used only for debugging platform dependent code. +func attributes(m interface{}) map[string]reflect.Type { + typ := reflect.TypeOf(m) + if typ.Kind() == reflect.Ptr { + typ = typ.Elem() + } + + attrs := make(map[string]reflect.Type) + if typ.Kind() != reflect.Struct { + return nil + } + + for i := 0; i < typ.NumField(); i++ { + p := typ.Field(i) + if !p.Anonymous { + attrs[p.Name] = p.Type + } + } + + return attrs +} + +func PathExists(filename string) bool { + if _, err := os.Stat(filename); err == nil { + return true + } + return false +} + +// GetEnv retrieves the environment variable key. If it does not exist it returns the default. +func GetEnv(key, dfault string, combineWith ...string) string { + value := os.Getenv(key) + if value == "" { + value = dfault + } + + switch len(combineWith) { + case 0: + return value + case 1: + return filepath.Join(value, combineWith[0]) + default: + all := make([]string, len(combineWith)+1) + all[0] = value + copy(all[1:], combineWith) + return filepath.Join(all...) + } +} + +func HostProc(combineWith ...string) string { + return GetEnv("HOST_PROC", "/proc", combineWith...) +} + +func HostSys(combineWith ...string) string { + return GetEnv("HOST_SYS", "/sys", combineWith...) +} + +func HostEtc(combineWith ...string) string { + return GetEnv("HOST_ETC", "/etc", combineWith...) +} + +func HostVar(combineWith ...string) string { + return GetEnv("HOST_VAR", "/var", combineWith...) +} + +func HostRun(combineWith ...string) string { + return GetEnv("HOST_RUN", "/run", combineWith...) +} + +func HostDev(combineWith ...string) string { + return GetEnv("HOST_DEV", "/dev", combineWith...) +} + +// getSysctrlEnv sets LC_ALL=C in a list of env vars for use when running +// sysctl commands (see DoSysctrl). +func getSysctrlEnv(env []string) []string { + foundLC := false + for i, line := range env { + if strings.HasPrefix(line, "LC_ALL") { + env[i] = "LC_ALL=C" + foundLC = true + } + } + if !foundLC { + env = append(env, "LC_ALL=C") + } + return env +} diff --git a/internal/gopsutil/common/common_darwin.go b/internal/gopsutil/common/common_darwin.go new file mode 100644 index 0000000..ffc7576 --- /dev/null +++ b/internal/gopsutil/common/common_darwin.go @@ -0,0 +1,69 @@ +//go:build darwin + +package common + +import ( + "context" + "os" + "os/exec" + "strings" + "unsafe" + + "golang.org/x/sys/unix" +) + +func DoSysctrlWithContext(ctx context.Context, mib string) ([]string, error) { + sysctl, err := exec.LookPath("sysctl") + if err != nil { + return []string{}, err + } + cmd := exec.CommandContext(ctx, sysctl, "-n", mib) + cmd.Env = getSysctrlEnv(os.Environ()) + out, err := cmd.Output() + if err != nil { + return []string{}, err + } + v := strings.Replace(string(out), "{ ", "", 1) + v = strings.Replace(string(v), " }", "", 1) + values := strings.Fields(string(v)) + + return values, nil +} + +func CallSyscall(mib []int32) ([]byte, uint64, error) { + miblen := uint64(len(mib)) + + // get required buffer size + length := uint64(0) + _, _, err := unix.Syscall6( + 202, // unix.SYS___SYSCTL https://github.com/golang/sys/blob/76b94024e4b621e672466e8db3d7f084e7ddcad2/unix/zsysnum_darwin_amd64.go#L146 + uintptr(unsafe.Pointer(&mib[0])), + uintptr(miblen), + 0, + uintptr(unsafe.Pointer(&length)), + 0, + 0) + if err != 0 { + var b []byte + return b, length, err + } + if length == 0 { + var b []byte + return b, length, err + } + // get proc info itself + buf := make([]byte, length) + _, _, err = unix.Syscall6( + 202, // unix.SYS___SYSCTL https://github.com/golang/sys/blob/76b94024e4b621e672466e8db3d7f084e7ddcad2/unix/zsysnum_darwin_amd64.go#L146 + uintptr(unsafe.Pointer(&mib[0])), + uintptr(miblen), + uintptr(unsafe.Pointer(&buf[0])), + uintptr(unsafe.Pointer(&length)), + 0, + 0) + if err != 0 { + return buf, length, err + } + + return buf, length, nil +} diff --git a/internal/gopsutil/common/common_freebsd.go b/internal/gopsutil/common/common_freebsd.go new file mode 100644 index 0000000..3364e34 --- /dev/null +++ b/internal/gopsutil/common/common_freebsd.go @@ -0,0 +1,85 @@ +//go:build freebsd || openbsd + +package common + +import ( + "fmt" + "os" + "os/exec" + "strings" + "unsafe" + + "golang.org/x/sys/unix" +) + +func SysctlUint(mib string) (uint64, error) { + buf, err := unix.SysctlRaw(mib) + if err != nil { + return 0, err + } + if len(buf) == 8 { // 64 bit + return *(*uint64)(unsafe.Pointer(&buf[0])), nil + } + if len(buf) == 4 { // 32bit + t := *(*uint32)(unsafe.Pointer(&buf[0])) + return uint64(t), nil + } + return 0, fmt.Errorf("unexpected size: %s, %d", mib, len(buf)) +} + +func DoSysctrl(mib string) ([]string, error) { + sysctl, err := exec.LookPath("sysctl") + if err != nil { + return []string{}, err + } + cmd := exec.Command(sysctl, "-n", mib) + cmd.Env = getSysctrlEnv(os.Environ()) + out, err := cmd.Output() + if err != nil { + return []string{}, err + } + v := strings.Replace(string(out), "{ ", "", 1) + v = strings.Replace(string(v), " }", "", 1) + values := strings.Fields(string(v)) + + return values, nil +} + +func CallSyscall(mib []int32) ([]byte, uint64, error) { + mibptr := unsafe.Pointer(&mib[0]) + miblen := uint64(len(mib)) + + // get required buffer size + length := uint64(0) + _, _, err := unix.Syscall6( + unix.SYS___SYSCTL, + uintptr(mibptr), + uintptr(miblen), + 0, + uintptr(unsafe.Pointer(&length)), + 0, + 0) + if err != 0 { + var b []byte + return b, length, err + } + if length == 0 { + var b []byte + return b, length, err + } + // get proc info itself + buf := make([]byte, length) + _, _, err = unix.Syscall6( + unix.SYS___SYSCTL, + uintptr(mibptr), + uintptr(miblen), + uintptr(unsafe.Pointer(&buf[0])), + uintptr(unsafe.Pointer(&length)), + 0, + 0) + if err != 0 { + return buf, length, err + } + + return buf, length, nil +} diff --git a/internal/gopsutil/common/common_linux.go b/internal/gopsutil/common/common_linux.go new file mode 100644 index 0000000..ae0f495 --- /dev/null +++ b/internal/gopsutil/common/common_linux.go @@ -0,0 +1,269 @@ +//go:build linux + +package common + +import ( + "context" + "fmt" + "log" + "os" + "os/exec" + "path/filepath" + "strconv" + "strings" + "time" +) + +func DoSysctrl(mib string) ([]string, error) { + sysctl, err := exec.LookPath("sysctl") + if err != nil { + return []string{}, err + } + cmd := exec.Command(sysctl, "-n", mib) + cmd.Env = getSysctrlEnv(os.Environ()) + out, err := cmd.Output() + if err != nil { + return []string{}, err + } + v := strings.Replace(string(out), "{ ", "", 1) + v = strings.Replace(string(v), " }", "", 1) + values := strings.Fields(string(v)) + + return values, nil +} + +func NumProcs() (uint64, error) { + f, err := os.Open(HostProc()) + if err != nil { + return 0, err + } + defer func(f *os.File) { + err := f.Close() + if err != nil { + log.Fatalln(err) + } + }(f) + + list, err := f.Readdirnames(-1) + if err != nil { + return 0, err + } + var cnt uint64 + + for _, v := range list { + if _, err = strconv.ParseUint(v, 10, 64); err == nil { + cnt++ + } + } + + return cnt, nil +} + +func BootTimeWithContext(ctx context.Context) (uint64, error) { + system, role, err := Virtualization() + if err != nil { + return 0, err + } + + statFile := "stat" + if system == "lxc" && role == "guest" { + // if lxc, /proc/uptime is used. + statFile = "uptime" + } else if system == "docker" && role == "guest" { + // also docker, guest + statFile = "uptime" + } + + filename := HostProc(statFile) + lines, err := ReadLines(filename) + if err != nil { + return 0, err + } + + if statFile == "stat" { + for _, line := range lines { + if strings.HasPrefix(line, "btime") { + f := strings.Fields(line) + if len(f) != 2 { + return 0, fmt.Errorf("wrong btime format") + } + b, err := strconv.ParseInt(f[1], 10, 64) + if err != nil { + return 0, err + } + t := uint64(b) + return t, nil + } + } + } else if statFile == "uptime" { + if len(lines) != 1 { + return 0, fmt.Errorf("wrong uptime format") + } + f := strings.Fields(lines[0]) + b, err := strconv.ParseFloat(f[0], 64) + if err != nil { + return 0, err + } + t := uint64(time.Now().Unix()) - uint64(b) + return t, nil + } + + return 0, fmt.Errorf("could not find btime") +} + +func Virtualization() (string, string, error) { + return VirtualizationWithContext(context.Background()) +} + +func VirtualizationWithContext(ctx context.Context) (string, string, error) { + var system string + var role string + + filename := HostProc("xen") + if PathExists(filename) { + system = "xen" + role = "guest" // assume guest + + if PathExists(filepath.Join(filename, "capabilities")) { + contents, err := ReadLines(filepath.Join(filename, "capabilities")) + if err == nil { + if StringsContains(contents, "control_d") { + role = "host" + } + } + } + } + + filename = HostProc("modules") + if PathExists(filename) { + contents, err := ReadLines(filename) + if err == nil { + if StringsContains(contents, "kvm") { + system = "kvm" + role = "host" + } else if StringsContains(contents, "vboxdrv") { + system = "vbox" + role = "host" + } else if StringsContains(contents, "vboxguest") { + system = "vbox" + role = "guest" + } else if StringsContains(contents, "vmware") { + system = "vmware" + role = "guest" + } + } + } + + filename = HostProc("cpuinfo") + if PathExists(filename) { + contents, err := ReadLines(filename) + if err == nil { + if StringsContains(contents, "QEMU Virtual CPU") || + StringsContains(contents, "Common KVM processor") || + StringsContains(contents, "Common 32-bit KVM processor") { + system = "kvm" + role = "guest" + } + } + } + + filename = HostProc("bus/pci/devices") + if PathExists(filename) { + contents, err := ReadLines(filename) + if err == nil { + if StringsContains(contents, "virtio-pci") { + role = "guest" + } + } + } + + filename = HostProc() + if PathExists(filepath.Join(filename, "bc", "0")) { + system = "openvz" + role = "host" + } else if PathExists(filepath.Join(filename, "vz")) { + system = "openvz" + role = "guest" + } + + // not use dmidecode because it requires root + if PathExists(filepath.Join(filename, "self", "status")) { + contents, err := ReadLines(filepath.Join(filename, "self", "status")) + if err == nil { + if StringsContains(contents, "s_context:") || + StringsContains(contents, "VxID:") { + system = "linux-vserver" + } + // TODO: guest or host + } + } + + if PathExists(filepath.Join(filename, "1", "environ")) { + contents, err := ReadFile(filepath.Join(filename, "1", "environ")) + + if err == nil { + if strings.Contains(contents, "container=lxc") { + system = "lxc" + role = "guest" + } + } + } + + if PathExists(filepath.Join(filename, "self", "cgroup")) { + contents, err := ReadLines(filepath.Join(filename, "self", "cgroup")) + if err == nil { + if StringsContains(contents, "lxc") { + system = "lxc" + role = "guest" + } else if StringsContains(contents, "docker") { + system = "docker" + role = "guest" + } else if StringsContains(contents, "machine-rkt") { + system = "rkt" + role = "guest" + } else if PathExists("/usr/bin/lxc-version") { + system = "lxc" + role = "host" + } + } + } + + if PathExists(HostEtc("os-release")) { + p, _, err := GetOSRelease() + if err == nil && p == "coreos" { + system = "rkt" // Is it true? + role = "host" + } + } + return system, role, nil +} + +func GetOSRelease() (platform, version string, err error) { + contents, err := ReadLines(HostEtc("os-release")) + if err != nil { + return "", "", nil // return empty + } + for _, line := range contents { + field := strings.Split(line, "=") + if len(field) < 2 { + continue + } + switch field[0] { + case "ID": // use ID for lowercase + platform = trimQuotes(field[1]) + case "VERSION": + version = trimQuotes(field[1]) + } + } + return platform, version, nil +} + +// Remove quotes of the source string +func trimQuotes(s string) string { + if len(s) >= 2 { + if s[0] == '"' && s[len(s)-1] == '"' { + return s[1 : len(s)-1] + } + } + return s +} diff --git a/internal/gopsutil/common/common_openbsd.go b/internal/gopsutil/common/common_openbsd.go new file mode 100644 index 0000000..262d430 --- /dev/null +++ b/internal/gopsutil/common/common_openbsd.go @@ -0,0 +1,69 @@ +//go:build openbsd + +package common + +import ( + "os" + "os/exec" + "strings" + "unsafe" + + "golang.org/x/sys/unix" +) + +func DoSysctrl(mib string) ([]string, error) { + sysctl, err := exec.LookPath("sysctl") + if err != nil { + return []string{}, err + } + cmd := exec.Command(sysctl, "-n", mib) + cmd.Env = getSysctrlEnv(os.Environ()) + out, err := cmd.Output() + if err != nil { + return []string{}, err + } + v := strings.Replace(string(out), "{ ", "", 1) + v = strings.Replace(string(v), " }", "", 1) + values := strings.Fields(string(v)) + + return values, nil +} + +func CallSyscall(mib []int32) ([]byte, uint64, error) { + mibptr := unsafe.Pointer(&mib[0]) + miblen := uint64(len(mib)) + + // get required buffer size + length := uint64(0) + _, _, err := unix.Syscall6( + unix.SYS___SYSCTL, + uintptr(mibptr), + uintptr(miblen), + 0, + uintptr(unsafe.Pointer(&length)), + 0, + 0) + if err != 0 { + var b []byte + return b, length, err + } + if length == 0 { + var b []byte + return b, length, err + } + // get proc info itself + buf := make([]byte, length) + _, _, err = unix.Syscall6( + unix.SYS___SYSCTL, + uintptr(mibptr), + uintptr(miblen), + uintptr(unsafe.Pointer(&buf[0])), + uintptr(unsafe.Pointer(&length)), + 0, + 0) + if err != 0 { + return buf, length, err + } + + return buf, length, nil +} diff --git a/internal/gopsutil/common/common_unix.go b/internal/gopsutil/common/common_unix.go new file mode 100644 index 0000000..cbe32ef --- /dev/null +++ b/internal/gopsutil/common/common_unix.go @@ -0,0 +1,66 @@ +//go:build linux || freebsd || darwin || openbsd + +package common + +import ( + "context" + "os/exec" + "strconv" + "strings" +) + +func CallLsofWithContext(ctx context.Context, invoke Invoker, pid int32, args ...string) ([]string, error) { + var cmd []string + if pid == 0 { // will get from all processes. + cmd = []string{"-a", "-n", "-P"} + } else { + cmd = []string{"-a", "-n", "-P", "-p", strconv.Itoa(int(pid))} + } + cmd = append(cmd, args...) + lsof, err := exec.LookPath("lsof") + if err != nil { + return []string{}, err + } + out, err := invoke.CommandWithContext(ctx, lsof, cmd...) + if err != nil { + // if no pid found, lsof returns code 1. + if err.Error() == "exit status 1" && len(out) == 0 { + return []string{}, nil + } + } + lines := strings.Split(string(out), "\n") + + var ret []string + for _, l := range lines[1:] { + if len(l) == 0 { + continue + } + ret = append(ret, l) + } + return ret, nil +} + +func CallPgrepWithContext(ctx context.Context, invoke Invoker, pid int32) ([]int32, error) { + cmd := []string{"-P", strconv.Itoa(int(pid))} + pgrep, err := exec.LookPath("pgrep") + if err != nil { + return []int32{}, err + } + out, err := invoke.CommandWithContext(ctx, pgrep, cmd...) + if err != nil { + return []int32{}, err + } + lines := strings.Split(string(out), "\n") + ret := make([]int32, 0, len(lines)) + for _, l := range lines { + if len(l) == 0 { + continue + } + i, err := strconv.Atoi(l) + if err != nil { + continue + } + ret = append(ret, int32(i)) + } + return ret, nil +} diff --git a/internal/gopsutil/common/common_windows.go b/internal/gopsutil/common/common_windows.go new file mode 100644 index 0000000..ebd7170 --- /dev/null +++ b/internal/gopsutil/common/common_windows.go @@ -0,0 +1,234 @@ +//go:build windows + +package common + +import ( + "context" + "fmt" + "path/filepath" + "strings" + "syscall" + "unsafe" + + "golang.org/x/sys/windows" + + "github.com/gofiber/fiber/v2/internal/wmi" +) + +// for double values +type PDH_FMT_COUNTERVALUE_DOUBLE struct { + CStatus uint32 + DoubleValue float64 +} + +// for 64 bit integer values +type PDH_FMT_COUNTERVALUE_LARGE struct { + CStatus uint32 + LargeValue int64 +} + +// for long values +type PDH_FMT_COUNTERVALUE_LONG struct { + CStatus uint32 + LongValue int32 + padding [4]byte +} + +// windows system const +const ( + ERROR_SUCCESS = 0 + ERROR_FILE_NOT_FOUND = 2 + DRIVE_REMOVABLE = 2 + DRIVE_FIXED = 3 + HKEY_LOCAL_MACHINE = 0x80000002 + RRF_RT_REG_SZ = 0x00000002 + RRF_RT_REG_DWORD = 0x00000010 + PDH_FMT_LONG = 0x00000100 + PDH_FMT_DOUBLE = 0x00000200 + PDH_FMT_LARGE = 0x00000400 + PDH_INVALID_DATA = 0xc0000bc6 + PDH_INVALID_HANDLE = 0xC0000bbc + PDH_NO_DATA = 0x800007d5 +) + +const ( + ProcessBasicInformation = 0 + ProcessWow64Information = 26 +) + +var ( + Modkernel32 = windows.NewLazySystemDLL("kernel32.dll") + ModNt = windows.NewLazySystemDLL("ntdll.dll") + ModPdh = windows.NewLazySystemDLL("pdh.dll") + ModPsapi = windows.NewLazySystemDLL("psapi.dll") + + ProcGetSystemTimes = Modkernel32.NewProc("GetSystemTimes") + ProcNtQuerySystemInformation = ModNt.NewProc("NtQuerySystemInformation") + ProcRtlGetNativeSystemInformation = ModNt.NewProc("RtlGetNativeSystemInformation") + ProcRtlNtStatusToDosError = ModNt.NewProc("RtlNtStatusToDosError") + ProcNtQueryInformationProcess = ModNt.NewProc("NtQueryInformationProcess") + ProcNtReadVirtualMemory = ModNt.NewProc("NtReadVirtualMemory") + ProcNtWow64QueryInformationProcess64 = ModNt.NewProc("NtWow64QueryInformationProcess64") + ProcNtWow64ReadVirtualMemory64 = ModNt.NewProc("NtWow64ReadVirtualMemory64") + + PdhOpenQuery = ModPdh.NewProc("PdhOpenQuery") + PdhAddEnglishCounterW = ModPdh.NewProc("PdhAddEnglishCounterW") + PdhAddCounter = ModPdh.NewProc("PdhAddCounterW") + PdhCollectQueryData = ModPdh.NewProc("PdhCollectQueryData") + PdhGetFormattedCounterValue = ModPdh.NewProc("PdhGetFormattedCounterValue") + PdhCloseQuery = ModPdh.NewProc("PdhCloseQuery") + + procQueryDosDeviceW = Modkernel32.NewProc("QueryDosDeviceW") +) + +type FILETIME struct { + DwLowDateTime uint32 + DwHighDateTime uint32 +} + +// borrowed from net/interface_windows.go +func BytePtrToString(p *uint8) string { + a := (*[10000]uint8)(unsafe.Pointer(p)) + i := 0 + for a[i] != 0 { + i++ + } + return string(a[:i]) +} + +// CounterInfo +// copied from https://github.com/mackerelio/mackerel-agent/ +type CounterInfo struct { + PostName string + CounterName string + Counter windows.Handle +} + +// CreateQuery XXX +// copied from https://github.com/mackerelio/mackerel-agent/ +func CreateQuery() (windows.Handle, error) { + var query windows.Handle + r, _, err := PdhOpenQuery.Call(0, 0, uintptr(unsafe.Pointer(&query))) + if r != 0 { + return 0, err + } + return query, nil +} + +// CreateCounter XXX +func CreateCounter(query windows.Handle, pname, cname string) (*CounterInfo, error) { + var counter windows.Handle + r, _, err := PdhAddCounter.Call( + uintptr(query), + uintptr(unsafe.Pointer(windows.StringToUTF16Ptr(cname))), + 0, + uintptr(unsafe.Pointer(&counter))) + if r != 0 { + return nil, err + } + return &CounterInfo{ + PostName: pname, + CounterName: cname, + Counter: counter, + }, nil +} + +// GetCounterValue get counter value from handle +// adapted from https://github.com/mackerelio/mackerel-agent/ +func GetCounterValue(counter windows.Handle) (float64, error) { + var value PDH_FMT_COUNTERVALUE_DOUBLE + r, _, err := PdhGetFormattedCounterValue.Call(uintptr(counter), PDH_FMT_DOUBLE, uintptr(0), uintptr(unsafe.Pointer(&value))) + if r != 0 && r != PDH_INVALID_DATA { + return 0.0, err + } + return value.DoubleValue, nil +} + +type Win32PerformanceCounter struct { + PostName string + CounterName string + Query windows.Handle + Counter windows.Handle +} + +func NewWin32PerformanceCounter(postName, counterName string) (*Win32PerformanceCounter, error) { + query, err := CreateQuery() + if err != nil { + return nil, err + } + counter := Win32PerformanceCounter{ + Query: query, + PostName: postName, + CounterName: counterName, + } + r, _, err := PdhAddEnglishCounterW.Call( + uintptr(counter.Query), + uintptr(unsafe.Pointer(windows.StringToUTF16Ptr(counter.CounterName))), + 0, + uintptr(unsafe.Pointer(&counter.Counter)), + ) + if r != 0 { + return nil, err + } + return &counter, nil +} + +func (w *Win32PerformanceCounter) GetValue() (float64, error) { + r, _, err := PdhCollectQueryData.Call(uintptr(w.Query)) + if r != 0 && err != nil { + if r == PDH_NO_DATA { + return 0.0, fmt.Errorf("%w: this counter has not data", err) + } + return 0.0, err + } + + return GetCounterValue(w.Counter) +} + +func ProcessorQueueLengthCounter() (*Win32PerformanceCounter, error) { + return NewWin32PerformanceCounter("processor_queue_length", `\System\Processor Queue Length`) +} + +// WMIQueryWithContext - wraps wmi.Query with a timed-out context to avoid hanging +func WMIQueryWithContext(ctx context.Context, query string, dst interface{}, connectServerArgs ...interface{}) error { + if _, ok := ctx.Deadline(); !ok { + ctxTimeout, cancel := context.WithTimeout(ctx, Timeout) + defer cancel() + ctx = ctxTimeout + } + + errChan := make(chan error, 1) + go func() { + errChan <- wmi.Query(query, dst, connectServerArgs...) + }() + + select { + case <-ctx.Done(): + return ctx.Err() + case err := <-errChan: + return err + } +} + +// Convert paths using native DOS format like: +// +// "\Device\HarddiskVolume1\Windows\systemew\file.txt" +// +// into: +// +// "C:\Windows\systemew\file.txt" +func ConvertDOSPath(p string) string { + rawDrive := strings.Join(strings.Split(p, `\`)[:3], `\`) + + for d := 'A'; d <= 'Z'; d++ { + szDeviceName := string(d) + ":" + szTarget := make([]uint16, 512) + ret, _, _ := procQueryDosDeviceW.Call(uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr(szDeviceName))), + uintptr(unsafe.Pointer(&szTarget[0])), + uintptr(len(szTarget))) + if ret != 0 && windows.UTF16ToString(szTarget[:]) == rawDrive { + return filepath.Join(szDeviceName, p[len(rawDrive):]) + } + } + return p +} diff --git a/internal/gopsutil/common/sleep.go b/internal/gopsutil/common/sleep.go new file mode 100644 index 0000000..9bed241 --- /dev/null +++ b/internal/gopsutil/common/sleep.go @@ -0,0 +1,21 @@ +package common + +import ( + "context" + "time" +) + +// Sleep awaits for provided interval. +// Can be interrupted by context cancelation. +func Sleep(ctx context.Context, interval time.Duration) error { + timer := time.NewTimer(interval) + select { + case <-ctx.Done(): + if !timer.Stop() { + <-timer.C + } + return ctx.Err() + case <-timer.C: + return nil + } +} diff --git a/internal/gopsutil/cpu/cpu.go b/internal/gopsutil/cpu/cpu.go new file mode 100644 index 0000000..5814d78 --- /dev/null +++ b/internal/gopsutil/cpu/cpu.go @@ -0,0 +1,187 @@ +package cpu + +import ( + "context" + "encoding/json" + "fmt" + "math" + "strconv" + "strings" + "sync" + "time" + + "github.com/gofiber/fiber/v2/internal/gopsutil/common" +) + +// TimesStat contains the amounts of time the CPU has spent performing different +// kinds of work. Time units are in seconds. It is based on linux /proc/stat file. +type TimesStat struct { + CPU string `json:"cpu"` + User float64 `json:"user"` + System float64 `json:"system"` + Idle float64 `json:"idle"` + Nice float64 `json:"nice"` + Iowait float64 `json:"iowait"` + Irq float64 `json:"irq"` + Softirq float64 `json:"softirq"` + Steal float64 `json:"steal"` + Guest float64 `json:"guest"` + GuestNice float64 `json:"guestNice"` +} + +type InfoStat struct { + CPU int32 `json:"cpu"` + VendorID string `json:"vendorId"` + Family string `json:"family"` + Model string `json:"model"` + Stepping int32 `json:"stepping"` + PhysicalID string `json:"physicalId"` + CoreID string `json:"coreId"` + Cores int32 `json:"cores"` + ModelName string `json:"modelName"` + Mhz float64 `json:"mhz"` + CacheSize int32 `json:"cacheSize"` + Flags []string `json:"flags"` + Microcode string `json:"microcode"` +} + +type lastPercent struct { + sync.Mutex + lastCPUTimes []TimesStat + lastPerCPUTimes []TimesStat +} + +var ( + lastCPUPercent lastPercent + invoke common.Invoker = common.Invoke{} +) + +func init() { + lastCPUPercent.Lock() + lastCPUPercent.lastCPUTimes, _ = Times(false) + lastCPUPercent.lastPerCPUTimes, _ = Times(true) + lastCPUPercent.Unlock() +} + +// Counts returns the number of physical or logical cores in the system +func Counts(logical bool) (int, error) { + return CountsWithContext(context.Background(), logical) +} + +func (c TimesStat) String() string { + v := []string{ + `"cpu":"` + c.CPU + `"`, + `"user":` + strconv.FormatFloat(c.User, 'f', 1, 64), + `"system":` + strconv.FormatFloat(c.System, 'f', 1, 64), + `"idle":` + strconv.FormatFloat(c.Idle, 'f', 1, 64), + `"nice":` + strconv.FormatFloat(c.Nice, 'f', 1, 64), + `"iowait":` + strconv.FormatFloat(c.Iowait, 'f', 1, 64), + `"irq":` + strconv.FormatFloat(c.Irq, 'f', 1, 64), + `"softirq":` + strconv.FormatFloat(c.Softirq, 'f', 1, 64), + `"steal":` + strconv.FormatFloat(c.Steal, 'f', 1, 64), + `"guest":` + strconv.FormatFloat(c.Guest, 'f', 1, 64), + `"guestNice":` + strconv.FormatFloat(c.GuestNice, 'f', 1, 64), + } + + return `{` + strings.Join(v, ",") + `}` +} + +// Total returns the total number of seconds in a CPUTimesStat +func (c TimesStat) Total() float64 { + total := c.User + c.System + c.Nice + c.Iowait + c.Irq + c.Softirq + + c.Steal + c.Idle + return total +} + +func (c InfoStat) String() string { + s, _ := json.Marshal(c) + return string(s) +} + +func getAllBusy(t TimesStat) (float64, float64) { + busy := t.User + t.System + t.Nice + t.Iowait + t.Irq + + t.Softirq + t.Steal + return busy + t.Idle, busy +} + +func calculateBusy(t1, t2 TimesStat) float64 { + t1All, t1Busy := getAllBusy(t1) + t2All, t2Busy := getAllBusy(t2) + + if t2Busy <= t1Busy { + return 0 + } + if t2All <= t1All { + return 100 + } + return math.Min(100, math.Max(0, (t2Busy-t1Busy)/(t2All-t1All)*100)) +} + +func calculateAllBusy(t1, t2 []TimesStat) ([]float64, error) { + // Make sure the CPU measurements have the same length. + if len(t1) != len(t2) { + return nil, fmt.Errorf( + "received two CPU counts: %d != %d", + len(t1), len(t2), + ) + } + + ret := make([]float64, len(t1)) + for i, t := range t2 { + ret[i] = calculateBusy(t1[i], t) + } + return ret, nil +} + +// Percent calculates the percentage of cpu used either per CPU or combined. +// If an interval of 0 is given it will compare the current cpu times against the last call. +// Returns one value per cpu, or a single value if percpu is set to false. +func Percent(interval time.Duration, percpu bool) ([]float64, error) { + return PercentWithContext(context.Background(), interval, percpu) +} + +func PercentWithContext(ctx context.Context, interval time.Duration, percpu bool) ([]float64, error) { + if interval <= 0 { + return percentUsedFromLastCall(percpu) + } + + // Get CPU usage at the start of the interval. + cpuTimes1, err := Times(percpu) + if err != nil { + return nil, err + } + + if err := common.Sleep(ctx, interval); err != nil { + return nil, err + } + + // And at the end of the interval. + cpuTimes2, err := Times(percpu) + if err != nil { + return nil, err + } + + return calculateAllBusy(cpuTimes1, cpuTimes2) +} + +func percentUsedFromLastCall(percpu bool) ([]float64, error) { + cpuTimes, err := Times(percpu) + if err != nil { + return nil, err + } + lastCPUPercent.Lock() + defer lastCPUPercent.Unlock() + var lastTimes []TimesStat + if percpu { + lastTimes = lastCPUPercent.lastPerCPUTimes + lastCPUPercent.lastPerCPUTimes = cpuTimes + } else { + lastTimes = lastCPUPercent.lastCPUTimes + lastCPUPercent.lastCPUTimes = cpuTimes + } + + if lastTimes == nil { + return nil, fmt.Errorf("error getting times for cpu percent. lastTimes was nil") + } + return calculateAllBusy(lastTimes, cpuTimes) +} diff --git a/internal/gopsutil/cpu/cpu_darwin.go b/internal/gopsutil/cpu/cpu_darwin.go new file mode 100644 index 0000000..936f26f --- /dev/null +++ b/internal/gopsutil/cpu/cpu_darwin.go @@ -0,0 +1,119 @@ +//go:build darwin + +package cpu + +import ( + "context" + "os/exec" + "strconv" + "strings" + + "golang.org/x/sys/unix" +) + +// sys/resource.h +const ( + CPUser = 0 + CPNice = 1 + CPSys = 2 + CPIntr = 3 + CPIdle = 4 + CPUStates = 5 +) + +// default value. from time.h +var ClocksPerSec = float64(128) + +func init() { + getconf, err := exec.LookPath("getconf") + if err != nil { + return + } + out, err := invoke.Command(getconf, "CLK_TCK") + // ignore errors + if err == nil { + i, err := strconv.ParseFloat(strings.TrimSpace(string(out)), 64) + if err == nil { + ClocksPerSec = float64(i) + } + } +} + +func Times(percpu bool) ([]TimesStat, error) { + return TimesWithContext(context.Background(), percpu) +} + +func TimesWithContext(ctx context.Context, percpu bool) ([]TimesStat, error) { + if percpu { + return perCPUTimes() + } + + return allCPUTimes() +} + +// Returns only one CPUInfoStat on FreeBSD +func Info() ([]InfoStat, error) { + return InfoWithContext(context.Background()) +} + +func InfoWithContext(ctx context.Context) ([]InfoStat, error) { + var ret []InfoStat + + c := InfoStat{} + c.ModelName, _ = unix.Sysctl("machdep.cpu.brand_string") + family, _ := unix.SysctlUint32("machdep.cpu.family") + c.Family = strconv.FormatUint(uint64(family), 10) + model, _ := unix.SysctlUint32("machdep.cpu.model") + c.Model = strconv.FormatUint(uint64(model), 10) + stepping, _ := unix.SysctlUint32("machdep.cpu.stepping") + c.Stepping = int32(stepping) + features, err := unix.Sysctl("machdep.cpu.features") + if err == nil { + for _, v := range strings.Fields(features) { + c.Flags = append(c.Flags, strings.ToLower(v)) + } + } + leaf7Features, err := unix.Sysctl("machdep.cpu.leaf7_features") + if err == nil { + for _, v := range strings.Fields(leaf7Features) { + c.Flags = append(c.Flags, strings.ToLower(v)) + } + } + extfeatures, err := unix.Sysctl("machdep.cpu.extfeatures") + if err == nil { + for _, v := range strings.Fields(extfeatures) { + c.Flags = append(c.Flags, strings.ToLower(v)) + } + } + cores, _ := unix.SysctlUint32("machdep.cpu.core_count") + c.Cores = int32(cores) + cacheSize, _ := unix.SysctlUint32("machdep.cpu.cache.size") + c.CacheSize = int32(cacheSize) + c.VendorID, _ = unix.Sysctl("machdep.cpu.vendor") + + // Use the rated frequency of the CPU. This is a static value and does not + // account for low power or Turbo Boost modes. + cpuFrequency, err := unix.SysctlUint64("hw.cpufrequency") + if err != nil { + return ret, err + } + c.Mhz = float64(cpuFrequency) / 1000000.0 + + return append(ret, c), nil +} + +func CountsWithContext(ctx context.Context, logical bool) (int, error) { + var cpuArgument string + if logical { + cpuArgument = "hw.logicalcpu" + } else { + cpuArgument = "hw.physicalcpu" + } + + count, err := unix.SysctlUint32(cpuArgument) + if err != nil { + return 0, err + } + + return int(count), nil +} diff --git a/internal/gopsutil/cpu/cpu_darwin_cgo.go b/internal/gopsutil/cpu/cpu_darwin_cgo.go new file mode 100644 index 0000000..0ac8804 --- /dev/null +++ b/internal/gopsutil/cpu/cpu_darwin_cgo.go @@ -0,0 +1,110 @@ +//go:build darwin && cgo + +package cpu + +/* +#include +#include +#include +#include +#include +#include +#include +#if TARGET_OS_MAC +#include +#endif +#include +#include +*/ +import "C" + +import ( + "bytes" + "encoding/binary" + "fmt" + "unsafe" +) + +// these CPU times for darwin is borrowed from influxdb/telegraf. + +func perCPUTimes() ([]TimesStat, error) { + var ( + count C.mach_msg_type_number_t + cpuload *C.processor_cpu_load_info_data_t + ncpu C.natural_t + ) + + status := C.host_processor_info(C.host_t(C.mach_host_self()), + C.PROCESSOR_CPU_LOAD_INFO, + &ncpu, + (*C.processor_info_array_t)(unsafe.Pointer(&cpuload)), + &count) + + if status != C.KERN_SUCCESS { + return nil, fmt.Errorf("host_processor_info error=%d", status) + } + + // jump through some cgo casting hoops and ensure we properly free + // the memory that cpuload points to + target := C.vm_map_t(C.mach_task_self_) + address := C.vm_address_t(uintptr(unsafe.Pointer(cpuload))) + defer C.vm_deallocate(target, address, C.vm_size_t(ncpu)) + + // the body of struct processor_cpu_load_info + // aka processor_cpu_load_info_data_t + var cpu_ticks [C.CPU_STATE_MAX]uint32 + + // copy the cpuload array to a []byte buffer + // where we can binary.Read the data + size := int(ncpu) * binary.Size(cpu_ticks) + buf := (*[1 << 30]byte)(unsafe.Pointer(cpuload))[:size:size] + + bbuf := bytes.NewBuffer(buf) + + var ret []TimesStat + + for i := 0; i < int(ncpu); i++ { + err := binary.Read(bbuf, binary.LittleEndian, &cpu_ticks) + if err != nil { + return nil, err + } + + c := TimesStat{ + CPU: fmt.Sprintf("cpu%d", i), + User: float64(cpu_ticks[C.CPU_STATE_USER]) / ClocksPerSec, + System: float64(cpu_ticks[C.CPU_STATE_SYSTEM]) / ClocksPerSec, + Nice: float64(cpu_ticks[C.CPU_STATE_NICE]) / ClocksPerSec, + Idle: float64(cpu_ticks[C.CPU_STATE_IDLE]) / ClocksPerSec, + } + + ret = append(ret, c) + } + + return ret, nil +} + +func allCPUTimes() ([]TimesStat, error) { + var count C.mach_msg_type_number_t + var cpuload C.host_cpu_load_info_data_t + + count = C.HOST_CPU_LOAD_INFO_COUNT + + status := C.host_statistics(C.host_t(C.mach_host_self()), + C.HOST_CPU_LOAD_INFO, + C.host_info_t(unsafe.Pointer(&cpuload)), + &count) + + if status != C.KERN_SUCCESS { + return nil, fmt.Errorf("host_statistics error=%d", status) + } + + c := TimesStat{ + CPU: "cpu-total", + User: float64(cpuload.cpu_ticks[C.CPU_STATE_USER]) / ClocksPerSec, + System: float64(cpuload.cpu_ticks[C.CPU_STATE_SYSTEM]) / ClocksPerSec, + Nice: float64(cpuload.cpu_ticks[C.CPU_STATE_NICE]) / ClocksPerSec, + Idle: float64(cpuload.cpu_ticks[C.CPU_STATE_IDLE]) / ClocksPerSec, + } + + return []TimesStat{c}, nil +} diff --git a/internal/gopsutil/cpu/cpu_darwin_nocgo.go b/internal/gopsutil/cpu/cpu_darwin_nocgo.go new file mode 100644 index 0000000..b2eedef --- /dev/null +++ b/internal/gopsutil/cpu/cpu_darwin_nocgo.go @@ -0,0 +1,13 @@ +//go:build darwin && !cgo + +package cpu + +import "github.com/gofiber/fiber/v2/internal/gopsutil/common" + +func perCPUTimes() ([]TimesStat, error) { + return []TimesStat{}, common.ErrNotImplementedError +} + +func allCPUTimes() ([]TimesStat, error) { + return []TimesStat{}, common.ErrNotImplementedError +} diff --git a/internal/gopsutil/cpu/cpu_dragonfly.go b/internal/gopsutil/cpu/cpu_dragonfly.go new file mode 100644 index 0000000..8c51580 --- /dev/null +++ b/internal/gopsutil/cpu/cpu_dragonfly.go @@ -0,0 +1,164 @@ +package cpu + +import ( + "context" + "fmt" + "os/exec" + "reflect" + "regexp" + "runtime" + "strconv" + "strings" + "unsafe" + + "golang.org/x/sys/unix" + + "github.com/gofiber/fiber/v2/internal/gopsutil/common" +) + +var ( + ClocksPerSec = float64(128) + cpuMatch = regexp.MustCompile(`^CPU:`) + originMatch = regexp.MustCompile(`Origin\s*=\s*"(.+)"\s+Id\s*=\s*(.+)\s+Stepping\s*=\s*(.+)`) + featuresMatch = regexp.MustCompile(`Features=.+<(.+)>`) + featuresMatch2 = regexp.MustCompile(`Features2=[a-f\dx]+<(.+)>`) + cpuEnd = regexp.MustCompile(`^Trying to mount root`) + cpuTimesSize int + emptyTimes cpuTimes +) + +func init() { + getconf, err := exec.LookPath("getconf") + if err != nil { + return + } + out, err := invoke.Command(getconf, "CLK_TCK") + // ignore errors + if err == nil { + i, err := strconv.ParseFloat(strings.TrimSpace(string(out)), 64) + if err == nil { + ClocksPerSec = float64(i) + } + } +} + +func timeStat(name string, t *cpuTimes) *TimesStat { + return &TimesStat{ + User: float64(t.User) / ClocksPerSec, + Nice: float64(t.Nice) / ClocksPerSec, + System: float64(t.Sys) / ClocksPerSec, + Idle: float64(t.Idle) / ClocksPerSec, + Irq: float64(t.Intr) / ClocksPerSec, + CPU: name, + } +} + +func Times(percpu bool) ([]TimesStat, error) { + return TimesWithContext(context.Background(), percpu) +} + +func TimesWithContext(ctx context.Context, percpu bool) ([]TimesStat, error) { + if percpu { + buf, err := unix.SysctlRaw("kern.cp_times") + if err != nil { + return nil, err + } + + // We can't do this in init due to the conflict with cpu.init() + if cpuTimesSize == 0 { + cpuTimesSize = int(reflect.TypeOf(cpuTimes{}).Size()) + } + + ncpus := len(buf) / cpuTimesSize + ret := make([]TimesStat, 0, ncpus) + for i := 0; i < ncpus; i++ { + times := (*cpuTimes)(unsafe.Pointer(&buf[i*cpuTimesSize])) + if *times == emptyTimes { + // CPU not present + continue + } + ret = append(ret, *timeStat(fmt.Sprintf("cpu%d", len(ret)), times)) + } + return ret, nil + } + + buf, err := unix.SysctlRaw("kern.cp_time") + if err != nil { + return nil, err + } + + times := (*cpuTimes)(unsafe.Pointer(&buf[0])) + return []TimesStat{*timeStat("cpu-total", times)}, nil +} + +// Returns only one InfoStat on DragonflyBSD. The information regarding core +// count, however is accurate and it is assumed that all InfoStat attributes +// are the same across CPUs. +func Info() ([]InfoStat, error) { + return InfoWithContext(context.Background()) +} + +func InfoWithContext(ctx context.Context) ([]InfoStat, error) { + const dmesgBoot = "/var/run/dmesg.boot" + + c, err := parseDmesgBoot(dmesgBoot) + if err != nil { + return nil, err + } + + var u32 uint32 + if u32, err = unix.SysctlUint32("hw.clockrate"); err != nil { + return nil, err + } + c.Mhz = float64(u32) + + var num int + var buf string + if buf, err = unix.Sysctl("hw.cpu_topology.tree"); err != nil { + return nil, err + } + num = strings.Count(buf, "CHIP") + c.Cores = int32(strings.Count(string(buf), "CORE") / num) + + if c.ModelName, err = unix.Sysctl("hw.model"); err != nil { + return nil, err + } + + ret := make([]InfoStat, num) + for i := 0; i < num; i++ { + ret[i] = c + } + + return ret, nil +} + +func parseDmesgBoot(fileName string) (InfoStat, error) { + c := InfoStat{} + lines, _ := common.ReadLines(fileName) + for _, line := range lines { + if matches := cpuEnd.FindStringSubmatch(line); matches != nil { + break + } else if matches := originMatch.FindStringSubmatch(line); matches != nil { + c.VendorID = matches[1] + t, err := strconv.ParseInt(matches[2], 10, 32) + if err != nil { + return c, fmt.Errorf("unable to parse DragonflyBSD CPU stepping information from %q: %v", line, err) + } + c.Stepping = int32(t) + } else if matches := featuresMatch.FindStringSubmatch(line); matches != nil { + for _, v := range strings.Split(matches[1], ",") { + c.Flags = append(c.Flags, strings.ToLower(v)) + } + } else if matches := featuresMatch2.FindStringSubmatch(line); matches != nil { + for _, v := range strings.Split(matches[1], ",") { + c.Flags = append(c.Flags, strings.ToLower(v)) + } + } + } + + return c, nil +} + +func CountsWithContext(ctx context.Context, logical bool) (int, error) { + return runtime.NumCPU(), nil +} diff --git a/internal/gopsutil/cpu/cpu_dragonfly_amd64.go b/internal/gopsutil/cpu/cpu_dragonfly_amd64.go new file mode 100644 index 0000000..57e1452 --- /dev/null +++ b/internal/gopsutil/cpu/cpu_dragonfly_amd64.go @@ -0,0 +1,9 @@ +package cpu + +type cpuTimes struct { + User uint64 + Nice uint64 + Sys uint64 + Intr uint64 + Idle uint64 +} diff --git a/internal/gopsutil/cpu/cpu_fallback.go b/internal/gopsutil/cpu/cpu_fallback.go new file mode 100644 index 0000000..b4bd5f5 --- /dev/null +++ b/internal/gopsutil/cpu/cpu_fallback.go @@ -0,0 +1,30 @@ +//go:build !darwin && !linux && !freebsd && !openbsd && !solaris && !windows && !dragonfly + +package cpu + +import ( + "context" + "runtime" + + "github.com/gofiber/fiber/v2/internal/gopsutil/common" +) + +func Times(percpu bool) ([]TimesStat, error) { + return TimesWithContext(context.Background(), percpu) +} + +func TimesWithContext(ctx context.Context, percpu bool) ([]TimesStat, error) { + return []TimesStat{}, common.ErrNotImplementedError +} + +func Info() ([]InfoStat, error) { + return InfoWithContext(context.Background()) +} + +func InfoWithContext(ctx context.Context) ([]InfoStat, error) { + return []InfoStat{}, common.ErrNotImplementedError +} + +func CountsWithContext(ctx context.Context, logical bool) (int, error) { + return runtime.NumCPU(), nil +} diff --git a/internal/gopsutil/cpu/cpu_freebsd.go b/internal/gopsutil/cpu/cpu_freebsd.go new file mode 100644 index 0000000..50e0240 --- /dev/null +++ b/internal/gopsutil/cpu/cpu_freebsd.go @@ -0,0 +1,176 @@ +package cpu + +import ( + "context" + "fmt" + "os/exec" + "reflect" + "regexp" + "runtime" + "strconv" + "strings" + "unsafe" + + "golang.org/x/sys/unix" + + "github.com/gofiber/fiber/v2/internal/gopsutil/common" +) + +var ( + ClocksPerSec = float64(128) + cpuMatch = regexp.MustCompile(`^CPU:`) + originMatch = regexp.MustCompile(`Origin\s*=\s*"(.+)"\s+Id\s*=\s*(.+)\s+Family\s*=\s*(.+)\s+Model\s*=\s*(.+)\s+Stepping\s*=\s*(.+)`) + featuresMatch = regexp.MustCompile(`Features=.+<(.+)>`) + featuresMatch2 = regexp.MustCompile(`Features2=[a-f\dx]+<(.+)>`) + cpuEnd = regexp.MustCompile(`^Trying to mount root`) + cpuCores = regexp.MustCompile(`FreeBSD/SMP: (\d*) package\(s\) x (\d*) core\(s\)`) + cpuTimesSize int + emptyTimes cpuTimes +) + +func init() { + getconf, err := exec.LookPath("getconf") + if err != nil { + return + } + out, err := invoke.Command(getconf, "CLK_TCK") + // ignore errors + if err == nil { + i, err := strconv.ParseFloat(strings.TrimSpace(string(out)), 64) + if err == nil { + ClocksPerSec = float64(i) + } + } +} + +func timeStat(name string, t *cpuTimes) *TimesStat { + return &TimesStat{ + User: float64(t.User) / ClocksPerSec, + Nice: float64(t.Nice) / ClocksPerSec, + System: float64(t.Sys) / ClocksPerSec, + Idle: float64(t.Idle) / ClocksPerSec, + Irq: float64(t.Intr) / ClocksPerSec, + CPU: name, + } +} + +func Times(percpu bool) ([]TimesStat, error) { + return TimesWithContext(context.Background(), percpu) +} + +func TimesWithContext(ctx context.Context, percpu bool) ([]TimesStat, error) { + if percpu { + buf, err := unix.SysctlRaw("kern.cp_times") + if err != nil { + return nil, err + } + + // We can't do this in init due to the conflict with cpu.init() + if cpuTimesSize == 0 { + cpuTimesSize = int(reflect.TypeOf(cpuTimes{}).Size()) + } + + ncpus := len(buf) / cpuTimesSize + ret := make([]TimesStat, 0, ncpus) + for i := 0; i < ncpus; i++ { + times := (*cpuTimes)(unsafe.Pointer(&buf[i*cpuTimesSize])) + if *times == emptyTimes { + // CPU not present + continue + } + ret = append(ret, *timeStat(fmt.Sprintf("cpu%d", len(ret)), times)) + } + return ret, nil + } + + buf, err := unix.SysctlRaw("kern.cp_time") + if err != nil { + return nil, err + } + + times := (*cpuTimes)(unsafe.Pointer(&buf[0])) + return []TimesStat{*timeStat("cpu-total", times)}, nil +} + +// Returns only one InfoStat on FreeBSD. The information regarding core +// count, however is accurate and it is assumed that all InfoStat attributes +// are the same across CPUs. +func Info() ([]InfoStat, error) { + return InfoWithContext(context.Background()) +} + +func InfoWithContext(ctx context.Context) ([]InfoStat, error) { + const dmesgBoot = "/var/run/dmesg.boot" + + c, num, err := parseDmesgBoot(dmesgBoot) + if err != nil { + return nil, err + } + + var u32 uint32 + if u32, err = unix.SysctlUint32("hw.clockrate"); err != nil { + return nil, err + } + c.Mhz = float64(u32) + + if u32, err = unix.SysctlUint32("hw.ncpu"); err != nil { + return nil, err + } + c.Cores = int32(u32) + + if c.ModelName, err = unix.Sysctl("hw.model"); err != nil { + return nil, err + } + + ret := make([]InfoStat, num) + for i := 0; i < num; i++ { + ret[i] = c + } + + return ret, nil +} + +func parseDmesgBoot(fileName string) (InfoStat, int, error) { + c := InfoStat{} + lines, _ := common.ReadLines(fileName) + cpuNum := 1 // default cpu num is 1 + for _, line := range lines { + if matches := cpuEnd.FindStringSubmatch(line); matches != nil { + break + } else if matches := originMatch.FindStringSubmatch(line); matches != nil { + c.VendorID = matches[1] + c.Family = matches[3] + c.Model = matches[4] + t, err := strconv.ParseInt(matches[5], 10, 32) + if err != nil { + return c, 0, fmt.Errorf("unable to parse FreeBSD CPU stepping information from %q: %v", line, err) + } + c.Stepping = int32(t) + } else if matches := featuresMatch.FindStringSubmatch(line); matches != nil { + for _, v := range strings.Split(matches[1], ",") { + c.Flags = append(c.Flags, strings.ToLower(v)) + } + } else if matches := featuresMatch2.FindStringSubmatch(line); matches != nil { + for _, v := range strings.Split(matches[1], ",") { + c.Flags = append(c.Flags, strings.ToLower(v)) + } + } else if matches := cpuCores.FindStringSubmatch(line); matches != nil { + t, err := strconv.ParseInt(matches[1], 10, 32) + if err != nil { + return c, 0, fmt.Errorf("unable to parse FreeBSD CPU Nums from %q: %v", line, err) + } + cpuNum = int(t) + t2, err := strconv.ParseInt(matches[2], 10, 32) + if err != nil { + return c, 0, fmt.Errorf("unable to parse FreeBSD CPU cores from %q: %v", line, err) + } + c.Cores = int32(t2) + } + } + + return c, cpuNum, nil +} + +func CountsWithContext(ctx context.Context, logical bool) (int, error) { + return runtime.NumCPU(), nil +} diff --git a/internal/gopsutil/cpu/cpu_freebsd_386.go b/internal/gopsutil/cpu/cpu_freebsd_386.go new file mode 100644 index 0000000..8b7f4c3 --- /dev/null +++ b/internal/gopsutil/cpu/cpu_freebsd_386.go @@ -0,0 +1,9 @@ +package cpu + +type cpuTimes struct { + User uint32 + Nice uint32 + Sys uint32 + Intr uint32 + Idle uint32 +} diff --git a/internal/gopsutil/cpu/cpu_freebsd_amd64.go b/internal/gopsutil/cpu/cpu_freebsd_amd64.go new file mode 100644 index 0000000..57e1452 --- /dev/null +++ b/internal/gopsutil/cpu/cpu_freebsd_amd64.go @@ -0,0 +1,9 @@ +package cpu + +type cpuTimes struct { + User uint64 + Nice uint64 + Sys uint64 + Intr uint64 + Idle uint64 +} diff --git a/internal/gopsutil/cpu/cpu_freebsd_arm.go b/internal/gopsutil/cpu/cpu_freebsd_arm.go new file mode 100644 index 0000000..8b7f4c3 --- /dev/null +++ b/internal/gopsutil/cpu/cpu_freebsd_arm.go @@ -0,0 +1,9 @@ +package cpu + +type cpuTimes struct { + User uint32 + Nice uint32 + Sys uint32 + Intr uint32 + Idle uint32 +} diff --git a/internal/gopsutil/cpu/cpu_freebsd_arm64.go b/internal/gopsutil/cpu/cpu_freebsd_arm64.go new file mode 100644 index 0000000..57e1452 --- /dev/null +++ b/internal/gopsutil/cpu/cpu_freebsd_arm64.go @@ -0,0 +1,9 @@ +package cpu + +type cpuTimes struct { + User uint64 + Nice uint64 + Sys uint64 + Intr uint64 + Idle uint64 +} diff --git a/internal/gopsutil/cpu/cpu_linux.go b/internal/gopsutil/cpu/cpu_linux.go new file mode 100644 index 0000000..ddc65b9 --- /dev/null +++ b/internal/gopsutil/cpu/cpu_linux.go @@ -0,0 +1,369 @@ +//go:build linux + +package cpu + +import ( + "context" + "errors" + "fmt" + "os/exec" + "path/filepath" + "strconv" + "strings" + + "github.com/gofiber/fiber/v2/internal/gopsutil/common" +) + +var ClocksPerSec = float64(100) + +func init() { + getconf, err := exec.LookPath("getconf") + if err != nil { + return + } + out, err := invoke.CommandWithContext(context.Background(), getconf, "CLK_TCK") + // ignore errors + if err == nil { + i, err := strconv.ParseFloat(strings.TrimSpace(string(out)), 64) + if err == nil { + ClocksPerSec = i + } + } +} + +func Times(percpu bool) ([]TimesStat, error) { + return TimesWithContext(context.Background(), percpu) +} + +func TimesWithContext(ctx context.Context, percpu bool) ([]TimesStat, error) { + filename := common.HostProc("stat") + lines := []string{} + if percpu { + statlines, err := common.ReadLines(filename) + if err != nil || len(statlines) < 2 { + return []TimesStat{}, nil + } + for _, line := range statlines[1:] { + if !strings.HasPrefix(line, "cpu") { + break + } + lines = append(lines, line) + } + } else { + lines, _ = common.ReadLinesOffsetN(filename, 0, 1) + } + + ret := make([]TimesStat, 0, len(lines)) + + for _, line := range lines { + ct, err := parseStatLine(line) + if err != nil { + continue + } + ret = append(ret, *ct) + + } + return ret, nil +} + +func sysCPUPath(cpu int32, relPath string) string { + return common.HostSys(fmt.Sprintf("devices/system/cpu/cpu%d", cpu), relPath) +} + +func finishCPUInfo(c *InfoStat) error { + var lines []string + var err error + var value float64 + + if len(c.CoreID) == 0 { + lines, err = common.ReadLines(sysCPUPath(c.CPU, "topology/core_id")) + if err == nil { + c.CoreID = lines[0] + } + } + + // override the value of c.Mhz with cpufreq/cpuinfo_max_freq regardless + // of the value from /proc/cpuinfo because we want to report the maximum + // clock-speed of the CPU for c.Mhz, matching the behaviour of Windows + lines, err = common.ReadLines(sysCPUPath(c.CPU, "cpufreq/cpuinfo_max_freq")) + // if we encounter errors below such as there are no cpuinfo_max_freq file, + // we just ignore. so let Mhz is 0. + if err != nil || len(lines) == 0 { + return nil + } + value, err = strconv.ParseFloat(lines[0], 64) + if err != nil { + return nil + } + c.Mhz = value / 1000.0 // value is in kHz + if c.Mhz > 9999 { + c.Mhz = c.Mhz / 1000.0 // value in Hz + } + return nil +} + +// CPUInfo on linux will return 1 item per physical thread. +// +// CPUs have three levels of counting: sockets, cores, threads. +// Cores with HyperThreading count as having 2 threads per core. +// Sockets often come with many physical CPU cores. +// For example a single socket board with two cores each with HT will +// return 4 CPUInfoStat structs on Linux and the "Cores" field set to 1. +func Info() ([]InfoStat, error) { + return InfoWithContext(context.Background()) +} + +func InfoWithContext(ctx context.Context) ([]InfoStat, error) { + filename := common.HostProc("cpuinfo") + lines, _ := common.ReadLines(filename) + + var ret []InfoStat + var processorName string + + c := InfoStat{CPU: -1, Cores: 1} + for _, line := range lines { + fields := strings.Split(line, ":") + if len(fields) < 2 { + continue + } + key := strings.TrimSpace(fields[0]) + value := strings.TrimSpace(fields[1]) + + switch key { + case "Processor": + processorName = value + case "processor": + if c.CPU >= 0 { + err := finishCPUInfo(&c) + if err != nil { + return ret, err + } + ret = append(ret, c) + } + c = InfoStat{Cores: 1, ModelName: processorName} + t, err := strconv.ParseInt(value, 10, 64) + if err != nil { + return ret, err + } + c.CPU = int32(t) + case "vendorId", "vendor_id": + c.VendorID = value + case "cpu family": + c.Family = value + case "model": + c.Model = value + case "model name", "cpu": + c.ModelName = value + if strings.Contains(value, "POWER8") || + strings.Contains(value, "POWER7") { + c.Model = strings.Split(value, " ")[0] + c.Family = "POWER" + c.VendorID = "IBM" + } + case "stepping", "revision": + val := value + + if key == "revision" { + val = strings.Split(value, ".")[0] + } + + t, err := strconv.ParseInt(val, 10, 64) + if err != nil { + return ret, err + } + c.Stepping = int32(t) + case "cpu MHz", "clock": + // treat this as the fallback value, thus we ignore error + if t, err := strconv.ParseFloat(strings.Replace(value, "MHz", "", 1), 64); err == nil { + c.Mhz = t + } + case "cache size": + t, err := strconv.ParseInt(strings.Replace(value, " KB", "", 1), 10, 64) + if err != nil { + return ret, err + } + c.CacheSize = int32(t) + case "physical id": + c.PhysicalID = value + case "core id": + c.CoreID = value + case "flags", "Features": + c.Flags = strings.FieldsFunc(value, func(r rune) bool { + return r == ',' || r == ' ' + }) + case "microcode": + c.Microcode = value + } + } + if c.CPU >= 0 { + err := finishCPUInfo(&c) + if err != nil { + return ret, err + } + ret = append(ret, c) + } + return ret, nil +} + +func parseStatLine(line string) (*TimesStat, error) { + fields := strings.Fields(line) + + if len(fields) == 0 { + return nil, errors.New("stat does not contain cpu info") + } + + if !strings.HasPrefix(fields[0], "cpu") { + return nil, errors.New("not contain cpu") + } + + cpu := fields[0] + if cpu == "cpu" { + cpu = "cpu-total" + } + user, err := strconv.ParseFloat(fields[1], 64) + if err != nil { + return nil, err + } + nice, err := strconv.ParseFloat(fields[2], 64) + if err != nil { + return nil, err + } + system, err := strconv.ParseFloat(fields[3], 64) + if err != nil { + return nil, err + } + idle, err := strconv.ParseFloat(fields[4], 64) + if err != nil { + return nil, err + } + iowait, err := strconv.ParseFloat(fields[5], 64) + if err != nil { + return nil, err + } + irq, err := strconv.ParseFloat(fields[6], 64) + if err != nil { + return nil, err + } + softirq, err := strconv.ParseFloat(fields[7], 64) + if err != nil { + return nil, err + } + + ct := &TimesStat{ + CPU: cpu, + User: user / ClocksPerSec, + Nice: nice / ClocksPerSec, + System: system / ClocksPerSec, + Idle: idle / ClocksPerSec, + Iowait: iowait / ClocksPerSec, + Irq: irq / ClocksPerSec, + Softirq: softirq / ClocksPerSec, + } + if len(fields) > 8 { // Linux >= 2.6.11 + steal, err := strconv.ParseFloat(fields[8], 64) + if err != nil { + return nil, err + } + ct.Steal = steal / ClocksPerSec + } + if len(fields) > 9 { // Linux >= 2.6.24 + guest, err := strconv.ParseFloat(fields[9], 64) + if err != nil { + return nil, err + } + ct.Guest = guest / ClocksPerSec + } + if len(fields) > 10 { // Linux >= 3.2.0 + guestNice, err := strconv.ParseFloat(fields[10], 64) + if err != nil { + return nil, err + } + ct.GuestNice = guestNice / ClocksPerSec + } + + return ct, nil +} + +func CountsWithContext(ctx context.Context, logical bool) (int, error) { + if logical { + ret := 0 + // https://github.com/giampaolo/psutil/blob/d01a9eaa35a8aadf6c519839e987a49d8be2d891/psutil/_pslinux.py#L599 + procCpuinfo := common.HostProc("cpuinfo") + lines, err := common.ReadLines(procCpuinfo) + if err == nil { + for _, line := range lines { + line = strings.ToLower(line) + if strings.HasPrefix(line, "processor") { + ret++ + } + } + } + if ret == 0 { + procStat := common.HostProc("stat") + lines, err = common.ReadLines(procStat) + if err != nil { + return 0, err + } + for _, line := range lines { + if len(line) >= 4 && strings.HasPrefix(line, "cpu") && '0' <= line[3] && line[3] <= '9' { // `^cpu\d` regexp matching + ret++ + } + } + } + return ret, nil + } + // physical cores + // https://github.com/giampaolo/psutil/blob/122174a10b75c9beebe15f6c07dcf3afbe3b120d/psutil/_pslinux.py#L621-L629 + threadSiblingsLists := make(map[string]bool) + if files, err := filepath.Glob(common.HostSys("devices/system/cpu/cpu[0-9]*/topology/thread_siblings_list")); err == nil { + for _, file := range files { + lines, err := common.ReadLines(file) + if err != nil || len(lines) != 1 { + continue + } + threadSiblingsLists[lines[0]] = true + } + ret := len(threadSiblingsLists) + if ret != 0 { + return ret, nil + } + } + // https://github.com/giampaolo/psutil/blob/122174a10b75c9beebe15f6c07dcf3afbe3b120d/psutil/_pslinux.py#L631-L652 + filename := common.HostProc("cpuinfo") + lines, err := common.ReadLines(filename) + if err != nil { + return 0, err + } + mapping := make(map[int]int) + currentInfo := make(map[string]int) + for _, line := range lines { + line = strings.ToLower(strings.TrimSpace(line)) + if line == "" { + // new section + id, okID := currentInfo["physical id"] + cores, okCores := currentInfo["cpu cores"] + if okID && okCores { + mapping[id] = cores + } + currentInfo = make(map[string]int) + continue + } + fields := strings.Split(line, ":") + if len(fields) < 2 { + continue + } + fields[0] = strings.TrimSpace(fields[0]) + if fields[0] == "physical id" || fields[0] == "cpu cores" { + val, err := strconv.Atoi(strings.TrimSpace(fields[1])) + if err != nil { + continue + } + currentInfo[fields[0]] = val + } + } + ret := 0 + for _, v := range mapping { + ret += v + } + return ret, nil +} diff --git a/internal/gopsutil/cpu/cpu_openbsd.go b/internal/gopsutil/cpu/cpu_openbsd.go new file mode 100644 index 0000000..317f078 --- /dev/null +++ b/internal/gopsutil/cpu/cpu_openbsd.go @@ -0,0 +1,187 @@ +//go:build openbsd + +package cpu + +import ( + "bytes" + "context" + "encoding/binary" + "fmt" + "os/exec" + "runtime" + "strconv" + "strings" + "syscall" + + "golang.org/x/sys/unix" + + "github.com/gofiber/fiber/v2/internal/gopsutil/common" +) + +// sys/sched.h +var ( + CPUser = 0 + CPNice = 1 + CPSys = 2 + CPIntr = 3 + CPIdle = 4 + CPUStates = 5 +) + +// sys/sysctl.h +const ( + CTLKern = 1 // "high kernel": proc, limits + CTLHw = 6 // CTL_HW + SMT = 24 // HW_SMT + KernCptime = 40 // KERN_CPTIME + KernCptime2 = 71 // KERN_CPTIME2 +) + +var ClocksPerSec = float64(128) + +func init() { + func() { + getconf, err := exec.LookPath("getconf") + if err != nil { + return + } + out, err := invoke.Command(getconf, "CLK_TCK") + // ignore errors + if err == nil { + i, err := strconv.ParseFloat(strings.TrimSpace(string(out)), 64) + if err == nil { + ClocksPerSec = float64(i) + } + } + }() + func() { + v, err := unix.Sysctl("kern.osrelease") // can't reuse host.PlatformInformation because of circular import + if err != nil { + return + } + v = strings.ToLower(v) + version, err := strconv.ParseFloat(v, 64) + if err != nil { + return + } + if version >= 6.4 { + CPIntr = 4 + CPIdle = 5 + CPUStates = 6 + } + }() +} + +func smt() (bool, error) { + mib := []int32{CTLHw, SMT} + buf, _, err := common.CallSyscall(mib) + if err != nil { + return false, err + } + + var ret bool + br := bytes.NewReader(buf) + if err := binary.Read(br, binary.LittleEndian, &ret); err != nil { + return false, err + } + + return ret, nil +} + +func Times(percpu bool) ([]TimesStat, error) { + return TimesWithContext(context.Background(), percpu) +} + +func TimesWithContext(ctx context.Context, percpu bool) ([]TimesStat, error) { + var ret []TimesStat + + var ncpu int + if percpu { + ncpu, _ = Counts(true) + } else { + ncpu = 1 + } + + smt, err := smt() + if err == syscall.EOPNOTSUPP { + // if hw.smt is not applicable for this platform (e.g. i386), + // pretend it's enabled + smt = true + } else if err != nil { + return nil, err + } + + for i := 0; i < ncpu; i++ { + j := i + if !smt { + j *= 2 + } + + cpuTimes := make([]int32, CPUStates) + var mib []int32 + if percpu { + mib = []int32{CTLKern, KernCptime2, int32(j)} + } else { + mib = []int32{CTLKern, KernCptime} + } + buf, _, err := common.CallSyscall(mib) + if err != nil { + return ret, err + } + + br := bytes.NewReader(buf) + err = binary.Read(br, binary.LittleEndian, &cpuTimes) + if err != nil { + return ret, err + } + c := TimesStat{ + User: float64(cpuTimes[CPUser]) / ClocksPerSec, + Nice: float64(cpuTimes[CPNice]) / ClocksPerSec, + System: float64(cpuTimes[CPSys]) / ClocksPerSec, + Idle: float64(cpuTimes[CPIdle]) / ClocksPerSec, + Irq: float64(cpuTimes[CPIntr]) / ClocksPerSec, + } + if percpu { + c.CPU = fmt.Sprintf("cpu%d", j) + } else { + c.CPU = "cpu-total" + } + ret = append(ret, c) + } + + return ret, nil +} + +// Returns only one (minimal) CPUInfoStat on OpenBSD +func Info() ([]InfoStat, error) { + return InfoWithContext(context.Background()) +} + +func InfoWithContext(ctx context.Context) ([]InfoStat, error) { + var ret []InfoStat + var err error + + c := InfoStat{} + + mhz, err := unix.SysctlUint32("hw.cpuspeed") + if err != nil { + return nil, err + } + c.Mhz = float64(mhz) + + ncpu, err := unix.SysctlUint32("hw.ncpuonline") + if err != nil { + return nil, err + } + c.Cores = int32(ncpu) + + if c.ModelName, err = unix.Sysctl("hw.model"); err != nil { + return nil, err + } + + return append(ret, c), nil +} + +func CountsWithContext(ctx context.Context, logical bool) (int, error) { + return runtime.NumCPU(), nil +} diff --git a/internal/gopsutil/cpu/cpu_solaris.go b/internal/gopsutil/cpu/cpu_solaris.go new file mode 100644 index 0000000..d5a1470 --- /dev/null +++ b/internal/gopsutil/cpu/cpu_solaris.go @@ -0,0 +1,286 @@ +package cpu + +import ( + "context" + "errors" + "fmt" + "os/exec" + "regexp" + "runtime" + "sort" + "strconv" + "strings" +) + +var ClocksPerSec = float64(128) + +func init() { + getconf, err := exec.LookPath("getconf") + if err != nil { + return + } + out, err := invoke.Command(getconf, "CLK_TCK") + // ignore errors + if err == nil { + i, err := strconv.ParseFloat(strings.TrimSpace(string(out)), 64) + if err == nil { + ClocksPerSec = float64(i) + } + } +} + +// sum all values in a float64 map with float64 keys +func msum(x map[float64]float64) float64 { + total := 0.0 + for _, y := range x { + total += y + } + return total +} + +func Times(percpu bool) ([]TimesStat, error) { + return TimesWithContext(context.Background(), percpu) +} + +func TimesWithContext(ctx context.Context, percpu bool) ([]TimesStat, error) { + kstatSys, err := exec.LookPath("kstat") + if err != nil { + return nil, fmt.Errorf("cannot find kstat: %s", err) + } + cpu := make(map[float64]float64) + idle := make(map[float64]float64) + user := make(map[float64]float64) + kern := make(map[float64]float64) + iowt := make(map[float64]float64) + // swap := make(map[float64]float64) + kstatSysOut, err := invoke.CommandWithContext(ctx, kstatSys, "-p", "cpu_stat:*:*:/^idle$|^user$|^kernel$|^iowait$|^swap$/") + if err != nil { + return nil, fmt.Errorf("cannot execute kstat: %s", err) + } + re := regexp.MustCompile(`[:\s]+`) + for _, line := range strings.Split(string(kstatSysOut), "\n") { + fields := re.Split(line, -1) + if fields[0] != "cpu_stat" { + continue + } + cpuNumber, err := strconv.ParseFloat(fields[1], 64) + if err != nil { + return nil, fmt.Errorf("cannot parse cpu number: %s", err) + } + cpu[cpuNumber] = cpuNumber + switch fields[3] { + case "idle": + idle[cpuNumber], err = strconv.ParseFloat(fields[4], 64) + if err != nil { + return nil, fmt.Errorf("cannot parse idle: %s", err) + } + case "user": + user[cpuNumber], err = strconv.ParseFloat(fields[4], 64) + if err != nil { + return nil, fmt.Errorf("cannot parse user: %s", err) + } + case "kernel": + kern[cpuNumber], err = strconv.ParseFloat(fields[4], 64) + if err != nil { + return nil, fmt.Errorf("cannot parse kernel: %s", err) + } + case "iowait": + iowt[cpuNumber], err = strconv.ParseFloat(fields[4], 64) + if err != nil { + return nil, fmt.Errorf("cannot parse iowait: %s", err) + } + //not sure how this translates, don't report, add to kernel, something else? + /*case "swap": + swap[cpuNumber], err = strconv.ParseFloat(fields[4], 64) + if err != nil { + return nil, fmt.Errorf("cannot parse swap: %s", err) + } */ + } + } + ret := make([]TimesStat, 0, len(cpu)) + if percpu { + for _, c := range cpu { + ct := &TimesStat{ + CPU: fmt.Sprintf("cpu%d", int(cpu[c])), + Idle: idle[c] / ClocksPerSec, + User: user[c] / ClocksPerSec, + System: kern[c] / ClocksPerSec, + Iowait: iowt[c] / ClocksPerSec, + } + ret = append(ret, *ct) + } + } else { + ct := &TimesStat{ + CPU: "cpu-total", + Idle: msum(idle) / ClocksPerSec, + User: msum(user) / ClocksPerSec, + System: msum(kern) / ClocksPerSec, + Iowait: msum(iowt) / ClocksPerSec, + } + ret = append(ret, *ct) + } + return ret, nil +} + +func Info() ([]InfoStat, error) { + return InfoWithContext(context.Background()) +} + +func InfoWithContext(ctx context.Context) ([]InfoStat, error) { + psrInfo, err := exec.LookPath("psrinfo") + if err != nil { + return nil, fmt.Errorf("cannot find psrinfo: %s", err) + } + psrInfoOut, err := invoke.CommandWithContext(ctx, psrInfo, "-p", "-v") + if err != nil { + return nil, fmt.Errorf("cannot execute psrinfo: %s", err) + } + + isaInfo, err := exec.LookPath("isainfo") + if err != nil { + return nil, fmt.Errorf("cannot find isainfo: %s", err) + } + isaInfoOut, err := invoke.CommandWithContext(ctx, isaInfo, "-b", "-v") + if err != nil { + return nil, fmt.Errorf("cannot execute isainfo: %s", err) + } + + procs, err := parseProcessorInfo(string(psrInfoOut)) + if err != nil { + return nil, fmt.Errorf("error parsing psrinfo output: %s", err) + } + + flags, err := parseISAInfo(string(isaInfoOut)) + if err != nil { + return nil, fmt.Errorf("error parsing isainfo output: %s", err) + } + + result := make([]InfoStat, 0, len(flags)) + for _, proc := range procs { + procWithFlags := proc + procWithFlags.Flags = flags + result = append(result, procWithFlags) + } + + return result, nil +} + +var flagsMatch = regexp.MustCompile(`[\w\.]+`) + +func parseISAInfo(cmdOutput string) ([]string, error) { + words := flagsMatch.FindAllString(cmdOutput, -1) + + // Sanity check the output + if len(words) < 4 || words[1] != "bit" || words[3] != "applications" { + return nil, errors.New("attempted to parse invalid isainfo output") + } + + flags := make([]string, len(words)-4) + for i, val := range words[4:] { + flags[i] = val + } + sort.Strings(flags) + + return flags, nil +} + +var psrInfoMatch = regexp.MustCompile(`The physical processor has (?:([\d]+) virtual processor \(([\d]+)\)|([\d]+) cores and ([\d]+) virtual processors[^\n]+)\n(?:\s+ The core has.+\n)*\s+.+ \((\w+) ([\S]+) family (.+) model (.+) step (.+) clock (.+) MHz\)\n[\s]*(.*)`) + +const ( + psrNumCoresOffset = 1 + psrNumCoresHTOffset = 3 + psrNumHTOffset = 4 + psrVendorIDOffset = 5 + psrFamilyOffset = 7 + psrModelOffset = 8 + psrStepOffset = 9 + psrClockOffset = 10 + psrModelNameOffset = 11 +) + +func parseProcessorInfo(cmdOutput string) ([]InfoStat, error) { + matches := psrInfoMatch.FindAllStringSubmatch(cmdOutput, -1) + + var infoStatCount int32 + result := make([]InfoStat, 0, len(matches)) + for physicalIndex, physicalCPU := range matches { + var step int32 + var clock float64 + + if physicalCPU[psrStepOffset] != "" { + stepParsed, err := strconv.ParseInt(physicalCPU[psrStepOffset], 10, 32) + if err != nil { + return nil, fmt.Errorf("cannot parse value %q for step as 32-bit integer: %s", physicalCPU[9], err) + } + step = int32(stepParsed) + } + + if physicalCPU[psrClockOffset] != "" { + clockParsed, err := strconv.ParseInt(physicalCPU[psrClockOffset], 10, 64) + if err != nil { + return nil, fmt.Errorf("cannot parse value %q for clock as 32-bit integer: %s", physicalCPU[10], err) + } + clock = float64(clockParsed) + } + + var err error + var numCores int64 + var numHT int64 + switch { + case physicalCPU[psrNumCoresOffset] != "": + numCores, err = strconv.ParseInt(physicalCPU[psrNumCoresOffset], 10, 32) + if err != nil { + return nil, fmt.Errorf("cannot parse value %q for core count as 32-bit integer: %s", physicalCPU[1], err) + } + + for i := 0; i < int(numCores); i++ { + result = append(result, InfoStat{ + CPU: infoStatCount, + PhysicalID: strconv.Itoa(physicalIndex), + CoreID: strconv.Itoa(i), + Cores: 1, + VendorID: physicalCPU[psrVendorIDOffset], + ModelName: physicalCPU[psrModelNameOffset], + Family: physicalCPU[psrFamilyOffset], + Model: physicalCPU[psrModelOffset], + Stepping: step, + Mhz: clock, + }) + infoStatCount++ + } + case physicalCPU[psrNumCoresHTOffset] != "": + numCores, err = strconv.ParseInt(physicalCPU[psrNumCoresHTOffset], 10, 32) + if err != nil { + return nil, fmt.Errorf("cannot parse value %q for core count as 32-bit integer: %s", physicalCPU[3], err) + } + + numHT, err = strconv.ParseInt(physicalCPU[psrNumHTOffset], 10, 32) + if err != nil { + return nil, fmt.Errorf("cannot parse value %q for hyperthread count as 32-bit integer: %s", physicalCPU[4], err) + } + + for i := 0; i < int(numCores); i++ { + result = append(result, InfoStat{ + CPU: infoStatCount, + PhysicalID: strconv.Itoa(physicalIndex), + CoreID: strconv.Itoa(i), + Cores: int32(numHT) / int32(numCores), + VendorID: physicalCPU[psrVendorIDOffset], + ModelName: physicalCPU[psrModelNameOffset], + Family: physicalCPU[psrFamilyOffset], + Model: physicalCPU[psrModelOffset], + Stepping: step, + Mhz: clock, + }) + infoStatCount++ + } + default: + return nil, errors.New("values for cores with and without hyperthreading are both set") + } + } + return result, nil +} + +func CountsWithContext(ctx context.Context, logical bool) (int, error) { + return runtime.NumCPU(), nil +} diff --git a/internal/gopsutil/cpu/cpu_windows.go b/internal/gopsutil/cpu/cpu_windows.go new file mode 100644 index 0000000..bae5d09 --- /dev/null +++ b/internal/gopsutil/cpu/cpu_windows.go @@ -0,0 +1,256 @@ +//go:build windows + +package cpu + +import ( + "context" + "fmt" + "unsafe" + + "golang.org/x/sys/windows" + + "github.com/gofiber/fiber/v2/internal/gopsutil/common" + "github.com/gofiber/fiber/v2/internal/wmi" +) + +var ( + procGetActiveProcessorCount = common.Modkernel32.NewProc("GetActiveProcessorCount") + procGetNativeSystemInfo = common.Modkernel32.NewProc("GetNativeSystemInfo") +) + +type Win32_Processor struct { + LoadPercentage *uint16 + Family uint16 + Manufacturer string + Name string + NumberOfLogicalProcessors uint32 + NumberOfCores uint32 + ProcessorID *string + Stepping *string + MaxClockSpeed uint32 +} + +// SYSTEM_PROCESSOR_PERFORMANCE_INFORMATION +// defined in windows api doc with the following +// https://docs.microsoft.com/en-us/windows/desktop/api/winternl/nf-winternl-ntquerysysteminformation#system_processor_performance_information +// additional fields documented here +// https://www.geoffchappell.com/studies/windows/km/ntoskrnl/api/ex/sysinfo/processor_performance.htm +type win32_SystemProcessorPerformanceInformation struct { + IdleTime int64 // idle time in 100ns (this is not a filetime). + KernelTime int64 // kernel time in 100ns. kernel time includes idle time. (this is not a filetime). + UserTime int64 // usertime in 100ns (this is not a filetime). + DpcTime int64 // dpc time in 100ns (this is not a filetime). + InterruptTime int64 // interrupt time in 100ns + InterruptCount uint32 +} + +// Win32_PerfFormattedData_PerfOS_System struct to have count of processes and processor queue length +type Win32_PerfFormattedData_PerfOS_System struct { + Processes uint32 + ProcessorQueueLength uint32 +} + +const ( + ClocksPerSec = 10000000.0 + + // systemProcessorPerformanceInformationClass information class to query with NTQuerySystemInformation + // https://processhacker.sourceforge.io/doc/ntexapi_8h.html#ad5d815b48e8f4da1ef2eb7a2f18a54e0 + win32_SystemProcessorPerformanceInformationClass = 8 + + // size of systemProcessorPerformanceInfoSize in memory + win32_SystemProcessorPerformanceInfoSize = uint32(unsafe.Sizeof(win32_SystemProcessorPerformanceInformation{})) +) + +// Times returns times stat per cpu and combined for all CPUs +func Times(percpu bool) ([]TimesStat, error) { + return TimesWithContext(context.Background(), percpu) +} + +func TimesWithContext(ctx context.Context, percpu bool) ([]TimesStat, error) { + if percpu { + return perCPUTimes() + } + + var ret []TimesStat + var lpIdleTime common.FILETIME + var lpKernelTime common.FILETIME + var lpUserTime common.FILETIME + r, _, _ := common.ProcGetSystemTimes.Call( + uintptr(unsafe.Pointer(&lpIdleTime)), + uintptr(unsafe.Pointer(&lpKernelTime)), + uintptr(unsafe.Pointer(&lpUserTime))) + if r == 0 { + return ret, windows.GetLastError() + } + + LOT := float64(0.0000001) + HIT := (LOT * 4294967296.0) + idle := ((HIT * float64(lpIdleTime.DwHighDateTime)) + (LOT * float64(lpIdleTime.DwLowDateTime))) + user := ((HIT * float64(lpUserTime.DwHighDateTime)) + (LOT * float64(lpUserTime.DwLowDateTime))) + kernel := ((HIT * float64(lpKernelTime.DwHighDateTime)) + (LOT * float64(lpKernelTime.DwLowDateTime))) + system := (kernel - idle) + + ret = append(ret, TimesStat{ + CPU: "cpu-total", + Idle: float64(idle), + User: float64(user), + System: float64(system), + }) + return ret, nil +} + +func Info() ([]InfoStat, error) { + return InfoWithContext(context.Background()) +} + +func InfoWithContext(ctx context.Context) ([]InfoStat, error) { + var ret []InfoStat + var dst []Win32_Processor + q := wmi.CreateQuery(&dst, "") + if err := common.WMIQueryWithContext(ctx, q, &dst); err != nil { + return ret, err + } + + var procID string + for i, l := range dst { + procID = "" + if l.ProcessorID != nil { + procID = *l.ProcessorID + } + + cpu := InfoStat{ + CPU: int32(i), + Family: fmt.Sprintf("%d", l.Family), + VendorID: l.Manufacturer, + ModelName: l.Name, + Cores: int32(l.NumberOfLogicalProcessors), + PhysicalID: procID, + Mhz: float64(l.MaxClockSpeed), + Flags: []string{}, + } + ret = append(ret, cpu) + } + + return ret, nil +} + +// ProcInfo returns processes count and processor queue length in the system. +// There is a single queue for processor even on multiprocessors systems. +func ProcInfo() ([]Win32_PerfFormattedData_PerfOS_System, error) { + return ProcInfoWithContext(context.Background()) +} + +func ProcInfoWithContext(ctx context.Context) ([]Win32_PerfFormattedData_PerfOS_System, error) { + var ret []Win32_PerfFormattedData_PerfOS_System + q := wmi.CreateQuery(&ret, "") + err := common.WMIQueryWithContext(ctx, q, &ret) + if err != nil { + return []Win32_PerfFormattedData_PerfOS_System{}, err + } + return ret, err +} + +// perCPUTimes returns times stat per cpu, per core and overall for all CPUs +func perCPUTimes() ([]TimesStat, error) { + var ret []TimesStat + stats, err := perfInfo() + if err != nil { + return nil, err + } + for core, v := range stats { + c := TimesStat{ + CPU: fmt.Sprintf("cpu%d", core), + User: float64(v.UserTime) / ClocksPerSec, + System: float64(v.KernelTime-v.IdleTime) / ClocksPerSec, + Idle: float64(v.IdleTime) / ClocksPerSec, + Irq: float64(v.InterruptTime) / ClocksPerSec, + } + ret = append(ret, c) + } + return ret, nil +} + +// makes call to Windows API function to retrieve performance information for each core +func perfInfo() ([]win32_SystemProcessorPerformanceInformation, error) { + // Make maxResults large for safety. + // We can't invoke the api call with a results array that's too small. + // If we have more than 2056 cores on a single host, then it's probably the future. + maxBuffer := 2056 + // buffer for results from the windows proc + resultBuffer := make([]win32_SystemProcessorPerformanceInformation, maxBuffer) + // size of the buffer in memory + bufferSize := uintptr(win32_SystemProcessorPerformanceInfoSize) * uintptr(maxBuffer) + // size of the returned response + var retSize uint32 + + // Invoke windows api proc. + // The returned err from the windows dll proc will always be non-nil even when successful. + // See https://godoc.org/golang.org/x/sys/windows#LazyProc.Call for more information + retCode, _, err := common.ProcNtQuerySystemInformation.Call( + win32_SystemProcessorPerformanceInformationClass, // System Information Class -> SystemProcessorPerformanceInformation + uintptr(unsafe.Pointer(&resultBuffer[0])), // pointer to first element in result buffer + bufferSize, // size of the buffer in memory + uintptr(unsafe.Pointer(&retSize)), // pointer to the size of the returned results the windows proc will set this + ) + + // check return code for errors + if retCode != 0 { + return nil, fmt.Errorf("call to NtQuerySystemInformation returned %d. err: %s", retCode, err.Error()) + } + + // calculate the number of returned elements based on the returned size + numReturnedElements := retSize / win32_SystemProcessorPerformanceInfoSize + + // trim results to the number of returned elements + resultBuffer = resultBuffer[:numReturnedElements] + + return resultBuffer, nil +} + +// SystemInfo is an equivalent representation of SYSTEM_INFO in the Windows API. +// https://msdn.microsoft.com/en-us/library/ms724958%28VS.85%29.aspx?f=255&MSPPError=-2147217396 +// https://github.com/elastic/go-windows/blob/bb1581babc04d5cb29a2bfa7a9ac6781c730c8dd/kernel32.go#L43 +type systemInfo struct { + wProcessorArchitecture uint16 + wReserved uint16 + dwPageSize uint32 + lpMinimumApplicationAddress uintptr + lpMaximumApplicationAddress uintptr + dwActiveProcessorMask uintptr + dwNumberOfProcessors uint32 + dwProcessorType uint32 + dwAllocationGranularity uint32 + wProcessorLevel uint16 + wProcessorRevision uint16 +} + +func CountsWithContext(ctx context.Context, logical bool) (int, error) { + if logical { + // https://github.com/giampaolo/psutil/blob/d01a9eaa35a8aadf6c519839e987a49d8be2d891/psutil/_psutil_windows.c#L97 + err := procGetActiveProcessorCount.Find() + if err == nil { // Win7+ + ret, _, _ := procGetActiveProcessorCount.Call(uintptr(0xffff)) // ALL_PROCESSOR_GROUPS is 0xffff according to Rust's winapi lib https://docs.rs/winapi/*/x86_64-pc-windows-msvc/src/winapi/shared/ntdef.rs.html#120 + if ret != 0 { + return int(ret), nil + } + } + var systemInfo systemInfo + _, _, err = procGetNativeSystemInfo.Call(uintptr(unsafe.Pointer(&systemInfo))) + if systemInfo.dwNumberOfProcessors == 0 { + return 0, err + } + return int(systemInfo.dwNumberOfProcessors), nil + } + // physical cores https://github.com/giampaolo/psutil/blob/d01a9eaa35a8aadf6c519839e987a49d8be2d891/psutil/_psutil_windows.c#L499 + // for the time being, try with unreliable and slow WMI call… + var dst []Win32_Processor + q := wmi.CreateQuery(&dst, "") + if err := common.WMIQueryWithContext(ctx, q, &dst); err != nil { + return 0, err + } + var count uint32 + for _, d := range dst { + count += d.NumberOfCores + } + return int(count), nil +} diff --git a/internal/gopsutil/load/load.go b/internal/gopsutil/load/load.go new file mode 100644 index 0000000..d5bd2c4 --- /dev/null +++ b/internal/gopsutil/load/load.go @@ -0,0 +1,30 @@ +package load + +import ( + "encoding/json" +) + +// var invoke common.Invoker = common.Invoke{} +type AvgStat struct { + Load1 float64 `json:"load1"` + Load5 float64 `json:"load5"` + Load15 float64 `json:"load15"` +} + +func (l AvgStat) String() string { + s, _ := json.Marshal(l) + return string(s) +} + +type MiscStat struct { + ProcsTotal int64 `json:"procsTotal"` + ProcsCreated int64 `json:"procsCreated"` + ProcsRunning int64 `json:"procsRunning"` + ProcsBlocked int64 `json:"procsBlocked"` + Ctxt int64 `json:"ctxt"` +} + +func (m MiscStat) String() string { + s, _ := json.Marshal(m) + return string(s) +} diff --git a/internal/gopsutil/load/load_bsd.go b/internal/gopsutil/load/load_bsd.go new file mode 100644 index 0000000..0412ade --- /dev/null +++ b/internal/gopsutil/load/load_bsd.go @@ -0,0 +1,84 @@ +//go:build freebsd || openbsd + +package load + +import ( + "context" + "os/exec" + "strings" + "unsafe" + + "golang.org/x/sys/unix" + + "github.com/gofiber/fiber/v2/internal/gopsutil/common" +) + +var invoke common.Invoker = common.Invoke{} + +func Avg() (*AvgStat, error) { + return AvgWithContext(context.Background()) +} + +func AvgWithContext(ctx context.Context) (*AvgStat, error) { + // This SysctlRaw method borrowed from + // https://github.com/prometheus/node_exporter/blob/master/collector/loadavg_freebsd.go + type loadavg struct { + load [3]uint32 + scale int + } + b, err := unix.SysctlRaw("vm.loadavg") + if err != nil { + return nil, err + } + load := *(*loadavg)(unsafe.Pointer((&b[0]))) + scale := float64(load.scale) + ret := &AvgStat{ + Load1: float64(load.load[0]) / scale, + Load5: float64(load.load[1]) / scale, + Load15: float64(load.load[2]) / scale, + } + + return ret, nil +} + +type forkstat struct { + forks int + vforks int + __tforks int +} + +// Misc returns miscellaneous host-wide statistics. +// darwin use ps command to get process running/blocked count. +// Almost same as Darwin implementation, but state is different. +func Misc() (*MiscStat, error) { + return MiscWithContext(context.Background()) +} + +func MiscWithContext(ctx context.Context) (*MiscStat, error) { + bin, err := exec.LookPath("ps") + if err != nil { + return nil, err + } + out, err := invoke.CommandWithContext(ctx, bin, "axo", "state") + if err != nil { + return nil, err + } + lines := strings.Split(string(out), "\n") + + ret := MiscStat{} + for _, l := range lines { + if strings.Contains(l, "R") { + ret.ProcsRunning++ + } else if strings.Contains(l, "D") { + ret.ProcsBlocked++ + } + } + + f, err := getForkStat() + if err != nil { + return nil, err + } + ret.ProcsCreated = int64(f.forks) + + return &ret, nil +} diff --git a/internal/gopsutil/load/load_darwin.go b/internal/gopsutil/load/load_darwin.go new file mode 100644 index 0000000..37b7eac --- /dev/null +++ b/internal/gopsutil/load/load_darwin.go @@ -0,0 +1,75 @@ +//go:build darwin + +package load + +import ( + "context" + "os/exec" + "strings" + "unsafe" + + "golang.org/x/sys/unix" + + "github.com/gofiber/fiber/v2/internal/gopsutil/common" +) + +var invoke common.Invoker = common.Invoke{} + +func Avg() (*AvgStat, error) { + return AvgWithContext(context.Background()) +} + +func AvgWithContext(ctx context.Context) (*AvgStat, error) { + // This SysctlRaw method borrowed from + // https://github.com/prometheus/node_exporter/blob/master/collector/loadavg_freebsd.go + // this implementation is common with BSDs + type loadavg struct { + load [3]uint32 + scale int + } + b, err := unix.SysctlRaw("vm.loadavg") + if err != nil { + return nil, err + } + load := *(*loadavg)(unsafe.Pointer((&b[0]))) + scale := float64(load.scale) + ret := &AvgStat{ + Load1: float64(load.load[0]) / scale, + Load5: float64(load.load[1]) / scale, + Load15: float64(load.load[2]) / scale, + } + + return ret, nil +} + +// Misc returnes miscellaneous host-wide statistics. +// darwin use ps command to get process running/blocked count. +// Almost same as FreeBSD implementation, but state is different. +// U means 'Uninterruptible Sleep'. +func Misc() (*MiscStat, error) { + return MiscWithContext(context.Background()) +} + +func MiscWithContext(ctx context.Context) (*MiscStat, error) { + bin, err := exec.LookPath("ps") + if err != nil { + return nil, err + } + out, err := invoke.CommandWithContext(ctx, bin, "axo", "state") + if err != nil { + return nil, err + } + lines := strings.Split(string(out), "\n") + + ret := MiscStat{} + for _, l := range lines { + if strings.Contains(l, "R") { + ret.ProcsRunning++ + } else if strings.Contains(l, "U") { + // uninterruptible sleep == blocked + ret.ProcsBlocked++ + } + } + + return &ret, nil +} diff --git a/internal/gopsutil/load/load_fallback.go b/internal/gopsutil/load/load_fallback.go new file mode 100644 index 0000000..d643d1f --- /dev/null +++ b/internal/gopsutil/load/load_fallback.go @@ -0,0 +1,25 @@ +//go:build !darwin && !linux && !freebsd && !openbsd && !windows && !solaris + +package load + +import ( + "context" + + "github.com/gofiber/fiber/v2/internal/gopsutil/common" +) + +func Avg() (*AvgStat, error) { + return AvgWithContext(context.Background()) +} + +func AvgWithContext(ctx context.Context) (*AvgStat, error) { + return nil, common.ErrNotImplementedError +} + +func Misc() (*MiscStat, error) { + return MiscWithContext(context.Background()) +} + +func MiscWithContext(ctx context.Context) (*MiscStat, error) { + return nil, common.ErrNotImplementedError +} diff --git a/internal/gopsutil/load/load_freebsd.go b/internal/gopsutil/load/load_freebsd.go new file mode 100644 index 0000000..a7011ba --- /dev/null +++ b/internal/gopsutil/load/load_freebsd.go @@ -0,0 +1,7 @@ +//go:build freebsd + +package load + +func getForkStat() (forkstat, error) { + return forkstat{}, nil +} diff --git a/internal/gopsutil/load/load_linux.go b/internal/gopsutil/load/load_linux.go new file mode 100644 index 0000000..e9d95a9 --- /dev/null +++ b/internal/gopsutil/load/load_linux.go @@ -0,0 +1,135 @@ +//go:build linux + +package load + +import ( + "context" + "os" + "strconv" + "strings" + "syscall" + + "github.com/gofiber/fiber/v2/internal/gopsutil/common" +) + +func Avg() (*AvgStat, error) { + return AvgWithContext(context.Background()) +} + +func AvgWithContext(ctx context.Context) (*AvgStat, error) { + stat, err := fileAvgWithContext(ctx) + if err != nil { + stat, err = sysinfoAvgWithContext(ctx) + } + return stat, err +} + +func sysinfoAvgWithContext(ctx context.Context) (*AvgStat, error) { + var info syscall.Sysinfo_t + err := syscall.Sysinfo(&info) + if err != nil { + return nil, err + } + + const si_load_shift = 16 + return &AvgStat{ + Load1: float64(info.Loads[0]) / float64(1< +*/ +import "C" + +import ( + "context" + "fmt" + "unsafe" + + "golang.org/x/sys/unix" +) + +// VirtualMemory returns VirtualmemoryStat. +func VirtualMemory() (*VirtualMemoryStat, error) { + return VirtualMemoryWithContext(context.Background()) +} + +func VirtualMemoryWithContext(ctx context.Context) (*VirtualMemoryStat, error) { + count := C.mach_msg_type_number_t(C.HOST_VM_INFO_COUNT) + var vmstat C.vm_statistics_data_t + + status := C.host_statistics(C.host_t(C.mach_host_self()), + C.HOST_VM_INFO, + C.host_info_t(unsafe.Pointer(&vmstat)), + &count) + + if status != C.KERN_SUCCESS { + return nil, fmt.Errorf("host_statistics error=%d", status) + } + + pageSize := uint64(unix.Getpagesize()) + total, err := getHwMemsize() + if err != nil { + return nil, err + } + totalCount := C.natural_t(total / pageSize) + + availableCount := vmstat.inactive_count + vmstat.free_count + usedPercent := 100 * float64(totalCount-availableCount) / float64(totalCount) + + usedCount := totalCount - availableCount + + return &VirtualMemoryStat{ + Total: total, + Available: pageSize * uint64(availableCount), + Used: pageSize * uint64(usedCount), + UsedPercent: usedPercent, + Free: pageSize * uint64(vmstat.free_count), + Active: pageSize * uint64(vmstat.active_count), + Inactive: pageSize * uint64(vmstat.inactive_count), + Wired: pageSize * uint64(vmstat.wire_count), + }, nil +} diff --git a/internal/gopsutil/mem/mem_darwin_nocgo.go b/internal/gopsutil/mem/mem_darwin_nocgo.go new file mode 100644 index 0000000..67d28d3 --- /dev/null +++ b/internal/gopsutil/mem/mem_darwin_nocgo.go @@ -0,0 +1,93 @@ +//go:build darwin && !cgo + +package mem + +import ( + "context" + "os/exec" + "strconv" + "strings" + + "golang.org/x/sys/unix" +) + +// Runs vm_stat and returns Free and inactive pages +func getVMStat(vms *VirtualMemoryStat) error { + vm_stat, err := exec.LookPath("vm_stat") + if err != nil { + return err + } + out, err := invoke.Command(vm_stat) + if err != nil { + return err + } + return parseVMStat(string(out), vms) +} + +func parseVMStat(out string, vms *VirtualMemoryStat) error { + var err error + + lines := strings.Split(out, "\n") + pagesize := uint64(unix.Getpagesize()) + for _, line := range lines { + fields := strings.Split(line, ":") + if len(fields) < 2 { + continue + } + key := strings.TrimSpace(fields[0]) + value := strings.Trim(fields[1], " .") + switch key { + case "Pages free": + free, e := strconv.ParseUint(value, 10, 64) + if e != nil { + err = e + } + vms.Free = free * pagesize + case "Pages inactive": + inactive, e := strconv.ParseUint(value, 10, 64) + if e != nil { + err = e + } + vms.Inactive = inactive * pagesize + case "Pages active": + active, e := strconv.ParseUint(value, 10, 64) + if e != nil { + err = e + } + vms.Active = active * pagesize + case "Pages wired down": + wired, e := strconv.ParseUint(value, 10, 64) + if e != nil { + err = e + } + vms.Wired = wired * pagesize + } + } + return err +} + +// VirtualMemory returns VirtualmemoryStat. +func VirtualMemory() (*VirtualMemoryStat, error) { + return VirtualMemoryWithContext(context.Background()) +} + +func VirtualMemoryWithContext(ctx context.Context) (*VirtualMemoryStat, error) { + ret := &VirtualMemoryStat{} + + total, err := getHwMemsize() + if err != nil { + return nil, err + } + err = getVMStat(ret) + if err != nil { + return nil, err + } + + ret.Available = ret.Free + ret.Inactive + ret.Total = total + + ret.Used = ret.Total - ret.Available + ret.UsedPercent = 100 * float64(ret.Used) / float64(ret.Total) + + return ret, nil +} diff --git a/internal/gopsutil/mem/mem_fallback.go b/internal/gopsutil/mem/mem_fallback.go new file mode 100644 index 0000000..b2b5856 --- /dev/null +++ b/internal/gopsutil/mem/mem_fallback.go @@ -0,0 +1,25 @@ +//go:build !darwin && !linux && !freebsd && !openbsd && !solaris && !windows + +package mem + +import ( + "context" + + "github.com/gofiber/fiber/v2/internal/gopsutil/common" +) + +func VirtualMemory() (*VirtualMemoryStat, error) { + return VirtualMemoryWithContext(context.Background()) +} + +func VirtualMemoryWithContext(ctx context.Context) (*VirtualMemoryStat, error) { + return nil, common.ErrNotImplementedError +} + +func SwapMemory() (*SwapMemoryStat, error) { + return SwapMemoryWithContext(context.Background()) +} + +func SwapMemoryWithContext(ctx context.Context) (*SwapMemoryStat, error) { + return nil, common.ErrNotImplementedError +} diff --git a/internal/gopsutil/mem/mem_freebsd.go b/internal/gopsutil/mem/mem_freebsd.go new file mode 100644 index 0000000..253bf9d --- /dev/null +++ b/internal/gopsutil/mem/mem_freebsd.go @@ -0,0 +1,167 @@ +//go:build freebsd + +package mem + +import ( + "context" + "errors" + "unsafe" + + "golang.org/x/sys/unix" + + "github.com/gofiber/fiber/v2/internal/gopsutil/common" +) + +func VirtualMemory() (*VirtualMemoryStat, error) { + return VirtualMemoryWithContext(context.Background()) +} + +func VirtualMemoryWithContext(ctx context.Context) (*VirtualMemoryStat, error) { + pageSize, err := common.SysctlUint("vm.stats.vm.v_page_size") + if err != nil { + return nil, err + } + physmem, err := common.SysctlUint("hw.physmem") + if err != nil { + return nil, err + } + + free, err := common.SysctlUint("vm.stats.vm.v_free_count") + if err != nil { + return nil, err + } + active, err := common.SysctlUint("vm.stats.vm.v_active_count") + if err != nil { + return nil, err + } + inactive, err := common.SysctlUint("vm.stats.vm.v_inactive_count") + if err != nil { + return nil, err + } + buffers, err := common.SysctlUint("vfs.bufspace") + if err != nil { + return nil, err + } + wired, err := common.SysctlUint("vm.stats.vm.v_wire_count") + if err != nil { + return nil, err + } + var cached, laundry uint64 + osreldate, _ := common.SysctlUint("kern.osreldate") + if osreldate < 1102000 { + cached, err = common.SysctlUint("vm.stats.vm.v_cache_count") + if err != nil { + return nil, err + } + } else { + laundry, err = common.SysctlUint("vm.stats.vm.v_laundry_count") + if err != nil { + return nil, err + } + } + + p := pageSize + ret := &VirtualMemoryStat{ + Total: physmem, + Free: free * p, + Active: active * p, + Inactive: inactive * p, + Cached: cached * p, + Buffers: buffers, + Wired: wired * p, + Laundry: laundry * p, + } + + ret.Available = ret.Inactive + ret.Cached + ret.Free + ret.Laundry + ret.Used = ret.Total - ret.Available + ret.UsedPercent = float64(ret.Used) / float64(ret.Total) * 100.0 + + return ret, nil +} + +// Return swapinfo +func SwapMemory() (*SwapMemoryStat, error) { + return SwapMemoryWithContext(context.Background()) +} + +// Constants from vm/vm_param.h +// nolint: golint +const ( + XSWDEV_VERSION11 = 1 + XSWDEV_VERSION = 2 +) + +// Types from vm/vm_param.h +type xswdev struct { + Version uint32 // Version is the version + Dev uint64 // Dev is the device identifier + Flags int32 // Flags is the swap flags applied to the device + NBlks int32 // NBlks is the total number of blocks + Used int32 // Used is the number of blocks used +} + +// xswdev11 is a compatibility for under FreeBSD 11 +// sys/vm/swap_pager.c +type xswdev11 struct { + Version uint32 // Version is the version + Dev uint32 // Dev is the device identifier + Flags int32 // Flags is the swap flags applied to the device + NBlks int32 // NBlks is the total number of blocks + Used int32 // Used is the number of blocks used +} + +func SwapMemoryWithContext(ctx context.Context) (*SwapMemoryStat, error) { + // FreeBSD can have multiple swap devices so we total them up + i, err := common.SysctlUint("vm.nswapdev") + if err != nil { + return nil, err + } + + if i == 0 { + return nil, errors.New("no swap devices found") + } + + c := int(i) + + i, err = common.SysctlUint("vm.stats.vm.v_page_size") + if err != nil { + return nil, err + } + pageSize := i + + var buf []byte + s := &SwapMemoryStat{} + for n := 0; n < c; n++ { + buf, err = unix.SysctlRaw("vm.swap_info", n) + if err != nil { + return nil, err + } + + // first, try to parse with version 2 + xsw := (*xswdev)(unsafe.Pointer(&buf[0])) + if xsw.Version == XSWDEV_VERSION11 { + // this is version 1, so try to parse again + xsw := (*xswdev11)(unsafe.Pointer(&buf[0])) + if xsw.Version != XSWDEV_VERSION11 { + return nil, errors.New("xswdev version mismatch(11)") + } + s.Total += uint64(xsw.NBlks) + s.Used += uint64(xsw.Used) + } else if xsw.Version != XSWDEV_VERSION { + return nil, errors.New("xswdev version mismatch") + } else { + s.Total += uint64(xsw.NBlks) + s.Used += uint64(xsw.Used) + } + + } + + if s.Total != 0 { + s.UsedPercent = float64(s.Used) / float64(s.Total) * 100 + } + s.Total *= pageSize + s.Used *= pageSize + s.Free = s.Total - s.Used + + return s, nil +} diff --git a/internal/gopsutil/mem/mem_linux.go b/internal/gopsutil/mem/mem_linux.go new file mode 100644 index 0000000..d9e0321 --- /dev/null +++ b/internal/gopsutil/mem/mem_linux.go @@ -0,0 +1,283 @@ +//go:build linux + +package mem + +import ( + "context" + "encoding/json" + "math" + "os" + "strconv" + "strings" + + "golang.org/x/sys/unix" + + "github.com/gofiber/fiber/v2/internal/gopsutil/common" +) + +type VirtualMemoryExStat struct { + ActiveFile uint64 `json:"activefile"` + InactiveFile uint64 `json:"inactivefile"` + ActiveAnon uint64 `json:"activeanon"` + InactiveAnon uint64 `json:"inactiveanon"` + Unevictable uint64 `json:"unevictable"` +} + +func (v VirtualMemoryExStat) String() string { + s, _ := json.Marshal(v) + return string(s) +} + +func VirtualMemory() (*VirtualMemoryStat, error) { + return VirtualMemoryWithContext(context.Background()) +} + +func VirtualMemoryWithContext(ctx context.Context) (*VirtualMemoryStat, error) { + vm, _, err := fillFromMeminfoWithContext(ctx) + if err != nil { + return nil, err + } + return vm, nil +} + +func VirtualMemoryEx() (*VirtualMemoryExStat, error) { + return VirtualMemoryExWithContext(context.Background()) +} + +func VirtualMemoryExWithContext(ctx context.Context) (*VirtualMemoryExStat, error) { + _, vmEx, err := fillFromMeminfoWithContext(ctx) + if err != nil { + return nil, err + } + return vmEx, nil +} + +func fillFromMeminfoWithContext(ctx context.Context) (*VirtualMemoryStat, *VirtualMemoryExStat, error) { + filename := common.HostProc("meminfo") + lines, _ := common.ReadLines(filename) + + // flag if MemAvailable is in /proc/meminfo (kernel 3.14+) + memavail := false + activeFile := false // "Active(file)" not available: 2.6.28 / Dec 2008 + inactiveFile := false // "Inactive(file)" not available: 2.6.28 / Dec 2008 + sReclaimable := false // "SReclaimable:" not available: 2.6.19 / Nov 2006 + + ret := &VirtualMemoryStat{} + retEx := &VirtualMemoryExStat{} + + for _, line := range lines { + fields := strings.Split(line, ":") + if len(fields) != 2 { + continue + } + key := strings.TrimSpace(fields[0]) + value := strings.TrimSpace(fields[1]) + value = strings.Replace(value, " kB", "", -1) + + t, err := strconv.ParseUint(value, 10, 64) + if err != nil { + return ret, retEx, err + } + switch key { + case "MemTotal": + ret.Total = t * 1024 + case "MemFree": + ret.Free = t * 1024 + case "MemAvailable": + memavail = true + ret.Available = t * 1024 + case "Buffers": + ret.Buffers = t * 1024 + case "Cached": + ret.Cached = t * 1024 + case "Active": + ret.Active = t * 1024 + case "Inactive": + ret.Inactive = t * 1024 + case "Active(anon)": + retEx.ActiveAnon = t * 1024 + case "Inactive(anon)": + retEx.InactiveAnon = t * 1024 + case "Active(file)": + activeFile = true + retEx.ActiveFile = t * 1024 + case "Inactive(file)": + inactiveFile = true + retEx.InactiveFile = t * 1024 + case "Unevictable": + retEx.Unevictable = t * 1024 + case "Writeback": + ret.Writeback = t * 1024 + case "WritebackTmp": + ret.WritebackTmp = t * 1024 + case "Dirty": + ret.Dirty = t * 1024 + case "Shmem": + ret.Shared = t * 1024 + case "Slab": + ret.Slab = t * 1024 + case "SReclaimable": + sReclaimable = true + ret.SReclaimable = t * 1024 + case "SUnreclaim": + ret.SUnreclaim = t * 1024 + case "PageTables": + ret.PageTables = t * 1024 + case "SwapCached": + ret.SwapCached = t * 1024 + case "CommitLimit": + ret.CommitLimit = t * 1024 + case "Committed_AS": + ret.CommittedAS = t * 1024 + case "HighTotal": + ret.HighTotal = t * 1024 + case "HighFree": + ret.HighFree = t * 1024 + case "LowTotal": + ret.LowTotal = t * 1024 + case "LowFree": + ret.LowFree = t * 1024 + case "SwapTotal": + ret.SwapTotal = t * 1024 + case "SwapFree": + ret.SwapFree = t * 1024 + case "Mapped": + ret.Mapped = t * 1024 + case "VmallocTotal": + ret.VMallocTotal = t * 1024 + case "VmallocUsed": + ret.VMallocUsed = t * 1024 + case "VmallocChunk": + ret.VMallocChunk = t * 1024 + case "HugePages_Total": + ret.HugePagesTotal = t + case "HugePages_Free": + ret.HugePagesFree = t + case "Hugepagesize": + ret.HugePageSize = t * 1024 + } + } + + ret.Cached += ret.SReclaimable + + if !memavail { + if activeFile && inactiveFile && sReclaimable { + ret.Available = calcuateAvailVmem(ret, retEx) + } else { + ret.Available = ret.Cached + ret.Free + } + } + + ret.Used = ret.Total - ret.Free - ret.Buffers - ret.Cached + ret.UsedPercent = float64(ret.Used) / float64(ret.Total) * 100.0 + + return ret, retEx, nil +} + +func SwapMemory() (*SwapMemoryStat, error) { + return SwapMemoryWithContext(context.Background()) +} + +func SwapMemoryWithContext(ctx context.Context) (*SwapMemoryStat, error) { + sysinfo := &unix.Sysinfo_t{} + + if err := unix.Sysinfo(sysinfo); err != nil { + return nil, err + } + ret := &SwapMemoryStat{ + Total: uint64(sysinfo.Totalswap) * uint64(sysinfo.Unit), + Free: uint64(sysinfo.Freeswap) * uint64(sysinfo.Unit), + } + ret.Used = ret.Total - ret.Free + // check Infinity + if ret.Total != 0 { + ret.UsedPercent = float64(ret.Total-ret.Free) / float64(ret.Total) * 100.0 + } else { + ret.UsedPercent = 0 + } + filename := common.HostProc("vmstat") + lines, _ := common.ReadLines(filename) + for _, l := range lines { + fields := strings.Fields(l) + if len(fields) < 2 { + continue + } + switch fields[0] { + case "pswpin": + value, err := strconv.ParseUint(fields[1], 10, 64) + if err != nil { + continue + } + ret.Sin = value * 4 * 1024 + case "pswpout": + value, err := strconv.ParseUint(fields[1], 10, 64) + if err != nil { + continue + } + ret.Sout = value * 4 * 1024 + case "pgpgin": + value, err := strconv.ParseUint(fields[1], 10, 64) + if err != nil { + continue + } + ret.PgIn = value * 4 * 1024 + case "pgpgout": + value, err := strconv.ParseUint(fields[1], 10, 64) + if err != nil { + continue + } + ret.PgOut = value * 4 * 1024 + case "pgfault": + value, err := strconv.ParseUint(fields[1], 10, 64) + if err != nil { + continue + } + ret.PgFault = value * 4 * 1024 + case "pgmajfault": + value, err := strconv.ParseUint(fields[1], 10, 64) + if err != nil { + continue + } + ret.PgMajFault = value * 4 * 1024 + } + } + return ret, nil +} + +// calcuateAvailVmem is a fallback under kernel 3.14 where /proc/meminfo does not provide +// "MemAvailable:" column. It reimplements an algorithm from the link below +// https://github.com/giampaolo/psutil/pull/890 +func calcuateAvailVmem(ret *VirtualMemoryStat, retEx *VirtualMemoryExStat) uint64 { + var watermarkLow uint64 + + fn := common.HostProc("zoneinfo") + lines, err := common.ReadLines(fn) + if err != nil { + return ret.Free + ret.Cached // fallback under kernel 2.6.13 + } + + pagesize := uint64(os.Getpagesize()) + watermarkLow = 0 + + for _, line := range lines { + fields := strings.Fields(line) + + if strings.HasPrefix(fields[0], "low") { + lowValue, err := strconv.ParseUint(fields[1], 10, 64) + if err != nil { + lowValue = 0 + } + watermarkLow += lowValue + } + } + + watermarkLow *= pagesize + + availMemory := ret.Free - watermarkLow + pageCache := retEx.ActiveFile + retEx.InactiveFile + pageCache -= uint64(math.Min(float64(pageCache/2), float64(watermarkLow))) + availMemory += pageCache + availMemory += ret.SReclaimable - uint64(math.Min(float64(ret.SReclaimable/2.0), float64(watermarkLow))) + + return availMemory +} diff --git a/internal/gopsutil/mem/mem_openbsd.go b/internal/gopsutil/mem/mem_openbsd.go new file mode 100644 index 0000000..cacbb13 --- /dev/null +++ b/internal/gopsutil/mem/mem_openbsd.go @@ -0,0 +1,106 @@ +//go:build openbsd + +package mem + +import ( + "bytes" + "context" + "encoding/binary" + "errors" + "fmt" + "os/exec" + + "golang.org/x/sys/unix" + + "github.com/gofiber/fiber/v2/internal/gopsutil/common" +) + +func GetPageSize() (uint64, error) { + return GetPageSizeWithContext(context.Background()) +} + +func GetPageSizeWithContext(ctx context.Context) (uint64, error) { + uvmexp, err := unix.SysctlUvmexp("vm.uvmexp") + if err != nil { + return 0, err + } + return uint64(uvmexp.Pagesize), nil +} + +func VirtualMemory() (*VirtualMemoryStat, error) { + return VirtualMemoryWithContext(context.Background()) +} + +func VirtualMemoryWithContext(ctx context.Context) (*VirtualMemoryStat, error) { + uvmexp, err := unix.SysctlUvmexp("vm.uvmexp") + if err != nil { + return nil, err + } + p := uint64(uvmexp.Pagesize) + + ret := &VirtualMemoryStat{ + Total: uint64(uvmexp.Npages) * p, + Free: uint64(uvmexp.Free) * p, + Active: uint64(uvmexp.Active) * p, + Inactive: uint64(uvmexp.Inactive) * p, + Cached: 0, // not available + Wired: uint64(uvmexp.Wired) * p, + } + + ret.Available = ret.Inactive + ret.Cached + ret.Free + ret.Used = ret.Total - ret.Available + ret.UsedPercent = float64(ret.Used) / float64(ret.Total) * 100.0 + + mib := []int32{CTLVfs, VfsGeneric, VfsBcacheStat} + buf, length, err := common.CallSyscall(mib) + if err != nil { + return nil, err + } + if length < sizeOfBcachestats { + return nil, fmt.Errorf("short syscall ret %d bytes", length) + } + var bcs Bcachestats + br := bytes.NewReader(buf) + err = common.Read(br, binary.LittleEndian, &bcs) + if err != nil { + return nil, err + } + ret.Buffers = uint64(bcs.Numbufpages) * p + + return ret, nil +} + +// Return swapctl summary info +func SwapMemory() (*SwapMemoryStat, error) { + return SwapMemoryWithContext(context.Background()) +} + +func SwapMemoryWithContext(ctx context.Context) (*SwapMemoryStat, error) { + swapctl, err := exec.LookPath("swapctl") + if err != nil { + return nil, err + } + + out, err := invoke.CommandWithContext(ctx, swapctl, "-sk") + if err != nil { + return &SwapMemoryStat{}, nil + } + + line := string(out) + var total, used, free uint64 + + _, err = fmt.Sscanf(line, + "total: %d 1K-blocks allocated, %d used, %d available", + &total, &used, &free) + if err != nil { + return nil, errors.New("failed to parse swapctl output") + } + + percent := float64(used) / float64(total) * 100 + return &SwapMemoryStat{ + Total: total * 1024, + Used: used * 1024, + Free: free * 1024, + UsedPercent: percent, + }, nil +} diff --git a/internal/gopsutil/mem/mem_openbsd_386.go b/internal/gopsutil/mem/mem_openbsd_386.go new file mode 100644 index 0000000..77272f3 --- /dev/null +++ b/internal/gopsutil/mem/mem_openbsd_386.go @@ -0,0 +1,37 @@ +//go:build openbsd && 386 + +// Code generated by cmd/cgo -godefs; DO NOT EDIT. +// cgo -godefs mem/types_openbsd.go + +package mem + +const ( + CTLVfs = 10 + VfsGeneric = 0 + VfsBcacheStat = 3 +) + +const ( + sizeOfBcachestats = 0x90 +) + +type Bcachestats struct { + Numbufs int64 + Numbufpages int64 + Numdirtypages int64 + Numcleanpages int64 + Pendingwrites int64 + Pendingreads int64 + Numwrites int64 + Numreads int64 + Cachehits int64 + Busymapped int64 + Dmapages int64 + Highpages int64 + Delwribufs int64 + Kvaslots int64 + Avail int64 + Highflips int64 + Highflops int64 + Dmaflips int64 +} diff --git a/internal/gopsutil/mem/mem_openbsd_amd64.go b/internal/gopsutil/mem/mem_openbsd_amd64.go new file mode 100644 index 0000000..d187abf --- /dev/null +++ b/internal/gopsutil/mem/mem_openbsd_amd64.go @@ -0,0 +1,32 @@ +// Created by cgo -godefs - DO NOT EDIT +// cgo -godefs types_openbsd.go + +package mem + +const ( + CTLVfs = 10 + VfsGeneric = 0 + VfsBcacheStat = 3 +) + +const ( + sizeOfBcachestats = 0x78 +) + +type Bcachestats struct { + Numbufs int64 + Numbufpages int64 + Numdirtypages int64 + Numcleanpages int64 + Pendingwrites int64 + Pendingreads int64 + Numwrites int64 + Numreads int64 + Cachehits int64 + Busymapped int64 + Dmapages int64 + Highpages int64 + Delwribufs int64 + Kvaslots int64 + Avail int64 +} diff --git a/internal/gopsutil/mem/mem_solaris.go b/internal/gopsutil/mem/mem_solaris.go new file mode 100644 index 0000000..8fe7f27 --- /dev/null +++ b/internal/gopsutil/mem/mem_solaris.go @@ -0,0 +1,121 @@ +package mem + +import ( + "context" + "errors" + "fmt" + "os/exec" + "regexp" + "strconv" + "strings" + + "github.com/gofiber/fiber/v2/internal/gopsutil/common" +) + +// VirtualMemory for Solaris is a minimal implementation which only returns +// what Nomad needs. It does take into account global vs zone, however. +func VirtualMemory() (*VirtualMemoryStat, error) { + return VirtualMemoryWithContext(context.Background()) +} + +func VirtualMemoryWithContext(ctx context.Context) (*VirtualMemoryStat, error) { + result := &VirtualMemoryStat{} + + zoneName, err := zoneName() + if err != nil { + return nil, err + } + + if zoneName == "global" { + cap, err := globalZoneMemoryCapacity() + if err != nil { + return nil, err + } + result.Total = cap + } else { + cap, err := nonGlobalZoneMemoryCapacity() + if err != nil { + return nil, err + } + result.Total = cap + } + + return result, nil +} + +func SwapMemory() (*SwapMemoryStat, error) { + return SwapMemoryWithContext(context.Background()) +} + +func SwapMemoryWithContext(ctx context.Context) (*SwapMemoryStat, error) { + return nil, common.ErrNotImplementedError +} + +func zoneName() (string, error) { + zonename, err := exec.LookPath("zonename") + if err != nil { + return "", err + } + + ctx := context.Background() + out, err := invoke.CommandWithContext(ctx, zonename) + if err != nil { + return "", err + } + + return strings.TrimSpace(string(out)), nil +} + +var globalZoneMemoryCapacityMatch = regexp.MustCompile(`memory size: ([\d]+) Megabytes`) + +func globalZoneMemoryCapacity() (uint64, error) { + prtconf, err := exec.LookPath("prtconf") + if err != nil { + return 0, err + } + + ctx := context.Background() + out, err := invoke.CommandWithContext(ctx, prtconf) + if err != nil { + return 0, err + } + + match := globalZoneMemoryCapacityMatch.FindAllStringSubmatch(string(out), -1) + if len(match) != 1 { + return 0, errors.New("memory size not contained in output of /usr/sbin/prtconf") + } + + totalMB, err := strconv.ParseUint(match[0][1], 10, 64) + if err != nil { + return 0, err + } + + return totalMB * 1024 * 1024, nil +} + +var kstatMatch = regexp.MustCompile(`([^\s]+)[\s]+([^\s]*)`) + +func nonGlobalZoneMemoryCapacity() (uint64, error) { + kstat, err := exec.LookPath("kstat") + if err != nil { + return 0, err + } + + ctx := context.Background() + out, err := invoke.CommandWithContext(ctx, kstat, "-p", "-c", "zone_memory_cap", "memory_cap:*:*:physcap") + if err != nil { + return 0, err + } + + kstats := kstatMatch.FindAllStringSubmatch(string(out), -1) + if len(kstats) != 1 { + return 0, fmt.Errorf("expected 1 kstat, found %d", len(kstats)) + } + + memSizeBytes, err := strconv.ParseUint(kstats[0][2], 10, 64) + if err != nil { + return 0, err + } + + return memSizeBytes, nil +} diff --git a/internal/gopsutil/mem/mem_windows.go b/internal/gopsutil/mem/mem_windows.go new file mode 100644 index 0000000..dbec732 --- /dev/null +++ b/internal/gopsutil/mem/mem_windows.go @@ -0,0 +1,99 @@ +//go:build windows + +package mem + +import ( + "context" + "unsafe" + + "golang.org/x/sys/windows" + + "github.com/gofiber/fiber/v2/internal/gopsutil/common" +) + +var ( + procGlobalMemoryStatusEx = common.Modkernel32.NewProc("GlobalMemoryStatusEx") + procGetPerformanceInfo = common.ModPsapi.NewProc("GetPerformanceInfo") +) + +type memoryStatusEx struct { + cbSize uint32 + dwMemoryLoad uint32 + ullTotalPhys uint64 // in bytes + ullAvailPhys uint64 + ullTotalPageFile uint64 + ullAvailPageFile uint64 + ullTotalVirtual uint64 + ullAvailVirtual uint64 + ullAvailExtendedVirtual uint64 +} + +func VirtualMemory() (*VirtualMemoryStat, error) { + return VirtualMemoryWithContext(context.Background()) +} + +func VirtualMemoryWithContext(ctx context.Context) (*VirtualMemoryStat, error) { + var memInfo memoryStatusEx + memInfo.cbSize = uint32(unsafe.Sizeof(memInfo)) + mem, _, _ := procGlobalMemoryStatusEx.Call(uintptr(unsafe.Pointer(&memInfo))) + if mem == 0 { + return nil, windows.GetLastError() + } + + ret := &VirtualMemoryStat{ + Total: memInfo.ullTotalPhys, + Available: memInfo.ullAvailPhys, + Free: memInfo.ullAvailPhys, + UsedPercent: float64(memInfo.dwMemoryLoad), + } + + ret.Used = ret.Total - ret.Available + return ret, nil +} + +type performanceInformation struct { + cb uint32 + commitTotal uint64 + commitLimit uint64 + commitPeak uint64 + physicalTotal uint64 + physicalAvailable uint64 + systemCache uint64 + kernelTotal uint64 + kernelPaged uint64 + kernelNonpaged uint64 + pageSize uint64 + handleCount uint32 + processCount uint32 + threadCount uint32 +} + +func SwapMemory() (*SwapMemoryStat, error) { + return SwapMemoryWithContext(context.Background()) +} + +func SwapMemoryWithContext(ctx context.Context) (*SwapMemoryStat, error) { + var perfInfo performanceInformation + perfInfo.cb = uint32(unsafe.Sizeof(perfInfo)) + mem, _, _ := procGetPerformanceInfo.Call(uintptr(unsafe.Pointer(&perfInfo)), uintptr(perfInfo.cb)) + if mem == 0 { + return nil, windows.GetLastError() + } + tot := perfInfo.commitLimit * perfInfo.pageSize + used := perfInfo.commitTotal * perfInfo.pageSize + free := tot - used + var usedPercent float64 + if tot == 0 { + usedPercent = 0 + } else { + usedPercent = float64(used) / float64(tot) * 100 + } + ret := &SwapMemoryStat{ + Total: tot, + Used: used, + Free: free, + UsedPercent: usedPercent, + } + + return ret, nil +} diff --git a/internal/gopsutil/mem/types_openbsd.go b/internal/gopsutil/mem/types_openbsd.go new file mode 100644 index 0000000..07ccbb2 --- /dev/null +++ b/internal/gopsutil/mem/types_openbsd.go @@ -0,0 +1,28 @@ +//go:build ignore + +/* +Input to cgo -godefs. +*/ + +package mem + +/* +#include +#include +#include +*/ +import "C" + +// Machine characteristics; for internal use. + +const ( + CTLVfs = 10 + VfsGeneric = 0 + VfsBcacheStat = 3 +) + +const ( + sizeOfBcachestats = C.sizeof_struct_bcachestats +) + +type Bcachestats C.struct_bcachestats diff --git a/internal/gopsutil/net/net.go b/internal/gopsutil/net/net.go new file mode 100644 index 0000000..4c4e91f --- /dev/null +++ b/internal/gopsutil/net/net.go @@ -0,0 +1,262 @@ +package net + +import ( + "context" + "encoding/json" + "net" + + "github.com/gofiber/fiber/v2/internal/gopsutil/common" +) + +var invoke common.Invoker = common.Invoke{} + +type IOCountersStat struct { + Name string `json:"name"` // interface name + BytesSent uint64 `json:"bytesSent"` // number of bytes sent + BytesRecv uint64 `json:"bytesRecv"` // number of bytes received + PacketsSent uint64 `json:"packetsSent"` // number of packets sent + PacketsRecv uint64 `json:"packetsRecv"` // number of packets received + Errin uint64 `json:"errin"` // total number of errors while receiving + Errout uint64 `json:"errout"` // total number of errors while sending + Dropin uint64 `json:"dropin"` // total number of incoming packets which were dropped + Dropout uint64 `json:"dropout"` // total number of outgoing packets which were dropped (always 0 on OSX and BSD) + Fifoin uint64 `json:"fifoin"` // total number of FIFO buffers errors while receiving + Fifoout uint64 `json:"fifoout"` // total number of FIFO buffers errors while sending +} + +// Addr is implemented compatibility to psutil +type Addr struct { + IP string `json:"ip"` + Port uint32 `json:"port"` +} + +type ConnectionStat struct { + Fd uint32 `json:"fd"` + Family uint32 `json:"family"` + Type uint32 `json:"type"` + Laddr Addr `json:"localaddr"` + Raddr Addr `json:"remoteaddr"` + Status string `json:"status"` + Uids []int32 `json:"uids"` + Pid int32 `json:"pid"` +} + +// System wide stats about different network protocols +type ProtoCountersStat struct { + Protocol string `json:"protocol"` + Stats map[string]int64 `json:"stats"` +} + +// NetInterfaceAddr is designed for represent interface addresses +type InterfaceAddr struct { + Addr string `json:"addr"` +} + +type InterfaceStat struct { + Index int `json:"index"` + MTU int `json:"mtu"` // maximum transmission unit + Name string `json:"name"` // e.g., "en0", "lo0", "eth0.100" + HardwareAddr string `json:"hardwareaddr"` // IEEE MAC-48, EUI-48 and EUI-64 form + Flags []string `json:"flags"` // e.g., FlagUp, FlagLoopback, FlagMulticast + Addrs []InterfaceAddr `json:"addrs"` +} + +type FilterStat struct { + ConnTrackCount int64 `json:"conntrackCount"` + ConnTrackMax int64 `json:"conntrackMax"` +} + +// ConntrackStat has conntrack summary info +type ConntrackStat struct { + Entries uint32 `json:"entries"` // Number of entries in the conntrack table + Searched uint32 `json:"searched"` // Number of conntrack table lookups performed + Found uint32 `json:"found"` // Number of searched entries which were successful + New uint32 `json:"new"` // Number of entries added which were not expected before + Invalid uint32 `json:"invalid"` // Number of packets seen which can not be tracked + Ignore uint32 `json:"ignore"` // Packets seen which are already connected to an entry + Delete uint32 `json:"delete"` // Number of entries which were removed + DeleteList uint32 `json:"delete_list"` // Number of entries which were put to dying list + Insert uint32 `json:"insert"` // Number of entries inserted into the list + InsertFailed uint32 `json:"insert_failed"` // # insertion attempted but failed (same entry exists) + Drop uint32 `json:"drop"` // Number of packets dropped due to conntrack failure. + EarlyDrop uint32 `json:"early_drop"` // Dropped entries to make room for new ones, if maxsize reached + IcmpError uint32 `json:"icmp_error"` // Subset of invalid. Packets that can't be tracked d/t error + ExpectNew uint32 `json:"expect_new"` // Entries added after an expectation was already present + ExpectCreate uint32 `json:"expect_create"` // Expectations added + ExpectDelete uint32 `json:"expect_delete"` // Expectations deleted + SearchRestart uint32 `json:"search_restart"` // Conntrack table lookups restarted due to hashtable resizes +} + +func NewConntrackStat(e, s, f, n, inv, ign, del, dlst, ins, insfail, drop, edrop, ie, en, ec, ed, sr uint32) *ConntrackStat { + return &ConntrackStat{ + Entries: e, + Searched: s, + Found: f, + New: n, + Invalid: inv, + Ignore: ign, + Delete: del, + DeleteList: dlst, + Insert: ins, + InsertFailed: insfail, + Drop: drop, + EarlyDrop: edrop, + IcmpError: ie, + ExpectNew: en, + ExpectCreate: ec, + ExpectDelete: ed, + SearchRestart: sr, + } +} + +type ConntrackStatList struct { + items []*ConntrackStat +} + +func NewConntrackStatList() *ConntrackStatList { + return &ConntrackStatList{ + items: []*ConntrackStat{}, + } +} + +func (l *ConntrackStatList) Append(c *ConntrackStat) { + l.items = append(l.items, c) +} + +func (l *ConntrackStatList) Items() []ConntrackStat { + items := make([]ConntrackStat, len(l.items)) + for i, el := range l.items { + items[i] = *el + } + return items +} + +// Summary returns a single-element list with totals from all list items. +func (l *ConntrackStatList) Summary() []ConntrackStat { + summary := NewConntrackStat(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0) + for _, cs := range l.items { + summary.Entries += cs.Entries + summary.Searched += cs.Searched + summary.Found += cs.Found + summary.New += cs.New + summary.Invalid += cs.Invalid + summary.Ignore += cs.Ignore + summary.Delete += cs.Delete + summary.DeleteList += cs.DeleteList + summary.Insert += cs.Insert + summary.InsertFailed += cs.InsertFailed + summary.Drop += cs.Drop + summary.EarlyDrop += cs.EarlyDrop + summary.IcmpError += cs.IcmpError + summary.ExpectNew += cs.ExpectNew + summary.ExpectCreate += cs.ExpectCreate + summary.ExpectDelete += cs.ExpectDelete + summary.SearchRestart += cs.SearchRestart + } + return []ConntrackStat{*summary} +} + +func (n IOCountersStat) String() string { + s, _ := json.Marshal(n) + return string(s) +} + +func (n ConnectionStat) String() string { + s, _ := json.Marshal(n) + return string(s) +} + +func (n ProtoCountersStat) String() string { + s, _ := json.Marshal(n) + return string(s) +} + +func (a Addr) String() string { + s, _ := json.Marshal(a) + return string(s) +} + +func (n InterfaceStat) String() string { + s, _ := json.Marshal(n) + return string(s) +} + +func (n InterfaceAddr) String() string { + s, _ := json.Marshal(n) + return string(s) +} + +func (n ConntrackStat) String() string { + s, _ := json.Marshal(n) + return string(s) +} + +func Interfaces() ([]InterfaceStat, error) { + return InterfacesWithContext(context.Background()) +} + +func InterfacesWithContext(ctx context.Context) ([]InterfaceStat, error) { + is, err := net.Interfaces() + if err != nil { + return nil, err + } + ret := make([]InterfaceStat, 0, len(is)) + for _, ifi := range is { + + var flags []string + if ifi.Flags&net.FlagUp != 0 { + flags = append(flags, "up") + } + if ifi.Flags&net.FlagBroadcast != 0 { + flags = append(flags, "broadcast") + } + if ifi.Flags&net.FlagLoopback != 0 { + flags = append(flags, "loopback") + } + if ifi.Flags&net.FlagPointToPoint != 0 { + flags = append(flags, "pointtopoint") + } + if ifi.Flags&net.FlagMulticast != 0 { + flags = append(flags, "multicast") + } + + r := InterfaceStat{ + Index: ifi.Index, + Name: ifi.Name, + MTU: ifi.MTU, + HardwareAddr: ifi.HardwareAddr.String(), + Flags: flags, + } + addrs, err := ifi.Addrs() + if err == nil { + r.Addrs = make([]InterfaceAddr, 0, len(addrs)) + for _, addr := range addrs { + r.Addrs = append(r.Addrs, InterfaceAddr{ + Addr: addr.String(), + }) + } + + } + ret = append(ret, r) + } + + return ret, nil +} + +func getIOCountersAll(n []IOCountersStat) ([]IOCountersStat, error) { + r := IOCountersStat{ + Name: "all", + } + for _, nic := range n { + r.BytesRecv += nic.BytesRecv + r.PacketsRecv += nic.PacketsRecv + r.Errin += nic.Errin + r.Dropin += nic.Dropin + r.BytesSent += nic.BytesSent + r.PacketsSent += nic.PacketsSent + r.Errout += nic.Errout + r.Dropout += nic.Dropout + } + + return []IOCountersStat{r}, nil +} diff --git a/internal/gopsutil/net/net_aix.go b/internal/gopsutil/net/net_aix.go new file mode 100644 index 0000000..4b43de0 --- /dev/null +++ b/internal/gopsutil/net/net_aix.go @@ -0,0 +1,420 @@ +//go:build aix + +package net + +import ( + "context" + "fmt" + "os/exec" + "regexp" + "strconv" + "strings" + "syscall" + + "github.com/gofiber/fiber/v2/internal/gopsutil/common" +) + +func parseNetstatI(output string) ([]IOCountersStat, error) { + lines := strings.Split(string(output), "\n") + ret := make([]IOCountersStat, 0, len(lines)-1) + exists := make([]string, 0, len(ret)) + + // Check first line is header + if len(lines) > 0 && strings.Fields(lines[0])[0] != "Name" { + return nil, fmt.Errorf("not a 'netstat -i' output") + } + + for _, line := range lines[1:] { + values := strings.Fields(line) + if len(values) < 1 || values[0] == "Name" { + continue + } + if common.StringsHas(exists, values[0]) { + // skip if already get + continue + } + exists = append(exists, values[0]) + + if len(values) < 9 { + continue + } + + base := 1 + // sometimes Address is omitted + if len(values) < 10 { + base = 0 + } + + parsed := make([]uint64, 0, 5) + vv := []string{ + values[base+3], // Ipkts == PacketsRecv + values[base+4], // Ierrs == Errin + values[base+5], // Opkts == PacketsSent + values[base+6], // Oerrs == Errout + values[base+8], // Drops == Dropout + } + + for _, target := range vv { + if target == "-" { + parsed = append(parsed, 0) + continue + } + + t, err := strconv.ParseUint(target, 10, 64) + if err != nil { + return nil, err + } + parsed = append(parsed, t) + } + + n := IOCountersStat{ + Name: values[0], + PacketsRecv: parsed[0], + Errin: parsed[1], + PacketsSent: parsed[2], + Errout: parsed[3], + Dropout: parsed[4], + } + ret = append(ret, n) + } + return ret, nil +} + +func IOCounters(pernic bool) ([]IOCountersStat, error) { + return IOCountersWithContext(context.Background(), pernic) +} + +func IOCountersWithContext(ctx context.Context, pernic bool) ([]IOCountersStat, error) { + netstat, err := exec.LookPath("netstat") + if err != nil { + return nil, err + } + out, err := invoke.CommandWithContext(ctx, netstat, "-idn") + if err != nil { + return nil, err + } + + iocounters, err := parseNetstatI(string(out)) + if err != nil { + return nil, err + } + if pernic == false { + return getIOCountersAll(iocounters) + } + return iocounters, nil +} + +// NetIOCountersByFile is an method which is added just a compatibility for linux. +func IOCountersByFile(pernic bool, filename string) ([]IOCountersStat, error) { + return IOCountersByFileWithContext(context.Background(), pernic, filename) +} + +func IOCountersByFileWithContext(ctx context.Context, pernic bool, filename string) ([]IOCountersStat, error) { + return IOCounters(pernic) +} + +func FilterCounters() ([]FilterStat, error) { + return FilterCountersWithContext(context.Background()) +} + +func FilterCountersWithContext(ctx context.Context) ([]FilterStat, error) { + return nil, common.ErrNotImplementedError +} + +func ConntrackStats(percpu bool) ([]ConntrackStat, error) { + return ConntrackStatsWithContext(context.Background(), percpu) +} + +func ConntrackStatsWithContext(ctx context.Context, percpu bool) ([]ConntrackStat, error) { + return nil, common.ErrNotImplementedError +} + +func ProtoCounters(protocols []string) ([]ProtoCountersStat, error) { + return ProtoCountersWithContext(context.Background(), protocols) +} + +func ProtoCountersWithContext(ctx context.Context, protocols []string) ([]ProtoCountersStat, error) { + return nil, common.ErrNotImplementedError +} + +func parseNetstatNetLine(line string) (ConnectionStat, error) { + f := strings.Fields(line) + if len(f) < 5 { + return ConnectionStat{}, fmt.Errorf("wrong line,%s", line) + } + + var netType, netFamily uint32 + switch f[0] { + case "tcp", "tcp4": + netType = syscall.SOCK_STREAM + netFamily = syscall.AF_INET + case "udp", "udp4": + netType = syscall.SOCK_DGRAM + netFamily = syscall.AF_INET + case "tcp6": + netType = syscall.SOCK_STREAM + netFamily = syscall.AF_INET6 + case "udp6": + netType = syscall.SOCK_DGRAM + netFamily = syscall.AF_INET6 + default: + return ConnectionStat{}, fmt.Errorf("unknown type, %s", f[0]) + } + + laddr, raddr, err := parseNetstatAddr(f[3], f[4], netFamily) + if err != nil { + return ConnectionStat{}, fmt.Errorf("failed to parse netaddr, %s %s", f[3], f[4]) + } + + n := ConnectionStat{ + Fd: uint32(0), // not supported + Family: uint32(netFamily), + Type: uint32(netType), + Laddr: laddr, + Raddr: raddr, + Pid: int32(0), // not supported + } + if len(f) == 6 { + n.Status = f[5] + } + + return n, nil +} + +var portMatch = regexp.MustCompile(`(.*)\.(\d+)$`) + +// This function only works for netstat returning addresses with a "." +// before the port (0.0.0.0.22 instead of 0.0.0.0:22). +func parseNetstatAddr(local, remote string, family uint32) (laddr, raddr Addr, err error) { + parse := func(l string) (Addr, error) { + matches := portMatch.FindStringSubmatch(l) + if matches == nil { + return Addr{}, fmt.Errorf("wrong addr, %s", l) + } + host := matches[1] + port := matches[2] + if host == "*" { + switch family { + case syscall.AF_INET: + host = "0.0.0.0" + case syscall.AF_INET6: + host = "::" + default: + return Addr{}, fmt.Errorf("unknown family, %d", family) + } + } + lport, err := strconv.Atoi(port) + if err != nil { + return Addr{}, err + } + return Addr{IP: host, Port: uint32(lport)}, nil + } + + laddr, err = parse(local) + if remote != "*.*" { // remote addr exists + raddr, err = parse(remote) + if err != nil { + return laddr, raddr, err + } + } + + return laddr, raddr, err +} + +func parseNetstatUnixLine(f []string) (ConnectionStat, error) { + if len(f) < 8 { + return ConnectionStat{}, fmt.Errorf("wrong number of fields: expected >=8 got %d", len(f)) + } + + var netType uint32 + + switch f[1] { + case "dgram": + netType = syscall.SOCK_DGRAM + case "stream": + netType = syscall.SOCK_STREAM + default: + return ConnectionStat{}, fmt.Errorf("unknown type: %s", f[1]) + } + + // Some Unix Socket don't have any address associated + addr := "" + if len(f) == 9 { + addr = f[8] + } + + c := ConnectionStat{ + Fd: uint32(0), // not supported + Family: uint32(syscall.AF_UNIX), + Type: uint32(netType), + Laddr: Addr{ + IP: addr, + }, + Status: "NONE", + Pid: int32(0), // not supported + } + + return c, nil +} + +// Return true if proto is the corresponding to the kind parameter +// Only for Inet lines +func hasCorrectInetProto(kind, proto string) bool { + switch kind { + case "all", "inet": + return true + case "unix": + return false + case "inet4": + return !strings.HasSuffix(proto, "6") + case "inet6": + return strings.HasSuffix(proto, "6") + case "tcp": + return proto == "tcp" || proto == "tcp4" || proto == "tcp6" + case "tcp4": + return proto == "tcp" || proto == "tcp4" + case "tcp6": + return proto == "tcp6" + case "udp": + return proto == "udp" || proto == "udp4" || proto == "udp6" + case "udp4": + return proto == "udp" || proto == "udp4" + case "udp6": + return proto == "udp6" + } + return false +} + +func parseNetstatA(output, kind string) ([]ConnectionStat, error) { + var ret []ConnectionStat + lines := strings.Split(string(output), "\n") + + for _, line := range lines { + fields := strings.Fields(line) + if len(fields) < 1 { + continue + } + + if strings.HasPrefix(fields[0], "f1") { + // Unix lines + if len(fields) < 2 { + // every unix connections have two lines + continue + } + + c, err := parseNetstatUnixLine(fields) + if err != nil { + return nil, fmt.Errorf("failed to parse Unix Address (%s): %s", line, err) + } + + ret = append(ret, c) + + } else if strings.HasPrefix(fields[0], "tcp") || strings.HasPrefix(fields[0], "udp") { + // Inet lines + if !hasCorrectInetProto(kind, fields[0]) { + continue + } + + // On AIX, netstat display some connections with "*.*" as local addresses + // Skip them as they aren't real connections. + if fields[3] == "*.*" { + continue + } + + c, err := parseNetstatNetLine(line) + if err != nil { + return nil, fmt.Errorf("failed to parse Inet Address (%s): %s", line, err) + } + + ret = append(ret, c) + } else { + // Header lines + continue + } + } + + return ret, nil +} + +func Connections(kind string) ([]ConnectionStat, error) { + return ConnectionsWithContext(context.Background(), kind) +} + +func ConnectionsWithContext(ctx context.Context, kind string) ([]ConnectionStat, error) { + args := []string{"-na"} + switch strings.ToLower(kind) { + default: + fallthrough + case "": + kind = "all" + case "all": + // nothing to add + case "inet", "inet4", "inet6": + args = append(args, "-finet") + case "tcp", "tcp4", "tcp6": + args = append(args, "-finet") + case "udp", "udp4", "udp6": + args = append(args, "-finet") + case "unix": + args = append(args, "-funix") + } + + netstat, err := exec.LookPath("netstat") + if err != nil { + return nil, err + } + out, err := invoke.CommandWithContext(ctx, netstat, args...) + if err != nil { + return nil, err + } + + ret, err := parseNetstatA(string(out), kind) + if err != nil { + return nil, err + } + + return ret, nil +} + +func ConnectionsMax(kind string, max int) ([]ConnectionStat, error) { + return ConnectionsMaxWithContext(context.Background(), kind, max) +} + +func ConnectionsMaxWithContext(ctx context.Context, kind string, max int) ([]ConnectionStat, error) { + return []ConnectionStat{}, common.ErrNotImplementedError +} + +// Return a list of network connections opened, omitting `Uids`. +// WithoutUids functions are reliant on implementation details. They may be altered to be an alias for Connections or be +// removed from the API in the future. +func ConnectionsWithoutUids(kind string) ([]ConnectionStat, error) { + return ConnectionsWithoutUidsWithContext(context.Background(), kind) +} + +func ConnectionsWithoutUidsWithContext(ctx context.Context, kind string) ([]ConnectionStat, error) { + return ConnectionsMaxWithoutUidsWithContext(ctx, kind, 0) +} + +func ConnectionsMaxWithoutUidsWithContext(ctx context.Context, kind string, max int) ([]ConnectionStat, error) { + return ConnectionsPidMaxWithoutUidsWithContext(ctx, kind, 0, max) +} + +func ConnectionsPidWithoutUids(kind string, pid int32) ([]ConnectionStat, error) { + return ConnectionsPidWithoutUidsWithContext(context.Background(), kind, pid) +} + +func ConnectionsPidWithoutUidsWithContext(ctx context.Context, kind string, pid int32) ([]ConnectionStat, error) { + return ConnectionsPidMaxWithoutUidsWithContext(ctx, kind, pid, 0) +} + +func ConnectionsPidMaxWithoutUids(kind string, pid int32, max int) ([]ConnectionStat, error) { + return ConnectionsPidMaxWithoutUidsWithContext(context.Background(), kind, pid, max) +} + +func ConnectionsPidMaxWithoutUidsWithContext(ctx context.Context, kind string, pid int32, max int) ([]ConnectionStat, error) { + return connectionsPidMaxWithoutUidsWithContext(ctx, kind, pid, max) +} + +func connectionsPidMaxWithoutUidsWithContext(ctx context.Context, kind string, pid int32, max int) ([]ConnectionStat, error) { + return []ConnectionStat{}, common.ErrNotImplementedError +} diff --git a/internal/gopsutil/net/net_darwin.go b/internal/gopsutil/net/net_darwin.go new file mode 100644 index 0000000..a407159 --- /dev/null +++ b/internal/gopsutil/net/net_darwin.go @@ -0,0 +1,294 @@ +//go:build darwin + +package net + +import ( + "context" + "errors" + "fmt" + "os/exec" + "regexp" + "strconv" + "strings" + + "github.com/gofiber/fiber/v2/internal/gopsutil/common" +) + +var ( + errNetstatHeader = errors.New("Can't parse header of netstat output") + netstatLinkRegexp = regexp.MustCompile(`^$`) +) + +const endOfLine = "\n" + +func parseNetstatLine(line string) (stat *IOCountersStat, linkID *uint, err error) { + var ( + numericValue uint64 + columns = strings.Fields(line) + ) + + if columns[0] == "Name" { + err = errNetstatHeader + return + } + + // try to extract the numeric value from + if subMatch := netstatLinkRegexp.FindStringSubmatch(columns[2]); len(subMatch) == 2 { + numericValue, err = strconv.ParseUint(subMatch[1], 10, 64) + if err != nil { + return + } + linkIDUint := uint(numericValue) + linkID = &linkIDUint + } + + base := 1 + numberColumns := len(columns) + // sometimes Address is omitted + if numberColumns < 12 { + base = 0 + } + if numberColumns < 11 || numberColumns > 13 { + err = fmt.Errorf("Line %q do have an invalid number of columns %d", line, numberColumns) + return + } + + parsed := make([]uint64, 0, 7) + vv := []string{ + columns[base+3], // Ipkts == PacketsRecv + columns[base+4], // Ierrs == Errin + columns[base+5], // Ibytes == BytesRecv + columns[base+6], // Opkts == PacketsSent + columns[base+7], // Oerrs == Errout + columns[base+8], // Obytes == BytesSent + } + if len(columns) == 12 { + vv = append(vv, columns[base+10]) + } + + for _, target := range vv { + if target == "-" { + parsed = append(parsed, 0) + continue + } + + if numericValue, err = strconv.ParseUint(target, 10, 64); err != nil { + return + } + parsed = append(parsed, numericValue) + } + + stat = &IOCountersStat{ + Name: strings.Trim(columns[0], "*"), // remove the * that sometimes is on right on interface + PacketsRecv: parsed[0], + Errin: parsed[1], + BytesRecv: parsed[2], + PacketsSent: parsed[3], + Errout: parsed[4], + BytesSent: parsed[5], + } + if len(parsed) == 7 { + stat.Dropout = parsed[6] + } + return +} + +type netstatInterface struct { + linkID *uint + stat *IOCountersStat +} + +func parseNetstatOutput(output string) ([]netstatInterface, error) { + var ( + err error + lines = strings.Split(strings.Trim(output, endOfLine), endOfLine) + ) + + // number of interfaces is number of lines less one for the header + numberInterfaces := len(lines) - 1 + + interfaces := make([]netstatInterface, numberInterfaces) + // no output beside header + if numberInterfaces == 0 { + return interfaces, nil + } + + for index := 0; index < numberInterfaces; index++ { + nsIface := netstatInterface{} + if nsIface.stat, nsIface.linkID, err = parseNetstatLine(lines[index+1]); err != nil { + return nil, err + } + interfaces[index] = nsIface + } + return interfaces, nil +} + +// map that hold the name of a network interface and the number of usage +type mapInterfaceNameUsage map[string]uint + +func newMapInterfaceNameUsage(ifaces []netstatInterface) mapInterfaceNameUsage { + output := make(mapInterfaceNameUsage) + for index := range ifaces { + if ifaces[index].linkID != nil { + ifaceName := ifaces[index].stat.Name + usage, ok := output[ifaceName] + if ok { + output[ifaceName] = usage + 1 + } else { + output[ifaceName] = 1 + } + } + } + return output +} + +func (min mapInterfaceNameUsage) isTruncated() bool { + for _, usage := range min { + if usage > 1 { + return true + } + } + return false +} + +func (min mapInterfaceNameUsage) notTruncated() []string { + output := make([]string, 0) + for ifaceName, usage := range min { + if usage == 1 { + output = append(output, ifaceName) + } + } + return output +} + +// example of `netstat -ibdnW` output on yosemite +// Name Mtu Network Address Ipkts Ierrs Ibytes Opkts Oerrs Obytes Coll Drop +// lo0 16384 869107 0 169411755 869107 0 169411755 0 0 +// lo0 16384 ::1/128 ::1 869107 - 169411755 869107 - 169411755 - - +// lo0 16384 127 127.0.0.1 869107 - 169411755 869107 - 169411755 - - +func IOCounters(pernic bool) ([]IOCountersStat, error) { + return IOCountersWithContext(context.Background(), pernic) +} + +func IOCountersWithContext(ctx context.Context, pernic bool) ([]IOCountersStat, error) { + var ( + ret []IOCountersStat + retIndex int + ) + + netstat, err := exec.LookPath("netstat") + if err != nil { + return nil, err + } + + // try to get all interface metrics, and hope there won't be any truncated + out, err := invoke.CommandWithContext(ctx, netstat, "-ibdnW") + if err != nil { + return nil, err + } + + nsInterfaces, err := parseNetstatOutput(string(out)) + if err != nil { + return nil, err + } + + ifaceUsage := newMapInterfaceNameUsage(nsInterfaces) + notTruncated := ifaceUsage.notTruncated() + ret = make([]IOCountersStat, len(notTruncated)) + + if !ifaceUsage.isTruncated() { + // no truncated interface name, return stats of all interface with + for index := range nsInterfaces { + if nsInterfaces[index].linkID != nil { + ret[retIndex] = *nsInterfaces[index].stat + retIndex++ + } + } + } else { + // duplicated interface, list all interfaces + ifconfig, err := exec.LookPath("ifconfig") + if err != nil { + return nil, err + } + if out, err = invoke.CommandWithContext(ctx, ifconfig, "-l"); err != nil { + return nil, err + } + interfaceNames := strings.Fields(strings.TrimRight(string(out), endOfLine)) + + // for each of the interface name, run netstat if we don't have any stats yet + for _, interfaceName := range interfaceNames { + truncated := true + for index := range nsInterfaces { + if nsInterfaces[index].linkID != nil && nsInterfaces[index].stat.Name == interfaceName { + // handle the non truncated name to avoid execute netstat for them again + ret[retIndex] = *nsInterfaces[index].stat + retIndex++ + truncated = false + break + } + } + if truncated { + // run netstat with -I$ifacename + if out, err = invoke.CommandWithContext(ctx, netstat, "-ibdnWI"+interfaceName); err != nil { + return nil, err + } + parsedIfaces, err := parseNetstatOutput(string(out)) + if err != nil { + return nil, err + } + if len(parsedIfaces) == 0 { + // interface had been removed since `ifconfig -l` had been executed + continue + } + for index := range parsedIfaces { + if parsedIfaces[index].linkID != nil { + ret = append(ret, *parsedIfaces[index].stat) + break + } + } + } + } + } + + if !pernic { + return getIOCountersAll(ret) + } + return ret, nil +} + +// NetIOCountersByFile is an method which is added just a compatibility for linux. +func IOCountersByFile(pernic bool, filename string) ([]IOCountersStat, error) { + return IOCountersByFileWithContext(context.Background(), pernic, filename) +} + +func IOCountersByFileWithContext(ctx context.Context, pernic bool, filename string) ([]IOCountersStat, error) { + return IOCounters(pernic) +} + +func FilterCounters() ([]FilterStat, error) { + return FilterCountersWithContext(context.Background()) +} + +func FilterCountersWithContext(ctx context.Context) ([]FilterStat, error) { + return nil, errors.New("NetFilterCounters not implemented for darwin") +} + +func ConntrackStats(percpu bool) ([]ConntrackStat, error) { + return ConntrackStatsWithContext(context.Background(), percpu) +} + +func ConntrackStatsWithContext(ctx context.Context, percpu bool) ([]ConntrackStat, error) { + return nil, common.ErrNotImplementedError +} + +// NetProtoCounters returns network statistics for the entire system +// If protocols is empty then all protocols are returned, otherwise +// just the protocols in the list are returned. +// Not Implemented for Darwin +func ProtoCounters(protocols []string) ([]ProtoCountersStat, error) { + return ProtoCountersWithContext(context.Background(), protocols) +} + +func ProtoCountersWithContext(ctx context.Context, protocols []string) ([]ProtoCountersStat, error) { + return nil, errors.New("NetProtoCounters not implemented for darwin") +} diff --git a/internal/gopsutil/net/net_fallback.go b/internal/gopsutil/net/net_fallback.go new file mode 100644 index 0000000..e73295c --- /dev/null +++ b/internal/gopsutil/net/net_fallback.go @@ -0,0 +1,92 @@ +//go:build !aix && !darwin && !linux && !freebsd && !openbsd && !windows + +package net + +import ( + "context" + + "github.com/gofiber/fiber/v2/internal/gopsutil/common" +) + +func IOCounters(pernic bool) ([]IOCountersStat, error) { + return IOCountersWithContext(context.Background(), pernic) +} + +func IOCountersWithContext(ctx context.Context, pernic bool) ([]IOCountersStat, error) { + return []IOCountersStat{}, common.ErrNotImplementedError +} + +func FilterCounters() ([]FilterStat, error) { + return FilterCountersWithContext(context.Background()) +} + +func FilterCountersWithContext(ctx context.Context) ([]FilterStat, error) { + return []FilterStat{}, common.ErrNotImplementedError +} + +func ConntrackStats(percpu bool) ([]ConntrackStat, error) { + return ConntrackStatsWithContext(context.Background(), percpu) +} + +func ConntrackStatsWithContext(ctx context.Context, percpu bool) ([]ConntrackStat, error) { + return nil, common.ErrNotImplementedError +} + +func ProtoCounters(protocols []string) ([]ProtoCountersStat, error) { + return ProtoCountersWithContext(context.Background(), protocols) +} + +func ProtoCountersWithContext(ctx context.Context, protocols []string) ([]ProtoCountersStat, error) { + return []ProtoCountersStat{}, common.ErrNotImplementedError +} + +func Connections(kind string) ([]ConnectionStat, error) { + return ConnectionsWithContext(context.Background(), kind) +} + +func ConnectionsWithContext(ctx context.Context, kind string) ([]ConnectionStat, error) { + return []ConnectionStat{}, common.ErrNotImplementedError +} + +func ConnectionsMax(kind string, max int) ([]ConnectionStat, error) { + return ConnectionsMaxWithContext(context.Background(), kind, max) +} + +func ConnectionsMaxWithContext(ctx context.Context, kind string, max int) ([]ConnectionStat, error) { + return []ConnectionStat{}, common.ErrNotImplementedError +} + +// Return a list of network connections opened, omitting `Uids`. +// WithoutUids functions are reliant on implementation details. They may be altered to be an alias for Connections or be +// removed from the API in the future. +func ConnectionsWithoutUids(kind string) ([]ConnectionStat, error) { + return ConnectionsWithoutUidsWithContext(context.Background(), kind) +} + +func ConnectionsWithoutUidsWithContext(ctx context.Context, kind string) ([]ConnectionStat, error) { + return ConnectionsMaxWithoutUidsWithContext(ctx, kind, 0) +} + +func ConnectionsMaxWithoutUidsWithContext(ctx context.Context, kind string, max int) ([]ConnectionStat, error) { + return ConnectionsPidMaxWithoutUidsWithContext(ctx, kind, 0, max) +} + +func ConnectionsPidWithoutUids(kind string, pid int32) ([]ConnectionStat, error) { + return ConnectionsPidWithoutUidsWithContext(context.Background(), kind, pid) +} + +func ConnectionsPidWithoutUidsWithContext(ctx context.Context, kind string, pid int32) ([]ConnectionStat, error) { + return ConnectionsPidMaxWithoutUidsWithContext(ctx, kind, pid, 0) +} + +func ConnectionsPidMaxWithoutUids(kind string, pid int32, max int) ([]ConnectionStat, error) { + return ConnectionsPidMaxWithoutUidsWithContext(context.Background(), kind, pid, max) +} + +func ConnectionsPidMaxWithoutUidsWithContext(ctx context.Context, kind string, pid int32, max int) ([]ConnectionStat, error) { + return connectionsPidMaxWithoutUidsWithContext(ctx, kind, pid, max) +} + +func connectionsPidMaxWithoutUidsWithContext(ctx context.Context, kind string, pid int32, max int) ([]ConnectionStat, error) { + return []ConnectionStat{}, common.ErrNotImplementedError +} diff --git a/internal/gopsutil/net/net_freebsd.go b/internal/gopsutil/net/net_freebsd.go new file mode 100644 index 0000000..1c7430b --- /dev/null +++ b/internal/gopsutil/net/net_freebsd.go @@ -0,0 +1,133 @@ +//go:build freebsd + +package net + +import ( + "context" + "errors" + "os/exec" + "strconv" + "strings" + + "github.com/gofiber/fiber/v2/internal/gopsutil/common" +) + +func IOCounters(pernic bool) ([]IOCountersStat, error) { + return IOCountersWithContext(context.Background(), pernic) +} + +func IOCountersWithContext(ctx context.Context, pernic bool) ([]IOCountersStat, error) { + netstat, err := exec.LookPath("netstat") + if err != nil { + return nil, err + } + out, err := invoke.CommandWithContext(ctx, netstat, "-ibdnW") + if err != nil { + return nil, err + } + + lines := strings.Split(string(out), "\n") + ret := make([]IOCountersStat, 0, len(lines)-1) + exists := make([]string, 0, len(ret)) + + for _, line := range lines { + values := strings.Fields(line) + if len(values) < 1 || values[0] == "Name" { + continue + } + if common.StringsHas(exists, values[0]) { + // skip if already get + continue + } + exists = append(exists, values[0]) + + if len(values) < 12 { + continue + } + base := 1 + // sometimes Address is omitted + if len(values) < 13 { + base = 0 + } + + parsed := make([]uint64, 0, 8) + vv := []string{ + values[base+3], // PacketsRecv + values[base+4], // Errin + values[base+5], // Dropin + values[base+6], // BytesRecvn + values[base+7], // PacketSent + values[base+8], // Errout + values[base+9], // BytesSent + values[base+11], // Dropout + } + for _, target := range vv { + if target == "-" { + parsed = append(parsed, 0) + continue + } + + t, err := strconv.ParseUint(target, 10, 64) + if err != nil { + return nil, err + } + parsed = append(parsed, t) + } + + n := IOCountersStat{ + Name: values[0], + PacketsRecv: parsed[0], + Errin: parsed[1], + Dropin: parsed[2], + BytesRecv: parsed[3], + PacketsSent: parsed[4], + Errout: parsed[5], + BytesSent: parsed[6], + Dropout: parsed[7], + } + ret = append(ret, n) + } + + if pernic == false { + return getIOCountersAll(ret) + } + + return ret, nil +} + +// NetIOCountersByFile is an method which is added just a compatibility for linux. +func IOCountersByFile(pernic bool, filename string) ([]IOCountersStat, error) { + return IOCountersByFileWithContext(context.Background(), pernic, filename) +} + +func IOCountersByFileWithContext(ctx context.Context, pernic bool, filename string) ([]IOCountersStat, error) { + return IOCounters(pernic) +} + +func FilterCounters() ([]FilterStat, error) { + return FilterCountersWithContext(context.Background()) +} + +func FilterCountersWithContext(ctx context.Context) ([]FilterStat, error) { + return nil, errors.New("NetFilterCounters not implemented for freebsd") +} + +func ConntrackStats(percpu bool) ([]ConntrackStat, error) { + return ConntrackStatsWithContext(context.Background(), percpu) +} + +func ConntrackStatsWithContext(ctx context.Context, percpu bool) ([]ConntrackStat, error) { + return nil, errors.New("ConntrackStats not implemented for freebsd") +} + +// NetProtoCounters returns network statistics for the entire system +// If protocols is empty then all protocols are returned, otherwise +// just the protocols in the list are returned. +// Not Implemented for FreeBSD +func ProtoCounters(protocols []string) ([]ProtoCountersStat, error) { + return ProtoCountersWithContext(context.Background(), protocols) +} + +func ProtoCountersWithContext(ctx context.Context, protocols []string) ([]ProtoCountersStat, error) { + return nil, errors.New("NetProtoCounters not implemented for freebsd") +} diff --git a/internal/gopsutil/net/net_linux.go b/internal/gopsutil/net/net_linux.go new file mode 100644 index 0000000..7016de6 --- /dev/null +++ b/internal/gopsutil/net/net_linux.go @@ -0,0 +1,887 @@ +//go:build linux + +package net + +import ( + "bytes" + "context" + "encoding/hex" + "errors" + "fmt" + "io" + "io/fs" + "net" + "os" + "strconv" + "strings" + "syscall" + + "github.com/gofiber/fiber/v2/internal/gopsutil/common" +) + +const ( // Conntrack Column numbers + CT_ENTRIES = iota + CT_SEARCHED + CT_FOUND + CT_NEW + CT_INVALID + CT_IGNORE + CT_DELETE + CT_DELETE_LIST + CT_INSERT + CT_INSERT_FAILED + CT_DROP + CT_EARLY_DROP + CT_ICMP_ERROR + CT_EXPECT_NEW + CT_EXPECT_CREATE + CT_EXPECT_DELETE + CT_SEARCH_RESTART +) + +// NetIOCounters returnes network I/O statistics for every network +// interface installed on the system. If pernic argument is false, +// return only sum of all information (which name is 'all'). If true, +// every network interface installed on the system is returned +// separately. +func IOCounters(pernic bool) ([]IOCountersStat, error) { + return IOCountersWithContext(context.Background(), pernic) +} + +func IOCountersWithContext(ctx context.Context, pernic bool) ([]IOCountersStat, error) { + filename := common.HostProc("net/dev") + return IOCountersByFileWithContext(ctx, pernic, filename) +} + +func IOCountersByFile(pernic bool, filename string) ([]IOCountersStat, error) { + return IOCountersByFileWithContext(context.Background(), pernic, filename) +} + +func IOCountersByFileWithContext(ctx context.Context, pernic bool, filename string) ([]IOCountersStat, error) { + lines, err := common.ReadLines(filename) + if err != nil { + return nil, err + } + + parts := make([]string, 2) + + statlen := len(lines) - 1 + + ret := make([]IOCountersStat, 0, statlen) + + for _, line := range lines[2:] { + separatorPos := strings.LastIndex(line, ":") + if separatorPos == -1 { + continue + } + parts[0] = line[0:separatorPos] + parts[1] = line[separatorPos+1:] + + interfaceName := strings.TrimSpace(parts[0]) + if interfaceName == "" { + continue + } + + fields := strings.Fields(strings.TrimSpace(parts[1])) + bytesRecv, err := strconv.ParseUint(fields[0], 10, 64) + if err != nil { + return ret, err + } + packetsRecv, err := strconv.ParseUint(fields[1], 10, 64) + if err != nil { + return ret, err + } + errIn, err := strconv.ParseUint(fields[2], 10, 64) + if err != nil { + return ret, err + } + dropIn, err := strconv.ParseUint(fields[3], 10, 64) + if err != nil { + return ret, err + } + fifoIn, err := strconv.ParseUint(fields[4], 10, 64) + if err != nil { + return ret, err + } + bytesSent, err := strconv.ParseUint(fields[8], 10, 64) + if err != nil { + return ret, err + } + packetsSent, err := strconv.ParseUint(fields[9], 10, 64) + if err != nil { + return ret, err + } + errOut, err := strconv.ParseUint(fields[10], 10, 64) + if err != nil { + return ret, err + } + dropOut, err := strconv.ParseUint(fields[11], 10, 64) + if err != nil { + return ret, err + } + fifoOut, err := strconv.ParseUint(fields[12], 10, 64) + if err != nil { + return ret, err + } + + nic := IOCountersStat{ + Name: interfaceName, + BytesRecv: bytesRecv, + PacketsRecv: packetsRecv, + Errin: errIn, + Dropin: dropIn, + Fifoin: fifoIn, + BytesSent: bytesSent, + PacketsSent: packetsSent, + Errout: errOut, + Dropout: dropOut, + Fifoout: fifoOut, + } + ret = append(ret, nic) + } + + if !pernic { + return getIOCountersAll(ret) + } + + return ret, nil +} + +var netProtocols = []string{ + "ip", + "icmp", + "icmpmsg", + "tcp", + "udp", + "udplite", +} + +// NetProtoCounters returns network statistics for the entire system +// If protocols is empty then all protocols are returned, otherwise +// just the protocols in the list are returned. +// Available protocols: +// +// ip,icmp,icmpmsg,tcp,udp,udplite +func ProtoCounters(protocols []string) ([]ProtoCountersStat, error) { + return ProtoCountersWithContext(context.Background(), protocols) +} + +func ProtoCountersWithContext(ctx context.Context, protocols []string) ([]ProtoCountersStat, error) { + if len(protocols) == 0 { + protocols = netProtocols + } + + stats := make([]ProtoCountersStat, 0, len(protocols)) + protos := make(map[string]bool, len(protocols)) + for _, p := range protocols { + protos[p] = true + } + + filename := common.HostProc("net/snmp") + lines, err := common.ReadLines(filename) + if err != nil { + return nil, err + } + + linecount := len(lines) + for i := 0; i < linecount; i++ { + line := lines[i] + r := strings.IndexRune(line, ':') + if r == -1 { + return nil, errors.New(filename + " is not fomatted correctly, expected ':'.") + } + proto := strings.ToLower(line[:r]) + if !protos[proto] { + // skip protocol and data line + i++ + continue + } + + // Read header line + statNames := strings.Split(line[r+2:], " ") + + // Read data line + i++ + statValues := strings.Split(lines[i][r+2:], " ") + if len(statNames) != len(statValues) { + return nil, errors.New(filename + " is not fomatted correctly, expected same number of columns.") + } + stat := ProtoCountersStat{ + Protocol: proto, + Stats: make(map[string]int64, len(statNames)), + } + for j := range statNames { + value, err := strconv.ParseInt(statValues[j], 10, 64) + if err != nil { + return nil, err + } + stat.Stats[statNames[j]] = value + } + stats = append(stats, stat) + } + return stats, nil +} + +// NetFilterCounters returns iptables conntrack statistics +// the currently in use conntrack count and the max. +// If the file does not exist or is invalid it will return nil. +func FilterCounters() ([]FilterStat, error) { + return FilterCountersWithContext(context.Background()) +} + +func FilterCountersWithContext(ctx context.Context) ([]FilterStat, error) { + countfile := common.HostProc("sys/net/netfilter/nf_conntrack_count") + maxfile := common.HostProc("sys/net/netfilter/nf_conntrack_max") + + count, err := common.ReadInts(countfile) + if err != nil { + return nil, err + } + stats := make([]FilterStat, 0, 1) + + max, err := common.ReadInts(maxfile) + if err != nil { + return nil, err + } + + payload := FilterStat{ + ConnTrackCount: count[0], + ConnTrackMax: max[0], + } + + stats = append(stats, payload) + return stats, nil +} + +// ConntrackStats returns more detailed info about the conntrack table +func ConntrackStats(percpu bool) ([]ConntrackStat, error) { + return ConntrackStatsWithContext(context.Background(), percpu) +} + +// ConntrackStatsWithContext returns more detailed info about the conntrack table +func ConntrackStatsWithContext(ctx context.Context, percpu bool) ([]ConntrackStat, error) { + return conntrackStatsFromFile(common.HostProc("net/stat/nf_conntrack"), percpu) +} + +// conntrackStatsFromFile returns more detailed info about the conntrack table +// from `filename` +// If 'percpu' is false, the result will contain exactly one item with totals/summary +func conntrackStatsFromFile(filename string, percpu bool) ([]ConntrackStat, error) { + lines, err := common.ReadLines(filename) + if err != nil { + return nil, err + } + + statlist := NewConntrackStatList() + + for _, line := range lines { + fields := strings.Fields(line) + if len(fields) == 17 && fields[0] != "entries" { + statlist.Append(NewConntrackStat( + common.HexToUint32(fields[CT_ENTRIES]), + common.HexToUint32(fields[CT_SEARCHED]), + common.HexToUint32(fields[CT_FOUND]), + common.HexToUint32(fields[CT_NEW]), + common.HexToUint32(fields[CT_INVALID]), + common.HexToUint32(fields[CT_IGNORE]), + common.HexToUint32(fields[CT_DELETE]), + common.HexToUint32(fields[CT_DELETE_LIST]), + common.HexToUint32(fields[CT_INSERT]), + common.HexToUint32(fields[CT_INSERT_FAILED]), + common.HexToUint32(fields[CT_DROP]), + common.HexToUint32(fields[CT_EARLY_DROP]), + common.HexToUint32(fields[CT_ICMP_ERROR]), + common.HexToUint32(fields[CT_EXPECT_NEW]), + common.HexToUint32(fields[CT_EXPECT_CREATE]), + common.HexToUint32(fields[CT_EXPECT_DELETE]), + common.HexToUint32(fields[CT_SEARCH_RESTART]), + )) + } + } + + if percpu { + return statlist.Items(), nil + } + return statlist.Summary(), nil +} + +// http://students.mimuw.edu.pl/lxr/source/include/net/tcp_states.h +var TCPStatuses = map[string]string{ + "01": "ESTABLISHED", + "02": "SYN_SENT", + "03": "SYN_RECV", + "04": "FIN_WAIT1", + "05": "FIN_WAIT2", + "06": "TIME_WAIT", + "07": "CLOSE", + "08": "CLOSE_WAIT", + "09": "LAST_ACK", + "0A": "LISTEN", + "0B": "CLOSING", +} + +type netConnectionKindType struct { + family uint32 + sockType uint32 + filename string +} + +var kindTCP4 = netConnectionKindType{ + family: syscall.AF_INET, + sockType: syscall.SOCK_STREAM, + filename: "tcp", +} + +var kindTCP6 = netConnectionKindType{ + family: syscall.AF_INET6, + sockType: syscall.SOCK_STREAM, + filename: "tcp6", +} + +var kindUDP4 = netConnectionKindType{ + family: syscall.AF_INET, + sockType: syscall.SOCK_DGRAM, + filename: "udp", +} + +var kindUDP6 = netConnectionKindType{ + family: syscall.AF_INET6, + sockType: syscall.SOCK_DGRAM, + filename: "udp6", +} + +var kindUNIX = netConnectionKindType{ + family: syscall.AF_UNIX, + filename: "unix", +} + +var netConnectionKindMap = map[string][]netConnectionKindType{ + "all": {kindTCP4, kindTCP6, kindUDP4, kindUDP6, kindUNIX}, + "tcp": {kindTCP4, kindTCP6}, + "tcp4": {kindTCP4}, + "tcp6": {kindTCP6}, + "udp": {kindUDP4, kindUDP6}, + "udp4": {kindUDP4}, + "udp6": {kindUDP6}, + "unix": {kindUNIX}, + "inet": {kindTCP4, kindTCP6, kindUDP4, kindUDP6}, + "inet4": {kindTCP4, kindUDP4}, + "inet6": {kindTCP6, kindUDP6}, +} + +type inodeMap struct { + pid int32 + fd uint32 +} + +type connTmp struct { + fd uint32 + family uint32 + sockType uint32 + laddr Addr + raddr Addr + status string + pid int32 + boundPid int32 + path string +} + +// Return a list of network connections opened. +func Connections(kind string) ([]ConnectionStat, error) { + return ConnectionsWithContext(context.Background(), kind) +} + +func ConnectionsWithContext(ctx context.Context, kind string) ([]ConnectionStat, error) { + return ConnectionsPid(kind, 0) +} + +// Return a list of network connections opened returning at most `max` +// connections for each running process. +func ConnectionsMax(kind string, max int) ([]ConnectionStat, error) { + return ConnectionsMaxWithContext(context.Background(), kind, max) +} + +func ConnectionsMaxWithContext(ctx context.Context, kind string, max int) ([]ConnectionStat, error) { + return ConnectionsPidMax(kind, 0, max) +} + +// Return a list of network connections opened, omitting `Uids`. +// WithoutUids functions are reliant on implementation details. They may be altered to be an alias for Connections or be +// removed from the API in the future. +func ConnectionsWithoutUids(kind string) ([]ConnectionStat, error) { + return ConnectionsWithoutUidsWithContext(context.Background(), kind) +} + +func ConnectionsWithoutUidsWithContext(ctx context.Context, kind string) ([]ConnectionStat, error) { + return ConnectionsMaxWithoutUidsWithContext(ctx, kind, 0) +} + +func ConnectionsMaxWithoutUidsWithContext(ctx context.Context, kind string, max int) ([]ConnectionStat, error) { + return ConnectionsPidMaxWithoutUidsWithContext(ctx, kind, 0, max) +} + +// Return a list of network connections opened by a process. +func ConnectionsPid(kind string, pid int32) ([]ConnectionStat, error) { + return ConnectionsPidWithContext(context.Background(), kind, pid) +} + +func ConnectionsPidWithoutUids(kind string, pid int32) ([]ConnectionStat, error) { + return ConnectionsPidWithoutUidsWithContext(context.Background(), kind, pid) +} + +func ConnectionsPidWithContext(ctx context.Context, kind string, pid int32) ([]ConnectionStat, error) { + return ConnectionsPidMaxWithContext(ctx, kind, pid, 0) +} + +func ConnectionsPidWithoutUidsWithContext(ctx context.Context, kind string, pid int32) ([]ConnectionStat, error) { + return ConnectionsPidMaxWithoutUidsWithContext(ctx, kind, pid, 0) +} + +// Return up to `max` network connections opened by a process. +func ConnectionsPidMax(kind string, pid int32, max int) ([]ConnectionStat, error) { + return ConnectionsPidMaxWithContext(context.Background(), kind, pid, max) +} + +func ConnectionsPidMaxWithoutUids(kind string, pid int32, max int) ([]ConnectionStat, error) { + return ConnectionsPidMaxWithoutUidsWithContext(context.Background(), kind, pid, max) +} + +func ConnectionsPidMaxWithContext(ctx context.Context, kind string, pid int32, max int) ([]ConnectionStat, error) { + return connectionsPidMaxWithoutUidsWithContext(ctx, kind, pid, max, false) +} + +func ConnectionsPidMaxWithoutUidsWithContext(ctx context.Context, kind string, pid int32, max int) ([]ConnectionStat, error) { + return connectionsPidMaxWithoutUidsWithContext(ctx, kind, pid, max, true) +} + +func connectionsPidMaxWithoutUidsWithContext(ctx context.Context, kind string, pid int32, max int, skipUids bool) ([]ConnectionStat, error) { + tmap, ok := netConnectionKindMap[kind] + if !ok { + return nil, fmt.Errorf("invalid kind, %s", kind) + } + root := common.HostProc() + var err error + var inodes map[string][]inodeMap + if pid == 0 { + inodes, err = getProcInodesAll(root, max) + } else { + inodes, err = getProcInodes(root, pid, max) + if len(inodes) == 0 { + // no connection for the pid + return []ConnectionStat{}, nil + } + } + if err != nil { + return nil, fmt.Errorf("cound not get pid(s), %d: %s", pid, err) + } + return statsFromInodes(root, pid, tmap, inodes, skipUids) +} + +func statsFromInodes(root string, pid int32, tmap []netConnectionKindType, inodes map[string][]inodeMap, skipUids bool) ([]ConnectionStat, error) { + dupCheckMap := make(map[string]struct{}) + var ret []ConnectionStat + + var err error + for _, t := range tmap { + var path string + var connKey string + var ls []connTmp + if pid == 0 { + path = fmt.Sprintf("%s/net/%s", root, t.filename) + } else { + path = fmt.Sprintf("%s/%d/net/%s", root, pid, t.filename) + } + switch t.family { + case syscall.AF_INET, syscall.AF_INET6: + ls, err = processInet(path, t, inodes, pid) + case syscall.AF_UNIX: + ls, err = processUnix(path, t, inodes, pid) + } + if err != nil { + return nil, err + } + for _, c := range ls { + // Build TCP key to id the connection uniquely + // socket type, src ip, src port, dst ip, dst port and state should be enough + // to prevent duplications. + connKey = fmt.Sprintf("%d-%s:%d-%s:%d-%s", c.sockType, c.laddr.IP, c.laddr.Port, c.raddr.IP, c.raddr.Port, c.status) + if _, ok := dupCheckMap[connKey]; ok { + continue + } + + conn := ConnectionStat{ + Fd: c.fd, + Family: c.family, + Type: c.sockType, + Laddr: c.laddr, + Raddr: c.raddr, + Status: c.status, + Pid: c.pid, + } + if c.pid == 0 { + conn.Pid = c.boundPid + } else { + conn.Pid = c.pid + } + + if !skipUids { + // fetch process owner Real, effective, saved set, and filesystem UIDs + proc := process{Pid: conn.Pid} + conn.Uids, _ = proc.getUids() + } + + ret = append(ret, conn) + dupCheckMap[connKey] = struct{}{} + } + + } + + return ret, nil +} + +// getProcInodes returnes fd of the pid. +func getProcInodes(root string, pid int32, max int) (map[string][]inodeMap, error) { + ret := make(map[string][]inodeMap) + + dir := fmt.Sprintf("%s/%d/fd", root, pid) + f, err := os.Open(dir) + if err != nil { + return ret, err + } + defer f.Close() + files, err := f.Readdir(max) + if err != nil { + return ret, err + } + for _, fd := range files { + inodePath := fmt.Sprintf("%s/%d/fd/%s", root, pid, fd.Name()) + + inode, err := os.Readlink(inodePath) + if err != nil { + continue + } + if !strings.HasPrefix(inode, "socket:[") { + continue + } + // the process is using a socket + l := len(inode) + inode = inode[8 : l-1] + _, ok := ret[inode] + if !ok { + ret[inode] = make([]inodeMap, 0) + } + fd, err := strconv.Atoi(fd.Name()) + if err != nil { + continue + } + + i := inodeMap{ + pid: pid, + fd: uint32(fd), + } + ret[inode] = append(ret[inode], i) + } + return ret, nil +} + +// Pids retunres all pids. +// Note: this is a copy of process_linux.Pids() +// FIXME: Import process occures import cycle. +// move to common made other platform breaking. Need consider. +func Pids() ([]int32, error) { + return PidsWithContext(context.Background()) +} + +func PidsWithContext(ctx context.Context) ([]int32, error) { + var ret []int32 + + d, err := os.Open(common.HostProc()) + if err != nil { + return nil, err + } + defer d.Close() + + fnames, err := d.Readdirnames(-1) + if err != nil { + return nil, err + } + for _, fname := range fnames { + pid, err := strconv.ParseInt(fname, 10, 32) + if err != nil { + // if not numeric name, just skip + continue + } + ret = append(ret, int32(pid)) + } + + return ret, nil +} + +// Note: the following is based off process_linux structs and methods +// we need these to fetch the owner of a process ID +// FIXME: Import process occures import cycle. +// see remarks on pids() +type process struct { + Pid int32 `json:"pid"` + uids []int32 +} + +// Uids returns user ids of the process as a slice of the int +func (p *process) getUids() ([]int32, error) { + err := p.fillFromStatus() + if err != nil { + return []int32{}, err + } + return p.uids, nil +} + +// Get status from /proc/(pid)/status +func (p *process) fillFromStatus() error { + pid := p.Pid + statPath := common.HostProc(strconv.Itoa(int(pid)), "status") + contents, err := os.ReadFile(statPath) + if err != nil { + return err + } + lines := strings.Split(string(contents), "\n") + for _, line := range lines { + tabParts := strings.SplitN(line, "\t", 2) + if len(tabParts) < 2 { + continue + } + value := tabParts[1] + switch strings.TrimRight(tabParts[0], ":") { + case "Uid": + p.uids = make([]int32, 0, 4) + for _, i := range strings.Split(value, "\t") { + v, err := strconv.ParseInt(i, 10, 32) + if err != nil { + return err + } + p.uids = append(p.uids, int32(v)) + } + } + } + return nil +} + +func getProcInodesAll(root string, max int) (map[string][]inodeMap, error) { + pids, err := Pids() + if err != nil { + return nil, err + } + ret := make(map[string][]inodeMap) + + for _, pid := range pids { + t, err := getProcInodes(root, pid, max) + if err != nil { + // skip if permission error or no longer exists + if os.IsPermission(err) || errors.Is(err, fs.ErrNotExist) || err == io.EOF { + continue + } + return ret, err + } + if len(t) == 0 { + continue + } + // TODO: update ret. + ret = updateMap(ret, t) + } + return ret, nil +} + +// decodeAddress decode addresse represents addr in proc/net/* +// ex: +// "0500000A:0016" -> "10.0.0.5", 22 +// "0085002452100113070057A13F025401:0035" -> "2400:8500:1301:1052:a157:7:154:23f", 53 +func decodeAddress(family uint32, src string) (Addr, error) { + t := strings.Split(src, ":") + if len(t) != 2 { + return Addr{}, fmt.Errorf("does not contain port, %s", src) + } + addr := t[0] + port, err := strconv.ParseUint(t[1], 16, 16) + if err != nil { + return Addr{}, fmt.Errorf("invalid port, %s", src) + } + decoded, err := hex.DecodeString(addr) + if err != nil { + return Addr{}, fmt.Errorf("decode error, %s", err) + } + var ip net.IP + // Assumes this is little_endian + if family == syscall.AF_INET { + ip = net.IP(Reverse(decoded)) + } else { // IPv6 + ip, err = parseIPv6HexString(decoded) + if err != nil { + return Addr{}, err + } + } + return Addr{ + IP: ip.String(), + Port: uint32(port), + }, nil +} + +// Reverse reverses array of bytes. +func Reverse(s []byte) []byte { + return ReverseWithContext(context.Background(), s) +} + +func ReverseWithContext(ctx context.Context, s []byte) []byte { + for i, j := 0, len(s)-1; i < j; i, j = i+1, j-1 { + s[i], s[j] = s[j], s[i] + } + return s +} + +// parseIPv6HexString parse array of bytes to IPv6 string +func parseIPv6HexString(src []byte) (net.IP, error) { + if len(src) != 16 { + return nil, fmt.Errorf("invalid IPv6 string") + } + + buf := make([]byte, 0, 16) + for i := 0; i < len(src); i += 4 { + r := Reverse(src[i : i+4]) + buf = append(buf, r...) + } + return net.IP(buf), nil +} + +func processInet(file string, kind netConnectionKindType, inodes map[string][]inodeMap, filterPid int32) ([]connTmp, error) { + if strings.HasSuffix(file, "6") && !common.PathExists(file) { + // IPv6 not supported, return empty. + return []connTmp{}, nil + } + + // Read the contents of the /proc file with a single read sys call. + // This minimizes duplicates in the returned connections + // For more info: + // https://github.com/shirou/gopsutil/pull/361 + contents, err := os.ReadFile(file) + if err != nil { + return nil, err + } + + lines := bytes.Split(contents, []byte("\n")) + + var ret []connTmp + // skip first line + for _, line := range lines[1:] { + l := strings.Fields(string(line)) + if len(l) < 10 { + continue + } + laddr := l[1] + raddr := l[2] + status := l[3] + inode := l[9] + pid := int32(0) + fd := uint32(0) + i, exists := inodes[inode] + if exists { + pid = i[0].pid + fd = i[0].fd + } + if filterPid > 0 && filterPid != pid { + continue + } + if kind.sockType == syscall.SOCK_STREAM { + status = TCPStatuses[status] + } else { + status = "NONE" + } + la, err := decodeAddress(kind.family, laddr) + if err != nil { + continue + } + ra, err := decodeAddress(kind.family, raddr) + if err != nil { + continue + } + + ret = append(ret, connTmp{ + fd: fd, + family: kind.family, + sockType: kind.sockType, + laddr: la, + raddr: ra, + status: status, + pid: pid, + }) + } + + return ret, nil +} + +func processUnix(file string, kind netConnectionKindType, inodes map[string][]inodeMap, filterPid int32) ([]connTmp, error) { + // Read the contents of the /proc file with a single read sys call. + // This minimizes duplicates in the returned connections + // For more info: + // https://github.com/shirou/gopsutil/pull/361 + contents, err := os.ReadFile(file) + if err != nil { + return nil, err + } + + lines := bytes.Split(contents, []byte("\n")) + + var ret []connTmp + // skip first line + for _, line := range lines[1:] { + tokens := strings.Fields(string(line)) + if len(tokens) < 6 { + continue + } + st, err := strconv.Atoi(tokens[4]) + if err != nil { + return nil, err + } + + inode := tokens[6] + + var pairs []inodeMap + pairs, exists := inodes[inode] + if !exists { + pairs = []inodeMap{ + {}, + } + } + for _, pair := range pairs { + if filterPid > 0 && filterPid != pair.pid { + continue + } + var path string + if len(tokens) == 8 { + path = tokens[len(tokens)-1] + } + ret = append(ret, connTmp{ + fd: pair.fd, + family: kind.family, + sockType: uint32(st), + laddr: Addr{ + IP: path, + }, + pid: pair.pid, + status: "NONE", + path: path, + }) + } + } + + return ret, nil +} + +func updateMap(src, add map[string][]inodeMap) map[string][]inodeMap { + for key, value := range add { + a, exists := src[key] + if !exists { + src[key] = value + continue + } + src[key] = append(a, value...) + } + return src +} diff --git a/internal/gopsutil/net/net_openbsd.go b/internal/gopsutil/net/net_openbsd.go new file mode 100644 index 0000000..2afd242 --- /dev/null +++ b/internal/gopsutil/net/net_openbsd.go @@ -0,0 +1,320 @@ +//go:build openbsd + +package net + +import ( + "context" + "errors" + "fmt" + "os/exec" + "regexp" + "strconv" + "strings" + "syscall" + + "github.com/gofiber/fiber/v2/internal/gopsutil/common" +) + +var portMatch = regexp.MustCompile(`(.*)\.(\d+)$`) + +func ParseNetstat(output, mode string, + iocs map[string]IOCountersStat, +) error { + lines := strings.Split(output, "\n") + + exists := make([]string, 0, len(lines)-1) + + columns := 6 + if mode == "ind" { + columns = 10 + } + for _, line := range lines { + values := strings.Fields(line) + if len(values) < 1 || values[0] == "Name" { + continue + } + if common.StringsHas(exists, values[0]) { + // skip if already get + continue + } + + if len(values) < columns { + continue + } + base := 1 + // sometimes Address is omitted + if len(values) < columns { + base = 0 + } + + parsed := make([]uint64, 0, 8) + var vv []string + if mode == "inb" { + vv = []string{ + values[base+3], // BytesRecv + values[base+4], // BytesSent + } + } else { + vv = []string{ + values[base+3], // Ipkts + values[base+4], // Ierrs + values[base+5], // Opkts + values[base+6], // Oerrs + values[base+8], // Drops + } + } + for _, target := range vv { + if target == "-" { + parsed = append(parsed, 0) + continue + } + + t, err := strconv.ParseUint(target, 10, 64) + if err != nil { + return err + } + parsed = append(parsed, t) + } + exists = append(exists, values[0]) + + n, present := iocs[values[0]] + if !present { + n = IOCountersStat{Name: values[0]} + } + if mode == "inb" { + n.BytesRecv = parsed[0] + n.BytesSent = parsed[1] + } else { + n.PacketsRecv = parsed[0] + n.Errin = parsed[1] + n.PacketsSent = parsed[2] + n.Errout = parsed[3] + n.Dropin = parsed[4] + n.Dropout = parsed[4] + } + + iocs[n.Name] = n + } + return nil +} + +func IOCounters(pernic bool) ([]IOCountersStat, error) { + return IOCountersWithContext(context.Background(), pernic) +} + +func IOCountersWithContext(ctx context.Context, pernic bool) ([]IOCountersStat, error) { + netstat, err := exec.LookPath("netstat") + if err != nil { + return nil, err + } + out, err := invoke.CommandWithContext(ctx, netstat, "-inb") + if err != nil { + return nil, err + } + out2, err := invoke.CommandWithContext(ctx, netstat, "-ind") + if err != nil { + return nil, err + } + iocs := make(map[string]IOCountersStat) + + lines := strings.Split(string(out), "\n") + ret := make([]IOCountersStat, 0, len(lines)-1) + + err = ParseNetstat(string(out), "inb", iocs) + if err != nil { + return nil, err + } + err = ParseNetstat(string(out2), "ind", iocs) + if err != nil { + return nil, err + } + + for _, ioc := range iocs { + ret = append(ret, ioc) + } + + if pernic == false { + return getIOCountersAll(ret) + } + + return ret, nil +} + +// NetIOCountersByFile is an method which is added just a compatibility for linux. +func IOCountersByFile(pernic bool, filename string) ([]IOCountersStat, error) { + return IOCountersByFileWithContext(context.Background(), pernic, filename) +} + +func IOCountersByFileWithContext(ctx context.Context, pernic bool, filename string) ([]IOCountersStat, error) { + return IOCounters(pernic) +} + +func FilterCounters() ([]FilterStat, error) { + return FilterCountersWithContext(context.Background()) +} + +func FilterCountersWithContext(ctx context.Context) ([]FilterStat, error) { + return nil, errors.New("NetFilterCounters not implemented for openbsd") +} + +func ConntrackStats(percpu bool) ([]ConntrackStat, error) { + return ConntrackStatsWithContext(context.Background(), percpu) +} + +func ConntrackStatsWithContext(ctx context.Context, percpu bool) ([]ConntrackStat, error) { + return nil, common.ErrNotImplementedError +} + +// NetProtoCounters returns network statistics for the entire system +// If protocols is empty then all protocols are returned, otherwise +// just the protocols in the list are returned. +// Not Implemented for OpenBSD +func ProtoCounters(protocols []string) ([]ProtoCountersStat, error) { + return ProtoCountersWithContext(context.Background(), protocols) +} + +func ProtoCountersWithContext(ctx context.Context, protocols []string) ([]ProtoCountersStat, error) { + return nil, errors.New("NetProtoCounters not implemented for openbsd") +} + +func parseNetstatLine(line string) (ConnectionStat, error) { + f := strings.Fields(line) + if len(f) < 5 { + return ConnectionStat{}, fmt.Errorf("wrong line,%s", line) + } + + var netType, netFamily uint32 + switch f[0] { + case "tcp": + netType = syscall.SOCK_STREAM + netFamily = syscall.AF_INET + case "udp": + netType = syscall.SOCK_DGRAM + netFamily = syscall.AF_INET + case "tcp6": + netType = syscall.SOCK_STREAM + netFamily = syscall.AF_INET6 + case "udp6": + netType = syscall.SOCK_DGRAM + netFamily = syscall.AF_INET6 + default: + return ConnectionStat{}, fmt.Errorf("unknown type, %s", f[0]) + } + + laddr, raddr, err := parseNetstatAddr(f[3], f[4], netFamily) + if err != nil { + return ConnectionStat{}, fmt.Errorf("failed to parse netaddr, %s %s", f[3], f[4]) + } + + n := ConnectionStat{ + Fd: uint32(0), // not supported + Family: uint32(netFamily), + Type: uint32(netType), + Laddr: laddr, + Raddr: raddr, + Pid: int32(0), // not supported + } + if len(f) == 6 { + n.Status = f[5] + } + + return n, nil +} + +func parseNetstatAddr(local, remote string, family uint32) (laddr, raddr Addr, err error) { + parse := func(l string) (Addr, error) { + matches := portMatch.FindStringSubmatch(l) + if matches == nil { + return Addr{}, fmt.Errorf("wrong addr, %s", l) + } + host := matches[1] + port := matches[2] + if host == "*" { + switch family { + case syscall.AF_INET: + host = "0.0.0.0" + case syscall.AF_INET6: + host = "::" + default: + return Addr{}, fmt.Errorf("unknown family, %d", family) + } + } + lport, err := strconv.Atoi(port) + if err != nil { + return Addr{}, err + } + return Addr{IP: host, Port: uint32(lport)}, nil + } + + laddr, err = parse(local) + if remote != "*.*" { // remote addr exists + raddr, err = parse(remote) + if err != nil { + return laddr, raddr, err + } + } + + return laddr, raddr, err +} + +// Return a list of network connections opened. +func Connections(kind string) ([]ConnectionStat, error) { + return ConnectionsWithContext(context.Background(), kind) +} + +func ConnectionsWithContext(ctx context.Context, kind string) ([]ConnectionStat, error) { + var ret []ConnectionStat + + args := []string{"-na"} + switch strings.ToLower(kind) { + default: + fallthrough + case "": + fallthrough + case "all": + fallthrough + case "inet": + // nothing to add + case "inet4": + args = append(args, "-finet") + case "inet6": + args = append(args, "-finet6") + case "tcp": + args = append(args, "-ptcp") + case "tcp4": + args = append(args, "-ptcp", "-finet") + case "tcp6": + args = append(args, "-ptcp", "-finet6") + case "udp": + args = append(args, "-pudp") + case "udp4": + args = append(args, "-pudp", "-finet") + case "udp6": + args = append(args, "-pudp", "-finet6") + case "unix": + return ret, common.ErrNotImplementedError + } + + netstat, err := exec.LookPath("netstat") + if err != nil { + return nil, err + } + out, err := invoke.CommandWithContext(ctx, netstat, args...) + if err != nil { + return nil, err + } + lines := strings.Split(string(out), "\n") + for _, line := range lines { + if !(strings.HasPrefix(line, "tcp") || strings.HasPrefix(line, "udp")) { + continue + } + n, err := parseNetstatLine(line) + if err != nil { + continue + } + + ret = append(ret, n) + } + + return ret, nil +} diff --git a/internal/gopsutil/net/net_unix.go b/internal/gopsutil/net/net_unix.go new file mode 100644 index 0000000..67ef17f --- /dev/null +++ b/internal/gopsutil/net/net_unix.go @@ -0,0 +1,223 @@ +//go:build freebsd || darwin + +package net + +import ( + "context" + "fmt" + "net" + "strconv" + "strings" + "syscall" + + "github.com/gofiber/fiber/v2/internal/gopsutil/common" +) + +// Return a list of network connections opened. +func Connections(kind string) ([]ConnectionStat, error) { + return ConnectionsWithContext(context.Background(), kind) +} + +func ConnectionsWithContext(ctx context.Context, kind string) ([]ConnectionStat, error) { + return ConnectionsPid(kind, 0) +} + +// Return a list of network connections opened returning at most `max` +// connections for each running process. +func ConnectionsMax(kind string, max int) ([]ConnectionStat, error) { + return ConnectionsMaxWithContext(context.Background(), kind, max) +} + +func ConnectionsMaxWithContext(ctx context.Context, kind string, max int) ([]ConnectionStat, error) { + return []ConnectionStat{}, common.ErrNotImplementedError +} + +// Return a list of network connections opened by a process. +func ConnectionsPid(kind string, pid int32) ([]ConnectionStat, error) { + return ConnectionsPidWithContext(context.Background(), kind, pid) +} + +func ConnectionsPidWithContext(ctx context.Context, kind string, pid int32) ([]ConnectionStat, error) { + var ret []ConnectionStat + + args := []string{"-i"} + switch strings.ToLower(kind) { + default: + fallthrough + case "": + fallthrough + case "all": + fallthrough + case "inet": + args = append(args, "tcp", "-i", "udp") + case "inet4": + args = append(args, "4") + case "inet6": + args = append(args, "6") + case "tcp": + args = append(args, "tcp") + case "tcp4": + args = append(args, "4tcp") + case "tcp6": + args = append(args, "6tcp") + case "udp": + args = append(args, "udp") + case "udp4": + args = append(args, "6udp") + case "udp6": + args = append(args, "6udp") + case "unix": + args = []string{"-U"} + } + + r, err := common.CallLsofWithContext(ctx, invoke, pid, args...) + if err != nil { + return nil, err + } + for _, rr := range r { + if strings.HasPrefix(rr, "COMMAND") { + continue + } + n, err := parseNetLine(rr) + if err != nil { + continue + } + + ret = append(ret, n) + } + + return ret, nil +} + +var constMap = map[string]int{ + "unix": syscall.AF_UNIX, + "TCP": syscall.SOCK_STREAM, + "UDP": syscall.SOCK_DGRAM, + "IPv4": syscall.AF_INET, + "IPv6": syscall.AF_INET6, +} + +func parseNetLine(line string) (ConnectionStat, error) { + f := strings.Fields(line) + if len(f) < 8 { + return ConnectionStat{}, fmt.Errorf("wrong line,%s", line) + } + + if len(f) == 8 { + f = append(f, f[7]) + f[7] = "unix" + } + + pid, err := strconv.Atoi(f[1]) + if err != nil { + return ConnectionStat{}, err + } + fd, err := strconv.Atoi(strings.Trim(f[3], "u")) + if err != nil { + return ConnectionStat{}, fmt.Errorf("unknown fd, %s", f[3]) + } + netFamily, ok := constMap[f[4]] + if !ok { + return ConnectionStat{}, fmt.Errorf("unknown family, %s", f[4]) + } + netType, ok := constMap[f[7]] + if !ok { + return ConnectionStat{}, fmt.Errorf("unknown type, %s", f[7]) + } + + var laddr, raddr Addr + if f[7] == "unix" { + laddr.IP = f[8] + } else { + laddr, raddr, err = parseNetAddr(f[8]) + if err != nil { + return ConnectionStat{}, fmt.Errorf("failed to parse netaddr, %s", f[8]) + } + } + + n := ConnectionStat{ + Fd: uint32(fd), + Family: uint32(netFamily), + Type: uint32(netType), + Laddr: laddr, + Raddr: raddr, + Pid: int32(pid), + } + if len(f) == 10 { + n.Status = strings.Trim(f[9], "()") + } + + return n, nil +} + +func parseNetAddr(line string) (laddr, raddr Addr, err error) { + parse := func(l string) (Addr, error) { + host, port, err := net.SplitHostPort(l) + if err != nil { + return Addr{}, fmt.Errorf("wrong addr, %s", l) + } + lport, err := strconv.Atoi(port) + if err != nil { + return Addr{}, err + } + return Addr{IP: host, Port: uint32(lport)}, nil + } + + addrs := strings.Split(line, "->") + if len(addrs) == 0 { + return laddr, raddr, fmt.Errorf("wrong netaddr, %s", line) + } + laddr, err = parse(addrs[0]) + if len(addrs) == 2 { // remote addr exists + raddr, err = parse(addrs[1]) + if err != nil { + return laddr, raddr, err + } + } + + return laddr, raddr, err +} + +// Return up to `max` network connections opened by a process. +func ConnectionsPidMax(kind string, pid int32, max int) ([]ConnectionStat, error) { + return ConnectionsPidMaxWithContext(context.Background(), kind, pid, max) +} + +func ConnectionsPidMaxWithContext(ctx context.Context, kind string, pid int32, max int) ([]ConnectionStat, error) { + return []ConnectionStat{}, common.ErrNotImplementedError +} + +// Return a list of network connections opened, omitting `Uids`. +// WithoutUids functions are reliant on implementation details. They may be altered to be an alias for Connections or be +// removed from the API in the future. +func ConnectionsWithoutUids(kind string) ([]ConnectionStat, error) { + return ConnectionsWithoutUidsWithContext(context.Background(), kind) +} + +func ConnectionsWithoutUidsWithContext(ctx context.Context, kind string) ([]ConnectionStat, error) { + return ConnectionsMaxWithoutUidsWithContext(ctx, kind, 0) +} + +func ConnectionsMaxWithoutUidsWithContext(ctx context.Context, kind string, max int) ([]ConnectionStat, error) { + return ConnectionsPidMaxWithoutUidsWithContext(ctx, kind, 0, max) +} + +func ConnectionsPidWithoutUids(kind string, pid int32) ([]ConnectionStat, error) { + return ConnectionsPidWithoutUidsWithContext(context.Background(), kind, pid) +} + +func ConnectionsPidWithoutUidsWithContext(ctx context.Context, kind string, pid int32) ([]ConnectionStat, error) { + return ConnectionsPidMaxWithoutUidsWithContext(ctx, kind, pid, 0) +} + +func ConnectionsPidMaxWithoutUids(kind string, pid int32, max int) ([]ConnectionStat, error) { + return ConnectionsPidMaxWithoutUidsWithContext(context.Background(), kind, pid, max) +} + +func ConnectionsPidMaxWithoutUidsWithContext(ctx context.Context, kind string, pid int32, max int) ([]ConnectionStat, error) { + return connectionsPidMaxWithoutUidsWithContext(ctx, kind, pid, max) +} + +func connectionsPidMaxWithoutUidsWithContext(ctx context.Context, kind string, pid int32, max int) ([]ConnectionStat, error) { + return []ConnectionStat{}, common.ErrNotImplementedError +} diff --git a/internal/gopsutil/net/net_windows.go b/internal/gopsutil/net/net_windows.go new file mode 100644 index 0000000..865f2aa --- /dev/null +++ b/internal/gopsutil/net/net_windows.go @@ -0,0 +1,779 @@ +//go:build windows + +package net + +import ( + "context" + "fmt" + "net" + "os" + "syscall" + "unsafe" + + "golang.org/x/sys/windows" + + "github.com/gofiber/fiber/v2/internal/gopsutil/common" +) + +var ( + modiphlpapi = windows.NewLazySystemDLL("iphlpapi.dll") + procGetExtendedTCPTable = modiphlpapi.NewProc("GetExtendedTcpTable") + procGetExtendedUDPTable = modiphlpapi.NewProc("GetExtendedUdpTable") + procGetIfEntry2 = modiphlpapi.NewProc("GetIfEntry2") +) + +const ( + TCPTableBasicListener = iota + TCPTableBasicConnections + TCPTableBasicAll + TCPTableOwnerPIDListener + TCPTableOwnerPIDConnections + TCPTableOwnerPIDAll + TCPTableOwnerModuleListener + TCPTableOwnerModuleConnections + TCPTableOwnerModuleAll +) + +type netConnectionKindType struct { + family uint32 + sockType uint32 + filename string +} + +var kindTCP4 = netConnectionKindType{ + family: syscall.AF_INET, + sockType: syscall.SOCK_STREAM, + filename: "tcp", +} + +var kindTCP6 = netConnectionKindType{ + family: syscall.AF_INET6, + sockType: syscall.SOCK_STREAM, + filename: "tcp6", +} + +var kindUDP4 = netConnectionKindType{ + family: syscall.AF_INET, + sockType: syscall.SOCK_DGRAM, + filename: "udp", +} + +var kindUDP6 = netConnectionKindType{ + family: syscall.AF_INET6, + sockType: syscall.SOCK_DGRAM, + filename: "udp6", +} + +var netConnectionKindMap = map[string][]netConnectionKindType{ + "all": {kindTCP4, kindTCP6, kindUDP4, kindUDP6}, + "tcp": {kindTCP4, kindTCP6}, + "tcp4": {kindTCP4}, + "tcp6": {kindTCP6}, + "udp": {kindUDP4, kindUDP6}, + "udp4": {kindUDP4}, + "udp6": {kindUDP6}, + "inet": {kindTCP4, kindTCP6, kindUDP4, kindUDP6}, + "inet4": {kindTCP4, kindUDP4}, + "inet6": {kindTCP6, kindUDP6}, +} + +// https://github.com/microsoft/ethr/blob/aecdaf923970e5a9b4c461b4e2e3963d781ad2cc/plt_windows.go#L114-L170 +type guid struct { + Data1 uint32 + Data2 uint16 + Data3 uint16 + Data4 [8]byte +} + +const ( + maxStringSize = 256 + maxPhysAddressLength = 32 + pad0for64_4for32 = 0 +) + +type mibIfRow2 struct { + InterfaceLuid uint64 + InterfaceIndex uint32 + InterfaceGuid guid + Alias [maxStringSize + 1]uint16 + Description [maxStringSize + 1]uint16 + PhysicalAddressLength uint32 + PhysicalAddress [maxPhysAddressLength]uint8 + PermanentPhysicalAddress [maxPhysAddressLength]uint8 + Mtu uint32 + Type uint32 + TunnelType uint32 + MediaType uint32 + PhysicalMediumType uint32 + AccessType uint32 + DirectionType uint32 + InterfaceAndOperStatusFlags uint32 + OperStatus uint32 + AdminStatus uint32 + MediaConnectState uint32 + NetworkGuid guid + ConnectionType uint32 + padding1 [pad0for64_4for32]byte + TransmitLinkSpeed uint64 + ReceiveLinkSpeed uint64 + InOctets uint64 + InUcastPkts uint64 + InNUcastPkts uint64 + InDiscards uint64 + InErrors uint64 + InUnknownProtos uint64 + InUcastOctets uint64 + InMulticastOctets uint64 + InBroadcastOctets uint64 + OutOctets uint64 + OutUcastPkts uint64 + OutNUcastPkts uint64 + OutDiscards uint64 + OutErrors uint64 + OutUcastOctets uint64 + OutMulticastOctets uint64 + OutBroadcastOctets uint64 + OutQLen uint64 +} + +func IOCounters(pernic bool) ([]IOCountersStat, error) { + return IOCountersWithContext(context.Background(), pernic) +} + +func IOCountersWithContext(ctx context.Context, pernic bool) ([]IOCountersStat, error) { + ifs, err := net.Interfaces() + if err != nil { + return nil, err + } + var counters []IOCountersStat + + err = procGetIfEntry2.Find() + if err == nil { // Vista+, uint64 values (issue#693) + for _, ifi := range ifs { + c := IOCountersStat{ + Name: ifi.Name, + } + + row := mibIfRow2{InterfaceIndex: uint32(ifi.Index)} + ret, _, err := procGetIfEntry2.Call(uintptr(unsafe.Pointer(&row))) + if ret != 0 { + return nil, os.NewSyscallError("GetIfEntry2", err) + } + c.BytesSent = uint64(row.OutOctets) + c.BytesRecv = uint64(row.InOctets) + c.PacketsSent = uint64(row.OutUcastPkts) + c.PacketsRecv = uint64(row.InUcastPkts) + c.Errin = uint64(row.InErrors) + c.Errout = uint64(row.OutErrors) + c.Dropin = uint64(row.InDiscards) + c.Dropout = uint64(row.OutDiscards) + + counters = append(counters, c) + } + } else { // WinXP fallback, uint32 values + for _, ifi := range ifs { + c := IOCountersStat{ + Name: ifi.Name, + } + + row := windows.MibIfRow{Index: uint32(ifi.Index)} + err = windows.GetIfEntry(&row) + if err != nil { + return nil, os.NewSyscallError("GetIfEntry", err) + } + c.BytesSent = uint64(row.OutOctets) + c.BytesRecv = uint64(row.InOctets) + c.PacketsSent = uint64(row.OutUcastPkts) + c.PacketsRecv = uint64(row.InUcastPkts) + c.Errin = uint64(row.InErrors) + c.Errout = uint64(row.OutErrors) + c.Dropin = uint64(row.InDiscards) + c.Dropout = uint64(row.OutDiscards) + + counters = append(counters, c) + } + } + + if !pernic { + return getIOCountersAll(counters) + } + return counters, nil +} + +// NetIOCountersByFile is an method which is added just a compatibility for linux. +func IOCountersByFile(pernic bool, filename string) ([]IOCountersStat, error) { + return IOCountersByFileWithContext(context.Background(), pernic, filename) +} + +func IOCountersByFileWithContext(ctx context.Context, pernic bool, filename string) ([]IOCountersStat, error) { + return IOCounters(pernic) +} + +// Return a list of network connections +// Available kind: +// +// reference to netConnectionKindMap +func Connections(kind string) ([]ConnectionStat, error) { + return ConnectionsWithContext(context.Background(), kind) +} + +func ConnectionsWithContext(ctx context.Context, kind string) ([]ConnectionStat, error) { + return ConnectionsPidWithContext(ctx, kind, 0) +} + +// ConnectionsPid Return a list of network connections opened by a process +func ConnectionsPid(kind string, pid int32) ([]ConnectionStat, error) { + return ConnectionsPidWithContext(context.Background(), kind, pid) +} + +func ConnectionsPidWithContext(ctx context.Context, kind string, pid int32) ([]ConnectionStat, error) { + tmap, ok := netConnectionKindMap[kind] + if !ok { + return nil, fmt.Errorf("invalid kind, %s", kind) + } + return getProcInet(tmap, pid) +} + +func getProcInet(kinds []netConnectionKindType, pid int32) ([]ConnectionStat, error) { + stats := make([]ConnectionStat, 0) + + for _, kind := range kinds { + s, err := getNetStatWithKind(kind) + if err != nil { + continue + } + + if pid == 0 { + stats = append(stats, s...) + } else { + for _, ns := range s { + if ns.Pid != pid { + continue + } + stats = append(stats, ns) + } + } + } + + return stats, nil +} + +func getNetStatWithKind(kindType netConnectionKindType) ([]ConnectionStat, error) { + if kindType.filename == "" { + return nil, fmt.Errorf("kind filename must be required") + } + + switch kindType.filename { + case kindTCP4.filename: + return getTCPConnections(kindTCP4.family) + case kindTCP6.filename: + return getTCPConnections(kindTCP6.family) + case kindUDP4.filename: + return getUDPConnections(kindUDP4.family) + case kindUDP6.filename: + return getUDPConnections(kindUDP6.family) + } + + return nil, fmt.Errorf("invalid kind filename, %s", kindType.filename) +} + +// Return a list of network connections opened returning at most `max` +// connections for each running process. +func ConnectionsMax(kind string, max int) ([]ConnectionStat, error) { + return ConnectionsMaxWithContext(context.Background(), kind, max) +} + +func ConnectionsMaxWithContext(ctx context.Context, kind string, max int) ([]ConnectionStat, error) { + return []ConnectionStat{}, common.ErrNotImplementedError +} + +// Return a list of network connections opened, omitting `Uids`. +// WithoutUids functions are reliant on implementation details. They may be altered to be an alias for Connections or be +// removed from the API in the future. +func ConnectionsWithoutUids(kind string) ([]ConnectionStat, error) { + return ConnectionsWithoutUidsWithContext(context.Background(), kind) +} + +func ConnectionsWithoutUidsWithContext(ctx context.Context, kind string) ([]ConnectionStat, error) { + return ConnectionsMaxWithoutUidsWithContext(ctx, kind, 0) +} + +func ConnectionsMaxWithoutUidsWithContext(ctx context.Context, kind string, max int) ([]ConnectionStat, error) { + return ConnectionsPidMaxWithoutUidsWithContext(ctx, kind, 0, max) +} + +func ConnectionsPidWithoutUids(kind string, pid int32) ([]ConnectionStat, error) { + return ConnectionsPidWithoutUidsWithContext(context.Background(), kind, pid) +} + +func ConnectionsPidWithoutUidsWithContext(ctx context.Context, kind string, pid int32) ([]ConnectionStat, error) { + return ConnectionsPidMaxWithoutUidsWithContext(ctx, kind, pid, 0) +} + +func ConnectionsPidMaxWithoutUids(kind string, pid int32, max int) ([]ConnectionStat, error) { + return ConnectionsPidMaxWithoutUidsWithContext(context.Background(), kind, pid, max) +} + +func ConnectionsPidMaxWithoutUidsWithContext(ctx context.Context, kind string, pid int32, max int) ([]ConnectionStat, error) { + return connectionsPidMaxWithoutUidsWithContext(ctx, kind, pid, max) +} + +func connectionsPidMaxWithoutUidsWithContext(ctx context.Context, kind string, pid int32, max int) ([]ConnectionStat, error) { + return []ConnectionStat{}, common.ErrNotImplementedError +} + +func FilterCounters() ([]FilterStat, error) { + return FilterCountersWithContext(context.Background()) +} + +func FilterCountersWithContext(ctx context.Context) ([]FilterStat, error) { + return nil, common.ErrNotImplementedError +} + +func ConntrackStats(percpu bool) ([]ConntrackStat, error) { + return ConntrackStatsWithContext(context.Background(), percpu) +} + +func ConntrackStatsWithContext(ctx context.Context, percpu bool) ([]ConntrackStat, error) { + return nil, common.ErrNotImplementedError +} + +// NetProtoCounters returns network statistics for the entire system +// If protocols is empty then all protocols are returned, otherwise +// just the protocols in the list are returned. +// Not Implemented for Windows +func ProtoCounters(protocols []string) ([]ProtoCountersStat, error) { + return ProtoCountersWithContext(context.Background(), protocols) +} + +func ProtoCountersWithContext(ctx context.Context, protocols []string) ([]ProtoCountersStat, error) { + return nil, common.ErrNotImplementedError +} + +func getTableUintptr(family uint32, buf []byte) uintptr { + var ( + pmibTCPTable pmibTCPTableOwnerPidAll + pmibTCP6Table pmibTCP6TableOwnerPidAll + + p uintptr + ) + switch family { + case kindTCP4.family: + if len(buf) > 0 { + pmibTCPTable = (*mibTCPTableOwnerPid)(unsafe.Pointer(&buf[0])) + p = uintptr(unsafe.Pointer(pmibTCPTable)) + } else { + p = uintptr(unsafe.Pointer(pmibTCPTable)) + } + case kindTCP6.family: + if len(buf) > 0 { + pmibTCP6Table = (*mibTCP6TableOwnerPid)(unsafe.Pointer(&buf[0])) + p = uintptr(unsafe.Pointer(pmibTCP6Table)) + } else { + p = uintptr(unsafe.Pointer(pmibTCP6Table)) + } + } + return p +} + +func getTableInfo(filename string, table interface{}) (index, step, length int) { + switch filename { + case kindTCP4.filename: + index = int(unsafe.Sizeof(table.(pmibTCPTableOwnerPidAll).DwNumEntries)) + step = int(unsafe.Sizeof(table.(pmibTCPTableOwnerPidAll).Table)) + length = int(table.(pmibTCPTableOwnerPidAll).DwNumEntries) + case kindTCP6.filename: + index = int(unsafe.Sizeof(table.(pmibTCP6TableOwnerPidAll).DwNumEntries)) + step = int(unsafe.Sizeof(table.(pmibTCP6TableOwnerPidAll).Table)) + length = int(table.(pmibTCP6TableOwnerPidAll).DwNumEntries) + case kindUDP4.filename: + index = int(unsafe.Sizeof(table.(pmibUDPTableOwnerPid).DwNumEntries)) + step = int(unsafe.Sizeof(table.(pmibUDPTableOwnerPid).Table)) + length = int(table.(pmibUDPTableOwnerPid).DwNumEntries) + case kindUDP6.filename: + index = int(unsafe.Sizeof(table.(pmibUDP6TableOwnerPid).DwNumEntries)) + step = int(unsafe.Sizeof(table.(pmibUDP6TableOwnerPid).Table)) + length = int(table.(pmibUDP6TableOwnerPid).DwNumEntries) + } + + return +} + +func getTCPConnections(family uint32) ([]ConnectionStat, error) { + var ( + p uintptr + buf []byte + size uint32 + + pmibTCPTable pmibTCPTableOwnerPidAll + pmibTCP6Table pmibTCP6TableOwnerPidAll + ) + + if family == 0 { + return nil, fmt.Errorf("faimly must be required") + } + + for { + switch family { + case kindTCP4.family: + if len(buf) > 0 { + pmibTCPTable = (*mibTCPTableOwnerPid)(unsafe.Pointer(&buf[0])) + p = uintptr(unsafe.Pointer(pmibTCPTable)) + } else { + p = uintptr(unsafe.Pointer(pmibTCPTable)) + } + case kindTCP6.family: + if len(buf) > 0 { + pmibTCP6Table = (*mibTCP6TableOwnerPid)(unsafe.Pointer(&buf[0])) + p = uintptr(unsafe.Pointer(pmibTCP6Table)) + } else { + p = uintptr(unsafe.Pointer(pmibTCP6Table)) + } + } + + err := getExtendedTcpTable(p, + &size, + true, + family, + tcpTableOwnerPidAll, + 0) + if err == nil { + break + } + if err != windows.ERROR_INSUFFICIENT_BUFFER { + return nil, err + } + buf = make([]byte, size) + } + + var ( + index, step int + length int + ) + + stats := make([]ConnectionStat, 0) + switch family { + case kindTCP4.family: + index, step, length = getTableInfo(kindTCP4.filename, pmibTCPTable) + case kindTCP6.family: + index, step, length = getTableInfo(kindTCP6.filename, pmibTCP6Table) + } + + if length == 0 { + return nil, nil + } + + for i := 0; i < length; i++ { + switch family { + case kindTCP4.family: + mibs := (*mibTCPRowOwnerPid)(unsafe.Pointer(&buf[index])) + ns := mibs.convertToConnectionStat() + stats = append(stats, ns) + case kindTCP6.family: + mibs := (*mibTCP6RowOwnerPid)(unsafe.Pointer(&buf[index])) + ns := mibs.convertToConnectionStat() + stats = append(stats, ns) + } + + index += step + } + return stats, nil +} + +func getUDPConnections(family uint32) ([]ConnectionStat, error) { + var ( + p uintptr + buf []byte + size uint32 + + pmibUDPTable pmibUDPTableOwnerPid + pmibUDP6Table pmibUDP6TableOwnerPid + ) + + if family == 0 { + return nil, fmt.Errorf("faimly must be required") + } + + for { + switch family { + case kindUDP4.family: + if len(buf) > 0 { + pmibUDPTable = (*mibUDPTableOwnerPid)(unsafe.Pointer(&buf[0])) + p = uintptr(unsafe.Pointer(pmibUDPTable)) + } else { + p = uintptr(unsafe.Pointer(pmibUDPTable)) + } + case kindUDP6.family: + if len(buf) > 0 { + pmibUDP6Table = (*mibUDP6TableOwnerPid)(unsafe.Pointer(&buf[0])) + p = uintptr(unsafe.Pointer(pmibUDP6Table)) + } else { + p = uintptr(unsafe.Pointer(pmibUDP6Table)) + } + } + + err := getExtendedUdpTable( + p, + &size, + true, + family, + udpTableOwnerPid, + 0, + ) + if err == nil { + break + } + if err != windows.ERROR_INSUFFICIENT_BUFFER { + return nil, err + } + buf = make([]byte, size) + } + + var index, step, length int + + stats := make([]ConnectionStat, 0) + switch family { + case kindUDP4.family: + index, step, length = getTableInfo(kindUDP4.filename, pmibUDPTable) + case kindUDP6.family: + index, step, length = getTableInfo(kindUDP6.filename, pmibUDP6Table) + } + + if length == 0 { + return nil, nil + } + + for i := 0; i < length; i++ { + switch family { + case kindUDP4.family: + mibs := (*mibUDPRowOwnerPid)(unsafe.Pointer(&buf[index])) + ns := mibs.convertToConnectionStat() + stats = append(stats, ns) + case kindUDP4.family: + mibs := (*mibUDP6RowOwnerPid)(unsafe.Pointer(&buf[index])) + ns := mibs.convertToConnectionStat() + stats = append(stats, ns) + } + + index += step + } + return stats, nil +} + +// tcpStatuses https://msdn.microsoft.com/en-us/library/windows/desktop/bb485761(v=vs.85).aspx +var tcpStatuses = map[mibTCPState]string{ + 1: "CLOSED", + 2: "LISTEN", + 3: "SYN_SENT", + 4: "SYN_RECEIVED", + 5: "ESTABLISHED", + 6: "FIN_WAIT_1", + 7: "FIN_WAIT_2", + 8: "CLOSE_WAIT", + 9: "CLOSING", + 10: "LAST_ACK", + 11: "TIME_WAIT", + 12: "DELETE", +} + +func getExtendedTcpTable(pTcpTable uintptr, pdwSize *uint32, bOrder bool, ulAf uint32, tableClass tcpTableClass, reserved uint32) (errcode error) { + r1, _, _ := syscall.Syscall6(procGetExtendedTCPTable.Addr(), 6, pTcpTable, uintptr(unsafe.Pointer(pdwSize)), getUintptrFromBool(bOrder), uintptr(ulAf), uintptr(tableClass), uintptr(reserved)) + if r1 != 0 { + errcode = syscall.Errno(r1) + } + return +} + +func getExtendedUdpTable(pUdpTable uintptr, pdwSize *uint32, bOrder bool, ulAf uint32, tableClass udpTableClass, reserved uint32) (errcode error) { + r1, _, _ := syscall.Syscall6(procGetExtendedUDPTable.Addr(), 6, pUdpTable, uintptr(unsafe.Pointer(pdwSize)), getUintptrFromBool(bOrder), uintptr(ulAf), uintptr(tableClass), uintptr(reserved)) + if r1 != 0 { + errcode = syscall.Errno(r1) + } + return +} + +func getUintptrFromBool(b bool) uintptr { + if b { + return 1 + } + return 0 +} + +const anySize = 1 + +// type MIB_TCP_STATE int32 +type mibTCPState int32 + +type tcpTableClass int32 + +const ( + tcpTableBasicListener tcpTableClass = iota + tcpTableBasicConnections + tcpTableBasicAll + tcpTableOwnerPidListener + tcpTableOwnerPidConnections + tcpTableOwnerPidAll + tcpTableOwnerModuleListener + tcpTableOwnerModuleConnections + tcpTableOwnerModuleAll +) + +type udpTableClass int32 + +const ( + udpTableBasic udpTableClass = iota + udpTableOwnerPid + udpTableOwnerModule +) + +// TCP + +type mibTCPRowOwnerPid struct { + DwState uint32 + DwLocalAddr uint32 + DwLocalPort uint32 + DwRemoteAddr uint32 + DwRemotePort uint32 + DwOwningPid uint32 +} + +func (m *mibTCPRowOwnerPid) convertToConnectionStat() ConnectionStat { + ns := ConnectionStat{ + Family: kindTCP4.family, + Type: kindTCP4.sockType, + Laddr: Addr{ + IP: parseIPv4HexString(m.DwLocalAddr), + Port: uint32(decodePort(m.DwLocalPort)), + }, + Raddr: Addr{ + IP: parseIPv4HexString(m.DwRemoteAddr), + Port: uint32(decodePort(m.DwRemotePort)), + }, + Pid: int32(m.DwOwningPid), + Status: tcpStatuses[mibTCPState(m.DwState)], + } + + return ns +} + +type mibTCPTableOwnerPid struct { + DwNumEntries uint32 + Table [anySize]mibTCPRowOwnerPid +} + +type mibTCP6RowOwnerPid struct { + UcLocalAddr [16]byte + DwLocalScopeId uint32 + DwLocalPort uint32 + UcRemoteAddr [16]byte + DwRemoteScopeId uint32 + DwRemotePort uint32 + DwState uint32 + DwOwningPid uint32 +} + +func (m *mibTCP6RowOwnerPid) convertToConnectionStat() ConnectionStat { + ns := ConnectionStat{ + Family: kindTCP6.family, + Type: kindTCP6.sockType, + Laddr: Addr{ + IP: parseIPv6HexString(m.UcLocalAddr), + Port: uint32(decodePort(m.DwLocalPort)), + }, + Raddr: Addr{ + IP: parseIPv6HexString(m.UcRemoteAddr), + Port: uint32(decodePort(m.DwRemotePort)), + }, + Pid: int32(m.DwOwningPid), + Status: tcpStatuses[mibTCPState(m.DwState)], + } + + return ns +} + +type mibTCP6TableOwnerPid struct { + DwNumEntries uint32 + Table [anySize]mibTCP6RowOwnerPid +} + +type ( + pmibTCPTableOwnerPidAll *mibTCPTableOwnerPid + pmibTCP6TableOwnerPidAll *mibTCP6TableOwnerPid +) + +// UDP + +type mibUDPRowOwnerPid struct { + DwLocalAddr uint32 + DwLocalPort uint32 + DwOwningPid uint32 +} + +func (m *mibUDPRowOwnerPid) convertToConnectionStat() ConnectionStat { + ns := ConnectionStat{ + Family: kindUDP4.family, + Type: kindUDP4.sockType, + Laddr: Addr{ + IP: parseIPv4HexString(m.DwLocalAddr), + Port: uint32(decodePort(m.DwLocalPort)), + }, + Pid: int32(m.DwOwningPid), + } + + return ns +} + +type mibUDPTableOwnerPid struct { + DwNumEntries uint32 + Table [anySize]mibUDPRowOwnerPid +} + +type mibUDP6RowOwnerPid struct { + UcLocalAddr [16]byte + DwLocalScopeId uint32 + DwLocalPort uint32 + DwOwningPid uint32 +} + +func (m *mibUDP6RowOwnerPid) convertToConnectionStat() ConnectionStat { + ns := ConnectionStat{ + Family: kindUDP6.family, + Type: kindUDP6.sockType, + Laddr: Addr{ + IP: parseIPv6HexString(m.UcLocalAddr), + Port: uint32(decodePort(m.DwLocalPort)), + }, + Pid: int32(m.DwOwningPid), + } + + return ns +} + +type mibUDP6TableOwnerPid struct { + DwNumEntries uint32 + Table [anySize]mibUDP6RowOwnerPid +} + +type ( + pmibUDPTableOwnerPid *mibUDPTableOwnerPid + pmibUDP6TableOwnerPid *mibUDP6TableOwnerPid +) + +func decodePort(port uint32) uint16 { + return syscall.Ntohs(uint16(port)) +} + +func parseIPv4HexString(addr uint32) string { + return fmt.Sprintf("%d.%d.%d.%d", addr&255, addr>>8&255, addr>>16&255, addr>>24&255) +} + +func parseIPv6HexString(addr [16]byte) string { + var ret [16]byte + for i := 0; i < 16; i++ { + ret[i] = uint8(addr[i]) + } + + // convert []byte to net.IP + ip := net.IP(ret[:]) + return ip.String() +} diff --git a/internal/gopsutil/process/process.go b/internal/gopsutil/process/process.go new file mode 100644 index 0000000..80336d8 --- /dev/null +++ b/internal/gopsutil/process/process.go @@ -0,0 +1,322 @@ +package process + +import ( + "context" + "encoding/json" + "errors" + "runtime" + "sort" + "time" + + "github.com/gofiber/fiber/v2/internal/gopsutil/common" + "github.com/gofiber/fiber/v2/internal/gopsutil/cpu" + "github.com/gofiber/fiber/v2/internal/gopsutil/mem" +) + +var ( + invoke common.Invoker = common.Invoke{} + ErrorNoChildren = errors.New("process does not have children") + ErrorProcessNotRunning = errors.New("process does not exist") +) + +type Process struct { + Pid int32 `json:"pid"` + name string + status string + parent int32 + numCtxSwitches *NumCtxSwitchesStat + uids []int32 + gids []int32 + groups []int32 + numThreads int32 + memInfo *MemoryInfoStat + sigInfo *SignalInfoStat + createTime int64 + + lastCPUTimes *cpu.TimesStat + lastCPUTime time.Time + + tgid int32 +} + +type OpenFilesStat struct { + Path string `json:"path"` + Fd uint64 `json:"fd"` +} + +type MemoryInfoStat struct { + RSS uint64 `json:"rss"` // bytes + VMS uint64 `json:"vms"` // bytes + HWM uint64 `json:"hwm"` // bytes + Data uint64 `json:"data"` // bytes + Stack uint64 `json:"stack"` // bytes + Locked uint64 `json:"locked"` // bytes + Swap uint64 `json:"swap"` // bytes +} + +type SignalInfoStat struct { + PendingProcess uint64 `json:"pending_process"` + PendingThread uint64 `json:"pending_thread"` + Blocked uint64 `json:"blocked"` + Ignored uint64 `json:"ignored"` + Caught uint64 `json:"caught"` +} + +type RlimitStat struct { + Resource int32 `json:"resource"` + Soft int32 `json:"soft"` // TODO too small. needs to be uint64 + Hard int32 `json:"hard"` // TODO too small. needs to be uint64 + Used uint64 `json:"used"` +} + +type IOCountersStat struct { + ReadCount uint64 `json:"readCount"` + WriteCount uint64 `json:"writeCount"` + ReadBytes uint64 `json:"readBytes"` + WriteBytes uint64 `json:"writeBytes"` +} + +type NumCtxSwitchesStat struct { + Voluntary int64 `json:"voluntary"` + Involuntary int64 `json:"involuntary"` +} + +type PageFaultsStat struct { + MinorFaults uint64 `json:"minorFaults"` + MajorFaults uint64 `json:"majorFaults"` + ChildMinorFaults uint64 `json:"childMinorFaults"` + ChildMajorFaults uint64 `json:"childMajorFaults"` +} + +// Resource limit constants are from /usr/include/x86_64-linux-gnu/bits/resource.h +// from libc6-dev package in Ubuntu 16.10 +const ( + RLIMIT_CPU int32 = 0 + RLIMIT_FSIZE int32 = 1 + RLIMIT_DATA int32 = 2 + RLIMIT_STACK int32 = 3 + RLIMIT_CORE int32 = 4 + RLIMIT_RSS int32 = 5 + RLIMIT_NPROC int32 = 6 + RLIMIT_NOFILE int32 = 7 + RLIMIT_MEMLOCK int32 = 8 + RLIMIT_AS int32 = 9 + RLIMIT_LOCKS int32 = 10 + RLIMIT_SIGPENDING int32 = 11 + RLIMIT_MSGQUEUE int32 = 12 + RLIMIT_NICE int32 = 13 + RLIMIT_RTPRIO int32 = 14 + RLIMIT_RTTIME int32 = 15 +) + +func (p Process) String() string { + s, _ := json.Marshal(p) + return string(s) +} + +func (o OpenFilesStat) String() string { + s, _ := json.Marshal(o) + return string(s) +} + +func (m MemoryInfoStat) String() string { + s, _ := json.Marshal(m) + return string(s) +} + +func (r RlimitStat) String() string { + s, _ := json.Marshal(r) + return string(s) +} + +func (i IOCountersStat) String() string { + s, _ := json.Marshal(i) + return string(s) +} + +func (p NumCtxSwitchesStat) String() string { + s, _ := json.Marshal(p) + return string(s) +} + +// Pids returns a slice of process ID list which are running now. +func Pids() ([]int32, error) { + return PidsWithContext(context.Background()) +} + +func PidsWithContext(ctx context.Context) ([]int32, error) { + pids, err := pidsWithContext(ctx) + sort.Slice(pids, func(i, j int) bool { return pids[i] < pids[j] }) + return pids, err +} + +// NewProcess creates a new Process instance, it only stores the pid and +// checks that the process exists. Other method on Process can be used +// to get more information about the process. An error will be returned +// if the process does not exist. +func NewProcess(pid int32) (*Process, error) { + p := &Process{Pid: pid} + + exists, err := PidExists(pid) + if err != nil { + return p, err + } + if !exists { + return p, ErrorProcessNotRunning + } + _, err = p.CreateTime() + return p, err +} + +func PidExists(pid int32) (bool, error) { + return PidExistsWithContext(context.Background(), pid) +} + +// Background returns true if the process is in background, false otherwise. +func (p *Process) Background() (bool, error) { + return p.BackgroundWithContext(context.Background()) +} + +func (p *Process) BackgroundWithContext(ctx context.Context) (bool, error) { + fg, err := p.ForegroundWithContext(ctx) + if err != nil { + return false, err + } + return !fg, err +} + +// If interval is 0, return difference from last call(non-blocking). +// If interval > 0, wait interval sec and return diffrence between start and end. +func (p *Process) Percent(interval time.Duration) (float64, error) { + return p.PercentWithContext(context.Background(), interval) +} + +func (p *Process) PercentWithContext(ctx context.Context, interval time.Duration) (float64, error) { + cpuTimes, err := p.Times() + if err != nil { + return 0, err + } + now := time.Now() + + if interval > 0 { + p.lastCPUTimes = cpuTimes + p.lastCPUTime = now + if err := common.Sleep(ctx, interval); err != nil { + return 0, err + } + cpuTimes, err = p.Times() + now = time.Now() + if err != nil { + return 0, err + } + } else { + if p.lastCPUTimes == nil { + // invoked first time + p.lastCPUTimes = cpuTimes + p.lastCPUTime = now + return 0, nil + } + } + + numcpu := runtime.NumCPU() + delta := (now.Sub(p.lastCPUTime).Seconds()) * float64(numcpu) + ret := calculatePercent(p.lastCPUTimes, cpuTimes, delta, numcpu) + p.lastCPUTimes = cpuTimes + p.lastCPUTime = now + return ret, nil +} + +// IsRunning returns whether the process is still running or not. +func (p *Process) IsRunning() (bool, error) { + return p.IsRunningWithContext(context.Background()) +} + +func (p *Process) IsRunningWithContext(ctx context.Context) (bool, error) { + createTime, err := p.CreateTimeWithContext(ctx) + if err != nil { + return false, err + } + p2, err := NewProcess(p.Pid) + if err == ErrorProcessNotRunning { + return false, nil + } + createTime2, err := p2.CreateTimeWithContext(ctx) + if err != nil { + return false, err + } + return createTime == createTime2, nil +} + +// CreateTime returns created time of the process in milliseconds since the epoch, in UTC. +func (p *Process) CreateTime() (int64, error) { + return p.CreateTimeWithContext(context.Background()) +} + +func (p *Process) CreateTimeWithContext(ctx context.Context) (int64, error) { + if p.createTime != 0 { + return p.createTime, nil + } + createTime, err := p.createTimeWithContext(ctx) + p.createTime = createTime + return p.createTime, err +} + +func calculatePercent(t1, t2 *cpu.TimesStat, delta float64, numcpu int) float64 { + if delta == 0 { + return 0 + } + delta_proc := t2.Total() - t1.Total() + overall_percent := ((delta_proc / delta) * 100) * float64(numcpu) + return overall_percent +} + +// MemoryPercent returns how many percent of the total RAM this process uses +func (p *Process) MemoryPercent() (float32, error) { + return p.MemoryPercentWithContext(context.Background()) +} + +func (p *Process) MemoryPercentWithContext(ctx context.Context) (float32, error) { + machineMemory, err := mem.VirtualMemory() + if err != nil { + return 0, err + } + total := machineMemory.Total + + processMemory, err := p.MemoryInfo() + if err != nil { + return 0, err + } + used := processMemory.RSS + + return (100 * float32(used) / float32(total)), nil +} + +// CPU_Percent returns how many percent of the CPU time this process uses +func (p *Process) CPUPercent() (float64, error) { + return p.CPUPercentWithContext(context.Background()) +} + +func (p *Process) CPUPercentWithContext(ctx context.Context) (float64, error) { + crt_time, err := p.CreateTime() + if err != nil { + return 0, err + } + + cput, err := p.Times() + if err != nil { + return 0, err + } + + created := time.Unix(0, crt_time*int64(time.Millisecond)) + totalTime := time.Since(created).Seconds() + if totalTime <= 0 { + return 0, nil + } + + return 100 * cput.Total() / totalTime, nil +} + +// Groups returns all group IDs(include supplementary groups) of the process as a slice of the int +func (p *Process) Groups() ([]int32, error) { + return p.GroupsWithContext(context.Background()) +} diff --git a/internal/gopsutil/process/process_darwin.go b/internal/gopsutil/process/process_darwin.go new file mode 100644 index 0000000..6a8deeb --- /dev/null +++ b/internal/gopsutil/process/process_darwin.go @@ -0,0 +1,689 @@ +//go:build darwin + +package process + +import ( + "bytes" + "context" + "encoding/binary" + "fmt" + "os/exec" + "path/filepath" + "strconv" + "strings" + "time" + "unsafe" + + "golang.org/x/sys/unix" + + "github.com/gofiber/fiber/v2/internal/gopsutil/common" + "github.com/gofiber/fiber/v2/internal/gopsutil/cpu" + "github.com/gofiber/fiber/v2/internal/gopsutil/net" +) + +// copied from sys/sysctl.h +const ( + CTLKern = 1 // "high kernel": proc, limits + KernProc = 14 // struct: process entries + KernProcPID = 1 // by process id + KernProcProc = 8 // only return procs + KernProcAll = 0 // everything + KernProcPathname = 12 // path to executable +) + +const ( + ClockTicks = 100 // C.sysconf(C._SC_CLK_TCK) +) + +type _Ctype_struct___0 struct { + Pad uint64 +} + +// MemoryInfoExStat is different between OSes +type MemoryInfoExStat struct{} + +type MemoryMapsStat struct{} + +func pidsWithContext(ctx context.Context) ([]int32, error) { + var ret []int32 + + pids, err := callPsWithContext(ctx, "pid", 0, false) + if err != nil { + return ret, err + } + + for _, pid := range pids { + v, err := strconv.Atoi(pid[0]) + if err != nil { + return ret, err + } + ret = append(ret, int32(v)) + } + + return ret, nil +} + +func (p *Process) Ppid() (int32, error) { + return p.PpidWithContext(context.Background()) +} + +func (p *Process) PpidWithContext(ctx context.Context) (int32, error) { + r, err := callPsWithContext(ctx, "ppid", p.Pid, false) + if err != nil { + return 0, err + } + + v, err := strconv.Atoi(r[0][0]) + if err != nil { + return 0, err + } + + return int32(v), err +} + +func (p *Process) Name() (string, error) { + return p.NameWithContext(context.Background()) +} + +func (p *Process) NameWithContext(ctx context.Context) (string, error) { + k, err := p.getKProc() + if err != nil { + return "", err + } + name := common.IntToString(k.Proc.P_comm[:]) + + if len(name) >= 15 { + cmdlineSlice, err := p.CmdlineSliceWithContext(ctx) + if err != nil { + return "", err + } + if len(cmdlineSlice) > 0 { + extendedName := filepath.Base(cmdlineSlice[0]) + if strings.HasPrefix(extendedName, p.name) { + name = extendedName + } else { + name = cmdlineSlice[0] + } + } + } + + return name, nil +} + +func (p *Process) Tgid() (int32, error) { + return 0, common.ErrNotImplementedError +} + +func (p *Process) Exe() (string, error) { + return p.ExeWithContext(context.Background()) +} + +// Cmdline returns the command line arguments of the process as a string with +// each argument separated by 0x20 ascii character. +func (p *Process) Cmdline() (string, error) { + return p.CmdlineWithContext(context.Background()) +} + +func (p *Process) CmdlineWithContext(ctx context.Context) (string, error) { + r, err := callPsWithContext(ctx, "command", p.Pid, false) + if err != nil { + return "", err + } + return strings.Join(r[0], " "), err +} + +// CmdlineSlice returns the command line arguments of the process as a slice with each +// element being an argument. Because of current deficiencies in the way that the command +// line arguments are found, single arguments that have spaces in the will actually be +// reported as two separate items. In order to do something better CGO would be needed +// to use the native darwin functions. +func (p *Process) CmdlineSlice() ([]string, error) { + return p.CmdlineSliceWithContext(context.Background()) +} + +func (p *Process) CmdlineSliceWithContext(ctx context.Context) ([]string, error) { + r, err := callPsWithContext(ctx, "command", p.Pid, false) + if err != nil { + return nil, err + } + return r[0], err +} + +func (p *Process) createTimeWithContext(ctx context.Context) (int64, error) { + r, err := callPsWithContext(ctx, "etime", p.Pid, false) + if err != nil { + return 0, err + } + + elapsedSegments := strings.Split(strings.Replace(r[0][0], "-", ":", 1), ":") + var elapsedDurations []time.Duration + for i := len(elapsedSegments) - 1; i >= 0; i-- { + p, err := strconv.ParseInt(elapsedSegments[i], 10, 0) + if err != nil { + return 0, err + } + elapsedDurations = append(elapsedDurations, time.Duration(p)) + } + + elapsed := time.Duration(elapsedDurations[0]) * time.Second + if len(elapsedDurations) > 1 { + elapsed += time.Duration(elapsedDurations[1]) * time.Minute + } + if len(elapsedDurations) > 2 { + elapsed += time.Duration(elapsedDurations[2]) * time.Hour + } + if len(elapsedDurations) > 3 { + elapsed += time.Duration(elapsedDurations[3]) * time.Hour * 24 + } + + start := time.Now().Add(-elapsed) + return start.Unix() * 1000, nil +} + +func (p *Process) Cwd() (string, error) { + return p.CwdWithContext(context.Background()) +} + +func (p *Process) CwdWithContext(ctx context.Context) (string, error) { + return "", common.ErrNotImplementedError +} + +func (p *Process) Parent() (*Process, error) { + return p.ParentWithContext(context.Background()) +} + +func (p *Process) ParentWithContext(ctx context.Context) (*Process, error) { + rr, err := common.CallLsofWithContext(ctx, invoke, p.Pid, "-FR") + if err != nil { + return nil, err + } + for _, r := range rr { + if strings.HasPrefix(r, "p") { // skip if process + continue + } + l := string(r) + v, err := strconv.Atoi(strings.Replace(l, "R", "", 1)) + if err != nil { + return nil, err + } + return NewProcess(int32(v)) + } + return nil, fmt.Errorf("could not find parent line") +} + +func (p *Process) Status() (string, error) { + return p.StatusWithContext(context.Background()) +} + +func (p *Process) StatusWithContext(ctx context.Context) (string, error) { + r, err := callPsWithContext(ctx, "state", p.Pid, false) + if err != nil { + return "", err + } + + return r[0][0][0:1], err +} + +func (p *Process) Foreground() (bool, error) { + return p.ForegroundWithContext(context.Background()) +} + +func (p *Process) ForegroundWithContext(ctx context.Context) (bool, error) { + // see https://github.com/shirou/gopsutil/issues/596#issuecomment-432707831 for implementation details + pid := p.Pid + ps, err := exec.LookPath("ps") + if err != nil { + return false, err + } + out, err := invoke.CommandWithContext(ctx, ps, "-o", "stat=", "-p", strconv.Itoa(int(pid))) + if err != nil { + return false, err + } + return strings.IndexByte(string(out), '+') != -1, nil +} + +func (p *Process) Uids() ([]int32, error) { + return p.UidsWithContext(context.Background()) +} + +func (p *Process) UidsWithContext(ctx context.Context) ([]int32, error) { + k, err := p.getKProc() + if err != nil { + return nil, err + } + + // See: http://unix.superglobalmegacorp.com/Net2/newsrc/sys/ucred.h.html + userEffectiveUID := int32(k.Eproc.Ucred.UID) + + return []int32{userEffectiveUID}, nil +} + +func (p *Process) Gids() ([]int32, error) { + return p.GidsWithContext(context.Background()) +} + +func (p *Process) GidsWithContext(ctx context.Context) ([]int32, error) { + k, err := p.getKProc() + if err != nil { + return nil, err + } + + gids := make([]int32, 0, 3) + gids = append(gids, int32(k.Eproc.Pcred.P_rgid), int32(k.Eproc.Ucred.Ngroups), int32(k.Eproc.Pcred.P_svgid)) + + return gids, nil +} + +func (p *Process) GroupsWithContext(ctx context.Context) ([]int32, error) { + k, err := p.getKProc() + if err != nil { + return nil, err + } + + groups := make([]int32, k.Eproc.Ucred.Ngroups) + for i := int16(0); i < k.Eproc.Ucred.Ngroups; i++ { + groups[i] = int32(k.Eproc.Ucred.Groups[i]) + } + + return groups, nil +} + +func (p *Process) Terminal() (string, error) { + return p.TerminalWithContext(context.Background()) +} + +func (p *Process) TerminalWithContext(ctx context.Context) (string, error) { + return "", common.ErrNotImplementedError + /* + k, err := p.getKProc() + if err != nil { + return "", err + } + + ttyNr := uint64(k.Eproc.Tdev) + termmap, err := getTerminalMap() + if err != nil { + return "", err + } + + return termmap[ttyNr], nil + */ +} + +func (p *Process) Nice() (int32, error) { + return p.NiceWithContext(context.Background()) +} + +func (p *Process) NiceWithContext(ctx context.Context) (int32, error) { + k, err := p.getKProc() + if err != nil { + return 0, err + } + return int32(k.Proc.P_nice), nil +} + +func (p *Process) IOnice() (int32, error) { + return p.IOniceWithContext(context.Background()) +} + +func (p *Process) IOniceWithContext(ctx context.Context) (int32, error) { + return 0, common.ErrNotImplementedError +} + +func (p *Process) Rlimit() ([]RlimitStat, error) { + return p.RlimitWithContext(context.Background()) +} + +func (p *Process) RlimitWithContext(ctx context.Context) ([]RlimitStat, error) { + var rlimit []RlimitStat + return rlimit, common.ErrNotImplementedError +} + +func (p *Process) RlimitUsage(gatherUsed bool) ([]RlimitStat, error) { + return p.RlimitUsageWithContext(context.Background(), gatherUsed) +} + +func (p *Process) RlimitUsageWithContext(ctx context.Context, gatherUsed bool) ([]RlimitStat, error) { + var rlimit []RlimitStat + return rlimit, common.ErrNotImplementedError +} + +func (p *Process) IOCounters() (*IOCountersStat, error) { + return p.IOCountersWithContext(context.Background()) +} + +func (p *Process) IOCountersWithContext(ctx context.Context) (*IOCountersStat, error) { + return nil, common.ErrNotImplementedError +} + +func (p *Process) NumCtxSwitches() (*NumCtxSwitchesStat, error) { + return p.NumCtxSwitchesWithContext(context.Background()) +} + +func (p *Process) NumCtxSwitchesWithContext(ctx context.Context) (*NumCtxSwitchesStat, error) { + return nil, common.ErrNotImplementedError +} + +func (p *Process) NumFDs() (int32, error) { + return p.NumFDsWithContext(context.Background()) +} + +func (p *Process) NumFDsWithContext(ctx context.Context) (int32, error) { + return 0, common.ErrNotImplementedError +} + +func (p *Process) NumThreads() (int32, error) { + return p.NumThreadsWithContext(context.Background()) +} + +func (p *Process) NumThreadsWithContext(ctx context.Context) (int32, error) { + r, err := callPsWithContext(ctx, "utime,stime", p.Pid, true) + if err != nil { + return 0, err + } + return int32(len(r)), nil +} + +func (p *Process) Threads() (map[int32]*cpu.TimesStat, error) { + return p.ThreadsWithContext(context.Background()) +} + +func (p *Process) ThreadsWithContext(ctx context.Context) (map[int32]*cpu.TimesStat, error) { + ret := make(map[int32]*cpu.TimesStat) + return ret, common.ErrNotImplementedError +} + +func convertCPUTimes(s string) (ret float64, err error) { + var t int + var _tmp string + if strings.Contains(s, ":") { + _t := strings.Split(s, ":") + switch len(_t) { + case 3: + hour, err := strconv.Atoi(_t[0]) + if err != nil { + return ret, err + } + t += hour * 60 * 60 * ClockTicks + + mins, err := strconv.Atoi(_t[1]) + if err != nil { + return ret, err + } + t += mins * 60 * ClockTicks + _tmp = _t[2] + case 2: + mins, err := strconv.Atoi(_t[0]) + if err != nil { + return ret, err + } + t += mins * 60 * ClockTicks + _tmp = _t[1] + case 1, 0: + _tmp = s + default: + return ret, fmt.Errorf("wrong cpu time string") + } + } else { + _tmp = s + } + + _t := strings.Split(_tmp, ".") + if err != nil { + return ret, err + } + h, _ := strconv.Atoi(_t[0]) + t += h * ClockTicks + h, _ = strconv.Atoi(_t[1]) + t += h + return float64(t) / ClockTicks, nil +} + +func (p *Process) Times() (*cpu.TimesStat, error) { + return p.TimesWithContext(context.Background()) +} + +func (p *Process) TimesWithContext(ctx context.Context) (*cpu.TimesStat, error) { + r, err := callPsWithContext(ctx, "utime,stime", p.Pid, false) + if err != nil { + return nil, err + } + + utime, err := convertCPUTimes(r[0][0]) + if err != nil { + return nil, err + } + stime, err := convertCPUTimes(r[0][1]) + if err != nil { + return nil, err + } + + ret := &cpu.TimesStat{ + CPU: "cpu", + User: utime, + System: stime, + } + return ret, nil +} + +func (p *Process) CPUAffinity() ([]int32, error) { + return p.CPUAffinityWithContext(context.Background()) +} + +func (p *Process) CPUAffinityWithContext(ctx context.Context) ([]int32, error) { + return nil, common.ErrNotImplementedError +} + +func (p *Process) MemoryInfo() (*MemoryInfoStat, error) { + return p.MemoryInfoWithContext(context.Background()) +} + +func (p *Process) MemoryInfoWithContext(ctx context.Context) (*MemoryInfoStat, error) { + r, err := callPsWithContext(ctx, "rss,vsize,pagein", p.Pid, false) + if err != nil { + return nil, err + } + rss, err := strconv.Atoi(r[0][0]) + if err != nil { + return nil, err + } + vms, err := strconv.Atoi(r[0][1]) + if err != nil { + return nil, err + } + pagein, err := strconv.Atoi(r[0][2]) + if err != nil { + return nil, err + } + + ret := &MemoryInfoStat{ + RSS: uint64(rss) * 1024, + VMS: uint64(vms) * 1024, + Swap: uint64(pagein), + } + + return ret, nil +} + +func (p *Process) MemoryInfoEx() (*MemoryInfoExStat, error) { + return p.MemoryInfoExWithContext(context.Background()) +} + +func (p *Process) MemoryInfoExWithContext(ctx context.Context) (*MemoryInfoExStat, error) { + return nil, common.ErrNotImplementedError +} + +func (p *Process) PageFaults() (*PageFaultsStat, error) { + return p.PageFaultsWithContext(context.Background()) +} + +func (p *Process) PageFaultsWithContext(ctx context.Context) (*PageFaultsStat, error) { + return nil, common.ErrNotImplementedError +} + +func (p *Process) Children() ([]*Process, error) { + return p.ChildrenWithContext(context.Background()) +} + +func (p *Process) ChildrenWithContext(ctx context.Context) ([]*Process, error) { + pids, err := common.CallPgrepWithContext(ctx, invoke, p.Pid) + if err != nil { + return nil, err + } + ret := make([]*Process, 0, len(pids)) + for _, pid := range pids { + np, err := NewProcess(pid) + if err != nil { + return nil, err + } + ret = append(ret, np) + } + return ret, nil +} + +func (p *Process) OpenFiles() ([]OpenFilesStat, error) { + return p.OpenFilesWithContext(context.Background()) +} + +func (p *Process) OpenFilesWithContext(ctx context.Context) ([]OpenFilesStat, error) { + return nil, common.ErrNotImplementedError +} + +func (p *Process) Connections() ([]net.ConnectionStat, error) { + return p.ConnectionsWithContext(context.Background()) +} + +func (p *Process) ConnectionsWithContext(ctx context.Context) ([]net.ConnectionStat, error) { + return net.ConnectionsPid("all", p.Pid) +} + +// Connections returns a slice of net.ConnectionStat used by the process at most `max` +func (p *Process) ConnectionsMax(max int) ([]net.ConnectionStat, error) { + return p.ConnectionsMaxWithContext(context.Background(), max) +} + +func (p *Process) ConnectionsMaxWithContext(ctx context.Context, max int) ([]net.ConnectionStat, error) { + return net.ConnectionsPidMax("all", p.Pid, max) +} + +func (p *Process) NetIOCounters(pernic bool) ([]net.IOCountersStat, error) { + return p.NetIOCountersWithContext(context.Background(), pernic) +} + +func (p *Process) NetIOCountersWithContext(ctx context.Context, pernic bool) ([]net.IOCountersStat, error) { + return nil, common.ErrNotImplementedError +} + +func (p *Process) MemoryMaps(grouped bool) (*[]MemoryMapsStat, error) { + return p.MemoryMapsWithContext(context.Background(), grouped) +} + +func (p *Process) MemoryMapsWithContext(ctx context.Context, grouped bool) (*[]MemoryMapsStat, error) { + var ret []MemoryMapsStat + return &ret, common.ErrNotImplementedError +} + +func Processes() ([]*Process, error) { + return ProcessesWithContext(context.Background()) +} + +func ProcessesWithContext(ctx context.Context) ([]*Process, error) { + out := []*Process{} + + pids, err := PidsWithContext(ctx) + if err != nil { + return out, err + } + + for _, pid := range pids { + p, err := NewProcess(pid) + if err != nil { + continue + } + out = append(out, p) + } + + return out, nil +} + +func parseKinfoProc(buf []byte) (KinfoProc, error) { + var k KinfoProc + br := bytes.NewReader(buf) + + err := common.Read(br, binary.LittleEndian, &k) + if err != nil { + return k, err + } + + return k, nil +} + +// Returns a proc as defined here: +// http://unix.superglobalmegacorp.com/Net2/newsrc/sys/kinfo_proc.h.html +func (p *Process) getKProc() (*KinfoProc, error) { + return p.getKProcWithContext(context.Background()) +} + +func (p *Process) getKProcWithContext(ctx context.Context) (*KinfoProc, error) { + mib := []int32{CTLKern, KernProc, KernProcPID, p.Pid} + length := uint64(unsafe.Sizeof(KinfoProc{})) + buf := make([]byte, length) + _, _, syserr := unix.Syscall6( + 202, // unix.SYS___SYSCTL https://github.com/golang/sys/blob/76b94024e4b621e672466e8db3d7f084e7ddcad2/unix/zsysnum_darwin_amd64.go#L146 + uintptr(unsafe.Pointer(&mib[0])), + uintptr(len(mib)), + uintptr(unsafe.Pointer(&buf[0])), + uintptr(unsafe.Pointer(&length)), + 0, + 0) + if syserr != 0 { + return nil, syserr + } + k, err := parseKinfoProc(buf) + if err != nil { + return nil, err + } + + return &k, nil +} + +// call ps command. +// Return value deletes Header line(you must not input wrong arg). +// And splited by Space. Caller have responsibility to manage. +// If passed arg pid is 0, get information from all process. +func callPsWithContext(ctx context.Context, arg string, pid int32, threadOption bool) ([][]string, error) { + bin, err := exec.LookPath("ps") + if err != nil { + return [][]string{}, err + } + + var cmd []string + if pid == 0 { // will get from all processes. + cmd = []string{"-ax", "-o", arg} + } else if threadOption { + cmd = []string{"-x", "-o", arg, "-M", "-p", strconv.Itoa(int(pid))} + } else { + cmd = []string{"-x", "-o", arg, "-p", strconv.Itoa(int(pid))} + } + out, err := invoke.CommandWithContext(ctx, bin, cmd...) + if err != nil { + return [][]string{}, err + } + lines := strings.Split(string(out), "\n") + + var ret [][]string + for _, l := range lines[1:] { + var lr []string + for _, r := range strings.Split(l, " ") { + if r == "" { + continue + } + lr = append(lr, strings.TrimSpace(r)) + } + if len(lr) != 0 { + ret = append(ret, lr) + } + } + + return ret, nil +} diff --git a/internal/gopsutil/process/process_darwin_386.go b/internal/gopsutil/process/process_darwin_386.go new file mode 100644 index 0000000..f8e9223 --- /dev/null +++ b/internal/gopsutil/process/process_darwin_386.go @@ -0,0 +1,234 @@ +// Created by cgo -godefs - DO NOT EDIT +// cgo -godefs types_darwin.go + +package process + +const ( + sizeofPtr = 0x8 + sizeofShort = 0x2 + sizeofInt = 0x4 + sizeofLong = 0x8 + sizeofLongLong = 0x8 +) + +type ( + _C_short int16 + _C_int int32 + _C_long int64 + _C_long_long int64 +) + +type Timespec struct { + Sec int64 + Nsec int64 +} + +type Timeval struct { + Sec int64 + Usec int32 + Pad_cgo_0 [4]byte +} + +type Rusage struct { + Utime Timeval + Stime Timeval + Maxrss int64 + Ixrss int64 + Idrss int64 + Isrss int64 + Minflt int64 + Majflt int64 + Nswap int64 + Inblock int64 + Oublock int64 + Msgsnd int64 + Msgrcv int64 + Nsignals int64 + Nvcsw int64 + Nivcsw int64 +} + +type Rlimit struct { + Cur uint64 + Max uint64 +} + +type UGid_t uint32 + +type KinfoProc struct { + Proc ExternProc + Eproc Eproc +} + +type Eproc struct { + Paddr *uint64 + Sess *Session + Pcred Upcred + Ucred Uucred + Pad_cgo_0 [4]byte + Vm Vmspace + Ppid int32 + Pgid int32 + Jobc int16 + Pad_cgo_1 [2]byte + Tdev int32 + Tpgid int32 + Pad_cgo_2 [4]byte + Tsess *Session + Wmesg [8]int8 + Xsize int32 + Xrssize int16 + Xccount int16 + Xswrss int16 + Pad_cgo_3 [2]byte + Flag int32 + Login [12]int8 + Spare [4]int32 + Pad_cgo_4 [4]byte +} + +type Proc struct{} + +type Session struct{} + +type ucred struct { + Link _Ctype_struct___0 + Ref uint64 + Posix Posix_cred + Label *Label + Audit Au_session +} + +type Uucred struct { + Ref int32 + UID uint32 + Ngroups int16 + Pad_cgo_0 [2]byte + Groups [16]uint32 +} + +type Upcred struct { + Pc_lock [72]int8 + Pc_ucred *ucred + P_ruid uint32 + P_svuid uint32 + P_rgid uint32 + P_svgid uint32 + P_refcnt int32 + Pad_cgo_0 [4]byte +} + +type Vmspace struct { + Dummy int32 + Pad_cgo_0 [4]byte + Dummy2 *int8 + Dummy3 [5]int32 + Pad_cgo_1 [4]byte + Dummy4 [3]*int8 +} + +type Sigacts struct{} + +type ExternProc struct { + P_un [16]byte + P_vmspace uint64 + P_sigacts uint64 + Pad_cgo_0 [3]byte + P_flag int32 + P_stat int8 + P_pid int32 + P_oppid int32 + P_dupfd int32 + Pad_cgo_1 [4]byte + User_stack uint64 + Exit_thread uint64 + P_debugger int32 + Sigwait int32 + P_estcpu uint32 + P_cpticks int32 + P_pctcpu uint32 + Pad_cgo_2 [4]byte + P_wchan uint64 + P_wmesg uint64 + P_swtime uint32 + P_slptime uint32 + P_realtimer Itimerval + P_rtime Timeval + P_uticks uint64 + P_sticks uint64 + P_iticks uint64 + P_traceflag int32 + Pad_cgo_3 [4]byte + P_tracep uint64 + P_siglist int32 + Pad_cgo_4 [4]byte + P_textvp uint64 + P_holdcnt int32 + P_sigmask uint32 + P_sigignore uint32 + P_sigcatch uint32 + P_priority uint8 + P_usrpri uint8 + P_nice int8 + P_comm [17]int8 + Pad_cgo_5 [4]byte + P_pgrp uint64 + P_addr uint64 + P_xstat uint16 + P_acflag uint16 + Pad_cgo_6 [4]byte + P_ru uint64 +} + +type Itimerval struct { + Interval Timeval + Value Timeval +} + +type Vnode struct{} + +type Pgrp struct{} + +type UserStruct struct{} + +type Au_session struct { + Aia_p *AuditinfoAddr + Mask AuMask +} + +type Posix_cred struct { + UID uint32 + Ruid uint32 + Svuid uint32 + Ngroups int16 + Pad_cgo_0 [2]byte + Groups [16]uint32 + Rgid uint32 + Svgid uint32 + Gmuid uint32 + Flags int32 +} + +type Label struct{} + +type AuditinfoAddr struct { + Auid uint32 + Mask AuMask + Termid AuTidAddr + Asid int32 + Flags uint64 +} +type AuMask struct { + Success uint32 + Failure uint32 +} +type AuTidAddr struct { + Port int32 + Type uint32 + Addr [4]uint32 +} + +type UcredQueue struct { + Next *ucred + Prev **ucred +} diff --git a/internal/gopsutil/process/process_darwin_amd64.go b/internal/gopsutil/process/process_darwin_amd64.go new file mode 100644 index 0000000..f8e9223 --- /dev/null +++ b/internal/gopsutil/process/process_darwin_amd64.go @@ -0,0 +1,234 @@ +// Created by cgo -godefs - DO NOT EDIT +// cgo -godefs types_darwin.go + +package process + +const ( + sizeofPtr = 0x8 + sizeofShort = 0x2 + sizeofInt = 0x4 + sizeofLong = 0x8 + sizeofLongLong = 0x8 +) + +type ( + _C_short int16 + _C_int int32 + _C_long int64 + _C_long_long int64 +) + +type Timespec struct { + Sec int64 + Nsec int64 +} + +type Timeval struct { + Sec int64 + Usec int32 + Pad_cgo_0 [4]byte +} + +type Rusage struct { + Utime Timeval + Stime Timeval + Maxrss int64 + Ixrss int64 + Idrss int64 + Isrss int64 + Minflt int64 + Majflt int64 + Nswap int64 + Inblock int64 + Oublock int64 + Msgsnd int64 + Msgrcv int64 + Nsignals int64 + Nvcsw int64 + Nivcsw int64 +} + +type Rlimit struct { + Cur uint64 + Max uint64 +} + +type UGid_t uint32 + +type KinfoProc struct { + Proc ExternProc + Eproc Eproc +} + +type Eproc struct { + Paddr *uint64 + Sess *Session + Pcred Upcred + Ucred Uucred + Pad_cgo_0 [4]byte + Vm Vmspace + Ppid int32 + Pgid int32 + Jobc int16 + Pad_cgo_1 [2]byte + Tdev int32 + Tpgid int32 + Pad_cgo_2 [4]byte + Tsess *Session + Wmesg [8]int8 + Xsize int32 + Xrssize int16 + Xccount int16 + Xswrss int16 + Pad_cgo_3 [2]byte + Flag int32 + Login [12]int8 + Spare [4]int32 + Pad_cgo_4 [4]byte +} + +type Proc struct{} + +type Session struct{} + +type ucred struct { + Link _Ctype_struct___0 + Ref uint64 + Posix Posix_cred + Label *Label + Audit Au_session +} + +type Uucred struct { + Ref int32 + UID uint32 + Ngroups int16 + Pad_cgo_0 [2]byte + Groups [16]uint32 +} + +type Upcred struct { + Pc_lock [72]int8 + Pc_ucred *ucred + P_ruid uint32 + P_svuid uint32 + P_rgid uint32 + P_svgid uint32 + P_refcnt int32 + Pad_cgo_0 [4]byte +} + +type Vmspace struct { + Dummy int32 + Pad_cgo_0 [4]byte + Dummy2 *int8 + Dummy3 [5]int32 + Pad_cgo_1 [4]byte + Dummy4 [3]*int8 +} + +type Sigacts struct{} + +type ExternProc struct { + P_un [16]byte + P_vmspace uint64 + P_sigacts uint64 + Pad_cgo_0 [3]byte + P_flag int32 + P_stat int8 + P_pid int32 + P_oppid int32 + P_dupfd int32 + Pad_cgo_1 [4]byte + User_stack uint64 + Exit_thread uint64 + P_debugger int32 + Sigwait int32 + P_estcpu uint32 + P_cpticks int32 + P_pctcpu uint32 + Pad_cgo_2 [4]byte + P_wchan uint64 + P_wmesg uint64 + P_swtime uint32 + P_slptime uint32 + P_realtimer Itimerval + P_rtime Timeval + P_uticks uint64 + P_sticks uint64 + P_iticks uint64 + P_traceflag int32 + Pad_cgo_3 [4]byte + P_tracep uint64 + P_siglist int32 + Pad_cgo_4 [4]byte + P_textvp uint64 + P_holdcnt int32 + P_sigmask uint32 + P_sigignore uint32 + P_sigcatch uint32 + P_priority uint8 + P_usrpri uint8 + P_nice int8 + P_comm [17]int8 + Pad_cgo_5 [4]byte + P_pgrp uint64 + P_addr uint64 + P_xstat uint16 + P_acflag uint16 + Pad_cgo_6 [4]byte + P_ru uint64 +} + +type Itimerval struct { + Interval Timeval + Value Timeval +} + +type Vnode struct{} + +type Pgrp struct{} + +type UserStruct struct{} + +type Au_session struct { + Aia_p *AuditinfoAddr + Mask AuMask +} + +type Posix_cred struct { + UID uint32 + Ruid uint32 + Svuid uint32 + Ngroups int16 + Pad_cgo_0 [2]byte + Groups [16]uint32 + Rgid uint32 + Svgid uint32 + Gmuid uint32 + Flags int32 +} + +type Label struct{} + +type AuditinfoAddr struct { + Auid uint32 + Mask AuMask + Termid AuTidAddr + Asid int32 + Flags uint64 +} +type AuMask struct { + Success uint32 + Failure uint32 +} +type AuTidAddr struct { + Port int32 + Type uint32 + Addr [4]uint32 +} + +type UcredQueue struct { + Next *ucred + Prev **ucred +} diff --git a/internal/gopsutil/process/process_darwin_arm64.go b/internal/gopsutil/process/process_darwin_arm64.go new file mode 100644 index 0000000..8c13a22 --- /dev/null +++ b/internal/gopsutil/process/process_darwin_arm64.go @@ -0,0 +1,205 @@ +//go:build darwin && arm64 + +// Code generated by cmd/cgo -godefs; DO NOT EDIT. +// cgo -godefs process/types_darwin.go + +package process + +const ( + sizeofPtr = 0x8 + sizeofShort = 0x2 + sizeofInt = 0x4 + sizeofLong = 0x8 + sizeofLongLong = 0x8 +) + +type ( + _C_short int16 + _C_int int32 + _C_long int64 + _C_long_long int64 +) + +type Timespec struct { + Sec int64 + Nsec int64 +} + +type Timeval struct { + Sec int64 + Usec int32 + Pad_cgo_0 [4]byte +} + +type Rusage struct { + Utime Timeval + Stime Timeval + Maxrss int64 + Ixrss int64 + Idrss int64 + Isrss int64 + Minflt int64 + Majflt int64 + Nswap int64 + Inblock int64 + Oublock int64 + Msgsnd int64 + Msgrcv int64 + Nsignals int64 + Nvcsw int64 + Nivcsw int64 +} + +type Rlimit struct { + Cur uint64 + Max uint64 +} + +type UGid_t uint32 + +type KinfoProc struct { + Proc ExternProc + Eproc Eproc +} + +type Eproc struct { + Paddr *Proc + Sess *Session + Pcred Upcred + Ucred Uucred + Vm Vmspace + Ppid int32 + Pgid int32 + Jobc int16 + Tdev int32 + Tpgid int32 + Tsess *Session + Wmesg [8]int8 + Xsize int32 + Xrssize int16 + Xccount int16 + Xswrss int16 + Flag int32 + Login [12]int8 + Spare [4]int32 + Pad_cgo_0 [4]byte +} + +type Proc struct{} + +type Session struct{} + +type ucred struct{} + +type Uucred struct { + Ref int32 + UID uint32 + Ngroups int16 + Groups [16]uint32 +} + +type Upcred struct { + Pc_lock [72]int8 + Pc_ucred *ucred + P_ruid uint32 + P_svuid uint32 + P_rgid uint32 + P_svgid uint32 + P_refcnt int32 + Pad_cgo_0 [4]byte +} + +type Vmspace struct { + Dummy int32 + Dummy2 *int8 + Dummy3 [5]int32 + Dummy4 [3]*int8 +} + +type Sigacts struct{} + +type ExternProc struct { + P_un [16]byte + P_vmspace *Vmspace + P_sigacts *Sigacts + P_flag int32 + P_stat int8 + P_pid int32 + P_oppid int32 + P_dupfd int32 + User_stack *int8 + Exit_thread *byte + P_debugger int32 + Sigwait int32 + P_estcpu uint32 + P_cpticks int32 + P_pctcpu uint32 + P_wchan *byte + P_wmesg *int8 + P_swtime uint32 + P_slptime uint32 + P_realtimer Itimerval + P_rtime Timeval + P_uticks uint64 + P_sticks uint64 + P_iticks uint64 + P_traceflag int32 + P_tracep *Vnode + P_siglist int32 + P_textvp *Vnode + P_holdcnt int32 + P_sigmask uint32 + P_sigignore uint32 + P_sigcatch uint32 + P_priority uint8 + P_usrpri uint8 + P_nice int8 + P_comm [17]int8 + P_pgrp *Pgrp + P_addr *UserStruct + P_xstat uint16 + P_acflag uint16 + P_ru *Rusage +} + +type Itimerval struct { + Interval Timeval + Value Timeval +} + +type Vnode struct{} + +type Pgrp struct{} + +type UserStruct struct{} + +type Au_session struct { + Aia_p *AuditinfoAddr + Mask AuMask +} + +type Posix_cred struct{} + +type Label struct{} + +type AuditinfoAddr struct { + Auid uint32 + Mask AuMask + Termid AuTidAddr + Asid int32 + Flags uint64 +} +type AuMask struct { + Success uint32 + Failure uint32 +} +type AuTidAddr struct { + Port int32 + Type uint32 + Addr [4]uint32 +} + +type UcredQueue struct { + Next *ucred + Prev **ucred +} diff --git a/internal/gopsutil/process/process_darwin_cgo.go b/internal/gopsutil/process/process_darwin_cgo.go new file mode 100644 index 0000000..57714d9 --- /dev/null +++ b/internal/gopsutil/process/process_darwin_cgo.go @@ -0,0 +1,30 @@ +//go:build darwin && cgo + +package process + +// #include +// #include +import "C" + +import ( + "context" + "fmt" + "unsafe" +) + +func (p *Process) ExeWithContext(ctx context.Context) (string, error) { + var c C.char // need a var for unsafe.Sizeof need a var + const bufsize = C.PROC_PIDPATHINFO_MAXSIZE * unsafe.Sizeof(c) + buffer := (*C.char)(C.malloc(C.size_t(bufsize))) + defer C.free(unsafe.Pointer(buffer)) + + ret, err := C.proc_pidpath(C.int(p.Pid), unsafe.Pointer(buffer), C.uint32_t(bufsize)) + if err != nil { + return "", err + } + if ret <= 0 { + return "", fmt.Errorf("unknown error: proc_pidpath returned %d", ret) + } + + return C.GoString(buffer), nil +} diff --git a/internal/gopsutil/process/process_darwin_nocgo.go b/internal/gopsutil/process/process_darwin_nocgo.go new file mode 100644 index 0000000..1df5522 --- /dev/null +++ b/internal/gopsutil/process/process_darwin_nocgo.go @@ -0,0 +1,33 @@ +//go:build darwin && !cgo + +package process + +import ( + "context" + "fmt" + "os/exec" + "strconv" + "strings" +) + +func (p *Process) ExeWithContext(ctx context.Context) (string, error) { + lsof_bin, err := exec.LookPath("lsof") + if err != nil { + return "", err + } + out, err := invoke.CommandWithContext(ctx, lsof_bin, "-p", strconv.Itoa(int(p.Pid)), "-Fpfn") + if err != nil { + return "", fmt.Errorf("bad call to lsof: %s", err) + } + txtFound := 0 + lines := strings.Split(string(out), "\n") + for i := 1; i < len(lines); i++ { + if lines[i] == "ftxt" { + txtFound++ + if txtFound == 2 { + return lines[i-1][1:], nil + } + } + } + return "", fmt.Errorf("missing txt data returned by lsof") +} diff --git a/internal/gopsutil/process/process_fallback.go b/internal/gopsutil/process/process_fallback.go new file mode 100644 index 0000000..ab3ab44 --- /dev/null +++ b/internal/gopsutil/process/process_fallback.go @@ -0,0 +1,371 @@ +//go:build !darwin && !linux && !freebsd && !openbsd && !windows + +package process + +import ( + "context" + "syscall" + + "github.com/gofiber/fiber/v2/internal/gopsutil/common" + "github.com/gofiber/fiber/v2/internal/gopsutil/cpu" + "github.com/gofiber/fiber/v2/internal/gopsutil/net" +) + +type MemoryMapsStat struct { + Path string `json:"path"` + Rss uint64 `json:"rss"` + Size uint64 `json:"size"` + Pss uint64 `json:"pss"` + SharedClean uint64 `json:"sharedClean"` + SharedDirty uint64 `json:"sharedDirty"` + PrivateClean uint64 `json:"privateClean"` + PrivateDirty uint64 `json:"privateDirty"` + Referenced uint64 `json:"referenced"` + Anonymous uint64 `json:"anonymous"` + Swap uint64 `json:"swap"` +} + +type MemoryInfoExStat struct{} + +func pidsWithContext(ctx context.Context) ([]int32, error) { + return []int32{}, common.ErrNotImplementedError +} + +func Processes() ([]*Process, error) { + return nil, common.ErrNotImplementedError +} + +func ProcessesWithContext(ctx context.Context) ([]*Process, error) { + return nil, common.ErrNotImplementedError +} + +func PidExistsWithContext(ctx context.Context, pid int32) (bool, error) { + pids, err := PidsWithContext(ctx) + if err != nil { + return false, err + } + + for _, i := range pids { + if i == pid { + return true, err + } + } + + return false, err +} + +func (p *Process) Ppid() (int32, error) { + return p.PpidWithContext(context.Background()) +} + +func (p *Process) PpidWithContext(ctx context.Context) (int32, error) { + return 0, common.ErrNotImplementedError +} + +func (p *Process) Name() (string, error) { + return p.NameWithContext(context.Background()) +} + +func (p *Process) NameWithContext(ctx context.Context) (string, error) { + return "", common.ErrNotImplementedError +} + +func (p *Process) Tgid() (int32, error) { + return 0, common.ErrNotImplementedError +} + +func (p *Process) Exe() (string, error) { + return p.ExeWithContext(context.Background()) +} + +func (p *Process) ExeWithContext(ctx context.Context) (string, error) { + return "", common.ErrNotImplementedError +} + +func (p *Process) Cmdline() (string, error) { + return p.CmdlineWithContext(context.Background()) +} + +func (p *Process) CmdlineWithContext(ctx context.Context) (string, error) { + return "", common.ErrNotImplementedError +} + +func (p *Process) CmdlineSlice() ([]string, error) { + return p.CmdlineSliceWithContext(context.Background()) +} + +func (p *Process) CmdlineSliceWithContext(ctx context.Context) ([]string, error) { + return []string{}, common.ErrNotImplementedError +} + +func (p *Process) createTimeWithContext(ctx context.Context) (int64, error) { + return 0, common.ErrNotImplementedError +} + +func (p *Process) Cwd() (string, error) { + return p.CwdWithContext(context.Background()) +} + +func (p *Process) CwdWithContext(ctx context.Context) (string, error) { + return "", common.ErrNotImplementedError +} + +func (p *Process) Parent() (*Process, error) { + return p.ParentWithContext(context.Background()) +} + +func (p *Process) ParentWithContext(ctx context.Context) (*Process, error) { + return nil, common.ErrNotImplementedError +} + +func (p *Process) Status() (string, error) { + return p.StatusWithContext(context.Background()) +} + +func (p *Process) StatusWithContext(ctx context.Context) (string, error) { + return "", common.ErrNotImplementedError +} + +func (p *Process) Foreground() (bool, error) { + return p.ForegroundWithContext(context.Background()) +} + +func (p *Process) ForegroundWithContext(ctx context.Context) (bool, error) { + return false, common.ErrNotImplementedError +} + +func (p *Process) Uids() ([]int32, error) { + return p.UidsWithContext(context.Background()) +} + +func (p *Process) UidsWithContext(ctx context.Context) ([]int32, error) { + return []int32{}, common.ErrNotImplementedError +} + +func (p *Process) Gids() ([]int32, error) { + return p.GidsWithContext(context.Background()) +} + +func (p *Process) GidsWithContext(ctx context.Context) ([]int32, error) { + return []int32{}, common.ErrNotImplementedError +} + +func (p *Process) GroupsWithContext(ctx context.Context) ([]int32, error) { + return []int32{}, common.ErrNotImplementedError +} + +func (p *Process) Terminal() (string, error) { + return p.TerminalWithContext(context.Background()) +} + +func (p *Process) TerminalWithContext(ctx context.Context) (string, error) { + return "", common.ErrNotImplementedError +} + +func (p *Process) Nice() (int32, error) { + return p.NiceWithContext(context.Background()) +} + +func (p *Process) NiceWithContext(ctx context.Context) (int32, error) { + return 0, common.ErrNotImplementedError +} + +func (p *Process) IOnice() (int32, error) { + return p.IOniceWithContext(context.Background()) +} + +func (p *Process) IOniceWithContext(ctx context.Context) (int32, error) { + return 0, common.ErrNotImplementedError +} + +func (p *Process) Rlimit() ([]RlimitStat, error) { + return p.RlimitWithContext(context.Background()) +} + +func (p *Process) RlimitWithContext(ctx context.Context) ([]RlimitStat, error) { + return nil, common.ErrNotImplementedError +} + +func (p *Process) RlimitUsage(gatherUsed bool) ([]RlimitStat, error) { + return p.RlimitUsageWithContext(context.Background(), gatherUsed) +} + +func (p *Process) RlimitUsageWithContext(ctx context.Context, gatherUsed bool) ([]RlimitStat, error) { + return nil, common.ErrNotImplementedError +} + +func (p *Process) IOCounters() (*IOCountersStat, error) { + return p.IOCountersWithContext(context.Background()) +} + +func (p *Process) IOCountersWithContext(ctx context.Context) (*IOCountersStat, error) { + return nil, common.ErrNotImplementedError +} + +func (p *Process) NumCtxSwitches() (*NumCtxSwitchesStat, error) { + return p.NumCtxSwitchesWithContext(context.Background()) +} + +func (p *Process) NumCtxSwitchesWithContext(ctx context.Context) (*NumCtxSwitchesStat, error) { + return nil, common.ErrNotImplementedError +} + +func (p *Process) NumFDs() (int32, error) { + return p.NumFDsWithContext(context.Background()) +} + +func (p *Process) NumFDsWithContext(ctx context.Context) (int32, error) { + return 0, common.ErrNotImplementedError +} + +func (p *Process) NumThreads() (int32, error) { + return p.NumThreadsWithContext(context.Background()) +} + +func (p *Process) NumThreadsWithContext(ctx context.Context) (int32, error) { + return 0, common.ErrNotImplementedError +} + +func (p *Process) Threads() (map[int32]*cpu.TimesStat, error) { + return p.ThreadsWithContext(context.Background()) +} + +func (p *Process) ThreadsWithContext(ctx context.Context) (map[int32]*cpu.TimesStat, error) { + return nil, common.ErrNotImplementedError +} + +func (p *Process) Times() (*cpu.TimesStat, error) { + return p.TimesWithContext(context.Background()) +} + +func (p *Process) TimesWithContext(ctx context.Context) (*cpu.TimesStat, error) { + return nil, common.ErrNotImplementedError +} + +func (p *Process) CPUAffinity() ([]int32, error) { + return p.CPUAffinityWithContext(context.Background()) +} + +func (p *Process) CPUAffinityWithContext(ctx context.Context) ([]int32, error) { + return nil, common.ErrNotImplementedError +} + +func (p *Process) MemoryInfo() (*MemoryInfoStat, error) { + return p.MemoryInfoWithContext(context.Background()) +} + +func (p *Process) MemoryInfoWithContext(ctx context.Context) (*MemoryInfoStat, error) { + return nil, common.ErrNotImplementedError +} + +func (p *Process) MemoryInfoEx() (*MemoryInfoExStat, error) { + return p.MemoryInfoExWithContext(context.Background()) +} + +func (p *Process) MemoryInfoExWithContext(ctx context.Context) (*MemoryInfoExStat, error) { + return nil, common.ErrNotImplementedError +} + +func (p *Process) PageFaults() (*PageFaultsStat, error) { + return p.PageFaultsWithContext(context.Background()) +} + +func (p *Process) PageFaultsWithContext(ctx context.Context) (*PageFaultsStat, error) { + return nil, common.ErrNotImplementedError +} + +func (p *Process) Children() ([]*Process, error) { + return p.ChildrenWithContext(context.Background()) +} + +func (p *Process) ChildrenWithContext(ctx context.Context) ([]*Process, error) { + return nil, common.ErrNotImplementedError +} + +func (p *Process) OpenFiles() ([]OpenFilesStat, error) { + return p.OpenFilesWithContext(context.Background()) +} + +func (p *Process) OpenFilesWithContext(ctx context.Context) ([]OpenFilesStat, error) { + return []OpenFilesStat{}, common.ErrNotImplementedError +} + +func (p *Process) Connections() ([]net.ConnectionStat, error) { + return p.ConnectionsWithContext(context.Background()) +} + +func (p *Process) ConnectionsWithContext(ctx context.Context) ([]net.ConnectionStat, error) { + return []net.ConnectionStat{}, common.ErrNotImplementedError +} + +func (p *Process) ConnectionsMax(max int) ([]net.ConnectionStat, error) { + return p.ConnectionsMaxWithContext(context.Background(), max) +} + +func (p *Process) ConnectionsMaxWithContext(ctx context.Context, max int) ([]net.ConnectionStat, error) { + return []net.ConnectionStat{}, common.ErrNotImplementedError +} + +func (p *Process) NetIOCounters(pernic bool) ([]net.IOCountersStat, error) { + return p.NetIOCountersWithContext(context.Background(), pernic) +} + +func (p *Process) NetIOCountersWithContext(ctx context.Context, pernic bool) ([]net.IOCountersStat, error) { + return []net.IOCountersStat{}, common.ErrNotImplementedError +} + +func (p *Process) MemoryMaps(grouped bool) (*[]MemoryMapsStat, error) { + return p.MemoryMapsWithContext(context.Background(), grouped) +} + +func (p *Process) MemoryMapsWithContext(ctx context.Context, grouped bool) (*[]MemoryMapsStat, error) { + return nil, common.ErrNotImplementedError +} + +func (p *Process) SendSignal(sig syscall.Signal) error { + return p.SendSignalWithContext(context.Background(), sig) +} + +func (p *Process) SendSignalWithContext(ctx context.Context, sig syscall.Signal) error { + return common.ErrNotImplementedError +} + +func (p *Process) Suspend() error { + return p.SuspendWithContext(context.Background()) +} + +func (p *Process) SuspendWithContext(ctx context.Context) error { + return common.ErrNotImplementedError +} + +func (p *Process) Resume() error { + return p.ResumeWithContext(context.Background()) +} + +func (p *Process) ResumeWithContext(ctx context.Context) error { + return common.ErrNotImplementedError +} + +func (p *Process) Terminate() error { + return p.TerminateWithContext(context.Background()) +} + +func (p *Process) TerminateWithContext(ctx context.Context) error { + return common.ErrNotImplementedError +} + +func (p *Process) Kill() error { + return p.KillWithContext(context.Background()) +} + +func (p *Process) KillWithContext(ctx context.Context) error { + return common.ErrNotImplementedError +} + +func (p *Process) Username() (string, error) { + return p.UsernameWithContext(context.Background()) +} + +func (p *Process) UsernameWithContext(ctx context.Context) (string, error) { + return "", common.ErrNotImplementedError +} diff --git a/internal/gopsutil/process/process_freebsd.go b/internal/gopsutil/process/process_freebsd.go new file mode 100644 index 0000000..e86c52e --- /dev/null +++ b/internal/gopsutil/process/process_freebsd.go @@ -0,0 +1,549 @@ +//go:build freebsd + +package process + +import ( + "bytes" + "context" + "encoding/binary" + "os/exec" + "path/filepath" + "strconv" + "strings" + + "golang.org/x/sys/unix" + + "github.com/gofiber/fiber/v2/internal/gopsutil/common" + cpu "github.com/gofiber/fiber/v2/internal/gopsutil/cpu" + net "github.com/gofiber/fiber/v2/internal/gopsutil/net" +) + +// MemoryInfoExStat is different between OSes +type MemoryInfoExStat struct{} + +type MemoryMapsStat struct{} + +func pidsWithContext(ctx context.Context) ([]int32, error) { + var ret []int32 + procs, err := Processes() + if err != nil { + return ret, nil + } + + for _, p := range procs { + ret = append(ret, p.Pid) + } + + return ret, nil +} + +func (p *Process) Ppid() (int32, error) { + return p.PpidWithContext(context.Background()) +} + +func (p *Process) PpidWithContext(ctx context.Context) (int32, error) { + k, err := p.getKProc() + if err != nil { + return 0, err + } + + return k.Ppid, nil +} + +func (p *Process) Name() (string, error) { + return p.NameWithContext(context.Background()) +} + +func (p *Process) NameWithContext(ctx context.Context) (string, error) { + k, err := p.getKProc() + if err != nil { + return "", err + } + name := common.IntToString(k.Comm[:]) + + if len(name) >= 15 { + cmdlineSlice, err := p.CmdlineSliceWithContext(ctx) + if err != nil { + return "", err + } + if len(cmdlineSlice) > 0 { + extendedName := filepath.Base(cmdlineSlice[0]) + if strings.HasPrefix(extendedName, p.name) { + name = extendedName + } else { + name = cmdlineSlice[0] + } + } + } + + return name, nil +} + +func (p *Process) Tgid() (int32, error) { + return 0, common.ErrNotImplementedError +} + +func (p *Process) Exe() (string, error) { + return p.ExeWithContext(context.Background()) +} + +func (p *Process) ExeWithContext(ctx context.Context) (string, error) { + return "", common.ErrNotImplementedError +} + +func (p *Process) Cmdline() (string, error) { + return p.CmdlineWithContext(context.Background()) +} + +func (p *Process) CmdlineWithContext(ctx context.Context) (string, error) { + mib := []int32{CTLKern, KernProc, KernProcArgs, p.Pid} + buf, _, err := common.CallSyscall(mib) + if err != nil { + return "", err + } + ret := strings.FieldsFunc(string(buf), func(r rune) bool { + if r == '\u0000' { + return true + } + return false + }) + + return strings.Join(ret, " "), nil +} + +func (p *Process) CmdlineSlice() ([]string, error) { + return p.CmdlineSliceWithContext(context.Background()) +} + +func (p *Process) CmdlineSliceWithContext(ctx context.Context) ([]string, error) { + mib := []int32{CTLKern, KernProc, KernProcArgs, p.Pid} + buf, _, err := common.CallSyscall(mib) + if err != nil { + return nil, err + } + if len(buf) == 0 { + return nil, nil + } + if buf[len(buf)-1] == 0 { + buf = buf[:len(buf)-1] + } + parts := bytes.Split(buf, []byte{0}) + var strParts []string + for _, p := range parts { + strParts = append(strParts, string(p)) + } + + return strParts, nil +} + +func (p *Process) createTimeWithContext(ctx context.Context) (int64, error) { + return 0, common.ErrNotImplementedError +} + +func (p *Process) Cwd() (string, error) { + return p.CwdWithContext(context.Background()) +} + +func (p *Process) CwdWithContext(ctx context.Context) (string, error) { + return "", common.ErrNotImplementedError +} + +func (p *Process) Parent() (*Process, error) { + return p.ParentWithContext(context.Background()) +} + +func (p *Process) ParentWithContext(ctx context.Context) (*Process, error) { + return p, common.ErrNotImplementedError +} + +func (p *Process) Status() (string, error) { + return p.StatusWithContext(context.Background()) +} + +func (p *Process) StatusWithContext(ctx context.Context) (string, error) { + k, err := p.getKProc() + if err != nil { + return "", err + } + var s string + switch k.Stat { + case SIDL: + s = "I" + case SRUN: + s = "R" + case SSLEEP: + s = "S" + case SSTOP: + s = "T" + case SZOMB: + s = "Z" + case SWAIT: + s = "W" + case SLOCK: + s = "L" + } + + return s, nil +} + +func (p *Process) Foreground() (bool, error) { + return p.ForegroundWithContext(context.Background()) +} + +func (p *Process) ForegroundWithContext(ctx context.Context) (bool, error) { + // see https://github.com/shirou/gopsutil/issues/596#issuecomment-432707831 for implementation details + pid := p.Pid + ps, err := exec.LookPath("ps") + if err != nil { + return false, err + } + out, err := invoke.CommandWithContext(ctx, ps, "-o", "stat=", "-p", strconv.Itoa(int(pid))) + if err != nil { + return false, err + } + return strings.IndexByte(string(out), '+') != -1, nil +} + +func (p *Process) Uids() ([]int32, error) { + return p.UidsWithContext(context.Background()) +} + +func (p *Process) UidsWithContext(ctx context.Context) ([]int32, error) { + k, err := p.getKProc() + if err != nil { + return nil, err + } + + uids := make([]int32, 0, 3) + + uids = append(uids, int32(k.Ruid), int32(k.Uid), int32(k.Svuid)) + + return uids, nil +} + +func (p *Process) Gids() ([]int32, error) { + return p.GidsWithContext(context.Background()) +} + +func (p *Process) GidsWithContext(ctx context.Context) ([]int32, error) { + k, err := p.getKProc() + if err != nil { + return nil, err + } + + gids := make([]int32, 0, 3) + gids = append(gids, int32(k.Rgid), int32(k.Ngroups), int32(k.Svgid)) + + return gids, nil +} + +func (p *Process) GroupsWithContext(ctx context.Context) ([]int32, error) { + k, err := p.getKProc() + if err != nil { + return nil, err + } + + groups := make([]int32, k.Ngroups) + for i := int16(0); i < k.Ngroups; i++ { + groups[i] = int32(k.Groups[i]) + } + + return groups, nil +} + +func (p *Process) Terminal() (string, error) { + return p.TerminalWithContext(context.Background()) +} + +func (p *Process) TerminalWithContext(ctx context.Context) (string, error) { + k, err := p.getKProc() + if err != nil { + return "", err + } + + ttyNr := uint64(k.Tdev) + + termmap, err := getTerminalMap() + if err != nil { + return "", err + } + + return termmap[ttyNr], nil +} + +func (p *Process) Nice() (int32, error) { + return p.NiceWithContext(context.Background()) +} + +func (p *Process) NiceWithContext(ctx context.Context) (int32, error) { + k, err := p.getKProc() + if err != nil { + return 0, err + } + return int32(k.Nice), nil +} + +func (p *Process) IOnice() (int32, error) { + return p.IOniceWithContext(context.Background()) +} + +func (p *Process) IOniceWithContext(ctx context.Context) (int32, error) { + return 0, common.ErrNotImplementedError +} + +func (p *Process) Rlimit() ([]RlimitStat, error) { + return p.RlimitWithContext(context.Background()) +} + +func (p *Process) RlimitWithContext(ctx context.Context) ([]RlimitStat, error) { + var rlimit []RlimitStat + return rlimit, common.ErrNotImplementedError +} + +func (p *Process) RlimitUsage(gatherUsed bool) ([]RlimitStat, error) { + return p.RlimitUsageWithContext(context.Background(), gatherUsed) +} + +func (p *Process) RlimitUsageWithContext(ctx context.Context, gatherUsed bool) ([]RlimitStat, error) { + var rlimit []RlimitStat + return rlimit, common.ErrNotImplementedError +} + +func (p *Process) IOCounters() (*IOCountersStat, error) { + return p.IOCountersWithContext(context.Background()) +} + +func (p *Process) IOCountersWithContext(ctx context.Context) (*IOCountersStat, error) { + k, err := p.getKProc() + if err != nil { + return nil, err + } + return &IOCountersStat{ + ReadCount: uint64(k.Rusage.Inblock), + WriteCount: uint64(k.Rusage.Oublock), + }, nil +} + +func (p *Process) NumCtxSwitches() (*NumCtxSwitchesStat, error) { + return p.NumCtxSwitchesWithContext(context.Background()) +} + +func (p *Process) NumCtxSwitchesWithContext(ctx context.Context) (*NumCtxSwitchesStat, error) { + return nil, common.ErrNotImplementedError +} + +func (p *Process) NumFDs() (int32, error) { + return p.NumFDsWithContext(context.Background()) +} + +func (p *Process) NumFDsWithContext(ctx context.Context) (int32, error) { + return 0, common.ErrNotImplementedError +} + +func (p *Process) NumThreads() (int32, error) { + return p.NumThreadsWithContext(context.Background()) +} + +func (p *Process) NumThreadsWithContext(ctx context.Context) (int32, error) { + k, err := p.getKProc() + if err != nil { + return 0, err + } + + return k.Numthreads, nil +} + +func (p *Process) Threads() (map[int32]*cpu.TimesStat, error) { + return p.ThreadsWithContext(context.Background()) +} + +func (p *Process) ThreadsWithContext(ctx context.Context) (map[int32]*cpu.TimesStat, error) { + ret := make(map[int32]*cpu.TimesStat) + return ret, common.ErrNotImplementedError +} + +func (p *Process) Times() (*cpu.TimesStat, error) { + return p.TimesWithContext(context.Background()) +} + +func (p *Process) TimesWithContext(ctx context.Context) (*cpu.TimesStat, error) { + k, err := p.getKProc() + if err != nil { + return nil, err + } + return &cpu.TimesStat{ + CPU: "cpu", + User: float64(k.Rusage.Utime.Sec) + float64(k.Rusage.Utime.Usec)/1000000, + System: float64(k.Rusage.Stime.Sec) + float64(k.Rusage.Stime.Usec)/1000000, + }, nil +} + +func (p *Process) CPUAffinity() ([]int32, error) { + return p.CPUAffinityWithContext(context.Background()) +} + +func (p *Process) CPUAffinityWithContext(ctx context.Context) ([]int32, error) { + return nil, common.ErrNotImplementedError +} + +func (p *Process) MemoryInfo() (*MemoryInfoStat, error) { + return p.MemoryInfoWithContext(context.Background()) +} + +func (p *Process) MemoryInfoWithContext(ctx context.Context) (*MemoryInfoStat, error) { + k, err := p.getKProc() + if err != nil { + return nil, err + } + v, err := unix.Sysctl("vm.stats.vm.v_page_size") + if err != nil { + return nil, err + } + pageSize := common.LittleEndian.Uint16([]byte(v)) + + return &MemoryInfoStat{ + RSS: uint64(k.Rssize) * uint64(pageSize), + VMS: uint64(k.Size), + }, nil +} + +func (p *Process) MemoryInfoEx() (*MemoryInfoExStat, error) { + return p.MemoryInfoExWithContext(context.Background()) +} + +func (p *Process) MemoryInfoExWithContext(ctx context.Context) (*MemoryInfoExStat, error) { + return nil, common.ErrNotImplementedError +} + +func (p *Process) PageFaults() (*PageFaultsStat, error) { + return p.PageFaultsWithContext(context.Background()) +} + +func (p *Process) PageFaultsWithContext(ctx context.Context) (*PageFaultsStat, error) { + return nil, common.ErrNotImplementedError +} + +func (p *Process) Children() ([]*Process, error) { + return p.ChildrenWithContext(context.Background()) +} + +func (p *Process) ChildrenWithContext(ctx context.Context) ([]*Process, error) { + pids, err := common.CallPgrepWithContext(ctx, invoke, p.Pid) + if err != nil { + return nil, err + } + ret := make([]*Process, 0, len(pids)) + for _, pid := range pids { + np, err := NewProcess(pid) + if err != nil { + return nil, err + } + ret = append(ret, np) + } + return ret, nil +} + +func (p *Process) OpenFiles() ([]OpenFilesStat, error) { + return p.OpenFilesWithContext(context.Background()) +} + +func (p *Process) OpenFilesWithContext(ctx context.Context) ([]OpenFilesStat, error) { + return nil, common.ErrNotImplementedError +} + +func (p *Process) Connections() ([]net.ConnectionStat, error) { + return p.ConnectionsWithContext(context.Background()) +} + +func (p *Process) ConnectionsWithContext(ctx context.Context) ([]net.ConnectionStat, error) { + return nil, common.ErrNotImplementedError +} + +// Connections returns a slice of net.ConnectionStat used by the process at most `max` +func (p *Process) ConnectionsMax(max int) ([]net.ConnectionStat, error) { + return p.ConnectionsMaxWithContext(context.Background(), max) +} + +func (p *Process) ConnectionsMaxWithContext(ctx context.Context, max int) ([]net.ConnectionStat, error) { + return []net.ConnectionStat{}, common.ErrNotImplementedError +} + +func (p *Process) NetIOCounters(pernic bool) ([]net.IOCountersStat, error) { + return p.NetIOCountersWithContext(context.Background(), pernic) +} + +func (p *Process) NetIOCountersWithContext(ctx context.Context, pernic bool) ([]net.IOCountersStat, error) { + return nil, common.ErrNotImplementedError +} + +func (p *Process) MemoryMaps(grouped bool) (*[]MemoryMapsStat, error) { + return p.MemoryMapsWithContext(context.Background(), grouped) +} + +func (p *Process) MemoryMapsWithContext(ctx context.Context, grouped bool) (*[]MemoryMapsStat, error) { + var ret []MemoryMapsStat + return &ret, common.ErrNotImplementedError +} + +func Processes() ([]*Process, error) { + return ProcessesWithContext(context.Background()) +} + +func ProcessesWithContext(ctx context.Context) ([]*Process, error) { + results := []*Process{} + + mib := []int32{CTLKern, KernProc, KernProcProc, 0} + buf, length, err := common.CallSyscall(mib) + if err != nil { + return results, err + } + + // get kinfo_proc size + count := int(length / uint64(sizeOfKinfoProc)) + + // parse buf to procs + for i := 0; i < count; i++ { + b := buf[i*sizeOfKinfoProc : (i+1)*sizeOfKinfoProc] + k, err := parseKinfoProc(b) + if err != nil { + continue + } + p, err := NewProcess(int32(k.Pid)) + if err != nil { + continue + } + + results = append(results, p) + } + + return results, nil +} + +func parseKinfoProc(buf []byte) (KinfoProc, error) { + var k KinfoProc + br := bytes.NewReader(buf) + err := common.Read(br, binary.LittleEndian, &k) + return k, err +} + +func (p *Process) getKProc() (*KinfoProc, error) { + return p.getKProcWithContext(context.Background()) +} + +func (p *Process) getKProcWithContext(ctx context.Context) (*KinfoProc, error) { + mib := []int32{CTLKern, KernProc, KernProcPID, p.Pid} + + buf, length, err := common.CallSyscall(mib) + if err != nil { + return nil, err + } + if length != sizeOfKinfoProc { + return nil, err + } + + k, err := parseKinfoProc(buf) + if err != nil { + return nil, err + } + return &k, nil +} diff --git a/internal/gopsutil/process/process_freebsd_386.go b/internal/gopsutil/process/process_freebsd_386.go new file mode 100644 index 0000000..08ab333 --- /dev/null +++ b/internal/gopsutil/process/process_freebsd_386.go @@ -0,0 +1,192 @@ +// Created by cgo -godefs - DO NOT EDIT +// cgo -godefs types_freebsd.go + +package process + +const ( + CTLKern = 1 + KernProc = 14 + KernProcPID = 1 + KernProcProc = 8 + KernProcPathname = 12 + KernProcArgs = 7 +) + +const ( + sizeofPtr = 0x4 + sizeofShort = 0x2 + sizeofInt = 0x4 + sizeofLong = 0x4 + sizeofLongLong = 0x8 +) + +const ( + sizeOfKinfoVmentry = 0x488 + sizeOfKinfoProc = 0x300 +) + +const ( + SIDL = 1 + SRUN = 2 + SSLEEP = 3 + SSTOP = 4 + SZOMB = 5 + SWAIT = 6 + SLOCK = 7 +) + +type ( + _C_short int16 + _C_int int32 + _C_long int32 + _C_long_long int64 +) + +type Timespec struct { + Sec int32 + Nsec int32 +} + +type Timeval struct { + Sec int32 + Usec int32 +} + +type Rusage struct { + Utime Timeval + Stime Timeval + Maxrss int32 + Ixrss int32 + Idrss int32 + Isrss int32 + Minflt int32 + Majflt int32 + Nswap int32 + Inblock int32 + Oublock int32 + Msgsnd int32 + Msgrcv int32 + Nsignals int32 + Nvcsw int32 + Nivcsw int32 +} + +type Rlimit struct { + Cur int64 + Max int64 +} + +type KinfoProc struct { + Structsize int32 + Layout int32 + Args int32 /* pargs */ + Paddr int32 /* proc */ + Addr int32 /* user */ + Tracep int32 /* vnode */ + Textvp int32 /* vnode */ + Fd int32 /* filedesc */ + Vmspace int32 /* vmspace */ + Wchan int32 + Pid int32 + Ppid int32 + Pgid int32 + Tpgid int32 + Sid int32 + Tsid int32 + Jobc int16 + Spare_short1 int16 + Tdev uint32 + Siglist [16]byte /* sigset */ + Sigmask [16]byte /* sigset */ + Sigignore [16]byte /* sigset */ + Sigcatch [16]byte /* sigset */ + Uid uint32 + Ruid uint32 + Svuid uint32 + Rgid uint32 + Svgid uint32 + Ngroups int16 + Spare_short2 int16 + Groups [16]uint32 + Size uint32 + Rssize int32 + Swrss int32 + Tsize int32 + Dsize int32 + Ssize int32 + Xstat uint16 + Acflag uint16 + Pctcpu uint32 + Estcpu uint32 + Slptime uint32 + Swtime uint32 + Cow uint32 + Runtime uint64 + Start Timeval + Childtime Timeval + Flag int32 + Kiflag int32 + Traceflag int32 + Stat int8 + Nice int8 + Lock int8 + Rqindex int8 + Oncpu uint8 + Lastcpu uint8 + Tdname [17]int8 + Wmesg [9]int8 + Login [18]int8 + Lockname [9]int8 + Comm [20]int8 + Emul [17]int8 + Loginclass [18]int8 + Sparestrings [50]int8 + Spareints [7]int32 + Flag2 int32 + Fibnum int32 + Cr_flags uint32 + Jid int32 + Numthreads int32 + Tid int32 + Pri Priority + Rusage Rusage + Rusage_ch Rusage + Pcb int32 /* pcb */ + Kstack int32 + Udata int32 + Tdaddr int32 /* thread */ + Spareptrs [6]int32 + Sparelongs [12]int32 + Sflag int32 + Tdflags int32 +} + +type Priority struct { + Class uint8 + Level uint8 + Native uint8 + User uint8 +} + +type KinfoVmentry struct { + Structsize int32 + Type int32 + Start uint64 + End uint64 + Offset uint64 + Vn_fileid uint64 + Vn_fsid uint32 + Flags int32 + Resident int32 + Private_resident int32 + Protection int32 + Ref_count int32 + Shadow_count int32 + Vn_type int32 + Vn_size uint64 + Vn_rdev uint32 + Vn_mode uint16 + Status uint16 + X_kve_ispare [12]int32 + Path [1024]int8 +} diff --git a/internal/gopsutil/process/process_freebsd_amd64.go b/internal/gopsutil/process/process_freebsd_amd64.go new file mode 100644 index 0000000..560e627 --- /dev/null +++ b/internal/gopsutil/process/process_freebsd_amd64.go @@ -0,0 +1,192 @@ +// Created by cgo -godefs - DO NOT EDIT +// cgo -godefs types_freebsd.go + +package process + +const ( + CTLKern = 1 + KernProc = 14 + KernProcPID = 1 + KernProcProc = 8 + KernProcPathname = 12 + KernProcArgs = 7 +) + +const ( + sizeofPtr = 0x8 + sizeofShort = 0x2 + sizeofInt = 0x4 + sizeofLong = 0x8 + sizeofLongLong = 0x8 +) + +const ( + sizeOfKinfoVmentry = 0x488 + sizeOfKinfoProc = 0x440 +) + +const ( + SIDL = 1 + SRUN = 2 + SSLEEP = 3 + SSTOP = 4 + SZOMB = 5 + SWAIT = 6 + SLOCK = 7 +) + +type ( + _C_short int16 + _C_int int32 + _C_long int64 + _C_long_long int64 +) + +type Timespec struct { + Sec int64 + Nsec int64 +} + +type Timeval struct { + Sec int64 + Usec int64 +} + +type Rusage struct { + Utime Timeval + Stime Timeval + Maxrss int64 + Ixrss int64 + Idrss int64 + Isrss int64 + Minflt int64 + Majflt int64 + Nswap int64 + Inblock int64 + Oublock int64 + Msgsnd int64 + Msgrcv int64 + Nsignals int64 + Nvcsw int64 + Nivcsw int64 +} + +type Rlimit struct { + Cur int64 + Max int64 +} + +type KinfoProc struct { + Structsize int32 + Layout int32 + Args int64 /* pargs */ + Paddr int64 /* proc */ + Addr int64 /* user */ + Tracep int64 /* vnode */ + Textvp int64 /* vnode */ + Fd int64 /* filedesc */ + Vmspace int64 /* vmspace */ + Wchan int64 + Pid int32 + Ppid int32 + Pgid int32 + Tpgid int32 + Sid int32 + Tsid int32 + Jobc int16 + Spare_short1 int16 + Tdev uint32 + Siglist [16]byte /* sigset */ + Sigmask [16]byte /* sigset */ + Sigignore [16]byte /* sigset */ + Sigcatch [16]byte /* sigset */ + Uid uint32 + Ruid uint32 + Svuid uint32 + Rgid uint32 + Svgid uint32 + Ngroups int16 + Spare_short2 int16 + Groups [16]uint32 + Size uint64 + Rssize int64 + Swrss int64 + Tsize int64 + Dsize int64 + Ssize int64 + Xstat uint16 + Acflag uint16 + Pctcpu uint32 + Estcpu uint32 + Slptime uint32 + Swtime uint32 + Cow uint32 + Runtime uint64 + Start Timeval + Childtime Timeval + Flag int64 + Kiflag int64 + Traceflag int32 + Stat int8 + Nice int8 + Lock int8 + Rqindex int8 + Oncpu uint8 + Lastcpu uint8 + Tdname [17]int8 + Wmesg [9]int8 + Login [18]int8 + Lockname [9]int8 + Comm [20]int8 + Emul [17]int8 + Loginclass [18]int8 + Sparestrings [50]int8 + Spareints [7]int32 + Flag2 int32 + Fibnum int32 + Cr_flags uint32 + Jid int32 + Numthreads int32 + Tid int32 + Pri Priority + Rusage Rusage + Rusage_ch Rusage + Pcb int64 /* pcb */ + Kstack int64 + Udata int64 + Tdaddr int64 /* thread */ + Spareptrs [6]int64 + Sparelongs [12]int64 + Sflag int64 + Tdflags int64 +} + +type Priority struct { + Class uint8 + Level uint8 + Native uint8 + User uint8 +} + +type KinfoVmentry struct { + Structsize int32 + Type int32 + Start uint64 + End uint64 + Offset uint64 + Vn_fileid uint64 + Vn_fsid uint32 + Flags int32 + Resident int32 + Private_resident int32 + Protection int32 + Ref_count int32 + Shadow_count int32 + Vn_type int32 + Vn_size uint64 + Vn_rdev uint32 + Vn_mode uint16 + Status uint16 + X_kve_ispare [12]int32 + Path [1024]int8 +} diff --git a/internal/gopsutil/process/process_freebsd_arm.go b/internal/gopsutil/process/process_freebsd_arm.go new file mode 100644 index 0000000..81ae0b9 --- /dev/null +++ b/internal/gopsutil/process/process_freebsd_arm.go @@ -0,0 +1,192 @@ +// Created by cgo -godefs - DO NOT EDIT +// cgo -godefs types_freebsd.go + +package process + +const ( + CTLKern = 1 + KernProc = 14 + KernProcPID = 1 + KernProcProc = 8 + KernProcPathname = 12 + KernProcArgs = 7 +) + +const ( + sizeofPtr = 0x4 + sizeofShort = 0x2 + sizeofInt = 0x4 + sizeofLong = 0x4 + sizeofLongLong = 0x8 +) + +const ( + sizeOfKinfoVmentry = 0x488 + sizeOfKinfoProc = 0x440 +) + +const ( + SIDL = 1 + SRUN = 2 + SSLEEP = 3 + SSTOP = 4 + SZOMB = 5 + SWAIT = 6 + SLOCK = 7 +) + +type ( + _C_short int16 + _C_int int32 + _C_long int32 + _C_long_long int64 +) + +type Timespec struct { + Sec int64 + Nsec int64 +} + +type Timeval struct { + Sec int64 + Usec int64 +} + +type Rusage struct { + Utime Timeval + Stime Timeval + Maxrss int32 + Ixrss int32 + Idrss int32 + Isrss int32 + Minflt int32 + Majflt int32 + Nswap int32 + Inblock int32 + Oublock int32 + Msgsnd int32 + Msgrcv int32 + Nsignals int32 + Nvcsw int32 + Nivcsw int32 +} + +type Rlimit struct { + Cur int32 + Max int32 +} + +type KinfoProc struct { + Structsize int32 + Layout int32 + Args int32 /* pargs */ + Paddr int32 /* proc */ + Addr int32 /* user */ + Tracep int32 /* vnode */ + Textvp int32 /* vnode */ + Fd int32 /* filedesc */ + Vmspace int32 /* vmspace */ + Wchan int32 + Pid int32 + Ppid int32 + Pgid int32 + Tpgid int32 + Sid int32 + Tsid int32 + Jobc int16 + Spare_short1 int16 + Tdev uint32 + Siglist [16]byte /* sigset */ + Sigmask [16]byte /* sigset */ + Sigignore [16]byte /* sigset */ + Sigcatch [16]byte /* sigset */ + Uid uint32 + Ruid uint32 + Svuid uint32 + Rgid uint32 + Svgid uint32 + Ngroups int16 + Spare_short2 int16 + Groups [16]uint32 + Size uint32 + Rssize int32 + Swrss int32 + Tsize int32 + Dsize int32 + Ssize int32 + Xstat uint16 + Acflag uint16 + Pctcpu uint32 + Estcpu uint32 + Slptime uint32 + Swtime uint32 + Cow uint32 + Runtime uint64 + Start Timeval + Childtime Timeval + Flag int32 + Kiflag int32 + Traceflag int32 + Stat int8 + Nice int8 + Lock int8 + Rqindex int8 + Oncpu uint8 + Lastcpu uint8 + Tdname [17]int8 + Wmesg [9]int8 + Login [18]int8 + Lockname [9]int8 + Comm [20]int8 + Emul [17]int8 + Loginclass [18]int8 + Sparestrings [50]int8 + Spareints [4]int32 + Flag2 int32 + Fibnum int32 + Cr_flags uint32 + Jid int32 + Numthreads int32 + Tid int32 + Pri Priority + Rusage Rusage + Rusage_ch Rusage + Pcb int32 /* pcb */ + Kstack int32 + Udata int32 + Tdaddr int32 /* thread */ + Spareptrs [6]int64 + Sparelongs [12]int64 + Sflag int64 + Tdflags int64 +} + +type Priority struct { + Class uint8 + Level uint8 + Native uint8 + User uint8 +} + +type KinfoVmentry struct { + Structsize int32 + Type int32 + Start uint64 + End uint64 + Offset uint64 + Vn_fileid uint64 + Vn_fsid uint32 + Flags int32 + Resident int32 + Private_resident int32 + Protection int32 + Ref_count int32 + Shadow_count int32 + Vn_type int32 + Vn_size uint64 + Vn_rdev uint32 + Vn_mode uint16 + Status uint16 + X_kve_ispare [12]int32 + Path [1024]int8 +} diff --git a/internal/gopsutil/process/process_freebsd_arm64.go b/internal/gopsutil/process/process_freebsd_arm64.go new file mode 100644 index 0000000..c83fa7c --- /dev/null +++ b/internal/gopsutil/process/process_freebsd_arm64.go @@ -0,0 +1,201 @@ +//go:build freebsd && arm64 + +// Code generated by cmd/cgo -godefs; DO NOT EDIT. +// cgo -godefs process/types_freebsd.go + +package process + +const ( + CTLKern = 1 + KernProc = 14 + KernProcPID = 1 + KernProcProc = 8 + KernProcPathname = 12 + KernProcArgs = 7 +) + +const ( + sizeofPtr = 0x8 + sizeofShort = 0x2 + sizeofInt = 0x4 + sizeofLong = 0x8 + sizeofLongLong = 0x8 +) + +const ( + sizeOfKinfoVmentry = 0x488 + sizeOfKinfoProc = 0x440 +) + +const ( + SIDL = 1 + SRUN = 2 + SSLEEP = 3 + SSTOP = 4 + SZOMB = 5 + SWAIT = 6 + SLOCK = 7 +) + +type ( + _C_short int16 + _C_int int32 + _C_long int64 + _C_long_long int64 +) + +type Timespec struct { + Sec int64 + Nsec int64 +} + +type Timeval struct { + Sec int64 + Usec int64 +} + +type Rusage struct { + Utime Timeval + Stime Timeval + Maxrss int64 + Ixrss int64 + Idrss int64 + Isrss int64 + Minflt int64 + Majflt int64 + Nswap int64 + Inblock int64 + Oublock int64 + Msgsnd int64 + Msgrcv int64 + Nsignals int64 + Nvcsw int64 + Nivcsw int64 +} + +type Rlimit struct { + Cur int64 + Max int64 +} + +type KinfoProc struct { + Structsize int32 + Layout int32 + Args *int64 /* pargs */ + Paddr *int64 /* proc */ + Addr *int64 /* user */ + Tracep *int64 /* vnode */ + Textvp *int64 /* vnode */ + Fd *int64 /* filedesc */ + Vmspace *int64 /* vmspace */ + Wchan *byte + Pid int32 + Ppid int32 + Pgid int32 + Tpgid int32 + Sid int32 + Tsid int32 + Jobc int16 + Spare_short1 int16 + Tdev_freebsd11 uint32 + Siglist [16]byte /* sigset */ + Sigmask [16]byte /* sigset */ + Sigignore [16]byte /* sigset */ + Sigcatch [16]byte /* sigset */ + Uid uint32 + Ruid uint32 + Svuid uint32 + Rgid uint32 + Svgid uint32 + Ngroups int16 + Spare_short2 int16 + Groups [16]uint32 + Size uint64 + Rssize int64 + Swrss int64 + Tsize int64 + Dsize int64 + Ssize int64 + Xstat uint16 + Acflag uint16 + Pctcpu uint32 + Estcpu uint32 + Slptime uint32 + Swtime uint32 + Cow uint32 + Runtime uint64 + Start Timeval + Childtime Timeval + Flag int64 + Kiflag int64 + Traceflag int32 + Stat uint8 + Nice int8 + Lock uint8 + Rqindex uint8 + Oncpu_old uint8 + Lastcpu_old uint8 + Tdname [17]uint8 + Wmesg [9]uint8 + Login [18]uint8 + Lockname [9]uint8 + Comm [20]int8 + Emul [17]uint8 + Loginclass [18]uint8 + Moretdname [4]uint8 + Sparestrings [46]uint8 + Spareints [2]int32 + Tdev uint64 + Oncpu int32 + Lastcpu int32 + Tracer int32 + Flag2 int32 + Fibnum int32 + Cr_flags uint32 + Jid int32 + Numthreads int32 + Tid int32 + Pri Priority + Rusage Rusage + Rusage_ch Rusage + Pcb *int64 /* pcb */ + Kstack *byte + Udata *byte + Tdaddr *int64 /* thread */ + Spareptrs [6]*byte + Sparelongs [12]int64 + Sflag int64 + Tdflags int64 +} + +type Priority struct { + Class uint8 + Level uint8 + Native uint8 + User uint8 +} + +type KinfoVmentry struct { + Structsize int32 + Type int32 + Start uint64 + End uint64 + Offset uint64 + Vn_fileid uint64 + Vn_fsid_freebsd11 uint32 + Flags int32 + Resident int32 + Private_resident int32 + Protection int32 + Ref_count int32 + Shadow_count int32 + Vn_type int32 + Vn_size uint64 + Vn_rdev_freebsd11 uint32 + Vn_mode uint16 + Status uint16 + Vn_fsid uint64 + Vn_rdev uint64 + X_kve_ispare [8]int32 + Path [1024]uint8 +} diff --git a/internal/gopsutil/process/process_linux.go b/internal/gopsutil/process/process_linux.go new file mode 100644 index 0000000..23007d5 --- /dev/null +++ b/internal/gopsutil/process/process_linux.go @@ -0,0 +1,1299 @@ +//go:build linux + +package process + +import ( + "bufio" + "bytes" + "context" + "encoding/json" + "fmt" + "math" + "os" + "path/filepath" + "strconv" + "strings" + + "golang.org/x/sys/unix" + + "github.com/gofiber/fiber/v2/internal/gopsutil/common" + "github.com/gofiber/fiber/v2/internal/gopsutil/cpu" + "github.com/gofiber/fiber/v2/internal/gopsutil/net" +) + +var PageSize = uint64(os.Getpagesize()) + +const ( + PrioProcess = 0 // linux/resource.h + ClockTicks = 100 // C.sysconf(C._SC_CLK_TCK) +) + +// MemoryInfoExStat is different between OSes +type MemoryInfoExStat struct { + RSS uint64 `json:"rss"` // bytes + VMS uint64 `json:"vms"` // bytes + Shared uint64 `json:"shared"` // bytes + Text uint64 `json:"text"` // bytes + Lib uint64 `json:"lib"` // bytes + Data uint64 `json:"data"` // bytes + Dirty uint64 `json:"dirty"` // bytes +} + +func (m MemoryInfoExStat) String() string { + s, _ := json.Marshal(m) + return string(s) +} + +type MemoryMapsStat struct { + Path string `json:"path"` + Rss uint64 `json:"rss"` + Size uint64 `json:"size"` + Pss uint64 `json:"pss"` + SharedClean uint64 `json:"sharedClean"` + SharedDirty uint64 `json:"sharedDirty"` + PrivateClean uint64 `json:"privateClean"` + PrivateDirty uint64 `json:"privateDirty"` + Referenced uint64 `json:"referenced"` + Anonymous uint64 `json:"anonymous"` + Swap uint64 `json:"swap"` +} + +// String returns JSON value of the process. +func (m MemoryMapsStat) String() string { + s, _ := json.Marshal(m) + return string(s) +} + +// Ppid returns Parent Process ID of the process. +func (p *Process) Ppid() (int32, error) { + return p.PpidWithContext(context.Background()) +} + +func (p *Process) PpidWithContext(ctx context.Context) (int32, error) { + _, ppid, _, _, _, _, _, err := p.fillFromStatWithContext(ctx) + if err != nil { + return -1, err + } + return ppid, nil +} + +// Name returns name of the process. +func (p *Process) Name() (string, error) { + return p.NameWithContext(context.Background()) +} + +func (p *Process) NameWithContext(ctx context.Context) (string, error) { + if p.name == "" { + if err := p.fillFromStatusWithContext(ctx); err != nil { + return "", err + } + } + return p.name, nil +} + +// Tgid returns tgid, a Linux-synonym for user-space Pid +func (p *Process) Tgid() (int32, error) { + if p.tgid == 0 { + if err := p.fillFromStatusWithContext(context.Background()); err != nil { + return 0, err + } + } + return p.tgid, nil +} + +// Exe returns executable path of the process. +func (p *Process) Exe() (string, error) { + return p.ExeWithContext(context.Background()) +} + +func (p *Process) ExeWithContext(ctx context.Context) (string, error) { + return p.fillFromExeWithContext(ctx) +} + +// Cmdline returns the command line arguments of the process as a string with +// each argument separated by 0x20 ascii character. +func (p *Process) Cmdline() (string, error) { + return p.CmdlineWithContext(context.Background()) +} + +func (p *Process) CmdlineWithContext(ctx context.Context) (string, error) { + return p.fillFromCmdlineWithContext(ctx) +} + +// CmdlineSlice returns the command line arguments of the process as a slice with each +// element being an argument. +func (p *Process) CmdlineSlice() ([]string, error) { + return p.CmdlineSliceWithContext(context.Background()) +} + +func (p *Process) CmdlineSliceWithContext(ctx context.Context) ([]string, error) { + return p.fillSliceFromCmdlineWithContext(ctx) +} + +func (p *Process) createTimeWithContext(ctx context.Context) (int64, error) { + _, _, _, createTime, _, _, _, err := p.fillFromStatWithContext(ctx) + if err != nil { + return 0, err + } + return createTime, nil +} + +// Cwd returns current working directory of the process. +func (p *Process) Cwd() (string, error) { + return p.CwdWithContext(context.Background()) +} + +func (p *Process) CwdWithContext(ctx context.Context) (string, error) { + return p.fillFromCwdWithContext(ctx) +} + +// Parent returns parent Process of the process. +func (p *Process) Parent() (*Process, error) { + return p.ParentWithContext(context.Background()) +} + +func (p *Process) ParentWithContext(ctx context.Context) (*Process, error) { + err := p.fillFromStatusWithContext(ctx) + if err != nil { + return nil, err + } + if p.parent == 0 { + return nil, fmt.Errorf("wrong number of parents") + } + return NewProcess(p.parent) +} + +// Status returns the process status. +// Return value could be one of these. +// R: Running S: Sleep T: Stop I: Idle +// Z: Zombie W: Wait L: Lock +// The character is same within all supported platforms. +func (p *Process) Status() (string, error) { + return p.StatusWithContext(context.Background()) +} + +func (p *Process) StatusWithContext(ctx context.Context) (string, error) { + err := p.fillFromStatusWithContext(ctx) + if err != nil { + return "", err + } + return p.status, nil +} + +// Foreground returns true if the process is in foreground, false otherwise. +func (p *Process) Foreground() (bool, error) { + return p.ForegroundWithContext(context.Background()) +} + +func (p *Process) ForegroundWithContext(ctx context.Context) (bool, error) { + // see https://github.com/shirou/gopsutil/issues/596#issuecomment-432707831 for implementation details + pid := p.Pid + statPath := common.HostProc(strconv.Itoa(int(pid)), "stat") + contents, err := os.ReadFile(statPath) + if err != nil { + return false, err + } + fields := strings.Fields(string(contents)) + if len(fields) < 8 { + return false, fmt.Errorf("insufficient data in %s", statPath) + } + pgid := fields[4] + tpgid := fields[7] + return pgid == tpgid, nil +} + +// Uids returns user ids of the process as a slice of the int +func (p *Process) Uids() ([]int32, error) { + return p.UidsWithContext(context.Background()) +} + +func (p *Process) UidsWithContext(ctx context.Context) ([]int32, error) { + err := p.fillFromStatusWithContext(ctx) + if err != nil { + return []int32{}, err + } + return p.uids, nil +} + +// Gids returns group ids of the process as a slice of the int +func (p *Process) Gids() ([]int32, error) { + return p.GidsWithContext(context.Background()) +} + +func (p *Process) GidsWithContext(ctx context.Context) ([]int32, error) { + err := p.fillFromStatusWithContext(ctx) + if err != nil { + return []int32{}, err + } + return p.gids, nil +} + +func (p *Process) GroupsWithContext(ctx context.Context) ([]int32, error) { + err := p.fillFromStatusWithContext(ctx) + if err != nil { + return []int32{}, err + } + return p.groups, nil +} + +// Terminal returns a terminal which is associated with the process. +func (p *Process) Terminal() (string, error) { + return p.TerminalWithContext(context.Background()) +} + +func (p *Process) TerminalWithContext(ctx context.Context) (string, error) { + t, _, _, _, _, _, _, err := p.fillFromStatWithContext(ctx) + if err != nil { + return "", err + } + termmap, err := getTerminalMap() + if err != nil { + return "", err + } + terminal := termmap[t] + return terminal, nil +} + +// Nice returns a nice value (priority). +// Notice: gopsutil can not set nice value. +func (p *Process) Nice() (int32, error) { + return p.NiceWithContext(context.Background()) +} + +func (p *Process) NiceWithContext(ctx context.Context) (int32, error) { + _, _, _, _, _, nice, _, err := p.fillFromStatWithContext(ctx) + if err != nil { + return 0, err + } + return nice, nil +} + +// IOnice returns process I/O nice value (priority). +func (p *Process) IOnice() (int32, error) { + return p.IOniceWithContext(context.Background()) +} + +func (p *Process) IOniceWithContext(ctx context.Context) (int32, error) { + return 0, common.ErrNotImplementedError +} + +// Rlimit returns Resource Limits. +func (p *Process) Rlimit() ([]RlimitStat, error) { + return p.RlimitWithContext(context.Background()) +} + +func (p *Process) RlimitWithContext(ctx context.Context) ([]RlimitStat, error) { + return p.RlimitUsage(false) +} + +// RlimitUsage returns Resource Limits. +// If gatherUsed is true, the currently used value will be gathered and added +// to the resulting RlimitStat. +func (p *Process) RlimitUsage(gatherUsed bool) ([]RlimitStat, error) { + return p.RlimitUsageWithContext(context.Background(), gatherUsed) +} + +func (p *Process) RlimitUsageWithContext(ctx context.Context, gatherUsed bool) ([]RlimitStat, error) { + rlimits, err := p.fillFromLimitsWithContext(ctx) + if !gatherUsed || err != nil { + return rlimits, err + } + + _, _, _, _, rtprio, nice, _, err := p.fillFromStatWithContext(ctx) + if err != nil { + return nil, err + } + if err := p.fillFromStatusWithContext(ctx); err != nil { + return nil, err + } + + for i := range rlimits { + rs := &rlimits[i] + switch rs.Resource { + case RLIMIT_CPU: + times, err := p.Times() + if err != nil { + return nil, err + } + rs.Used = uint64(times.User + times.System) + case RLIMIT_DATA: + rs.Used = uint64(p.memInfo.Data) + case RLIMIT_STACK: + rs.Used = uint64(p.memInfo.Stack) + case RLIMIT_RSS: + rs.Used = uint64(p.memInfo.RSS) + case RLIMIT_NOFILE: + n, err := p.NumFDs() + if err != nil { + return nil, err + } + rs.Used = uint64(n) + case RLIMIT_MEMLOCK: + rs.Used = uint64(p.memInfo.Locked) + case RLIMIT_AS: + rs.Used = uint64(p.memInfo.VMS) + case RLIMIT_LOCKS: + // TODO we can get the used value from /proc/$pid/locks. But linux doesn't enforce it, so not a high priority. + case RLIMIT_SIGPENDING: + rs.Used = p.sigInfo.PendingProcess + case RLIMIT_NICE: + // The rlimit for nice is a little unusual, in that 0 means the niceness cannot be decreased beyond the current value, but it can be increased. + // So effectively: if rs.Soft == 0 { rs.Soft = rs.Used } + rs.Used = uint64(nice) + case RLIMIT_RTPRIO: + rs.Used = uint64(rtprio) + } + } + + return rlimits, err +} + +// IOCounters returns IO Counters. +func (p *Process) IOCounters() (*IOCountersStat, error) { + return p.IOCountersWithContext(context.Background()) +} + +func (p *Process) IOCountersWithContext(ctx context.Context) (*IOCountersStat, error) { + return p.fillFromIOWithContext(ctx) +} + +// NumCtxSwitches returns the number of the context switches of the process. +func (p *Process) NumCtxSwitches() (*NumCtxSwitchesStat, error) { + return p.NumCtxSwitchesWithContext(context.Background()) +} + +func (p *Process) NumCtxSwitchesWithContext(ctx context.Context) (*NumCtxSwitchesStat, error) { + err := p.fillFromStatusWithContext(ctx) + if err != nil { + return nil, err + } + return p.numCtxSwitches, nil +} + +// NumFDs returns the number of File Descriptors used by the process. +func (p *Process) NumFDs() (int32, error) { + return p.NumFDsWithContext(context.Background()) +} + +func (p *Process) NumFDsWithContext(ctx context.Context) (int32, error) { + _, fnames, err := p.fillFromfdListWithContext(ctx) + return int32(len(fnames)), err +} + +// NumThreads returns the number of threads used by the process. +func (p *Process) NumThreads() (int32, error) { + return p.NumThreadsWithContext(context.Background()) +} + +func (p *Process) NumThreadsWithContext(ctx context.Context) (int32, error) { + err := p.fillFromStatusWithContext(ctx) + if err != nil { + return 0, err + } + return p.numThreads, nil +} + +func (p *Process) Threads() (map[int32]*cpu.TimesStat, error) { + return p.ThreadsWithContext(context.Background()) +} + +func (p *Process) ThreadsWithContext(ctx context.Context) (map[int32]*cpu.TimesStat, error) { + ret := make(map[int32]*cpu.TimesStat) + taskPath := common.HostProc(strconv.Itoa(int(p.Pid)), "task") + + tids, err := readPidsFromDir(taskPath) + if err != nil { + return nil, err + } + + for _, tid := range tids { + _, _, cpuTimes, _, _, _, _, err := p.fillFromTIDStatWithContext(ctx, tid) + if err != nil { + return nil, err + } + ret[tid] = cpuTimes + } + + return ret, nil +} + +// Times returns CPU times of the process. +func (p *Process) Times() (*cpu.TimesStat, error) { + return p.TimesWithContext(context.Background()) +} + +func (p *Process) TimesWithContext(ctx context.Context) (*cpu.TimesStat, error) { + _, _, cpuTimes, _, _, _, _, err := p.fillFromStatWithContext(ctx) + if err != nil { + return nil, err + } + return cpuTimes, nil +} + +// CPUAffinity returns CPU affinity of the process. +// +// Notice: Not implemented yet. +func (p *Process) CPUAffinity() ([]int32, error) { + return p.CPUAffinityWithContext(context.Background()) +} + +func (p *Process) CPUAffinityWithContext(ctx context.Context) ([]int32, error) { + return nil, common.ErrNotImplementedError +} + +// MemoryInfo returns platform in-dependend memory information, such as RSS, VMS and Swap +func (p *Process) MemoryInfo() (*MemoryInfoStat, error) { + return p.MemoryInfoWithContext(context.Background()) +} + +func (p *Process) MemoryInfoWithContext(ctx context.Context) (*MemoryInfoStat, error) { + meminfo, _, err := p.fillFromStatmWithContext(ctx) + if err != nil { + return nil, err + } + return meminfo, nil +} + +// MemoryInfoEx returns platform dependend memory information. +func (p *Process) MemoryInfoEx() (*MemoryInfoExStat, error) { + return p.MemoryInfoExWithContext(context.Background()) +} + +func (p *Process) MemoryInfoExWithContext(ctx context.Context) (*MemoryInfoExStat, error) { + _, memInfoEx, err := p.fillFromStatmWithContext(ctx) + if err != nil { + return nil, err + } + return memInfoEx, nil +} + +// PageFaultsInfo returns the process's page fault counters +func (p *Process) PageFaults() (*PageFaultsStat, error) { + return p.PageFaultsWithContext(context.Background()) +} + +func (p *Process) PageFaultsWithContext(ctx context.Context) (*PageFaultsStat, error) { + _, _, _, _, _, _, pageFaults, err := p.fillFromStatWithContext(ctx) + if err != nil { + return nil, err + } + return pageFaults, nil +} + +// Children returns a slice of Process of the process. +func (p *Process) Children() ([]*Process, error) { + return p.ChildrenWithContext(context.Background()) +} + +func (p *Process) ChildrenWithContext(ctx context.Context) ([]*Process, error) { + pids, err := common.CallPgrepWithContext(ctx, invoke, p.Pid) + if err != nil { + if pids == nil || len(pids) == 0 { + return nil, ErrorNoChildren + } + return nil, err + } + ret := make([]*Process, 0, len(pids)) + for _, pid := range pids { + np, err := NewProcess(pid) + if err != nil { + return nil, err + } + ret = append(ret, np) + } + return ret, nil +} + +// OpenFiles returns a slice of OpenFilesStat opend by the process. +// OpenFilesStat includes a file path and file descriptor. +func (p *Process) OpenFiles() ([]OpenFilesStat, error) { + return p.OpenFilesWithContext(context.Background()) +} + +func (p *Process) OpenFilesWithContext(ctx context.Context) ([]OpenFilesStat, error) { + _, ofs, err := p.fillFromfdWithContext(ctx) + if err != nil { + return nil, err + } + ret := make([]OpenFilesStat, len(ofs)) + for i, o := range ofs { + ret[i] = *o + } + + return ret, nil +} + +// Connections returns a slice of net.ConnectionStat used by the process. +// This returns all kind of the connection. This measn TCP, UDP or UNIX. +func (p *Process) Connections() ([]net.ConnectionStat, error) { + return p.ConnectionsWithContext(context.Background()) +} + +func (p *Process) ConnectionsWithContext(ctx context.Context) ([]net.ConnectionStat, error) { + return net.ConnectionsPid("all", p.Pid) +} + +// Connections returns a slice of net.ConnectionStat used by the process at most `max` +func (p *Process) ConnectionsMax(max int) ([]net.ConnectionStat, error) { + return p.ConnectionsMaxWithContext(context.Background(), max) +} + +func (p *Process) ConnectionsMaxWithContext(ctx context.Context, max int) ([]net.ConnectionStat, error) { + return net.ConnectionsPidMax("all", p.Pid, max) +} + +// NetIOCounters returns NetIOCounters of the process. +func (p *Process) NetIOCounters(pernic bool) ([]net.IOCountersStat, error) { + return p.NetIOCountersWithContext(context.Background(), pernic) +} + +func (p *Process) NetIOCountersWithContext(ctx context.Context, pernic bool) ([]net.IOCountersStat, error) { + filename := common.HostProc(strconv.Itoa(int(p.Pid)), "net/dev") + return net.IOCountersByFile(pernic, filename) +} + +// MemoryMaps get memory maps from /proc/(pid)/smaps +func (p *Process) MemoryMaps(grouped bool) (*[]MemoryMapsStat, error) { + return p.MemoryMapsWithContext(context.Background(), grouped) +} + +func (p *Process) MemoryMapsWithContext(ctx context.Context, grouped bool) (*[]MemoryMapsStat, error) { + pid := p.Pid + var ret []MemoryMapsStat + if grouped { + ret = make([]MemoryMapsStat, 1) + } + smapsPath := common.HostProc(strconv.Itoa(int(pid)), "smaps") + contents, err := os.ReadFile(smapsPath) + if err != nil { + return nil, err + } + lines := strings.Split(string(contents), "\n") + + // function of parsing a block + getBlock := func(first_line, block []string) (MemoryMapsStat, error) { + m := MemoryMapsStat{} + m.Path = first_line[len(first_line)-1] + + for _, line := range block { + if strings.Contains(line, "VmFlags") { + continue + } + field := strings.Split(line, ":") + if len(field) < 2 { + continue + } + v := strings.Trim(field[1], "kB") // remove last "kB" + v = strings.TrimSpace(v) + t, err := strconv.ParseUint(v, 10, 64) + if err != nil { + return m, err + } + + switch field[0] { + case "Size": + m.Size = t + case "Rss": + m.Rss = t + case "Pss": + m.Pss = t + case "Shared_Clean": + m.SharedClean = t + case "Shared_Dirty": + m.SharedDirty = t + case "Private_Clean": + m.PrivateClean = t + case "Private_Dirty": + m.PrivateDirty = t + case "Referenced": + m.Referenced = t + case "Anonymous": + m.Anonymous = t + case "Swap": + m.Swap = t + } + } + return m, nil + } + + blocks := make([]string, 16) + for _, line := range lines { + fields := strings.Fields(line) + if len(fields) > 0 && !strings.HasSuffix(fields[0], ":") { + // new block section + if len(blocks) > 0 { + g, err := getBlock(fields, blocks) + if err != nil { + return &ret, err + } + if grouped { + ret[0].Size += g.Size + ret[0].Rss += g.Rss + ret[0].Pss += g.Pss + ret[0].SharedClean += g.SharedClean + ret[0].SharedDirty += g.SharedDirty + ret[0].PrivateClean += g.PrivateClean + ret[0].PrivateDirty += g.PrivateDirty + ret[0].Referenced += g.Referenced + ret[0].Anonymous += g.Anonymous + ret[0].Swap += g.Swap + } else { + ret = append(ret, g) + } + } + // starts new block + blocks = make([]string, 16) + } else { + blocks = append(blocks, line) + } + } + + return &ret, nil +} + +/** +** Internal functions +**/ + +func limitToInt(val string) (int32, error) { + if val == "unlimited" { + return math.MaxInt32, nil + } else { + res, err := strconv.ParseInt(val, 10, 32) + if err != nil { + return 0, err + } + return int32(res), nil + } +} + +// Get num_fds from /proc/(pid)/limits +func (p *Process) fillFromLimitsWithContext(ctx context.Context) ([]RlimitStat, error) { + pid := p.Pid + limitsFile := common.HostProc(strconv.Itoa(int(pid)), "limits") + d, err := os.Open(limitsFile) + if err != nil { + return nil, err + } + defer d.Close() + + var limitStats []RlimitStat + + limitsScanner := bufio.NewScanner(d) + for limitsScanner.Scan() { + var statItem RlimitStat + + str := strings.Fields(limitsScanner.Text()) + + // Remove the header line + if strings.Contains(str[len(str)-1], "Units") { + continue + } + + // Assert that last item is a Hard limit + statItem.Hard, err = limitToInt(str[len(str)-1]) + if err != nil { + // On error remove last item an try once again since it can be unit or header line + str = str[:len(str)-1] + statItem.Hard, err = limitToInt(str[len(str)-1]) + if err != nil { + return nil, err + } + } + // Remove last item from string + str = str[:len(str)-1] + + // Now last item is a Soft limit + statItem.Soft, err = limitToInt(str[len(str)-1]) + if err != nil { + return nil, err + } + // Remove last item from string + str = str[:len(str)-1] + + // The rest is a stats name + resourceName := strings.Join(str, " ") + switch resourceName { + case "Max cpu time": + statItem.Resource = RLIMIT_CPU + case "Max file size": + statItem.Resource = RLIMIT_FSIZE + case "Max data size": + statItem.Resource = RLIMIT_DATA + case "Max stack size": + statItem.Resource = RLIMIT_STACK + case "Max core file size": + statItem.Resource = RLIMIT_CORE + case "Max resident set": + statItem.Resource = RLIMIT_RSS + case "Max processes": + statItem.Resource = RLIMIT_NPROC + case "Max open files": + statItem.Resource = RLIMIT_NOFILE + case "Max locked memory": + statItem.Resource = RLIMIT_MEMLOCK + case "Max address space": + statItem.Resource = RLIMIT_AS + case "Max file locks": + statItem.Resource = RLIMIT_LOCKS + case "Max pending signals": + statItem.Resource = RLIMIT_SIGPENDING + case "Max msgqueue size": + statItem.Resource = RLIMIT_MSGQUEUE + case "Max nice priority": + statItem.Resource = RLIMIT_NICE + case "Max realtime priority": + statItem.Resource = RLIMIT_RTPRIO + case "Max realtime timeout": + statItem.Resource = RLIMIT_RTTIME + default: + continue + } + + limitStats = append(limitStats, statItem) + } + + if err := limitsScanner.Err(); err != nil { + return nil, err + } + + return limitStats, nil +} + +// Get list of /proc/(pid)/fd files +func (p *Process) fillFromfdListWithContext(ctx context.Context) (string, []string, error) { + pid := p.Pid + statPath := common.HostProc(strconv.Itoa(int(pid)), "fd") + d, err := os.Open(statPath) + if err != nil { + return statPath, []string{}, err + } + defer d.Close() + fnames, err := d.Readdirnames(-1) + return statPath, fnames, err +} + +// Get num_fds from /proc/(pid)/fd +func (p *Process) fillFromfdWithContext(ctx context.Context) (int32, []*OpenFilesStat, error) { + statPath, fnames, err := p.fillFromfdListWithContext(ctx) + if err != nil { + return 0, nil, err + } + numFDs := int32(len(fnames)) + + var openfiles []*OpenFilesStat + for _, fd := range fnames { + fpath := filepath.Join(statPath, fd) + filepath, err := os.Readlink(fpath) + if err != nil { + continue + } + t, err := strconv.ParseUint(fd, 10, 64) + if err != nil { + return numFDs, openfiles, err + } + o := &OpenFilesStat{ + Path: filepath, + Fd: t, + } + openfiles = append(openfiles, o) + } + + return numFDs, openfiles, nil +} + +// Get cwd from /proc/(pid)/cwd +func (p *Process) fillFromCwdWithContext(ctx context.Context) (string, error) { + pid := p.Pid + cwdPath := common.HostProc(strconv.Itoa(int(pid)), "cwd") + cwd, err := os.Readlink(cwdPath) + if err != nil { + return "", err + } + return string(cwd), nil +} + +// Get exe from /proc/(pid)/exe +func (p *Process) fillFromExeWithContext(ctx context.Context) (string, error) { + pid := p.Pid + exePath := common.HostProc(strconv.Itoa(int(pid)), "exe") + exe, err := os.Readlink(exePath) + if err != nil { + return "", err + } + return string(exe), nil +} + +// Get cmdline from /proc/(pid)/cmdline +func (p *Process) fillFromCmdlineWithContext(ctx context.Context) (string, error) { + pid := p.Pid + cmdPath := common.HostProc(strconv.Itoa(int(pid)), "cmdline") + cmdline, err := os.ReadFile(cmdPath) + if err != nil { + return "", err + } + ret := strings.FieldsFunc(string(cmdline), func(r rune) bool { + if r == '\u0000' { + return true + } + return false + }) + + return strings.Join(ret, " "), nil +} + +func (p *Process) fillSliceFromCmdlineWithContext(ctx context.Context) ([]string, error) { + pid := p.Pid + cmdPath := common.HostProc(strconv.Itoa(int(pid)), "cmdline") + cmdline, err := os.ReadFile(cmdPath) + if err != nil { + return nil, err + } + if len(cmdline) == 0 { + return nil, nil + } + if cmdline[len(cmdline)-1] == 0 { + cmdline = cmdline[:len(cmdline)-1] + } + parts := bytes.Split(cmdline, []byte{0}) + var strParts []string + for _, p := range parts { + strParts = append(strParts, string(p)) + } + + return strParts, nil +} + +// Get IO status from /proc/(pid)/io +func (p *Process) fillFromIOWithContext(ctx context.Context) (*IOCountersStat, error) { + pid := p.Pid + ioPath := common.HostProc(strconv.Itoa(int(pid)), "io") + ioline, err := os.ReadFile(ioPath) + if err != nil { + return nil, err + } + lines := strings.Split(string(ioline), "\n") + ret := &IOCountersStat{} + + for _, line := range lines { + field := strings.Fields(line) + if len(field) < 2 { + continue + } + t, err := strconv.ParseUint(field[1], 10, 64) + if err != nil { + return nil, err + } + param := field[0] + if strings.HasSuffix(param, ":") { + param = param[:len(param)-1] + } + switch param { + case "syscr": + ret.ReadCount = t + case "syscw": + ret.WriteCount = t + case "read_bytes": + ret.ReadBytes = t + case "write_bytes": + ret.WriteBytes = t + } + } + + return ret, nil +} + +// Get memory info from /proc/(pid)/statm +func (p *Process) fillFromStatmWithContext(ctx context.Context) (*MemoryInfoStat, *MemoryInfoExStat, error) { + pid := p.Pid + memPath := common.HostProc(strconv.Itoa(int(pid)), "statm") + contents, err := os.ReadFile(memPath) + if err != nil { + return nil, nil, err + } + fields := strings.Split(string(contents), " ") + + vms, err := strconv.ParseUint(fields[0], 10, 64) + if err != nil { + return nil, nil, err + } + rss, err := strconv.ParseUint(fields[1], 10, 64) + if err != nil { + return nil, nil, err + } + memInfo := &MemoryInfoStat{ + RSS: rss * PageSize, + VMS: vms * PageSize, + } + + shared, err := strconv.ParseUint(fields[2], 10, 64) + if err != nil { + return nil, nil, err + } + text, err := strconv.ParseUint(fields[3], 10, 64) + if err != nil { + return nil, nil, err + } + lib, err := strconv.ParseUint(fields[4], 10, 64) + if err != nil { + return nil, nil, err + } + dirty, err := strconv.ParseUint(fields[5], 10, 64) + if err != nil { + return nil, nil, err + } + + memInfoEx := &MemoryInfoExStat{ + RSS: rss * PageSize, + VMS: vms * PageSize, + Shared: shared * PageSize, + Text: text * PageSize, + Lib: lib * PageSize, + Dirty: dirty * PageSize, + } + + return memInfo, memInfoEx, nil +} + +// Get various status from /proc/(pid)/status +func (p *Process) fillFromStatusWithContext(ctx context.Context) error { + pid := p.Pid + statPath := common.HostProc(strconv.Itoa(int(pid)), "status") + contents, err := os.ReadFile(statPath) + if err != nil { + return err + } + lines := strings.Split(string(contents), "\n") + p.numCtxSwitches = &NumCtxSwitchesStat{} + p.memInfo = &MemoryInfoStat{} + p.sigInfo = &SignalInfoStat{} + for _, line := range lines { + tabParts := strings.SplitN(line, "\t", 2) + if len(tabParts) < 2 { + continue + } + value := tabParts[1] + switch strings.TrimRight(tabParts[0], ":") { + case "Name": + p.name = strings.Trim(value, " \t") + if len(p.name) >= 15 { + cmdlineSlice, err := p.CmdlineSlice() + if err != nil { + return err + } + if len(cmdlineSlice) > 0 { + extendedName := filepath.Base(cmdlineSlice[0]) + if strings.HasPrefix(extendedName, p.name) { + p.name = extendedName + } else { + p.name = cmdlineSlice[0] + } + } + } + case "State": + p.status = value[0:1] + case "PPid", "Ppid": + pval, err := strconv.ParseInt(value, 10, 32) + if err != nil { + return err + } + p.parent = int32(pval) + case "Tgid": + pval, err := strconv.ParseInt(value, 10, 32) + if err != nil { + return err + } + p.tgid = int32(pval) + case "Uid": + p.uids = make([]int32, 0, 4) + for _, i := range strings.Split(value, "\t") { + v, err := strconv.ParseInt(i, 10, 32) + if err != nil { + return err + } + p.uids = append(p.uids, int32(v)) + } + case "Gid": + p.gids = make([]int32, 0, 4) + for _, i := range strings.Split(value, "\t") { + v, err := strconv.ParseInt(i, 10, 32) + if err != nil { + return err + } + p.gids = append(p.gids, int32(v)) + } + case "Groups": + groups := strings.Fields(value) + p.groups = make([]int32, 0, len(groups)) + for _, i := range groups { + v, err := strconv.ParseInt(i, 10, 32) + if err != nil { + return err + } + p.groups = append(p.groups, int32(v)) + } + case "Threads": + v, err := strconv.ParseInt(value, 10, 32) + if err != nil { + return err + } + p.numThreads = int32(v) + case "voluntary_ctxt_switches": + v, err := strconv.ParseInt(value, 10, 64) + if err != nil { + return err + } + p.numCtxSwitches.Voluntary = v + case "nonvoluntary_ctxt_switches": + v, err := strconv.ParseInt(value, 10, 64) + if err != nil { + return err + } + p.numCtxSwitches.Involuntary = v + case "VmRSS": + value := strings.Trim(value, " kB") // remove last "kB" + v, err := strconv.ParseUint(value, 10, 64) + if err != nil { + return err + } + p.memInfo.RSS = v * 1024 + case "VmSize": + value := strings.Trim(value, " kB") // remove last "kB" + v, err := strconv.ParseUint(value, 10, 64) + if err != nil { + return err + } + p.memInfo.VMS = v * 1024 + case "VmSwap": + value := strings.Trim(value, " kB") // remove last "kB" + v, err := strconv.ParseUint(value, 10, 64) + if err != nil { + return err + } + p.memInfo.Swap = v * 1024 + case "VmHWM": + value := strings.Trim(value, " kB") // remove last "kB" + v, err := strconv.ParseUint(value, 10, 64) + if err != nil { + return err + } + p.memInfo.HWM = v * 1024 + case "VmData": + value := strings.Trim(value, " kB") // remove last "kB" + v, err := strconv.ParseUint(value, 10, 64) + if err != nil { + return err + } + p.memInfo.Data = v * 1024 + case "VmStk": + value := strings.Trim(value, " kB") // remove last "kB" + v, err := strconv.ParseUint(value, 10, 64) + if err != nil { + return err + } + p.memInfo.Stack = v * 1024 + case "VmLck": + value := strings.Trim(value, " kB") // remove last "kB" + v, err := strconv.ParseUint(value, 10, 64) + if err != nil { + return err + } + p.memInfo.Locked = v * 1024 + case "SigPnd": + v, err := strconv.ParseUint(value, 16, 64) + if err != nil { + return err + } + p.sigInfo.PendingThread = v + case "ShdPnd": + v, err := strconv.ParseUint(value, 16, 64) + if err != nil { + return err + } + p.sigInfo.PendingProcess = v + case "SigBlk": + v, err := strconv.ParseUint(value, 16, 64) + if err != nil { + return err + } + p.sigInfo.Blocked = v + case "SigIgn": + v, err := strconv.ParseUint(value, 16, 64) + if err != nil { + return err + } + p.sigInfo.Ignored = v + case "SigCgt": + v, err := strconv.ParseUint(value, 16, 64) + if err != nil { + return err + } + p.sigInfo.Caught = v + } + + } + return nil +} + +func (p *Process) fillFromTIDStatWithContext(ctx context.Context, tid int32) (uint64, int32, *cpu.TimesStat, int64, uint32, int32, *PageFaultsStat, error) { + pid := p.Pid + var statPath string + + if tid == -1 { + statPath = common.HostProc(strconv.Itoa(int(pid)), "stat") + } else { + statPath = common.HostProc(strconv.Itoa(int(pid)), "task", strconv.Itoa(int(tid)), "stat") + } + + contents, err := os.ReadFile(statPath) + if err != nil { + return 0, 0, nil, 0, 0, 0, nil, err + } + fields := strings.Fields(string(contents)) + + i := 1 + for !strings.HasSuffix(fields[i], ")") { + i++ + } + + terminal, err := strconv.ParseUint(fields[i+5], 10, 64) + if err != nil { + return 0, 0, nil, 0, 0, 0, nil, err + } + + ppid, err := strconv.ParseInt(fields[i+2], 10, 32) + if err != nil { + return 0, 0, nil, 0, 0, 0, nil, err + } + utime, err := strconv.ParseFloat(fields[i+12], 64) + if err != nil { + return 0, 0, nil, 0, 0, 0, nil, err + } + + stime, err := strconv.ParseFloat(fields[i+13], 64) + if err != nil { + return 0, 0, nil, 0, 0, 0, nil, err + } + + // There is no such thing as iotime in stat file. As an approximation, we + // will use delayacct_blkio_ticks (aggregated block I/O delays, as per Linux + // docs). Note: I am assuming at least Linux 2.6.18 + iotime, err := strconv.ParseFloat(fields[i+40], 64) + if err != nil { + iotime = 0 // Ancient linux version, most likely + } + + cpuTimes := &cpu.TimesStat{ + CPU: "cpu", + User: float64(utime / ClockTicks), + System: float64(stime / ClockTicks), + Iowait: float64(iotime / ClockTicks), + } + + bootTime, _ := common.BootTimeWithContext(ctx) + t, err := strconv.ParseUint(fields[i+20], 10, 64) + if err != nil { + return 0, 0, nil, 0, 0, 0, nil, err + } + ctime := (t / uint64(ClockTicks)) + uint64(bootTime) + createTime := int64(ctime * 1000) + + rtpriority, err := strconv.ParseInt(fields[i+16], 10, 32) + if err != nil { + return 0, 0, nil, 0, 0, 0, nil, err + } + if rtpriority < 0 { + rtpriority = rtpriority*-1 - 1 + } else { + rtpriority = 0 + } + + // p.Nice = mustParseInt32(fields[18]) + // use syscall instead of parse Stat file + snice, _ := unix.Getpriority(PrioProcess, int(pid)) + nice := int32(snice) // FIXME: is this true? + + minFault, err := strconv.ParseUint(fields[i+8], 10, 64) + if err != nil { + return 0, 0, nil, 0, 0, 0, nil, err + } + cMinFault, err := strconv.ParseUint(fields[i+9], 10, 64) + if err != nil { + return 0, 0, nil, 0, 0, 0, nil, err + } + majFault, err := strconv.ParseUint(fields[i+10], 10, 64) + if err != nil { + return 0, 0, nil, 0, 0, 0, nil, err + } + cMajFault, err := strconv.ParseUint(fields[i+11], 10, 64) + if err != nil { + return 0, 0, nil, 0, 0, 0, nil, err + } + + faults := &PageFaultsStat{ + MinorFaults: minFault, + MajorFaults: majFault, + ChildMinorFaults: cMinFault, + ChildMajorFaults: cMajFault, + } + + return terminal, int32(ppid), cpuTimes, createTime, uint32(rtpriority), nice, faults, nil +} + +func (p *Process) fillFromStatWithContext(ctx context.Context) (uint64, int32, *cpu.TimesStat, int64, uint32, int32, *PageFaultsStat, error) { + return p.fillFromTIDStatWithContext(ctx, -1) +} + +func pidsWithContext(ctx context.Context) ([]int32, error) { + return readPidsFromDir(common.HostProc()) +} + +// Process returns a slice of pointers to Process structs for all +// currently running processes. +func Processes() ([]*Process, error) { + return ProcessesWithContext(context.Background()) +} + +func ProcessesWithContext(ctx context.Context) ([]*Process, error) { + out := []*Process{} + + pids, err := PidsWithContext(ctx) + if err != nil { + return out, err + } + + for _, pid := range pids { + p, err := NewProcess(pid) + if err != nil { + continue + } + out = append(out, p) + } + + return out, nil +} + +func readPidsFromDir(path string) ([]int32, error) { + var ret []int32 + + d, err := os.Open(path) + if err != nil { + return nil, err + } + defer d.Close() + + fnames, err := d.Readdirnames(-1) + if err != nil { + return nil, err + } + for _, fname := range fnames { + pid, err := strconv.ParseInt(fname, 10, 32) + if err != nil { + // if not numeric name, just skip + continue + } + ret = append(ret, int32(pid)) + } + + return ret, nil +} diff --git a/internal/gopsutil/process/process_openbsd.go b/internal/gopsutil/process/process_openbsd.go new file mode 100644 index 0000000..08be6bd --- /dev/null +++ b/internal/gopsutil/process/process_openbsd.go @@ -0,0 +1,570 @@ +//go:build openbsd + +package process + +import ( + "C" + "bytes" + "context" + "encoding/binary" + "os/exec" + "path/filepath" + "strconv" + "strings" + "unsafe" + + "golang.org/x/sys/unix" + + "github.com/gofiber/fiber/v2/internal/gopsutil/common" + cpu "github.com/gofiber/fiber/v2/internal/gopsutil/cpu" + mem "github.com/gofiber/fiber/v2/internal/gopsutil/mem" + net "github.com/gofiber/fiber/v2/internal/gopsutil/net" +) + +// MemoryInfoExStat is different between OSes +type MemoryInfoExStat struct{} + +type MemoryMapsStat struct{} + +func pidsWithContext(ctx context.Context) ([]int32, error) { + var ret []int32 + procs, err := Processes() + if err != nil { + return ret, nil + } + + for _, p := range procs { + ret = append(ret, p.Pid) + } + + return ret, nil +} + +func (p *Process) Ppid() (int32, error) { + return p.PpidWithContext(context.Background()) +} + +func (p *Process) PpidWithContext(ctx context.Context) (int32, error) { + k, err := p.getKProc() + if err != nil { + return 0, err + } + + return k.Ppid, nil +} + +func (p *Process) Name() (string, error) { + return p.NameWithContext(context.Background()) +} + +func (p *Process) NameWithContext(ctx context.Context) (string, error) { + k, err := p.getKProc() + if err != nil { + return "", err + } + name := common.IntToString(k.Comm[:]) + + if len(name) >= 15 { + cmdlineSlice, err := p.CmdlineSliceWithContext(ctx) + if err != nil { + return "", err + } + if len(cmdlineSlice) > 0 { + extendedName := filepath.Base(cmdlineSlice[0]) + if strings.HasPrefix(extendedName, p.name) { + name = extendedName + } else { + name = cmdlineSlice[0] + } + } + } + + return name, nil +} + +func (p *Process) Tgid() (int32, error) { + return 0, common.ErrNotImplementedError +} + +func (p *Process) Exe() (string, error) { + return p.ExeWithContext(context.Background()) +} + +func (p *Process) ExeWithContext(ctx context.Context) (string, error) { + return "", common.ErrNotImplementedError +} + +func (p *Process) CmdlineSlice() ([]string, error) { + return p.CmdlineSliceWithContext(context.Background()) +} + +func (p *Process) CmdlineSliceWithContext(ctx context.Context) ([]string, error) { + mib := []int32{CTLKern, KernProcArgs, p.Pid, KernProcArgv} + buf, _, err := common.CallSyscall(mib) + if err != nil { + return nil, err + } + + argc := 0 + argvp := unsafe.Pointer(&buf[0]) + argv := *(**C.char)(unsafe.Pointer(argvp)) + size := unsafe.Sizeof(argv) + var strParts []string + + for argv != nil { + strParts = append(strParts, C.GoString(argv)) + + argc++ + argv = *(**C.char)(unsafe.Pointer(uintptr(argvp) + uintptr(argc)*size)) + } + return strParts, nil +} + +func (p *Process) Cmdline() (string, error) { + return p.CmdlineWithContext(context.Background()) +} + +func (p *Process) CmdlineWithContext(ctx context.Context) (string, error) { + argv, err := p.CmdlineSlice() + if err != nil { + return "", err + } + return strings.Join(argv, " "), nil +} + +func (p *Process) createTimeWithContext(ctx context.Context) (int64, error) { + return 0, common.ErrNotImplementedError +} + +func (p *Process) Cwd() (string, error) { + return p.CwdWithContext(context.Background()) +} + +func (p *Process) CwdWithContext(ctx context.Context) (string, error) { + return "", common.ErrNotImplementedError +} + +func (p *Process) Parent() (*Process, error) { + return p.ParentWithContext(context.Background()) +} + +func (p *Process) ParentWithContext(ctx context.Context) (*Process, error) { + return p, common.ErrNotImplementedError +} + +func (p *Process) Status() (string, error) { + return p.StatusWithContext(context.Background()) +} + +func (p *Process) StatusWithContext(ctx context.Context) (string, error) { + k, err := p.getKProc() + if err != nil { + return "", err + } + var s string + switch k.Stat { + case SIDL: + case SRUN: + case SONPROC: + s = "R" + case SSLEEP: + s = "S" + case SSTOP: + s = "T" + case SDEAD: + s = "Z" + } + + return s, nil +} + +func (p *Process) Foreground() (bool, error) { + return p.ForegroundWithContext(context.Background()) +} + +func (p *Process) ForegroundWithContext(ctx context.Context) (bool, error) { + // see https://github.com/shirou/gopsutil/issues/596#issuecomment-432707831 for implementation details + pid := p.Pid + ps, err := exec.LookPath("ps") + if err != nil { + return false, err + } + out, err := invoke.CommandWithContext(ctx, ps, "-o", "stat=", "-p", strconv.Itoa(int(pid))) + if err != nil { + return false, err + } + return strings.IndexByte(string(out), '+') != -1, nil +} + +func (p *Process) Uids() ([]int32, error) { + return p.UidsWithContext(context.Background()) +} + +func (p *Process) UidsWithContext(ctx context.Context) ([]int32, error) { + k, err := p.getKProc() + if err != nil { + return nil, err + } + + uids := make([]int32, 0, 3) + + uids = append(uids, int32(k.Ruid), int32(k.Uid), int32(k.Svuid)) + + return uids, nil +} + +func (p *Process) Gids() ([]int32, error) { + return p.GidsWithContext(context.Background()) +} + +func (p *Process) GidsWithContext(ctx context.Context) ([]int32, error) { + k, err := p.getKProc() + if err != nil { + return nil, err + } + + gids := make([]int32, 0, 3) + gids = append(gids, int32(k.Rgid), int32(k.Ngroups), int32(k.Svgid)) + + return gids, nil +} + +func (p *Process) GroupsWithContext(ctx context.Context) ([]int32, error) { + k, err := p.getKProc() + if err != nil { + return nil, err + } + + return k.Groups, nil +} + +func (p *Process) Terminal() (string, error) { + return p.TerminalWithContext(context.Background()) +} + +func (p *Process) TerminalWithContext(ctx context.Context) (string, error) { + k, err := p.getKProc() + if err != nil { + return "", err + } + + ttyNr := uint64(k.Tdev) + + termmap, err := getTerminalMap() + if err != nil { + return "", err + } + + return termmap[ttyNr], nil +} + +func (p *Process) Nice() (int32, error) { + return p.NiceWithContext(context.Background()) +} + +func (p *Process) NiceWithContext(ctx context.Context) (int32, error) { + k, err := p.getKProc() + if err != nil { + return 0, err + } + return int32(k.Nice), nil +} + +func (p *Process) IOnice() (int32, error) { + return p.IOniceWithContext(context.Background()) +} + +func (p *Process) IOniceWithContext(ctx context.Context) (int32, error) { + return 0, common.ErrNotImplementedError +} + +func (p *Process) Rlimit() ([]RlimitStat, error) { + return p.RlimitWithContext(context.Background()) +} + +func (p *Process) RlimitWithContext(ctx context.Context) ([]RlimitStat, error) { + var rlimit []RlimitStat + return rlimit, common.ErrNotImplementedError +} + +func (p *Process) RlimitUsage(gatherUsed bool) ([]RlimitStat, error) { + return p.RlimitUsageWithContext(context.Background(), gatherUsed) +} + +func (p *Process) RlimitUsageWithContext(ctx context.Context, gatherUsed bool) ([]RlimitStat, error) { + var rlimit []RlimitStat + return rlimit, common.ErrNotImplementedError +} + +func (p *Process) IOCounters() (*IOCountersStat, error) { + return p.IOCountersWithContext(context.Background()) +} + +func (p *Process) IOCountersWithContext(ctx context.Context) (*IOCountersStat, error) { + k, err := p.getKProc() + if err != nil { + return nil, err + } + return &IOCountersStat{ + ReadCount: uint64(k.Uru_inblock), + WriteCount: uint64(k.Uru_oublock), + }, nil +} + +func (p *Process) NumCtxSwitches() (*NumCtxSwitchesStat, error) { + return p.NumCtxSwitchesWithContext(context.Background()) +} + +func (p *Process) NumCtxSwitchesWithContext(ctx context.Context) (*NumCtxSwitchesStat, error) { + return nil, common.ErrNotImplementedError +} + +func (p *Process) NumFDs() (int32, error) { + return p.NumFDsWithContext(context.Background()) +} + +func (p *Process) NumFDsWithContext(ctx context.Context) (int32, error) { + return 0, common.ErrNotImplementedError +} + +func (p *Process) NumThreads() (int32, error) { + return p.NumThreadsWithContext(context.Background()) +} + +func (p *Process) NumThreadsWithContext(ctx context.Context) (int32, error) { + /* not supported, just return 1 */ + return 1, nil +} + +func (p *Process) Threads() (map[int32]*cpu.TimesStat, error) { + return p.ThreadsWithContext(context.Background()) +} + +func (p *Process) ThreadsWithContext(ctx context.Context) (map[int32]*cpu.TimesStat, error) { + ret := make(map[int32]*cpu.TimesStat) + return ret, common.ErrNotImplementedError +} + +func (p *Process) Times() (*cpu.TimesStat, error) { + return p.TimesWithContext(context.Background()) +} + +func (p *Process) TimesWithContext(ctx context.Context) (*cpu.TimesStat, error) { + k, err := p.getKProc() + if err != nil { + return nil, err + } + return &cpu.TimesStat{ + CPU: "cpu", + User: float64(k.Uutime_sec) + float64(k.Uutime_usec)/1000000, + System: float64(k.Ustime_sec) + float64(k.Ustime_usec)/1000000, + }, nil +} + +func (p *Process) CPUAffinity() ([]int32, error) { + return p.CPUAffinityWithContext(context.Background()) +} + +func (p *Process) CPUAffinityWithContext(ctx context.Context) ([]int32, error) { + return nil, common.ErrNotImplementedError +} + +func (p *Process) MemoryInfo() (*MemoryInfoStat, error) { + return p.MemoryInfoWithContext(context.Background()) +} + +func (p *Process) MemoryInfoWithContext(ctx context.Context) (*MemoryInfoStat, error) { + k, err := p.getKProc() + if err != nil { + return nil, err + } + pageSize, err := mem.GetPageSize() + if err != nil { + return nil, err + } + + return &MemoryInfoStat{ + RSS: uint64(k.Vm_rssize) * pageSize, + VMS: uint64(k.Vm_tsize) + uint64(k.Vm_dsize) + + uint64(k.Vm_ssize), + }, nil +} + +func (p *Process) MemoryInfoEx() (*MemoryInfoExStat, error) { + return p.MemoryInfoExWithContext(context.Background()) +} + +func (p *Process) MemoryInfoExWithContext(ctx context.Context) (*MemoryInfoExStat, error) { + return nil, common.ErrNotImplementedError +} + +func (p *Process) PageFaults() (*PageFaultsStat, error) { + return p.PageFaultsWithContext(context.Background()) +} + +func (p *Process) PageFaultsWithContext(ctx context.Context) (*PageFaultsStat, error) { + return nil, common.ErrNotImplementedError +} + +func (p *Process) Children() ([]*Process, error) { + return p.ChildrenWithContext(context.Background()) +} + +func (p *Process) ChildrenWithContext(ctx context.Context) ([]*Process, error) { + pids, err := common.CallPgrepWithContext(ctx, invoke, p.Pid) + if err != nil { + return nil, err + } + ret := make([]*Process, 0, len(pids)) + for _, pid := range pids { + np, err := NewProcess(pid) + if err != nil { + return nil, err + } + ret = append(ret, np) + } + return ret, nil +} + +func (p *Process) OpenFiles() ([]OpenFilesStat, error) { + return p.OpenFilesWithContext(context.Background()) +} + +func (p *Process) OpenFilesWithContext(ctx context.Context) ([]OpenFilesStat, error) { + return nil, common.ErrNotImplementedError +} + +func (p *Process) Connections() ([]net.ConnectionStat, error) { + return p.ConnectionsWithContext(context.Background()) +} + +func (p *Process) ConnectionsWithContext(ctx context.Context) ([]net.ConnectionStat, error) { + return nil, common.ErrNotImplementedError +} + +func (p *Process) ConnectionsMax(max int) ([]net.ConnectionStat, error) { + return p.ConnectionsMaxWithContext(context.Background(), max) +} + +func (p *Process) ConnectionsMaxWithContext(ctx context.Context, max int) ([]net.ConnectionStat, error) { + return []net.ConnectionStat{}, common.ErrNotImplementedError +} + +func (p *Process) NetIOCounters(pernic bool) ([]net.IOCountersStat, error) { + return p.NetIOCountersWithContext(context.Background(), pernic) +} + +func (p *Process) NetIOCountersWithContext(ctx context.Context, pernic bool) ([]net.IOCountersStat, error) { + return nil, common.ErrNotImplementedError +} + +func (p *Process) MemoryMaps(grouped bool) (*[]MemoryMapsStat, error) { + return p.MemoryMapsWithContext(context.Background(), grouped) +} + +func (p *Process) MemoryMapsWithContext(ctx context.Context, grouped bool) (*[]MemoryMapsStat, error) { + var ret []MemoryMapsStat + return &ret, common.ErrNotImplementedError +} + +func Processes() ([]*Process, error) { + return ProcessesWithContext(context.Background()) +} + +func ProcessesWithContext(ctx context.Context) ([]*Process, error) { + results := []*Process{} + + buf, length, err := CallKernProcSyscall(KernProcAll, 0) + if err != nil { + return results, err + } + + // get kinfo_proc size + count := int(length / uint64(sizeOfKinfoProc)) + + // parse buf to procs + for i := 0; i < count; i++ { + b := buf[i*sizeOfKinfoProc : (i+1)*sizeOfKinfoProc] + k, err := parseKinfoProc(b) + if err != nil { + continue + } + p, err := NewProcess(int32(k.Pid)) + if err != nil { + continue + } + + results = append(results, p) + } + + return results, nil +} + +func parseKinfoProc(buf []byte) (KinfoProc, error) { + var k KinfoProc + br := bytes.NewReader(buf) + err := common.Read(br, binary.LittleEndian, &k) + return k, err +} + +func (p *Process) getKProc() (*KinfoProc, error) { + return p.getKProcWithContext(context.Background()) +} + +func (p *Process) getKProcWithContext(ctx context.Context) (*KinfoProc, error) { + buf, length, err := CallKernProcSyscall(KernProcPID, p.Pid) + if err != nil { + return nil, err + } + if length != sizeOfKinfoProc { + return nil, err + } + + k, err := parseKinfoProc(buf) + if err != nil { + return nil, err + } + return &k, nil +} + +func CallKernProcSyscall(op, arg int32) ([]byte, uint64, error) { + return CallKernProcSyscallWithContext(context.Background(), op, arg) +} + +func CallKernProcSyscallWithContext(ctx context.Context, op, arg int32) ([]byte, uint64, error) { + mib := []int32{CTLKern, KernProc, op, arg, sizeOfKinfoProc, 0} + mibptr := unsafe.Pointer(&mib[0]) + miblen := uint64(len(mib)) + length := uint64(0) + _, _, err := unix.Syscall6( + unix.SYS___SYSCTL, + uintptr(mibptr), + uintptr(miblen), + 0, + uintptr(unsafe.Pointer(&length)), + 0, + 0) + if err != 0 { + return nil, length, err + } + + count := int32(length / uint64(sizeOfKinfoProc)) + mib = []int32{CTLKern, KernProc, op, arg, sizeOfKinfoProc, count} + mibptr = unsafe.Pointer(&mib[0]) + miblen = uint64(len(mib)) + // get proc info itself + buf := make([]byte, length) + _, _, err = unix.Syscall6( + unix.SYS___SYSCTL, + uintptr(mibptr), + uintptr(miblen), + uintptr(unsafe.Pointer(&buf[0])), + uintptr(unsafe.Pointer(&length)), + 0, + 0) + if err != 0 { + return buf, length, err + } + + return buf, length, nil +} diff --git a/internal/gopsutil/process/process_openbsd_386.go b/internal/gopsutil/process/process_openbsd_386.go new file mode 100644 index 0000000..7ac9067 --- /dev/null +++ b/internal/gopsutil/process/process_openbsd_386.go @@ -0,0 +1,201 @@ +//go:build openbsd && 386 + +// Code generated by cmd/cgo -godefs; DO NOT EDIT. +// cgo -godefs process/types_openbsd.go + +package process + +const ( + CTLKern = 1 + KernProc = 66 + KernProcAll = 0 + KernProcPID = 1 + KernProcProc = 8 + KernProcPathname = 12 + KernProcArgs = 55 + KernProcArgv = 1 + KernProcEnv = 3 +) + +const ( + ArgMax = 256 * 1024 +) + +const ( + sizeofPtr = 0x4 + sizeofShort = 0x2 + sizeofInt = 0x4 + sizeofLong = 0x4 + sizeofLongLong = 0x8 +) + +const ( + sizeOfKinfoVmentry = 0x38 + sizeOfKinfoProc = 0x264 +) + +const ( + SIDL = 1 + SRUN = 2 + SSLEEP = 3 + SSTOP = 4 + SZOMB = 5 + SDEAD = 6 + SONPROC = 7 +) + +type ( + _C_short int16 + _C_int int32 + _C_long int32 + _C_long_long int64 +) + +type Timespec struct { + Sec int64 + Nsec int32 +} + +type Timeval struct { + Sec int64 + Usec int32 +} + +type Rusage struct { + Utime Timeval + Stime Timeval + Maxrss int32 + Ixrss int32 + Idrss int32 + Isrss int32 + Minflt int32 + Majflt int32 + Nswap int32 + Inblock int32 + Oublock int32 + Msgsnd int32 + Msgrcv int32 + Nsignals int32 + Nvcsw int32 + Nivcsw int32 +} + +type Rlimit struct { + Cur uint64 + Max uint64 +} + +type KinfoProc struct { + Forw uint64 + Back uint64 + Paddr uint64 + Addr uint64 + Fd uint64 + Stats uint64 + Limit uint64 + Vmspace uint64 + Sigacts uint64 + Sess uint64 + Tsess uint64 + Ru uint64 + Eflag int32 + Exitsig int32 + Flag int32 + Pid int32 + Ppid int32 + Sid int32 + X_pgid int32 + Tpgid int32 + Uid uint32 + Ruid uint32 + Gid uint32 + Rgid uint32 + Groups [16]uint32 + Ngroups int16 + Jobc int16 + Tdev uint32 + Estcpu uint32 + Rtime_sec uint32 + Rtime_usec uint32 + Cpticks int32 + Pctcpu uint32 + Swtime uint32 + Slptime uint32 + Schedflags int32 + Uticks uint64 + Sticks uint64 + Iticks uint64 + Tracep uint64 + Traceflag int32 + Holdcnt int32 + Siglist int32 + Sigmask uint32 + Sigignore uint32 + Sigcatch uint32 + Stat int8 + Priority uint8 + Usrpri uint8 + Nice uint8 + Xstat uint16 + Acflag uint16 + Comm [24]int8 + Wmesg [8]int8 + Wchan uint64 + Login [32]int8 + Vm_rssize int32 + Vm_tsize int32 + Vm_dsize int32 + Vm_ssize int32 + Uvalid int64 + Ustart_sec uint64 + Ustart_usec uint32 + Uutime_sec uint32 + Uutime_usec uint32 + Ustime_sec uint32 + Ustime_usec uint32 + Uru_maxrss uint64 + Uru_ixrss uint64 + Uru_idrss uint64 + Uru_isrss uint64 + Uru_minflt uint64 + Uru_majflt uint64 + Uru_nswap uint64 + Uru_inblock uint64 + Uru_oublock uint64 + Uru_msgsnd uint64 + Uru_msgrcv uint64 + Uru_nsignals uint64 + Uru_nvcsw uint64 + Uru_nivcsw uint64 + Uctime_sec uint32 + Uctime_usec uint32 + Psflags int32 + Spare int32 + Svuid uint32 + Svgid uint32 + Emul [8]int8 + Rlim_rss_cur uint64 + Cpuid uint64 + Vm_map_size uint64 + Tid int32 + Rtableid uint32 +} + +type Priority struct{} + +type KinfoVmentry struct { + Start uint32 + End uint32 + Guard uint32 + Fspace uint32 + Fspace_augment uint32 + Offset uint64 + Wired_count int32 + Etype int32 + Protection int32 + Max_protection int32 + Advice int32 + Inheritance int32 + Flags uint8 + Pad_cgo_0 [3]byte +} diff --git a/internal/gopsutil/process/process_openbsd_amd64.go b/internal/gopsutil/process/process_openbsd_amd64.go new file mode 100644 index 0000000..8607422 --- /dev/null +++ b/internal/gopsutil/process/process_openbsd_amd64.go @@ -0,0 +1,200 @@ +// Created by cgo -godefs - DO NOT EDIT +// cgo -godefs types_openbsd.go + +package process + +const ( + CTLKern = 1 + KernProc = 66 + KernProcAll = 0 + KernProcPID = 1 + KernProcProc = 8 + KernProcPathname = 12 + KernProcArgs = 55 + KernProcArgv = 1 + KernProcEnv = 3 +) + +const ( + ArgMax = 256 * 1024 +) + +const ( + sizeofPtr = 0x8 + sizeofShort = 0x2 + sizeofInt = 0x4 + sizeofLong = 0x8 + sizeofLongLong = 0x8 +) + +const ( + sizeOfKinfoVmentry = 0x50 + sizeOfKinfoProc = 0x268 +) + +const ( + SIDL = 1 + SRUN = 2 + SSLEEP = 3 + SSTOP = 4 + SZOMB = 5 + SDEAD = 6 + SONPROC = 7 +) + +type ( + _C_short int16 + _C_int int32 + _C_long int64 + _C_long_long int64 +) + +type Timespec struct { + Sec int64 + Nsec int64 +} + +type Timeval struct { + Sec int64 + Usec int64 +} + +type Rusage struct { + Utime Timeval + Stime Timeval + Maxrss int64 + Ixrss int64 + Idrss int64 + Isrss int64 + Minflt int64 + Majflt int64 + Nswap int64 + Inblock int64 + Oublock int64 + Msgsnd int64 + Msgrcv int64 + Nsignals int64 + Nvcsw int64 + Nivcsw int64 +} + +type Rlimit struct { + Cur uint64 + Max uint64 +} + +type KinfoProc struct { + Forw uint64 + Back uint64 + Paddr uint64 + Addr uint64 + Fd uint64 + Stats uint64 + Limit uint64 + Vmspace uint64 + Sigacts uint64 + Sess uint64 + Tsess uint64 + Ru uint64 + Eflag int32 + Exitsig int32 + Flag int32 + Pid int32 + Ppid int32 + Sid int32 + X_pgid int32 + Tpgid int32 + Uid uint32 + Ruid uint32 + Gid uint32 + Rgid uint32 + Groups [16]uint32 + Ngroups int16 + Jobc int16 + Tdev uint32 + Estcpu uint32 + Rtime_sec uint32 + Rtime_usec uint32 + Cpticks int32 + Pctcpu uint32 + Swtime uint32 + Slptime uint32 + Schedflags int32 + Uticks uint64 + Sticks uint64 + Iticks uint64 + Tracep uint64 + Traceflag int32 + Holdcnt int32 + Siglist int32 + Sigmask uint32 + Sigignore uint32 + Sigcatch uint32 + Stat int8 + Priority uint8 + Usrpri uint8 + Nice uint8 + Xstat uint16 + Acflag uint16 + Comm [24]int8 + Wmesg [8]int8 + Wchan uint64 + Login [32]int8 + Vm_rssize int32 + Vm_tsize int32 + Vm_dsize int32 + Vm_ssize int32 + Uvalid int64 + Ustart_sec uint64 + Ustart_usec uint32 + Uutime_sec uint32 + Uutime_usec uint32 + Ustime_sec uint32 + Ustime_usec uint32 + Pad_cgo_0 [4]byte + Uru_maxrss uint64 + Uru_ixrss uint64 + Uru_idrss uint64 + Uru_isrss uint64 + Uru_minflt uint64 + Uru_majflt uint64 + Uru_nswap uint64 + Uru_inblock uint64 + Uru_oublock uint64 + Uru_msgsnd uint64 + Uru_msgrcv uint64 + Uru_nsignals uint64 + Uru_nvcsw uint64 + Uru_nivcsw uint64 + Uctime_sec uint32 + Uctime_usec uint32 + Psflags int32 + Spare int32 + Svuid uint32 + Svgid uint32 + Emul [8]int8 + Rlim_rss_cur uint64 + Cpuid uint64 + Vm_map_size uint64 + Tid int32 + Rtableid uint32 +} + +type Priority struct{} + +type KinfoVmentry struct { + Start uint64 + End uint64 + Guard uint64 + Fspace uint64 + Fspace_augment uint64 + Offset uint64 + Wired_count int32 + Etype int32 + Protection int32 + Max_protection int32 + Advice int32 + Inheritance int32 + Flags uint8 + Pad_cgo_0 [7]byte +} diff --git a/internal/gopsutil/process/process_posix.go b/internal/gopsutil/process/process_posix.go new file mode 100644 index 0000000..6701fa8 --- /dev/null +++ b/internal/gopsutil/process/process_posix.go @@ -0,0 +1,194 @@ +//go:build linux || freebsd || openbsd || darwin + +package process + +import ( + "context" + "errors" + "fmt" + "io/fs" + "os" + "os/user" + "path/filepath" + "strconv" + "strings" + "syscall" + + "golang.org/x/sys/unix" + + "github.com/gofiber/fiber/v2/internal/gopsutil/common" +) + +// POSIX +func getTerminalMap() (map[uint64]string, error) { + ret := make(map[uint64]string) + var termfiles []string + + d, err := os.Open("/dev") + if err != nil { + return nil, err + } + defer d.Close() + + devnames, err := d.Readdirnames(-1) + if err != nil { + return nil, err + } + for _, devname := range devnames { + if strings.HasPrefix(devname, "/dev/tty") { + termfiles = append(termfiles, "/dev/tty/"+devname) + } + } + + var ptsnames []string + ptsd, err := os.Open("/dev/pts") + if err != nil { + ptsnames, _ = filepath.Glob("/dev/ttyp*") + if ptsnames == nil { + return nil, err + } + } + defer ptsd.Close() + + if ptsnames == nil { + defer ptsd.Close() + ptsnames, err = ptsd.Readdirnames(-1) + if err != nil { + return nil, err + } + for _, ptsname := range ptsnames { + termfiles = append(termfiles, "/dev/pts/"+ptsname) + } + } else { + termfiles = ptsnames + } + + for _, name := range termfiles { + stat := unix.Stat_t{} + if err = unix.Stat(name, &stat); err != nil { + return nil, err + } + rdev := uint64(stat.Rdev) + ret[rdev] = strings.Replace(name, "/dev", "", -1) + } + return ret, nil +} + +func PidExistsWithContext(ctx context.Context, pid int32) (bool, error) { + if pid <= 0 { + return false, fmt.Errorf("invalid pid %v", pid) + } + proc, err := os.FindProcess(int(pid)) + if err != nil { + return false, err + } + + if _, err := os.Stat(common.HostProc()); err == nil { // Means that proc filesystem exist + // Checking PID existence based on existence of //proc/ folder + // This covers the case when running inside container with a different process namespace (by default) + + _, err := os.Stat(common.HostProc(strconv.Itoa(int(pid)))) + if errors.Is(err, fs.ErrNotExist) { + return false, nil + } + return err == nil, err + } + + //'/proc' filesystem is not exist, checking of PID existence is done via signalling the process + //Make sense only if we run in the same process namespace + err = proc.Signal(syscall.Signal(0)) + if err == nil { + return true, nil + } + if err.Error() == "os: process already finished" { + return false, nil + } + errno, ok := err.(syscall.Errno) + if !ok { + return false, err + } + switch errno { + case syscall.ESRCH: + return false, nil + case syscall.EPERM: + return true, nil + } + + return false, err +} + +// SendSignal sends a unix.Signal to the process. +// Currently, SIGSTOP, SIGCONT, SIGTERM and SIGKILL are supported. +func (p *Process) SendSignal(sig syscall.Signal) error { + return p.SendSignalWithContext(context.Background(), sig) +} + +func (p *Process) SendSignalWithContext(ctx context.Context, sig syscall.Signal) error { + process, err := os.FindProcess(int(p.Pid)) + if err != nil { + return err + } + + err = process.Signal(sig) + if err != nil { + return err + } + + return nil +} + +// Suspend sends SIGSTOP to the process. +func (p *Process) Suspend() error { + return p.SuspendWithContext(context.Background()) +} + +func (p *Process) SuspendWithContext(ctx context.Context) error { + return p.SendSignal(unix.SIGSTOP) +} + +// Resume sends SIGCONT to the process. +func (p *Process) Resume() error { + return p.ResumeWithContext(context.Background()) +} + +func (p *Process) ResumeWithContext(ctx context.Context) error { + return p.SendSignal(unix.SIGCONT) +} + +// Terminate sends SIGTERM to the process. +func (p *Process) Terminate() error { + return p.TerminateWithContext(context.Background()) +} + +func (p *Process) TerminateWithContext(ctx context.Context) error { + return p.SendSignal(unix.SIGTERM) +} + +// Kill sends SIGKILL to the process. +func (p *Process) Kill() error { + return p.KillWithContext(context.Background()) +} + +func (p *Process) KillWithContext(ctx context.Context) error { + return p.SendSignal(unix.SIGKILL) +} + +// Username returns a username of the process. +func (p *Process) Username() (string, error) { + return p.UsernameWithContext(context.Background()) +} + +func (p *Process) UsernameWithContext(ctx context.Context) (string, error) { + uids, err := p.Uids() + if err != nil { + return "", err + } + if len(uids) > 0 { + u, err := user.LookupId(strconv.Itoa(int(uids[0]))) + if err != nil { + return "", err + } + return u.Username, nil + } + return "", nil +} diff --git a/internal/gopsutil/process/process_windows.go b/internal/gopsutil/process/process_windows.go new file mode 100644 index 0000000..57f4cb6 --- /dev/null +++ b/internal/gopsutil/process/process_windows.go @@ -0,0 +1,1024 @@ +//go:build windows + +package process + +import ( + "context" + "errors" + "fmt" + "os" + "strings" + "syscall" + "unsafe" + + "golang.org/x/sys/windows" + + "github.com/gofiber/fiber/v2/internal/gopsutil/common" + cpu "github.com/gofiber/fiber/v2/internal/gopsutil/cpu" + net "github.com/gofiber/fiber/v2/internal/gopsutil/net" +) + +var ( + modntdll = windows.NewLazySystemDLL("ntdll.dll") + procNtResumeProcess = modntdll.NewProc("NtResumeProcess") + procNtSuspendProcess = modntdll.NewProc("NtSuspendProcess") + + modpsapi = windows.NewLazySystemDLL("psapi.dll") + procGetProcessMemoryInfo = modpsapi.NewProc("GetProcessMemoryInfo") + procGetProcessImageFileNameW = modpsapi.NewProc("GetProcessImageFileNameW") + + advapi32 = windows.NewLazySystemDLL("advapi32.dll") + procLookupPrivilegeValue = advapi32.NewProc("LookupPrivilegeValueW") + procAdjustTokenPrivileges = advapi32.NewProc("AdjustTokenPrivileges") + + procQueryFullProcessImageNameW = common.Modkernel32.NewProc("QueryFullProcessImageNameW") + procGetPriorityClass = common.Modkernel32.NewProc("GetPriorityClass") + procGetProcessIoCounters = common.Modkernel32.NewProc("GetProcessIoCounters") + procGetNativeSystemInfo = common.Modkernel32.NewProc("GetNativeSystemInfo") + + processorArchitecture uint +) + +type SystemProcessInformation struct { + NextEntryOffset uint64 + NumberOfThreads uint64 + Reserved1 [48]byte + Reserved2 [3]byte + UniqueProcessID uintptr + Reserved3 uintptr + HandleCount uint64 + Reserved4 [4]byte + Reserved5 [11]byte + PeakPagefileUsage uint64 + PrivatePageCount uint64 + Reserved6 [6]uint64 +} + +type systemProcessorInformation struct { + ProcessorArchitecture uint16 + ProcessorLevel uint16 + ProcessorRevision uint16 + Reserved uint16 + ProcessorFeatureBits uint16 +} + +type systemInfo struct { + wProcessorArchitecture uint16 + wReserved uint16 + dwPageSize uint32 + lpMinimumApplicationAddress uintptr + lpMaximumApplicationAddress uintptr + dwActiveProcessorMask uintptr + dwNumberOfProcessors uint32 + dwProcessorType uint32 + dwAllocationGranularity uint32 + wProcessorLevel uint16 + wProcessorRevision uint16 +} + +// Memory_info_ex is different between OSes +type MemoryInfoExStat struct{} + +type MemoryMapsStat struct{} + +// ioCounters is an equivalent representation of IO_COUNTERS in the Windows API. +// https://docs.microsoft.com/windows/win32/api/winnt/ns-winnt-io_counters +type ioCounters struct { + ReadOperationCount uint64 + WriteOperationCount uint64 + OtherOperationCount uint64 + ReadTransferCount uint64 + WriteTransferCount uint64 + OtherTransferCount uint64 +} + +type processBasicInformation32 struct { + Reserved1 uint32 + PebBaseAddress uint32 + Reserved2 uint32 + Reserved3 uint32 + UniqueProcessId uint32 + Reserved4 uint32 +} + +type processBasicInformation64 struct { + Reserved1 uint64 + PebBaseAddress uint64 + Reserved2 uint64 + Reserved3 uint64 + UniqueProcessId uint64 + Reserved4 uint64 +} + +type winLUID struct { + LowPart winDWord + HighPart winLong +} + +// LUID_AND_ATTRIBUTES +type winLUIDAndAttributes struct { + Luid winLUID + Attributes winDWord +} + +// TOKEN_PRIVILEGES +type winTokenPriviledges struct { + PrivilegeCount winDWord + Privileges [1]winLUIDAndAttributes +} + +type ( + winLong int32 + winDWord uint32 +) + +func init() { + var systemInfo systemInfo + + procGetNativeSystemInfo.Call(uintptr(unsafe.Pointer(&systemInfo))) + processorArchitecture = uint(systemInfo.wProcessorArchitecture) + + // enable SeDebugPrivilege https://github.com/midstar/proci/blob/6ec79f57b90ba3d9efa2a7b16ef9c9369d4be875/proci_windows.go#L80-L119 + handle, err := syscall.GetCurrentProcess() + if err != nil { + return + } + + var token syscall.Token + err = syscall.OpenProcessToken(handle, 0x0028, &token) + if err != nil { + return + } + defer token.Close() + + tokenPriviledges := winTokenPriviledges{PrivilegeCount: 1} + lpName := syscall.StringToUTF16("SeDebugPrivilege") + ret, _, _ := procLookupPrivilegeValue.Call( + 0, + uintptr(unsafe.Pointer(&lpName[0])), + uintptr(unsafe.Pointer(&tokenPriviledges.Privileges[0].Luid))) + if ret == 0 { + return + } + + tokenPriviledges.Privileges[0].Attributes = 0x00000002 // SE_PRIVILEGE_ENABLED + + procAdjustTokenPrivileges.Call( + uintptr(token), + 0, + uintptr(unsafe.Pointer(&tokenPriviledges)), + uintptr(unsafe.Sizeof(tokenPriviledges)), + 0, + 0) +} + +func pidsWithContext(ctx context.Context) ([]int32, error) { + // inspired by https://gist.github.com/henkman/3083408 + // and https://github.com/giampaolo/psutil/blob/1c3a15f637521ba5c0031283da39c733fda53e4c/psutil/arch/windows/process_info.c#L315-L329 + var ret []int32 + var read uint32 = 0 + var psSize uint32 = 1024 + const dwordSize uint32 = 4 + + for { + ps := make([]uint32, psSize) + if err := windows.EnumProcesses(ps, &read); err != nil { + return nil, err + } + if uint32(len(ps)) == read { // ps buffer was too small to host every results, retry with a bigger one + psSize += 1024 + continue + } + for _, pid := range ps[:read/dwordSize] { + ret = append(ret, int32(pid)) + } + return ret, nil + + } +} + +func PidExistsWithContext(ctx context.Context, pid int32) (bool, error) { + if pid == 0 { // special case for pid 0 System Idle Process + return true, nil + } + if pid < 0 { + return false, fmt.Errorf("invalid pid %v", pid) + } + if pid%4 != 0 { + // OpenProcess will succeed even on non-existing pid here https://devblogs.microsoft.com/oldnewthing/20080606-00/?p=22043 + // so we list every pid just to be sure and be future-proof + pids, err := PidsWithContext(ctx) + if err != nil { + return false, err + } + for _, i := range pids { + if i == pid { + return true, err + } + } + return false, err + } + const STILL_ACTIVE = 259 // https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-getexitcodeprocess + h, err := windows.OpenProcess(windows.PROCESS_QUERY_LIMITED_INFORMATION, false, uint32(pid)) + if err == windows.ERROR_ACCESS_DENIED { + return true, nil + } + if err == windows.ERROR_INVALID_PARAMETER { + return false, nil + } + if err != nil { + return false, err + } + defer syscall.CloseHandle(syscall.Handle(h)) + var exitCode uint32 + err = windows.GetExitCodeProcess(h, &exitCode) + return exitCode == STILL_ACTIVE, err +} + +func (p *Process) Ppid() (int32, error) { + return p.PpidWithContext(context.Background()) +} + +func (p *Process) PpidWithContext(ctx context.Context) (int32, error) { + ppid, _, _, err := getFromSnapProcess(p.Pid) + if err != nil { + return 0, err + } + return ppid, nil +} + +func (p *Process) Name() (string, error) { + return p.NameWithContext(context.Background()) +} + +func (p *Process) NameWithContext(ctx context.Context) (string, error) { + _, _, name, err := getFromSnapProcess(p.Pid) + if err != nil { + return "", fmt.Errorf("could not get Name: %s", err) + } + return name, nil +} + +func (p *Process) Tgid() (int32, error) { + return 0, common.ErrNotImplementedError +} + +func (p *Process) Exe() (string, error) { + return p.ExeWithContext(context.Background()) +} + +func (p *Process) ExeWithContext(ctx context.Context) (string, error) { + c, err := windows.OpenProcess(windows.PROCESS_QUERY_LIMITED_INFORMATION, false, uint32(p.Pid)) + if err != nil { + return "", err + } + defer windows.CloseHandle(c) + buf := make([]uint16, syscall.MAX_LONG_PATH) + size := uint32(syscall.MAX_LONG_PATH) + if err := procQueryFullProcessImageNameW.Find(); err == nil { // Vista+ + ret, _, err := procQueryFullProcessImageNameW.Call( + uintptr(c), + uintptr(0), + uintptr(unsafe.Pointer(&buf[0])), + uintptr(unsafe.Pointer(&size))) + if ret == 0 { + return "", err + } + return windows.UTF16ToString(buf[:]), nil + } + // XP fallback + ret, _, err := procGetProcessImageFileNameW.Call(uintptr(c), uintptr(unsafe.Pointer(&buf[0])), uintptr(size)) + if ret == 0 { + return "", err + } + return common.ConvertDOSPath(windows.UTF16ToString(buf[:])), nil +} + +func (p *Process) Cmdline() (string, error) { + return p.CmdlineWithContext(context.Background()) +} + +func (p *Process) CmdlineWithContext(_ context.Context) (string, error) { + cmdline, err := getProcessCommandLine(p.Pid) + if err != nil { + return "", fmt.Errorf("could not get CommandLine: %s", err) + } + return cmdline, nil +} + +// CmdlineSlice returns the command line arguments of the process as a slice with each +// element being an argument. This merely returns the CommandLine informations passed +// to the process split on the 0x20 ASCII character. +func (p *Process) CmdlineSlice() ([]string, error) { + return p.CmdlineSliceWithContext(context.Background()) +} + +func (p *Process) CmdlineSliceWithContext(ctx context.Context) ([]string, error) { + cmdline, err := p.CmdlineWithContext(ctx) + if err != nil { + return nil, err + } + return strings.Split(cmdline, " "), nil +} + +func (p *Process) createTimeWithContext(ctx context.Context) (int64, error) { + ru, err := getRusage(p.Pid) + if err != nil { + return 0, fmt.Errorf("could not get CreationDate: %s", err) + } + + return ru.CreationTime.Nanoseconds() / 1000000, nil +} + +func (p *Process) Cwd() (string, error) { + return p.CwdWithContext(context.Background()) +} + +func (p *Process) CwdWithContext(ctx context.Context) (string, error) { + return "", common.ErrNotImplementedError +} + +func (p *Process) Parent() (*Process, error) { + return p.ParentWithContext(context.Background()) +} + +func (p *Process) ParentWithContext(ctx context.Context) (*Process, error) { + ppid, err := p.PpidWithContext(ctx) + if err != nil { + return nil, fmt.Errorf("could not get ParentProcessID: %s", err) + } + + return NewProcess(ppid) +} + +func (p *Process) Status() (string, error) { + return p.StatusWithContext(context.Background()) +} + +func (p *Process) StatusWithContext(ctx context.Context) (string, error) { + return "", common.ErrNotImplementedError +} + +func (p *Process) Foreground() (bool, error) { + return p.ForegroundWithContext(context.Background()) +} + +func (p *Process) ForegroundWithContext(ctx context.Context) (bool, error) { + return false, common.ErrNotImplementedError +} + +func (p *Process) Username() (string, error) { + return p.UsernameWithContext(context.Background()) +} + +func (p *Process) UsernameWithContext(ctx context.Context) (string, error) { + pid := p.Pid + c, err := windows.OpenProcess(windows.PROCESS_QUERY_LIMITED_INFORMATION, false, uint32(pid)) + if err != nil { + return "", err + } + defer windows.CloseHandle(c) + + var token syscall.Token + err = syscall.OpenProcessToken(syscall.Handle(c), syscall.TOKEN_QUERY, &token) + if err != nil { + return "", err + } + defer token.Close() + tokenUser, err := token.GetTokenUser() + if err != nil { + return "", err + } + + user, domain, _, err := tokenUser.User.Sid.LookupAccount("") + return domain + "\\" + user, err +} + +func (p *Process) Uids() ([]int32, error) { + return p.UidsWithContext(context.Background()) +} + +func (p *Process) UidsWithContext(ctx context.Context) ([]int32, error) { + var uids []int32 + + return uids, common.ErrNotImplementedError +} + +func (p *Process) Gids() ([]int32, error) { + return p.GidsWithContext(context.Background()) +} + +func (p *Process) GidsWithContext(ctx context.Context) ([]int32, error) { + var gids []int32 + return gids, common.ErrNotImplementedError +} + +func (p *Process) GroupsWithContext(ctx context.Context) ([]int32, error) { + var groups []int32 + return groups, common.ErrNotImplementedError +} + +func (p *Process) Terminal() (string, error) { + return p.TerminalWithContext(context.Background()) +} + +func (p *Process) TerminalWithContext(ctx context.Context) (string, error) { + return "", common.ErrNotImplementedError +} + +// priorityClasses maps a win32 priority class to its WMI equivalent Win32_Process.Priority +// https://docs.microsoft.com/en-us/windows/desktop/api/processthreadsapi/nf-processthreadsapi-getpriorityclass +// https://docs.microsoft.com/en-us/windows/desktop/cimwin32prov/win32-process +var priorityClasses = map[int]int32{ + 0x00008000: 10, // ABOVE_NORMAL_PRIORITY_CLASS + 0x00004000: 6, // BELOW_NORMAL_PRIORITY_CLASS + 0x00000080: 13, // HIGH_PRIORITY_CLASS + 0x00000040: 4, // IDLE_PRIORITY_CLASS + 0x00000020: 8, // NORMAL_PRIORITY_CLASS + 0x00000100: 24, // REALTIME_PRIORITY_CLASS +} + +// Nice returns priority in Windows +func (p *Process) Nice() (int32, error) { + return p.NiceWithContext(context.Background()) +} + +func (p *Process) NiceWithContext(ctx context.Context) (int32, error) { + c, err := windows.OpenProcess(windows.PROCESS_QUERY_LIMITED_INFORMATION, false, uint32(p.Pid)) + if err != nil { + return 0, err + } + defer windows.CloseHandle(c) + ret, _, err := procGetPriorityClass.Call(uintptr(c)) + if ret == 0 { + return 0, err + } + priority, ok := priorityClasses[int(ret)] + if !ok { + return 0, fmt.Errorf("unknown priority class %v", ret) + } + return priority, nil +} + +func (p *Process) IOnice() (int32, error) { + return p.IOniceWithContext(context.Background()) +} + +func (p *Process) IOniceWithContext(ctx context.Context) (int32, error) { + return 0, common.ErrNotImplementedError +} + +func (p *Process) Rlimit() ([]RlimitStat, error) { + return p.RlimitWithContext(context.Background()) +} + +func (p *Process) RlimitWithContext(ctx context.Context) ([]RlimitStat, error) { + var rlimit []RlimitStat + + return rlimit, common.ErrNotImplementedError +} + +func (p *Process) RlimitUsage(gatherUsed bool) ([]RlimitStat, error) { + return p.RlimitUsageWithContext(context.Background(), gatherUsed) +} + +func (p *Process) RlimitUsageWithContext(ctx context.Context, gatherUsed bool) ([]RlimitStat, error) { + var rlimit []RlimitStat + + return rlimit, common.ErrNotImplementedError +} + +func (p *Process) IOCounters() (*IOCountersStat, error) { + return p.IOCountersWithContext(context.Background()) +} + +func (p *Process) IOCountersWithContext(ctx context.Context) (*IOCountersStat, error) { + c, err := windows.OpenProcess(windows.PROCESS_QUERY_LIMITED_INFORMATION, false, uint32(p.Pid)) + if err != nil { + return nil, err + } + defer windows.CloseHandle(c) + var ioCounters ioCounters + ret, _, err := procGetProcessIoCounters.Call(uintptr(c), uintptr(unsafe.Pointer(&ioCounters))) + if ret == 0 { + return nil, err + } + stats := &IOCountersStat{ + ReadCount: ioCounters.ReadOperationCount, + ReadBytes: ioCounters.ReadTransferCount, + WriteCount: ioCounters.WriteOperationCount, + WriteBytes: ioCounters.WriteTransferCount, + } + + return stats, nil +} + +func (p *Process) NumCtxSwitches() (*NumCtxSwitchesStat, error) { + return p.NumCtxSwitchesWithContext(context.Background()) +} + +func (p *Process) NumCtxSwitchesWithContext(ctx context.Context) (*NumCtxSwitchesStat, error) { + return nil, common.ErrNotImplementedError +} + +func (p *Process) NumFDs() (int32, error) { + return p.NumFDsWithContext(context.Background()) +} + +func (p *Process) NumFDsWithContext(ctx context.Context) (int32, error) { + return 0, common.ErrNotImplementedError +} + +func (p *Process) NumThreads() (int32, error) { + return p.NumThreadsWithContext(context.Background()) +} + +func (p *Process) NumThreadsWithContext(ctx context.Context) (int32, error) { + _, ret, _, err := getFromSnapProcess(p.Pid) + if err != nil { + return 0, err + } + return ret, nil +} + +func (p *Process) Threads() (map[int32]*cpu.TimesStat, error) { + return p.ThreadsWithContext(context.Background()) +} + +func (p *Process) ThreadsWithContext(ctx context.Context) (map[int32]*cpu.TimesStat, error) { + ret := make(map[int32]*cpu.TimesStat) + return ret, common.ErrNotImplementedError +} + +func (p *Process) Times() (*cpu.TimesStat, error) { + return p.TimesWithContext(context.Background()) +} + +func (p *Process) TimesWithContext(ctx context.Context) (*cpu.TimesStat, error) { + sysTimes, err := getProcessCPUTimes(p.Pid) + if err != nil { + return nil, err + } + + // User and kernel times are represented as a FILETIME structure + // which contains a 64-bit value representing the number of + // 100-nanosecond intervals since January 1, 1601 (UTC): + // http://msdn.microsoft.com/en-us/library/ms724284(VS.85).aspx + // To convert it into a float representing the seconds that the + // process has executed in user/kernel mode I borrowed the code + // below from psutil's _psutil_windows.c, and in turn from Python's + // Modules/posixmodule.c + + user := float64(sysTimes.UserTime.HighDateTime)*429.4967296 + float64(sysTimes.UserTime.LowDateTime)*1e-7 + kernel := float64(sysTimes.KernelTime.HighDateTime)*429.4967296 + float64(sysTimes.KernelTime.LowDateTime)*1e-7 + + return &cpu.TimesStat{ + User: user, + System: kernel, + }, nil +} + +func (p *Process) CPUAffinity() ([]int32, error) { + return p.CPUAffinityWithContext(context.Background()) +} + +func (p *Process) CPUAffinityWithContext(ctx context.Context) ([]int32, error) { + return nil, common.ErrNotImplementedError +} + +func (p *Process) MemoryInfo() (*MemoryInfoStat, error) { + return p.MemoryInfoWithContext(context.Background()) +} + +func (p *Process) MemoryInfoWithContext(ctx context.Context) (*MemoryInfoStat, error) { + mem, err := getMemoryInfo(p.Pid) + if err != nil { + return nil, err + } + + ret := &MemoryInfoStat{ + RSS: uint64(mem.WorkingSetSize), + VMS: uint64(mem.PagefileUsage), + } + + return ret, nil +} + +func (p *Process) MemoryInfoEx() (*MemoryInfoExStat, error) { + return p.MemoryInfoExWithContext(context.Background()) +} + +func (p *Process) MemoryInfoExWithContext(ctx context.Context) (*MemoryInfoExStat, error) { + return nil, common.ErrNotImplementedError +} + +func (p *Process) PageFaults() (*PageFaultsStat, error) { + return p.PageFaultsWithContext(context.Background()) +} + +func (p *Process) PageFaultsWithContext(ctx context.Context) (*PageFaultsStat, error) { + return nil, common.ErrNotImplementedError +} + +func (p *Process) Children() ([]*Process, error) { + return p.ChildrenWithContext(context.Background()) +} + +func (p *Process) ChildrenWithContext(ctx context.Context) ([]*Process, error) { + out := []*Process{} + snap, err := windows.CreateToolhelp32Snapshot(windows.TH32CS_SNAPPROCESS, uint32(0)) + if err != nil { + return out, err + } + defer windows.CloseHandle(snap) + var pe32 windows.ProcessEntry32 + pe32.Size = uint32(unsafe.Sizeof(pe32)) + if err := windows.Process32First(snap, &pe32); err != nil { + return out, err + } + for { + if pe32.ParentProcessID == uint32(p.Pid) { + p, err := NewProcess(int32(pe32.ProcessID)) + if err == nil { + out = append(out, p) + } + } + if err = windows.Process32Next(snap, &pe32); err != nil { + break + } + } + return out, nil +} + +func (p *Process) OpenFiles() ([]OpenFilesStat, error) { + return p.OpenFilesWithContext(context.Background()) +} + +func (p *Process) OpenFilesWithContext(ctx context.Context) ([]OpenFilesStat, error) { + return nil, common.ErrNotImplementedError +} + +func (p *Process) Connections() ([]net.ConnectionStat, error) { + return p.ConnectionsWithContext(context.Background()) +} + +func (p *Process) ConnectionsWithContext(ctx context.Context) ([]net.ConnectionStat, error) { + return net.ConnectionsPidWithContext(ctx, "all", p.Pid) +} + +func (p *Process) ConnectionsMax(max int) ([]net.ConnectionStat, error) { + return p.ConnectionsMaxWithContext(context.Background(), max) +} + +func (p *Process) ConnectionsMaxWithContext(ctx context.Context, max int) ([]net.ConnectionStat, error) { + return []net.ConnectionStat{}, common.ErrNotImplementedError +} + +func (p *Process) NetIOCounters(pernic bool) ([]net.IOCountersStat, error) { + return p.NetIOCountersWithContext(context.Background(), pernic) +} + +func (p *Process) NetIOCountersWithContext(ctx context.Context, pernic bool) ([]net.IOCountersStat, error) { + return nil, common.ErrNotImplementedError +} + +func (p *Process) MemoryMaps(grouped bool) (*[]MemoryMapsStat, error) { + return p.MemoryMapsWithContext(context.Background(), grouped) +} + +func (p *Process) MemoryMapsWithContext(ctx context.Context, grouped bool) (*[]MemoryMapsStat, error) { + var ret []MemoryMapsStat + return &ret, common.ErrNotImplementedError +} + +func (p *Process) SendSignal(sig windows.Signal) error { + return p.SendSignalWithContext(context.Background(), sig) +} + +func (p *Process) SendSignalWithContext(ctx context.Context, sig windows.Signal) error { + return common.ErrNotImplementedError +} + +func (p *Process) Suspend() error { + return p.SuspendWithContext(context.Background()) +} + +func (p *Process) SuspendWithContext(ctx context.Context) error { + c, err := windows.OpenProcess(windows.PROCESS_SUSPEND_RESUME, false, uint32(p.Pid)) + if err != nil { + return err + } + defer windows.CloseHandle(c) + + r1, _, _ := procNtSuspendProcess.Call(uintptr(unsafe.Pointer(c))) + if r1 != 0 { + // See https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-erref/596a1078-e883-4972-9bbc-49e60bebca55 + return fmt.Errorf("NtStatus='0x%.8X'", r1) + } + + return nil +} + +func (p *Process) Resume() error { + return p.ResumeWithContext(context.Background()) +} + +func (p *Process) ResumeWithContext(ctx context.Context) error { + c, err := windows.OpenProcess(windows.PROCESS_SUSPEND_RESUME, false, uint32(p.Pid)) + if err != nil { + return err + } + defer windows.CloseHandle(c) + + r1, _, _ := procNtResumeProcess.Call(uintptr(unsafe.Pointer(c))) + if r1 != 0 { + // See https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-erref/596a1078-e883-4972-9bbc-49e60bebca55 + return fmt.Errorf("NtStatus='0x%.8X'", r1) + } + + return nil +} + +func (p *Process) Terminate() error { + return p.TerminateWithContext(context.Background()) +} + +func (p *Process) TerminateWithContext(ctx context.Context) error { + proc, err := windows.OpenProcess(windows.PROCESS_TERMINATE, false, uint32(p.Pid)) + if err != nil { + return err + } + err = windows.TerminateProcess(proc, 0) + windows.CloseHandle(proc) + return err +} + +func (p *Process) Kill() error { + return p.KillWithContext(context.Background()) +} + +func (p *Process) KillWithContext(ctx context.Context) error { + process := os.Process{Pid: int(p.Pid)} + return process.Kill() +} + +func getFromSnapProcess(pid int32) (int32, int32, string, error) { + snap, err := windows.CreateToolhelp32Snapshot(windows.TH32CS_SNAPPROCESS, uint32(pid)) + if err != nil { + return 0, 0, "", err + } + defer windows.CloseHandle(snap) + var pe32 windows.ProcessEntry32 + pe32.Size = uint32(unsafe.Sizeof(pe32)) + if err = windows.Process32First(snap, &pe32); err != nil { + return 0, 0, "", err + } + for { + if pe32.ProcessID == uint32(pid) { + szexe := windows.UTF16ToString(pe32.ExeFile[:]) + return int32(pe32.ParentProcessID), int32(pe32.Threads), szexe, nil + } + if err = windows.Process32Next(snap, &pe32); err != nil { + break + } + } + return 0, 0, "", fmt.Errorf("couldn't find pid: %d", pid) +} + +// Get processes +func Processes() ([]*Process, error) { + return ProcessesWithContext(context.Background()) +} + +func ProcessesWithContext(ctx context.Context) ([]*Process, error) { + out := []*Process{} + + pids, err := PidsWithContext(ctx) + if err != nil { + return out, fmt.Errorf("could not get Processes %s", err) + } + + for _, pid := range pids { + p, err := NewProcess(pid) + if err != nil { + continue + } + out = append(out, p) + } + + return out, nil +} + +func getRusage(pid int32) (*windows.Rusage, error) { + var CPU windows.Rusage + + c, err := windows.OpenProcess(windows.PROCESS_QUERY_LIMITED_INFORMATION, false, uint32(pid)) + if err != nil { + return nil, err + } + defer windows.CloseHandle(c) + + if err := windows.GetProcessTimes(c, &CPU.CreationTime, &CPU.ExitTime, &CPU.KernelTime, &CPU.UserTime); err != nil { + return nil, err + } + + return &CPU, nil +} + +func getMemoryInfo(pid int32) (PROCESS_MEMORY_COUNTERS, error) { + var mem PROCESS_MEMORY_COUNTERS + c, err := windows.OpenProcess(windows.PROCESS_QUERY_LIMITED_INFORMATION, false, uint32(pid)) + if err != nil { + return mem, err + } + defer windows.CloseHandle(c) + if err := getProcessMemoryInfo(c, &mem); err != nil { + return mem, err + } + + return mem, err +} + +func getProcessMemoryInfo(h windows.Handle, mem *PROCESS_MEMORY_COUNTERS) (err error) { + r1, _, e1 := syscall.Syscall(procGetProcessMemoryInfo.Addr(), 3, uintptr(h), uintptr(unsafe.Pointer(mem)), uintptr(unsafe.Sizeof(*mem))) + if r1 == 0 { + if e1 != 0 { + err = error(e1) + } else { + err = syscall.EINVAL + } + } + return +} + +type SYSTEM_TIMES struct { + CreateTime syscall.Filetime + ExitTime syscall.Filetime + KernelTime syscall.Filetime + UserTime syscall.Filetime +} + +func getProcessCPUTimes(pid int32) (SYSTEM_TIMES, error) { + var times SYSTEM_TIMES + + h, err := windows.OpenProcess(windows.PROCESS_QUERY_LIMITED_INFORMATION, false, uint32(pid)) + if err != nil { + return times, err + } + defer windows.CloseHandle(h) + + err = syscall.GetProcessTimes( + syscall.Handle(h), + ×.CreateTime, + ×.ExitTime, + ×.KernelTime, + ×.UserTime, + ) + + return times, err +} + +func is32BitProcess(procHandle syscall.Handle) bool { + var wow64 uint + + ret, _, _ := common.ProcNtQueryInformationProcess.Call( + uintptr(procHandle), + uintptr(common.ProcessWow64Information), + uintptr(unsafe.Pointer(&wow64)), + uintptr(unsafe.Sizeof(wow64)), + uintptr(0), + ) + if int(ret) >= 0 { + if wow64 != 0 { + return true + } + } else { + // if the OS does not support the call, we fallback into the bitness of the app + if unsafe.Sizeof(wow64) == 4 { + return true + } + } + return false +} + +func getProcessCommandLine(pid int32) (string, error) { + h, err := windows.OpenProcess(windows.PROCESS_QUERY_LIMITED_INFORMATION|windows.PROCESS_VM_READ, false, uint32(pid)) + if err == windows.ERROR_ACCESS_DENIED || err == windows.ERROR_INVALID_PARAMETER { + return "", nil + } + if err != nil { + return "", err + } + defer syscall.CloseHandle(syscall.Handle(h)) + + const ( + PROCESSOR_ARCHITECTURE_INTEL = 0 + PROCESSOR_ARCHITECTURE_ARM = 5 + PROCESSOR_ARCHITECTURE_ARM64 = 12 + PROCESSOR_ARCHITECTURE_IA64 = 6 + PROCESSOR_ARCHITECTURE_AMD64 = 9 + ) + + procIs32Bits := true + switch processorArchitecture { + case PROCESSOR_ARCHITECTURE_INTEL: + fallthrough + case PROCESSOR_ARCHITECTURE_ARM: + procIs32Bits = true + + case PROCESSOR_ARCHITECTURE_ARM64: + fallthrough + case PROCESSOR_ARCHITECTURE_IA64: + fallthrough + case PROCESSOR_ARCHITECTURE_AMD64: + procIs32Bits = is32BitProcess(syscall.Handle(h)) + + default: + // for other unknown platforms, we rely on process platform + if unsafe.Sizeof(processorArchitecture) == 8 { + procIs32Bits = false + } + } + + pebAddress := queryPebAddress(syscall.Handle(h), procIs32Bits) + if pebAddress == 0 { + return "", errors.New("cannot locate process PEB") + } + + if procIs32Bits { + buf := readProcessMemory(syscall.Handle(h), procIs32Bits, pebAddress+uint64(16), 4) + if len(buf) != 4 { + return "", errors.New("cannot locate process user parameters") + } + userProcParams := uint64(buf[0]) | (uint64(buf[1]) << 8) | (uint64(buf[2]) << 16) | (uint64(buf[3]) << 24) + + // read CommandLine field from PRTL_USER_PROCESS_PARAMETERS + remoteCmdLine := readProcessMemory(syscall.Handle(h), procIs32Bits, userProcParams+uint64(64), 8) + if len(remoteCmdLine) != 8 { + return "", errors.New("cannot read cmdline field") + } + + // remoteCmdLine is actually a UNICODE_STRING32 + // the first two bytes has the length + cmdLineLength := uint(remoteCmdLine[0]) | (uint(remoteCmdLine[1]) << 8) + if cmdLineLength > 0 { + // and, at offset 4, is the pointer to the buffer + bufferAddress := uint32(remoteCmdLine[4]) | (uint32(remoteCmdLine[5]) << 8) | + (uint32(remoteCmdLine[6]) << 16) | (uint32(remoteCmdLine[7]) << 24) + + cmdLine := readProcessMemory(syscall.Handle(h), procIs32Bits, uint64(bufferAddress), cmdLineLength) + if len(cmdLine) != int(cmdLineLength) { + return "", errors.New("cannot read cmdline") + } + + return convertUTF16ToString(cmdLine), nil + } + } else { + buf := readProcessMemory(syscall.Handle(h), procIs32Bits, pebAddress+uint64(32), 8) + if len(buf) != 8 { + return "", errors.New("cannot locate process user parameters") + } + userProcParams := uint64(buf[0]) | (uint64(buf[1]) << 8) | (uint64(buf[2]) << 16) | (uint64(buf[3]) << 24) | + (uint64(buf[4]) << 32) | (uint64(buf[5]) << 40) | (uint64(buf[6]) << 48) | (uint64(buf[7]) << 56) + + // read CommandLine field from PRTL_USER_PROCESS_PARAMETERS + remoteCmdLine := readProcessMemory(syscall.Handle(h), procIs32Bits, userProcParams+uint64(112), 16) + if len(remoteCmdLine) != 16 { + return "", errors.New("cannot read cmdline field") + } + + // remoteCmdLine is actually a UNICODE_STRING64 + // the first two bytes has the length + cmdLineLength := uint(remoteCmdLine[0]) | (uint(remoteCmdLine[1]) << 8) + if cmdLineLength > 0 { + // and, at offset 8, is the pointer to the buffer + bufferAddress := uint64(remoteCmdLine[8]) | (uint64(remoteCmdLine[9]) << 8) | + (uint64(remoteCmdLine[10]) << 16) | (uint64(remoteCmdLine[11]) << 24) | + (uint64(remoteCmdLine[12]) << 32) | (uint64(remoteCmdLine[13]) << 40) | + (uint64(remoteCmdLine[14]) << 48) | (uint64(remoteCmdLine[15]) << 56) + + cmdLine := readProcessMemory(syscall.Handle(h), procIs32Bits, bufferAddress, cmdLineLength) + if len(cmdLine) != int(cmdLineLength) { + return "", errors.New("cannot read cmdline") + } + + return convertUTF16ToString(cmdLine), nil + } + } + + // if we reach here, we have no command line + return "", nil +} + +func convertUTF16ToString(src []byte) string { + srcLen := len(src) / 2 + + codePoints := make([]uint16, srcLen) + + srcIdx := 0 + for i := 0; i < srcLen; i++ { + codePoints[i] = uint16(src[srcIdx]) | uint16(src[srcIdx+1])<<8 + srcIdx += 2 + } + return syscall.UTF16ToString(codePoints) +} diff --git a/internal/gopsutil/process/process_windows_386.go b/internal/gopsutil/process/process_windows_386.go new file mode 100644 index 0000000..9f9f263 --- /dev/null +++ b/internal/gopsutil/process/process_windows_386.go @@ -0,0 +1,102 @@ +//go:build windows + +package process + +import ( + "syscall" + "unsafe" + + "github.com/gofiber/fiber/v2/internal/gopsutil/common" +) + +type PROCESS_MEMORY_COUNTERS struct { + CB uint32 + PageFaultCount uint32 + PeakWorkingSetSize uint32 + WorkingSetSize uint32 + QuotaPeakPagedPoolUsage uint32 + QuotaPagedPoolUsage uint32 + QuotaPeakNonPagedPoolUsage uint32 + QuotaNonPagedPoolUsage uint32 + PagefileUsage uint32 + PeakPagefileUsage uint32 +} + +func queryPebAddress(procHandle syscall.Handle, is32BitProcess bool) uint64 { + if is32BitProcess { + // we are on a 32-bit process reading an external 32-bit process + var info processBasicInformation32 + + ret, _, _ := common.ProcNtQueryInformationProcess.Call( + uintptr(procHandle), + uintptr(common.ProcessBasicInformation), + uintptr(unsafe.Pointer(&info)), + uintptr(unsafe.Sizeof(info)), + uintptr(0), + ) + if int(ret) >= 0 { + return uint64(info.PebBaseAddress) + } + } else { + // we are on a 32-bit process reading an external 64-bit process + if common.ProcNtWow64QueryInformationProcess64.Find() == nil { // avoid panic + var info processBasicInformation64 + + ret, _, _ := common.ProcNtWow64QueryInformationProcess64.Call( + uintptr(procHandle), + uintptr(common.ProcessBasicInformation), + uintptr(unsafe.Pointer(&info)), + uintptr(unsafe.Sizeof(info)), + uintptr(0), + ) + if int(ret) >= 0 { + return info.PebBaseAddress + } + } + } + + // return 0 on error + return 0 +} + +func readProcessMemory(h syscall.Handle, is32BitProcess bool, address uint64, size uint) []byte { + if is32BitProcess { + var read uint + + buffer := make([]byte, size) + + ret, _, _ := common.ProcNtReadVirtualMemory.Call( + uintptr(h), + uintptr(address), + uintptr(unsafe.Pointer(&buffer[0])), + uintptr(size), + uintptr(unsafe.Pointer(&read)), + ) + if int(ret) >= 0 && read > 0 { + return buffer[:read] + } + } else { + // reading a 64-bit process from a 32-bit one + if common.ProcNtWow64ReadVirtualMemory64.Find() == nil { // avoid panic + var read uint64 + + buffer := make([]byte, size) + + ret, _, _ := common.ProcNtWow64ReadVirtualMemory64.Call( + uintptr(h), + uintptr(address&0xFFFFFFFF), // the call expects a 64-bit value + uintptr(address>>32), + uintptr(unsafe.Pointer(&buffer[0])), + uintptr(size), // the call expects a 64-bit value + uintptr(0), // but size is 32-bit so pass zero as the high dword + uintptr(unsafe.Pointer(&read)), + ) + if int(ret) >= 0 && read > 0 { + return buffer[:uint(read)] + } + } + } + + // if we reach here, an error happened + return nil +} diff --git a/internal/gopsutil/process/process_windows_amd64.go b/internal/gopsutil/process/process_windows_amd64.go new file mode 100644 index 0000000..d9012a0 --- /dev/null +++ b/internal/gopsutil/process/process_windows_amd64.go @@ -0,0 +1,76 @@ +//go:build windows + +package process + +import ( + "syscall" + "unsafe" + + "github.com/gofiber/fiber/v2/internal/gopsutil/common" +) + +type PROCESS_MEMORY_COUNTERS struct { + CB uint32 + PageFaultCount uint32 + PeakWorkingSetSize uint64 + WorkingSetSize uint64 + QuotaPeakPagedPoolUsage uint64 + QuotaPagedPoolUsage uint64 + QuotaPeakNonPagedPoolUsage uint64 + QuotaNonPagedPoolUsage uint64 + PagefileUsage uint64 + PeakPagefileUsage uint64 +} + +func queryPebAddress(procHandle syscall.Handle, is32BitProcess bool) uint64 { + if is32BitProcess { + // we are on a 64-bit process reading an external 32-bit process + var wow64 uint + + ret, _, _ := common.ProcNtQueryInformationProcess.Call( + uintptr(procHandle), + uintptr(common.ProcessWow64Information), + uintptr(unsafe.Pointer(&wow64)), + uintptr(unsafe.Sizeof(wow64)), + uintptr(0), + ) + if int(ret) >= 0 { + return uint64(wow64) + } + } else { + // we are on a 64-bit process reading an external 64-bit process + var info processBasicInformation64 + + ret, _, _ := common.ProcNtQueryInformationProcess.Call( + uintptr(procHandle), + uintptr(common.ProcessBasicInformation), + uintptr(unsafe.Pointer(&info)), + uintptr(unsafe.Sizeof(info)), + uintptr(0), + ) + if int(ret) >= 0 { + return info.PebBaseAddress + } + } + + // return 0 on error + return 0 +} + +func readProcessMemory(procHandle syscall.Handle, _ bool, address uint64, size uint) []byte { + var read uint + + buffer := make([]byte, size) + + ret, _, _ := common.ProcNtReadVirtualMemory.Call( + uintptr(procHandle), + uintptr(address), + uintptr(unsafe.Pointer(&buffer[0])), + uintptr(size), + uintptr(unsafe.Pointer(&read)), + ) + if int(ret) >= 0 && read > 0 { + return buffer[:read] + } + return nil +} diff --git a/internal/gopsutil/process/types_darwin.go b/internal/gopsutil/process/types_darwin.go new file mode 100644 index 0000000..7b8c7ac --- /dev/null +++ b/internal/gopsutil/process/types_darwin.go @@ -0,0 +1,163 @@ +// Copyright 2009 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Hand Writing +// - all pointer in ExternProc to uint64 + +//go:build ignore + +/* +Input to cgo -godefs. +*/ + +// +godefs map struct_in_addr [4]byte /* in_addr */ +// +godefs map struct_in6_addr [16]byte /* in6_addr */ +// +godefs map struct_ [16]byte /* in6_addr */ + +package process + +/* +#define __DARWIN_UNIX03 0 +#define KERNEL +#define _DARWIN_USE_64_BIT_INODE +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +enum { + sizeofPtr = sizeof(void*), +}; + +union sockaddr_all { + struct sockaddr s1; // this one gets used for fields + struct sockaddr_in s2; // these pad it out + struct sockaddr_in6 s3; + struct sockaddr_un s4; + struct sockaddr_dl s5; +}; + +struct sockaddr_any { + struct sockaddr addr; + char pad[sizeof(union sockaddr_all) - sizeof(struct sockaddr)]; +}; + +struct ucred_queue { + struct ucred *tqe_next; + struct ucred **tqe_prev; + TRACEBUF +}; + +*/ +import "C" + +// Machine characteristics; for internal use. + +const ( + sizeofPtr = C.sizeofPtr + sizeofShort = C.sizeof_short + sizeofInt = C.sizeof_int + sizeofLong = C.sizeof_long + sizeofLongLong = C.sizeof_longlong +) + +// Basic types + +type ( + _C_short C.short + _C_int C.int + _C_long C.long + _C_long_long C.longlong +) + +// Time + +type Timespec C.struct_timespec + +type Timeval C.struct_timeval + +// Processes + +type Rusage C.struct_rusage + +type Rlimit C.struct_rlimit + +type UGid_t C.gid_t + +type KinfoProc C.struct_kinfo_proc + +type Eproc C.struct_eproc + +type Proc C.struct_proc + +type Session C.struct_session + +type ucred C.struct_ucred + +type Uucred C.struct__ucred + +type Upcred C.struct__pcred + +type Vmspace C.struct_vmspace + +type Sigacts C.struct_sigacts + +type ExternProc C.struct_extern_proc + +type Itimerval C.struct_itimerval + +type Vnode C.struct_vnode + +type Pgrp C.struct_pgrp + +type UserStruct C.struct_user + +type Au_session C.struct_au_session + +type Posix_cred C.struct_posix_cred + +type Label C.struct_label + +type ( + AuditinfoAddr C.struct_auditinfo_addr + AuMask C.struct_au_mask + AuTidAddr C.struct_au_tid_addr +) + +// TAILQ(ucred) +type UcredQueue C.struct_ucred_queue diff --git a/internal/gopsutil/process/types_freebsd.go b/internal/gopsutil/process/types_freebsd.go new file mode 100644 index 0000000..5ae6d01 --- /dev/null +++ b/internal/gopsutil/process/types_freebsd.go @@ -0,0 +1,95 @@ +//go:build ignore + +// We still need editing by hands. +// go tool cgo -godefs types_freebsd.go | sed 's/\*int64/int64/' | sed 's/\*byte/int64/' > process_freebsd_amd64.go + +/* +Input to cgo -godefs. +*/ + +// +godefs map struct_pargs int64 /* pargs */ +// +godefs map struct_proc int64 /* proc */ +// +godefs map struct_user int64 /* user */ +// +godefs map struct_vnode int64 /* vnode */ +// +godefs map struct_vnode int64 /* vnode */ +// +godefs map struct_filedesc int64 /* filedesc */ +// +godefs map struct_vmspace int64 /* vmspace */ +// +godefs map struct_pcb int64 /* pcb */ +// +godefs map struct_thread int64 /* thread */ +// +godefs map struct___sigset [16]byte /* sigset */ + +package process + +/* +#include +#include + +enum { + sizeofPtr = sizeof(void*), +}; + + +*/ +import "C" + +// Machine characteristics; for internal use. + +const ( + CTLKern = 1 // "high kernel": proc, limits + KernProc = 14 // struct: process entries + KernProcPID = 1 // by process id + KernProcProc = 8 // only return procs + KernProcPathname = 12 // path to executable + KernProcArgs = 7 // get/set arguments/proctitle +) + +const ( + sizeofPtr = C.sizeofPtr + sizeofShort = C.sizeof_short + sizeofInt = C.sizeof_int + sizeofLong = C.sizeof_long + sizeofLongLong = C.sizeof_longlong +) + +const ( + sizeOfKinfoVmentry = C.sizeof_struct_kinfo_vmentry + sizeOfKinfoProc = C.sizeof_struct_kinfo_proc +) + +// from sys/proc.h +const ( + SIDL = 1 /* Process being created by fork. */ + SRUN = 2 /* Currently runnable. */ + SSLEEP = 3 /* Sleeping on an address. */ + SSTOP = 4 /* Process debugging or suspension. */ + SZOMB = 5 /* Awaiting collection by parent. */ + SWAIT = 6 /* Waiting for interrupt. */ + SLOCK = 7 /* Blocked on a lock. */ +) + +// Basic types + +type ( + _C_short C.short + _C_int C.int + _C_long C.long + _C_long_long C.longlong +) + +// Time + +type Timespec C.struct_timespec + +type Timeval C.struct_timeval + +// Processes + +type Rusage C.struct_rusage + +type Rlimit C.struct_rlimit + +type KinfoProc C.struct_kinfo_proc + +type Priority C.struct_priority + +type KinfoVmentry C.struct_kinfo_vmentry diff --git a/internal/gopsutil/process/types_openbsd.go b/internal/gopsutil/process/types_openbsd.go new file mode 100644 index 0000000..d55eec6 --- /dev/null +++ b/internal/gopsutil/process/types_openbsd.go @@ -0,0 +1,103 @@ +//go:build ignore + +// We still need editing by hands. +// go tool cgo -godefs types_openbsd.go | sed 's/\*int64/int64/' | sed 's/\*byte/int64/' > process_openbsd_amd64.go + +/* +Input to cgo -godefs. +*/ + +// +godefs map struct_pargs int64 /* pargs */ +// +godefs map struct_proc int64 /* proc */ +// +godefs map struct_user int64 /* user */ +// +godefs map struct_vnode int64 /* vnode */ +// +godefs map struct_vnode int64 /* vnode */ +// +godefs map struct_filedesc int64 /* filedesc */ +// +godefs map struct_vmspace int64 /* vmspace */ +// +godefs map struct_pcb int64 /* pcb */ +// +godefs map struct_thread int64 /* thread */ +// +godefs map struct___sigset [16]byte /* sigset */ + +package process + +/* +#include +#include +#include + +enum { + sizeofPtr = sizeof(void*), +}; + + +*/ +import "C" + +// Machine characteristics; for internal use. + +const ( + CTLKern = 1 // "high kernel": proc, limits + KernProc = 66 // struct: process entries + KernProcAll = 0 + KernProcPID = 1 // by process id + KernProcProc = 8 // only return procs + KernProcPathname = 12 // path to executable + KernProcArgs = 55 // get/set arguments/proctitle + KernProcArgv = 1 + KernProcEnv = 3 +) + +const ( + ArgMax = 256 * 1024 // sys/syslimits.h:#define ARG_MAX +) + +const ( + sizeofPtr = C.sizeofPtr + sizeofShort = C.sizeof_short + sizeofInt = C.sizeof_int + sizeofLong = C.sizeof_long + sizeofLongLong = C.sizeof_longlong +) + +const ( + sizeOfKinfoVmentry = C.sizeof_struct_kinfo_vmentry + sizeOfKinfoProc = C.sizeof_struct_kinfo_proc +) + +// from sys/proc.h +const ( + SIDL = 1 /* Process being created by fork. */ + SRUN = 2 /* Currently runnable. */ + SSLEEP = 3 /* Sleeping on an address. */ + SSTOP = 4 /* Process debugging or suspension. */ + SZOMB = 5 /* Awaiting collection by parent. */ + SDEAD = 6 /* Thread is almost gone */ + SONPROC = 7 /* Thread is currently on a CPU. */ +) + +// Basic types + +type ( + _C_short C.short + _C_int C.int + _C_long C.long + _C_long_long C.longlong +) + +// Time + +type Timespec C.struct_timespec + +type Timeval C.struct_timeval + +// Processes + +type Rusage C.struct_rusage + +type Rlimit C.struct_rlimit + +type KinfoProc C.struct_kinfo_proc + +type Priority C.struct_priority + +type KinfoVmentry C.struct_kinfo_vmentry diff --git a/internal/memory/memory.go b/internal/memory/memory.go new file mode 100644 index 0000000..d7b053d --- /dev/null +++ b/internal/memory/memory.go @@ -0,0 +1,97 @@ +// Package memory Is a slight copy of the memory storage, but far from the storage interface it can not only work with bytes +// but directly store any kind of data without having to encode it each time, which gives a huge speed advantage +package memory + +import ( + "sync" + "sync/atomic" + "time" + + "github.com/gofiber/fiber/v2/utils" +) + +type Storage struct { + sync.RWMutex + data map[string]item // data +} + +type item struct { + // max value is 4294967295 -> Sun Feb 07 2106 06:28:15 GMT+0000 + e uint32 // exp + v interface{} // val +} + +func New() *Storage { + store := &Storage{ + data: make(map[string]item), + } + utils.StartTimeStampUpdater() + go store.gc(1 * time.Second) + return store +} + +// Get value by key +func (s *Storage) Get(key string) interface{} { + s.RLock() + v, ok := s.data[key] + s.RUnlock() + if !ok || v.e != 0 && v.e <= atomic.LoadUint32(&utils.Timestamp) { + return nil + } + return v.v +} + +// Set key with value +func (s *Storage) Set(key string, val interface{}, ttl time.Duration) { + var exp uint32 + if ttl > 0 { + exp = uint32(ttl.Seconds()) + atomic.LoadUint32(&utils.Timestamp) + } + i := item{exp, val} + s.Lock() + s.data[key] = i + s.Unlock() +} + +// Delete key by key +func (s *Storage) Delete(key string) { + s.Lock() + delete(s.data, key) + s.Unlock() +} + +// Reset all keys +func (s *Storage) Reset() { + nd := make(map[string]item) + s.Lock() + s.data = nd + s.Unlock() +} + +func (s *Storage) gc(sleep time.Duration) { + ticker := time.NewTicker(sleep) + defer ticker.Stop() + var expired []string + + for range ticker.C { + ts := atomic.LoadUint32(&utils.Timestamp) + expired = expired[:0] + s.RLock() + for key, v := range s.data { + if v.e != 0 && v.e <= ts { + expired = append(expired, key) + } + } + s.RUnlock() + s.Lock() + // Double-checked locking. + // We might have replaced the item in the meantime. + for i := range expired { + v := s.data[expired[i]] + if v.e != 0 && v.e <= ts { + delete(s.data, expired[i]) + } + } + s.Unlock() + } +} diff --git a/internal/memory/memory_test.go b/internal/memory/memory_test.go new file mode 100644 index 0000000..a060482 --- /dev/null +++ b/internal/memory/memory_test.go @@ -0,0 +1,81 @@ +package memory + +import ( + "testing" + "time" + + "github.com/gofiber/fiber/v2/utils" +) + +// go test -run Test_Memory -v -race + +func Test_Memory(t *testing.T) { + t.Parallel() + store := New() + var ( + key = "john" + val interface{} = []byte("doe") + exp = 1 * time.Second + ) + + store.Set(key, val, 0) + store.Set(key, val, 0) + + result := store.Get(key) + utils.AssertEqual(t, val, result) + + result = store.Get("empty") + utils.AssertEqual(t, nil, result) + + store.Set(key, val, exp) + time.Sleep(1100 * time.Millisecond) + + result = store.Get(key) + utils.AssertEqual(t, nil, result) + + store.Set(key, val, 0) + result = store.Get(key) + utils.AssertEqual(t, val, result) + + store.Delete(key) + result = store.Get(key) + utils.AssertEqual(t, nil, result) + + store.Set("john", val, 0) + store.Set("doe", val, 0) + store.Reset() + + result = store.Get("john") + utils.AssertEqual(t, nil, result) + + result = store.Get("doe") + utils.AssertEqual(t, nil, result) +} + +// go test -v -run=^$ -bench=Benchmark_Memory -benchmem -count=4 +func Benchmark_Memory(b *testing.B) { + keyLength := 1000 + keys := make([]string, keyLength) + for i := 0; i < keyLength; i++ { + keys[i] = utils.UUID() + } + value := []byte("joe") + + ttl := 2 * time.Second + b.Run("fiber_memory", func(b *testing.B) { + d := New() + b.ReportAllocs() + b.ResetTimer() + for n := 0; n < b.N; n++ { + for _, key := range keys { + d.Set(key, value, ttl) + } + for _, key := range keys { + _ = d.Get(key) + } + for _, key := range keys { + d.Delete(key) + } + } + }) +} diff --git a/internal/schema/LICENSE b/internal/schema/LICENSE new file mode 100644 index 0000000..0e5fb87 --- /dev/null +++ b/internal/schema/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 2012 Rodrigo Moraes. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/internal/schema/cache.go b/internal/schema/cache.go new file mode 100644 index 0000000..bf21697 --- /dev/null +++ b/internal/schema/cache.go @@ -0,0 +1,305 @@ +// Copyright 2012 The Gorilla Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package schema + +import ( + "errors" + "reflect" + "strconv" + "strings" + "sync" +) + +var errInvalidPath = errors.New("schema: invalid path") + +// newCache returns a new cache. +func newCache() *cache { + c := cache{ + m: make(map[reflect.Type]*structInfo), + regconv: make(map[reflect.Type]Converter), + tag: "schema", + } + return &c +} + +// cache caches meta-data about a struct. +type cache struct { + l sync.RWMutex + m map[reflect.Type]*structInfo + regconv map[reflect.Type]Converter + tag string +} + +// registerConverter registers a converter function for a custom type. +func (c *cache) registerConverter(value interface{}, converterFunc Converter) { + c.regconv[reflect.TypeOf(value)] = converterFunc +} + +// parsePath parses a path in dotted notation verifying that it is a valid +// path to a struct field. +// +// It returns "path parts" which contain indices to fields to be used by +// reflect.Value.FieldByString(). Multiple parts are required for slices of +// structs. +func (c *cache) parsePath(p string, t reflect.Type) ([]pathPart, error) { + var struc *structInfo + var field *fieldInfo + var index64 int64 + var err error + parts := make([]pathPart, 0) + path := make([]string, 0) + keys := strings.Split(p, ".") + for i := 0; i < len(keys); i++ { + if t.Kind() != reflect.Struct { + return nil, errInvalidPath + } + if struc = c.get(t); struc == nil { + return nil, errInvalidPath + } + if field = struc.get(keys[i]); field == nil { + return nil, errInvalidPath + } + // Valid field. Append index. + path = append(path, field.name) + if field.isSliceOfStructs && (!field.unmarshalerInfo.IsValid || (field.unmarshalerInfo.IsValid && field.unmarshalerInfo.IsSliceElement)) { + // Parse a special case: slices of structs. + // i+1 must be the slice index. + // + // Now that struct can implements TextUnmarshaler interface, + // we don't need to force the struct's fields to appear in the path. + // So checking i+2 is not necessary anymore. + i++ + if i+1 > len(keys) { + return nil, errInvalidPath + } + if index64, err = strconv.ParseInt(keys[i], 10, 0); err != nil { + return nil, errInvalidPath + } + parts = append(parts, pathPart{ + path: path, + field: field, + index: int(index64), + }) + path = make([]string, 0) + + // Get the next struct type, dropping ptrs. + if field.typ.Kind() == reflect.Ptr { + t = field.typ.Elem() + } else { + t = field.typ + } + if t.Kind() == reflect.Slice { + t = t.Elem() + if t.Kind() == reflect.Ptr { + t = t.Elem() + } + } + } else if field.typ.Kind() == reflect.Ptr { + t = field.typ.Elem() + } else { + t = field.typ + } + } + // Add the remaining. + parts = append(parts, pathPart{ + path: path, + field: field, + index: -1, + }) + return parts, nil +} + +// get returns a cached structInfo, creating it if necessary. +func (c *cache) get(t reflect.Type) *structInfo { + c.l.RLock() + info := c.m[t] + c.l.RUnlock() + if info == nil { + info = c.create(t, "") + c.l.Lock() + c.m[t] = info + c.l.Unlock() + } + return info +} + +// create creates a structInfo with meta-data about a struct. +func (c *cache) create(t reflect.Type, parentAlias string) *structInfo { + info := &structInfo{} + var anonymousInfos []*structInfo + for i := 0; i < t.NumField(); i++ { + if f := c.createField(t.Field(i), parentAlias); f != nil { + info.fields = append(info.fields, f) + if ft := indirectType(f.typ); ft.Kind() == reflect.Struct && f.isAnonymous { + anonymousInfos = append(anonymousInfos, c.create(ft, f.canonicalAlias)) + } + } + } + for i, a := range anonymousInfos { + others := []*structInfo{info} + others = append(others, anonymousInfos[:i]...) + others = append(others, anonymousInfos[i+1:]...) + for _, f := range a.fields { + if !containsAlias(others, f.alias) { + info.fields = append(info.fields, f) + } + } + } + return info +} + +// createField creates a fieldInfo for the given field. +func (c *cache) createField(field reflect.StructField, parentAlias string) *fieldInfo { + alias, options := fieldAlias(field, c.tag) + if alias == "-" { + // Ignore this field. + return nil + } + canonicalAlias := alias + if parentAlias != "" { + canonicalAlias = parentAlias + "." + alias + } + // Check if the type is supported and don't cache it if not. + // First let's get the basic type. + isSlice, isStruct := false, false + ft := field.Type + m := isTextUnmarshaler(reflect.Zero(ft)) + if ft.Kind() == reflect.Ptr { + ft = ft.Elem() + } + if isSlice = ft.Kind() == reflect.Slice; isSlice { + ft = ft.Elem() + if ft.Kind() == reflect.Ptr { + ft = ft.Elem() + } + } + if ft.Kind() == reflect.Array { + ft = ft.Elem() + if ft.Kind() == reflect.Ptr { + ft = ft.Elem() + } + } + if isStruct = ft.Kind() == reflect.Struct; !isStruct { + if c.converter(ft) == nil && builtinConverters[ft.Kind()] == nil { + // Type is not supported. + return nil + } + } + + return &fieldInfo{ + typ: field.Type, + name: field.Name, + alias: alias, + canonicalAlias: canonicalAlias, + unmarshalerInfo: m, + isSliceOfStructs: isSlice && isStruct, + isAnonymous: field.Anonymous, + isRequired: options.Contains("required"), + } +} + +// converter returns the converter for a type. +func (c *cache) converter(t reflect.Type) Converter { + return c.regconv[t] +} + +// ---------------------------------------------------------------------------- + +type structInfo struct { + fields []*fieldInfo +} + +func (i *structInfo) get(alias string) *fieldInfo { + for _, field := range i.fields { + if strings.EqualFold(field.alias, alias) { + return field + } + } + return nil +} + +func containsAlias(infos []*structInfo, alias string) bool { + for _, info := range infos { + if info.get(alias) != nil { + return true + } + } + return false +} + +type fieldInfo struct { + typ reflect.Type + // name is the field name in the struct. + name string + alias string + // canonicalAlias is almost the same as the alias, but is prefixed with + // an embedded struct field alias in dotted notation if this field is + // promoted from the struct. + // For instance, if the alias is "N" and this field is an embedded field + // in a struct "X", canonicalAlias will be "X.N". + canonicalAlias string + // unmarshalerInfo contains information regarding the + // encoding.TextUnmarshaler implementation of the field type. + unmarshalerInfo unmarshaler + // isSliceOfStructs indicates if the field type is a slice of structs. + isSliceOfStructs bool + // isAnonymous indicates whether the field is embedded in the struct. + isAnonymous bool + isRequired bool +} + +func (f *fieldInfo) paths(prefix string) []string { + if f.alias == f.canonicalAlias { + return []string{prefix + f.alias} + } + return []string{prefix + f.alias, prefix + f.canonicalAlias} +} + +type pathPart struct { + field *fieldInfo + path []string // path to the field: walks structs using field names. + index int // struct index in slices of structs. +} + +// ---------------------------------------------------------------------------- + +func indirectType(typ reflect.Type) reflect.Type { + if typ.Kind() == reflect.Ptr { + return typ.Elem() + } + return typ +} + +// fieldAlias parses a field tag to get a field alias. +func fieldAlias(field reflect.StructField, tagName string) (alias string, options tagOptions) { + if tag := field.Tag.Get(tagName); tag != "" { + alias, options = parseTag(tag) + } + if alias == "" { + alias = field.Name + } + return alias, options +} + +// tagOptions is the string following a comma in a struct field's tag, or +// the empty string. It does not include the leading comma. +type tagOptions []string + +// parseTag splits a struct field's url tag into its name and comma-separated +// options. +func parseTag(tag string) (string, tagOptions) { + s := strings.Split(tag, ",") + return s[0], s[1:] +} + +// Contains checks whether the tagOptions contains the specified option. +func (o tagOptions) Contains(option string) bool { + for _, s := range o { + if s == option { + return true + } + } + return false +} diff --git a/internal/schema/converter.go b/internal/schema/converter.go new file mode 100644 index 0000000..4f2116a --- /dev/null +++ b/internal/schema/converter.go @@ -0,0 +1,145 @@ +// Copyright 2012 The Gorilla Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package schema + +import ( + "reflect" + "strconv" +) + +type Converter func(string) reflect.Value + +var ( + invalidValue = reflect.Value{} + boolType = reflect.Bool + float32Type = reflect.Float32 + float64Type = reflect.Float64 + intType = reflect.Int + int8Type = reflect.Int8 + int16Type = reflect.Int16 + int32Type = reflect.Int32 + int64Type = reflect.Int64 + stringType = reflect.String + uintType = reflect.Uint + uint8Type = reflect.Uint8 + uint16Type = reflect.Uint16 + uint32Type = reflect.Uint32 + uint64Type = reflect.Uint64 +) + +// Default converters for basic types. +var builtinConverters = map[reflect.Kind]Converter{ + boolType: convertBool, + float32Type: convertFloat32, + float64Type: convertFloat64, + intType: convertInt, + int8Type: convertInt8, + int16Type: convertInt16, + int32Type: convertInt32, + int64Type: convertInt64, + stringType: convertString, + uintType: convertUint, + uint8Type: convertUint8, + uint16Type: convertUint16, + uint32Type: convertUint32, + uint64Type: convertUint64, +} + +func convertBool(value string) reflect.Value { + if value == "on" { + return reflect.ValueOf(true) + } else if v, err := strconv.ParseBool(value); err == nil { + return reflect.ValueOf(v) + } + return invalidValue +} + +func convertFloat32(value string) reflect.Value { + if v, err := strconv.ParseFloat(value, 32); err == nil { + return reflect.ValueOf(float32(v)) + } + return invalidValue +} + +func convertFloat64(value string) reflect.Value { + if v, err := strconv.ParseFloat(value, 64); err == nil { + return reflect.ValueOf(v) + } + return invalidValue +} + +func convertInt(value string) reflect.Value { + if v, err := strconv.ParseInt(value, 10, 0); err == nil { + return reflect.ValueOf(int(v)) + } + return invalidValue +} + +func convertInt8(value string) reflect.Value { + if v, err := strconv.ParseInt(value, 10, 8); err == nil { + return reflect.ValueOf(int8(v)) + } + return invalidValue +} + +func convertInt16(value string) reflect.Value { + if v, err := strconv.ParseInt(value, 10, 16); err == nil { + return reflect.ValueOf(int16(v)) + } + return invalidValue +} + +func convertInt32(value string) reflect.Value { + if v, err := strconv.ParseInt(value, 10, 32); err == nil { + return reflect.ValueOf(int32(v)) + } + return invalidValue +} + +func convertInt64(value string) reflect.Value { + if v, err := strconv.ParseInt(value, 10, 64); err == nil { + return reflect.ValueOf(v) + } + return invalidValue +} + +func convertString(value string) reflect.Value { + return reflect.ValueOf(value) +} + +func convertUint(value string) reflect.Value { + if v, err := strconv.ParseUint(value, 10, 0); err == nil { + return reflect.ValueOf(uint(v)) + } + return invalidValue +} + +func convertUint8(value string) reflect.Value { + if v, err := strconv.ParseUint(value, 10, 8); err == nil { + return reflect.ValueOf(uint8(v)) + } + return invalidValue +} + +func convertUint16(value string) reflect.Value { + if v, err := strconv.ParseUint(value, 10, 16); err == nil { + return reflect.ValueOf(uint16(v)) + } + return invalidValue +} + +func convertUint32(value string) reflect.Value { + if v, err := strconv.ParseUint(value, 10, 32); err == nil { + return reflect.ValueOf(uint32(v)) + } + return invalidValue +} + +func convertUint64(value string) reflect.Value { + if v, err := strconv.ParseUint(value, 10, 64); err == nil { + return reflect.ValueOf(v) + } + return invalidValue +} diff --git a/internal/schema/decoder.go b/internal/schema/decoder.go new file mode 100644 index 0000000..b63c45e --- /dev/null +++ b/internal/schema/decoder.go @@ -0,0 +1,534 @@ +// Copyright 2012 The Gorilla Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package schema + +import ( + "encoding" + "errors" + "fmt" + "reflect" + "strings" +) + +// NewDecoder returns a new Decoder. +func NewDecoder() *Decoder { + return &Decoder{cache: newCache()} +} + +// Decoder decodes values from a map[string][]string to a struct. +type Decoder struct { + cache *cache + zeroEmpty bool + ignoreUnknownKeys bool +} + +// SetAliasTag changes the tag used to locate custom field aliases. +// The default tag is "schema". +func (d *Decoder) SetAliasTag(tag string) { + d.cache.tag = tag +} + +// ZeroEmpty controls the behaviour when the decoder encounters empty values +// in a map. +// If z is true and a key in the map has the empty string as a value +// then the corresponding struct field is set to the zero value. +// If z is false then empty strings are ignored. +// +// The default value is false, that is empty values do not change +// the value of the struct field. +func (d *Decoder) ZeroEmpty(z bool) { + d.zeroEmpty = z +} + +// IgnoreUnknownKeys controls the behaviour when the decoder encounters unknown +// keys in the map. +// If i is true and an unknown field is encountered, it is ignored. This is +// similar to how unknown keys are handled by encoding/json. +// If i is false then Decode will return an error. Note that any valid keys +// will still be decoded in to the target struct. +// +// To preserve backwards compatibility, the default value is false. +func (d *Decoder) IgnoreUnknownKeys(i bool) { + d.ignoreUnknownKeys = i +} + +// RegisterConverter registers a converter function for a custom type. +func (d *Decoder) RegisterConverter(value interface{}, converterFunc Converter) { + d.cache.registerConverter(value, converterFunc) +} + +// Decode decodes a map[string][]string to a struct. +// +// The first parameter must be a pointer to a struct. +// +// The second parameter is a map, typically url.Values from an HTTP request. +// Keys are "paths" in dotted notation to the struct fields and nested structs. +// +// See the package documentation for a full explanation of the mechanics. +func (d *Decoder) Decode(dst interface{}, src map[string][]string) error { + v := reflect.ValueOf(dst) + if v.Kind() != reflect.Ptr || v.Elem().Kind() != reflect.Struct { + return errors.New("schema: interface must be a pointer to struct") + } + v = v.Elem() + t := v.Type() + multiError := MultiError{} + for path, values := range src { + if parts, err := d.cache.parsePath(path, t); err == nil { + if err = d.decode(v, path, parts, values); err != nil { + multiError[path] = err + } + } else if !d.ignoreUnknownKeys { + multiError[path] = UnknownKeyError{Key: path} + } + } + multiError.merge(d.checkRequired(t, src)) + if len(multiError) > 0 { + return multiError + } + return nil +} + +// checkRequired checks whether required fields are empty +// +// check type t recursively if t has struct fields. +// +// src is the source map for decoding, we use it here to see if those required fields are included in src +func (d *Decoder) checkRequired(t reflect.Type, src map[string][]string) MultiError { + m, errs := d.findRequiredFields(t, "", "") + for key, fields := range m { + if isEmptyFields(fields, src) { + errs[key] = EmptyFieldError{Key: key} + } + } + return errs +} + +// findRequiredFields recursively searches the struct type t for required fields. +// +// canonicalPrefix and searchPrefix are used to resolve full paths in dotted notation +// for nested struct fields. canonicalPrefix is a complete path which never omits +// any embedded struct fields. searchPrefix is a user-friendly path which may omit +// some embedded struct fields to point promoted fields. +func (d *Decoder) findRequiredFields(t reflect.Type, canonicalPrefix, searchPrefix string) (map[string][]fieldWithPrefix, MultiError) { + struc := d.cache.get(t) + if struc == nil { + // unexpect, cache.get never return nil + return nil, MultiError{canonicalPrefix + "*": errors.New("cache fail")} + } + + m := map[string][]fieldWithPrefix{} + errs := MultiError{} + for _, f := range struc.fields { + if f.typ.Kind() == reflect.Struct { + fcprefix := canonicalPrefix + f.canonicalAlias + "." + for _, fspath := range f.paths(searchPrefix) { + fm, ferrs := d.findRequiredFields(f.typ, fcprefix, fspath+".") + for key, fields := range fm { + m[key] = append(m[key], fields...) + } + errs.merge(ferrs) + } + } + if f.isRequired { + key := canonicalPrefix + f.canonicalAlias + m[key] = append(m[key], fieldWithPrefix{ + fieldInfo: f, + prefix: searchPrefix, + }) + } + } + return m, errs +} + +type fieldWithPrefix struct { + *fieldInfo + prefix string +} + +// isEmptyFields returns true if all of specified fields are empty. +func isEmptyFields(fields []fieldWithPrefix, src map[string][]string) bool { + for _, f := range fields { + for _, path := range f.paths(f.prefix) { + v, ok := src[path] + if ok && !isEmpty(f.typ, v) { + return false + } + for key := range src { + // issue references: + // https://github.com/gofiber/fiber/issues/1414 + // https://github.com/gorilla/schema/issues/176 + nested := strings.IndexByte(key, '.') != -1 + + // for non required nested structs + c1 := strings.HasSuffix(f.prefix, ".") && key == path + + // for required nested structs + c2 := f.prefix == "" && nested && strings.HasPrefix(key, path) + + // for non nested fields + c3 := f.prefix == "" && !nested && key == path + if !isEmpty(f.typ, src[key]) && (c1 || c2 || c3) { + return false + } + } + } + } + return true +} + +// isEmpty returns true if value is empty for specific type +func isEmpty(t reflect.Type, value []string) bool { + if len(value) == 0 { + return true + } + switch t.Kind() { + case boolType, float32Type, float64Type, intType, int8Type, int32Type, int64Type, stringType, uint8Type, uint16Type, uint32Type, uint64Type: + return len(value[0]) == 0 + } + return false +} + +// decode fills a struct field using a parsed path. +func (d *Decoder) decode(v reflect.Value, path string, parts []pathPart, values []string) error { + // Get the field walking the struct fields by index. + for _, name := range parts[0].path { + if v.Type().Kind() == reflect.Ptr { + if v.IsNil() { + v.Set(reflect.New(v.Type().Elem())) + } + v = v.Elem() + } + + // alloc embedded structs + if v.Type().Kind() == reflect.Struct { + for i := 0; i < v.NumField(); i++ { + field := v.Field(i) + if field.Type().Kind() == reflect.Ptr && field.IsNil() && v.Type().Field(i).Anonymous { + field.Set(reflect.New(field.Type().Elem())) + } + } + } + + v = v.FieldByName(name) + } + // Don't even bother for unexported fields. + if !v.CanSet() { + return nil + } + + // Dereference if needed. + t := v.Type() + if t.Kind() == reflect.Ptr { + t = t.Elem() + if v.IsNil() { + v.Set(reflect.New(t)) + } + v = v.Elem() + } + + // Slice of structs. Let's go recursive. + if len(parts) > 1 { + idx := parts[0].index + if v.IsNil() || v.Len() < idx+1 { + value := reflect.MakeSlice(t, idx+1, idx+1) + if v.Len() < idx+1 { + // Resize it. + reflect.Copy(value, v) + } + v.Set(value) + } + return d.decode(v.Index(idx), path, parts[1:], values) + } + + // Get the converter early in case there is one for a slice type. + conv := d.cache.converter(t) + m := isTextUnmarshaler(v) + if conv == nil && t.Kind() == reflect.Slice && m.IsSliceElement { + var items []reflect.Value + elemT := t.Elem() + isPtrElem := elemT.Kind() == reflect.Ptr + if isPtrElem { + elemT = elemT.Elem() + } + + // Try to get a converter for the element type. + conv := d.cache.converter(elemT) + if conv == nil { + conv = builtinConverters[elemT.Kind()] + if conv == nil { + // As we are not dealing with slice of structs here, we don't need to check if the type + // implements TextUnmarshaler interface + return fmt.Errorf("schema: converter not found for %v", elemT) + } + } + + for key, value := range values { + if value == "" { + if d.zeroEmpty { + items = append(items, reflect.Zero(elemT)) + } + } else if m.IsValid { + u := reflect.New(elemT) + if m.IsSliceElementPtr { + u = reflect.New(reflect.PtrTo(elemT).Elem()) + } + if err := u.Interface().(encoding.TextUnmarshaler).UnmarshalText([]byte(value)); err != nil { + return ConversionError{ + Key: path, + Type: t, + Index: key, + Err: err, + } + } + if m.IsSliceElementPtr { + items = append(items, u.Elem().Addr()) + } else if u.Kind() == reflect.Ptr { + items = append(items, u.Elem()) + } else { + items = append(items, u) + } + } else if item := conv(value); item.IsValid() { + if isPtrElem { + ptr := reflect.New(elemT) + ptr.Elem().Set(item) + item = ptr + } + if item.Type() != elemT && !isPtrElem { + item = item.Convert(elemT) + } + items = append(items, item) + } else { + if strings.Contains(value, ",") { + values := strings.Split(value, ",") + for _, value := range values { + if value == "" { + if d.zeroEmpty { + items = append(items, reflect.Zero(elemT)) + } + } else if item := conv(value); item.IsValid() { + if isPtrElem { + ptr := reflect.New(elemT) + ptr.Elem().Set(item) + item = ptr + } + if item.Type() != elemT && !isPtrElem { + item = item.Convert(elemT) + } + items = append(items, item) + } else { + return ConversionError{ + Key: path, + Type: elemT, + Index: key, + } + } + } + } else { + return ConversionError{ + Key: path, + Type: elemT, + Index: key, + } + } + } + } + value := reflect.Append(reflect.MakeSlice(t, 0, 0), items...) + v.Set(value) + } else { + val := "" + // Use the last value provided if any values were provided + if len(values) > 0 { + val = values[len(values)-1] + } + + if conv != nil { + if value := conv(val); value.IsValid() { + v.Set(value.Convert(t)) + } else { + return ConversionError{ + Key: path, + Type: t, + Index: -1, + } + } + } else if m.IsValid { + if m.IsPtr { + u := reflect.New(v.Type()) + if err := u.Interface().(encoding.TextUnmarshaler).UnmarshalText([]byte(val)); err != nil { + return ConversionError{ + Key: path, + Type: t, + Index: -1, + Err: err, + } + } + v.Set(reflect.Indirect(u)) + } else { + // If the value implements the encoding.TextUnmarshaler interface + // apply UnmarshalText as the converter + if err := m.Unmarshaler.UnmarshalText([]byte(val)); err != nil { + return ConversionError{ + Key: path, + Type: t, + Index: -1, + Err: err, + } + } + } + } else if val == "" { + if d.zeroEmpty { + v.Set(reflect.Zero(t)) + } + } else if conv := builtinConverters[t.Kind()]; conv != nil { + if value := conv(val); value.IsValid() { + v.Set(value.Convert(t)) + } else { + return ConversionError{ + Key: path, + Type: t, + Index: -1, + } + } + } else { + return fmt.Errorf("schema: converter not found for %v", t) + } + } + return nil +} + +func isTextUnmarshaler(v reflect.Value) unmarshaler { + // Create a new unmarshaller instance + m := unmarshaler{} + if m.Unmarshaler, m.IsValid = v.Interface().(encoding.TextUnmarshaler); m.IsValid { + return m + } + // As the UnmarshalText function should be applied to the pointer of the + // type, we check that type to see if it implements the necessary + // method. + if m.Unmarshaler, m.IsValid = reflect.New(v.Type()).Interface().(encoding.TextUnmarshaler); m.IsValid { + m.IsPtr = true + return m + } + + // if v is []T or *[]T create new T + t := v.Type() + if t.Kind() == reflect.Ptr { + t = t.Elem() + } + if t.Kind() == reflect.Slice { + // Check if the slice implements encoding.TextUnmarshaller + if m.Unmarshaler, m.IsValid = v.Interface().(encoding.TextUnmarshaler); m.IsValid { + return m + } + // If t is a pointer slice, check if its elements implement + // encoding.TextUnmarshaler + m.IsSliceElement = true + if t = t.Elem(); t.Kind() == reflect.Ptr { + t = reflect.PtrTo(t.Elem()) + v = reflect.Zero(t) + m.IsSliceElementPtr = true + m.Unmarshaler, m.IsValid = v.Interface().(encoding.TextUnmarshaler) + return m + } + } + + v = reflect.New(t) + m.Unmarshaler, m.IsValid = v.Interface().(encoding.TextUnmarshaler) + return m +} + +// TextUnmarshaler helpers ---------------------------------------------------- +// unmarshaller contains information about a TextUnmarshaler type +type unmarshaler struct { + Unmarshaler encoding.TextUnmarshaler + // IsValid indicates whether the resolved type indicated by the other + // flags implements the encoding.TextUnmarshaler interface. + IsValid bool + // IsPtr indicates that the resolved type is the pointer of the original + // type. + IsPtr bool + // IsSliceElement indicates that the resolved type is a slice element of + // the original type. + IsSliceElement bool + // IsSliceElementPtr indicates that the resolved type is a pointer to a + // slice element of the original type. + IsSliceElementPtr bool +} + +// Errors --------------------------------------------------------------------- + +// ConversionError stores information about a failed conversion. +type ConversionError struct { + Key string // key from the source map. + Type reflect.Type // expected type of elem + Index int // index for multi-value fields; -1 for single-value fields. + Err error // low-level error (when it exists) +} + +func (e ConversionError) Error() string { + var output string + + if e.Index < 0 { + output = fmt.Sprintf("schema: error converting value for %q", e.Key) + } else { + output = fmt.Sprintf("schema: error converting value for index %d of %q", + e.Index, e.Key) + } + + if e.Err != nil { + output = fmt.Sprintf("%s. Details: %s", output, e.Err) + } + + return output +} + +// UnknownKeyError stores information about an unknown key in the source map. +type UnknownKeyError struct { + Key string // key from the source map. +} + +func (e UnknownKeyError) Error() string { + return fmt.Sprintf("schema: invalid path %q", e.Key) +} + +// EmptyFieldError stores information about an empty required field. +type EmptyFieldError struct { + Key string // required key in the source map. +} + +func (e EmptyFieldError) Error() string { + return fmt.Sprintf("%v is empty", e.Key) +} + +// MultiError stores multiple decoding errors. +// +// Borrowed from the App Engine SDK. +type MultiError map[string]error + +func (e MultiError) Error() string { + s := "" + for _, err := range e { + s = err.Error() + break + } + switch len(e) { + case 0: + return "(0 errors)" + case 1: + return s + case 2: + return s + " (and 1 other error)" + } + return fmt.Sprintf("%s (and %d other errors)", s, len(e)-1) +} + +func (e MultiError) merge(errors MultiError) { + for key, err := range errors { + if e[key] == nil { + e[key] = err + } + } +} diff --git a/internal/schema/doc.go b/internal/schema/doc.go new file mode 100644 index 0000000..fff0fe7 --- /dev/null +++ b/internal/schema/doc.go @@ -0,0 +1,148 @@ +// Copyright 2012 The Gorilla Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +/* +Package gorilla/schema fills a struct with form values. + +The basic usage is really simple. Given this struct: + + type Person struct { + Name string + Phone string + } + +...we can fill it passing a map to the Decode() function: + + values := map[string][]string{ + "Name": {"John"}, + "Phone": {"999-999-999"}, + } + person := new(Person) + decoder := schema.NewDecoder() + decoder.Decode(person, values) + +This is just a simple example and it doesn't make a lot of sense to create +the map manually. Typically it will come from a http.Request object and +will be of type url.Values, http.Request.Form, or http.Request.MultipartForm: + + func MyHandler(w http.ResponseWriter, r *http.Request) { + err := r.ParseForm() + + if err != nil { + // Handle error + } + + decoder := schema.NewDecoder() + // r.PostForm is a map of our POST form values + err := decoder.Decode(person, r.PostForm) + + if err != nil { + // Handle error + } + + // Do something with person.Name or person.Phone + } + +Note: it is a good idea to set a Decoder instance as a package global, +because it caches meta-data about structs, and an instance can be shared safely: + + var decoder = schema.NewDecoder() + +To define custom names for fields, use a struct tag "schema". To not populate +certain fields, use a dash for the name and it will be ignored: + + type Person struct { + Name string `schema:"name"` // custom name + Phone string `schema:"phone"` // custom name + Admin bool `schema:"-"` // this field is never set + } + +The supported field types in the destination struct are: + + - bool + - float variants (float32, float64) + - int variants (int, int8, int16, int32, int64) + - string + - uint variants (uint, uint8, uint16, uint32, uint64) + - struct + - a pointer to one of the above types + - a slice or a pointer to a slice of one of the above types + +Non-supported types are simply ignored, however custom types can be registered +to be converted. + +To fill nested structs, keys must use a dotted notation as the "path" for the +field. So for example, to fill the struct Person below: + + type Phone struct { + Label string + Number string + } + + type Person struct { + Name string + Phone Phone + } + +...the source map must have the keys "Name", "Phone.Label" and "Phone.Number". +This means that an HTML form to fill a Person struct must look like this: + +
+ + + +
+ +Single values are filled using the first value for a key from the source map. +Slices are filled using all values for a key from the source map. So to fill +a Person with multiple Phone values, like: + + type Person struct { + Name string + Phones []Phone + } + +...an HTML form that accepts three Phone values would look like this: + +
+ + + + + + + +
+ +Notice that only for slices of structs the slice index is required. +This is needed for disambiguation: if the nested struct also had a slice +field, we could not translate multiple values to it if we did not use an +index for the parent struct. + +There's also the possibility to create a custom type that implements the +TextUnmarshaler interface, and in this case there's no need to register +a converter, like: + + type Person struct { + Emails []Email + } + + type Email struct { + *mail.Address + } + + func (e *Email) UnmarshalText(text []byte) (err error) { + e.Address, err = mail.ParseAddress(string(text)) + return + } + +...an HTML form that accepts three Email values would look like this: + +
+ + + +
+*/ +package schema diff --git a/internal/schema/encoder.go b/internal/schema/encoder.go new file mode 100644 index 0000000..c01de00 --- /dev/null +++ b/internal/schema/encoder.go @@ -0,0 +1,202 @@ +package schema + +import ( + "errors" + "fmt" + "reflect" + "strconv" +) + +type encoderFunc func(reflect.Value) string + +// Encoder encodes values from a struct into url.Values. +type Encoder struct { + cache *cache + regenc map[reflect.Type]encoderFunc +} + +// NewEncoder returns a new Encoder with defaults. +func NewEncoder() *Encoder { + return &Encoder{cache: newCache(), regenc: make(map[reflect.Type]encoderFunc)} +} + +// Encode encodes a struct into map[string][]string. +// +// Intended for use with url.Values. +func (e *Encoder) Encode(src interface{}, dst map[string][]string) error { + v := reflect.ValueOf(src) + + return e.encode(v, dst) +} + +// RegisterEncoder registers a converter for encoding a custom type. +func (e *Encoder) RegisterEncoder(value interface{}, encoder func(reflect.Value) string) { + e.regenc[reflect.TypeOf(value)] = encoder +} + +// SetAliasTag changes the tag used to locate custom field aliases. +// The default tag is "schema". +func (e *Encoder) SetAliasTag(tag string) { + e.cache.tag = tag +} + +// isValidStructPointer test if input value is a valid struct pointer. +func isValidStructPointer(v reflect.Value) bool { + return v.Type().Kind() == reflect.Ptr && v.Elem().IsValid() && v.Elem().Type().Kind() == reflect.Struct +} + +func isZero(v reflect.Value) bool { + switch v.Kind() { + case reflect.Func: + case reflect.Map, reflect.Slice: + return v.IsNil() || v.Len() == 0 + case reflect.Array: + z := true + for i := 0; i < v.Len(); i++ { + z = z && isZero(v.Index(i)) + } + return z + case reflect.Struct: + type zero interface { + IsZero() bool + } + if v.Type().Implements(reflect.TypeOf((*zero)(nil)).Elem()) { + iz := v.MethodByName("IsZero").Call([]reflect.Value{})[0] + return iz.Interface().(bool) + } + z := true + for i := 0; i < v.NumField(); i++ { + z = z && isZero(v.Field(i)) + } + return z + } + // Compare other types directly: + z := reflect.Zero(v.Type()) + return v.Interface() == z.Interface() +} + +func (e *Encoder) encode(v reflect.Value, dst map[string][]string) error { + if v.Kind() == reflect.Ptr { + v = v.Elem() + } + if v.Kind() != reflect.Struct { + return errors.New("schema: interface must be a struct") + } + t := v.Type() + + errors := MultiError{} + + for i := 0; i < v.NumField(); i++ { + name, opts := fieldAlias(t.Field(i), e.cache.tag) + if name == "-" { + continue + } + + // Encode struct pointer types if the field is a valid pointer and a struct. + if isValidStructPointer(v.Field(i)) { + _ = e.encode(v.Field(i).Elem(), dst) + continue + } + + encFunc := typeEncoder(v.Field(i).Type(), e.regenc) + + // Encode non-slice types and custom implementations immediately. + if encFunc != nil { + value := encFunc(v.Field(i)) + if opts.Contains("omitempty") && isZero(v.Field(i)) { + continue + } + + dst[name] = append(dst[name], value) + continue + } + + if v.Field(i).Type().Kind() == reflect.Struct { + _ = e.encode(v.Field(i), dst) + continue + } + + if v.Field(i).Type().Kind() == reflect.Slice { + encFunc = typeEncoder(v.Field(i).Type().Elem(), e.regenc) + } + + if encFunc == nil { + errors[v.Field(i).Type().String()] = fmt.Errorf("schema: encoder not found for %v", v.Field(i)) + continue + } + + // Encode a slice. + if v.Field(i).Len() == 0 && opts.Contains("omitempty") { + continue + } + + dst[name] = []string{} + for j := 0; j < v.Field(i).Len(); j++ { + dst[name] = append(dst[name], encFunc(v.Field(i).Index(j))) + } + } + + if len(errors) > 0 { + return errors + } + return nil +} + +func typeEncoder(t reflect.Type, reg map[reflect.Type]encoderFunc) encoderFunc { + if f, ok := reg[t]; ok { + return f + } + + switch t.Kind() { + case reflect.Bool: + return encodeBool + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return encodeInt + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + return encodeUint + case reflect.Float32: + return encodeFloat32 + case reflect.Float64: + return encodeFloat64 + case reflect.Ptr: + f := typeEncoder(t.Elem(), reg) + return func(v reflect.Value) string { + if v.IsNil() { + return "null" + } + return f(v.Elem()) + } + case reflect.String: + return encodeString + default: + return nil + } +} + +func encodeBool(v reflect.Value) string { + return strconv.FormatBool(v.Bool()) +} + +func encodeInt(v reflect.Value) string { + return strconv.FormatInt(int64(v.Int()), 10) +} + +func encodeUint(v reflect.Value) string { + return strconv.FormatUint(uint64(v.Uint()), 10) +} + +func encodeFloat(v reflect.Value, bits int) string { + return strconv.FormatFloat(v.Float(), 'f', 6, bits) +} + +func encodeFloat32(v reflect.Value) string { + return encodeFloat(v, 32) +} + +func encodeFloat64(v reflect.Value) string { + return encodeFloat(v, 64) +} + +func encodeString(v reflect.Value) string { + return v.String() +} diff --git a/internal/storage/memory/config.go b/internal/storage/memory/config.go new file mode 100644 index 0000000..07d13ed --- /dev/null +++ b/internal/storage/memory/config.go @@ -0,0 +1,33 @@ +package memory + +import "time" + +// Config defines the config for storage. +type Config struct { + // Time before deleting expired keys + // + // Default is 10 * time.Second + GCInterval time.Duration +} + +// ConfigDefault is the default config +var ConfigDefault = Config{ + GCInterval: 10 * time.Second, +} + +// configDefault is a helper function to set default values +func configDefault(config ...Config) Config { + // Return default config if nothing provided + if len(config) < 1 { + return ConfigDefault + } + + // Override default config + cfg := config[0] + + // Set default values + if int(cfg.GCInterval.Seconds()) <= 0 { + cfg.GCInterval = ConfigDefault.GCInterval + } + return cfg +} diff --git a/internal/storage/memory/memory.go b/internal/storage/memory/memory.go new file mode 100644 index 0000000..b3f70e6 --- /dev/null +++ b/internal/storage/memory/memory.go @@ -0,0 +1,145 @@ +// Package memory Is a copy of the storage memory from the external storage packet as a purpose to test the behavior +// in the unittests when using a storages from these packets +package memory + +import ( + "sync" + "sync/atomic" + "time" + + "github.com/gofiber/fiber/v2/utils" +) + +// Storage interface that is implemented by storage providers +type Storage struct { + mux sync.RWMutex + db map[string]entry + gcInterval time.Duration + done chan struct{} +} + +type entry struct { + data []byte + // max value is 4294967295 -> Sun Feb 07 2106 06:28:15 GMT+0000 + expiry uint32 +} + +// New creates a new memory storage +func New(config ...Config) *Storage { + // Set default config + cfg := configDefault(config...) + + // Create storage + store := &Storage{ + db: make(map[string]entry), + gcInterval: cfg.GCInterval, + done: make(chan struct{}), + } + + // Start garbage collector + utils.StartTimeStampUpdater() + go store.gc() + + return store +} + +// Get value by key +func (s *Storage) Get(key string) ([]byte, error) { + if len(key) <= 0 { + return nil, nil + } + s.mux.RLock() + v, ok := s.db[key] + s.mux.RUnlock() + if !ok || v.expiry != 0 && v.expiry <= atomic.LoadUint32(&utils.Timestamp) { + return nil, nil + } + + return v.data, nil +} + +// Set key with value +func (s *Storage) Set(key string, val []byte, exp time.Duration) error { + // Ain't Nobody Got Time For That + if len(key) <= 0 || len(val) <= 0 { + return nil + } + + var expire uint32 + if exp != 0 { + expire = uint32(exp.Seconds()) + atomic.LoadUint32(&utils.Timestamp) + } + + e := entry{val, expire} + s.mux.Lock() + s.db[key] = e + s.mux.Unlock() + return nil +} + +// Delete key by key +func (s *Storage) Delete(key string) error { + // Ain't Nobody Got Time For That + if len(key) <= 0 { + return nil + } + s.mux.Lock() + delete(s.db, key) + s.mux.Unlock() + return nil +} + +// Reset all keys +func (s *Storage) Reset() error { + ndb := make(map[string]entry) + s.mux.Lock() + s.db = ndb + s.mux.Unlock() + return nil +} + +// Close the memory storage +func (s *Storage) Close() error { + s.done <- struct{}{} + return nil +} + +func (s *Storage) gc() { + ticker := time.NewTicker(s.gcInterval) + defer ticker.Stop() + var expired []string + + for { + select { + case <-s.done: + return + case <-ticker.C: + ts := atomic.LoadUint32(&utils.Timestamp) + expired = expired[:0] + s.mux.RLock() + for id, v := range s.db { + if v.expiry != 0 && v.expiry <= ts { + expired = append(expired, id) + } + } + s.mux.RUnlock() + s.mux.Lock() + // Double-checked locking. + // We might have replaced the item in the meantime. + for i := range expired { + v := s.db[expired[i]] + if v.expiry != 0 && v.expiry <= ts { + delete(s.db, expired[i]) + } + } + s.mux.Unlock() + } + } +} + +// Return database client +func (s *Storage) Conn() map[string]entry { + s.mux.RLock() + defer s.mux.RUnlock() + return s.db +} diff --git a/internal/storage/memory/memory_test.go b/internal/storage/memory/memory_test.go new file mode 100644 index 0000000..7269242 --- /dev/null +++ b/internal/storage/memory/memory_test.go @@ -0,0 +1,158 @@ +package memory + +import ( + "testing" + "time" + + "github.com/gofiber/fiber/v2/utils" +) + +var testStore = New() + +func Test_Storage_Memory_Set(t *testing.T) { + t.Parallel() + var ( + key = "john" + val = []byte("doe") + ) + + err := testStore.Set(key, val, 0) + utils.AssertEqual(t, nil, err) +} + +func Test_Storage_Memory_Set_Override(t *testing.T) { + t.Parallel() + var ( + key = "john" + val = []byte("doe") + ) + + err := testStore.Set(key, val, 0) + utils.AssertEqual(t, nil, err) + + err = testStore.Set(key, val, 0) + utils.AssertEqual(t, nil, err) +} + +func Test_Storage_Memory_Get(t *testing.T) { + t.Parallel() + var ( + key = "john" + val = []byte("doe") + ) + + err := testStore.Set(key, val, 0) + utils.AssertEqual(t, nil, err) + + result, err := testStore.Get(key) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, val, result) +} + +func Test_Storage_Memory_Set_Expiration(t *testing.T) { + t.Parallel() + var ( + key = "john" + val = []byte("doe") + exp = 1 * time.Second + ) + + err := testStore.Set(key, val, exp) + utils.AssertEqual(t, nil, err) + + time.Sleep(1100 * time.Millisecond) +} + +func Test_Storage_Memory_Get_Expired(t *testing.T) { + key := "john" + + result, err := testStore.Get(key) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, true, len(result) == 0) +} + +func Test_Storage_Memory_Get_NotExist(t *testing.T) { + t.Parallel() + + result, err := testStore.Get("notexist") + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, true, len(result) == 0) +} + +func Test_Storage_Memory_Delete(t *testing.T) { + t.Parallel() + var ( + key = "john" + val = []byte("doe") + ) + + err := testStore.Set(key, val, 0) + utils.AssertEqual(t, nil, err) + + err = testStore.Delete(key) + utils.AssertEqual(t, nil, err) + + result, err := testStore.Get(key) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, true, len(result) == 0) +} + +func Test_Storage_Memory_Reset(t *testing.T) { + t.Parallel() + val := []byte("doe") + + err := testStore.Set("john1", val, 0) + utils.AssertEqual(t, nil, err) + + err = testStore.Set("john2", val, 0) + utils.AssertEqual(t, nil, err) + + err = testStore.Reset() + utils.AssertEqual(t, nil, err) + + result, err := testStore.Get("john1") + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, true, len(result) == 0) + + result, err = testStore.Get("john2") + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, true, len(result) == 0) +} + +func Test_Storage_Memory_Close(t *testing.T) { + t.Parallel() + utils.AssertEqual(t, nil, testStore.Close()) +} + +func Test_Storage_Memory_Conn(t *testing.T) { + t.Parallel() + utils.AssertEqual(t, true, testStore.Conn() != nil) +} + +// go test -v -run=^$ -bench=Benchmark_Storage_Memory -benchmem -count=4 +func Benchmark_Storage_Memory(b *testing.B) { + keyLength := 1000 + keys := make([]string, keyLength) + for i := 0; i < keyLength; i++ { + keys[i] = utils.UUID() + } + value := []byte("joe") + + ttl := 2 * time.Second + b.Run("fiber_memory", func(b *testing.B) { + d := New() + b.ReportAllocs() + b.ResetTimer() + for n := 0; n < b.N; n++ { + for _, key := range keys { + d.Set(key, value, ttl) + } + for _, key := range keys { + _, _ = d.Get(key) + } + for _, key := range keys { + d.Delete(key) + } + } + }) +} diff --git a/internal/template/html/html.go b/internal/template/html/html.go new file mode 100644 index 0000000..98e2826 --- /dev/null +++ b/internal/template/html/html.go @@ -0,0 +1,206 @@ +package html + +import ( + "fmt" + "html/template" + "io" + "net/http" + "os" + "path/filepath" + "strings" + "sync" + + "github.com/gofiber/fiber/v2/internal/template/utils" + "github.com/gofiber/fiber/v2/log" +) + +// Engine struct +type Engine struct { + // delimiters + left string + right string + // views folder + directory string + // http.FileSystem supports embedded files + fileSystem http.FileSystem + // views extension + extension string + // layout variable name that incapsulates the template + layout string + // determines if the engine parsed all templates + loaded bool + // reload on each render + reload bool + // debug prints the parsed templates + debug bool + // lock for funcmap and templates + mutex sync.RWMutex + // template funcmap + funcmap map[string]interface{} + // templates + Templates *template.Template +} + +// New returns a HTML render engine for Fiber +func New(directory, extension string) *Engine { + engine := &Engine{ + left: "{{", + right: "}}", + directory: directory, + extension: extension, + layout: "embed", + funcmap: make(map[string]interface{}), + } + engine.AddFunc(engine.layout, func() error { + return fmt.Errorf("layout called unexpectedly.") + }) + return engine +} + +// NewFileSystem ... +func NewFileSystem(fs http.FileSystem, extension string) *Engine { + engine := &Engine{ + left: "{{", + right: "}}", + directory: "/", + fileSystem: fs, + extension: extension, + layout: "embed", + funcmap: make(map[string]interface{}), + } + engine.AddFunc(engine.layout, func() error { + return fmt.Errorf("layout called unexpectedly.") + }) + return engine +} + +// Layout defines the variable name that will incapsulate the template +func (e *Engine) Layout(key string) *Engine { + e.layout = key + return e +} + +// Delims sets the action delimiters to the specified strings, to be used in +// templates. An empty delimiter stands for the +// corresponding default: {{ or }}. +func (e *Engine) Delims(left, right string) *Engine { + e.left, e.right = left, right + return e +} + +// AddFunc adds the function to the template's function map. +// It is legal to overwrite elements of the default actions +func (e *Engine) AddFunc(name string, fn interface{}) *Engine { + e.mutex.Lock() + e.funcmap[name] = fn + e.mutex.Unlock() + return e +} + +// Reload if set to true the templates are reloading on each render, +// use it when you're in development and you don't want to restart +// the application when you edit a template file. +func (e *Engine) Reload(enabled bool) *Engine { + e.reload = enabled + return e +} + +// Debug will print the parsed templates when Load is triggered. +func (e *Engine) Debug(enabled bool) *Engine { + e.debug = enabled + return e +} + +// Parse is deprecated, please use Load() instead +func (e *Engine) Parse() error { + log.Warn("Parse() is deprecated, please use Load() instead.") + return e.Load() +} + +// Load parses the templates to the engine. +func (e *Engine) Load() error { + if e.loaded { + return nil + } + // race safe + e.mutex.Lock() + defer e.mutex.Unlock() + e.Templates = template.New(e.directory) + + // Set template settings + e.Templates.Delims(e.left, e.right) + e.Templates.Funcs(e.funcmap) + + walkFn := func(path string, info os.FileInfo, err error) error { + // Return error if exist + if err != nil { + return err + } + // Skip file if it's a directory or has no file info + if info == nil || info.IsDir() { + return nil + } + // Skip file if it does not equal the given template extension + if len(e.extension) >= len(path) || path[len(path)-len(e.extension):] != e.extension { + return nil + } + // Get the relative file path + // ./views/html/index.tmpl -> index.tmpl + rel, err := filepath.Rel(e.directory, path) + if err != nil { + return err + } + // Reverse slashes '\' -> '/' and + // partials\footer.tmpl -> partials/footer.tmpl + name := filepath.ToSlash(rel) + // Remove ext from name 'index.tmpl' -> 'index' + name = strings.TrimSuffix(name, e.extension) + // name = strings.Replace(name, e.extension, "", -1) + // Read the file + // #gosec G304 + buf, err := utils.ReadFile(path, e.fileSystem) + if err != nil { + return err + } + // Create new template associated with the current one + // This enable use to invoke other templates {{ template .. }} + _, err = e.Templates.New(name).Parse(string(buf)) + if err != nil { + return err + } + // Debugging + if e.debug { + log.Infof("views: parsed template: %s", name) + } + return err + } + // notify engine that we parsed all templates + e.loaded = true + if e.fileSystem != nil { + return utils.Walk(e.fileSystem, e.directory, walkFn) + } + return filepath.Walk(e.directory, walkFn) +} + +// Render will execute the template name along with the given values. +func (e *Engine) Render(out io.Writer, template string, binding interface{}, layout ...string) error { + tmpl := e.Templates.Lookup(template) + if tmpl == nil { + return fmt.Errorf("render: template %s does not exist", template) + } + if len(layout) > 0 && layout[0] != "" { + lay := e.Templates.Lookup(layout[0]) + if lay == nil { + return fmt.Errorf("render: layout %s does not exist", layout[0]) + } + e.mutex.Lock() + defer e.mutex.Unlock() + lay.Funcs(map[string]interface{}{ + e.layout: func() error { + return tmpl.Execute(out, binding) + }, + }) + return lay.Execute(out, binding) + } + return tmpl.Execute(out, binding) +} diff --git a/internal/template/utils/utils.go b/internal/template/utils/utils.go new file mode 100644 index 0000000..0ae8f22 --- /dev/null +++ b/internal/template/utils/utils.go @@ -0,0 +1,110 @@ +package utils + +import ( + "io" + "net/http" + "os" + pathpkg "path" + "path/filepath" + "sort" +) + +// Walk walks the filesystem rooted at root, calling walkFn for each file or +// directory in the filesystem, including root. All errors that arise visiting files +// and directories are filtered by walkFn. The files are walked in lexical +// order. +func Walk(fs http.FileSystem, root string, walkFn filepath.WalkFunc) error { + info, err := stat(fs, root) + if err != nil { + return walkFn(root, nil, err) + } + return walk(fs, root, info, walkFn) +} + +// #nosec G304 +// ReadFile returns the raw content of a file +func ReadFile(path string, fs http.FileSystem) ([]byte, error) { + if fs != nil { + file, err := fs.Open(path) + if err != nil { + return nil, err + } + defer file.Close() + return io.ReadAll(file) + } + return os.ReadFile(path) +} + +// readDirNames reads the directory named by dirname and returns +// a sorted list of directory entries. +func readDirNames(fs http.FileSystem, dirname string) ([]string, error) { + fis, err := readDir(fs, dirname) + if err != nil { + return nil, err + } + names := make([]string, len(fis)) + for i := range fis { + names[i] = fis[i].Name() + } + sort.Strings(names) + return names, nil +} + +// walk recursively descends path, calling walkFn. +func walk(fs http.FileSystem, path string, info os.FileInfo, walkFn filepath.WalkFunc) error { + err := walkFn(path, info, nil) + if err != nil { + if info.IsDir() && err == filepath.SkipDir { + return nil + } + return err + } + + if !info.IsDir() { + return nil + } + + names, err := readDirNames(fs, path) + if err != nil { + return walkFn(path, info, err) + } + + for _, name := range names { + filename := pathpkg.Join(path, name) + fileInfo, err := stat(fs, filename) + if err != nil { + if err := walkFn(filename, fileInfo, err); err != nil && err != filepath.SkipDir { + return err + } + } else { + err = walk(fs, filename, fileInfo, walkFn) + if err != nil { + if !fileInfo.IsDir() || err != filepath.SkipDir { + return err + } + } + } + } + return nil +} + +// readDir reads the contents of the directory associated with file and +// returns a slice of FileInfo values in directory order. +func readDir(fs http.FileSystem, name string) ([]os.FileInfo, error) { + f, err := fs.Open(name) + if err != nil { + return nil, err + } + defer f.Close() + return f.Readdir(0) +} + +// stat returns the FileInfo structure describing file. +func stat(fs http.FileSystem, name string) (os.FileInfo, error) { + f, err := fs.Open(name) + if err != nil { + return nil, err + } + defer f.Close() + return f.Stat() +} diff --git a/internal/tlstest/tls.go b/internal/tlstest/tls.go new file mode 100644 index 0000000..c3a5357 --- /dev/null +++ b/internal/tlstest/tls.go @@ -0,0 +1,118 @@ +package tlstest + +import ( + "bytes" + "crypto/rand" + "crypto/rsa" + "crypto/tls" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "math/big" + "net" + "time" +) + +func GetTLSConfigs() (serverTLSConf, clientTLSConf *tls.Config, err error) { + // set up our CA certificate + ca := &x509.Certificate{ + SerialNumber: big.NewInt(2021), + Subject: pkix.Name{ + Organization: []string{"Fiber"}, + Country: []string{"NL"}, + Province: []string{""}, + Locality: []string{"Amsterdam"}, + StreetAddress: []string{"Huidenstraat"}, + PostalCode: []string{"1011 AA"}, + }, + NotBefore: time.Now(), + NotAfter: time.Now().AddDate(10, 0, 0), + IsCA: true, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth}, + KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, + BasicConstraintsValid: true, + } + + // create our private and public key + caPrivateKey, err := rsa.GenerateKey(rand.Reader, 4096) + if err != nil { + return nil, nil, err + } + + // create the CA + caBytes, err := x509.CreateCertificate(rand.Reader, ca, ca, &caPrivateKey.PublicKey, caPrivateKey) + if err != nil { + return nil, nil, err + } + + // pem encode + var caPEM bytes.Buffer + _ = pem.Encode(&caPEM, &pem.Block{ + Type: "CERTIFICATE", + Bytes: caBytes, + }) + + var caPrivKeyPEM bytes.Buffer + _ = pem.Encode(&caPrivKeyPEM, &pem.Block{ + Type: "RSA PRIVATE KEY", + Bytes: x509.MarshalPKCS1PrivateKey(caPrivateKey), + }) + + // set up our server certificate + cert := &x509.Certificate{ + SerialNumber: big.NewInt(2021), + Subject: pkix.Name{ + Organization: []string{"Fiber"}, + Country: []string{"NL"}, + Province: []string{""}, + Locality: []string{"Amsterdam"}, + StreetAddress: []string{"Huidenstraat"}, + PostalCode: []string{"1011 AA"}, + }, + IPAddresses: []net.IP{net.IPv4(127, 0, 0, 1), net.IPv6loopback}, + NotBefore: time.Now(), + NotAfter: time.Now().AddDate(10, 0, 0), + SubjectKeyId: []byte{1, 2, 3, 4, 6}, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth}, + KeyUsage: x509.KeyUsageDigitalSignature, + } + + certPrivateKey, err := rsa.GenerateKey(rand.Reader, 4096) + if err != nil { + return nil, nil, err + } + + certBytes, err := x509.CreateCertificate(rand.Reader, cert, ca, &certPrivateKey.PublicKey, caPrivateKey) + if err != nil { + return nil, nil, err + } + + var certPEM bytes.Buffer + _ = pem.Encode(&certPEM, &pem.Block{ + Type: "CERTIFICATE", + Bytes: certBytes, + }) + + var certPrivateKeyPEM bytes.Buffer + _ = pem.Encode(&certPrivateKeyPEM, &pem.Block{ + Type: "RSA PRIVATE KEY", + Bytes: x509.MarshalPKCS1PrivateKey(certPrivateKey), + }) + + serverCert, err := tls.X509KeyPair(certPEM.Bytes(), certPrivateKeyPEM.Bytes()) + if err != nil { + return nil, nil, err + } + + serverTLSConf = &tls.Config{ + Certificates: []tls.Certificate{serverCert}, + } + + certPool := x509.NewCertPool() + certPool.AppendCertsFromPEM(caPEM.Bytes()) + clientTLSConf = &tls.Config{ + RootCAs: certPool, + } + + return serverTLSConf, clientTLSConf, nil +} diff --git a/internal/wmi/LICENSE b/internal/wmi/LICENSE new file mode 100644 index 0000000..ae80b67 --- /dev/null +++ b/internal/wmi/LICENSE @@ -0,0 +1,20 @@ +The MIT License (MIT) + +Copyright (c) 2013 Stack Exchange + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/internal/wmi/swbemservices.go b/internal/wmi/swbemservices.go new file mode 100644 index 0000000..fe81253 --- /dev/null +++ b/internal/wmi/swbemservices.go @@ -0,0 +1,261 @@ +//go:build windows +// +build windows + +package wmi + +import ( + "fmt" + "reflect" + "runtime" + "sync" + + "github.com/gofiber/fiber/v2/internal/go-ole" + "github.com/gofiber/fiber/v2/internal/go-ole/oleutil" +) + +// SWbemServices is used to access wmi. See https://msdn.microsoft.com/en-us/library/aa393719(v=vs.85).aspx +type SWbemServices struct { + // TODO: track namespace. Not sure if we can re connect to a different namespace using the same instance + cWMIClient *Client // This could also be an embedded struct, but then we would need to branch on Client vs SWbemServices in the Query method + sWbemLocatorIUnknown *ole.IUnknown + sWbemLocatorIDispatch *ole.IDispatch + queries chan *queryRequest + closeError chan error + lQueryorClose sync.Mutex +} + +type queryRequest struct { + query string + dst interface{} + args []interface{} + finished chan error +} + +// InitializeSWbemServices will return a new SWbemServices object that can be used to query WMI +func InitializeSWbemServices(c *Client, connectServerArgs ...interface{}) (*SWbemServices, error) { + // fmt.Println("InitializeSWbemServices: Starting") + // TODO: implement connectServerArgs as optional argument for init with connectServer call + s := new(SWbemServices) + s.cWMIClient = c + s.queries = make(chan *queryRequest) + initError := make(chan error) + go s.process(initError) + + err, ok := <-initError + if ok { + return nil, err // Send error to caller + } + // fmt.Println("InitializeSWbemServices: Finished") + return s, nil +} + +// Close will clear and release all of the SWbemServices resources +func (s *SWbemServices) Close() error { + s.lQueryorClose.Lock() + if s == nil || s.sWbemLocatorIDispatch == nil { + s.lQueryorClose.Unlock() + return fmt.Errorf("SWbemServices is not Initialized") + } + if s.queries == nil { + s.lQueryorClose.Unlock() + return fmt.Errorf("SWbemServices has been closed") + } + // fmt.Println("Close: sending close request") + var result error + ce := make(chan error) + s.closeError = ce // Race condition if multiple callers to close. May need to lock here + close(s.queries) // Tell background to shut things down + s.lQueryorClose.Unlock() + err, ok := <-ce + if ok { + result = err + } + // fmt.Println("Close: finished") + return result +} + +func (s *SWbemServices) process(initError chan error) { + // fmt.Println("process: starting background thread initialization") + // All OLE/WMI calls must happen on the same initialized thead, so lock this goroutine + runtime.LockOSThread() + defer runtime.UnlockOSThread() + + err := ole.CoInitializeEx(0, ole.COINIT_MULTITHREADED) + if err != nil { + oleCode := err.(*ole.OleError).Code() + if oleCode != ole.S_OK && oleCode != S_FALSE { + initError <- fmt.Errorf("ole.CoInitializeEx error: %v", err) + return + } + } + defer ole.CoUninitialize() + + unknown, err := oleutil.CreateObject("WbemScripting.SWbemLocator") + if err != nil { + initError <- fmt.Errorf("CreateObject SWbemLocator error: %v", err) + return + } else if unknown == nil { + initError <- ErrNilCreateObject + return + } + defer unknown.Release() + s.sWbemLocatorIUnknown = unknown + + dispatch, err := s.sWbemLocatorIUnknown.QueryInterface(ole.IID_IDispatch) + if err != nil { + initError <- fmt.Errorf("SWbemLocator QueryInterface error: %v", err) + return + } + defer dispatch.Release() + s.sWbemLocatorIDispatch = dispatch + + // we can't do the ConnectServer call outside the loop unless we find a way to track and re-init the connectServerArgs + // fmt.Println("process: initialized. closing initError") + close(initError) + // fmt.Println("process: waiting for queries") + for q := range s.queries { + // fmt.Printf("process: new query: len(query)=%d\n", len(q.query)) + errQuery := s.queryBackground(q) + // fmt.Println("process: s.queryBackground finished") + if errQuery != nil { + q.finished <- errQuery + } + close(q.finished) + } + // fmt.Println("process: queries channel closed") + s.queries = nil // set channel to nil so we know it is closed + // TODO: I think the Release/Clear calls can panic if things are in a bad state. + // TODO: May need to recover from panics and send error to method caller instead. + close(s.closeError) +} + +// Query runs the WQL query using a SWbemServices instance and appends the values to dst. +// +// dst must have type *[]S or *[]*S, for some struct type S. Fields selected in +// the query must have the same name in dst. Supported types are all signed and +// unsigned integers, time.Time, string, bool, or a pointer to one of those. +// Array types are not supported. +// +// By default, the local machine and default namespace are used. These can be +// changed using connectServerArgs. See +// http://msdn.microsoft.com/en-us/library/aa393720.aspx for details. +func (s *SWbemServices) Query(query string, dst interface{}, connectServerArgs ...interface{}) error { + s.lQueryorClose.Lock() + if s == nil || s.sWbemLocatorIDispatch == nil { + s.lQueryorClose.Unlock() + return fmt.Errorf("SWbemServices is not Initialized") + } + if s.queries == nil { + s.lQueryorClose.Unlock() + return fmt.Errorf("SWbemServices has been closed") + } + + // fmt.Println("Query: Sending query request") + qr := queryRequest{ + query: query, + dst: dst, + args: connectServerArgs, + finished: make(chan error), + } + s.queries <- &qr + s.lQueryorClose.Unlock() + err, ok := <-qr.finished + if ok { + // fmt.Println("Query: Finished with error") + return err // Send error to caller + } + // fmt.Println("Query: Finished") + return nil +} + +func (s *SWbemServices) queryBackground(q *queryRequest) error { + if s == nil || s.sWbemLocatorIDispatch == nil { + return fmt.Errorf("SWbemServices is not Initialized") + } + wmi := s.sWbemLocatorIDispatch // Should just rename in the code, but this will help as we break things apart + // fmt.Println("queryBackground: Starting") + + dv := reflect.ValueOf(q.dst) + if dv.Kind() != reflect.Ptr || dv.IsNil() { + return ErrInvalidEntityType + } + dv = dv.Elem() + mat, elemType := checkMultiArg(dv) + if mat == multiArgTypeInvalid { + return ErrInvalidEntityType + } + + // service is a SWbemServices + serviceRaw, err := oleutil.CallMethod(wmi, "ConnectServer", q.args...) + if err != nil { + return err + } + service := serviceRaw.ToIDispatch() + defer serviceRaw.Clear() + + // result is a SWBemObjectSet + resultRaw, err := oleutil.CallMethod(service, "ExecQuery", q.query) + if err != nil { + return err + } + result := resultRaw.ToIDispatch() + defer resultRaw.Clear() + + count, err := oleInt64(result, "Count") + if err != nil { + return err + } + + enumProperty, err := result.GetProperty("_NewEnum") + if err != nil { + return err + } + defer enumProperty.Clear() + + enum, err := enumProperty.ToIUnknown().IEnumVARIANT(ole.IID_IEnumVariant) + if err != nil { + return err + } + if enum == nil { + return fmt.Errorf("can't get IEnumVARIANT, enum is nil") + } + defer enum.Release() + + // Initialize a slice with Count capacity + dv.Set(reflect.MakeSlice(dv.Type(), 0, int(count))) + + var errFieldMismatch error + for itemRaw, length, err := enum.Next(1); length > 0; itemRaw, length, err = enum.Next(1) { + if err != nil { + return err + } + + err := func() error { + // item is a SWbemObject, but really a Win32_Process + item := itemRaw.ToIDispatch() + defer item.Release() + + ev := reflect.New(elemType) + if err = s.cWMIClient.loadEntity(ev.Interface(), item); err != nil { + if _, ok := err.(*ErrFieldMismatch); ok { + // We continue loading entities even in the face of field mismatch errors. + // If we encounter any other error, that other error is returned. Otherwise, + // an ErrFieldMismatch is returned. + errFieldMismatch = err + } else { + return err + } + } + if mat != multiArgTypeStructPtr { + ev = ev.Elem() + } + dv.Set(reflect.Append(dv, ev)) + return nil + }() + if err != nil { + return err + } + } + // fmt.Println("queryBackground: Finished") + return errFieldMismatch +} diff --git a/internal/wmi/wmi.go b/internal/wmi/wmi.go new file mode 100644 index 0000000..5992c6c --- /dev/null +++ b/internal/wmi/wmi.go @@ -0,0 +1,501 @@ +//go:build windows +// +build windows + +/* +Package wmi provides a WQL interface for WMI on Windows. + +Example code to print names of running processes: + + type Win32_Process struct { + Name string + } + + func main() { + var dst []Win32_Process + q := wmi.CreateQuery(&dst, "") + err := wmi.Query(q, &dst) + if err != nil { + log.Fatal(err) + } + for i, v := range dst { + println(i, v.Name) + } + } +*/ +package wmi + +import ( + "bytes" + "errors" + "fmt" + "log" + "os" + "reflect" + "runtime" + "strconv" + "strings" + "sync" + "time" + + "github.com/gofiber/fiber/v2/internal/go-ole" + "github.com/gofiber/fiber/v2/internal/go-ole/oleutil" +) + +var l = log.New(os.Stdout, "", log.LstdFlags) + +var ( + ErrInvalidEntityType = errors.New("wmi: invalid entity type") + // ErrNilCreateObject is the error returned if CreateObject returns nil even + // if the error was nil. + ErrNilCreateObject = errors.New("wmi: create object returned nil") + lock sync.Mutex +) + +// S_FALSE is returned by CoInitializeEx if it was already called on this thread. +const S_FALSE = 0x00000001 + +// QueryNamespace invokes Query with the given namespace on the local machine. +func QueryNamespace(query string, dst interface{}, namespace string) error { + return Query(query, dst, nil, namespace) +} + +// Query runs the WQL query and appends the values to dst. +// +// dst must have type *[]S or *[]*S, for some struct type S. Fields selected in +// the query must have the same name in dst. Supported types are all signed and +// unsigned integers, time.Time, string, bool, or a pointer to one of those. +// Array types are not supported. +// +// By default, the local machine and default namespace are used. These can be +// changed using connectServerArgs. See +// http://msdn.microsoft.com/en-us/library/aa393720.aspx for details. +// +// Query is a wrapper around DefaultClient.Query. +func Query(query string, dst interface{}, connectServerArgs ...interface{}) error { + if DefaultClient.SWbemServicesClient == nil { + return DefaultClient.Query(query, dst, connectServerArgs...) + } + return DefaultClient.SWbemServicesClient.Query(query, dst, connectServerArgs...) +} + +// A Client is an WMI query client. +// +// Its zero value (DefaultClient) is a usable client. +type Client struct { + // NonePtrZero specifies if nil values for fields which aren't pointers + // should be returned as the field types zero value. + // + // Setting this to true allows stucts without pointer fields to be used + // without the risk failure should a nil value returned from WMI. + NonePtrZero bool + + // PtrNil specifies if nil values for pointer fields should be returned + // as nil. + // + // Setting this to true will set pointer fields to nil where WMI + // returned nil, otherwise the types zero value will be returned. + PtrNil bool + + // AllowMissingFields specifies that struct fields not present in the + // query result should not result in an error. + // + // Setting this to true allows custom queries to be used with full + // struct definitions instead of having to define multiple structs. + AllowMissingFields bool + + // SWbemServiceClient is an optional SWbemServices object that can be + // initialized and then reused across multiple queries. If it is null + // then the method will initialize a new temporary client each time. + SWbemServicesClient *SWbemServices +} + +// DefaultClient is the default Client and is used by Query, QueryNamespace +var DefaultClient = &Client{} + +// Query runs the WQL query and appends the values to dst. +// +// dst must have type *[]S or *[]*S, for some struct type S. Fields selected in +// the query must have the same name in dst. Supported types are all signed and +// unsigned integers, time.Time, string, bool, or a pointer to one of those. +// Array types are not supported. +// +// By default, the local machine and default namespace are used. These can be +// changed using connectServerArgs. See +// http://msdn.microsoft.com/en-us/library/aa393720.aspx for details. +func (c *Client) Query(query string, dst interface{}, connectServerArgs ...interface{}) error { + dv := reflect.ValueOf(dst) + if dv.Kind() != reflect.Ptr || dv.IsNil() { + return ErrInvalidEntityType + } + dv = dv.Elem() + mat, elemType := checkMultiArg(dv) + if mat == multiArgTypeInvalid { + return ErrInvalidEntityType + } + + lock.Lock() + defer lock.Unlock() + runtime.LockOSThread() + defer runtime.UnlockOSThread() + + err := ole.CoInitializeEx(0, ole.COINIT_MULTITHREADED) + if err != nil { + oleCode := err.(*ole.OleError).Code() + if oleCode != ole.S_OK && oleCode != S_FALSE { + return err + } + } + defer ole.CoUninitialize() + + unknown, err := oleutil.CreateObject("WbemScripting.SWbemLocator") + if err != nil { + return err + } else if unknown == nil { + return ErrNilCreateObject + } + defer unknown.Release() + + wmi, err := unknown.QueryInterface(ole.IID_IDispatch) + if err != nil { + return err + } + defer wmi.Release() + + // service is a SWbemServices + serviceRaw, err := oleutil.CallMethod(wmi, "ConnectServer", connectServerArgs...) + if err != nil { + return err + } + service := serviceRaw.ToIDispatch() + defer serviceRaw.Clear() + + // result is a SWBemObjectSet + resultRaw, err := oleutil.CallMethod(service, "ExecQuery", query) + if err != nil { + return err + } + result := resultRaw.ToIDispatch() + defer resultRaw.Clear() + + count, err := oleInt64(result, "Count") + if err != nil { + return err + } + + enumProperty, err := result.GetProperty("_NewEnum") + if err != nil { + return err + } + defer enumProperty.Clear() + + enum, err := enumProperty.ToIUnknown().IEnumVARIANT(ole.IID_IEnumVariant) + if err != nil { + return err + } + if enum == nil { + return fmt.Errorf("can't get IEnumVARIANT, enum is nil") + } + defer enum.Release() + + // Initialize a slice with Count capacity + dv.Set(reflect.MakeSlice(dv.Type(), 0, int(count))) + + var errFieldMismatch error + for itemRaw, length, err := enum.Next(1); length > 0; itemRaw, length, err = enum.Next(1) { + if err != nil { + return err + } + + err := func() error { + // item is a SWbemObject, but really a Win32_Process + item := itemRaw.ToIDispatch() + defer item.Release() + + ev := reflect.New(elemType) + if err = c.loadEntity(ev.Interface(), item); err != nil { + if _, ok := err.(*ErrFieldMismatch); ok { + // We continue loading entities even in the face of field mismatch errors. + // If we encounter any other error, that other error is returned. Otherwise, + // an ErrFieldMismatch is returned. + errFieldMismatch = err + } else { + return err + } + } + if mat != multiArgTypeStructPtr { + ev = ev.Elem() + } + dv.Set(reflect.Append(dv, ev)) + return nil + }() + if err != nil { + return err + } + } + return errFieldMismatch +} + +// ErrFieldMismatch is returned when a field is to be loaded into a different +// type than the one it was stored from, or when a field is missing or +// unexported in the destination struct. +// StructType is the type of the struct pointed to by the destination argument. +type ErrFieldMismatch struct { + StructType reflect.Type + FieldName string + Reason string +} + +func (e *ErrFieldMismatch) Error() string { + return fmt.Sprintf("wmi: cannot load field %q into a %q: %s", + e.FieldName, e.StructType, e.Reason) +} + +var timeType = reflect.TypeOf(time.Time{}) + +// loadEntity loads a SWbemObject into a struct pointer. +func (c *Client) loadEntity(dst interface{}, src *ole.IDispatch) (errFieldMismatch error) { + v := reflect.ValueOf(dst).Elem() + for i := 0; i < v.NumField(); i++ { + f := v.Field(i) + of := f + isPtr := f.Kind() == reflect.Ptr + if isPtr { + ptr := reflect.New(f.Type().Elem()) + f.Set(ptr) + f = f.Elem() + } + n := v.Type().Field(i).Name + if !f.CanSet() { + return &ErrFieldMismatch{ + StructType: of.Type(), + FieldName: n, + Reason: "CanSet() is false", + } + } + prop, err := oleutil.GetProperty(src, n) + if err != nil { + if !c.AllowMissingFields { + errFieldMismatch = &ErrFieldMismatch{ + StructType: of.Type(), + FieldName: n, + Reason: "no such struct field", + } + } + continue + } + defer prop.Clear() + + if prop.VT == 0x1 { // VT_NULL + continue + } + + switch val := prop.Value().(type) { + case int8, int16, int32, int64, int: + v := reflect.ValueOf(val).Int() + switch f.Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + f.SetInt(v) + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + f.SetUint(uint64(v)) + default: + return &ErrFieldMismatch{ + StructType: of.Type(), + FieldName: n, + Reason: "not an integer class", + } + } + case uint8, uint16, uint32, uint64: + v := reflect.ValueOf(val).Uint() + switch f.Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + f.SetInt(int64(v)) + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + f.SetUint(v) + default: + return &ErrFieldMismatch{ + StructType: of.Type(), + FieldName: n, + Reason: "not an integer class", + } + } + case string: + switch f.Kind() { + case reflect.String: + f.SetString(val) + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + iv, err := strconv.ParseInt(val, 10, 64) + if err != nil { + return err + } + f.SetInt(iv) + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + uv, err := strconv.ParseUint(val, 10, 64) + if err != nil { + return err + } + f.SetUint(uv) + case reflect.Struct: + switch f.Type() { + case timeType: + if len(val) == 25 { + mins, err := strconv.Atoi(val[22:]) + if err != nil { + return err + } + val = val[:22] + fmt.Sprintf("%02d%02d", mins/60, mins%60) + } + t, err := time.Parse("20060102150405.000000-0700", val) + if err != nil { + return err + } + f.Set(reflect.ValueOf(t)) + } + } + case bool: + switch f.Kind() { + case reflect.Bool: + f.SetBool(val) + default: + return &ErrFieldMismatch{ + StructType: of.Type(), + FieldName: n, + Reason: "not a bool", + } + } + case float32: + switch f.Kind() { + case reflect.Float32: + f.SetFloat(float64(val)) + default: + return &ErrFieldMismatch{ + StructType: of.Type(), + FieldName: n, + Reason: "not a Float32", + } + } + default: + if f.Kind() == reflect.Slice { + switch f.Type().Elem().Kind() { + case reflect.String: + safeArray := prop.ToArray() + if safeArray != nil { + arr := safeArray.ToValueArray() + fArr := reflect.MakeSlice(f.Type(), len(arr), len(arr)) + for i, v := range arr { + s := fArr.Index(i) + s.SetString(v.(string)) + } + f.Set(fArr) + } + case reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uint: + safeArray := prop.ToArray() + if safeArray != nil { + arr := safeArray.ToValueArray() + fArr := reflect.MakeSlice(f.Type(), len(arr), len(arr)) + for i, v := range arr { + s := fArr.Index(i) + s.SetUint(reflect.ValueOf(v).Uint()) + } + f.Set(fArr) + } + case reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Int: + safeArray := prop.ToArray() + if safeArray != nil { + arr := safeArray.ToValueArray() + fArr := reflect.MakeSlice(f.Type(), len(arr), len(arr)) + for i, v := range arr { + s := fArr.Index(i) + s.SetInt(reflect.ValueOf(v).Int()) + } + f.Set(fArr) + } + default: + return &ErrFieldMismatch{ + StructType: of.Type(), + FieldName: n, + Reason: fmt.Sprintf("unsupported slice type (%T)", val), + } + } + } else { + typeof := reflect.TypeOf(val) + if typeof == nil && (isPtr || c.NonePtrZero) { + if (isPtr && c.PtrNil) || (!isPtr && c.NonePtrZero) { + of.Set(reflect.Zero(of.Type())) + } + break + } + return &ErrFieldMismatch{ + StructType: of.Type(), + FieldName: n, + Reason: fmt.Sprintf("unsupported type (%T)", val), + } + } + } + } + return errFieldMismatch +} + +type multiArgType int + +const ( + multiArgTypeInvalid multiArgType = iota + multiArgTypeStruct + multiArgTypeStructPtr +) + +// checkMultiArg checks that v has type []S, []*S for some struct type S. +// +// It returns what category the slice's elements are, and the reflect.Type +// that represents S. +func checkMultiArg(v reflect.Value) (m multiArgType, elemType reflect.Type) { + if v.Kind() != reflect.Slice { + return multiArgTypeInvalid, nil + } + elemType = v.Type().Elem() + switch elemType.Kind() { + case reflect.Struct: + return multiArgTypeStruct, elemType + case reflect.Ptr: + elemType = elemType.Elem() + if elemType.Kind() == reflect.Struct { + return multiArgTypeStructPtr, elemType + } + } + return multiArgTypeInvalid, nil +} + +func oleInt64(item *ole.IDispatch, prop string) (int64, error) { + v, err := oleutil.GetProperty(item, prop) + if err != nil { + return 0, err + } + defer v.Clear() + + i := int64(v.Val) + return i, nil +} + +// CreateQuery returns a WQL query string that queries all columns of src. where +// is an optional string that is appended to the query, to be used with WHERE +// clauses. In such a case, the "WHERE" string should appear at the beginning. +func CreateQuery(src interface{}, where string) string { + var b bytes.Buffer + b.WriteString("SELECT ") + s := reflect.Indirect(reflect.ValueOf(src)) + t := s.Type() + if s.Kind() == reflect.Slice { + t = t.Elem() + } + if t.Kind() != reflect.Struct { + return "" + } + var fields []string + for i := 0; i < t.NumField(); i++ { + fields = append(fields, t.Field(i).Name) + } + b.WriteString(strings.Join(fields, ", ")) + b.WriteString(" FROM ") + b.WriteString(t.Name()) + b.WriteString(" " + where) + return b.String() +} diff --git a/listen.go b/listen.go new file mode 100644 index 0000000..342b05f --- /dev/null +++ b/listen.go @@ -0,0 +1,502 @@ +// ⚡️ Fiber is an Express inspired web framework written in Go with ☕️ +// 🤖 Github Repository: https://github.com/gofiber/fiber +// 📌 API Documentation: https://docs.gofiber.io + +package fiber + +import ( + "crypto/tls" + "crypto/x509" + "errors" + "fmt" + "net" + "os" + "path/filepath" + "reflect" + "runtime" + "sort" + "strconv" + "strings" + "text/tabwriter" + + "github.com/mattn/go-colorable" + "github.com/mattn/go-isatty" + "github.com/mattn/go-runewidth" + + "github.com/gofiber/fiber/v2/log" +) + +const ( + globalIpv4Addr = "0.0.0.0" +) + +// Listener can be used to pass a custom listener. +func (app *App) Listener(ln net.Listener) error { + // prepare the server for the start + app.startupProcess() + + // run hooks + app.runOnListenHooks(app.prepareListenData(ln.Addr().String(), getTLSConfig(ln) != nil)) + + // Print startup message + if !app.config.DisableStartupMessage { + app.startupMessage(ln.Addr().String(), getTLSConfig(ln) != nil, "") + } + + // Print routes + if app.config.EnablePrintRoutes { + app.printRoutesMessage() + } + + // Prefork is not supported for custom listeners + if app.config.Prefork { + log.Warn("Prefork isn't supported for custom listeners.") + } + + // Start listening + return app.server.Serve(ln) +} + +// Listen serves HTTP requests from the given addr. +// +// app.Listen(":8080") +// app.Listen("127.0.0.1:8080") +func (app *App) Listen(addr string) error { + // Start prefork + if app.config.Prefork { + return app.prefork(app.config.Network, addr, nil) + } + + // Setup listener + ln, err := net.Listen(app.config.Network, addr) + if err != nil { + return fmt.Errorf("failed to listen: %w", err) + } + + // prepare the server for the start + app.startupProcess() + + // run hooks + app.runOnListenHooks(app.prepareListenData(ln.Addr().String(), false)) + + // Print startup message + if !app.config.DisableStartupMessage { + app.startupMessage(ln.Addr().String(), false, "") + } + + // Print routes + if app.config.EnablePrintRoutes { + app.printRoutesMessage() + } + + // Start listening + return app.server.Serve(ln) +} + +// ListenTLS serves HTTPS requests from the given addr. +// certFile and keyFile are the paths to TLS certificate and key file: +// +// app.ListenTLS(":8080", "./cert.pem", "./cert.key") +func (app *App) ListenTLS(addr, certFile, keyFile string) error { + // Check for valid cert/key path + if len(certFile) == 0 || len(keyFile) == 0 { + return errors.New("tls: provide a valid cert or key path") + } + + // Set TLS config with handler + cert, err := tls.LoadX509KeyPair(certFile, keyFile) + if err != nil { + return fmt.Errorf("tls: cannot load TLS key pair from certFile=%q and keyFile=%q: %w", certFile, keyFile, err) + } + + return app.ListenTLSWithCertificate(addr, cert) +} + +// ListenTLS serves HTTPS requests from the given addr. +// cert is a tls.Certificate +// +// app.ListenTLSWithCertificate(":8080", cert) +func (app *App) ListenTLSWithCertificate(addr string, cert tls.Certificate) error { + tlsHandler := &TLSHandler{} + config := &tls.Config{ + MinVersion: tls.VersionTLS12, + Certificates: []tls.Certificate{ + cert, + }, + GetCertificate: tlsHandler.GetClientInfo, + } + + // Prefork is supported + if app.config.Prefork { + return app.prefork(app.config.Network, addr, config) + } + + // Setup listener + ln, err := net.Listen(app.config.Network, addr) + ln = tls.NewListener(ln, config) + if err != nil { + return fmt.Errorf("failed to listen: %w", err) + } + + // prepare the server for the start + app.startupProcess() + + // run hooks + app.runOnListenHooks(app.prepareListenData(ln.Addr().String(), getTLSConfig(ln) != nil)) + + // Print startup message + if !app.config.DisableStartupMessage { + app.startupMessage(ln.Addr().String(), true, "") + } + + // Print routes + if app.config.EnablePrintRoutes { + app.printRoutesMessage() + } + + // Attach the tlsHandler to the config + app.SetTLSHandler(tlsHandler) + + // Start listening + return app.server.Serve(ln) +} + +// ListenMutualTLS serves HTTPS requests from the given addr. +// certFile, keyFile and clientCertFile are the paths to TLS certificate and key file: +// +// app.ListenMutualTLS(":8080", "./cert.pem", "./cert.key", "./client.pem") +func (app *App) ListenMutualTLS(addr, certFile, keyFile, clientCertFile string) error { + // Check for valid cert/key path + if len(certFile) == 0 || len(keyFile) == 0 { + return errors.New("tls: provide a valid cert or key path") + } + + cert, err := tls.LoadX509KeyPair(certFile, keyFile) + if err != nil { + return fmt.Errorf("tls: cannot load TLS key pair from certFile=%q and keyFile=%q: %w", certFile, keyFile, err) + } + + clientCACert, err := os.ReadFile(filepath.Clean(clientCertFile)) + if err != nil { + return fmt.Errorf("failed to read file: %w", err) + } + clientCertPool := x509.NewCertPool() + clientCertPool.AppendCertsFromPEM(clientCACert) + + return app.ListenMutualTLSWithCertificate(addr, cert, clientCertPool) +} + +// ListenMutualTLSWithCertificate serves HTTPS requests from the given addr. +// cert is a tls.Certificate and clientCertPool is a *x509.CertPool: +// +// app.ListenMutualTLS(":8080", cert, clientCertPool) +func (app *App) ListenMutualTLSWithCertificate(addr string, cert tls.Certificate, clientCertPool *x509.CertPool) error { + tlsHandler := &TLSHandler{} + config := &tls.Config{ + MinVersion: tls.VersionTLS12, + ClientAuth: tls.RequireAndVerifyClientCert, + ClientCAs: clientCertPool, + Certificates: []tls.Certificate{ + cert, + }, + GetCertificate: tlsHandler.GetClientInfo, + } + + // Prefork is supported + if app.config.Prefork { + return app.prefork(app.config.Network, addr, config) + } + + // Setup listener + ln, err := tls.Listen(app.config.Network, addr, config) + if err != nil { + return fmt.Errorf("failed to listen: %w", err) + } + + // prepare the server for the start + app.startupProcess() + + // run hooks + app.runOnListenHooks(app.prepareListenData(ln.Addr().String(), getTLSConfig(ln) != nil)) + + // Print startup message + if !app.config.DisableStartupMessage { + app.startupMessage(ln.Addr().String(), true, "") + } + + // Print routes + if app.config.EnablePrintRoutes { + app.printRoutesMessage() + } + + // Attach the tlsHandler to the config + app.SetTLSHandler(tlsHandler) + + // Start listening + return app.server.Serve(ln) +} + +// prepareListenData create an slice of ListenData +func (app *App) prepareListenData(addr string, isTLS bool) ListenData { //revive:disable-line:flag-parameter // Accepting a bool param named isTLS if fine here + host, port := parseAddr(addr) + if host == "" { + if app.config.Network == NetworkTCP6 { + host = "[::1]" + } else { + host = globalIpv4Addr + } + } + + return ListenData{ + Host: host, + Port: port, + TLS: isTLS, + } +} + +// startupMessage prepares the startup message with the handler number, port, address and other information +func (app *App) startupMessage(addr string, isTLS bool, pids string) { //nolint: revive // Accepting a bool param named isTLS if fine here + // ignore child processes + if IsChild() { + return + } + + // Alias colors + colors := app.config.ColorScheme + + value := func(s string, width int) string { + pad := width - len(s) + str := "" + for i := 0; i < pad; i++ { + str += "." + } + if s == "Disabled" { + str += " " + s + } else { + str += fmt.Sprintf(" %s%s%s", colors.Cyan, s, colors.Black) + } + return str + } + + center := func(s string, width int) string { + const padDiv = 2 + pad := strconv.Itoa((width - len(s)) / padDiv) + str := fmt.Sprintf("%"+pad+"s", " ") + str += s + str += fmt.Sprintf("%"+pad+"s", " ") + if len(str) < width { + str += " " + } + return str + } + + centerValue := func(s string, width int) string { + const padDiv = 2 + pad := strconv.Itoa((width - runewidth.StringWidth(s)) / padDiv) + str := fmt.Sprintf("%"+pad+"s", " ") + str += fmt.Sprintf("%s%s%s", colors.Cyan, s, colors.Black) + str += fmt.Sprintf("%"+pad+"s", " ") + if runewidth.StringWidth(s)-10 < width && runewidth.StringWidth(s)%2 == 0 { + // add an ending space if the length of str is even and str is not too long + str += " " + } + return str + } + + pad := func(s string, width int) string { + toAdd := width - len(s) + str := s + for i := 0; i < toAdd; i++ { + str += " " + } + return str + } + + host, port := parseAddr(addr) + if host == "" { + if app.config.Network == NetworkTCP6 { + host = "[::1]" + } else { + host = globalIpv4Addr + } + } + + scheme := schemeHTTP + if isTLS { + scheme = schemeHTTPS + } + + isPrefork := "Disabled" + if app.config.Prefork { + isPrefork = "Enabled" + } + + procs := strconv.Itoa(runtime.GOMAXPROCS(0)) + if !app.config.Prefork { + procs = "1" + } + + const lineLen = 49 + mainLogo := colors.Black + " ┌───────────────────────────────────────────────────┐\n" + if app.config.AppName != "" { + mainLogo += " │ " + centerValue(app.config.AppName, lineLen) + " │\n" + } + mainLogo += " │ " + centerValue("Fiber v"+Version, lineLen) + " │\n" + + if host == globalIpv4Addr { + mainLogo += " │ " + center(fmt.Sprintf("%s://127.0.0.1:%s", scheme, port), lineLen) + " │\n" + + " │ " + center(fmt.Sprintf("(bound on host 0.0.0.0 and port %s)", port), lineLen) + " │\n" + } else { + mainLogo += " │ " + center(fmt.Sprintf("%s://%s:%s", scheme, host, port), lineLen) + " │\n" + } + + mainLogo += fmt.Sprintf( + " │ │\n"+ + " │ Handlers %s Processes %s │\n"+ + " │ Prefork .%s PID ....%s │\n"+ + " └───────────────────────────────────────────────────┘"+ + colors.Reset, + value(strconv.Itoa(int(app.handlersCount)), 14), value(procs, 12), + value(isPrefork, 14), value(strconv.Itoa(os.Getpid()), 14), + ) + + var childPidsLogo string + if app.config.Prefork { + var childPidsTemplate string + childPidsTemplate += "%s" + childPidsTemplate += " ┌───────────────────────────────────────────────────┐\n%s" + childPidsTemplate += " └───────────────────────────────────────────────────┘" + childPidsTemplate += "%s" + + newLine := " │ %s%s%s │" + + // Turn the `pids` variable (in the form ",a,b,c,d,e,f,etc") into a slice of PIDs + var pidSlice []string + for _, v := range strings.Split(pids, ",") { + if v != "" { + pidSlice = append(pidSlice, v) + } + } + + var lines []string + thisLine := "Child PIDs ... " + var itemsOnThisLine []string + + const maxLineLen = 49 + + addLine := func() { + lines = append(lines, + fmt.Sprintf( + newLine, + colors.Black, + thisLine+colors.Cyan+pad(strings.Join(itemsOnThisLine, ", "), maxLineLen-len(thisLine)), + colors.Black, + ), + ) + } + + for _, pid := range pidSlice { + if len(thisLine+strings.Join(append(itemsOnThisLine, pid), ", ")) > maxLineLen { + addLine() + thisLine = "" + itemsOnThisLine = []string{pid} + } else { + itemsOnThisLine = append(itemsOnThisLine, pid) + } + } + + // Add left over items to their own line + if len(itemsOnThisLine) != 0 { + addLine() + } + + // Form logo + childPidsLogo = fmt.Sprintf(childPidsTemplate, + colors.Black, + strings.Join(lines, "\n")+"\n", + colors.Reset, + ) + } + + // Combine both the child PID logo and the main Fiber logo + + // Pad the shorter logo to the length of the longer one + splitMainLogo := strings.Split(mainLogo, "\n") + splitChildPidsLogo := strings.Split(childPidsLogo, "\n") + + mainLen := len(splitMainLogo) + childLen := len(splitChildPidsLogo) + + if mainLen > childLen { + diff := mainLen - childLen + for i := 0; i < diff; i++ { + splitChildPidsLogo = append(splitChildPidsLogo, "") + } + } else { + diff := childLen - mainLen + for i := 0; i < diff; i++ { + splitMainLogo = append(splitMainLogo, "") + } + } + + // Combine the two logos, line by line + output := "\n" + for i := range splitMainLogo { + output += colors.Black + splitMainLogo[i] + " " + splitChildPidsLogo[i] + "\n" + } + + out := colorable.NewColorableStdout() + if os.Getenv("TERM") == "dumb" || os.Getenv("NO_COLOR") == "1" || (!isatty.IsTerminal(os.Stdout.Fd()) && !isatty.IsCygwinTerminal(os.Stdout.Fd())) { + out = colorable.NewNonColorable(os.Stdout) + } + + _, _ = fmt.Fprintln(out, output) +} + +// printRoutesMessage print all routes with method, path, name and handlers +// in a format of table, like this: +// method | path | name | handlers +// GET | / | routeName | github.com/gofiber/fiber/v2.emptyHandler +// HEAD | / | | github.com/gofiber/fiber/v2.emptyHandler +func (app *App) printRoutesMessage() { + // ignore child processes + if IsChild() { + return + } + + // Alias colors + colors := app.config.ColorScheme + + var routes []RouteMessage + for _, routeStack := range app.stack { + for _, route := range routeStack { + var newRoute RouteMessage + newRoute.name = route.Name + newRoute.method = route.Method + newRoute.path = route.Path + for _, handler := range route.Handlers { + newRoute.handlers += runtime.FuncForPC(reflect.ValueOf(handler).Pointer()).Name() + " " + } + routes = append(routes, newRoute) + } + } + + out := colorable.NewColorableStdout() + if os.Getenv("TERM") == "dumb" || os.Getenv("NO_COLOR") == "1" || (!isatty.IsTerminal(os.Stdout.Fd()) && !isatty.IsCygwinTerminal(os.Stdout.Fd())) { + out = colorable.NewNonColorable(os.Stdout) + } + + w := tabwriter.NewWriter(out, 1, 1, 1, ' ', 0) + // Sort routes by path + sort.Slice(routes, func(i, j int) bool { + return routes[i].path < routes[j].path + }) + + _, _ = fmt.Fprintf(w, "%smethod\t%s| %spath\t%s| %sname\t%s| %shandlers\t%s\n", colors.Blue, colors.White, colors.Green, colors.White, colors.Cyan, colors.White, colors.Yellow, colors.Reset) + _, _ = fmt.Fprintf(w, "%s------\t%s| %s----\t%s| %s----\t%s| %s--------\t%s\n", colors.Blue, colors.White, colors.Green, colors.White, colors.Cyan, colors.White, colors.Yellow, colors.Reset) + for _, route := range routes { + _, _ = fmt.Fprintf(w, "%s%s\t%s| %s%s\t%s| %s%s\t%s| %s%s%s\n", colors.Blue, route.method, colors.White, colors.Green, route.path, colors.White, colors.Cyan, route.name, colors.White, colors.Yellow, route.handlers, colors.Reset) + } + + _ = w.Flush() //nolint:errcheck // It is fine to ignore the error here +} diff --git a/listen_test.go b/listen_test.go new file mode 100644 index 0000000..6a44bf4 --- /dev/null +++ b/listen_test.go @@ -0,0 +1,342 @@ +// ⚡️ Fiber is an Express inspired web framework written in Go with ☕️ +// 🤖 Github Repository: https://github.com/gofiber/fiber +// 📌 API Documentation: https://docs.gofiber.io + +package fiber + +import ( + "bytes" + "crypto/tls" + "crypto/x509" + "io" + "log" + "os" + "path/filepath" + "strings" + "sync" + "testing" + "time" + + "github.com/gofiber/fiber/v2/utils" + + "github.com/valyala/fasthttp/fasthttputil" +) + +// go test -run Test_App_Listen +func Test_App_Listen(t *testing.T) { + t.Parallel() + app := New(Config{DisableStartupMessage: true}) + + utils.AssertEqual(t, false, app.Listen(":99999") == nil) + + go func() { + time.Sleep(1000 * time.Millisecond) + utils.AssertEqual(t, nil, app.Shutdown()) + }() + + utils.AssertEqual(t, nil, app.Listen(":4003")) +} + +// go test -run Test_App_Listen_Prefork +func Test_App_Listen_Prefork(t *testing.T) { + testPreforkMaster = true + + app := New(Config{DisableStartupMessage: true, Prefork: true}) + + utils.AssertEqual(t, nil, app.Listen(":99999")) +} + +// go test -run Test_App_ListenTLS +func Test_App_ListenTLS(t *testing.T) { + t.Parallel() + app := New() + + // invalid port + utils.AssertEqual(t, false, app.ListenTLS(":99999", "./.github/testdata/ssl.pem", "./.github/testdata/ssl.key") == nil) + // missing perm/cert file + utils.AssertEqual(t, false, app.ListenTLS(":0", "", "./.github/testdata/ssl.key") == nil) + + go func() { + time.Sleep(1000 * time.Millisecond) + utils.AssertEqual(t, nil, app.Shutdown()) + }() + + utils.AssertEqual(t, nil, app.ListenTLS(":0", "./.github/testdata/ssl.pem", "./.github/testdata/ssl.key")) +} + +// go test -run Test_App_ListenTLS_Prefork +func Test_App_ListenTLS_Prefork(t *testing.T) { + testPreforkMaster = true + + app := New(Config{DisableStartupMessage: true, Prefork: true}) + + // invalid key file content + utils.AssertEqual(t, false, app.ListenTLS(":0", "./.github/testdata/ssl.pem", "./.github/testdata/template.tmpl") == nil) + + utils.AssertEqual(t, nil, app.ListenTLS(":99999", "./.github/testdata/ssl.pem", "./.github/testdata/ssl.key")) +} + +// go test -run Test_App_ListenMutualTLS +func Test_App_ListenMutualTLS(t *testing.T) { + t.Parallel() + app := New() + + // invalid port + utils.AssertEqual(t, false, app.ListenMutualTLS(":99999", "./.github/testdata/ssl.pem", "./.github/testdata/ssl.key", "./.github/testdata/ca-chain.cert.pem") == nil) + // missing perm/cert file + utils.AssertEqual(t, false, app.ListenMutualTLS(":0", "", "./.github/testdata/ssl.key", "") == nil) + + go func() { + time.Sleep(1000 * time.Millisecond) + utils.AssertEqual(t, nil, app.Shutdown()) + }() + + utils.AssertEqual(t, nil, app.ListenMutualTLS(":0", "./.github/testdata/ssl.pem", "./.github/testdata/ssl.key", "./.github/testdata/ca-chain.cert.pem")) +} + +// go test -run Test_App_ListenMutualTLS_Prefork +func Test_App_ListenMutualTLS_Prefork(t *testing.T) { + testPreforkMaster = true + + app := New(Config{DisableStartupMessage: true, Prefork: true}) + + // invalid key file content + utils.AssertEqual(t, false, app.ListenMutualTLS(":0", "./.github/testdata/ssl.pem", "./.github/testdata/template.html", "") == nil) + + utils.AssertEqual(t, nil, app.ListenMutualTLS(":99999", "./.github/testdata/ssl.pem", "./.github/testdata/ssl.key", "./.github/testdata/ca-chain.cert.pem")) +} + +// go test -run Test_App_Listener +func Test_App_Listener(t *testing.T) { + t.Parallel() + app := New() + + go func() { + time.Sleep(500 * time.Millisecond) + utils.AssertEqual(t, nil, app.Shutdown()) + }() + + ln := fasthttputil.NewInmemoryListener() + utils.AssertEqual(t, nil, app.Listener(ln)) +} + +func Test_App_Listener_TLS_Listener(t *testing.T) { + t.Parallel() + // Create tls certificate + cer, err := tls.LoadX509KeyPair("./.github/testdata/ssl.pem", "./.github/testdata/ssl.key") + if err != nil { + utils.AssertEqual(t, nil, err) + } + //nolint:gosec // We're in a test so using old ciphers is fine + config := &tls.Config{Certificates: []tls.Certificate{cer}} + + //nolint:gosec // We're in a test so listening on all interfaces is fine + ln, err := tls.Listen(NetworkTCP4, ":0", config) + utils.AssertEqual(t, nil, err) + + app := New() + + go func() { + time.Sleep(time.Millisecond * 500) + utils.AssertEqual(t, nil, app.Shutdown()) + }() + + utils.AssertEqual(t, nil, app.Listener(ln)) +} + +// go test -run Test_App_ListenTLSWithCertificate +func Test_App_ListenTLSWithCertificate(t *testing.T) { + t.Parallel() + + // Create tls certificate + cer, err := tls.LoadX509KeyPair("./.github/testdata/ssl.pem", "./.github/testdata/ssl.key") + if err != nil { + utils.AssertEqual(t, nil, err) + } + + app := New() + + // invalid port + utils.AssertEqual(t, false, app.ListenTLSWithCertificate(":99999", cer) == nil) + + go func() { + time.Sleep(1000 * time.Millisecond) + utils.AssertEqual(t, nil, app.Shutdown()) + }() + + utils.AssertEqual(t, nil, app.ListenTLSWithCertificate(":0", cer)) +} + +// go test -run Test_App_ListenTLSWithCertificate_Prefork +func Test_App_ListenTLSWithCertificate_Prefork(t *testing.T) { + testPreforkMaster = true + + // Create tls certificate + cer, err := tls.LoadX509KeyPair("./.github/testdata/ssl.pem", "./.github/testdata/ssl.key") + if err != nil { + utils.AssertEqual(t, nil, err) + } + + app := New(Config{DisableStartupMessage: true, Prefork: true}) + + utils.AssertEqual(t, nil, app.ListenTLSWithCertificate(":99999", cer)) +} + +// go test -run Test_App_ListenMutualTLSWithCertificate +func Test_App_ListenMutualTLSWithCertificate(t *testing.T) { + t.Parallel() + + // Create tls certificate + cer, err := tls.LoadX509KeyPair("./.github/testdata/ssl.pem", "./.github/testdata/ssl.key") + if err != nil { + utils.AssertEqual(t, nil, err) + } + + // Create pool + clientCACert, err := os.ReadFile(filepath.Clean("./.github/testdata/ca-chain.cert.pem")) + if err != nil { + utils.AssertEqual(t, nil, err) + } + clientCertPool := x509.NewCertPool() + clientCertPool.AppendCertsFromPEM(clientCACert) + + app := New() + + // invalid port + utils.AssertEqual(t, false, app.ListenMutualTLSWithCertificate(":99999", cer, clientCertPool) == nil) + + go func() { + time.Sleep(1000 * time.Millisecond) + utils.AssertEqual(t, nil, app.Shutdown()) + }() + + utils.AssertEqual(t, nil, app.ListenMutualTLSWithCertificate(":0", cer, clientCertPool)) +} + +// go test -run Test_App_ListenMutualTLS_Prefork +func Test_App_ListenMutualTLSWithCertificate_Prefork(t *testing.T) { + testPreforkMaster = true + + // Create tls certificate + cer, err := tls.LoadX509KeyPair("./.github/testdata/ssl.pem", "./.github/testdata/ssl.key") + if err != nil { + utils.AssertEqual(t, nil, err) + } + + // Create pool + clientCACert, err := os.ReadFile(filepath.Clean("./.github/testdata/ca-chain.cert.pem")) + if err != nil { + utils.AssertEqual(t, nil, err) + } + clientCertPool := x509.NewCertPool() + clientCertPool.AppendCertsFromPEM(clientCACert) + + app := New(Config{DisableStartupMessage: true, Prefork: true}) + + utils.AssertEqual(t, nil, app.ListenMutualTLSWithCertificate(":99999", cer, clientCertPool)) +} + +func captureOutput(f func()) string { + reader, writer, err := os.Pipe() + if err != nil { + panic(err) + } + stdout := os.Stdout + stderr := os.Stderr + defer func() { + os.Stdout = stdout + os.Stderr = stderr + log.SetOutput(os.Stderr) + }() + os.Stdout = writer + os.Stderr = writer + log.SetOutput(writer) + out := make(chan string) + wg := new(sync.WaitGroup) + wg.Add(1) + go func() { + var buf bytes.Buffer + wg.Done() + _, err := io.Copy(&buf, reader) + if err != nil { + panic(err) + } + out <- buf.String() + }() + wg.Wait() + f() + err = writer.Close() + if err != nil { + panic(err) + } + return <-out +} + +func Test_App_Master_Process_Show_Startup_Message(t *testing.T) { + startupMessage := captureOutput(func() { + New(Config{Prefork: true}). + startupMessage(":3000", true, strings.Repeat(",11111,22222,33333,44444,55555,60000", 10)) + }) + utils.AssertEqual(t, true, strings.Contains(startupMessage, "https://127.0.0.1:3000")) + utils.AssertEqual(t, true, strings.Contains(startupMessage, "(bound on host 0.0.0.0 and port 3000)")) + utils.AssertEqual(t, true, strings.Contains(startupMessage, "Child PIDs")) + utils.AssertEqual(t, true, strings.Contains(startupMessage, "11111, 22222, 33333, 44444, 55555, 60000")) + utils.AssertEqual(t, true, strings.Contains(startupMessage, "Prefork ........ Enabled")) +} + +func Test_App_Master_Process_Show_Startup_MessageWithAppName(t *testing.T) { + app := New(Config{Prefork: true, AppName: "Test App v1.0.1"}) + startupMessage := captureOutput(func() { + app.startupMessage(":3000", true, strings.Repeat(",11111,22222,33333,44444,55555,60000", 10)) + }) + utils.AssertEqual(t, "Test App v1.0.1", app.Config().AppName) + utils.AssertEqual(t, true, strings.Contains(startupMessage, app.Config().AppName)) +} + +func Test_App_Master_Process_Show_Startup_MessageWithAppNameNonAscii(t *testing.T) { + appName := "Serveur de vérification des données" + app := New(Config{Prefork: true, AppName: appName}) + startupMessage := captureOutput(func() { + app.startupMessage(":3000", false, "") + }) + utils.AssertEqual(t, true, strings.Contains(startupMessage, "│ Serveur de vérification des données │")) +} + +func Test_App_print_Route(t *testing.T) { + app := New(Config{EnablePrintRoutes: true}) + app.Get("/", emptyHandler).Name("routeName") + printRoutesMessage := captureOutput(func() { + app.printRoutesMessage() + }) + utils.AssertEqual(t, true, strings.Contains(printRoutesMessage, MethodGet)) + utils.AssertEqual(t, true, strings.Contains(printRoutesMessage, "/")) + utils.AssertEqual(t, true, strings.Contains(printRoutesMessage, "emptyHandler")) + utils.AssertEqual(t, true, strings.Contains(printRoutesMessage, "routeName")) +} + +func Test_App_print_Route_with_group(t *testing.T) { + app := New(Config{EnablePrintRoutes: true}) + app.Get("/", emptyHandler) + + v1 := app.Group("v1") + v1.Get("/test", emptyHandler).Name("v1") + v1.Post("/test/fiber", emptyHandler) + v1.Put("/test/fiber/*", emptyHandler) + + printRoutesMessage := captureOutput(func() { + app.printRoutesMessage() + }) + + utils.AssertEqual(t, true, strings.Contains(printRoutesMessage, MethodGet)) + utils.AssertEqual(t, true, strings.Contains(printRoutesMessage, "/")) + utils.AssertEqual(t, true, strings.Contains(printRoutesMessage, "emptyHandler")) + utils.AssertEqual(t, true, strings.Contains(printRoutesMessage, "/v1/test")) + utils.AssertEqual(t, true, strings.Contains(printRoutesMessage, MethodPost)) + utils.AssertEqual(t, true, strings.Contains(printRoutesMessage, "/v1/test/fiber")) + utils.AssertEqual(t, true, strings.Contains(printRoutesMessage, "PUT")) + utils.AssertEqual(t, true, strings.Contains(printRoutesMessage, "/v1/test/fiber/*")) +} + +func emptyHandler(_ *Ctx) error { + return nil +} diff --git a/log/default.go b/log/default.go new file mode 100644 index 0000000..c898cd6 --- /dev/null +++ b/log/default.go @@ -0,0 +1,209 @@ +package log + +import ( + "context" + "fmt" + "io" + "log" + "os" + "sync" + + "github.com/valyala/bytebufferpool" +) + +var _ AllLogger = (*defaultLogger)(nil) + +type defaultLogger struct { + stdlog *log.Logger + level Level + depth int +} + +// privateLog logs a message at a given level log the default logger. +// when the level is fatal, it will exit the program. +func (l *defaultLogger) privateLog(lv Level, fmtArgs []interface{}) { + if l.level > lv { + return + } + level := lv.toString() + buf := bytebufferpool.Get() + _, _ = buf.WriteString(level) //nolint:errcheck // It is fine to ignore the error + _, _ = buf.WriteString(fmt.Sprint(fmtArgs...)) //nolint:errcheck // It is fine to ignore the error + + _ = l.stdlog.Output(l.depth, buf.String()) //nolint:errcheck // It is fine to ignore the error + buf.Reset() + bytebufferpool.Put(buf) + if lv == LevelFatal { + os.Exit(1) //nolint:revive // we want to exit the program when Fatal is called + } +} + +// privateLog logs a message at a given level log the default logger. +// when the level is fatal, it will exit the program. +func (l *defaultLogger) privateLogf(lv Level, format string, fmtArgs []interface{}) { + if l.level > lv { + return + } + level := lv.toString() + buf := bytebufferpool.Get() + _, _ = buf.WriteString(level) //nolint:errcheck // It is fine to ignore the error + + if len(fmtArgs) > 0 { + _, _ = fmt.Fprintf(buf, format, fmtArgs...) + } else { + _, _ = fmt.Fprint(buf, fmtArgs...) + } + _ = l.stdlog.Output(l.depth, buf.String()) //nolint:errcheck // It is fine to ignore the error + buf.Reset() + bytebufferpool.Put(buf) + if lv == LevelFatal { + os.Exit(1) //nolint:revive // we want to exit the program when Fatal is called + } +} + +// privateLogw logs a message at a given level log the default logger. +// when the level is fatal, it will exit the program. +func (l *defaultLogger) privateLogw(lv Level, format string, keysAndValues []interface{}) { + if l.level > lv { + return + } + level := lv.toString() + buf := bytebufferpool.Get() + _, _ = buf.WriteString(level) //nolint:errcheck // It is fine to ignore the error + + // Write format privateLog buffer + if format != "" { + _, _ = buf.WriteString(format) //nolint:errcheck // It is fine to ignore the error + } + var once sync.Once + isFirst := true + // Write keys and values privateLog buffer + if len(keysAndValues) > 0 { + if (len(keysAndValues) & 1) == 1 { + keysAndValues = append(keysAndValues, "KEYVALS UNPAIRED") + } + + for i := 0; i < len(keysAndValues); i += 2 { + if format == "" && isFirst { + once.Do(func() { + _, _ = fmt.Fprintf(buf, "%s=%v", keysAndValues[i], keysAndValues[i+1]) + isFirst = false + }) + continue + } + _, _ = fmt.Fprintf(buf, " %s=%v", keysAndValues[i], keysAndValues[i+1]) + } + } + + _ = l.stdlog.Output(l.depth, buf.String()) //nolint:errcheck // It is fine to ignore the error + buf.Reset() + bytebufferpool.Put(buf) + if lv == LevelFatal { + os.Exit(1) //nolint:revive // we want to exit the program when Fatal is called + } +} + +func (l *defaultLogger) Trace(v ...interface{}) { + l.privateLog(LevelTrace, v) +} + +func (l *defaultLogger) Debug(v ...interface{}) { + l.privateLog(LevelDebug, v) +} + +func (l *defaultLogger) Info(v ...interface{}) { + l.privateLog(LevelInfo, v) +} + +func (l *defaultLogger) Warn(v ...interface{}) { + l.privateLog(LevelWarn, v) +} + +func (l *defaultLogger) Error(v ...interface{}) { + l.privateLog(LevelError, v) +} + +func (l *defaultLogger) Fatal(v ...interface{}) { + l.privateLog(LevelFatal, v) +} + +func (l *defaultLogger) Panic(v ...interface{}) { + l.privateLog(LevelPanic, v) +} + +func (l *defaultLogger) Tracef(format string, v ...interface{}) { + l.privateLogf(LevelTrace, format, v) +} + +func (l *defaultLogger) Debugf(format string, v ...interface{}) { + l.privateLogf(LevelDebug, format, v) +} + +func (l *defaultLogger) Infof(format string, v ...interface{}) { + l.privateLogf(LevelInfo, format, v) +} + +func (l *defaultLogger) Warnf(format string, v ...interface{}) { + l.privateLogf(LevelWarn, format, v) +} + +func (l *defaultLogger) Errorf(format string, v ...interface{}) { + l.privateLogf(LevelError, format, v) +} + +func (l *defaultLogger) Fatalf(format string, v ...interface{}) { + l.privateLogf(LevelFatal, format, v) +} + +func (l *defaultLogger) Panicf(format string, v ...interface{}) { + l.privateLogf(LevelPanic, format, v) +} + +func (l *defaultLogger) Tracew(msg string, keysAndValues ...interface{}) { + l.privateLogw(LevelTrace, msg, keysAndValues) +} + +func (l *defaultLogger) Debugw(msg string, keysAndValues ...interface{}) { + l.privateLogw(LevelDebug, msg, keysAndValues) +} + +func (l *defaultLogger) Infow(msg string, keysAndValues ...interface{}) { + l.privateLogw(LevelInfo, msg, keysAndValues) +} + +func (l *defaultLogger) Warnw(msg string, keysAndValues ...interface{}) { + l.privateLogw(LevelWarn, msg, keysAndValues) +} + +func (l *defaultLogger) Errorw(msg string, keysAndValues ...interface{}) { + l.privateLogw(LevelError, msg, keysAndValues) +} + +func (l *defaultLogger) Fatalw(msg string, keysAndValues ...interface{}) { + l.privateLogw(LevelFatal, msg, keysAndValues) +} + +func (l *defaultLogger) Panicw(msg string, keysAndValues ...interface{}) { + l.privateLogw(LevelPanic, msg, keysAndValues) +} + +func (l *defaultLogger) WithContext(_ context.Context) CommonLogger { + return &defaultLogger{ + stdlog: l.stdlog, + level: l.level, + depth: l.depth - 1, + } +} + +func (l *defaultLogger) SetLevel(level Level) { + l.level = level +} + +func (l *defaultLogger) SetOutput(writer io.Writer) { + l.stdlog.SetOutput(writer) +} + +// DefaultLogger returns the default logger. +func DefaultLogger() AllLogger { + return logger +} diff --git a/log/default_test.go b/log/default_test.go new file mode 100644 index 0000000..9dd5fd7 --- /dev/null +++ b/log/default_test.go @@ -0,0 +1,212 @@ +package log + +import ( + "bytes" + "context" + "log" + "os" + "testing" + + "github.com/gofiber/fiber/v2/utils" +) + +const work = "work" + +func initDefaultLogger() { + logger = &defaultLogger{ + stdlog: log.New(os.Stderr, "", 0), + depth: 4, + } +} + +type byteSliceWriter struct { + b []byte +} + +func (w *byteSliceWriter) Write(p []byte) (int, error) { + w.b = append(w.b, p...) + return len(p), nil +} + +func Test_DefaultLogger(t *testing.T) { + initDefaultLogger() + + var w byteSliceWriter + SetOutput(&w) + + Trace("trace work") + Debug("received work order") + Info("starting work") + Warn("work may fail") + Error("work failed") + Panic("work panic") + utils.AssertEqual(t, "[Trace] trace work\n"+ + "[Debug] received work order\n"+ + "[Info] starting work\n"+ + "[Warn] work may fail\n"+ + "[Error] work failed\n"+ + "[Panic] work panic\n", string(w.b)) +} + +func Test_DefaultFormatLogger(t *testing.T) { + initDefaultLogger() + + var w byteSliceWriter + SetOutput(&w) + + Tracef("trace %s", work) + Debugf("received %s order", work) + Infof("starting %s", work) + Warnf("%s may fail", work) + Errorf("%s failed", work) + Panicf("%s panic", work) + + utils.AssertEqual(t, "[Trace] trace work\n"+ + "[Debug] received work order\n"+ + "[Info] starting work\n"+ + "[Warn] work may fail\n"+ + "[Error] work failed\n"+ + "[Panic] work panic\n", string(w.b)) +} + +func Test_CtxLogger(t *testing.T) { + initDefaultLogger() + + var w byteSliceWriter + SetOutput(&w) + + ctx := context.Background() + + WithContext(ctx).Tracef("trace %s", work) + WithContext(ctx).Debugf("received %s order", work) + WithContext(ctx).Infof("starting %s", work) + WithContext(ctx).Warnf("%s may fail", work) + WithContext(ctx).Errorf("%s failed", work) + WithContext(ctx).Panicf("%s panic", work) + + utils.AssertEqual(t, "[Trace] trace work\n"+ + "[Debug] received work order\n"+ + "[Info] starting work\n"+ + "[Warn] work may fail\n"+ + "[Error] work failed\n"+ + "[Panic] work panic\n", string(w.b)) +} + +func Test_LogfKeyAndValues(t *testing.T) { + tests := []struct { + name string + level Level + format string + fmtArgs []interface{} + keysAndValues []interface{} + wantOutput string + }{ + { + name: "test logf with debug level and key-values", + level: LevelDebug, + format: "", + fmtArgs: nil, + keysAndValues: []interface{}{"name", "Bob", "age", 30}, + wantOutput: "[Debug] name=Bob age=30\n", + }, + { + name: "test logf with info level and key-values", + level: LevelInfo, + format: "", + fmtArgs: nil, + keysAndValues: []interface{}{"status", "ok", "code", 200}, + wantOutput: "[Info] status=ok code=200\n", + }, + { + name: "test logf with warn level and key-values", + level: LevelWarn, + format: "", + fmtArgs: nil, + keysAndValues: []interface{}{"error", "not found", "id", 123}, + wantOutput: "[Warn] error=not found id=123\n", + }, + { + name: "test logf with format and key-values", + level: LevelWarn, + format: "test", + fmtArgs: nil, + keysAndValues: []interface{}{"error", "not found", "id", 123}, + wantOutput: "[Warn] test error=not found id=123\n", + }, + { + name: "test logf with one key", + level: LevelWarn, + format: "", + fmtArgs: nil, + keysAndValues: []interface{}{"error"}, + wantOutput: "[Warn] error=KEYVALS UNPAIRED\n", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var buf bytes.Buffer + l := &defaultLogger{ + stdlog: log.New(&buf, "", 0), + level: tt.level, + depth: 4, + } + l.privateLogw(tt.level, tt.format, tt.keysAndValues) + utils.AssertEqual(t, tt.wantOutput, buf.String()) + }) + } +} + +func Test_WithContextCaller(t *testing.T) { + logger = &defaultLogger{ + stdlog: log.New(os.Stderr, "", log.Lshortfile), + depth: 4, + } + + var w byteSliceWriter + SetOutput(&w) + ctx := context.TODO() + + WithContext(ctx).Info("") + Info("") + + utils.AssertEqual(t, "default_test.go:169: [Info] \ndefault_test.go:170: [Info] \n", string(w.b)) +} + +func Test_SetLevel(t *testing.T) { + setLogger := &defaultLogger{ + stdlog: log.New(os.Stderr, "", log.LstdFlags|log.Lshortfile|log.Lmicroseconds), + depth: 4, + } + + setLogger.SetLevel(LevelTrace) + utils.AssertEqual(t, LevelTrace, setLogger.level) + utils.AssertEqual(t, LevelTrace.toString(), setLogger.level.toString()) + + setLogger.SetLevel(LevelDebug) + utils.AssertEqual(t, LevelDebug, setLogger.level) + utils.AssertEqual(t, LevelDebug.toString(), setLogger.level.toString()) + + setLogger.SetLevel(LevelInfo) + utils.AssertEqual(t, LevelInfo, setLogger.level) + utils.AssertEqual(t, LevelInfo.toString(), setLogger.level.toString()) + + setLogger.SetLevel(LevelWarn) + utils.AssertEqual(t, LevelWarn, setLogger.level) + utils.AssertEqual(t, LevelWarn.toString(), setLogger.level.toString()) + + setLogger.SetLevel(LevelError) + utils.AssertEqual(t, LevelError, setLogger.level) + utils.AssertEqual(t, LevelError.toString(), setLogger.level.toString()) + + setLogger.SetLevel(LevelFatal) + utils.AssertEqual(t, LevelFatal, setLogger.level) + utils.AssertEqual(t, LevelFatal.toString(), setLogger.level.toString()) + + setLogger.SetLevel(LevelPanic) + utils.AssertEqual(t, LevelPanic, setLogger.level) + utils.AssertEqual(t, LevelPanic.toString(), setLogger.level.toString()) + + setLogger.SetLevel(8) + utils.AssertEqual(t, 8, int(setLogger.level)) + utils.AssertEqual(t, "[?8] ", setLogger.level.toString()) +} diff --git a/log/fiberlog.go b/log/fiberlog.go new file mode 100644 index 0000000..90333ee --- /dev/null +++ b/log/fiberlog.go @@ -0,0 +1,141 @@ +package log + +import ( + "context" + "io" +) + +// Fatal calls the default logger's Fatal method and then os.Exit(1). +func Fatal(v ...interface{}) { + logger.Fatal(v...) +} + +// Error calls the default logger's Error method. +func Error(v ...interface{}) { + logger.Error(v...) +} + +// Warn calls the default logger's Warn method. +func Warn(v ...interface{}) { + logger.Warn(v...) +} + +// Info calls the default logger's Info method. +func Info(v ...interface{}) { + logger.Info(v...) +} + +// Debug calls the default logger's Debug method. +func Debug(v ...interface{}) { + logger.Debug(v...) +} + +// Trace calls the default logger's Trace method. +func Trace(v ...interface{}) { + logger.Trace(v...) +} + +// Panic calls the default logger's Panic method. +func Panic(v ...interface{}) { + logger.Panic(v...) +} + +// Fatalf calls the default logger's Fatalf method and then os.Exit(1). +func Fatalf(format string, v ...interface{}) { + logger.Fatalf(format, v...) +} + +// Errorf calls the default logger's Errorf method. +func Errorf(format string, v ...interface{}) { + logger.Errorf(format, v...) +} + +// Warnf calls the default logger's Warnf method. +func Warnf(format string, v ...interface{}) { + logger.Warnf(format, v...) +} + +// Infof calls the default logger's Infof method. +func Infof(format string, v ...interface{}) { + logger.Infof(format, v...) +} + +// Debugf calls the default logger's Debugf method. +func Debugf(format string, v ...interface{}) { + logger.Debugf(format, v...) +} + +// Tracef calls the default logger's Tracef method. +func Tracef(format string, v ...interface{}) { + logger.Tracef(format, v...) +} + +// Panicf calls the default logger's Tracef method. +func Panicf(format string, v ...interface{}) { + logger.Panicf(format, v...) +} + +// Tracew logs a message with some additional context. The variadic key-value +// pairs are treated as they are privateLog With. +func Tracew(msg string, keysAndValues ...interface{}) { + logger.Tracew(msg, keysAndValues...) +} + +// Debugw logs a message with some additional context. The variadic key-value +// pairs are treated as they are privateLog With. +func Debugw(msg string, keysAndValues ...interface{}) { + logger.Debugw(msg, keysAndValues...) +} + +// Infow logs a message with some additional context. The variadic key-value +// pairs are treated as they are privateLog With. +func Infow(msg string, keysAndValues ...interface{}) { + logger.Infow(msg, keysAndValues...) +} + +// Warnw logs a message with some additional context. The variadic key-value +// pairs are treated as they are privateLog With. +func Warnw(msg string, keysAndValues ...interface{}) { + logger.Warnw(msg, keysAndValues...) +} + +// Errorw logs a message with some additional context. The variadic key-value +// pairs are treated as they are privateLog With. +func Errorw(msg string, keysAndValues ...interface{}) { + logger.Errorw(msg, keysAndValues...) +} + +// Fatalw logs a message with some additional context. The variadic key-value +// pairs are treated as they are privateLog With. +func Fatalw(msg string, keysAndValues ...interface{}) { + logger.Fatalw(msg, keysAndValues...) +} + +// Panicw logs a message with some additional context. The variadic key-value +// pairs are treated as they are privateLog With. +func Panicw(msg string, keysAndValues ...interface{}) { + logger.Panicw(msg, keysAndValues...) +} + +func WithContext(ctx context.Context) CommonLogger { + return logger.WithContext(ctx) +} + +// SetLogger sets the default logger and the system logger. +// Note that this method is not concurrent-safe and must not be called +// after the use of DefaultLogger and global functions privateLog this package. +func SetLogger(v AllLogger) { + logger = v +} + +// SetOutput sets the output of default logger and system logger. By default, it is stderr. +func SetOutput(w io.Writer) { + logger.SetOutput(w) +} + +// SetLevel sets the level of logs below which logs will not be output. +// The default logger is LevelTrace. +// Note that this method is not concurrent-safe. +func SetLevel(lv Level) { + logger.SetLevel(lv) +} diff --git a/log/fiberlog_test.go b/log/fiberlog_test.go new file mode 100644 index 0000000..15b1a2c --- /dev/null +++ b/log/fiberlog_test.go @@ -0,0 +1,24 @@ +package log + +import ( + "log" + "os" + "testing" + + "github.com/gofiber/fiber/v2/utils" +) + +func Test_DefaultSystemLogger(t *testing.T) { + defaultL := DefaultLogger() + utils.AssertEqual(t, logger, defaultL) +} + +func Test_SetLogger(t *testing.T) { + setLog := &defaultLogger{ + stdlog: log.New(os.Stderr, "", log.LstdFlags|log.Lshortfile|log.Lmicroseconds), + depth: 6, + } + + SetLogger(setLog) + utils.AssertEqual(t, logger, setLog) +} diff --git a/log/log.go b/log/log.go new file mode 100644 index 0000000..31b4cc8 --- /dev/null +++ b/log/log.go @@ -0,0 +1,100 @@ +package log + +import ( + "context" + "fmt" + "io" + "log" + "os" +) + +var logger AllLogger = &defaultLogger{ + stdlog: log.New(os.Stderr, "", log.LstdFlags|log.Lshortfile|log.Lmicroseconds), + depth: 4, +} + +// Logger is a logger interface that provides logging function with levels. +type Logger interface { + Trace(v ...interface{}) + Debug(v ...interface{}) + Info(v ...interface{}) + Warn(v ...interface{}) + Error(v ...interface{}) + Fatal(v ...interface{}) + Panic(v ...interface{}) +} + +// FormatLogger is a logger interface that output logs with a format. +type FormatLogger interface { + Tracef(format string, v ...interface{}) + Debugf(format string, v ...interface{}) + Infof(format string, v ...interface{}) + Warnf(format string, v ...interface{}) + Errorf(format string, v ...interface{}) + Fatalf(format string, v ...interface{}) + Panicf(format string, v ...interface{}) +} + +// WithLogger is a logger interface that output logs with a message and key-value pairs. +type WithLogger interface { + Tracew(msg string, keysAndValues ...interface{}) + Debugw(msg string, keysAndValues ...interface{}) + Infow(msg string, keysAndValues ...interface{}) + Warnw(msg string, keysAndValues ...interface{}) + Errorw(msg string, keysAndValues ...interface{}) + Fatalw(msg string, keysAndValues ...interface{}) + Panicw(msg string, keysAndValues ...interface{}) +} + +type CommonLogger interface { + Logger + FormatLogger + WithLogger +} + +// ControlLogger provides methods to config a logger. +type ControlLogger interface { + SetLevel(Level) + SetOutput(io.Writer) +} + +// AllLogger is the combination of Logger, FormatLogger, CtxLogger and ControlLogger. +// Custom extensions can be made through AllLogger +type AllLogger interface { + CommonLogger + ControlLogger + WithContext(ctx context.Context) CommonLogger +} + +// Level defines the priority of a log message. +// When a logger is configured with a level, any log message with a lower +// log level (smaller by integer comparison) will not be output. +type Level int + +// The levels of logs. +const ( + LevelTrace Level = iota + LevelDebug + LevelInfo + LevelWarn + LevelError + LevelFatal + LevelPanic +) + +var strs = []string{ + "[Trace] ", + "[Debug] ", + "[Info] ", + "[Warn] ", + "[Error] ", + "[Fatal] ", + "[Panic] ", +} + +func (lv Level) toString() string { + if lv >= LevelTrace && lv <= LevelPanic { + return strs[lv] + } + return fmt.Sprintf("[?%d] ", lv) +} diff --git a/middleware/adaptor/adaptor.go b/middleware/adaptor/adaptor.go new file mode 100644 index 0000000..8bd6f3f --- /dev/null +++ b/middleware/adaptor/adaptor.go @@ -0,0 +1,171 @@ +package adaptor + +import ( + "io" + "net" + "net/http" + "reflect" + "unsafe" + + "github.com/valyala/fasthttp" + "github.com/valyala/fasthttp/fasthttpadaptor" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/utils" +) + +// HTTPHandlerFunc wraps net/http handler func to fiber handler +func HTTPHandlerFunc(h http.HandlerFunc) fiber.Handler { + return HTTPHandler(h) +} + +// HTTPHandler wraps net/http handler to fiber handler +func HTTPHandler(h http.Handler) fiber.Handler { + return func(c *fiber.Ctx) error { + handler := fasthttpadaptor.NewFastHTTPHandler(h) + handler(c.Context()) + return nil + } +} + +// ConvertRequest converts a fiber.Ctx to a http.Request. +// forServer should be set to true when the http.Request is going to be passed to a http.Handler. +func ConvertRequest(c *fiber.Ctx, forServer bool) (*http.Request, error) { + var req http.Request + if err := fasthttpadaptor.ConvertRequest(c.Context(), &req, forServer); err != nil { + return nil, err //nolint:wrapcheck // This must not be wrapped + } + return &req, nil +} + +// CopyContextToFiberContext copies the values of context.Context to a fasthttp.RequestCtx +func CopyContextToFiberContext(context interface{}, requestContext *fasthttp.RequestCtx) { + contextValues := reflect.ValueOf(context).Elem() + contextKeys := reflect.TypeOf(context).Elem() + if contextKeys.Kind() == reflect.Struct { + var lastKey interface{} + for i := 0; i < contextValues.NumField(); i++ { + reflectValue := contextValues.Field(i) + /* #nosec */ + reflectValue = reflect.NewAt(reflectValue.Type(), unsafe.Pointer(reflectValue.UnsafeAddr())).Elem() + + reflectField := contextKeys.Field(i) + + if reflectField.Name == "noCopy" { + break + } else if reflectField.Name == "Context" { + CopyContextToFiberContext(reflectValue.Interface(), requestContext) + } else if reflectField.Name == "key" { + lastKey = reflectValue.Interface() + } else if lastKey != nil && reflectField.Name == "val" { + requestContext.SetUserValue(lastKey, reflectValue.Interface()) + } else { + lastKey = nil + } + } + } +} + +// HTTPMiddleware wraps net/http middleware to fiber middleware +func HTTPMiddleware(mw func(http.Handler) http.Handler) fiber.Handler { + return func(c *fiber.Ctx) error { + var next bool + nextHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + next = true + // Convert again in case request may modify by middleware + c.Request().Header.SetMethod(r.Method) + c.Request().SetRequestURI(r.RequestURI) + c.Request().SetHost(r.Host) + c.Request().Header.SetHost(r.Host) + for key, val := range r.Header { + for _, v := range val { + c.Request().Header.Set(key, v) + } + } + CopyContextToFiberContext(r.Context(), c.Context()) + }) + + if err := HTTPHandler(mw(nextHandler))(c); err != nil { + return err + } + + if next { + return c.Next() + } + return nil + } +} + +// FiberHandler wraps fiber handler to net/http handler +func FiberHandler(h fiber.Handler) http.Handler { + return FiberHandlerFunc(h) +} + +// FiberHandlerFunc wraps fiber handler to net/http handler func +func FiberHandlerFunc(h fiber.Handler) http.HandlerFunc { + return handlerFunc(fiber.New(), h) +} + +// FiberApp wraps fiber app to net/http handler func +func FiberApp(app *fiber.App) http.HandlerFunc { + return handlerFunc(app) +} + +func handlerFunc(app *fiber.App, h ...fiber.Handler) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + // New fasthttp request + req := fasthttp.AcquireRequest() + defer fasthttp.ReleaseRequest(req) + // Convert net/http -> fasthttp request + if r.Body != nil { + n, err := io.Copy(req.BodyWriter(), r.Body) + req.Header.SetContentLength(int(n)) + + if err != nil { + http.Error(w, utils.StatusMessage(fiber.StatusInternalServerError), fiber.StatusInternalServerError) + return + } + } + req.Header.SetMethod(r.Method) + req.SetRequestURI(r.RequestURI) + req.SetHost(r.Host) + req.Header.SetHost(r.Host) + for key, val := range r.Header { + for _, v := range val { + req.Header.Set(key, v) + } + } + if _, _, err := net.SplitHostPort(r.RemoteAddr); err != nil && err.(*net.AddrError).Err == "missing port in address" { //nolint:errorlint, forcetypeassert // overlinting + r.RemoteAddr = net.JoinHostPort(r.RemoteAddr, "80") + } + remoteAddr, err := net.ResolveTCPAddr("tcp", r.RemoteAddr) + if err != nil { + http.Error(w, utils.StatusMessage(fiber.StatusInternalServerError), fiber.StatusInternalServerError) + return + } + + // New fasthttp Ctx + var fctx fasthttp.RequestCtx + fctx.Init(req, remoteAddr, nil) + if len(h) > 0 { + // New fiber Ctx + ctx := app.AcquireCtx(&fctx) + defer app.ReleaseCtx(ctx) + // Execute fiber Ctx + err := h[0](ctx) + if err != nil { + _ = app.Config().ErrorHandler(ctx, err) //nolint:errcheck // not needed + } + } else { + // Execute fasthttp Ctx though app.Handler + app.Handler()(&fctx) + } + + // Convert fasthttp Ctx > net/http + fctx.Response.Header.VisitAll(func(k, v []byte) { + w.Header().Add(string(k), string(v)) + }) + w.WriteHeader(fctx.Response.StatusCode()) + _, _ = w.Write(fctx.Response.Body()) //nolint:errcheck // not needed + } +} diff --git a/middleware/adaptor/adaptor_test.go b/middleware/adaptor/adaptor_test.go new file mode 100644 index 0000000..dc52704 --- /dev/null +++ b/middleware/adaptor/adaptor_test.go @@ -0,0 +1,492 @@ +//nolint:bodyclose, contextcheck, revive // Much easier to just ignore memory leaks in tests +package adaptor + +import ( + "bytes" + "context" + "fmt" + "io" + "net" + "net/http" + "net/http/httptest" + "net/url" + "testing" + + "github.com/valyala/fasthttp" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/utils" +) + +func Test_HTTPHandler(t *testing.T) { + expectedMethod := fiber.MethodPost + expectedProto := "HTTP/1.1" + expectedProtoMajor := 1 + expectedProtoMinor := 1 + expectedRequestURI := "/foo/bar?baz=123" + expectedBody := "body 123 foo bar baz" + expectedContentLength := len(expectedBody) + expectedHost := "foobar.com" + expectedRemoteAddr := "1.2.3.4:6789" + expectedHeader := map[string]string{ + "Foo-Bar": "baz", + "Abc": "defg", + "XXX-Remote-Addr": "123.43.4543.345", + } + expectedURL, err := url.ParseRequestURI(expectedRequestURI) + utils.AssertEqual(t, nil, err) + + type contextKeyType string + expectedContextKey := contextKeyType("contextKey") + expectedContextValue := "contextValue" + + callsCount := 0 + nethttpH := func(w http.ResponseWriter, r *http.Request) { + callsCount++ + utils.AssertEqual(t, expectedMethod, r.Method, "Method") + utils.AssertEqual(t, expectedProto, r.Proto, "Proto") + utils.AssertEqual(t, expectedProtoMajor, r.ProtoMajor, "ProtoMajor") + utils.AssertEqual(t, expectedProtoMinor, r.ProtoMinor, "ProtoMinor") + utils.AssertEqual(t, expectedRequestURI, r.RequestURI, "RequestURI") + utils.AssertEqual(t, expectedContentLength, int(r.ContentLength), "ContentLength") + utils.AssertEqual(t, 0, len(r.TransferEncoding), "TransferEncoding") + utils.AssertEqual(t, expectedHost, r.Host, "Host") + utils.AssertEqual(t, expectedRemoteAddr, r.RemoteAddr, "RemoteAddr") + + body, err := io.ReadAll(r.Body) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, expectedBody, string(body), "Body") + utils.AssertEqual(t, expectedURL, r.URL, "URL") + utils.AssertEqual(t, expectedContextValue, r.Context().Value(expectedContextKey), "Context") + + for k, expectedV := range expectedHeader { + v := r.Header.Get(k) + utils.AssertEqual(t, expectedV, v, "Header") + } + + w.Header().Set("Header1", "value1") + w.Header().Set("Header2", "value2") + w.WriteHeader(http.StatusBadRequest) + fmt.Fprintf(w, "request body is %q", body) + } + fiberH := HTTPHandlerFunc(http.HandlerFunc(nethttpH)) + fiberH = setFiberContextValueMiddleware(fiberH, expectedContextKey, expectedContextValue) + + var fctx fasthttp.RequestCtx + var req fasthttp.Request + + req.Header.SetMethod(expectedMethod) + req.SetRequestURI(expectedRequestURI) + req.Header.SetHost(expectedHost) + req.BodyWriter().Write([]byte(expectedBody)) //nolint:errcheck, gosec // not needed + for k, v := range expectedHeader { + req.Header.Set(k, v) + } + + remoteAddr, err := net.ResolveTCPAddr("tcp", expectedRemoteAddr) + utils.AssertEqual(t, nil, err) + + fctx.Init(&req, remoteAddr, nil) + app := fiber.New() + ctx := app.AcquireCtx(&fctx) + defer app.ReleaseCtx(ctx) + + err = fiberH(ctx) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, 1, callsCount, "callsCount") + + resp := &fctx.Response + utils.AssertEqual(t, http.StatusBadRequest, resp.StatusCode(), "StatusCode") + utils.AssertEqual(t, "value1", string(resp.Header.Peek("Header1")), "Header1") + utils.AssertEqual(t, "value2", string(resp.Header.Peek("Header2")), "Header2") + + expectedResponseBody := fmt.Sprintf("request body is %q", expectedBody) + utils.AssertEqual(t, expectedResponseBody, string(resp.Body()), "Body") +} + +type contextKey string + +func (c contextKey) String() string { + return "test-" + string(c) +} + +var ( + TestContextKey = contextKey("TestContextKey") + TestContextSecondKey = contextKey("TestContextSecondKey") +) + +func Test_HTTPMiddleware(t *testing.T) { + const expectedHost = "foobar.com" + tests := []struct { + name string + url string + method string + statusCode int + }{ + { + name: "Should return 200", + url: "/", + method: "POST", + statusCode: 200, + }, + { + name: "Should return 405", + url: "/", + method: "GET", + statusCode: 405, + }, + { + name: "Should return 400", + url: "/unknown", + method: "POST", + statusCode: 404, + }, + } + + nethttpMW := func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + w.WriteHeader(http.StatusMethodNotAllowed) + return + } + + r = r.WithContext(context.WithValue(r.Context(), TestContextKey, "okay")) + r = r.WithContext(context.WithValue(r.Context(), TestContextSecondKey, "not_okay")) + r = r.WithContext(context.WithValue(r.Context(), TestContextSecondKey, "okay")) + + next.ServeHTTP(w, r) + }) + } + + app := fiber.New() + app.Use(HTTPMiddleware(nethttpMW)) + app.Post("/", func(c *fiber.Ctx) error { + value := c.Context().Value(TestContextKey) + val, ok := value.(string) + if !ok { + t.Error("unexpected error on type-assertion") + } + if value != nil { + c.Set("context_okay", val) + } + value = c.Context().Value(TestContextSecondKey) + if value != nil { + val, ok := value.(string) + if !ok { + t.Error("unexpected error on type-assertion") + } + c.Set("context_second_okay", val) + } + return c.SendStatus(fiber.StatusOK) + }) + + for _, tt := range tests { + req, err := http.NewRequestWithContext(context.Background(), tt.method, tt.url, nil) + req.Host = expectedHost + utils.AssertEqual(t, nil, err) + + resp, err := app.Test(req) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, tt.statusCode, resp.StatusCode, "StatusCode") + } + + req, err := http.NewRequestWithContext(context.Background(), fiber.MethodPost, "/", nil) + req.Host = expectedHost + utils.AssertEqual(t, nil, err) + + resp, err := app.Test(req) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, resp.Header.Get("context_okay"), "okay") + utils.AssertEqual(t, resp.Header.Get("context_second_okay"), "okay") +} + +func Test_FiberHandler(t *testing.T) { + testFiberToHandlerFunc(t, false) +} + +func Test_FiberApp(t *testing.T) { + testFiberToHandlerFunc(t, false, fiber.New()) +} + +func Test_FiberHandlerDefaultPort(t *testing.T) { + testFiberToHandlerFunc(t, true) +} + +func Test_FiberAppDefaultPort(t *testing.T) { + testFiberToHandlerFunc(t, true, fiber.New()) +} + +func testFiberToHandlerFunc(t *testing.T, checkDefaultPort bool, app ...*fiber.App) { + t.Helper() + + expectedMethod := fiber.MethodPost + expectedRequestURI := "/foo/bar?baz=123" + expectedBody := "body 123 foo bar baz" + expectedContentLength := len(expectedBody) + expectedHost := "foobar.com" + expectedRemoteAddr := "1.2.3.4:6789" + if checkDefaultPort { + expectedRemoteAddr = "1.2.3.4:80" + } + expectedHeader := map[string]string{ + "Foo-Bar": "baz", + "Abc": "defg", + "XXX-Remote-Addr": "123.43.4543.345", + } + expectedURL, err := url.ParseRequestURI(expectedRequestURI) + utils.AssertEqual(t, nil, err) + + callsCount := 0 + fiberH := func(c *fiber.Ctx) error { + callsCount++ + utils.AssertEqual(t, expectedMethod, c.Method(), "Method") + utils.AssertEqual(t, expectedRequestURI, string(c.Context().RequestURI()), "RequestURI") + utils.AssertEqual(t, expectedContentLength, c.Context().Request.Header.ContentLength(), "ContentLength") + utils.AssertEqual(t, expectedHost, c.Hostname(), "Host") + utils.AssertEqual(t, expectedHost, string(c.Request().Header.Host()), "Host") + utils.AssertEqual(t, "http://"+expectedHost, c.BaseURL(), "BaseURL") + utils.AssertEqual(t, expectedRemoteAddr, c.Context().RemoteAddr().String(), "RemoteAddr") + + body := string(c.Body()) + utils.AssertEqual(t, expectedBody, body, "Body") + utils.AssertEqual(t, expectedURL.String(), c.OriginalURL(), "URL") + + for k, expectedV := range expectedHeader { + v := c.Get(k) + utils.AssertEqual(t, expectedV, v, "Header") + } + + c.Set("Header1", "value1") + c.Set("Header2", "value2") + c.Status(fiber.StatusBadRequest) + _, err := c.Write([]byte(fmt.Sprintf("request body is %q", body))) + return err + } + + var handlerFunc http.HandlerFunc + if len(app) > 0 { + app[0].Post("/foo/bar", fiberH) + handlerFunc = FiberApp(app[0]) + } else { + handlerFunc = FiberHandlerFunc(fiberH) + } + + var r http.Request + + r.Method = expectedMethod + r.Body = &netHTTPBody{[]byte(expectedBody)} + r.RequestURI = expectedRequestURI + r.ContentLength = int64(expectedContentLength) + r.Host = expectedHost + r.RemoteAddr = expectedRemoteAddr + if checkDefaultPort { + r.RemoteAddr = "1.2.3.4" + } + + hdr := make(http.Header) + for k, v := range expectedHeader { + hdr.Set(k, v) + } + r.Header = hdr + + var w netHTTPResponseWriter + handlerFunc.ServeHTTP(&w, &r) + + utils.AssertEqual(t, http.StatusBadRequest, w.StatusCode(), "StatusCode") + utils.AssertEqual(t, "value1", w.Header().Get("Header1"), "Header1") + utils.AssertEqual(t, "value2", w.Header().Get("Header2"), "Header2") + + expectedResponseBody := fmt.Sprintf("request body is %q", expectedBody) + utils.AssertEqual(t, expectedResponseBody, string(w.body), "Body") +} + +func setFiberContextValueMiddleware(next fiber.Handler, key, value interface{}) fiber.Handler { + return func(c *fiber.Ctx) error { + c.Locals(key, value) + return next(c) + } +} + +func Test_FiberHandler_RequestNilBody(t *testing.T) { + expectedMethod := fiber.MethodGet + expectedRequestURI := "/foo/bar" + expectedContentLength := 0 + + callsCount := 0 + fiberH := func(c *fiber.Ctx) error { + callsCount++ + utils.AssertEqual(t, expectedMethod, c.Method(), "Method") + utils.AssertEqual(t, expectedRequestURI, string(c.Context().RequestURI()), "RequestURI") + utils.AssertEqual(t, expectedContentLength, c.Context().Request.Header.ContentLength(), "ContentLength") + + _, err := c.Write([]byte("request body is nil")) + return err + } + nethttpH := FiberHandler(fiberH) + + var r http.Request + + r.Method = expectedMethod + r.RequestURI = expectedRequestURI + + var w netHTTPResponseWriter + nethttpH.ServeHTTP(&w, &r) + + expectedResponseBody := "request body is nil" + utils.AssertEqual(t, expectedResponseBody, string(w.body), "Body") +} + +type netHTTPBody struct { + b []byte +} + +func (r *netHTTPBody) Read(p []byte) (int, error) { + if len(r.b) == 0 { + return 0, io.EOF + } + n := copy(p, r.b) + r.b = r.b[n:] + return n, nil +} + +func (r *netHTTPBody) Close() error { + r.b = r.b[:0] + return nil +} + +type netHTTPResponseWriter struct { + statusCode int + h http.Header + body []byte +} + +func (w *netHTTPResponseWriter) StatusCode() int { + if w.statusCode == 0 { + return http.StatusOK + } + return w.statusCode +} + +func (w *netHTTPResponseWriter) Header() http.Header { + if w.h == nil { + w.h = make(http.Header) + } + return w.h +} + +func (w *netHTTPResponseWriter) WriteHeader(statusCode int) { + w.statusCode = statusCode +} + +func (w *netHTTPResponseWriter) Write(p []byte) (int, error) { + w.body = append(w.body, p...) + return len(p), nil +} + +func Test_ConvertRequest(t *testing.T) { + t.Parallel() + + app := fiber.New() + + app.Get("/test", func(c *fiber.Ctx) error { + httpReq, err := ConvertRequest(c, false) + if err != nil { + return err + } + + return c.SendString("Request URL: " + httpReq.URL.String()) + }) + + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/test?hello=world&another=test", http.NoBody)) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, http.StatusOK, resp.StatusCode, "Status code") + + body, err := io.ReadAll(resp.Body) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, "Request URL: /test?hello=world&another=test", string(body)) +} + +// Benchmark for FiberHandlerFunc +func Benchmark_FiberHandlerFunc_1MB(b *testing.B) { + fiberH := func(c *fiber.Ctx) error { + return c.SendStatus(fiber.StatusOK) + } + handlerFunc := FiberHandlerFunc(fiberH) + + // Create body content + bodyContent := make([]byte, 1*1024*1024) + bodyBuffer := bytes.NewBuffer(bodyContent) + + r := http.Request{ + Method: http.MethodPost, + Body: http.NoBody, + } + + // Replace the empty Body with our buffer + r.Body = io.NopCloser(bodyBuffer) + defer r.Body.Close() //nolint:errcheck // not needed + + // Create recorder + w := httptest.NewRecorder() + + b.ResetTimer() + for i := 0; i < b.N; i++ { + handlerFunc.ServeHTTP(w, &r) + } +} + +func Benchmark_FiberHandlerFunc_10MB(b *testing.B) { + fiberH := func(c *fiber.Ctx) error { + return c.SendStatus(fiber.StatusOK) + } + handlerFunc := FiberHandlerFunc(fiberH) + + // Create body content + bodyContent := make([]byte, 10*1024*1024) + bodyBuffer := bytes.NewBuffer(bodyContent) + + r := http.Request{ + Method: http.MethodPost, + Body: http.NoBody, + } + + // Replace the empty Body with our buffer + r.Body = io.NopCloser(bodyBuffer) + defer r.Body.Close() //nolint:errcheck // not needed + + // Create recorder + w := httptest.NewRecorder() + + b.ResetTimer() + for i := 0; i < b.N; i++ { + handlerFunc.ServeHTTP(w, &r) + } +} + +func Benchmark_FiberHandlerFunc_50MB(b *testing.B) { + fiberH := func(c *fiber.Ctx) error { + return c.SendStatus(fiber.StatusOK) + } + handlerFunc := FiberHandlerFunc(fiberH) + + // Create body content + bodyContent := make([]byte, 50*1024*1024) + bodyBuffer := bytes.NewBuffer(bodyContent) + + r := http.Request{ + Method: http.MethodPost, + Body: http.NoBody, + } + + // Replace the empty Body with our buffer + r.Body = io.NopCloser(bodyBuffer) + defer r.Body.Close() //nolint:errcheck // not needed + + // Create recorder + w := httptest.NewRecorder() + + b.ResetTimer() + for i := 0; i < b.N; i++ { + handlerFunc.ServeHTTP(w, &r) + } +} diff --git a/middleware/basicauth/basicauth.go b/middleware/basicauth/basicauth.go new file mode 100644 index 0000000..c3ed62b --- /dev/null +++ b/middleware/basicauth/basicauth.go @@ -0,0 +1,60 @@ +package basicauth + +import ( + "encoding/base64" + "strings" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/utils" +) + +// New creates a new middleware handler +func New(config Config) fiber.Handler { + // Set default config + cfg := configDefault(config) + + // Return new handler + return func(c *fiber.Ctx) error { + // Don't execute middleware if Next returns true + if cfg.Next != nil && cfg.Next(c) { + return c.Next() + } + + // Get authorization header + auth := c.Get(fiber.HeaderAuthorization) + + // Check if the header contains content besides "basic". + if len(auth) <= 6 || !utils.EqualFold(auth[:6], "basic ") { + return cfg.Unauthorized(c) + } + + // Decode the header contents + raw, err := base64.StdEncoding.DecodeString(auth[6:]) + if err != nil { + return cfg.Unauthorized(c) + } + + // Get the credentials + creds := utils.UnsafeString(raw) + + // Check if the credentials are in the correct form + // which is "username:password". + index := strings.Index(creds, ":") + if index == -1 { + return cfg.Unauthorized(c) + } + + // Get the username and password + username := creds[:index] + password := creds[index+1:] + + if cfg.Authorizer(username, password) { + c.Locals(cfg.ContextUsername, username) + c.Locals(cfg.ContextPassword, password) + return c.Next() + } + + // Authentication failed + return cfg.Unauthorized(c) + } +} diff --git a/middleware/basicauth/basicauth_test.go b/middleware/basicauth/basicauth_test.go new file mode 100644 index 0000000..0722516 --- /dev/null +++ b/middleware/basicauth/basicauth_test.go @@ -0,0 +1,154 @@ +package basicauth + +import ( + "encoding/base64" + "fmt" + "io" + "net/http/httptest" + "testing" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/utils" + + "github.com/valyala/fasthttp" +) + +// go test -run Test_BasicAuth_Next +func Test_BasicAuth_Next(t *testing.T) { + t.Parallel() + app := fiber.New() + app.Use(New(Config{ + Next: func(_ *fiber.Ctx) bool { + return true + }, + })) + + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, fiber.StatusNotFound, resp.StatusCode) +} + +func Test_Middleware_BasicAuth(t *testing.T) { + t.Parallel() + app := fiber.New() + + app.Use(New(Config{ + Users: map[string]string{ + "john": "doe", + "admin": "123456", + }, + })) + + //nolint:forcetypeassert,errcheck // TODO: Do not force-type assert + app.Get("/testauth", func(c *fiber.Ctx) error { + username := c.Locals("username").(string) + password := c.Locals("password").(string) + + return c.SendString(username + password) + }) + + tests := []struct { + url string + statusCode int + username string + password string + }{ + { + url: "/testauth", + statusCode: 200, + username: "john", + password: "doe", + }, + { + url: "/testauth", + statusCode: 200, + username: "admin", + password: "123456", + }, + { + url: "/testauth", + statusCode: 401, + username: "ee", + password: "123456", + }, + } + + for _, tt := range tests { + // Base64 encode credentials for http auth header + creds := base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s:%s", tt.username, tt.password))) + + req := httptest.NewRequest(fiber.MethodGet, "/testauth", nil) + req.Header.Add("Authorization", "Basic "+creds) + resp, err := app.Test(req) + utils.AssertEqual(t, nil, err) + + body, err := io.ReadAll(resp.Body) + + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, tt.statusCode, resp.StatusCode) + + if tt.statusCode == 200 { + utils.AssertEqual(t, fmt.Sprintf("%s%s", tt.username, tt.password), string(body)) + } + } +} + +// go test -v -run=^$ -bench=Benchmark_Middleware_BasicAuth -benchmem -count=4 +func Benchmark_Middleware_BasicAuth(b *testing.B) { + app := fiber.New() + + app.Use(New(Config{ + Users: map[string]string{ + "john": "doe", + }, + })) + app.Get("/", func(c *fiber.Ctx) error { + return c.SendStatus(fiber.StatusTeapot) + }) + + h := app.Handler() + + fctx := &fasthttp.RequestCtx{} + fctx.Request.Header.SetMethod(fiber.MethodGet) + fctx.Request.SetRequestURI("/") + fctx.Request.Header.Set(fiber.HeaderAuthorization, "basic am9objpkb2U=") // john:doe + + b.ReportAllocs() + b.ResetTimer() + + for n := 0; n < b.N; n++ { + h(fctx) + } + + utils.AssertEqual(b, fiber.StatusTeapot, fctx.Response.Header.StatusCode()) +} + +// go test -v -run=^$ -bench=Benchmark_Middleware_BasicAuth -benchmem -count=4 +func Benchmark_Middleware_BasicAuth_Upper(b *testing.B) { + app := fiber.New() + + app.Use(New(Config{ + Users: map[string]string{ + "john": "doe", + }, + })) + app.Get("/", func(c *fiber.Ctx) error { + return c.SendStatus(fiber.StatusTeapot) + }) + + h := app.Handler() + + fctx := &fasthttp.RequestCtx{} + fctx.Request.Header.SetMethod(fiber.MethodGet) + fctx.Request.SetRequestURI("/") + fctx.Request.Header.Set(fiber.HeaderAuthorization, "Basic am9objpkb2U=") // john:doe + + b.ReportAllocs() + b.ResetTimer() + + for n := 0; n < b.N; n++ { + h(fctx) + } + + utils.AssertEqual(b, fiber.StatusTeapot, fctx.Response.Header.StatusCode()) +} diff --git a/middleware/basicauth/config.go b/middleware/basicauth/config.go new file mode 100644 index 0000000..d69f48b --- /dev/null +++ b/middleware/basicauth/config.go @@ -0,0 +1,105 @@ +package basicauth + +import ( + "crypto/subtle" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/utils" +) + +// Config defines the config for middleware. +type Config struct { + // Next defines a function to skip this middleware when returned true. + // + // Optional. Default: nil + Next func(c *fiber.Ctx) bool + + // Users defines the allowed credentials + // + // Required. Default: map[string]string{} + Users map[string]string + + // Realm is a string to define realm attribute of BasicAuth. + // the realm identifies the system to authenticate against + // and can be used by clients to save credentials + // + // Optional. Default: "Restricted". + Realm string + + // Authorizer defines a function you can pass + // to check the credentials however you want. + // It will be called with a username and password + // and is expected to return true or false to indicate + // that the credentials were approved or not. + // + // Optional. Default: nil. + Authorizer func(string, string) bool + + // Unauthorized defines the response body for unauthorized responses. + // By default it will return with a 401 Unauthorized and the correct WWW-Auth header + // + // Optional. Default: nil + Unauthorized fiber.Handler + + // ContextUser is the key to store the username in Locals + // + // Optional. Default: "username" + ContextUsername interface{} + + // ContextPass is the key to store the password in Locals + // + // Optional. Default: "password" + ContextPassword interface{} +} + +// ConfigDefault is the default config +var ConfigDefault = Config{ + Next: nil, + Users: map[string]string{}, + Realm: "Restricted", + Authorizer: nil, + Unauthorized: nil, + ContextUsername: "username", + ContextPassword: "password", +} + +// Helper function to set default values +func configDefault(config ...Config) Config { + // Return default config if nothing provided + if len(config) < 1 { + return ConfigDefault + } + + // Override default config + cfg := config[0] + + // Set default values + if cfg.Next == nil { + cfg.Next = ConfigDefault.Next + } + if cfg.Users == nil { + cfg.Users = ConfigDefault.Users + } + if cfg.Realm == "" { + cfg.Realm = ConfigDefault.Realm + } + if cfg.Authorizer == nil { + cfg.Authorizer = func(user, pass string) bool { + userPwd, exist := cfg.Users[user] + return exist && subtle.ConstantTimeCompare(utils.UnsafeBytes(userPwd), utils.UnsafeBytes(pass)) == 1 + } + } + if cfg.Unauthorized == nil { + cfg.Unauthorized = func(c *fiber.Ctx) error { + c.Set(fiber.HeaderWWWAuthenticate, "basic realm="+cfg.Realm) + return c.SendStatus(fiber.StatusUnauthorized) + } + } + if cfg.ContextUsername == nil { + cfg.ContextUsername = ConfigDefault.ContextUsername + } + if cfg.ContextPassword == nil { + cfg.ContextPassword = ConfigDefault.ContextPassword + } + return cfg +} diff --git a/middleware/cache/cache.go b/middleware/cache/cache.go new file mode 100644 index 0000000..f7149d1 --- /dev/null +++ b/middleware/cache/cache.go @@ -0,0 +1,252 @@ +// Special thanks to @codemicro for moving this to fiber core +// Original middleware: github.com/codemicro/fiber-cache +package cache + +import ( + "strconv" + "strings" + "sync" + "sync/atomic" + "time" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/utils" +) + +// timestampUpdatePeriod is the period which is used to check the cache expiration. +// It should not be too long to provide more or less acceptable expiration error, and in the same +// time it should not be too short to avoid overwhelming of the system +const timestampUpdatePeriod = 300 * time.Millisecond + +// cache status +// unreachable: when cache is bypass, or invalid +// hit: cache is served +// miss: do not have cache record +const ( + cacheUnreachable = "unreachable" + cacheHit = "hit" + cacheMiss = "miss" +) + +// directives +const ( + noCache = "no-cache" + noStore = "no-store" +) + +var ignoreHeaders = map[string]interface{}{ + "Connection": nil, + "Keep-Alive": nil, + "Proxy-Authenticate": nil, + "Proxy-Authorization": nil, + "TE": nil, + "Trailers": nil, + "Transfer-Encoding": nil, + "Upgrade": nil, + "Content-Type": nil, // already stored explicitly by the cache manager + "Content-Encoding": nil, // already stored explicitly by the cache manager +} + +// New creates a new middleware handler +func New(config ...Config) fiber.Handler { + // Set default config + cfg := configDefault(config...) + + // Nothing to cache + if int(cfg.Expiration.Seconds()) < 0 { + return func(c *fiber.Ctx) error { + return c.Next() + } + } + + var ( + // Cache settings + mux = &sync.RWMutex{} + timestamp = uint64(time.Now().Unix()) + ) + // Create manager to simplify storage operations ( see manager.go ) + manager := newManager(cfg.Storage) + // Create indexed heap for tracking expirations ( see heap.go ) + heap := &indexedHeap{} + // count stored bytes (sizes of response bodies) + var storedBytes uint + + // Update timestamp in the configured interval + go func() { + for { + atomic.StoreUint64(×tamp, uint64(time.Now().Unix())) + time.Sleep(timestampUpdatePeriod) + } + }() + + // Delete key from both manager and storage + deleteKey := func(dkey string) { + manager.del(dkey) + // External storage saves body data with different key + if cfg.Storage != nil { + manager.del(dkey + "_body") + } + } + + // Return new handler + return func(c *fiber.Ctx) error { + // Refrain from caching + if hasRequestDirective(c, noStore) { + return c.Next() + } + + // Only cache selected methods + var isExists bool + for _, method := range cfg.Methods { + if c.Method() == method { + isExists = true + } + } + + if !isExists { + c.Set(cfg.CacheHeader, cacheUnreachable) + return c.Next() + } + + // Get key from request + // TODO(allocation optimization): try to minimize the allocation from 2 to 1 + key := cfg.KeyGenerator(c) + "_" + c.Method() + + // Get entry from pool + e := manager.get(key) + + // Lock entry + mux.Lock() + + // Get timestamp + ts := atomic.LoadUint64(×tamp) + + // Check if entry is expired + if e.exp != 0 && ts >= e.exp { + deleteKey(key) + if cfg.MaxBytes > 0 { + _, size := heap.remove(e.heapidx) + storedBytes -= size + } + } else if e.exp != 0 && !hasRequestDirective(c, noCache) { + // Separate body value to avoid msgp serialization + // We can store raw bytes with Storage 👍 + if cfg.Storage != nil { + e.body = manager.getRaw(key + "_body") + } + // Set response headers from cache + c.Response().SetBodyRaw(e.body) + c.Response().SetStatusCode(e.status) + c.Response().Header.SetContentTypeBytes(e.ctype) + if len(e.cencoding) > 0 { + c.Response().Header.SetBytesV(fiber.HeaderContentEncoding, e.cencoding) + } + for k, v := range e.headers { + c.Response().Header.SetBytesV(k, v) + } + // Set Cache-Control header if enabled + if cfg.CacheControl { + maxAge := strconv.FormatUint(e.exp-ts, 10) + c.Set(fiber.HeaderCacheControl, "public, max-age="+maxAge) + } + + c.Set(cfg.CacheHeader, cacheHit) + + mux.Unlock() + + // Return response + return nil + } + + // make sure we're not blocking concurrent requests - do unlock + mux.Unlock() + + // Continue stack, return err to Fiber if exist + if err := c.Next(); err != nil { + return err + } + + // lock entry back and unlock on finish + mux.Lock() + defer mux.Unlock() + + // Don't cache response if Next returns true + if cfg.Next != nil && cfg.Next(c) { + c.Set(cfg.CacheHeader, cacheUnreachable) + return nil + } + + // Don't try to cache if body won't fit into cache + bodySize := uint(len(c.Response().Body())) + if cfg.MaxBytes > 0 && bodySize > cfg.MaxBytes { + c.Set(cfg.CacheHeader, cacheUnreachable) + return nil + } + + // Remove oldest to make room for new + if cfg.MaxBytes > 0 { + for storedBytes+bodySize > cfg.MaxBytes { + key, size := heap.removeFirst() + deleteKey(key) + storedBytes -= size + } + } + + // Cache response + e.body = utils.CopyBytes(c.Response().Body()) + e.status = c.Response().StatusCode() + e.ctype = utils.CopyBytes(c.Response().Header.ContentType()) + e.cencoding = utils.CopyBytes(c.Response().Header.Peek(fiber.HeaderContentEncoding)) + + // Store all response headers + // (more: https://datatracker.ietf.org/doc/html/rfc2616#section-13.5.1) + if cfg.StoreResponseHeaders { + e.headers = make(map[string][]byte) + c.Response().Header.VisitAll( + func(key, value []byte) { + // create real copy + keyS := string(key) + if _, ok := ignoreHeaders[keyS]; !ok { + e.headers[keyS] = utils.CopyBytes(value) + } + }, + ) + } + + // default cache expiration + expiration := cfg.Expiration + // Calculate expiration by response header or other setting + if cfg.ExpirationGenerator != nil { + expiration = cfg.ExpirationGenerator(c, &cfg) + } + e.exp = ts + uint64(expiration.Seconds()) + + // Store entry in heap + if cfg.MaxBytes > 0 { + e.heapidx = heap.put(key, e.exp, bodySize) + storedBytes += bodySize + } + + // For external Storage we store raw body separated + if cfg.Storage != nil { + manager.setRaw(key+"_body", e.body, expiration) + // avoid body msgp encoding + e.body = nil + manager.set(key, e, expiration) + manager.release(e) + } else { + // Store entry in memory + manager.set(key, e, expiration) + } + + c.Set(cfg.CacheHeader, cacheMiss) + + // Finish response + return nil + } +} + +// Check if request has directive +func hasRequestDirective(c *fiber.Ctx, directive string) bool { + return strings.Contains(c.Get(fiber.HeaderCacheControl), directive) +} diff --git a/middleware/cache/cache_test.go b/middleware/cache/cache_test.go new file mode 100644 index 0000000..404a1ed --- /dev/null +++ b/middleware/cache/cache_test.go @@ -0,0 +1,901 @@ +// Special thanks to @codemicro for moving this to fiber core +// Original middleware: github.com/codemicro/fiber-cache +package cache + +import ( + "bytes" + "fmt" + "io" + "math" + "net/http/httptest" + "os" + "strconv" + "testing" + "time" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/internal/storage/memory" + "github.com/gofiber/fiber/v2/middleware/etag" + "github.com/gofiber/fiber/v2/utils" + + "github.com/valyala/fasthttp" +) + +func Test_Cache_CacheControl(t *testing.T) { + t.Parallel() + + app := fiber.New() + + app.Use(New(Config{ + CacheControl: true, + Expiration: 10 * time.Second, + })) + + app.Get("/", func(c *fiber.Ctx) error { + return c.SendString("Hello, World!") + }) + + _, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) + utils.AssertEqual(t, nil, err) + + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, "public, max-age=10", resp.Header.Get(fiber.HeaderCacheControl)) +} + +func Test_Cache_Expired(t *testing.T) { + t.Parallel() + + app := fiber.New() + app.Use(New(Config{Expiration: 2 * time.Second})) + + app.Get("/", func(c *fiber.Ctx) error { + return c.SendString(fmt.Sprintf("%d", time.Now().UnixNano())) + }) + + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) + utils.AssertEqual(t, nil, err) + body, err := io.ReadAll(resp.Body) + utils.AssertEqual(t, nil, err) + + // Sleep until the cache is expired + time.Sleep(3 * time.Second) + + respCached, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) + utils.AssertEqual(t, nil, err) + bodyCached, err := io.ReadAll(respCached.Body) + utils.AssertEqual(t, nil, err) + + if bytes.Equal(body, bodyCached) { + t.Errorf("Cache should have expired: %s, %s", body, bodyCached) + } + + // Next response should be also cached + respCachedNextRound, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) + utils.AssertEqual(t, nil, err) + bodyCachedNextRound, err := io.ReadAll(respCachedNextRound.Body) + utils.AssertEqual(t, nil, err) + + if !bytes.Equal(bodyCachedNextRound, bodyCached) { + t.Errorf("Cache should not have expired: %s, %s", bodyCached, bodyCachedNextRound) + } +} + +func Test_Cache(t *testing.T) { + t.Parallel() + + app := fiber.New() + app.Use(New()) + + app.Get("/", func(c *fiber.Ctx) error { + now := fmt.Sprintf("%d", time.Now().UnixNano()) + return c.SendString(now) + }) + + req := httptest.NewRequest(fiber.MethodGet, "/", nil) + resp, err := app.Test(req) + utils.AssertEqual(t, nil, err) + + cachedReq := httptest.NewRequest(fiber.MethodGet, "/", nil) + cachedResp, err := app.Test(cachedReq) + utils.AssertEqual(t, nil, err) + + body, err := io.ReadAll(resp.Body) + utils.AssertEqual(t, nil, err) + cachedBody, err := io.ReadAll(cachedResp.Body) + utils.AssertEqual(t, nil, err) + + utils.AssertEqual(t, cachedBody, body) +} + +// go test -run Test_Cache_WithNoCacheRequestDirective +func Test_Cache_WithNoCacheRequestDirective(t *testing.T) { + t.Parallel() + + app := fiber.New() + app.Use(New()) + + app.Get("/", func(c *fiber.Ctx) error { + return c.SendString(c.Query("id", "1")) + }) + + // Request id = 1 + req := httptest.NewRequest(fiber.MethodGet, "/", nil) + resp, err := app.Test(req) + utils.AssertEqual(t, nil, err) + body, err := io.ReadAll(resp.Body) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, cacheMiss, resp.Header.Get("X-Cache")) + utils.AssertEqual(t, []byte("1"), body) + // Response cached, entry id = 1 + + // Request id = 2 without Cache-Control: no-cache + cachedReq := httptest.NewRequest(fiber.MethodGet, "/?id=2", nil) + cachedResp, err := app.Test(cachedReq) + utils.AssertEqual(t, nil, err) + cachedBody, err := io.ReadAll(cachedResp.Body) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, cacheHit, cachedResp.Header.Get("X-Cache")) + utils.AssertEqual(t, []byte("1"), cachedBody) + // Response not cached, returns cached response, entry id = 1 + + // Request id = 2 with Cache-Control: no-cache + noCacheReq := httptest.NewRequest(fiber.MethodGet, "/?id=2", nil) + noCacheReq.Header.Set(fiber.HeaderCacheControl, noCache) + noCacheResp, err := app.Test(noCacheReq) + utils.AssertEqual(t, nil, err) + noCacheBody, err := io.ReadAll(noCacheResp.Body) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, cacheMiss, noCacheResp.Header.Get("X-Cache")) + utils.AssertEqual(t, []byte("2"), noCacheBody) + // Response cached, returns updated response, entry = 2 + + /* Check Test_Cache_WithETagAndNoCacheRequestDirective */ + // Request id = 2 with Cache-Control: no-cache again + noCacheReq1 := httptest.NewRequest(fiber.MethodGet, "/?id=2", nil) + noCacheReq1.Header.Set(fiber.HeaderCacheControl, noCache) + noCacheResp1, err := app.Test(noCacheReq1) + utils.AssertEqual(t, nil, err) + noCacheBody1, err := io.ReadAll(noCacheResp1.Body) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, cacheMiss, noCacheResp1.Header.Get("X-Cache")) + utils.AssertEqual(t, []byte("2"), noCacheBody1) + // Response cached, returns updated response, entry = 2 + + // Request id = 1 without Cache-Control: no-cache + cachedReq1 := httptest.NewRequest(fiber.MethodGet, "/", nil) + cachedResp1, err := app.Test(cachedReq1) + utils.AssertEqual(t, nil, err) + cachedBody1, err := io.ReadAll(cachedResp1.Body) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, cacheHit, cachedResp1.Header.Get("X-Cache")) + utils.AssertEqual(t, []byte("2"), cachedBody1) + // Response not cached, returns cached response, entry id = 2 +} + +// go test -run Test_Cache_WithETagAndNoCacheRequestDirective +func Test_Cache_WithETagAndNoCacheRequestDirective(t *testing.T) { + t.Parallel() + + app := fiber.New() + app.Use( + etag.New(), + New(), + ) + + app.Get("/", func(c *fiber.Ctx) error { + return c.SendString(c.Query("id", "1")) + }) + + // Request id = 1 + req := httptest.NewRequest(fiber.MethodGet, "/", nil) + resp, err := app.Test(req) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, cacheMiss, resp.Header.Get("X-Cache")) + utils.AssertEqual(t, fiber.StatusOK, resp.StatusCode) + // Response cached, entry id = 1 + + // If response status 200 + etagToken := resp.Header.Get("Etag") + + // Request id = 2 with ETag but without Cache-Control: no-cache + cachedReq := httptest.NewRequest(fiber.MethodGet, "/?id=2", nil) + cachedReq.Header.Set(fiber.HeaderIfNoneMatch, etagToken) + cachedResp, err := app.Test(cachedReq) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, cacheHit, cachedResp.Header.Get("X-Cache")) + utils.AssertEqual(t, fiber.StatusNotModified, cachedResp.StatusCode) + // Response not cached, returns cached response, entry id = 1, status not modified + + // Request id = 2 with ETag and Cache-Control: no-cache + noCacheReq := httptest.NewRequest(fiber.MethodGet, "/?id=2", nil) + noCacheReq.Header.Set(fiber.HeaderCacheControl, noCache) + noCacheReq.Header.Set(fiber.HeaderIfNoneMatch, etagToken) + noCacheResp, err := app.Test(noCacheReq) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, cacheMiss, noCacheResp.Header.Get("X-Cache")) + utils.AssertEqual(t, fiber.StatusOK, noCacheResp.StatusCode) + // Response cached, returns updated response, entry id = 2 + + // If response status 200 + etagToken = noCacheResp.Header.Get("Etag") + + // Request id = 2 with ETag and Cache-Control: no-cache again + noCacheReq1 := httptest.NewRequest(fiber.MethodGet, "/?id=2", nil) + noCacheReq1.Header.Set(fiber.HeaderCacheControl, noCache) + noCacheReq1.Header.Set(fiber.HeaderIfNoneMatch, etagToken) + noCacheResp1, err := app.Test(noCacheReq1) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, cacheMiss, noCacheResp1.Header.Get("X-Cache")) + utils.AssertEqual(t, fiber.StatusNotModified, noCacheResp1.StatusCode) + // Response cached, returns updated response, entry id = 2, status not modified + + // Request id = 1 without ETag and Cache-Control: no-cache + cachedReq1 := httptest.NewRequest(fiber.MethodGet, "/", nil) + cachedResp1, err := app.Test(cachedReq1) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, cacheHit, cachedResp1.Header.Get("X-Cache")) + utils.AssertEqual(t, fiber.StatusOK, cachedResp1.StatusCode) + // Response not cached, returns cached response, entry id = 2 +} + +// go test -run Test_Cache_WithNoStoreRequestDirective +func Test_Cache_WithNoStoreRequestDirective(t *testing.T) { + t.Parallel() + + app := fiber.New() + app.Use(New()) + + app.Get("/", func(c *fiber.Ctx) error { + return c.SendString(c.Query("id", "1")) + }) + + // Request id = 2 + noStoreReq := httptest.NewRequest(fiber.MethodGet, "/?id=2", nil) + noStoreReq.Header.Set(fiber.HeaderCacheControl, noStore) + noStoreResp, err := app.Test(noStoreReq) + utils.AssertEqual(t, nil, err) + noStoreBody, err := io.ReadAll(noStoreResp.Body) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, []byte("2"), noStoreBody) + // Response not cached, returns updated response +} + +func Test_Cache_WithSeveralRequests(t *testing.T) { + t.Parallel() + + app := fiber.New() + + app.Use(New(Config{ + CacheControl: true, + Expiration: 10 * time.Second, + })) + + app.Get("/:id", func(c *fiber.Ctx) error { + return c.SendString(c.Params("id")) + }) + + for runs := 0; runs < 10; runs++ { + for i := 0; i < 10; i++ { + func(id int) { + rsp, err := app.Test(httptest.NewRequest(fiber.MethodGet, fmt.Sprintf("/%d", id), nil)) + utils.AssertEqual(t, nil, err) + + defer func(body io.ReadCloser) { + err := body.Close() + utils.AssertEqual(t, nil, err) + }(rsp.Body) + + idFromServ, err := io.ReadAll(rsp.Body) + utils.AssertEqual(t, nil, err) + + a, err := strconv.Atoi(string(idFromServ)) + utils.AssertEqual(t, nil, err) + + // SomeTimes,The id is not equal with a + utils.AssertEqual(t, id, a) + }(i) + } + } +} + +func Test_Cache_Invalid_Expiration(t *testing.T) { + t.Parallel() + + app := fiber.New() + cache := New(Config{Expiration: 0 * time.Second}) + app.Use(cache) + + app.Get("/", func(c *fiber.Ctx) error { + now := fmt.Sprintf("%d", time.Now().UnixNano()) + return c.SendString(now) + }) + + req := httptest.NewRequest(fiber.MethodGet, "/", nil) + resp, err := app.Test(req) + utils.AssertEqual(t, nil, err) + + cachedReq := httptest.NewRequest(fiber.MethodGet, "/", nil) + cachedResp, err := app.Test(cachedReq) + utils.AssertEqual(t, nil, err) + + body, err := io.ReadAll(resp.Body) + utils.AssertEqual(t, nil, err) + cachedBody, err := io.ReadAll(cachedResp.Body) + utils.AssertEqual(t, nil, err) + + utils.AssertEqual(t, cachedBody, body) +} + +func Test_Cache_Get(t *testing.T) { + t.Parallel() + + app := fiber.New() + + app.Use(New()) + + app.Post("/", func(c *fiber.Ctx) error { + return c.SendString(c.Query("cache")) + }) + + app.Get("/get", func(c *fiber.Ctx) error { + return c.SendString(c.Query("cache")) + }) + + resp, err := app.Test(httptest.NewRequest(fiber.MethodPost, "/?cache=123", nil)) + utils.AssertEqual(t, nil, err) + body, err := io.ReadAll(resp.Body) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, "123", string(body)) + + resp, err = app.Test(httptest.NewRequest(fiber.MethodPost, "/?cache=12345", nil)) + utils.AssertEqual(t, nil, err) + body, err = io.ReadAll(resp.Body) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, "12345", string(body)) + + resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/get?cache=123", nil)) + utils.AssertEqual(t, nil, err) + body, err = io.ReadAll(resp.Body) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, "123", string(body)) + + resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/get?cache=12345", nil)) + utils.AssertEqual(t, nil, err) + body, err = io.ReadAll(resp.Body) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, "123", string(body)) +} + +func Test_Cache_Post(t *testing.T) { + t.Parallel() + + app := fiber.New() + + app.Use(New(Config{ + Methods: []string{fiber.MethodPost}, + })) + + app.Post("/", func(c *fiber.Ctx) error { + return c.SendString(c.Query("cache")) + }) + + app.Get("/get", func(c *fiber.Ctx) error { + return c.SendString(c.Query("cache")) + }) + + resp, err := app.Test(httptest.NewRequest(fiber.MethodPost, "/?cache=123", nil)) + utils.AssertEqual(t, nil, err) + body, err := io.ReadAll(resp.Body) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, "123", string(body)) + + resp, err = app.Test(httptest.NewRequest(fiber.MethodPost, "/?cache=12345", nil)) + utils.AssertEqual(t, nil, err) + body, err = io.ReadAll(resp.Body) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, "123", string(body)) + + resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/get?cache=123", nil)) + utils.AssertEqual(t, nil, err) + body, err = io.ReadAll(resp.Body) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, "123", string(body)) + + resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/get?cache=12345", nil)) + utils.AssertEqual(t, nil, err) + body, err = io.ReadAll(resp.Body) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, "12345", string(body)) +} + +func Test_Cache_NothingToCache(t *testing.T) { + t.Parallel() + + app := fiber.New() + + app.Use(New(Config{Expiration: -(time.Second * 1)})) + + app.Get("/", func(c *fiber.Ctx) error { + return c.SendString(time.Now().String()) + }) + + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) + utils.AssertEqual(t, nil, err) + body, err := io.ReadAll(resp.Body) + utils.AssertEqual(t, nil, err) + + time.Sleep(500 * time.Millisecond) + + respCached, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) + utils.AssertEqual(t, nil, err) + bodyCached, err := io.ReadAll(respCached.Body) + utils.AssertEqual(t, nil, err) + + if bytes.Equal(body, bodyCached) { + t.Errorf("Cache should have expired: %s, %s", body, bodyCached) + } +} + +func Test_Cache_CustomNext(t *testing.T) { + t.Parallel() + + app := fiber.New() + + app.Use(New(Config{ + Next: func(c *fiber.Ctx) bool { + return c.Response().StatusCode() != fiber.StatusOK + }, + CacheControl: true, + })) + + app.Get("/", func(c *fiber.Ctx) error { + return c.SendString(time.Now().String()) + }) + + app.Get("/error", func(c *fiber.Ctx) error { + return c.Status(fiber.StatusInternalServerError).SendString(time.Now().String()) + }) + + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) + utils.AssertEqual(t, nil, err) + body, err := io.ReadAll(resp.Body) + utils.AssertEqual(t, nil, err) + + respCached, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) + utils.AssertEqual(t, nil, err) + bodyCached, err := io.ReadAll(respCached.Body) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, true, bytes.Equal(body, bodyCached)) + utils.AssertEqual(t, true, respCached.Header.Get(fiber.HeaderCacheControl) != "") + + _, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/error", nil)) + utils.AssertEqual(t, nil, err) + + errRespCached, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/error", nil)) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, true, errRespCached.Header.Get(fiber.HeaderCacheControl) == "") +} + +func Test_CustomKey(t *testing.T) { + t.Parallel() + + app := fiber.New() + var called bool + app.Use(New(Config{KeyGenerator: func(c *fiber.Ctx) string { + called = true + return utils.CopyString(c.Path()) + }})) + + app.Get("/", func(c *fiber.Ctx) error { + return c.SendString("hi") + }) + + req := httptest.NewRequest(fiber.MethodGet, "/", nil) + _, err := app.Test(req) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, true, called) +} + +func Test_CustomExpiration(t *testing.T) { + t.Parallel() + + app := fiber.New() + var called bool + var newCacheTime int + app.Use(New(Config{ExpirationGenerator: func(c *fiber.Ctx, cfg *Config) time.Duration { + called = true + var err error + newCacheTime, err = strconv.Atoi(c.GetRespHeader("Cache-Time", "600")) + utils.AssertEqual(t, nil, err) + return time.Second * time.Duration(newCacheTime) + }})) + + app.Get("/", func(c *fiber.Ctx) error { + c.Response().Header.Add("Cache-Time", "1") + now := fmt.Sprintf("%d", time.Now().UnixNano()) + return c.SendString(now) + }) + + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, true, called) + utils.AssertEqual(t, 1, newCacheTime) + + // Sleep until the cache is expired + time.Sleep(1 * time.Second) + + cachedResp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) + utils.AssertEqual(t, nil, err) + + body, err := io.ReadAll(resp.Body) + utils.AssertEqual(t, nil, err) + cachedBody, err := io.ReadAll(cachedResp.Body) + utils.AssertEqual(t, nil, err) + + if bytes.Equal(body, cachedBody) { + t.Errorf("Cache should have expired: %s, %s", body, cachedBody) + } + + // Next response should be cached + cachedRespNextRound, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) + utils.AssertEqual(t, nil, err) + cachedBodyNextRound, err := io.ReadAll(cachedRespNextRound.Body) + utils.AssertEqual(t, nil, err) + + if !bytes.Equal(cachedBodyNextRound, cachedBody) { + t.Errorf("Cache should not have expired: %s, %s", cachedBodyNextRound, cachedBody) + } +} + +func Test_AdditionalE2EResponseHeaders(t *testing.T) { + t.Parallel() + + app := fiber.New() + app.Use(New(Config{ + StoreResponseHeaders: true, + })) + + app.Get("/", func(c *fiber.Ctx) error { + c.Response().Header.Add("X-Foobar", "foobar") + return c.SendString("hi") + }) + + req := httptest.NewRequest(fiber.MethodGet, "/", nil) + resp, err := app.Test(req) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, "foobar", resp.Header.Get("X-Foobar")) + + req = httptest.NewRequest(fiber.MethodGet, "/", nil) + resp, err = app.Test(req) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, "foobar", resp.Header.Get("X-Foobar")) +} + +func Test_CacheHeader(t *testing.T) { + t.Parallel() + + app := fiber.New() + + app.Use(New(Config{ + Expiration: 10 * time.Second, + Next: func(c *fiber.Ctx) bool { + return c.Response().StatusCode() != fiber.StatusOK + }, + })) + + app.Get("/", func(c *fiber.Ctx) error { + return c.SendString("Hello, World!") + }) + + app.Post("/", func(c *fiber.Ctx) error { + return c.SendString(c.Query("cache")) + }) + + app.Get("/error", func(c *fiber.Ctx) error { + return c.Status(fiber.StatusInternalServerError).SendString(time.Now().String()) + }) + + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, cacheMiss, resp.Header.Get("X-Cache")) + + resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, cacheHit, resp.Header.Get("X-Cache")) + + resp, err = app.Test(httptest.NewRequest(fiber.MethodPost, "/?cache=12345", nil)) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, cacheUnreachable, resp.Header.Get("X-Cache")) + + errRespCached, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/error", nil)) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, cacheUnreachable, errRespCached.Header.Get("X-Cache")) +} + +func Test_Cache_WithHead(t *testing.T) { + t.Parallel() + + app := fiber.New() + app.Use(New()) + + app.Get("/", func(c *fiber.Ctx) error { + now := fmt.Sprintf("%d", time.Now().UnixNano()) + return c.SendString(now) + }) + + req := httptest.NewRequest(fiber.MethodHead, "/", nil) + resp, err := app.Test(req) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, cacheMiss, resp.Header.Get("X-Cache")) + + cachedReq := httptest.NewRequest(fiber.MethodHead, "/", nil) + cachedResp, err := app.Test(cachedReq) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, cacheHit, cachedResp.Header.Get("X-Cache")) + + body, err := io.ReadAll(resp.Body) + utils.AssertEqual(t, nil, err) + cachedBody, err := io.ReadAll(cachedResp.Body) + utils.AssertEqual(t, nil, err) + + utils.AssertEqual(t, cachedBody, body) +} + +func Test_Cache_WithHeadThenGet(t *testing.T) { + t.Parallel() + + app := fiber.New() + app.Use(New()) + app.Get("/", func(c *fiber.Ctx) error { + return c.SendString(c.Query("cache")) + }) + + headResp, err := app.Test(httptest.NewRequest(fiber.MethodHead, "/?cache=123", nil)) + utils.AssertEqual(t, nil, err) + headBody, err := io.ReadAll(headResp.Body) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, "", string(headBody)) + utils.AssertEqual(t, cacheMiss, headResp.Header.Get("X-Cache")) + + headResp, err = app.Test(httptest.NewRequest(fiber.MethodHead, "/?cache=123", nil)) + utils.AssertEqual(t, nil, err) + headBody, err = io.ReadAll(headResp.Body) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, "", string(headBody)) + utils.AssertEqual(t, cacheHit, headResp.Header.Get("X-Cache")) + + getResp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/?cache=123", nil)) + utils.AssertEqual(t, nil, err) + getBody, err := io.ReadAll(getResp.Body) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, "123", string(getBody)) + utils.AssertEqual(t, cacheMiss, getResp.Header.Get("X-Cache")) + + getResp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/?cache=123", nil)) + utils.AssertEqual(t, nil, err) + getBody, err = io.ReadAll(getResp.Body) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, "123", string(getBody)) + utils.AssertEqual(t, cacheHit, getResp.Header.Get("X-Cache")) +} + +func Test_CustomCacheHeader(t *testing.T) { + t.Parallel() + + app := fiber.New() + + app.Use(New(Config{ + CacheHeader: "Cache-Status", + })) + + app.Get("/", func(c *fiber.Ctx) error { + return c.SendString("Hello, World!") + }) + + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, cacheMiss, resp.Header.Get("Cache-Status")) +} + +// Because time points are updated once every X milliseconds, entries in tests can often have +// equal expiration times and thus be in an random order. This closure hands out increasing +// time intervals to maintain strong ascending order of expiration +func stableAscendingExpiration() func(c1 *fiber.Ctx, c2 *Config) time.Duration { + i := 0 + return func(c1 *fiber.Ctx, c2 *Config) time.Duration { + i++ + return time.Hour * time.Duration(i) + } +} + +func Test_Cache_MaxBytesOrder(t *testing.T) { + t.Parallel() + + app := fiber.New() + app.Use(New(Config{ + MaxBytes: 2, + ExpirationGenerator: stableAscendingExpiration(), + })) + + app.Get("/*", func(c *fiber.Ctx) error { + return c.SendString("1") + }) + + cases := [][]string{ + // Insert a, b into cache of size 2 bytes (responses are 1 byte) + {"/a", cacheMiss}, + {"/b", cacheMiss}, + {"/a", cacheHit}, + {"/b", cacheHit}, + // Add c -> a evicted + {"/c", cacheMiss}, + {"/b", cacheHit}, + // Add a again -> b evicted + {"/a", cacheMiss}, + {"/c", cacheHit}, + // Add b -> c evicted + {"/b", cacheMiss}, + {"/c", cacheMiss}, + } + + for idx, tcase := range cases { + rsp, err := app.Test(httptest.NewRequest(fiber.MethodGet, tcase[0], nil)) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, tcase[1], rsp.Header.Get("X-Cache"), fmt.Sprintf("Case %v", idx)) + } +} + +func Test_Cache_MaxBytesSizes(t *testing.T) { + t.Parallel() + + app := fiber.New() + + app.Use(New(Config{ + MaxBytes: 7, + ExpirationGenerator: stableAscendingExpiration(), + })) + + app.Get("/*", func(c *fiber.Ctx) error { + path := c.Context().URI().LastPathSegment() + size, err := strconv.Atoi(string(path)) + utils.AssertEqual(t, nil, err) + return c.Send(make([]byte, size)) + }) + + cases := [][]string{ + {"/1", cacheMiss}, + {"/2", cacheMiss}, + {"/3", cacheMiss}, + {"/4", cacheMiss}, // 1+2+3+4 > 7 => 1,2 are evicted now + {"/3", cacheHit}, + {"/1", cacheMiss}, + {"/2", cacheMiss}, + {"/8", cacheUnreachable}, // too big to cache -> unreachable + } + + for idx, tcase := range cases { + rsp, err := app.Test(httptest.NewRequest(fiber.MethodGet, tcase[0], nil)) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, tcase[1], rsp.Header.Get("X-Cache"), fmt.Sprintf("Case %v", idx)) + } +} + +// go test -v -run=^$ -bench=Benchmark_Cache -benchmem -count=4 +func Benchmark_Cache(b *testing.B) { + app := fiber.New() + + app.Use(New()) + + app.Get("/demo", func(c *fiber.Ctx) error { + data, _ := os.ReadFile("../../.github/README.md") //nolint:errcheck // We're inside a benchmark + return c.Status(fiber.StatusTeapot).Send(data) + }) + + h := app.Handler() + + fctx := &fasthttp.RequestCtx{} + fctx.Request.Header.SetMethod(fiber.MethodGet) + fctx.Request.SetRequestURI("/demo") + + b.ReportAllocs() + b.ResetTimer() + + for n := 0; n < b.N; n++ { + h(fctx) + } + + utils.AssertEqual(b, fiber.StatusTeapot, fctx.Response.Header.StatusCode()) + utils.AssertEqual(b, true, len(fctx.Response.Body()) > 30000) +} + +// go test -v -run=^$ -bench=Benchmark_Cache_Storage -benchmem -count=4 +func Benchmark_Cache_Storage(b *testing.B) { + app := fiber.New() + + app.Use(New(Config{ + Storage: memory.New(), + })) + + app.Get("/demo", func(c *fiber.Ctx) error { + data, _ := os.ReadFile("../../.github/README.md") //nolint:errcheck // We're inside a benchmark + return c.Status(fiber.StatusTeapot).Send(data) + }) + + h := app.Handler() + + fctx := &fasthttp.RequestCtx{} + fctx.Request.Header.SetMethod(fiber.MethodGet) + fctx.Request.SetRequestURI("/demo") + + b.ReportAllocs() + b.ResetTimer() + + for n := 0; n < b.N; n++ { + h(fctx) + } + + utils.AssertEqual(b, fiber.StatusTeapot, fctx.Response.Header.StatusCode()) + utils.AssertEqual(b, true, len(fctx.Response.Body()) > 30000) +} + +func Benchmark_Cache_AdditionalHeaders(b *testing.B) { + app := fiber.New() + app.Use(New(Config{ + StoreResponseHeaders: true, + })) + + app.Get("/demo", func(c *fiber.Ctx) error { + c.Response().Header.Add("X-Foobar", "foobar") + return c.SendStatus(418) + }) + + h := app.Handler() + + fctx := &fasthttp.RequestCtx{} + fctx.Request.Header.SetMethod(fiber.MethodGet) + fctx.Request.SetRequestURI("/demo") + + b.ReportAllocs() + b.ResetTimer() + + for n := 0; n < b.N; n++ { + h(fctx) + } + + utils.AssertEqual(b, fiber.StatusTeapot, fctx.Response.Header.StatusCode()) + utils.AssertEqual(b, []byte("foobar"), fctx.Response.Header.Peek("X-Foobar")) +} + +func Benchmark_Cache_MaxSize(b *testing.B) { + // The benchmark is run with three different MaxSize parameters + // 1) 0: Tracking is disabled = no overhead + // 2) MaxInt32: Enough to store all entries = no removals + // 3) 100: Small size = constant insertions and removals + cases := []uint{0, math.MaxUint32, 100} + names := []string{"Disabled", "Unlim", "LowBounded"} + for i, size := range cases { + b.Run(names[i], func(b *testing.B) { + app := fiber.New() + app.Use(New(Config{MaxBytes: size})) + + app.Get("/*", func(c *fiber.Ctx) error { + return c.Status(fiber.StatusTeapot).SendString("1") + }) + + h := app.Handler() + fctx := &fasthttp.RequestCtx{} + fctx.Request.Header.SetMethod(fiber.MethodGet) + + b.ReportAllocs() + b.ResetTimer() + + for n := 0; n < b.N; n++ { + fctx.Request.SetRequestURI(fmt.Sprintf("/%v", n)) + h(fctx) + } + + utils.AssertEqual(b, fiber.StatusTeapot, fctx.Response.Header.StatusCode()) + }) + } +} diff --git a/middleware/cache/config.go b/middleware/cache/config.go new file mode 100644 index 0000000..6fe6162 --- /dev/null +++ b/middleware/cache/config.go @@ -0,0 +1,128 @@ +package cache + +import ( + "time" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/log" + "github.com/gofiber/fiber/v2/utils" +) + +// Config defines the config for middleware. +type Config struct { + // Next defines a function to skip this middleware when returned true. + // + // Optional. Default: nil + Next func(c *fiber.Ctx) bool + + // Expiration is the time that an cached response will live + // + // Optional. Default: 1 * time.Minute + Expiration time.Duration + + // CacheHeader header on response header, indicate cache status, with the following possible return value + // + // hit, miss, unreachable + // + // Optional. Default: X-Cache + CacheHeader string + + // CacheControl enables client side caching if set to true + // + // Optional. Default: false + CacheControl bool + + // Key allows you to generate custom keys, by default c.Path() is used + // + // Default: func(c *fiber.Ctx) string { + // return utils.CopyString(c.Path()) + // } + KeyGenerator func(*fiber.Ctx) string + + // allows you to generate custom Expiration Key By Key, default is Expiration (Optional) + // + // Default: nil + ExpirationGenerator func(*fiber.Ctx, *Config) time.Duration + + // Store is used to store the state of the middleware + // + // Default: an in memory store for this process only + Storage fiber.Storage + + // Deprecated: Use Storage instead + Store fiber.Storage + + // Deprecated: Use KeyGenerator instead + Key func(*fiber.Ctx) string + + // allows you to store additional headers generated by next middlewares & handler + // + // Default: false + StoreResponseHeaders bool + + // Max number of bytes of response bodies simultaneously stored in cache. When limit is reached, + // entries with the nearest expiration are deleted to make room for new. + // 0 means no limit + // + // Default: 0 + MaxBytes uint + + // You can specify HTTP methods to cache. + // The middleware just caches the routes of its methods in this slice. + // + // Default: []string{fiber.MethodGet, fiber.MethodHead} + Methods []string +} + +// ConfigDefault is the default config +var ConfigDefault = Config{ + Next: nil, + Expiration: 1 * time.Minute, + CacheHeader: "X-Cache", + CacheControl: false, + KeyGenerator: func(c *fiber.Ctx) string { + return utils.CopyString(c.Path()) + }, + ExpirationGenerator: nil, + StoreResponseHeaders: false, + Storage: nil, + MaxBytes: 0, + Methods: []string{fiber.MethodGet, fiber.MethodHead}, +} + +// Helper function to set default values +func configDefault(config ...Config) Config { + // Return default config if nothing provided + if len(config) < 1 { + return ConfigDefault + } + + // Override default config + cfg := config[0] + + // Set default values + if cfg.Store != nil { + log.Warn("[CACHE] Store is deprecated, please use Storage") + cfg.Storage = cfg.Store + } + if cfg.Key != nil { + log.Warn("[CACHE] Key is deprecated, please use KeyGenerator") + cfg.KeyGenerator = cfg.Key + } + if cfg.Next == nil { + cfg.Next = ConfigDefault.Next + } + if int(cfg.Expiration.Seconds()) == 0 { + cfg.Expiration = ConfigDefault.Expiration + } + if cfg.CacheHeader == "" { + cfg.CacheHeader = ConfigDefault.CacheHeader + } + if cfg.KeyGenerator == nil { + cfg.KeyGenerator = ConfigDefault.KeyGenerator + } + if len(cfg.Methods) == 0 { + cfg.Methods = ConfigDefault.Methods + } + return cfg +} diff --git a/middleware/cache/heap.go b/middleware/cache/heap.go new file mode 100644 index 0000000..fcd8356 --- /dev/null +++ b/middleware/cache/heap.go @@ -0,0 +1,92 @@ +package cache + +import ( + "container/heap" +) + +type heapEntry struct { + key string + exp uint64 + bytes uint + idx int +} + +// indexedHeap is a regular min-heap that allows finding +// elements in constant time. It does so by handing out special indices +// and tracking entry movement. +// +// indexdedHeap is used for quickly finding entries with the lowest +// expiration timestamp and deleting arbitrary entries. +type indexedHeap struct { + // Slice the heap is built on + entries []heapEntry + // Mapping "index" to position in heap slice + indices []int + // Max index handed out + maxidx int +} + +func (h indexedHeap) Len() int { + return len(h.entries) +} + +func (h indexedHeap) Less(i, j int) bool { + return h.entries[i].exp < h.entries[j].exp +} + +func (h indexedHeap) Swap(i, j int) { + h.entries[i], h.entries[j] = h.entries[j], h.entries[i] + h.indices[h.entries[i].idx] = i + h.indices[h.entries[j].idx] = j +} + +func (h *indexedHeap) Push(x interface{}) { + h.pushInternal(x.(heapEntry)) //nolint:forcetypeassert // Forced type assertion required to implement the heap.Interface interface +} + +func (h *indexedHeap) Pop() interface{} { + n := len(h.entries) + h.entries = h.entries[0 : n-1] + return h.entries[0:n][n-1] +} + +func (h *indexedHeap) pushInternal(entry heapEntry) { + h.indices[entry.idx] = len(h.entries) + h.entries = append(h.entries, entry) +} + +// Returns index to track entry +func (h *indexedHeap) put(key string, exp uint64, bytes uint) int { + idx := 0 + if len(h.entries) < h.maxidx { + // Steal index from previously removed entry + // capacity > size is guaranteed + n := len(h.entries) + idx = h.entries[:n+1][n].idx + } else { + idx = h.maxidx + h.maxidx++ + h.indices = append(h.indices, idx) + } + // Push manually to avoid allocation + h.pushInternal(heapEntry{ + key: key, exp: exp, idx: idx, bytes: bytes, + }) + heap.Fix(h, h.Len()-1) + return idx +} + +func (h *indexedHeap) removeInternal(realIdx int) (string, uint) { + x := heap.Remove(h, realIdx).(heapEntry) //nolint:forcetypeassert,errcheck // Forced type assertion required to implement the heap.Interface interface + return x.key, x.bytes +} + +// Remove entry by index +func (h *indexedHeap) remove(idx int) (string, uint) { + return h.removeInternal(h.indices[idx]) +} + +// Remove entry with lowest expiration time +func (h *indexedHeap) removeFirst() (string, uint) { + return h.removeInternal(0) +} diff --git a/middleware/cache/manager.go b/middleware/cache/manager.go new file mode 100644 index 0000000..8770478 --- /dev/null +++ b/middleware/cache/manager.go @@ -0,0 +1,132 @@ +package cache + +import ( + "sync" + "time" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/internal/memory" +) + +// go:generate msgp +// msgp -file="manager.go" -o="manager_msgp.go" -tests=false -unexported +type item struct { + body []byte + ctype []byte + cencoding []byte + status int + exp uint64 + headers map[string][]byte + // used for finding the item in an indexed heap + heapidx int +} + +//msgp:ignore manager +type manager struct { + pool sync.Pool + memory *memory.Storage + storage fiber.Storage +} + +func newManager(storage fiber.Storage) *manager { + // Create new storage handler + manager := &manager{ + pool: sync.Pool{ + New: func() interface{} { + return new(item) + }, + }, + } + if storage != nil { + // Use provided storage if provided + manager.storage = storage + } else { + // Fallback to memory storage + manager.memory = memory.New() + } + return manager +} + +// acquire returns an *entry from the sync.Pool +func (m *manager) acquire() *item { + return m.pool.Get().(*item) //nolint:forcetypeassert // We store nothing else in the pool +} + +// release and reset *entry to sync.Pool +func (m *manager) release(e *item) { + // don't release item if we using memory storage + if m.storage != nil { + return + } + e.body = nil + e.ctype = nil + e.status = 0 + e.exp = 0 + e.headers = nil + m.pool.Put(e) +} + +// get data from storage or memory +func (m *manager) get(key string) *item { + var it *item + if m.storage != nil { + it = m.acquire() + raw, err := m.storage.Get(key) + if err != nil { + return it + } + if raw != nil { + if _, err := it.UnmarshalMsg(raw); err != nil { + return it + } + } + return it + } + if it, _ = m.memory.Get(key).(*item); it == nil { //nolint:errcheck // We store nothing else in the pool + it = m.acquire() + return it + } + return it +} + +// get raw data from storage or memory +func (m *manager) getRaw(key string) []byte { + var raw []byte + if m.storage != nil { + raw, _ = m.storage.Get(key) //nolint:errcheck // TODO: Handle error here + } else { + raw, _ = m.memory.Get(key).([]byte) //nolint:errcheck // TODO: Handle error here + } + return raw +} + +// set data to storage or memory +func (m *manager) set(key string, it *item, exp time.Duration) { + if m.storage != nil { + if raw, err := it.MarshalMsg(nil); err == nil { + _ = m.storage.Set(key, raw, exp) //nolint:errcheck // TODO: Handle error here + } + // we can release data because it's serialized to database + m.release(it) + } else { + m.memory.Set(key, it, exp) + } +} + +// set data to storage or memory +func (m *manager) setRaw(key string, raw []byte, exp time.Duration) { + if m.storage != nil { + _ = m.storage.Set(key, raw, exp) //nolint:errcheck // TODO: Handle error here + } else { + m.memory.Set(key, raw, exp) + } +} + +// delete data from storage or memory +func (m *manager) del(key string) { + if m.storage != nil { + _ = m.storage.Delete(key) //nolint:errcheck // TODO: Handle error here + } else { + m.memory.Delete(key) + } +} diff --git a/middleware/cache/manager_msgp.go b/middleware/cache/manager_msgp.go new file mode 100644 index 0000000..3d45903 --- /dev/null +++ b/middleware/cache/manager_msgp.go @@ -0,0 +1,300 @@ +package cache + +// NOTE: THIS FILE WAS PRODUCED BY THE +// MSGP CODE GENERATION TOOL (github.com/tinylib/msgp) +// DO NOT EDIT + +import ( + "github.com/tinylib/msgp/msgp" +) + +// DecodeMsg implements msgp.Decodable +func (z *item) DecodeMsg(dc *msgp.Reader) (err error) { + var field []byte + _ = field + var zbai uint32 + zbai, err = dc.ReadMapHeader() + if err != nil { + return + } + for zbai > 0 { + zbai-- + field, err = dc.ReadMapKeyPtr() + if err != nil { + return + } + switch msgp.UnsafeString(field) { + case "body": + z.body, err = dc.ReadBytes(z.body) + if err != nil { + return + } + case "ctype": + z.ctype, err = dc.ReadBytes(z.ctype) + if err != nil { + return + } + case "cencoding": + z.cencoding, err = dc.ReadBytes(z.cencoding) + if err != nil { + return + } + case "status": + z.status, err = dc.ReadInt() + if err != nil { + return + } + case "exp": + z.exp, err = dc.ReadUint64() + if err != nil { + return + } + case "headers": + var zcmr uint32 + zcmr, err = dc.ReadMapHeader() + if err != nil { + return + } + if z.headers == nil && zcmr > 0 { + z.headers = make(map[string][]byte, zcmr) + } else if len(z.headers) > 0 { + for key := range z.headers { + delete(z.headers, key) + } + } + for zcmr > 0 { + zcmr-- + var zxvk string + var zbzg []byte + zxvk, err = dc.ReadString() + if err != nil { + return + } + zbzg, err = dc.ReadBytes(zbzg) + if err != nil { + return + } + z.headers[zxvk] = zbzg + } + case "heapidx": + z.heapidx, err = dc.ReadInt() + if err != nil { + return + } + default: + err = dc.Skip() + if err != nil { + return + } + } + } + return +} + +// EncodeMsg implements msgp.Encodable +func (z *item) EncodeMsg(en *msgp.Writer) (err error) { + // map header, size 7 + // write "body" + err = en.Append(0x87, 0xa4, 0x62, 0x6f, 0x64, 0x79) + if err != nil { + return err + } + err = en.WriteBytes(z.body) + if err != nil { + return + } + // write "ctype" + err = en.Append(0xa5, 0x63, 0x74, 0x79, 0x70, 0x65) + if err != nil { + return err + } + err = en.WriteBytes(z.ctype) + if err != nil { + return + } + // write "cencoding" + err = en.Append(0xa9, 0x63, 0x65, 0x6e, 0x63, 0x6f, 0x64, 0x69, 0x6e, 0x67) + if err != nil { + return err + } + err = en.WriteBytes(z.cencoding) + if err != nil { + return + } + // write "status" + err = en.Append(0xa6, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73) + if err != nil { + return err + } + err = en.WriteInt(z.status) + if err != nil { + return + } + // write "exp" + err = en.Append(0xa3, 0x65, 0x78, 0x70) + if err != nil { + return err + } + err = en.WriteUint64(z.exp) + if err != nil { + return + } + // write "headers" + err = en.Append(0xa7, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x73) + if err != nil { + return err + } + err = en.WriteMapHeader(uint32(len(z.headers))) + if err != nil { + return + } + for zxvk, zbzg := range z.headers { + err = en.WriteString(zxvk) + if err != nil { + return + } + err = en.WriteBytes(zbzg) + if err != nil { + return + } + } + // write "heapidx" + err = en.Append(0xa7, 0x68, 0x65, 0x61, 0x70, 0x69, 0x64, 0x78) + if err != nil { + return err + } + err = en.WriteInt(z.heapidx) + if err != nil { + return + } + return +} + +// MarshalMsg implements msgp.Marshaler +func (z *item) MarshalMsg(b []byte) (o []byte, err error) { + o = msgp.Require(b, z.Msgsize()) + // map header, size 7 + // string "body" + o = append(o, 0x87, 0xa4, 0x62, 0x6f, 0x64, 0x79) + o = msgp.AppendBytes(o, z.body) + // string "ctype" + o = append(o, 0xa5, 0x63, 0x74, 0x79, 0x70, 0x65) + o = msgp.AppendBytes(o, z.ctype) + // string "cencoding" + o = append(o, 0xa9, 0x63, 0x65, 0x6e, 0x63, 0x6f, 0x64, 0x69, 0x6e, 0x67) + o = msgp.AppendBytes(o, z.cencoding) + // string "status" + o = append(o, 0xa6, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73) + o = msgp.AppendInt(o, z.status) + // string "exp" + o = append(o, 0xa3, 0x65, 0x78, 0x70) + o = msgp.AppendUint64(o, z.exp) + // string "headers" + o = append(o, 0xa7, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x73) + o = msgp.AppendMapHeader(o, uint32(len(z.headers))) + for zxvk, zbzg := range z.headers { + o = msgp.AppendString(o, zxvk) + o = msgp.AppendBytes(o, zbzg) + } + // string "heapidx" + o = append(o, 0xa7, 0x68, 0x65, 0x61, 0x70, 0x69, 0x64, 0x78) + o = msgp.AppendInt(o, z.heapidx) + return +} + +// UnmarshalMsg implements msgp.Unmarshaler +func (z *item) UnmarshalMsg(bts []byte) (o []byte, err error) { + var field []byte + _ = field + var zajw uint32 + zajw, bts, err = msgp.ReadMapHeaderBytes(bts) + if err != nil { + return + } + for zajw > 0 { + zajw-- + field, bts, err = msgp.ReadMapKeyZC(bts) + if err != nil { + return + } + switch msgp.UnsafeString(field) { + case "body": + z.body, bts, err = msgp.ReadBytesBytes(bts, z.body) + if err != nil { + return + } + case "ctype": + z.ctype, bts, err = msgp.ReadBytesBytes(bts, z.ctype) + if err != nil { + return + } + case "cencoding": + z.cencoding, bts, err = msgp.ReadBytesBytes(bts, z.cencoding) + if err != nil { + return + } + case "status": + z.status, bts, err = msgp.ReadIntBytes(bts) + if err != nil { + return + } + case "exp": + z.exp, bts, err = msgp.ReadUint64Bytes(bts) + if err != nil { + return + } + case "headers": + var zwht uint32 + zwht, bts, err = msgp.ReadMapHeaderBytes(bts) + if err != nil { + return + } + if z.headers == nil && zwht > 0 { + z.headers = make(map[string][]byte, zwht) + } else if len(z.headers) > 0 { + for key := range z.headers { + delete(z.headers, key) + } + } + for zwht > 0 { + var zxvk string + var zbzg []byte + zwht-- + zxvk, bts, err = msgp.ReadStringBytes(bts) + if err != nil { + return + } + zbzg, bts, err = msgp.ReadBytesBytes(bts, zbzg) + if err != nil { + return + } + z.headers[zxvk] = zbzg + } + case "heapidx": + z.heapidx, bts, err = msgp.ReadIntBytes(bts) + if err != nil { + return + } + default: + bts, err = msgp.Skip(bts) + if err != nil { + return + } + } + } + o = bts + return +} + +// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message +func (z *item) Msgsize() (s int) { + s = 1 + 5 + msgp.BytesPrefixSize + len(z.body) + 6 + msgp.BytesPrefixSize + len(z.ctype) + 10 + msgp.BytesPrefixSize + len(z.cencoding) + 7 + msgp.IntSize + 4 + msgp.Uint64Size + 8 + msgp.MapHeaderSize + if z.headers != nil { + for zxvk, zbzg := range z.headers { + _ = zbzg + s += msgp.StringPrefixSize + len(zxvk) + msgp.BytesPrefixSize + len(zbzg) + } + } + s += 8 + msgp.IntSize + return +} diff --git a/middleware/compress/compress.go b/middleware/compress/compress.go new file mode 100644 index 0000000..626faf4 --- /dev/null +++ b/middleware/compress/compress.go @@ -0,0 +1,65 @@ +package compress + +import ( + "github.com/gofiber/fiber/v2" + + "github.com/valyala/fasthttp" +) + +// New creates a new middleware handler +func New(config ...Config) fiber.Handler { + // Set default config + cfg := configDefault(config...) + + // Setup request handlers + var ( + fctx = func(c *fasthttp.RequestCtx) {} + compressor fasthttp.RequestHandler + ) + + // Setup compression algorithm + switch cfg.Level { + case LevelDefault: + // LevelDefault + compressor = fasthttp.CompressHandlerBrotliLevel(fctx, + fasthttp.CompressBrotliDefaultCompression, + fasthttp.CompressDefaultCompression, + ) + case LevelBestSpeed: + // LevelBestSpeed + compressor = fasthttp.CompressHandlerBrotliLevel(fctx, + fasthttp.CompressBrotliBestSpeed, + fasthttp.CompressBestSpeed, + ) + case LevelBestCompression: + // LevelBestCompression + compressor = fasthttp.CompressHandlerBrotliLevel(fctx, + fasthttp.CompressBrotliBestCompression, + fasthttp.CompressBestCompression, + ) + default: + // LevelDisabled + return func(c *fiber.Ctx) error { + return c.Next() + } + } + + // Return new handler + return func(c *fiber.Ctx) error { + // Don't execute middleware if Next returns true + if cfg.Next != nil && cfg.Next(c) { + return c.Next() + } + + // Continue stack + if err := c.Next(); err != nil { + return err + } + + // Compress response + compressor(c.Context()) + + // Return from handler + return nil + } +} diff --git a/middleware/compress/compress_test.go b/middleware/compress/compress_test.go new file mode 100644 index 0000000..2c5c52d --- /dev/null +++ b/middleware/compress/compress_test.go @@ -0,0 +1,192 @@ +package compress + +import ( + "errors" + "fmt" + "io" + "net/http/httptest" + "os" + "testing" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/utils" +) + +var filedata []byte + +func init() { + dat, err := os.ReadFile("../../.github/README.md") + if err != nil { + panic(err) + } + filedata = dat +} + +// go test -run Test_Compress_Gzip +func Test_Compress_Gzip(t *testing.T) { + t.Parallel() + app := fiber.New() + + app.Use(New()) + + app.Get("/", func(c *fiber.Ctx) error { + c.Set(fiber.HeaderContentType, fiber.MIMETextPlainCharsetUTF8) + return c.Send(filedata) + }) + + req := httptest.NewRequest(fiber.MethodGet, "/", nil) + req.Header.Set("Accept-Encoding", "gzip") + + resp, err := app.Test(req) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, 200, resp.StatusCode, "Status code") + utils.AssertEqual(t, "gzip", resp.Header.Get(fiber.HeaderContentEncoding)) + + // Validate that the file size has shrunk + body, err := io.ReadAll(resp.Body) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, true, len(body) < len(filedata)) +} + +// go test -run Test_Compress_Different_Level +func Test_Compress_Different_Level(t *testing.T) { + t.Parallel() + levels := []Level{LevelBestSpeed, LevelBestCompression} + for _, level := range levels { + level := level + t.Run(fmt.Sprintf("level %d", level), func(t *testing.T) { + t.Parallel() + app := fiber.New() + + app.Use(New(Config{Level: level})) + + app.Get("/", func(c *fiber.Ctx) error { + c.Set(fiber.HeaderContentType, fiber.MIMETextPlainCharsetUTF8) + return c.Send(filedata) + }) + + req := httptest.NewRequest(fiber.MethodGet, "/", nil) + req.Header.Set("Accept-Encoding", "gzip") + + resp, err := app.Test(req) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, 200, resp.StatusCode, "Status code") + utils.AssertEqual(t, "gzip", resp.Header.Get(fiber.HeaderContentEncoding)) + + // Validate that the file size has shrunk + body, err := io.ReadAll(resp.Body) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, true, len(body) < len(filedata)) + }) + } +} + +func Test_Compress_Deflate(t *testing.T) { + t.Parallel() + app := fiber.New() + + app.Use(New()) + + app.Get("/", func(c *fiber.Ctx) error { + return c.Send(filedata) + }) + + req := httptest.NewRequest(fiber.MethodGet, "/", nil) + req.Header.Set("Accept-Encoding", "deflate") + + resp, err := app.Test(req) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, 200, resp.StatusCode, "Status code") + utils.AssertEqual(t, "deflate", resp.Header.Get(fiber.HeaderContentEncoding)) + + // Validate that the file size has shrunk + body, err := io.ReadAll(resp.Body) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, true, len(body) < len(filedata)) +} + +func Test_Compress_Brotli(t *testing.T) { + t.Parallel() + app := fiber.New() + + app.Use(New()) + + app.Get("/", func(c *fiber.Ctx) error { + return c.Send(filedata) + }) + + req := httptest.NewRequest(fiber.MethodGet, "/", nil) + req.Header.Set("Accept-Encoding", "br") + + resp, err := app.Test(req, 10000) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, 200, resp.StatusCode, "Status code") + utils.AssertEqual(t, "br", resp.Header.Get(fiber.HeaderContentEncoding)) + + // Validate that the file size has shrunk + body, err := io.ReadAll(resp.Body) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, true, len(body) < len(filedata)) +} + +func Test_Compress_Disabled(t *testing.T) { + t.Parallel() + app := fiber.New() + + app.Use(New(Config{Level: LevelDisabled})) + + app.Get("/", func(c *fiber.Ctx) error { + return c.Send(filedata) + }) + + req := httptest.NewRequest(fiber.MethodGet, "/", nil) + req.Header.Set("Accept-Encoding", "br") + + resp, err := app.Test(req) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, 200, resp.StatusCode, "Status code") + utils.AssertEqual(t, "", resp.Header.Get(fiber.HeaderContentEncoding)) + + // Validate the file size is not shrunk + body, err := io.ReadAll(resp.Body) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, true, len(body) == len(filedata)) +} + +func Test_Compress_Next_Error(t *testing.T) { + t.Parallel() + app := fiber.New() + + app.Use(New()) + + app.Get("/", func(c *fiber.Ctx) error { + return errors.New("next error") + }) + + req := httptest.NewRequest(fiber.MethodGet, "/", nil) + req.Header.Set("Accept-Encoding", "gzip") + + resp, err := app.Test(req) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, 500, resp.StatusCode, "Status code") + utils.AssertEqual(t, "", resp.Header.Get(fiber.HeaderContentEncoding)) + + body, err := io.ReadAll(resp.Body) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, "next error", string(body)) +} + +// go test -run Test_Compress_Next +func Test_Compress_Next(t *testing.T) { + t.Parallel() + app := fiber.New() + app.Use(New(Config{ + Next: func(_ *fiber.Ctx) bool { + return true + }, + })) + + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, fiber.StatusNotFound, resp.StatusCode) +} diff --git a/middleware/compress/config.go b/middleware/compress/config.go new file mode 100644 index 0000000..5495ad4 --- /dev/null +++ b/middleware/compress/config.go @@ -0,0 +1,56 @@ +package compress + +import ( + "github.com/gofiber/fiber/v2" +) + +// Config defines the config for middleware. +type Config struct { + // Next defines a function to skip this middleware when returned true. + // + // Optional. Default: nil + Next func(c *fiber.Ctx) bool + + // Level determines the compression algorithm + // + // Optional. Default: LevelDefault + // LevelDisabled: -1 + // LevelDefault: 0 + // LevelBestSpeed: 1 + // LevelBestCompression: 2 + Level Level +} + +// Level is numeric representation of compression level +type Level int + +// Represents compression level that will be used in the middleware +const ( + LevelDisabled Level = -1 + LevelDefault Level = 0 + LevelBestSpeed Level = 1 + LevelBestCompression Level = 2 +) + +// ConfigDefault is the default config +var ConfigDefault = Config{ + Next: nil, + Level: LevelDefault, +} + +// Helper function to set default values +func configDefault(config ...Config) Config { + // Return default config if nothing provided + if len(config) < 1 { + return ConfigDefault + } + + // Override default config + cfg := config[0] + + // Set default values + if cfg.Level < LevelDisabled || cfg.Level > LevelBestCompression { + cfg.Level = ConfigDefault.Level + } + return cfg +} diff --git a/middleware/cors/cors.go b/middleware/cors/cors.go new file mode 100644 index 0000000..f38da1b --- /dev/null +++ b/middleware/cors/cors.go @@ -0,0 +1,289 @@ +package cors + +import ( + "strconv" + "strings" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/log" +) + +// Config defines the config for middleware. +type Config struct { + // Next defines a function to skip this middleware when returned true. + // + // Optional. Default: nil + Next func(c *fiber.Ctx) bool + + // AllowOriginsFunc defines a function that will set the 'Access-Control-Allow-Origin' + // response header to the 'origin' request header when returned true. This allows for + // dynamic evaluation of allowed origins. Note if AllowCredentials is true, wildcard origins + // will be not have the 'Access-Control-Allow-Credentials' header set to 'true'. + // + // Optional. Default: nil + AllowOriginsFunc func(origin string) bool + + // AllowOrigin defines a comma separated list of origins that may access the resource. + // + // Optional. Default value "*" + AllowOrigins string + + // AllowMethods defines a list methods allowed when accessing the resource. + // This is used in response to a preflight request. + // + // Optional. Default value "GET,POST,HEAD,PUT,DELETE,PATCH" + AllowMethods string + + // AllowHeaders defines a list of request headers that can be used when + // making the actual request. This is in response to a preflight request. + // + // Optional. Default value "". + AllowHeaders string + + // AllowCredentials indicates whether or not the response to the request + // can be exposed when the credentials flag is true. When used as part of + // a response to a preflight request, this indicates whether or not the + // actual request can be made using credentials. Note: If true, AllowOrigins + // cannot be set to a wildcard ("*") to prevent security vulnerabilities. + // + // Optional. Default value false. + AllowCredentials bool + + // ExposeHeaders defines a whitelist headers that clients are allowed to + // access. + // + // Optional. Default value "". + ExposeHeaders string + + // MaxAge indicates how long (in seconds) the results of a preflight request + // can be cached. + // If you pass MaxAge 0, Access-Control-Max-Age header will not be added and + // browser will use 5 seconds by default. + // To disable caching completely, pass MaxAge value negative. It will set the Access-Control-Max-Age header 0. + // + // Optional. Default value 0. + MaxAge int +} + +// ConfigDefault is the default config +var ConfigDefault = Config{ + Next: nil, + AllowOriginsFunc: nil, + AllowOrigins: "*", + AllowMethods: strings.Join([]string{ + fiber.MethodGet, + fiber.MethodPost, + fiber.MethodHead, + fiber.MethodPut, + fiber.MethodDelete, + fiber.MethodPatch, + }, ","), + AllowHeaders: "", + AllowCredentials: false, + ExposeHeaders: "", + MaxAge: 0, +} + +// New creates a new middleware handler +func New(config ...Config) fiber.Handler { + // Set default config + cfg := ConfigDefault + + // Override config if provided + if len(config) > 0 { + cfg = config[0] + + // Set default values + if cfg.AllowMethods == "" { + cfg.AllowMethods = ConfigDefault.AllowMethods + } + // When none of the AllowOrigins or AllowOriginsFunc config was defined, set the default AllowOrigins value with "*" + if cfg.AllowOrigins == "" && cfg.AllowOriginsFunc == nil { + cfg.AllowOrigins = ConfigDefault.AllowOrigins + } + } + + // Warning logs if both AllowOrigins and AllowOriginsFunc are set + if cfg.AllowOrigins != "" && cfg.AllowOriginsFunc != nil { + log.Warn("[CORS] Both 'AllowOrigins' and 'AllowOriginsFunc' have been defined.") + } + + // Validate CORS credentials configuration + if cfg.AllowCredentials && cfg.AllowOrigins == "*" { + panic("[CORS] Insecure setup, 'AllowCredentials' is set to true, and 'AllowOrigins' is set to a wildcard.") + } + + // allowOrigins is a slice of strings that contains the allowed origins + // defined in the 'AllowOrigins' configuration. + allowOrigins := []string{} + allowSOrigins := []subdomain{} + allowAllOrigins := false + + // Validate and normalize static AllowOrigins + if cfg.AllowOrigins != "" && cfg.AllowOrigins != "*" { + origins := strings.Split(cfg.AllowOrigins, ",") + for _, origin := range origins { + if i := strings.Index(origin, "://*."); i != -1 { + trimmedOrigin := strings.TrimSpace(origin[:i+3] + origin[i+4:]) + isValid, normalizedOrigin := normalizeOrigin(trimmedOrigin) + if !isValid { + panic("[CORS] Invalid origin format in configuration: " + trimmedOrigin) + } + sd := subdomain{prefix: normalizedOrigin[:i+3], suffix: normalizedOrigin[i+3:]} + allowSOrigins = append(allowSOrigins, sd) + } else { + trimmedOrigin := strings.TrimSpace(origin) + isValid, normalizedOrigin := normalizeOrigin(trimmedOrigin) + if !isValid { + panic("[CORS] Invalid origin format in configuration: " + trimmedOrigin) + } + allowOrigins = append(allowOrigins, normalizedOrigin) + } + } + } else if cfg.AllowOrigins == "*" { + allowAllOrigins = true + } + + // Strip white spaces + allowMethods := strings.ReplaceAll(cfg.AllowMethods, " ", "") + allowHeaders := strings.ReplaceAll(cfg.AllowHeaders, " ", "") + exposeHeaders := strings.ReplaceAll(cfg.ExposeHeaders, " ", "") + + // Convert int to string + maxAge := strconv.Itoa(cfg.MaxAge) + + // Return new handler + return func(c *fiber.Ctx) error { + // Don't execute middleware if Next returns true + if cfg.Next != nil && cfg.Next(c) { + return c.Next() + } + + // Get originHeader header + originHeader := strings.ToLower(c.Get(fiber.HeaderOrigin)) + + // If the request does not have Origin header, the request is outside the scope of CORS + if originHeader == "" { + // See https://fetch.spec.whatwg.org/#cors-protocol-and-http-caches + // Unless all origins are allowed, we include the Vary header to cache the response correctly + if !allowAllOrigins { + c.Vary(fiber.HeaderOrigin) + } + + return c.Next() + } + + // If it's a preflight request and doesn't have Access-Control-Request-Method header, it's outside the scope of CORS + if c.Method() == fiber.MethodOptions && c.Get(fiber.HeaderAccessControlRequestMethod) == "" { + // Response to OPTIONS request should not be cached but, + // some caching can be configured to cache such responses. + // To Avoid poisoning the cache, we include the Vary header + // for non-CORS OPTIONS requests: + c.Vary(fiber.HeaderOrigin) + return c.Next() + } + + // Set default allowOrigin to empty string + allowOrigin := "" + + // Check allowed origins + if allowAllOrigins { + allowOrigin = "*" + } else { + // Check if the origin is in the list of allowed origins + for _, origin := range allowOrigins { + if origin == originHeader { + allowOrigin = originHeader + break + } + } + + // Check if the origin is in the list of allowed subdomains + if allowOrigin == "" { + for _, sOrigin := range allowSOrigins { + if sOrigin.match(originHeader) { + allowOrigin = originHeader + break + } + } + } + } + + // Run AllowOriginsFunc if the logic for + // handling the value in 'AllowOrigins' does + // not result in allowOrigin being set. + if allowOrigin == "" && cfg.AllowOriginsFunc != nil && cfg.AllowOriginsFunc(originHeader) { + allowOrigin = originHeader + } + + // Simple request + // Ommit allowMethods and allowHeaders, only used for pre-flight requests + if c.Method() != fiber.MethodOptions { + if !allowAllOrigins { + // See https://fetch.spec.whatwg.org/#cors-protocol-and-http-caches + c.Vary(fiber.HeaderOrigin) + } + setCORSHeaders(c, allowOrigin, "", "", exposeHeaders, maxAge, cfg) + return c.Next() + } + + // Pre-flight request + + // Response to OPTIONS request should not be cached but, + // some caching can be configured to cache such responses. + // To Avoid poisoning the cache, we include the Vary header + // of preflight responses: + c.Vary(fiber.HeaderAccessControlRequestMethod) + c.Vary(fiber.HeaderAccessControlRequestHeaders) + c.Vary(fiber.HeaderOrigin) + + setCORSHeaders(c, allowOrigin, allowMethods, allowHeaders, exposeHeaders, maxAge, cfg) + + // Send 204 No Content + return c.SendStatus(fiber.StatusNoContent) + } +} + +// Function to set CORS headers +func setCORSHeaders(c *fiber.Ctx, allowOrigin, allowMethods, allowHeaders, exposeHeaders, maxAge string, cfg Config) { + if cfg.AllowCredentials { + // When AllowCredentials is true, set the Access-Control-Allow-Origin to the specific origin instead of '*' + if allowOrigin == "*" { + c.Set(fiber.HeaderAccessControlAllowOrigin, allowOrigin) + log.Warn("[CORS] 'AllowCredentials' is true, but 'AllowOrigins' cannot be set to '*'.") + } else if allowOrigin != "" { + c.Set(fiber.HeaderAccessControlAllowOrigin, allowOrigin) + c.Set(fiber.HeaderAccessControlAllowCredentials, "true") + } + } else if allowOrigin != "" { + // For non-credential requests, it's safe to set to '*' or specific origins + c.Set(fiber.HeaderAccessControlAllowOrigin, allowOrigin) + } + + // Set Allow-Methods if not empty + if allowMethods != "" { + c.Set(fiber.HeaderAccessControlAllowMethods, allowMethods) + } + + // Set Allow-Headers if not empty + if allowHeaders != "" { + c.Set(fiber.HeaderAccessControlAllowHeaders, allowHeaders) + } else { + h := c.Get(fiber.HeaderAccessControlRequestHeaders) + if h != "" { + c.Set(fiber.HeaderAccessControlAllowHeaders, h) + } + } + + // Set MaxAge if set + if cfg.MaxAge > 0 { + c.Set(fiber.HeaderAccessControlMaxAge, maxAge) + } else if cfg.MaxAge < 0 { + c.Set(fiber.HeaderAccessControlMaxAge, "0") + } + + // Set Expose-Headers if not empty + if exposeHeaders != "" { + c.Set(fiber.HeaderAccessControlExposeHeaders, exposeHeaders) + } +} diff --git a/middleware/cors/cors_test.go b/middleware/cors/cors_test.go new file mode 100644 index 0000000..cea2ea5 --- /dev/null +++ b/middleware/cors/cors_test.go @@ -0,0 +1,1335 @@ +package cors + +import ( + "net/http/httptest" + "strings" + "testing" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/utils" + + "github.com/valyala/fasthttp" +) + +func Test_CORS_Defaults(t *testing.T) { + t.Parallel() + app := fiber.New() + app.Use(New()) + + testDefaultOrEmptyConfig(t, app) +} + +func Test_CORS_Empty_Config(t *testing.T) { + t.Parallel() + app := fiber.New() + app.Use(New(Config{})) + + testDefaultOrEmptyConfig(t, app) +} + +func Test_CORS_Negative_MaxAge(t *testing.T) { + t.Parallel() + + app := fiber.New() + app.Use(New(Config{MaxAge: -1})) + + ctx := &fasthttp.RequestCtx{} + ctx.Request.Header.SetMethod(fiber.MethodOptions) + ctx.Request.Header.Set(fiber.HeaderAccessControlRequestMethod, fiber.MethodGet) + ctx.Request.Header.Set(fiber.HeaderOrigin, "http://localhost") + app.Handler()(ctx) + + utils.AssertEqual(t, "0", string(ctx.Response.Header.Peek(fiber.HeaderAccessControlMaxAge))) +} + +func testDefaultOrEmptyConfig(t *testing.T, app *fiber.App) { + t.Helper() + + h := app.Handler() + + // Test default GET response headers + ctx := &fasthttp.RequestCtx{} + ctx.Request.Header.SetMethod(fiber.MethodGet) + ctx.Request.Header.Set(fiber.HeaderOrigin, "http://localhost") + h(ctx) + + utils.AssertEqual(t, "*", string(ctx.Response.Header.Peek(fiber.HeaderAccessControlAllowOrigin))) + utils.AssertEqual(t, "", string(ctx.Response.Header.Peek(fiber.HeaderAccessControlAllowCredentials))) + utils.AssertEqual(t, "", string(ctx.Response.Header.Peek(fiber.HeaderAccessControlExposeHeaders))) + + // Test default OPTIONS (preflight) response headers + ctx = &fasthttp.RequestCtx{} + ctx.Request.Header.SetMethod(fiber.MethodOptions) + ctx.Request.Header.Set(fiber.HeaderAccessControlRequestMethod, fiber.MethodGet) + ctx.Request.Header.Set(fiber.HeaderOrigin, "http://localhost") + h(ctx) + + utils.AssertEqual(t, "GET,POST,HEAD,PUT,DELETE,PATCH", string(ctx.Response.Header.Peek(fiber.HeaderAccessControlAllowMethods))) + utils.AssertEqual(t, "", string(ctx.Response.Header.Peek(fiber.HeaderAccessControlAllowHeaders))) + utils.AssertEqual(t, "", string(ctx.Response.Header.Peek(fiber.HeaderAccessControlMaxAge))) +} + +func Test_CORS_AllowOrigins_Vary(t *testing.T) { + t.Parallel() + app := fiber.New() + app.Use(New( + Config{ + AllowOrigins: "http://localhost", + }, + )) + + h := app.Handler() + + // Test Vary header non-Cors request + ctx := &fasthttp.RequestCtx{} + ctx.Request.Header.SetMethod(fiber.MethodGet) + h(ctx) + utils.AssertEqual(t, true, strings.Contains(string(ctx.Response.Header.Peek(fiber.HeaderVary)), fiber.HeaderOrigin), "Vary header should be set for Origin") + + // Test Vary header Cors preflight request + ctx.Request.Reset() + ctx.Response.Reset() + ctx.Request.Header.SetMethod(fiber.MethodOptions) + ctx.Request.Header.Set(fiber.HeaderAccessControlRequestMethod, fiber.MethodGet) + ctx.Request.Header.Set(fiber.HeaderOrigin, "http://localhost") + h(ctx) + vh := string(ctx.Response.Header.Peek(fiber.HeaderVary)) + utils.AssertEqual(t, true, strings.Contains(vh, fiber.HeaderOrigin), "Vary header should be set for Origin") + utils.AssertEqual(t, true, strings.Contains(vh, fiber.HeaderAccessControlRequestMethod), "Vary header should be set for Access-Control-Request-Method") + utils.AssertEqual(t, true, strings.Contains(vh, fiber.HeaderAccessControlRequestHeaders), "Vary header should be set for Access-Control-Request-Headers") + + // Test Vary header Cors request + ctx.Request.Reset() + ctx.Response.Reset() + ctx.Request.Header.SetMethod(fiber.MethodGet) + ctx.Request.Header.Set(fiber.HeaderOrigin, "http://localhost") + h(ctx) + utils.AssertEqual(t, true, strings.Contains(string(ctx.Response.Header.Peek(fiber.HeaderVary)), fiber.HeaderOrigin), "Vary header should be set for Origin") +} + +// go test -run -v Test_CORS_Wildcard +func Test_CORS_Wildcard(t *testing.T) { + t.Parallel() + // New fiber instance + app := fiber.New() + // OPTIONS (preflight) response headers when AllowOrigins is * + app.Use(New(Config{ + AllowOrigins: "*", + MaxAge: 3600, + ExposeHeaders: "X-Request-ID", + AllowHeaders: "Authentication", + })) + // Get handler pointer + handler := app.Handler() + + // Make request + ctx := &fasthttp.RequestCtx{} + ctx.Request.SetRequestURI("/") + ctx.Request.Header.Set(fiber.HeaderOrigin, "http://localhost") + ctx.Request.Header.SetMethod(fiber.MethodOptions) + ctx.Request.Header.Set(fiber.HeaderAccessControlRequestMethod, fiber.MethodGet) + + // Perform request + handler(ctx) + + // Check result + utils.AssertEqual(t, "*", string(ctx.Response.Header.Peek(fiber.HeaderAccessControlAllowOrigin))) // Validates request is not reflecting origin in the response + vh := string(ctx.Response.Header.Peek(fiber.HeaderVary)) + utils.AssertEqual(t, true, strings.Contains(vh, fiber.HeaderOrigin), "Vary header should be set for Origin") + utils.AssertEqual(t, true, strings.Contains(vh, fiber.HeaderAccessControlRequestMethod), "Vary header should be set for Access-Control-Request-Method") + utils.AssertEqual(t, true, strings.Contains(vh, fiber.HeaderAccessControlRequestHeaders), "Vary header should be set for Access-Control-Request-Headers") + utils.AssertEqual(t, "", string(ctx.Response.Header.Peek(fiber.HeaderAccessControlAllowCredentials))) + utils.AssertEqual(t, "3600", string(ctx.Response.Header.Peek(fiber.HeaderAccessControlMaxAge))) + utils.AssertEqual(t, "Authentication", string(ctx.Response.Header.Peek(fiber.HeaderAccessControlAllowHeaders))) + + // Test non OPTIONS (preflight) response headers + ctx = &fasthttp.RequestCtx{} + ctx.Request.Header.SetMethod(fiber.MethodGet) + ctx.Request.Header.Set(fiber.HeaderOrigin, "http://localhost") + handler(ctx) + + utils.AssertEqual(t, false, strings.Contains(string(ctx.Response.Header.Peek(fiber.HeaderVary)), fiber.HeaderOrigin), "Vary header should not be set for Origin") + utils.AssertEqual(t, "", string(ctx.Response.Header.Peek(fiber.HeaderAccessControlAllowCredentials))) + utils.AssertEqual(t, "X-Request-ID", string(ctx.Response.Header.Peek(fiber.HeaderAccessControlExposeHeaders))) +} + +// go test -run -v Test_CORS_Origin_AllowCredentials +func Test_CORS_Origin_AllowCredentials(t *testing.T) { + t.Parallel() + // New fiber instance + app := fiber.New() + // OPTIONS (preflight) response headers when AllowOrigins is * + app.Use(New(Config{ + AllowOrigins: "http://localhost", + AllowCredentials: true, + MaxAge: 3600, + ExposeHeaders: "X-Request-ID", + AllowHeaders: "Authentication", + })) + // Get handler pointer + handler := app.Handler() + + // Make request + ctx := &fasthttp.RequestCtx{} + ctx.Request.SetRequestURI("/") + ctx.Request.Header.Set(fiber.HeaderOrigin, "http://localhost") + ctx.Request.Header.SetMethod(fiber.MethodOptions) + ctx.Request.Header.Set(fiber.HeaderAccessControlRequestMethod, fiber.MethodGet) + + // Perform request + handler(ctx) + + // Check result + utils.AssertEqual(t, "http://localhost", string(ctx.Response.Header.Peek(fiber.HeaderAccessControlAllowOrigin))) + utils.AssertEqual(t, "true", string(ctx.Response.Header.Peek(fiber.HeaderAccessControlAllowCredentials))) + utils.AssertEqual(t, "3600", string(ctx.Response.Header.Peek(fiber.HeaderAccessControlMaxAge))) + utils.AssertEqual(t, "Authentication", string(ctx.Response.Header.Peek(fiber.HeaderAccessControlAllowHeaders))) + + // Test non OPTIONS (preflight) response headers + ctx = &fasthttp.RequestCtx{} + ctx.Request.Header.Set(fiber.HeaderOrigin, "http://localhost") + ctx.Request.Header.SetMethod(fiber.MethodGet) + handler(ctx) + + utils.AssertEqual(t, "true", string(ctx.Response.Header.Peek(fiber.HeaderAccessControlAllowCredentials))) + utils.AssertEqual(t, "X-Request-ID", string(ctx.Response.Header.Peek(fiber.HeaderAccessControlExposeHeaders))) +} + +// go test -run -v Test_CORS_Wildcard_AllowCredentials_Panic +// Test for fiber-ghsa-fmg4-x8pw-hjhg +func Test_CORS_Wildcard_AllowCredentials_Panic(t *testing.T) { + t.Parallel() + // New fiber instance + app := fiber.New() + + didPanic := false + func() { + defer func() { + if r := recover(); r != nil { + didPanic = true + } + }() + + app.Use(New(Config{ + AllowOrigins: "*", + AllowCredentials: true, + })) + }() + + if !didPanic { + t.Errorf("Expected a panic when AllowOrigins is '*' and AllowCredentials is true") + } +} + +// go test -run -v Test_CORS_Invalid_Origin_Panic +func Test_CORS_Invalid_Origins_Panic(t *testing.T) { + t.Parallel() + + invalidOrigins := []string{ + "localhost", + "http://foo.[a-z]*.example.com", + "http://*", + "https://*", + "http://*.com*", + "invalid url", + "http://origin.com,invalid url", + // add more invalid origins as needed + } + + for _, origin := range invalidOrigins { + // New fiber instance + app := fiber.New() + + didPanic := false + func() { + defer func() { + if r := recover(); r != nil { + didPanic = true + } + }() + + app.Use(New(Config{ + AllowOrigins: origin, + AllowCredentials: true, + })) + }() + + if !didPanic { + t.Errorf("Expected a panic for invalid origin: %s", origin) + } + } +} + +// go test -run -v Test_CORS_Subdomain +func Test_CORS_Subdomain(t *testing.T) { + t.Parallel() + // New fiber instance + app := fiber.New() + // OPTIONS (preflight) response headers when AllowOrigins is set to a subdomain + app.Use("/", New(Config{AllowOrigins: "http://*.example.com"})) + + // Get handler pointer + handler := app.Handler() + + // Make request with disallowed origin + ctx := &fasthttp.RequestCtx{} + ctx.Request.SetRequestURI("/") + ctx.Request.Header.SetMethod(fiber.MethodOptions) + ctx.Request.Header.Set(fiber.HeaderAccessControlRequestMethod, fiber.MethodGet) + ctx.Request.Header.Set(fiber.HeaderOrigin, "http://google.com") + + // Perform request + handler(ctx) + + // Allow-Origin header should be "" because http://google.com does not satisfy http://*.example.com + utils.AssertEqual(t, "", string(ctx.Response.Header.Peek(fiber.HeaderAccessControlAllowOrigin))) + + ctx.Request.Reset() + ctx.Response.Reset() + + // Make request with domain only (disallowed) + ctx.Request.SetRequestURI("/") + ctx.Request.Header.SetMethod(fiber.MethodOptions) + ctx.Request.Header.Set(fiber.HeaderAccessControlRequestMethod, fiber.MethodGet) + ctx.Request.Header.Set(fiber.HeaderOrigin, "http://example.com") + + handler(ctx) + + utils.AssertEqual(t, "", string(ctx.Response.Header.Peek(fiber.HeaderAccessControlAllowOrigin))) + + ctx.Request.Reset() + ctx.Response.Reset() + + // Make request with allowed origin + ctx.Request.SetRequestURI("/") + ctx.Request.Header.SetMethod(fiber.MethodOptions) + ctx.Request.Header.Set(fiber.HeaderAccessControlRequestMethod, fiber.MethodGet) + ctx.Request.Header.Set(fiber.HeaderOrigin, "http://test.example.com") + + handler(ctx) + + utils.AssertEqual(t, "http://test.example.com", string(ctx.Response.Header.Peek(fiber.HeaderAccessControlAllowOrigin))) +} + +func Test_CORS_AllowOriginScheme(t *testing.T) { + t.Parallel() + tests := []struct { + reqOrigin, pattern string + shouldAllowOrigin bool + }{ + { + pattern: "http://example.com", + reqOrigin: "http://example.com", + shouldAllowOrigin: true, + }, + { + pattern: "HTTP://EXAMPLE.COM", + reqOrigin: "http://example.com", + shouldAllowOrigin: true, + }, + { + pattern: "https://example.com", + reqOrigin: "https://example.com", + shouldAllowOrigin: true, + }, + { + pattern: "http://example.com", + reqOrigin: "https://example.com", + shouldAllowOrigin: false, + }, + { + pattern: "http://*.example.com", + reqOrigin: "http://aaa.example.com", + shouldAllowOrigin: true, + }, + { + pattern: "http://*.example.com", + reqOrigin: "http://bbb.aaa.example.com", + shouldAllowOrigin: true, + }, + { + pattern: "http://*.aaa.example.com", + reqOrigin: "http://bbb.aaa.example.com", + shouldAllowOrigin: true, + }, + { + pattern: "http://*.example.com:8080", + reqOrigin: "http://aaa.example.com:8080", + shouldAllowOrigin: true, + }, + { + pattern: "http://*.example.com", + reqOrigin: "http://1.2.aaa.example.com", + shouldAllowOrigin: true, + }, + { + pattern: "http://example.com", + reqOrigin: "http://gofiber.com", + shouldAllowOrigin: false, + }, + { + pattern: "http://*.aaa.example.com", + reqOrigin: "http://ccc.bbb.example.com", + shouldAllowOrigin: false, + }, + { + pattern: "http://*.example.com", + reqOrigin: "http://1234567890.1234567890.1234567890.1234567890.1234567890.1234567890.1234567890.1234567890.1234567890.1234567890.1234567890.example.com", + shouldAllowOrigin: true, + }, + { + pattern: "http://example.com", + reqOrigin: "http://ccc.bbb.example.com", + shouldAllowOrigin: false, + }, + { + pattern: "https://--aaa.bbb.com", + reqOrigin: "https://prod-preview--aaa.bbb.com", + shouldAllowOrigin: false, + }, + { + pattern: "http://*.example.com", + reqOrigin: "http://ccc.bbb.example.com", + shouldAllowOrigin: true, + }, + { + pattern: "http://domain-1.com, http://example.com", + reqOrigin: "http://example.com", + shouldAllowOrigin: true, + }, + { + pattern: "http://domain-1.com, http://example.com", + reqOrigin: "http://domain-2.com", + shouldAllowOrigin: false, + }, + { + pattern: "http://domain-1.com, http://example.com", + reqOrigin: "http://example.com", + shouldAllowOrigin: true, + }, + { + pattern: "http://domain-1.com, http://example.com", + reqOrigin: "http://domain-2.com", + shouldAllowOrigin: false, + }, + { + pattern: "http://domain-1.com,http://example.com", + reqOrigin: "http://domain-1.com", + shouldAllowOrigin: true, + }, + } + + for _, tt := range tests { + app := fiber.New() + app.Use("/", New(Config{AllowOrigins: tt.pattern})) + + handler := app.Handler() + + ctx := &fasthttp.RequestCtx{} + ctx.Request.SetRequestURI("/") + ctx.Request.Header.SetMethod(fiber.MethodOptions) + ctx.Request.Header.Set(fiber.HeaderAccessControlRequestMethod, fiber.MethodGet) + ctx.Request.Header.Set(fiber.HeaderOrigin, tt.reqOrigin) + + handler(ctx) + + if tt.shouldAllowOrigin { + utils.AssertEqual(t, tt.reqOrigin, string(ctx.Response.Header.Peek(fiber.HeaderAccessControlAllowOrigin))) + } else { + utils.AssertEqual(t, "", string(ctx.Response.Header.Peek(fiber.HeaderAccessControlAllowOrigin))) + } + } +} + +func Test_CORS_AllowOriginHeader_NoMatch(t *testing.T) { + t.Parallel() + // New fiber instance + app := fiber.New() + app.Use("/", New(Config{ + AllowOrigins: "http://example-1.com, https://example-1.com", + })) + + // Get handler pointer + handler := app.Handler() + + // Make request with disallowed origin + ctx := &fasthttp.RequestCtx{} + ctx.Request.SetRequestURI("/") + ctx.Request.Header.SetMethod(fiber.MethodOptions) + ctx.Request.Header.Set(fiber.HeaderOrigin, "http://google.com") + + // Perform request + handler(ctx) + + var headerExists bool + ctx.Response.Header.VisitAll(func(key, _ []byte) { + if string(key) == fiber.HeaderAccessControlAllowOrigin { + headerExists = true + } + }) + utils.AssertEqual(t, false, headerExists, "Access-Control-Allow-Origin header should not be set") +} + +// go test -run Test_CORS_Next +func Test_CORS_Next(t *testing.T) { + t.Parallel() + app := fiber.New() + app.Use(New(Config{ + Next: func(_ *fiber.Ctx) bool { + return true + }, + })) + + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, fiber.StatusNotFound, resp.StatusCode) +} + +// go test -run Test_CORS_Headers_BasedOnRequestType +func Test_CORS_Headers_BasedOnRequestType(t *testing.T) { + t.Parallel() + app := fiber.New() + app.Use(New(Config{})) + app.Use(func(c *fiber.Ctx) error { + return c.SendStatus(fiber.StatusOK) + }) + + methods := []string{ + fiber.MethodGet, + fiber.MethodPost, + fiber.MethodPut, + fiber.MethodDelete, + fiber.MethodPatch, + fiber.MethodHead, + } + + // Get handler pointer + handler := app.Handler() + + t.Run("Without origin", func(t *testing.T) { + t.Parallel() + // Make request without origin header, and without Access-Control-Request-Method + for _, method := range methods { + ctx := &fasthttp.RequestCtx{} + ctx.Request.Header.SetMethod(method) + ctx.Request.SetRequestURI("https://example.com/") + handler(ctx) + utils.AssertEqual(t, 200, ctx.Response.StatusCode(), "Status code should be 200") + utils.AssertEqual(t, "", string(ctx.Response.Header.Peek(fiber.HeaderAccessControlAllowOrigin)), "Access-Control-Allow-Origin header should not be set") + } + }) + + t.Run("Preflight request with origin and Access-Control-Request-Method headers", func(t *testing.T) { + t.Parallel() + // Make preflight request with origin header and with Access-Control-Request-Method + for _, method := range methods { + ctx := &fasthttp.RequestCtx{} + ctx.Request.Header.SetMethod(fiber.MethodOptions) + ctx.Request.SetRequestURI("https://example.com/") + ctx.Request.Header.Set(fiber.HeaderOrigin, "http://example.com") + ctx.Request.Header.Set(fiber.HeaderAccessControlRequestMethod, method) + handler(ctx) + utils.AssertEqual(t, 204, ctx.Response.StatusCode(), "Status code should be 204") + utils.AssertEqual(t, "*", string(ctx.Response.Header.Peek(fiber.HeaderAccessControlAllowOrigin)), "Access-Control-Allow-Origin header should be set") + utils.AssertEqual(t, "GET,POST,HEAD,PUT,DELETE,PATCH", string(ctx.Response.Header.Peek(fiber.HeaderAccessControlAllowMethods)), "Access-Control-Allow-Methods header should be set (preflight request)") + utils.AssertEqual(t, "", string(ctx.Response.Header.Peek(fiber.HeaderAccessControlAllowHeaders)), "Access-Control-Allow-Headers header should be set (preflight request)") + } + }) + + t.Run("Non-preflight request with origin", func(t *testing.T) { + t.Parallel() + // Make non-preflight request with origin header and with Access-Control-Request-Method + for _, method := range methods { + ctx := &fasthttp.RequestCtx{} + ctx.Request.Header.SetMethod(method) + ctx.Request.SetRequestURI("https://example.com/api/action") + ctx.Request.Header.Set(fiber.HeaderOrigin, "http://example.com") + handler(ctx) + utils.AssertEqual(t, 200, ctx.Response.StatusCode(), "Status code should be 200") + utils.AssertEqual(t, "*", string(ctx.Response.Header.Peek(fiber.HeaderAccessControlAllowOrigin)), "Access-Control-Allow-Origin header should be set") + utils.AssertEqual(t, "", string(ctx.Response.Header.Peek(fiber.HeaderAccessControlAllowMethods)), "Access-Control-Allow-Methods header should not be set (non-preflight request)") + utils.AssertEqual(t, "", string(ctx.Response.Header.Peek(fiber.HeaderAccessControlAllowHeaders)), "Access-Control-Allow-Headers header should not be set (non-preflight request)") + } + }) +} + +func Test_CORS_AllowOriginsAndAllowOriginsFunc(t *testing.T) { + t.Parallel() + // New fiber instance + app := fiber.New() + app.Use("/", New(Config{ + AllowOrigins: "http://example-1.com", + AllowOriginsFunc: func(origin string) bool { + return strings.Contains(origin, "example-2") + }, + })) + + // Get handler pointer + handler := app.Handler() + + // Make request with disallowed origin + ctx := &fasthttp.RequestCtx{} + ctx.Request.SetRequestURI("/") + ctx.Request.Header.SetMethod(fiber.MethodOptions) + ctx.Request.Header.Set(fiber.HeaderAccessControlRequestMethod, fiber.MethodGet) + ctx.Request.Header.Set(fiber.HeaderOrigin, "http://google.com") + + // Perform request + handler(ctx) + + // Allow-Origin header should be "" because http://google.com does not satisfy http://example-1.com or 'strings.Contains(origin, "example-2")' + utils.AssertEqual(t, "", string(ctx.Response.Header.Peek(fiber.HeaderAccessControlAllowOrigin))) + + ctx.Request.Reset() + ctx.Response.Reset() + + // Make request with allowed origin + ctx.Request.SetRequestURI("/") + ctx.Request.Header.SetMethod(fiber.MethodOptions) + ctx.Request.Header.Set(fiber.HeaderAccessControlRequestMethod, fiber.MethodGet) + ctx.Request.Header.Set(fiber.HeaderOrigin, "http://example-1.com") + + handler(ctx) + + utils.AssertEqual(t, "http://example-1.com", string(ctx.Response.Header.Peek(fiber.HeaderAccessControlAllowOrigin))) + + ctx.Request.Reset() + ctx.Response.Reset() + + // Make request with allowed origin + ctx.Request.SetRequestURI("/") + ctx.Request.Header.SetMethod(fiber.MethodOptions) + ctx.Request.Header.Set(fiber.HeaderAccessControlRequestMethod, fiber.MethodGet) + ctx.Request.Header.Set(fiber.HeaderOrigin, "http://example-2.com") + + handler(ctx) + + utils.AssertEqual(t, "http://example-2.com", string(ctx.Response.Header.Peek(fiber.HeaderAccessControlAllowOrigin))) +} + +func Test_CORS_AllowOriginsFunc(t *testing.T) { + t.Parallel() + // New fiber instance + app := fiber.New() + app.Use("/", New(Config{ + AllowOriginsFunc: func(origin string) bool { + return strings.Contains(origin, "example-2") + }, + })) + + // Get handler pointer + handler := app.Handler() + + // Make request with disallowed origin + ctx := &fasthttp.RequestCtx{} + ctx.Request.SetRequestURI("/") + ctx.Request.Header.SetMethod(fiber.MethodOptions) + ctx.Request.Header.Set(fiber.HeaderOrigin, "http://google.com") + + // Perform request + handler(ctx) + + // Allow-Origin header should be empty because http://google.com does not satisfy 'strings.Contains(origin, "example-2")' + // and AllowOrigins has not been set + utils.AssertEqual(t, "", string(ctx.Response.Header.Peek(fiber.HeaderAccessControlAllowOrigin))) + + ctx.Request.Reset() + ctx.Response.Reset() + + // Make request with allowed origin + ctx.Request.SetRequestURI("/") + ctx.Request.Header.SetMethod(fiber.MethodOptions) + ctx.Request.Header.Set(fiber.HeaderAccessControlRequestMethod, fiber.MethodGet) + ctx.Request.Header.Set(fiber.HeaderOrigin, "http://example-2.com") + + handler(ctx) + + // Allow-Origin header should be "http://example-2.com" + utils.AssertEqual(t, "http://example-2.com", string(ctx.Response.Header.Peek(fiber.HeaderAccessControlAllowOrigin))) +} + +func Test_CORS_AllowOriginsAndAllowOriginsFunc_AllUseCases(t *testing.T) { + testCases := []struct { + Name string + Config Config + RequestOrigin string + ResponseOrigin string + }{ + { + Name: "AllowOriginsDefined/AllowOriginsFuncUndefined/OriginAllowed", + Config: Config{ + AllowOrigins: "http://aaa.com", + AllowOriginsFunc: nil, + }, + RequestOrigin: "http://aaa.com", + ResponseOrigin: "http://aaa.com", + }, + { + Name: "AllowOriginsDefined/AllowOriginsFuncUndefined/MultipleOrigins/NoWhitespace/OriginAllowed", + Config: Config{ + AllowOrigins: "http://aaa.com,http://bbb.com", + AllowOriginsFunc: nil, + }, + RequestOrigin: "http://bbb.com", + ResponseOrigin: "http://bbb.com", + }, + { + Name: "AllowOriginsDefined/AllowOriginsFuncUndefined/MultipleOrigins/NoWhitespace/OriginNotAllowed", + Config: Config{ + AllowOrigins: "http://aaa.com,http://bbb.com", + AllowOriginsFunc: nil, + }, + RequestOrigin: "http://ccc.com", + ResponseOrigin: "", + }, + { + Name: "AllowOriginsDefined/AllowOriginsFuncUndefined/MultipleOrigins/Whitespace/OriginAllowed", + Config: Config{ + AllowOrigins: "http://aaa.com, http://bbb.com", + AllowOriginsFunc: nil, + }, + RequestOrigin: "http://aaa.com", + ResponseOrigin: "http://aaa.com", + }, + { + Name: "AllowOriginsDefined/AllowOriginsFuncUndefined/OriginNotAllowed", + Config: Config{ + AllowOrigins: "http://aaa.com", + AllowOriginsFunc: nil, + }, + RequestOrigin: "http://bbb.com", + ResponseOrigin: "", + }, + { + Name: "AllowOriginsDefined/AllowOriginsFuncReturnsTrue/OriginAllowed", + Config: Config{ + AllowOrigins: "http://aaa.com", + AllowOriginsFunc: func(_ string) bool { + return true + }, + }, + RequestOrigin: "http://aaa.com", + ResponseOrigin: "http://aaa.com", + }, + { + Name: "AllowOriginsDefined/AllowOriginsFuncReturnsTrue/OriginNotAllowed", + Config: Config{ + AllowOrigins: "http://aaa.com", + AllowOriginsFunc: func(_ string) bool { + return true + }, + }, + RequestOrigin: "http://bbb.com", + ResponseOrigin: "http://bbb.com", + }, + { + Name: "AllowOriginsDefined/AllowOriginsFuncReturnsFalse/OriginAllowed", + Config: Config{ + AllowOrigins: "http://aaa.com", + AllowOriginsFunc: func(_ string) bool { + return false + }, + }, + RequestOrigin: "http://aaa.com", + ResponseOrigin: "http://aaa.com", + }, + { + Name: "AllowOriginsDefined/AllowOriginsFuncReturnsFalse/OriginNotAllowed", + Config: Config{ + AllowOrigins: "http://aaa.com", + AllowOriginsFunc: func(_ string) bool { + return false + }, + }, + RequestOrigin: "http://bbb.com", + ResponseOrigin: "", + }, + { + Name: "AllowOriginsEmpty/AllowOriginsFuncUndefined/OriginAllowed", + Config: Config{ + AllowOrigins: "", + AllowOriginsFunc: nil, + }, + RequestOrigin: "http://aaa.com", + ResponseOrigin: "*", + }, + { + Name: "AllowOriginsEmpty/AllowOriginsFuncReturnsTrue/OriginAllowed", + Config: Config{ + AllowOrigins: "", + AllowOriginsFunc: func(_ string) bool { + return true + }, + }, + RequestOrigin: "http://aaa.com", + ResponseOrigin: "http://aaa.com", + }, + { + Name: "AllowOriginsEmpty/AllowOriginsFuncReturnsFalse/OriginNotAllowed", + Config: Config{ + AllowOrigins: "", + AllowOriginsFunc: func(_ string) bool { + return false + }, + }, + RequestOrigin: "http://aaa.com", + ResponseOrigin: "", + }, + } + + for _, tc := range testCases { + t.Run(tc.Name, func(t *testing.T) { + app := fiber.New() + app.Use("/", New(tc.Config)) + + handler := app.Handler() + + ctx := &fasthttp.RequestCtx{} + ctx.Request.SetRequestURI("/") + ctx.Request.Header.SetMethod(fiber.MethodOptions) + ctx.Request.Header.Set(fiber.HeaderAccessControlRequestMethod, fiber.MethodGet) + ctx.Request.Header.Set(fiber.HeaderOrigin, tc.RequestOrigin) + + handler(ctx) + + utils.AssertEqual(t, tc.ResponseOrigin, string(ctx.Response.Header.Peek(fiber.HeaderAccessControlAllowOrigin))) + }) + } +} + +// The fix for issue #2422 +func Test_CORS_AllowCredentials(t *testing.T) { + testCases := []struct { + Name string + Config Config + RequestOrigin string + ResponseOrigin string + ResponseCredentials string + }{ + { + Name: "AllowOriginsFuncDefined", + Config: Config{ + AllowCredentials: true, + AllowOriginsFunc: func(_ string) bool { + return true + }, + }, + RequestOrigin: "http://aaa.com", + // The AllowOriginsFunc config was defined, should use the real origin of the function + ResponseOrigin: "http://aaa.com", + ResponseCredentials: "true", + }, + { + Name: "fiber-ghsa-fmg4-x8pw-hjhg-wildcard-credentials", + Config: Config{ + AllowCredentials: true, + AllowOriginsFunc: func(_ string) bool { + return true + }, + }, + RequestOrigin: "*", + ResponseOrigin: "*", + // Middleware will validate that wildcard wont set credentials to true + ResponseCredentials: "", + }, + { + Name: "AllowOriginsFuncNotDefined", + Config: Config{ + // Setting this to true will cause the middleware to panic since default AllowOrigins is "*" + AllowCredentials: false, + }, + RequestOrigin: "http://aaa.com", + // None of the AllowOrigins or AllowOriginsFunc config was defined, should use the default origin of "*" + // which will cause the CORS error in the client: + // The value of the 'Access-Control-Allow-Origin' header in the response must not be the wildcard '*' + // when the request's credentials mode is 'include'. + ResponseOrigin: "*", + ResponseCredentials: "", + }, + { + Name: "AllowOriginsDefined", + Config: Config{ + AllowCredentials: true, + AllowOrigins: "http://aaa.com", + }, + RequestOrigin: "http://aaa.com", + ResponseOrigin: "http://aaa.com", + ResponseCredentials: "true", + }, + { + Name: "AllowOriginsDefined/UnallowedOrigin", + Config: Config{ + AllowCredentials: true, + AllowOrigins: "http://aaa.com", + }, + RequestOrigin: "http://bbb.com", + ResponseOrigin: "", + ResponseCredentials: "", + }, + } + + for _, tc := range testCases { + t.Run(tc.Name, func(t *testing.T) { + app := fiber.New() + app.Use("/", New(tc.Config)) + + handler := app.Handler() + + ctx := &fasthttp.RequestCtx{} + ctx.Request.SetRequestURI("/") + ctx.Request.Header.SetMethod(fiber.MethodOptions) + ctx.Request.Header.Set(fiber.HeaderAccessControlRequestMethod, fiber.MethodGet) + ctx.Request.Header.Set(fiber.HeaderOrigin, tc.RequestOrigin) + + handler(ctx) + + utils.AssertEqual(t, tc.ResponseCredentials, string(ctx.Response.Header.Peek(fiber.HeaderAccessControlAllowCredentials))) + utils.AssertEqual(t, tc.ResponseOrigin, string(ctx.Response.Header.Peek(fiber.HeaderAccessControlAllowOrigin))) + }) + } +} + +// go test -v -run=^$ -bench=Benchmark_CORS_NewHandler -benchmem -count=4 +func Benchmark_CORS_NewHandler(b *testing.B) { + app := fiber.New() + c := New(Config{ + AllowOrigins: "http://localhost,http://example.com", + AllowMethods: "GET,POST,PUT,DELETE", + AllowHeaders: "Origin,Content-Type,Accept", + AllowCredentials: true, + MaxAge: 600, + }) + + app.Use(c) + app.Use(func(c *fiber.Ctx) error { + return c.SendStatus(fiber.StatusOK) + }) + + h := app.Handler() + ctx := &fasthttp.RequestCtx{} + + req := &fasthttp.Request{} + req.Header.SetMethod(fiber.MethodGet) + req.SetRequestURI("/") + req.Header.Set(fiber.HeaderOrigin, "http://localhost") + req.Header.Set(fiber.HeaderAccessControlRequestHeaders, "Origin,Content-Type,Accept") + + ctx.Init(req, nil, nil) + + b.ReportAllocs() + b.ResetTimer() + + for i := 0; i < b.N; i++ { + h(ctx) + } +} + +// go test -v -run=^$ -bench=Benchmark_CORS_NewHandlerParallel -benchmem -count=4 +func Benchmark_CORS_NewHandlerParallel(b *testing.B) { + app := fiber.New() + c := New(Config{ + AllowOrigins: "http://localhost,http://example.com", + AllowMethods: "GET,POST,PUT,DELETE", + AllowHeaders: "Origin,Content-Type,Accept", + AllowCredentials: true, + MaxAge: 600, + }) + + app.Use(c) + app.Use(func(c *fiber.Ctx) error { + return c.SendStatus(fiber.StatusOK) + }) + + h := app.Handler() + + b.ReportAllocs() + b.ResetTimer() + + b.RunParallel(func(pb *testing.PB) { + ctx := &fasthttp.RequestCtx{} + + req := &fasthttp.Request{} + req.Header.SetMethod(fiber.MethodGet) + req.SetRequestURI("/") + req.Header.Set(fiber.HeaderOrigin, "http://localhost") + req.Header.Set(fiber.HeaderAccessControlRequestHeaders, "Origin,Content-Type,Accept") + + ctx.Init(req, nil, nil) + + for pb.Next() { + h(ctx) + } + }) +} + +// go test -v -run=^$ -bench=Benchmark_CORS_NewHandlerSingleOrigin -benchmem -count=4 +func Benchmark_CORS_NewHandlerSingleOrigin(b *testing.B) { + app := fiber.New() + c := New(Config{ + AllowOrigins: "http://example.com", + AllowMethods: "GET,POST,PUT,DELETE", + AllowHeaders: "Origin,Content-Type,Accept", + AllowCredentials: true, + MaxAge: 600, + }) + + app.Use(c) + app.Use(func(c *fiber.Ctx) error { + return c.SendStatus(fiber.StatusOK) + }) + + h := app.Handler() + ctx := &fasthttp.RequestCtx{} + + req := &fasthttp.Request{} + req.Header.SetMethod(fiber.MethodGet) + req.SetRequestURI("/") + req.Header.Set(fiber.HeaderOrigin, "http://example.com") + req.Header.Set(fiber.HeaderAccessControlRequestHeaders, "Origin,Content-Type,Accept") + + ctx.Init(req, nil, nil) + + b.ReportAllocs() + b.ResetTimer() + + for i := 0; i < b.N; i++ { + h(ctx) + } +} + +// go test -v -run=^$ -bench=Benchmark_CORS_NewHandlerSingleOriginParallel -benchmem -count=4 +func Benchmark_CORS_NewHandlerSingleOriginParallel(b *testing.B) { + app := fiber.New() + c := New(Config{ + AllowOrigins: "http://example.com", + AllowMethods: "GET,POST,PUT,DELETE", + AllowHeaders: "Origin,Content-Type,Accept", + AllowCredentials: true, + MaxAge: 600, + }) + + app.Use(c) + app.Use(func(c *fiber.Ctx) error { + return c.SendStatus(fiber.StatusOK) + }) + + h := app.Handler() + + b.ReportAllocs() + b.ResetTimer() + + b.RunParallel(func(pb *testing.PB) { + ctx := &fasthttp.RequestCtx{} + + req := &fasthttp.Request{} + req.Header.SetMethod(fiber.MethodGet) + req.SetRequestURI("/") + req.Header.Set(fiber.HeaderOrigin, "http://example.com") + req.Header.Set(fiber.HeaderAccessControlRequestHeaders, "Origin,Content-Type,Accept") + + ctx.Init(req, nil, nil) + + for pb.Next() { + h(ctx) + } + }) +} + +// go test -v -run=^$ -bench=Benchmark_CORS_NewHandlerWildcard -benchmem -count=4 +func Benchmark_CORS_NewHandlerWildcard(b *testing.B) { + app := fiber.New() + c := New(Config{ + AllowOrigins: "*", + AllowMethods: "GET,POST,PUT,DELETE", + AllowHeaders: "Origin,Content-Type,Accept", + AllowCredentials: false, + MaxAge: 600, + }) + + app.Use(c) + app.Use(func(c *fiber.Ctx) error { + return c.SendStatus(fiber.StatusOK) + }) + + h := app.Handler() + ctx := &fasthttp.RequestCtx{} + + req := &fasthttp.Request{} + req.Header.SetMethod(fiber.MethodGet) + req.SetRequestURI("/") + req.Header.Set(fiber.HeaderOrigin, "http://example.com") + req.Header.Set(fiber.HeaderAccessControlRequestHeaders, "Origin,Content-Type,Accept") + + ctx.Init(req, nil, nil) + + b.ReportAllocs() + b.ResetTimer() + + for i := 0; i < b.N; i++ { + h(ctx) + } +} + +// go test -v -run=^$ -bench=Benchmark_CORS_NewHandlerWildcardParallel -benchmem -count=4 +func Benchmark_CORS_NewHandlerWildcardParallel(b *testing.B) { + app := fiber.New() + c := New(Config{ + AllowOrigins: "*", + AllowMethods: "GET,POST,PUT,DELETE", + AllowHeaders: "Origin,Content-Type,Accept", + AllowCredentials: false, + MaxAge: 600, + }) + + app.Use(c) + app.Use(func(c *fiber.Ctx) error { + return c.SendStatus(fiber.StatusOK) + }) + + h := app.Handler() + + b.ReportAllocs() + b.ResetTimer() + + b.RunParallel(func(pb *testing.PB) { + ctx := &fasthttp.RequestCtx{} + + req := &fasthttp.Request{} + req.Header.SetMethod(fiber.MethodGet) + req.SetRequestURI("/") + req.Header.Set(fiber.HeaderOrigin, "http://example.com") + req.Header.Set(fiber.HeaderAccessControlRequestHeaders, "Origin,Content-Type,Accept") + + ctx.Init(req, nil, nil) + + for pb.Next() { + h(ctx) + } + }) +} + +// go test -v -run=^$ -bench=Benchmark_CORS_NewHandlerPreflight -benchmem -count=4 +func Benchmark_CORS_NewHandlerPreflight(b *testing.B) { + app := fiber.New() + c := New(Config{ + AllowOrigins: "http://localhost,http://example.com", + AllowMethods: "GET,POST,PUT,DELETE", + AllowHeaders: "Origin,Content-Type,Accept", + AllowCredentials: true, + MaxAge: 600, + }) + + app.Use(c) + app.Use(func(c *fiber.Ctx) error { + return c.SendStatus(fiber.StatusOK) + }) + + h := app.Handler() + ctx := &fasthttp.RequestCtx{} + + // Preflight request + req := &fasthttp.Request{} + req.Header.SetMethod(fiber.MethodOptions) + req.SetRequestURI("/") + req.Header.Set(fiber.HeaderOrigin, "http://example.com") + req.Header.Set(fiber.HeaderAccessControlRequestMethod, fiber.MethodPost) + req.Header.Set(fiber.HeaderAccessControlRequestHeaders, "Origin,Content-Type,Accept") + + ctx.Init(req, nil, nil) + + b.ReportAllocs() + b.ResetTimer() + + for i := 0; i < b.N; i++ { + h(ctx) + } +} + +// go test -v -run=^$ -bench=Benchmark_CORS_NewHandlerPreflightParallel -benchmem -count=4 +func Benchmark_CORS_NewHandlerPreflightParallel(b *testing.B) { + app := fiber.New() + c := New(Config{ + AllowOrigins: "http://localhost,http://example.com", + AllowMethods: "GET,POST,PUT,DELETE", + AllowHeaders: "Origin,Content-Type,Accept", + AllowCredentials: true, + MaxAge: 600, + }) + + app.Use(c) + app.Use(func(c *fiber.Ctx) error { + return c.SendStatus(fiber.StatusOK) + }) + + h := app.Handler() + + b.ReportAllocs() + b.ResetTimer() + + b.RunParallel(func(pb *testing.PB) { + ctx := &fasthttp.RequestCtx{} + + req := &fasthttp.Request{} + req.Header.SetMethod(fiber.MethodOptions) + req.SetRequestURI("/") + req.Header.Set(fiber.HeaderOrigin, "http://example.com") + req.Header.Set(fiber.HeaderAccessControlRequestMethod, fiber.MethodPost) + req.Header.Set(fiber.HeaderAccessControlRequestHeaders, "Origin,Content-Type,Accept") + + ctx.Init(req, nil, nil) + + for pb.Next() { + h(ctx) + } + }) +} + +// go test -v -run=^$ -bench=Benchmark_CORS_NewHandlerPreflightSingleOrigin -benchmem -count=4 +func Benchmark_CORS_NewHandlerPreflightSingleOrigin(b *testing.B) { + app := fiber.New() + c := New(Config{ + AllowOrigins: "http://example.com", + AllowMethods: "GET,POST,PUT,DELETE", + AllowHeaders: "Origin,Content-Type,Accept", + AllowCredentials: true, + MaxAge: 600, + }) + + app.Use(c) + app.Use(func(c *fiber.Ctx) error { + return c.SendStatus(fiber.StatusOK) + }) + + h := app.Handler() + ctx := &fasthttp.RequestCtx{} + + req := &fasthttp.Request{} + req.Header.SetMethod(fiber.MethodOptions) + req.SetRequestURI("/") + req.Header.Set(fiber.HeaderOrigin, "http://example.com") + req.Header.Set(fiber.HeaderAccessControlRequestMethod, fiber.MethodPost) + req.Header.Set(fiber.HeaderAccessControlRequestHeaders, "Origin,Content-Type,Accept") + + ctx.Init(req, nil, nil) + + b.ReportAllocs() + b.ResetTimer() + + for i := 0; i < b.N; i++ { + h(ctx) + } +} + +// go test -v -run=^$ -bench=Benchmark_CORS_NewHandlerPreflightSingleOriginParallel -benchmem -count=4 +func Benchmark_CORS_NewHandlerPreflightSingleOriginParallel(b *testing.B) { + app := fiber.New() + c := New(Config{ + AllowOrigins: "http://example.com", + AllowMethods: "GET,POST,PUT,DELETE", + AllowHeaders: "Origin,Content-Type,Accept", + AllowCredentials: true, + MaxAge: 600, + }) + + app.Use(c) + app.Use(func(c *fiber.Ctx) error { + return c.SendStatus(fiber.StatusOK) + }) + + h := app.Handler() + + b.ReportAllocs() + b.ResetTimer() + + b.RunParallel(func(pb *testing.PB) { + ctx := &fasthttp.RequestCtx{} + + req := &fasthttp.Request{} + req.Header.SetMethod(fiber.MethodOptions) + req.SetRequestURI("/") + req.Header.Set(fiber.HeaderOrigin, "http://example.com") + req.Header.Set(fiber.HeaderAccessControlRequestMethod, fiber.MethodPost) + req.Header.Set(fiber.HeaderAccessControlRequestHeaders, "Origin,Content-Type,Accept") + + ctx.Init(req, nil, nil) + + for pb.Next() { + h(ctx) + } + }) +} + +// go test -v -run=^$ -bench=Benchmark_CORS_NewHandlerPreflightWildcard -benchmem -count=4 +func Benchmark_CORS_NewHandlerPreflightWildcard(b *testing.B) { + app := fiber.New() + c := New(Config{ + AllowOrigins: "*", + AllowMethods: "GET,POST,PUT,DELETE", + AllowHeaders: "Origin,Content-Type,Accept", + AllowCredentials: false, + MaxAge: 600, + }) + + app.Use(c) + app.Use(func(c *fiber.Ctx) error { + return c.SendStatus(fiber.StatusOK) + }) + + h := app.Handler() + ctx := &fasthttp.RequestCtx{} + + req := &fasthttp.Request{} + req.Header.SetMethod(fiber.MethodOptions) + req.SetRequestURI("/") + req.Header.Set(fiber.HeaderOrigin, "http://example.com") + req.Header.Set(fiber.HeaderAccessControlRequestMethod, fiber.MethodPost) + req.Header.Set(fiber.HeaderAccessControlRequestHeaders, "Origin,Content-Type,Accept") + + ctx.Init(req, nil, nil) + + b.ReportAllocs() + b.ResetTimer() + + for i := 0; i < b.N; i++ { + h(ctx) + } +} + +// go test -v -run=^$ -bench=Benchmark_CORS_NewHandlerPreflightWildcardParallel -benchmem -count=4 +func Benchmark_CORS_NewHandlerPreflightWildcardParallel(b *testing.B) { + app := fiber.New() + c := New(Config{ + AllowOrigins: "*", + AllowMethods: "GET,POST,PUT,DELETE", + AllowHeaders: "Origin,Content-Type,Accept", + AllowCredentials: false, + MaxAge: 600, + }) + + app.Use(c) + app.Use(func(c *fiber.Ctx) error { + return c.SendStatus(fiber.StatusOK) + }) + + h := app.Handler() + + b.ReportAllocs() + b.ResetTimer() + + b.RunParallel(func(pb *testing.PB) { + ctx := &fasthttp.RequestCtx{} + + req := &fasthttp.Request{} + req.Header.SetMethod(fiber.MethodOptions) + req.SetRequestURI("/") + req.Header.Set(fiber.HeaderOrigin, "http://example.com") + req.Header.Set(fiber.HeaderAccessControlRequestMethod, fiber.MethodPost) + req.Header.Set(fiber.HeaderAccessControlRequestHeaders, "Origin,Content-Type,Accept") + + ctx.Init(req, nil, nil) + + for pb.Next() { + h(ctx) + } + }) +} diff --git a/middleware/cors/utils.go b/middleware/cors/utils.go new file mode 100644 index 0000000..4278010 --- /dev/null +++ b/middleware/cors/utils.go @@ -0,0 +1,66 @@ +package cors + +import ( + "net/url" + "strings" +) + +// matchScheme compares the scheme of the domain and pattern +func matchScheme(domain, pattern string) bool { + didx := strings.Index(domain, ":") + pidx := strings.Index(pattern, ":") + return didx != -1 && pidx != -1 && domain[:didx] == pattern[:pidx] +} + +// normalizeDomain removes the scheme and port from the input domain +func normalizeDomain(input string) string { + // Remove scheme + input = strings.TrimPrefix(strings.TrimPrefix(input, "http://"), "https://") + + // Find and remove port, if present + if len(input) > 0 && input[0] != '[' { + if portIndex := strings.Index(input, ":"); portIndex != -1 { + input = input[:portIndex] + } + } + + return input +} + +// normalizeOrigin checks if the provided origin is in a correct format +// and normalizes it by removing any path or trailing slash. +// It returns a boolean indicating whether the origin is valid +// and the normalized origin. +func normalizeOrigin(origin string) (bool, string) { + parsedOrigin, err := url.Parse(origin) + if err != nil { + return false, "" + } + + // Don't allow a wildcard with a protocol + // wildcards cannot be used within any other value. For example, the following header is not valid: + // Access-Control-Allow-Origin: https://* + if strings.Contains(parsedOrigin.Host, "*") { + return false, "" + } + + // Validate there is a host present. The presence of a path, query, or fragment components + // is checked, but a trailing "/" (indicative of the root) is allowed for the path and will be normalized + if parsedOrigin.Host == "" || (parsedOrigin.Path != "" && parsedOrigin.Path != "/") || parsedOrigin.RawQuery != "" || parsedOrigin.Fragment != "" { + return false, "" + } + + // Normalize the origin by constructing it from the scheme and host. + // The path or trailing slash is not included in the normalized origin. + return true, strings.ToLower(parsedOrigin.Scheme) + "://" + strings.ToLower(parsedOrigin.Host) +} + +type subdomain struct { + // The wildcard pattern + prefix string + suffix string +} + +func (s subdomain) match(o string) bool { + return len(o) >= len(s.prefix)+len(s.suffix) && strings.HasPrefix(o, s.prefix) && strings.HasSuffix(o, s.suffix) +} diff --git a/middleware/cors/utils_test.go b/middleware/cors/utils_test.go new file mode 100644 index 0000000..aff50de --- /dev/null +++ b/middleware/cors/utils_test.go @@ -0,0 +1,196 @@ +package cors + +import ( + "testing" + + "github.com/gofiber/fiber/v2/utils" +) + +// go test -run -v Test_normalizeOrigin +func Test_normalizeOrigin(t *testing.T) { + testCases := []struct { + origin string + expectedValid bool + expectedOrigin string + }{ + {origin: "http://example.com", expectedValid: true, expectedOrigin: "http://example.com"}, // Simple case should work. + {origin: "http://example.com/", expectedValid: true, expectedOrigin: "http://example.com"}, // Trailing slash should be removed. + {origin: "http://example.com:3000", expectedValid: true, expectedOrigin: "http://example.com:3000"}, // Port should be preserved. + {origin: "http://example.com:3000/", expectedValid: true, expectedOrigin: "http://example.com:3000"}, // Trailing slash should be removed. + {origin: "app://example.com/", expectedValid: true, expectedOrigin: "app://example.com"}, // App scheme should be accepted. + {origin: "http://", expectedValid: false, expectedOrigin: ""}, // Invalid origin should not be accepted. + {origin: "file:///etc/passwd", expectedValid: false, expectedOrigin: ""}, // File scheme should not be accepted. + {origin: "https://*example.com", expectedValid: false, expectedOrigin: ""}, // Wildcard domain should not be accepted. + {origin: "http://*.example.com", expectedValid: false, expectedOrigin: ""}, // Wildcard subdomain should not be accepted. + {origin: "http://example.com/path", expectedValid: false, expectedOrigin: ""}, // Path should not be accepted. + {origin: "http://example.com?query=123", expectedValid: false, expectedOrigin: ""}, // Query should not be accepted. + {origin: "http://example.com#fragment", expectedValid: false, expectedOrigin: ""}, // Fragment should not be accepted. + {origin: "http://localhost", expectedValid: true, expectedOrigin: "http://localhost"}, // Localhost should be accepted. + {origin: "http://127.0.0.1", expectedValid: true, expectedOrigin: "http://127.0.0.1"}, // IPv4 address should be accepted. + {origin: "http://[::1]", expectedValid: true, expectedOrigin: "http://[::1]"}, // IPv6 address should be accepted. + {origin: "http://[::1]:8080", expectedValid: true, expectedOrigin: "http://[::1]:8080"}, // IPv6 address with port should be accepted. + {origin: "http://[::1]:8080/", expectedValid: true, expectedOrigin: "http://[::1]:8080"}, // IPv6 address with port and trailing slash should be accepted. + {origin: "http://[::1]:8080/path", expectedValid: false, expectedOrigin: ""}, // IPv6 address with port and path should not be accepted. + {origin: "http://[::1]:8080?query=123", expectedValid: false, expectedOrigin: ""}, // IPv6 address with port and query should not be accepted. + {origin: "http://[::1]:8080#fragment", expectedValid: false, expectedOrigin: ""}, // IPv6 address with port and fragment should not be accepted. + {origin: "http://[::1]:8080/path?query=123#fragment", expectedValid: false, expectedOrigin: ""}, // IPv6 address with port, path, query, and fragment should not be accepted. + {origin: "http://[::1]:8080/path?query=123#fragment/", expectedValid: false, expectedOrigin: ""}, // IPv6 address with port, path, query, fragment, and trailing slash should not be accepted. + {origin: "http://[::1]:8080/path?query=123#fragment/invalid", expectedValid: false, expectedOrigin: ""}, // IPv6 address with port, path, query, fragment, trailing slash, and invalid segment should not be accepted. + {origin: "http://[::1]:8080/path?query=123#fragment/invalid/", expectedValid: false, expectedOrigin: ""}, // IPv6 address with port, path, query, fragment, trailing slash, and invalid segment with trailing slash should not be accepted. + {origin: "http://[::1]:8080/path?query=123#fragment/invalid/segment", expectedValid: false, expectedOrigin: ""}, // IPv6 address with port, path, query, fragment, trailing slash, and invalid segment with additional segment should not be accepted. + } + + for _, tc := range testCases { + valid, normalizedOrigin := normalizeOrigin(tc.origin) + + if valid != tc.expectedValid { + t.Errorf("Expected origin '%s' to be valid: %v, but got: %v", tc.origin, tc.expectedValid, valid) + } + + if normalizedOrigin != tc.expectedOrigin { + t.Errorf("Expected normalized origin '%s' for origin '%s', but got: '%s'", tc.expectedOrigin, tc.origin, normalizedOrigin) + } + } +} + +// go test -run -v Test_matchScheme +func Test_matchScheme(t *testing.T) { + testCases := []struct { + domain string + pattern string + expected bool + }{ + {"http://example.com", "http://example.com", true}, // Exact match should work. + {"https://example.com", "http://example.com", false}, // Scheme mismatch should matter. + {"http://example.com", "https://example.com", false}, // Scheme mismatch should matter. + {"http://example.com", "http://example.org", true}, // Different domains should not matter. + {"http://example.com", "http://example.com:8080", true}, // Port should not matter. + {"http://example.com:8080", "http://example.com", true}, // Port should not matter. + {"http://example.com:8080", "http://example.com:8081", true}, // Different ports should not matter. + {"http://localhost", "http://localhost", true}, // Localhost should match. + {"http://127.0.0.1", "http://127.0.0.1", true}, // IPv4 address should match. + {"http://[::1]", "http://[::1]", true}, // IPv6 address should match. + } + + for _, tc := range testCases { + result := matchScheme(tc.domain, tc.pattern) + + if result != tc.expected { + t.Errorf("Expected matchScheme('%s', '%s') to be %v, but got %v", tc.domain, tc.pattern, tc.expected, result) + } + } +} + +// go test -run -v Test_normalizeDomain +func Test_normalizeDomain(t *testing.T) { + testCases := []struct { + input string + expectedOutput string + }{ + {"http://example.com", "example.com"}, // Simple case with http scheme. + {"https://example.com", "example.com"}, // Simple case with https scheme. + {"http://example.com:3000", "example.com"}, // Case with port. + {"https://example.com:3000", "example.com"}, // Case with port and https scheme. + {"http://example.com/path", "example.com/path"}, // Case with path. + {"http://example.com?query=123", "example.com?query=123"}, // Case with query. + {"http://example.com#fragment", "example.com#fragment"}, // Case with fragment. + {"example.com", "example.com"}, // Case without scheme. + {"example.com:8080", "example.com"}, // Case without scheme but with port. + {"sub.example.com", "sub.example.com"}, // Case with subdomain. + {"sub.sub.example.com", "sub.sub.example.com"}, // Case with nested subdomain. + {"http://localhost", "localhost"}, // Case with localhost. + {"http://127.0.0.1", "127.0.0.1"}, // Case with IPv4 address. + {"http://[::1]", "[::1]"}, // Case with IPv6 address. + } + + for _, tc := range testCases { + output := normalizeDomain(tc.input) + + if output != tc.expectedOutput { + t.Errorf("Expected normalized domain '%s' for input '%s', but got: '%s'", tc.expectedOutput, tc.input, output) + } + } +} + +// go test -v -run=^$ -bench=Benchmark_CORS_SubdomainMatch -benchmem -count=4 +func Benchmark_CORS_SubdomainMatch(b *testing.B) { + s := subdomain{ + prefix: "www", + suffix: ".example.com", + } + + o := "www.example.com" + + b.ResetTimer() + b.ReportAllocs() + + for i := 0; i < b.N; i++ { + s.match(o) + } +} + +func Test_CORS_SubdomainMatch(t *testing.T) { + tests := []struct { + name string + sub subdomain + origin string + expected bool + }{ + { + name: "match with different scheme", + sub: subdomain{prefix: "http://api.", suffix: ".example.com"}, + origin: "https://api.service.example.com", + expected: false, + }, + { + name: "match with different scheme", + sub: subdomain{prefix: "https://", suffix: ".example.com"}, + origin: "http://api.service.example.com", + expected: false, + }, + { + name: "match with valid subdomain", + sub: subdomain{prefix: "https://", suffix: ".example.com"}, + origin: "https://api.service.example.com", + expected: true, + }, + { + name: "match with valid nested subdomain", + sub: subdomain{prefix: "https://", suffix: ".example.com"}, + origin: "https://1.2.api.service.example.com", + expected: true, + }, + + { + name: "no match with invalid prefix", + sub: subdomain{prefix: "https://abc.", suffix: ".example.com"}, + origin: "https://service.example.com", + expected: false, + }, + { + name: "no match with invalid suffix", + sub: subdomain{prefix: "https://", suffix: ".example.com"}, + origin: "https://api.example.org", + expected: false, + }, + { + name: "no match with empty origin", + sub: subdomain{prefix: "https://", suffix: ".example.com"}, + origin: "", + expected: false, + }, + { + name: "partial match not considered a match", + sub: subdomain{prefix: "https://service.", suffix: ".example.com"}, + origin: "https://api.example.com", + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := tt.sub.match(tt.origin) + utils.AssertEqual(t, tt.expected, got, "subdomain.match()") + }) + } +} diff --git a/middleware/csrf/config.go b/middleware/csrf/config.go new file mode 100644 index 0000000..9ab6cab --- /dev/null +++ b/middleware/csrf/config.go @@ -0,0 +1,243 @@ +package csrf + +import ( + "net/textproto" + "strings" + "time" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/log" + "github.com/gofiber/fiber/v2/middleware/session" + "github.com/gofiber/fiber/v2/utils" +) + +// Config defines the config for middleware. +type Config struct { + // Next defines a function to skip this middleware when returned true. + // + // Optional. Default: nil + Next func(c *fiber.Ctx) bool + + // KeyLookup is a string in the form of ":" that is used + // to create an Extractor that extracts the token from the request. + // Possible values: + // - "header:" + // - "query:" + // - "param:" + // - "form:" + // - "cookie:" + // + // Ignored if an Extractor is explicitly set. + // + // Optional. Default: "header:X-Csrf-Token" + KeyLookup string + + // Name of the session cookie. This cookie will store session key. + // Optional. Default value "csrf_". + // Overridden if KeyLookup == "cookie:" + CookieName string + + // Domain of the CSRF cookie. + // Optional. Default value "". + CookieDomain string + + // Path of the CSRF cookie. + // Optional. Default value "". + CookiePath string + + // Indicates if CSRF cookie is secure. + // Optional. Default value false. + CookieSecure bool + + // Indicates if CSRF cookie is HTTP only. + // Optional. Default value false. + CookieHTTPOnly bool + + // Value of SameSite cookie. + // Optional. Default value "Lax". + CookieSameSite string + + // Decides whether cookie should last for only the browser sesison. + // Ignores Expiration if set to true + CookieSessionOnly bool + + // Expiration is the duration before csrf token will expire + // + // Optional. Default: 1 * time.Hour + Expiration time.Duration + + // SingleUseToken indicates if the CSRF token be destroyed + // and a new one generated on each use. + // + // Optional. Default: false + SingleUseToken bool + + // Store is used to store the state of the middleware + // + // Optional. Default: memory.New() + // Ignored if Session is set. + Storage fiber.Storage + + // Session is used to store the state of the middleware + // + // Optional. Default: nil + // If set, the middleware will use the session store instead of the storage + Session *session.Store + + // SessionKey is the key used to store the token in the session + // + // Default: "fiber.csrf.token" + SessionKey string + + // Context key to store generated CSRF token into context. + // If left empty, token will not be stored in context. + // + // Optional. Default: "" + ContextKey interface{} + + // KeyGenerator creates a new CSRF token + // + // Optional. Default: utils.UUID + KeyGenerator func() string + + // Deprecated: Please use Expiration + CookieExpires time.Duration + + // Deprecated: Please use Cookie* related fields + Cookie *fiber.Cookie + + // Deprecated: Please use KeyLookup + TokenLookup string + + // ErrorHandler is executed when an error is returned from fiber.Handler. + // + // Optional. Default: DefaultErrorHandler + ErrorHandler fiber.ErrorHandler + + // Extractor returns the csrf token + // + // If set this will be used in place of an Extractor based on KeyLookup. + // + // Optional. Default will create an Extractor based on KeyLookup. + Extractor func(c *fiber.Ctx) (string, error) + + // HandlerContextKey is used to store the CSRF Handler into context + // + // Default: "fiber.csrf.handler" + HandlerContextKey interface{} +} + +const HeaderName = "X-Csrf-Token" + +// ConfigDefault is the default config +var ConfigDefault = Config{ + KeyLookup: "header:" + HeaderName, + CookieName: "csrf_", + CookieSameSite: "Lax", + Expiration: 1 * time.Hour, + KeyGenerator: utils.UUIDv4, + ErrorHandler: defaultErrorHandler, + Extractor: CsrfFromHeader(HeaderName), + SessionKey: "fiber.csrf.token", + HandlerContextKey: "fiber.csrf.handler", +} + +// default ErrorHandler that process return error from fiber.Handler +func defaultErrorHandler(_ *fiber.Ctx, _ error) error { + return fiber.ErrForbidden +} + +// Helper function to set default values +func configDefault(config ...Config) Config { + // Return default config if nothing provided + if len(config) < 1 { + return ConfigDefault + } + + // Override default config + cfg := config[0] + + // Set default values + if cfg.TokenLookup != "" { + log.Warn("[CSRF] TokenLookup is deprecated, please use KeyLookup") + cfg.KeyLookup = cfg.TokenLookup + } + if int(cfg.CookieExpires.Seconds()) > 0 { + log.Warn("[CSRF] CookieExpires is deprecated, please use Expiration") + cfg.Expiration = cfg.CookieExpires + } + if cfg.Cookie != nil { + log.Warn("[CSRF] Cookie is deprecated, please use Cookie* related fields") + if cfg.Cookie.Name != "" { + cfg.CookieName = cfg.Cookie.Name + } + if cfg.Cookie.Domain != "" { + cfg.CookieDomain = cfg.Cookie.Domain + } + if cfg.Cookie.Path != "" { + cfg.CookiePath = cfg.Cookie.Path + } + cfg.CookieSecure = cfg.Cookie.Secure + cfg.CookieHTTPOnly = cfg.Cookie.HTTPOnly + if cfg.Cookie.SameSite != "" { + cfg.CookieSameSite = cfg.Cookie.SameSite + } + } + if cfg.KeyLookup == "" { + cfg.KeyLookup = ConfigDefault.KeyLookup + } + if int(cfg.Expiration.Seconds()) <= 0 { + cfg.Expiration = ConfigDefault.Expiration + } + if cfg.CookieName == "" { + cfg.CookieName = ConfigDefault.CookieName + } + if cfg.CookieSameSite == "" { + cfg.CookieSameSite = ConfigDefault.CookieSameSite + } + if cfg.KeyGenerator == nil { + cfg.KeyGenerator = ConfigDefault.KeyGenerator + } + if cfg.ErrorHandler == nil { + cfg.ErrorHandler = ConfigDefault.ErrorHandler + } + if cfg.SessionKey == "" { + cfg.SessionKey = ConfigDefault.SessionKey + } + if cfg.HandlerContextKey == nil { + cfg.HandlerContextKey = ConfigDefault.HandlerContextKey + } + + // Generate the correct extractor to get the token from the correct location + selectors := strings.Split(cfg.KeyLookup, ":") + + const numParts = 2 + if len(selectors) != numParts { + panic("[CSRF] KeyLookup must in the form of :") + } + + if cfg.Extractor == nil { + // By default we extract from a header + cfg.Extractor = CsrfFromHeader(textproto.CanonicalMIMEHeaderKey(selectors[1])) + + switch selectors[0] { + case "form": + cfg.Extractor = CsrfFromForm(selectors[1]) + case "query": + cfg.Extractor = CsrfFromQuery(selectors[1]) + case "param": + cfg.Extractor = CsrfFromParam(selectors[1]) + case "cookie": + if cfg.Session == nil { + log.Warn("[CSRF] Cookie extractor is not recommended without a session store") + } + if cfg.CookieSameSite == "None" || cfg.CookieSameSite != "Lax" && cfg.CookieSameSite != "Strict" { + log.Warn("[CSRF] Cookie extractor is only recommended for use with SameSite=Lax or SameSite=Strict") + } + cfg.Extractor = CsrfFromCookie(selectors[1]) + cfg.CookieName = selectors[1] // Cookie name is the same as the key + } + } + + return cfg +} diff --git a/middleware/csrf/csrf.go b/middleware/csrf/csrf.go new file mode 100644 index 0000000..3c4fdc0 --- /dev/null +++ b/middleware/csrf/csrf.go @@ -0,0 +1,239 @@ +package csrf + +import ( + "errors" + "net/url" + "reflect" + "strings" + "time" + + "github.com/gofiber/fiber/v2" +) + +var ( + ErrTokenNotFound = errors.New("csrf token not found") + ErrTokenInvalid = errors.New("csrf token invalid") + ErrNoReferer = errors.New("referer not supplied") + ErrBadReferer = errors.New("referer invalid") + dummyValue = []byte{'+'} +) + +type CSRFHandler struct { + config *Config + sessionManager *sessionManager + storageManager *storageManager +} + +// New creates a new middleware handler +func New(config ...Config) fiber.Handler { + // Set default config + cfg := configDefault(config...) + + // Create manager to simplify storage operations ( see *_manager.go ) + var sessionManager *sessionManager + var storageManager *storageManager + if cfg.Session != nil { + // Register the Token struct in the session store + cfg.Session.RegisterType(Token{}) + + sessionManager = newSessionManager(cfg.Session, cfg.SessionKey) + } else { + storageManager = newStorageManager(cfg.Storage) + } + + // Return new handler + return func(c *fiber.Ctx) error { + // Don't execute middleware if Next returns true + if cfg.Next != nil && cfg.Next(c) { + return c.Next() + } + + // Store the CSRF handler in the context if a context key is specified + if cfg.HandlerContextKey != "" { + c.Locals(cfg.HandlerContextKey, &CSRFHandler{ + config: &cfg, + sessionManager: sessionManager, + storageManager: storageManager, + }) + } + + var token string + + // Action depends on the HTTP method + switch c.Method() { + case fiber.MethodGet, fiber.MethodHead, fiber.MethodOptions, fiber.MethodTrace: + cookieToken := c.Cookies(cfg.CookieName) + + if cookieToken != "" { + raw := getRawFromStorage(c, cookieToken, cfg, sessionManager, storageManager) + + if raw != nil { + token = cookieToken // Token is valid, safe to set it + } + } + default: + // Assume that anything not defined as 'safe' by RFC7231 needs protection + + // Enforce an origin check for HTTPS connections. + if c.Protocol() == "https" { + if err := refererMatchesHost(c); err != nil { + return cfg.ErrorHandler(c, err) + } + } + + // Extract token from client request i.e. header, query, param, form or cookie + extractedToken, err := cfg.Extractor(c) + if err != nil { + return cfg.ErrorHandler(c, err) + } + + if extractedToken == "" { + return cfg.ErrorHandler(c, ErrTokenNotFound) + } + + // If not using CsrfFromCookie extractor, check that the token matches the cookie + // This is to prevent CSRF attacks by using a Double Submit Cookie method + // Useful when we do not have access to the users Session + if !isCsrfFromCookie(cfg.Extractor) && !compareStrings(extractedToken, c.Cookies(cfg.CookieName)) { + return cfg.ErrorHandler(c, ErrTokenInvalid) + } + + raw := getRawFromStorage(c, extractedToken, cfg, sessionManager, storageManager) + + if raw == nil { + // If token is not in storage, expire the cookie + expireCSRFCookie(c, cfg) + // and return an error + return cfg.ErrorHandler(c, ErrTokenNotFound) + } + if cfg.SingleUseToken { + // If token is single use, delete it from storage + deleteTokenFromStorage(c, extractedToken, cfg, sessionManager, storageManager) + } else { + token = extractedToken // Token is valid, safe to set it + } + } + + // Generate CSRF token if not exist + if token == "" { + // And generate a new token + token = cfg.KeyGenerator() + } + + // Create or extend the token in the storage + createOrExtendTokenInStorage(c, token, cfg, sessionManager, storageManager) + + // Update the CSRF cookie + updateCSRFCookie(c, cfg, token) + + // Tell the browser that a new header value is generated + c.Vary(fiber.HeaderCookie) + + // Store the token in the context if a context key is specified + if cfg.ContextKey != nil { + c.Locals(cfg.ContextKey, token) + } + + // Continue stack + return c.Next() + } +} + +// getRawFromStorage returns the raw value from the storage for the given token +// returns nil if the token does not exist, is expired or is invalid +func getRawFromStorage(c *fiber.Ctx, token string, cfg Config, sessionManager *sessionManager, storageManager *storageManager) []byte { + if cfg.Session != nil { + return sessionManager.getRaw(c, token, dummyValue) + } + return storageManager.getRaw(token) +} + +// createOrExtendTokenInStorage creates or extends the token in the storage +func createOrExtendTokenInStorage(c *fiber.Ctx, token string, cfg Config, sessionManager *sessionManager, storageManager *storageManager) { + if cfg.Session != nil { + sessionManager.setRaw(c, token, dummyValue, cfg.Expiration) + } else { + storageManager.setRaw(token, dummyValue, cfg.Expiration) + } +} + +func deleteTokenFromStorage(c *fiber.Ctx, token string, cfg Config, sessionManager *sessionManager, storageManager *storageManager) { + if cfg.Session != nil { + sessionManager.delRaw(c) + } else { + storageManager.delRaw(token) + } +} + +// Update CSRF cookie +// if expireCookie is true, the cookie will expire immediately +func updateCSRFCookie(c *fiber.Ctx, cfg Config, token string) { + setCSRFCookie(c, cfg, token, cfg.Expiration) +} + +func expireCSRFCookie(c *fiber.Ctx, cfg Config) { + setCSRFCookie(c, cfg, "", -time.Hour) +} + +func setCSRFCookie(c *fiber.Ctx, cfg Config, token string, expiry time.Duration) { + cookie := &fiber.Cookie{ + Name: cfg.CookieName, + Value: token, + Domain: cfg.CookieDomain, + Path: cfg.CookiePath, + Secure: cfg.CookieSecure, + HTTPOnly: cfg.CookieHTTPOnly, + SameSite: cfg.CookieSameSite, + SessionOnly: cfg.CookieSessionOnly, + Expires: time.Now().Add(expiry), + } + + // Set the CSRF cookie to the response + c.Cookie(cookie) +} + +// DeleteToken removes the token found in the context from the storage +// and expires the CSRF cookie +func (handler *CSRFHandler) DeleteToken(c *fiber.Ctx) error { + // Get the config from the context + config := handler.config + if config == nil { + panic("CSRFHandler config not found in context") + } + // Extract token from the client request cookie + cookieToken := c.Cookies(config.CookieName) + if cookieToken == "" { + return config.ErrorHandler(c, ErrTokenNotFound) + } + // Remove the token from storage + deleteTokenFromStorage(c, cookieToken, *config, handler.sessionManager, handler.storageManager) + // Expire the cookie + expireCSRFCookie(c, *config) + return nil +} + +// isCsrfFromCookie checks if the extractor is set to ExtractFromCookie +func isCsrfFromCookie(extractor interface{}) bool { + return reflect.ValueOf(extractor).Pointer() == reflect.ValueOf(CsrfFromCookie).Pointer() +} + +// refererMatchesHost checks that the referer header matches the host header +// returns an error if the referer header is not present or is invalid +// returns nil if the referer header is valid +func refererMatchesHost(c *fiber.Ctx) error { + referer := strings.ToLower(c.Get(fiber.HeaderReferer)) + if referer == "" { + return ErrNoReferer + } + + refererURL, err := url.Parse(referer) + if err != nil { + return ErrBadReferer + } + + if refererURL.Scheme == c.Protocol() && refererURL.Host == c.Hostname() { + return nil + } + + return ErrBadReferer +} diff --git a/middleware/csrf/csrf_test.go b/middleware/csrf/csrf_test.go new file mode 100644 index 0000000..d02661b --- /dev/null +++ b/middleware/csrf/csrf_test.go @@ -0,0 +1,1060 @@ +package csrf + +import ( + "net/http/httptest" + "strings" + "testing" + "time" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/session" + "github.com/gofiber/fiber/v2/utils" + + "github.com/valyala/fasthttp" +) + +func Test_CSRF(t *testing.T) { + t.Parallel() + app := fiber.New() + + app.Use(New()) + + app.Post("/", func(c *fiber.Ctx) error { + return c.SendStatus(fiber.StatusOK) + }) + + h := app.Handler() + ctx := &fasthttp.RequestCtx{} + + methods := [4]string{fiber.MethodGet, fiber.MethodHead, fiber.MethodOptions, fiber.MethodTrace} + + for _, method := range methods { + // Generate CSRF token + ctx.Request.Header.SetMethod(method) + h(ctx) + + // Without CSRF cookie + ctx.Request.Reset() + ctx.Response.Reset() + ctx.Request.Header.SetMethod(fiber.MethodPost) + h(ctx) + utils.AssertEqual(t, 403, ctx.Response.StatusCode()) + + // Empty/invalid CSRF token + ctx.Request.Reset() + ctx.Response.Reset() + ctx.Request.Header.SetMethod(fiber.MethodPost) + ctx.Request.Header.Set(HeaderName, "johndoe") + h(ctx) + utils.AssertEqual(t, 403, ctx.Response.StatusCode()) + + // Valid CSRF token + ctx.Request.Reset() + ctx.Response.Reset() + ctx.Request.Header.SetMethod(method) + h(ctx) + token := string(ctx.Response.Header.Peek(fiber.HeaderSetCookie)) + token = strings.Split(strings.Split(token, ";")[0], "=")[1] + + ctx.Request.Reset() + ctx.Response.Reset() + ctx.Request.Header.SetMethod(fiber.MethodPost) + ctx.Request.Header.Set(HeaderName, token) + ctx.Request.Header.SetCookie(ConfigDefault.CookieName, token) + h(ctx) + utils.AssertEqual(t, 200, ctx.Response.StatusCode()) + } +} + +func Test_CSRF_WithSession(t *testing.T) { + t.Parallel() + + // session store + store := session.New(session.Config{ + KeyLookup: "cookie:_session", + }) + + // fiber instance + app := fiber.New() + + // fiber context + ctx := &fasthttp.RequestCtx{} + defer app.ReleaseCtx(app.AcquireCtx(ctx)) + + // get session + sess, err := store.Get(app.AcquireCtx(ctx)) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, true, sess.Fresh()) + + // the session string is no longer be 123 + newSessionIDString := sess.ID() + err = sess.Save() + utils.AssertEqual(t, nil, err) + + app.AcquireCtx(ctx).Request().Header.SetCookie("_session", newSessionIDString) + + // middleware config + config := Config{ + Session: store, + } + + // middleware + app.Use(New(config)) + + app.Post("/", func(c *fiber.Ctx) error { + return c.SendStatus(fiber.StatusOK) + }) + + h := app.Handler() + + methods := [4]string{fiber.MethodGet, fiber.MethodHead, fiber.MethodOptions, fiber.MethodTrace} + + for _, method := range methods { + // Generate CSRF token + ctx.Request.Header.SetMethod(fiber.MethodGet) + ctx.Request.Header.SetCookie("_session", newSessionIDString) + h(ctx) + + // Without CSRF cookie + ctx.Request.Reset() + ctx.Response.Reset() + ctx.Request.Header.SetMethod(fiber.MethodPost) + ctx.Request.Header.SetCookie("_session", newSessionIDString) + h(ctx) + utils.AssertEqual(t, 403, ctx.Response.StatusCode()) + + // Empty/invalid CSRF token + ctx.Request.Reset() + ctx.Response.Reset() + ctx.Request.Header.SetMethod(fiber.MethodPost) + ctx.Request.Header.Set(HeaderName, "johndoe") + ctx.Request.Header.SetCookie("_session", newSessionIDString) + h(ctx) + utils.AssertEqual(t, 403, ctx.Response.StatusCode()) + + // Valid CSRF token + ctx.Request.Reset() + ctx.Response.Reset() + ctx.Request.Header.SetMethod(method) + ctx.Request.Header.SetCookie("_session", newSessionIDString) + h(ctx) + token := string(ctx.Response.Header.Peek(fiber.HeaderSetCookie)) + for _, header := range strings.Split(token, ";") { + if strings.Split(strings.TrimSpace(header), "=")[0] == ConfigDefault.CookieName { + token = strings.Split(header, "=")[1] + break + } + } + + ctx.Request.Reset() + ctx.Response.Reset() + ctx.Request.Header.SetMethod(fiber.MethodPost) + ctx.Request.Header.Set(HeaderName, token) + ctx.Request.Header.SetCookie("_session", newSessionIDString) + ctx.Request.Header.SetCookie(ConfigDefault.CookieName, token) + h(ctx) + utils.AssertEqual(t, 200, ctx.Response.StatusCode()) + } +} + +// go test -run Test_CSRF_ExpiredToken +func Test_CSRF_ExpiredToken(t *testing.T) { + t.Parallel() + app := fiber.New() + + app.Use(New(Config{ + Expiration: 1 * time.Second, + })) + + app.Post("/", func(c *fiber.Ctx) error { + return c.SendStatus(fiber.StatusOK) + }) + + h := app.Handler() + ctx := &fasthttp.RequestCtx{} + + // Generate CSRF token + ctx.Request.Header.SetMethod(fiber.MethodGet) + h(ctx) + token := string(ctx.Response.Header.Peek(fiber.HeaderSetCookie)) + token = strings.Split(strings.Split(token, ";")[0], "=")[1] + + // Use the CSRF token + ctx.Request.Reset() + ctx.Response.Reset() + ctx.Request.Header.SetMethod(fiber.MethodPost) + ctx.Request.Header.Set(HeaderName, token) + ctx.Request.Header.SetCookie(ConfigDefault.CookieName, token) + h(ctx) + utils.AssertEqual(t, 200, ctx.Response.StatusCode()) + + // Wait for the token to expire + time.Sleep(1250 * time.Millisecond) + + // Expired CSRF token + ctx.Request.Reset() + ctx.Response.Reset() + ctx.Request.Header.SetMethod(fiber.MethodPost) + ctx.Request.Header.Set(HeaderName, token) + ctx.Request.Header.SetCookie(ConfigDefault.CookieName, token) + h(ctx) + utils.AssertEqual(t, 403, ctx.Response.StatusCode()) +} + +// go test -run Test_CSRF_ExpiredToken_WithSession +func Test_CSRF_ExpiredToken_WithSession(t *testing.T) { + t.Parallel() + + // session store + store := session.New(session.Config{ + KeyLookup: "cookie:_session", + }) + + // fiber instance + app := fiber.New() + + // fiber context + ctx := &fasthttp.RequestCtx{} + defer app.ReleaseCtx(app.AcquireCtx(ctx)) + + // get session + sess, err := store.Get(app.AcquireCtx(ctx)) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, true, sess.Fresh()) + + // get session id + newSessionIDString := sess.ID() + err = sess.Save() + utils.AssertEqual(t, nil, err) + + app.AcquireCtx(ctx).Request().Header.SetCookie("_session", newSessionIDString) + + // middleware config + config := Config{ + Session: store, + Expiration: 1 * time.Second, + } + + // middleware + app.Use(New(config)) + + app.Post("/", func(c *fiber.Ctx) error { + return c.SendStatus(fiber.StatusOK) + }) + + h := app.Handler() + + // Generate CSRF token + ctx.Request.Header.SetMethod(fiber.MethodGet) + ctx.Request.Header.SetCookie("_session", newSessionIDString) + h(ctx) + token := string(ctx.Response.Header.Peek(fiber.HeaderSetCookie)) + for _, header := range strings.Split(token, ";") { + if strings.Split(strings.TrimSpace(header), "=")[0] == ConfigDefault.CookieName { + token = strings.Split(header, "=")[1] + break + } + } + + // Use the CSRF token + ctx.Request.Reset() + ctx.Response.Reset() + ctx.Request.Header.SetMethod(fiber.MethodPost) + ctx.Request.Header.Set(HeaderName, token) + ctx.Request.Header.SetCookie("_session", newSessionIDString) + ctx.Request.Header.SetCookie(ConfigDefault.CookieName, token) + h(ctx) + utils.AssertEqual(t, 200, ctx.Response.StatusCode()) + + // Wait for the token to expire + time.Sleep(1 * time.Second) + + // Expired CSRF token + ctx.Request.Reset() + ctx.Response.Reset() + ctx.Request.Header.SetMethod(fiber.MethodPost) + ctx.Request.Header.Set(HeaderName, token) + ctx.Request.Header.SetCookie("_session", newSessionIDString) + ctx.Request.Header.SetCookie(ConfigDefault.CookieName, token) + h(ctx) + utils.AssertEqual(t, 403, ctx.Response.StatusCode()) +} + +// go test -run Test_CSRF_MultiUseToken +func Test_CSRF_MultiUseToken(t *testing.T) { + t.Parallel() + app := fiber.New() + + app.Use(New(Config{ + KeyLookup: "header:X-Csrf-Token", + })) + + app.Post("/", func(c *fiber.Ctx) error { + return c.SendStatus(fiber.StatusOK) + }) + + h := app.Handler() + ctx := &fasthttp.RequestCtx{} + + // Invalid CSRF token + ctx.Request.Header.SetMethod(fiber.MethodPost) + ctx.Request.Header.Set("X-Csrf-Token", "johndoe") + h(ctx) + utils.AssertEqual(t, 403, ctx.Response.StatusCode()) + + // Generate CSRF token + ctx.Request.Reset() + ctx.Response.Reset() + ctx.Request.Header.SetMethod(fiber.MethodGet) + h(ctx) + token := string(ctx.Response.Header.Peek(fiber.HeaderSetCookie)) + token = strings.Split(strings.Split(token, ";")[0], "=")[1] + + ctx.Request.Reset() + ctx.Response.Reset() + ctx.Request.Header.SetMethod(fiber.MethodPost) + ctx.Request.Header.Set("X-Csrf-Token", token) + ctx.Request.Header.SetCookie(ConfigDefault.CookieName, token) + h(ctx) + newToken := string(ctx.Response.Header.Peek(fiber.HeaderSetCookie)) + newToken = strings.Split(strings.Split(newToken, ";")[0], "=")[1] + utils.AssertEqual(t, 200, ctx.Response.StatusCode()) + + // Check if the token is not a dummy value + utils.AssertEqual(t, token, newToken) +} + +// go test -run Test_CSRF_SingleUseToken +func Test_CSRF_SingleUseToken(t *testing.T) { + t.Parallel() + app := fiber.New() + + app.Use(New(Config{ + SingleUseToken: true, + })) + + app.Post("/", func(c *fiber.Ctx) error { + return c.SendStatus(fiber.StatusOK) + }) + + h := app.Handler() + ctx := &fasthttp.RequestCtx{} + + // Generate CSRF token + ctx.Request.Header.SetMethod(fiber.MethodGet) + h(ctx) + token := string(ctx.Response.Header.Peek(fiber.HeaderSetCookie)) + token = strings.Split(strings.Split(token, ";")[0], "=")[1] + + // Use the CSRF token + ctx.Request.Reset() + ctx.Response.Reset() + ctx.Request.Header.SetMethod(fiber.MethodPost) + ctx.Request.Header.Set(HeaderName, token) + ctx.Request.Header.SetCookie(ConfigDefault.CookieName, token) + h(ctx) + utils.AssertEqual(t, 200, ctx.Response.StatusCode()) + newToken := string(ctx.Response.Header.Peek(fiber.HeaderSetCookie)) + newToken = strings.Split(strings.Split(newToken, ";")[0], "=")[1] + if token == newToken { + t.Error("new token should not be the same as the old token") + } + + // Use the CSRF token again + ctx.Request.Reset() + ctx.Response.Reset() + ctx.Request.Header.SetMethod(fiber.MethodPost) + ctx.Request.Header.Set(HeaderName, token) + ctx.Request.Header.SetCookie(ConfigDefault.CookieName, token) + h(ctx) + utils.AssertEqual(t, 403, ctx.Response.StatusCode()) +} + +// go test -run Test_CSRF_Next +func Test_CSRF_Next(t *testing.T) { + t.Parallel() + app := fiber.New() + app.Use(New(Config{ + Next: func(_ *fiber.Ctx) bool { + return true + }, + })) + + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, fiber.StatusNotFound, resp.StatusCode) +} + +func Test_CSRF_Invalid_KeyLookup(t *testing.T) { + t.Parallel() + defer func() { + utils.AssertEqual(t, "[CSRF] KeyLookup must in the form of :", recover()) + }() + app := fiber.New() + + app.Use(New(Config{KeyLookup: "I:am:invalid"})) + + app.Post("/", func(c *fiber.Ctx) error { + return c.SendStatus(fiber.StatusOK) + }) + + h := app.Handler() + ctx := &fasthttp.RequestCtx{} + ctx.Request.Header.SetMethod(fiber.MethodGet) + h(ctx) +} + +func Test_CSRF_From_Form(t *testing.T) { + t.Parallel() + app := fiber.New() + + app.Use(New(Config{KeyLookup: "form:_csrf"})) + + app.Post("/", func(c *fiber.Ctx) error { + return c.SendStatus(fiber.StatusOK) + }) + + h := app.Handler() + ctx := &fasthttp.RequestCtx{} + + // Invalid CSRF token + ctx.Request.Header.SetMethod(fiber.MethodPost) + ctx.Request.Header.Set(fiber.HeaderContentType, fiber.MIMEApplicationForm) + h(ctx) + utils.AssertEqual(t, 403, ctx.Response.StatusCode()) + + // Generate CSRF token + ctx.Request.Reset() + ctx.Response.Reset() + ctx.Request.Header.SetMethod(fiber.MethodGet) + h(ctx) + token := string(ctx.Response.Header.Peek(fiber.HeaderSetCookie)) + token = strings.Split(strings.Split(token, ";")[0], "=")[1] + + ctx.Request.Reset() + ctx.Response.Reset() + ctx.Request.Header.SetMethod(fiber.MethodPost) + ctx.Request.Header.Set(fiber.HeaderContentType, fiber.MIMEApplicationForm) + ctx.Request.SetBodyString("_csrf=" + token) + ctx.Request.Header.SetCookie(ConfigDefault.CookieName, token) + h(ctx) + utils.AssertEqual(t, 200, ctx.Response.StatusCode()) +} + +func Test_CSRF_From_Query(t *testing.T) { + t.Parallel() + app := fiber.New() + + app.Use(New(Config{KeyLookup: "query:_csrf"})) + + app.Post("/", func(c *fiber.Ctx) error { + return c.SendStatus(fiber.StatusOK) + }) + + h := app.Handler() + ctx := &fasthttp.RequestCtx{} + + // Invalid CSRF token + ctx.Request.Header.SetMethod(fiber.MethodPost) + ctx.Request.SetRequestURI("/?_csrf=" + utils.UUIDv4()) + h(ctx) + utils.AssertEqual(t, 403, ctx.Response.StatusCode()) + + // Generate CSRF token + ctx.Request.Reset() + ctx.Response.Reset() + ctx.Request.Header.SetMethod(fiber.MethodGet) + ctx.Request.SetRequestURI("/") + h(ctx) + token := string(ctx.Response.Header.Peek(fiber.HeaderSetCookie)) + token = strings.Split(strings.Split(token, ";")[0], "=")[1] + + ctx.Request.Reset() + ctx.Response.Reset() + ctx.Request.SetRequestURI("/?_csrf=" + token) + ctx.Request.Header.SetMethod(fiber.MethodPost) + ctx.Request.Header.SetCookie(ConfigDefault.CookieName, token) + h(ctx) + utils.AssertEqual(t, 200, ctx.Response.StatusCode()) + utils.AssertEqual(t, "OK", string(ctx.Response.Body())) +} + +func Test_CSRF_From_Param(t *testing.T) { + t.Parallel() + app := fiber.New() + + csrfGroup := app.Group("/:csrf", New(Config{KeyLookup: "param:csrf"})) + + csrfGroup.Post("/", func(c *fiber.Ctx) error { + return c.SendStatus(fiber.StatusOK) + }) + + h := app.Handler() + ctx := &fasthttp.RequestCtx{} + + // Invalid CSRF token + ctx.Request.Header.SetMethod(fiber.MethodPost) + ctx.Request.SetRequestURI("/" + utils.UUIDv4()) + h(ctx) + utils.AssertEqual(t, 403, ctx.Response.StatusCode()) + + // Generate CSRF token + ctx.Request.Reset() + ctx.Response.Reset() + ctx.Request.Header.SetMethod(fiber.MethodGet) + ctx.Request.SetRequestURI("/" + utils.UUIDv4()) + h(ctx) + token := string(ctx.Response.Header.Peek(fiber.HeaderSetCookie)) + token = strings.Split(strings.Split(token, ";")[0], "=")[1] + + ctx.Request.Reset() + ctx.Response.Reset() + ctx.Request.SetRequestURI("/" + token) + ctx.Request.Header.SetMethod(fiber.MethodPost) + ctx.Request.Header.SetCookie(ConfigDefault.CookieName, token) + h(ctx) + utils.AssertEqual(t, 200, ctx.Response.StatusCode()) + utils.AssertEqual(t, "OK", string(ctx.Response.Body())) +} + +func Test_CSRF_From_Cookie(t *testing.T) { + t.Parallel() + app := fiber.New() + + csrfGroup := app.Group("/", New(Config{KeyLookup: "cookie:csrf"})) + + csrfGroup.Post("/", func(c *fiber.Ctx) error { + return c.SendStatus(fiber.StatusOK) + }) + + h := app.Handler() + ctx := &fasthttp.RequestCtx{} + + // Invalid CSRF token + ctx.Request.Header.SetMethod(fiber.MethodPost) + ctx.Request.SetRequestURI("/") + ctx.Request.Header.Set(fiber.HeaderCookie, "csrf="+utils.UUIDv4()+";") + h(ctx) + utils.AssertEqual(t, 403, ctx.Response.StatusCode()) + + // Generate CSRF token + ctx.Request.Reset() + ctx.Response.Reset() + ctx.Request.Header.SetMethod(fiber.MethodGet) + ctx.Request.SetRequestURI("/") + h(ctx) + token := string(ctx.Response.Header.Peek(fiber.HeaderSetCookie)) + token = strings.Split(strings.Split(token, ";")[0], "=")[1] + + ctx.Request.Reset() + ctx.Response.Reset() + ctx.Request.Header.SetMethod(fiber.MethodPost) + ctx.Request.Header.Set(fiber.HeaderCookie, "csrf="+token+";") + ctx.Request.SetRequestURI("/") + h(ctx) + utils.AssertEqual(t, 200, ctx.Response.StatusCode()) + utils.AssertEqual(t, "OK", string(ctx.Response.Body())) +} + +func Test_CSRF_From_Custom(t *testing.T) { + t.Parallel() + app := fiber.New() + + extractor := func(c *fiber.Ctx) (string, error) { + body := string(c.Body()) + // Generate the correct extractor to get the token from the correct location + selectors := strings.Split(body, "=") + + if len(selectors) != 2 || selectors[1] == "" { + return "", ErrMissingParam + } + return selectors[1], nil + } + + app.Use(New(Config{Extractor: extractor})) + + app.Post("/", func(c *fiber.Ctx) error { + return c.SendStatus(fiber.StatusOK) + }) + + h := app.Handler() + ctx := &fasthttp.RequestCtx{} + + // Invalid CSRF token + ctx.Request.Header.SetMethod(fiber.MethodPost) + ctx.Request.Header.Set(fiber.HeaderContentType, fiber.MIMETextPlain) + h(ctx) + utils.AssertEqual(t, 403, ctx.Response.StatusCode()) + + // Generate CSRF token + ctx.Request.Reset() + ctx.Response.Reset() + ctx.Request.Header.SetMethod(fiber.MethodGet) + h(ctx) + token := string(ctx.Response.Header.Peek(fiber.HeaderSetCookie)) + token = strings.Split(strings.Split(token, ";")[0], "=")[1] + + ctx.Request.Reset() + ctx.Response.Reset() + ctx.Request.Header.SetMethod(fiber.MethodPost) + ctx.Request.Header.Set(fiber.HeaderContentType, fiber.MIMETextPlain) + ctx.Request.SetBodyString("_csrf=" + token) + ctx.Request.Header.SetCookie(ConfigDefault.CookieName, token) + h(ctx) + utils.AssertEqual(t, 200, ctx.Response.StatusCode()) +} + +func Test_CSRF_Referer(t *testing.T) { + t.Parallel() + app := fiber.New() + + app.Use(New(Config{CookieSecure: true})) + + app.Post("/", func(c *fiber.Ctx) error { + return c.SendStatus(fiber.StatusOK) + }) + + h := app.Handler() + ctx := &fasthttp.RequestCtx{} + ctx.Request.Header.SetMethod(fiber.MethodGet) + ctx.Request.Header.Set(fiber.HeaderXForwardedProto, "https") + h(ctx) + token := string(ctx.Response.Header.Peek(fiber.HeaderSetCookie)) + token = strings.Split(strings.Split(token, ";")[0], "=")[1] + + // Test Correct Referer with port + ctx.Request.Reset() + ctx.Response.Reset() + ctx.Request.Header.SetMethod(fiber.MethodPost) + ctx.Request.URI().SetScheme("https") + ctx.Request.URI().SetHost("example.com:8443") + ctx.Request.Header.SetProtocol("https") + ctx.Request.Header.SetHost("example.com:8443") + ctx.Request.Header.Set(fiber.HeaderReferer, ctx.Request.URI().String()) + ctx.Request.Header.Set(HeaderName, token) + ctx.Request.Header.SetCookie(ConfigDefault.CookieName, token) + h(ctx) + utils.AssertEqual(t, 200, ctx.Response.StatusCode()) + + // Test Correct Referer with ReverseProxy + ctx.Request.Reset() + ctx.Response.Reset() + ctx.Request.Header.SetMethod(fiber.MethodPost) + ctx.Request.URI().SetScheme("https") + ctx.Request.URI().SetHost("10.0.1.42.com:8443") + ctx.Request.Header.SetProtocol("https") + ctx.Request.Header.SetHost("10.0.1.42:8443") + ctx.Request.Header.Set(fiber.HeaderXForwardedProto, "https") + ctx.Request.Header.Set(fiber.HeaderXForwardedHost, "example.com") + ctx.Request.Header.Set(fiber.HeaderXForwardedFor, `192.0.2.43, "[2001:db8:cafe::17]"`) + ctx.Request.Header.Set(fiber.HeaderReferer, "https://example.com") + ctx.Request.Header.Set(HeaderName, token) + ctx.Request.Header.SetCookie(ConfigDefault.CookieName, token) + h(ctx) + utils.AssertEqual(t, 200, ctx.Response.StatusCode()) + + // Test Correct Referer with ReverseProxy Missing X-Forwarded-* Headers + ctx.Request.Reset() + ctx.Response.Reset() + ctx.Request.Header.SetMethod(fiber.MethodPost) + ctx.Request.URI().SetScheme("https") + ctx.Request.URI().SetHost("10.0.1.42:8443") + ctx.Request.Header.SetProtocol("https") + ctx.Request.Header.SetHost("10.0.1.42:8443") + ctx.Request.Header.Set(fiber.HeaderXUrlScheme, "https") // We need to set this header to make sure c.Protocol() returns https + ctx.Request.Header.Set(fiber.HeaderReferer, "https://example.com") + ctx.Request.Header.Set(HeaderName, token) + ctx.Request.Header.SetCookie(ConfigDefault.CookieName, token) + h(ctx) + utils.AssertEqual(t, 403, ctx.Response.StatusCode()) + + // Test Correct Referer with path + ctx.Request.Reset() + ctx.Response.Reset() + ctx.Request.Header.SetMethod(fiber.MethodPost) + ctx.Request.Header.Set(fiber.HeaderXForwardedProto, "https") + ctx.Request.Header.Set(fiber.HeaderXForwardedHost, "example.com") + ctx.Request.Header.Set(fiber.HeaderReferer, "https://example.com/action/items?gogogo=true") + ctx.Request.Header.Set(HeaderName, token) + ctx.Request.Header.SetCookie(ConfigDefault.CookieName, token) + h(ctx) + utils.AssertEqual(t, 200, ctx.Response.StatusCode()) + + // Test Wrong Referer + ctx.Request.Reset() + ctx.Response.Reset() + ctx.Request.Header.SetMethod(fiber.MethodPost) + ctx.Request.Header.Set(fiber.HeaderXForwardedProto, "https") + ctx.Request.Header.Set(fiber.HeaderXForwardedHost, "example.com") + ctx.Request.Header.Set(fiber.HeaderReferer, "https://csrf.example.com") + ctx.Request.Header.Set(HeaderName, token) + ctx.Request.Header.SetCookie(ConfigDefault.CookieName, token) + h(ctx) + utils.AssertEqual(t, 403, ctx.Response.StatusCode()) +} + +func Test_CSRF_DeleteToken(t *testing.T) { + t.Parallel() + app := fiber.New() + + config := ConfigDefault + + app.Use(New(config)) + + app.Post("/", func(c *fiber.Ctx) error { + return c.SendStatus(fiber.StatusOK) + }) + + h := app.Handler() + ctx := &fasthttp.RequestCtx{} + + // Generate CSRF token + ctx.Request.Header.SetMethod(fiber.MethodGet) + h(ctx) + token := string(ctx.Response.Header.Peek(fiber.HeaderSetCookie)) + token = strings.Split(strings.Split(token, ";")[0], "=")[1] + + // Delete the CSRF token + ctx.Request.Reset() + ctx.Response.Reset() + ctx.Request.Header.SetMethod(fiber.MethodPost) + ctx.Request.Header.Set(HeaderName, token) + ctx.Request.Header.SetCookie(ConfigDefault.CookieName, token) + if handler, ok := app.AcquireCtx(ctx).Locals(ConfigDefault.HandlerContextKey).(*CSRFHandler); ok { + if err := handler.DeleteToken(app.AcquireCtx(ctx)); err != nil { + t.Fatal(err) + } + } + h(ctx) + + ctx.Request.Reset() + ctx.Response.Reset() + ctx.Request.Header.SetMethod(fiber.MethodPost) + ctx.Request.Header.Set(HeaderName, token) + ctx.Request.Header.SetCookie(ConfigDefault.CookieName, token) + h(ctx) + utils.AssertEqual(t, 403, ctx.Response.StatusCode()) +} + +func Test_CSRF_DeleteToken_WithSession(t *testing.T) { + t.Parallel() + + // session store + store := session.New(session.Config{ + KeyLookup: "cookie:_session", + }) + + // fiber instance + app := fiber.New() + + // fiber context + ctx := &fasthttp.RequestCtx{} + defer app.ReleaseCtx(app.AcquireCtx(ctx)) + + // get session + sess, err := store.Get(app.AcquireCtx(ctx)) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, true, sess.Fresh()) + + // the session string is no longer be 123 + newSessionIDString := sess.ID() + app.AcquireCtx(ctx).Request().Header.SetCookie("_session", newSessionIDString) + + // middleware config + config := Config{ + Session: store, + } + + // middleware + app.Use(New(config)) + + app.Post("/", func(c *fiber.Ctx) error { + return c.SendStatus(fiber.StatusOK) + }) + + h := app.Handler() + + // Generate CSRF token + ctx.Request.Header.SetMethod(fiber.MethodGet) + ctx.Request.Header.SetCookie("_session", newSessionIDString) + h(ctx) + token := string(ctx.Response.Header.Peek(fiber.HeaderSetCookie)) + token = strings.Split(strings.Split(token, ";")[0], "=")[1] + + // Delete the CSRF token + ctx.Request.Reset() + ctx.Response.Reset() + ctx.Request.Header.SetMethod(fiber.MethodPost) + ctx.Request.Header.Set(HeaderName, token) + ctx.Request.Header.SetCookie(ConfigDefault.CookieName, token) + if handler, ok := app.AcquireCtx(ctx).Locals(ConfigDefault.HandlerContextKey).(*CSRFHandler); ok { + if err := handler.DeleteToken(app.AcquireCtx(ctx)); err != nil { + t.Fatal(err) + } + } + h(ctx) + + ctx.Request.Reset() + ctx.Response.Reset() + ctx.Request.Header.SetMethod(fiber.MethodPost) + ctx.Request.Header.Set(HeaderName, token) + ctx.Request.Header.SetCookie(ConfigDefault.CookieName, token) + ctx.Request.Header.SetCookie("_session", newSessionIDString) + h(ctx) + utils.AssertEqual(t, 403, ctx.Response.StatusCode()) +} + +func Test_CSRF_ErrorHandler_InvalidToken(t *testing.T) { + t.Parallel() + app := fiber.New() + + errHandler := func(ctx *fiber.Ctx, err error) error { + utils.AssertEqual(t, ErrTokenInvalid, err) + return ctx.Status(419).Send([]byte("invalid CSRF token")) + } + + app.Use(New(Config{ErrorHandler: errHandler})) + + app.Post("/", func(c *fiber.Ctx) error { + return c.SendStatus(fiber.StatusOK) + }) + + h := app.Handler() + ctx := &fasthttp.RequestCtx{} + + // Generate CSRF token + ctx.Request.Header.SetMethod(fiber.MethodGet) + h(ctx) + + // invalid CSRF token + ctx.Request.Reset() + ctx.Response.Reset() + ctx.Request.Header.SetMethod(fiber.MethodPost) + ctx.Request.Header.Set(HeaderName, "johndoe") + h(ctx) + utils.AssertEqual(t, 419, ctx.Response.StatusCode()) + utils.AssertEqual(t, "invalid CSRF token", string(ctx.Response.Body())) +} + +func Test_CSRF_ErrorHandler_EmptyToken(t *testing.T) { + t.Parallel() + app := fiber.New() + + errHandler := func(ctx *fiber.Ctx, err error) error { + utils.AssertEqual(t, ErrMissingHeader, err) + return ctx.Status(419).Send([]byte("empty CSRF token")) + } + + app.Use(New(Config{ErrorHandler: errHandler})) + + app.Post("/", func(c *fiber.Ctx) error { + return c.SendStatus(fiber.StatusOK) + }) + + h := app.Handler() + ctx := &fasthttp.RequestCtx{} + + // Generate CSRF token + ctx.Request.Header.SetMethod(fiber.MethodGet) + h(ctx) + + // empty CSRF token + ctx.Request.Reset() + ctx.Response.Reset() + ctx.Request.Header.SetMethod(fiber.MethodPost) + h(ctx) + utils.AssertEqual(t, 419, ctx.Response.StatusCode()) + utils.AssertEqual(t, "empty CSRF token", string(ctx.Response.Body())) +} + +func Test_CSRF_ErrorHandler_MissingReferer(t *testing.T) { + t.Parallel() + app := fiber.New() + + errHandler := func(ctx *fiber.Ctx, err error) error { + utils.AssertEqual(t, ErrNoReferer, err) + return ctx.Status(419).Send([]byte("empty CSRF token")) + } + + app.Use(New(Config{ + CookieSecure: true, + ErrorHandler: errHandler, + })) + + app.Post("/", func(c *fiber.Ctx) error { + return c.SendStatus(fiber.StatusOK) + }) + + h := app.Handler() + ctx := &fasthttp.RequestCtx{} + ctx.Request.Header.SetMethod(fiber.MethodGet) + ctx.Request.Header.Set(fiber.HeaderXForwardedProto, "https") + h(ctx) + token := string(ctx.Response.Header.Peek(fiber.HeaderSetCookie)) + token = strings.Split(strings.Split(token, ";")[0], "=")[1] + + ctx.Request.Reset() + ctx.Response.Reset() + ctx.Request.Header.SetMethod(fiber.MethodPost) + ctx.Request.Header.Set(fiber.HeaderXForwardedProto, "https") + ctx.Request.Header.Set(fiber.HeaderXForwardedHost, "example.com") + ctx.Request.Header.Set(HeaderName, token) + ctx.Request.Header.SetCookie(ConfigDefault.CookieName, token) + h(ctx) + utils.AssertEqual(t, 419, ctx.Response.StatusCode()) +} + +func Test_CSRF_Cookie_Injection_Exploit(t *testing.T) { + t.Parallel() + app := fiber.New() + + app.Use(New()) + + app.Post("/", func(c *fiber.Ctx) error { + return c.SendStatus(fiber.StatusOK) + }) + + h := app.Handler() + ctx := &fasthttp.RequestCtx{} + + // Inject CSRF token + ctx.Request.Reset() + ctx.Response.Reset() + ctx.Request.Header.SetMethod(fiber.MethodGet) + ctx.Request.Header.Set(fiber.HeaderCookie, "csrf_=pwned;") + ctx.Request.SetRequestURI("/") + h(ctx) + token := string(ctx.Response.Header.Peek(fiber.HeaderSetCookie)) + token = strings.Split(strings.Split(token, ";")[0], "=")[1] + + // Exploit CSRF token we just injected + ctx.Request.Reset() + ctx.Response.Reset() + ctx.Request.Header.SetMethod(fiber.MethodPost) + ctx.Request.Header.Set(HeaderName, token) + ctx.Request.Header.Set(fiber.HeaderCookie, "csrf_=pwned;") + h(ctx) + utils.AssertEqual(t, 403, ctx.Response.StatusCode(), "CSRF exploit successful") +} + +// TODO: use this test case and make the unsafe header value bug from https://github.com/gofiber/fiber/issues/2045 reproducible and permanently fixed/tested by this testcase +// func Test_CSRF_UnsafeHeaderValue(t *testing.T) { +// t.Parallel() +// app := fiber.New() + +// app.Use(New()) +// app.Get("/", func(c *fiber.Ctx) error { +// return c.SendStatus(fiber.StatusOK) +// }) +// app.Get("/test", func(c *fiber.Ctx) error { +// return c.SendStatus(fiber.StatusOK) +// }) +// app.Post("/", func(c *fiber.Ctx) error { +// return c.SendStatus(fiber.StatusOK) +// }) + +// resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) +// utils.AssertEqual(t, nil, err) +// utils.AssertEqual(t, fiber.StatusOK, resp.StatusCode) + +// var token string +// for _, c := range resp.Cookies() { +// if c.Name != ConfigDefault.CookieName { +// continue +// } +// token = c.Value +// break +// } + +// fmt.Println("token", token) + +// getReq := httptest.NewRequest(fiber.MethodGet, "/", nil) +// getReq.Header.Set(HeaderName, token) +// resp, err = app.Test(getReq) + +// getReq = httptest.NewRequest(fiber.MethodGet, "/test", nil) +// getReq.Header.Set("X-Requested-With", "XMLHttpRequest") +// getReq.Header.Set(fiber.HeaderCacheControl, "no") +// getReq.Header.Set(HeaderName, token) + +// resp, err = app.Test(getReq) + +// getReq.Header.Set(fiber.HeaderAccept, "*/*") +// getReq.Header.Del(HeaderName) +// resp, err = app.Test(getReq) + +// postReq := httptest.NewRequest(fiber.MethodPost, "/", nil) +// postReq.Header.Set("X-Requested-With", "XMLHttpRequest") +// postReq.Header.Set(HeaderName, token) +// resp, err = app.Test(postReq) +// } + +// go test -v -run=^$ -bench=Benchmark_Middleware_CSRF_Check -benchmem -count=4 +func Benchmark_Middleware_CSRF_Check(b *testing.B) { + app := fiber.New() + + app.Use(New()) + app.Get("/", func(c *fiber.Ctx) error { + return c.SendStatus(fiber.StatusTeapot) + }) + + app.Post("/", func(c *fiber.Ctx) error { + return c.SendStatus(fiber.StatusTeapot) + }) + + h := app.Handler() + ctx := &fasthttp.RequestCtx{} + + // Generate CSRF token + ctx.Request.Header.SetMethod(fiber.MethodGet) + h(ctx) + token := string(ctx.Response.Header.Peek(fiber.HeaderSetCookie)) + token = strings.Split(strings.Split(token, ";")[0], "=")[1] + + // Test Correct Referer POST + ctx.Request.Reset() + ctx.Response.Reset() + ctx.Request.Header.SetMethod(fiber.MethodPost) + ctx.Request.Header.Set(fiber.HeaderXForwardedProto, "https") + ctx.Request.URI().SetScheme("https") + ctx.Request.URI().SetHost("example.com") + ctx.Request.Header.SetProtocol("https") + ctx.Request.Header.SetHost("example.com") + ctx.Request.Header.Set(fiber.HeaderReferer, "https://example.com") + ctx.Request.Header.Set(HeaderName, token) + ctx.Request.Header.SetCookie(ConfigDefault.CookieName, token) + + b.ReportAllocs() + b.ResetTimer() + + for n := 0; n < b.N; n++ { + h(ctx) + } + + utils.AssertEqual(b, fiber.StatusTeapot, ctx.Response.Header.StatusCode()) +} + +// go test -v -run=^$ -bench=Benchmark_Middleware_CSRF_GenerateToken -benchmem -count=4 +func Benchmark_Middleware_CSRF_GenerateToken(b *testing.B) { + app := fiber.New() + + app.Use(New()) + app.Get("/", func(c *fiber.Ctx) error { + return c.SendStatus(fiber.StatusTeapot) + }) + + h := app.Handler() + ctx := &fasthttp.RequestCtx{} + + // Generate CSRF token + ctx.Request.Header.SetMethod(fiber.MethodGet) + b.ReportAllocs() + b.ResetTimer() + + for n := 0; n < b.N; n++ { + h(ctx) + } + + // Ensure the GET request returns a 418 status code + utils.AssertEqual(b, fiber.StatusTeapot, ctx.Response.Header.StatusCode()) +} diff --git a/middleware/csrf/extractors.go b/middleware/csrf/extractors.go new file mode 100644 index 0000000..a9260e8 --- /dev/null +++ b/middleware/csrf/extractors.go @@ -0,0 +1,70 @@ +package csrf + +import ( + "errors" + + "github.com/gofiber/fiber/v2" +) + +var ( + ErrMissingHeader = errors.New("missing csrf token in header") + ErrMissingQuery = errors.New("missing csrf token in query") + ErrMissingParam = errors.New("missing csrf token in param") + ErrMissingForm = errors.New("missing csrf token in form") + ErrMissingCookie = errors.New("missing csrf token in cookie") +) + +// csrfFromParam returns a function that extracts token from the url param string. +func CsrfFromParam(param string) func(c *fiber.Ctx) (string, error) { + return func(c *fiber.Ctx) (string, error) { + token := c.Params(param) + if token == "" { + return "", ErrMissingParam + } + return token, nil + } +} + +// csrfFromForm returns a function that extracts a token from a multipart-form. +func CsrfFromForm(param string) func(c *fiber.Ctx) (string, error) { + return func(c *fiber.Ctx) (string, error) { + token := c.FormValue(param) + if token == "" { + return "", ErrMissingForm + } + return token, nil + } +} + +// csrfFromCookie returns a function that extracts token from the cookie header. +func CsrfFromCookie(param string) func(c *fiber.Ctx) (string, error) { + return func(c *fiber.Ctx) (string, error) { + token := c.Cookies(param) + if token == "" { + return "", ErrMissingCookie + } + return token, nil + } +} + +// csrfFromHeader returns a function that extracts token from the request header. +func CsrfFromHeader(param string) func(c *fiber.Ctx) (string, error) { + return func(c *fiber.Ctx) (string, error) { + token := c.Get(param) + if token == "" { + return "", ErrMissingHeader + } + return token, nil + } +} + +// csrfFromQuery returns a function that extracts token from the query string. +func CsrfFromQuery(param string) func(c *fiber.Ctx) (string, error) { + return func(c *fiber.Ctx) (string, error) { + token := c.Query(param) + if token == "" { + return "", ErrMissingQuery + } + return token, nil + } +} diff --git a/middleware/csrf/helpers.go b/middleware/csrf/helpers.go new file mode 100644 index 0000000..708762c --- /dev/null +++ b/middleware/csrf/helpers.go @@ -0,0 +1,13 @@ +package csrf + +import ( + "crypto/subtle" +) + +func compareTokens(a, b []byte) bool { + return subtle.ConstantTimeCompare(a, b) == 1 +} + +func compareStrings(a, b string) bool { + return subtle.ConstantTimeCompare([]byte(a), []byte(b)) == 1 +} diff --git a/middleware/csrf/session_manager.go b/middleware/csrf/session_manager.go new file mode 100644 index 0000000..5497f8e --- /dev/null +++ b/middleware/csrf/session_manager.go @@ -0,0 +1,68 @@ +package csrf + +import ( + "time" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/log" + "github.com/gofiber/fiber/v2/middleware/session" +) + +type sessionManager struct { + key string + session *session.Store +} + +func newSessionManager(s *session.Store, k string) *sessionManager { + // Create new storage handler + sessionManager := &sessionManager{ + key: k, + } + if s != nil { + // Use provided storage if provided + sessionManager.session = s + } + return sessionManager +} + +// get token from session +func (m *sessionManager) getRaw(c *fiber.Ctx, key string, raw []byte) []byte { + sess, err := m.session.Get(c) + if err != nil { + return nil + } + token, ok := sess.Get(m.key).(Token) + if ok { + if token.Expiration.Before(time.Now()) || key != token.Key || !compareTokens(raw, token.Raw) { + return nil + } + return token.Raw + } + + return nil +} + +// set token in session +func (m *sessionManager) setRaw(c *fiber.Ctx, key string, raw []byte, exp time.Duration) { + sess, err := m.session.Get(c) + if err != nil { + return + } + // the key is crucial in crsf and sometimes a reference to another value which can be reused later(pool/unsafe values concept), so a copy is made here + sess.Set(m.key, &Token{key, raw, time.Now().Add(exp)}) + if err := sess.Save(); err != nil { + log.Warn("csrf: failed to save session: ", err) + } +} + +// delete token from session +func (m *sessionManager) delRaw(c *fiber.Ctx) { + sess, err := m.session.Get(c) + if err != nil { + return + } + sess.Delete(m.key) + if err := sess.Save(); err != nil { + log.Warn("csrf: failed to save session: ", err) + } +} diff --git a/middleware/csrf/storage_manager.go b/middleware/csrf/storage_manager.go new file mode 100644 index 0000000..49b3186 --- /dev/null +++ b/middleware/csrf/storage_manager.go @@ -0,0 +1,70 @@ +package csrf + +import ( + "sync" + "time" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/internal/memory" + "github.com/gofiber/fiber/v2/utils" +) + +// go:generate msgp +// msgp -file="storage_manager.go" -o="storage_manager_msgp.go" -tests=false -unexported +type item struct{} + +//msgp:ignore manager +type storageManager struct { + pool sync.Pool + memory *memory.Storage + storage fiber.Storage +} + +func newStorageManager(storage fiber.Storage) *storageManager { + // Create new storage handler + storageManager := &storageManager{ + pool: sync.Pool{ + New: func() interface{} { + return new(item) + }, + }, + } + if storage != nil { + // Use provided storage if provided + storageManager.storage = storage + } else { + // Fallback too memory storage + storageManager.memory = memory.New() + } + return storageManager +} + +// get raw data from storage or memory +func (m *storageManager) getRaw(key string) []byte { + var raw []byte + if m.storage != nil { + raw, _ = m.storage.Get(key) //nolint:errcheck // TODO: Do not ignore error + } else { + raw, _ = m.memory.Get(key).([]byte) //nolint:errcheck // TODO: Do not ignore error + } + return raw +} + +// set data to storage or memory +func (m *storageManager) setRaw(key string, raw []byte, exp time.Duration) { + if m.storage != nil { + _ = m.storage.Set(key, raw, exp) //nolint:errcheck // TODO: Do not ignore error + } else { + // the key is crucial in crsf and sometimes a reference to another value which can be reused later(pool/unsafe values concept), so a copy is made here + m.memory.Set(utils.CopyString(key), raw, exp) + } +} + +// delete data from storage or memory +func (m *storageManager) delRaw(key string) { + if m.storage != nil { + _ = m.storage.Delete(key) //nolint:errcheck // TODO: Do not ignore error + } else { + m.memory.Delete(key) + } +} diff --git a/middleware/csrf/storage_manager_msgp.go b/middleware/csrf/storage_manager_msgp.go new file mode 100644 index 0000000..337870b --- /dev/null +++ b/middleware/csrf/storage_manager_msgp.go @@ -0,0 +1,90 @@ +package csrf + +// Code generated by github.com/tinylib/msgp DO NOT EDIT. + +import ( + "github.com/tinylib/msgp/msgp" +) + +// DecodeMsg implements msgp.Decodable +func (z *item) DecodeMsg(dc *msgp.Reader) (err error) { + var field []byte + _ = field + var zb0001 uint32 + zb0001, err = dc.ReadMapHeader() + if err != nil { + err = msgp.WrapError(err) + return + } + for zb0001 > 0 { + zb0001-- + field, err = dc.ReadMapKeyPtr() + if err != nil { + err = msgp.WrapError(err) + return + } + switch msgp.UnsafeString(field) { + default: + err = dc.Skip() + if err != nil { + err = msgp.WrapError(err) + return + } + } + } + return +} + +// EncodeMsg implements msgp.Encodable +func (z item) EncodeMsg(en *msgp.Writer) (err error) { + // map header, size 0 + err = en.Append(0x80) + if err != nil { + return + } + return +} + +// MarshalMsg implements msgp.Marshaler +func (z item) MarshalMsg(b []byte) (o []byte, err error) { + o = msgp.Require(b, z.Msgsize()) + // map header, size 0 + o = append(o, 0x80) + return +} + +// UnmarshalMsg implements msgp.Unmarshaler +func (z *item) UnmarshalMsg(bts []byte) (o []byte, err error) { + var field []byte + _ = field + var zb0001 uint32 + zb0001, bts, err = msgp.ReadMapHeaderBytes(bts) + if err != nil { + err = msgp.WrapError(err) + return + } + for zb0001 > 0 { + zb0001-- + field, bts, err = msgp.ReadMapKeyZC(bts) + if err != nil { + err = msgp.WrapError(err) + return + } + switch msgp.UnsafeString(field) { + default: + bts, err = msgp.Skip(bts) + if err != nil { + err = msgp.WrapError(err) + return + } + } + } + o = bts + return +} + +// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message +func (z item) Msgsize() (s int) { + s = 1 + return +} diff --git a/middleware/csrf/token.go b/middleware/csrf/token.go new file mode 100644 index 0000000..ee88b9a --- /dev/null +++ b/middleware/csrf/token.go @@ -0,0 +1,11 @@ +package csrf + +import ( + "time" +) + +type Token struct { + Key string `json:"key"` + Raw []byte `json:"raw"` + Expiration time.Time `json:"expiration"` +} diff --git a/middleware/earlydata/config.go b/middleware/earlydata/config.go new file mode 100644 index 0000000..2fffa46 --- /dev/null +++ b/middleware/earlydata/config.go @@ -0,0 +1,73 @@ +package earlydata + +import ( + "github.com/gofiber/fiber/v2" +) + +const ( + DefaultHeaderName = "Early-Data" + DefaultHeaderTrueValue = "1" +) + +// Config defines the config for middleware. +type Config struct { + // Next defines a function to skip this middleware when returned true. + // + // Optional. Default: nil + Next func(c *fiber.Ctx) bool + + // IsEarlyData returns whether the request is an early-data request. + // + // Optional. Default: a function which checks if the "Early-Data" request header equals "1". + IsEarlyData func(c *fiber.Ctx) bool + + // AllowEarlyData returns whether the early-data request should be allowed or rejected. + // + // Optional. Default: a function which rejects the request on unsafe and allows the request on safe HTTP request methods. + AllowEarlyData func(c *fiber.Ctx) bool + + // Error is returned in case an early-data request is rejected. + // + // Optional. Default: fiber.ErrTooEarly. + Error error +} + +// ConfigDefault is the default config +var ConfigDefault = Config{ + IsEarlyData: func(c *fiber.Ctx) bool { + return c.Get(DefaultHeaderName) == DefaultHeaderTrueValue + }, + + AllowEarlyData: func(c *fiber.Ctx) bool { + return fiber.IsMethodSafe(c.Method()) + }, + + Error: fiber.ErrTooEarly, +} + +// Helper function to set default values +func configDefault(config ...Config) Config { + // Return default config if nothing provided + if len(config) < 1 { + return ConfigDefault + } + + // Override default config + cfg := config[0] + + // Set default values + + if cfg.IsEarlyData == nil { + cfg.IsEarlyData = ConfigDefault.IsEarlyData + } + + if cfg.AllowEarlyData == nil { + cfg.AllowEarlyData = ConfigDefault.AllowEarlyData + } + + if cfg.Error == nil { + cfg.Error = ConfigDefault.Error + } + + return cfg +} diff --git a/middleware/earlydata/earlydata.go b/middleware/earlydata/earlydata.go new file mode 100644 index 0000000..638db3c --- /dev/null +++ b/middleware/earlydata/earlydata.go @@ -0,0 +1,47 @@ +package earlydata + +import ( + "github.com/gofiber/fiber/v2" +) + +const ( + localsKeyAllowed = "earlydata_allowed" +) + +func IsEarly(c *fiber.Ctx) bool { + return c.Locals(localsKeyAllowed) != nil +} + +// New creates a new middleware handler +// https://datatracker.ietf.org/doc/html/rfc8470#section-5.1 +func New(config ...Config) fiber.Handler { + // Set default config + cfg := configDefault(config...) + + // Return new handler + return func(c *fiber.Ctx) error { + // Don't execute middleware if Next returns true + if cfg.Next != nil && cfg.Next(c) { + return c.Next() + } + + // Abort if we can't trust the early-data header + if !c.IsProxyTrusted() { + return cfg.Error + } + + // Continue stack if request is not an early-data request + if !cfg.IsEarlyData(c) { + return c.Next() + } + + // Continue stack if we allow early-data for this request + if cfg.AllowEarlyData(c) { + _ = c.Locals(localsKeyAllowed, true) + return c.Next() + } + + // Else return our error + return cfg.Error + } +} diff --git a/middleware/earlydata/earlydata_test.go b/middleware/earlydata/earlydata_test.go new file mode 100644 index 0000000..c4d9606 --- /dev/null +++ b/middleware/earlydata/earlydata_test.go @@ -0,0 +1,193 @@ +//nolint:bodyclose // Much easier to just ignore memory leaks in tests +package earlydata_test + +import ( + "errors" + "fmt" + "net/http" + "net/http/httptest" + "testing" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/earlydata" + "github.com/gofiber/fiber/v2/utils" +) + +const ( + headerName = "Early-Data" + headerValOn = "1" + headerValOff = "0" +) + +func appWithConfig(t *testing.T, c *fiber.Config) *fiber.App { + t.Helper() + t.Parallel() + + var app *fiber.App + if c == nil { + app = fiber.New() + } else { + app = fiber.New(*c) + } + + app.Use(earlydata.New()) + + // Middleware to test IsEarly func + const localsKeyTestValid = "earlydata_testvalid" + app.Use(func(c *fiber.Ctx) error { + isEarly := earlydata.IsEarly(c) + + switch h := c.Get(headerName); h { + case "", headerValOff: + if isEarly { + return errors.New("is early-data even though it's not") + } + + case headerValOn: + switch { + case fiber.IsMethodSafe(c.Method()): + if !isEarly { + return errors.New("should be early-data on safe HTTP methods") + } + default: + if isEarly { + return errors.New("early-data unsuported on unsafe HTTP methods") + } + } + + default: + return fmt.Errorf("header has unsupported value: %s", h) + } + + _ = c.Locals(localsKeyTestValid, true) + + return c.Next() + }) + + { + { + handler := func(c *fiber.Ctx) error { + if !c.Locals(localsKeyTestValid).(bool) { //nolint:forcetypeassert // We store nothing else in the pool + return errors.New("handler called even though validation failed") + } + + return nil + } + + app.Get("/", handler) + app.Post("/", handler) + } + } + + return app +} + +// go test -run Test_EarlyData +func Test_EarlyData(t *testing.T) { + t.Parallel() + + trustedRun := func(t *testing.T, app *fiber.App) { + t.Helper() + + { + req := httptest.NewRequest(fiber.MethodGet, "/", http.NoBody) + + resp, err := app.Test(req) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, fiber.StatusOK, resp.StatusCode) + + req.Header.Set(headerName, headerValOff) + resp, err = app.Test(req) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, fiber.StatusOK, resp.StatusCode) + + req.Header.Set(headerName, headerValOn) + resp, err = app.Test(req) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, fiber.StatusOK, resp.StatusCode) + } + + { + req := httptest.NewRequest(fiber.MethodPost, "/", http.NoBody) + + resp, err := app.Test(req) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, fiber.StatusOK, resp.StatusCode) + + req.Header.Set(headerName, headerValOff) + resp, err = app.Test(req) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, fiber.StatusOK, resp.StatusCode) + + req.Header.Set(headerName, headerValOn) + resp, err = app.Test(req) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, fiber.StatusTooEarly, resp.StatusCode) + } + } + + untrustedRun := func(t *testing.T, app *fiber.App) { + t.Helper() + + { + req := httptest.NewRequest(fiber.MethodGet, "/", http.NoBody) + + resp, err := app.Test(req) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, fiber.StatusTooEarly, resp.StatusCode) + + req.Header.Set(headerName, headerValOff) + resp, err = app.Test(req) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, fiber.StatusTooEarly, resp.StatusCode) + + req.Header.Set(headerName, headerValOn) + resp, err = app.Test(req) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, fiber.StatusTooEarly, resp.StatusCode) + } + + { + req := httptest.NewRequest(fiber.MethodPost, "/", http.NoBody) + + resp, err := app.Test(req) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, fiber.StatusTooEarly, resp.StatusCode) + + req.Header.Set(headerName, headerValOff) + resp, err = app.Test(req) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, fiber.StatusTooEarly, resp.StatusCode) + + req.Header.Set(headerName, headerValOn) + resp, err = app.Test(req) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, fiber.StatusTooEarly, resp.StatusCode) + } + } + + t.Run("empty config", func(t *testing.T) { + app := appWithConfig(t, nil) + trustedRun(t, app) + }) + t.Run("default config", func(t *testing.T) { + app := appWithConfig(t, &fiber.Config{}) + trustedRun(t, app) + }) + + t.Run("config with EnableTrustedProxyCheck", func(t *testing.T) { + app := appWithConfig(t, &fiber.Config{ + EnableTrustedProxyCheck: true, + }) + untrustedRun(t, app) + }) + t.Run("config with EnableTrustedProxyCheck and trusted TrustedProxies", func(t *testing.T) { + app := appWithConfig(t, &fiber.Config{ + EnableTrustedProxyCheck: true, + TrustedProxies: []string{ + "0.0.0.0", + }, + }) + trustedRun(t, app) + }) +} diff --git a/middleware/encryptcookie/config.go b/middleware/encryptcookie/config.go new file mode 100644 index 0000000..731ac07 --- /dev/null +++ b/middleware/encryptcookie/config.go @@ -0,0 +1,78 @@ +package encryptcookie + +import ( + "github.com/gofiber/fiber/v2" +) + +// Config defines the config for middleware. +type Config struct { + // Next defines a function to skip this middleware when returned true. + // + // Optional. Default: nil + Next func(c *fiber.Ctx) bool + + // Array of cookie keys that should not be encrypted. + // + // Optional. Default: [] + Except []string + + // Base64 encoded unique key to encode & decode cookies. + // + // Required. Key length should be 32 characters. + // You may use `encryptcookie.GenerateKey()` to generate a new key. + Key string + + // Custom function to encrypt cookies. + // + // Optional. Default: EncryptCookie + Encryptor func(decryptedString, key string) (string, error) + + // Custom function to decrypt cookies. + // + // Optional. Default: DecryptCookie + Decryptor func(encryptedString, key string) (string, error) +} + +// ConfigDefault is the default config +var ConfigDefault = Config{ + Next: nil, + Except: []string{}, + Key: "", + Encryptor: EncryptCookie, + Decryptor: DecryptCookie, +} + +// Helper function to set default values +func configDefault(config ...Config) Config { + // Set default config + cfg := ConfigDefault + + // Override config if provided + if len(config) > 0 { + cfg = config[0] + + // Set default values + + if cfg.Next == nil { + cfg.Next = ConfigDefault.Next + } + + if cfg.Except == nil { + cfg.Except = ConfigDefault.Except + } + + if cfg.Encryptor == nil { + cfg.Encryptor = ConfigDefault.Encryptor + } + + if cfg.Decryptor == nil { + cfg.Decryptor = ConfigDefault.Decryptor + } + } + + if cfg.Key == "" { + panic("fiber: encrypt cookie middleware requires key") + } + + return cfg +} diff --git a/middleware/encryptcookie/encryptcookie.go b/middleware/encryptcookie/encryptcookie.go new file mode 100644 index 0000000..96047a4 --- /dev/null +++ b/middleware/encryptcookie/encryptcookie.go @@ -0,0 +1,57 @@ +package encryptcookie + +import ( + "github.com/gofiber/fiber/v2" + + "github.com/valyala/fasthttp" +) + +// New creates a new middleware handler +func New(config ...Config) fiber.Handler { + // Set default config + cfg := configDefault(config...) + + // Return new handler + return func(c *fiber.Ctx) error { + // Don't execute middleware if Next returns true + if cfg.Next != nil && cfg.Next(c) { + return c.Next() + } + + // Decrypt request cookies + c.Request().Header.VisitAllCookie(func(key, value []byte) { + keyString := string(key) + if !isDisabled(keyString, cfg.Except) { + decryptedValue, err := cfg.Decryptor(string(value), cfg.Key) + if err != nil { + c.Request().Header.SetCookieBytesKV(key, nil) + } else { + c.Request().Header.SetCookie(string(key), decryptedValue) + } + } + }) + + // Continue stack + err := c.Next() + + // Encrypt response cookies + c.Response().Header.VisitAllCookie(func(key, value []byte) { + keyString := string(key) + if !isDisabled(keyString, cfg.Except) { + cookieValue := fasthttp.Cookie{} + cookieValue.SetKeyBytes(key) + if c.Response().Header.Cookie(&cookieValue) { + encryptedValue, err := cfg.Encryptor(string(cookieValue.Value()), cfg.Key) + if err != nil { + panic(err) + } + + cookieValue.SetValue(encryptedValue) + c.Response().Header.SetCookie(&cookieValue) + } + } + }) + + return err + } +} diff --git a/middleware/encryptcookie/encryptcookie_test.go b/middleware/encryptcookie/encryptcookie_test.go new file mode 100644 index 0000000..47d9889 --- /dev/null +++ b/middleware/encryptcookie/encryptcookie_test.go @@ -0,0 +1,192 @@ +package encryptcookie + +import ( + "encoding/base64" + "net/http/httptest" + "testing" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/utils" + + "github.com/valyala/fasthttp" +) + +var testKey = GenerateKey() + +func Test_Middleware_Encrypt_Cookie(t *testing.T) { + t.Parallel() + app := fiber.New() + + app.Use(New(Config{ + Key: testKey, + })) + + app.Get("/", func(c *fiber.Ctx) error { + return c.SendString("value=" + c.Cookies("test")) + }) + app.Post("/", func(c *fiber.Ctx) error { + c.Cookie(&fiber.Cookie{ + Name: "test", + Value: "SomeThing", + }) + return nil + }) + + h := app.Handler() + + // Test empty cookie + ctx := &fasthttp.RequestCtx{} + ctx.Request.Header.SetMethod(fiber.MethodGet) + h(ctx) + utils.AssertEqual(t, 200, ctx.Response.StatusCode()) + utils.AssertEqual(t, "value=", string(ctx.Response.Body())) + + // Test invalid cookie + ctx = &fasthttp.RequestCtx{} + ctx.Request.Header.SetMethod(fiber.MethodGet) + ctx.Request.Header.SetCookie("test", "Invalid") + h(ctx) + utils.AssertEqual(t, 200, ctx.Response.StatusCode()) + utils.AssertEqual(t, "value=", string(ctx.Response.Body())) + ctx.Request.Header.SetCookie("test", "ixQURE2XOyZUs0WAOh2ehjWcP7oZb07JvnhWOsmeNUhPsj4+RyI=") + h(ctx) + utils.AssertEqual(t, 200, ctx.Response.StatusCode()) + utils.AssertEqual(t, "value=", string(ctx.Response.Body())) + + // Test valid cookie + ctx = &fasthttp.RequestCtx{} + ctx.Request.Header.SetMethod(fiber.MethodPost) + h(ctx) + utils.AssertEqual(t, 200, ctx.Response.StatusCode()) + + encryptedCookie := fasthttp.Cookie{} + encryptedCookie.SetKey("test") + utils.AssertEqual(t, true, ctx.Response.Header.Cookie(&encryptedCookie), "Get cookie value") + decryptedCookieValue, err := DecryptCookie(string(encryptedCookie.Value()), testKey) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, "SomeThing", decryptedCookieValue) + + ctx = &fasthttp.RequestCtx{} + ctx.Request.Header.SetMethod(fiber.MethodGet) + ctx.Request.Header.SetCookie("test", string(encryptedCookie.Value())) + h(ctx) + utils.AssertEqual(t, 200, ctx.Response.StatusCode()) + utils.AssertEqual(t, "value=SomeThing", string(ctx.Response.Body())) +} + +func Test_Encrypt_Cookie_Next(t *testing.T) { + t.Parallel() + app := fiber.New() + + app.Use(New(Config{ + Key: testKey, + Next: func(_ *fiber.Ctx) bool { + return true + }, + })) + + app.Get("/", func(c *fiber.Ctx) error { + c.Cookie(&fiber.Cookie{ + Name: "test", + Value: "SomeThing", + }) + return nil + }) + + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, "SomeThing", resp.Cookies()[0].Value) +} + +func Test_Encrypt_Cookie_Except(t *testing.T) { + t.Parallel() + app := fiber.New() + + app.Use(New(Config{ + Key: testKey, + Except: []string{ + "test1", + }, + })) + + app.Get("/", func(c *fiber.Ctx) error { + c.Cookie(&fiber.Cookie{ + Name: "test1", + Value: "SomeThing", + }) + c.Cookie(&fiber.Cookie{ + Name: "test2", + Value: "SomeThing", + }) + + return nil + }) + + h := app.Handler() + + ctx := &fasthttp.RequestCtx{} + ctx.Request.Header.SetMethod(fiber.MethodGet) + h(ctx) + utils.AssertEqual(t, 200, ctx.Response.StatusCode()) + + rawCookie := fasthttp.Cookie{} + rawCookie.SetKey("test1") + utils.AssertEqual(t, true, ctx.Response.Header.Cookie(&rawCookie), "Get cookie value") + utils.AssertEqual(t, "SomeThing", string(rawCookie.Value())) + + encryptedCookie := fasthttp.Cookie{} + encryptedCookie.SetKey("test2") + utils.AssertEqual(t, true, ctx.Response.Header.Cookie(&encryptedCookie), "Get cookie value") + decryptedCookieValue, err := DecryptCookie(string(encryptedCookie.Value()), testKey) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, "SomeThing", decryptedCookieValue) +} + +func Test_Encrypt_Cookie_Custom_Encryptor(t *testing.T) { + t.Parallel() + app := fiber.New() + + app.Use(New(Config{ + Key: testKey, + Encryptor: func(decryptedString, _ string) (string, error) { + return base64.StdEncoding.EncodeToString([]byte(decryptedString)), nil + }, + Decryptor: func(encryptedString, _ string) (string, error) { + decodedBytes, err := base64.StdEncoding.DecodeString(encryptedString) + return string(decodedBytes), err + }, + })) + + app.Get("/", func(c *fiber.Ctx) error { + return c.SendString("value=" + c.Cookies("test")) + }) + app.Post("/", func(c *fiber.Ctx) error { + c.Cookie(&fiber.Cookie{ + Name: "test", + Value: "SomeThing", + }) + + return nil + }) + + h := app.Handler() + + ctx := &fasthttp.RequestCtx{} + ctx.Request.Header.SetMethod(fiber.MethodPost) + h(ctx) + utils.AssertEqual(t, 200, ctx.Response.StatusCode()) + + encryptedCookie := fasthttp.Cookie{} + encryptedCookie.SetKey("test") + utils.AssertEqual(t, true, ctx.Response.Header.Cookie(&encryptedCookie), "Get cookie value") + decodedBytes, err := base64.StdEncoding.DecodeString(string(encryptedCookie.Value())) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, "SomeThing", string(decodedBytes)) + + ctx = &fasthttp.RequestCtx{} + ctx.Request.Header.SetMethod(fiber.MethodGet) + ctx.Request.Header.SetCookie("test", string(encryptedCookie.Value())) + h(ctx) + utils.AssertEqual(t, 200, ctx.Response.StatusCode()) + utils.AssertEqual(t, "value=SomeThing", string(ctx.Response.Body())) +} diff --git a/middleware/encryptcookie/utils.go b/middleware/encryptcookie/utils.go new file mode 100644 index 0000000..c35064d --- /dev/null +++ b/middleware/encryptcookie/utils.go @@ -0,0 +1,98 @@ +package encryptcookie + +import ( + "crypto/aes" + "crypto/cipher" + "crypto/rand" + "encoding/base64" + "errors" + "fmt" + "io" +) + +// EncryptCookie Encrypts a cookie value with specific encryption key +func EncryptCookie(value, key string) (string, error) { + keyDecoded, err := base64.StdEncoding.DecodeString(key) + if err != nil { + return "", fmt.Errorf("failed to base64-decode key: %w", err) + } + + block, err := aes.NewCipher(keyDecoded) + if err != nil { + return "", fmt.Errorf("failed to create AES cipher: %w", err) + } + + gcm, err := cipher.NewGCM(block) + if err != nil { + return "", fmt.Errorf("failed to create GCM mode: %w", err) + } + + nonce := make([]byte, gcm.NonceSize()) + if _, err = io.ReadFull(rand.Reader, nonce); err != nil { + return "", fmt.Errorf("failed to read: %w", err) + } + + ciphertext := gcm.Seal(nonce, nonce, []byte(value), nil) + + return base64.StdEncoding.EncodeToString(ciphertext), nil +} + +// DecryptCookie Decrypts a cookie value with specific encryption key +func DecryptCookie(value, key string) (string, error) { + keyDecoded, err := base64.StdEncoding.DecodeString(key) + if err != nil { + return "", fmt.Errorf("failed to base64-decode key: %w", err) + } + enc, err := base64.StdEncoding.DecodeString(value) + if err != nil { + return "", fmt.Errorf("failed to base64-decode value: %w", err) + } + + block, err := aes.NewCipher(keyDecoded) + if err != nil { + return "", fmt.Errorf("failed to create AES cipher: %w", err) + } + + gcm, err := cipher.NewGCM(block) + if err != nil { + return "", fmt.Errorf("failed to create GCM mode: %w", err) + } + + nonceSize := gcm.NonceSize() + + if len(enc) < nonceSize { + return "", errors.New("encrypted value is not valid") + } + + nonce, ciphertext := enc[:nonceSize], enc[nonceSize:] + + plaintext, err := gcm.Open(nil, nonce, ciphertext, nil) + if err != nil { + return "", fmt.Errorf("failed to decrypt ciphertext: %w", err) + } + + return string(plaintext), nil +} + +// GenerateKey Generates an encryption key +func GenerateKey() string { + const keyLen = 32 + ret := make([]byte, keyLen) + + if _, err := rand.Read(ret); err != nil { + panic(err) + } + + return base64.StdEncoding.EncodeToString(ret) +} + +// Check given cookie key is disabled for encryption or not +func isDisabled(key string, except []string) bool { + for _, k := range except { + if key == k { + return true + } + } + + return false +} diff --git a/middleware/envvar/envvar.go b/middleware/envvar/envvar.go new file mode 100644 index 0000000..debfc1b --- /dev/null +++ b/middleware/envvar/envvar.go @@ -0,0 +1,68 @@ +package envvar + +import ( + "os" + "strings" + + "github.com/gofiber/fiber/v2" +) + +// Config defines the config for middleware. +type Config struct { + // ExportVars specifies the environment variables that should export + ExportVars map[string]string + // ExcludeVars specifies the environment variables that should not export + ExcludeVars map[string]string +} + +type EnvVar struct { + Vars map[string]string `json:"vars"` +} + +func (envVar *EnvVar) set(key, val string) { + envVar.Vars[key] = val +} + +func New(config ...Config) fiber.Handler { + var cfg Config + if len(config) > 0 { + cfg = config[0] + } + + return func(c *fiber.Ctx) error { + if c.Method() != fiber.MethodGet { + return fiber.ErrMethodNotAllowed + } + + envVar := newEnvVar(cfg) + varsByte, err := c.App().Config().JSONEncoder(envVar) + if err != nil { + return c.Status(fiber.StatusInternalServerError).SendString(err.Error()) + } + c.Set(fiber.HeaderContentType, fiber.MIMEApplicationJSONCharsetUTF8) + return c.Send(varsByte) + } +} + +func newEnvVar(cfg Config) *EnvVar { + vars := &EnvVar{Vars: make(map[string]string)} + + if len(cfg.ExportVars) > 0 { + for key, defaultVal := range cfg.ExportVars { + vars.set(key, defaultVal) + if envVal, exists := os.LookupEnv(key); exists { + vars.set(key, envVal) + } + } + } else { + const numElems = 2 + for _, envVal := range os.Environ() { + keyVal := strings.SplitN(envVal, "=", numElems) + if _, exists := cfg.ExcludeVars[keyVal[0]]; !exists { + vars.set(keyVal[0], keyVal[1]) + } + } + } + + return vars +} diff --git a/middleware/envvar/envvar_test.go b/middleware/envvar/envvar_test.go new file mode 100644 index 0000000..83101a4 --- /dev/null +++ b/middleware/envvar/envvar_test.go @@ -0,0 +1,172 @@ +//nolint:bodyclose // Much easier to just ignore memory leaks in tests +package envvar + +import ( + "context" + "encoding/json" + "io" + "net/http" + "os" + "testing" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/utils" +) + +func TestEnvVarStructWithExportVarsExcludeVars(t *testing.T) { + err := os.Setenv("testKey", "testEnvValue") + utils.AssertEqual(t, nil, err) + err = os.Setenv("anotherEnvKey", "anotherEnvVal") + utils.AssertEqual(t, nil, err) + err = os.Setenv("excludeKey", "excludeEnvValue") + utils.AssertEqual(t, nil, err) + defer func() { + err := os.Unsetenv("testKey") + utils.AssertEqual(t, nil, err) + err = os.Unsetenv("anotherEnvKey") + utils.AssertEqual(t, nil, err) + err = os.Unsetenv("excludeKey") + utils.AssertEqual(t, nil, err) + }() + + vars := newEnvVar(Config{ + ExportVars: map[string]string{"testKey": "", "testDefaultKey": "testDefaultVal"}, + ExcludeVars: map[string]string{"excludeKey": ""}, + }) + + utils.AssertEqual(t, vars.Vars["testKey"], "testEnvValue") + utils.AssertEqual(t, vars.Vars["testDefaultKey"], "testDefaultVal") + utils.AssertEqual(t, vars.Vars["excludeKey"], "") + utils.AssertEqual(t, vars.Vars["anotherEnvKey"], "") +} + +func TestEnvVarHandler(t *testing.T) { + err := os.Setenv("testKey", "testVal") + utils.AssertEqual(t, nil, err) + defer func() { + err := os.Unsetenv("testKey") + utils.AssertEqual(t, nil, err) + }() + + expectedEnvVarResponse, err := json.Marshal( + struct { + Vars map[string]string `json:"vars"` + }{ + map[string]string{"testKey": "testVal"}, + }) + utils.AssertEqual(t, nil, err) + + app := fiber.New() + app.Use("/envvars", New(Config{ + ExportVars: map[string]string{"testKey": ""}, + })) + + req, err := http.NewRequestWithContext(context.Background(), fiber.MethodGet, "http://localhost/envvars", nil) + utils.AssertEqual(t, nil, err) + resp, err := app.Test(req) + utils.AssertEqual(t, nil, err) + + respBody, err := io.ReadAll(resp.Body) + utils.AssertEqual(t, nil, err) + + utils.AssertEqual(t, expectedEnvVarResponse, respBody) +} + +func TestEnvVarHandlerNotMatched(t *testing.T) { + app := fiber.New() + app.Use("/envvars", New(Config{ + ExportVars: map[string]string{"testKey": ""}, + })) + + app.Get("/another-path", func(ctx *fiber.Ctx) error { + utils.AssertEqual(t, nil, ctx.SendString("OK")) + return nil + }) + + req, err := http.NewRequestWithContext(context.Background(), fiber.MethodGet, "http://localhost/another-path", nil) + utils.AssertEqual(t, nil, err) + resp, err := app.Test(req) + utils.AssertEqual(t, nil, err) + + respBody, err := io.ReadAll(resp.Body) + utils.AssertEqual(t, nil, err) + + utils.AssertEqual(t, []byte("OK"), respBody) +} + +func TestEnvVarHandlerDefaultConfig(t *testing.T) { + err := os.Setenv("testEnvKey", "testEnvVal") + utils.AssertEqual(t, nil, err) + defer func() { + err := os.Unsetenv("testEnvKey") + utils.AssertEqual(t, nil, err) + }() + + app := fiber.New() + app.Use("/envvars", New()) + + req, err := http.NewRequestWithContext(context.Background(), fiber.MethodGet, "http://localhost/envvars", nil) + utils.AssertEqual(t, nil, err) + resp, err := app.Test(req) + utils.AssertEqual(t, nil, err) + + respBody, err := io.ReadAll(resp.Body) + utils.AssertEqual(t, nil, err) + + var envVars EnvVar + utils.AssertEqual(t, nil, json.Unmarshal(respBody, &envVars)) + val := envVars.Vars["testEnvKey"] + utils.AssertEqual(t, "testEnvVal", val) +} + +func TestEnvVarHandlerMethod(t *testing.T) { + app := fiber.New() + app.Use("/envvars", New()) + + req, err := http.NewRequestWithContext(context.Background(), fiber.MethodPost, "http://localhost/envvars", nil) + utils.AssertEqual(t, nil, err) + resp, err := app.Test(req) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, fiber.StatusMethodNotAllowed, resp.StatusCode) +} + +func TestEnvVarHandlerSpecialValue(t *testing.T) { + testEnvKey := "testEnvKey" + fakeBase64 := "testBase64:TQ==" + err := os.Setenv(testEnvKey, fakeBase64) + utils.AssertEqual(t, nil, err) + defer func() { + err := os.Unsetenv(testEnvKey) + utils.AssertEqual(t, nil, err) + }() + + app := fiber.New() + app.Use("/envvars", New()) + app.Use("/envvars/export", New(Config{ExportVars: map[string]string{testEnvKey: ""}})) + + req, err := http.NewRequestWithContext(context.Background(), fiber.MethodGet, "http://localhost/envvars", nil) + utils.AssertEqual(t, nil, err) + resp, err := app.Test(req) + utils.AssertEqual(t, nil, err) + + respBody, err := io.ReadAll(resp.Body) + utils.AssertEqual(t, nil, err) + + var envVars EnvVar + utils.AssertEqual(t, nil, json.Unmarshal(respBody, &envVars)) + val := envVars.Vars[testEnvKey] + utils.AssertEqual(t, fakeBase64, val) + + req, err = http.NewRequestWithContext(context.Background(), fiber.MethodGet, "http://localhost/envvars/export", nil) + utils.AssertEqual(t, nil, err) + resp, err = app.Test(req) + utils.AssertEqual(t, nil, err) + + respBody, err = io.ReadAll(resp.Body) + utils.AssertEqual(t, nil, err) + + var envVarsExport EnvVar + utils.AssertEqual(t, nil, json.Unmarshal(respBody, &envVarsExport)) + val = envVarsExport.Vars[testEnvKey] + utils.AssertEqual(t, fakeBase64, val) +} diff --git a/middleware/etag/config.go b/middleware/etag/config.go new file mode 100644 index 0000000..57a7c78 --- /dev/null +++ b/middleware/etag/config.go @@ -0,0 +1,44 @@ +package etag + +import ( + "github.com/gofiber/fiber/v2" +) + +// Config defines the config for middleware. +type Config struct { + // Weak indicates that a weak validator is used. Weak etags are easy + // to generate, but are far less useful for comparisons. Strong + // validators are ideal for comparisons but can be very difficult + // to generate efficiently. Weak ETag values of two representations + // of the same resources might be semantically equivalent, but not + // byte-for-byte identical. This means weak etags prevent caching + // when byte range requests are used, but strong etags mean range + // requests can still be cached. + Weak bool + + // Next defines a function to skip this middleware when returned true. + // + // Optional. Default: nil + Next func(c *fiber.Ctx) bool +} + +// ConfigDefault is the default config +var ConfigDefault = Config{ + Weak: false, + Next: nil, +} + +// Helper function to set default values +func configDefault(config ...Config) Config { + // Return default config if nothing provided + if len(config) < 1 { + return ConfigDefault + } + + // Override default config + cfg := config[0] + + // Set default values + + return cfg +} diff --git a/middleware/etag/etag.go b/middleware/etag/etag.go new file mode 100644 index 0000000..13148fc --- /dev/null +++ b/middleware/etag/etag.go @@ -0,0 +1,116 @@ +package etag + +import ( + "bytes" + "hash/crc32" + + "github.com/gofiber/fiber/v2" + + "github.com/valyala/bytebufferpool" +) + +// New creates a new middleware handler +func New(config ...Config) fiber.Handler { + // Set default config + cfg := configDefault(config...) + + var ( + normalizedHeaderETag = []byte("Etag") + weakPrefix = []byte("W/") + ) + + const crcPol = 0xD5828281 + crc32q := crc32.MakeTable(crcPol) + + // Return new handler + return func(c *fiber.Ctx) error { + // Don't execute middleware if Next returns true + if cfg.Next != nil && cfg.Next(c) { + return c.Next() + } + + // Return err if next handler returns one + if err := c.Next(); err != nil { + return err + } + + // Don't generate ETags for invalid responses + if c.Response().StatusCode() != fiber.StatusOK { + return nil + } + body := c.Response().Body() + // Skips ETag if no response body is present + if len(body) == 0 { + return nil + } + // Skip ETag if header is already present + if c.Response().Header.PeekBytes(normalizedHeaderETag) != nil { + return nil + } + + // Generate ETag for response + bb := bytebufferpool.Get() + defer bytebufferpool.Put(bb) + + // Enable weak tag + if cfg.Weak { + _, _ = bb.Write(weakPrefix) //nolint:errcheck // This will never fail + } + + _ = bb.WriteByte('"') //nolint:errcheck // This will never fail + bb.B = appendUint(bb.Bytes(), uint32(len(body))) + _ = bb.WriteByte('-') //nolint:errcheck // This will never fail + bb.B = appendUint(bb.Bytes(), crc32.Checksum(body, crc32q)) + _ = bb.WriteByte('"') //nolint:errcheck // This will never fail + + etag := bb.Bytes() + + // Get ETag header from request + clientEtag := c.Request().Header.Peek(fiber.HeaderIfNoneMatch) + + // Check if client's ETag is weak + if bytes.HasPrefix(clientEtag, weakPrefix) { + // Check if server's ETag is weak + if bytes.Equal(clientEtag[2:], etag) || bytes.Equal(clientEtag[2:], etag[2:]) { + // W/1 == 1 || W/1 == W/1 + c.Context().ResetBody() + + return c.SendStatus(fiber.StatusNotModified) + } + // W/1 != W/2 || W/1 != 2 + c.Response().Header.SetCanonical(normalizedHeaderETag, etag) + + return nil + } + + if bytes.Contains(clientEtag, etag) { + // 1 == 1 + c.Context().ResetBody() + + return c.SendStatus(fiber.StatusNotModified) + } + // 1 != 2 + c.Response().Header.SetCanonical(normalizedHeaderETag, etag) + + return nil + } +} + +// appendUint appends n to dst and returns the extended dst. +func appendUint(dst []byte, n uint32) []byte { + var b [20]byte + buf := b[:] + i := len(buf) + var q uint32 + for n >= 10 { + i-- + q = n / 10 + buf[i] = '0' + byte(n-q*10) + n = q + } + i-- + buf[i] = '0' + byte(n) + + dst = append(dst, buf[i:]...) + return dst +} diff --git a/middleware/etag/etag_test.go b/middleware/etag/etag_test.go new file mode 100644 index 0000000..ff9833f --- /dev/null +++ b/middleware/etag/etag_test.go @@ -0,0 +1,291 @@ +package etag + +import ( + "bytes" + "io" + "net/http/httptest" + "testing" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/utils" + + "github.com/valyala/fasthttp" +) + +// go test -run Test_ETag_Next +func Test_ETag_Next(t *testing.T) { + t.Parallel() + app := fiber.New() + app.Use(New(Config{ + Next: func(_ *fiber.Ctx) bool { + return true + }, + })) + + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, fiber.StatusNotFound, resp.StatusCode) +} + +// go test -run Test_ETag_SkipError +func Test_ETag_SkipError(t *testing.T) { + t.Parallel() + app := fiber.New() + + app.Use(New()) + + app.Get("/", func(c *fiber.Ctx) error { + return fiber.ErrForbidden + }) + + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, fiber.StatusForbidden, resp.StatusCode) +} + +// go test -run Test_ETag_NotStatusOK +func Test_ETag_NotStatusOK(t *testing.T) { + t.Parallel() + app := fiber.New() + + app.Use(New()) + + app.Get("/", func(c *fiber.Ctx) error { + return c.SendStatus(fiber.StatusCreated) + }) + + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, fiber.StatusCreated, resp.StatusCode) +} + +// go test -run Test_ETag_NoBody +func Test_ETag_NoBody(t *testing.T) { + t.Parallel() + app := fiber.New() + + app.Use(New()) + + app.Get("/", func(c *fiber.Ctx) error { + return nil + }) + + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, fiber.StatusOK, resp.StatusCode) +} + +// go test -run Test_ETag_NewEtag +func Test_ETag_NewEtag(t *testing.T) { + t.Parallel() + t.Run("without HeaderIfNoneMatch", func(t *testing.T) { + t.Parallel() + testETagNewEtag(t, false, false) + }) + t.Run("with HeaderIfNoneMatch and not matched", func(t *testing.T) { + t.Parallel() + testETagNewEtag(t, true, false) + }) + t.Run("with HeaderIfNoneMatch and matched", func(t *testing.T) { + t.Parallel() + testETagNewEtag(t, true, true) + }) +} + +func testETagNewEtag(t *testing.T, headerIfNoneMatch, matched bool) { //nolint:revive // We're in a test, so using bools as a flow-control is fine + t.Helper() + + app := fiber.New() + + app.Use(New()) + + app.Get("/", func(c *fiber.Ctx) error { + return c.SendString("Hello, World!") + }) + + req := httptest.NewRequest(fiber.MethodGet, "/", nil) + if headerIfNoneMatch { + etag := `"non-match"` + if matched { + etag = `"13-1831710635"` + } + req.Header.Set(fiber.HeaderIfNoneMatch, etag) + } + + resp, err := app.Test(req) + utils.AssertEqual(t, nil, err) + + if !headerIfNoneMatch || !matched { + utils.AssertEqual(t, fiber.StatusOK, resp.StatusCode) + utils.AssertEqual(t, `"13-1831710635"`, resp.Header.Get(fiber.HeaderETag)) + return + } + + if matched { + utils.AssertEqual(t, fiber.StatusNotModified, resp.StatusCode) + b, err := io.ReadAll(resp.Body) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, 0, len(b)) + } +} + +// go test -run Test_ETag_WeakEtag +func Test_ETag_WeakEtag(t *testing.T) { + t.Parallel() + t.Run("without HeaderIfNoneMatch", func(t *testing.T) { + t.Parallel() + testETagWeakEtag(t, false, false) + }) + t.Run("with HeaderIfNoneMatch and not matched", func(t *testing.T) { + t.Parallel() + testETagWeakEtag(t, true, false) + }) + t.Run("with HeaderIfNoneMatch and matched", func(t *testing.T) { + t.Parallel() + testETagWeakEtag(t, true, true) + }) +} + +func testETagWeakEtag(t *testing.T, headerIfNoneMatch, matched bool) { //nolint:revive // We're in a test, so using bools as a flow-control is fine + t.Helper() + + app := fiber.New() + + app.Use(New(Config{Weak: true})) + + app.Get("/", func(c *fiber.Ctx) error { + return c.SendString("Hello, World!") + }) + + req := httptest.NewRequest(fiber.MethodGet, "/", nil) + if headerIfNoneMatch { + etag := `W/"non-match"` + if matched { + etag = `W/"13-1831710635"` + } + req.Header.Set(fiber.HeaderIfNoneMatch, etag) + } + + resp, err := app.Test(req) + utils.AssertEqual(t, nil, err) + + if !headerIfNoneMatch || !matched { + utils.AssertEqual(t, fiber.StatusOK, resp.StatusCode) + utils.AssertEqual(t, `W/"13-1831710635"`, resp.Header.Get(fiber.HeaderETag)) + return + } + + if matched { + utils.AssertEqual(t, fiber.StatusNotModified, resp.StatusCode) + b, err := io.ReadAll(resp.Body) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, 0, len(b)) + } +} + +// go test -run Test_ETag_CustomEtag +func Test_ETag_CustomEtag(t *testing.T) { + t.Parallel() + t.Run("without HeaderIfNoneMatch", func(t *testing.T) { + t.Parallel() + testETagCustomEtag(t, false, false) + }) + t.Run("with HeaderIfNoneMatch and not matched", func(t *testing.T) { + t.Parallel() + testETagCustomEtag(t, true, false) + }) + t.Run("with HeaderIfNoneMatch and matched", func(t *testing.T) { + t.Parallel() + testETagCustomEtag(t, true, true) + }) +} + +func testETagCustomEtag(t *testing.T, headerIfNoneMatch, matched bool) { //nolint:revive // We're in a test, so using bools as a flow-control is fine + t.Helper() + + app := fiber.New() + + app.Use(New()) + + app.Get("/", func(c *fiber.Ctx) error { + c.Set(fiber.HeaderETag, `"custom"`) + if bytes.Equal(c.Request().Header.Peek(fiber.HeaderIfNoneMatch), []byte(`"custom"`)) { + return c.SendStatus(fiber.StatusNotModified) + } + return c.SendString("Hello, World!") + }) + + req := httptest.NewRequest(fiber.MethodGet, "/", nil) + if headerIfNoneMatch { + etag := `"non-match"` + if matched { + etag = `"custom"` + } + req.Header.Set(fiber.HeaderIfNoneMatch, etag) + } + + resp, err := app.Test(req) + utils.AssertEqual(t, nil, err) + + if !headerIfNoneMatch || !matched { + utils.AssertEqual(t, fiber.StatusOK, resp.StatusCode) + utils.AssertEqual(t, `"custom"`, resp.Header.Get(fiber.HeaderETag)) + return + } + + if matched { + utils.AssertEqual(t, fiber.StatusNotModified, resp.StatusCode) + b, err := io.ReadAll(resp.Body) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, 0, len(b)) + } +} + +// go test -run Test_ETag_CustomEtagPut +func Test_ETag_CustomEtagPut(t *testing.T) { + t.Parallel() + app := fiber.New() + + app.Use(New()) + + app.Put("/", func(c *fiber.Ctx) error { + c.Set(fiber.HeaderETag, `"custom"`) + if !bytes.Equal(c.Request().Header.Peek(fiber.HeaderIfMatch), []byte(`"custom"`)) { + return c.SendStatus(fiber.StatusPreconditionFailed) + } + return c.SendString("Hello, World!") + }) + + req := httptest.NewRequest(fiber.MethodPut, "/", nil) + req.Header.Set(fiber.HeaderIfMatch, `"non-match"`) + resp, err := app.Test(req) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, fiber.StatusPreconditionFailed, resp.StatusCode) +} + +// go test -v -run=^$ -bench=Benchmark_Etag -benchmem -count=4 +func Benchmark_Etag(b *testing.B) { + app := fiber.New() + + app.Use(New()) + + app.Get("/", func(c *fiber.Ctx) error { + return c.SendString("Hello, World!") + }) + + h := app.Handler() + + fctx := &fasthttp.RequestCtx{} + fctx.Request.Header.SetMethod(fiber.MethodGet) + fctx.Request.SetRequestURI("/") + + b.ReportAllocs() + b.ResetTimer() + + for n := 0; n < b.N; n++ { + h(fctx) + } + + utils.AssertEqual(b, 200, fctx.Response.Header.StatusCode()) + utils.AssertEqual(b, `"13-1831710635"`, string(fctx.Response.Header.Peek(fiber.HeaderETag))) +} diff --git a/middleware/expvar/config.go b/middleware/expvar/config.go new file mode 100644 index 0000000..8d9caa4 --- /dev/null +++ b/middleware/expvar/config.go @@ -0,0 +1,34 @@ +package expvar + +import ( + "github.com/gofiber/fiber/v2" +) + +// Config defines the config for middleware. +type Config struct { + // Next defines a function to skip this middleware when returned true. + // + // Optional. Default: nil + Next func(c *fiber.Ctx) bool +} + +var ConfigDefault = Config{ + Next: nil, +} + +func configDefault(config ...Config) Config { + // Return default config if nothing provided + if len(config) < 1 { + return ConfigDefault + } + + // Override default config + cfg := config[0] + + // Set default values + if cfg.Next == nil { + cfg.Next = ConfigDefault.Next + } + + return cfg +} diff --git a/middleware/expvar/expvar.go b/middleware/expvar/expvar.go new file mode 100644 index 0000000..6436395 --- /dev/null +++ b/middleware/expvar/expvar.go @@ -0,0 +1,35 @@ +package expvar + +import ( + "strings" + + "github.com/gofiber/fiber/v2" + + "github.com/valyala/fasthttp/expvarhandler" +) + +// New creates a new middleware handler +func New(config ...Config) fiber.Handler { + // Set default config + cfg := configDefault(config...) + + // Return new handler + return func(c *fiber.Ctx) error { + // Don't execute middleware if Next returns true + if cfg.Next != nil && cfg.Next(c) { + return c.Next() + } + + path := c.Path() + // We are only interested in /debug/vars routes + if len(path) < 11 || !strings.HasPrefix(path, "/debug/vars") { + return c.Next() + } + if path == "/debug/vars" { + expvarhandler.ExpvarHandler(c.Context()) + return nil + } + + return c.Redirect("/debug/vars", fiber.StatusFound) + } +} diff --git a/middleware/expvar/expvar_test.go b/middleware/expvar/expvar_test.go new file mode 100644 index 0000000..3306952 --- /dev/null +++ b/middleware/expvar/expvar_test.go @@ -0,0 +1,103 @@ +package expvar + +import ( + "bytes" + "io" + "net/http/httptest" + "testing" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/utils" +) + +func Test_Non_Expvar_Path(t *testing.T) { + t.Parallel() + app := fiber.New() + + app.Use(New()) + + app.Get("/", func(c *fiber.Ctx) error { + return c.SendString("escaped") + }) + + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, 200, resp.StatusCode) + + b, err := io.ReadAll(resp.Body) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, "escaped", string(b)) +} + +func Test_Expvar_Index(t *testing.T) { + t.Parallel() + app := fiber.New() + + app.Use(New()) + + app.Get("/", func(c *fiber.Ctx) error { + return c.SendString("escaped") + }) + + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/debug/vars", nil)) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, 200, resp.StatusCode) + utils.AssertEqual(t, fiber.MIMEApplicationJSONCharsetUTF8, resp.Header.Get(fiber.HeaderContentType)) + + b, err := io.ReadAll(resp.Body) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, true, bytes.Contains(b, []byte("cmdline"))) + utils.AssertEqual(t, true, bytes.Contains(b, []byte("memstat"))) +} + +func Test_Expvar_Filter(t *testing.T) { + t.Parallel() + app := fiber.New() + + app.Use(New()) + + app.Get("/", func(c *fiber.Ctx) error { + return c.SendString("escaped") + }) + + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/debug/vars?r=cmd", nil)) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, 200, resp.StatusCode) + utils.AssertEqual(t, fiber.MIMEApplicationJSONCharsetUTF8, resp.Header.Get(fiber.HeaderContentType)) + + b, err := io.ReadAll(resp.Body) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, true, bytes.Contains(b, []byte("cmdline"))) + utils.AssertEqual(t, false, bytes.Contains(b, []byte("memstat"))) +} + +func Test_Expvar_Other_Path(t *testing.T) { + t.Parallel() + app := fiber.New() + + app.Use(New()) + + app.Get("/", func(c *fiber.Ctx) error { + return c.SendString("escaped") + }) + + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/debug/vars/302", nil)) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, 302, resp.StatusCode) +} + +// go test -run Test_Expvar_Next +func Test_Expvar_Next(t *testing.T) { + t.Parallel() + app := fiber.New() + + app.Use(New(Config{ + Next: func(_ *fiber.Ctx) bool { + return true + }, + })) + + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/debug/vars", nil)) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, 404, resp.StatusCode) +} diff --git a/middleware/favicon/favicon.go b/middleware/favicon/favicon.go new file mode 100644 index 0000000..4575289 --- /dev/null +++ b/middleware/favicon/favicon.go @@ -0,0 +1,146 @@ +package favicon + +import ( + "io" + "net/http" + "os" + "strconv" + + "github.com/gofiber/fiber/v2" +) + +// Config defines the config for middleware. +type Config struct { + // Next defines a function to skip this middleware when returned true. + // + // Optional. Default: nil + Next func(c *fiber.Ctx) bool + + // Raw data of the favicon file + // + // Optional. Default: nil + Data []byte `json:"-"` + + // File holds the path to an actual favicon that will be cached + // + // Optional. Default: "" + File string `json:"file"` + + // URL for favicon handler + // + // Optional. Default: "/favicon.ico" + URL string `json:"url"` + + // FileSystem is an optional alternate filesystem to search for the favicon in. + // An example of this could be an embedded or network filesystem + // + // Optional. Default: nil + FileSystem http.FileSystem `json:"-"` + + // CacheControl defines how the Cache-Control header in the response should be set + // + // Optional. Default: "public, max-age=31536000" + CacheControl string `json:"cache_control"` +} + +// ConfigDefault is the default config +var ConfigDefault = Config{ + Next: nil, + File: "", + URL: fPath, + CacheControl: "public, max-age=31536000", +} + +const ( + fPath = "/favicon.ico" + hType = "image/x-icon" + hAllow = "GET, HEAD, OPTIONS" + hZero = "0" +) + +// New creates a new middleware handler +func New(config ...Config) fiber.Handler { + // Set default config + cfg := ConfigDefault + + // Override config if provided + if len(config) > 0 { + cfg = config[0] + + // Set default values + if cfg.Next == nil { + cfg.Next = ConfigDefault.Next + } + if cfg.URL == "" { + cfg.URL = ConfigDefault.URL + } + if cfg.File == "" { + cfg.File = ConfigDefault.File + } + if cfg.CacheControl == "" { + cfg.CacheControl = ConfigDefault.CacheControl + } + } + + // Load icon if provided + var ( + err error + icon []byte + iconLen string + ) + if cfg.Data != nil { + // use the provided favicon data + icon = cfg.Data + iconLen = strconv.Itoa(len(cfg.Data)) + } else if cfg.File != "" { + // read from configured filesystem if present + if cfg.FileSystem != nil { + f, err := cfg.FileSystem.Open(cfg.File) + if err != nil { + panic(err) + } + if icon, err = io.ReadAll(f); err != nil { + panic(err) + } + } else if icon, err = os.ReadFile(cfg.File); err != nil { + panic(err) + } + + iconLen = strconv.Itoa(len(icon)) + } + + // Return new handler + return func(c *fiber.Ctx) error { + // Don't execute middleware if Next returns true + if cfg.Next != nil && cfg.Next(c) { + return c.Next() + } + + // Only respond to favicon requests + if c.Path() != cfg.URL { + return c.Next() + } + + // Only allow GET, HEAD and OPTIONS requests + if c.Method() != fiber.MethodGet && c.Method() != fiber.MethodHead { + if c.Method() != fiber.MethodOptions { + c.Status(fiber.StatusMethodNotAllowed) + } else { + c.Status(fiber.StatusOK) + } + c.Set(fiber.HeaderAllow, hAllow) + c.Set(fiber.HeaderContentLength, hZero) + return nil + } + + // Serve cached favicon + if len(icon) > 0 { + c.Set(fiber.HeaderContentLength, iconLen) + c.Set(fiber.HeaderContentType, hType) + c.Set(fiber.HeaderCacheControl, cfg.CacheControl) + return c.Status(fiber.StatusOK).Send(icon) + } + + return c.SendStatus(fiber.StatusNoContent) + } +} diff --git a/middleware/favicon/favicon_test.go b/middleware/favicon/favicon_test.go new file mode 100644 index 0000000..f7837e3 --- /dev/null +++ b/middleware/favicon/favicon_test.go @@ -0,0 +1,208 @@ +//nolint:bodyclose // Much easier to just ignore memory leaks in tests +package favicon + +import ( + "fmt" + "net/http" + "net/http/httptest" + "os" + "strings" + "testing" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/utils" + + "github.com/valyala/fasthttp" +) + +// go test -run Test_Middleware_Favicon +func Test_Middleware_Favicon(t *testing.T) { + t.Parallel() + app := fiber.New() + + app.Use(New()) + + app.Get("/", func(c *fiber.Ctx) error { + return nil + }) + + // Skip Favicon middleware + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, fiber.StatusOK, resp.StatusCode, "Status code") + + resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/favicon.ico", nil)) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, fiber.StatusNoContent, resp.StatusCode, "Status code") + + resp, err = app.Test(httptest.NewRequest(fiber.MethodOptions, "/favicon.ico", nil)) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, fiber.StatusOK, resp.StatusCode, "Status code") + + resp, err = app.Test(httptest.NewRequest(fiber.MethodPut, "/favicon.ico", nil)) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, fiber.StatusMethodNotAllowed, resp.StatusCode, "Status code") + utils.AssertEqual(t, strings.Join([]string{fiber.MethodGet, fiber.MethodHead, fiber.MethodOptions}, ", "), resp.Header.Get(fiber.HeaderAllow)) +} + +// go test -run Test_Middleware_Favicon_Not_Found +func Test_Middleware_Favicon_Not_Found(t *testing.T) { + t.Parallel() + defer func() { + if err := recover(); err == nil { + t.Fatal("should cache panic") + } + }() + + fiber.New().Use(New(Config{ + File: "non-exist.ico", + })) +} + +// go test -run Test_Middleware_Favicon_Found +func Test_Middleware_Favicon_Found(t *testing.T) { + t.Parallel() + app := fiber.New() + + app.Use(New(Config{ + File: "../../.github/testdata/favicon.ico", + })) + + app.Get("/", func(c *fiber.Ctx) error { + return nil + }) + + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/favicon.ico", nil)) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, fiber.StatusOK, resp.StatusCode, "Status code") + utils.AssertEqual(t, "image/x-icon", resp.Header.Get(fiber.HeaderContentType)) + utils.AssertEqual(t, "public, max-age=31536000", resp.Header.Get(fiber.HeaderCacheControl), "CacheControl Control") +} + +// go test -run Test_Custom_Favicon_Url +func Test_Custom_Favicon_Url(t *testing.T) { + app := fiber.New() + const customURL = "/favicon.svg" + app.Use(New(Config{ + File: "../../.github/testdata/favicon.ico", + URL: customURL, + })) + + app.Get("/", func(c *fiber.Ctx) error { + return nil + }) + + resp, err := app.Test(httptest.NewRequest(http.MethodGet, customURL, nil)) + + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, fiber.StatusOK, resp.StatusCode, "Status code") + utils.AssertEqual(t, "image/x-icon", resp.Header.Get(fiber.HeaderContentType)) +} + +// go test -run Test_Custom_Favicon_Data +func Test_Custom_Favicon_Data(t *testing.T) { + data, err := os.ReadFile("../../.github/testdata/favicon.ico") + utils.AssertEqual(t, nil, err) + + app := fiber.New() + + app.Use(New(Config{ + Data: data, + })) + + app.Get("/", func(c *fiber.Ctx) error { + return nil + }) + + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/favicon.ico", nil)) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, fiber.StatusOK, resp.StatusCode, "Status code") + utils.AssertEqual(t, "image/x-icon", resp.Header.Get(fiber.HeaderContentType)) + utils.AssertEqual(t, "public, max-age=31536000", resp.Header.Get(fiber.HeaderCacheControl), "CacheControl Control") +} + +// mockFS wraps local filesystem for the purposes of +// Test_Middleware_Favicon_FileSystem located below +// TODO use os.Dir if fiber upgrades to 1.16 +type mockFS struct{} + +func (mockFS) Open(name string) (http.File, error) { + if name == "/" { + name = "." + } else { + name = strings.TrimPrefix(name, "/") + } + file, err := os.Open(name) //nolint:gosec // We're in a test func, so this is fine + if err != nil { + return nil, fmt.Errorf("failed to open: %w", err) + } + return file, nil +} + +// go test -run Test_Middleware_Favicon_FileSystem +func Test_Middleware_Favicon_FileSystem(t *testing.T) { + t.Parallel() + app := fiber.New() + + app.Use(New(Config{ + File: "../../.github/testdata/favicon.ico", + FileSystem: mockFS{}, + })) + + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/favicon.ico", nil)) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, fiber.StatusOK, resp.StatusCode, "Status code") + utils.AssertEqual(t, "image/x-icon", resp.Header.Get(fiber.HeaderContentType)) + utils.AssertEqual(t, "public, max-age=31536000", resp.Header.Get(fiber.HeaderCacheControl), "CacheControl Control") +} + +// go test -run Test_Middleware_Favicon_CacheControl +func Test_Middleware_Favicon_CacheControl(t *testing.T) { + t.Parallel() + app := fiber.New() + + app.Use(New(Config{ + CacheControl: "public, max-age=100", + File: "../../.github/testdata/favicon.ico", + })) + + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/favicon.ico", nil)) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, fiber.StatusOK, resp.StatusCode, "Status code") + utils.AssertEqual(t, "image/x-icon", resp.Header.Get(fiber.HeaderContentType)) + utils.AssertEqual(t, "public, max-age=100", resp.Header.Get(fiber.HeaderCacheControl), "CacheControl Control") +} + +// go test -v -run=^$ -bench=Benchmark_Middleware_Favicon -benchmem -count=4 +func Benchmark_Middleware_Favicon(b *testing.B) { + app := fiber.New() + app.Use(New()) + app.Get("/", func(c *fiber.Ctx) error { + return nil + }) + handler := app.Handler() + + c := &fasthttp.RequestCtx{} + c.Request.SetRequestURI("/") + + b.ReportAllocs() + b.ResetTimer() + for n := 0; n < b.N; n++ { + handler(c) + } +} + +// go test -run Test_Favicon_Next +func Test_Favicon_Next(t *testing.T) { + t.Parallel() + app := fiber.New() + app.Use(New(Config{ + Next: func(_ *fiber.Ctx) bool { + return true + }, + })) + + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, fiber.StatusNotFound, resp.StatusCode) +} diff --git a/middleware/filesystem/filesystem.go b/middleware/filesystem/filesystem.go new file mode 100644 index 0000000..547dc34 --- /dev/null +++ b/middleware/filesystem/filesystem.go @@ -0,0 +1,287 @@ +package filesystem + +import ( + "errors" + "fmt" + "io/fs" + "net/http" + "strconv" + "strings" + "sync" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/utils" +) + +// Config defines the config for middleware. +type Config struct { + // Next defines a function to skip this middleware when returned true. + // + // Optional. Default: nil + Next func(c *fiber.Ctx) bool + + // Root is a FileSystem that provides access + // to a collection of files and directories. + // + // Required. Default: nil + Root http.FileSystem `json:"-"` + + // PathPrefix defines a prefix to be added to a filepath when + // reading a file from the FileSystem. + // + // Use when using Go 1.16 embed.FS + // + // Optional. Default "" + PathPrefix string `json:"path_prefix"` + + // Enable directory browsing. + // + // Optional. Default: false + Browse bool `json:"browse"` + + // Index file for serving a directory. + // + // Optional. Default: "index.html" + Index string `json:"index"` + + // The value for the Cache-Control HTTP-header + // that is set on the file response. MaxAge is defined in seconds. + // + // Optional. Default value 0. + MaxAge int `json:"max_age"` + + // File to return if path is not found. Useful for SPA's. + // + // Optional. Default: "" + NotFoundFile string `json:"not_found_file"` + + // The value for the Content-Type HTTP-header + // that is set on the file response + // + // Optional. Default: "" + ContentTypeCharset string `json:"content_type_charset"` +} + +// ConfigDefault is the default config +var ConfigDefault = Config{ + Next: nil, + Root: nil, + PathPrefix: "", + Browse: false, + Index: "/index.html", + MaxAge: 0, + ContentTypeCharset: "", +} + +// New creates a new middleware handler. +// +// filesystem does not handle url encoded values (for example spaces) +// on it's own. If you need that functionality, set "UnescapePath" +// in fiber.Config +func New(config ...Config) fiber.Handler { + // Set default config + cfg := ConfigDefault + + // Override config if provided + if len(config) > 0 { + cfg = config[0] + + // Set default values + if cfg.Index == "" { + cfg.Index = ConfigDefault.Index + } + if !strings.HasPrefix(cfg.Index, "/") { + cfg.Index = "/" + cfg.Index + } + if cfg.NotFoundFile != "" && !strings.HasPrefix(cfg.NotFoundFile, "/") { + cfg.NotFoundFile = "/" + cfg.NotFoundFile + } + } + + if cfg.Root == nil { + panic("filesystem: Root cannot be nil") + } + + if cfg.PathPrefix != "" && !strings.HasPrefix(cfg.PathPrefix, "/") { + cfg.PathPrefix = "/" + cfg.PathPrefix + } + + var once sync.Once + var prefix string + cacheControlStr := "public, max-age=" + strconv.Itoa(cfg.MaxAge) + + // Return new handler + return func(c *fiber.Ctx) error { + // Don't execute middleware if Next returns true + if cfg.Next != nil && cfg.Next(c) { + return c.Next() + } + + method := c.Method() + + // We only serve static assets on GET or HEAD methods + if method != fiber.MethodGet && method != fiber.MethodHead { + return c.Next() + } + + // Set prefix once + once.Do(func() { + prefix = c.Route().Path + }) + + // Strip prefix + path := strings.TrimPrefix(c.Path(), prefix) + if !strings.HasPrefix(path, "/") { + path = "/" + path + } + // Add PathPrefix + if cfg.PathPrefix != "" { + // PathPrefix already has a "/" prefix + path = cfg.PathPrefix + path + } + + if len(path) > 1 { + path = utils.TrimRight(path, '/') + } + file, err := cfg.Root.Open(path) + if err != nil && errors.Is(err, fs.ErrNotExist) && cfg.NotFoundFile != "" { + file, err = cfg.Root.Open(cfg.NotFoundFile) + } + if err != nil { + if errors.Is(err, fs.ErrNotExist) { + return c.Status(fiber.StatusNotFound).Next() + } + return fmt.Errorf("failed to open: %w", err) + } + + stat, err := file.Stat() + if err != nil { + return fmt.Errorf("failed to stat: %w", err) + } + + // Serve index if path is directory + if stat.IsDir() { + indexPath := utils.TrimRight(path, '/') + cfg.Index + index, err := cfg.Root.Open(indexPath) + if err == nil { + indexStat, err := index.Stat() + if err == nil { + file = index + stat = indexStat + } + } + } + + // Browse directory if no index found and browsing is enabled + if stat.IsDir() { + if cfg.Browse { + return dirList(c, file) + } + return fiber.ErrForbidden + } + + c.Status(fiber.StatusOK) + + modTime := stat.ModTime() + contentLength := int(stat.Size()) + + // Set Content Type header + if cfg.ContentTypeCharset == "" { + c.Type(getFileExtension(stat.Name())) + } else { + c.Type(getFileExtension(stat.Name()), cfg.ContentTypeCharset) + } + + // Set Last Modified header + if !modTime.IsZero() { + c.Set(fiber.HeaderLastModified, modTime.UTC().Format(http.TimeFormat)) + } + + if method == fiber.MethodGet { + if cfg.MaxAge > 0 { + c.Set(fiber.HeaderCacheControl, cacheControlStr) + } + c.Response().SetBodyStream(file, contentLength) + return nil + } + if method == fiber.MethodHead { + c.Request().ResetBody() + // Fasthttp should skipbody by default if HEAD? + c.Response().SkipBody = true + c.Response().Header.SetContentLength(contentLength) + if err := file.Close(); err != nil { + return fmt.Errorf("failed to close: %w", err) + } + return nil + } + + return c.Next() + } +} + +// SendFile serves a file from an HTTP file system at the specified path. +// It handles content serving, sets appropriate headers, and returns errors when needed. +// Usage: err := SendFile(ctx, fs, "/path/to/file.txt") +func SendFile(c *fiber.Ctx, filesystem http.FileSystem, path string) error { + file, err := filesystem.Open(path) + if err != nil { + if errors.Is(err, fs.ErrNotExist) { + return fiber.ErrNotFound + } + return fmt.Errorf("failed to open: %w", err) + } + + stat, err := file.Stat() + if err != nil { + return fmt.Errorf("failed to stat: %w", err) + } + + // Serve index if path is directory + if stat.IsDir() { + indexPath := utils.TrimRight(path, '/') + ConfigDefault.Index + index, err := filesystem.Open(indexPath) + if err == nil { + indexStat, err := index.Stat() + if err == nil { + file = index + stat = indexStat + } + } + } + + // Return forbidden if no index found + if stat.IsDir() { + return fiber.ErrForbidden + } + + c.Status(fiber.StatusOK) + + modTime := stat.ModTime() + contentLength := int(stat.Size()) + + // Set Content Type header + c.Type(getFileExtension(stat.Name())) + + // Set Last Modified header + if !modTime.IsZero() { + c.Set(fiber.HeaderLastModified, modTime.UTC().Format(http.TimeFormat)) + } + + method := c.Method() + if method == fiber.MethodGet { + c.Response().SetBodyStream(file, contentLength) + return nil + } + if method == fiber.MethodHead { + c.Request().ResetBody() + // Fasthttp should skipbody by default if HEAD? + c.Response().SkipBody = true + c.Response().Header.SetContentLength(contentLength) + if err := file.Close(); err != nil { + return fmt.Errorf("failed to close: %w", err) + } + return nil + } + + return nil +} diff --git a/middleware/filesystem/filesystem_test.go b/middleware/filesystem/filesystem_test.go new file mode 100644 index 0000000..4c646fd --- /dev/null +++ b/middleware/filesystem/filesystem_test.go @@ -0,0 +1,235 @@ +//nolint:bodyclose // Much easier to just ignore memory leaks in tests +package filesystem + +import ( + "context" + "net/http" + "net/http/httptest" + "testing" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/utils" +) + +// go test -run Test_FileSystem +func Test_FileSystem(t *testing.T) { + t.Parallel() + app := fiber.New() + + app.Use("/test", New(Config{ + Root: http.Dir("../../.github/testdata/fs"), + })) + + app.Use("/dir", New(Config{ + Root: http.Dir("../../.github/testdata/fs"), + Browse: true, + })) + + app.Get("/", func(c *fiber.Ctx) error { + return c.SendString("Hello, World!") + }) + + app.Use("/spatest", New(Config{ + Root: http.Dir("../../.github/testdata/fs"), + Index: "index.html", + NotFoundFile: "index.html", + })) + + app.Use("/prefix", New(Config{ + Root: http.Dir("../../.github/testdata/fs"), + PathPrefix: "img", + })) + + tests := []struct { + name string + url string + statusCode int + contentType string + modifiedTime string + }{ + { + name: "Should be returns status 200 with suitable content-type", + url: "/test/index.html", + statusCode: 200, + contentType: "text/html", + }, + { + name: "Should be returns status 200 with suitable content-type", + url: "/test", + statusCode: 200, + contentType: "text/html", + }, + { + name: "Should be returns status 200 with suitable content-type", + url: "/test/css/style.css", + statusCode: 200, + contentType: "text/css", + }, + { + name: "Should be returns status 404", + url: "/test/nofile.js", + statusCode: 404, + }, + { + name: "Should be returns status 404", + url: "/test/nofile", + statusCode: 404, + }, + { + name: "Should be returns status 200", + url: "/", + statusCode: 200, + contentType: "text/plain; charset=utf-8", + }, + { + name: "Should be returns status 403", + url: "/test/img", + statusCode: 403, + }, + { + name: "Should list the directory contents", + url: "/dir/img", + statusCode: 200, + contentType: "text/html", + }, + { + name: "Should list the directory contents", + url: "/dir/img/", + statusCode: 200, + contentType: "text/html", + }, + { + name: "Should be returns status 200", + url: "/dir/img/fiber.png", + statusCode: 200, + contentType: "image/png", + }, + { + name: "Should be return status 200", + url: "/spatest/doesnotexist", + statusCode: 200, + contentType: "text/html", + }, + { + name: "PathPrefix should be applied", + url: "/prefix/fiber.png", + statusCode: 200, + contentType: "image/png", + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, tt.url, nil)) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, tt.statusCode, resp.StatusCode) + + if tt.contentType != "" { + ct := resp.Header.Get("Content-Type") + utils.AssertEqual(t, tt.contentType, ct) + } + }) + } +} + +// go test -run Test_FileSystem_Next +func Test_FileSystem_Next(t *testing.T) { + t.Parallel() + app := fiber.New() + app.Use(New(Config{ + Root: http.Dir("../../.github/testdata/fs"), + Next: func(_ *fiber.Ctx) bool { + return true + }, + })) + + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, fiber.StatusNotFound, resp.StatusCode) +} + +func Test_FileSystem_NonGetAndHead(t *testing.T) { + t.Parallel() + app := fiber.New() + + app.Use("/test", New(Config{ + Root: http.Dir("../../.github/testdata/fs"), + })) + + resp, err := app.Test(httptest.NewRequest(fiber.MethodPost, "/test", nil)) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, 404, resp.StatusCode) +} + +func Test_FileSystem_Head(t *testing.T) { + t.Parallel() + app := fiber.New() + + app.Use("/test", New(Config{ + Root: http.Dir("../../.github/testdata/fs"), + })) + + req, err := http.NewRequestWithContext(context.Background(), fiber.MethodHead, "/test", nil) + utils.AssertEqual(t, nil, err) + resp, err := app.Test(req) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, 200, resp.StatusCode) +} + +func Test_FileSystem_NoRoot(t *testing.T) { + t.Parallel() + defer func() { + utils.AssertEqual(t, "filesystem: Root cannot be nil", recover()) + }() + + app := fiber.New() + app.Use(New()) + _, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) + utils.AssertEqual(t, nil, err) +} + +func Test_FileSystem_UsingParam(t *testing.T) { + t.Parallel() + app := fiber.New() + + app.Use("/:path", func(c *fiber.Ctx) error { + return SendFile(c, http.Dir("../../.github/testdata/fs"), c.Params("path")+".html") + }) + + req, err := http.NewRequestWithContext(context.Background(), fiber.MethodHead, "/index", nil) + utils.AssertEqual(t, nil, err) + resp, err := app.Test(req) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, 200, resp.StatusCode) +} + +func Test_FileSystem_UsingParam_NonFile(t *testing.T) { + t.Parallel() + app := fiber.New() + + app.Use("/:path", func(c *fiber.Ctx) error { + return SendFile(c, http.Dir("../../.github/testdata/fs"), c.Params("path")+".html") + }) + + req, err := http.NewRequestWithContext(context.Background(), fiber.MethodHead, "/template", nil) + utils.AssertEqual(t, nil, err) + resp, err := app.Test(req) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, 404, resp.StatusCode) +} + +func Test_FileSystem_UsingContentTypeCharset(t *testing.T) { + t.Parallel() + app := fiber.New() + app.Use(New(Config{ + Root: http.Dir("../../.github/testdata/fs/index.html"), + ContentTypeCharset: "UTF-8", + })) + + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, 200, resp.StatusCode) + utils.AssertEqual(t, "text/html; charset=UTF-8", resp.Header.Get("Content-Type")) +} diff --git a/middleware/filesystem/utils.go b/middleware/filesystem/utils.go new file mode 100644 index 0000000..4e96db6 --- /dev/null +++ b/middleware/filesystem/utils.go @@ -0,0 +1,66 @@ +package filesystem + +import ( + "fmt" + "html" + "net/http" + "os" + "path" + "sort" + "strings" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/utils" +) + +func getFileExtension(p string) string { + n := strings.LastIndexByte(p, '.') + if n < 0 { + return "" + } + return p[n:] +} + +func dirList(c *fiber.Ctx, f http.File) error { + fileinfos, err := f.Readdir(-1) + if err != nil { + return fmt.Errorf("failed to read dir: %w", err) + } + + fm := make(map[string]os.FileInfo, len(fileinfos)) + filenames := make([]string, 0, len(fileinfos)) + for _, fi := range fileinfos { + name := fi.Name() + fm[name] = fi + filenames = append(filenames, name) + } + + basePathEscaped := html.EscapeString(c.Path()) + _, _ = fmt.Fprintf(c, "%s", basePathEscaped) + _, _ = fmt.Fprintf(c, "

%s

", basePathEscaped) + _, _ = fmt.Fprint(c, "
    ") + + if len(basePathEscaped) > 1 { + parentPathEscaped := html.EscapeString(utils.TrimRight(c.Path(), '/') + "/..") + _, _ = fmt.Fprintf(c, `
  • ..
  • `, parentPathEscaped) + } + + sort.Strings(filenames) + for _, name := range filenames { + pathEscaped := html.EscapeString(path.Join(c.Path() + "/" + name)) + fi := fm[name] + auxStr := "dir" + className := "dir" + if !fi.IsDir() { + auxStr = fmt.Sprintf("file, %d bytes", fi.Size()) + className = "file" + } + _, _ = fmt.Fprintf(c, `
  • %s, %s, last modified %s
  • `, + pathEscaped, className, html.EscapeString(name), auxStr, fi.ModTime()) + } + _, _ = fmt.Fprint(c, "
") + + c.Type("html") + + return nil +} diff --git a/middleware/healthcheck/config.go b/middleware/healthcheck/config.go new file mode 100644 index 0000000..d4e5fac --- /dev/null +++ b/middleware/healthcheck/config.go @@ -0,0 +1,84 @@ +package healthcheck + +import ( + "github.com/gofiber/fiber/v2" +) + +// Config defines the configuration options for the healthcheck middleware. +type Config struct { + // Next defines a function to skip this middleware when returned true. + // + // Optional. Default: nil + Next func(c *fiber.Ctx) bool + + // Function used for checking the liveness of the application. Returns true if the application + // is running and false if it is not. The liveness probe is typically used to indicate if + // the application is in a state where it can handle requests (e.g., the server is up and running). + // + // Optional. Default: func(c *fiber.Ctx) bool { return true } + LivenessProbe HealthChecker + + // HTTP endpoint at which the liveness probe will be available. + // + // Optional. Default: "/livez" + LivenessEndpoint string + + // Function used for checking the readiness of the application. Returns true if the application + // is ready to process requests and false otherwise. The readiness probe typically checks if all necessary + // services, databases, and other dependencies are available for the application to function correctly. + // + // Optional. Default: func(c *fiber.Ctx) bool { return true } + ReadinessProbe HealthChecker + + // HTTP endpoint at which the readiness probe will be available. + // Optional. Default: "/readyz" + ReadinessEndpoint string +} + +const ( + DefaultLivenessEndpoint = "/livez" + DefaultReadinessEndpoint = "/readyz" +) + +func defaultLivenessProbe(*fiber.Ctx) bool { return true } + +func defaultReadinessProbe(*fiber.Ctx) bool { return true } + +// ConfigDefault is the default config +var ConfigDefault = Config{ + LivenessProbe: defaultLivenessProbe, + ReadinessProbe: defaultReadinessProbe, + LivenessEndpoint: DefaultLivenessEndpoint, + ReadinessEndpoint: DefaultReadinessEndpoint, +} + +// defaultConfig returns a default config for the healthcheck middleware. +func defaultConfig(config ...Config) Config { + if len(config) < 1 { + return ConfigDefault + } + + cfg := config[0] + + if cfg.Next == nil { + cfg.Next = ConfigDefault.Next + } + + if cfg.LivenessProbe == nil { + cfg.LivenessProbe = defaultLivenessProbe + } + + if cfg.ReadinessProbe == nil { + cfg.ReadinessProbe = defaultReadinessProbe + } + + if cfg.LivenessEndpoint == "" { + cfg.LivenessEndpoint = DefaultLivenessEndpoint + } + + if cfg.ReadinessEndpoint == "" { + cfg.ReadinessEndpoint = DefaultReadinessEndpoint + } + + return cfg +} diff --git a/middleware/healthcheck/healthcheck.go b/middleware/healthcheck/healthcheck.go new file mode 100644 index 0000000..c9d6a64 --- /dev/null +++ b/middleware/healthcheck/healthcheck.go @@ -0,0 +1,61 @@ +package healthcheck + +import ( + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/utils" +) + +// HealthChecker defines a function to check liveness or readiness of the application +type HealthChecker func(*fiber.Ctx) bool + +// ProbeCheckerHandler defines a function that returns a ProbeChecker +type HealthCheckerHandler func(HealthChecker) fiber.Handler + +func healthCheckerHandler(checker HealthChecker) fiber.Handler { + return func(c *fiber.Ctx) error { + if checker == nil { + return c.Next() + } + + if checker(c) { + return c.SendStatus(fiber.StatusOK) + } + + return c.SendStatus(fiber.StatusServiceUnavailable) + } +} + +func New(config ...Config) fiber.Handler { + cfg := defaultConfig(config...) + + isLiveHandler := healthCheckerHandler(cfg.LivenessProbe) + isReadyHandler := healthCheckerHandler(cfg.ReadinessProbe) + + return func(c *fiber.Ctx) error { + // Don't execute middleware if Next returns true + if cfg.Next != nil && cfg.Next(c) { + return c.Next() + } + + if c.Method() != fiber.MethodGet { + return c.Next() + } + + prefixCount := len(utils.TrimRight(c.Route().Path, '/')) + if len(c.Path()) >= prefixCount { + checkPath := c.Path()[prefixCount:] + checkPathTrimmed := checkPath + if !c.App().Config().StrictRouting { + checkPathTrimmed = utils.TrimRight(checkPath, '/') + } + switch { + case checkPath == cfg.ReadinessEndpoint || checkPathTrimmed == cfg.ReadinessEndpoint: + return isReadyHandler(c) + case checkPath == cfg.LivenessEndpoint || checkPathTrimmed == cfg.LivenessEndpoint: + return isLiveHandler(c) + } + } + + return c.Next() + } +} diff --git a/middleware/healthcheck/healthcheck_test.go b/middleware/healthcheck/healthcheck_test.go new file mode 100644 index 0000000..84fbb43 --- /dev/null +++ b/middleware/healthcheck/healthcheck_test.go @@ -0,0 +1,237 @@ +package healthcheck + +import ( + "fmt" + "net/http/httptest" + "testing" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/utils" + "github.com/valyala/fasthttp" +) + +func shouldGiveStatus(t *testing.T, app *fiber.App, path string, expectedStatus int) { + t.Helper() + req, err := app.Test(httptest.NewRequest(fiber.MethodGet, path, nil)) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, expectedStatus, req.StatusCode, "path: "+path+" should match "+fmt.Sprint(expectedStatus)) +} + +func shouldGiveOK(t *testing.T, app *fiber.App, path string) { + t.Helper() + shouldGiveStatus(t, app, path, fiber.StatusOK) +} + +func shouldGiveNotFound(t *testing.T, app *fiber.App, path string) { + t.Helper() + shouldGiveStatus(t, app, path, fiber.StatusNotFound) +} + +func Test_HealthCheck_Strict_Routing_Default(t *testing.T) { + t.Parallel() + + app := fiber.New(fiber.Config{ + StrictRouting: true, + }) + + app.Use(New()) + + shouldGiveOK(t, app, "/readyz") + shouldGiveOK(t, app, "/livez") + shouldGiveNotFound(t, app, "/readyz/") + shouldGiveNotFound(t, app, "/livez/") + shouldGiveNotFound(t, app, "/notDefined/readyz") + shouldGiveNotFound(t, app, "/notDefined/livez") +} + +func Test_HealthCheck_Group_Default(t *testing.T) { + t.Parallel() + + app := fiber.New() + app.Group("/v1", New()) + v2Group := app.Group("/v2/") + customer := v2Group.Group("/customer/") + customer.Use(New()) + + v3Group := app.Group("/v3/") + v3Group.Group("/todos/", New(Config{ReadinessEndpoint: "/readyz/", LivenessEndpoint: "/livez/"})) + + shouldGiveOK(t, app, "/v1/readyz") + shouldGiveOK(t, app, "/v1/livez") + shouldGiveOK(t, app, "/v1/readyz/") + shouldGiveOK(t, app, "/v1/livez/") + shouldGiveOK(t, app, "/v2/customer/readyz") + shouldGiveOK(t, app, "/v2/customer/livez") + shouldGiveOK(t, app, "/v2/customer/readyz/") + shouldGiveOK(t, app, "/v2/customer/livez/") + shouldGiveNotFound(t, app, "/v3/todos/readyz") + shouldGiveNotFound(t, app, "/v3/todos/livez") + shouldGiveOK(t, app, "/v3/todos/readyz/") + shouldGiveOK(t, app, "/v3/todos/livez/") + shouldGiveNotFound(t, app, "/notDefined/readyz") + shouldGiveNotFound(t, app, "/notDefined/livez") + shouldGiveNotFound(t, app, "/notDefined/readyz/") + shouldGiveNotFound(t, app, "/notDefined/livez/") + + // strict routing + app = fiber.New(fiber.Config{ + StrictRouting: true, + }) + app.Group("/v1", New()) + v2Group = app.Group("/v2/") + customer = v2Group.Group("/customer/") + customer.Use(New()) + + v3Group = app.Group("/v3/") + v3Group.Group("/todos/", New(Config{ReadinessEndpoint: "/readyz/", LivenessEndpoint: "/livez/"})) + + shouldGiveOK(t, app, "/v1/readyz") + shouldGiveOK(t, app, "/v1/livez") + shouldGiveNotFound(t, app, "/v1/readyz/") + shouldGiveNotFound(t, app, "/v1/livez/") + shouldGiveOK(t, app, "/v2/customer/readyz") + shouldGiveOK(t, app, "/v2/customer/livez") + shouldGiveNotFound(t, app, "/v2/customer/readyz/") + shouldGiveNotFound(t, app, "/v2/customer/livez/") + shouldGiveNotFound(t, app, "/v3/todos/readyz") + shouldGiveNotFound(t, app, "/v3/todos/livez") + shouldGiveOK(t, app, "/v3/todos/readyz/") + shouldGiveOK(t, app, "/v3/todos/livez/") + shouldGiveNotFound(t, app, "/notDefined/readyz") + shouldGiveNotFound(t, app, "/notDefined/livez") + shouldGiveNotFound(t, app, "/notDefined/readyz/") + shouldGiveNotFound(t, app, "/notDefined/livez/") +} + +func Test_HealthCheck_Default(t *testing.T) { + t.Parallel() + + app := fiber.New() + app.Use(New()) + + shouldGiveOK(t, app, "/readyz") + shouldGiveOK(t, app, "/livez") + shouldGiveOK(t, app, "/readyz/") + shouldGiveOK(t, app, "/livez/") + shouldGiveNotFound(t, app, "/notDefined/readyz") + shouldGiveNotFound(t, app, "/notDefined/livez") +} + +func Test_HealthCheck_Custom(t *testing.T) { + t.Parallel() + + app := fiber.New() + + c1 := make(chan struct{}, 1) + app.Use(New(Config{ + LivenessProbe: func(c *fiber.Ctx) bool { + return true + }, + LivenessEndpoint: "/live", + ReadinessProbe: func(c *fiber.Ctx) bool { + select { + case <-c1: + return true + default: + return false + } + }, + ReadinessEndpoint: "/ready", + })) + + // Live should return 200 with GET request + shouldGiveOK(t, app, "/live") + // Live should return 404 with POST request + req, err := app.Test(httptest.NewRequest(fiber.MethodPost, "/live", nil)) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, fiber.StatusNotFound, req.StatusCode) + + // Ready should return 404 with POST request + req, err = app.Test(httptest.NewRequest(fiber.MethodPost, "/ready", nil)) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, fiber.StatusNotFound, req.StatusCode) + + // Ready should return 503 with GET request before the channel is closed + shouldGiveStatus(t, app, "/ready", fiber.StatusServiceUnavailable) + + // Ready should return 200 with GET request after the channel is closed + c1 <- struct{}{} + shouldGiveOK(t, app, "/ready") +} + +func Test_HealthCheck_Custom_Nested(t *testing.T) { + t.Parallel() + + app := fiber.New() + + c1 := make(chan struct{}, 1) + + app.Use(New(Config{ + LivenessProbe: func(c *fiber.Ctx) bool { + return true + }, + LivenessEndpoint: "/probe/live", + ReadinessProbe: func(c *fiber.Ctx) bool { + select { + case <-c1: + return true + default: + return false + } + }, + ReadinessEndpoint: "/probe/ready", + })) + + shouldGiveOK(t, app, "/probe/live") + shouldGiveStatus(t, app, "/probe/ready", fiber.StatusServiceUnavailable) + shouldGiveOK(t, app, "/probe/live/") + shouldGiveStatus(t, app, "/probe/ready/", fiber.StatusServiceUnavailable) + shouldGiveNotFound(t, app, "/probe/livez") + shouldGiveNotFound(t, app, "/probe/readyz") + shouldGiveNotFound(t, app, "/probe/livez/") + shouldGiveNotFound(t, app, "/probe/readyz/") + shouldGiveNotFound(t, app, "/livez") + shouldGiveNotFound(t, app, "/readyz") + shouldGiveNotFound(t, app, "/readyz/") + shouldGiveNotFound(t, app, "/livez/") + + c1 <- struct{}{} + shouldGiveOK(t, app, "/probe/ready") + c1 <- struct{}{} + shouldGiveOK(t, app, "/probe/ready/") +} + +func Test_HealthCheck_Next(t *testing.T) { + t.Parallel() + + app := fiber.New() + + app.Use(New(Config{ + Next: func(c *fiber.Ctx) bool { + return true + }, + })) + + shouldGiveNotFound(t, app, "/readyz") + shouldGiveNotFound(t, app, "/livez") +} + +func Benchmark_HealthCheck(b *testing.B) { + app := fiber.New() + + app.Use(New()) + + h := app.Handler() + fctx := &fasthttp.RequestCtx{} + fctx.Request.Header.SetMethod(fiber.MethodGet) + fctx.Request.SetRequestURI("/livez") + + b.ReportAllocs() + b.ResetTimer() + + for i := 0; i < b.N; i++ { + h(fctx) + } + + utils.AssertEqual(b, fiber.StatusOK, fctx.Response.Header.StatusCode()) +} diff --git a/middleware/helmet/config.go b/middleware/helmet/config.go new file mode 100644 index 0000000..49c059e --- /dev/null +++ b/middleware/helmet/config.go @@ -0,0 +1,154 @@ +package helmet + +import ( + "github.com/gofiber/fiber/v2" +) + +// Config defines the config for middleware. +type Config struct { + // Next defines a function to skip middleware. + // Optional. Default: nil + Next func(*fiber.Ctx) bool + + // XSSProtection + // Optional. Default value "0". + XSSProtection string + + // ContentTypeNosniff + // Optional. Default value "nosniff". + ContentTypeNosniff string + + // XFrameOptions + // Optional. Default value "SAMEORIGIN". + // Possible values: "SAMEORIGIN", "DENY", "ALLOW-FROM uri" + XFrameOptions string + + // HSTSMaxAge + // Optional. Default value 0. + HSTSMaxAge int + + // HSTSExcludeSubdomains + // Optional. Default value false. + HSTSExcludeSubdomains bool + + // ContentSecurityPolicy + // Optional. Default value "". + ContentSecurityPolicy string + + // CSPReportOnly + // Optional. Default value false. + CSPReportOnly bool + + // HSTSPreloadEnabled + // Optional. Default value false. + HSTSPreloadEnabled bool + + // ReferrerPolicy + // Optional. Default value "ReferrerPolicy". + ReferrerPolicy string + + // Permissions-Policy + // Optional. Default value "". + PermissionPolicy string + + // Cross-Origin-Embedder-Policy + // Optional. Default value "require-corp". + CrossOriginEmbedderPolicy string + + // Cross-Origin-Opener-Policy + // Optional. Default value "same-origin". + CrossOriginOpenerPolicy string + + // Cross-Origin-Resource-Policy + // Optional. Default value "same-origin". + CrossOriginResourcePolicy string + + // Origin-Agent-Cluster + // Optional. Default value "?1". + OriginAgentCluster string + + // X-DNS-Prefetch-Control + // Optional. Default value "off". + XDNSPrefetchControl string + + // X-Download-Options + // Optional. Default value "noopen". + XDownloadOptions string + + // X-Permitted-Cross-Domain-Policies + // Optional. Default value "none". + XPermittedCrossDomain string +} + +// ConfigDefault is the default config +var ConfigDefault = Config{ + XSSProtection: "0", + ContentTypeNosniff: "nosniff", + XFrameOptions: "SAMEORIGIN", + ReferrerPolicy: "no-referrer", + CrossOriginEmbedderPolicy: "require-corp", + CrossOriginOpenerPolicy: "same-origin", + CrossOriginResourcePolicy: "same-origin", + OriginAgentCluster: "?1", + XDNSPrefetchControl: "off", + XDownloadOptions: "noopen", + XPermittedCrossDomain: "none", +} + +// Helper function to set default values +func configDefault(config ...Config) Config { + // Return default config if nothing provided + if len(config) < 1 { + return ConfigDefault + } + + // Override default config + cfg := config[0] + + // Set default values + if cfg.XSSProtection == "" { + cfg.XSSProtection = ConfigDefault.XSSProtection + } + + if cfg.ContentTypeNosniff == "" { + cfg.ContentTypeNosniff = ConfigDefault.ContentTypeNosniff + } + + if cfg.XFrameOptions == "" { + cfg.XFrameOptions = ConfigDefault.XFrameOptions + } + + if cfg.ReferrerPolicy == "" { + cfg.ReferrerPolicy = ConfigDefault.ReferrerPolicy + } + + if cfg.CrossOriginEmbedderPolicy == "" { + cfg.CrossOriginEmbedderPolicy = ConfigDefault.CrossOriginEmbedderPolicy + } + + if cfg.CrossOriginOpenerPolicy == "" { + cfg.CrossOriginOpenerPolicy = ConfigDefault.CrossOriginOpenerPolicy + } + + if cfg.CrossOriginResourcePolicy == "" { + cfg.CrossOriginResourcePolicy = ConfigDefault.CrossOriginResourcePolicy + } + + if cfg.OriginAgentCluster == "" { + cfg.OriginAgentCluster = ConfigDefault.OriginAgentCluster + } + + if cfg.XDNSPrefetchControl == "" { + cfg.XDNSPrefetchControl = ConfigDefault.XDNSPrefetchControl + } + + if cfg.XDownloadOptions == "" { + cfg.XDownloadOptions = ConfigDefault.XDownloadOptions + } + + if cfg.XPermittedCrossDomain == "" { + cfg.XPermittedCrossDomain = ConfigDefault.XPermittedCrossDomain + } + + return cfg +} diff --git a/middleware/helmet/helmet.go b/middleware/helmet/helmet.go new file mode 100644 index 0000000..993036e --- /dev/null +++ b/middleware/helmet/helmet.go @@ -0,0 +1,94 @@ +package helmet + +import ( + "fmt" + + "github.com/gofiber/fiber/v2" +) + +// New creates a new middleware handler +func New(config ...Config) fiber.Handler { + // Init config + cfg := configDefault(config...) + + // Return middleware handler + return func(c *fiber.Ctx) error { + // Next request to skip middleware + if cfg.Next != nil && cfg.Next(c) { + return c.Next() + } + + // Set headers + if cfg.XSSProtection != "" { + c.Set(fiber.HeaderXXSSProtection, cfg.XSSProtection) + } + + if cfg.ContentTypeNosniff != "" { + c.Set(fiber.HeaderXContentTypeOptions, cfg.ContentTypeNosniff) + } + + if cfg.XFrameOptions != "" { + c.Set(fiber.HeaderXFrameOptions, cfg.XFrameOptions) + } + + if cfg.CrossOriginEmbedderPolicy != "" { + c.Set("Cross-Origin-Embedder-Policy", cfg.CrossOriginEmbedderPolicy) + } + + if cfg.CrossOriginOpenerPolicy != "" { + c.Set("Cross-Origin-Opener-Policy", cfg.CrossOriginOpenerPolicy) + } + + if cfg.CrossOriginResourcePolicy != "" { + c.Set("Cross-Origin-Resource-Policy", cfg.CrossOriginResourcePolicy) + } + + if cfg.OriginAgentCluster != "" { + c.Set("Origin-Agent-Cluster", cfg.OriginAgentCluster) + } + + if cfg.ReferrerPolicy != "" { + c.Set("Referrer-Policy", cfg.ReferrerPolicy) + } + + if cfg.XDNSPrefetchControl != "" { + c.Set("X-DNS-Prefetch-Control", cfg.XDNSPrefetchControl) + } + + if cfg.XDownloadOptions != "" { + c.Set("X-Download-Options", cfg.XDownloadOptions) + } + + if cfg.XPermittedCrossDomain != "" { + c.Set("X-Permitted-Cross-Domain-Policies", cfg.XPermittedCrossDomain) + } + + // Handle HSTS headers + if c.Protocol() == "https" && cfg.HSTSMaxAge != 0 { + subdomains := "" + if !cfg.HSTSExcludeSubdomains { + subdomains = "; includeSubDomains" + } + if cfg.HSTSPreloadEnabled { + subdomains = fmt.Sprintf("%s; preload", subdomains) + } + c.Set(fiber.HeaderStrictTransportSecurity, fmt.Sprintf("max-age=%d%s", cfg.HSTSMaxAge, subdomains)) + } + + // Handle Content-Security-Policy headers + if cfg.ContentSecurityPolicy != "" { + if cfg.CSPReportOnly { + c.Set(fiber.HeaderContentSecurityPolicyReportOnly, cfg.ContentSecurityPolicy) + } else { + c.Set(fiber.HeaderContentSecurityPolicy, cfg.ContentSecurityPolicy) + } + } + + // Handle Permissions-Policy headers + if cfg.PermissionPolicy != "" { + c.Set(fiber.HeaderPermissionsPolicy, cfg.PermissionPolicy) + } + + return c.Next() + } +} diff --git a/middleware/helmet/helmet_test.go b/middleware/helmet/helmet_test.go new file mode 100644 index 0000000..1dfbfc7 --- /dev/null +++ b/middleware/helmet/helmet_test.go @@ -0,0 +1,201 @@ +package helmet + +import ( + "net/http/httptest" + "testing" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/utils" +) + +func Test_Default(t *testing.T) { + app := fiber.New() + + app.Use(New()) + + app.Get("/", func(c *fiber.Ctx) error { + return c.SendString("Hello, World!") + }) + + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, "0", resp.Header.Get(fiber.HeaderXXSSProtection)) + utils.AssertEqual(t, "nosniff", resp.Header.Get(fiber.HeaderXContentTypeOptions)) + utils.AssertEqual(t, "SAMEORIGIN", resp.Header.Get(fiber.HeaderXFrameOptions)) + utils.AssertEqual(t, "", resp.Header.Get(fiber.HeaderContentSecurityPolicy)) + utils.AssertEqual(t, "no-referrer", resp.Header.Get(fiber.HeaderReferrerPolicy)) + utils.AssertEqual(t, "", resp.Header.Get(fiber.HeaderPermissionsPolicy)) + utils.AssertEqual(t, "require-corp", resp.Header.Get("Cross-Origin-Embedder-Policy")) + utils.AssertEqual(t, "same-origin", resp.Header.Get("Cross-Origin-Opener-Policy")) + utils.AssertEqual(t, "same-origin", resp.Header.Get("Cross-Origin-Resource-Policy")) + utils.AssertEqual(t, "?1", resp.Header.Get("Origin-Agent-Cluster")) + utils.AssertEqual(t, "off", resp.Header.Get("X-DNS-Prefetch-Control")) + utils.AssertEqual(t, "noopen", resp.Header.Get("X-Download-Options")) + utils.AssertEqual(t, "none", resp.Header.Get("X-Permitted-Cross-Domain-Policies")) +} + +func Test_CustomValues_AllHeaders(t *testing.T) { + app := fiber.New() + + app.Use(New(Config{ + // Custom values for all headers + XSSProtection: "0", + ContentTypeNosniff: "custom-nosniff", + XFrameOptions: "DENY", + HSTSExcludeSubdomains: true, + ContentSecurityPolicy: "default-src 'none'", + CSPReportOnly: true, + HSTSPreloadEnabled: true, + ReferrerPolicy: "origin", + PermissionPolicy: "geolocation=(self)", + CrossOriginEmbedderPolicy: "custom-value", + CrossOriginOpenerPolicy: "custom-value", + CrossOriginResourcePolicy: "custom-value", + OriginAgentCluster: "custom-value", + XDNSPrefetchControl: "custom-control", + XDownloadOptions: "custom-options", + XPermittedCrossDomain: "custom-policies", + })) + + app.Get("/", func(c *fiber.Ctx) error { + return c.SendString("Hello, World!") + }) + + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) + utils.AssertEqual(t, nil, err) + // Assertions for custom header values + utils.AssertEqual(t, "0", resp.Header.Get(fiber.HeaderXXSSProtection)) + utils.AssertEqual(t, "custom-nosniff", resp.Header.Get(fiber.HeaderXContentTypeOptions)) + utils.AssertEqual(t, "DENY", resp.Header.Get(fiber.HeaderXFrameOptions)) + utils.AssertEqual(t, "default-src 'none'", resp.Header.Get(fiber.HeaderContentSecurityPolicyReportOnly)) + utils.AssertEqual(t, "origin", resp.Header.Get(fiber.HeaderReferrerPolicy)) + utils.AssertEqual(t, "geolocation=(self)", resp.Header.Get(fiber.HeaderPermissionsPolicy)) + utils.AssertEqual(t, "custom-value", resp.Header.Get("Cross-Origin-Embedder-Policy")) + utils.AssertEqual(t, "custom-value", resp.Header.Get("Cross-Origin-Opener-Policy")) + utils.AssertEqual(t, "custom-value", resp.Header.Get("Cross-Origin-Resource-Policy")) + utils.AssertEqual(t, "custom-value", resp.Header.Get("Origin-Agent-Cluster")) + utils.AssertEqual(t, "custom-control", resp.Header.Get("X-DNS-Prefetch-Control")) + utils.AssertEqual(t, "custom-options", resp.Header.Get("X-Download-Options")) + utils.AssertEqual(t, "custom-policies", resp.Header.Get("X-Permitted-Cross-Domain-Policies")) +} + +func Test_RealWorldValues_AllHeaders(t *testing.T) { + app := fiber.New() + + app.Use(New(Config{ + // Real-world values for all headers + XSSProtection: "0", + ContentTypeNosniff: "nosniff", + XFrameOptions: "SAMEORIGIN", + HSTSExcludeSubdomains: false, + ContentSecurityPolicy: "default-src 'self';base-uri 'self';font-src 'self' https: data:;form-action 'self';frame-ancestors 'self';img-src 'self' data:;object-src 'none';script-src 'self';script-src-attr 'none';style-src 'self' https: 'unsafe-inline';upgrade-insecure-requests", + CSPReportOnly: false, + HSTSPreloadEnabled: true, + ReferrerPolicy: "no-referrer", + PermissionPolicy: "geolocation=(self)", + CrossOriginEmbedderPolicy: "require-corp", + CrossOriginOpenerPolicy: "same-origin", + CrossOriginResourcePolicy: "same-origin", + OriginAgentCluster: "?1", + XDNSPrefetchControl: "off", + XDownloadOptions: "noopen", + XPermittedCrossDomain: "none", + })) + + app.Get("/", func(c *fiber.Ctx) error { + return c.SendString("Hello, World!") + }) + + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) + utils.AssertEqual(t, nil, err) + // Assertions for real-world header values + utils.AssertEqual(t, "0", resp.Header.Get(fiber.HeaderXXSSProtection)) + utils.AssertEqual(t, "nosniff", resp.Header.Get(fiber.HeaderXContentTypeOptions)) + utils.AssertEqual(t, "SAMEORIGIN", resp.Header.Get(fiber.HeaderXFrameOptions)) + utils.AssertEqual(t, "default-src 'self';base-uri 'self';font-src 'self' https: data:;form-action 'self';frame-ancestors 'self';img-src 'self' data:;object-src 'none';script-src 'self';script-src-attr 'none';style-src 'self' https: 'unsafe-inline';upgrade-insecure-requests", resp.Header.Get(fiber.HeaderContentSecurityPolicy)) + utils.AssertEqual(t, "no-referrer", resp.Header.Get(fiber.HeaderReferrerPolicy)) + utils.AssertEqual(t, "geolocation=(self)", resp.Header.Get(fiber.HeaderPermissionsPolicy)) + utils.AssertEqual(t, "require-corp", resp.Header.Get("Cross-Origin-Embedder-Policy")) + utils.AssertEqual(t, "same-origin", resp.Header.Get("Cross-Origin-Opener-Policy")) + utils.AssertEqual(t, "same-origin", resp.Header.Get("Cross-Origin-Resource-Policy")) + utils.AssertEqual(t, "?1", resp.Header.Get("Origin-Agent-Cluster")) + utils.AssertEqual(t, "off", resp.Header.Get("X-DNS-Prefetch-Control")) + utils.AssertEqual(t, "noopen", resp.Header.Get("X-Download-Options")) + utils.AssertEqual(t, "none", resp.Header.Get("X-Permitted-Cross-Domain-Policies")) +} + +func Test_Next(t *testing.T) { + app := fiber.New() + + app.Use(New(Config{ + Next: func(ctx *fiber.Ctx) bool { + return ctx.Path() == "/next" + }, + ReferrerPolicy: "no-referrer", + })) + + app.Get("/", func(c *fiber.Ctx) error { + return c.SendString("Hello, World!") + }) + app.Get("/next", func(c *fiber.Ctx) error { + return c.SendString("Skipped!") + }) + + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, "no-referrer", resp.Header.Get(fiber.HeaderReferrerPolicy)) + + resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/next", nil)) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, "", resp.Header.Get(fiber.HeaderReferrerPolicy)) +} + +func Test_ContentSecurityPolicy(t *testing.T) { + app := fiber.New() + + app.Use(New(Config{ + ContentSecurityPolicy: "default-src 'none'", + })) + + app.Get("/", func(c *fiber.Ctx) error { + return c.SendString("Hello, World!") + }) + + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, "default-src 'none'", resp.Header.Get(fiber.HeaderContentSecurityPolicy)) +} + +func Test_ContentSecurityPolicyReportOnly(t *testing.T) { + app := fiber.New() + + app.Use(New(Config{ + ContentSecurityPolicy: "default-src 'none'", + CSPReportOnly: true, + })) + + app.Get("/", func(c *fiber.Ctx) error { + return c.SendString("Hello, World!") + }) + + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, "default-src 'none'", resp.Header.Get(fiber.HeaderContentSecurityPolicyReportOnly)) + utils.AssertEqual(t, "", resp.Header.Get(fiber.HeaderContentSecurityPolicy)) +} + +func Test_PermissionsPolicy(t *testing.T) { + app := fiber.New() + + app.Use(New(Config{ + PermissionPolicy: "microphone=()", + })) + + app.Get("/", func(c *fiber.Ctx) error { + return c.SendString("Hello, World!") + }) + + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, "microphone=()", resp.Header.Get(fiber.HeaderPermissionsPolicy)) +} diff --git a/middleware/idempotency/config.go b/middleware/idempotency/config.go new file mode 100644 index 0000000..fee7759 --- /dev/null +++ b/middleware/idempotency/config.go @@ -0,0 +1,125 @@ +package idempotency + +import ( + "errors" + "fmt" + "time" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/internal/storage/memory" +) + +var ErrInvalidIdempotencyKey = errors.New("invalid idempotency key") + +// Config defines the config for middleware. +type Config struct { + // Next defines a function to skip this middleware when returned true. + // + // Optional. Default: a function which skips the middleware on safe HTTP request method. + Next func(c *fiber.Ctx) bool + + // Lifetime is the maximum lifetime of an idempotency key. + // + // Optional. Default: 30 * time.Minute + Lifetime time.Duration + + // KeyHeader is the name of the header that contains the idempotency key. + // + // Optional. Default: X-Idempotency-Key + KeyHeader string + // KeyHeaderValidate defines a function to validate the syntax of the idempotency header. + // + // Optional. Default: a function which ensures the header is 36 characters long (the size of an UUID). + KeyHeaderValidate func(string) error + + // KeepResponseHeaders is a list of headers that should be kept from the original response. + // + // Optional. Default: nil (to keep all headers) + KeepResponseHeaders []string + + // Lock locks an idempotency key. + // + // Optional. Default: an in-memory locker for this process only. + Lock Locker + + // Storage stores response data by idempotency key. + // + // Optional. Default: an in-memory storage for this process only. + Storage fiber.Storage +} + +// ConfigDefault is the default config +var ConfigDefault = Config{ + Next: func(c *fiber.Ctx) bool { + // Skip middleware if the request was done using a safe HTTP method + return fiber.IsMethodSafe(c.Method()) + }, + + Lifetime: 30 * time.Minute, + + KeyHeader: "X-Idempotency-Key", + KeyHeaderValidate: func(k string) error { + if l, wl := len(k), 36; l != wl { // UUID length is 36 chars + return fmt.Errorf("%w: invalid length: %d != %d", ErrInvalidIdempotencyKey, l, wl) + } + + return nil + }, + + KeepResponseHeaders: nil, + + Lock: nil, // Set in configDefault so we don't allocate data here. + + Storage: nil, // Set in configDefault so we don't allocate data here. +} + +// Helper function to set default values +func configDefault(config ...Config) Config { + // Return default config if nothing provided + if len(config) < 1 { + cfg := ConfigDefault + + cfg.Lock = NewMemoryLock() + cfg.Storage = memory.New(memory.Config{ + GCInterval: cfg.Lifetime / 2, // Half the lifetime interval + }) + + return cfg + } + + // Override default config + cfg := config[0] + + // Set default values + + if cfg.Next == nil { + cfg.Next = ConfigDefault.Next + } + + if cfg.Lifetime.Nanoseconds() == 0 { + cfg.Lifetime = ConfigDefault.Lifetime + } + + if cfg.KeyHeader == "" { + cfg.KeyHeader = ConfigDefault.KeyHeader + } + if cfg.KeyHeaderValidate == nil { + cfg.KeyHeaderValidate = ConfigDefault.KeyHeaderValidate + } + + if cfg.KeepResponseHeaders != nil && len(cfg.KeepResponseHeaders) == 0 { + cfg.KeepResponseHeaders = ConfigDefault.KeepResponseHeaders + } + + if cfg.Lock == nil { + cfg.Lock = NewMemoryLock() + } + + if cfg.Storage == nil { + cfg.Storage = memory.New(memory.Config{ + GCInterval: cfg.Lifetime / 2, // Half the lifetime interval + }) + } + + return cfg +} diff --git a/middleware/idempotency/idempotency.go b/middleware/idempotency/idempotency.go new file mode 100644 index 0000000..5affc59 --- /dev/null +++ b/middleware/idempotency/idempotency.go @@ -0,0 +1,153 @@ +package idempotency + +import ( + "fmt" + "strings" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/log" + "github.com/gofiber/fiber/v2/utils" +) + +// Inspired by https://datatracker.ietf.org/doc/html/draft-ietf-httpapi-idempotency-key-header-02 +// and https://github.com/penguin-statistics/backend-next/blob/f2f7d5ba54fc8a58f168d153baa17b2ad4a14e45/internal/pkg/middlewares/idempotency.go + +type localsKeys string + +const ( + localsKeyIsFromCache localsKeys = "idempotency_isfromcache" + localsKeyWasPutToCache localsKeys = "idempotency_wasputtocache" +) + +func IsFromCache(c *fiber.Ctx) bool { + return c.Locals(localsKeyIsFromCache) != nil +} + +func WasPutToCache(c *fiber.Ctx) bool { + return c.Locals(localsKeyWasPutToCache) != nil +} + +func New(config ...Config) fiber.Handler { + // Set default config + cfg := configDefault(config...) + + keepResponseHeadersMap := make(map[string]struct{}, len(cfg.KeepResponseHeaders)) + for _, h := range cfg.KeepResponseHeaders { + keepResponseHeadersMap[strings.ToLower(h)] = struct{}{} + } + + maybeWriteCachedResponse := func(c *fiber.Ctx, key string) (bool, error) { + if val, err := cfg.Storage.Get(key); err != nil { + return false, fmt.Errorf("failed to read response: %w", err) + } else if val != nil { + var res response + if _, err := res.UnmarshalMsg(val); err != nil { + return false, fmt.Errorf("failed to unmarshal response: %w", err) + } + + _ = c.Status(res.StatusCode) + + for header, vals := range res.Headers { + for _, val := range vals { + c.Context().Response.Header.Add(header, val) + } + } + + if len(res.Body) != 0 { + if err := c.Send(res.Body); err != nil { + return true, err + } + } + + _ = c.Locals(localsKeyIsFromCache, true) + + return true, nil + } + + return false, nil + } + + return func(c *fiber.Ctx) error { + // Don't execute middleware if Next returns true + if cfg.Next != nil && cfg.Next(c) { + return c.Next() + } + + // Don't execute middleware if the idempotency key is empty + key := utils.CopyString(c.Get(cfg.KeyHeader)) + if key == "" { + return c.Next() + } + + // Validate key + if err := cfg.KeyHeaderValidate(key); err != nil { + return err + } + + // First-pass: if the idempotency key is in the storage, get and return the response + if ok, err := maybeWriteCachedResponse(c, key); err != nil { + return fmt.Errorf("failed to write cached response at fastpath: %w", err) + } else if ok { + return nil + } + + if err := cfg.Lock.Lock(key); err != nil { + return fmt.Errorf("failed to lock: %w", err) + } + defer func() { + if err := cfg.Lock.Unlock(key); err != nil { + log.Errorf("[IDEMPOTENCY] failed to unlock key %q: %v", key, err) + } + }() + + // Lock acquired. If the idempotency key now is in the storage, get and return the response + if ok, err := maybeWriteCachedResponse(c, key); err != nil { + return fmt.Errorf("failed to write cached response while locked: %w", err) + } else if ok { + return nil + } + + // Execute the request handler + if err := c.Next(); err != nil { + // If the request handler returned an error, return it and skip idempotency + return err + } + + // Construct response + res := &response{ + StatusCode: c.Response().StatusCode(), + + Body: utils.CopyBytes(c.Response().Body()), + } + { + headers := c.GetRespHeaders() + if cfg.KeepResponseHeaders == nil { + // Keep all + res.Headers = headers + } else { + // Filter + res.Headers = make(map[string][]string) + for h := range headers { + if _, ok := keepResponseHeadersMap[utils.ToLower(h)]; ok { + res.Headers[h] = headers[h] + } + } + } + } + + // Marshal response + bs, err := res.MarshalMsg(nil) + if err != nil { + return fmt.Errorf("failed to marshal response: %w", err) + } + + // Store response + if err := cfg.Storage.Set(key, bs, cfg.Lifetime); err != nil { + return fmt.Errorf("failed to save response: %w", err) + } + + _ = c.Locals(localsKeyWasPutToCache, true) + + return nil + } +} diff --git a/middleware/idempotency/idempotency_test.go b/middleware/idempotency/idempotency_test.go new file mode 100644 index 0000000..6612cc6 --- /dev/null +++ b/middleware/idempotency/idempotency_test.go @@ -0,0 +1,177 @@ +//nolint:bodyclose // Much easier to just ignore memory leaks in tests +package idempotency_test + +import ( + "errors" + "io" + "net/http" + "net/http/httptest" + "strconv" + "sync" + "sync/atomic" + "testing" + "time" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/idempotency" + "github.com/gofiber/fiber/v2/utils" + + "github.com/valyala/fasthttp" +) + +// go test -run Test_Idempotency +func Test_Idempotency(t *testing.T) { + t.Parallel() + + app := fiber.New() + + app.Use(func(c *fiber.Ctx) error { + if err := c.Next(); err != nil { + return err + } + + isMethodSafe := fiber.IsMethodSafe(c.Method()) + isIdempotent := idempotency.IsFromCache(c) || idempotency.WasPutToCache(c) + hasReqHeader := c.Get("X-Idempotency-Key") != "" + + if isMethodSafe { + if isIdempotent { + return errors.New("request with safe HTTP method should not be idempotent") + } + } else { + // Unsafe + if hasReqHeader { + if !isIdempotent { + return errors.New("request with unsafe HTTP method should be idempotent if X-Idempotency-Key request header is set") + } + } else { + // No request header + if isIdempotent { + return errors.New("request with unsafe HTTP method should not be idempotent if X-Idempotency-Key request header is not set") + } + } + } + + return nil + }) + + // Needs to be at least a second as the memory storage doesn't support shorter durations. + const lifetime = 1 * time.Second + + app.Use(idempotency.New(idempotency.Config{ + Lifetime: lifetime, + })) + + nextCount := func() func() int { + var count int32 + return func() int { + return int(atomic.AddInt32(&count, 1)) + } + }() + + { + handler := func(c *fiber.Ctx) error { + return c.SendString(strconv.Itoa(nextCount())) + } + + app.Get("/", handler) + app.Post("/", handler) + } + + app.Post("/slow", func(c *fiber.Ctx) error { + time.Sleep(2 * lifetime) + + return c.SendString(strconv.Itoa(nextCount())) + }) + + doReq := func(method, route, idempotencyKey string) string { + req := httptest.NewRequest(method, route, http.NoBody) + if idempotencyKey != "" { + req.Header.Set("X-Idempotency-Key", idempotencyKey) + } + resp, err := app.Test(req, 3*int(lifetime.Milliseconds())) + utils.AssertEqual(t, nil, err) + body, err := io.ReadAll(resp.Body) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, fiber.StatusOK, resp.StatusCode, string(body)) + return string(body) + } + + utils.AssertEqual(t, "1", doReq(fiber.MethodGet, "/", "")) + utils.AssertEqual(t, "2", doReq(fiber.MethodGet, "/", "")) + + utils.AssertEqual(t, "3", doReq(fiber.MethodPost, "/", "")) + utils.AssertEqual(t, "4", doReq(fiber.MethodPost, "/", "")) + + utils.AssertEqual(t, "5", doReq(fiber.MethodGet, "/", "00000000-0000-0000-0000-000000000000")) + utils.AssertEqual(t, "6", doReq(fiber.MethodGet, "/", "00000000-0000-0000-0000-000000000000")) + + utils.AssertEqual(t, "7", doReq(fiber.MethodPost, "/", "00000000-0000-0000-0000-000000000000")) + utils.AssertEqual(t, "7", doReq(fiber.MethodPost, "/", "00000000-0000-0000-0000-000000000000")) + utils.AssertEqual(t, "8", doReq(fiber.MethodPost, "/", "")) + utils.AssertEqual(t, "9", doReq(fiber.MethodPost, "/", "11111111-1111-1111-1111-111111111111")) + + utils.AssertEqual(t, "7", doReq(fiber.MethodPost, "/", "00000000-0000-0000-0000-000000000000")) + time.Sleep(2 * lifetime) + utils.AssertEqual(t, "10", doReq(fiber.MethodPost, "/", "00000000-0000-0000-0000-000000000000")) + utils.AssertEqual(t, "10", doReq(fiber.MethodPost, "/", "00000000-0000-0000-0000-000000000000")) + + // Test raciness + { + var wg sync.WaitGroup + for i := 0; i < 100; i++ { + wg.Add(1) + go func() { + defer wg.Done() + utils.AssertEqual(t, "11", doReq(fiber.MethodPost, "/slow", "22222222-2222-2222-2222-222222222222")) + }() + } + wg.Wait() + utils.AssertEqual(t, "11", doReq(fiber.MethodPost, "/slow", "22222222-2222-2222-2222-222222222222")) + } + time.Sleep(2 * lifetime) + utils.AssertEqual(t, "12", doReq(fiber.MethodPost, "/slow", "22222222-2222-2222-2222-222222222222")) +} + +// go test -v -run=^$ -bench=Benchmark_Idempotency -benchmem -count=4 +func Benchmark_Idempotency(b *testing.B) { + app := fiber.New() + + // Needs to be at least a second as the memory storage doesn't support shorter durations. + const lifetime = 1 * time.Second + + app.Use(idempotency.New(idempotency.Config{ + Lifetime: lifetime, + })) + + app.Post("/", func(c *fiber.Ctx) error { + return nil + }) + + h := app.Handler() + + b.Run("hit", func(b *testing.B) { + c := &fasthttp.RequestCtx{} + c.Request.Header.SetMethod(fiber.MethodPost) + c.Request.SetRequestURI("/") + c.Request.Header.Set("X-Idempotency-Key", "00000000-0000-0000-0000-000000000000") + + b.ReportAllocs() + b.ResetTimer() + for n := 0; n < b.N; n++ { + h(c) + } + }) + + b.Run("skip", func(b *testing.B) { + c := &fasthttp.RequestCtx{} + c.Request.Header.SetMethod(fiber.MethodPost) + c.Request.SetRequestURI("/") + + b.ReportAllocs() + b.ResetTimer() + for n := 0; n < b.N; n++ { + h(c) + } + }) +} diff --git a/middleware/idempotency/locker.go b/middleware/idempotency/locker.go new file mode 100644 index 0000000..bf8bf0e --- /dev/null +++ b/middleware/idempotency/locker.go @@ -0,0 +1,53 @@ +package idempotency + +import ( + "sync" +) + +// Locker implements a spinlock for a string key. +type Locker interface { + Lock(key string) error + Unlock(key string) error +} + +type MemoryLock struct { + mu sync.Mutex + + keys map[string]*sync.Mutex +} + +func (l *MemoryLock) Lock(key string) error { + l.mu.Lock() + mu, ok := l.keys[key] + if !ok { + mu = new(sync.Mutex) + l.keys[key] = mu + } + l.mu.Unlock() + + mu.Lock() + + return nil +} + +func (l *MemoryLock) Unlock(key string) error { + l.mu.Lock() + mu, ok := l.keys[key] + l.mu.Unlock() + if !ok { + // This happens if we try to unlock an unknown key + return nil + } + + mu.Unlock() + + return nil +} + +func NewMemoryLock() *MemoryLock { + return &MemoryLock{ + keys: make(map[string]*sync.Mutex), + } +} + +var _ Locker = (*MemoryLock)(nil) diff --git a/middleware/idempotency/locker_test.go b/middleware/idempotency/locker_test.go new file mode 100644 index 0000000..3063b2b --- /dev/null +++ b/middleware/idempotency/locker_test.go @@ -0,0 +1,59 @@ +package idempotency_test + +import ( + "testing" + "time" + + "github.com/gofiber/fiber/v2/middleware/idempotency" + "github.com/gofiber/fiber/v2/utils" +) + +// go test -run Test_MemoryLock +func Test_MemoryLock(t *testing.T) { + t.Parallel() + + l := idempotency.NewMemoryLock() + + { + err := l.Lock("a") + utils.AssertEqual(t, nil, err) + } + { + done := make(chan struct{}) + go func() { + defer close(done) + + err := l.Lock("a") + utils.AssertEqual(t, nil, err) + }() + + select { + case <-done: + t.Fatal("lock acquired again") + case <-time.After(time.Second): + } + } + + { + err := l.Lock("b") + utils.AssertEqual(t, nil, err) + } + { + err := l.Unlock("b") + utils.AssertEqual(t, nil, err) + } + { + err := l.Lock("b") + utils.AssertEqual(t, nil, err) + } + + { + err := l.Unlock("c") + utils.AssertEqual(t, nil, err) + } + + { + err := l.Lock("d") + utils.AssertEqual(t, nil, err) + } +} diff --git a/middleware/idempotency/response.go b/middleware/idempotency/response.go new file mode 100644 index 0000000..f42d1a3 --- /dev/null +++ b/middleware/idempotency/response.go @@ -0,0 +1,10 @@ +package idempotency + +//go:generate msgp -o=response_msgp.go -io=false -unexported +type response struct { + StatusCode int `msg:"sc"` + + Headers map[string][]string `msg:"hs"` + + Body []byte `msg:"b"` +} diff --git a/middleware/idempotency/response_msgp.go b/middleware/idempotency/response_msgp.go new file mode 100644 index 0000000..410d118 --- /dev/null +++ b/middleware/idempotency/response_msgp.go @@ -0,0 +1,131 @@ +package idempotency + +// Code generated by github.com/tinylib/msgp DO NOT EDIT. + +import ( + "github.com/tinylib/msgp/msgp" +) + +// MarshalMsg implements msgp.Marshaler +func (z *response) MarshalMsg(b []byte) (o []byte, err error) { + o = msgp.Require(b, z.Msgsize()) + // map header, size 3 + // string "sc" + o = append(o, 0x83, 0xa2, 0x73, 0x63) + o = msgp.AppendInt(o, z.StatusCode) + // string "hs" + o = append(o, 0xa2, 0x68, 0x73) + o = msgp.AppendMapHeader(o, uint32(len(z.Headers))) + for za0001, za0002 := range z.Headers { + o = msgp.AppendString(o, za0001) + o = msgp.AppendArrayHeader(o, uint32(len(za0002))) + for za0003 := range za0002 { + o = msgp.AppendString(o, za0002[za0003]) + } + } + // string "b" + o = append(o, 0xa1, 0x62) + o = msgp.AppendBytes(o, z.Body) + return +} + +// UnmarshalMsg implements msgp.Unmarshaler +func (z *response) UnmarshalMsg(bts []byte) (o []byte, err error) { + var field []byte + _ = field + var zb0001 uint32 + zb0001, bts, err = msgp.ReadMapHeaderBytes(bts) + if err != nil { + err = msgp.WrapError(err) + return + } + for zb0001 > 0 { + zb0001-- + field, bts, err = msgp.ReadMapKeyZC(bts) + if err != nil { + err = msgp.WrapError(err) + return + } + switch msgp.UnsafeString(field) { + case "sc": + z.StatusCode, bts, err = msgp.ReadIntBytes(bts) + if err != nil { + err = msgp.WrapError(err, "StatusCode") + return + } + case "hs": + var zb0002 uint32 + zb0002, bts, err = msgp.ReadMapHeaderBytes(bts) + if err != nil { + err = msgp.WrapError(err, "Headers") + return + } + if z.Headers == nil { + z.Headers = make(map[string][]string, zb0002) + } else if len(z.Headers) > 0 { + for key := range z.Headers { + delete(z.Headers, key) + } + } + for zb0002 > 0 { + var za0001 string + var za0002 []string + zb0002-- + za0001, bts, err = msgp.ReadStringBytes(bts) + if err != nil { + err = msgp.WrapError(err, "Headers") + return + } + var zb0003 uint32 + zb0003, bts, err = msgp.ReadArrayHeaderBytes(bts) + if err != nil { + err = msgp.WrapError(err, "Headers", za0001) + return + } + if cap(za0002) >= int(zb0003) { + za0002 = (za0002)[:zb0003] + } else { + za0002 = make([]string, zb0003) + } + for za0003 := range za0002 { + za0002[za0003], bts, err = msgp.ReadStringBytes(bts) + if err != nil { + err = msgp.WrapError(err, "Headers", za0001, za0003) + return + } + } + z.Headers[za0001] = za0002 + } + case "b": + z.Body, bts, err = msgp.ReadBytesBytes(bts, z.Body) + if err != nil { + err = msgp.WrapError(err, "Body") + return + } + default: + bts, err = msgp.Skip(bts) + if err != nil { + err = msgp.WrapError(err) + return + } + } + } + o = bts + return +} + +// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message +func (z *response) Msgsize() (s int) { + s = 1 + 3 + msgp.IntSize + 3 + msgp.MapHeaderSize + if z.Headers != nil { + for za0001, za0002 := range z.Headers { + _ = za0002 + s += msgp.StringPrefixSize + len(za0001) + msgp.ArrayHeaderSize + for za0003 := range za0002 { + s += msgp.StringPrefixSize + len(za0002[za0003]) + } + } + } + s += 2 + msgp.BytesPrefixSize + len(z.Body) + return +} diff --git a/middleware/idempotency/response_msgp_test.go b/middleware/idempotency/response_msgp_test.go new file mode 100644 index 0000000..cf41da4 --- /dev/null +++ b/middleware/idempotency/response_msgp_test.go @@ -0,0 +1,67 @@ +package idempotency + +// Code generated by github.com/tinylib/msgp DO NOT EDIT. + +import ( + "testing" + + "github.com/tinylib/msgp/msgp" +) + +func TestMarshalUnmarshalresponse(t *testing.T) { + v := response{} + bts, err := v.MarshalMsg(nil) + if err != nil { + t.Fatal(err) + } + left, err := v.UnmarshalMsg(bts) + if err != nil { + t.Fatal(err) + } + if len(left) > 0 { + t.Errorf("%d bytes left over after UnmarshalMsg(): %q", len(left), left) + } + + left, err = msgp.Skip(bts) + if err != nil { + t.Fatal(err) + } + if len(left) > 0 { + t.Errorf("%d bytes left over after Skip(): %q", len(left), left) + } +} + +func BenchmarkMarshalMsgresponse(b *testing.B) { + v := response{} + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + v.MarshalMsg(nil) + } +} + +func BenchmarkAppendMsgresponse(b *testing.B) { + v := response{} + bts := make([]byte, 0, v.Msgsize()) + bts, _ = v.MarshalMsg(bts[0:0]) + b.SetBytes(int64(len(bts))) + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + bts, _ = v.MarshalMsg(bts[0:0]) + } +} + +func BenchmarkUnmarshalresponse(b *testing.B) { + v := response{} + bts, _ := v.MarshalMsg(nil) + b.ReportAllocs() + b.SetBytes(int64(len(bts))) + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, err := v.UnmarshalMsg(bts) + if err != nil { + b.Fatal(err) + } + } +} diff --git a/middleware/keyauth/config.go b/middleware/keyauth/config.go new file mode 100644 index 0000000..c762d72 --- /dev/null +++ b/middleware/keyauth/config.go @@ -0,0 +1,95 @@ +package keyauth + +import ( + "errors" + + "github.com/gofiber/fiber/v2" +) + +// Config defines the config for middleware. +type Config struct { + // Next defines a function to skip middleware. + // Optional. Default: nil + Next func(*fiber.Ctx) bool + + // SuccessHandler defines a function which is executed for a valid key. + // Optional. Default: nil + SuccessHandler fiber.Handler + + // ErrorHandler defines a function which is executed for an invalid key. + // It may be used to define a custom error. + // Optional. Default: 401 Invalid or expired key + ErrorHandler fiber.ErrorHandler + + // KeyLookup is a string in the form of ":" that is used + // to extract key from the request. + // Optional. Default value "header:Authorization". + // Possible values: + // - "header:" + // - "query:" + // - "form:" + // - "param:" + // - "cookie:" + KeyLookup string + + // AuthScheme to be used in the Authorization header. + // Optional. Default value "Bearer". + AuthScheme string + + // Validator is a function to validate key. + Validator func(*fiber.Ctx, string) (bool, error) + + // Context key to store the bearertoken from the token into context. + // Optional. Default: "token". + ContextKey interface{} +} + +// ConfigDefault is the default config +var ConfigDefault = Config{ + SuccessHandler: func(c *fiber.Ctx) error { + return c.Next() + }, + ErrorHandler: func(c *fiber.Ctx, err error) error { + if errors.Is(err, ErrMissingOrMalformedAPIKey) { + return c.Status(fiber.StatusUnauthorized).SendString(err.Error()) + } + return c.Status(fiber.StatusUnauthorized).SendString("Invalid or expired API Key") + }, + KeyLookup: "header:" + fiber.HeaderAuthorization, + AuthScheme: "Bearer", + ContextKey: "token", +} + +// Helper function to set default values +func configDefault(config ...Config) Config { + // Return default config if nothing provided + if len(config) < 1 { + return ConfigDefault + } + + // Override default config + cfg := config[0] + + // Set default values + if cfg.SuccessHandler == nil { + cfg.SuccessHandler = ConfigDefault.SuccessHandler + } + if cfg.ErrorHandler == nil { + cfg.ErrorHandler = ConfigDefault.ErrorHandler + } + if cfg.KeyLookup == "" { + cfg.KeyLookup = ConfigDefault.KeyLookup + // set AuthScheme as "Bearer" only if KeyLookup is set to default. + if cfg.AuthScheme == "" { + cfg.AuthScheme = ConfigDefault.AuthScheme + } + } + if cfg.Validator == nil { + panic("fiber: keyauth middleware requires a validator function") + } + if cfg.ContextKey == nil { + cfg.ContextKey = ConfigDefault.ContextKey + } + + return cfg +} diff --git a/middleware/keyauth/keyauth.go b/middleware/keyauth/keyauth.go new file mode 100644 index 0000000..ce18524 --- /dev/null +++ b/middleware/keyauth/keyauth.go @@ -0,0 +1,121 @@ +// Special thanks to Echo: https://github.com/labstack/echo/blob/master/middleware/key_auth.go +package keyauth + +import ( + "errors" + "net/url" + "strings" + + "github.com/gofiber/fiber/v2" +) + +// When there is no request of the key thrown ErrMissingOrMalformedAPIKey +var ErrMissingOrMalformedAPIKey = errors.New("missing or malformed API Key") + +const ( + query = "query" + form = "form" + param = "param" + cookie = "cookie" +) + +// New creates a new middleware handler +func New(config ...Config) fiber.Handler { + // Init config + cfg := configDefault(config...) + + // Initialize + parts := strings.Split(cfg.KeyLookup, ":") + extractor := keyFromHeader(parts[1], cfg.AuthScheme) + switch parts[0] { + case query: + extractor = keyFromQuery(parts[1]) + case form: + extractor = keyFromForm(parts[1]) + case param: + extractor = keyFromParam(parts[1]) + case cookie: + extractor = keyFromCookie(parts[1]) + } + + // Return middleware handler + return func(c *fiber.Ctx) error { + // Filter request to skip middleware + if cfg.Next != nil && cfg.Next(c) { + return c.Next() + } + + // Extract and verify key + key, err := extractor(c) + if err != nil { + return cfg.ErrorHandler(c, err) + } + + valid, err := cfg.Validator(c, key) + + if err == nil && valid { + c.Locals(cfg.ContextKey, key) + return cfg.SuccessHandler(c) + } + return cfg.ErrorHandler(c, err) + } +} + +// keyFromHeader returns a function that extracts api key from the request header. +func keyFromHeader(header, authScheme string) func(c *fiber.Ctx) (string, error) { + return func(c *fiber.Ctx) (string, error) { + auth := c.Get(header) + l := len(authScheme) + if len(auth) > 0 && l == 0 { + return auth, nil + } + if len(auth) > l+1 && auth[:l] == authScheme { + return auth[l+1:], nil + } + return "", ErrMissingOrMalformedAPIKey + } +} + +// keyFromQuery returns a function that extracts api key from the query string. +func keyFromQuery(param string) func(c *fiber.Ctx) (string, error) { + return func(c *fiber.Ctx) (string, error) { + key := c.Query(param) + if key == "" { + return "", ErrMissingOrMalformedAPIKey + } + return key, nil + } +} + +// keyFromForm returns a function that extracts api key from the form. +func keyFromForm(param string) func(c *fiber.Ctx) (string, error) { + return func(c *fiber.Ctx) (string, error) { + key := c.FormValue(param) + if key == "" { + return "", ErrMissingOrMalformedAPIKey + } + return key, nil + } +} + +// keyFromParam returns a function that extracts api key from the url param string. +func keyFromParam(param string) func(c *fiber.Ctx) (string, error) { + return func(c *fiber.Ctx) (string, error) { + key, err := url.PathUnescape(c.Params(param)) + if err != nil { + return "", ErrMissingOrMalformedAPIKey + } + return key, nil + } +} + +// keyFromCookie returns a function that extracts api key from the named cookie. +func keyFromCookie(name string) func(c *fiber.Ctx) (string, error) { + return func(c *fiber.Ctx) (string, error) { + key := c.Cookies(name) + if key == "" { + return "", ErrMissingOrMalformedAPIKey + } + return key, nil + } +} diff --git a/middleware/keyauth/keyauth_test.go b/middleware/keyauth/keyauth_test.go new file mode 100644 index 0000000..9d9b339 --- /dev/null +++ b/middleware/keyauth/keyauth_test.go @@ -0,0 +1,461 @@ +//nolint:bodyclose // Much easier to just ignore memory leaks in tests +package keyauth + +import ( + "context" + "fmt" + "io" + "net/http" + "net/http/httptest" + "net/url" + "testing" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/utils" +) + +const CorrectKey = "specials: !$%,.#\"!?~`<>@$^*(){}[]|/\\123" + +func TestAuthSources(t *testing.T) { + // define test cases + testSources := []string{"header", "cookie", "query", "param", "form"} + + tests := []struct { + route string + authTokenName string + description string + APIKey string + expectedCode int + expectedBody string + }{ + { + route: "/", + authTokenName: "access_token", + description: "auth with correct key", + APIKey: CorrectKey, + expectedCode: 200, + expectedBody: "Success!", + }, + { + route: "/", + authTokenName: "access_token", + description: "auth with no key", + APIKey: "", + expectedCode: 401, // 404 in case of param authentication + expectedBody: "missing or malformed API Key", + }, + { + route: "/", + authTokenName: "access_token", + description: "auth with wrong key", + APIKey: "WRONGKEY", + expectedCode: 401, + expectedBody: "missing or malformed API Key", + }, + } + + for _, authSource := range testSources { + t.Run(authSource, func(t *testing.T) { + for _, test := range tests { + // setup the fiber endpoint + // note that if UnescapePath: false (the default) + // escaped characters (such as `\"`) will not be handled correctly in the tests + app := fiber.New(fiber.Config{UnescapePath: true}) + + authMiddleware := New(Config{ + KeyLookup: authSource + ":" + test.authTokenName, + Validator: func(c *fiber.Ctx, key string) (bool, error) { + if key == CorrectKey { + return true, nil + } + return false, ErrMissingOrMalformedAPIKey + }, + }) + + var route string + if authSource == param { + route = test.route + ":" + test.authTokenName + app.Use(route, authMiddleware) + } else { + route = test.route + app.Use(authMiddleware) + } + + app.Get(route, func(c *fiber.Ctx) error { + return c.SendString("Success!") + }) + + // construct the test HTTP request + var req *http.Request + req, err := http.NewRequestWithContext(context.Background(), fiber.MethodGet, test.route, nil) + utils.AssertEqual(t, err, nil) + + // setup the apikey for the different auth schemes + if authSource == "header" { + req.Header.Set(test.authTokenName, test.APIKey) + } else if authSource == "cookie" { + req.Header.Set("Cookie", test.authTokenName+"="+test.APIKey) + } else if authSource == "query" || authSource == "form" { + q := req.URL.Query() + q.Add(test.authTokenName, test.APIKey) + req.URL.RawQuery = q.Encode() + } else if authSource == "param" { + r := req.URL.Path + r += url.PathEscape(test.APIKey) + req.URL.Path = r + } + + res, err := app.Test(req, -1) + + utils.AssertEqual(t, nil, err, test.description) + + // test the body of the request + body, err := io.ReadAll(res.Body) + // for param authentication, the route would be /:access_token + // when the access_token is empty, it leads to a 404 (not found) + // not a 401 (auth error) + if authSource == "param" && test.APIKey == "" { + test.expectedCode = 404 + test.expectedBody = "Cannot GET /" + } + utils.AssertEqual(t, test.expectedCode, res.StatusCode, test.description) + + // body + utils.AssertEqual(t, nil, err, test.description) + utils.AssertEqual(t, test.expectedBody, string(body), test.description) + + err = res.Body.Close() + utils.AssertEqual(t, err, nil) + } + }) + } +} + +func TestMultipleKeyAuth(t *testing.T) { + // setup the fiber endpoint + app := fiber.New() + + // setup keyauth for /auth1 + app.Use(New(Config{ + Next: func(c *fiber.Ctx) bool { + return c.OriginalURL() != "/auth1" + }, + KeyLookup: "header:key", + Validator: func(c *fiber.Ctx, key string) (bool, error) { + if key == "password1" { + return true, nil + } + return false, ErrMissingOrMalformedAPIKey + }, + })) + + // setup keyauth for /auth2 + app.Use(New(Config{ + Next: func(c *fiber.Ctx) bool { + return c.OriginalURL() != "/auth2" + }, + KeyLookup: "header:key", + Validator: func(c *fiber.Ctx, key string) (bool, error) { + if key == "password2" { + return true, nil + } + return false, ErrMissingOrMalformedAPIKey + }, + })) + + app.Get("/", func(c *fiber.Ctx) error { + return c.SendString("No auth needed!") + }) + + app.Get("/auth1", func(c *fiber.Ctx) error { + return c.SendString("Successfully authenticated for auth1!") + }) + + app.Get("/auth2", func(c *fiber.Ctx) error { + return c.SendString("Successfully authenticated for auth2!") + }) + + // define test cases + tests := []struct { + route string + description string + APIKey string + expectedCode int + expectedBody string + }{ + // No auth needed for / + { + route: "/", + description: "No password needed", + APIKey: "", + expectedCode: 200, + expectedBody: "No auth needed!", + }, + + // auth needed for auth1 + { + route: "/auth1", + description: "Normal Authentication Case", + APIKey: "password1", + expectedCode: 200, + expectedBody: "Successfully authenticated for auth1!", + }, + { + route: "/auth1", + description: "Wrong API Key", + APIKey: "WRONG KEY", + expectedCode: 401, + expectedBody: "missing or malformed API Key", + }, + { + route: "/auth1", + description: "Wrong API Key", + APIKey: "", // NO KEY + expectedCode: 401, + expectedBody: "missing or malformed API Key", + }, + + // Auth 2 has a different password + { + route: "/auth2", + description: "Normal Authentication Case for auth2", + APIKey: "password2", + expectedCode: 200, + expectedBody: "Successfully authenticated for auth2!", + }, + { + route: "/auth2", + description: "Wrong API Key", + APIKey: "WRONG KEY", + expectedCode: 401, + expectedBody: "missing or malformed API Key", + }, + { + route: "/auth2", + description: "Wrong API Key", + APIKey: "", // NO KEY + expectedCode: 401, + expectedBody: "missing or malformed API Key", + }, + } + + // run the tests + for _, test := range tests { + var req *http.Request + req, err := http.NewRequestWithContext(context.Background(), fiber.MethodGet, test.route, nil) + utils.AssertEqual(t, err, nil) + if test.APIKey != "" { + req.Header.Set("key", test.APIKey) + } + + res, err := app.Test(req, -1) + + utils.AssertEqual(t, nil, err, test.description) + + // test the body of the request + body, err := io.ReadAll(res.Body) + utils.AssertEqual(t, test.expectedCode, res.StatusCode, test.description) + + // body + utils.AssertEqual(t, nil, err, test.description) + utils.AssertEqual(t, test.expectedBody, string(body), test.description) + } +} + +func TestCustomSuccessAndFailureHandlers(t *testing.T) { + app := fiber.New() + + app.Use(New(Config{ + SuccessHandler: func(c *fiber.Ctx) error { + return c.Status(fiber.StatusOK).SendString("API key is valid and request was handled by custom success handler") + }, + ErrorHandler: func(c *fiber.Ctx, err error) error { + return c.Status(fiber.StatusUnauthorized).SendString("API key is invalid and request was handled by custom error handler") + }, + Validator: func(c *fiber.Ctx, key string) (bool, error) { + if key == CorrectKey { + return true, nil + } + return false, ErrMissingOrMalformedAPIKey + }, + })) + + // Define a test handler that should not be called + app.Get("/", func(c *fiber.Ctx) error { + t.Error("Test handler should not be called") + return nil + }) + + // Create a request without an API key and send it to the app + res, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) + utils.AssertEqual(t, err, nil) + + // Read the response body into a string + body, err := io.ReadAll(res.Body) + utils.AssertEqual(t, err, nil) + + // Check that the response has the expected status code and body + utils.AssertEqual(t, res.StatusCode, http.StatusUnauthorized) + utils.AssertEqual(t, string(body), "API key is invalid and request was handled by custom error handler") + + // Create a request with a valid API key in the Authorization header + req := httptest.NewRequest(fiber.MethodGet, "/", nil) + req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", CorrectKey)) + + // Send the request to the app + res, err = app.Test(req) + utils.AssertEqual(t, err, nil) + + // Read the response body into a string + body, err = io.ReadAll(res.Body) + utils.AssertEqual(t, err, nil) + + // Check that the response has the expected status code and body + utils.AssertEqual(t, res.StatusCode, http.StatusOK) + utils.AssertEqual(t, string(body), "API key is valid and request was handled by custom success handler") +} + +func TestCustomNextFunc(t *testing.T) { + app := fiber.New() + + app.Use(New(Config{ + Next: func(c *fiber.Ctx) bool { + return c.Path() == "/allowed" + }, + Validator: func(c *fiber.Ctx, key string) (bool, error) { + if key == CorrectKey { + return true, nil + } + return false, ErrMissingOrMalformedAPIKey + }, + })) + + // Define a test handler + app.Get("/allowed", func(c *fiber.Ctx) error { + return c.SendString("API key is valid and request was allowed by custom filter") + }) + + // Create a request with the "/allowed" path and send it to the app + req := httptest.NewRequest(fiber.MethodGet, "/allowed", nil) + res, err := app.Test(req) + utils.AssertEqual(t, err, nil) + + // Read the response body into a string + body, err := io.ReadAll(res.Body) + utils.AssertEqual(t, err, nil) + + // Check that the response has the expected status code and body + utils.AssertEqual(t, res.StatusCode, http.StatusOK) + utils.AssertEqual(t, string(body), "API key is valid and request was allowed by custom filter") + + // Create a request with a different path and send it to the app without correct key + req = httptest.NewRequest(fiber.MethodGet, "/not-allowed", nil) + res, err = app.Test(req) + utils.AssertEqual(t, err, nil) + + // Read the response body into a string + body, err = io.ReadAll(res.Body) + utils.AssertEqual(t, err, nil) + + // Check that the response has the expected status code and body + utils.AssertEqual(t, res.StatusCode, http.StatusUnauthorized) + utils.AssertEqual(t, string(body), ErrMissingOrMalformedAPIKey.Error()) + + // Create a request with a different path and send it to the app with correct key + req = httptest.NewRequest(fiber.MethodGet, "/not-allowed", nil) + req.Header.Add("Authorization", fmt.Sprintf("Basic %s", CorrectKey)) + + res, err = app.Test(req) + utils.AssertEqual(t, err, nil) + + // Read the response body into a string + body, err = io.ReadAll(res.Body) + utils.AssertEqual(t, err, nil) + + // Check that the response has the expected status code and body + utils.AssertEqual(t, res.StatusCode, http.StatusUnauthorized) + utils.AssertEqual(t, string(body), ErrMissingOrMalformedAPIKey.Error()) +} + +func TestAuthSchemeToken(t *testing.T) { + app := fiber.New() + + app.Use(New(Config{ + AuthScheme: "Token", + Validator: func(c *fiber.Ctx, key string) (bool, error) { + if key == CorrectKey { + return true, nil + } + return false, ErrMissingOrMalformedAPIKey + }, + })) + + // Define a test handler + app.Get("/", func(c *fiber.Ctx) error { + return c.SendString("API key is valid") + }) + + // Create a request with a valid API key in the "Token" Authorization header + req := httptest.NewRequest(fiber.MethodGet, "/", nil) + req.Header.Add("Authorization", fmt.Sprintf("Token %s", CorrectKey)) + + // Send the request to the app + res, err := app.Test(req) + utils.AssertEqual(t, err, nil) + + // Read the response body into a string + body, err := io.ReadAll(res.Body) + utils.AssertEqual(t, err, nil) + + // Check that the response has the expected status code and body + utils.AssertEqual(t, res.StatusCode, http.StatusOK) + utils.AssertEqual(t, string(body), "API key is valid") +} + +func TestAuthSchemeBasic(t *testing.T) { + app := fiber.New() + + app.Use(New(Config{ + KeyLookup: "header:Authorization", + AuthScheme: "Basic", + Validator: func(c *fiber.Ctx, key string) (bool, error) { + if key == CorrectKey { + return true, nil + } + return false, ErrMissingOrMalformedAPIKey + }, + })) + + // Define a test handler + app.Get("/", func(c *fiber.Ctx) error { + return c.SendString("API key is valid") + }) + + // Create a request without an API key and Send the request to the app + res, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) + utils.AssertEqual(t, err, nil) + + // Read the response body into a string + body, err := io.ReadAll(res.Body) + utils.AssertEqual(t, err, nil) + + // Check that the response has the expected status code and body + utils.AssertEqual(t, res.StatusCode, http.StatusUnauthorized) + utils.AssertEqual(t, string(body), ErrMissingOrMalformedAPIKey.Error()) + + // Create a request with a valid API key in the "Authorization" header using the "Basic" scheme + req := httptest.NewRequest(fiber.MethodGet, "/", nil) + req.Header.Add("Authorization", fmt.Sprintf("Basic %s", CorrectKey)) + + // Send the request to the app + res, err = app.Test(req) + utils.AssertEqual(t, err, nil) + + // Read the response body into a string + body, err = io.ReadAll(res.Body) + utils.AssertEqual(t, err, nil) + + // Check that the response has the expected status code and body + utils.AssertEqual(t, res.StatusCode, http.StatusOK) + utils.AssertEqual(t, string(body), "API key is valid") +} diff --git a/middleware/limiter/config.go b/middleware/limiter/config.go new file mode 100644 index 0000000..5ec826d --- /dev/null +++ b/middleware/limiter/config.go @@ -0,0 +1,128 @@ +package limiter + +import ( + "time" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/log" +) + +// Config defines the config for middleware. +type Config struct { + // Next defines a function to skip this middleware when returned true. + // + // Optional. Default: nil + Next func(c *fiber.Ctx) bool + + // Max number of recent connections during `Expiration` seconds before sending a 429 response + // + // Default: 5 + Max int + + // KeyGenerator allows you to generate custom keys, by default c.IP() is used + // + // Default: func(c *fiber.Ctx) string { + // return c.IP() + // } + KeyGenerator func(*fiber.Ctx) string + + // Expiration is the time on how long to keep records of requests in memory + // + // Default: 1 * time.Minute + Expiration time.Duration + + // LimitReached is called when a request hits the limit + // + // Default: func(c *fiber.Ctx) error { + // return c.SendStatus(fiber.StatusTooManyRequests) + // } + LimitReached fiber.Handler + + // When set to true, requests with StatusCode >= 400 won't be counted. + // + // Default: false + SkipFailedRequests bool + + // When set to true, requests with StatusCode < 400 won't be counted. + // + // Default: false + SkipSuccessfulRequests bool + + // Store is used to store the state of the middleware + // + // Default: an in memory store for this process only + Storage fiber.Storage + + // LimiterMiddleware is the struct that implements a limiter middleware. + // + // Default: a new Fixed Window Rate Limiter + LimiterMiddleware LimiterHandler + + // Deprecated: Use Expiration instead + Duration time.Duration + + // Deprecated: Use Storage instead + Store fiber.Storage + + // Deprecated: Use KeyGenerator instead + Key func(*fiber.Ctx) string +} + +// ConfigDefault is the default config +var ConfigDefault = Config{ + Max: 5, + Expiration: 1 * time.Minute, + KeyGenerator: func(c *fiber.Ctx) string { + return c.IP() + }, + LimitReached: func(c *fiber.Ctx) error { + return c.SendStatus(fiber.StatusTooManyRequests) + }, + SkipFailedRequests: false, + SkipSuccessfulRequests: false, + LimiterMiddleware: FixedWindow{}, +} + +// Helper function to set default values +func configDefault(config ...Config) Config { + // Return default config if nothing provided + if len(config) < 1 { + return ConfigDefault + } + + // Override default config + cfg := config[0] + + // Set default values + if int(cfg.Duration.Seconds()) > 0 { + log.Warn("[LIMITER] Duration is deprecated, please use Expiration") + cfg.Expiration = cfg.Duration + } + if cfg.Key != nil { + log.Warn("[LIMITER] Key is deprecated, please us KeyGenerator") + cfg.KeyGenerator = cfg.Key + } + if cfg.Store != nil { + log.Warn("[LIMITER] Store is deprecated, please use Storage") + cfg.Storage = cfg.Store + } + if cfg.Next == nil { + cfg.Next = ConfigDefault.Next + } + if cfg.Max <= 0 { + cfg.Max = ConfigDefault.Max + } + if int(cfg.Expiration.Seconds()) <= 0 { + cfg.Expiration = ConfigDefault.Expiration + } + if cfg.KeyGenerator == nil { + cfg.KeyGenerator = ConfigDefault.KeyGenerator + } + if cfg.LimitReached == nil { + cfg.LimitReached = ConfigDefault.LimitReached + } + if cfg.LimiterMiddleware == nil { + cfg.LimiterMiddleware = ConfigDefault.LimiterMiddleware + } + return cfg +} diff --git a/middleware/limiter/limiter.go b/middleware/limiter/limiter.go new file mode 100644 index 0000000..b6da1e0 --- /dev/null +++ b/middleware/limiter/limiter.go @@ -0,0 +1,25 @@ +package limiter + +import ( + "github.com/gofiber/fiber/v2" +) + +const ( + // X-RateLimit-* headers + xRateLimitLimit = "X-RateLimit-Limit" + xRateLimitRemaining = "X-RateLimit-Remaining" + xRateLimitReset = "X-RateLimit-Reset" +) + +type LimiterHandler interface { + New(config Config) fiber.Handler +} + +// New creates a new middleware handler +func New(config ...Config) fiber.Handler { + // Set default config + cfg := configDefault(config...) + + // Return the specified middleware handler. + return cfg.LimiterMiddleware.New(cfg) +} diff --git a/middleware/limiter/limiter_fixed.go b/middleware/limiter/limiter_fixed.go new file mode 100644 index 0000000..b6b6d35 --- /dev/null +++ b/middleware/limiter/limiter_fixed.go @@ -0,0 +1,106 @@ +package limiter + +import ( + "strconv" + "sync" + "sync/atomic" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/utils" +) + +type FixedWindow struct{} + +// New creates a new fixed window middleware handler +func (FixedWindow) New(cfg Config) fiber.Handler { + var ( + // Limiter variables + mux = &sync.RWMutex{} + max = strconv.Itoa(cfg.Max) + expiration = uint64(cfg.Expiration.Seconds()) + ) + + // Create manager to simplify storage operations ( see manager.go ) + manager := newManager(cfg.Storage) + + // Update timestamp every second + utils.StartTimeStampUpdater() + + // Return new handler + return func(c *fiber.Ctx) error { + // Don't execute middleware if Next returns true + if cfg.Next != nil && cfg.Next(c) { + return c.Next() + } + + // Get key from request + key := cfg.KeyGenerator(c) + + // Lock entry + mux.Lock() + + // Get entry from pool and release when finished + e := manager.get(key) + + // Get timestamp + ts := uint64(atomic.LoadUint32(&utils.Timestamp)) + + // Set expiration if entry does not exist + if e.exp == 0 { + e.exp = ts + expiration + } else if ts >= e.exp { + // Check if entry is expired + e.currHits = 0 + e.exp = ts + expiration + } + + // Increment hits + e.currHits++ + + // Calculate when it resets in seconds + resetInSec := e.exp - ts + + // Set how many hits we have left + remaining := cfg.Max - e.currHits + + // Update storage + manager.set(key, e, cfg.Expiration) + + // Unlock entry + mux.Unlock() + + // Check if hits exceed the cfg.Max + if remaining < 0 { + // Return response with Retry-After header + // https://tools.ietf.org/html/rfc6584 + c.Set(fiber.HeaderRetryAfter, strconv.FormatUint(resetInSec, 10)) + + // Call LimitReached handler + return cfg.LimitReached(c) + } + + // Continue stack for reaching c.Response().StatusCode() + // Store err for returning + err := c.Next() + + // Check for SkipFailedRequests and SkipSuccessfulRequests + if (cfg.SkipSuccessfulRequests && c.Response().StatusCode() < fiber.StatusBadRequest) || + (cfg.SkipFailedRequests && c.Response().StatusCode() >= fiber.StatusBadRequest) { + // Lock entry + mux.Lock() + e = manager.get(key) + e.currHits-- + remaining++ + manager.set(key, e, cfg.Expiration) + // Unlock entry + mux.Unlock() + } + + // We can continue, update RateLimit headers + c.Set(xRateLimitLimit, max) + c.Set(xRateLimitRemaining, strconv.Itoa(remaining)) + c.Set(xRateLimitReset, strconv.FormatUint(resetInSec, 10)) + + return err + } +} diff --git a/middleware/limiter/limiter_sliding.go b/middleware/limiter/limiter_sliding.go new file mode 100644 index 0000000..5043486 --- /dev/null +++ b/middleware/limiter/limiter_sliding.go @@ -0,0 +1,137 @@ +package limiter + +import ( + "strconv" + "sync" + "sync/atomic" + "time" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/utils" +) + +type SlidingWindow struct{} + +// New creates a new sliding window middleware handler +func (SlidingWindow) New(cfg Config) fiber.Handler { + var ( + // Limiter variables + mux = &sync.RWMutex{} + max = strconv.Itoa(cfg.Max) + expiration = uint64(cfg.Expiration.Seconds()) + ) + + // Create manager to simplify storage operations ( see manager.go ) + manager := newManager(cfg.Storage) + + // Update timestamp every second + utils.StartTimeStampUpdater() + + // Return new handler + return func(c *fiber.Ctx) error { + // Don't execute middleware if Next returns true + if cfg.Next != nil && cfg.Next(c) { + return c.Next() + } + + // Get key from request + key := cfg.KeyGenerator(c) + + // Lock entry + mux.Lock() + + // Get entry from pool and release when finished + e := manager.get(key) + + // Get timestamp + ts := uint64(atomic.LoadUint32(&utils.Timestamp)) + + // Set expiration if entry does not exist + if e.exp == 0 { + e.exp = ts + expiration + } else if ts >= e.exp { + // The entry has expired, handle the expiration. + // Set the prevHits to the current hits and reset the hits to 0. + e.prevHits = e.currHits + + // Reset the current hits to 0. + e.currHits = 0 + + // Check how much into the current window it currently is and sets the + // expiry based on that, otherwise this would only reset on + // the next request and not show the correct expiry. + elapsed := ts - e.exp + if elapsed >= expiration { + e.exp = ts + expiration + } else { + e.exp = ts + expiration - elapsed + } + } + + // Increment hits + e.currHits++ + + // Calculate when it resets in seconds + resetInSec := e.exp - ts + + // weight = time until current window reset / total window length + weight := float64(resetInSec) / float64(expiration) + + // rate = request count in previous window - weight + request count in current window + rate := int(float64(e.prevHits)*weight) + e.currHits + + // Calculate how many hits can be made based on the current rate + remaining := cfg.Max - rate + + // Update storage. Garbage collect when the next window ends. + // |--------------------------|--------------------------| + // ^ ^ ^ ^ + // ts e.exp End sample window End next window + // <------------> + // resetInSec + // resetInSec = e.exp - ts - time until end of current window. + // duration + expiration = end of next window. + // Because we don't want to garbage collect in the middle of a window + // we add the expiration to the duration. + // Otherwise after the end of "sample window", attackers could launch + // a new request with the full window length. + manager.set(key, e, time.Duration(resetInSec+expiration)*time.Second) + + // Unlock entry + mux.Unlock() + + // Check if hits exceed the cfg.Max + if remaining < 0 { + // Return response with Retry-After header + // https://tools.ietf.org/html/rfc6584 + c.Set(fiber.HeaderRetryAfter, strconv.FormatUint(resetInSec, 10)) + + // Call LimitReached handler + return cfg.LimitReached(c) + } + + // Continue stack for reaching c.Response().StatusCode() + // Store err for returning + err := c.Next() + + // Check for SkipFailedRequests and SkipSuccessfulRequests + if (cfg.SkipSuccessfulRequests && c.Response().StatusCode() < fiber.StatusBadRequest) || + (cfg.SkipFailedRequests && c.Response().StatusCode() >= fiber.StatusBadRequest) { + // Lock entry + mux.Lock() + e = manager.get(key) + e.currHits-- + remaining++ + manager.set(key, e, cfg.Expiration) + // Unlock entry + mux.Unlock() + } + + // We can continue, update RateLimit headers + c.Set(xRateLimitLimit, max) + c.Set(xRateLimitRemaining, strconv.Itoa(remaining)) + c.Set(xRateLimitReset, strconv.FormatUint(resetInSec, 10)) + + return err + } +} diff --git a/middleware/limiter/limiter_test.go b/middleware/limiter/limiter_test.go new file mode 100644 index 0000000..5392982 --- /dev/null +++ b/middleware/limiter/limiter_test.go @@ -0,0 +1,727 @@ +package limiter + +import ( + "io" + "net/http/httptest" + "sync" + "testing" + "time" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/internal/storage/memory" + "github.com/gofiber/fiber/v2/utils" + + "github.com/valyala/fasthttp" +) + +// go test -run Test_Limiter_Concurrency_Store -race -v +func Test_Limiter_Concurrency_Store(t *testing.T) { + t.Parallel() + // Test concurrency using a custom store + + app := fiber.New() + + app.Use(New(Config{ + Max: 50, + Expiration: 2 * time.Second, + Storage: memory.New(), + })) + + app.Get("/", func(c *fiber.Ctx) error { + return c.SendString("Hello tester!") + }) + + var wg sync.WaitGroup + singleRequest := func(wg *sync.WaitGroup) { + defer wg.Done() + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, fiber.StatusOK, resp.StatusCode) + + body, err := io.ReadAll(resp.Body) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, "Hello tester!", string(body)) + } + + for i := 0; i <= 49; i++ { + wg.Add(1) + go singleRequest(&wg) + } + + wg.Wait() + + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, 429, resp.StatusCode) + + time.Sleep(3 * time.Second) + + resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, 200, resp.StatusCode) +} + +// go test -run Test_Limiter_Concurrency -race -v +func Test_Limiter_Concurrency(t *testing.T) { + t.Parallel() + // Test concurrency using a default store + + app := fiber.New() + + app.Use(New(Config{ + Max: 50, + Expiration: 2 * time.Second, + })) + + app.Get("/", func(c *fiber.Ctx) error { + return c.SendString("Hello tester!") + }) + + var wg sync.WaitGroup + singleRequest := func(wg *sync.WaitGroup) { + defer wg.Done() + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, fiber.StatusOK, resp.StatusCode) + + body, err := io.ReadAll(resp.Body) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, "Hello tester!", string(body)) + } + + for i := 0; i <= 49; i++ { + wg.Add(1) + go singleRequest(&wg) + } + + wg.Wait() + + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, 429, resp.StatusCode) + + time.Sleep(3 * time.Second) + + resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, 200, resp.StatusCode) +} + +// go test -run Test_Limiter_Fixed_Window_No_Skip_Choices -v +func Test_Limiter_Fixed_Window_No_Skip_Choices(t *testing.T) { + t.Parallel() + app := fiber.New() + + app.Use(New(Config{ + Max: 2, + Expiration: 2 * time.Second, + SkipFailedRequests: false, + SkipSuccessfulRequests: false, + LimiterMiddleware: FixedWindow{}, + })) + + app.Get("/:status", func(c *fiber.Ctx) error { + if c.Params("status") == "fail" { //nolint:goconst // False positive + return c.SendStatus(400) + } + return c.SendStatus(200) + }) + + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/fail", nil)) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, 400, resp.StatusCode) + + resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/success", nil)) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, 200, resp.StatusCode) + + resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/success", nil)) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, 429, resp.StatusCode) + + time.Sleep(3 * time.Second) + + resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/success", nil)) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, 200, resp.StatusCode) +} + +// go test -run Test_Limiter_Fixed_Window_Custom_Storage_No_Skip_Choices -v +func Test_Limiter_Fixed_Window_Custom_Storage_No_Skip_Choices(t *testing.T) { + t.Parallel() + app := fiber.New() + + app.Use(New(Config{ + Max: 2, + Expiration: 2 * time.Second, + SkipFailedRequests: false, + SkipSuccessfulRequests: false, + Storage: memory.New(), + LimiterMiddleware: FixedWindow{}, + })) + + app.Get("/:status", func(c *fiber.Ctx) error { + if c.Params("status") == "fail" { + return c.SendStatus(400) + } + return c.SendStatus(200) + }) + + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/fail", nil)) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, 400, resp.StatusCode) + + resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/success", nil)) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, 200, resp.StatusCode) + + resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/success", nil)) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, 429, resp.StatusCode) + + time.Sleep(3 * time.Second) + + resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/success", nil)) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, 200, resp.StatusCode) +} + +// go test -run Test_Limiter_Sliding_Window_No_Skip_Choices -v +func Test_Limiter_Sliding_Window_No_Skip_Choices(t *testing.T) { + t.Parallel() + app := fiber.New() + + app.Use(New(Config{ + Max: 2, + Expiration: 2 * time.Second, + SkipFailedRequests: false, + SkipSuccessfulRequests: false, + LimiterMiddleware: SlidingWindow{}, + })) + + app.Get("/:status", func(c *fiber.Ctx) error { + if c.Params("status") == "fail" { + return c.SendStatus(400) + } + return c.SendStatus(200) + }) + + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/fail", nil)) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, 400, resp.StatusCode) + + resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/success", nil)) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, 200, resp.StatusCode) + + resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/success", nil)) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, 429, resp.StatusCode) + + time.Sleep(4*time.Second + 500*time.Millisecond) + + resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/success", nil)) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, 200, resp.StatusCode) +} + +// go test -run Test_Limiter_Sliding_Window_Custom_Storage_No_Skip_Choices -v +func Test_Limiter_Sliding_Window_Custom_Storage_No_Skip_Choices(t *testing.T) { + t.Parallel() + app := fiber.New() + + app.Use(New(Config{ + Max: 2, + Expiration: 2 * time.Second, + SkipFailedRequests: false, + SkipSuccessfulRequests: false, + Storage: memory.New(), + LimiterMiddleware: SlidingWindow{}, + })) + + app.Get("/:status", func(c *fiber.Ctx) error { + if c.Params("status") == "fail" { + return c.SendStatus(400) + } + return c.SendStatus(200) + }) + + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/fail", nil)) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, 400, resp.StatusCode) + + resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/success", nil)) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, 200, resp.StatusCode) + + resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/success", nil)) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, 429, resp.StatusCode) + + time.Sleep(4*time.Second + 500*time.Millisecond) + + resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/success", nil)) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, 200, resp.StatusCode) +} + +// go test -run Test_Limiter_Fixed_Window_Skip_Failed_Requests -v +func Test_Limiter_Fixed_Window_Skip_Failed_Requests(t *testing.T) { + t.Parallel() + app := fiber.New() + + app.Use(New(Config{ + Max: 1, + Expiration: 2 * time.Second, + SkipFailedRequests: true, + LimiterMiddleware: FixedWindow{}, + })) + + app.Get("/:status", func(c *fiber.Ctx) error { + if c.Params("status") == "fail" { + return c.SendStatus(400) + } + return c.SendStatus(200) + }) + + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/fail", nil)) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, 400, resp.StatusCode) + + resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/success", nil)) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, 200, resp.StatusCode) + + resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/success", nil)) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, 429, resp.StatusCode) + + time.Sleep(3 * time.Second) + + resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/success", nil)) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, 200, resp.StatusCode) +} + +// go test -run Test_Limiter_Fixed_Window_Custom_Storage_Skip_Failed_Requests -v +func Test_Limiter_Fixed_Window_Custom_Storage_Skip_Failed_Requests(t *testing.T) { + t.Parallel() + app := fiber.New() + + app.Use(New(Config{ + Max: 1, + Expiration: 2 * time.Second, + Storage: memory.New(), + SkipFailedRequests: true, + LimiterMiddleware: FixedWindow{}, + })) + + app.Get("/:status", func(c *fiber.Ctx) error { + if c.Params("status") == "fail" { + return c.SendStatus(400) + } + return c.SendStatus(200) + }) + + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/fail", nil)) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, 400, resp.StatusCode) + + resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/success", nil)) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, 200, resp.StatusCode) + + resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/success", nil)) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, 429, resp.StatusCode) + + time.Sleep(3 * time.Second) + + resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/success", nil)) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, 200, resp.StatusCode) +} + +// go test -run Test_Limiter_Sliding_Window_Skip_Failed_Requests -v +func Test_Limiter_Sliding_Window_Skip_Failed_Requests(t *testing.T) { + t.Parallel() + app := fiber.New() + + app.Use(New(Config{ + Max: 1, + Expiration: 2 * time.Second, + SkipFailedRequests: true, + LimiterMiddleware: SlidingWindow{}, + })) + + app.Get("/:status", func(c *fiber.Ctx) error { + if c.Params("status") == "fail" { + return c.SendStatus(400) + } + return c.SendStatus(200) + }) + + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/fail", nil)) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, 400, resp.StatusCode) + + resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/success", nil)) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, 200, resp.StatusCode) + + resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/success", nil)) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, 429, resp.StatusCode) + + time.Sleep(4*time.Second + 500*time.Millisecond) + + resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/success", nil)) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, 200, resp.StatusCode) +} + +// go test -run Test_Limiter_Sliding_Window_Custom_Storage_Skip_Failed_Requests -v +func Test_Limiter_Sliding_Window_Custom_Storage_Skip_Failed_Requests(t *testing.T) { + t.Parallel() + app := fiber.New() + + app.Use(New(Config{ + Max: 1, + Expiration: 2 * time.Second, + Storage: memory.New(), + SkipFailedRequests: true, + LimiterMiddleware: SlidingWindow{}, + })) + + app.Get("/:status", func(c *fiber.Ctx) error { + if c.Params("status") == "fail" { + return c.SendStatus(400) + } + return c.SendStatus(200) + }) + + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/fail", nil)) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, 400, resp.StatusCode) + + resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/success", nil)) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, 200, resp.StatusCode) + + resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/success", nil)) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, 429, resp.StatusCode) + + time.Sleep(4*time.Second + 500*time.Millisecond) + + resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/success", nil)) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, 200, resp.StatusCode) +} + +// go test -run Test_Limiter_Fixed_Window_Skip_Successful_Requests -v +func Test_Limiter_Fixed_Window_Skip_Successful_Requests(t *testing.T) { + t.Parallel() + // Test concurrency using a default store + + app := fiber.New() + + app.Use(New(Config{ + Max: 1, + Expiration: 2 * time.Second, + SkipSuccessfulRequests: true, + LimiterMiddleware: FixedWindow{}, + })) + + app.Get("/:status", func(c *fiber.Ctx) error { + if c.Params("status") == "fail" { + return c.SendStatus(400) + } + return c.SendStatus(200) + }) + + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/success", nil)) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, 200, resp.StatusCode) + + resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/fail", nil)) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, 400, resp.StatusCode) + + resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/fail", nil)) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, 429, resp.StatusCode) + + time.Sleep(3 * time.Second) + + resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/fail", nil)) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, 400, resp.StatusCode) +} + +// go test -run Test_Limiter_Fixed_Window_Custom_Storage_Skip_Successful_Requests -v +func Test_Limiter_Fixed_Window_Custom_Storage_Skip_Successful_Requests(t *testing.T) { + t.Parallel() + // Test concurrency using a default store + + app := fiber.New() + + app.Use(New(Config{ + Max: 1, + Expiration: 2 * time.Second, + Storage: memory.New(), + SkipSuccessfulRequests: true, + LimiterMiddleware: FixedWindow{}, + })) + + app.Get("/:status", func(c *fiber.Ctx) error { + if c.Params("status") == "fail" { + return c.SendStatus(400) + } + return c.SendStatus(200) + }) + + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/success", nil)) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, 200, resp.StatusCode) + + resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/fail", nil)) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, 400, resp.StatusCode) + + resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/fail", nil)) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, 429, resp.StatusCode) + + time.Sleep(3 * time.Second) + + resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/fail", nil)) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, 400, resp.StatusCode) +} + +// go test -run Test_Limiter_Sliding_Window_Skip_Successful_Requests -v +func Test_Limiter_Sliding_Window_Skip_Successful_Requests(t *testing.T) { + t.Parallel() + // Test concurrency using a default store + + app := fiber.New() + + app.Use(New(Config{ + Max: 1, + Expiration: 2 * time.Second, + SkipSuccessfulRequests: true, + LimiterMiddleware: SlidingWindow{}, + })) + + app.Get("/:status", func(c *fiber.Ctx) error { + if c.Params("status") == "fail" { + return c.SendStatus(400) + } + return c.SendStatus(200) + }) + + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/success", nil)) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, 200, resp.StatusCode) + + resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/fail", nil)) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, 400, resp.StatusCode) + + resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/fail", nil)) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, 429, resp.StatusCode) + + time.Sleep(4*time.Second + 500*time.Millisecond) + + resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/fail", nil)) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, 400, resp.StatusCode) +} + +// go test -run Test_Limiter_Sliding_Window_Custom_Storage_Skip_Successful_Requests -v +func Test_Limiter_Sliding_Window_Custom_Storage_Skip_Successful_Requests(t *testing.T) { + t.Parallel() + // Test concurrency using a default store + + app := fiber.New() + + app.Use(New(Config{ + Max: 1, + Expiration: 2 * time.Second, + Storage: memory.New(), + SkipSuccessfulRequests: true, + LimiterMiddleware: SlidingWindow{}, + })) + + app.Get("/:status", func(c *fiber.Ctx) error { + if c.Params("status") == "fail" { + return c.SendStatus(400) + } + return c.SendStatus(200) + }) + + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/success", nil)) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, 200, resp.StatusCode) + + resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/fail", nil)) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, 400, resp.StatusCode) + + resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/fail", nil)) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, 429, resp.StatusCode) + + time.Sleep(4*time.Second + 500*time.Millisecond) + + resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/fail", nil)) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, 400, resp.StatusCode) +} + +// go test -v -run=^$ -bench=Benchmark_Limiter_Custom_Store -benchmem -count=4 +func Benchmark_Limiter_Custom_Store(b *testing.B) { + app := fiber.New() + + app.Use(New(Config{ + Max: 100, + Expiration: 60 * time.Second, + Storage: memory.New(), + })) + + app.Get("/", func(c *fiber.Ctx) error { + return c.SendString("Hello, World!") + }) + + h := app.Handler() + + fctx := &fasthttp.RequestCtx{} + fctx.Request.Header.SetMethod(fiber.MethodGet) + fctx.Request.SetRequestURI("/") + + b.ResetTimer() + + for n := 0; n < b.N; n++ { + h(fctx) + } +} + +// go test -run Test_Limiter_Next +func Test_Limiter_Next(t *testing.T) { + t.Parallel() + app := fiber.New() + app.Use(New(Config{ + Next: func(_ *fiber.Ctx) bool { + return true + }, + })) + + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, fiber.StatusNotFound, resp.StatusCode) +} + +func Test_Limiter_Headers(t *testing.T) { + t.Parallel() + app := fiber.New() + + app.Use(New(Config{ + Max: 50, + Expiration: 2 * time.Second, + })) + + app.Get("/", func(c *fiber.Ctx) error { + return c.SendString("Hello tester!") + }) + + fctx := &fasthttp.RequestCtx{} + fctx.Request.Header.SetMethod(fiber.MethodGet) + fctx.Request.SetRequestURI("/") + + app.Handler()(fctx) + + utils.AssertEqual(t, "50", string(fctx.Response.Header.Peek("X-RateLimit-Limit"))) + if v := string(fctx.Response.Header.Peek("X-RateLimit-Remaining")); v == "" { + t.Errorf("The X-RateLimit-Remaining header is not set correctly - value is an empty string.") + } + if v := string(fctx.Response.Header.Peek("X-RateLimit-Reset")); !(v == "1" || v == "2") { + t.Errorf("The X-RateLimit-Reset header is not set correctly - value is out of bounds.") + } +} + +// go test -v -run=^$ -bench=Benchmark_Limiter -benchmem -count=4 +func Benchmark_Limiter(b *testing.B) { + app := fiber.New() + + app.Use(New(Config{ + Max: 100, + Expiration: 60 * time.Second, + })) + + app.Get("/", func(c *fiber.Ctx) error { + return c.SendString("Hello, World!") + }) + + h := app.Handler() + + fctx := &fasthttp.RequestCtx{} + fctx.Request.Header.SetMethod(fiber.MethodGet) + fctx.Request.SetRequestURI("/") + + b.ResetTimer() + + for n := 0; n < b.N; n++ { + h(fctx) + } +} + +// go test -run Test_Sliding_Window -race -v +func Test_Sliding_Window(t *testing.T) { + t.Parallel() + app := fiber.New() + app.Use(New(Config{ + Max: 10, + Expiration: 2 * time.Second, + Storage: memory.New(), + LimiterMiddleware: SlidingWindow{}, + })) + + app.Get("/", func(c *fiber.Ctx) error { + return c.SendString("Hello tester!") + }) + + singleRequest := func(shouldFail bool) { + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) + if shouldFail { + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, 429, resp.StatusCode) + } else { + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, fiber.StatusOK, resp.StatusCode) + } + } + + for i := 0; i < 5; i++ { + singleRequest(false) + } + + time.Sleep(2 * time.Second) + + for i := 0; i < 5; i++ { + singleRequest(false) + } + + time.Sleep(3 * time.Second) + + for i := 0; i < 5; i++ { + singleRequest(false) + } + + time.Sleep(4 * time.Second) + + for i := 0; i < 9; i++ { + singleRequest(false) + } +} diff --git a/middleware/limiter/manager.go b/middleware/limiter/manager.go new file mode 100644 index 0000000..374d3a1 --- /dev/null +++ b/middleware/limiter/manager.go @@ -0,0 +1,92 @@ +package limiter + +import ( + "sync" + "time" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/internal/memory" +) + +// go:generate msgp +// msgp -file="manager.go" -o="manager_msgp.go" -tests=false -unexported +type item struct { + currHits int + prevHits int + exp uint64 +} + +//msgp:ignore manager +type manager struct { + pool sync.Pool + memory *memory.Storage + storage fiber.Storage +} + +func newManager(storage fiber.Storage) *manager { + // Create new storage handler + manager := &manager{ + pool: sync.Pool{ + New: func() interface{} { + return new(item) + }, + }, + } + if storage != nil { + // Use provided storage if provided + manager.storage = storage + } else { + // Fallback too memory storage + manager.memory = memory.New() + } + return manager +} + +// acquire returns an *entry from the sync.Pool +func (m *manager) acquire() *item { + return m.pool.Get().(*item) //nolint:forcetypeassert // We store nothing else in the pool +} + +// release and reset *entry to sync.Pool +func (m *manager) release(e *item) { + e.prevHits = 0 + e.currHits = 0 + e.exp = 0 + m.pool.Put(e) +} + +// get data from storage or memory +func (m *manager) get(key string) *item { + var it *item + if m.storage != nil { + it = m.acquire() + raw, err := m.storage.Get(key) + if err != nil { + return it + } + if raw != nil { + if _, err := it.UnmarshalMsg(raw); err != nil { + return it + } + } + return it + } + if it, _ = m.memory.Get(key).(*item); it == nil { //nolint:errcheck // We store nothing else in the pool + it = m.acquire() + return it + } + return it +} + +// set data to storage or memory +func (m *manager) set(key string, it *item, exp time.Duration) { + if m.storage != nil { + if raw, err := it.MarshalMsg(nil); err == nil { + _ = m.storage.Set(key, raw, exp) //nolint:errcheck // TODO: Handle error here + } + // we can release data because it's serialized to database + m.release(it) + } else { + m.memory.Set(key, it, exp) + } +} diff --git a/middleware/limiter/manager_msgp.go b/middleware/limiter/manager_msgp.go new file mode 100644 index 0000000..a0d81ec --- /dev/null +++ b/middleware/limiter/manager_msgp.go @@ -0,0 +1,160 @@ +package limiter + +// Code generated by github.com/tinylib/msgp DO NOT EDIT. + +import ( + "github.com/tinylib/msgp/msgp" +) + +// DecodeMsg implements msgp.Decodable +func (z *item) DecodeMsg(dc *msgp.Reader) (err error) { + var field []byte + _ = field + var zb0001 uint32 + zb0001, err = dc.ReadMapHeader() + if err != nil { + err = msgp.WrapError(err) + return + } + for zb0001 > 0 { + zb0001-- + field, err = dc.ReadMapKeyPtr() + if err != nil { + err = msgp.WrapError(err) + return + } + switch msgp.UnsafeString(field) { + case "currHits": + z.currHits, err = dc.ReadInt() + if err != nil { + err = msgp.WrapError(err, "currHits") + return + } + case "prevHits": + z.prevHits, err = dc.ReadInt() + if err != nil { + err = msgp.WrapError(err, "prevHits") + return + } + case "exp": + z.exp, err = dc.ReadUint64() + if err != nil { + err = msgp.WrapError(err, "exp") + return + } + default: + err = dc.Skip() + if err != nil { + err = msgp.WrapError(err) + return + } + } + } + return +} + +// EncodeMsg implements msgp.Encodable +func (z item) EncodeMsg(en *msgp.Writer) (err error) { + // map header, size 3 + // write "currHits" + err = en.Append(0x83, 0xa8, 0x63, 0x75, 0x72, 0x72, 0x48, 0x69, 0x74, 0x73) + if err != nil { + return + } + err = en.WriteInt(z.currHits) + if err != nil { + err = msgp.WrapError(err, "currHits") + return + } + // write "prevHits" + err = en.Append(0xa8, 0x70, 0x72, 0x65, 0x76, 0x48, 0x69, 0x74, 0x73) + if err != nil { + return + } + err = en.WriteInt(z.prevHits) + if err != nil { + err = msgp.WrapError(err, "prevHits") + return + } + // write "exp" + err = en.Append(0xa3, 0x65, 0x78, 0x70) + if err != nil { + return + } + err = en.WriteUint64(z.exp) + if err != nil { + err = msgp.WrapError(err, "exp") + return + } + return +} + +// MarshalMsg implements msgp.Marshaler +func (z item) MarshalMsg(b []byte) (o []byte, err error) { + o = msgp.Require(b, z.Msgsize()) + // map header, size 3 + // string "currHits" + o = append(o, 0x83, 0xa8, 0x63, 0x75, 0x72, 0x72, 0x48, 0x69, 0x74, 0x73) + o = msgp.AppendInt(o, z.currHits) + // string "prevHits" + o = append(o, 0xa8, 0x70, 0x72, 0x65, 0x76, 0x48, 0x69, 0x74, 0x73) + o = msgp.AppendInt(o, z.prevHits) + // string "exp" + o = append(o, 0xa3, 0x65, 0x78, 0x70) + o = msgp.AppendUint64(o, z.exp) + return +} + +// UnmarshalMsg implements msgp.Unmarshaler +func (z *item) UnmarshalMsg(bts []byte) (o []byte, err error) { + var field []byte + _ = field + var zb0001 uint32 + zb0001, bts, err = msgp.ReadMapHeaderBytes(bts) + if err != nil { + err = msgp.WrapError(err) + return + } + for zb0001 > 0 { + zb0001-- + field, bts, err = msgp.ReadMapKeyZC(bts) + if err != nil { + err = msgp.WrapError(err) + return + } + switch msgp.UnsafeString(field) { + case "currHits": + z.currHits, bts, err = msgp.ReadIntBytes(bts) + if err != nil { + err = msgp.WrapError(err, "currHits") + return + } + case "prevHits": + z.prevHits, bts, err = msgp.ReadIntBytes(bts) + if err != nil { + err = msgp.WrapError(err, "prevHits") + return + } + case "exp": + z.exp, bts, err = msgp.ReadUint64Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "exp") + return + } + default: + bts, err = msgp.Skip(bts) + if err != nil { + err = msgp.WrapError(err) + return + } + } + } + o = bts + return +} + +// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message +func (z item) Msgsize() (s int) { + s = 1 + 9 + msgp.IntSize + 9 + msgp.IntSize + 4 + msgp.Uint64Size + return +} diff --git a/middleware/logger/config.go b/middleware/logger/config.go new file mode 100644 index 0000000..b84714c --- /dev/null +++ b/middleware/logger/config.go @@ -0,0 +1,136 @@ +package logger + +import ( + "io" + "os" + "time" + + "github.com/gofiber/fiber/v2" +) + +// Config defines the config for middleware. +type Config struct { + // Next defines a function to skip this middleware when returned true. + // + // Optional. Default: nil + Next func(c *fiber.Ctx) bool + + // Done is a function that is called after the log string for a request is written to Output, + // and pass the log string as parameter. + // + // Optional. Default: nil + Done func(c *fiber.Ctx, logString []byte) + + // tagFunctions defines the custom tag action + // + // Optional. Default: map[string]LogFunc + CustomTags map[string]LogFunc + + // Format defines the logging tags + // + // Optional. Default: ${time} | ${status} | ${latency} | ${ip} | ${method} | ${path} | ${error}\n + Format string + + // TimeFormat https://programming.guide/go/format-parse-string-time-date-example.html + // + // Optional. Default: 15:04:05 + TimeFormat string + + // TimeZone can be specified, such as "UTC" and "America/New_York" and "Asia/Chongqing", etc + // + // Optional. Default: "Local" + TimeZone string + + // TimeInterval is the delay before the timestamp is updated + // + // Optional. Default: 500 * time.Millisecond + TimeInterval time.Duration + + // Output is a writer where logs are written + // + // Default: os.Stdout + Output io.Writer + + // DisableColors defines if the logs output should be colorized + // + // Default: false + DisableColors bool + + enableColors bool + enableLatency bool + timeZoneLocation *time.Location +} + +const ( + startTag = "${" + endTag = "}" + paramSeparator = ":" +) + +type Buffer interface { + Len() int + ReadFrom(r io.Reader) (int64, error) + WriteTo(w io.Writer) (int64, error) + Bytes() []byte + Write(p []byte) (int, error) + WriteByte(c byte) error + WriteString(s string) (int, error) + Set(p []byte) + SetString(s string) + String() string +} + +type LogFunc func(output Buffer, c *fiber.Ctx, data *Data, extraParam string) (int, error) + +// ConfigDefault is the default config +var ConfigDefault = Config{ + Next: nil, + Done: nil, + Format: "${time} | ${status} | ${latency} | ${ip} | ${method} | ${path} | ${error}\n", + TimeFormat: "15:04:05", + TimeZone: "Local", + TimeInterval: 500 * time.Millisecond, + Output: os.Stdout, + DisableColors: false, + enableColors: true, +} + +// Helper function to set default values +func configDefault(config ...Config) Config { + // Return default config if nothing provided + if len(config) < 1 { + return ConfigDefault + } + + // Override default config + cfg := config[0] + + // Set default values + if cfg.Next == nil { + cfg.Next = ConfigDefault.Next + } + if cfg.Done == nil { + cfg.Done = ConfigDefault.Done + } + if cfg.Format == "" { + cfg.Format = ConfigDefault.Format + } + if cfg.TimeZone == "" { + cfg.TimeZone = ConfigDefault.TimeZone + } + if cfg.TimeFormat == "" { + cfg.TimeFormat = ConfigDefault.TimeFormat + } + if int(cfg.TimeInterval) <= 0 { + cfg.TimeInterval = ConfigDefault.TimeInterval + } + if cfg.Output == nil { + cfg.Output = ConfigDefault.Output + } + + if !cfg.DisableColors && cfg.Output == ConfigDefault.Output { + cfg.enableColors = true + } + + return cfg +} diff --git a/middleware/logger/data.go b/middleware/logger/data.go new file mode 100644 index 0000000..912d016 --- /dev/null +++ b/middleware/logger/data.go @@ -0,0 +1,16 @@ +package logger + +import ( + "sync/atomic" + "time" +) + +// Data is a struct to define some variables to use in custom logger function. +type Data struct { + Pid string + ErrPaddingStr string + ChainErr error + Start time.Time + Stop time.Time + Timestamp atomic.Value +} diff --git a/middleware/logger/logger.go b/middleware/logger/logger.go new file mode 100644 index 0000000..b03617b --- /dev/null +++ b/middleware/logger/logger.go @@ -0,0 +1,182 @@ +package logger + +import ( + "fmt" + "os" + "strconv" + "strings" + "sync" + "sync/atomic" + "time" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/utils" + + "github.com/mattn/go-colorable" + "github.com/mattn/go-isatty" + "github.com/valyala/bytebufferpool" + "github.com/valyala/fasthttp" +) + +// New creates a new middleware handler +func New(config ...Config) fiber.Handler { + // Set default config + cfg := configDefault(config...) + + // Get timezone location + tz, err := time.LoadLocation(cfg.TimeZone) + if err != nil || tz == nil { + cfg.timeZoneLocation = time.Local + } else { + cfg.timeZoneLocation = tz + } + + // Check if format contains latency + cfg.enableLatency = strings.Contains(cfg.Format, "${"+TagLatency+"}") + + var timestamp atomic.Value + // Create correct timeformat + timestamp.Store(time.Now().In(cfg.timeZoneLocation).Format(cfg.TimeFormat)) + + // Update date/time every 500 milliseconds in a separate go routine + if strings.Contains(cfg.Format, "${"+TagTime+"}") { + go func() { + for { + time.Sleep(cfg.TimeInterval) + timestamp.Store(time.Now().In(cfg.timeZoneLocation).Format(cfg.TimeFormat)) + } + }() + } + + // Set PID once + pid := strconv.Itoa(os.Getpid()) + + // Set variables + var ( + once sync.Once + mu sync.Mutex + errHandler fiber.ErrorHandler + + dataPool = sync.Pool{New: func() interface{} { return new(Data) }} + ) + + // If colors are enabled, check terminal compatibility + if cfg.enableColors { + cfg.Output = colorable.NewColorableStdout() + if os.Getenv("TERM") == "dumb" || os.Getenv("NO_COLOR") == "1" || (!isatty.IsTerminal(os.Stdout.Fd()) && !isatty.IsCygwinTerminal(os.Stdout.Fd())) { + cfg.Output = colorable.NewNonColorable(os.Stdout) + } + } + + errPadding := 15 + errPaddingStr := strconv.Itoa(errPadding) + + // instead of analyzing the template inside(handler) each time, this is done once before + // and we create several slices of the same length with the functions to be executed and fixed parts. + templateChain, logFunChain, err := buildLogFuncChain(&cfg, createTagMap(&cfg)) + if err != nil { + panic(err) + } + + // Return new handler + return func(c *fiber.Ctx) error { + // Don't execute middleware if Next returns true + if cfg.Next != nil && cfg.Next(c) { + return c.Next() + } + + // Set error handler once + once.Do(func() { + // get longested possible path + stack := c.App().Stack() + for m := range stack { + for r := range stack[m] { + if len(stack[m][r].Path) > errPadding { + errPadding = len(stack[m][r].Path) + errPaddingStr = strconv.Itoa(errPadding) + } + } + } + // override error handler + errHandler = c.App().ErrorHandler + }) + + // Logger data + data := dataPool.Get().(*Data) //nolint:forcetypeassert,errcheck // We store nothing else in the pool + // no need for a reset, as long as we always override everything + data.Pid = pid + data.ErrPaddingStr = errPaddingStr + data.Timestamp = timestamp + // put data back in the pool + defer dataPool.Put(data) + + // Set latency start time + if cfg.enableLatency { + data.Start = time.Now() + } + + // Handle request, store err for logging + chainErr := c.Next() + + data.ChainErr = chainErr + // Manually call error handler + if chainErr != nil { + if err := errHandler(c, chainErr); err != nil { + _ = c.SendStatus(fiber.StatusInternalServerError) //nolint:errcheck // TODO: Explain why we ignore the error here + } + } + + // Set latency stop time + if cfg.enableLatency { + data.Stop = time.Now() + } + + // Get new buffer + buf := bytebufferpool.Get() + + var err error + // Loop over template parts execute dynamic parts and add fixed parts to the buffer + for i, logFunc := range logFunChain { + if logFunc == nil { + _, _ = buf.Write(templateChain[i]) //nolint:errcheck // This will never fail + } else if templateChain[i] == nil { + _, err = logFunc(buf, c, data, "") + } else { + _, err = logFunc(buf, c, data, utils.UnsafeString(templateChain[i])) + } + if err != nil { + break + } + } + + // Also write errors to the buffer + if err != nil { + _, _ = buf.WriteString(err.Error()) //nolint:errcheck // This will never fail + } + mu.Lock() + // Write buffer to output + if _, err := cfg.Output.Write(buf.Bytes()); err != nil { + // Write error to output + if _, err := cfg.Output.Write([]byte(err.Error())); err != nil { + // There is something wrong with the given io.Writer + _, _ = fmt.Fprintf(os.Stderr, "Failed to write to log, %v\n", err) + } + } + mu.Unlock() + + if cfg.Done != nil { + cfg.Done(c, buf.Bytes()) + } + + // Put buffer back to pool + bytebufferpool.Put(buf) + + return nil + } +} + +func appendInt(output Buffer, v int) (int, error) { + old := output.Len() + output.Set(fasthttp.AppendUint(output.Bytes(), v)) + return output.Len() - old, nil +} diff --git a/middleware/logger/logger_test.go b/middleware/logger/logger_test.go new file mode 100644 index 0000000..94b68d9 --- /dev/null +++ b/middleware/logger/logger_test.go @@ -0,0 +1,650 @@ +//nolint:bodyclose // Much easier to just ignore memory leaks in tests +package logger + +import ( + "bufio" + "bytes" + "errors" + "fmt" + "io" + "net/http" + "net/http/httptest" + "os" + "runtime" + "sync" + "testing" + "time" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/requestid" + "github.com/gofiber/fiber/v2/utils" + + "github.com/valyala/bytebufferpool" + "github.com/valyala/fasthttp" +) + +// go test -run Test_Logger +func Test_Logger(t *testing.T) { + t.Parallel() + app := fiber.New() + + buf := bytebufferpool.Get() + defer bytebufferpool.Put(buf) + + app.Use(New(Config{ + Format: "${error}", + Output: buf, + })) + + app.Get("/", func(c *fiber.Ctx) error { + return errors.New("some random error") + }) + + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, fiber.StatusInternalServerError, resp.StatusCode) + utils.AssertEqual(t, "some random error", buf.String()) +} + +// go test -run Test_Logger_locals +func Test_Logger_locals(t *testing.T) { + t.Parallel() + app := fiber.New() + + buf := bytebufferpool.Get() + defer bytebufferpool.Put(buf) + + app.Use(New(Config{ + Format: "${locals:demo}", + Output: buf, + })) + + app.Get("/", func(c *fiber.Ctx) error { + c.Locals("demo", "johndoe") + return c.SendStatus(fiber.StatusOK) + }) + + app.Get("/int", func(c *fiber.Ctx) error { + c.Locals("demo", 55) + return c.SendStatus(fiber.StatusOK) + }) + + app.Get("/empty", func(c *fiber.Ctx) error { + return c.SendStatus(fiber.StatusOK) + }) + + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, fiber.StatusOK, resp.StatusCode) + utils.AssertEqual(t, "johndoe", buf.String()) + + buf.Reset() + + resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/int", nil)) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, fiber.StatusOK, resp.StatusCode) + utils.AssertEqual(t, "55", buf.String()) + + buf.Reset() + + resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/empty", nil)) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, fiber.StatusOK, resp.StatusCode) + utils.AssertEqual(t, "", buf.String()) +} + +// go test -run Test_Logger_Next +func Test_Logger_Next(t *testing.T) { + t.Parallel() + app := fiber.New() + app.Use(New(Config{ + Next: func(_ *fiber.Ctx) bool { + return true + }, + })) + + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, fiber.StatusNotFound, resp.StatusCode) +} + +// go test -run Test_Logger_Done +func Test_Logger_Done(t *testing.T) { + t.Parallel() + buf := bytes.NewBuffer(nil) + app := fiber.New() + app.Use(New(Config{ + Done: func(c *fiber.Ctx, logString []byte) { + if c.Response().StatusCode() == fiber.StatusOK { + _, err := buf.Write(logString) + utils.AssertEqual(t, nil, err) + } + }, + })).Get("/logging", func(ctx *fiber.Ctx) error { + return ctx.SendStatus(fiber.StatusOK) + }) + + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/logging", nil)) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, fiber.StatusOK, resp.StatusCode) + utils.AssertEqual(t, true, buf.Len() > 0) +} + +// go test -run Test_Logger_ErrorTimeZone +func Test_Logger_ErrorTimeZone(t *testing.T) { + t.Parallel() + app := fiber.New() + app.Use(New(Config{ + TimeZone: "invalid", + })) + + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, fiber.StatusNotFound, resp.StatusCode) +} + +type fakeOutput int + +func (o *fakeOutput) Write([]byte) (int, error) { + *o++ + return 0, nil +} + +// go test -run Test_Logger_ErrorOutput_WithoutColor +func Test_Logger_ErrorOutput_WithoutColor(t *testing.T) { + t.Parallel() + o := new(fakeOutput) + app := fiber.New() + app.Use(New(Config{ + Output: o, + DisableColors: true, + })) + + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, fiber.StatusNotFound, resp.StatusCode) + + utils.AssertEqual(t, 1, int(*o)) +} + +// go test -run Test_Logger_ErrorOutput +func Test_Logger_ErrorOutput(t *testing.T) { + t.Parallel() + o := new(fakeOutput) + app := fiber.New() + app.Use(New(Config{ + Output: o, + })) + + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, fiber.StatusNotFound, resp.StatusCode) + + utils.AssertEqual(t, 1, int(*o)) +} + +// go test -run Test_Logger_All +func Test_Logger_All(t *testing.T) { + t.Parallel() + buf := bytebufferpool.Get() + defer bytebufferpool.Put(buf) + + app := fiber.New() + app.Use(New(Config{ + Format: "${pid}${reqHeaders}${referer}${protocol}${ip}${ips}${host}${url}${ua}${body}${route}${black}${red}${green}${yellow}${blue}${magenta}${cyan}${white}${reset}${error}${header:test}${query:test}${form:test}${cookie:test}${non}", + Output: buf, + })) + + // Alias colors + colors := app.Config().ColorScheme + + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/?foo=bar", nil)) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, fiber.StatusNotFound, resp.StatusCode) + + expected := fmt.Sprintf("%dHost=example.comhttp0.0.0.0example.com/?foo=bar/%s%s%s%s%s%s%s%s%sCannot GET /", os.Getpid(), colors.Black, colors.Red, colors.Green, colors.Yellow, colors.Blue, colors.Magenta, colors.Cyan, colors.White, colors.Reset) + utils.AssertEqual(t, expected, buf.String()) +} + +func getLatencyTimeUnits() []struct { + unit string + div time.Duration +} { + // windows does not support µs sleep precision + // https://github.com/golang/go/issues/29485 + if runtime.GOOS == "windows" { + return []struct { + unit string + div time.Duration + }{ + {"ms", time.Millisecond}, + {"s", time.Second}, + } + } + return []struct { + unit string + div time.Duration + }{ + {"µs", time.Microsecond}, + {"ms", time.Millisecond}, + {"s", time.Second}, + } +} + +// go test -run Test_Logger_WithLatency +func Test_Logger_WithLatency(t *testing.T) { + t.Parallel() + buff := bytebufferpool.Get() + defer bytebufferpool.Put(buff) + app := fiber.New() + logger := New(Config{ + Output: buff, + Format: "${latency}", + }) + app.Use(logger) + + // Define a list of time units to test + timeUnits := getLatencyTimeUnits() + + // Initialize a new time unit + sleepDuration := 1 * time.Nanosecond + + // Define a test route that sleeps + app.Get("/test", func(c *fiber.Ctx) error { + time.Sleep(sleepDuration) + return c.SendStatus(fiber.StatusOK) + }) + + // Loop through each time unit and assert that the log output contains the expected latency value + for _, tu := range timeUnits { + // Update the sleep duration for the next iteration + sleepDuration = 1 * tu.div + + // Create a new HTTP request to the test route + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/test", nil), int(2*time.Second)) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, fiber.StatusOK, resp.StatusCode) + + // Assert that the log output contains the expected latency value in the current time unit + utils.AssertEqual(t, bytes.HasSuffix(buff.Bytes(), []byte(tu.unit)), true, fmt.Sprintf("Expected latency to be in %s, got %s", tu.unit, buff.String())) + + // Reset the buffer + buff.Reset() + } +} + +// go test -run Test_Logger_WithLatency_DefaultFormat +func Test_Logger_WithLatency_DefaultFormat(t *testing.T) { + t.Parallel() + buff := bytebufferpool.Get() + defer bytebufferpool.Put(buff) + app := fiber.New() + logger := New(Config{ + Output: buff, + }) + app.Use(logger) + + // Define a list of time units to test + timeUnits := getLatencyTimeUnits() + + // Initialize a new time unit + sleepDuration := 1 * time.Nanosecond + + // Define a test route that sleeps + app.Get("/test", func(c *fiber.Ctx) error { + time.Sleep(sleepDuration) + return c.SendStatus(fiber.StatusOK) + }) + + // Loop through each time unit and assert that the log output contains the expected latency value + for _, tu := range timeUnits { + // Update the sleep duration for the next iteration + sleepDuration = 1 * tu.div + + // Create a new HTTP request to the test route + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/test", nil), int(2*time.Second)) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, fiber.StatusOK, resp.StatusCode) + + // Assert that the log output contains the expected latency value in the current time unit + // parse out the latency value from the log output + latency := bytes.Split(buff.Bytes(), []byte(" | "))[2] + // Assert that the latency value is in the current time unit + utils.AssertEqual(t, bytes.HasSuffix(latency, []byte(tu.unit)), true, fmt.Sprintf("Expected latency to be in %s, got %s", tu.unit, latency)) + + // Reset the buffer + buff.Reset() + } +} + +// go test -run Test_Query_Params +func Test_Query_Params(t *testing.T) { + t.Parallel() + buf := bytebufferpool.Get() + defer bytebufferpool.Put(buf) + + app := fiber.New() + app.Use(New(Config{ + Format: "${queryParams}", + Output: buf, + })) + + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/?foo=bar&baz=moz", nil)) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, fiber.StatusNotFound, resp.StatusCode) + + expected := "foo=bar&baz=moz" + utils.AssertEqual(t, expected, buf.String()) +} + +// go test -run Test_Response_Body +func Test_Response_Body(t *testing.T) { + t.Parallel() + buf := bytebufferpool.Get() + defer bytebufferpool.Put(buf) + + app := fiber.New() + app.Use(New(Config{ + Format: "${resBody}", + Output: buf, + })) + + app.Get("/", func(c *fiber.Ctx) error { + return c.SendString("Sample response body") + }) + + app.Post("/test", func(c *fiber.Ctx) error { + return c.Send([]byte("Post in test")) + }) + + _, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) + utils.AssertEqual(t, nil, err) + + expectedGetResponse := "Sample response body" + utils.AssertEqual(t, expectedGetResponse, buf.String()) + + buf.Reset() // Reset buffer to test POST + + _, err = app.Test(httptest.NewRequest(fiber.MethodPost, "/test", nil)) + utils.AssertEqual(t, nil, err) + + expectedPostResponse := "Post in test" + utils.AssertEqual(t, expectedPostResponse, buf.String()) +} + +// go test -run Test_Logger_AppendUint +func Test_Logger_AppendUint(t *testing.T) { + t.Parallel() + app := fiber.New() + + buf := bytebufferpool.Get() + defer bytebufferpool.Put(buf) + + app.Use(New(Config{ + Format: "${bytesReceived} ${bytesSent} ${status}", + Output: buf, + })) + + app.Get("/", func(c *fiber.Ctx) error { + c.Response().Header.SetContentLength(5) + return c.SendString("hello") + }) + + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, fiber.StatusOK, resp.StatusCode) + utils.AssertEqual(t, "-2 5 200", buf.String()) +} + +// go test -run Test_Logger_Data_Race -race +func Test_Logger_Data_Race(t *testing.T) { + t.Parallel() + app := fiber.New() + + buf := bytebufferpool.Get() + defer bytebufferpool.Put(buf) + + app.Use(New(ConfigDefault)) + app.Use(New(Config{ + Format: "${time} | ${pid} | ${locals:requestid} | ${status} | ${latency} | ${method} | ${path}\n", + })) + + app.Get("/", func(c *fiber.Ctx) error { + return c.SendString("hello") + }) + + var ( + resp1, resp2 *http.Response + err1, err2 error + ) + wg := &sync.WaitGroup{} + wg.Add(1) + go func() { + resp1, err1 = app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) + wg.Done() + }() + resp2, err2 = app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) + wg.Wait() + utils.AssertEqual(t, nil, err1) + utils.AssertEqual(t, fiber.StatusOK, resp1.StatusCode) + utils.AssertEqual(t, nil, err2) + utils.AssertEqual(t, fiber.StatusOK, resp2.StatusCode) +} + +// go test -v -run=^$ -bench=Benchmark_Logger -benchmem -count=4 +func Benchmark_Logger(b *testing.B) { + benchSetup := func(b *testing.B, app *fiber.App) { + b.Helper() + + h := app.Handler() + + fctx := &fasthttp.RequestCtx{} + fctx.Request.Header.SetMethod(fiber.MethodGet) + fctx.Request.SetRequestURI("/") + + b.ReportAllocs() + b.ResetTimer() + + for n := 0; n < b.N; n++ { + h(fctx) + } + + utils.AssertEqual(b, 200, fctx.Response.Header.StatusCode()) + } + + b.Run("Base", func(bb *testing.B) { + app := fiber.New() + app.Use(New(Config{ + Format: "${bytesReceived} ${bytesSent} ${status}", + Output: io.Discard, + })) + app.Get("/", func(c *fiber.Ctx) error { + c.Set("test", "test") + return c.SendString("Hello, World!") + }) + benchSetup(bb, app) + }) + + b.Run("DefaultFormat", func(bb *testing.B) { + app := fiber.New() + app.Use(New(Config{ + Output: io.Discard, + })) + app.Get("/", func(c *fiber.Ctx) error { + return c.SendString("Hello, World!") + }) + benchSetup(bb, app) + }) + + b.Run("WithTagParameter", func(bb *testing.B) { + app := fiber.New() + app.Use(New(Config{ + Format: "${bytesReceived} ${bytesSent} ${status} ${reqHeader:test}", + Output: io.Discard, + })) + app.Get("/", func(c *fiber.Ctx) error { + c.Set("test", "test") + return c.SendString("Hello, World!") + }) + benchSetup(bb, app) + }) +} + +// go test -run Test_Response_Header +func Test_Response_Header(t *testing.T) { + t.Parallel() + buf := bytebufferpool.Get() + defer bytebufferpool.Put(buf) + + app := fiber.New() + app.Use(requestid.New(requestid.Config{ + Next: nil, + Header: fiber.HeaderXRequestID, + Generator: func() string { return "Hello fiber!" }, + ContextKey: "requestid", + })) + app.Use(New(Config{ + Format: "${respHeader:X-Request-ID}", + Output: buf, + })) + app.Get("/", func(c *fiber.Ctx) error { + return c.SendString("Hello fiber!") + }) + + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, fiber.StatusOK, resp.StatusCode) + utils.AssertEqual(t, "Hello fiber!", buf.String()) +} + +// go test -run Test_Req_Header +func Test_Req_Header(t *testing.T) { + t.Parallel() + buf := bytebufferpool.Get() + defer bytebufferpool.Put(buf) + + app := fiber.New() + app.Use(New(Config{ + Format: "${header:test}", + Output: buf, + })) + app.Get("/", func(c *fiber.Ctx) error { + return c.SendString("Hello fiber!") + }) + headerReq := httptest.NewRequest(fiber.MethodGet, "/", nil) + headerReq.Header.Add("test", "Hello fiber!") + + resp, err := app.Test(headerReq) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, fiber.StatusOK, resp.StatusCode) + utils.AssertEqual(t, "Hello fiber!", buf.String()) +} + +// go test -run Test_ReqHeader_Header +func Test_ReqHeader_Header(t *testing.T) { + t.Parallel() + buf := bytebufferpool.Get() + defer bytebufferpool.Put(buf) + + app := fiber.New() + app.Use(New(Config{ + Format: "${reqHeader:test}", + Output: buf, + })) + app.Get("/", func(c *fiber.Ctx) error { + return c.SendString("Hello fiber!") + }) + reqHeaderReq := httptest.NewRequest(fiber.MethodGet, "/", nil) + reqHeaderReq.Header.Add("test", "Hello fiber!") + + resp, err := app.Test(reqHeaderReq) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, fiber.StatusOK, resp.StatusCode) + utils.AssertEqual(t, "Hello fiber!", buf.String()) +} + +// go test -run Test_CustomTags +func Test_CustomTags(t *testing.T) { + t.Parallel() + customTag := "it is a custom tag" + + buf := bytebufferpool.Get() + defer bytebufferpool.Put(buf) + + app := fiber.New() + app.Use(New(Config{ + Format: "${custom_tag}", + CustomTags: map[string]LogFunc{ + "custom_tag": func(output Buffer, c *fiber.Ctx, data *Data, extraParam string) (int, error) { + return output.WriteString(customTag) + }, + }, + Output: buf, + })) + app.Get("/", func(c *fiber.Ctx) error { + return c.SendString("Hello fiber!") + }) + reqHeaderReq := httptest.NewRequest(fiber.MethodGet, "/", nil) + reqHeaderReq.Header.Add("test", "Hello fiber!") + + resp, err := app.Test(reqHeaderReq) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, fiber.StatusOK, resp.StatusCode) + utils.AssertEqual(t, customTag, buf.String()) +} + +// go test -run Test_Logger_ByteSent_Streaming +func Test_Logger_ByteSent_Streaming(t *testing.T) { + t.Parallel() + app := fiber.New() + + buf := bytebufferpool.Get() + defer bytebufferpool.Put(buf) + + app.Use(New(Config{ + Format: "${bytesReceived} ${bytesSent} ${status}", + Output: buf, + })) + + app.Get("/", func(c *fiber.Ctx) error { + c.Set("Connection", "keep-alive") + c.Set("Transfer-Encoding", "chunked") + c.Context().SetBodyStreamWriter(func(w *bufio.Writer) { + var i int + for { + i++ + msg := fmt.Sprintf("%d - the time is %v", i, time.Now()) + fmt.Fprintf(w, "data: Message: %s\n\n", msg) + err := w.Flush() + if err != nil { + break + } + if i == 10 { + break + } + } + }) + return nil + }) + + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, fiber.StatusOK, resp.StatusCode) + utils.AssertEqual(t, "-2 -1 200", buf.String()) +} + +// go test -run Test_Logger_EnableColors +func Test_Logger_EnableColors(t *testing.T) { + t.Parallel() + o := new(fakeOutput) + app := fiber.New() + app.Use(New(Config{ + Output: o, + })) + + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, fiber.StatusNotFound, resp.StatusCode) + + utils.AssertEqual(t, 1, int(*o)) +} diff --git a/middleware/logger/tags.go b/middleware/logger/tags.go new file mode 100644 index 0000000..c348871 --- /dev/null +++ b/middleware/logger/tags.go @@ -0,0 +1,209 @@ +package logger + +import ( + "fmt" + "strconv" + "strings" + + "github.com/gofiber/fiber/v2" +) + +// Logger variables +const ( + TagPid = "pid" + TagTime = "time" + TagReferer = "referer" + TagProtocol = "protocol" + TagPort = "port" + TagIP = "ip" + TagIPs = "ips" + TagHost = "host" + TagMethod = "method" + TagPath = "path" + TagURL = "url" + TagUA = "ua" + TagLatency = "latency" + TagStatus = "status" + TagResBody = "resBody" + TagReqHeaders = "reqHeaders" + TagQueryStringParams = "queryParams" + TagBody = "body" + TagBytesSent = "bytesSent" + TagBytesReceived = "bytesReceived" + TagRoute = "route" + TagError = "error" + // Deprecated: Use TagReqHeader instead + TagHeader = "header:" + TagReqHeader = "reqHeader:" + TagRespHeader = "respHeader:" + TagLocals = "locals:" + TagQuery = "query:" + TagForm = "form:" + TagCookie = "cookie:" + TagBlack = "black" + TagRed = "red" + TagGreen = "green" + TagYellow = "yellow" + TagBlue = "blue" + TagMagenta = "magenta" + TagCyan = "cyan" + TagWhite = "white" + TagReset = "reset" +) + +// createTagMap function merged the default with the custom tags +func createTagMap(cfg *Config) map[string]LogFunc { + // Set default tags + tagFunctions := map[string]LogFunc{ + TagReferer: func(output Buffer, c *fiber.Ctx, data *Data, extraParam string) (int, error) { + return output.WriteString(c.Get(fiber.HeaderReferer)) + }, + TagProtocol: func(output Buffer, c *fiber.Ctx, data *Data, extraParam string) (int, error) { + return output.WriteString(c.Protocol()) + }, + TagPort: func(output Buffer, c *fiber.Ctx, data *Data, extraParam string) (int, error) { + return output.WriteString(c.Port()) + }, + TagIP: func(output Buffer, c *fiber.Ctx, data *Data, extraParam string) (int, error) { + return output.WriteString(c.IP()) + }, + TagIPs: func(output Buffer, c *fiber.Ctx, data *Data, extraParam string) (int, error) { + return output.WriteString(c.Get(fiber.HeaderXForwardedFor)) + }, + TagHost: func(output Buffer, c *fiber.Ctx, data *Data, extraParam string) (int, error) { + return output.WriteString(c.Hostname()) + }, + TagPath: func(output Buffer, c *fiber.Ctx, data *Data, extraParam string) (int, error) { + return output.WriteString(c.Path()) + }, + TagURL: func(output Buffer, c *fiber.Ctx, data *Data, extraParam string) (int, error) { + return output.WriteString(c.OriginalURL()) + }, + TagUA: func(output Buffer, c *fiber.Ctx, data *Data, extraParam string) (int, error) { + return output.WriteString(c.Get(fiber.HeaderUserAgent)) + }, + TagBody: func(output Buffer, c *fiber.Ctx, data *Data, extraParam string) (int, error) { + return output.Write(c.Body()) + }, + TagBytesReceived: func(output Buffer, c *fiber.Ctx, data *Data, extraParam string) (int, error) { + return output.WriteString(strconv.Itoa((c.Request().Header.ContentLength()))) + }, + TagBytesSent: func(output Buffer, c *fiber.Ctx, data *Data, extraParam string) (int, error) { + return output.WriteString(strconv.Itoa((c.Response().Header.ContentLength()))) + }, + TagRoute: func(output Buffer, c *fiber.Ctx, data *Data, extraParam string) (int, error) { + return output.WriteString(c.Route().Path) + }, + TagResBody: func(output Buffer, c *fiber.Ctx, data *Data, extraParam string) (int, error) { + return output.Write(c.Response().Body()) + }, + TagReqHeaders: func(output Buffer, c *fiber.Ctx, data *Data, extraParam string) (int, error) { + reqHeaders := make([]string, 0) + for k, v := range c.GetReqHeaders() { + reqHeaders = append(reqHeaders, k+"="+strings.Join(v, ",")) + } + return output.Write([]byte(strings.Join(reqHeaders, "&"))) + }, + TagQueryStringParams: func(output Buffer, c *fiber.Ctx, data *Data, extraParam string) (int, error) { + return output.WriteString(c.Request().URI().QueryArgs().String()) + }, + + TagBlack: func(output Buffer, c *fiber.Ctx, data *Data, extraParam string) (int, error) { + return output.WriteString(c.App().Config().ColorScheme.Black) + }, + TagRed: func(output Buffer, c *fiber.Ctx, data *Data, extraParam string) (int, error) { + return output.WriteString(c.App().Config().ColorScheme.Red) + }, + TagGreen: func(output Buffer, c *fiber.Ctx, data *Data, extraParam string) (int, error) { + return output.WriteString(c.App().Config().ColorScheme.Green) + }, + TagYellow: func(output Buffer, c *fiber.Ctx, data *Data, extraParam string) (int, error) { + return output.WriteString(c.App().Config().ColorScheme.Yellow) + }, + TagBlue: func(output Buffer, c *fiber.Ctx, data *Data, extraParam string) (int, error) { + return output.WriteString(c.App().Config().ColorScheme.Blue) + }, + TagMagenta: func(output Buffer, c *fiber.Ctx, data *Data, extraParam string) (int, error) { + return output.WriteString(c.App().Config().ColorScheme.Magenta) + }, + TagCyan: func(output Buffer, c *fiber.Ctx, data *Data, extraParam string) (int, error) { + return output.WriteString(c.App().Config().ColorScheme.Cyan) + }, + TagWhite: func(output Buffer, c *fiber.Ctx, data *Data, extraParam string) (int, error) { + return output.WriteString(c.App().Config().ColorScheme.White) + }, + TagReset: func(output Buffer, c *fiber.Ctx, data *Data, extraParam string) (int, error) { + return output.WriteString(c.App().Config().ColorScheme.Reset) + }, + TagError: func(output Buffer, c *fiber.Ctx, data *Data, extraParam string) (int, error) { + if data.ChainErr != nil { + if cfg.enableColors { + colors := c.App().Config().ColorScheme + return output.WriteString(fmt.Sprintf("%s%s%s", colors.Red, data.ChainErr.Error(), colors.Reset)) + } + return output.WriteString(data.ChainErr.Error()) + } + return output.WriteString("-") + }, + TagReqHeader: func(output Buffer, c *fiber.Ctx, data *Data, extraParam string) (int, error) { + return output.WriteString(c.Get(extraParam)) + }, + TagHeader: func(output Buffer, c *fiber.Ctx, data *Data, extraParam string) (int, error) { + return output.WriteString(c.Get(extraParam)) + }, + TagRespHeader: func(output Buffer, c *fiber.Ctx, data *Data, extraParam string) (int, error) { + return output.WriteString(c.GetRespHeader(extraParam)) + }, + TagQuery: func(output Buffer, c *fiber.Ctx, data *Data, extraParam string) (int, error) { + return output.WriteString(c.Query(extraParam)) + }, + TagForm: func(output Buffer, c *fiber.Ctx, data *Data, extraParam string) (int, error) { + return output.WriteString(c.FormValue(extraParam)) + }, + TagCookie: func(output Buffer, c *fiber.Ctx, data *Data, extraParam string) (int, error) { + return output.WriteString(c.Cookies(extraParam)) + }, + TagLocals: func(output Buffer, c *fiber.Ctx, data *Data, extraParam string) (int, error) { + switch v := c.Locals(extraParam).(type) { + case []byte: + return output.Write(v) + case string: + return output.WriteString(v) + case nil: + return 0, nil + default: + return output.WriteString(fmt.Sprintf("%v", v)) + } + }, + TagStatus: func(output Buffer, c *fiber.Ctx, data *Data, extraParam string) (int, error) { + if cfg.enableColors { + colors := c.App().Config().ColorScheme + return output.WriteString(fmt.Sprintf("%s%3d%s", statusColor(c.Response().StatusCode(), colors), c.Response().StatusCode(), colors.Reset)) + } + return appendInt(output, c.Response().StatusCode()) + }, + TagMethod: func(output Buffer, c *fiber.Ctx, data *Data, extraParam string) (int, error) { + if cfg.enableColors { + colors := c.App().Config().ColorScheme + return output.WriteString(fmt.Sprintf("%s%s%s", methodColor(c.Method(), colors), c.Method(), colors.Reset)) + } + return output.WriteString(c.Method()) + }, + TagPid: func(output Buffer, c *fiber.Ctx, data *Data, extraParam string) (int, error) { + return output.WriteString(data.Pid) + }, + TagLatency: func(output Buffer, c *fiber.Ctx, data *Data, extraParam string) (int, error) { + latency := data.Stop.Sub(data.Start) + return output.WriteString(fmt.Sprintf("%13v", latency)) + }, + TagTime: func(output Buffer, c *fiber.Ctx, data *Data, extraParam string) (int, error) { + return output.WriteString(data.Timestamp.Load().(string)) //nolint:forcetypeassert // We always store a string in here + }, + } + // merge with custom tags from user + for k, v := range cfg.CustomTags { + tagFunctions[k] = v + } + + return tagFunctions +} diff --git a/middleware/logger/template_chain.go b/middleware/logger/template_chain.go new file mode 100644 index 0000000..1a5dd44 --- /dev/null +++ b/middleware/logger/template_chain.go @@ -0,0 +1,70 @@ +package logger + +import ( + "bytes" + "errors" + + "github.com/gofiber/fiber/v2/utils" +) + +// buildLogFuncChain analyzes the template and creates slices with the functions for execution and +// slices with the fixed parts of the template and the parameters +// +// fixParts contains the fixed parts of the template or parameters if a function is stored in the funcChain at this position +// funcChain contains for the parts which exist the functions for the dynamic parts +// funcChain and fixParts always have the same length and contain nil for the parts where no data is required in the chain, +// if a function exists for the part, a parameter for it can also exist in the fixParts slice +func buildLogFuncChain(cfg *Config, tagFunctions map[string]LogFunc) ([][]byte, []LogFunc, error) { + // process flow is copied from the fasttemplate flow https://github.com/valyala/fasttemplate/blob/2a2d1afadadf9715bfa19683cdaeac8347e5d9f9/template.go#L23-L62 + templateB := utils.UnsafeBytes(cfg.Format) + startTagB := utils.UnsafeBytes(startTag) + endTagB := utils.UnsafeBytes(endTag) + paramSeparatorB := utils.UnsafeBytes(paramSeparator) + + var fixParts [][]byte + var funcChain []LogFunc + + for { + currentPos := bytes.Index(templateB, startTagB) + if currentPos < 0 { + // no starting tag found in the existing template part + break + } + // add fixed part + funcChain = append(funcChain, nil) + fixParts = append(fixParts, templateB[:currentPos]) + + templateB = templateB[currentPos+len(startTagB):] + currentPos = bytes.Index(templateB, endTagB) + if currentPos < 0 { + // cannot find end tag - just write it to the output. + funcChain = append(funcChain, nil) + fixParts = append(fixParts, startTagB) + break + } + // ## function block ## + // first check for tags with parameters + if index := bytes.Index(templateB[:currentPos], paramSeparatorB); index != -1 { + logFunc, ok := tagFunctions[utils.UnsafeString(templateB[:index+1])] + if !ok { + return nil, nil, errors.New("No parameter found in \"" + utils.UnsafeString(templateB[:currentPos]) + "\"") + } + funcChain = append(funcChain, logFunc) + // add param to the fixParts + fixParts = append(fixParts, templateB[index+1:currentPos]) + } else if logFunc, ok := tagFunctions[utils.UnsafeString(templateB[:currentPos])]; ok { + // add functions without parameter + funcChain = append(funcChain, logFunc) + fixParts = append(fixParts, nil) + } + // ## function block end ## + + // reduce the template string + templateB = templateB[currentPos+len(endTagB):] + } + // set the rest + funcChain = append(funcChain, nil) + fixParts = append(fixParts, templateB) + + return fixParts, funcChain, nil +} diff --git a/middleware/logger/utils.go b/middleware/logger/utils.go new file mode 100644 index 0000000..2af456d --- /dev/null +++ b/middleware/logger/utils.go @@ -0,0 +1,39 @@ +package logger + +import ( + "github.com/gofiber/fiber/v2" +) + +func methodColor(method string, colors fiber.Colors) string { + switch method { + case fiber.MethodGet: + return colors.Cyan + case fiber.MethodPost: + return colors.Green + case fiber.MethodPut: + return colors.Yellow + case fiber.MethodDelete: + return colors.Red + case fiber.MethodPatch: + return colors.White + case fiber.MethodHead: + return colors.Magenta + case fiber.MethodOptions: + return colors.Blue + default: + return colors.Reset + } +} + +func statusColor(code int, colors fiber.Colors) string { + switch { + case code >= fiber.StatusOK && code < fiber.StatusMultipleChoices: + return colors.Green + case code >= fiber.StatusMultipleChoices && code < fiber.StatusBadRequest: + return colors.Blue + case code >= fiber.StatusBadRequest && code < fiber.StatusInternalServerError: + return colors.Yellow + default: + return colors.Red + } +} diff --git a/middleware/monitor/config.go b/middleware/monitor/config.go new file mode 100644 index 0000000..3c56ada --- /dev/null +++ b/middleware/monitor/config.go @@ -0,0 +1,132 @@ +package monitor + +import ( + "time" + + "github.com/gofiber/fiber/v2" +) + +// Config defines the config for middleware. +type Config struct { + // Metrics page title + // + // Optional. Default: "Fiber Monitor" + Title string + + // Refresh period + // + // Optional. Default: 3 seconds + Refresh time.Duration + + // Whether the service should expose only the monitoring API. + // + // Optional. Default: false + APIOnly bool + + // Next defines a function to skip this middleware when returned true. + // + // Optional. Default: nil + Next func(c *fiber.Ctx) bool + + // Custom HTML Code to Head Section(Before End) + // + // Optional. Default: empty + CustomHead string + + // FontURL for specify font resource path or URL . also you can use relative path + // + // Optional. Default: https://fonts.googleapis.com/css2?family=Roboto:wght@400;900&display=swap + FontURL string + + // ChartJsURL for specify ChartJS library path or URL . also you can use relative path + // + // Optional. Default: https://cdn.jsdelivr.net/npm/chart.js@2.9/dist/Chart.bundle.min.js + ChartJsURL string // TODO: Rename to "ChartJSURL" in v3 + + index string +} + +var ConfigDefault = Config{ + Title: defaultTitle, + Refresh: defaultRefresh, + FontURL: defaultFontURL, + ChartJsURL: defaultChartJSURL, + CustomHead: defaultCustomHead, + APIOnly: false, + Next: nil, + index: newIndex(viewBag{ + defaultTitle, + defaultRefresh, + defaultFontURL, + defaultChartJSURL, + defaultCustomHead, + }), +} + +func configDefault(config ...Config) Config { + // Users can change ConfigDefault.Title/Refresh which then + // become incompatible with ConfigDefault.index + if ConfigDefault.Title != defaultTitle || + ConfigDefault.Refresh != defaultRefresh || + ConfigDefault.FontURL != defaultFontURL || + ConfigDefault.ChartJsURL != defaultChartJSURL || + ConfigDefault.CustomHead != defaultCustomHead { + if ConfigDefault.Refresh < minRefresh { + ConfigDefault.Refresh = minRefresh + } + // update default index with new default title/refresh + ConfigDefault.index = newIndex(viewBag{ + ConfigDefault.Title, + ConfigDefault.Refresh, + ConfigDefault.FontURL, + ConfigDefault.ChartJsURL, + ConfigDefault.CustomHead, + }) + } + + // Return default config if nothing provided + if len(config) < 1 { + return ConfigDefault + } + + // Override default config + cfg := config[0] + + // Set default values + if cfg.Title == "" { + cfg.Title = ConfigDefault.Title + } + + if cfg.Refresh == 0 { + cfg.Refresh = ConfigDefault.Refresh + } + if cfg.FontURL == "" { + cfg.FontURL = defaultFontURL + } + + if cfg.ChartJsURL == "" { + cfg.ChartJsURL = defaultChartJSURL + } + if cfg.Refresh < minRefresh { + cfg.Refresh = minRefresh + } + + if cfg.Next == nil { + cfg.Next = ConfigDefault.Next + } + + if !cfg.APIOnly { + cfg.APIOnly = ConfigDefault.APIOnly + } + + // update cfg.index with custom title/refresh + cfg.index = newIndex(viewBag{ + title: cfg.Title, + refresh: cfg.Refresh, + fontURL: cfg.FontURL, + chartJSURL: cfg.ChartJsURL, + customHead: cfg.CustomHead, + }) + + return cfg +} diff --git a/middleware/monitor/config_test.go b/middleware/monitor/config_test.go new file mode 100644 index 0000000..fadc17e --- /dev/null +++ b/middleware/monitor/config_test.go @@ -0,0 +1,163 @@ +package monitor + +import ( + "testing" + "time" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/utils" +) + +func Test_Config_Default(t *testing.T) { + t.Parallel() + + t.Run("use default", func(t *testing.T) { + t.Parallel() + cfg := configDefault() + + utils.AssertEqual(t, defaultTitle, cfg.Title) + utils.AssertEqual(t, defaultRefresh, cfg.Refresh) + utils.AssertEqual(t, defaultFontURL, cfg.FontURL) + utils.AssertEqual(t, defaultChartJSURL, cfg.ChartJsURL) + utils.AssertEqual(t, defaultCustomHead, cfg.CustomHead) + utils.AssertEqual(t, false, cfg.APIOnly) + utils.AssertEqual(t, (func(*fiber.Ctx) bool)(nil), cfg.Next) + utils.AssertEqual(t, newIndex(viewBag{defaultTitle, defaultRefresh, defaultFontURL, defaultChartJSURL, defaultCustomHead}), cfg.index) + }) + + t.Run("set title", func(t *testing.T) { + t.Parallel() + title := "title" + cfg := configDefault(Config{ + Title: title, + }) + + utils.AssertEqual(t, title, cfg.Title) + utils.AssertEqual(t, defaultRefresh, cfg.Refresh) + utils.AssertEqual(t, defaultFontURL, cfg.FontURL) + utils.AssertEqual(t, defaultChartJSURL, cfg.ChartJsURL) + utils.AssertEqual(t, defaultCustomHead, cfg.CustomHead) + utils.AssertEqual(t, false, cfg.APIOnly) + utils.AssertEqual(t, (func(*fiber.Ctx) bool)(nil), cfg.Next) + utils.AssertEqual(t, newIndex(viewBag{title, defaultRefresh, defaultFontURL, defaultChartJSURL, defaultCustomHead}), cfg.index) + }) + + t.Run("set refresh less than default", func(t *testing.T) { + t.Parallel() + cfg := configDefault(Config{ + Refresh: 100 * time.Millisecond, + }) + + utils.AssertEqual(t, defaultTitle, cfg.Title) + utils.AssertEqual(t, minRefresh, cfg.Refresh) + utils.AssertEqual(t, defaultFontURL, cfg.FontURL) + utils.AssertEqual(t, defaultChartJSURL, cfg.ChartJsURL) + utils.AssertEqual(t, defaultCustomHead, cfg.CustomHead) + utils.AssertEqual(t, false, cfg.APIOnly) + utils.AssertEqual(t, (func(*fiber.Ctx) bool)(nil), cfg.Next) + utils.AssertEqual(t, newIndex(viewBag{defaultTitle, minRefresh, defaultFontURL, defaultChartJSURL, defaultCustomHead}), cfg.index) + }) + + t.Run("set refresh", func(t *testing.T) { + t.Parallel() + refresh := time.Second + cfg := configDefault(Config{ + Refresh: refresh, + }) + + utils.AssertEqual(t, defaultTitle, cfg.Title) + utils.AssertEqual(t, refresh, cfg.Refresh) + utils.AssertEqual(t, defaultFontURL, cfg.FontURL) + utils.AssertEqual(t, defaultChartJSURL, cfg.ChartJsURL) + utils.AssertEqual(t, defaultCustomHead, cfg.CustomHead) + utils.AssertEqual(t, false, cfg.APIOnly) + utils.AssertEqual(t, (func(*fiber.Ctx) bool)(nil), cfg.Next) + utils.AssertEqual(t, newIndex(viewBag{defaultTitle, refresh, defaultFontURL, defaultChartJSURL, defaultCustomHead}), cfg.index) + }) + + t.Run("set font url", func(t *testing.T) { + t.Parallel() + fontURL := "https://example.com" + cfg := configDefault(Config{ + FontURL: fontURL, + }) + + utils.AssertEqual(t, defaultTitle, cfg.Title) + utils.AssertEqual(t, defaultRefresh, cfg.Refresh) + utils.AssertEqual(t, fontURL, cfg.FontURL) + utils.AssertEqual(t, defaultChartJSURL, cfg.ChartJsURL) + utils.AssertEqual(t, defaultCustomHead, cfg.CustomHead) + utils.AssertEqual(t, false, cfg.APIOnly) + utils.AssertEqual(t, (func(*fiber.Ctx) bool)(nil), cfg.Next) + utils.AssertEqual(t, newIndex(viewBag{defaultTitle, defaultRefresh, fontURL, defaultChartJSURL, defaultCustomHead}), cfg.index) + }) + + t.Run("set chart js url", func(t *testing.T) { + t.Parallel() + chartURL := "http://example.com" + cfg := configDefault(Config{ + ChartJsURL: chartURL, + }) + + utils.AssertEqual(t, defaultTitle, cfg.Title) + utils.AssertEqual(t, defaultRefresh, cfg.Refresh) + utils.AssertEqual(t, defaultFontURL, cfg.FontURL) + utils.AssertEqual(t, chartURL, cfg.ChartJsURL) + utils.AssertEqual(t, defaultCustomHead, cfg.CustomHead) + utils.AssertEqual(t, false, cfg.APIOnly) + utils.AssertEqual(t, (func(*fiber.Ctx) bool)(nil), cfg.Next) + utils.AssertEqual(t, newIndex(viewBag{defaultTitle, defaultRefresh, defaultFontURL, chartURL, defaultCustomHead}), cfg.index) + }) + + t.Run("set custom head", func(t *testing.T) { + t.Parallel() + head := "head" + cfg := configDefault(Config{ + CustomHead: head, + }) + + utils.AssertEqual(t, defaultTitle, cfg.Title) + utils.AssertEqual(t, defaultRefresh, cfg.Refresh) + utils.AssertEqual(t, defaultFontURL, cfg.FontURL) + utils.AssertEqual(t, defaultChartJSURL, cfg.ChartJsURL) + utils.AssertEqual(t, head, cfg.CustomHead) + utils.AssertEqual(t, false, cfg.APIOnly) + utils.AssertEqual(t, (func(*fiber.Ctx) bool)(nil), cfg.Next) + utils.AssertEqual(t, newIndex(viewBag{defaultTitle, defaultRefresh, defaultFontURL, defaultChartJSURL, head}), cfg.index) + }) + + t.Run("set api only", func(t *testing.T) { + t.Parallel() + cfg := configDefault(Config{ + APIOnly: true, + }) + + utils.AssertEqual(t, defaultTitle, cfg.Title) + utils.AssertEqual(t, defaultRefresh, cfg.Refresh) + utils.AssertEqual(t, defaultFontURL, cfg.FontURL) + utils.AssertEqual(t, defaultChartJSURL, cfg.ChartJsURL) + utils.AssertEqual(t, defaultCustomHead, cfg.CustomHead) + utils.AssertEqual(t, true, cfg.APIOnly) + utils.AssertEqual(t, (func(*fiber.Ctx) bool)(nil), cfg.Next) + utils.AssertEqual(t, newIndex(viewBag{defaultTitle, defaultRefresh, defaultFontURL, defaultChartJSURL, defaultCustomHead}), cfg.index) + }) + + t.Run("set next", func(t *testing.T) { + t.Parallel() + f := func(c *fiber.Ctx) bool { + return true + } + cfg := configDefault(Config{ + Next: f, + }) + + utils.AssertEqual(t, defaultTitle, cfg.Title) + utils.AssertEqual(t, defaultRefresh, cfg.Refresh) + utils.AssertEqual(t, defaultFontURL, cfg.FontURL) + utils.AssertEqual(t, defaultChartJSURL, cfg.ChartJsURL) + utils.AssertEqual(t, defaultCustomHead, cfg.CustomHead) + utils.AssertEqual(t, false, cfg.APIOnly) + utils.AssertEqual(t, f(nil), cfg.Next(nil)) + utils.AssertEqual(t, newIndex(viewBag{defaultTitle, defaultRefresh, defaultFontURL, defaultChartJSURL, defaultCustomHead}), cfg.index) + }) +} diff --git a/middleware/monitor/index.go b/middleware/monitor/index.go new file mode 100644 index 0000000..c873290 --- /dev/null +++ b/middleware/monitor/index.go @@ -0,0 +1,271 @@ +package monitor + +import ( + "strconv" + "strings" + "time" +) + +type viewBag struct { + title string + refresh time.Duration + fontURL string + chartJSURL string + customHead string +} + +// returns index with new title/refresh +func newIndex(dat viewBag) string { + timeout := dat.refresh.Milliseconds() - timeoutDiff + if timeout < timeoutDiff { + timeout = timeoutDiff + } + ts := strconv.FormatInt(timeout, 10) + replacer := strings.NewReplacer("$TITLE", dat.title, "$TIMEOUT", ts, + "$FONT_URL", dat.fontURL, "$CHART_JS_URL", dat.chartJSURL, "$CUSTOM_HEAD", dat.customHead, + ) + return replacer.Replace(indexHTML) +} + +const ( + defaultTitle = "Fiber Monitor" + + defaultRefresh = 3 * time.Second + timeoutDiff = 200 // timeout will be Refresh (in milliseconds) - timeoutDiff + minRefresh = timeoutDiff * time.Millisecond + defaultFontURL = `https://fonts.googleapis.com/css2?family=Roboto:wght@400;900&display=swap` + defaultChartJSURL = `https://cdn.jsdelivr.net/npm/chart.js@2.9/dist/Chart.bundle.min.js` + defaultCustomHead = `` + + // parametrized by $TITLE and $TIMEOUT + indexHTML = ` + + + + + + + + $TITLE + + + +
+

$TITLE

+
+
+
+
CPU Usage
+

0.00%

+
+
+ +
+
+
+
+
Memory Usage
+

0.00 MB

+
+
+ +
+
+
+
+
Response Time
+

0ms

+
+
+ +
+
+
+
+
Open Connections
+

0

+
+
+ +
+
+
+
+ + + +` +) diff --git a/middleware/monitor/monitor.go b/middleware/monitor/monitor.go new file mode 100644 index 0000000..3f64df9 --- /dev/null +++ b/middleware/monitor/monitor.go @@ -0,0 +1,137 @@ +package monitor + +import ( + "os" + "runtime" + "sync" + "sync/atomic" + "time" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/internal/gopsutil/cpu" + "github.com/gofiber/fiber/v2/internal/gopsutil/load" + "github.com/gofiber/fiber/v2/internal/gopsutil/mem" + "github.com/gofiber/fiber/v2/internal/gopsutil/net" + "github.com/gofiber/fiber/v2/internal/gopsutil/process" +) + +type stats struct { + PID statsPID `json:"pid"` + OS statsOS `json:"os"` +} + +type statsPID struct { + CPU float64 `json:"cpu"` + RAM uint64 `json:"ram"` + Conns int `json:"conns"` +} + +type statsOS struct { + CPU float64 `json:"cpu"` + RAM uint64 `json:"ram"` + TotalRAM uint64 `json:"total_ram"` + LoadAvg float64 `json:"load_avg"` + Conns int `json:"conns"` +} + +var ( + monitPIDCPU atomic.Value + monitPIDRAM atomic.Value + monitPIDConns atomic.Value + + monitOSCPU atomic.Value + monitOSRAM atomic.Value + monitOSTotalRAM atomic.Value + monitOSLoadAvg atomic.Value + monitOSConns atomic.Value +) + +var ( + mutex sync.RWMutex + once sync.Once + data = &stats{} +) + +// New creates a new middleware handler +func New(config ...Config) fiber.Handler { + // Set default config + cfg := configDefault(config...) + + // Start routine to update statistics + once.Do(func() { + p, _ := process.NewProcess(int32(os.Getpid())) //nolint:errcheck // TODO: Handle error + numcpu := runtime.NumCPU() + updateStatistics(p, numcpu) + + go func() { + for { + time.Sleep(cfg.Refresh) + + updateStatistics(p, numcpu) + } + }() + }) + + // Return new handler + //nolint:errcheck // Ignore the type-assertion errors + return func(c *fiber.Ctx) error { + // Don't execute middleware if Next returns true + if cfg.Next != nil && cfg.Next(c) { + return c.Next() + } + + if c.Method() != fiber.MethodGet { + return fiber.ErrMethodNotAllowed + } + if c.Get(fiber.HeaderAccept) == fiber.MIMEApplicationJSON || cfg.APIOnly { + mutex.Lock() + data.PID.CPU, _ = monitPIDCPU.Load().(float64) + data.PID.RAM, _ = monitPIDRAM.Load().(uint64) + data.PID.Conns, _ = monitPIDConns.Load().(int) + + data.OS.CPU, _ = monitOSCPU.Load().(float64) + data.OS.RAM, _ = monitOSRAM.Load().(uint64) + data.OS.TotalRAM, _ = monitOSTotalRAM.Load().(uint64) + data.OS.LoadAvg, _ = monitOSLoadAvg.Load().(float64) + data.OS.Conns, _ = monitOSConns.Load().(int) + mutex.Unlock() + return c.Status(fiber.StatusOK).JSON(data) + } + c.Set(fiber.HeaderContentType, fiber.MIMETextHTMLCharsetUTF8) + return c.Status(fiber.StatusOK).SendString(cfg.index) + } +} + +func updateStatistics(p *process.Process, numcpu int) { + pidCPU, err := p.Percent(0) + if err == nil { + monitPIDCPU.Store(pidCPU / float64(numcpu)) + } + + if osCPU, err := cpu.Percent(0, false); err == nil && len(osCPU) > 0 { + monitOSCPU.Store(osCPU[0]) + } + + if pidRAM, err := p.MemoryInfo(); err == nil && pidRAM != nil { + monitPIDRAM.Store(pidRAM.RSS) + } + + if osRAM, err := mem.VirtualMemory(); err == nil && osRAM != nil { + monitOSRAM.Store(osRAM.Used) + monitOSTotalRAM.Store(osRAM.Total) + } + + if loadAvg, err := load.Avg(); err == nil && loadAvg != nil { + monitOSLoadAvg.Store(loadAvg.Load1) + } + + pidConns, err := net.ConnectionsPid("tcp", p.Pid) + if err == nil { + monitPIDConns.Store(len(pidConns)) + } + + osConns, err := net.Connections("tcp") + if err == nil { + monitOSConns.Store(len(osConns)) + } +} diff --git a/middleware/monitor/monitor_test.go b/middleware/monitor/monitor_test.go new file mode 100644 index 0000000..f12c505 --- /dev/null +++ b/middleware/monitor/monitor_test.go @@ -0,0 +1,198 @@ +package monitor + +import ( + "bytes" + "fmt" + "io" + "net/http/httptest" + "testing" + "time" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/utils" + + "github.com/valyala/fasthttp" +) + +func Test_Monitor_405(t *testing.T) { + t.Parallel() + + app := fiber.New() + + app.Use("/", New()) + + resp, err := app.Test(httptest.NewRequest(fiber.MethodPost, "/", nil)) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, 405, resp.StatusCode) +} + +func Test_Monitor_Html(t *testing.T) { + t.Parallel() + + app := fiber.New() + + // defaults + app.Get("/", New()) + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) + + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, 200, resp.StatusCode) + utils.AssertEqual(t, fiber.MIMETextHTMLCharsetUTF8, + resp.Header.Get(fiber.HeaderContentType)) + buf, err := io.ReadAll(resp.Body) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, true, bytes.Contains(buf, []byte(""+defaultTitle+""))) + timeoutLine := fmt.Sprintf("setTimeout(fetchJSON, %d)", + defaultRefresh.Milliseconds()-timeoutDiff) + utils.AssertEqual(t, true, bytes.Contains(buf, []byte(timeoutLine))) + + // custom config + conf := Config{Title: "New " + defaultTitle, Refresh: defaultRefresh + time.Second} + app.Get("/custom", New(conf)) + resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/custom", nil)) + + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, 200, resp.StatusCode) + utils.AssertEqual(t, fiber.MIMETextHTMLCharsetUTF8, + resp.Header.Get(fiber.HeaderContentType)) + buf, err = io.ReadAll(resp.Body) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, true, bytes.Contains(buf, []byte(""+conf.Title+""))) + timeoutLine = fmt.Sprintf("setTimeout(fetchJSON, %d)", + conf.Refresh.Milliseconds()-timeoutDiff) + utils.AssertEqual(t, true, bytes.Contains(buf, []byte(timeoutLine))) +} + +func Test_Monitor_Html_CustomCodes(t *testing.T) { + t.Parallel() + + app := fiber.New() + + // defaults + app.Get("/", New()) + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) + + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, 200, resp.StatusCode) + utils.AssertEqual(t, fiber.MIMETextHTMLCharsetUTF8, + resp.Header.Get(fiber.HeaderContentType)) + buf, err := io.ReadAll(resp.Body) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, true, bytes.Contains(buf, []byte(""+defaultTitle+""))) + timeoutLine := fmt.Sprintf("setTimeout(fetchJSON, %d)", + defaultRefresh.Milliseconds()-timeoutDiff) + utils.AssertEqual(t, true, bytes.Contains(buf, []byte(timeoutLine))) + + // custom config + conf := Config{ + Title: "New " + defaultTitle, + Refresh: defaultRefresh + time.Second, + ChartJsURL: "https://cdnjs.com/libraries/Chart.js", + FontURL: "/public/my-font.css", + CustomHead: ``, + } + app.Get("/custom", New(conf)) + resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/custom", nil)) + + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, 200, resp.StatusCode) + utils.AssertEqual(t, fiber.MIMETextHTMLCharsetUTF8, + resp.Header.Get(fiber.HeaderContentType)) + buf, err = io.ReadAll(resp.Body) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, true, bytes.Contains(buf, []byte(""+conf.Title+""))) + utils.AssertEqual(t, true, bytes.Contains(buf, []byte("https://cdnjs.com/libraries/Chart.js"))) + utils.AssertEqual(t, true, bytes.Contains(buf, []byte("/public/my-font.css"))) + utils.AssertEqual(t, true, bytes.Contains(buf, []byte(conf.CustomHead))) + + timeoutLine = fmt.Sprintf("setTimeout(fetchJSON, %d)", + conf.Refresh.Milliseconds()-timeoutDiff) + utils.AssertEqual(t, true, bytes.Contains(buf, []byte(timeoutLine))) +} + +// go test -run Test_Monitor_JSON -race +func Test_Monitor_JSON(t *testing.T) { + t.Parallel() + + app := fiber.New() + + app.Get("/", New()) + + req := httptest.NewRequest(fiber.MethodGet, "/", nil) + req.Header.Set(fiber.HeaderAccept, fiber.MIMEApplicationJSON) + resp, err := app.Test(req) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, 200, resp.StatusCode) + utils.AssertEqual(t, fiber.MIMEApplicationJSON, resp.Header.Get(fiber.HeaderContentType)) + + b, err := io.ReadAll(resp.Body) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, true, bytes.Contains(b, []byte("pid"))) + utils.AssertEqual(t, true, bytes.Contains(b, []byte("os"))) +} + +// go test -v -run=^$ -bench=Benchmark_Monitor -benchmem -count=4 +func Benchmark_Monitor(b *testing.B) { + app := fiber.New() + + app.Get("/", New()) + + h := app.Handler() + + fctx := &fasthttp.RequestCtx{} + fctx.Request.Header.SetMethod(fiber.MethodGet) + fctx.Request.SetRequestURI("/") + fctx.Request.Header.Set(fiber.HeaderAccept, fiber.MIMEApplicationJSON) + + b.ReportAllocs() + b.ResetTimer() + + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + h(fctx) + } + }) + + utils.AssertEqual(b, 200, fctx.Response.Header.StatusCode()) + utils.AssertEqual(b, + fiber.MIMEApplicationJSON, + string(fctx.Response.Header.Peek(fiber.HeaderContentType))) +} + +// go test -run Test_Monitor_Next +func Test_Monitor_Next(t *testing.T) { + t.Parallel() + + app := fiber.New() + + app.Use("/", New(Config{ + Next: func(_ *fiber.Ctx) bool { + return true + }, + })) + + resp, err := app.Test(httptest.NewRequest(fiber.MethodPost, "/", nil)) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, 404, resp.StatusCode) +} + +// go test -run Test_Monitor_APIOnly -race +func Test_Monitor_APIOnly(t *testing.T) { + app := fiber.New() + + app.Get("/", New(Config{ + APIOnly: true, + })) + + req := httptest.NewRequest(fiber.MethodGet, "/", nil) + req.Header.Set(fiber.HeaderAccept, fiber.MIMEApplicationJSON) + resp, err := app.Test(req) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, 200, resp.StatusCode) + utils.AssertEqual(t, fiber.MIMEApplicationJSON, resp.Header.Get(fiber.HeaderContentType)) + + b, err := io.ReadAll(resp.Body) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, true, bytes.Contains(b, []byte("pid"))) + utils.AssertEqual(t, true, bytes.Contains(b, []byte("os"))) +} diff --git a/middleware/pprof/config.go b/middleware/pprof/config.go new file mode 100644 index 0000000..ef8f05e --- /dev/null +++ b/middleware/pprof/config.go @@ -0,0 +1,41 @@ +package pprof + +import ( + "github.com/gofiber/fiber/v2" +) + +// Config defines the config for middleware. +type Config struct { + // Next defines a function to skip this middleware when returned true. + // + // Optional. Default: nil + Next func(c *fiber.Ctx) bool + + // Prefix defines a URL prefix added before "/debug/pprof". + // Note that it should start with (but not end with) a slash. + // Example: "/federated-fiber" + // + // Optional. Default: "" + Prefix string +} + +var ConfigDefault = Config{ + Next: nil, +} + +func configDefault(config ...Config) Config { + // Return default config if nothing provided + if len(config) < 1 { + return ConfigDefault + } + + // Override default config + cfg := config[0] + + // Set default values + if cfg.Next == nil { + cfg.Next = ConfigDefault.Next + } + + return cfg +} diff --git a/middleware/pprof/pprof.go b/middleware/pprof/pprof.go new file mode 100644 index 0000000..7e0c8cc --- /dev/null +++ b/middleware/pprof/pprof.go @@ -0,0 +1,95 @@ +package pprof + +import ( + "net/http/pprof" + "strings" + + "github.com/gofiber/fiber/v2" + + "github.com/valyala/fasthttp/fasthttpadaptor" +) + +// New creates a new middleware handler +func New(config ...Config) fiber.Handler { + // Set default config + cfg := configDefault(config...) + + // Set pprof adaptors + var ( + pprofIndex = fasthttpadaptor.NewFastHTTPHandlerFunc(pprof.Index) + pprofCmdline = fasthttpadaptor.NewFastHTTPHandlerFunc(pprof.Cmdline) + pprofProfile = fasthttpadaptor.NewFastHTTPHandlerFunc(pprof.Profile) + pprofSymbol = fasthttpadaptor.NewFastHTTPHandlerFunc(pprof.Symbol) + pprofTrace = fasthttpadaptor.NewFastHTTPHandlerFunc(pprof.Trace) + pprofAllocs = fasthttpadaptor.NewFastHTTPHandlerFunc(pprof.Handler("allocs").ServeHTTP) + pprofBlock = fasthttpadaptor.NewFastHTTPHandlerFunc(pprof.Handler("block").ServeHTTP) + pprofGoroutine = fasthttpadaptor.NewFastHTTPHandlerFunc(pprof.Handler("goroutine").ServeHTTP) + pprofHeap = fasthttpadaptor.NewFastHTTPHandlerFunc(pprof.Handler("heap").ServeHTTP) + pprofMutex = fasthttpadaptor.NewFastHTTPHandlerFunc(pprof.Handler("mutex").ServeHTTP) + pprofThreadcreate = fasthttpadaptor.NewFastHTTPHandlerFunc(pprof.Handler("threadcreate").ServeHTTP) + ) + + // Construct actual prefix + prefix := cfg.Prefix + "/debug/pprof" + + // Return new handler + return func(c *fiber.Ctx) error { + // Don't execute middleware if Next returns true + if cfg.Next != nil && cfg.Next(c) { + return c.Next() + } + + path := c.Path() + // We are only interested in /debug/pprof routes + path, found := cutPrefix(path, prefix) + if !found { + return c.Next() + } + // Switch on trimmed path against constant strings + switch path { + case "/": + pprofIndex(c.Context()) + case "/cmdline": + pprofCmdline(c.Context()) + case "/profile": + pprofProfile(c.Context()) + case "/symbol": + pprofSymbol(c.Context()) + case "/trace": + pprofTrace(c.Context()) + case "/allocs": + pprofAllocs(c.Context()) + case "/block": + pprofBlock(c.Context()) + case "/goroutine": + pprofGoroutine(c.Context()) + case "/heap": + pprofHeap(c.Context()) + case "/mutex": + pprofMutex(c.Context()) + case "/threadcreate": + pprofThreadcreate(c.Context()) + default: + // pprof index only works with trailing slash + if strings.HasSuffix(path, "/") { + path = strings.TrimRight(path, "/") + } else { + path = prefix + "/" + } + + return c.Redirect(path, fiber.StatusFound) + } + return nil + } +} + +// cutPrefix is a copy of [strings.CutPrefix] added in Go 1.20. +// Remove this function when we drop support for Go 1.19. +// +//nolint:nonamedreturns // Align with its original form in std. +func cutPrefix(s, prefix string) (after string, found bool) { + if !strings.HasPrefix(s, prefix) { + return s, false + } + return s[len(prefix):], true +} diff --git a/middleware/pprof/pprof_test.go b/middleware/pprof/pprof_test.go new file mode 100644 index 0000000..c9c8e07 --- /dev/null +++ b/middleware/pprof/pprof_test.go @@ -0,0 +1,200 @@ +package pprof + +import ( + "bytes" + "io" + "net/http/httptest" + "testing" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/utils" +) + +func Test_Non_Pprof_Path(t *testing.T) { + app := fiber.New(fiber.Config{DisableStartupMessage: true}) + + app.Use(New()) + + app.Get("/", func(c *fiber.Ctx) error { + return c.SendString("escaped") + }) + + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, 200, resp.StatusCode) + + b, err := io.ReadAll(resp.Body) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, "escaped", string(b)) +} + +func Test_Non_Pprof_Path_WithPrefix(t *testing.T) { + app := fiber.New(fiber.Config{DisableStartupMessage: true}) + + app.Use(New(Config{Prefix: "/federated-fiber"})) + + app.Get("/", func(c *fiber.Ctx) error { + return c.SendString("escaped") + }) + + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, 200, resp.StatusCode) + + b, err := io.ReadAll(resp.Body) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, "escaped", string(b)) +} + +func Test_Pprof_Index(t *testing.T) { + app := fiber.New(fiber.Config{DisableStartupMessage: true}) + + app.Use(New()) + + app.Get("/", func(c *fiber.Ctx) error { + return c.SendString("escaped") + }) + + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/debug/pprof/", nil)) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, 200, resp.StatusCode) + utils.AssertEqual(t, fiber.MIMETextHTMLCharsetUTF8, resp.Header.Get(fiber.HeaderContentType)) + + b, err := io.ReadAll(resp.Body) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, true, bytes.Contains(b, []byte("/debug/pprof/"))) +} + +func Test_Pprof_Index_WithPrefix(t *testing.T) { + app := fiber.New(fiber.Config{DisableStartupMessage: true}) + + app.Use(New(Config{Prefix: "/federated-fiber"})) + + app.Get("/", func(c *fiber.Ctx) error { + return c.SendString("escaped") + }) + + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/federated-fiber/debug/pprof/", nil)) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, 200, resp.StatusCode) + utils.AssertEqual(t, fiber.MIMETextHTMLCharsetUTF8, resp.Header.Get(fiber.HeaderContentType)) + + b, err := io.ReadAll(resp.Body) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, true, bytes.Contains(b, []byte("/debug/pprof/"))) +} + +func Test_Pprof_Subs(t *testing.T) { + app := fiber.New(fiber.Config{DisableStartupMessage: true}) + + app.Use(New()) + + app.Get("/", func(c *fiber.Ctx) error { + return c.SendString("escaped") + }) + + subs := []string{ + "cmdline", "profile", "symbol", "trace", "allocs", "block", + "goroutine", "heap", "mutex", "threadcreate", + } + + for _, sub := range subs { + sub := sub + t.Run(sub, func(t *testing.T) { + target := "/debug/pprof/" + sub + if sub == "profile" { + target += "?seconds=1" + } + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, target, nil), 5000) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, 200, resp.StatusCode) + }) + } +} + +func Test_Pprof_Subs_WithPrefix(t *testing.T) { + app := fiber.New(fiber.Config{DisableStartupMessage: true}) + + app.Use(New(Config{Prefix: "/federated-fiber"})) + + app.Get("/", func(c *fiber.Ctx) error { + return c.SendString("escaped") + }) + + subs := []string{ + "cmdline", "profile", "symbol", "trace", "allocs", "block", + "goroutine", "heap", "mutex", "threadcreate", + } + + for _, sub := range subs { + sub := sub + t.Run(sub, func(t *testing.T) { + target := "/federated-fiber/debug/pprof/" + sub + if sub == "profile" { + target += "?seconds=1" + } + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, target, nil), 5000) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, 200, resp.StatusCode) + }) + } +} + +func Test_Pprof_Other(t *testing.T) { + app := fiber.New(fiber.Config{DisableStartupMessage: true}) + + app.Use(New()) + + app.Get("/", func(c *fiber.Ctx) error { + return c.SendString("escaped") + }) + + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/debug/pprof/302", nil)) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, 302, resp.StatusCode) +} + +func Test_Pprof_Other_WithPrefix(t *testing.T) { + app := fiber.New(fiber.Config{DisableStartupMessage: true}) + + app.Use(New(Config{Prefix: "/federated-fiber"})) + + app.Get("/", func(c *fiber.Ctx) error { + return c.SendString("escaped") + }) + + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/federated-fiber/debug/pprof/302", nil)) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, 302, resp.StatusCode) +} + +// go test -run Test_Pprof_Next +func Test_Pprof_Next(t *testing.T) { + app := fiber.New() + + app.Use(New(Config{ + Next: func(_ *fiber.Ctx) bool { + return true + }, + })) + + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/debug/pprof/", nil)) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, 404, resp.StatusCode) +} + +// go test -run Test_Pprof_Next_WithPrefix +func Test_Pprof_Next_WithPrefix(t *testing.T) { + app := fiber.New() + + app.Use(New(Config{ + Next: func(_ *fiber.Ctx) bool { + return true + }, + Prefix: "/federated-fiber", + })) + + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/federated-fiber/debug/pprof/", nil)) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, 404, resp.StatusCode) +} diff --git a/middleware/proxy/config.go b/middleware/proxy/config.go new file mode 100644 index 0000000..ca0f894 --- /dev/null +++ b/middleware/proxy/config.go @@ -0,0 +1,88 @@ +package proxy + +import ( + "crypto/tls" + "time" + + "github.com/gofiber/fiber/v2" + + "github.com/valyala/fasthttp" +) + +// Config defines the config for middleware. +type Config struct { + // Next defines a function to skip this middleware when returned true. + // + // Optional. Default: nil + Next func(c *fiber.Ctx) bool + + // Servers defines a list of :// HTTP servers, + // + // which are used in a round-robin manner. + // i.e.: "https://foobar.com, http://www.foobar.com" + // + // Required + Servers []string + + // ModifyRequest allows you to alter the request + // + // Optional. Default: nil + ModifyRequest fiber.Handler + + // ModifyResponse allows you to alter the response + // + // Optional. Default: nil + ModifyResponse fiber.Handler + + // Timeout is the request timeout used when calling the proxy client + // + // Optional. Default: 1 second + Timeout time.Duration + + // Per-connection buffer size for requests' reading. + // This also limits the maximum header size. + // Increase this buffer if your clients send multi-KB RequestURIs + // and/or multi-KB headers (for example, BIG cookies). + ReadBufferSize int + + // Per-connection buffer size for responses' writing. + WriteBufferSize int + + // tls config for the http client. + TlsConfig *tls.Config //nolint:stylecheck,revive // TODO: Rename to "TLSConfig" in v3 + + // Client is custom client when client config is complex. + // Note that Servers, Timeout, WriteBufferSize, ReadBufferSize and TlsConfig + // will not be used if the client are set. + Client *fasthttp.LBClient +} + +// ConfigDefault is the default config +var ConfigDefault = Config{ + Next: nil, + ModifyRequest: nil, + ModifyResponse: nil, + Timeout: fasthttp.DefaultLBClientTimeout, +} + +// configDefault function to set default values +func configDefault(config ...Config) Config { + // Return default config if nothing provided + if len(config) < 1 { + return ConfigDefault + } + + // Override default config + cfg := config[0] + + // Set default values + if cfg.Timeout <= 0 { + cfg.Timeout = ConfigDefault.Timeout + } + + // Set default values + if len(cfg.Servers) == 0 && cfg.Client == nil { + panic("Servers cannot be empty") + } + return cfg +} diff --git a/middleware/proxy/proxy.go b/middleware/proxy/proxy.go new file mode 100644 index 0000000..eb23c56 --- /dev/null +++ b/middleware/proxy/proxy.go @@ -0,0 +1,267 @@ +package proxy + +import ( + "bytes" + "crypto/tls" + "net/url" + "strings" + "sync" + "time" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/log" + "github.com/gofiber/fiber/v2/utils" + + "github.com/valyala/fasthttp" +) + +// New is deprecated +func New(config Config) fiber.Handler { + log.Warn("[PROXY] proxy.New is deprecated, please use proxy.Balancer instead") + return Balancer(config) +} + +// Balancer creates a load balancer among multiple upstream servers +func Balancer(config Config) fiber.Handler { + // Set default config + cfg := configDefault(config) + + // Load balanced client + lbc := &fasthttp.LBClient{} + // Note that Servers, Timeout, WriteBufferSize, ReadBufferSize and TlsConfig + // will not be used if the client are set. + if config.Client == nil { + // Set timeout + lbc.Timeout = cfg.Timeout + // Scheme must be provided, falls back to http + for _, server := range cfg.Servers { + if !strings.HasPrefix(server, "http") { + server = "http://" + server + } + + u, err := url.Parse(server) + if err != nil { + panic(err) + } + + client := &fasthttp.HostClient{ + NoDefaultUserAgentHeader: true, + DisablePathNormalizing: true, + Addr: u.Host, + + ReadBufferSize: config.ReadBufferSize, + WriteBufferSize: config.WriteBufferSize, + + TLSConfig: config.TlsConfig, + } + + lbc.Clients = append(lbc.Clients, client) + } + } else { + // Set custom client + lbc = config.Client + } + + // Return new handler + return func(c *fiber.Ctx) error { + // Don't execute middleware if Next returns true + if cfg.Next != nil && cfg.Next(c) { + return c.Next() + } + + // Set request and response + req := c.Request() + res := c.Response() + + // Don't proxy "Connection" header + req.Header.Del(fiber.HeaderConnection) + + // Modify request + if cfg.ModifyRequest != nil { + if err := cfg.ModifyRequest(c); err != nil { + return err + } + } + + req.SetRequestURI(utils.UnsafeString(req.RequestURI())) + + // Forward request + if err := lbc.Do(req, res); err != nil { + return err + } + + // Don't proxy "Connection" header + res.Header.Del(fiber.HeaderConnection) + + // Modify response + if cfg.ModifyResponse != nil { + if err := cfg.ModifyResponse(c); err != nil { + return err + } + } + + // Return nil to end proxying if no error + return nil + } +} + +var client = &fasthttp.Client{ + NoDefaultUserAgentHeader: true, + DisablePathNormalizing: true, +} + +var lock sync.RWMutex + +// WithTlsConfig update http client with a user specified tls.config +// This function should be called before Do and Forward. +// Deprecated: use WithClient instead. +// +//nolint:stylecheck,revive // TODO: Rename to "WithTLSConfig" in v3 +func WithTlsConfig(tlsConfig *tls.Config) { + client.TLSConfig = tlsConfig +} + +// WithClient sets the global proxy client. +// This function should be called before Do and Forward. +func WithClient(cli *fasthttp.Client) { + lock.Lock() + defer lock.Unlock() + client = cli +} + +// Forward performs the given http request and fills the given http response. +// This method will return an fiber.Handler +func Forward(addr string, clients ...*fasthttp.Client) fiber.Handler { + return func(c *fiber.Ctx) error { + return Do(c, addr, clients...) + } +} + +// Do performs the given http request and fills the given http response. +// This method can be used within a fiber.Handler +func Do(c *fiber.Ctx, addr string, clients ...*fasthttp.Client) error { + return doAction(c, addr, func(cli *fasthttp.Client, req *fasthttp.Request, resp *fasthttp.Response) error { + return cli.Do(req, resp) + }, clients...) +} + +// DoRedirects performs the given http request and fills the given http response, following up to maxRedirectsCount redirects. +// When the redirect count exceeds maxRedirectsCount, ErrTooManyRedirects is returned. +// This method can be used within a fiber.Handler +func DoRedirects(c *fiber.Ctx, addr string, maxRedirectsCount int, clients ...*fasthttp.Client) error { + return doAction(c, addr, func(cli *fasthttp.Client, req *fasthttp.Request, resp *fasthttp.Response) error { + return cli.DoRedirects(req, resp, maxRedirectsCount) + }, clients...) +} + +// DoDeadline performs the given request and waits for response until the given deadline. +// This method can be used within a fiber.Handler +func DoDeadline(c *fiber.Ctx, addr string, deadline time.Time, clients ...*fasthttp.Client) error { + return doAction(c, addr, func(cli *fasthttp.Client, req *fasthttp.Request, resp *fasthttp.Response) error { + return cli.DoDeadline(req, resp, deadline) + }, clients...) +} + +// DoTimeout performs the given request and waits for response during the given timeout duration. +// This method can be used within a fiber.Handler +func DoTimeout(c *fiber.Ctx, addr string, timeout time.Duration, clients ...*fasthttp.Client) error { + return doAction(c, addr, func(cli *fasthttp.Client, req *fasthttp.Request, resp *fasthttp.Response) error { + return cli.DoTimeout(req, resp, timeout) + }, clients...) +} + +func doAction( + c *fiber.Ctx, + addr string, + action func(cli *fasthttp.Client, req *fasthttp.Request, resp *fasthttp.Response) error, + clients ...*fasthttp.Client, +) error { + var cli *fasthttp.Client + + // set local or global client + if len(clients) != 0 { + cli = clients[0] + } else { + lock.RLock() + cli = client + lock.RUnlock() + } + + req := c.Request() + res := c.Response() + originalURL := utils.CopyString(c.OriginalURL()) + defer req.SetRequestURI(originalURL) + + copiedURL := utils.CopyString(addr) + req.SetRequestURI(copiedURL) + // NOTE: if req.isTLS is true, SetRequestURI keeps the scheme as https. + // Reference: https://github.com/gofiber/fiber/issues/1762 + if scheme := getScheme(utils.UnsafeBytes(copiedURL)); len(scheme) > 0 { + req.URI().SetSchemeBytes(scheme) + } + + req.Header.Del(fiber.HeaderConnection) + if err := action(cli, req, res); err != nil { + return err + } + res.Header.Del(fiber.HeaderConnection) + return nil +} + +func getScheme(uri []byte) []byte { + i := bytes.IndexByte(uri, '/') + if i < 1 || uri[i-1] != ':' || i == len(uri)-1 || uri[i+1] != '/' { + return nil + } + return uri[:i-1] +} + +// DomainForward performs an http request based on the given domain and populates the given http response. +// This method will return an fiber.Handler +func DomainForward(hostname, addr string, clients ...*fasthttp.Client) fiber.Handler { + return func(c *fiber.Ctx) error { + host := string(c.Request().Host()) + if host == hostname { + return Do(c, addr+c.OriginalURL(), clients...) + } + return nil + } +} + +type roundrobin struct { + sync.Mutex + + current int + pool []string +} + +// this method will return a string of addr server from list server. +func (r *roundrobin) get() string { + r.Lock() + defer r.Unlock() + + if r.current >= len(r.pool) { + r.current %= len(r.pool) + } + + result := r.pool[r.current] + r.current++ + return result +} + +// BalancerForward Forward performs the given http request with round robin algorithm to server and fills the given http response. +// This method will return an fiber.Handler +func BalancerForward(servers []string, clients ...*fasthttp.Client) fiber.Handler { + r := &roundrobin{ + current: 0, + pool: servers, + } + return func(c *fiber.Ctx) error { + server := r.get() + if !strings.HasPrefix(server, "http") { + server = "http://" + server + } + c.Request().Header.Add("X-Real-IP", c.IP()) + return Do(c, server+c.OriginalURL(), clients...) + } +} diff --git a/middleware/proxy/proxy_test.go b/middleware/proxy/proxy_test.go new file mode 100644 index 0000000..49be2a2 --- /dev/null +++ b/middleware/proxy/proxy_test.go @@ -0,0 +1,689 @@ +package proxy + +import ( + "crypto/tls" + "errors" + "io" + "net" + "net/http/httptest" + "strings" + "testing" + "time" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/internal/tlstest" + "github.com/gofiber/fiber/v2/utils" + + "github.com/valyala/fasthttp" +) + +func createProxyTestServer(t *testing.T, handler fiber.Handler) (*fiber.App, string) { + t.Helper() + + target := fiber.New(fiber.Config{DisableStartupMessage: true}) + target.Get("/", handler) + + ln, err := net.Listen(fiber.NetworkTCP4, "127.0.0.1:0") + utils.AssertEqual(t, nil, err) + + go func() { + utils.AssertEqual(t, nil, target.Listener(ln)) + }() + + time.Sleep(2 * time.Second) + addr := ln.Addr().String() + + return target, addr +} + +// go test -run Test_Proxy_Empty_Host +func Test_Proxy_Empty_Upstream_Servers(t *testing.T) { + t.Parallel() + + defer func() { + if r := recover(); r != nil { + utils.AssertEqual(t, "Servers cannot be empty", r) + } + }() + app := fiber.New() + app.Use(Balancer(Config{Servers: []string{}})) +} + +// go test -run Test_Proxy_Empty_Config +func Test_Proxy_Empty_Config(t *testing.T) { + t.Parallel() + + defer func() { + if r := recover(); r != nil { + utils.AssertEqual(t, "Servers cannot be empty", r) + } + }() + app := fiber.New() + app.Use(New(Config{})) +} + +// go test -run Test_Proxy_Next +func Test_Proxy_Next(t *testing.T) { + t.Parallel() + + app := fiber.New() + app.Use(Balancer(Config{ + Servers: []string{"127.0.0.1"}, + Next: func(_ *fiber.Ctx) bool { + return true + }, + })) + + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, fiber.StatusNotFound, resp.StatusCode) +} + +// go test -run Test_Proxy +func Test_Proxy(t *testing.T) { + t.Parallel() + + target, addr := createProxyTestServer(t, func(c *fiber.Ctx) error { + return c.SendStatus(fiber.StatusTeapot) + }) + + resp, err := target.Test(httptest.NewRequest(fiber.MethodGet, "/", nil), 2000) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, fiber.StatusTeapot, resp.StatusCode) + + app := fiber.New(fiber.Config{DisableStartupMessage: true}) + + app.Use(Balancer(Config{Servers: []string{addr}})) + + req := httptest.NewRequest(fiber.MethodGet, "/", nil) + req.Host = addr + resp, err = app.Test(req) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, fiber.StatusTeapot, resp.StatusCode) +} + +// go test -run Test_Proxy_Balancer_WithTlsConfig +func Test_Proxy_Balancer_WithTlsConfig(t *testing.T) { + t.Parallel() + + serverTLSConf, _, err := tlstest.GetTLSConfigs() + utils.AssertEqual(t, nil, err) + + ln, err := net.Listen(fiber.NetworkTCP4, "127.0.0.1:0") + utils.AssertEqual(t, nil, err) + + ln = tls.NewListener(ln, serverTLSConf) + + app := fiber.New(fiber.Config{DisableStartupMessage: true}) + + app.Get("/tlsbalaner", func(c *fiber.Ctx) error { + return c.SendString("tls balancer") + }) + + addr := ln.Addr().String() + clientTLSConf := &tls.Config{InsecureSkipVerify: true} //nolint:gosec // We're in a test func, so this is fine + + // disable certificate verification in Balancer + app.Use(Balancer(Config{ + Servers: []string{addr}, + TlsConfig: clientTLSConf, + })) + + go func() { utils.AssertEqual(t, nil, app.Listener(ln)) }() + + code, body, errs := fiber.Get("https://" + addr + "/tlsbalaner").TLSConfig(clientTLSConf).String() + + utils.AssertEqual(t, 0, len(errs)) + utils.AssertEqual(t, fiber.StatusOK, code) + utils.AssertEqual(t, "tls balancer", body) +} + +// go test -run Test_Proxy_Forward_WithTlsConfig_To_Http +func Test_Proxy_Forward_WithTlsConfig_To_Http(t *testing.T) { + t.Parallel() + + _, targetAddr := createProxyTestServer(t, func(c *fiber.Ctx) error { + return c.SendString("hello from target") + }) + + proxyServerTLSConf, _, err := tlstest.GetTLSConfigs() + utils.AssertEqual(t, nil, err) + + proxyServerLn, err := net.Listen(fiber.NetworkTCP4, "127.0.0.1:0") + utils.AssertEqual(t, nil, err) + + proxyServerLn = tls.NewListener(proxyServerLn, proxyServerTLSConf) + + app := fiber.New(fiber.Config{DisableStartupMessage: true}) + + proxyAddr := proxyServerLn.Addr().String() + + app.Use(Forward("http://" + targetAddr)) + + go func() { utils.AssertEqual(t, nil, app.Listener(proxyServerLn)) }() + + code, body, errs := fiber.Get("https://" + proxyAddr). + InsecureSkipVerify(). + Timeout(5 * time.Second). + String() + + utils.AssertEqual(t, 0, len(errs)) + utils.AssertEqual(t, fiber.StatusOK, code) + utils.AssertEqual(t, "hello from target", body) +} + +// go test -run Test_Proxy_Forward +func Test_Proxy_Forward(t *testing.T) { + t.Parallel() + + app := fiber.New() + + _, addr := createProxyTestServer(t, func(c *fiber.Ctx) error { + return c.SendString("forwarded") + }) + + app.Use(Forward("http://" + addr)) + + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, fiber.StatusOK, resp.StatusCode) + + b, err := io.ReadAll(resp.Body) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, "forwarded", string(b)) +} + +// go test -run Test_Proxy_Forward_WithTlsConfig +func Test_Proxy_Forward_WithTlsConfig(t *testing.T) { + t.Parallel() + + serverTLSConf, _, err := tlstest.GetTLSConfigs() + utils.AssertEqual(t, nil, err) + + ln, err := net.Listen(fiber.NetworkTCP4, "127.0.0.1:0") + utils.AssertEqual(t, nil, err) + + ln = tls.NewListener(ln, serverTLSConf) + + app := fiber.New(fiber.Config{DisableStartupMessage: true}) + + app.Get("/tlsfwd", func(c *fiber.Ctx) error { + return c.SendString("tls forward") + }) + + addr := ln.Addr().String() + clientTLSConf := &tls.Config{InsecureSkipVerify: true} //nolint:gosec // We're in a test func, so this is fine + + // disable certificate verification + WithTlsConfig(clientTLSConf) + app.Use(Forward("https://" + addr + "/tlsfwd")) + + go func() { utils.AssertEqual(t, nil, app.Listener(ln)) }() + + code, body, errs := fiber.Get("https://" + addr).TLSConfig(clientTLSConf).String() + + utils.AssertEqual(t, 0, len(errs)) + utils.AssertEqual(t, fiber.StatusOK, code) + utils.AssertEqual(t, "tls forward", body) +} + +// go test -run Test_Proxy_Modify_Response +func Test_Proxy_Modify_Response(t *testing.T) { + t.Parallel() + + _, addr := createProxyTestServer(t, func(c *fiber.Ctx) error { + return c.Status(500).SendString("not modified") + }) + + app := fiber.New() + app.Use(Balancer(Config{ + Servers: []string{addr}, + ModifyResponse: func(c *fiber.Ctx) error { + c.Response().SetStatusCode(fiber.StatusOK) + return c.SendString("modified response") + }, + })) + + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, fiber.StatusOK, resp.StatusCode) + + b, err := io.ReadAll(resp.Body) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, "modified response", string(b)) +} + +// go test -run Test_Proxy_Modify_Request +func Test_Proxy_Modify_Request(t *testing.T) { + t.Parallel() + + _, addr := createProxyTestServer(t, func(c *fiber.Ctx) error { + b := c.Request().Body() + return c.SendString(string(b)) + }) + + app := fiber.New() + app.Use(Balancer(Config{ + Servers: []string{addr}, + ModifyRequest: func(c *fiber.Ctx) error { + c.Request().SetBody([]byte("modified request")) + return nil + }, + })) + + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, fiber.StatusOK, resp.StatusCode) + + b, err := io.ReadAll(resp.Body) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, "modified request", string(b)) +} + +// go test -run Test_Proxy_Timeout_Slow_Server +func Test_Proxy_Timeout_Slow_Server(t *testing.T) { + t.Parallel() + + _, addr := createProxyTestServer(t, func(c *fiber.Ctx) error { + time.Sleep(2 * time.Second) + return c.SendString("fiber is awesome") + }) + + app := fiber.New() + app.Use(Balancer(Config{ + Servers: []string{addr}, + Timeout: 3 * time.Second, + })) + + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil), 5000) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, fiber.StatusOK, resp.StatusCode) + + b, err := io.ReadAll(resp.Body) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, "fiber is awesome", string(b)) +} + +// go test -run Test_Proxy_With_Timeout +func Test_Proxy_With_Timeout(t *testing.T) { + t.Parallel() + + _, addr := createProxyTestServer(t, func(c *fiber.Ctx) error { + time.Sleep(1 * time.Second) + return c.SendString("fiber is awesome") + }) + + app := fiber.New() + app.Use(Balancer(Config{ + Servers: []string{addr}, + Timeout: 100 * time.Millisecond, + })) + + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil), 2000) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, fiber.StatusInternalServerError, resp.StatusCode) + + b, err := io.ReadAll(resp.Body) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, "timeout", string(b)) +} + +// go test -run Test_Proxy_Buffer_Size_Response +func Test_Proxy_Buffer_Size_Response(t *testing.T) { + t.Parallel() + + _, addr := createProxyTestServer(t, func(c *fiber.Ctx) error { + long := strings.Join(make([]string, 5000), "-") + c.Set("Very-Long-Header", long) + return c.SendString("ok") + }) + + app := fiber.New() + app.Use(Balancer(Config{Servers: []string{addr}})) + + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, fiber.StatusInternalServerError, resp.StatusCode) + + app = fiber.New() + app.Use(Balancer(Config{ + Servers: []string{addr}, + ReadBufferSize: 1024 * 8, + })) + + resp, err = app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, fiber.StatusOK, resp.StatusCode) +} + +// go test -race -run Test_Proxy_Do_RestoreOriginalURL +func Test_Proxy_Do_RestoreOriginalURL(t *testing.T) { + t.Parallel() + _, addr := createProxyTestServer(t, func(c *fiber.Ctx) error { + return c.SendString("proxied") + }) + + app := fiber.New() + app.Get("/test", func(c *fiber.Ctx) error { + return Do(c, "http://"+addr) + }) + resp, err1 := app.Test(httptest.NewRequest(fiber.MethodGet, "/test", nil)) + utils.AssertEqual(t, nil, err1) + utils.AssertEqual(t, "/test", resp.Request.URL.String()) + utils.AssertEqual(t, fiber.StatusOK, resp.StatusCode) + body, err := io.ReadAll(resp.Body) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, "proxied", string(body)) +} + +// go test -race -run Test_Proxy_Do_WithRealURL +func Test_Proxy_Do_WithRealURL(t *testing.T) { + t.Parallel() + app := fiber.New() + app.Get("/test", func(c *fiber.Ctx) error { + return Do(c, "https://www.google.com") + }) + + resp, err1 := app.Test(httptest.NewRequest(fiber.MethodGet, "/test", nil)) + utils.AssertEqual(t, nil, err1) + utils.AssertEqual(t, fiber.StatusOK, resp.StatusCode) + utils.AssertEqual(t, "/test", resp.Request.URL.String()) + body, err := io.ReadAll(resp.Body) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, true, strings.Contains(string(body), "https://www.google.com/")) +} + +// go test -race -run Test_Proxy_Do_WithRedirect +func Test_Proxy_Do_WithRedirect(t *testing.T) { + t.Parallel() + app := fiber.New() + app.Get("/test", func(c *fiber.Ctx) error { + return Do(c, "https://google.com") + }) + + resp, err1 := app.Test(httptest.NewRequest(fiber.MethodGet, "/test", nil)) + utils.AssertEqual(t, nil, err1) + body, err := io.ReadAll(resp.Body) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, true, strings.Contains(string(body), "https://www.google.com/")) + utils.AssertEqual(t, 301, resp.StatusCode) +} + +// go test -race -run Test_Proxy_DoRedirects_RestoreOriginalURL +func Test_Proxy_DoRedirects_RestoreOriginalURL(t *testing.T) { + t.Parallel() + app := fiber.New() + app.Get("/test", func(c *fiber.Ctx) error { + return DoRedirects(c, "http://google.com", 1) + }) + + resp, err1 := app.Test(httptest.NewRequest(fiber.MethodGet, "/test", nil)) + utils.AssertEqual(t, nil, err1) + _, err := io.ReadAll(resp.Body) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, fiber.StatusOK, resp.StatusCode) + utils.AssertEqual(t, "/test", resp.Request.URL.String()) +} + +// go test -race -run Test_Proxy_DoRedirects_TooManyRedirects +func Test_Proxy_DoRedirects_TooManyRedirects(t *testing.T) { + t.Parallel() + app := fiber.New() + app.Get("/test", func(c *fiber.Ctx) error { + return DoRedirects(c, "http://google.com", 0) + }) + + resp, err1 := app.Test(httptest.NewRequest(fiber.MethodGet, "/test", nil)) + utils.AssertEqual(t, nil, err1) + body, err := io.ReadAll(resp.Body) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, "too many redirects detected when doing the request", string(body)) + utils.AssertEqual(t, fiber.StatusInternalServerError, resp.StatusCode) + utils.AssertEqual(t, "/test", resp.Request.URL.String()) +} + +// go test -race -run Test_Proxy_DoTimeout_RestoreOriginalURL +func Test_Proxy_DoTimeout_RestoreOriginalURL(t *testing.T) { + t.Parallel() + + _, addr := createProxyTestServer(t, func(c *fiber.Ctx) error { + return c.SendString("proxied") + }) + + app := fiber.New() + app.Get("/test", func(c *fiber.Ctx) error { + return DoTimeout(c, "http://"+addr, time.Second) + }) + + resp, err1 := app.Test(httptest.NewRequest(fiber.MethodGet, "/test", nil)) + utils.AssertEqual(t, nil, err1) + body, err := io.ReadAll(resp.Body) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, "proxied", string(body)) + utils.AssertEqual(t, fiber.StatusOK, resp.StatusCode) + utils.AssertEqual(t, "/test", resp.Request.URL.String()) +} + +// go test -race -run Test_Proxy_DoTimeout_Timeout +func Test_Proxy_DoTimeout_Timeout(t *testing.T) { + t.Parallel() + + _, addr := createProxyTestServer(t, func(c *fiber.Ctx) error { + time.Sleep(time.Second * 5) + return c.SendString("proxied") + }) + + app := fiber.New() + app.Get("/test", func(c *fiber.Ctx) error { + return DoTimeout(c, "http://"+addr, time.Second) + }) + + _, err1 := app.Test(httptest.NewRequest(fiber.MethodGet, "/test", nil)) + utils.AssertEqual(t, errors.New("test: timeout error 1000ms"), err1) +} + +// go test -race -run Test_Proxy_DoDeadline_RestoreOriginalURL +func Test_Proxy_DoDeadline_RestoreOriginalURL(t *testing.T) { + t.Parallel() + + _, addr := createProxyTestServer(t, func(c *fiber.Ctx) error { + return c.SendString("proxied") + }) + + app := fiber.New() + app.Get("/test", func(c *fiber.Ctx) error { + return DoDeadline(c, "http://"+addr, time.Now().Add(time.Second)) + }) + + resp, err1 := app.Test(httptest.NewRequest(fiber.MethodGet, "/test", nil)) + utils.AssertEqual(t, nil, err1) + body, err := io.ReadAll(resp.Body) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, "proxied", string(body)) + utils.AssertEqual(t, fiber.StatusOK, resp.StatusCode) + utils.AssertEqual(t, "/test", resp.Request.URL.String()) +} + +// go test -race -run Test_Proxy_DoDeadline_PastDeadline +func Test_Proxy_DoDeadline_PastDeadline(t *testing.T) { + t.Parallel() + + _, addr := createProxyTestServer(t, func(c *fiber.Ctx) error { + time.Sleep(time.Second * 5) + return c.SendString("proxied") + }) + + app := fiber.New() + app.Get("/test", func(c *fiber.Ctx) error { + return DoDeadline(c, "http://"+addr, time.Now().Add(time.Second)) + }) + + _, err1 := app.Test(httptest.NewRequest(fiber.MethodGet, "/test", nil)) + utils.AssertEqual(t, errors.New("test: timeout error 1000ms"), err1) +} + +// go test -race -run Test_Proxy_Do_HTTP_Prefix_URL +func Test_Proxy_Do_HTTP_Prefix_URL(t *testing.T) { + t.Parallel() + + _, addr := createProxyTestServer(t, func(c *fiber.Ctx) error { + return c.SendString("hello world") + }) + + app := fiber.New(fiber.Config{DisableStartupMessage: true}) + app.Get("/*", func(c *fiber.Ctx) error { + path := c.OriginalURL() + url := strings.TrimPrefix(path, "/") + + utils.AssertEqual(t, "http://"+addr, url) + if err := Do(c, url); err != nil { + return err + } + c.Response().Header.Del(fiber.HeaderServer) + return nil + }) + + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/http://"+addr, nil)) + utils.AssertEqual(t, nil, err) + s, err := io.ReadAll(resp.Body) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, "hello world", string(s)) +} + +// go test -race -run Test_Proxy_Forward_Global_Client +func Test_Proxy_Forward_Global_Client(t *testing.T) { + t.Parallel() + ln, err := net.Listen(fiber.NetworkTCP4, "127.0.0.1:0") + utils.AssertEqual(t, nil, err) + WithClient(&fasthttp.Client{ + NoDefaultUserAgentHeader: true, + DisablePathNormalizing: true, + }) + app := fiber.New(fiber.Config{DisableStartupMessage: true}) + app.Get("/test_global_client", func(c *fiber.Ctx) error { + return c.SendString("test_global_client") + }) + + addr := ln.Addr().String() + app.Use(Forward("http://" + addr + "/test_global_client")) + go func() { utils.AssertEqual(t, nil, app.Listener(ln)) }() + + code, body, errs := fiber.Get("http://" + addr).String() + utils.AssertEqual(t, 0, len(errs)) + utils.AssertEqual(t, fiber.StatusOK, code) + utils.AssertEqual(t, "test_global_client", body) +} + +// go test -race -run Test_Proxy_Forward_Local_Client +func Test_Proxy_Forward_Local_Client(t *testing.T) { + t.Parallel() + ln, err := net.Listen(fiber.NetworkTCP4, "127.0.0.1:0") + utils.AssertEqual(t, nil, err) + app := fiber.New(fiber.Config{DisableStartupMessage: true}) + app.Get("/test_local_client", func(c *fiber.Ctx) error { + return c.SendString("test_local_client") + }) + + addr := ln.Addr().String() + app.Use(Forward("http://"+addr+"/test_local_client", &fasthttp.Client{ + NoDefaultUserAgentHeader: true, + DisablePathNormalizing: true, + + Dial: fasthttp.Dial, + })) + go func() { utils.AssertEqual(t, nil, app.Listener(ln)) }() + + code, body, errs := fiber.Get("http://" + addr).String() + utils.AssertEqual(t, 0, len(errs)) + utils.AssertEqual(t, fiber.StatusOK, code) + utils.AssertEqual(t, "test_local_client", body) +} + +// go test -run Test_ProxyBalancer_Custom_Client +func Test_ProxyBalancer_Custom_Client(t *testing.T) { + t.Parallel() + + target, addr := createProxyTestServer(t, func(c *fiber.Ctx) error { + return c.SendStatus(fiber.StatusTeapot) + }) + + resp, err := target.Test(httptest.NewRequest(fiber.MethodGet, "/", nil), 2000) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, fiber.StatusTeapot, resp.StatusCode) + + app := fiber.New(fiber.Config{DisableStartupMessage: true}) + + app.Use(Balancer(Config{Client: &fasthttp.LBClient{ + Clients: []fasthttp.BalancingClient{ + &fasthttp.HostClient{ + NoDefaultUserAgentHeader: true, + DisablePathNormalizing: true, + Addr: addr, + }, + }, + Timeout: time.Second, + }})) + + req := httptest.NewRequest(fiber.MethodGet, "/", nil) + req.Host = addr + resp, err = app.Test(req) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, fiber.StatusTeapot, resp.StatusCode) +} + +// go test -run Test_Proxy_Domain_Forward_Local +func Test_Proxy_Domain_Forward_Local(t *testing.T) { + t.Parallel() + ln, err := net.Listen(fiber.NetworkTCP4, "127.0.0.1:0") + utils.AssertEqual(t, nil, err) + app := fiber.New(fiber.Config{DisableStartupMessage: true}) + + // target server + ln1, err := net.Listen(fiber.NetworkTCP4, "127.0.0.1:0") + utils.AssertEqual(t, nil, err) + app1 := fiber.New(fiber.Config{DisableStartupMessage: true}) + + app1.Get("/test", func(c *fiber.Ctx) error { + return c.SendString("test_local_client:" + c.Query("query_test")) + }) + + proxyAddr := ln.Addr().String() + targetAddr := ln1.Addr().String() + localDomain := strings.Replace(proxyAddr, "127.0.0.1", "localhost", 1) + app.Use(DomainForward(localDomain, "http://"+targetAddr, &fasthttp.Client{ + NoDefaultUserAgentHeader: true, + DisablePathNormalizing: true, + + Dial: fasthttp.Dial, + })) + + go func() { utils.AssertEqual(t, nil, app.Listener(ln)) }() + go func() { utils.AssertEqual(t, nil, app1.Listener(ln1)) }() + + code, body, errs := fiber.Get("http://" + localDomain + "/test?query_test=true").String() + utils.AssertEqual(t, 0, len(errs)) + utils.AssertEqual(t, fiber.StatusOK, code) + utils.AssertEqual(t, "test_local_client:true", body) +} + +// go test -run Test_Proxy_Balancer_Forward_Local +func Test_Proxy_Balancer_Forward_Local(t *testing.T) { + t.Parallel() + + app := fiber.New() + + _, addr := createProxyTestServer(t, func(c *fiber.Ctx) error { + return c.SendString("forwarded") + }) + + app.Use(BalancerForward([]string{addr})) + + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, fiber.StatusOK, resp.StatusCode) + + b, err := io.ReadAll(resp.Body) + utils.AssertEqual(t, nil, err) + + utils.AssertEqual(t, string(b), "forwarded") +} diff --git a/middleware/recover/config.go b/middleware/recover/config.go new file mode 100644 index 0000000..b29e750 --- /dev/null +++ b/middleware/recover/config.go @@ -0,0 +1,47 @@ +package recover //nolint:predeclared // TODO: Rename to some non-builtin + +import ( + "github.com/gofiber/fiber/v2" +) + +// Config defines the config for middleware. +type Config struct { + // Next defines a function to skip this middleware when returned true. + // + // Optional. Default: nil + Next func(c *fiber.Ctx) bool + + // EnableStackTrace enables handling stack trace + // + // Optional. Default: false + EnableStackTrace bool + + // StackTraceHandler defines a function to handle stack trace + // + // Optional. Default: defaultStackTraceHandler + StackTraceHandler func(c *fiber.Ctx, e interface{}) +} + +// ConfigDefault is the default config +var ConfigDefault = Config{ + Next: nil, + EnableStackTrace: false, + StackTraceHandler: defaultStackTraceHandler, +} + +// Helper function to set default values +func configDefault(config ...Config) Config { + // Return default config if nothing provided + if len(config) < 1 { + return ConfigDefault + } + + // Override default config + cfg := config[0] + + if cfg.EnableStackTrace && cfg.StackTraceHandler == nil { + cfg.StackTraceHandler = defaultStackTraceHandler + } + + return cfg +} diff --git a/middleware/recover/recover.go b/middleware/recover/recover.go new file mode 100644 index 0000000..252d8be --- /dev/null +++ b/middleware/recover/recover.go @@ -0,0 +1,45 @@ +package recover //nolint:predeclared // TODO: Rename to some non-builtin + +import ( + "fmt" + "os" + "runtime/debug" + + "github.com/gofiber/fiber/v2" +) + +func defaultStackTraceHandler(_ *fiber.Ctx, e interface{}) { + _, _ = os.Stderr.WriteString(fmt.Sprintf("panic: %v\n%s\n", e, debug.Stack())) //nolint:errcheck // This will never fail +} + +// New creates a new middleware handler +func New(config ...Config) fiber.Handler { + // Set default config + cfg := configDefault(config...) + + // Return new handler + return func(c *fiber.Ctx) (err error) { //nolint:nonamedreturns // Uses recover() to overwrite the error + // Don't execute middleware if Next returns true + if cfg.Next != nil && cfg.Next(c) { + return c.Next() + } + + // Catch panics + defer func() { + if r := recover(); r != nil { + if cfg.EnableStackTrace { + cfg.StackTraceHandler(c, r) + } + + var ok bool + if err, ok = r.(error); !ok { + // Set error that will call the global error handler + err = fmt.Errorf("%v", r) + } + } + }() + + // Return err if exist, else move to next handler + return c.Next() + } +} diff --git a/middleware/recover/recover_test.go b/middleware/recover/recover_test.go new file mode 100644 index 0000000..82e34b6 --- /dev/null +++ b/middleware/recover/recover_test.go @@ -0,0 +1,61 @@ +package recover //nolint:predeclared // TODO: Rename to some non-builtin + +import ( + "net/http/httptest" + "testing" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/utils" +) + +// go test -run Test_Recover +func Test_Recover(t *testing.T) { + t.Parallel() + app := fiber.New(fiber.Config{ + ErrorHandler: func(c *fiber.Ctx, err error) error { + utils.AssertEqual(t, "Hi, I'm an error!", err.Error()) + return c.SendStatus(fiber.StatusTeapot) + }, + }) + + app.Use(New()) + + app.Get("/panic", func(c *fiber.Ctx) error { + panic("Hi, I'm an error!") + }) + + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/panic", nil)) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, fiber.StatusTeapot, resp.StatusCode) +} + +// go test -run Test_Recover_Next +func Test_Recover_Next(t *testing.T) { + t.Parallel() + app := fiber.New() + app.Use(New(Config{ + Next: func(_ *fiber.Ctx) bool { + return true + }, + })) + + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, fiber.StatusNotFound, resp.StatusCode) +} + +func Test_Recover_EnableStackTrace(t *testing.T) { + t.Parallel() + app := fiber.New() + app.Use(New(Config{ + EnableStackTrace: true, + })) + + app.Get("/panic", func(c *fiber.Ctx) error { + panic("Hi, I'm an error!") + }) + + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/panic", nil)) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, fiber.StatusInternalServerError, resp.StatusCode) +} diff --git a/middleware/redirect/config.go b/middleware/redirect/config.go new file mode 100644 index 0000000..62b868c --- /dev/null +++ b/middleware/redirect/config.go @@ -0,0 +1,53 @@ +package redirect + +import ( + "regexp" + + "github.com/gofiber/fiber/v2" +) + +// Config defines the config for middleware. +type Config struct { + // Filter defines a function to skip middleware. + // Optional. Default: nil + Next func(*fiber.Ctx) bool + + // Rules defines the URL path rewrite rules. The values captured in asterisk can be + // retrieved by index e.g. $1, $2 and so on. + // Required. Example: + // "/old": "/new", + // "/api/*": "/$1", + // "/js/*": "/public/javascripts/$1", + // "/users/*/orders/*": "/user/$1/order/$2", + Rules map[string]string + + // The status code when redirecting + // This is ignored if Redirect is disabled + // Optional. Default: 302 Temporary Redirect + StatusCode int + + rulesRegex map[*regexp.Regexp]string +} + +// ConfigDefault is the default config +var ConfigDefault = Config{ + StatusCode: fiber.StatusFound, +} + +// Helper function to set default values +func configDefault(config ...Config) Config { + // Return default config if nothing provided + if len(config) < 1 { + return ConfigDefault + } + + // Override default config + cfg := config[0] + + // Set default values + if cfg.StatusCode == 0 { + cfg.StatusCode = ConfigDefault.StatusCode + } + + return cfg +} diff --git a/middleware/redirect/redirect.go b/middleware/redirect/redirect.go new file mode 100644 index 0000000..2818142 --- /dev/null +++ b/middleware/redirect/redirect.go @@ -0,0 +1,61 @@ +package redirect + +import ( + "regexp" + "strconv" + "strings" + + "github.com/gofiber/fiber/v2" +) + +// New creates a new middleware handler +func New(config ...Config) fiber.Handler { + cfg := configDefault(config...) + + // Initialize + cfg.rulesRegex = map[*regexp.Regexp]string{} + for k, v := range cfg.Rules { + k = strings.ReplaceAll(k, "*", "(.*)") + k += "$" + cfg.rulesRegex[regexp.MustCompile(k)] = v + } + + // Middleware function + return func(c *fiber.Ctx) error { + // Next request to skip middleware + if cfg.Next != nil && cfg.Next(c) { + return c.Next() + } + // Rewrite + for k, v := range cfg.rulesRegex { + replacer := captureTokens(k, c.Path()) + if replacer != nil { + queryString := string(c.Context().QueryArgs().QueryString()) + if queryString != "" { + queryString = "?" + queryString + } + return c.Redirect(replacer.Replace(v)+queryString, cfg.StatusCode) + } + } + return c.Next() + } +} + +// https://github.com/labstack/echo/blob/master/middleware/rewrite.go +func captureTokens(pattern *regexp.Regexp, input string) *strings.Replacer { + if len(input) > 1 { + input = strings.TrimSuffix(input, "/") + } + groups := pattern.FindAllStringSubmatch(input, -1) + if groups == nil { + return nil + } + values := groups[0][1:] + replace := make([]string, 2*len(values)) + for i, v := range values { + j := 2 * i + replace[j] = "$" + strconv.Itoa(i+1) + replace[j+1] = v + } + return strings.NewReplacer(replace...) +} diff --git a/middleware/redirect/redirect_test.go b/middleware/redirect/redirect_test.go new file mode 100644 index 0000000..61fc00a --- /dev/null +++ b/middleware/redirect/redirect_test.go @@ -0,0 +1,295 @@ +//nolint:bodyclose // Much easier to just ignore memory leaks in tests +package redirect + +import ( + "context" + "net/http" + "testing" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/utils" +) + +func Test_Redirect(t *testing.T) { + app := *fiber.New() + + app.Use(New(Config{ + Rules: map[string]string{ + "/default": "google.com", + }, + StatusCode: fiber.StatusMovedPermanently, + })) + app.Use(New(Config{ + Rules: map[string]string{ + "/default/*": "fiber.wiki", + }, + StatusCode: fiber.StatusTemporaryRedirect, + })) + app.Use(New(Config{ + Rules: map[string]string{ + "/redirect/*": "$1", + }, + StatusCode: fiber.StatusSeeOther, + })) + app.Use(New(Config{ + Rules: map[string]string{ + "/pattern/*": "golang.org", + }, + StatusCode: fiber.StatusFound, + })) + + app.Use(New(Config{ + Rules: map[string]string{ + "/": "/swagger", + }, + StatusCode: fiber.StatusMovedPermanently, + })) + app.Use(New(Config{ + Rules: map[string]string{ + "/params": "/with_params", + }, + StatusCode: fiber.StatusMovedPermanently, + })) + + app.Get("/api/*", func(c *fiber.Ctx) error { + return c.SendString("API") + }) + + app.Get("/new", func(c *fiber.Ctx) error { + return c.SendString("Hello, World!") + }) + + tests := []struct { + name string + url string + redirectTo string + statusCode int + }{ + { + name: "should be returns status StatusFound without a wildcard", + url: "/default", + redirectTo: "google.com", + statusCode: fiber.StatusMovedPermanently, + }, + { + name: "should be returns status StatusTemporaryRedirect using wildcard", + url: "/default/xyz", + redirectTo: "fiber.wiki", + statusCode: fiber.StatusTemporaryRedirect, + }, + { + name: "should be returns status StatusSeeOther without set redirectTo to use the default", + url: "/redirect/github.com/gofiber/redirect", + redirectTo: "github.com/gofiber/redirect", + statusCode: fiber.StatusSeeOther, + }, + { + name: "should return the status code default", + url: "/pattern/xyz", + redirectTo: "golang.org", + statusCode: fiber.StatusFound, + }, + { + name: "access URL without rule", + url: "/new", + statusCode: fiber.StatusOK, + }, + { + name: "redirect to swagger route", + url: "/", + redirectTo: "/swagger", + statusCode: fiber.StatusMovedPermanently, + }, + { + name: "no redirect to swagger route", + url: "/api/", + statusCode: fiber.StatusOK, + }, + { + name: "no redirect to swagger route #2", + url: "/api/test", + statusCode: fiber.StatusOK, + }, + { + name: "redirect with query params", + url: "/params?query=abc", + redirectTo: "/with_params?query=abc", + statusCode: fiber.StatusMovedPermanently, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + req, err := http.NewRequestWithContext(context.Background(), fiber.MethodGet, tt.url, nil) + utils.AssertEqual(t, err, nil) + req.Header.Set("Location", "github.com/gofiber/redirect") + resp, err := app.Test(req) + + utils.AssertEqual(t, err, nil) + utils.AssertEqual(t, tt.statusCode, resp.StatusCode) + utils.AssertEqual(t, tt.redirectTo, resp.Header.Get("Location")) + }) + } +} + +func Test_Next(t *testing.T) { + // Case 1 : Next function always returns true + app := *fiber.New() + app.Use(New(Config{ + Next: func(*fiber.Ctx) bool { + return true + }, + Rules: map[string]string{ + "/default": "google.com", + }, + StatusCode: fiber.StatusMovedPermanently, + })) + + app.Use(func(c *fiber.Ctx) error { + return c.SendStatus(fiber.StatusOK) + }) + + req, err := http.NewRequestWithContext(context.Background(), fiber.MethodGet, "/default", nil) + utils.AssertEqual(t, err, nil) + resp, err := app.Test(req) + utils.AssertEqual(t, err, nil) + + utils.AssertEqual(t, fiber.StatusOK, resp.StatusCode) + + // Case 2 : Next function always returns false + app = *fiber.New() + app.Use(New(Config{ + Next: func(*fiber.Ctx) bool { + return false + }, + Rules: map[string]string{ + "/default": "google.com", + }, + StatusCode: fiber.StatusMovedPermanently, + })) + + req, err = http.NewRequestWithContext(context.Background(), fiber.MethodGet, "/default", nil) + utils.AssertEqual(t, err, nil) + resp, err = app.Test(req) + utils.AssertEqual(t, err, nil) + + utils.AssertEqual(t, fiber.StatusMovedPermanently, resp.StatusCode) + utils.AssertEqual(t, "google.com", resp.Header.Get("Location")) +} + +func Test_NoRules(t *testing.T) { + // Case 1: No rules with default route defined + app := *fiber.New() + + app.Use(New(Config{ + StatusCode: fiber.StatusMovedPermanently, + })) + + app.Use(func(c *fiber.Ctx) error { + return c.SendStatus(fiber.StatusOK) + }) + + req, err := http.NewRequestWithContext(context.Background(), fiber.MethodGet, "/default", nil) + utils.AssertEqual(t, err, nil) + resp, err := app.Test(req) + utils.AssertEqual(t, err, nil) + utils.AssertEqual(t, fiber.StatusOK, resp.StatusCode) + + // Case 2: No rules and no default route defined + app = *fiber.New() + + app.Use(New(Config{ + StatusCode: fiber.StatusMovedPermanently, + })) + + req, err = http.NewRequestWithContext(context.Background(), fiber.MethodGet, "/default", nil) + utils.AssertEqual(t, err, nil) + resp, err = app.Test(req) + utils.AssertEqual(t, err, nil) + utils.AssertEqual(t, fiber.StatusNotFound, resp.StatusCode) +} + +func Test_DefaultConfig(t *testing.T) { + // Case 1: Default config and no default route + app := *fiber.New() + + app.Use(New()) + + req, err := http.NewRequestWithContext(context.Background(), fiber.MethodGet, "/default", nil) + utils.AssertEqual(t, err, nil) + resp, err := app.Test(req) + + utils.AssertEqual(t, err, nil) + utils.AssertEqual(t, fiber.StatusNotFound, resp.StatusCode) + + // Case 2: Default config and default route + app = *fiber.New() + + app.Use(New()) + app.Use(func(c *fiber.Ctx) error { + return c.SendStatus(fiber.StatusOK) + }) + + req, err = http.NewRequestWithContext(context.Background(), fiber.MethodGet, "/default", nil) + utils.AssertEqual(t, err, nil) + resp, err = app.Test(req) + + utils.AssertEqual(t, err, nil) + utils.AssertEqual(t, fiber.StatusOK, resp.StatusCode) +} + +func Test_RegexRules(t *testing.T) { + // Case 1: Rules regex is empty + app := *fiber.New() + app.Use(New(Config{ + Rules: map[string]string{}, + StatusCode: fiber.StatusMovedPermanently, + })) + + app.Use(func(c *fiber.Ctx) error { + return c.SendStatus(fiber.StatusOK) + }) + + req, err := http.NewRequestWithContext(context.Background(), fiber.MethodGet, "/default", nil) + utils.AssertEqual(t, err, nil) + resp, err := app.Test(req) + + utils.AssertEqual(t, err, nil) + utils.AssertEqual(t, fiber.StatusOK, resp.StatusCode) + + // Case 2: Rules regex map contains valid regex and well-formed replacement URLs + app = *fiber.New() + app.Use(New(Config{ + Rules: map[string]string{ + "/default": "google.com", + }, + StatusCode: fiber.StatusMovedPermanently, + })) + + app.Use(func(c *fiber.Ctx) error { + return c.SendStatus(fiber.StatusOK) + }) + + req, err = http.NewRequestWithContext(context.Background(), fiber.MethodGet, "/default", nil) + utils.AssertEqual(t, err, nil) + resp, err = app.Test(req) + + utils.AssertEqual(t, err, nil) + utils.AssertEqual(t, fiber.StatusMovedPermanently, resp.StatusCode) + utils.AssertEqual(t, "google.com", resp.Header.Get("Location")) + + // Case 3: Test invalid regex throws panic + defer func() { + if r := recover(); r != nil { + t.Log("Recovered from invalid regex: ", r) + } + }() + + app = *fiber.New() + app.Use(New(Config{ + Rules: map[string]string{ + "(": "google.com", + }, + StatusCode: fiber.StatusMovedPermanently, + })) + t.Error("Expected panic, got nil") +} diff --git a/middleware/requestid/config.go b/middleware/requestid/config.go new file mode 100644 index 0000000..b535ec9 --- /dev/null +++ b/middleware/requestid/config.go @@ -0,0 +1,66 @@ +package requestid + +import ( + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/utils" +) + +// Config defines the config for middleware. +type Config struct { + // Next defines a function to skip this middleware when returned true. + // + // Optional. Default: nil + Next func(c *fiber.Ctx) bool + + // Header is the header key where to get/set the unique request ID + // + // Optional. Default: "X-Request-ID" + Header string + + // Generator defines a function to generate the unique identifier. + // + // Optional. Default: utils.UUID + Generator func() string + + // ContextKey defines the key used when storing the request ID in + // the locals for a specific request. + // Should be a private type instead of string, but too many apps probably + // rely on this exact value. + // + // Optional. Default: "requestid" + ContextKey interface{} +} + +// ConfigDefault is the default config +// It uses a fast UUID generator which will expose the number of +// requests made to the server. To conceal this value for better +// privacy, use the "utils.UUIDv4" generator. +var ConfigDefault = Config{ + Next: nil, + Header: fiber.HeaderXRequestID, + Generator: utils.UUID, + ContextKey: "requestid", +} + +// Helper function to set default values +func configDefault(config ...Config) Config { + // Return default config if nothing provided + if len(config) < 1 { + return ConfigDefault + } + + // Override default config + cfg := config[0] + + // Set default values + if cfg.Header == "" { + cfg.Header = ConfigDefault.Header + } + if cfg.Generator == nil { + cfg.Generator = ConfigDefault.Generator + } + if cfg.ContextKey == nil { + cfg.ContextKey = ConfigDefault.ContextKey + } + return cfg +} diff --git a/middleware/requestid/requestid.go b/middleware/requestid/requestid.go new file mode 100644 index 0000000..6e07628 --- /dev/null +++ b/middleware/requestid/requestid.go @@ -0,0 +1,33 @@ +package requestid + +import ( + "github.com/gofiber/fiber/v2" +) + +// New creates a new middleware handler +func New(config ...Config) fiber.Handler { + // Set default config + cfg := configDefault(config...) + + // Return new handler + return func(c *fiber.Ctx) error { + // Don't execute middleware if Next returns true + if cfg.Next != nil && cfg.Next(c) { + return c.Next() + } + // Get id from request, else we generate one + rid := c.Get(cfg.Header) + if rid == "" { + rid = cfg.Generator() + } + + // Set new id to response header + c.Set(cfg.Header, rid) + + // Add the request ID to locals + c.Locals(cfg.ContextKey, rid) + + // Continue stack + return c.Next() + } +} diff --git a/middleware/requestid/requestid_test.go b/middleware/requestid/requestid_test.go new file mode 100644 index 0000000..b2dc2ac --- /dev/null +++ b/middleware/requestid/requestid_test.go @@ -0,0 +1,103 @@ +package requestid + +import ( + "net/http/httptest" + "testing" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/utils" +) + +// go test -run Test_RequestID +func Test_RequestID(t *testing.T) { + t.Parallel() + app := fiber.New() + + app.Use(New()) + + app.Get("/", func(c *fiber.Ctx) error { + return c.SendString("Hello, World 👋!") + }) + + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, fiber.StatusOK, resp.StatusCode) + + reqid := resp.Header.Get(fiber.HeaderXRequestID) + utils.AssertEqual(t, 36, len(reqid)) + + req := httptest.NewRequest(fiber.MethodGet, "/", nil) + req.Header.Add(fiber.HeaderXRequestID, reqid) + + resp, err = app.Test(req) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, fiber.StatusOK, resp.StatusCode) + utils.AssertEqual(t, reqid, resp.Header.Get(fiber.HeaderXRequestID)) +} + +// go test -run Test_RequestID_Next +func Test_RequestID_Next(t *testing.T) { + t.Parallel() + app := fiber.New() + app.Use(New(Config{ + Next: func(_ *fiber.Ctx) bool { + return true + }, + })) + + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, resp.Header.Get(fiber.HeaderXRequestID), "") + utils.AssertEqual(t, fiber.StatusNotFound, resp.StatusCode) +} + +// go test -run Test_RequestID_Locals +func Test_RequestID_Locals(t *testing.T) { + t.Parallel() + reqID := "ThisIsARequestId" + type ContextKey int + const requestContextKey ContextKey = iota + + app := fiber.New() + app.Use(New(Config{ + Generator: func() string { + return reqID + }, + ContextKey: requestContextKey, + })) + + var ctxVal string + + app.Use(func(c *fiber.Ctx) error { + ctxVal = c.Locals(requestContextKey).(string) //nolint:forcetypeassert,errcheck // We always store a string in here + return c.Next() + }) + + _, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, reqID, ctxVal) +} + +// go test -run Test_RequestID_DefaultKey +func Test_RequestID_DefaultKey(t *testing.T) { + t.Parallel() + reqID := "ThisIsARequestId" + + app := fiber.New() + app.Use(New(Config{ + Generator: func() string { + return reqID + }, + })) + + var ctxVal string + + app.Use(func(c *fiber.Ctx) error { + ctxVal = c.Locals("requestid").(string) //nolint:forcetypeassert,errcheck // We always store a string in here + return c.Next() + }) + + _, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, reqID, ctxVal) +} diff --git a/middleware/rewrite/config.go b/middleware/rewrite/config.go new file mode 100644 index 0000000..8873253 --- /dev/null +++ b/middleware/rewrite/config.go @@ -0,0 +1,38 @@ +package rewrite + +import ( + "regexp" + + "github.com/gofiber/fiber/v2" +) + +// Config defines the config for middleware. +type Config struct { + // Next defines a function to skip middleware. + // Optional. Default: nil + Next func(*fiber.Ctx) bool + + // Rules defines the URL path rewrite rules. The values captured in asterisk can be + // retrieved by index e.g. $1, $2 and so on. + // Required. Example: + // "/old": "/new", + // "/api/*": "/$1", + // "/js/*": "/public/javascripts/$1", + // "/users/*/orders/*": "/user/$1/order/$2", + Rules map[string]string + + rulesRegex map[*regexp.Regexp]string +} + +// Helper function to set default values +func configDefault(config ...Config) Config { + // Return default config if nothing provided + if len(config) < 1 { + return Config{} + } + + // Override default config + cfg := config[0] + + return cfg +} diff --git a/middleware/rewrite/rewrite.go b/middleware/rewrite/rewrite.go new file mode 100644 index 0000000..4465df1 --- /dev/null +++ b/middleware/rewrite/rewrite.go @@ -0,0 +1,54 @@ +package rewrite + +import ( + "regexp" + "strconv" + "strings" + + "github.com/gofiber/fiber/v2" +) + +// New creates a new middleware handler +func New(config ...Config) fiber.Handler { + cfg := configDefault(config...) + + // Initialize + cfg.rulesRegex = map[*regexp.Regexp]string{} + for k, v := range cfg.Rules { + k = strings.ReplaceAll(k, "*", "(.*)") + k += "$" + cfg.rulesRegex[regexp.MustCompile(k)] = v + } + // Middleware function + return func(c *fiber.Ctx) error { + // Next request to skip middleware + if cfg.Next != nil && cfg.Next(c) { + return c.Next() + } + // Rewrite + for k, v := range cfg.rulesRegex { + replacer := captureTokens(k, c.Path()) + if replacer != nil { + c.Path(replacer.Replace(v)) + break + } + } + return c.Next() + } +} + +// https://github.com/labstack/echo/blob/master/middleware/rewrite.go +func captureTokens(pattern *regexp.Regexp, input string) *strings.Replacer { + groups := pattern.FindAllStringSubmatch(input, -1) + if groups == nil { + return nil + } + values := groups[0][1:] + replace := make([]string, 2*len(values)) + for i, v := range values { + j := 2 * i + replace[j] = "$" + strconv.Itoa(i+1) + replace[j+1] = v + } + return strings.NewReplacer(replace...) +} diff --git a/middleware/rewrite/rewrite_test.go b/middleware/rewrite/rewrite_test.go new file mode 100644 index 0000000..cfd6565 --- /dev/null +++ b/middleware/rewrite/rewrite_test.go @@ -0,0 +1,173 @@ +//nolint:bodyclose // Much easier to just ignore memory leaks in tests +package rewrite + +import ( + "context" + "fmt" + "io" + "net/http" + "testing" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/utils" +) + +func Test_New(t *testing.T) { + // Test with no config + m := New() + + if m == nil { + t.Error("Expected middleware to be returned, got nil") + } + + // Test with config + m = New(Config{ + Rules: map[string]string{ + "/old": "/new", + }, + }) + + if m == nil { + t.Error("Expected middleware to be returned, got nil") + } + + // Test with full config + m = New(Config{ + Next: func(*fiber.Ctx) bool { + return true + }, + Rules: map[string]string{ + "/old": "/new", + }, + }) + + if m == nil { + t.Error("Expected middleware to be returned, got nil") + } +} + +func Test_Rewrite(t *testing.T) { + // Case 1: Next function always returns true + app := fiber.New() + app.Use(New(Config{ + Next: func(*fiber.Ctx) bool { + return true + }, + Rules: map[string]string{ + "/old": "/new", + }, + })) + + app.Get("/old", func(c *fiber.Ctx) error { + return c.SendString("Rewrite Successful") + }) + + req, err := http.NewRequestWithContext(context.Background(), fiber.MethodGet, "/old", nil) + utils.AssertEqual(t, err, nil) + resp, err := app.Test(req) + utils.AssertEqual(t, err, nil) + body, err := io.ReadAll(resp.Body) + utils.AssertEqual(t, err, nil) + bodyString := string(body) + + utils.AssertEqual(t, err, nil) + utils.AssertEqual(t, fiber.StatusOK, resp.StatusCode) + utils.AssertEqual(t, "Rewrite Successful", bodyString) + + // Case 2: Next function always returns false + app = fiber.New() + app.Use(New(Config{ + Next: func(*fiber.Ctx) bool { + return false + }, + Rules: map[string]string{ + "/old": "/new", + }, + })) + + app.Get("/new", func(c *fiber.Ctx) error { + return c.SendString("Rewrite Successful") + }) + + req, err = http.NewRequestWithContext(context.Background(), fiber.MethodGet, "/old", nil) + utils.AssertEqual(t, err, nil) + resp, err = app.Test(req) + utils.AssertEqual(t, err, nil) + body, err = io.ReadAll(resp.Body) + utils.AssertEqual(t, err, nil) + bodyString = string(body) + + utils.AssertEqual(t, err, nil) + utils.AssertEqual(t, fiber.StatusOK, resp.StatusCode) + utils.AssertEqual(t, "Rewrite Successful", bodyString) + + // Case 3: check for captured tokens in rewrite rule + app = fiber.New() + app.Use(New(Config{ + Rules: map[string]string{ + "/users/*/orders/*": "/user/$1/order/$2", + }, + })) + + app.Get("/user/:userID/order/:orderID", func(c *fiber.Ctx) error { + return c.SendString(fmt.Sprintf("User ID: %s, Order ID: %s", c.Params("userID"), c.Params("orderID"))) + }) + + req, err = http.NewRequestWithContext(context.Background(), fiber.MethodGet, "/users/123/orders/456", nil) + utils.AssertEqual(t, err, nil) + resp, err = app.Test(req) + utils.AssertEqual(t, err, nil) + body, err = io.ReadAll(resp.Body) + utils.AssertEqual(t, err, nil) + bodyString = string(body) + + utils.AssertEqual(t, err, nil) + utils.AssertEqual(t, fiber.StatusOK, resp.StatusCode) + utils.AssertEqual(t, "User ID: 123, Order ID: 456", bodyString) + + // Case 4: Send non-matching request, handled by default route + app = fiber.New() + app.Use(New(Config{ + Rules: map[string]string{ + "/users/*/orders/*": "/user/$1/order/$2", + }, + })) + + app.Get("/user/:userID/order/:orderID", func(c *fiber.Ctx) error { + return c.SendString(fmt.Sprintf("User ID: %s, Order ID: %s", c.Params("userID"), c.Params("orderID"))) + }) + + app.Use(func(c *fiber.Ctx) error { + return c.SendStatus(fiber.StatusOK) + }) + + req, err = http.NewRequestWithContext(context.Background(), fiber.MethodGet, "/not-matching-any-rule", nil) + utils.AssertEqual(t, err, nil) + resp, err = app.Test(req) + utils.AssertEqual(t, err, nil) + body, err = io.ReadAll(resp.Body) + utils.AssertEqual(t, err, nil) + bodyString = string(body) + + utils.AssertEqual(t, err, nil) + utils.AssertEqual(t, fiber.StatusOK, resp.StatusCode) + utils.AssertEqual(t, "OK", bodyString) + + // Case 4: Send non-matching request, with no default route + app = fiber.New() + app.Use(New(Config{ + Rules: map[string]string{ + "/users/*/orders/*": "/user/$1/order/$2", + }, + })) + + app.Get("/user/:userID/order/:orderID", func(c *fiber.Ctx) error { + return c.SendString(fmt.Sprintf("User ID: %s, Order ID: %s", c.Params("userID"), c.Params("orderID"))) + }) + + req, err = http.NewRequestWithContext(context.Background(), fiber.MethodGet, "/not-matching-any-rule", nil) + utils.AssertEqual(t, err, nil) + resp, err = app.Test(req) + utils.AssertEqual(t, err, nil) + utils.AssertEqual(t, fiber.StatusNotFound, resp.StatusCode) +} diff --git a/middleware/session/config.go b/middleware/session/config.go new file mode 100644 index 0000000..fe74132 --- /dev/null +++ b/middleware/session/config.go @@ -0,0 +1,128 @@ +package session + +import ( + "fmt" + "strings" + "time" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/log" + "github.com/gofiber/fiber/v2/utils" +) + +// Config defines the config for middleware. +type Config struct { + // Allowed session duration + // Optional. Default value 24 * time.Hour + Expiration time.Duration + + // Storage interface to store the session data + // Optional. Default value memory.New() + Storage fiber.Storage + + // KeyLookup is a string in the form of ":" that is used + // to extract session id from the request. + // Possible values: "header:", "query:" or "cookie:" + // Optional. Default value "cookie:session_id". + KeyLookup string + + // Domain of the cookie. + // Optional. Default value "". + CookieDomain string + + // Path of the cookie. + // Optional. Default value "". + CookiePath string + + // Indicates if cookie is secure. + // Optional. Default value false. + CookieSecure bool + + // Indicates if cookie is HTTP only. + // Optional. Default value false. + CookieHTTPOnly bool + + // Value of SameSite cookie. + // Optional. Default value "Lax". + CookieSameSite string + + // Decides whether cookie should last for only the browser sesison. + // Ignores Expiration if set to true + // Optional. Default value false. + CookieSessionOnly bool + + // KeyGenerator generates the session key. + // Optional. Default value utils.UUIDv4 + KeyGenerator func() string + + // Deprecated: Please use KeyLookup + CookieName string + + // Source defines where to obtain the session id + source Source + + // The session name + sessionName string +} + +type Source string + +const ( + SourceCookie Source = "cookie" + SourceHeader Source = "header" + SourceURLQuery Source = "query" +) + +// ConfigDefault is the default config +var ConfigDefault = Config{ + Expiration: 24 * time.Hour, + KeyLookup: "cookie:session_id", + KeyGenerator: utils.UUIDv4, + source: "cookie", + sessionName: "session_id", +} + +// Helper function to set default values +func configDefault(config ...Config) Config { + // Return default config if nothing provided + if len(config) < 1 { + return ConfigDefault + } + + // Override default config + cfg := config[0] + + // Set default values + if int(cfg.Expiration.Seconds()) <= 0 { + cfg.Expiration = ConfigDefault.Expiration + } + if cfg.CookieName != "" { + log.Warn("[SESSION] CookieName is deprecated, please use KeyLookup") + cfg.KeyLookup = fmt.Sprintf("cookie:%s", cfg.CookieName) + } + if cfg.KeyLookup == "" { + cfg.KeyLookup = ConfigDefault.KeyLookup + } + if cfg.KeyGenerator == nil { + cfg.KeyGenerator = ConfigDefault.KeyGenerator + } + + selectors := strings.Split(cfg.KeyLookup, ":") + const numSelectors = 2 + if len(selectors) != numSelectors { + panic("[session] KeyLookup must in the form of :") + } + switch Source(selectors[0]) { + case SourceCookie: + cfg.source = SourceCookie + case SourceHeader: + cfg.source = SourceHeader + case SourceURLQuery: + cfg.source = SourceURLQuery + default: + panic("[session] source is not supported") + } + cfg.sessionName = selectors[1] + + return cfg +} diff --git a/middleware/session/data.go b/middleware/session/data.go new file mode 100644 index 0000000..75024f8 --- /dev/null +++ b/middleware/session/data.go @@ -0,0 +1,63 @@ +package session + +import ( + "sync" +) + +// go:generate msgp +// msgp -file="data.go" -o="data_msgp.go" -tests=false -unexported +type data struct { + sync.RWMutex + Data map[string]interface{} +} + +var dataPool = sync.Pool{ + New: func() interface{} { + d := new(data) + d.Data = make(map[string]interface{}) + return d + }, +} + +func acquireData() *data { + return dataPool.Get().(*data) //nolint:forcetypeassert // We store nothing else in the pool +} + +func (d *data) Reset() { + d.Lock() + d.Data = make(map[string]interface{}) + d.Unlock() +} + +func (d *data) Get(key string) interface{} { + d.RLock() + v := d.Data[key] + d.RUnlock() + return v +} + +func (d *data) Set(key string, value interface{}) { + d.Lock() + d.Data[key] = value + d.Unlock() +} + +func (d *data) Delete(key string) { + d.Lock() + delete(d.Data, key) + d.Unlock() +} + +func (d *data) Keys() []string { + d.Lock() + keys := make([]string, 0, len(d.Data)) + for k := range d.Data { + keys = append(keys, k) + } + d.Unlock() + return keys +} + +func (d *data) Len() int { + return len(d.Data) +} diff --git a/middleware/session/session.go b/middleware/session/session.go new file mode 100644 index 0000000..b4115fa --- /dev/null +++ b/middleware/session/session.go @@ -0,0 +1,311 @@ +package session + +import ( + "bytes" + "encoding/gob" + "fmt" + "sync" + "time" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/utils" + + "github.com/valyala/fasthttp" +) + +type Session struct { + mu sync.RWMutex // Mutex to protect non-data fields + id string // session id + fresh bool // if new session + ctx *fiber.Ctx // fiber context + config *Store // store configuration + data *data // key value data + byteBuffer *bytes.Buffer // byte buffer for the en- and decode + exp time.Duration // expiration of this session +} + +var sessionPool = sync.Pool{ + New: func() interface{} { + return new(Session) + }, +} + +func acquireSession() *Session { + s := sessionPool.Get().(*Session) //nolint:forcetypeassert,errcheck // We store nothing else in the pool + if s.data == nil { + s.data = acquireData() + } + if s.byteBuffer == nil { + s.byteBuffer = new(bytes.Buffer) + } + s.fresh = true + return s +} + +func releaseSession(s *Session) { + s.mu.Lock() + s.id = "" + s.exp = 0 + s.ctx = nil + s.config = nil + if s.data != nil { + s.data.Reset() + } + if s.byteBuffer != nil { + s.byteBuffer.Reset() + } + s.mu.Unlock() + sessionPool.Put(s) +} + +// Fresh is true if the current session is new +func (s *Session) Fresh() bool { + s.mu.RLock() + defer s.mu.RUnlock() + return s.fresh +} + +// ID returns the session id +func (s *Session) ID() string { + s.mu.RLock() + defer s.mu.RUnlock() + return s.id +} + +// Get will return the value +func (s *Session) Get(key string) interface{} { + // Better safe than sorry + if s.data == nil { + return nil + } + return s.data.Get(key) +} + +// Set will update or create a new key value +func (s *Session) Set(key string, val interface{}) { + // Better safe than sorry + if s.data == nil { + return + } + s.data.Set(key, val) +} + +// Delete will delete the value +func (s *Session) Delete(key string) { + // Better safe than sorry + if s.data == nil { + return + } + s.data.Delete(key) +} + +// Destroy will delete the session from Storage and expire session cookie +func (s *Session) Destroy() error { + // Better safe than sorry + if s.data == nil { + return nil + } + + // Reset local data + s.data.Reset() + + s.mu.RLock() + defer s.mu.RUnlock() + + // Use external Storage if exist + if err := s.config.Storage.Delete(s.id); err != nil { + return err + } + + // Expire session + s.delSession() + return nil +} + +// Regenerate generates a new session id and delete the old one from Storage +func (s *Session) Regenerate() error { + s.mu.Lock() + defer s.mu.Unlock() + + // Delete old id from storage + if err := s.config.Storage.Delete(s.id); err != nil { + return err + } + + // Generate a new session, and set session.fresh to true + s.refresh() + + return nil +} + +// Reset generates a new session id, deletes the old one from storage, and resets the associated data +func (s *Session) Reset() error { + // Reset local data + if s.data != nil { + s.data.Reset() + } + + s.mu.Lock() + defer s.mu.Unlock() + + // Reset byte buffer + if s.byteBuffer != nil { + s.byteBuffer.Reset() + } + // Reset expiration + s.exp = 0 + + // Delete old id from storage + if err := s.config.Storage.Delete(s.id); err != nil { + return err + } + + // Expire session + s.delSession() + + // Generate a new session, and set session.fresh to true + s.refresh() + + return nil +} + +// refresh generates a new session, and set session.fresh to be true +func (s *Session) refresh() { + s.id = s.config.KeyGenerator() + s.fresh = true +} + +// Save will update the storage and client cookie +// +// sess.Save() will save the session data to the storage and update the +// client cookie, and it will release the session after saving. +// +// It's not safe to use the session after calling Save(). +func (s *Session) Save() error { + // Better safe than sorry + if s.data == nil { + return nil + } + + s.mu.Lock() + + // Check if session has your own expiration, otherwise use default value + if s.exp <= 0 { + s.exp = s.config.Expiration + } + + // Update client cookie + s.setSession() + + // Convert data to bytes + encCache := gob.NewEncoder(s.byteBuffer) + err := encCache.Encode(&s.data.Data) + if err != nil { + return fmt.Errorf("failed to encode data: %w", err) + } + + // Copy the data in buffer + encodedBytes := make([]byte, s.byteBuffer.Len()) + copy(encodedBytes, s.byteBuffer.Bytes()) + + // Pass copied bytes with session id to provider + if err := s.config.Storage.Set(s.id, encodedBytes, s.exp); err != nil { + return err + } + + s.mu.Unlock() + + // Release session + // TODO: It's not safe to use the Session after calling Save() + releaseSession(s) + + return nil +} + +// Keys will retrieve all keys in current session +func (s *Session) Keys() []string { + if s.data == nil { + return []string{} + } + return s.data.Keys() +} + +// SetExpiry sets a specific expiration for this session +func (s *Session) SetExpiry(exp time.Duration) { + s.mu.Lock() + defer s.mu.Unlock() + s.exp = exp +} + +func (s *Session) setSession() { + if s.config.source == SourceHeader { + s.ctx.Request().Header.SetBytesV(s.config.sessionName, []byte(s.id)) + s.ctx.Response().Header.SetBytesV(s.config.sessionName, []byte(s.id)) + } else { + fcookie := fasthttp.AcquireCookie() + fcookie.SetKey(s.config.sessionName) + fcookie.SetValue(s.id) + fcookie.SetPath(s.config.CookiePath) + fcookie.SetDomain(s.config.CookieDomain) + // Cookies are also session cookies if they do not specify the Expires or Max-Age attribute. + // refer: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie + if !s.config.CookieSessionOnly { + fcookie.SetMaxAge(int(s.exp.Seconds())) + fcookie.SetExpire(time.Now().Add(s.exp)) + } + fcookie.SetSecure(s.config.CookieSecure) + fcookie.SetHTTPOnly(s.config.CookieHTTPOnly) + + switch utils.ToLower(s.config.CookieSameSite) { + case "strict": + fcookie.SetSameSite(fasthttp.CookieSameSiteStrictMode) + case "none": + fcookie.SetSameSite(fasthttp.CookieSameSiteNoneMode) + default: + fcookie.SetSameSite(fasthttp.CookieSameSiteLaxMode) + } + s.ctx.Response().Header.SetCookie(fcookie) + fasthttp.ReleaseCookie(fcookie) + } +} + +func (s *Session) delSession() { + if s.config.source == SourceHeader { + s.ctx.Request().Header.Del(s.config.sessionName) + s.ctx.Response().Header.Del(s.config.sessionName) + } else { + s.ctx.Request().Header.DelCookie(s.config.sessionName) + s.ctx.Response().Header.DelCookie(s.config.sessionName) + + fcookie := fasthttp.AcquireCookie() + fcookie.SetKey(s.config.sessionName) + fcookie.SetPath(s.config.CookiePath) + fcookie.SetDomain(s.config.CookieDomain) + fcookie.SetMaxAge(-1) + fcookie.SetExpire(time.Now().Add(-1 * time.Minute)) + fcookie.SetSecure(s.config.CookieSecure) + fcookie.SetHTTPOnly(s.config.CookieHTTPOnly) + + switch utils.ToLower(s.config.CookieSameSite) { + case "strict": + fcookie.SetSameSite(fasthttp.CookieSameSiteStrictMode) + case "none": + fcookie.SetSameSite(fasthttp.CookieSameSiteNoneMode) + default: + fcookie.SetSameSite(fasthttp.CookieSameSiteLaxMode) + } + + s.ctx.Response().Header.SetCookie(fcookie) + fasthttp.ReleaseCookie(fcookie) + } +} + +// decodeSessionData decodes the session data from raw bytes. +func (s *Session) decodeSessionData(rawData []byte) error { + _, _ = s.byteBuffer.Write(rawData) //nolint:errcheck // This will never fail + encCache := gob.NewDecoder(s.byteBuffer) + if err := encCache.Decode(&s.data.Data); err != nil { + return fmt.Errorf("failed to decode session data: %w", err) + } + return nil +} diff --git a/middleware/session/session_test.go b/middleware/session/session_test.go new file mode 100644 index 0000000..ccd3fd0 --- /dev/null +++ b/middleware/session/session_test.go @@ -0,0 +1,904 @@ +package session + +import ( + "errors" + "sync" + "testing" + "time" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/internal/storage/memory" + "github.com/gofiber/fiber/v2/utils" + + "github.com/valyala/fasthttp" +) + +// go test -run Test_Session +func Test_Session(t *testing.T) { + t.Parallel() + + // session store + store := New() + + // fiber instance + app := fiber.New() + + // fiber context + ctx := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(ctx) + + // Get a new session + sess, err := store.Get(ctx) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, true, sess.Fresh()) + token := sess.ID() + err = sess.Save() + utils.AssertEqual(t, nil, err) + + app.ReleaseCtx(ctx) + ctx = app.AcquireCtx(&fasthttp.RequestCtx{}) + + // set session + ctx.Request().Header.SetCookie(store.sessionName, token) + + // get session + sess, err = store.Get(ctx) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, false, sess.Fresh()) + + // get keys + keys := sess.Keys() + utils.AssertEqual(t, []string{}, keys) + + // get value + name := sess.Get("name") + utils.AssertEqual(t, nil, name) + + // set value + sess.Set("name", "john") + + // get value + name = sess.Get("name") + utils.AssertEqual(t, "john", name) + + keys = sess.Keys() + utils.AssertEqual(t, []string{"name"}, keys) + + // delete key + sess.Delete("name") + + // get value + name = sess.Get("name") + utils.AssertEqual(t, nil, name) + + // get keys + keys = sess.Keys() + utils.AssertEqual(t, []string{}, keys) + + // get id + id := sess.ID() + utils.AssertEqual(t, token, id) + + // save the old session first + err = sess.Save() + utils.AssertEqual(t, nil, err) + + app.ReleaseCtx(ctx) + + // requesting entirely new context to prevent falsy tests + ctx = app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(ctx) + + sess, err = store.Get(ctx) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, true, sess.Fresh()) + + // this id should be randomly generated as session key was deleted + utils.AssertEqual(t, 36, len(sess.ID())) + + // when we use the original session for the second time + // the session be should be same if the session is not expired + ctx = app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(ctx) + + // request the server with the old session + ctx.Request().Header.SetCookie(store.sessionName, id) + sess, err = store.Get(ctx) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, false, sess.Fresh()) + utils.AssertEqual(t, sess.id, id) +} + +// go test -run Test_Session_Types +// +//nolint:forcetypeassert // TODO: Do not force-type assert +func Test_Session_Types(t *testing.T) { + t.Parallel() + + // session store + store := New() + + // fiber instance + app := fiber.New() + + // fiber context + ctx := app.AcquireCtx(&fasthttp.RequestCtx{}) + + // set cookie + ctx.Request().Header.SetCookie(store.sessionName, "123") + + // get session + sess, err := store.Get(ctx) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, true, sess.Fresh()) + + // the session string is no longer be 123 + newSessionIDString := sess.ID() + + type User struct { + Name string + } + store.RegisterType(User{}) + vuser := User{ + Name: "John", + } + // set value + var ( + vbool = true + vstring = "str" + vint = 13 + vint8 int8 = 13 + vint16 int16 = 13 + vint32 int32 = 13 + vint64 int64 = 13 + vuint uint = 13 + vuint8 uint8 = 13 + vuint16 uint16 = 13 + vuint32 uint32 = 13 + vuint64 uint64 = 13 + vuintptr uintptr = 13 + vbyte byte = 'k' + vrune = 'k' + vfloat32 float32 = 13 + vfloat64 float64 = 13 + vcomplex64 complex64 = 13 + vcomplex128 complex128 = 13 + ) + sess.Set("vuser", vuser) + sess.Set("vbool", vbool) + sess.Set("vstring", vstring) + sess.Set("vint", vint) + sess.Set("vint8", vint8) + sess.Set("vint16", vint16) + sess.Set("vint32", vint32) + sess.Set("vint64", vint64) + sess.Set("vuint", vuint) + sess.Set("vuint8", vuint8) + sess.Set("vuint16", vuint16) + sess.Set("vuint32", vuint32) + sess.Set("vuint32", vuint32) + sess.Set("vuint64", vuint64) + sess.Set("vuintptr", vuintptr) + sess.Set("vbyte", vbyte) + sess.Set("vrune", vrune) + sess.Set("vfloat32", vfloat32) + sess.Set("vfloat64", vfloat64) + sess.Set("vcomplex64", vcomplex64) + sess.Set("vcomplex128", vcomplex128) + + // save session + err = sess.Save() + utils.AssertEqual(t, nil, err) + + app.ReleaseCtx(ctx) + ctx = app.AcquireCtx(&fasthttp.RequestCtx{}) + + ctx.Request().Header.SetCookie(store.sessionName, newSessionIDString) + + // get session + sess, err = store.Get(ctx) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, false, sess.Fresh()) + + // get value + utils.AssertEqual(t, vuser, sess.Get("vuser").(User)) + utils.AssertEqual(t, vbool, sess.Get("vbool").(bool)) + utils.AssertEqual(t, vstring, sess.Get("vstring").(string)) + utils.AssertEqual(t, vint, sess.Get("vint").(int)) + utils.AssertEqual(t, vint8, sess.Get("vint8").(int8)) + utils.AssertEqual(t, vint16, sess.Get("vint16").(int16)) + utils.AssertEqual(t, vint32, sess.Get("vint32").(int32)) + utils.AssertEqual(t, vint64, sess.Get("vint64").(int64)) + utils.AssertEqual(t, vuint, sess.Get("vuint").(uint)) + utils.AssertEqual(t, vuint8, sess.Get("vuint8").(uint8)) + utils.AssertEqual(t, vuint16, sess.Get("vuint16").(uint16)) + utils.AssertEqual(t, vuint32, sess.Get("vuint32").(uint32)) + utils.AssertEqual(t, vuint64, sess.Get("vuint64").(uint64)) + utils.AssertEqual(t, vuintptr, sess.Get("vuintptr").(uintptr)) + utils.AssertEqual(t, vbyte, sess.Get("vbyte").(byte)) + utils.AssertEqual(t, vrune, sess.Get("vrune").(rune)) + utils.AssertEqual(t, vfloat32, sess.Get("vfloat32").(float32)) + utils.AssertEqual(t, vfloat64, sess.Get("vfloat64").(float64)) + utils.AssertEqual(t, vcomplex64, sess.Get("vcomplex64").(complex64)) + utils.AssertEqual(t, vcomplex128, sess.Get("vcomplex128").(complex128)) + app.ReleaseCtx(ctx) +} + +// go test -run Test_Session_Store_Reset +func Test_Session_Store_Reset(t *testing.T) { + t.Parallel() + // session store + store := New() + // fiber instance + app := fiber.New() + // fiber context + ctx := app.AcquireCtx(&fasthttp.RequestCtx{}) + + // get session + sess, err := store.Get(ctx) + utils.AssertEqual(t, nil, err) + // make sure its new + utils.AssertEqual(t, true, sess.Fresh()) + // set value & save + sess.Set("hello", "world") + ctx.Request().Header.SetCookie(store.sessionName, sess.ID()) + utils.AssertEqual(t, nil, sess.Save()) + + // reset store + utils.AssertEqual(t, nil, store.Reset()) + id := sess.ID() + + app.ReleaseCtx(ctx) + ctx = app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(ctx) + ctx.Request().Header.SetCookie(store.sessionName, id) + + // make sure the session is recreated + sess, err = store.Get(ctx) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, true, sess.Fresh()) + utils.AssertEqual(t, nil, sess.Get("hello")) +} + +// go test -run Test_Session_Save +func Test_Session_Save(t *testing.T) { + t.Parallel() + + t.Run("save to cookie", func(t *testing.T) { + // session store + store := New() + // fiber instance + app := fiber.New() + // fiber context + ctx := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(ctx) + // get session + sess, err := store.Get(ctx) + utils.AssertEqual(t, nil, err) + // set value + sess.Set("name", "john") + + // save session + err = sess.Save() + utils.AssertEqual(t, nil, err) + }) + + t.Run("save to header", func(t *testing.T) { + // session store + store := New(Config{ + KeyLookup: "header:session_id", + }) + // fiber instance + app := fiber.New() + // fiber context + ctx := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(ctx) + // get session + sess, err := store.Get(ctx) + utils.AssertEqual(t, nil, err) + // set value + sess.Set("name", "john") + + // save session + err = sess.Save() + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, store.getSessionID(ctx), string(ctx.Response().Header.Peek(store.sessionName))) + utils.AssertEqual(t, store.getSessionID(ctx), string(ctx.Request().Header.Peek(store.sessionName))) + }) +} + +func Test_Session_Save_Expiration(t *testing.T) { + t.Parallel() + + t.Run("save to cookie", func(t *testing.T) { + const sessionDuration = 5 * time.Second + t.Parallel() + // session store + store := New() + // fiber instance + app := fiber.New() + // fiber context + ctx := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(ctx) + // get session + sess, err := store.Get(ctx) + utils.AssertEqual(t, nil, err) + // set value + sess.Set("name", "john") + + token := sess.ID() + + // expire this session in 5 seconds + sess.SetExpiry(sessionDuration) + + // save session + err = sess.Save() + utils.AssertEqual(t, nil, err) + + app.ReleaseCtx(ctx) + ctx = app.AcquireCtx(&fasthttp.RequestCtx{}) + + // here you need to get the old session yet + ctx.Request().Header.SetCookie(store.sessionName, token) + sess, err = store.Get(ctx) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, "john", sess.Get("name")) + + // just to make sure the session has been expired + time.Sleep(sessionDuration + (10 * time.Millisecond)) + + app.ReleaseCtx(ctx) + ctx = app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(ctx) + + // here you should get a new session + ctx.Request().Header.SetCookie(store.sessionName, token) + sess, err = store.Get(ctx) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, nil, sess.Get("name")) + utils.AssertEqual(t, true, sess.ID() != token) + }) +} + +// go test -run Test_Session_Destroy +func Test_Session_Destroy(t *testing.T) { + t.Parallel() + + t.Run("destroy from cookie", func(t *testing.T) { + t.Parallel() + // session store + store := New() + // fiber instance + app := fiber.New() + // fiber context + ctx := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(ctx) + // get session + sess, err := store.Get(ctx) + utils.AssertEqual(t, nil, err) + + sess.Set("name", "fenny") + utils.AssertEqual(t, nil, sess.Destroy()) + name := sess.Get("name") + utils.AssertEqual(t, nil, name) + }) + + t.Run("destroy from header", func(t *testing.T) { + t.Parallel() + // session store + store := New(Config{ + KeyLookup: "header:session_id", + }) + // fiber instance + app := fiber.New() + // fiber context + ctx := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(ctx) + // get session + sess, err := store.Get(ctx) + utils.AssertEqual(t, nil, err) + + // set value & save + sess.Set("name", "fenny") + id := sess.ID() + utils.AssertEqual(t, nil, sess.Save()) + + app.ReleaseCtx(ctx) + ctx = app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(ctx) + + // get session + ctx.Request().Header.Set(store.sessionName, id) + sess, err = store.Get(ctx) + utils.AssertEqual(t, nil, err) + + err = sess.Destroy() + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, "", string(ctx.Response().Header.Peek(store.sessionName))) + utils.AssertEqual(t, "", string(ctx.Request().Header.Peek(store.sessionName))) + }) +} + +// go test -run Test_Session_Custom_Config +func Test_Session_Custom_Config(t *testing.T) { + t.Parallel() + + store := New(Config{Expiration: time.Hour, KeyGenerator: func() string { return "very random" }}) + utils.AssertEqual(t, time.Hour, store.Expiration) + utils.AssertEqual(t, "very random", store.KeyGenerator()) + + store = New(Config{Expiration: 0}) + utils.AssertEqual(t, ConfigDefault.Expiration, store.Expiration) +} + +// go test -run Test_Session_Cookie +func Test_Session_Cookie(t *testing.T) { + t.Parallel() + // session store + store := New() + // fiber instance + app := fiber.New() + // fiber context + ctx := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(ctx) + + // get session + sess, err := store.Get(ctx) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, nil, sess.Save()) + + // cookie should be set on Save ( even if empty data ) + utils.AssertEqual(t, 84, len(ctx.Response().Header.PeekCookie(store.sessionName))) +} + +// go test -run Test_Session_Cookie_In_Response +// Regression: https://github.com/gofiber/fiber/pull/1191 +func Test_Session_Cookie_In_Middleware_Chain(t *testing.T) { + t.Parallel() + store := New() + app := fiber.New() + + // fiber context + ctx := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(ctx) + + // get session + sess, err := store.Get(ctx) + utils.AssertEqual(t, nil, err) + sess.Set("id", "1") + id := sess.ID() + utils.AssertEqual(t, true, sess.Fresh()) + utils.AssertEqual(t, nil, sess.Save()) + + sess, err = store.Get(ctx) + utils.AssertEqual(t, nil, err) + sess.Set("name", "john") + utils.AssertEqual(t, true, sess.Fresh()) + utils.AssertEqual(t, id, sess.ID()) // session id should be the same + + utils.AssertEqual(t, sess.ID() != "1", true) + utils.AssertEqual(t, "john", sess.Get("name")) +} + +// go test -run Test_Session_Deletes_Single_Key +// Regression: https://github.com/gofiber/fiber/issues/1365 +func Test_Session_Deletes_Single_Key(t *testing.T) { + t.Parallel() + store := New() + app := fiber.New() + + ctx := app.AcquireCtx(&fasthttp.RequestCtx{}) + + sess, err := store.Get(ctx) + utils.AssertEqual(t, nil, err) + id := sess.ID() + sess.Set("id", "1") + utils.AssertEqual(t, nil, sess.Save()) + + app.ReleaseCtx(ctx) + ctx = app.AcquireCtx(&fasthttp.RequestCtx{}) + ctx.Request().Header.SetCookie(store.sessionName, id) + + sess, err = store.Get(ctx) + utils.AssertEqual(t, nil, err) + sess.Delete("id") + utils.AssertEqual(t, nil, sess.Save()) + + app.ReleaseCtx(ctx) + ctx = app.AcquireCtx(&fasthttp.RequestCtx{}) + ctx.Request().Header.SetCookie(store.sessionName, id) + + sess, err = store.Get(ctx) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, false, sess.Fresh()) + utils.AssertEqual(t, nil, sess.Get("id")) + app.ReleaseCtx(ctx) +} + +// go test -run Test_Session_Reset +func Test_Session_Reset(t *testing.T) { + t.Parallel() + // fiber instance + app := fiber.New() + + // session store + store := New() + + // fiber context + ctx := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(ctx) + + t.Run("reset session data and id, and set fresh to be true", func(t *testing.T) { + t.Parallel() + // fiber context + ctx := app.AcquireCtx(&fasthttp.RequestCtx{}) + // a random session uuid + originalSessionUUIDString := "" + + // now the session is in the storage + freshSession, err := store.Get(ctx) + utils.AssertEqual(t, nil, err) + + originalSessionUUIDString = freshSession.ID() + + // set a value + freshSession.Set("name", "fenny") + freshSession.Set("email", "fenny@example.com") + + err = freshSession.Save() + utils.AssertEqual(t, nil, err) + + app.ReleaseCtx(ctx) + ctx = app.AcquireCtx(&fasthttp.RequestCtx{}) + + // set cookie + ctx.Request().Header.SetCookie(store.sessionName, originalSessionUUIDString) + + // as the session is in the storage, session.fresh should be false + acquiredSession, err := store.Get(ctx) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, false, acquiredSession.Fresh()) + + err = acquiredSession.Reset() + utils.AssertEqual(t, nil, err) + + utils.AssertEqual(t, false, acquiredSession.ID() == originalSessionUUIDString) + utils.AssertEqual(t, false, acquiredSession.ID() == "") + + // acquiredSession.fresh should be true after resetting + utils.AssertEqual(t, true, acquiredSession.Fresh()) + + // Check that the session data has been reset + keys := acquiredSession.Keys() + utils.AssertEqual(t, []string{}, keys) + + // Set a new value for 'name' and check that it's updated + acquiredSession.Set("name", "john") + utils.AssertEqual(t, "john", acquiredSession.Get("name")) + utils.AssertEqual(t, nil, acquiredSession.Get("email")) + + // Save after resetting + err = acquiredSession.Save() + utils.AssertEqual(t, nil, err) + + // Check that the session id is not in the header or cookie anymore + utils.AssertEqual(t, "", string(ctx.Response().Header.Peek(store.sessionName))) + utils.AssertEqual(t, "", string(ctx.Request().Header.Peek(store.sessionName))) + + app.ReleaseCtx(ctx) + }) +} + +// go test -run Test_Session_Regenerate +// Regression: https://github.com/gofiber/fiber/issues/1395 +func Test_Session_Regenerate(t *testing.T) { + t.Parallel() + // fiber instance + app := fiber.New() + t.Run("set fresh to be true when regenerating a session", func(t *testing.T) { + // session store + store := New() + // a random session uuid + originalSessionUUIDString := "" + // fiber context + ctx := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(ctx) + + // now the session is in the storage + freshSession, err := store.Get(ctx) + utils.AssertEqual(t, nil, err) + + originalSessionUUIDString = freshSession.ID() + + err = freshSession.Save() + utils.AssertEqual(t, nil, err) + + // release the context + app.ReleaseCtx(ctx) + + // acquire a new context + ctx = app.AcquireCtx(&fasthttp.RequestCtx{}) + + // set cookie + ctx.Request().Header.SetCookie(store.sessionName, originalSessionUUIDString) + + // as the session is in the storage, session.fresh should be false + acquiredSession, err := store.Get(ctx) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, false, acquiredSession.Fresh()) + + err = acquiredSession.Regenerate() + utils.AssertEqual(t, nil, err) + + utils.AssertEqual(t, false, acquiredSession.ID() == originalSessionUUIDString) + + // acquiredSession.fresh should be true after regenerating + utils.AssertEqual(t, true, acquiredSession.Fresh()) + + // release the context + app.ReleaseCtx(ctx) + }) +} + +// go test -v -run=^$ -bench=Benchmark_Session -benchmem -count=4 +func Benchmark_Session(b *testing.B) { + app, store := fiber.New(), New() + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + c.Request().Header.SetCookie(store.sessionName, "12356789") + + var err error + b.Run("default", func(b *testing.B) { + b.ReportAllocs() + b.ResetTimer() + for n := 0; n < b.N; n++ { + sess, _ := store.Get(c) //nolint:errcheck // We're inside a benchmark + sess.Set("john", "doe") + err = sess.Save() + } + + utils.AssertEqual(b, nil, err) + }) + + b.Run("storage", func(b *testing.B) { + store = New(Config{ + Storage: memory.New(), + }) + b.ReportAllocs() + b.ResetTimer() + for n := 0; n < b.N; n++ { + sess, _ := store.Get(c) //nolint:errcheck // We're inside a benchmark + sess.Set("john", "doe") + err = sess.Save() + } + + utils.AssertEqual(b, nil, err) + }) +} + +// go test -v -run=^$ -bench=Benchmark_Session_Parallel -benchmem -count=4 +func Benchmark_Session_Parallel(b *testing.B) { + b.Run("default", func(b *testing.B) { + app, store := fiber.New(), New() + b.ReportAllocs() + b.ResetTimer() + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + c.Request().Header.SetCookie(store.sessionName, "12356789") + + sess, _ := store.Get(c) //nolint:errcheck // We're inside a benchmark + sess.Set("john", "doe") + _ = sess.Save() //nolint:errcheck // We're inside a benchmark + app.ReleaseCtx(c) + } + }) + }) + + b.Run("storage", func(b *testing.B) { + app := fiber.New() + store := New(Config{ + Storage: memory.New(), + }) + b.ReportAllocs() + b.ResetTimer() + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + c.Request().Header.SetCookie(store.sessionName, "12356789") + + sess, _ := store.Get(c) //nolint:errcheck // We're inside a benchmark + sess.Set("john", "doe") + _ = sess.Save() //nolint:errcheck // We're inside a benchmark + app.ReleaseCtx(c) + } + }) + }) +} + +// go test -v -run=^$ -bench=Benchmark_Session_Asserted -benchmem -count=4 +func Benchmark_Session_Asserted(b *testing.B) { + b.Run("default", func(b *testing.B) { + app, store := fiber.New(), New() + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + c.Request().Header.SetCookie(store.sessionName, "12356789") + + b.ReportAllocs() + b.ResetTimer() + for n := 0; n < b.N; n++ { + sess, err := store.Get(c) + utils.AssertEqual(b, nil, err) + sess.Set("john", "doe") + err = sess.Save() + utils.AssertEqual(b, nil, err) + } + }) + + b.Run("storage", func(b *testing.B) { + app := fiber.New() + store := New(Config{ + Storage: memory.New(), + }) + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + c.Request().Header.SetCookie(store.sessionName, "12356789") + + b.ReportAllocs() + b.ResetTimer() + for n := 0; n < b.N; n++ { + sess, err := store.Get(c) + utils.AssertEqual(b, nil, err) + sess.Set("john", "doe") + err = sess.Save() + utils.AssertEqual(b, nil, err) + } + }) +} + +// go test -v -run=^$ -bench=Benchmark_Session_Asserted_Parallel -benchmem -count=4 +func Benchmark_Session_Asserted_Parallel(b *testing.B) { + b.Run("default", func(b *testing.B) { + app, store := fiber.New(), New() + b.ReportAllocs() + b.ResetTimer() + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + c.Request().Header.SetCookie(store.sessionName, "12356789") + + sess, err := store.Get(c) + utils.AssertEqual(b, nil, err) + sess.Set("john", "doe") + utils.AssertEqual(b, nil, sess.Save()) + app.ReleaseCtx(c) + } + }) + }) + + b.Run("storage", func(b *testing.B) { + app := fiber.New() + store := New(Config{ + Storage: memory.New(), + }) + b.ReportAllocs() + b.ResetTimer() + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + c.Request().Header.SetCookie(store.sessionName, "12356789") + + sess, err := store.Get(c) + utils.AssertEqual(b, nil, err) + sess.Set("john", "doe") + utils.AssertEqual(b, nil, sess.Save()) + app.ReleaseCtx(c) + } + }) + }) +} + +// go test -v -race -run Test_Session_Concurrency ./... +func Test_Session_Concurrency(t *testing.T) { + t.Parallel() + app := fiber.New() + store := New() + + var wg sync.WaitGroup + errChan := make(chan error, 10) // Buffered channel to collect errors + const numGoroutines = 10 // Number of concurrent goroutines to test + + // Start numGoroutines goroutines + for i := 0; i < numGoroutines; i++ { + wg.Add(1) + go func() { + defer wg.Done() + + localCtx := app.AcquireCtx(&fasthttp.RequestCtx{}) + + sess, err := store.Get(localCtx) + if err != nil { + errChan <- err + return + } + + // Set a value + sess.Set("name", "john") + + // get the session id + id := sess.ID() + + // Check if the session is fresh + if !sess.Fresh() { + errChan <- errors.New("session should be fresh") + return + } + + // Save the session + if err := sess.Save(); err != nil { + errChan <- err + return + } + + // Release the context + app.ReleaseCtx(localCtx) + + // Acquire a new context + localCtx = app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(localCtx) + + // Set the session id in the header + localCtx.Request().Header.SetCookie(store.sessionName, id) + + // Get the session + sess, err = store.Get(localCtx) + if err != nil { + errChan <- err + return + } + + // Get the value + name := sess.Get("name") + if name != "john" { + errChan <- errors.New("name should be john") + return + } + + // Get ID from the session + if sess.ID() != id { + errChan <- errors.New("id should be the same") + return + } + + // Check if the session is fresh + if sess.Fresh() { + errChan <- errors.New("session should not be fresh") + return + } + + // Delete the key + sess.Delete("name") + + // Get the value + name = sess.Get("name") + if name != nil { + errChan <- errors.New("name should be nil") + return + } + + // Destroy the session + if err := sess.Destroy(); err != nil { + errChan <- err + return + } + }() + } + + wg.Wait() // Wait for all goroutines to finish + close(errChan) // Close the channel to signal no more errors will be sent + + // Check for errors sent to errChan + for err := range errChan { + utils.AssertEqual(t, nil, err) + } +} diff --git a/middleware/session/store.go b/middleware/session/store.go new file mode 100644 index 0000000..65db815 --- /dev/null +++ b/middleware/session/store.go @@ -0,0 +1,136 @@ +package session + +import ( + "encoding/gob" + "errors" + "fmt" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/internal/storage/memory" + "github.com/gofiber/fiber/v2/utils" +) + +// ErrEmptySessionID is an error that occurs when the session ID is empty. +var ErrEmptySessionID = errors.New("session id cannot be empty") + +// sessionIDKey is the local key type used to store and retrieve the session ID in context. +type sessionIDKey int + +const ( + // sessionIDContextKey is the key used to store the session ID in the context locals. + sessionIDContextKey sessionIDKey = iota +) + +type Store struct { + Config +} + +// New creates a new session store with the provided configuration. +func New(config ...Config) *Store { + // Set default config + cfg := configDefault(config...) + + if cfg.Storage == nil { + cfg.Storage = memory.New() + } + + return &Store{ + cfg, + } +} + +// RegisterType registers a custom type for encoding/decoding into any storage provider. +func (*Store) RegisterType(i interface{}) { + gob.Register(i) +} + +// Get retrieves or creates a session for the given context. +func (s *Store) Get(c *fiber.Ctx) (*Session, error) { + var rawData []byte + var err error + + id, ok := c.Locals(sessionIDContextKey).(string) + if !ok { + id = s.getSessionID(c) + } + + fresh := ok // Assume the session is fresh if the ID is found in locals + + // Attempt to fetch session data if an ID is provided + if id != "" { + rawData, err = s.Storage.Get(id) + if err != nil { + return nil, err + } + if rawData == nil { + // Data not found, prepare to generate a new session + id = "" + } + } + + // Generate a new ID if needed + if id == "" { + fresh = true // The session is fresh if a new ID is generated + id = s.KeyGenerator() + c.Locals(sessionIDContextKey, id) + } + + // Create session object + sess := acquireSession() + + sess.mu.Lock() + defer sess.mu.Unlock() + + sess.ctx = c + sess.config = s + sess.id = id + sess.fresh = fresh + + // Decode session data if found + if rawData != nil { + sess.data.Lock() + defer sess.data.Unlock() + if err := sess.decodeSessionData(rawData); err != nil { + return nil, fmt.Errorf("failed to decode session data: %w", err) + } + } + + return sess, nil +} + +// getSessionID returns the session ID from cookies, headers, or query string. +func (s *Store) getSessionID(c *fiber.Ctx) string { + id := c.Cookies(s.sessionName) + if len(id) > 0 { + return utils.CopyString(id) + } + + if s.source == SourceHeader { + id = string(c.Request().Header.Peek(s.sessionName)) + if len(id) > 0 { + return id + } + } + + if s.source == SourceURLQuery { + id = c.Query(s.sessionName) + if len(id) > 0 { + return utils.CopyString(id) + } + } + + return "" +} + +// Reset deletes all sessions from the storage. +func (s *Store) Reset() error { + return s.Storage.Reset() +} + +// Delete deletes a session by its ID. +func (s *Store) Delete(id string) error { + if id == "" { + return ErrEmptySessionID + } + return s.Storage.Delete(id) +} diff --git a/middleware/session/store_test.go b/middleware/session/store_test.go new file mode 100644 index 0000000..d99868b --- /dev/null +++ b/middleware/session/store_test.go @@ -0,0 +1,119 @@ +package session + +import ( + "fmt" + "testing" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/utils" + + "github.com/valyala/fasthttp" +) + +// go test -run TestStore_getSessionID +func TestStore_getSessionID(t *testing.T) { + t.Parallel() + expectedID := "test-session-id" + + // fiber instance + app := fiber.New() + + t.Run("from cookie", func(t *testing.T) { + t.Parallel() + // session store + store := New() + // fiber context + ctx := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(ctx) + // set cookie + ctx.Request().Header.SetCookie(store.sessionName, expectedID) + + utils.AssertEqual(t, expectedID, store.getSessionID(ctx)) + }) + + t.Run("from header", func(t *testing.T) { + t.Parallel() + // session store + store := New(Config{ + KeyLookup: "header:session_id", + }) + // fiber context + ctx := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(ctx) + // set header + ctx.Request().Header.Set(store.sessionName, expectedID) + + utils.AssertEqual(t, expectedID, store.getSessionID(ctx)) + }) + + t.Run("from url query", func(t *testing.T) { + t.Parallel() + // session store + store := New(Config{ + KeyLookup: "query:session_id", + }) + // fiber context + ctx := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(ctx) + // set url parameter + ctx.Request().SetRequestURI(fmt.Sprintf("/path?%s=%s", store.sessionName, expectedID)) + + utils.AssertEqual(t, expectedID, store.getSessionID(ctx)) + }) +} + +// go test -run TestStore_Get +// Regression: https://github.com/gofiber/fiber/issues/1408 +// Regression: https://github.com/gofiber/fiber/security/advisories/GHSA-98j2-3j3p-fw2v +func TestStore_Get(t *testing.T) { + t.Parallel() + unexpectedID := "test-session-id" + // fiber instance + app := fiber.New() + t.Run("session should be re-generated if it is invalid", func(t *testing.T) { + t.Parallel() + // session store + store := New() + // fiber context + ctx := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(ctx) + // set cookie + ctx.Request().Header.SetCookie(store.sessionName, unexpectedID) + + acquiredSession, err := store.Get(ctx) + utils.AssertEqual(t, err, nil) + + utils.AssertEqual(t, acquiredSession.ID() != unexpectedID, true) + }) +} + +// go test -run TestStore_DeleteSession +func TestStore_DeleteSession(t *testing.T) { + t.Parallel() + // fiber instance + app := fiber.New() + // session store + store := New() + + // fiber context + ctx := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(ctx) + + // Create a new session + session, err := store.Get(ctx) + utils.AssertEqual(t, err, nil) + + // Save the session ID + sessionID := session.ID() + + // Delete the session + err = store.Delete(sessionID) + utils.AssertEqual(t, err, nil) + + // Try to get the session again + session, err = store.Get(ctx) + utils.AssertEqual(t, err, nil) + + // The session ID should be different now, because the old session was deleted + utils.AssertEqual(t, session.ID() == sessionID, false) +} diff --git a/middleware/skip/skip.go b/middleware/skip/skip.go new file mode 100644 index 0000000..de64601 --- /dev/null +++ b/middleware/skip/skip.go @@ -0,0 +1,21 @@ +package skip + +import ( + "github.com/gofiber/fiber/v2" +) + +// New creates a middleware handler which skips the wrapped handler +// if the exclude predicate returns true. +func New(handler fiber.Handler, exclude func(c *fiber.Ctx) bool) fiber.Handler { + if exclude == nil { + return handler + } + + return func(c *fiber.Ctx) error { + if exclude(c) { + return c.Next() + } + + return handler(c) + } +} diff --git a/middleware/skip/skip_test.go b/middleware/skip/skip_test.go new file mode 100644 index 0000000..6d09255 --- /dev/null +++ b/middleware/skip/skip_test.go @@ -0,0 +1,57 @@ +package skip_test + +import ( + "net/http/httptest" + "testing" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/skip" + "github.com/gofiber/fiber/v2/utils" +) + +// go test -run Test_Skip +func Test_Skip(t *testing.T) { + t.Parallel() + app := fiber.New() + + app.Use(skip.New(errTeapotHandler, func(*fiber.Ctx) bool { return true })) + app.Get("/", helloWorldHandler) + + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, fiber.StatusOK, resp.StatusCode) +} + +// go test -run Test_SkipFalse +func Test_SkipFalse(t *testing.T) { + t.Parallel() + app := fiber.New() + + app.Use(skip.New(errTeapotHandler, func(*fiber.Ctx) bool { return false })) + app.Get("/", helloWorldHandler) + + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, fiber.StatusTeapot, resp.StatusCode) +} + +// go test -run Test_SkipNilFunc +func Test_SkipNilFunc(t *testing.T) { + t.Parallel() + app := fiber.New() + + app.Use(skip.New(errTeapotHandler, nil)) + app.Get("/", helloWorldHandler) + + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil)) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, fiber.StatusTeapot, resp.StatusCode) +} + +func helloWorldHandler(c *fiber.Ctx) error { + return c.SendString("Hello, World 👋!") +} + +func errTeapotHandler(*fiber.Ctx) error { + return fiber.ErrTeapot +} diff --git a/middleware/timeout/timeout.go b/middleware/timeout/timeout.go new file mode 100644 index 0000000..2bafb3f --- /dev/null +++ b/middleware/timeout/timeout.go @@ -0,0 +1,74 @@ +package timeout + +import ( + "context" + "errors" + "sync" + "time" + + "github.com/gofiber/fiber/v2/log" + + "github.com/gofiber/fiber/v2" +) + +var once sync.Once + +// New wraps a handler and aborts the process of the handler if the timeout is reached. +// +// Deprecated: This implementation contains data race issues. Use NewWithContext instead. +// Find documentation and sample usage on https://docs.gofiber.io/api/middleware/timeout +func New(handler fiber.Handler, timeout time.Duration) fiber.Handler { + once.Do(func() { + log.Warn("[TIMEOUT] timeout contains data race issues, not ready for production!") + }) + + if timeout <= 0 { + return handler + } + + // logic is from fasthttp.TimeoutWithCodeHandler https://github.com/valyala/fasthttp/blob/master/server.go#L418 + return func(ctx *fiber.Ctx) error { + ch := make(chan struct{}, 1) + + go func() { + defer func() { + if err := recover(); err != nil { + log.Errorf("[TIMEOUT] recover error %v", err) + } + }() + if err := handler(ctx); err != nil { + log.Errorf("[TIMEOUT] handler error %v", err) + } + ch <- struct{}{} + }() + + select { + case <-ch: + case <-time.After(timeout): + return fiber.ErrRequestTimeout + } + + return nil + } +} + +// NewWithContext implementation of timeout middleware. Set custom errors(context.DeadlineExceeded vs) for get fiber.ErrRequestTimeout response. +func NewWithContext(h fiber.Handler, t time.Duration, tErrs ...error) fiber.Handler { + return func(ctx *fiber.Ctx) error { + timeoutContext, cancel := context.WithTimeout(ctx.UserContext(), t) + defer cancel() + ctx.SetUserContext(timeoutContext) + if err := h(ctx); err != nil { + if errors.Is(err, context.DeadlineExceeded) { + return fiber.ErrRequestTimeout + } + for i := range tErrs { + if errors.Is(err, tErrs[i]) { + return fiber.ErrRequestTimeout + } + } + return err + } + return nil + } +} diff --git a/middleware/timeout/timeout_test.go b/middleware/timeout/timeout_test.go new file mode 100644 index 0000000..413ac0d --- /dev/null +++ b/middleware/timeout/timeout_test.go @@ -0,0 +1,88 @@ +package timeout + +import ( + "context" + "errors" + "fmt" + "net/http/httptest" + "testing" + "time" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/utils" +) + +// go test -run Test_WithContextTimeout +func Test_WithContextTimeout(t *testing.T) { + t.Parallel() + // fiber instance + app := fiber.New() + h := NewWithContext(func(c *fiber.Ctx) error { + sleepTime, err := time.ParseDuration(c.Params("sleepTime") + "ms") + utils.AssertEqual(t, nil, err) + if err := sleepWithContext(c.UserContext(), sleepTime, context.DeadlineExceeded); err != nil { + return fmt.Errorf("%w: l2 wrap", fmt.Errorf("%w: l1 wrap ", err)) + } + return nil + }, 100*time.Millisecond) + app.Get("/test/:sleepTime", h) + testTimeout := func(timeoutStr string) { + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/test/"+timeoutStr, nil)) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, fiber.StatusRequestTimeout, resp.StatusCode, "Status code") + } + testSucces := func(timeoutStr string) { + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/test/"+timeoutStr, nil)) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, fiber.StatusOK, resp.StatusCode, "Status code") + } + testTimeout("300") + testTimeout("500") + testSucces("50") + testSucces("30") +} + +var ErrFooTimeOut = errors.New("foo context canceled") + +// go test -run Test_WithContextTimeoutWithCustomError +func Test_WithContextTimeoutWithCustomError(t *testing.T) { + t.Parallel() + // fiber instance + app := fiber.New() + h := NewWithContext(func(c *fiber.Ctx) error { + sleepTime, err := time.ParseDuration(c.Params("sleepTime") + "ms") + utils.AssertEqual(t, nil, err) + if err := sleepWithContext(c.UserContext(), sleepTime, ErrFooTimeOut); err != nil { + return fmt.Errorf("%w: execution error", err) + } + return nil + }, 100*time.Millisecond, ErrFooTimeOut) + app.Get("/test/:sleepTime", h) + testTimeout := func(timeoutStr string) { + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/test/"+timeoutStr, nil)) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, fiber.StatusRequestTimeout, resp.StatusCode, "Status code") + } + testSucces := func(timeoutStr string) { + resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/test/"+timeoutStr, nil)) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, fiber.StatusOK, resp.StatusCode, "Status code") + } + testTimeout("300") + testTimeout("500") + testSucces("50") + testSucces("30") +} + +func sleepWithContext(ctx context.Context, d time.Duration, te error) error { + timer := time.NewTimer(d) + select { + case <-ctx.Done(): + if !timer.Stop() { + <-timer.C + } + return te + case <-timer.C: + } + return nil +} diff --git a/mount.go b/mount.go new file mode 100644 index 0000000..abb5695 --- /dev/null +++ b/mount.go @@ -0,0 +1,230 @@ +// ⚡️ Fiber is an Express inspired web framework written in Go with ☕️ +// 🤖 Github Repository: https://github.com/gofiber/fiber +// 📌 API Documentation: https://docs.gofiber.io + +package fiber + +import ( + "sort" + "strings" + "sync" + "sync/atomic" +) + +// Put fields related to mounting. +type mountFields struct { + // Mounted and main apps + appList map[string]*App + // Ordered keys of apps (sorted by key length for Render) + appListKeys []string + // check added routes of sub-apps + subAppsRoutesAdded sync.Once + // check mounted sub-apps + subAppsProcessed sync.Once + // Prefix of app if it was mounted + mountPath string +} + +// Create empty mountFields instance +func newMountFields(app *App) *mountFields { + return &mountFields{ + appList: map[string]*App{"": app}, + appListKeys: make([]string, 0), + } +} + +// Mount attaches another app instance as a sub-router along a routing path. +// It's very useful to split up a large API as many independent routers and +// compose them as a single service using Mount. The fiber's error handler and +// any of the fiber's sub apps are added to the application's error handlers +// to be invoked on errors that happen within the prefix route. +func (app *App) Mount(prefix string, subApp *App) Router { + prefix = strings.TrimRight(prefix, "/") + if prefix == "" { + prefix = "/" + } + + // Support for configs of mounted-apps and sub-mounted-apps + for mountedPrefixes, subApp := range subApp.mountFields.appList { + path := getGroupPath(prefix, mountedPrefixes) + + subApp.mountFields.mountPath = path + app.mountFields.appList[path] = subApp + } + + // register mounted group + mountGroup := &Group{Prefix: prefix, app: subApp} + app.register(methodUse, prefix, mountGroup) + + // Execute onMount hooks + if err := subApp.hooks.executeOnMountHooks(app); err != nil { + panic(err) + } + + return app +} + +// Mount attaches another app instance as a sub-router along a routing path. +// It's very useful to split up a large API as many independent routers and +// compose them as a single service using Mount. +func (grp *Group) Mount(prefix string, subApp *App) Router { + groupPath := getGroupPath(grp.Prefix, prefix) + groupPath = strings.TrimRight(groupPath, "/") + if groupPath == "" { + groupPath = "/" + } + + // Support for configs of mounted-apps and sub-mounted-apps + for mountedPrefixes, subApp := range subApp.mountFields.appList { + path := getGroupPath(groupPath, mountedPrefixes) + + subApp.mountFields.mountPath = path + grp.app.mountFields.appList[path] = subApp + } + + // register mounted group + mountGroup := &Group{Prefix: groupPath, app: subApp} + grp.app.register(methodUse, groupPath, mountGroup) + + // Execute onMount hooks + if err := subApp.hooks.executeOnMountHooks(grp.app); err != nil { + panic(err) + } + + return grp +} + +// The MountPath property contains one or more path patterns on which a sub-app was mounted. +func (app *App) MountPath() string { + return app.mountFields.mountPath +} + +// hasMountedApps Checks if there are any mounted apps in the current application. +func (app *App) hasMountedApps() bool { + return len(app.mountFields.appList) > 1 +} + +// mountStartupProcess Handles the startup process of mounted apps by appending sub-app routes, generating app list keys, and processing sub-app routes. +func (app *App) mountStartupProcess() { + if app.hasMountedApps() { + // add routes of sub-apps + app.mountFields.subAppsProcessed.Do(func() { + app.appendSubAppLists(app.mountFields.appList) + app.generateAppListKeys() + }) + // adds the routes of the sub-apps to the current application. + app.mountFields.subAppsRoutesAdded.Do(func() { + app.processSubAppsRoutes() + }) + } +} + +// generateAppListKeys generates app list keys for Render, should work after appendSubAppLists +func (app *App) generateAppListKeys() { + for key := range app.mountFields.appList { + app.mountFields.appListKeys = append(app.mountFields.appListKeys, key) + } + + sort.Slice(app.mountFields.appListKeys, func(i, j int) bool { + return len(app.mountFields.appListKeys[i]) < len(app.mountFields.appListKeys[j]) + }) +} + +// appendSubAppLists supports nested for sub apps +func (app *App) appendSubAppLists(appList map[string]*App, parent ...string) { + // Optimize: Cache parent prefix + parentPrefix := "" + if len(parent) > 0 { + parentPrefix = parent[0] + } + + for prefix, subApp := range appList { + // skip real app + if prefix == "" { + continue + } + + if parentPrefix != "" { + prefix = getGroupPath(parentPrefix, prefix) + } + + if _, ok := app.mountFields.appList[prefix]; !ok { + app.mountFields.appList[prefix] = subApp + } + + // The first element of appList is always the app itself. If there are no other sub apps, we should skip appending nested apps. + if len(subApp.mountFields.appList) > 1 { + app.appendSubAppLists(subApp.mountFields.appList, prefix) + } + } +} + +// processSubAppsRoutes adds routes of sub-apps recursively when the server is started +func (app *App) processSubAppsRoutes() { + for prefix, subApp := range app.mountFields.appList { + // skip real app + if prefix == "" { + continue + } + // process the inner routes + if subApp.hasMountedApps() { + subApp.mountFields.subAppsRoutesAdded.Do(func() { + subApp.processSubAppsRoutes() + }) + } + } + var handlersCount uint32 + var routePos uint32 + // Iterate over the stack of the parent app + for m := range app.stack { + // Iterate over each route in the stack + stackLen := len(app.stack[m]) + for i := 0; i < stackLen; i++ { + route := app.stack[m][i] + // Check if the route has a mounted app + if !route.mount { + routePos++ + // If not, update the route's position and continue + route.pos = routePos + if !route.use || (route.use && m == 0) { + handlersCount += uint32(len(route.Handlers)) + } + continue + } + + // Create a slice to hold the sub-app's routes + subRoutes := make([]*Route, len(route.group.app.stack[m])) + + // Iterate over the sub-app's routes + for j, subAppRoute := range route.group.app.stack[m] { + // Clone the sub-app's route + subAppRouteClone := app.copyRoute(subAppRoute) + + // Add the parent route's path as a prefix to the sub-app's route + app.addPrefixToRoute(route.path, subAppRouteClone) + + // Add the cloned sub-app's route to the slice of sub-app routes + subRoutes[j] = subAppRouteClone + } + + // Insert the sub-app's routes into the parent app's stack + newStack := make([]*Route, len(app.stack[m])+len(subRoutes)-1) + copy(newStack[:i], app.stack[m][:i]) + copy(newStack[i:i+len(subRoutes)], subRoutes) + copy(newStack[i+len(subRoutes):], app.stack[m][i+1:]) + app.stack[m] = newStack + + // Decrease the parent app's route count to account for the mounted app's original route + atomic.AddUint32(&app.routesCount, ^uint32(0)) + i-- + // Increase the parent app's route count to account for the sub-app's routes + atomic.AddUint32(&app.routesCount, uint32(len(subRoutes))) + + // Mark the parent app's routes as refreshed + app.routesRefreshed = true + // update stackLen after appending subRoutes to app.stack[m] + stackLen = len(app.stack[m]) + } + } + atomic.StoreUint32(&app.handlersCount, handlersCount) +} diff --git a/mount_test.go b/mount_test.go new file mode 100644 index 0000000..c0ca6bf --- /dev/null +++ b/mount_test.go @@ -0,0 +1,594 @@ +// ⚡️ Fiber is an Express inspired web framework written in Go with ☕️ +// 🤖 Github Repository: https://github.com/gofiber/fiber +// 📌 API Documentation: https://docs.gofiber.io + +//nolint:bodyclose // Much easier to just ignore memory leaks in tests +package fiber + +import ( + "errors" + "io" + "net/http" + "net/http/httptest" + "testing" + + "github.com/gofiber/fiber/v2/internal/template/html" + "github.com/gofiber/fiber/v2/utils" +) + +// go test -run Test_App_Mount +func Test_App_Mount(t *testing.T) { + t.Parallel() + micro := New() + micro.Get("/doe", func(c *Ctx) error { + return c.SendStatus(StatusOK) + }) + + app := New() + app.Mount("/john", micro) + resp, err := app.Test(httptest.NewRequest(MethodGet, "/john/doe", http.NoBody)) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, 200, resp.StatusCode, "Status code") + utils.AssertEqual(t, uint32(2), app.handlersCount) +} + +func Test_App_Mount_RootPath_Nested(t *testing.T) { + t.Parallel() + app := New() + dynamic := New() + apiserver := New() + + apiroutes := apiserver.Group("/v1") + apiroutes.Get("/home", func(c *Ctx) error { + return c.SendString("home") + }) + + dynamic.Mount("/api", apiserver) + app.Mount("/", dynamic) + + resp, err := app.Test(httptest.NewRequest(MethodGet, "/api/v1/home", http.NoBody)) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, 200, resp.StatusCode, "Status code") + utils.AssertEqual(t, uint32(2), app.handlersCount) +} + +// go test -run Test_App_Mount_Nested +func Test_App_Mount_Nested(t *testing.T) { + t.Parallel() + app := New() + one := New() + two := New() + three := New() + + two.Mount("/three", three) + app.Mount("/one", one) + one.Mount("/two", two) + + one.Get("/doe", func(c *Ctx) error { + return c.SendStatus(StatusOK) + }) + + two.Get("/nested", func(c *Ctx) error { + return c.SendStatus(StatusOK) + }) + + three.Get("/test", func(c *Ctx) error { + return c.SendStatus(StatusOK) + }) + + resp, err := app.Test(httptest.NewRequest(MethodGet, "/one/doe", http.NoBody)) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, 200, resp.StatusCode, "Status code") + + resp, err = app.Test(httptest.NewRequest(MethodGet, "/one/two/nested", http.NoBody)) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, 200, resp.StatusCode, "Status code") + + resp, err = app.Test(httptest.NewRequest(MethodGet, "/one/two/three/test", http.NoBody)) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, 200, resp.StatusCode, "Status code") + + utils.AssertEqual(t, uint32(6), app.handlersCount) + utils.AssertEqual(t, uint32(6), app.routesCount) +} + +// go test -run Test_App_Mount_Express_Behavior +func Test_App_Mount_Express_Behavior(t *testing.T) { + t.Parallel() + createTestHandler := func(body string) func(c *Ctx) error { + return func(c *Ctx) error { + return c.SendString(body) + } + } + testEndpoint := func(app *App, route, expectedBody string, expectedStatusCode int) { + resp, err := app.Test(httptest.NewRequest(MethodGet, route, http.NoBody)) + utils.AssertEqual(t, nil, err, "app.Test(req)") + body, err := io.ReadAll(resp.Body) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, expectedStatusCode, resp.StatusCode, "Status code") + utils.AssertEqual(t, expectedBody, string(body), "Unexpected response body") + } + + app := New() + subApp := New() + // app setup + { + subApp.Get("/hello", createTestHandler("subapp hello!")) + subApp.Get("/world", createTestHandler("subapp world!")) // <- wins + + app.Get("/hello", createTestHandler("app hello!")) // <- wins + app.Mount("/", subApp) // <- subApp registration + app.Get("/world", createTestHandler("app world!")) + + app.Get("/bar", createTestHandler("app bar!")) + subApp.Get("/bar", createTestHandler("subapp bar!")) // <- wins + + subApp.Get("/foo", createTestHandler("subapp foo!")) // <- wins + app.Get("/foo", createTestHandler("app foo!")) + + // 404 Handler + app.Use(func(c *Ctx) error { + return c.SendStatus(StatusNotFound) + }) + } + // expectation check + testEndpoint(app, "/world", "subapp world!", StatusOK) + testEndpoint(app, "/hello", "app hello!", StatusOK) + testEndpoint(app, "/bar", "subapp bar!", StatusOK) + testEndpoint(app, "/foo", "subapp foo!", StatusOK) + testEndpoint(app, "/unknown", ErrNotFound.Message, StatusNotFound) + + utils.AssertEqual(t, uint32(17), app.handlersCount) + utils.AssertEqual(t, uint32(16+9), app.routesCount) +} + +// go test -run Test_App_Mount_RoutePositions +func Test_App_Mount_RoutePositions(t *testing.T) { + t.Parallel() + testEndpoint := func(app *App, route, expectedBody string) { + resp, err := app.Test(httptest.NewRequest(MethodGet, route, http.NoBody)) + utils.AssertEqual(t, nil, err, "app.Test(req)") + body, err := io.ReadAll(resp.Body) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, StatusOK, resp.StatusCode, "Status code") + utils.AssertEqual(t, expectedBody, string(body), "Unexpected response body") + } + + app := New() + subApp1 := New() + subApp2 := New() + // app setup + { + app.Use(func(c *Ctx) error { + // set initial value + c.Locals("world", "world") + return c.Next() + }) + app.Mount("/subApp1", subApp1) + app.Use(func(c *Ctx) error { + return c.Next() + }) + app.Get("/bar", func(c *Ctx) error { + return c.SendString("ok") + }) + app.Use(func(c *Ctx) error { + // is overwritten in case the positioning is not correct + c.Locals("world", "hello") + return c.Next() + }) + methods := subApp2.Group("/subApp2") + methods.Get("/world", func(c *Ctx) error { + v, ok := c.Locals("world").(string) + if !ok { + panic("unexpected data type") + } + return c.SendString(v) + }) + app.Mount("", subApp2) + } + + testEndpoint(app, "/subApp2/world", "hello") + + routeStackGET := app.Stack()[0] + utils.AssertEqual(t, true, routeStackGET[0].use) + utils.AssertEqual(t, "/", routeStackGET[0].path) + + utils.AssertEqual(t, true, routeStackGET[1].use) + utils.AssertEqual(t, "/", routeStackGET[1].path) + utils.AssertEqual(t, true, routeStackGET[0].pos < routeStackGET[1].pos, "wrong position of route 0") + + utils.AssertEqual(t, false, routeStackGET[2].use) + utils.AssertEqual(t, "/bar", routeStackGET[2].path) + utils.AssertEqual(t, true, routeStackGET[1].pos < routeStackGET[2].pos, "wrong position of route 1") + + utils.AssertEqual(t, true, routeStackGET[3].use) + utils.AssertEqual(t, "/", routeStackGET[3].path) + utils.AssertEqual(t, true, routeStackGET[2].pos < routeStackGET[3].pos, "wrong position of route 2") + + utils.AssertEqual(t, false, routeStackGET[4].use) + utils.AssertEqual(t, "/subapp2/world", routeStackGET[4].path) + utils.AssertEqual(t, true, routeStackGET[3].pos < routeStackGET[4].pos, "wrong position of route 3") + + utils.AssertEqual(t, 5, len(routeStackGET)) +} + +// go test -run Test_App_MountPath +func Test_App_MountPath(t *testing.T) { + t.Parallel() + app := New() + one := New() + two := New() + three := New() + + two.Mount("/three", three) + one.Mount("/two", two) + app.Mount("/one", one) + + utils.AssertEqual(t, "/one", one.MountPath()) + utils.AssertEqual(t, "/one/two", two.MountPath()) + utils.AssertEqual(t, "/one/two/three", three.MountPath()) + utils.AssertEqual(t, "", app.MountPath()) +} + +func Test_App_ErrorHandler_GroupMount(t *testing.T) { + t.Parallel() + micro := New(Config{ + ErrorHandler: func(c *Ctx, err error) error { + utils.AssertEqual(t, "0: GET error", err.Error()) + return c.Status(500).SendString("1: custom error") + }, + }) + micro.Get("/doe", func(c *Ctx) error { + return errors.New("0: GET error") + }) + + app := New() + v1 := app.Group("/v1") + v1.Mount("/john", micro) + + resp, err := app.Test(httptest.NewRequest(MethodGet, "/v1/john/doe", http.NoBody)) + testErrorResponse(t, err, resp, "1: custom error") +} + +func Test_App_ErrorHandler_GroupMountRootLevel(t *testing.T) { + t.Parallel() + micro := New(Config{ + ErrorHandler: func(c *Ctx, err error) error { + utils.AssertEqual(t, "0: GET error", err.Error()) + return c.Status(500).SendString("1: custom error") + }, + }) + micro.Get("/john/doe", func(c *Ctx) error { + return errors.New("0: GET error") + }) + + app := New() + v1 := app.Group("/v1") + v1.Mount("/", micro) + + resp, err := app.Test(httptest.NewRequest(MethodGet, "/v1/john/doe", http.NoBody)) + testErrorResponse(t, err, resp, "1: custom error") +} + +// go test -run Test_App_Group_Mount +func Test_App_Group_Mount(t *testing.T) { + t.Parallel() + micro := New() + micro.Get("/doe", func(c *Ctx) error { + return c.SendStatus(StatusOK) + }) + + app := New() + v1 := app.Group("/v1") + v1.Mount("/john", micro) + + resp, err := app.Test(httptest.NewRequest(MethodGet, "/v1/john/doe", http.NoBody)) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, 200, resp.StatusCode, "Status code") + utils.AssertEqual(t, uint32(2), app.handlersCount) +} + +func Test_App_UseParentErrorHandler(t *testing.T) { + t.Parallel() + app := New(Config{ + ErrorHandler: func(ctx *Ctx, err error) error { + return ctx.Status(500).SendString("hi, i'm a custom error") + }, + }) + + fiber := New() + fiber.Get("/", func(c *Ctx) error { + return errors.New("something happened") + }) + + app.Mount("/api", fiber) + + resp, err := app.Test(httptest.NewRequest(MethodGet, "/api", http.NoBody)) + testErrorResponse(t, err, resp, "hi, i'm a custom error") +} + +func Test_App_UseMountedErrorHandler(t *testing.T) { + t.Parallel() + app := New() + + fiber := New(Config{ + ErrorHandler: func(ctx *Ctx, err error) error { + return ctx.Status(500).SendString("hi, i'm a custom error") + }, + }) + fiber.Get("/", func(c *Ctx) error { + return errors.New("something happened") + }) + + app.Mount("/api", fiber) + + resp, err := app.Test(httptest.NewRequest(MethodGet, "/api", http.NoBody)) + testErrorResponse(t, err, resp, "hi, i'm a custom error") +} + +func Test_App_UseMountedErrorHandlerRootLevel(t *testing.T) { + t.Parallel() + app := New() + + fiber := New(Config{ + ErrorHandler: func(ctx *Ctx, err error) error { + return ctx.Status(500).SendString("hi, i'm a custom error") + }, + }) + fiber.Get("/api", func(c *Ctx) error { + return errors.New("something happened") + }) + + app.Mount("/", fiber) + + resp, err := app.Test(httptest.NewRequest(MethodGet, "/api", http.NoBody)) + testErrorResponse(t, err, resp, "hi, i'm a custom error") +} + +func Test_App_UseMountedErrorHandlerForBestPrefixMatch(t *testing.T) { + t.Parallel() + app := New() + + tsf := func(ctx *Ctx, err error) error { + return ctx.Status(200).SendString("hi, i'm a custom sub sub fiber error") + } + tripleSubFiber := New(Config{ + ErrorHandler: tsf, + }) + tripleSubFiber.Get("/", func(c *Ctx) error { + return errors.New("something happened") + }) + + sf := func(ctx *Ctx, err error) error { + return ctx.Status(200).SendString("hi, i'm a custom sub fiber error") + } + subfiber := New(Config{ + ErrorHandler: sf, + }) + subfiber.Get("/", func(c *Ctx) error { + return errors.New("something happened") + }) + subfiber.Mount("/third", tripleSubFiber) + + f := func(ctx *Ctx, err error) error { + return ctx.Status(200).SendString("hi, i'm a custom error") + } + fiber := New(Config{ + ErrorHandler: f, + }) + fiber.Get("/", func(c *Ctx) error { + return errors.New("something happened") + }) + fiber.Mount("/sub", subfiber) + + app.Mount("/api", fiber) + + resp, err := app.Test(httptest.NewRequest(MethodGet, "/api/sub", http.NoBody)) + utils.AssertEqual(t, nil, err, "/api/sub req") + utils.AssertEqual(t, 200, resp.StatusCode, "Status code") + + b, err := io.ReadAll(resp.Body) + utils.AssertEqual(t, nil, err, "iotuil.ReadAll()") + utils.AssertEqual(t, "hi, i'm a custom sub fiber error", string(b), "Response body") + + resp2, err := app.Test(httptest.NewRequest(MethodGet, "/api/sub/third", http.NoBody)) + utils.AssertEqual(t, nil, err, "/api/sub/third req") + utils.AssertEqual(t, 200, resp.StatusCode, "Status code") + + b, err = io.ReadAll(resp2.Body) + utils.AssertEqual(t, nil, err, "iotuil.ReadAll()") + utils.AssertEqual(t, "hi, i'm a custom sub sub fiber error", string(b), "Third fiber Response body") +} + +// go test -run Test_Mount_Route_Names +func Test_Mount_Route_Names(t *testing.T) { + // create sub-app with 2 handlers: + subApp1 := New() + subApp1.Get("/users", func(c *Ctx) error { + url, err := c.GetRouteURL("add-user", Map{}) + utils.AssertEqual(t, err, nil) + utils.AssertEqual(t, url, "/app1/users", "handler: app1.add-user") // the prefix is /app1 because of the mount + // if subApp1 is not mounted, expected url just /users + return nil + }).Name("get-users") + subApp1.Post("/users", func(c *Ctx) error { + route := c.App().GetRoute("get-users") + utils.AssertEqual(t, route.Method, MethodGet, "handler: app1.get-users method") + utils.AssertEqual(t, route.Path, "/app1/users", "handler: app1.get-users path") + return nil + }).Name("add-user") + + // create sub-app with 2 handlers inside a group: + subApp2 := New() + app2Grp := subApp2.Group("/users").Name("users.") + app2Grp.Get("", nil).Name("get") + app2Grp.Post("", nil).Name("add") + + // put both sub-apps into root app + rootApp := New() + _ = rootApp.Mount("/app1", subApp1) + _ = rootApp.Mount("/app2", subApp2) + + rootApp.startupProcess() + + // take route directly from sub-app + route := subApp1.GetRoute("get-users") + utils.AssertEqual(t, route.Method, MethodGet) + utils.AssertEqual(t, route.Path, "/users") + + route = subApp1.GetRoute("add-user") + utils.AssertEqual(t, route.Method, MethodPost) + utils.AssertEqual(t, route.Path, "/users") + + // take route directly from sub-app with group + route = subApp2.GetRoute("users.get") + utils.AssertEqual(t, route.Method, MethodGet) + utils.AssertEqual(t, route.Path, "/users") + + route = subApp2.GetRoute("users.add") + utils.AssertEqual(t, route.Method, MethodPost) + utils.AssertEqual(t, route.Path, "/users") + + // take route from root app (using names of sub-apps) + route = rootApp.GetRoute("add-user") + utils.AssertEqual(t, route.Method, MethodPost) + utils.AssertEqual(t, route.Path, "/app1/users") + + route = rootApp.GetRoute("users.add") + utils.AssertEqual(t, route.Method, MethodPost) + utils.AssertEqual(t, route.Path, "/app2/users") + + // GetRouteURL inside handler + req := httptest.NewRequest(MethodGet, "/app1/users", nil) + resp, err := rootApp.Test(req) + + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, StatusOK, resp.StatusCode, "Status code") + + // ctx.App().GetRoute() inside handler + req = httptest.NewRequest(MethodPost, "/app1/users", nil) + resp, err = rootApp.Test(req) + + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, StatusOK, resp.StatusCode, "Status code") +} + +// go test -run Test_Ctx_Render_Mount +func Test_Ctx_Render_Mount(t *testing.T) { + t.Parallel() + + sub := New(Config{ + Views: html.New("./.github/testdata/template", ".gohtml"), + }) + + sub.Get("/:name", func(ctx *Ctx) error { + return ctx.Render("hello_world", Map{ + "Name": ctx.Params("name"), + }) + }) + + app := New() + app.Mount("/hello", sub) + + resp, err := app.Test(httptest.NewRequest(MethodGet, "/hello/a", http.NoBody)) + utils.AssertEqual(t, StatusOK, resp.StatusCode, "Status code") + utils.AssertEqual(t, nil, err, "app.Test(req)") + + body, err := io.ReadAll(resp.Body) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, "

Hello a!

", string(body)) +} + +// go test -run Test_Ctx_Render_Mount_ParentOrSubHasViews +func Test_Ctx_Render_Mount_ParentOrSubHasViews(t *testing.T) { + t.Parallel() + + engine := &testTemplateEngine{} + err := engine.Load() + utils.AssertEqual(t, nil, err) + + engine2 := &testTemplateEngine{path: "testdata2"} + err = engine2.Load() + utils.AssertEqual(t, nil, err) + + sub := New(Config{ + Views: html.New("./.github/testdata/template", ".gohtml"), + }) + + sub2 := New(Config{ + Views: engine2, + }) + + app := New(Config{ + Views: engine, + }) + + app.Get("/test", func(c *Ctx) error { + return c.Render("index.tmpl", Map{ + "Title": "Hello, World!", + }) + }) + + sub.Get("/world/:name", func(c *Ctx) error { + return c.Render("hello_world", Map{ + "Name": c.Params("name"), + }) + }) + + sub2.Get("/moment", func(c *Ctx) error { + return c.Render("bruh.tmpl", Map{}) + }) + + sub.Mount("/bruh", sub2) + app.Mount("/hello", sub) + + resp, err := app.Test(httptest.NewRequest(MethodGet, "/hello/world/a", http.NoBody)) + utils.AssertEqual(t, StatusOK, resp.StatusCode, "Status code") + utils.AssertEqual(t, nil, err, "app.Test(req)") + + body, err := io.ReadAll(resp.Body) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, "

Hello a!

", string(body)) + + resp, err = app.Test(httptest.NewRequest(MethodGet, "/test", http.NoBody)) + utils.AssertEqual(t, StatusOK, resp.StatusCode, "Status code") + utils.AssertEqual(t, nil, err, "app.Test(req)") + + body, err = io.ReadAll(resp.Body) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, "

Hello, World!

", string(body)) + + resp, err = app.Test(httptest.NewRequest(MethodGet, "/hello/bruh/moment", http.NoBody)) + utils.AssertEqual(t, StatusOK, resp.StatusCode, "Status code") + utils.AssertEqual(t, nil, err, "app.Test(req)") + + body, err = io.ReadAll(resp.Body) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, "

I'm Bruh

", string(body)) +} + +func Test_Ctx_Render_MountGroup(t *testing.T) { + t.Parallel() + + micro := New(Config{ + Views: html.New("./.github/testdata/template", ".gohtml"), + }) + + micro.Get("/doe", func(c *Ctx) error { + return c.Render("hello_world", Map{ + "Name": "doe", + }) + }) + + app := New() + v1 := app.Group("/v1") + v1.Mount("/john", micro) + + resp, err := app.Test(httptest.NewRequest(MethodGet, "/v1/john/doe", http.NoBody)) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, 200, resp.StatusCode, "Status code") + + body, err := io.ReadAll(resp.Body) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, "

Hello doe!

", string(body)) +} diff --git a/path.go b/path.go new file mode 100644 index 0000000..2cf88c7 --- /dev/null +++ b/path.go @@ -0,0 +1,740 @@ +// ⚡️ Fiber is an Express inspired web framework written in Go with ☕️ +// 📄 Github Repository: https://github.com/gofiber/fiber +// 📌 API Documentation: https://docs.gofiber.io +// ⚠️ This path parser was inspired by ucarion/urlpath (MIT License). +// 💖 Maintained and modified for Fiber by @renewerner87 + +package fiber + +import ( + "regexp" + "strconv" + "strings" + "time" + "unicode" + + "github.com/google/uuid" + + "github.com/gofiber/fiber/v2/utils" +) + +// routeParser holds the path segments and param names +type routeParser struct { + segs []*routeSegment // the parsed segments of the route + params []string // that parameter names the parsed route + wildCardCount int // number of wildcard parameters, used internally to give the wildcard parameter its number + plusCount int // number of plus parameters, used internally to give the plus parameter its number +} + +// paramsSeg holds the segment metadata +type routeSegment struct { + // const information + Const string // constant part of the route + // parameter information + IsParam bool // Truth value that indicates whether it is a parameter or a constant part + ParamName string // name of the parameter for access to it, for wildcards and plus parameters access iterators starting with 1 are added + ComparePart string // search part to find the end of the parameter + PartCount int // how often is the search part contained in the non-param segments? -> necessary for greedy search + IsGreedy bool // indicates whether the parameter is greedy or not, is used with wildcard and plus + IsOptional bool // indicates whether the parameter is optional or not + // common information + IsLast bool // shows if the segment is the last one for the route + HasOptionalSlash bool // segment has the possibility of an optional slash + Constraints []*Constraint // Constraint type if segment is a parameter, if not it will be set to noConstraint by default + Length int // length of the parameter for segment, when its 0 then the length is undetermined + // future TODO: add support for optional groups "/abc(/def)?" +} + +// different special routing signs +const ( + wildcardParam byte = '*' // indicates an optional greedy parameter + plusParam byte = '+' // indicates a required greedy parameter + optionalParam byte = '?' // concludes a parameter by name and makes it optional + paramStarterChar byte = ':' // start character for a parameter with name + slashDelimiter byte = '/' // separator for the route, unlike the other delimiters this character at the end can be optional + escapeChar byte = '\\' // escape character + paramConstraintStart byte = '<' // start of type constraint for a parameter + paramConstraintEnd byte = '>' // end of type constraint for a parameter + paramConstraintSeparator byte = ';' // separator of type constraints for a parameter + paramConstraintDataStart byte = '(' // start of data of type constraint for a parameter + paramConstraintDataEnd byte = ')' // end of data of type constraint for a parameter + paramConstraintDataSeparator byte = ',' // separator of datas of type constraint for a parameter +) + +// TypeConstraint parameter constraint types +type TypeConstraint int16 + +type Constraint struct { + ID TypeConstraint + RegexCompiler *regexp.Regexp + Data []string +} + +const ( + noConstraint TypeConstraint = iota + 1 + intConstraint + boolConstraint + floatConstraint + alphaConstraint + datetimeConstraint + guidConstraint + minLenConstraint + maxLenConstraint + lenConstraint + betweenLenConstraint + minConstraint + maxConstraint + rangeConstraint + regexConstraint +) + +// list of possible parameter and segment delimiter +var ( + // slash has a special role, unlike the other parameters it must not be interpreted as a parameter + routeDelimiter = []byte{slashDelimiter, '-', '.'} + // list of greedy parameters + greedyParameters = []byte{wildcardParam, plusParam} + // list of chars for the parameter recognizing + parameterStartChars = []byte{wildcardParam, plusParam, paramStarterChar} + // list of chars of delimiters and the starting parameter name char + parameterDelimiterChars = append([]byte{paramStarterChar, escapeChar}, routeDelimiter...) + // list of chars to find the end of a parameter + parameterEndChars = append([]byte{optionalParam}, parameterDelimiterChars...) + // list of parameter constraint start + parameterConstraintStartChars = []byte{paramConstraintStart} + // list of parameter constraint end + parameterConstraintEndChars = []byte{paramConstraintEnd} + // list of parameter separator + parameterConstraintSeparatorChars = []byte{paramConstraintSeparator} + // list of parameter constraint data start + parameterConstraintDataStartChars = []byte{paramConstraintDataStart} + // list of parameter constraint data end + parameterConstraintDataEndChars = []byte{paramConstraintDataEnd} + // list of parameter constraint data separator + parameterConstraintDataSeparatorChars = []byte{paramConstraintDataSeparator} +) + +// RoutePatternMatch checks if a given path matches a Fiber route pattern. +func RoutePatternMatch(path, pattern string, cfg ...Config) bool { + // See logic in (*Route).match and (*App).register + var ctxParams [maxParams]string + + config := Config{} + if len(cfg) > 0 { + config = cfg[0] + } + + if path == "" { + path = "/" + } + + // Cannot have an empty pattern + if pattern == "" { + pattern = "/" + } + // Pattern always start with a '/' + if pattern[0] != '/' { + pattern = "/" + pattern + } + + patternPretty := pattern + + // Case-sensitive routing, all to lowercase + if !config.CaseSensitive { + patternPretty = utils.ToLower(patternPretty) + path = utils.ToLower(path) + } + // Strict routing, remove trailing slashes + if !config.StrictRouting && len(patternPretty) > 1 { + patternPretty = utils.TrimRight(patternPretty, '/') + } + + parser := parseRoute(patternPretty) + + if patternPretty == "/" && path == "/" { + return true + // '*' wildcard matches any path + } else if patternPretty == "/*" { + return true + } + + // Does this route have parameters + if len(parser.params) > 0 { + if match := parser.getMatch(path, path, &ctxParams, false); match { + return true + } + } + // Check for a simple match + patternPretty = RemoveEscapeChar(patternPretty) + if len(patternPretty) == len(path) && patternPretty == path { + return true + } + // No match + return false +} + +// parseRoute analyzes the route and divides it into segments for constant areas and parameters, +// this information is needed later when assigning the requests to the declared routes +func parseRoute(pattern string) routeParser { + parser := routeParser{} + + part := "" + for len(pattern) > 0 { + nextParamPosition := findNextParamPosition(pattern) + // handle the parameter part + if nextParamPosition == 0 { + processedPart, seg := parser.analyseParameterPart(pattern) + parser.params, parser.segs, part = append(parser.params, seg.ParamName), append(parser.segs, seg), processedPart + } else { + processedPart, seg := parser.analyseConstantPart(pattern, nextParamPosition) + parser.segs, part = append(parser.segs, seg), processedPart + } + + // reduce the pattern by the processed parts + if len(part) == len(pattern) { + break + } + pattern = pattern[len(part):] + } + // mark last segment + if len(parser.segs) > 0 { + parser.segs[len(parser.segs)-1].IsLast = true + } + parser.segs = addParameterMetaInfo(parser.segs) + + return parser +} + +// addParameterMetaInfo add important meta information to the parameter segments +// to simplify the search for the end of the parameter +func addParameterMetaInfo(segs []*routeSegment) []*routeSegment { + var comparePart string + segLen := len(segs) + // loop from end to begin + for i := segLen - 1; i >= 0; i-- { + // set the compare part for the parameter + if segs[i].IsParam { + // important for finding the end of the parameter + segs[i].ComparePart = RemoveEscapeChar(comparePart) + } else { + comparePart = segs[i].Const + if len(comparePart) > 1 { + comparePart = utils.TrimRight(comparePart, slashDelimiter) + } + } + } + + // loop from begin to end + for i := 0; i < segLen; i++ { + // check how often the compare part is in the following const parts + if segs[i].IsParam { + // check if parameter segments are directly after each other and if one of them is greedy + // in case the next parameter or the current parameter is not a wildcard it's not greedy, we only want one character + if segLen > i+1 && !segs[i].IsGreedy && segs[i+1].IsParam && !segs[i+1].IsGreedy { + segs[i].Length = 1 + } + if segs[i].ComparePart == "" { + continue + } + for j := i + 1; j <= len(segs)-1; j++ { + if !segs[j].IsParam { + // count is important for the greedy match + segs[i].PartCount += strings.Count(segs[j].Const, segs[i].ComparePart) + } + } + // check if the end of the segment is a optional slash and then if the segement is optional or the last one + } else if segs[i].Const[len(segs[i].Const)-1] == slashDelimiter && (segs[i].IsLast || (segLen > i+1 && segs[i+1].IsOptional)) { + segs[i].HasOptionalSlash = true + } + } + + return segs +} + +// findNextParamPosition search for the next possible parameter start position +func findNextParamPosition(pattern string) int { + nextParamPosition := findNextNonEscapedCharsetPosition(pattern, parameterStartChars) + if nextParamPosition != -1 && len(pattern) > nextParamPosition && pattern[nextParamPosition] != wildcardParam { + // search for parameter characters for the found parameter start, + // if there are more, move the parameter start to the last parameter char + for found := findNextNonEscapedCharsetPosition(pattern[nextParamPosition+1:], parameterStartChars); found == 0; { + nextParamPosition++ + if len(pattern) > nextParamPosition { + break + } + } + } + + return nextParamPosition +} + +// analyseConstantPart find the end of the constant part and create the route segment +func (*routeParser) analyseConstantPart(pattern string, nextParamPosition int) (string, *routeSegment) { + // handle the constant part + processedPart := pattern + if nextParamPosition != -1 { + // remove the constant part until the parameter + processedPart = pattern[:nextParamPosition] + } + constPart := RemoveEscapeChar(processedPart) + return processedPart, &routeSegment{ + Const: constPart, + Length: len(constPart), + } +} + +// analyseParameterPart find the parameter end and create the route segment +func (routeParser *routeParser) analyseParameterPart(pattern string) (string, *routeSegment) { + isWildCard := pattern[0] == wildcardParam + isPlusParam := pattern[0] == plusParam + + var parameterEndPosition int + if strings.ContainsRune(pattern, rune(paramConstraintStart)) && strings.ContainsRune(pattern, rune(paramConstraintEnd)) { + parameterEndPosition = findNextCharsetPositionConstraint(pattern[1:], parameterEndChars) + } else { + parameterEndPosition = findNextNonEscapedCharsetPosition(pattern[1:], parameterEndChars) + } + + parameterConstraintStart := -1 + parameterConstraintEnd := -1 + // handle wildcard end + switch { + case isWildCard, isPlusParam: + parameterEndPosition = 0 + case parameterEndPosition == -1: + parameterEndPosition = len(pattern) - 1 + case !isInCharset(pattern[parameterEndPosition+1], parameterDelimiterChars): + parameterEndPosition++ + } + + // find constraint part if exists in the parameter part and remove it + if parameterEndPosition > 0 { + parameterConstraintStart = findNextNonEscapedCharsetPosition(pattern[0:parameterEndPosition], parameterConstraintStartChars) + parameterConstraintEnd = findLastCharsetPosition(pattern[0:parameterEndPosition+1], parameterConstraintEndChars) + } + + // cut params part + processedPart := pattern[0 : parameterEndPosition+1] + paramName := RemoveEscapeChar(GetTrimmedParam(processedPart)) + + // Check has constraint + var constraints []*Constraint + + if hasConstraint := parameterConstraintStart != -1 && parameterConstraintEnd != -1; hasConstraint { + constraintString := pattern[parameterConstraintStart+1 : parameterConstraintEnd] + userConstraints := splitNonEscaped(constraintString, string(parameterConstraintSeparatorChars)) + constraints = make([]*Constraint, 0, len(userConstraints)) + + for _, c := range userConstraints { + start := findNextNonEscapedCharsetPosition(c, parameterConstraintDataStartChars) + end := findLastCharsetPosition(c, parameterConstraintDataEndChars) + + // Assign constraint + if start != -1 && end != -1 { + constraint := &Constraint{ + ID: getParamConstraintType(c[:start]), + } + + // remove escapes from data + if constraint.ID != regexConstraint { + constraint.Data = splitNonEscaped(c[start+1:end], string(parameterConstraintDataSeparatorChars)) + if len(constraint.Data) == 1 { + constraint.Data[0] = RemoveEscapeChar(constraint.Data[0]) + } else if len(constraint.Data) == 2 { // This is fine, we simply expect two parts + constraint.Data[0] = RemoveEscapeChar(constraint.Data[0]) + constraint.Data[1] = RemoveEscapeChar(constraint.Data[1]) + } + } + + // Precompile regex if has regex constraint + if constraint.ID == regexConstraint { + constraint.Data = []string{c[start+1 : end]} + constraint.RegexCompiler = regexp.MustCompile(constraint.Data[0]) + } + + constraints = append(constraints, constraint) + } else { + constraints = append(constraints, &Constraint{ + ID: getParamConstraintType(c), + Data: []string{}, + }) + } + } + + paramName = RemoveEscapeChar(GetTrimmedParam(pattern[0:parameterConstraintStart])) + } + + // add access iterator to wildcard and plus + if isWildCard { + routeParser.wildCardCount++ + paramName += strconv.Itoa(routeParser.wildCardCount) + } else if isPlusParam { + routeParser.plusCount++ + paramName += strconv.Itoa(routeParser.plusCount) + } + + segment := &routeSegment{ + ParamName: paramName, + IsParam: true, + IsOptional: isWildCard || pattern[parameterEndPosition] == optionalParam, + IsGreedy: isWildCard || isPlusParam, + } + + if len(constraints) > 0 { + segment.Constraints = constraints + } + + return processedPart, segment +} + +// isInCharset check is the given character in the charset list +func isInCharset(searchChar byte, charset []byte) bool { + for _, char := range charset { + if char == searchChar { + return true + } + } + return false +} + +// findNextCharsetPosition search the next char position from the charset +func findNextCharsetPosition(search string, charset []byte) int { + nextPosition := -1 + for _, char := range charset { + if pos := strings.IndexByte(search, char); pos != -1 && (pos < nextPosition || nextPosition == -1) { + nextPosition = pos + } + } + + return nextPosition +} + +// findNextCharsetPosition search the last char position from the charset +func findLastCharsetPosition(search string, charset []byte) int { + lastPosition := -1 + for _, char := range charset { + if pos := strings.LastIndexByte(search, char); pos != -1 && (pos < lastPosition || lastPosition == -1) { + lastPosition = pos + } + } + + return lastPosition +} + +// findNextCharsetPositionConstraint search the next char position from the charset +// unlike findNextCharsetPosition, it takes care of constraint start-end chars to parse route pattern +func findNextCharsetPositionConstraint(search string, charset []byte) int { + constraintStart := findNextNonEscapedCharsetPosition(search, parameterConstraintStartChars) + constraintEnd := findNextNonEscapedCharsetPosition(search, parameterConstraintEndChars) + nextPosition := -1 + + for _, char := range charset { + pos := strings.IndexByte(search, char) + + if pos != -1 && (pos < nextPosition || nextPosition == -1) { + if (pos > constraintStart && pos > constraintEnd) || (pos < constraintStart && pos < constraintEnd) { + nextPosition = pos + } + } + } + + return nextPosition +} + +// findNextNonEscapedCharsetPosition search the next char position from the charset and skip the escaped characters +func findNextNonEscapedCharsetPosition(search string, charset []byte) int { + pos := findNextCharsetPosition(search, charset) + for pos > 0 && search[pos-1] == escapeChar { + if len(search) == pos+1 { + // escaped character is at the end + return -1 + } + nextPossiblePos := findNextCharsetPosition(search[pos+1:], charset) + if nextPossiblePos == -1 { + return -1 + } + // the previous character is taken into consideration + pos = nextPossiblePos + pos + 1 + } + + return pos +} + +// splitNonEscaped slices s into all substrings separated by sep and returns a slice of the substrings between those separators +// This function also takes a care of escape char when splitting. +func splitNonEscaped(s, sep string) []string { + var result []string + i := findNextNonEscapedCharsetPosition(s, []byte(sep)) + + for i > -1 { + result = append(result, s[:i]) + s = s[i+len(sep):] + i = findNextNonEscapedCharsetPosition(s, []byte(sep)) + } + + return append(result, s) +} + +// getMatch parses the passed url and tries to match it against the route segments and determine the parameter positions +func (routeParser *routeParser) getMatch(detectionPath, path string, params *[maxParams]string, partialCheck bool) bool { //nolint: revive // Accepting a bool param is fine here + var i, paramsIterator, partLen int + for _, segment := range routeParser.segs { + partLen = len(detectionPath) + // check const segment + if !segment.IsParam { + i = segment.Length + // is optional part or the const part must match with the given string + // check if the end of the segment is an optional slash + if segment.HasOptionalSlash && partLen == i-1 && detectionPath == segment.Const[:i-1] { + i-- + } else if !(i <= partLen && detectionPath[:i] == segment.Const) { + return false + } + } else { + // determine parameter length + i = findParamLen(detectionPath, segment) + if !segment.IsOptional && i == 0 { + return false + } + // take over the params positions + params[paramsIterator] = path[:i] + + if !(segment.IsOptional && i == 0) { + // check constraint + for _, c := range segment.Constraints { + if matched := c.CheckConstraint(params[paramsIterator]); !matched { + return false + } + } + } + + paramsIterator++ + } + + // reduce founded part from the string + if partLen > 0 { + detectionPath, path = detectionPath[i:], path[i:] + } + } + if detectionPath != "" && !partialCheck { + return false + } + + return true +} + +// findParamLen for the expressjs wildcard behavior (right to left greedy) +// look at the other segments and take what is left for the wildcard from right to left +func findParamLen(s string, segment *routeSegment) int { + if segment.IsLast { + return findParamLenForLastSegment(s, segment) + } + + if segment.Length != 0 && len(s) >= segment.Length { + return segment.Length + } else if segment.IsGreedy { + // Search the parameters until the next constant part + // special logic for greedy params + searchCount := strings.Count(s, segment.ComparePart) + if searchCount > 1 { + return findGreedyParamLen(s, searchCount, segment) + } + } + + if len(segment.ComparePart) == 1 { + if constPosition := strings.IndexByte(s, segment.ComparePart[0]); constPosition != -1 { + return constPosition + } + } else if constPosition := strings.Index(s, segment.ComparePart); constPosition != -1 { + // if the compare part was found, but contains a slash although this part is not greedy, then it must not match + // example: /api/:param/fixedEnd -> path: /api/123/456/fixedEnd = no match , /api/123/fixedEnd = match + if !segment.IsGreedy && strings.IndexByte(s[:constPosition], slashDelimiter) != -1 { + return 0 + } + return constPosition + } + + return len(s) +} + +// findParamLenForLastSegment get the length of the parameter if it is the last segment +func findParamLenForLastSegment(s string, seg *routeSegment) int { + if !seg.IsGreedy { + if i := strings.IndexByte(s, slashDelimiter); i != -1 { + return i + } + } + + return len(s) +} + +// findGreedyParamLen get the length of the parameter for greedy segments from right to left +func findGreedyParamLen(s string, searchCount int, segment *routeSegment) int { + // check all from right to left segments + for i := segment.PartCount; i > 0 && searchCount > 0; i-- { + searchCount-- + if constPosition := strings.LastIndex(s, segment.ComparePart); constPosition != -1 { + s = s[:constPosition] + } else { + break + } + } + + return len(s) +} + +// GetTrimmedParam trims the ':' & '?' from a string +func GetTrimmedParam(param string) string { + start := 0 + end := len(param) + + if end == 0 || param[start] != paramStarterChar { // is not a param + return param + } + start++ + if param[end-1] == optionalParam { // is ? + end-- + } + + return param[start:end] +} + +// RemoveEscapeChar remove escape characters +func RemoveEscapeChar(word string) string { + if strings.IndexByte(word, escapeChar) != -1 { + return strings.ReplaceAll(word, string(escapeChar), "") + } + return word +} + +func getParamConstraintType(constraintPart string) TypeConstraint { + switch constraintPart { + case ConstraintInt: + return intConstraint + case ConstraintBool: + return boolConstraint + case ConstraintFloat: + return floatConstraint + case ConstraintAlpha: + return alphaConstraint + case ConstraintGuid: + return guidConstraint + case ConstraintMinLen, ConstraintMinLenLower: + return minLenConstraint + case ConstraintMaxLen, ConstraintMaxLenLower: + return maxLenConstraint + case ConstraintLen: + return lenConstraint + case ConstraintBetweenLen, ConstraintBetweenLenLower: + return betweenLenConstraint + case ConstraintMin: + return minConstraint + case ConstraintMax: + return maxConstraint + case ConstraintRange: + return rangeConstraint + case ConstraintDatetime: + return datetimeConstraint + case ConstraintRegex: + return regexConstraint + default: + return noConstraint + } +} + +//nolint:errcheck // TODO: Properly check _all_ errors in here, log them & immediately return +func (c *Constraint) CheckConstraint(param string) bool { + var err error + var num int + + // check data exists + needOneData := []TypeConstraint{minLenConstraint, maxLenConstraint, lenConstraint, minConstraint, maxConstraint, datetimeConstraint, regexConstraint} + needTwoData := []TypeConstraint{betweenLenConstraint, rangeConstraint} + + for _, data := range needOneData { + if c.ID == data && len(c.Data) == 0 { + return false + } + } + + for _, data := range needTwoData { + if c.ID == data && len(c.Data) < 2 { + return false + } + } + + // check constraints + switch c.ID { + case noConstraint: + // Nothing to check + case intConstraint: + _, err = strconv.Atoi(param) + case boolConstraint: + _, err = strconv.ParseBool(param) + case floatConstraint: + _, err = strconv.ParseFloat(param, 32) + case alphaConstraint: + for _, r := range param { + if !unicode.IsLetter(r) { + return false + } + } + case guidConstraint: + _, err = uuid.Parse(param) + case minLenConstraint: + data, _ := strconv.Atoi(c.Data[0]) + + if len(param) < data { + return false + } + case maxLenConstraint: + data, _ := strconv.Atoi(c.Data[0]) + + if len(param) > data { + return false + } + case lenConstraint: + data, _ := strconv.Atoi(c.Data[0]) + + if len(param) != data { + return false + } + case betweenLenConstraint: + data, _ := strconv.Atoi(c.Data[0]) + data2, _ := strconv.Atoi(c.Data[1]) + length := len(param) + if length < data || length > data2 { + return false + } + case minConstraint: + data, _ := strconv.Atoi(c.Data[0]) + num, err = strconv.Atoi(param) + + if num < data { + return false + } + case maxConstraint: + data, _ := strconv.Atoi(c.Data[0]) + num, err = strconv.Atoi(param) + + if num > data { + return false + } + case rangeConstraint: + data, _ := strconv.Atoi(c.Data[0]) + data2, _ := strconv.Atoi(c.Data[1]) + num, err = strconv.Atoi(param) + + if num < data || num > data2 { + return false + } + case datetimeConstraint: + _, err = time.Parse(c.Data[0], param) + case regexConstraint: + if match := c.RegexCompiler.MatchString(param); !match { + return false + } + } + + return err == nil +} diff --git a/path_test.go b/path_test.go new file mode 100644 index 0000000..d0d11d0 --- /dev/null +++ b/path_test.go @@ -0,0 +1,268 @@ +// ⚡️ Fiber is an Express inspired web framework written in Go with ☕️ +// 📝 Github Repository: https://github.com/gofiber/fiber +// 📌 API Documentation: https://docs.gofiber.io + +package fiber + +import ( + "fmt" + "testing" + + "github.com/gofiber/fiber/v2/utils" +) + +// go test -race -run Test_Path_parseRoute +func Test_Path_parseRoute(t *testing.T) { + t.Parallel() + var rp routeParser + + rp = parseRoute("/shop/product/::filter/color::color/size::size") + utils.AssertEqual(t, routeParser{ + segs: []*routeSegment{ + {Const: "/shop/product/:", Length: 15}, + {IsParam: true, ParamName: "filter", ComparePart: "/color:", PartCount: 1}, + {Const: "/color:", Length: 7}, + {IsParam: true, ParamName: "color", ComparePart: "/size:", PartCount: 1}, + {Const: "/size:", Length: 6}, + {IsParam: true, ParamName: "size", IsLast: true}, + }, + params: []string{"filter", "color", "size"}, + }, rp) + + rp = parseRoute("/api/v1/:param/abc/*") + utils.AssertEqual(t, routeParser{ + segs: []*routeSegment{ + {Const: "/api/v1/", Length: 8}, + {IsParam: true, ParamName: "param", ComparePart: "/abc", PartCount: 1}, + {Const: "/abc/", Length: 5, HasOptionalSlash: true}, + {IsParam: true, ParamName: "*1", IsGreedy: true, IsOptional: true, IsLast: true}, + }, + params: []string{"param", "*1"}, + wildCardCount: 1, + }, rp) + + rp = parseRoute("/v1/some/resource/name\\:customVerb") + utils.AssertEqual(t, routeParser{ + segs: []*routeSegment{ + {Const: "/v1/some/resource/name:customVerb", Length: 33, IsLast: true}, + }, + params: nil, + }, rp) + + rp = parseRoute("/v1/some/resource/:name\\:customVerb") + utils.AssertEqual(t, routeParser{ + segs: []*routeSegment{ + {Const: "/v1/some/resource/", Length: 18}, + {IsParam: true, ParamName: "name", ComparePart: ":customVerb", PartCount: 1}, + {Const: ":customVerb", Length: 11, IsLast: true}, + }, + params: []string{"name"}, + }, rp) + + // heavy test with escaped charaters + rp = parseRoute("/v1/some/resource/name\\\\:customVerb?\\?/:param/*") + utils.AssertEqual(t, routeParser{ + segs: []*routeSegment{ + {Const: "/v1/some/resource/name:customVerb??/", Length: 36}, + {IsParam: true, ParamName: "param", ComparePart: "/", PartCount: 1}, + {Const: "/", Length: 1, HasOptionalSlash: true}, + {IsParam: true, ParamName: "*1", IsGreedy: true, IsOptional: true, IsLast: true}, + }, + params: []string{"param", "*1"}, + wildCardCount: 1, + }, rp) + + rp = parseRoute("/api/*/:param/:param2") + utils.AssertEqual(t, routeParser{ + segs: []*routeSegment{ + {Const: "/api/", Length: 5, HasOptionalSlash: true}, + {IsParam: true, ParamName: "*1", IsGreedy: true, IsOptional: true, ComparePart: "/", PartCount: 2}, + {Const: "/", Length: 1}, + {IsParam: true, ParamName: "param", ComparePart: "/", PartCount: 1}, + {Const: "/", Length: 1}, + {IsParam: true, ParamName: "param2", IsLast: true}, + }, + params: []string{"*1", "param", "param2"}, + wildCardCount: 1, + }, rp) + + rp = parseRoute("/test:optional?:optional2?") + utils.AssertEqual(t, routeParser{ + segs: []*routeSegment{ + {Const: "/test", Length: 5}, + {IsParam: true, ParamName: "optional", IsOptional: true, Length: 1}, + {IsParam: true, ParamName: "optional2", IsOptional: true, IsLast: true}, + }, + params: []string{"optional", "optional2"}, + }, rp) + + rp = parseRoute("/config/+.json") + utils.AssertEqual(t, routeParser{ + segs: []*routeSegment{ + {Const: "/config/", Length: 8}, + {IsParam: true, ParamName: "+1", IsGreedy: true, IsOptional: false, ComparePart: ".json", PartCount: 1}, + {Const: ".json", Length: 5, IsLast: true}, + }, + params: []string{"+1"}, + plusCount: 1, + }, rp) + + rp = parseRoute("/api/:day.:month?.:year?") + utils.AssertEqual(t, routeParser{ + segs: []*routeSegment{ + {Const: "/api/", Length: 5}, + {IsParam: true, ParamName: "day", IsOptional: false, ComparePart: ".", PartCount: 2}, + {Const: ".", Length: 1}, + {IsParam: true, ParamName: "month", IsOptional: true, ComparePart: ".", PartCount: 1}, + {Const: ".", Length: 1}, + {IsParam: true, ParamName: "year", IsOptional: true, IsLast: true}, + }, + params: []string{"day", "month", "year"}, + }, rp) + + rp = parseRoute("/*v1*/proxy") + utils.AssertEqual(t, routeParser{ + segs: []*routeSegment{ + {Const: "/", Length: 1, HasOptionalSlash: true}, + {IsParam: true, ParamName: "*1", IsGreedy: true, IsOptional: true, ComparePart: "v1", PartCount: 1}, + {Const: "v1", Length: 2}, + {IsParam: true, ParamName: "*2", IsGreedy: true, IsOptional: true, ComparePart: "/proxy", PartCount: 1}, + {Const: "/proxy", Length: 6, IsLast: true}, + }, + params: []string{"*1", "*2"}, + wildCardCount: 2, + }, rp) +} + +// go test -race -run Test_Path_matchParams +func Test_Path_matchParams(t *testing.T) { + t.Parallel() + var ctxParams [maxParams]string + testCaseFn := func(testCollection routeCaseCollection) { + parser := parseRoute(testCollection.pattern) + for _, c := range testCollection.testCases { + match := parser.getMatch(c.url, c.url, &ctxParams, c.partialCheck) + utils.AssertEqual(t, c.match, match, fmt.Sprintf("route: '%s', url: '%s'", testCollection.pattern, c.url)) + if match && len(c.params) > 0 { + utils.AssertEqual(t, c.params[0:len(c.params)], ctxParams[0:len(c.params)], fmt.Sprintf("route: '%s', url: '%s'", testCollection.pattern, c.url)) + } + } + } + for _, testCaseCollection := range routeTestCases { + testCaseFn(testCaseCollection) + } +} + +// go test -race -run Test_RoutePatternMatch +func Test_RoutePatternMatch(t *testing.T) { + t.Parallel() + testCaseFn := func(pattern string, cases []routeTestCase) { + for _, c := range cases { + // skip all cases for partial checks + if c.partialCheck { + continue + } + match := RoutePatternMatch(c.url, pattern) + utils.AssertEqual(t, c.match, match, fmt.Sprintf("route: '%s', url: '%s'", pattern, c.url)) + } + } + for _, testCase := range routeTestCases { + testCaseFn(testCase.pattern, testCase.testCases) + } +} + +func Test_Utils_GetTrimmedParam(t *testing.T) { + t.Parallel() + res := GetTrimmedParam("") + utils.AssertEqual(t, "", res) + res = GetTrimmedParam("*") + utils.AssertEqual(t, "*", res) + res = GetTrimmedParam(":param") + utils.AssertEqual(t, "param", res) + res = GetTrimmedParam(":param1?") + utils.AssertEqual(t, "param1", res) + res = GetTrimmedParam("noParam") + utils.AssertEqual(t, "noParam", res) +} + +func Test_Utils_RemoveEscapeChar(t *testing.T) { + t.Parallel() + res := RemoveEscapeChar(":test\\:bla") + utils.AssertEqual(t, ":test:bla", res) + res = RemoveEscapeChar("\\abc") + utils.AssertEqual(t, "abc", res) + res = RemoveEscapeChar("noEscapeChar") + utils.AssertEqual(t, "noEscapeChar", res) +} + +func Benchmark_Utils_RemoveEscapeChar(b *testing.B) { + b.ReportAllocs() + b.ResetTimer() + var res string + for n := 0; n < b.N; n++ { + res = RemoveEscapeChar(":test\\:bla") + } + + utils.AssertEqual(b, ":test:bla", res) +} + +// go test -race -run Test_Path_matchParams +func Benchmark_Path_matchParams(t *testing.B) { + var ctxParams [maxParams]string + benchCaseFn := func(testCollection routeCaseCollection) { + parser := parseRoute(testCollection.pattern) + for _, c := range testCollection.testCases { + var matchRes bool + state := "match" + if !c.match { + state = "not match" + } + t.Run(testCollection.pattern+" | "+state+" | "+c.url, func(b *testing.B) { + for i := 0; i <= b.N; i++ { + if match := parser.getMatch(c.url, c.url, &ctxParams, c.partialCheck); match { + // Get testCases from the original path + matchRes = true + } + } + utils.AssertEqual(t, c.match, matchRes, fmt.Sprintf("route: '%s', url: '%s'", testCollection.pattern, c.url)) + if matchRes && len(c.params) > 0 { + utils.AssertEqual(t, c.params[0:len(c.params)-1], ctxParams[0:len(c.params)-1], fmt.Sprintf("route: '%s', url: '%s'", testCollection.pattern, c.url)) + } + }) + } + } + + for _, testCollection := range benchmarkCases { + benchCaseFn(testCollection) + } +} + +// go test -race -run Test_RoutePatternMatch +func Benchmark_RoutePatternMatch(t *testing.B) { + benchCaseFn := func(testCollection routeCaseCollection) { + for _, c := range testCollection.testCases { + // skip all cases for partial checks + if c.partialCheck { + continue + } + var matchRes bool + state := "match" + if !c.match { + state = "not match" + } + t.Run(testCollection.pattern+" | "+state+" | "+c.url, func(b *testing.B) { + for i := 0; i <= b.N; i++ { + if match := RoutePatternMatch(c.url, testCollection.pattern); match { + // Get testCases from the original path + matchRes = true + } + } + utils.AssertEqual(t, c.match, matchRes, fmt.Sprintf("route: '%s', url: '%s'", testCollection.pattern, c.url)) + }) + } + } + + for _, testCollection := range benchmarkCases { + benchCaseFn(testCollection) + } +} diff --git a/path_testcases_test.go b/path_testcases_test.go new file mode 100644 index 0000000..26ec5b7 --- /dev/null +++ b/path_testcases_test.go @@ -0,0 +1,718 @@ +// ⚡️ Fiber is an Express inspired web framework written in Go with ☕️ +// 📝 Github Repository: https://github.com/gofiber/fiber +// 📌 API Documentation: https://docs.gofiber.io + +package fiber + +import ( + "strings" +) + +type routeTestCase struct { + url string + match bool + params []string + partialCheck bool +} + +type routeCaseCollection struct { + pattern string + testCases []routeTestCase +} + +var ( + benchmarkCases []routeCaseCollection + routeTestCases []routeCaseCollection +) + +func init() { + // smaller list for benchmark cases + benchmarkCases = []routeCaseCollection{ + { + pattern: "/api/v1/const", + testCases: []routeTestCase{ + {url: "/api/v1/const", params: []string{}, match: true}, + {url: "/api/v1", params: nil, match: false}, + {url: "/api/v1/", params: nil, match: false}, + {url: "/api/v1/something", params: nil, match: false}, + }, + }, + { + pattern: "/api/:param/fixedEnd", + testCases: []routeTestCase{ + {url: "/api/abc/fixedEnd", params: []string{"abc"}, match: true}, + {url: "/api/abc/def/fixedEnd", params: nil, match: false}, + }, + }, + { + pattern: "/api/v1/:param/*", + testCases: []routeTestCase{ + {url: "/api/v1/entity", params: []string{"entity", ""}, match: true}, + {url: "/api/v1/entity/", params: []string{"entity", ""}, match: true}, + {url: "/api/v1/entity/1", params: []string{"entity", "1"}, match: true}, + {url: "/api/v", params: nil, match: false}, + {url: "/api/v2", params: nil, match: false}, + {url: "/api/v1/", params: nil, match: false}, + }, + }, + } + + // combine benchmark cases and other cases + routeTestCases = benchmarkCases + routeTestCases = append( + routeTestCases, + []routeCaseCollection{ + { + pattern: "/api/v1/:param/+", + testCases: []routeTestCase{ + {url: "/api/v1/entity", params: nil, match: false}, + {url: "/api/v1/entity/", params: nil, match: false}, + {url: "/api/v1/entity/1", params: []string{"entity", "1"}, match: true}, + {url: "/api/v", params: nil, match: false}, + {url: "/api/v2", params: nil, match: false}, + {url: "/api/v1/", params: nil, match: false}, + }, + }, + { + pattern: "/api/v1/:param?", + testCases: []routeTestCase{ + {url: "/api/v1", params: []string{""}, match: true}, + {url: "/api/v1/", params: []string{""}, match: true}, + {url: "/api/v1/optional", params: []string{"optional"}, match: true}, + {url: "/api/v", params: nil, match: false}, + {url: "/api/v2", params: nil, match: false}, + {url: "/api/xyz", params: nil, match: false}, + }, + }, + { + pattern: `/v1/some/resource/name\:customVerb`, + testCases: []routeTestCase{ + {url: "/v1/some/resource/name:customVerb", params: nil, match: true}, + {url: "/v1/some/resource/name:test", params: nil, match: false}, + }, + }, + { + pattern: `/v1/some/resource/:name\:customVerb`, + testCases: []routeTestCase{ + {url: "/v1/some/resource/test:customVerb", params: []string{"test"}, match: true}, + {url: "/v1/some/resource/test:test", params: nil, match: false}, + }, + }, + { + pattern: `/v1/some/resource/name\\:customVerb?\?/:param/*`, + testCases: []routeTestCase{ + {url: "/v1/some/resource/name:customVerb??/test/optionalWildCard/character", params: []string{"test", "optionalWildCard/character"}, match: true}, + {url: "/v1/some/resource/name:customVerb??/test", params: []string{"test", ""}, match: true}, + }, + }, + { + pattern: "/api/v1/*", + testCases: []routeTestCase{ + {url: "/api/v1", params: []string{""}, match: true}, + {url: "/api/v1/", params: []string{""}, match: true}, + {url: "/api/v1/entity", params: []string{"entity"}, match: true}, + {url: "/api/v1/entity/1/2", params: []string{"entity/1/2"}, match: true}, + {url: "/api/v1/Entity/1/2", params: []string{"Entity/1/2"}, match: true}, + {url: "/api/v", params: nil, match: false}, + {url: "/api/v2", params: nil, match: false}, + {url: "/api/abc", params: nil, match: false}, + }, + }, + { + pattern: "/api/v1/:param", + testCases: []routeTestCase{ + {url: "/api/v1/entity", params: []string{"entity"}, match: true}, + {url: "/api/v1/entity/8728382", params: nil, match: false}, + {url: "/api/v1", params: nil, match: false}, + {url: "/api/v1/", params: nil, match: false}, + }, + }, + { + pattern: "/api/v1/:param-:param2", + testCases: []routeTestCase{ + {url: "/api/v1/entity-entity2", params: []string{"entity", "entity2"}, match: true}, + {url: "/api/v1/entity/8728382", params: nil, match: false}, + {url: "/api/v1/entity-8728382", params: []string{"entity", "8728382"}, match: true}, + {url: "/api/v1", params: nil, match: false}, + {url: "/api/v1/", params: nil, match: false}, + }, + }, + { + pattern: "/api/v1/:filename.:extension", + testCases: []routeTestCase{ + {url: "/api/v1/test.pdf", params: []string{"test", "pdf"}, match: true}, + {url: "/api/v1/test/pdf", params: nil, match: false}, + {url: "/api/v1/test-pdf", params: nil, match: false}, + {url: "/api/v1/test_pdf", params: nil, match: false}, + {url: "/api/v1", params: nil, match: false}, + {url: "/api/v1/", params: nil, match: false}, + }, + }, + { + pattern: "/shop/product/::filter/color::color/size::size", + testCases: []routeTestCase{ + {url: "/shop/product/:test/color:blue/size:xs", params: []string{"test", "blue", "xs"}, match: true}, + {url: "/shop/product/test/color:blue/size:xs", params: nil, match: false}, + }, + }, + { + pattern: "/::param?", + testCases: []routeTestCase{ + {url: "/:hello", params: []string{"hello"}, match: true}, + {url: "/:", params: []string{""}, match: true}, + {url: "/", params: nil, match: false}, + }, + }, + // successive parameters, each take one character and the last parameter gets everything + { + pattern: "/test:sign:param", + testCases: []routeTestCase{ + {url: "/test-abc", params: []string{"-", "abc"}, match: true}, + {url: "/test", params: nil, match: false}, + }, + }, + // optional parameters are not greedy + { + pattern: "/:param1:param2?:param3", + testCases: []routeTestCase{ + {url: "/abbbc", params: []string{"a", "b", "bbc"}, match: true}, + // {url: "/ac", testCases: []string{"a", "", "c"}, match: true}, // TODO: fix it + {url: "/test", params: []string{"t", "e", "st"}, match: true}, + }, + }, + { + pattern: "/test:optional?:mandatory", + testCases: []routeTestCase{ + // {url: "/testo", testCases: []string{"", "o"}, match: true}, // TODO: fix it + {url: "/testoaaa", params: []string{"o", "aaa"}, match: true}, + {url: "/test", params: nil, match: false}, + }, + }, + { + pattern: "/test:optional?:optional2?", + testCases: []routeTestCase{ + {url: "/testo", params: []string{"o", ""}, match: true}, + {url: "/testoaaa", params: []string{"o", "aaa"}, match: true}, + {url: "/test", params: []string{"", ""}, match: true}, + {url: "/tes", params: nil, match: false}, + }, + }, + { + pattern: "/foo:param?bar", + testCases: []routeTestCase{ + {url: "/foofaselbar", params: []string{"fasel"}, match: true}, + {url: "/foobar", params: []string{""}, match: true}, + {url: "/fooba", params: nil, match: false}, + {url: "/fobar", params: nil, match: false}, + }, + }, + { + pattern: "/foo*bar", + testCases: []routeTestCase{ + {url: "/foofaselbar", params: []string{"fasel"}, match: true}, + {url: "/foobar", params: []string{""}, match: true}, + {url: "/", params: nil, match: false}, + }, + }, + { + pattern: "/foo+bar", + testCases: []routeTestCase{ + {url: "/foofaselbar", params: []string{"fasel"}, match: true}, + {url: "/foobar", params: nil, match: false}, + {url: "/", params: nil, match: false}, + }, + }, + { + pattern: "/a*cde*g/", + testCases: []routeTestCase{ + {url: "/abbbcdefffg", params: []string{"bbb", "fff"}, match: true}, + {url: "/acdeg", params: []string{"", ""}, match: true}, + {url: "/", params: nil, match: false}, + }, + }, + { + pattern: "/*v1*/proxy", + testCases: []routeTestCase{ + {url: "/customer/v1/cart/proxy", params: []string{"customer/", "/cart"}, match: true}, + {url: "/v1/proxy", params: []string{"", ""}, match: true}, + {url: "/v1/", params: nil, match: false}, + }, + }, + // successive wildcard -> first wildcard is greedy + { + pattern: "/foo***bar", + testCases: []routeTestCase{ + {url: "/foo*abar", params: []string{"*a", "", ""}, match: true}, + {url: "/foo*bar", params: []string{"*", "", ""}, match: true}, + {url: "/foobar", params: []string{"", "", ""}, match: true}, + {url: "/fooba", params: nil, match: false}, + }, + }, + // chars in front of an parameter + { + pattern: "/name::name", + testCases: []routeTestCase{ + {url: "/name:john", params: []string{"john"}, match: true}, + }, + }, + { + pattern: "/@:name", + testCases: []routeTestCase{ + {url: "/@john", params: []string{"john"}, match: true}, + }, + }, + { + pattern: "/-:name", + testCases: []routeTestCase{ + {url: "/-john", params: []string{"john"}, match: true}, + }, + }, + { + pattern: "/.:name", + testCases: []routeTestCase{ + {url: "/.john", params: []string{"john"}, match: true}, + }, + }, + { + pattern: "/api/v1/:param/abc/*", + testCases: []routeTestCase{ + {url: "/api/v1/well/abc/wildcard", params: []string{"well", "wildcard"}, match: true}, + {url: "/api/v1/well/abc/", params: []string{"well", ""}, match: true}, + {url: "/api/v1/well/abc", params: []string{"well", ""}, match: true}, + {url: "/api/v1/well/ttt", params: nil, match: false}, + }, + }, + { + pattern: "/api/:day/:month?/:year?", + testCases: []routeTestCase{ + {url: "/api/1", params: []string{"1", "", ""}, match: true}, + {url: "/api/1/", params: []string{"1", "", ""}, match: true}, + {url: "/api/1//", params: []string{"1", "", ""}, match: true}, + {url: "/api/1/-/", params: []string{"1", "-", ""}, match: true}, + {url: "/api/1-", params: []string{"1-", "", ""}, match: true}, + {url: "/api/1.", params: []string{"1.", "", ""}, match: true}, + {url: "/api/1/2", params: []string{"1", "2", ""}, match: true}, + {url: "/api/1/2/3", params: []string{"1", "2", "3"}, match: true}, + {url: "/api/", params: nil, match: false}, + }, + }, + { + pattern: "/api/:day.:month?.:year?", + testCases: []routeTestCase{ + {url: "/api/1", params: nil, match: false}, + {url: "/api/1/", params: nil, match: false}, + {url: "/api/1.", params: nil, match: false}, + {url: "/api/1..", params: []string{"1", "", ""}, match: true}, + {url: "/api/1.2", params: nil, match: false}, + {url: "/api/1.2.", params: []string{"1", "2", ""}, match: true}, + {url: "/api/1.2.3", params: []string{"1", "2", "3"}, match: true}, + {url: "/api/", params: nil, match: false}, + }, + }, + { + pattern: "/api/:day-:month?-:year?", + testCases: []routeTestCase{ + {url: "/api/1", params: nil, match: false}, + {url: "/api/1/", params: nil, match: false}, + {url: "/api/1-", params: nil, match: false}, + {url: "/api/1--", params: []string{"1", "", ""}, match: true}, + {url: "/api/1-/", params: nil, match: false}, + // {url: "/api/1-/-", testCases: nil, match: false}, // TODO: fix this part + {url: "/api/1-2", params: nil, match: false}, + {url: "/api/1-2-", params: []string{"1", "2", ""}, match: true}, + {url: "/api/1-2-3", params: []string{"1", "2", "3"}, match: true}, + {url: "/api/", params: nil, match: false}, + }, + }, + { + pattern: "/api/*", + testCases: []routeTestCase{ + {url: "/api/", params: []string{""}, match: true}, + {url: "/api/joker", params: []string{"joker"}, match: true}, + {url: "/api", params: []string{""}, match: true}, + {url: "/api/v1/entity", params: []string{"v1/entity"}, match: true}, + {url: "/api2/v1/entity", params: nil, match: false}, + {url: "/api_ignore/v1/entity", params: nil, match: false}, + }, + }, + { + pattern: "/partialCheck/foo/bar/:param", + testCases: []routeTestCase{ + {url: "/partialCheck/foo/bar/test", params: []string{"test"}, match: true, partialCheck: true}, + {url: "/partialCheck/foo/bar/test/test2", params: []string{"test"}, match: true, partialCheck: true}, + {url: "/partialCheck/foo/bar", params: nil, match: false, partialCheck: true}, + {url: "/partiaFoo", params: nil, match: false, partialCheck: true}, + }, + }, + { + pattern: "/", + testCases: []routeTestCase{ + {url: "/api", params: nil, match: false}, + {url: "", params: []string{}, match: true}, + {url: "/", params: []string{}, match: true}, + }, + }, + { + pattern: "/config/abc.json", + testCases: []routeTestCase{ + {url: "/config/abc.json", params: []string{}, match: true}, + {url: "config/abc.json", params: nil, match: false}, + {url: "/config/efg.json", params: nil, match: false}, + {url: "/config", params: nil, match: false}, + }, + }, + { + pattern: "/config/*.json", + testCases: []routeTestCase{ + {url: "/config/abc.json", params: []string{"abc"}, match: true}, + {url: "/config/efg.json", params: []string{"efg"}, match: true}, + {url: "/config/.json", params: []string{""}, match: true}, + {url: "/config/efg.csv", params: nil, match: false}, + {url: "config/abc.json", params: nil, match: false}, + {url: "/config", params: nil, match: false}, + }, + }, + { + pattern: "/config/+.json", + testCases: []routeTestCase{ + {url: "/config/abc.json", params: []string{"abc"}, match: true}, + {url: "/config/.json", params: nil, match: false}, + {url: "/config/efg.json", params: []string{"efg"}, match: true}, + {url: "/config/efg.csv", params: nil, match: false}, + {url: "config/abc.json", params: nil, match: false}, + {url: "/config", params: nil, match: false}, + }, + }, + { + pattern: "/xyz", + testCases: []routeTestCase{ + {url: "xyz", params: nil, match: false}, + {url: "xyz/", params: nil, match: false}, + }, + }, + { + pattern: "/api/*/:param?", + testCases: []routeTestCase{ + {url: "/api/", params: []string{"", ""}, match: true}, + {url: "/api/joker", params: []string{"joker", ""}, match: true}, + {url: "/api/joker/batman", params: []string{"joker", "batman"}, match: true}, + {url: "/api/joker//batman", params: []string{"joker/", "batman"}, match: true}, + {url: "/api/joker/batman/robin", params: []string{"joker/batman", "robin"}, match: true}, + {url: "/api/joker/batman/robin/1", params: []string{"joker/batman/robin", "1"}, match: true}, + {url: "/api/joker/batman/robin/1/", params: []string{"joker/batman/robin/1", ""}, match: true}, + {url: "/api/joker-batman/robin/1", params: []string{"joker-batman/robin", "1"}, match: true}, + {url: "/api/joker-batman-robin/1", params: []string{"joker-batman-robin", "1"}, match: true}, + {url: "/api/joker-batman-robin-1", params: []string{"joker-batman-robin-1", ""}, match: true}, + {url: "/api", params: []string{"", ""}, match: true}, + }, + }, + { + pattern: "/api/*/:param", + testCases: []routeTestCase{ + {url: "/api/test/abc", params: []string{"test", "abc"}, match: true}, + {url: "/api/joker/batman", params: []string{"joker", "batman"}, match: true}, + {url: "/api/joker/batman/robin", params: []string{"joker/batman", "robin"}, match: true}, + {url: "/api/joker/batman/robin/1", params: []string{"joker/batman/robin", "1"}, match: true}, + {url: "/api/joker/batman-robin/1", params: []string{"joker/batman-robin", "1"}, match: true}, + {url: "/api/joker-batman-robin-1", params: nil, match: false}, + {url: "/api", params: nil, match: false}, + }, + }, + { + pattern: "/api/+/:param", + testCases: []routeTestCase{ + {url: "/api/test/abc", params: []string{"test", "abc"}, match: true}, + {url: "/api/joker/batman/robin/1", params: []string{"joker/batman/robin", "1"}, match: true}, + {url: "/api/joker", params: nil, match: false}, + {url: "/api", params: nil, match: false}, + }, + }, + { + pattern: "/api/*/:param/:param2", + testCases: []routeTestCase{ + {url: "/api/test/abc/1", params: []string{"test", "abc", "1"}, match: true}, + {url: "/api/joker/batman", params: nil, match: false}, + {url: "/api/joker/batman-robin/1", params: []string{"joker", "batman-robin", "1"}, match: true}, + {url: "/api/joker-batman-robin-1", params: nil, match: false}, + {url: "/api/test/abc", params: nil, match: false}, + {url: "/api/joker/batman/robin", params: []string{"joker", "batman", "robin"}, match: true}, + {url: "/api/joker/batman/robin/1", params: []string{"joker/batman", "robin", "1"}, match: true}, + {url: "/api/joker/batman/robin/1/2", params: []string{"joker/batman/robin", "1", "2"}, match: true}, + {url: "/api", params: nil, match: false}, + {url: "/api/:test", params: nil, match: false}, + }, + }, + { + pattern: "/api/v1/:param", + testCases: []routeTestCase{ + {url: "/api/v1/entity", params: nil, match: false}, + {url: "/api/v1/8728382", params: []string{"8728382"}, match: true}, + {url: "/api/v1/true", params: nil, match: false}, + }, + }, + { + pattern: "/api/v1/:param", + testCases: []routeTestCase{ + {url: "/api/v1/entity", params: nil, match: false}, + {url: "/api/v1/8728382", params: nil, match: false}, + {url: "/api/v1/true", params: []string{"true"}, match: true}, + }, + }, + { + pattern: "/api/v1/:param", + testCases: []routeTestCase{ + {url: "/api/v1/entity", params: nil, match: false}, + {url: "/api/v1/8728382", params: []string{"8728382"}, match: true}, + {url: "/api/v1/8728382.5", params: []string{"8728382.5"}, match: true}, + {url: "/api/v1/true", params: nil, match: false}, + }, + }, + { + pattern: "/api/v1/:param", + testCases: []routeTestCase{ + {url: "/api/v1/entity", params: []string{"entity"}, match: true}, + {url: "/api/v1/#!?", params: nil, match: false}, + {url: "/api/v1/8728382", params: nil, match: false}, + }, + }, + { + pattern: "/api/v1/:param", + testCases: []routeTestCase{ + {url: "/api/v1/entity", params: nil, match: false}, + {url: "/api/v1/8728382", params: nil, match: false}, + {url: "/api/v1/f0fa66cc-d22e-445b-866d-1d76e776371d", params: []string{"f0fa66cc-d22e-445b-866d-1d76e776371d"}, match: true}, + }, + }, + { + pattern: "/api/v1/:param", + testCases: []routeTestCase{ + {url: "/api/v1/entity", params: nil, match: false}, + {url: "/api/v1/8728382", params: nil, match: false}, + }, + }, + { + pattern: "/api/v1/:param", + testCases: []routeTestCase{ + {url: "/api/v1/entity", params: []string{"entity"}, match: true}, + {url: "/api/v1/ent", params: nil, match: false}, + {url: "/api/v1/8728382", params: []string{"8728382"}, match: true}, + {url: "/api/v1/123", params: nil, match: false}, + {url: "/api/v1/12345", params: []string{"12345"}, match: true}, + }, + }, + { + pattern: "/api/v1/:param", + testCases: []routeTestCase{ + {url: "/api/v1/entity", params: nil, match: false}, + {url: "/api/v1/ent", params: []string{"ent"}, match: true}, + {url: "/api/v1/8728382", params: nil, match: false}, + {url: "/api/v1/123", params: []string{"123"}, match: true}, + {url: "/api/v1/12345", params: []string{"12345"}, match: true}, + }, + }, + { + pattern: "/api/v1/:param", + testCases: []routeTestCase{ + {url: "/api/v1/ent", params: nil, match: false}, + {url: "/api/v1/123", params: nil, match: false}, + {url: "/api/v1/12345", params: []string{"12345"}, match: true}, + }, + }, + { + pattern: "/api/v1/:param", + testCases: []routeTestCase{ + {url: "/api/v1/entity", params: nil, match: false}, + {url: "/api/v1/ent", params: nil, match: false}, + }, + }, + { + pattern: "/api/v1/:param", + testCases: []routeTestCase{ + {url: "/api/v1/e", params: nil, match: false}, + {url: "/api/v1/en", params: []string{"en"}, match: true}, + {url: "/api/v1/8728382", params: nil, match: false}, + {url: "/api/v1/123", params: []string{"123"}, match: true}, + {url: "/api/v1/12345", params: []string{"12345"}, match: true}, + }, + }, + { + pattern: "/api/v1/:param", + testCases: []routeTestCase{ + {url: "/api/v1/e", params: nil, match: false}, + {url: "/api/v1/en", params: []string{"en"}, match: true}, + {url: "/api/v1/8728382", params: nil, match: false}, + {url: "/api/v1/123", params: []string{"123"}, match: true}, + {url: "/api/v1/12345", params: []string{"12345"}, match: true}, + }, + }, + { + pattern: "/api/v1/:param", + testCases: []routeTestCase{ + {url: "/api/v1/ent", params: nil, match: false}, + {url: "/api/v1/1", params: nil, match: false}, + {url: "/api/v1/5", params: []string{"5"}, match: true}, + }, + }, + { + pattern: "/api/v1/:param", + testCases: []routeTestCase{ + {url: "/api/v1/ent", params: nil, match: false}, + {url: "/api/v1/1", params: []string{"1"}, match: true}, + {url: "/api/v1/5", params: []string{"5"}, match: true}, + {url: "/api/v1/15", params: nil, match: false}, + }, + }, + { + pattern: "/api/v1/:param", + testCases: []routeTestCase{ + {url: "/api/v1/ent", params: nil, match: false}, + {url: "/api/v1/9", params: []string{"9"}, match: true}, + {url: "/api/v1/5", params: []string{"5"}, match: true}, + {url: "/api/v1/15", params: nil, match: false}, + }, + }, + { + pattern: `/api/v1/:param`, + testCases: []routeTestCase{ + {url: "/api/v1/entity", params: nil, match: false}, + {url: "/api/v1/8728382", params: nil, match: false}, + {url: "/api/v1/2005-11-01", params: []string{"2005-11-01"}, match: true}, + }, + }, + { + pattern: "/api/v1/:param", + testCases: []routeTestCase{ + {url: "/api/v1/ent", params: nil, match: false}, + {url: "/api/v1/15", params: nil, match: false}, + {url: "/api/v1/peach", params: []string{"peach"}, match: true}, + {url: "/api/v1/p34ch", params: nil, match: false}, + }, + }, + { + pattern: "/api/v1/:param", + testCases: []routeTestCase{ + {url: "/api/v1/12", params: nil, match: false}, + {url: "/api/v1/xy", params: nil, match: false}, + {url: "/api/v1/test", params: []string{"test"}, match: true}, + {url: "/api/v1/" + strings.Repeat("a", 64), params: nil, match: false}, + }, + }, + { + pattern: `/api/v1/:param`, + testCases: []routeTestCase{ + {url: "/api/v1/ent", params: nil, match: false}, + {url: "/api/v1/15", params: nil, match: false}, + {url: "/api/v1/2022-08-27", params: []string{"2022-08-27"}, match: true}, + {url: "/api/v1/2022/08-27", params: nil, match: false}, + }, + }, + { + pattern: "/api/v1/:param", + testCases: []routeTestCase{ + {url: "/api/v1/entity", params: nil, match: false}, + {url: "/api/v1/8728382", params: []string{"8728382"}, match: true}, + {url: "/api/v1/true", params: nil, match: false}, + }, + }, + { + pattern: "/api/v1/:param", + testCases: []routeTestCase{ + {url: "/api/v1/entity", params: nil, match: false}, + {url: "/api/v1/8728382", params: nil, match: false}, + {url: "/api/v1/123", params: []string{"123"}, match: true}, + {url: "/api/v1/true", params: nil, match: false}, + }, + }, + { + pattern: "/api/v1/:param", + testCases: []routeTestCase{ + {url: "/api/v1/entity", params: nil, match: false}, + {url: "/api/v1/87283827683", params: nil, match: false}, + {url: "/api/v1/123", params: []string{"123"}, match: true}, + {url: "/api/v1/true", params: nil, match: false}, + }, + }, + { + pattern: "/api/v1/:param", + testCases: []routeTestCase{ + {url: "/api/v1/entity", params: nil, match: false}, + {url: "/api/v1/87283827683", params: nil, match: false}, + {url: "/api/v1/25", params: []string{"25"}, match: true}, + {url: "/api/v1/true", params: nil, match: false}, + }, + }, + { + pattern: `/api/v1/:param`, + testCases: []routeTestCase{ + {url: "/api/v1/entity", params: []string{"entity"}, match: true}, + {url: "/api/v1/87283827683", params: []string{"87283827683"}, match: true}, + {url: "/api/v1/25", params: []string{"25"}, match: true}, + {url: "/api/v1/true", params: []string{"true"}, match: true}, + }, + }, + { + pattern: `/api/v1/:param`, + testCases: []routeTestCase{ + {url: "/api/v1/entity", params: nil, match: false}, + {url: "/api/v1/87283827683", params: nil, match: false}, + {url: "/api/v1/25", params: []string{"25"}, match: true}, + {url: "/api/v1/1200", params: []string{"1200"}, match: true}, + {url: "/api/v1/true", params: nil, match: false}, + }, + }, + { + pattern: "/api/v1/:lang/videos/:page", + testCases: []routeTestCase{ + {url: "/api/v1/try/videos/200", params: nil, match: false}, + {url: "/api/v1/tr/videos/1800", params: nil, match: false}, + {url: "/api/v1/tr/videos/100", params: []string{"tr", "100"}, match: true}, + {url: "/api/v1/e/videos/10", params: nil, match: false}, + }, + }, + { + pattern: "/api/v1/:lang/:page", + testCases: []routeTestCase{ + {url: "/api/v1/try/200", params: nil, match: false}, + {url: "/api/v1/tr/1800", params: nil, match: false}, + {url: "/api/v1/tr/100", params: []string{"tr", "100"}, match: true}, + {url: "/api/v1/e/10", params: nil, match: false}, + }, + }, + { + pattern: "/api/v1/:lang/:page", + testCases: []routeTestCase{ + {url: "/api/v1/try/200", params: []string{"try", "200"}, match: true}, + {url: "/api/v1/tr/1800", params: nil, match: false}, + {url: "/api/v1/tr/100", params: []string{"tr", "100"}, match: true}, + {url: "/api/v1/e/10", params: nil, match: false}, + }, + }, + { + pattern: "/api/v1/:lang/:page", + testCases: []routeTestCase{ + {url: "/api/v1/try/200", params: nil, match: false}, + {url: "/api/v1/tr/1800", params: []string{"tr", "1800"}, match: true}, + {url: "/api/v1/tr/100", params: []string{"tr", "100"}, match: true}, + {url: "/api/v1/e/10", params: nil, match: false}, + }, + }, + { + pattern: `/api/v1/:date/:regex`, + testCases: []routeTestCase{ + {url: "/api/v1/2005-11-01/a", params: nil, match: false}, + {url: "/api/v1/2005-1101/paach", params: nil, match: false}, + {url: "/api/v1/2005-11-01/peach", params: []string{"2005-11-01", "peach"}, match: true}, + }, + }, + { + pattern: "/api/v1/:param?", + testCases: []routeTestCase{ + {url: "/api/v1/entity", params: nil, match: false}, + {url: "/api/v1/8728382", params: []string{"8728382"}, match: true}, + {url: "/api/v1/true", params: nil, match: false}, + {url: "/api/v1/", params: []string{""}, match: true}, + }, + }, + }..., + ) +} diff --git a/prefork.go b/prefork.go new file mode 100644 index 0000000..3f64012 --- /dev/null +++ b/prefork.go @@ -0,0 +1,179 @@ +package fiber + +import ( + "crypto/tls" + "errors" + "fmt" + "os" + "os/exec" + "runtime" + "strconv" + "strings" + "sync/atomic" + "time" + + "github.com/valyala/fasthttp/reuseport" + + "github.com/gofiber/fiber/v2/log" +) + +const ( + envPreforkChildKey = "FIBER_PREFORK_CHILD" + envPreforkChildVal = "1" +) + +var ( + testPreforkMaster = false + testOnPrefork = false +) + +// IsChild determines if the current process is a child of Prefork +func IsChild() bool { + return os.Getenv(envPreforkChildKey) == envPreforkChildVal +} + +// prefork manages child processes to make use of the OS REUSEPORT or REUSEADDR feature +func (app *App) prefork(network, addr string, tlsConfig *tls.Config) error { + // 👶 child process 👶 + if IsChild() { + // use 1 cpu core per child process + runtime.GOMAXPROCS(1) + // Linux will use SO_REUSEPORT and Windows falls back to SO_REUSEADDR + // Only tcp4 or tcp6 is supported when preforking, both are not supported + ln, err := reuseport.Listen(network, addr) + if err != nil { + if !app.config.DisableStartupMessage { + const sleepDuration = 100 * time.Millisecond + time.Sleep(sleepDuration) // avoid colliding with startup message + } + return fmt.Errorf("prefork: %w", err) + } + // wrap a tls config around the listener if provided + if tlsConfig != nil { + ln = tls.NewListener(ln, tlsConfig) + } + + // kill current child proc when master exits + go watchMaster() + + // prepare the server for the start + app.startupProcess() + + // listen for incoming connections + return app.server.Serve(ln) + } + + // 👮 master process 👮 + type child struct { + pid int + err error + } + // create variables + max := runtime.GOMAXPROCS(0) + childs := make(map[int]*exec.Cmd) + channel := make(chan child, max) + + // kill child procs when master exits + defer func() { + for _, proc := range childs { + if err := proc.Process.Kill(); err != nil { + if !errors.Is(err, os.ErrProcessDone) { + log.Errorf("prefork: failed to kill child: %v", err) + } + } + } + }() + + // collect child pids + var pids []string + + // launch child procs + for i := 0; i < max; i++ { + cmd := exec.Command(os.Args[0], os.Args[1:]...) //nolint:gosec // It's fine to launch the same process again + if testPreforkMaster { + // When test prefork master, + // just start the child process with a dummy cmd, + // which will exit soon + cmd = dummyCmd() + } + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + // add fiber prefork child flag into child proc env + cmd.Env = append(os.Environ(), + fmt.Sprintf("%s=%s", envPreforkChildKey, envPreforkChildVal), + ) + if err := cmd.Start(); err != nil { + return fmt.Errorf("failed to start a child prefork process, error: %w", err) + } + + // store child process + pid := cmd.Process.Pid + childs[pid] = cmd + pids = append(pids, strconv.Itoa(pid)) + + // execute fork hook + if app.hooks != nil { + if testOnPrefork { + app.hooks.executeOnForkHooks(dummyPid) + } else { + app.hooks.executeOnForkHooks(pid) + } + } + + // notify master if child crashes + go func() { + channel <- child{pid, cmd.Wait()} + }() + } + + // Run onListen hooks + // Hooks have to be run here as different as non-prefork mode due to they should run as child or master + app.runOnListenHooks(app.prepareListenData(addr, tlsConfig != nil)) + + // Print startup message + if !app.config.DisableStartupMessage { + app.startupMessage(addr, tlsConfig != nil, ","+strings.Join(pids, ",")) + } + + // return error if child crashes + return (<-channel).err +} + +// watchMaster watches child procs +func watchMaster() { + if runtime.GOOS == "windows" { + // finds parent process, + // and waits for it to exit + p, err := os.FindProcess(os.Getppid()) + if err == nil { + _, _ = p.Wait() //nolint:errcheck // It is fine to ignore the error here + } + os.Exit(1) //nolint:revive // Calling os.Exit is fine here in the prefork + } + // if it is equal to 1 (init process ID), + // it indicates that the master process has exited + const watchInterval = 500 * time.Millisecond + for range time.NewTicker(watchInterval).C { + if os.Getppid() == 1 { + os.Exit(1) //nolint:revive // Calling os.Exit is fine here in the prefork + } + } +} + +var ( + dummyPid = 1 + dummyChildCmd atomic.Value +) + +// dummyCmd is for internal prefork testing +func dummyCmd() *exec.Cmd { + command := "go" + if storeCommand := dummyChildCmd.Load(); storeCommand != nil && storeCommand != "" { + command = storeCommand.(string) //nolint:forcetypeassert,errcheck // We always store a string in here + } + if runtime.GOOS == "windows" { + return exec.Command("cmd", "/C", command, "version") + } + return exec.Command(command, "version") +} diff --git a/prefork_test.go b/prefork_test.go new file mode 100644 index 0000000..ef24d88 --- /dev/null +++ b/prefork_test.go @@ -0,0 +1,104 @@ +// ⚡️ Fiber is an Express inspired web framework written in Go with ☕️ +// 📄 Github Repository: https://github.com/gofiber/fiber +// 📌 API Documentation: https://docs.gofiber.io +// 💖 Maintained and modified for Fiber by @renewerner87 +package fiber + +import ( + "crypto/tls" + "io" + "os" + "testing" + "time" + + "github.com/gofiber/fiber/v2/utils" +) + +func Test_App_Prefork_Child_Process(t *testing.T) { + // Reset test var + testPreforkMaster = true + + setupIsChild(t) + defer teardownIsChild(t) + + app := New() + + err := app.prefork(NetworkTCP4, "invalid", nil) + utils.AssertEqual(t, false, err == nil) + + go func() { + time.Sleep(1000 * time.Millisecond) + utils.AssertEqual(t, nil, app.Shutdown()) + }() + + utils.AssertEqual(t, nil, app.prefork(NetworkTCP6, "[::1]:", nil)) + + // Create tls certificate + cer, err := tls.LoadX509KeyPair("./.github/testdata/ssl.pem", "./.github/testdata/ssl.key") + if err != nil { + utils.AssertEqual(t, nil, err) + } + //nolint:gosec // We're in a test so using old ciphers is fine + config := &tls.Config{Certificates: []tls.Certificate{cer}} + + go func() { + time.Sleep(1000 * time.Millisecond) + utils.AssertEqual(t, nil, app.Shutdown()) + }() + + utils.AssertEqual(t, nil, app.prefork(NetworkTCP4, "127.0.0.1:", config)) +} + +func Test_App_Prefork_Master_Process(t *testing.T) { + // Reset test var + testPreforkMaster = true + + app := New() + + go func() { + time.Sleep(1000 * time.Millisecond) + utils.AssertEqual(t, nil, app.Shutdown()) + }() + + utils.AssertEqual(t, nil, app.prefork(NetworkTCP4, ":3000", nil)) + + dummyChildCmd.Store("invalid") + + err := app.prefork(NetworkTCP4, "127.0.0.1:", nil) + utils.AssertEqual(t, false, err == nil) + + dummyChildCmd.Store("") +} + +func Test_App_Prefork_Child_Process_Never_Show_Startup_Message(t *testing.T) { + setupIsChild(t) + defer teardownIsChild(t) + + rescueStdout := os.Stdout + defer func() { os.Stdout = rescueStdout }() + + r, w, err := os.Pipe() + utils.AssertEqual(t, nil, err) + + os.Stdout = w + + New().startupProcess().startupMessage(":3000", false, "") + + utils.AssertEqual(t, nil, w.Close()) + + out, err := io.ReadAll(r) + utils.AssertEqual(t, nil, err) + utils.AssertEqual(t, 0, len(out)) +} + +func setupIsChild(t *testing.T) { + t.Helper() + + utils.AssertEqual(t, nil, os.Setenv(envPreforkChildKey, envPreforkChildVal)) +} + +func teardownIsChild(t *testing.T) { + t.Helper() + + utils.AssertEqual(t, nil, os.Setenv(envPreforkChildKey, "")) +} diff --git a/router.go b/router.go new file mode 100644 index 0000000..4afa741 --- /dev/null +++ b/router.go @@ -0,0 +1,518 @@ +// ⚡️ Fiber is an Express inspired web framework written in Go with ☕️ +// 🤖 Github Repository: https://github.com/gofiber/fiber +// 📌 API Documentation: https://docs.gofiber.io + +package fiber + +import ( + "fmt" + "html" + "sort" + "strconv" + "strings" + "sync/atomic" + "time" + + "github.com/gofiber/fiber/v2/utils" + + "github.com/valyala/fasthttp" +) + +// Router defines all router handle interface, including app and group router. +type Router interface { + Use(args ...interface{}) Router + + Get(path string, handlers ...Handler) Router + Head(path string, handlers ...Handler) Router + Post(path string, handlers ...Handler) Router + Put(path string, handlers ...Handler) Router + Delete(path string, handlers ...Handler) Router + Connect(path string, handlers ...Handler) Router + Options(path string, handlers ...Handler) Router + Trace(path string, handlers ...Handler) Router + Patch(path string, handlers ...Handler) Router + + Add(method, path string, handlers ...Handler) Router + Static(prefix, root string, config ...Static) Router + All(path string, handlers ...Handler) Router + + Group(prefix string, handlers ...Handler) Router + + Route(prefix string, fn func(router Router), name ...string) Router + + Mount(prefix string, fiber *App) Router + + Name(name string) Router +} + +// Route is a struct that holds all metadata for each registered handler. +type Route struct { + // ### important: always keep in sync with the copy method "app.copyRoute" ### + // Data for routing + pos uint32 // Position in stack -> important for the sort of the matched routes + use bool // USE matches path prefixes + mount bool // Indicated a mounted app on a specific route + star bool // Path equals '*' + root bool // Path equals '/' + path string // Prettified path + routeParser routeParser // Parameter parser + group *Group // Group instance. used for routes in groups + + // Public fields + Method string `json:"method"` // HTTP method + Name string `json:"name"` // Route's name + //nolint:revive // Having both a Path (uppercase) and a path (lowercase) is fine + Path string `json:"path"` // Original registered route path + Params []string `json:"params"` // Case sensitive param keys + Handlers []Handler `json:"-"` // Ctx handlers +} + +func (r *Route) match(detectionPath, path string, params *[maxParams]string) bool { + // root detectionPath check + if r.root && detectionPath == "/" { + return true + // '*' wildcard matches any detectionPath + } else if r.star { + if len(path) > 1 { + params[0] = path[1:] + } else { + params[0] = "" + } + return true + } + // Does this route have parameters + if len(r.Params) > 0 { + // Match params + if match := r.routeParser.getMatch(detectionPath, path, params, r.use); match { + // Get params from the path detectionPath + return match + } + } + // Is this route a Middleware? + if r.use { + // Single slash will match or detectionPath prefix + if r.root || strings.HasPrefix(detectionPath, r.path) { + return true + } + // Check for a simple detectionPath match + } else if len(r.path) == len(detectionPath) && r.path == detectionPath { + return true + } + // No match + return false +} + +func (app *App) next(c *Ctx) (bool, error) { + // Get stack length + tree, ok := app.treeStack[c.methodINT][c.treePath] + if !ok { + tree = app.treeStack[c.methodINT][""] + } + lenTree := len(tree) - 1 + + // Loop over the route stack starting from previous index + for c.indexRoute < lenTree { + // Increment route index + c.indexRoute++ + + // Get *Route + route := tree[c.indexRoute] + + var match bool + var err error + // skip for mounted apps + if route.mount { + continue + } + + // Check if it matches the request path + match = route.match(c.detectionPath, c.path, &c.values) + if !match { + // No match, next route + continue + } + // Pass route reference and param values + c.route = route + + // Non use handler matched + if !c.matched && !route.use { + c.matched = true + } + + // Execute first handler of route + c.indexHandler = 0 + if len(route.Handlers) > 0 { + err = route.Handlers[0](c) + } + return match, err // Stop scanning the stack + } + + // If c.Next() does not match, return 404 + err := NewError(StatusNotFound, "Cannot "+c.method+" "+html.EscapeString(c.pathOriginal)) + if !c.matched && app.methodExist(c) { + // If no match, scan stack again if other methods match the request + // Moved from app.handler because middleware may break the route chain + err = ErrMethodNotAllowed + } + return false, err +} + +func (app *App) handler(rctx *fasthttp.RequestCtx) { //revive:disable-line:confusing-naming // Having both a Handler() (uppercase) and a handler() (lowercase) is fine. TODO: Use nolint:revive directive instead. See https://github.com/golangci/golangci-lint/issues/3476 + // Acquire Ctx with fasthttp request from pool + c := app.AcquireCtx(rctx) + defer app.ReleaseCtx(c) + + // handle invalid http method directly + if c.methodINT == -1 { + _ = c.Status(StatusBadRequest).SendString("Invalid http method") //nolint:errcheck // It is fine to ignore the error here + return + } + + // Find match in stack + match, err := app.next(c) + if err != nil { + if catch := c.app.ErrorHandler(c, err); catch != nil { + _ = c.SendStatus(StatusInternalServerError) //nolint:errcheck // It is fine to ignore the error here + } + // TODO: Do we need to return here? + } + // Generate ETag if enabled + if match && app.config.ETag { + setETag(c, false) + } +} + +func (app *App) addPrefixToRoute(prefix string, route *Route) *Route { + prefixedPath := getGroupPath(prefix, route.Path) + prettyPath := prefixedPath + // Case-sensitive routing, all to lowercase + if !app.config.CaseSensitive { + prettyPath = utils.ToLower(prettyPath) + } + // Strict routing, remove trailing slashes + if !app.config.StrictRouting && len(prettyPath) > 1 { + prettyPath = utils.TrimRight(prettyPath, '/') + } + + route.Path = prefixedPath + route.path = RemoveEscapeChar(prettyPath) + route.routeParser = parseRoute(prettyPath) + route.root = false + route.star = false + + return route +} + +func (*App) copyRoute(route *Route) *Route { + return &Route{ + // Router booleans + use: route.use, + mount: route.mount, + star: route.star, + root: route.root, + + // Path data + path: route.path, + routeParser: route.routeParser, + + // misc + pos: route.pos, + + // Public data + Path: route.Path, + Params: route.Params, + Name: route.Name, + Method: route.Method, + Handlers: route.Handlers, + } +} + +func (app *App) register(method, pathRaw string, group *Group, handlers ...Handler) { + // Uppercase HTTP methods + method = utils.ToUpper(method) + // Check if the HTTP method is valid unless it's USE + if method != methodUse && app.methodInt(method) == -1 { + panic(fmt.Sprintf("add: invalid http method %s\n", method)) + } + // is mounted app + isMount := group != nil && group.app != app + // A route requires atleast one ctx handler + if len(handlers) == 0 && !isMount { + panic(fmt.Sprintf("missing handler in route: %s\n", pathRaw)) + } + // Cannot have an empty path + if pathRaw == "" { + pathRaw = "/" + } + // Path always start with a '/' + if pathRaw[0] != '/' { + pathRaw = "/" + pathRaw + } + // Create a stripped path in-case sensitive / trailing slashes + pathPretty := pathRaw + // Case-sensitive routing, all to lowercase + if !app.config.CaseSensitive { + pathPretty = utils.ToLower(pathPretty) + } + // Strict routing, remove trailing slashes + if !app.config.StrictRouting && len(pathPretty) > 1 { + pathPretty = utils.TrimRight(pathPretty, '/') + } + // Is layer a middleware? + isUse := method == methodUse + // Is path a direct wildcard? + isStar := pathPretty == "/*" + // Is path a root slash? + isRoot := pathPretty == "/" + // Parse path parameters + parsedRaw := parseRoute(pathRaw) + parsedPretty := parseRoute(pathPretty) + + // Create route metadata without pointer + route := Route{ + // Router booleans + use: isUse, + mount: isMount, + star: isStar, + root: isRoot, + + // Path data + path: RemoveEscapeChar(pathPretty), + routeParser: parsedPretty, + Params: parsedRaw.params, + + // Group data + group: group, + + // Public data + Path: pathRaw, + Method: method, + Handlers: handlers, + } + // Increment global handler count + atomic.AddUint32(&app.handlersCount, uint32(len(handlers))) + + // Middleware route matches all HTTP methods + if isUse { + // Add route to all HTTP methods stack + for _, m := range app.config.RequestMethods { + // Create a route copy to avoid duplicates during compression + r := route + app.addRoute(m, &r, isMount) + } + } else { + // Add route to stack + app.addRoute(method, &route, isMount) + } +} + +func (app *App) registerStatic(prefix, root string, config ...Static) { + // For security, we want to restrict to the current work directory. + if root == "" { + root = "." + } + // Cannot have an empty prefix + if prefix == "" { + prefix = "/" + } + // Prefix always start with a '/' or '*' + if prefix[0] != '/' { + prefix = "/" + prefix + } + // in case-sensitive routing, all to lowercase + if !app.config.CaseSensitive { + prefix = utils.ToLower(prefix) + } + // Strip trailing slashes from the root path + if len(root) > 0 && root[len(root)-1] == '/' { + root = root[:len(root)-1] + } + // Is prefix a direct wildcard? + isStar := prefix == "/*" + // Is prefix a root slash? + isRoot := prefix == "/" + // Is prefix a partial wildcard? + if strings.Contains(prefix, "*") { + // /john* -> /john + isStar = true + prefix = strings.Split(prefix, "*")[0] + // Fix this later + } + prefixLen := len(prefix) + if prefixLen > 1 && prefix[prefixLen-1:] == "/" { + // /john/ -> /john + prefixLen-- + prefix = prefix[:prefixLen] + } + const cacheDuration = 10 * time.Second + // Fileserver settings + fs := &fasthttp.FS{ + Root: root, + AllowEmptyRoot: true, + GenerateIndexPages: false, + AcceptByteRange: false, + Compress: false, + CompressedFileSuffix: app.config.CompressedFileSuffix, + CacheDuration: cacheDuration, + IndexNames: []string{"index.html"}, + PathRewrite: func(fctx *fasthttp.RequestCtx) []byte { + path := fctx.Path() + if len(path) >= prefixLen { + if isStar && app.getString(path[0:prefixLen]) == prefix { + path = append(path[0:0], '/') + } else { + path = path[prefixLen:] + if len(path) == 0 || path[len(path)-1] != '/' { + path = append(path, '/') + } + } + } + if len(path) > 0 && path[0] != '/' { + path = append([]byte("/"), path...) + } + return path + }, + PathNotFound: func(fctx *fasthttp.RequestCtx) { + fctx.Response.SetStatusCode(StatusNotFound) + }, + } + + // Set config if provided + var cacheControlValue string + var modifyResponse Handler + if len(config) > 0 { + maxAge := config[0].MaxAge + if maxAge > 0 { + cacheControlValue = "public, max-age=" + strconv.Itoa(maxAge) + } + fs.CacheDuration = config[0].CacheDuration + fs.Compress = config[0].Compress + fs.AcceptByteRange = config[0].ByteRange + fs.GenerateIndexPages = config[0].Browse + if config[0].Index != "" { + fs.IndexNames = []string{config[0].Index} + } + modifyResponse = config[0].ModifyResponse + } + fileHandler := fs.NewRequestHandler() + handler := func(c *Ctx) error { + // Don't execute middleware if Next returns true + if len(config) != 0 && config[0].Next != nil && config[0].Next(c) { + return c.Next() + } + // Serve file + fileHandler(c.fasthttp) + // Sets the response Content-Disposition header to attachment if the Download option is true + if len(config) > 0 && config[0].Download { + c.Attachment() + } + // Return request if found and not forbidden + status := c.fasthttp.Response.StatusCode() + if status != StatusNotFound && status != StatusForbidden { + if len(cacheControlValue) > 0 { + c.fasthttp.Response.Header.Set(HeaderCacheControl, cacheControlValue) + } + if modifyResponse != nil { + return modifyResponse(c) + } + return nil + } + // Reset response to default + c.fasthttp.SetContentType("") // Issue #420 + c.fasthttp.Response.SetStatusCode(StatusOK) + c.fasthttp.Response.SetBodyString("") + // Next middleware + return c.Next() + } + + // Create route metadata without pointer + route := Route{ + // Router booleans + use: true, + root: isRoot, + path: prefix, + // Public data + Method: MethodGet, + Path: prefix, + Handlers: []Handler{handler}, + } + // Increment global handler count + atomic.AddUint32(&app.handlersCount, 1) + // Add route to stack + app.addRoute(MethodGet, &route) + // Add HEAD route + app.addRoute(MethodHead, &route) +} + +func (app *App) addRoute(method string, route *Route, isMounted ...bool) { + // Check mounted routes + var mounted bool + if len(isMounted) > 0 { + mounted = isMounted[0] + } + + // Get unique HTTP method identifier + m := app.methodInt(method) + + // prevent identically route registration + l := len(app.stack[m]) + if l > 0 && app.stack[m][l-1].Path == route.Path && route.use == app.stack[m][l-1].use && !route.mount && !app.stack[m][l-1].mount { + preRoute := app.stack[m][l-1] + preRoute.Handlers = append(preRoute.Handlers, route.Handlers...) + } else { + // Increment global route position + route.pos = atomic.AddUint32(&app.routesCount, 1) + route.Method = method + // Add route to the stack + app.stack[m] = append(app.stack[m], route) + app.routesRefreshed = true + } + + // Execute onRoute hooks & change latestRoute if not adding mounted route + if !mounted { + app.mutex.Lock() + app.latestRoute = route + if err := app.hooks.executeOnRouteHooks(*route); err != nil { + panic(err) + } + app.mutex.Unlock() + } +} + +// buildTree build the prefix tree from the previously registered routes +func (app *App) buildTree() *App { + if !app.routesRefreshed { + return app + } + + // loop all the methods and stacks and create the prefix tree + for m := range app.config.RequestMethods { + tsMap := make(map[string][]*Route) + for _, route := range app.stack[m] { + treePath := "" + if len(route.routeParser.segs) > 0 && len(route.routeParser.segs[0].Const) >= 3 { + treePath = route.routeParser.segs[0].Const[:3] + } + // create tree stack + tsMap[treePath] = append(tsMap[treePath], route) + } + app.treeStack[m] = tsMap + } + + // loop the methods and tree stacks and add global stack and sort everything + for m := range app.config.RequestMethods { + tsMap := app.treeStack[m] + for treePart := range tsMap { + if treePart != "" { + // merge global tree routes in current tree stack + tsMap[treePart] = uniqueRouteStack(append(tsMap[treePart], tsMap[""]...)) + } + // sort tree slices with the positions + slc := tsMap[treePart] + sort.Slice(slc, func(i, j int) bool { return slc[i].pos < slc[j].pos }) + } + } + app.routesRefreshed = false + + return app +} diff --git a/router_test.go b/router_test.go new file mode 100644 index 0000000..8d1e40c --- /dev/null +++ b/router_test.go @@ -0,0 +1,914 @@ +// ⚡️ Fiber is an Express inspired web framework written in Go with ☕️ +// 📃 Github Repository: https://github.com/gofiber/fiber +// 📌 API Documentation: https://docs.gofiber.io + +//nolint:bodyclose // Much easier to just ignore memory leaks in tests +package fiber + +import ( + "encoding/json" + "errors" + "fmt" + "io" + "net/http/httptest" + "os" + "strings" + "testing" + + "github.com/gofiber/fiber/v2/utils" + + "github.com/valyala/fasthttp" +) + +var routesFixture routeJSON + +func init() { + dat, err := os.ReadFile("./.github/testdata/testRoutes.json") + if err != nil { + panic(err) + } + if err := json.Unmarshal(dat, &routesFixture); err != nil { + panic(err) + } +} + +func Test_Route_Match_SameLength(t *testing.T) { + t.Parallel() + + app := New() + + app.Get("/:param", func(c *Ctx) error { + return c.SendString(c.Params("param")) + }) + + resp, err := app.Test(httptest.NewRequest(MethodGet, "/:param", nil)) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, 200, resp.StatusCode, "Status code") + + body, err := io.ReadAll(resp.Body) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, ":param", app.getString(body)) + + // with param + resp, err = app.Test(httptest.NewRequest(MethodGet, "/test", nil)) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, 200, resp.StatusCode, "Status code") + + body, err = io.ReadAll(resp.Body) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, "test", app.getString(body)) +} + +func Test_Route_Match_Star(t *testing.T) { + t.Parallel() + + app := New() + + app.Get("/*", func(c *Ctx) error { + return c.SendString(c.Params("*")) + }) + + resp, err := app.Test(httptest.NewRequest(MethodGet, "/*", nil)) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, 200, resp.StatusCode, "Status code") + + body, err := io.ReadAll(resp.Body) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, "*", app.getString(body)) + + // with param + resp, err = app.Test(httptest.NewRequest(MethodGet, "/test", nil)) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, 200, resp.StatusCode, "Status code") + + body, err = io.ReadAll(resp.Body) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, "test", app.getString(body)) + + // without parameter + route := Route{ + star: true, + path: "/*", + routeParser: routeParser{}, + } + params := [maxParams]string{} + match := route.match("", "", ¶ms) + utils.AssertEqual(t, true, match) + utils.AssertEqual(t, [maxParams]string{}, params) + + // with parameter + match = route.match("/favicon.ico", "/favicon.ico", ¶ms) + utils.AssertEqual(t, true, match) + utils.AssertEqual(t, [maxParams]string{"favicon.ico"}, params) + + // without parameter again + match = route.match("", "", ¶ms) + utils.AssertEqual(t, true, match) + utils.AssertEqual(t, [maxParams]string{}, params) +} + +func Test_Route_Match_Root(t *testing.T) { + t.Parallel() + + app := New() + + app.Get("/", func(c *Ctx) error { + return c.SendString("root") + }) + + resp, err := app.Test(httptest.NewRequest(MethodGet, "/", nil)) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, 200, resp.StatusCode, "Status code") + + body, err := io.ReadAll(resp.Body) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, "root", app.getString(body)) +} + +func Test_Route_Match_Parser(t *testing.T) { + t.Parallel() + + app := New() + + app.Get("/foo/:ParamName", func(c *Ctx) error { + return c.SendString(c.Params("ParamName")) + }) + app.Get("/Foobar/*", func(c *Ctx) error { + return c.SendString(c.Params("*")) + }) + resp, err := app.Test(httptest.NewRequest(MethodGet, "/foo/bar", nil)) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, 200, resp.StatusCode, "Status code") + + body, err := io.ReadAll(resp.Body) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, "bar", app.getString(body)) + + // with star + resp, err = app.Test(httptest.NewRequest(MethodGet, "/Foobar/test", nil)) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, 200, resp.StatusCode, "Status code") + + body, err = io.ReadAll(resp.Body) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, "test", app.getString(body)) +} + +func Test_Route_Match_Middleware(t *testing.T) { + t.Parallel() + + app := New() + + app.Use("/foo/*", func(c *Ctx) error { + return c.SendString(c.Params("*")) + }) + + resp, err := app.Test(httptest.NewRequest(MethodGet, "/foo/*", nil)) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, 200, resp.StatusCode, "Status code") + + body, err := io.ReadAll(resp.Body) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, "*", app.getString(body)) + + // with param + resp, err = app.Test(httptest.NewRequest(MethodGet, "/foo/bar/fasel", nil)) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, 200, resp.StatusCode, "Status code") + + body, err = io.ReadAll(resp.Body) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, "bar/fasel", app.getString(body)) +} + +func Test_Route_Match_UnescapedPath(t *testing.T) { + t.Parallel() + + app := New(Config{UnescapePath: true}) + + app.Use("/créer", func(c *Ctx) error { + return c.SendString("test") + }) + + resp, err := app.Test(httptest.NewRequest(MethodGet, "/cr%C3%A9er", nil)) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, StatusOK, resp.StatusCode, "Status code") + + body, err := io.ReadAll(resp.Body) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, "test", app.getString(body)) + // without special chars + resp, err = app.Test(httptest.NewRequest(MethodGet, "/créer", nil)) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, StatusOK, resp.StatusCode, "Status code") + + // check deactivated behavior + app.config.UnescapePath = false + resp, err = app.Test(httptest.NewRequest(MethodGet, "/cr%C3%A9er", nil)) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, StatusNotFound, resp.StatusCode, "Status code") +} + +func Test_Route_Match_WithEscapeChar(t *testing.T) { + t.Parallel() + + app := New() + // static route and escaped part + app.Get("/v1/some/resource/name\\:customVerb", func(c *Ctx) error { + return c.SendString("static") + }) + // group route + group := app.Group("/v2/\\:firstVerb") + group.Get("/\\:customVerb", func(c *Ctx) error { + return c.SendString("group") + }) + // route with resource param and escaped part + app.Get("/v3/:resource/name\\:customVerb", func(c *Ctx) error { + return c.SendString(c.Params("resource")) + }) + + // check static route + resp, err := app.Test(httptest.NewRequest(MethodGet, "/v1/some/resource/name:customVerb", nil)) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, StatusOK, resp.StatusCode, "Status code") + + body, err := io.ReadAll(resp.Body) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, "static", app.getString(body)) + + // check group route + resp, err = app.Test(httptest.NewRequest(MethodGet, "/v2/:firstVerb/:customVerb", nil)) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, StatusOK, resp.StatusCode, "Status code") + + body, err = io.ReadAll(resp.Body) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, "group", app.getString(body)) + + // check param route + resp, err = app.Test(httptest.NewRequest(MethodGet, "/v3/awesome/name:customVerb", nil)) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, StatusOK, resp.StatusCode, "Status code") + + body, err = io.ReadAll(resp.Body) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, "awesome", app.getString(body)) +} + +func Test_Route_Match_Middleware_HasPrefix(t *testing.T) { + t.Parallel() + + app := New() + + app.Use("/foo", func(c *Ctx) error { + return c.SendString("middleware") + }) + + resp, err := app.Test(httptest.NewRequest(MethodGet, "/foo/bar", nil)) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, 200, resp.StatusCode, "Status code") + + body, err := io.ReadAll(resp.Body) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, "middleware", app.getString(body)) +} + +func Test_Route_Match_Middleware_Root(t *testing.T) { + t.Parallel() + + app := New() + + app.Use("/", func(c *Ctx) error { + return c.SendString("middleware") + }) + + resp, err := app.Test(httptest.NewRequest(MethodGet, "/everything", nil)) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, 200, resp.StatusCode, "Status code") + + body, err := io.ReadAll(resp.Body) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, "middleware", app.getString(body)) +} + +func Test_Router_Register_Missing_Handler(t *testing.T) { + t.Parallel() + + app := New() + defer func() { + if err := recover(); err != nil { + utils.AssertEqual(t, "missing handler in route: /doe\n", fmt.Sprintf("%v", err)) + } + }() + app.register("USE", "/doe", nil) +} + +func Test_Ensure_Router_Interface_Implementation(t *testing.T) { + t.Parallel() + + var app interface{} = (*App)(nil) + _, ok := app.(Router) + utils.AssertEqual(t, true, ok) + + var group interface{} = (*Group)(nil) + _, ok = group.(Router) + utils.AssertEqual(t, true, ok) +} + +func Test_Router_Handler_SetETag(t *testing.T) { + t.Parallel() + + app := New() + app.config.ETag = true + + app.Get("/", func(c *Ctx) error { + return c.SendString("Hello, World!") + }) + + c := &fasthttp.RequestCtx{} + + app.Handler()(c) + + utils.AssertEqual(t, `"13-1831710635"`, string(c.Response.Header.Peek(HeaderETag))) +} + +func Test_Router_Handler_Catch_Error(t *testing.T) { + t.Parallel() + + app := New() + app.config.ErrorHandler = func(ctx *Ctx, err error) error { + return errors.New("fake error") + } + + app.Get("/", func(c *Ctx) error { + return ErrForbidden + }) + + c := &fasthttp.RequestCtx{} + + app.Handler()(c) + + utils.AssertEqual(t, StatusInternalServerError, c.Response.Header.StatusCode()) +} + +func Test_Route_Static_Root(t *testing.T) { + t.Parallel() + + dir := "./.github/testdata/fs/css" + app := New() + app.Static("/", dir, Static{ + Browse: true, + }) + + resp, err := app.Test(httptest.NewRequest(MethodGet, "/", nil)) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, 200, resp.StatusCode, "Status code") + + resp, err = app.Test(httptest.NewRequest(MethodGet, "/style.css", nil)) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, 200, resp.StatusCode, "Status code") + + body, err := io.ReadAll(resp.Body) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, true, strings.Contains(app.getString(body), "color")) + + app = New() + app.Static("/", dir) + + resp, err = app.Test(httptest.NewRequest(MethodGet, "/", nil)) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, 404, resp.StatusCode, "Status code") + + resp, err = app.Test(httptest.NewRequest(MethodGet, "/style.css", nil)) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, 200, resp.StatusCode, "Status code") + + body, err = io.ReadAll(resp.Body) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, true, strings.Contains(app.getString(body), "color")) +} + +func Test_Route_Static_HasPrefix(t *testing.T) { + t.Parallel() + + dir := "./.github/testdata/fs/css" + app := New() + app.Static("/static", dir, Static{ + Browse: true, + }) + + resp, err := app.Test(httptest.NewRequest(MethodGet, "/static", nil)) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, 200, resp.StatusCode, "Status code") + + resp, err = app.Test(httptest.NewRequest(MethodGet, "/static/", nil)) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, 200, resp.StatusCode, "Status code") + + resp, err = app.Test(httptest.NewRequest(MethodGet, "/static/style.css", nil)) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, 200, resp.StatusCode, "Status code") + + body, err := io.ReadAll(resp.Body) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, true, strings.Contains(app.getString(body), "color")) + + app = New() + app.Static("/static/", dir, Static{ + Browse: true, + }) + + resp, err = app.Test(httptest.NewRequest(MethodGet, "/static", nil)) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, 200, resp.StatusCode, "Status code") + + resp, err = app.Test(httptest.NewRequest(MethodGet, "/static/", nil)) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, 200, resp.StatusCode, "Status code") + + resp, err = app.Test(httptest.NewRequest(MethodGet, "/static/style.css", nil)) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, 200, resp.StatusCode, "Status code") + + body, err = io.ReadAll(resp.Body) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, true, strings.Contains(app.getString(body), "color")) + + app = New() + app.Static("/static", dir) + + resp, err = app.Test(httptest.NewRequest(MethodGet, "/static", nil)) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, 404, resp.StatusCode, "Status code") + + resp, err = app.Test(httptest.NewRequest(MethodGet, "/static/", nil)) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, 404, resp.StatusCode, "Status code") + + resp, err = app.Test(httptest.NewRequest(MethodGet, "/static/style.css", nil)) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, 200, resp.StatusCode, "Status code") + + body, err = io.ReadAll(resp.Body) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, true, strings.Contains(app.getString(body), "color")) + + app = New() + app.Static("/static/", dir) + + resp, err = app.Test(httptest.NewRequest(MethodGet, "/static", nil)) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, 404, resp.StatusCode, "Status code") + + resp, err = app.Test(httptest.NewRequest(MethodGet, "/static/", nil)) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, 404, resp.StatusCode, "Status code") + + resp, err = app.Test(httptest.NewRequest(MethodGet, "/static/style.css", nil)) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, 200, resp.StatusCode, "Status code") + + body, err = io.ReadAll(resp.Body) + utils.AssertEqual(t, nil, err, "app.Test(req)") + utils.AssertEqual(t, true, strings.Contains(app.getString(body), "color")) +} + +func Test_Router_NotFound(t *testing.T) { + app := New() + app.Use(func(c *Ctx) error { + return c.Next() + }) + appHandler := app.Handler() + c := &fasthttp.RequestCtx{} + + c.Request.Header.SetMethod("DELETE") + c.URI().SetPath("/this/route/does/not/exist") + + appHandler(c) + + utils.AssertEqual(t, 404, c.Response.StatusCode()) + utils.AssertEqual(t, "Cannot DELETE /this/route/does/not/exist", string(c.Response.Body())) +} + +func Test_Router_NotFound_HTML_Inject(t *testing.T) { + app := New() + app.Use(func(c *Ctx) error { + return c.Next() + }) + appHandler := app.Handler() + c := &fasthttp.RequestCtx{} + + c.Request.Header.SetMethod("DELETE") + c.URI().SetPath("/does/not/exist") + + appHandler(c) + + utils.AssertEqual(t, 404, c.Response.StatusCode()) + utils.AssertEqual(t, "Cannot DELETE /does/not/exist<script>alert('foo');</script>", string(c.Response.Body())) +} + +////////////////////////////////////////////// +///////////////// BENCHMARKS ///////////////// +////////////////////////////////////////////// + +func registerDummyRoutes(app *App) { + h := func(c *Ctx) error { + return nil + } + for _, r := range routesFixture.GithubAPI { + app.Add(r.Method, r.Path, h) + } +} + +// go test -v -run=^$ -bench=Benchmark_App_MethodNotAllowed -benchmem -count=4 +func Benchmark_App_MethodNotAllowed(b *testing.B) { + app := New() + h := func(c *Ctx) error { + return c.SendString("Hello World!") + } + app.All("/this/is/a/", h) + app.Get("/this/is/a/dummy/route/oke", h) + appHandler := app.Handler() + c := &fasthttp.RequestCtx{} + + c.Request.Header.SetMethod("DELETE") + c.URI().SetPath("/this/is/a/dummy/route/oke") + + b.ResetTimer() + for n := 0; n < b.N; n++ { + appHandler(c) + } + b.StopTimer() + utils.AssertEqual(b, 405, c.Response.StatusCode()) + utils.AssertEqual(b, "GET, HEAD", string(c.Response.Header.Peek("Allow"))) + utils.AssertEqual(b, utils.StatusMessage(StatusMethodNotAllowed), string(c.Response.Body())) +} + +// go test -v ./... -run=^$ -bench=Benchmark_Router_NotFound -benchmem -count=4 +func Benchmark_Router_NotFound(b *testing.B) { + app := New() + app.Use(func(c *Ctx) error { + return c.Next() + }) + registerDummyRoutes(app) + appHandler := app.Handler() + c := &fasthttp.RequestCtx{} + + c.Request.Header.SetMethod("DELETE") + c.URI().SetPath("/this/route/does/not/exist") + + b.ResetTimer() + for n := 0; n < b.N; n++ { + appHandler(c) + } + utils.AssertEqual(b, 404, c.Response.StatusCode()) + utils.AssertEqual(b, "Cannot DELETE /this/route/does/not/exist", string(c.Response.Body())) +} + +// go test -v ./... -run=^$ -bench=Benchmark_Router_Handler -benchmem -count=4 +func Benchmark_Router_Handler(b *testing.B) { + app := New() + registerDummyRoutes(app) + appHandler := app.Handler() + + c := &fasthttp.RequestCtx{} + + c.Request.Header.SetMethod("DELETE") + c.URI().SetPath("/user/keys/1337") + + b.ResetTimer() + + for n := 0; n < b.N; n++ { + appHandler(c) + } +} + +func Benchmark_Router_Handler_Strict_Case(b *testing.B) { + app := New(Config{ + StrictRouting: true, + CaseSensitive: true, + }) + registerDummyRoutes(app) + appHandler := app.Handler() + + c := &fasthttp.RequestCtx{} + + c.Request.Header.SetMethod("DELETE") + c.URI().SetPath("/user/keys/1337") + + b.ResetTimer() + + for n := 0; n < b.N; n++ { + appHandler(c) + } +} + +// go test -v ./... -run=^$ -bench=Benchmark_Router_Chain -benchmem -count=4 +func Benchmark_Router_Chain(b *testing.B) { + app := New() + handler := func(c *Ctx) error { + return c.Next() + } + app.Get("/", handler, handler, handler, handler, handler, handler) + + appHandler := app.Handler() + + c := &fasthttp.RequestCtx{} + + c.Request.Header.SetMethod(MethodGet) + c.URI().SetPath("/") + b.ResetTimer() + for n := 0; n < b.N; n++ { + appHandler(c) + } +} + +// go test -v ./... -run=^$ -bench=Benchmark_Router_WithCompression -benchmem -count=4 +func Benchmark_Router_WithCompression(b *testing.B) { + app := New() + handler := func(c *Ctx) error { + return c.Next() + } + app.Get("/", handler) + app.Get("/", handler) + app.Get("/", handler) + app.Get("/", handler) + app.Get("/", handler) + app.Get("/", handler) + + appHandler := app.Handler() + c := &fasthttp.RequestCtx{} + + c.Request.Header.SetMethod(MethodGet) + c.URI().SetPath("/") + b.ResetTimer() + for n := 0; n < b.N; n++ { + appHandler(c) + } +} + +// go test -run=^$ -bench=Benchmark_Startup_Process -benchmem -count=9 +func Benchmark_Startup_Process(b *testing.B) { + for n := 0; n < b.N; n++ { + app := New() + registerDummyRoutes(app) + app.startupProcess() + } +} + +// go test -v ./... -run=^$ -bench=Benchmark_Router_Next -benchmem -count=4 +func Benchmark_Router_Next(b *testing.B) { + app := New() + registerDummyRoutes(app) + app.startupProcess() + + request := &fasthttp.RequestCtx{} + + request.Request.Header.SetMethod("DELETE") + request.URI().SetPath("/user/keys/1337") + var res bool + var err error + + c := app.AcquireCtx(request) + defer app.ReleaseCtx(c) + + b.ResetTimer() + for n := 0; n < b.N; n++ { + c.indexRoute = -1 + res, err = app.next(c) + } + utils.AssertEqual(b, nil, err) + utils.AssertEqual(b, true, res) + utils.AssertEqual(b, 4, c.indexRoute) +} + +// go test -v ./... -run=^$ -bench=Benchmark_Route_Match -benchmem -count=4 +func Benchmark_Route_Match(b *testing.B) { + var match bool + var params [maxParams]string + + parsed := parseRoute("/user/keys/:id") + route := &Route{ + use: false, + root: false, + star: false, + routeParser: parsed, + Params: parsed.params, + path: "/user/keys/:id", + + Path: "/user/keys/:id", + Method: "DELETE", + } + route.Handlers = append(route.Handlers, func(c *Ctx) error { + return nil + }) + b.ResetTimer() + for n := 0; n < b.N; n++ { + match = route.match("/user/keys/1337", "/user/keys/1337", ¶ms) + } + + utils.AssertEqual(b, true, match) + utils.AssertEqual(b, []string{"1337"}, params[0:len(parsed.params)]) +} + +// go test -v ./... -run=^$ -bench=Benchmark_Route_Match_Star -benchmem -count=4 +func Benchmark_Route_Match_Star(b *testing.B) { + var match bool + var params [maxParams]string + + parsed := parseRoute("/*") + route := &Route{ + use: false, + root: false, + star: true, + routeParser: parsed, + Params: parsed.params, + path: "/user/keys/bla", + + Path: "/user/keys/bla", + Method: "DELETE", + } + route.Handlers = append(route.Handlers, func(c *Ctx) error { + return nil + }) + b.ResetTimer() + + for n := 0; n < b.N; n++ { + match = route.match("/user/keys/bla", "/user/keys/bla", ¶ms) + } + + utils.AssertEqual(b, true, match) + utils.AssertEqual(b, []string{"user/keys/bla"}, params[0:len(parsed.params)]) +} + +// go test -v ./... -run=^$ -bench=Benchmark_Route_Match_Root -benchmem -count=4 +func Benchmark_Route_Match_Root(b *testing.B) { + var match bool + var params [maxParams]string + + parsed := parseRoute("/") + route := &Route{ + use: false, + root: true, + star: false, + path: "/", + routeParser: parsed, + Params: parsed.params, + + Path: "/", + Method: "DELETE", + } + route.Handlers = append(route.Handlers, func(c *Ctx) error { + return nil + }) + + b.ResetTimer() + + for n := 0; n < b.N; n++ { + match = route.match("/", "/", ¶ms) + } + + utils.AssertEqual(b, true, match) + utils.AssertEqual(b, []string{}, params[0:len(parsed.params)]) +} + +// go test -v ./... -run=^$ -bench=Benchmark_Router_Handler_CaseSensitive -benchmem -count=4 +func Benchmark_Router_Handler_CaseSensitive(b *testing.B) { + app := New() + app.config.CaseSensitive = true + registerDummyRoutes(app) + appHandler := app.Handler() + + c := &fasthttp.RequestCtx{} + + c.Request.Header.SetMethod("DELETE") + c.URI().SetPath("/user/keys/1337") + + b.ResetTimer() + + for n := 0; n < b.N; n++ { + appHandler(c) + } +} + +// go test -v ./... -run=^$ -bench=Benchmark_Router_Handler_Unescape -benchmem -count=4 +func Benchmark_Router_Handler_Unescape(b *testing.B) { + app := New() + app.config.UnescapePath = true + registerDummyRoutes(app) + app.Delete("/créer", func(c *Ctx) error { + return nil + }) + + appHandler := app.Handler() + + c := &fasthttp.RequestCtx{} + + c.Request.Header.SetMethod(MethodDelete) + c.URI().SetPath("/cr%C3%A9er") + + b.ResetTimer() + + for n := 0; n < b.N; n++ { + c.URI().SetPath("/cr%C3%A9er") + appHandler(c) + } +} + +// go test -run=^$ -bench=Benchmark_Router_Handler_StrictRouting -benchmem -count=4 +func Benchmark_Router_Handler_StrictRouting(b *testing.B) { + app := New() + app.config.CaseSensitive = true + registerDummyRoutes(app) + appHandler := app.Handler() + + c := &fasthttp.RequestCtx{} + + c.Request.Header.SetMethod("DELETE") + c.URI().SetPath("/user/keys/1337") + + b.ResetTimer() + + for n := 0; n < b.N; n++ { + appHandler(c) + } +} + +// go test -run=^$ -bench=Benchmark_Router_Github_API -benchmem -count=16 +func Benchmark_Router_Github_API(b *testing.B) { + app := New() + registerDummyRoutes(app) + app.startupProcess() + + c := &fasthttp.RequestCtx{} + var match bool + var err error + + b.ResetTimer() + + for i := range routesFixture.TestRoutes { + c.Request.Header.SetMethod(routesFixture.TestRoutes[i].Method) + for n := 0; n < b.N; n++ { + c.URI().SetPath(routesFixture.TestRoutes[i].Path) + ctx := app.AcquireCtx(c) + match, err = app.next(ctx) + app.ReleaseCtx(ctx) + } + utils.AssertEqual(b, nil, err) + utils.AssertEqual(b, true, match) + } +} + +type testRoute struct { + Method string `json:"method"` + Path string `json:"path"` +} + +type routeJSON struct { + TestRoutes []testRoute `json:"test_routes"` + GithubAPI []testRoute `json:"github_api"` +} + +// go test -v ./... -run=^$ -bench=Benchmark_Router_Next_Default -benchmem -count=4 +func Benchmark_Router_Next_Default(b *testing.B) { + app := New() + app.Get("/", func(_ *Ctx) error { + return nil + }) + + h := app.Handler() + + fctx := &fasthttp.RequestCtx{} + fctx.Request.Header.SetMethod(MethodGet) + fctx.Request.SetRequestURI("/") + + b.ReportAllocs() + b.ResetTimer() + + for n := 0; n < b.N; n++ { + h(fctx) + } +} + +// go test -v ./... -run=^$ -bench=Benchmark_Router_Next_Default_Parallel -benchmem -count=4 +func Benchmark_Router_Next_Default_Parallel(b *testing.B) { + app := New() + app.Get("/", func(_ *Ctx) error { + return nil + }) + + h := app.Handler() + + b.ReportAllocs() + b.ResetTimer() + + b.RunParallel(func(pb *testing.PB) { + fctx := &fasthttp.RequestCtx{} + fctx.Request.Header.SetMethod(MethodGet) + fctx.Request.SetRequestURI("/") + + for pb.Next() { + h(fctx) + } + }) +} diff --git a/utils/README.md b/utils/README.md new file mode 100644 index 0000000..0276ff3 --- /dev/null +++ b/utils/README.md @@ -0,0 +1,90 @@ +A collection of common functions but with better performance, less allocations and no dependencies created for [Fiber](https://github.com/gofiber/fiber). + +```go +// go test -benchmem -run=^$ -bench=Benchmark_ -count=2 + +Benchmark_ToLowerBytes/fiber-16 42847654 25.7 ns/op 0 B/op 0 allocs/op +Benchmark_ToLowerBytes/fiber-16 46143196 25.7 ns/op 0 B/op 0 allocs/op +Benchmark_ToLowerBytes/default-16 17387322 67.4 ns/op 48 B/op 1 allocs/op +Benchmark_ToLowerBytes/default-16 17906491 67.4 ns/op 48 B/op 1 allocs/op + +Benchmark_ToUpperBytes/fiber-16 46143729 25.7 ns/op 0 B/op 0 allocs/op +Benchmark_ToUpperBytes/fiber-16 47989250 25.6 ns/op 0 B/op 0 allocs/op +Benchmark_ToUpperBytes/default-16 15580854 76.7 ns/op 48 B/op 1 allocs/op +Benchmark_ToUpperBytes/default-16 15381202 76.9 ns/op 48 B/op 1 allocs/op + +Benchmark_TrimRightBytes/fiber-16 70572459 16.3 ns/op 8 B/op 1 allocs/op +Benchmark_TrimRightBytes/fiber-16 74983597 16.3 ns/op 8 B/op 1 allocs/op +Benchmark_TrimRightBytes/default-16 16212578 74.1 ns/op 40 B/op 2 allocs/op +Benchmark_TrimRightBytes/default-16 16434686 74.1 ns/op 40 B/op 2 allocs/op + +Benchmark_TrimLeftBytes/fiber-16 74983128 16.3 ns/op 8 B/op 1 allocs/op +Benchmark_TrimLeftBytes/fiber-16 74985002 16.3 ns/op 8 B/op 1 allocs/op +Benchmark_TrimLeftBytes/default-16 21047868 56.5 ns/op 40 B/op 2 allocs/op +Benchmark_TrimLeftBytes/default-16 21048015 56.5 ns/op 40 B/op 2 allocs/op + +Benchmark_TrimBytes/fiber-16 54533307 21.9 ns/op 16 B/op 1 allocs/op +Benchmark_TrimBytes/fiber-16 54532812 21.9 ns/op 16 B/op 1 allocs/op +Benchmark_TrimBytes/default-16 14282517 84.6 ns/op 48 B/op 2 allocs/op +Benchmark_TrimBytes/default-16 14114508 84.7 ns/op 48 B/op 2 allocs/op + +Benchmark_EqualFolds/fiber-16 36355153 32.6 ns/op 0 B/op 0 allocs/op +Benchmark_EqualFolds/fiber-16 36355593 32.6 ns/op 0 B/op 0 allocs/op +Benchmark_EqualFolds/default-16 15186220 78.1 ns/op 0 B/op 0 allocs/op +Benchmark_EqualFolds/default-16 15186412 78.3 ns/op 0 B/op 0 allocs/op + +Benchmark_UUID/fiber-16 23994625 49.8 ns/op 48 B/op 1 allocs/op +Benchmark_UUID/fiber-16 23994768 50.1 ns/op 48 B/op 1 allocs/op +Benchmark_UUID/default-16 3233772 371 ns/op 208 B/op 6 allocs/op +Benchmark_UUID/default-16 3251295 370 ns/op 208 B/op 6 allocs/op + +Benchmark_GetString/unsafe-16 1000000000 0.709 ns/op 0 B/op 0 allocs/op +Benchmark_GetString/unsafe-16 1000000000 0.713 ns/op 0 B/op 0 allocs/op +Benchmark_GetString/default-16 59986202 19.0 ns/op 16 B/op 1 allocs/op +Benchmark_GetString/default-16 63142939 19.0 ns/op 16 B/op 1 allocs/op + +Benchmark_GetBytes/unsafe-16 508360195 2.36 ns/op 0 B/op 0 allocs/op +Benchmark_GetBytes/unsafe-16 508359979 2.35 ns/op 0 B/op 0 allocs/op +Benchmark_GetBytes/default-16 46143019 25.7 ns/op 16 B/op 1 allocs/op +Benchmark_GetBytes/default-16 44434734 25.6 ns/op 16 B/op 1 allocs/op + +Benchmark_GetMIME/fiber-16 21423750 56.3 ns/op 0 B/op 0 allocs/op +Benchmark_GetMIME/fiber-16 21423559 55.4 ns/op 0 B/op 0 allocs/op +Benchmark_GetMIME/default-16 6735282 173 ns/op 0 B/op 0 allocs/op +Benchmark_GetMIME/default-16 6895002 172 ns/op 0 B/op 0 allocs/op + +Benchmark_StatusMessage/fiber-16 1000000000 0.766 ns/op 0 B/op 0 allocs/op +Benchmark_StatusMessage/fiber-16 1000000000 0.767 ns/op 0 B/op 0 allocs/op +Benchmark_StatusMessage/default-16 159538528 7.50 ns/op 0 B/op 0 allocs/op +Benchmark_StatusMessage/default-16 159750830 7.51 ns/op 0 B/op 0 allocs/op + +Benchmark_ToUpper/fiber-16 22217408 53.3 ns/op 48 B/op 1 allocs/op +Benchmark_ToUpper/fiber-16 22636554 53.2 ns/op 48 B/op 1 allocs/op +Benchmark_ToUpper/default-16 11108600 108 ns/op 48 B/op 1 allocs/op +Benchmark_ToUpper/default-16 11108580 108 ns/op 48 B/op 1 allocs/op + +Benchmark_ToLower/fiber-16 23994720 49.8 ns/op 48 B/op 1 allocs/op +Benchmark_ToLower/fiber-16 23994768 50.1 ns/op 48 B/op 1 allocs/op +Benchmark_ToLower/default-16 10808376 110 ns/op 48 B/op 1 allocs/op +Benchmark_ToLower/default-16 10617034 110 ns/op 48 B/op 1 allocs/op + +Benchmark_TrimRight/fiber-16 413699521 2.94 ns/op 0 B/op 0 allocs/op +Benchmark_TrimRight/fiber-16 415131687 2.91 ns/op 0 B/op 0 allocs/op +Benchmark_TrimRight/default-16 23994577 49.1 ns/op 32 B/op 1 allocs/op +Benchmark_TrimRight/default-16 24484249 49.4 ns/op 32 B/op 1 allocs/op + +Benchmark_TrimLeft/fiber-16 379661170 3.13 ns/op 0 B/op 0 allocs/op +Benchmark_TrimLeft/fiber-16 382079941 3.16 ns/op 0 B/op 0 allocs/op +Benchmark_TrimLeft/default-16 27900877 41.9 ns/op 32 B/op 1 allocs/op +Benchmark_TrimLeft/default-16 28564898 42.0 ns/op 32 B/op 1 allocs/op + +Benchmark_Trim/fiber-16 236632856 4.96 ns/op 0 B/op 0 allocs/op +Benchmark_Trim/fiber-16 237570085 4.93 ns/op 0 B/op 0 allocs/op +Benchmark_Trim/default-16 18457221 66.0 ns/op 32 B/op 1 allocs/op +Benchmark_Trim/default-16 18177328 65.9 ns/op 32 B/op 1 allocs/op +Benchmark_Trim/default.trimspace-16 188933770 6.33 ns/op 0 B/op 0 allocs/op +Benchmark_Trim/default.trimspace-16 184007649 6.42 ns/op 0 B/op 0 allocs/op + +Benchmark_ConvertToBytes/fiber-8 43773547 24.43 ns/op 0 B/op 0 allocs/op +Benchmark_ConvertToBytes/fiber-8 45849477 25.33 ns/op 0 B/op 0 allocs/op +``` diff --git a/utils/assertions.go b/utils/assertions.go new file mode 100644 index 0000000..3682d56 --- /dev/null +++ b/utils/assertions.go @@ -0,0 +1,68 @@ +// ⚡️ Fiber is an Express inspired web framework written in Go with ☕️ +// 🤖 Github Repository: https://github.com/gofiber/fiber +// 📌 API Documentation: https://docs.gofiber.io + +package utils + +import ( + "bytes" + "fmt" + "log" + "path/filepath" + "reflect" + "runtime" + "testing" + "text/tabwriter" +) + +// AssertEqual checks if values are equal +func AssertEqual(tb testing.TB, expected, actual interface{}, description ...string) { //nolint:thelper // TODO: Verify if tb can be nil + if tb != nil { + tb.Helper() + } + + if reflect.DeepEqual(expected, actual) { + return + } + + aType := "" + bType := "" + + if expected != nil { + aType = reflect.TypeOf(expected).String() + } + if actual != nil { + bType = reflect.TypeOf(actual).String() + } + + testName := "AssertEqual" + if tb != nil { + testName = tb.Name() + } + + _, file, line, _ := runtime.Caller(1) + + var buf bytes.Buffer + const pad = 5 + w := tabwriter.NewWriter(&buf, 0, 0, pad, ' ', 0) + _, _ = fmt.Fprintf(w, "\nTest:\t%s", testName) + _, _ = fmt.Fprintf(w, "\nTrace:\t%s:%d", filepath.Base(file), line) + if len(description) > 0 { + _, _ = fmt.Fprintf(w, "\nDescription:\t%s", description[0]) + } + _, _ = fmt.Fprintf(w, "\nExpect:\t%v\t(%s)", expected, aType) + _, _ = fmt.Fprintf(w, "\nResult:\t%v\t(%s)", actual, bType) + + var result string + if err := w.Flush(); err != nil { + result = err.Error() + } else { + result = buf.String() + } + + if tb != nil { + tb.Fatal(result) + } else { + log.Fatal(result) //nolint:revive // tb might be nil, so we need a fallback + } +} diff --git a/utils/assertions_test.go b/utils/assertions_test.go new file mode 100644 index 0000000..d1e7f08 --- /dev/null +++ b/utils/assertions_test.go @@ -0,0 +1,15 @@ +// ⚡️ Fiber is an Express inspired web framework written in Go with ☕️ +// 🤖 Github Repository: https://github.com/gofiber/fiber +// 📌 API Documentation: https://docs.gofiber.io + +package utils + +import ( + "testing" +) + +func Test_AssertEqual(t *testing.T) { + t.Parallel() + AssertEqual(nil, []string{}, []string{}) + AssertEqual(t, []string{}, []string{}) +} diff --git a/utils/bytes.go b/utils/bytes.go new file mode 100644 index 0000000..bd2c87b --- /dev/null +++ b/utils/bytes.go @@ -0,0 +1,69 @@ +// ⚡️ Fiber is an Express inspired web framework written in Go with ☕️ +// 🤖 Github Repository: https://github.com/gofiber/fiber +// 📌 API Documentation: https://docs.gofiber.io + +package utils + +// ToLowerBytes converts ascii slice to lower-case in-place. +func ToLowerBytes(b []byte) []byte { + for i := 0; i < len(b); i++ { + b[i] = toLowerTable[b[i]] + } + return b +} + +// ToUpperBytes converts ascii slice to upper-case in-place. +func ToUpperBytes(b []byte) []byte { + for i := 0; i < len(b); i++ { + b[i] = toUpperTable[b[i]] + } + return b +} + +// TrimRightBytes is the equivalent of bytes.TrimRight +func TrimRightBytes(b []byte, cutset byte) []byte { + lenStr := len(b) + for lenStr > 0 && b[lenStr-1] == cutset { + lenStr-- + } + return b[:lenStr] +} + +// TrimLeftBytes is the equivalent of bytes.TrimLeft +func TrimLeftBytes(b []byte, cutset byte) []byte { + lenStr, start := len(b), 0 + for start < lenStr && b[start] == cutset { + start++ + } + return b[start:] +} + +// TrimBytes is the equivalent of bytes.Trim +func TrimBytes(b []byte, cutset byte) []byte { + i, j := 0, len(b)-1 + for ; i <= j; i++ { + if b[i] != cutset { + break + } + } + for ; i < j; j-- { + if b[j] != cutset { + break + } + } + + return b[i : j+1] +} + +// EqualFoldBytes tests ascii slices for equality case-insensitively +func EqualFoldBytes(b, s []byte) bool { + if len(b) != len(s) { + return false + } + for i := len(b) - 1; i >= 0; i-- { + if toUpperTable[b[i]] != toUpperTable[s[i]] { + return false + } + } + return true +} diff --git a/utils/bytes_test.go b/utils/bytes_test.go new file mode 100644 index 0000000..a449856 --- /dev/null +++ b/utils/bytes_test.go @@ -0,0 +1,218 @@ +// ⚡️ Fiber is an Express inspired web framework written in Go with ☕️ +// 🤖 Github Repository: https://github.com/gofiber/fiber +// 📌 API Documentation: https://docs.gofiber.io + +package utils + +import ( + "bytes" + "testing" +) + +func Test_ToLowerBytes(t *testing.T) { + t.Parallel() + res := ToLowerBytes([]byte("/MY/NAME/IS/:PARAM/*")) + AssertEqual(t, true, bytes.Equal([]byte("/my/name/is/:param/*"), res)) + res = ToLowerBytes([]byte("/MY1/NAME/IS/:PARAM/*")) + AssertEqual(t, true, bytes.Equal([]byte("/my1/name/is/:param/*"), res)) + res = ToLowerBytes([]byte("/MY2/NAME/IS/:PARAM/*")) + AssertEqual(t, true, bytes.Equal([]byte("/my2/name/is/:param/*"), res)) + res = ToLowerBytes([]byte("/MY3/NAME/IS/:PARAM/*")) + AssertEqual(t, true, bytes.Equal([]byte("/my3/name/is/:param/*"), res)) + res = ToLowerBytes([]byte("/MY4/NAME/IS/:PARAM/*")) + AssertEqual(t, true, bytes.Equal([]byte("/my4/name/is/:param/*"), res)) +} + +func Benchmark_ToLowerBytes(b *testing.B) { + path := []byte(largeStr) + want := []byte(lowerStr) + var res []byte + b.Run("fiber", func(b *testing.B) { + for n := 0; n < b.N; n++ { + res = ToLowerBytes(path) + } + AssertEqual(b, bytes.Equal(want, res), true) + }) + b.Run("default", func(b *testing.B) { + for n := 0; n < b.N; n++ { + res = bytes.ToLower(path) + } + AssertEqual(b, bytes.Equal(want, res), true) + }) +} + +func Test_ToUpperBytes(t *testing.T) { + t.Parallel() + res := ToUpperBytes([]byte("/my/name/is/:param/*")) + AssertEqual(t, true, bytes.Equal([]byte("/MY/NAME/IS/:PARAM/*"), res)) + res = ToUpperBytes([]byte("/my1/name/is/:param/*")) + AssertEqual(t, true, bytes.Equal([]byte("/MY1/NAME/IS/:PARAM/*"), res)) + res = ToUpperBytes([]byte("/my2/name/is/:param/*")) + AssertEqual(t, true, bytes.Equal([]byte("/MY2/NAME/IS/:PARAM/*"), res)) + res = ToUpperBytes([]byte("/my3/name/is/:param/*")) + AssertEqual(t, true, bytes.Equal([]byte("/MY3/NAME/IS/:PARAM/*"), res)) + res = ToUpperBytes([]byte("/my4/name/is/:param/*")) + AssertEqual(t, true, bytes.Equal([]byte("/MY4/NAME/IS/:PARAM/*"), res)) +} + +func Benchmark_ToUpperBytes(b *testing.B) { + path := []byte(largeStr) + want := []byte(upperStr) + var res []byte + b.Run("fiber", func(b *testing.B) { + for n := 0; n < b.N; n++ { + res = ToUpperBytes(path) + } + AssertEqual(b, bytes.Equal(want, res), true) + }) + b.Run("default", func(b *testing.B) { + for n := 0; n < b.N; n++ { + res = bytes.ToUpper(path) + } + AssertEqual(b, bytes.Equal(want, res), true) + }) +} + +func Test_TrimRightBytes(t *testing.T) { + t.Parallel() + res := TrimRightBytes([]byte("/test//////"), '/') + AssertEqual(t, []byte("/test"), res) + + res = TrimRightBytes([]byte("/test"), '/') + AssertEqual(t, []byte("/test"), res) + + res = TrimRightBytes([]byte(" "), ' ') + AssertEqual(t, 0, len(res)) + + res = TrimRightBytes([]byte(" "), ' ') + AssertEqual(t, 0, len(res)) + + res = TrimRightBytes([]byte(""), ' ') + AssertEqual(t, 0, len(res)) +} + +func Benchmark_TrimRightBytes(b *testing.B) { + var res []byte + + b.Run("fiber", func(b *testing.B) { + for n := 0; n < b.N; n++ { + res = TrimRightBytes([]byte("foobar "), ' ') + } + AssertEqual(b, []byte("foobar"), res) + }) + b.Run("default", func(b *testing.B) { + for n := 0; n < b.N; n++ { + res = bytes.TrimRight([]byte("foobar "), " ") + } + AssertEqual(b, []byte("foobar"), res) + }) +} + +func Test_TrimLeftBytes(t *testing.T) { + t.Parallel() + res := TrimLeftBytes([]byte("////test/"), '/') + AssertEqual(t, []byte("test/"), res) + + res = TrimLeftBytes([]byte("test/"), '/') + AssertEqual(t, []byte("test/"), res) + + res = TrimLeftBytes([]byte(" "), ' ') + AssertEqual(t, 0, len(res)) + + res = TrimLeftBytes([]byte(" "), ' ') + AssertEqual(t, 0, len(res)) + + res = TrimLeftBytes([]byte(""), ' ') + AssertEqual(t, 0, len(res)) +} + +func Benchmark_TrimLeftBytes(b *testing.B) { + var res []byte + + b.Run("fiber", func(b *testing.B) { + for n := 0; n < b.N; n++ { + res = TrimLeftBytes([]byte(" foobar"), ' ') + } + AssertEqual(b, []byte("foobar"), res) + }) + b.Run("default", func(b *testing.B) { + for n := 0; n < b.N; n++ { + res = bytes.TrimLeft([]byte(" foobar"), " ") + } + AssertEqual(b, []byte("foobar"), res) + }) +} + +func Test_TrimBytes(t *testing.T) { + t.Parallel() + res := TrimBytes([]byte(" test "), ' ') + AssertEqual(t, []byte("test"), res) + + res = TrimBytes([]byte("test"), ' ') + AssertEqual(t, []byte("test"), res) + + res = TrimBytes([]byte(".test"), '.') + AssertEqual(t, []byte("test"), res) + + res = TrimBytes([]byte(" "), ' ') + AssertEqual(t, 0, len(res)) + + res = TrimBytes([]byte(" "), ' ') + AssertEqual(t, 0, len(res)) + + res = TrimBytes([]byte(""), ' ') + AssertEqual(t, 0, len(res)) +} + +func Benchmark_TrimBytes(b *testing.B) { + var res []byte + + b.Run("fiber", func(b *testing.B) { + for n := 0; n < b.N; n++ { + res = TrimBytes([]byte(" foobar "), ' ') + } + AssertEqual(b, []byte("foobar"), res) + }) + b.Run("default", func(b *testing.B) { + for n := 0; n < b.N; n++ { + res = bytes.Trim([]byte(" foobar "), " ") + } + AssertEqual(b, []byte("foobar"), res) + }) +} + +func Benchmark_EqualFoldBytes(b *testing.B) { + left := []byte(upperStr) + right := []byte(lowerStr) + var res bool + b.Run("fiber", func(b *testing.B) { + for n := 0; n < b.N; n++ { + res = EqualFoldBytes(left, right) + } + AssertEqual(b, true, res) + }) + b.Run("default", func(b *testing.B) { + for n := 0; n < b.N; n++ { + res = bytes.EqualFold(left, right) + } + AssertEqual(b, true, res) + }) +} + +func Test_EqualFoldBytes(t *testing.T) { + t.Parallel() + res := EqualFoldBytes([]byte("/MY/NAME/IS/:PARAM/*"), []byte("/my/name/is/:param/*")) + AssertEqual(t, true, res) + res = EqualFoldBytes([]byte("/MY1/NAME/IS/:PARAM/*"), []byte("/MY1/NAME/IS/:PARAM/*")) + AssertEqual(t, true, res) + res = EqualFoldBytes([]byte("/my2/name/is/:param/*"), []byte("/my2/name")) + AssertEqual(t, false, res) + res = EqualFoldBytes([]byte("/dddddd"), []byte("eeeeee")) + AssertEqual(t, false, res) + res = EqualFoldBytes([]byte("\na"), []byte("*A")) + AssertEqual(t, false, res) + res = EqualFoldBytes([]byte("/MY3/NAME/IS/:PARAM/*"), []byte("/my3/name/is/:param/*")) + AssertEqual(t, true, res) + res = EqualFoldBytes([]byte("/MY4/NAME/IS/:PARAM/*"), []byte("/my4/nAME/IS/:param/*")) + AssertEqual(t, true, res) +} diff --git a/utils/common.go b/utils/common.go new file mode 100644 index 0000000..6c1dd1e --- /dev/null +++ b/utils/common.go @@ -0,0 +1,160 @@ +// ⚡️ Fiber is an Express inspired web framework written in Go with ☕️ +// 🤖 Github Repository: https://github.com/gofiber/fiber +// 📌 API Documentation: https://docs.gofiber.io + +package utils + +import ( + "bytes" + "crypto/rand" + "encoding/binary" + "encoding/hex" + "math" + "net" + "os" + "reflect" + "runtime" + "strconv" + "sync" + "sync/atomic" + "unicode" + + googleuuid "github.com/google/uuid" +) + +const ( + toLowerTable = "\x00\x01\x02\x03\x04\x05\x06\a\b\t\n\v\f\r\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f !\"#$%&'()*+,-./0123456789:;<=>?@abcdefghijklmnopqrstuvwxyz[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~\u007f\x80\x81\x82\x83\x84\x85\x86\x87\x88\x89\x8a\x8b\x8c\x8d\x8e\x8f\x90\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9a\x9b\x9c\x9d\x9e\x9f\xa0\xa1\xa2\xa3\xa4\xa5\xa6\xa7\xa8\xa9\xaa\xab\xac\xad\xae\xaf\xb0\xb1\xb2\xb3\xb4\xb5\xb6\xb7\xb8\xb9\xba\xbb\xbc\xbd\xbe\xbf\xc0\xc1\xc2\xc3\xc4\xc5\xc6\xc7\xc8\xc9\xca\xcb\xcc\xcd\xce\xcf\xd0\xd1\xd2\xd3\xd4\xd5\xd6\xd7\xd8\xd9\xda\xdb\xdc\xdd\xde\xdf\xe0\xe1\xe2\xe3\xe4\xe5\xe6\xe7\xe8\xe9\xea\xeb\xec\xed\xee\xef\xf0\xf1\xf2\xf3\xf4\xf5\xf6\xf7\xf8\xf9\xfa\xfb\xfc\xfd\xfe\xff" + toUpperTable = "\x00\x01\x02\x03\x04\x05\x06\a\b\t\n\v\f\r\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`ABCDEFGHIJKLMNOPQRSTUVWXYZ{|}~\u007f\x80\x81\x82\x83\x84\x85\x86\x87\x88\x89\x8a\x8b\x8c\x8d\x8e\x8f\x90\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9a\x9b\x9c\x9d\x9e\x9f\xa0\xa1\xa2\xa3\xa4\xa5\xa6\xa7\xa8\xa9\xaa\xab\xac\xad\xae\xaf\xb0\xb1\xb2\xb3\xb4\xb5\xb6\xb7\xb8\xb9\xba\xbb\xbc\xbd\xbe\xbf\xc0\xc1\xc2\xc3\xc4\xc5\xc6\xc7\xc8\xc9\xca\xcb\xcc\xcd\xce\xcf\xd0\xd1\xd2\xd3\xd4\xd5\xd6\xd7\xd8\xd9\xda\xdb\xdc\xdd\xde\xdf\xe0\xe1\xe2\xe3\xe4\xe5\xe6\xe7\xe8\xe9\xea\xeb\xec\xed\xee\xef\xf0\xf1\xf2\xf3\xf4\xf5\xf6\xf7\xf8\xf9\xfa\xfb\xfc\xfd\xfe\xff" +) + +// Copyright © 2014, Roger Peppe +// github.com/rogpeppe/fastuuid +// All rights reserved. + +const ( + emptyUUID = "00000000-0000-0000-0000-000000000000" +) + +var ( + uuidSeed [24]byte + uuidCounter uint64 + uuidSetup sync.Once + unitsSlice = []byte("kmgtp") +) + +// UUID generates an universally unique identifier (UUID) +func UUID() string { + // Setup seed & counter once + uuidSetup.Do(func() { + if _, err := rand.Read(uuidSeed[:]); err != nil { + return + } + uuidCounter = binary.LittleEndian.Uint64(uuidSeed[:8]) + }) + if atomic.LoadUint64(&uuidCounter) <= 0 { + return emptyUUID + } + // first 8 bytes differ, taking a slice of the first 16 bytes + x := atomic.AddUint64(&uuidCounter, 1) + uuid := uuidSeed + binary.LittleEndian.PutUint64(uuid[:8], x) + uuid[6], uuid[9] = uuid[9], uuid[6] + + // RFC4122 v4 + uuid[6] = (uuid[6] & 0x0f) | 0x40 + uuid[8] = uuid[8]&0x3f | 0x80 + + // create UUID representation of the first 128 bits + b := make([]byte, 36) + hex.Encode(b[0:8], uuid[0:4]) + b[8] = '-' + hex.Encode(b[9:13], uuid[4:6]) + b[13] = '-' + hex.Encode(b[14:18], uuid[6:8]) + b[18] = '-' + hex.Encode(b[19:23], uuid[8:10]) + b[23] = '-' + hex.Encode(b[24:], uuid[10:16]) + + return UnsafeString(b) +} + +// UUIDv4 returns a Random (Version 4) UUID. +// The strength of the UUIDs is based on the strength of the crypto/rand package. +func UUIDv4() string { + token, err := googleuuid.NewRandom() + if err != nil { + return UUID() + } + return token.String() +} + +// FunctionName returns function name +func FunctionName(fn interface{}) string { + t := reflect.ValueOf(fn).Type() + if t.Kind() == reflect.Func { + return runtime.FuncForPC(reflect.ValueOf(fn).Pointer()).Name() + } + return t.String() +} + +// GetArgument check if key is in arguments +func GetArgument(arg string) bool { + for i := range os.Args[1:] { + if os.Args[1:][i] == arg { + return true + } + } + return false +} + +// IncrementIPRange Find available next IP address +func IncrementIPRange(ip net.IP) { + for j := len(ip) - 1; j >= 0; j-- { + ip[j]++ + if ip[j] > 0 { + break + } + } +} + +// ConvertToBytes returns integer size of bytes from human-readable string, ex. 42kb, 42M +// Returns 0 if string is unrecognized +func ConvertToBytes(humanReadableString string) int { + strLen := len(humanReadableString) + if strLen == 0 { + return 0 + } + var unitPrefixPos, lastNumberPos int + // loop the string + for i := strLen - 1; i >= 0; i-- { + // check if the char is a number + if unicode.IsDigit(rune(humanReadableString[i])) { + lastNumberPos = i + break + } else if humanReadableString[i] != ' ' { + unitPrefixPos = i + } + } + + if lastNumberPos < 0 { + return 0 + } + // fetch the number part and parse it to float + size, err := strconv.ParseFloat(humanReadableString[:lastNumberPos+1], 64) + if err != nil { + return 0 + } + + // check the multiplier from the string and use it + if unitPrefixPos > 0 { + // convert multiplier char to lowercase and check if exists in units slice + index := bytes.IndexByte(unitsSlice, toLowerTable[humanReadableString[unitPrefixPos]]) + if index != -1 { + const bytesPerKB = 1000 + size *= math.Pow(bytesPerKB, float64(index+1)) + } + } + + return int(size) +} diff --git a/utils/common_test.go b/utils/common_test.go new file mode 100644 index 0000000..1b0884f --- /dev/null +++ b/utils/common_test.go @@ -0,0 +1,127 @@ +// ⚡️ Fiber is an Express inspired web framework written in Go with ☕️ +// 🤖 Github Repository: https://github.com/gofiber/fiber +// 📌 API Documentation: https://docs.gofiber.io + +package utils + +import ( + "crypto/rand" + "fmt" + "testing" +) + +func Test_FunctionName(t *testing.T) { + t.Parallel() + AssertEqual(t, "github.com/gofiber/fiber/v2/utils.Test_UUID", FunctionName(Test_UUID)) + + AssertEqual(t, "github.com/gofiber/fiber/v2/utils.Test_FunctionName.func1", FunctionName(func() {})) + + dummyint := 20 + AssertEqual(t, "int", FunctionName(dummyint)) +} + +func Test_UUID(t *testing.T) { + t.Parallel() + res := UUID() + AssertEqual(t, 36, len(res)) + AssertEqual(t, true, res != emptyUUID) +} + +func Test_UUID_Concurrency(t *testing.T) { + t.Parallel() + iterations := 1000 + var res string + ch := make(chan string, iterations) + results := make(map[string]string) + for i := 0; i < iterations; i++ { + go func() { + ch <- UUID() + }() + } + for i := 0; i < iterations; i++ { + res = <-ch + results[res] = res + } + AssertEqual(t, iterations, len(results)) +} + +func Test_UUIDv4(t *testing.T) { + t.Parallel() + res := UUIDv4() + AssertEqual(t, 36, len(res)) + AssertEqual(t, true, res != emptyUUID) +} + +func Test_UUIDv4_Concurrency(t *testing.T) { + t.Parallel() + iterations := 1000 + var res string + ch := make(chan string, iterations) + results := make(map[string]string) + for i := 0; i < iterations; i++ { + go func() { + ch <- UUIDv4() + }() + } + for i := 0; i < iterations; i++ { + res = <-ch + results[res] = res + } + AssertEqual(t, iterations, len(results)) +} + +// go test -v -run=^$ -bench=Benchmark_UUID -benchmem -count=2 + +func Benchmark_UUID(b *testing.B) { + var res string + b.Run("fiber", func(b *testing.B) { + for n := 0; n < b.N; n++ { + res = UUID() + } + AssertEqual(b, 36, len(res)) + }) + b.Run("default", func(b *testing.B) { + rnd := make([]byte, 16) + _, err := rand.Read(rnd) + AssertEqual(b, nil, err) + for n := 0; n < b.N; n++ { + res = fmt.Sprintf("%x-%x-%x-%x-%x", rnd[0:4], rnd[4:6], rnd[6:8], rnd[8:10], rnd[10:]) + } + AssertEqual(b, 36, len(res)) + }) +} + +func Test_ConvertToBytes(t *testing.T) { + t.Parallel() + AssertEqual(t, 0, ConvertToBytes("")) + AssertEqual(t, 42, ConvertToBytes("42")) + AssertEqual(t, 42, ConvertToBytes("42b")) + AssertEqual(t, 42, ConvertToBytes("42B")) + AssertEqual(t, 42, ConvertToBytes("42 b")) + AssertEqual(t, 42, ConvertToBytes("42 B")) + + AssertEqual(t, 42*1000, ConvertToBytes("42k")) + AssertEqual(t, 42*1000, ConvertToBytes("42K")) + AssertEqual(t, 42*1000, ConvertToBytes("42kb")) + AssertEqual(t, 42*1000, ConvertToBytes("42KB")) + AssertEqual(t, 42*1000, ConvertToBytes("42 kb")) + AssertEqual(t, 42*1000, ConvertToBytes("42 KB")) + + AssertEqual(t, 42*1000000, ConvertToBytes("42M")) + AssertEqual(t, int(42.5*1000000), ConvertToBytes("42.5MB")) + AssertEqual(t, 42*1000000000, ConvertToBytes("42G")) + + AssertEqual(t, 0, ConvertToBytes("string")) + AssertEqual(t, 0, ConvertToBytes("MB")) +} + +// go test -v -run=^$ -bench=Benchmark_ConvertToBytes -benchmem -count=2 +func Benchmark_ConvertToBytes(b *testing.B) { + var res int + b.Run("fiber", func(b *testing.B) { + for n := 0; n < b.N; n++ { + res = ConvertToBytes("42B") + } + AssertEqual(b, 42, res) + }) +} diff --git a/utils/convert.go b/utils/convert.go new file mode 100644 index 0000000..a5317bf --- /dev/null +++ b/utils/convert.go @@ -0,0 +1,117 @@ +// ⚡️ Fiber is an Express inspired web framework written in Go with ☕️ +// 🤖 Github Repository: https://github.com/gofiber/fiber +// 📌 API Documentation: https://docs.gofiber.io + +package utils + +import ( + "fmt" + "reflect" + "strconv" + "strings" + "time" +) + +// CopyString copies a string to make it immutable +func CopyString(s string) string { + return string(UnsafeBytes(s)) +} + +// CopyBytes copies a slice to make it immutable +func CopyBytes(b []byte) []byte { + tmp := make([]byte, len(b)) + copy(tmp, b) + return tmp +} + +const ( + uByte = 1 << (10 * iota) // 1 << 10 == 1024 + uKilobyte + uMegabyte + uGigabyte + uTerabyte + uPetabyte + uExabyte +) + +// ByteSize returns a human-readable byte string of the form 10M, 12.5K, and so forth. +// The unit that results in the smallest number greater than or equal to 1 is always chosen. +func ByteSize(bytes uint64) string { + unit := "" + value := float64(bytes) + switch { + case bytes >= uExabyte: + unit = "EB" + value /= uExabyte + case bytes >= uPetabyte: + unit = "PB" + value /= uPetabyte + case bytes >= uTerabyte: + unit = "TB" + value /= uTerabyte + case bytes >= uGigabyte: + unit = "GB" + value /= uGigabyte + case bytes >= uMegabyte: + unit = "MB" + value /= uMegabyte + case bytes >= uKilobyte: + unit = "KB" + value /= uKilobyte + case bytes >= uByte: + unit = "B" + default: + return "0B" + } + result := strconv.FormatFloat(value, 'f', 1, 64) + result = strings.TrimSuffix(result, ".0") + return result + unit +} + +// ToString Change arg to string +func ToString(arg interface{}, timeFormat ...string) string { + tmp := reflect.Indirect(reflect.ValueOf(arg)).Interface() + switch v := tmp.(type) { + case int: + return strconv.Itoa(v) + case int8: + return strconv.FormatInt(int64(v), 10) + case int16: + return strconv.FormatInt(int64(v), 10) + case int32: + return strconv.FormatInt(int64(v), 10) + case int64: + return strconv.FormatInt(v, 10) + case uint: + return strconv.Itoa(int(v)) + case uint8: + return strconv.FormatInt(int64(v), 10) + case uint16: + return strconv.FormatInt(int64(v), 10) + case uint32: + return strconv.FormatInt(int64(v), 10) + case uint64: + return strconv.FormatInt(int64(v), 10) + case string: + return v + case []byte: + return string(v) + case bool: + return strconv.FormatBool(v) + case float32: + return strconv.FormatFloat(float64(v), 'f', -1, 32) + case float64: + return strconv.FormatFloat(v, 'f', -1, 64) + case time.Time: + if len(timeFormat) > 0 { + return v.Format(timeFormat[0]) + } + return v.Format("2006-01-02 15:04:05") + case reflect.Value: + return ToString(v.Interface(), timeFormat...) + case fmt.Stringer: + return v.String() + default: + return "" + } +} diff --git a/utils/convert_b2s_new.go b/utils/convert_b2s_new.go new file mode 100644 index 0000000..3fcf7d5 --- /dev/null +++ b/utils/convert_b2s_new.go @@ -0,0 +1,12 @@ +//go:build go1.20 + +package utils + +import ( + "unsafe" +) + +// UnsafeString returns a string pointer without allocation +func UnsafeString(b []byte) string { + return unsafe.String(unsafe.SliceData(b), len(b)) +} diff --git a/utils/convert_b2s_old.go b/utils/convert_b2s_old.go new file mode 100644 index 0000000..36cbe30 --- /dev/null +++ b/utils/convert_b2s_old.go @@ -0,0 +1,14 @@ +//go:build !go1.20 + +package utils + +import ( + "unsafe" +) + +// UnsafeString returns a string pointer without allocation +// +//nolint:gosec // unsafe is used for better performance here +func UnsafeString(b []byte) string { + return *(*string)(unsafe.Pointer(&b)) +} diff --git a/utils/convert_s2b_new.go b/utils/convert_s2b_new.go new file mode 100644 index 0000000..5da5c81 --- /dev/null +++ b/utils/convert_s2b_new.go @@ -0,0 +1,12 @@ +//go:build go1.20 + +package utils + +import ( + "unsafe" +) + +// UnsafeBytes returns a byte pointer without allocation. +func UnsafeBytes(s string) []byte { + return unsafe.Slice(unsafe.StringData(s), len(s)) +} diff --git a/utils/convert_s2b_old.go b/utils/convert_s2b_old.go new file mode 100644 index 0000000..c9435bd --- /dev/null +++ b/utils/convert_s2b_old.go @@ -0,0 +1,24 @@ +//go:build !go1.20 + +package utils + +import ( + "reflect" + "unsafe" +) + +const MaxStringLen = 0x7fff0000 // Maximum string length for UnsafeBytes. (decimal: 2147418112) + +// UnsafeBytes returns a byte pointer without allocation. +// String length shouldn't be more than 2147418112. +// +//nolint:gosec // unsafe is used for better performance here +func UnsafeBytes(s string) []byte { + if s == "" { + return nil + } + + return (*[MaxStringLen]byte)(unsafe.Pointer( + (*reflect.StringHeader)(unsafe.Pointer(&s)).Data), + )[:len(s):len(s)] +} diff --git a/utils/convert_test.go b/utils/convert_test.go new file mode 100644 index 0000000..4bfd6bf --- /dev/null +++ b/utils/convert_test.go @@ -0,0 +1,83 @@ +// ⚡️ Fiber is an Express inspired web framework written in Go with ☕️ +// 🤖 Github Repository: https://github.com/gofiber/fiber +// 📌 API Documentation: https://docs.gofiber.io + +package utils + +import ( + "testing" +) + +func Test_UnsafeString(t *testing.T) { + t.Parallel() + res := UnsafeString([]byte("Hello, World!")) + AssertEqual(t, "Hello, World!", res) +} + +// go test -v -run=^$ -bench=UnsafeString -benchmem -count=2 + +func Benchmark_UnsafeString(b *testing.B) { + hello := []byte("Hello, World!") + var res string + b.Run("unsafe", func(b *testing.B) { + for n := 0; n < b.N; n++ { + res = UnsafeString(hello) + } + AssertEqual(b, "Hello, World!", res) + }) + b.Run("default", func(b *testing.B) { + for n := 0; n < b.N; n++ { + res = string(hello) + } + AssertEqual(b, "Hello, World!", res) + }) +} + +func Test_UnsafeBytes(t *testing.T) { + t.Parallel() + res := UnsafeBytes("Hello, World!") + AssertEqual(t, []byte("Hello, World!"), res) +} + +// go test -v -run=^$ -bench=UnsafeBytes -benchmem -count=4 + +func Benchmark_UnsafeBytes(b *testing.B) { + hello := "Hello, World!" + var res []byte + b.Run("unsafe", func(b *testing.B) { + for n := 0; n < b.N; n++ { + res = UnsafeBytes(hello) + } + AssertEqual(b, []byte("Hello, World!"), res) + }) + b.Run("default", func(b *testing.B) { + for n := 0; n < b.N; n++ { + res = []byte(hello) + } + AssertEqual(b, []byte("Hello, World!"), res) + }) +} + +func Test_CopyString(t *testing.T) { + t.Parallel() + res := CopyString("Hello, World!") + AssertEqual(t, "Hello, World!", res) +} + +func Test_ToString(t *testing.T) { + t.Parallel() + res := ToString([]byte("Hello, World!")) + AssertEqual(t, "Hello, World!", res) + res = ToString(true) + AssertEqual(t, "true", res) + res = ToString(uint(100)) + AssertEqual(t, "100", res) +} + +// go test -v -run=^$ -bench=ToString -benchmem -count=2 +func Benchmark_ToString(b *testing.B) { + hello := []byte("Hello, World!") + for n := 0; n < b.N; n++ { + ToString(hello) + } +} diff --git a/utils/deprecated.go b/utils/deprecated.go new file mode 100644 index 0000000..a436e67 --- /dev/null +++ b/utils/deprecated.go @@ -0,0 +1,16 @@ +package utils + +// Deprecated: Please use UnsafeString instead +func GetString(b []byte) string { + return UnsafeString(b) +} + +// Deprecated: Please use UnsafeBytes instead +func GetBytes(s string) []byte { + return UnsafeBytes(s) +} + +// Deprecated: Please use CopyString instead +func ImmutableString(s string) string { + return CopyString(s) +} diff --git a/utils/http.go b/utils/http.go new file mode 100644 index 0000000..fe394f5 --- /dev/null +++ b/utils/http.go @@ -0,0 +1,267 @@ +// ⚡️ Fiber is an Express inspired web framework written in Go with ☕️ +// 🤖 Github Repository: https://github.com/gofiber/fiber +// 📌 API Documentation: https://docs.gofiber.io + +package utils + +import ( + "mime" + "strings" +) + +const MIMEOctetStream = "application/octet-stream" + +// GetMIME returns the content-type of a file extension +func GetMIME(extension string) string { + if len(extension) == 0 { + return "" + } + var foundMime string + if extension[0] == '.' { + foundMime = mimeExtensions[extension[1:]] + } else { + foundMime = mimeExtensions[extension] + } + + if len(foundMime) == 0 { + if extension[0] != '.' { + foundMime = mime.TypeByExtension("." + extension) + } else { + foundMime = mime.TypeByExtension(extension) + } + + if foundMime == "" { + return MIMEOctetStream + } + } + return foundMime +} + +// ParseVendorSpecificContentType check if content type is vendor specific and +// if it is parsable to any known types. If its not vendor specific then returns +// the original content type. +func ParseVendorSpecificContentType(cType string) string { + plusIndex := strings.Index(cType, "+") + + if plusIndex == -1 { + return cType + } + + var parsableType string + if semiColonIndex := strings.Index(cType, ";"); semiColonIndex == -1 { + parsableType = cType[plusIndex+1:] + } else if plusIndex < semiColonIndex { + parsableType = cType[plusIndex+1 : semiColonIndex] + } else { + return cType[:semiColonIndex] + } + + slashIndex := strings.Index(cType, "/") + + if slashIndex == -1 { + return cType + } + + return cType[0:slashIndex+1] + parsableType +} + +// limits for HTTP statuscodes +const ( + statusMessageMin = 100 + statusMessageMax = 511 +) + +// StatusMessage returns the correct message for the provided HTTP statuscode +func StatusMessage(status int) string { + if status < statusMessageMin || status > statusMessageMax { + return "" + } + return statusMessage[status] +} + +// NOTE: Keep this in sync with the status code list +var statusMessage = []string{ + 100: "Continue", // StatusContinue + 101: "Switching Protocols", // StatusSwitchingProtocols + 102: "Processing", // StatusProcessing + 103: "Early Hints", // StatusEarlyHints + + 200: "OK", // StatusOK + 201: "Created", // StatusCreated + 202: "Accepted", // StatusAccepted + 203: "Non-Authoritative Information", // StatusNonAuthoritativeInformation + 204: "No Content", // StatusNoContent + 205: "Reset Content", // StatusResetContent + 206: "Partial Content", // StatusPartialContent + 207: "Multi-Status", // StatusMultiStatus + 208: "Already Reported", // StatusAlreadyReported + 226: "IM Used", // StatusIMUsed + + 300: "Multiple Choices", // StatusMultipleChoices + 301: "Moved Permanently", // StatusMovedPermanently + 302: "Found", // StatusFound + 303: "See Other", // StatusSeeOther + 304: "Not Modified", // StatusNotModified + 305: "Use Proxy", // StatusUseProxy + 306: "Switch Proxy", // StatusSwitchProxy + 307: "Temporary Redirect", // StatusTemporaryRedirect + 308: "Permanent Redirect", // StatusPermanentRedirect + + 400: "Bad Request", // StatusBadRequest + 401: "Unauthorized", // StatusUnauthorized + 402: "Payment Required", // StatusPaymentRequired + 403: "Forbidden", // StatusForbidden + 404: "Not Found", // StatusNotFound + 405: "Method Not Allowed", // StatusMethodNotAllowed + 406: "Not Acceptable", // StatusNotAcceptable + 407: "Proxy Authentication Required", // StatusProxyAuthRequired + 408: "Request Timeout", // StatusRequestTimeout + 409: "Conflict", // StatusConflict + 410: "Gone", // StatusGone + 411: "Length Required", // StatusLengthRequired + 412: "Precondition Failed", // StatusPreconditionFailed + 413: "Request Entity Too Large", // StatusRequestEntityTooLarge + 414: "Request URI Too Long", // StatusRequestURITooLong + 415: "Unsupported Media Type", // StatusUnsupportedMediaType + 416: "Requested Range Not Satisfiable", // StatusRequestedRangeNotSatisfiable + 417: "Expectation Failed", // StatusExpectationFailed + 418: "I'm a teapot", // StatusTeapot + 421: "Misdirected Request", // StatusMisdirectedRequest + 422: "Unprocessable Entity", // StatusUnprocessableEntity + 423: "Locked", // StatusLocked + 424: "Failed Dependency", // StatusFailedDependency + 425: "Too Early", // StatusTooEarly + 426: "Upgrade Required", // StatusUpgradeRequired + 428: "Precondition Required", // StatusPreconditionRequired + 429: "Too Many Requests", // StatusTooManyRequests + 431: "Request Header Fields Too Large", // StatusRequestHeaderFieldsTooLarge + 451: "Unavailable For Legal Reasons", // StatusUnavailableForLegalReasons + + 500: "Internal Server Error", // StatusInternalServerError + 501: "Not Implemented", // StatusNotImplemented + 502: "Bad Gateway", // StatusBadGateway + 503: "Service Unavailable", // StatusServiceUnavailable + 504: "Gateway Timeout", // StatusGatewayTimeout + 505: "HTTP Version Not Supported", // StatusHTTPVersionNotSupported + 506: "Variant Also Negotiates", // StatusVariantAlsoNegotiates + 507: "Insufficient Storage", // StatusInsufficientStorage + 508: "Loop Detected", // StatusLoopDetected + 510: "Not Extended", // StatusNotExtended + 511: "Network Authentication Required", // StatusNetworkAuthenticationRequired +} + +// MIME types were copied from https://github.com/nginx/nginx/blob/67d2a9541826ecd5db97d604f23460210fd3e517/conf/mime.types with the following updates: +// - Use "application/xml" instead of "text/xml" as recommended per https://datatracker.ietf.org/doc/html/rfc7303#section-4.1 +// - Use "text/javascript" instead of "application/javascript" as recommended per https://www.rfc-editor.org/rfc/rfc9239#name-text-javascript +var mimeExtensions = map[string]string{ + "html": "text/html", + "htm": "text/html", + "shtml": "text/html", + "css": "text/css", + "xml": "application/xml", + "gif": "image/gif", + "jpeg": "image/jpeg", + "jpg": "image/jpeg", + "js": "text/javascript", + "atom": "application/atom+xml", + "rss": "application/rss+xml", + "mml": "text/mathml", + "txt": "text/plain", + "jad": "text/vnd.sun.j2me.app-descriptor", + "wml": "text/vnd.wap.wml", + "htc": "text/x-component", + "avif": "image/avif", + "png": "image/png", + "svg": "image/svg+xml", + "svgz": "image/svg+xml", + "tif": "image/tiff", + "tiff": "image/tiff", + "wbmp": "image/vnd.wap.wbmp", + "webp": "image/webp", + "ico": "image/x-icon", + "jng": "image/x-jng", + "bmp": "image/x-ms-bmp", + "woff": "font/woff", + "woff2": "font/woff2", + "jar": "application/java-archive", + "war": "application/java-archive", + "ear": "application/java-archive", + "json": "application/json", + "hqx": "application/mac-binhex40", + "doc": "application/msword", + "pdf": "application/pdf", + "ps": "application/postscript", + "eps": "application/postscript", + "ai": "application/postscript", + "rtf": "application/rtf", + "m3u8": "application/vnd.apple.mpegurl", + "kml": "application/vnd.google-earth.kml+xml", + "kmz": "application/vnd.google-earth.kmz", + "xls": "application/vnd.ms-excel", + "eot": "application/vnd.ms-fontobject", + "ppt": "application/vnd.ms-powerpoint", + "odg": "application/vnd.oasis.opendocument.graphics", + "odp": "application/vnd.oasis.opendocument.presentation", + "ods": "application/vnd.oasis.opendocument.spreadsheet", + "odt": "application/vnd.oasis.opendocument.text", + "pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation", + "xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + "docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + "wmlc": "application/vnd.wap.wmlc", + "wasm": "application/wasm", + "7z": "application/x-7z-compressed", + "cco": "application/x-cocoa", + "jardiff": "application/x-java-archive-diff", + "jnlp": "application/x-java-jnlp-file", + "run": "application/x-makeself", + "pl": "application/x-perl", + "pm": "application/x-perl", + "prc": "application/x-pilot", + "pdb": "application/x-pilot", + "rar": "application/x-rar-compressed", + "rpm": "application/x-redhat-package-manager", + "sea": "application/x-sea", + "swf": "application/x-shockwave-flash", + "sit": "application/x-stuffit", + "tcl": "application/x-tcl", + "tk": "application/x-tcl", + "der": "application/x-x509-ca-cert", + "pem": "application/x-x509-ca-cert", + "crt": "application/x-x509-ca-cert", + "xpi": "application/x-xpinstall", + "xhtml": "application/xhtml+xml", + "xspf": "application/xspf+xml", + "zip": "application/zip", + "bin": "application/octet-stream", + "exe": "application/octet-stream", + "dll": "application/octet-stream", + "deb": "application/octet-stream", + "dmg": "application/octet-stream", + "iso": "application/octet-stream", + "img": "application/octet-stream", + "msi": "application/octet-stream", + "msp": "application/octet-stream", + "msm": "application/octet-stream", + "mid": "audio/midi", + "midi": "audio/midi", + "kar": "audio/midi", + "mp3": "audio/mpeg", + "ogg": "audio/ogg", + "m4a": "audio/x-m4a", + "ra": "audio/x-realaudio", + "3gpp": "video/3gpp", + "3gp": "video/3gpp", + "ts": "video/mp2t", + "mp4": "video/mp4", + "mpeg": "video/mpeg", + "mpg": "video/mpeg", + "mov": "video/quicktime", + "webm": "video/webm", + "flv": "video/x-flv", + "m4v": "video/x-m4v", + "mng": "video/x-mng", + "asx": "video/x-ms-asf", + "asf": "video/x-ms-asf", + "wmv": "video/x-ms-wmv", + "avi": "video/x-msvideo", +} diff --git a/utils/http_test.go b/utils/http_test.go new file mode 100644 index 0000000..56a6029 --- /dev/null +++ b/utils/http_test.go @@ -0,0 +1,148 @@ +// ⚡️ Fiber is an Express inspired web framework written in Go with ☕️ +// 🤖 Github Repository: https://github.com/gofiber/fiber +// 📌 API Documentation: https://docs.gofiber.io + +package utils + +import ( + "mime" + "net/http" + "testing" +) + +func Test_GetMIME(t *testing.T) { + t.Parallel() + res := GetMIME(".json") + AssertEqual(t, "application/json", res) + + res = GetMIME(".xml") + AssertEqual(t, "application/xml", res) + + res = GetMIME("xml") + AssertEqual(t, "application/xml", res) + + res = GetMIME("unknown") + AssertEqual(t, MIMEOctetStream, res) + + err := mime.AddExtensionType(".mjs", "application/javascript") + if err == nil { + res = GetMIME(".mjs") + AssertEqual(t, "application/javascript", res) + } + AssertEqual(t, nil, err) + + // empty case + res = GetMIME("") + AssertEqual(t, "", res) +} + +// go test -v -run=^$ -bench=Benchmark_GetMIME -benchmem -count=2 +func Benchmark_GetMIME(b *testing.B) { + var res string + b.Run("fiber", func(b *testing.B) { + for n := 0; n < b.N; n++ { + res = GetMIME(".xml") + res = GetMIME(".txt") + res = GetMIME(".png") + res = GetMIME(".exe") + res = GetMIME(".json") + } + AssertEqual(b, "application/json", res) + }) + b.Run("default", func(b *testing.B) { + for n := 0; n < b.N; n++ { + res = mime.TypeByExtension(".xml") + res = mime.TypeByExtension(".txt") + res = mime.TypeByExtension(".png") + res = mime.TypeByExtension(".exe") + res = mime.TypeByExtension(".json") + } + AssertEqual(b, "application/json", res) + }) +} + +func Test_ParseVendorSpecificContentType(t *testing.T) { + t.Parallel() + + cType := ParseVendorSpecificContentType("application/json") + AssertEqual(t, "application/json", cType) + + cType = ParseVendorSpecificContentType("multipart/form-data; boundary=dart-http-boundary-ZnVy.ICWq+7HOdsHqWxCFa8g3D.KAhy+Y0sYJ_lBADypu8po3_X") + AssertEqual(t, "multipart/form-data", cType) + + cType = ParseVendorSpecificContentType("multipart/form-data") + AssertEqual(t, "multipart/form-data", cType) + + cType = ParseVendorSpecificContentType("application/vnd.api+json; version=1") + AssertEqual(t, "application/json", cType) + + cType = ParseVendorSpecificContentType("application/vnd.api+json") + AssertEqual(t, "application/json", cType) + + cType = ParseVendorSpecificContentType("application/vnd.dummy+x-www-form-urlencoded") + AssertEqual(t, "application/x-www-form-urlencoded", cType) + + cType = ParseVendorSpecificContentType("something invalid") + AssertEqual(t, "something invalid", cType) +} + +func Benchmark_ParseVendorSpecificContentType(b *testing.B) { + var cType string + b.Run("vendorContentType", func(b *testing.B) { + for n := 0; n < b.N; n++ { + cType = ParseVendorSpecificContentType("application/vnd.api+json; version=1") + } + AssertEqual(b, "application/json", cType) + }) + + b.Run("defaultContentType", func(b *testing.B) { + for n := 0; n < b.N; n++ { + cType = ParseVendorSpecificContentType("application/json") + } + AssertEqual(b, "application/json", cType) + }) +} + +func Test_StatusMessage(t *testing.T) { + t.Parallel() + res := StatusMessage(204) + AssertEqual(t, "No Content", res) + + res = StatusMessage(404) + AssertEqual(t, "Not Found", res) + + res = StatusMessage(426) + AssertEqual(t, "Upgrade Required", res) + + res = StatusMessage(511) + AssertEqual(t, "Network Authentication Required", res) + + res = StatusMessage(1337) + AssertEqual(t, "", res) + + res = StatusMessage(-1) + AssertEqual(t, "", res) + + res = StatusMessage(0) + AssertEqual(t, "", res) + + res = StatusMessage(600) + AssertEqual(t, "", res) +} + +// go test -run=^$ -bench=Benchmark_StatusMessage -benchmem -count=2 +func Benchmark_StatusMessage(b *testing.B) { + var res string + b.Run("fiber", func(b *testing.B) { + for n := 0; n < b.N; n++ { + res = StatusMessage(http.StatusNotExtended) + } + AssertEqual(b, "Not Extended", res) + }) + b.Run("default", func(b *testing.B) { + for n := 0; n < b.N; n++ { + res = http.StatusText(http.StatusNotExtended) + } + AssertEqual(b, "Not Extended", res) + }) +} diff --git a/utils/ips.go b/utils/ips.go new file mode 100644 index 0000000..54ab11b --- /dev/null +++ b/utils/ips.go @@ -0,0 +1,143 @@ +package utils + +import ( + "net" +) + +// IsIPv4 works the same way as net.ParseIP, +// but without check for IPv6 case and without returning net.IP slice, whereby IsIPv4 makes no allocations. +func IsIPv4(s string) bool { + for i := 0; i < net.IPv4len; i++ { + if len(s) == 0 { + return false + } + + if i > 0 { + if s[0] != '.' { + return false + } + s = s[1:] + } + + n, ci := 0, 0 + + for ci = 0; ci < len(s) && '0' <= s[ci] && s[ci] <= '9'; ci++ { + n = n*10 + int(s[ci]-'0') + if n > 0xFF { + return false + } + } + + if ci == 0 || (ci > 1 && s[0] == '0') { + return false + } + + s = s[ci:] + } + + return len(s) == 0 +} + +// IsIPv6 works the same way as net.ParseIP, +// but without check for IPv4 case and without returning net.IP slice, whereby IsIPv6 makes no allocations. +func IsIPv6(s string) bool { + ellipsis := -1 // position of ellipsis in ip + + // Might have leading ellipsis + if len(s) >= 2 && s[0] == ':' && s[1] == ':' { + ellipsis = 0 + s = s[2:] + // Might be only ellipsis + if len(s) == 0 { + return true + } + } + + // Loop, parsing hex numbers followed by colon. + i := 0 + for i < net.IPv6len { + // Hex number. + n, ci := 0, 0 + + for ci = 0; ci < len(s); ci++ { + if '0' <= s[ci] && s[ci] <= '9' { + n *= 16 + n += int(s[ci] - '0') + } else if 'a' <= s[ci] && s[ci] <= 'f' { + n *= 16 + n += int(s[ci]-'a') + 10 + } else if 'A' <= s[ci] && s[ci] <= 'F' { + n *= 16 + n += int(s[ci]-'A') + 10 + } else { + break + } + if n > 0xFFFF { + return false + } + } + if ci == 0 || n > 0xFFFF { + return false + } + + if ci < len(s) && s[ci] == '.' { + if ellipsis < 0 && i != net.IPv6len-net.IPv4len { + return false + } + if i+net.IPv4len > net.IPv6len { + return false + } + + if !IsIPv4(s) { + return false + } + + s = "" + i += net.IPv4len + break + } + + // Save this 16-bit chunk. + i += 2 + + // Stop at end of string. + s = s[ci:] + if len(s) == 0 { + break + } + + // Otherwise must be followed by colon and more. + if s[0] != ':' || len(s) == 1 { + return false + } + s = s[1:] + + // Look for ellipsis. + if s[0] == ':' { + if ellipsis >= 0 { // already have one + return false + } + ellipsis = i + s = s[1:] + if len(s) == 0 { // can be at end + break + } + } + } + + // Must have used entire string. + if len(s) != 0 { + return false + } + + // If didn't parse enough, expand ellipsis. + if i < net.IPv6len { + if ellipsis < 0 { + return false + } + } else if ellipsis >= 0 { + // Ellipsis must represent at least one 0 group. + return false + } + return true +} diff --git a/utils/ips_test.go b/utils/ips_test.go new file mode 100644 index 0000000..7a8a5e1 --- /dev/null +++ b/utils/ips_test.go @@ -0,0 +1,91 @@ +// ⚡️ Fiber is an Express inspired web framework written in Go with ☕️ +// 🤖 Github Repository: https://github.com/gofiber/fiber +// 📌 API Documentation: https://docs.gofiber.io + +package utils + +import ( + "net" + "testing" +) + +func Test_IsIPv4(t *testing.T) { + t.Parallel() + + AssertEqual(t, true, IsIPv4("174.23.33.100")) + AssertEqual(t, true, IsIPv4("127.0.0.1")) + AssertEqual(t, true, IsIPv4("127.255.255.255")) + AssertEqual(t, true, IsIPv4("0.0.0.0")) + + AssertEqual(t, false, IsIPv4(".0.0.0")) + AssertEqual(t, false, IsIPv4("0.0.0.")) + AssertEqual(t, false, IsIPv4("0.0.0")) + AssertEqual(t, false, IsIPv4(".0.0.0.")) + AssertEqual(t, false, IsIPv4("0.0.0.0.0")) + AssertEqual(t, false, IsIPv4("0")) + AssertEqual(t, false, IsIPv4("")) + AssertEqual(t, false, IsIPv4("2345:0425:2CA1::0567:5673:23b5")) + AssertEqual(t, false, IsIPv4("invalid")) + AssertEqual(t, false, IsIPv4("189.12.34.260")) + AssertEqual(t, false, IsIPv4("189.12.260.260")) + AssertEqual(t, false, IsIPv4("189.260.260.260")) + AssertEqual(t, false, IsIPv4("999.999.999.999")) + AssertEqual(t, false, IsIPv4("9999.9999.9999.9999")) +} + +// go test -v -run=^$ -bench=UnsafeString -benchmem -count=2 + +func Benchmark_IsIPv4(b *testing.B) { + ip := "174.23.33.100" + var res bool + + b.Run("fiber", func(b *testing.B) { + for n := 0; n < b.N; n++ { + res = IsIPv4(ip) + } + AssertEqual(b, true, res) + }) + + b.Run("default", func(b *testing.B) { + for n := 0; n < b.N; n++ { + res = net.ParseIP(ip) != nil + } + AssertEqual(b, true, res) + }) +} + +func Test_IsIPv6(t *testing.T) { + t.Parallel() + + AssertEqual(t, true, IsIPv6("9396:9549:b4f7:8ed0:4791:1330:8c06:e62d")) + AssertEqual(t, true, IsIPv6("2345:0425:2CA1::0567:5673:23b5")) + AssertEqual(t, true, IsIPv6("2001:1:2:3:4:5:6:7")) + + AssertEqual(t, false, IsIPv6("1.1.1.1")) + AssertEqual(t, false, IsIPv6("2001:1:2:3:4:5:6:")) + AssertEqual(t, false, IsIPv6(":1:2:3:4:5:6:")) + AssertEqual(t, false, IsIPv6("1:2:3:4:5:6:")) + AssertEqual(t, false, IsIPv6("")) + AssertEqual(t, false, IsIPv6("invalid")) +} + +// go test -v -run=^$ -bench=UnsafeString -benchmem -count=2 + +func Benchmark_IsIPv6(b *testing.B) { + ip := "9396:9549:b4f7:8ed0:4791:1330:8c06:e62d" + var res bool + + b.Run("fiber", func(b *testing.B) { + for n := 0; n < b.N; n++ { + res = IsIPv6(ip) + } + AssertEqual(b, true, res) + }) + + b.Run("default", func(b *testing.B) { + for n := 0; n < b.N; n++ { + res = net.ParseIP(ip) != nil + } + AssertEqual(b, true, res) + }) +} diff --git a/utils/json.go b/utils/json.go new file mode 100644 index 0000000..477c8c3 --- /dev/null +++ b/utils/json.go @@ -0,0 +1,9 @@ +package utils + +// JSONMarshal returns the JSON encoding of v. +type JSONMarshal func(v interface{}) ([]byte, error) + +// JSONUnmarshal parses the JSON-encoded data and stores the result +// in the value pointed to by v. If v is nil or not a pointer, +// Unmarshal returns an InvalidUnmarshalError. +type JSONUnmarshal func(data []byte, v interface{}) error diff --git a/utils/json_test.go b/utils/json_test.go new file mode 100644 index 0000000..1a79e48 --- /dev/null +++ b/utils/json_test.go @@ -0,0 +1,58 @@ +package utils + +import ( + "encoding/json" + "testing" +) + +type sampleStructure struct { + ImportantString string `json:"important_string"` +} + +func Test_GolangJSONEncoder(t *testing.T) { + t.Parallel() + + var ( + ss = &sampleStructure{ + ImportantString: "Hello World", + } + importantString = `{"important_string":"Hello World"}` + jsonEncoder JSONMarshal = json.Marshal + ) + + raw, err := jsonEncoder(ss) + AssertEqual(t, err, nil) + + AssertEqual(t, string(raw), importantString) +} + +func Test_DefaultJSONEncoder(t *testing.T) { + t.Parallel() + + var ( + ss = &sampleStructure{ + ImportantString: "Hello World", + } + importantString = `{"important_string":"Hello World"}` + jsonEncoder JSONMarshal = json.Marshal + ) + + raw, err := jsonEncoder(ss) + AssertEqual(t, err, nil) + + AssertEqual(t, string(raw), importantString) +} + +func Test_DefaultJSONDecoder(t *testing.T) { + t.Parallel() + + var ( + ss sampleStructure + importantString = []byte(`{"important_string":"Hello World"}`) + jsonDecoder JSONUnmarshal = json.Unmarshal + ) + + err := jsonDecoder(importantString, &ss) + AssertEqual(t, err, nil) + AssertEqual(t, "Hello World", ss.ImportantString) +} diff --git a/utils/strings.go b/utils/strings.go new file mode 100644 index 0000000..109d132 --- /dev/null +++ b/utils/strings.go @@ -0,0 +1,75 @@ +// ⚡️ Fiber is an Express inspired web framework written in Go with ☕️ +// 🤖 Github Repository: https://github.com/gofiber/fiber +// 📌 API Documentation: https://docs.gofiber.io + +package utils + +// ToLower converts ascii string to lower-case +func ToLower(b string) string { + res := make([]byte, len(b)) + copy(res, b) + for i := 0; i < len(res); i++ { + res[i] = toLowerTable[res[i]] + } + + return UnsafeString(res) +} + +// ToUpper converts ascii string to upper-case +func ToUpper(b string) string { + res := make([]byte, len(b)) + copy(res, b) + for i := 0; i < len(res); i++ { + res[i] = toUpperTable[res[i]] + } + + return UnsafeString(res) +} + +// TrimLeft is the equivalent of strings.TrimLeft +func TrimLeft(s string, cutset byte) string { + lenStr, start := len(s), 0 + for start < lenStr && s[start] == cutset { + start++ + } + return s[start:] +} + +// Trim is the equivalent of strings.Trim +func Trim(s string, cutset byte) string { + i, j := 0, len(s)-1 + for ; i <= j; i++ { + if s[i] != cutset { + break + } + } + for ; i < j; j-- { + if s[j] != cutset { + break + } + } + + return s[i : j+1] +} + +// TrimRight is the equivalent of strings.TrimRight +func TrimRight(s string, cutset byte) string { + lenStr := len(s) + for lenStr > 0 && s[lenStr-1] == cutset { + lenStr-- + } + return s[:lenStr] +} + +// EqualFold tests ascii strings for equality case-insensitively +func EqualFold(b, s string) bool { + if len(b) != len(s) { + return false + } + for i := len(b) - 1; i >= 0; i-- { + if toUpperTable[b[i]] != toUpperTable[s[i]] { + return false + } + } + return true +} diff --git a/utils/strings_test.go b/utils/strings_test.go new file mode 100644 index 0000000..c0de587 --- /dev/null +++ b/utils/strings_test.go @@ -0,0 +1,217 @@ +// ⚡️ Fiber is an Express inspired web framework written in Go with ☕️ +// 🤖 Github Repository: https://github.com/gofiber/fiber +// 📌 API Documentation: https://docs.gofiber.io + +package utils + +import ( + "strings" + "testing" +) + +func Test_ToUpper(t *testing.T) { + t.Parallel() + res := ToUpper("/my/name/is/:param/*") + AssertEqual(t, "/MY/NAME/IS/:PARAM/*", res) +} + +const ( + largeStr = "/RePos/GoFiBer/FibEr/iSsues/187643/CoMmEnts/RePos/GoFiBer/FibEr/iSsues/CoMmEnts" + upperStr = "/REPOS/GOFIBER/FIBER/ISSUES/187643/COMMENTS/REPOS/GOFIBER/FIBER/ISSUES/COMMENTS" + lowerStr = "/repos/gofiber/fiber/issues/187643/comments/repos/gofiber/fiber/issues/comments" +) + +func Benchmark_ToUpper(b *testing.B) { + var res string + b.Run("fiber", func(b *testing.B) { + for n := 0; n < b.N; n++ { + res = ToUpper(largeStr) + } + AssertEqual(b, upperStr, res) + }) + b.Run("default", func(b *testing.B) { + for n := 0; n < b.N; n++ { + res = strings.ToUpper(largeStr) + } + AssertEqual(b, upperStr, res) + }) +} + +func Test_ToLower(t *testing.T) { + t.Parallel() + res := ToLower("/MY/NAME/IS/:PARAM/*") + AssertEqual(t, "/my/name/is/:param/*", res) + res = ToLower("/MY1/NAME/IS/:PARAM/*") + AssertEqual(t, "/my1/name/is/:param/*", res) + res = ToLower("/MY2/NAME/IS/:PARAM/*") + AssertEqual(t, "/my2/name/is/:param/*", res) + res = ToLower("/MY3/NAME/IS/:PARAM/*") + AssertEqual(t, "/my3/name/is/:param/*", res) + res = ToLower("/MY4/NAME/IS/:PARAM/*") + AssertEqual(t, "/my4/name/is/:param/*", res) +} + +func Benchmark_ToLower(b *testing.B) { + var res string + b.Run("fiber", func(b *testing.B) { + for n := 0; n < b.N; n++ { + res = ToLower(largeStr) + } + AssertEqual(b, lowerStr, res) + }) + b.Run("default", func(b *testing.B) { + for n := 0; n < b.N; n++ { + res = strings.ToLower(largeStr) + } + AssertEqual(b, lowerStr, res) + }) +} + +func Test_TrimRight(t *testing.T) { + t.Parallel() + res := TrimRight("/test//////", '/') + AssertEqual(t, "/test", res) + + res = TrimRight("/test", '/') + AssertEqual(t, "/test", res) + + res = TrimRight(" ", ' ') + AssertEqual(t, "", res) + + res = TrimRight(" ", ' ') + AssertEqual(t, "", res) + + res = TrimRight("", ' ') + AssertEqual(t, "", res) +} + +func Benchmark_TrimRight(b *testing.B) { + var res string + + b.Run("fiber", func(b *testing.B) { + for n := 0; n < b.N; n++ { + res = TrimRight("foobar ", ' ') + } + AssertEqual(b, "foobar", res) + }) + b.Run("default", func(b *testing.B) { + for n := 0; n < b.N; n++ { + res = strings.TrimRight("foobar ", " ") + } + AssertEqual(b, "foobar", res) + }) +} + +func Test_TrimLeft(t *testing.T) { + t.Parallel() + res := TrimLeft("////test/", '/') + AssertEqual(t, "test/", res) + + res = TrimLeft("test/", '/') + AssertEqual(t, "test/", res) + + res = TrimLeft(" ", ' ') + AssertEqual(t, "", res) + + res = TrimLeft(" ", ' ') + AssertEqual(t, "", res) + + res = TrimLeft("", ' ') + AssertEqual(t, "", res) +} + +func Benchmark_TrimLeft(b *testing.B) { + var res string + + b.Run("fiber", func(b *testing.B) { + for n := 0; n < b.N; n++ { + res = TrimLeft(" foobar", ' ') + } + AssertEqual(b, "foobar", res) + }) + b.Run("default", func(b *testing.B) { + for n := 0; n < b.N; n++ { + res = strings.TrimLeft(" foobar", " ") + } + AssertEqual(b, "foobar", res) + }) +} + +func Test_Trim(t *testing.T) { + t.Parallel() + res := Trim(" test ", ' ') + AssertEqual(t, "test", res) + + res = Trim("test", ' ') + AssertEqual(t, "test", res) + + res = Trim(".test", '.') + AssertEqual(t, "test", res) + + res = Trim(" ", ' ') + AssertEqual(t, "", res) + + res = Trim(" ", ' ') + AssertEqual(t, "", res) + + res = Trim("", ' ') + AssertEqual(t, "", res) +} + +func Benchmark_Trim(b *testing.B) { + var res string + + b.Run("fiber", func(b *testing.B) { + for n := 0; n < b.N; n++ { + res = Trim(" foobar ", ' ') + } + AssertEqual(b, "foobar", res) + }) + b.Run("default", func(b *testing.B) { + for n := 0; n < b.N; n++ { + res = strings.Trim(" foobar ", " ") + } + AssertEqual(b, "foobar", res) + }) + b.Run("default.trimspace", func(b *testing.B) { + for n := 0; n < b.N; n++ { + res = strings.TrimSpace(" foobar ") + } + AssertEqual(b, "foobar", res) + }) +} + +// go test -v -run=^$ -bench=Benchmark_EqualFold -benchmem -count=4 +func Benchmark_EqualFold(b *testing.B) { + var res bool + b.Run("fiber", func(b *testing.B) { + for n := 0; n < b.N; n++ { + res = EqualFold(upperStr, lowerStr) + } + AssertEqual(b, true, res) + }) + b.Run("default", func(b *testing.B) { + for n := 0; n < b.N; n++ { + res = strings.EqualFold(upperStr, lowerStr) + } + AssertEqual(b, true, res) + }) +} + +func Test_EqualFold(t *testing.T) { + t.Parallel() + res := EqualFold("/MY/NAME/IS/:PARAM/*", "/my/name/is/:param/*") + AssertEqual(t, true, res) + res = EqualFold("/MY1/NAME/IS/:PARAM/*", "/MY1/NAME/IS/:PARAM/*") + AssertEqual(t, true, res) + res = EqualFold("/my2/name/is/:param/*", "/my2/name") + AssertEqual(t, false, res) + res = EqualFold("/dddddd", "eeeeee") + AssertEqual(t, false, res) + res = EqualFold("\na", "*A") + AssertEqual(t, false, res) + res = EqualFold("/MY3/NAME/IS/:PARAM/*", "/my3/name/is/:param/*") + AssertEqual(t, true, res) + res = EqualFold("/MY4/NAME/IS/:PARAM/*", "/my4/nAME/IS/:param/*") + AssertEqual(t, true, res) +} diff --git a/utils/time.go b/utils/time.go new file mode 100644 index 0000000..8ea13c2 --- /dev/null +++ b/utils/time.go @@ -0,0 +1,32 @@ +package utils + +import ( + "sync" + "sync/atomic" + "time" +) + +var ( + timestampTimer sync.Once + // Timestamp please start the timer function before you use this value + // please load the value with atomic `atomic.LoadUint32(&utils.Timestamp)` + Timestamp uint32 +) + +// StartTimeStampUpdater starts a concurrent function which stores the timestamp to an atomic value per second, +// which is much better for performance than determining it at runtime each time +func StartTimeStampUpdater() { + timestampTimer.Do(func() { + // set initial value + atomic.StoreUint32(&Timestamp, uint32(time.Now().Unix())) + go func(sleep time.Duration) { + ticker := time.NewTicker(sleep) + defer ticker.Stop() + + for t := range ticker.C { + // update timestamp + atomic.StoreUint32(&Timestamp, uint32(t.Unix())) + } + }(1 * time.Second) // duration + }) +} diff --git a/utils/time_test.go b/utils/time_test.go new file mode 100644 index 0000000..0f467f9 --- /dev/null +++ b/utils/time_test.go @@ -0,0 +1,48 @@ +package utils + +import ( + "sync/atomic" + "testing" + "time" +) + +func checkTimeStamp(tb testing.TB, expectedCurrent, actualCurrent uint32) { //nolint:thelper // TODO: Verify if tb can be nil + if tb != nil { + tb.Helper() + } + // test with some buffer in front and back of the expectedCurrent time -> because of the timing on the work machine + AssertEqual(tb, true, actualCurrent >= expectedCurrent-1 || actualCurrent <= expectedCurrent+1) +} + +func Test_TimeStampUpdater(t *testing.T) { + t.Parallel() + + StartTimeStampUpdater() + + now := uint32(time.Now().Unix()) + checkTimeStamp(t, now, atomic.LoadUint32(&Timestamp)) + // one second later + time.Sleep(1 * time.Second) + checkTimeStamp(t, now+1, atomic.LoadUint32(&Timestamp)) + // two seconds later + time.Sleep(1 * time.Second) + checkTimeStamp(t, now+2, atomic.LoadUint32(&Timestamp)) +} + +func Benchmark_CalculateTimestamp(b *testing.B) { + StartTimeStampUpdater() + + var res uint32 + b.Run("fiber", func(b *testing.B) { + for n := 0; n < b.N; n++ { + res = atomic.LoadUint32(&Timestamp) + } + checkTimeStamp(b, uint32(time.Now().Unix()), res) + }) + b.Run("default", func(b *testing.B) { + for n := 0; n < b.N; n++ { + res = uint32(time.Now().Unix()) + } + checkTimeStamp(b, uint32(time.Now().Unix()), res) + }) +} diff --git a/utils/xml.go b/utils/xml.go new file mode 100644 index 0000000..cc6a024 --- /dev/null +++ b/utils/xml.go @@ -0,0 +1,4 @@ +package utils + +// XMLMarshal returns the XML encoding of v. +type XMLMarshal func(v interface{}) ([]byte, error) diff --git a/utils/xml_test.go b/utils/xml_test.go new file mode 100644 index 0000000..d0e5381 --- /dev/null +++ b/utils/xml_test.go @@ -0,0 +1,59 @@ +package utils + +import ( + "encoding/xml" + "testing" +) + +type serversXMLStructure struct { + XMLName xml.Name `xml:"servers"` + Version string `xml:"version,attr"` + Servers []serverXMLStructure `xml:"server"` +} + +type serverXMLStructure struct { + XMLName xml.Name `xml:"server"` + Name string `xml:"name"` +} + +const xmlString = `fiber onefiber two` + +func Test_GolangXMLEncoder(t *testing.T) { + t.Parallel() + + var ( + ss = &serversXMLStructure{ + Version: "1", + Servers: []serverXMLStructure{ + {Name: "fiber one"}, + {Name: "fiber two"}, + }, + } + xmlEncoder XMLMarshal = xml.Marshal + ) + + raw, err := xmlEncoder(ss) + AssertEqual(t, err, nil) + + AssertEqual(t, string(raw), xmlString) +} + +func Test_DefaultXMLEncoder(t *testing.T) { + t.Parallel() + + var ( + ss = &serversXMLStructure{ + Version: "1", + Servers: []serverXMLStructure{ + {Name: "fiber one"}, + {Name: "fiber two"}, + }, + } + xmlEncoder XMLMarshal = xml.Marshal + ) + + raw, err := xmlEncoder(ss) + AssertEqual(t, err, nil) + + AssertEqual(t, string(raw), xmlString) +}