From 7b4c2521a3fd1bcc6492dcb8fc95c29a971a11a4 Mon Sep 17 00:00:00 2001 From: bvbej Date: Tue, 23 Jul 2024 10:23:43 +0800 Subject: [PATCH] first commit --- .gitignore | 5 + README.md | 4 + go.mod | 119 + go.sum | 2248 +++++++++++++++++ pkg/aes/aes.go | 120 + pkg/aes/padding.go | 25 + pkg/android_binary/apk/apk.go | 196 ++ pkg/android_binary/apk/apkxml.go | 108 + pkg/android_binary/apk/errors.go | 9 + pkg/android_binary/common.go | 258 ++ pkg/android_binary/table.go | 1093 ++++++++ pkg/android_binary/type.go | 333 +++ pkg/android_binary/xml.go | 271 ++ pkg/ants/pool.go | 108 + pkg/apollo/config.go | 97 + pkg/auth/config.go | 29 + pkg/auth/error.go | 12 + pkg/auth/generate.go | 17 + pkg/auth/jwt_access.go | 97 + pkg/auth/manager.go | 194 ++ pkg/auth/store.go | 334 +++ pkg/auth/token.go | 118 + pkg/bcrypt/password.go | 39 + pkg/browser/browser.go | 23 + pkg/cache/redis.go | 482 ++++ pkg/captcha/base64.go | 97 + pkg/cidr/calc.go | 119 + pkg/cidr/ip.go | 194 ++ pkg/cmap/cmap.go | 360 +++ pkg/color/string_darwin.go | 47 + pkg/color/string_linux.go | 47 + pkg/color/string_windows.go | 47 + pkg/compress/compress.go | 38 + pkg/crontab/crontab.go | 40 + pkg/database/mongo.go | 77 + pkg/database/mysql.go | 247 ++ pkg/database/tool.go | 88 + pkg/database/trace.go | 23 + pkg/ddm/README.md | 26 + pkg/ddm/benchmark.go | 35 + pkg/ddm/mark.go | 62 + pkg/ddm/type.go | 19 + pkg/downloader/downloader.go | 264 ++ pkg/downloader/downloader_test.go | 45 + pkg/downloader/event.go | 19 + pkg/duration_fmt/fmt.go | 340 +++ pkg/duration_fmt/units.go | 107 + pkg/env/env.go | 80 + pkg/errno/errno.go | 74 + pkg/excel/export.go | 111 + pkg/file/file.go | 189 ++ pkg/grpclient/client.go | 98 + pkg/grpclient/keepalive.go | 15 + pkg/hash/hash.go | 25 + pkg/hash/hash_hashids.go | 41 + pkg/hmac/hmac.go | 55 + pkg/httpclient/alarm.go | 29 + pkg/httpclient/client.go | 395 +++ pkg/httpclient/error.go | 46 + pkg/httpclient/option.go | 138 + pkg/httpclient/retry.go | 44 + pkg/httpclient/util.go | 147 ++ pkg/limiter/rate.go | 71 + pkg/lock/lock.go | 47 + pkg/logger/logger.go | 275 ++ pkg/mail/mail.go | 36 + pkg/md5/md5.go | 28 + pkg/metrics/metrics.go | 51 + pkg/mux/context.go | 406 +++ pkg/mux/core.go | 466 ++++ pkg/observable/iterable.go | 3 + pkg/observable/observable.go | 67 + pkg/observable/subscriber.go | 33 + pkg/p/print.go | 50 + pkg/proxy/io.go | 142 ++ pkg/proxy/service.go | 105 + pkg/proxy/tcp.go | 94 + pkg/qiniu/storage.go | 236 ++ pkg/rsa/rsa.go | 131 + pkg/shutdown/shutdown.go | 50 + pkg/signature/signature.go | 52 + pkg/signature/signature_generate.go | 62 + pkg/signature/signature_verify.go | 73 + pkg/sse/client.html | 26 + pkg/sse/server.go | 118 + pkg/ticker/ticker.go | 52 + pkg/time_parse/time_parse.go | 124 + pkg/token/README.md | 20 + pkg/token/token.go | 36 + pkg/token/token_jwt.go | 43 + pkg/token/token_url.go | 48 + pkg/trace/README.md | 81 + pkg/trace/debug.go | 7 + pkg/trace/dialog.go | 32 + pkg/trace/grpc.go | 17 + pkg/trace/redis.go | 10 + pkg/trace/sql.go | 9 + pkg/trace/trace.go | 154 ++ pkg/upload/tus.go | 228 ++ pkg/urltable/urltable.go | 169 ++ pkg/validator/rule.go | 32 + pkg/validator/validator.go | 55 + pkg/websocket/client/client.go | 242 ++ pkg/websocket/client/service/method.go | 28 + pkg/websocket/client/service/peer.go | 19 + pkg/websocket/client/service/receiver.go | 57 + pkg/websocket/codec/codec.go | 26 + pkg/websocket/codec/json/json.go | 97 + pkg/websocket/codec/meta.go | 43 + pkg/websocket/codec/protobuf/protobuf.go | 85 + .../codec/protobuf/protocol/base.pb.go | 226 ++ .../codec/protobuf/protocol/base.proto | 15 + pkg/websocket/peer/callback.go | 6 + pkg/websocket/peer/connect/acceptor.go | 98 + pkg/websocket/peer/connect/connection.go | 134 + pkg/websocket/peer/connection.go | 23 + pkg/websocket/peer/session.go | 16 + pkg/websocket/peer/session_manager.go | 119 + pkg/websocket/service/component.go | 27 + pkg/websocket/service/method.go | 33 + pkg/websocket/service/service.go | 98 + pkg/websocket/service/service_manager.go | 59 + pkg/websocket/service/session_callback.go | 102 + pkg/websocket/util/util.go | 11 + tool/helper.go | 328 +++ tool/http.go | 133 + 126 files changed, 15931 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 go.mod create mode 100644 go.sum create mode 100644 pkg/aes/aes.go create mode 100644 pkg/aes/padding.go create mode 100644 pkg/android_binary/apk/apk.go create mode 100644 pkg/android_binary/apk/apkxml.go create mode 100644 pkg/android_binary/apk/errors.go create mode 100644 pkg/android_binary/common.go create mode 100644 pkg/android_binary/table.go create mode 100644 pkg/android_binary/type.go create mode 100644 pkg/android_binary/xml.go create mode 100644 pkg/ants/pool.go create mode 100644 pkg/apollo/config.go create mode 100644 pkg/auth/config.go create mode 100644 pkg/auth/error.go create mode 100644 pkg/auth/generate.go create mode 100644 pkg/auth/jwt_access.go create mode 100644 pkg/auth/manager.go create mode 100644 pkg/auth/store.go create mode 100644 pkg/auth/token.go create mode 100644 pkg/bcrypt/password.go create mode 100644 pkg/browser/browser.go create mode 100644 pkg/cache/redis.go create mode 100644 pkg/captcha/base64.go create mode 100644 pkg/cidr/calc.go create mode 100644 pkg/cidr/ip.go create mode 100644 pkg/cmap/cmap.go create mode 100644 pkg/color/string_darwin.go create mode 100644 pkg/color/string_linux.go create mode 100644 pkg/color/string_windows.go create mode 100644 pkg/compress/compress.go create mode 100644 pkg/crontab/crontab.go create mode 100644 pkg/database/mongo.go create mode 100644 pkg/database/mysql.go create mode 100644 pkg/database/tool.go create mode 100644 pkg/database/trace.go create mode 100644 pkg/ddm/README.md create mode 100644 pkg/ddm/benchmark.go create mode 100644 pkg/ddm/mark.go create mode 100644 pkg/ddm/type.go create mode 100644 pkg/downloader/downloader.go create mode 100644 pkg/downloader/downloader_test.go create mode 100644 pkg/downloader/event.go create mode 100644 pkg/duration_fmt/fmt.go create mode 100644 pkg/duration_fmt/units.go create mode 100644 pkg/env/env.go create mode 100644 pkg/errno/errno.go create mode 100644 pkg/excel/export.go create mode 100644 pkg/file/file.go create mode 100644 pkg/grpclient/client.go create mode 100644 pkg/grpclient/keepalive.go create mode 100644 pkg/hash/hash.go create mode 100644 pkg/hash/hash_hashids.go create mode 100644 pkg/hmac/hmac.go create mode 100644 pkg/httpclient/alarm.go create mode 100644 pkg/httpclient/client.go create mode 100644 pkg/httpclient/error.go create mode 100644 pkg/httpclient/option.go create mode 100644 pkg/httpclient/retry.go create mode 100644 pkg/httpclient/util.go create mode 100644 pkg/limiter/rate.go create mode 100644 pkg/lock/lock.go create mode 100644 pkg/logger/logger.go create mode 100644 pkg/mail/mail.go create mode 100644 pkg/md5/md5.go create mode 100644 pkg/metrics/metrics.go create mode 100644 pkg/mux/context.go create mode 100644 pkg/mux/core.go create mode 100644 pkg/observable/iterable.go create mode 100644 pkg/observable/observable.go create mode 100644 pkg/observable/subscriber.go create mode 100644 pkg/p/print.go create mode 100644 pkg/proxy/io.go create mode 100644 pkg/proxy/service.go create mode 100644 pkg/proxy/tcp.go create mode 100644 pkg/qiniu/storage.go create mode 100644 pkg/rsa/rsa.go create mode 100644 pkg/shutdown/shutdown.go create mode 100644 pkg/signature/signature.go create mode 100644 pkg/signature/signature_generate.go create mode 100644 pkg/signature/signature_verify.go create mode 100644 pkg/sse/client.html create mode 100644 pkg/sse/server.go create mode 100644 pkg/ticker/ticker.go create mode 100644 pkg/time_parse/time_parse.go create mode 100644 pkg/token/README.md create mode 100644 pkg/token/token.go create mode 100644 pkg/token/token_jwt.go create mode 100644 pkg/token/token_url.go create mode 100644 pkg/trace/README.md create mode 100644 pkg/trace/debug.go create mode 100644 pkg/trace/dialog.go create mode 100644 pkg/trace/grpc.go create mode 100644 pkg/trace/redis.go create mode 100644 pkg/trace/sql.go create mode 100644 pkg/trace/trace.go create mode 100644 pkg/upload/tus.go create mode 100644 pkg/urltable/urltable.go create mode 100644 pkg/validator/rule.go create mode 100644 pkg/validator/validator.go create mode 100644 pkg/websocket/client/client.go create mode 100644 pkg/websocket/client/service/method.go create mode 100644 pkg/websocket/client/service/peer.go create mode 100644 pkg/websocket/client/service/receiver.go create mode 100644 pkg/websocket/codec/codec.go create mode 100644 pkg/websocket/codec/json/json.go create mode 100644 pkg/websocket/codec/meta.go create mode 100644 pkg/websocket/codec/protobuf/protobuf.go create mode 100644 pkg/websocket/codec/protobuf/protocol/base.pb.go create mode 100644 pkg/websocket/codec/protobuf/protocol/base.proto create mode 100644 pkg/websocket/peer/callback.go create mode 100644 pkg/websocket/peer/connect/acceptor.go create mode 100644 pkg/websocket/peer/connect/connection.go create mode 100644 pkg/websocket/peer/connection.go create mode 100644 pkg/websocket/peer/session.go create mode 100644 pkg/websocket/peer/session_manager.go create mode 100644 pkg/websocket/service/component.go create mode 100644 pkg/websocket/service/method.go create mode 100644 pkg/websocket/service/service.go create mode 100644 pkg/websocket/service/service_manager.go create mode 100644 pkg/websocket/service/session_callback.go create mode 100644 pkg/websocket/util/util.go create mode 100644 tool/helper.go create mode 100644 tool/http.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..49cdf9b --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +/.idea +/vendor + +*.yaml.json +*_test.go diff --git a/README.md b/README.md new file mode 100644 index 0000000..092e0e2 --- /dev/null +++ b/README.md @@ -0,0 +1,4 @@ +## Golang公共包 + +* pb协议文件生成命令 +`protoc --proto_path=pkg/websocket/codec/protobuf/protocol --go_out=pkg/websocket/codec/protobuf/protocol --go_opt=paths=source_relative base.proto` diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..c8fabba --- /dev/null +++ b/go.mod @@ -0,0 +1,119 @@ +module git.bvbej.com/bvbej/base-golang + +go 1.22.4 + +require ( + github.com/apolloconfig/agollo/v4 v4.4.0 + github.com/gin-contrib/pprof v1.5.0 + github.com/gin-gonic/gin v1.10.0 + github.com/go-playground/validator/v10 v10.22.0 + github.com/golang-jwt/jwt/v4 v4.5.0 + github.com/google/uuid v1.6.0 + github.com/gorilla/websocket v1.5.3 + github.com/jinzhu/now v1.1.5 + github.com/json-iterator/go v1.1.12 + github.com/mojocn/base64Captcha v1.3.6 + github.com/mritd/chinaid v1.0.4 + github.com/panjf2000/ants/v2 v2.10.0 + github.com/prometheus/client_golang v1.19.1 + github.com/qiniu/go-sdk/v7 v7.21.1 + github.com/redis/go-redis/v9 v9.5.3 + github.com/robfig/cron/v3 v3.0.1 + github.com/rs/cors v1.11.0 + github.com/rs/cors/wrapper/gin v0.0.0-20240515105523-1562b1715b35 + github.com/speps/go-hashids v2.0.0+incompatible + github.com/spf13/cast v1.6.0 + github.com/spf13/viper v1.19.0 + github.com/stretchr/testify v1.9.0 + github.com/tidwall/buntdb v1.3.1 + github.com/tidwall/gjson v1.17.1 + github.com/tus/tusd v1.13.0 + github.com/xuri/excelize/v2 v2.8.1 + go.mongodb.org/mongo-driver v1.15.1 + go.uber.org/atomic v1.11.0 + go.uber.org/multierr v1.11.0 + go.uber.org/zap v1.27.0 + golang.org/x/crypto v0.24.0 + golang.org/x/net v0.26.0 + golang.org/x/sync v0.7.0 + golang.org/x/time v0.5.0 + google.golang.org/grpc v1.64.0 + google.golang.org/protobuf v1.34.2 + gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df + gopkg.in/natefinch/lumberjack.v2 v2.2.1 + gorm.io/driver/mysql v1.5.7 + gorm.io/gorm v1.25.10 +) + +require ( + github.com/BurntSushi/toml v1.3.2 // indirect + github.com/alex-ant/gomath v0.0.0-20160516115720-89013a210a82 // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/bmizerany/pat v0.0.0-20170815010413-6226ea591a40 // indirect + github.com/bytedance/sonic v1.11.6 // indirect + github.com/bytedance/sonic/loader v0.1.1 // indirect + github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/cloudwego/base64x v0.1.4 // indirect + github.com/cloudwego/iasm v0.2.0 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect + github.com/fsnotify/fsnotify v1.7.0 // indirect + github.com/gabriel-vasile/mimetype v1.4.3 // indirect + github.com/gin-contrib/sse v0.1.0 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-sql-driver/mysql v1.7.0 // indirect + github.com/goccy/go-json v0.10.2 // indirect + github.com/gofrs/flock v0.8.1 // indirect + github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect + github.com/golang/snappy v0.0.4 // indirect + github.com/hashicorp/hcl v1.0.0 // indirect + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/klauspost/compress v1.17.2 // indirect + github.com/klauspost/cpuid/v2 v2.2.7 // indirect + github.com/leodido/go-urn v1.4.0 // indirect + github.com/magiconair/properties v1.8.7 // indirect + github.com/matishsiao/goInfo v0.0.0-20210923090445-da2e3fa8d45f // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect + github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe // indirect + github.com/pelletier/go-toml/v2 v2.2.2 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/prometheus/client_model v0.5.0 // indirect + github.com/prometheus/common v0.48.0 // indirect + github.com/prometheus/procfs v0.12.0 // indirect + github.com/richardlehane/mscfb v1.0.4 // indirect + github.com/richardlehane/msoleps v1.0.3 // indirect + github.com/sagikazarmark/locafero v0.4.0 // indirect + github.com/sagikazarmark/slog-shim v0.1.0 // indirect + github.com/sourcegraph/conc v0.3.0 // indirect + github.com/spf13/afero v1.11.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/subosito/gotenv v1.6.0 // indirect + github.com/tidwall/btree v1.4.2 // indirect + github.com/tidwall/grect v0.1.4 // indirect + github.com/tidwall/match v1.1.1 // indirect + github.com/tidwall/pretty v1.2.0 // indirect + github.com/tidwall/rtred v0.1.2 // indirect + github.com/tidwall/tinyqueue v0.1.1 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.2.12 // indirect + github.com/xdg-go/pbkdf2 v1.0.0 // indirect + github.com/xdg-go/scram v1.1.2 // indirect + github.com/xdg-go/stringprep v1.0.4 // indirect + github.com/xuri/efp v0.0.0-20231025114914-d1ff6096ae53 // indirect + github.com/xuri/nfp v0.0.0-20230919160717-d98342af3f05 // indirect + github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d // indirect + golang.org/x/arch v0.8.0 // indirect + golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect + golang.org/x/image v0.14.0 // indirect + golang.org/x/sys v0.21.0 // indirect + golang.org/x/text v0.16.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237 // indirect + gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect + gopkg.in/ini.v1 v1.67.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..1013a5b --- /dev/null +++ b/go.sum @@ -0,0 +1,2248 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= +cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= +cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= +cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= +cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= +cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= +cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= +cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= +cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= +cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= +cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY= +cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg= +cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8= +cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0= +cloud.google.com/go v0.83.0/go.mod h1:Z7MJUsANfY0pYPdw0lbnivPx4/vhy/e2FEkSkF7vAVY= +cloud.google.com/go v0.84.0/go.mod h1:RazrYuxIK6Kb7YrzzhPoLmCVzl7Sup4NrbKPg8KHSUM= +cloud.google.com/go v0.87.0/go.mod h1:TpDYlFy7vuLzZMMZ+B6iRiELaY7z/gJPaqbMx6mlWcY= +cloud.google.com/go v0.90.0/go.mod h1:kRX0mNRHe0e2rC6oNakvwQqzyDmg57xJ+SZU1eT2aDQ= +cloud.google.com/go v0.93.3/go.mod h1:8utlLll2EF5XMAV15woO4lSbWQlk8rer9aLOfLh7+YI= +cloud.google.com/go v0.94.1/go.mod h1:qAlAugsXlC+JWO+Bke5vCtc9ONxjQT3drlTTnAplMW4= +cloud.google.com/go v0.97.0/go.mod h1:GF7l59pYBVlXQIBLx3a761cZ41F9bBH3JUlihCt2Udc= +cloud.google.com/go v0.99.0/go.mod h1:w0Xx2nLzqWJPuozYQX+hFfCSI8WioryfRDzkoI/Y2ZA= +cloud.google.com/go v0.100.1/go.mod h1:fs4QogzfH5n2pBXBP9vRiU+eCny7lD2vmFZy79Iuw1U= +cloud.google.com/go v0.100.2/go.mod h1:4Xra9TjzAeYHrl5+oeLlzbM2k3mjVhZh4UqTZ//w99A= +cloud.google.com/go v0.102.0/go.mod h1:oWcCzKlqJ5zgHQt9YsaeTY9KzIvjyy0ArmiBUgpQ+nc= +cloud.google.com/go v0.102.1/go.mod h1:XZ77E9qnTEnrgEOvr4xzfdX5TRo7fB4T2F4O6+34hIU= +cloud.google.com/go v0.104.0/go.mod h1:OO6xxXdJyvuJPcEPBLN9BJPD+jep5G1+2U5B5gkRYtA= +cloud.google.com/go v0.105.0/go.mod h1:PrLgOJNe5nfE9UMxKxgXj4mD3voiP+YQ6gdt6KMFOKM= +cloud.google.com/go v0.107.0/go.mod h1:wpc2eNrD7hXUTy8EKS10jkxpZBjASrORK7goS+3YX2I= +cloud.google.com/go v0.110.0/go.mod h1:SJnCLqQ0FCFGSZMUNUf84MV3Aia54kn7pi8st7tMzaY= +cloud.google.com/go v0.110.2/go.mod h1:k04UEeEtb6ZBRTv3dZz4CeJC3jKGxyhl0sAiVVquxiw= +cloud.google.com/go v0.110.4/go.mod h1:+EYjdK8e5RME/VY/qLCAtuyALQ9q67dvuum8i+H5xsI= +cloud.google.com/go v0.110.6/go.mod h1:+EYjdK8e5RME/VY/qLCAtuyALQ9q67dvuum8i+H5xsI= +cloud.google.com/go/accessapproval v1.4.0/go.mod h1:zybIuC3KpDOvotz59lFe5qxRZx6C75OtwbisN56xYB4= +cloud.google.com/go/accessapproval v1.5.0/go.mod h1:HFy3tuiGvMdcd/u+Cu5b9NkO1pEICJ46IR82PoUdplw= +cloud.google.com/go/accessapproval v1.6.0/go.mod h1:R0EiYnwV5fsRFiKZkPHr6mwyk2wxUJ30nL4j2pcFY2E= +cloud.google.com/go/accessapproval v1.7.1/go.mod h1:JYczztsHRMK7NTXb6Xw+dwbs/WnOJxbo/2mTI+Kgg68= +cloud.google.com/go/accesscontextmanager v1.3.0/go.mod h1:TgCBehyr5gNMz7ZaH9xubp+CE8dkrszb4oK9CWyvD4o= +cloud.google.com/go/accesscontextmanager v1.4.0/go.mod h1:/Kjh7BBu/Gh83sv+K60vN9QE5NJcd80sU33vIe2IFPE= +cloud.google.com/go/accesscontextmanager v1.6.0/go.mod h1:8XCvZWfYw3K/ji0iVnp+6pu7huxoQTLmxAbVjbloTtM= +cloud.google.com/go/accesscontextmanager v1.7.0/go.mod h1:CEGLewx8dwa33aDAZQujl7Dx+uYhS0eay198wB/VumQ= +cloud.google.com/go/accesscontextmanager v1.8.0/go.mod h1:uI+AI/r1oyWK99NN8cQ3UK76AMelMzgZCvJfsi2c+ps= +cloud.google.com/go/accesscontextmanager v1.8.1/go.mod h1:JFJHfvuaTC+++1iL1coPiG1eu5D24db2wXCDWDjIrxo= +cloud.google.com/go/aiplatform v1.22.0/go.mod h1:ig5Nct50bZlzV6NvKaTwmplLLddFx0YReh9WfTO5jKw= +cloud.google.com/go/aiplatform v1.24.0/go.mod h1:67UUvRBKG6GTayHKV8DBv2RtR1t93YRu5B1P3x99mYY= +cloud.google.com/go/aiplatform v1.27.0/go.mod h1:Bvxqtl40l0WImSb04d0hXFU7gDOiq9jQmorivIiWcKg= +cloud.google.com/go/aiplatform v1.35.0/go.mod h1:7MFT/vCaOyZT/4IIFfxH4ErVg/4ku6lKv3w0+tFTgXQ= +cloud.google.com/go/aiplatform v1.36.1/go.mod h1:WTm12vJRPARNvJ+v6P52RDHCNe4AhvjcIZ/9/RRHy/k= +cloud.google.com/go/aiplatform v1.37.0/go.mod h1:IU2Cv29Lv9oCn/9LkFiiuKfwrRTq+QQMbW+hPCxJGZw= +cloud.google.com/go/aiplatform v1.45.0/go.mod h1:Iu2Q7sC7QGhXUeOhAj/oCK9a+ULz1O4AotZiqjQ8MYA= +cloud.google.com/go/aiplatform v1.48.0/go.mod h1:Iu2Q7sC7QGhXUeOhAj/oCK9a+ULz1O4AotZiqjQ8MYA= +cloud.google.com/go/analytics v0.11.0/go.mod h1:DjEWCu41bVbYcKyvlws9Er60YE4a//bK6mnhWvQeFNI= +cloud.google.com/go/analytics v0.12.0/go.mod h1:gkfj9h6XRf9+TS4bmuhPEShsh3hH8PAZzm/41OOhQd4= +cloud.google.com/go/analytics v0.17.0/go.mod h1:WXFa3WSym4IZ+JiKmavYdJwGG/CvpqiqczmL59bTD9M= +cloud.google.com/go/analytics v0.18.0/go.mod h1:ZkeHGQlcIPkw0R/GW+boWHhCOR43xz9RN/jn7WcqfIE= +cloud.google.com/go/analytics v0.19.0/go.mod h1:k8liqf5/HCnOUkbawNtrWWc+UAzyDlW89doe8TtoDsE= +cloud.google.com/go/analytics v0.21.2/go.mod h1:U8dcUtmDmjrmUTnnnRnI4m6zKn/yaA5N9RlEkYFHpQo= +cloud.google.com/go/analytics v0.21.3/go.mod h1:U8dcUtmDmjrmUTnnnRnI4m6zKn/yaA5N9RlEkYFHpQo= +cloud.google.com/go/apigateway v1.3.0/go.mod h1:89Z8Bhpmxu6AmUxuVRg/ECRGReEdiP3vQtk4Z1J9rJk= +cloud.google.com/go/apigateway v1.4.0/go.mod h1:pHVY9MKGaH9PQ3pJ4YLzoj6U5FUDeDFBllIz7WmzJoc= +cloud.google.com/go/apigateway v1.5.0/go.mod h1:GpnZR3Q4rR7LVu5951qfXPJCHquZt02jf7xQx7kpqN8= +cloud.google.com/go/apigateway v1.6.1/go.mod h1:ufAS3wpbRjqfZrzpvLC2oh0MFlpRJm2E/ts25yyqmXA= +cloud.google.com/go/apigeeconnect v1.3.0/go.mod h1:G/AwXFAKo0gIXkPTVfZDd2qA1TxBXJ3MgMRBQkIi9jc= +cloud.google.com/go/apigeeconnect v1.4.0/go.mod h1:kV4NwOKqjvt2JYR0AoIWo2QGfoRtn/pkS3QlHp0Ni04= +cloud.google.com/go/apigeeconnect v1.5.0/go.mod h1:KFaCqvBRU6idyhSNyn3vlHXc8VMDJdRmwDF6JyFRqZ8= +cloud.google.com/go/apigeeconnect v1.6.1/go.mod h1:C4awq7x0JpLtrlQCr8AzVIzAaYgngRqWf9S5Uhg+wWs= +cloud.google.com/go/apigeeregistry v0.4.0/go.mod h1:EUG4PGcsZvxOXAdyEghIdXwAEi/4MEaoqLMLDMIwKXY= +cloud.google.com/go/apigeeregistry v0.5.0/go.mod h1:YR5+s0BVNZfVOUkMa5pAR2xGd0A473vA5M7j247o1wM= +cloud.google.com/go/apigeeregistry v0.6.0/go.mod h1:BFNzW7yQVLZ3yj0TKcwzb8n25CFBri51GVGOEUcgQsc= +cloud.google.com/go/apigeeregistry v0.7.1/go.mod h1:1XgyjZye4Mqtw7T9TsY4NW10U7BojBvG4RMD+vRDrIw= +cloud.google.com/go/apikeys v0.4.0/go.mod h1:XATS/yqZbaBK0HOssf+ALHp8jAlNHUgyfprvNcBIszU= +cloud.google.com/go/apikeys v0.5.0/go.mod h1:5aQfwY4D+ewMMWScd3hm2en3hCj+BROlyrt3ytS7KLI= +cloud.google.com/go/apikeys v0.6.0/go.mod h1:kbpXu5upyiAlGkKrJgQl8A0rKNNJ7dQ377pdroRSSi8= +cloud.google.com/go/appengine v1.4.0/go.mod h1:CS2NhuBuDXM9f+qscZ6V86m1MIIqPj3WC/UoEuR1Sno= +cloud.google.com/go/appengine v1.5.0/go.mod h1:TfasSozdkFI0zeoxW3PTBLiNqRmzraodCWatWI9Dmak= +cloud.google.com/go/appengine v1.6.0/go.mod h1:hg6i0J/BD2cKmDJbaFSYHFyZkgBEfQrDg/X0V5fJn84= +cloud.google.com/go/appengine v1.7.0/go.mod h1:eZqpbHFCqRGa2aCdope7eC0SWLV1j0neb/QnMJVWx6A= +cloud.google.com/go/appengine v1.7.1/go.mod h1:IHLToyb/3fKutRysUlFO0BPt5j7RiQ45nrzEJmKTo6E= +cloud.google.com/go/appengine v1.8.1/go.mod h1:6NJXGLVhZCN9aQ/AEDvmfzKEfoYBlfB80/BHiKVputY= +cloud.google.com/go/area120 v0.5.0/go.mod h1:DE/n4mp+iqVyvxHN41Vf1CR602GiHQjFPusMFW6bGR4= +cloud.google.com/go/area120 v0.6.0/go.mod h1:39yFJqWVgm0UZqWTOdqkLhjoC7uFfgXRC8g/ZegeAh0= +cloud.google.com/go/area120 v0.7.0/go.mod h1:a3+8EUD1SX5RUcCs3MY5YasiO1z6yLiNLRiFrykbynY= +cloud.google.com/go/area120 v0.7.1/go.mod h1:j84i4E1RboTWjKtZVWXPqvK5VHQFJRF2c1Nm69pWm9k= +cloud.google.com/go/area120 v0.8.1/go.mod h1:BVfZpGpB7KFVNxPiQBuHkX6Ed0rS51xIgmGyjrAfzsg= +cloud.google.com/go/artifactregistry v1.6.0/go.mod h1:IYt0oBPSAGYj/kprzsBjZ/4LnG/zOcHyFHjWPCi6SAQ= +cloud.google.com/go/artifactregistry v1.7.0/go.mod h1:mqTOFOnGZx8EtSqK/ZWcsm/4U8B77rbcLP6ruDU2Ixk= +cloud.google.com/go/artifactregistry v1.8.0/go.mod h1:w3GQXkJX8hiKN0v+at4b0qotwijQbYUqF2GWkZzAhC0= +cloud.google.com/go/artifactregistry v1.9.0/go.mod h1:2K2RqvA2CYvAeARHRkLDhMDJ3OXy26h3XW+3/Jh2uYc= +cloud.google.com/go/artifactregistry v1.11.1/go.mod h1:lLYghw+Itq9SONbCa1YWBoWs1nOucMH0pwXN1rOBZFI= +cloud.google.com/go/artifactregistry v1.11.2/go.mod h1:nLZns771ZGAwVLzTX/7Al6R9ehma4WUEhZGWV6CeQNQ= +cloud.google.com/go/artifactregistry v1.12.0/go.mod h1:o6P3MIvtzTOnmvGagO9v/rOjjA0HmhJ+/6KAXrmYDCI= +cloud.google.com/go/artifactregistry v1.13.0/go.mod h1:uy/LNfoOIivepGhooAUpL1i30Hgee3Cu0l4VTWHUC08= +cloud.google.com/go/artifactregistry v1.14.1/go.mod h1:nxVdG19jTaSTu7yA7+VbWL346r3rIdkZ142BSQqhn5E= +cloud.google.com/go/asset v1.5.0/go.mod h1:5mfs8UvcM5wHhqtSv8J1CtxxaQq3AdBxxQi2jGW/K4o= +cloud.google.com/go/asset v1.7.0/go.mod h1:YbENsRK4+xTiL+Ofoj5Ckf+O17kJtgp3Y3nn4uzZz5s= +cloud.google.com/go/asset v1.8.0/go.mod h1:mUNGKhiqIdbr8X7KNayoYvyc4HbbFO9URsjbytpUaW0= +cloud.google.com/go/asset v1.9.0/go.mod h1:83MOE6jEJBMqFKadM9NLRcs80Gdw76qGuHn8m3h8oHQ= +cloud.google.com/go/asset v1.10.0/go.mod h1:pLz7uokL80qKhzKr4xXGvBQXnzHn5evJAEAtZiIb0wY= +cloud.google.com/go/asset v1.11.1/go.mod h1:fSwLhbRvC9p9CXQHJ3BgFeQNM4c9x10lqlrdEUYXlJo= +cloud.google.com/go/asset v1.12.0/go.mod h1:h9/sFOa4eDIyKmH6QMpm4eUK3pDojWnUhTgJlk762Hg= +cloud.google.com/go/asset v1.13.0/go.mod h1:WQAMyYek/b7NBpYq/K4KJWcRqzoalEsxz/t/dTk4THw= +cloud.google.com/go/asset v1.14.1/go.mod h1:4bEJ3dnHCqWCDbWJ/6Vn7GVI9LerSi7Rfdi03hd+WTQ= +cloud.google.com/go/assuredworkloads v1.5.0/go.mod h1:n8HOZ6pff6re5KYfBXcFvSViQjDwxFkAkmUFffJRbbY= +cloud.google.com/go/assuredworkloads v1.6.0/go.mod h1:yo2YOk37Yc89Rsd5QMVECvjaMKymF9OP+QXWlKXUkXw= +cloud.google.com/go/assuredworkloads v1.7.0/go.mod h1:z/736/oNmtGAyU47reJgGN+KVoYoxeLBoj4XkKYscNI= +cloud.google.com/go/assuredworkloads v1.8.0/go.mod h1:AsX2cqyNCOvEQC8RMPnoc0yEarXQk6WEKkxYfL6kGIo= +cloud.google.com/go/assuredworkloads v1.9.0/go.mod h1:kFuI1P78bplYtT77Tb1hi0FMxM0vVpRC7VVoJC3ZoT0= +cloud.google.com/go/assuredworkloads v1.10.0/go.mod h1:kwdUQuXcedVdsIaKgKTp9t0UJkE5+PAVNhdQm4ZVq2E= +cloud.google.com/go/assuredworkloads v1.11.1/go.mod h1:+F04I52Pgn5nmPG36CWFtxmav6+7Q+c5QyJoL18Lry0= +cloud.google.com/go/automl v1.5.0/go.mod h1:34EjfoFGMZ5sgJ9EoLsRtdPSNZLcfflJR39VbVNS2M0= +cloud.google.com/go/automl v1.6.0/go.mod h1:ugf8a6Fx+zP0D59WLhqgTDsQI9w07o64uf/Is3Nh5p8= +cloud.google.com/go/automl v1.7.0/go.mod h1:RL9MYCCsJEOmt0Wf3z9uzG0a7adTT1fe+aObgSpkCt8= +cloud.google.com/go/automl v1.8.0/go.mod h1:xWx7G/aPEe/NP+qzYXktoBSDfjO+vnKMGgsApGJJquM= +cloud.google.com/go/automl v1.12.0/go.mod h1:tWDcHDp86aMIuHmyvjuKeeHEGq76lD7ZqfGLN6B0NuU= +cloud.google.com/go/automl v1.13.1/go.mod h1:1aowgAHWYZU27MybSCFiukPO7xnyawv7pt3zK4bheQE= +cloud.google.com/go/baremetalsolution v0.3.0/go.mod h1:XOrocE+pvK1xFfleEnShBlNAXf+j5blPPxrhjKgnIFc= +cloud.google.com/go/baremetalsolution v0.4.0/go.mod h1:BymplhAadOO/eBa7KewQ0Ppg4A4Wplbn+PsFKRLo0uI= +cloud.google.com/go/baremetalsolution v0.5.0/go.mod h1:dXGxEkmR9BMwxhzBhV0AioD0ULBmuLZI8CdwalUxuss= +cloud.google.com/go/baremetalsolution v1.1.1/go.mod h1:D1AV6xwOksJMV4OSlWHtWuFNZZYujJknMAP4Qa27QIA= +cloud.google.com/go/batch v0.3.0/go.mod h1:TR18ZoAekj1GuirsUsR1ZTKN3FC/4UDnScjT8NXImFE= +cloud.google.com/go/batch v0.4.0/go.mod h1:WZkHnP43R/QCGQsZ+0JyG4i79ranE2u8xvjq/9+STPE= +cloud.google.com/go/batch v0.7.0/go.mod h1:vLZN95s6teRUqRQ4s3RLDsH8PvboqBK+rn1oevL159g= +cloud.google.com/go/batch v1.3.1/go.mod h1:VguXeQKXIYaeeIYbuozUmBR13AfL4SJP7IltNPS+A4A= +cloud.google.com/go/beyondcorp v0.2.0/go.mod h1:TB7Bd+EEtcw9PCPQhCJtJGjk/7TC6ckmnSFS+xwTfm4= +cloud.google.com/go/beyondcorp v0.3.0/go.mod h1:E5U5lcrcXMsCuoDNyGrpyTm/hn7ne941Jz2vmksAxW8= +cloud.google.com/go/beyondcorp v0.4.0/go.mod h1:3ApA0mbhHx6YImmuubf5pyW8srKnCEPON32/5hj+RmM= +cloud.google.com/go/beyondcorp v0.5.0/go.mod h1:uFqj9X+dSfrheVp7ssLTaRHd2EHqSL4QZmH4e8WXGGU= +cloud.google.com/go/beyondcorp v0.6.1/go.mod h1:YhxDWw946SCbmcWo3fAhw3V4XZMSpQ/VYfcKGAEU8/4= +cloud.google.com/go/beyondcorp v1.0.0/go.mod h1:YhxDWw946SCbmcWo3fAhw3V4XZMSpQ/VYfcKGAEU8/4= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= +cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= +cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= +cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= +cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= +cloud.google.com/go/bigquery v1.42.0/go.mod h1:8dRTJxhtG+vwBKzE5OseQn/hiydoQN3EedCaOdYmxRA= +cloud.google.com/go/bigquery v1.43.0/go.mod h1:ZMQcXHsl+xmU1z36G2jNGZmKp9zNY5BUua5wDgmNCfw= +cloud.google.com/go/bigquery v1.44.0/go.mod h1:0Y33VqXTEsbamHJvJHdFmtqHvMIY28aK1+dFsvaChGc= +cloud.google.com/go/bigquery v1.47.0/go.mod h1:sA9XOgy0A8vQK9+MWhEQTY6Tix87M/ZurWFIxmF9I/E= +cloud.google.com/go/bigquery v1.48.0/go.mod h1:QAwSz+ipNgfL5jxiaK7weyOhzdoAy1zFm0Nf1fysJac= +cloud.google.com/go/bigquery v1.49.0/go.mod h1:Sv8hMmTFFYBlt/ftw2uN6dFdQPzBlREY9yBh7Oy7/4Q= +cloud.google.com/go/bigquery v1.50.0/go.mod h1:YrleYEh2pSEbgTBZYMJ5SuSr0ML3ypjRB1zgf7pvQLU= +cloud.google.com/go/bigquery v1.52.0/go.mod h1:3b/iXjRQGU4nKa87cXeg6/gogLjO8C6PmuM8i5Bi/u4= +cloud.google.com/go/bigquery v1.53.0/go.mod h1:3b/iXjRQGU4nKa87cXeg6/gogLjO8C6PmuM8i5Bi/u4= +cloud.google.com/go/billing v1.4.0/go.mod h1:g9IdKBEFlItS8bTtlrZdVLWSSdSyFUZKXNS02zKMOZY= +cloud.google.com/go/billing v1.5.0/go.mod h1:mztb1tBc3QekhjSgmpf/CV4LzWXLzCArwpLmP2Gm88s= +cloud.google.com/go/billing v1.6.0/go.mod h1:WoXzguj+BeHXPbKfNWkqVtDdzORazmCjraY+vrxcyvI= +cloud.google.com/go/billing v1.7.0/go.mod h1:q457N3Hbj9lYwwRbnlD7vUpyjq6u5U1RAOArInEiD5Y= +cloud.google.com/go/billing v1.12.0/go.mod h1:yKrZio/eu+okO/2McZEbch17O5CB5NpZhhXG6Z766ss= +cloud.google.com/go/billing v1.13.0/go.mod h1:7kB2W9Xf98hP9Sr12KfECgfGclsH3CQR0R08tnRlRbc= +cloud.google.com/go/billing v1.16.0/go.mod h1:y8vx09JSSJG02k5QxbycNRrN7FGZB6F3CAcgum7jvGA= +cloud.google.com/go/binaryauthorization v1.1.0/go.mod h1:xwnoWu3Y84jbuHa0zd526MJYmtnVXn0syOjaJgy4+dM= +cloud.google.com/go/binaryauthorization v1.2.0/go.mod h1:86WKkJHtRcv5ViNABtYMhhNWRrD1Vpi//uKEy7aYEfI= +cloud.google.com/go/binaryauthorization v1.3.0/go.mod h1:lRZbKgjDIIQvzYQS1p99A7/U1JqvqeZg0wiI5tp6tg0= +cloud.google.com/go/binaryauthorization v1.4.0/go.mod h1:tsSPQrBd77VLplV70GUhBf/Zm3FsKmgSqgm4UmiDItk= +cloud.google.com/go/binaryauthorization v1.5.0/go.mod h1:OSe4OU1nN/VswXKRBmciKpo9LulY41gch5c68htf3/Q= +cloud.google.com/go/binaryauthorization v1.6.1/go.mod h1:TKt4pa8xhowwffiBmbrbcxijJRZED4zrqnwZ1lKH51U= +cloud.google.com/go/certificatemanager v1.3.0/go.mod h1:n6twGDvcUBFu9uBgt4eYvvf3sQ6My8jADcOVwHmzadg= +cloud.google.com/go/certificatemanager v1.4.0/go.mod h1:vowpercVFyqs8ABSmrdV+GiFf2H/ch3KyudYQEMM590= +cloud.google.com/go/certificatemanager v1.6.0/go.mod h1:3Hh64rCKjRAX8dXgRAyOcY5vQ/fE1sh8o+Mdd6KPgY8= +cloud.google.com/go/certificatemanager v1.7.1/go.mod h1:iW8J3nG6SaRYImIa+wXQ0g8IgoofDFRp5UMzaNk1UqI= +cloud.google.com/go/channel v1.8.0/go.mod h1:W5SwCXDJsq/rg3tn3oG0LOxpAo6IMxNa09ngphpSlnk= +cloud.google.com/go/channel v1.9.0/go.mod h1:jcu05W0my9Vx4mt3/rEHpfxc9eKi9XwsdDL8yBMbKUk= +cloud.google.com/go/channel v1.11.0/go.mod h1:IdtI0uWGqhEeatSB62VOoJ8FSUhJ9/+iGkJVqp74CGE= +cloud.google.com/go/channel v1.12.0/go.mod h1:VkxCGKASi4Cq7TbXxlaBezonAYpp1GCnKMY6tnMQnLU= +cloud.google.com/go/channel v1.16.0/go.mod h1:eN/q1PFSl5gyu0dYdmxNXscY/4Fi7ABmeHCJNf/oHmc= +cloud.google.com/go/cloudbuild v1.3.0/go.mod h1:WequR4ULxlqvMsjDEEEFnOG5ZSRSgWOywXYDb1vPE6U= +cloud.google.com/go/cloudbuild v1.4.0/go.mod h1:5Qwa40LHiOXmz3386FrjrYM93rM/hdRr7b53sySrTqA= +cloud.google.com/go/cloudbuild v1.6.0/go.mod h1:UIbc/w9QCbH12xX+ezUsgblrWv+Cv4Tw83GiSMHOn9M= +cloud.google.com/go/cloudbuild v1.7.0/go.mod h1:zb5tWh2XI6lR9zQmsm1VRA+7OCuve5d8S+zJUul8KTg= +cloud.google.com/go/cloudbuild v1.9.0/go.mod h1:qK1d7s4QlO0VwfYn5YuClDGg2hfmLZEb4wQGAbIgL1s= +cloud.google.com/go/cloudbuild v1.10.1/go.mod h1:lyJg7v97SUIPq4RC2sGsz/9tNczhyv2AjML/ci4ulzU= +cloud.google.com/go/cloudbuild v1.13.0/go.mod h1:lyJg7v97SUIPq4RC2sGsz/9tNczhyv2AjML/ci4ulzU= +cloud.google.com/go/clouddms v1.3.0/go.mod h1:oK6XsCDdW4Ib3jCCBugx+gVjevp2TMXFtgxvPSee3OM= +cloud.google.com/go/clouddms v1.4.0/go.mod h1:Eh7sUGCC+aKry14O1NRljhjyrr0NFC0G2cjwX0cByRk= +cloud.google.com/go/clouddms v1.5.0/go.mod h1:QSxQnhikCLUw13iAbffF2CZxAER3xDGNHjsTAkQJcQA= +cloud.google.com/go/clouddms v1.6.1/go.mod h1:Ygo1vL52Ov4TBZQquhz5fiw2CQ58gvu+PlS6PVXCpZI= +cloud.google.com/go/cloudtasks v1.5.0/go.mod h1:fD92REy1x5woxkKEkLdvavGnPJGEn8Uic9nWuLzqCpY= +cloud.google.com/go/cloudtasks v1.6.0/go.mod h1:C6Io+sxuke9/KNRkbQpihnW93SWDU3uXt92nu85HkYI= +cloud.google.com/go/cloudtasks v1.7.0/go.mod h1:ImsfdYWwlWNJbdgPIIGJWC+gemEGTBK/SunNQQNCAb4= +cloud.google.com/go/cloudtasks v1.8.0/go.mod h1:gQXUIwCSOI4yPVK7DgTVFiiP0ZW/eQkydWzwVMdHxrI= +cloud.google.com/go/cloudtasks v1.9.0/go.mod h1:w+EyLsVkLWHcOaqNEyvcKAsWp9p29dL6uL9Nst1cI7Y= +cloud.google.com/go/cloudtasks v1.10.0/go.mod h1:NDSoTLkZ3+vExFEWu2UJV1arUyzVDAiZtdWcsUyNwBs= +cloud.google.com/go/cloudtasks v1.11.1/go.mod h1:a9udmnou9KO2iulGscKR0qBYjreuX8oHwpmFsKspEvM= +cloud.google.com/go/cloudtasks v1.12.1/go.mod h1:a9udmnou9KO2iulGscKR0qBYjreuX8oHwpmFsKspEvM= +cloud.google.com/go/compute v0.1.0/go.mod h1:GAesmwr110a34z04OlxYkATPBEfVhkymfTBXtfbBFow= +cloud.google.com/go/compute v1.3.0/go.mod h1:cCZiE1NHEtai4wiufUhW8I8S1JKkAnhnQJWM7YD99wM= +cloud.google.com/go/compute v1.5.0/go.mod h1:9SMHyhJlzhlkJqrPAc839t2BZFTSk6Jdj6mkzQJeu0M= +cloud.google.com/go/compute v1.6.0/go.mod h1:T29tfhtVbq1wvAPo0E3+7vhgmkOYeXjhFvz/FMzPu0s= +cloud.google.com/go/compute v1.6.1/go.mod h1:g85FgpzFvNULZ+S8AYq87axRKuf2Kh7deLqV/jJ3thU= +cloud.google.com/go/compute v1.7.0/go.mod h1:435lt8av5oL9P3fv1OEzSbSUe+ybHXGMPQHHZWZxy9U= +cloud.google.com/go/compute v1.10.0/go.mod h1:ER5CLbMxl90o2jtNbGSbtfOpQKR0t15FOtRsugnLrlU= +cloud.google.com/go/compute v1.12.0/go.mod h1:e8yNOBcBONZU1vJKCvCoDw/4JQsA0dpM4x/6PIIOocU= +cloud.google.com/go/compute v1.12.1/go.mod h1:e8yNOBcBONZU1vJKCvCoDw/4JQsA0dpM4x/6PIIOocU= +cloud.google.com/go/compute v1.13.0/go.mod h1:5aPTS0cUNMIc1CE546K+Th6weJUNQErARyZtRXDJ8GE= +cloud.google.com/go/compute v1.14.0/go.mod h1:YfLtxrj9sU4Yxv+sXzZkyPjEyPBZfXHUvjxega5vAdo= +cloud.google.com/go/compute v1.15.1/go.mod h1:bjjoF/NtFUrkD/urWfdHaKuOPDR5nWIs63rR+SXhcpA= +cloud.google.com/go/compute v1.18.0/go.mod h1:1X7yHxec2Ga+Ss6jPyjxRxpu2uu7PLgsOVXvgU0yacs= +cloud.google.com/go/compute v1.19.0/go.mod h1:rikpw2y+UMidAe9tISo04EHNOIf42RLYF/q8Bs93scU= +cloud.google.com/go/compute v1.19.1/go.mod h1:6ylj3a05WF8leseCdIf77NK0g1ey+nj5IKd5/kvShxE= +cloud.google.com/go/compute v1.19.3/go.mod h1:qxvISKp/gYnXkSAD1ppcSOveRAmzxicEv/JlizULFrI= +cloud.google.com/go/compute v1.20.1/go.mod h1:4tCnrn48xsqlwSAiLf1HXMQk8CONslYbdiEZc9FEIbM= +cloud.google.com/go/compute v1.23.0/go.mod h1:4tCnrn48xsqlwSAiLf1HXMQk8CONslYbdiEZc9FEIbM= +cloud.google.com/go/compute/metadata v0.1.0/go.mod h1:Z1VN+bulIf6bt4P/C37K4DyZYZEXYonfTBHHFPO/4UU= +cloud.google.com/go/compute/metadata v0.2.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k= +cloud.google.com/go/compute/metadata v0.2.1/go.mod h1:jgHgmJd2RKBGzXqF5LR2EZMGxBkeanZ9wwa75XHJgOM= +cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= +cloud.google.com/go/contactcenterinsights v1.3.0/go.mod h1:Eu2oemoePuEFc/xKFPjbTuPSj0fYJcPls9TFlPNnHHY= +cloud.google.com/go/contactcenterinsights v1.4.0/go.mod h1:L2YzkGbPsv+vMQMCADxJoT9YiTTnSEd6fEvCeHTYVck= +cloud.google.com/go/contactcenterinsights v1.6.0/go.mod h1:IIDlT6CLcDoyv79kDv8iWxMSTZhLxSCofVV5W6YFM/w= +cloud.google.com/go/contactcenterinsights v1.9.1/go.mod h1:bsg/R7zGLYMVxFFzfh9ooLTruLRCG9fnzhH9KznHhbM= +cloud.google.com/go/contactcenterinsights v1.10.0/go.mod h1:bsg/R7zGLYMVxFFzfh9ooLTruLRCG9fnzhH9KznHhbM= +cloud.google.com/go/container v1.6.0/go.mod h1:Xazp7GjJSeUYo688S+6J5V+n/t+G5sKBTFkKNudGRxg= +cloud.google.com/go/container v1.7.0/go.mod h1:Dp5AHtmothHGX3DwwIHPgq45Y8KmNsgN3amoYfxVkLo= +cloud.google.com/go/container v1.13.1/go.mod h1:6wgbMPeQRw9rSnKBCAJXnds3Pzj03C4JHamr8asWKy4= +cloud.google.com/go/container v1.14.0/go.mod h1:3AoJMPhHfLDxLvrlVWaK57IXzaPnLaZq63WX59aQBfM= +cloud.google.com/go/container v1.15.0/go.mod h1:ft+9S0WGjAyjDggg5S06DXj+fHJICWg8L7isCQe9pQA= +cloud.google.com/go/container v1.22.1/go.mod h1:lTNExE2R7f+DLbAN+rJiKTisauFCaoDq6NURZ83eVH4= +cloud.google.com/go/container v1.24.0/go.mod h1:lTNExE2R7f+DLbAN+rJiKTisauFCaoDq6NURZ83eVH4= +cloud.google.com/go/containeranalysis v0.5.1/go.mod h1:1D92jd8gRR/c0fGMlymRgxWD3Qw9C1ff6/T7mLgVL8I= +cloud.google.com/go/containeranalysis v0.6.0/go.mod h1:HEJoiEIu+lEXM+k7+qLCci0h33lX3ZqoYFdmPcoO7s4= +cloud.google.com/go/containeranalysis v0.7.0/go.mod h1:9aUL+/vZ55P2CXfuZjS4UjQ9AgXoSw8Ts6lemfmxBxI= +cloud.google.com/go/containeranalysis v0.9.0/go.mod h1:orbOANbwk5Ejoom+s+DUCTTJ7IBdBQJDcSylAx/on9s= +cloud.google.com/go/containeranalysis v0.10.1/go.mod h1:Ya2jiILITMY68ZLPaogjmOMNkwsDrWBSTyBubGXO7j0= +cloud.google.com/go/datacatalog v1.3.0/go.mod h1:g9svFY6tuR+j+hrTw3J2dNcmI0dzmSiyOzm8kpLq0a0= +cloud.google.com/go/datacatalog v1.5.0/go.mod h1:M7GPLNQeLfWqeIm3iuiruhPzkt65+Bx8dAKvScX8jvs= +cloud.google.com/go/datacatalog v1.6.0/go.mod h1:+aEyF8JKg+uXcIdAmmaMUmZ3q1b/lKLtXCmXdnc0lbc= +cloud.google.com/go/datacatalog v1.7.0/go.mod h1:9mEl4AuDYWw81UGc41HonIHH7/sn52H0/tc8f8ZbZIE= +cloud.google.com/go/datacatalog v1.8.0/go.mod h1:KYuoVOv9BM8EYz/4eMFxrr4DUKhGIOXxZoKYF5wdISM= +cloud.google.com/go/datacatalog v1.8.1/go.mod h1:RJ58z4rMp3gvETA465Vg+ag8BGgBdnRPEMMSTr5Uv+M= +cloud.google.com/go/datacatalog v1.12.0/go.mod h1:CWae8rFkfp6LzLumKOnmVh4+Zle4A3NXLzVJ1d1mRm0= +cloud.google.com/go/datacatalog v1.13.0/go.mod h1:E4Rj9a5ZtAxcQJlEBTLgMTphfP11/lNaAshpoBgemX8= +cloud.google.com/go/datacatalog v1.14.0/go.mod h1:h0PrGtlihoutNMp/uvwhawLQ9+c63Kz65UFqh49Yo+E= +cloud.google.com/go/datacatalog v1.14.1/go.mod h1:d2CevwTG4yedZilwe+v3E3ZBDRMobQfSG/a6cCCN5R4= +cloud.google.com/go/datacatalog v1.16.0/go.mod h1:d2CevwTG4yedZilwe+v3E3ZBDRMobQfSG/a6cCCN5R4= +cloud.google.com/go/dataflow v0.6.0/go.mod h1:9QwV89cGoxjjSR9/r7eFDqqjtvbKxAK2BaYU6PVk9UM= +cloud.google.com/go/dataflow v0.7.0/go.mod h1:PX526vb4ijFMesO1o202EaUmouZKBpjHsTlCtB4parQ= +cloud.google.com/go/dataflow v0.8.0/go.mod h1:Rcf5YgTKPtQyYz8bLYhFoIV/vP39eL7fWNcSOyFfLJE= +cloud.google.com/go/dataflow v0.9.1/go.mod h1:Wp7s32QjYuQDWqJPFFlnBKhkAtiFpMTdg00qGbnIHVw= +cloud.google.com/go/dataform v0.3.0/go.mod h1:cj8uNliRlHpa6L3yVhDOBrUXH+BPAO1+KFMQQNSThKo= +cloud.google.com/go/dataform v0.4.0/go.mod h1:fwV6Y4Ty2yIFL89huYlEkwUPtS7YZinZbzzj5S9FzCE= +cloud.google.com/go/dataform v0.5.0/go.mod h1:GFUYRe8IBa2hcomWplodVmUx/iTL0FrsauObOM3Ipr0= +cloud.google.com/go/dataform v0.6.0/go.mod h1:QPflImQy33e29VuapFdf19oPbE4aYTJxr31OAPV+ulA= +cloud.google.com/go/dataform v0.7.0/go.mod h1:7NulqnVozfHvWUBpMDfKMUESr+85aJsC/2O0o3jWPDE= +cloud.google.com/go/dataform v0.8.1/go.mod h1:3BhPSiw8xmppbgzeBbmDvmSWlwouuJkXsXsb8UBih9M= +cloud.google.com/go/datafusion v1.4.0/go.mod h1:1Zb6VN+W6ALo85cXnM1IKiPw+yQMKMhB9TsTSRDo/38= +cloud.google.com/go/datafusion v1.5.0/go.mod h1:Kz+l1FGHB0J+4XF2fud96WMmRiq/wj8N9u007vyXZ2w= +cloud.google.com/go/datafusion v1.6.0/go.mod h1:WBsMF8F1RhSXvVM8rCV3AeyWVxcC2xY6vith3iw3S+8= +cloud.google.com/go/datafusion v1.7.1/go.mod h1:KpoTBbFmoToDExJUso/fcCiguGDk7MEzOWXUsJo0wsI= +cloud.google.com/go/datalabeling v0.5.0/go.mod h1:TGcJ0G2NzcsXSE/97yWjIZO0bXj0KbVlINXMG9ud42I= +cloud.google.com/go/datalabeling v0.6.0/go.mod h1:WqdISuk/+WIGeMkpw/1q7bK/tFEZxsrFJOJdY2bXvTQ= +cloud.google.com/go/datalabeling v0.7.0/go.mod h1:WPQb1y08RJbmpM3ww0CSUAGweL0SxByuW2E+FU+wXcM= +cloud.google.com/go/datalabeling v0.8.1/go.mod h1:XS62LBSVPbYR54GfYQsPXZjTW8UxCK2fkDciSrpRFdY= +cloud.google.com/go/dataplex v1.3.0/go.mod h1:hQuRtDg+fCiFgC8j0zV222HvzFQdRd+SVX8gdmFcZzA= +cloud.google.com/go/dataplex v1.4.0/go.mod h1:X51GfLXEMVJ6UN47ESVqvlsRplbLhcsAt0kZCCKsU0A= +cloud.google.com/go/dataplex v1.5.2/go.mod h1:cVMgQHsmfRoI5KFYq4JtIBEUbYwc3c7tXmIDhRmNNVQ= +cloud.google.com/go/dataplex v1.6.0/go.mod h1:bMsomC/aEJOSpHXdFKFGQ1b0TDPIeL28nJObeO1ppRs= +cloud.google.com/go/dataplex v1.8.1/go.mod h1:7TyrDT6BCdI8/38Uvp0/ZxBslOslP2X2MPDucliyvSE= +cloud.google.com/go/dataplex v1.9.0/go.mod h1:7TyrDT6BCdI8/38Uvp0/ZxBslOslP2X2MPDucliyvSE= +cloud.google.com/go/dataproc v1.7.0/go.mod h1:CKAlMjII9H90RXaMpSxQ8EU6dQx6iAYNPcYPOkSbi8s= +cloud.google.com/go/dataproc v1.8.0/go.mod h1:5OW+zNAH0pMpw14JVrPONsxMQYMBqJuzORhIBfBn9uI= +cloud.google.com/go/dataproc v1.12.0/go.mod h1:zrF3aX0uV3ikkMz6z4uBbIKyhRITnxvr4i3IjKsKrw4= +cloud.google.com/go/dataproc/v2 v2.0.1/go.mod h1:7Ez3KRHdFGcfY7GcevBbvozX+zyWGcwLJvvAMwCaoZ4= +cloud.google.com/go/dataqna v0.5.0/go.mod h1:90Hyk596ft3zUQ8NkFfvICSIfHFh1Bc7C4cK3vbhkeo= +cloud.google.com/go/dataqna v0.6.0/go.mod h1:1lqNpM7rqNLVgWBJyk5NF6Uen2PHym0jtVJonplVsDA= +cloud.google.com/go/dataqna v0.7.0/go.mod h1:Lx9OcIIeqCrw1a6KdO3/5KMP1wAmTc0slZWwP12Qq3c= +cloud.google.com/go/dataqna v0.8.1/go.mod h1:zxZM0Bl6liMePWsHA8RMGAfmTG34vJMapbHAxQ5+WA8= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= +cloud.google.com/go/datastore v1.10.0/go.mod h1:PC5UzAmDEkAmkfaknstTYbNpgE49HAgW2J1gcgUfmdM= +cloud.google.com/go/datastore v1.11.0/go.mod h1:TvGxBIHCS50u8jzG+AW/ppf87v1of8nwzFNgEZU1D3c= +cloud.google.com/go/datastore v1.12.0/go.mod h1:KjdB88W897MRITkvWWJrg2OUtrR5XVj1EoLgSp6/N70= +cloud.google.com/go/datastore v1.12.1/go.mod h1:KjdB88W897MRITkvWWJrg2OUtrR5XVj1EoLgSp6/N70= +cloud.google.com/go/datastore v1.13.0/go.mod h1:KjdB88W897MRITkvWWJrg2OUtrR5XVj1EoLgSp6/N70= +cloud.google.com/go/datastream v1.2.0/go.mod h1:i/uTP8/fZwgATHS/XFu0TcNUhuA0twZxxQ3EyCUQMwo= +cloud.google.com/go/datastream v1.3.0/go.mod h1:cqlOX8xlyYF/uxhiKn6Hbv6WjwPPuI9W2M9SAXwaLLQ= +cloud.google.com/go/datastream v1.4.0/go.mod h1:h9dpzScPhDTs5noEMQVWP8Wx8AFBRyS0s8KWPx/9r0g= +cloud.google.com/go/datastream v1.5.0/go.mod h1:6TZMMNPwjUqZHBKPQ1wwXpb0d5VDVPl2/XoS5yi88q4= +cloud.google.com/go/datastream v1.6.0/go.mod h1:6LQSuswqLa7S4rPAOZFVjHIG3wJIjZcZrw8JDEDJuIs= +cloud.google.com/go/datastream v1.7.0/go.mod h1:uxVRMm2elUSPuh65IbZpzJNMbuzkcvu5CjMqVIUHrww= +cloud.google.com/go/datastream v1.9.1/go.mod h1:hqnmr8kdUBmrnk65k5wNRoHSCYksvpdZIcZIEl8h43Q= +cloud.google.com/go/datastream v1.10.0/go.mod h1:hqnmr8kdUBmrnk65k5wNRoHSCYksvpdZIcZIEl8h43Q= +cloud.google.com/go/deploy v1.4.0/go.mod h1:5Xghikd4VrmMLNaF6FiRFDlHb59VM59YoDQnOUdsH/c= +cloud.google.com/go/deploy v1.5.0/go.mod h1:ffgdD0B89tToyW/U/D2eL0jN2+IEV/3EMuXHA0l4r+s= +cloud.google.com/go/deploy v1.6.0/go.mod h1:f9PTHehG/DjCom3QH0cntOVRm93uGBDt2vKzAPwpXQI= +cloud.google.com/go/deploy v1.8.0/go.mod h1:z3myEJnA/2wnB4sgjqdMfgxCA0EqC3RBTNcVPs93mtQ= +cloud.google.com/go/deploy v1.11.0/go.mod h1:tKuSUV5pXbn67KiubiUNUejqLs4f5cxxiCNCeyl0F2g= +cloud.google.com/go/deploy v1.13.0/go.mod h1:tKuSUV5pXbn67KiubiUNUejqLs4f5cxxiCNCeyl0F2g= +cloud.google.com/go/dialogflow v1.15.0/go.mod h1:HbHDWs33WOGJgn6rfzBW1Kv807BE3O1+xGbn59zZWI4= +cloud.google.com/go/dialogflow v1.16.1/go.mod h1:po6LlzGfK+smoSmTBnbkIZY2w8ffjz/RcGSS+sh1el0= +cloud.google.com/go/dialogflow v1.17.0/go.mod h1:YNP09C/kXA1aZdBgC/VtXX74G/TKn7XVCcVumTflA+8= +cloud.google.com/go/dialogflow v1.18.0/go.mod h1:trO7Zu5YdyEuR+BhSNOqJezyFQ3aUzz0njv7sMx/iek= +cloud.google.com/go/dialogflow v1.19.0/go.mod h1:JVmlG1TwykZDtxtTXujec4tQ+D8SBFMoosgy+6Gn0s0= +cloud.google.com/go/dialogflow v1.29.0/go.mod h1:b+2bzMe+k1s9V+F2jbJwpHPzrnIyHihAdRFMtn2WXuM= +cloud.google.com/go/dialogflow v1.31.0/go.mod h1:cuoUccuL1Z+HADhyIA7dci3N5zUssgpBJmCzI6fNRB4= +cloud.google.com/go/dialogflow v1.32.0/go.mod h1:jG9TRJl8CKrDhMEcvfcfFkkpp8ZhgPz3sBGmAUYJ2qE= +cloud.google.com/go/dialogflow v1.38.0/go.mod h1:L7jnH+JL2mtmdChzAIcXQHXMvQkE3U4hTaNltEuxXn4= +cloud.google.com/go/dialogflow v1.40.0/go.mod h1:L7jnH+JL2mtmdChzAIcXQHXMvQkE3U4hTaNltEuxXn4= +cloud.google.com/go/dlp v1.6.0/go.mod h1:9eyB2xIhpU0sVwUixfBubDoRwP+GjeUoxxeueZmqvmM= +cloud.google.com/go/dlp v1.7.0/go.mod h1:68ak9vCiMBjbasxeVD17hVPxDEck+ExiHavX8kiHG+Q= +cloud.google.com/go/dlp v1.9.0/go.mod h1:qdgmqgTyReTz5/YNSSuueR8pl7hO0o9bQ39ZhtgkWp4= +cloud.google.com/go/dlp v1.10.1/go.mod h1:IM8BWz1iJd8njcNcG0+Kyd9OPnqnRNkDV8j42VT5KOI= +cloud.google.com/go/documentai v1.7.0/go.mod h1:lJvftZB5NRiFSX4moiye1SMxHx0Bc3x1+p9e/RfXYiU= +cloud.google.com/go/documentai v1.8.0/go.mod h1:xGHNEB7CtsnySCNrCFdCyyMz44RhFEEX2Q7UD0c5IhU= +cloud.google.com/go/documentai v1.9.0/go.mod h1:FS5485S8R00U10GhgBC0aNGrJxBP8ZVpEeJ7PQDZd6k= +cloud.google.com/go/documentai v1.10.0/go.mod h1:vod47hKQIPeCfN2QS/jULIvQTugbmdc0ZvxxfQY1bg4= +cloud.google.com/go/documentai v1.16.0/go.mod h1:o0o0DLTEZ+YnJZ+J4wNfTxmDVyrkzFvttBXXtYRMHkM= +cloud.google.com/go/documentai v1.18.0/go.mod h1:F6CK6iUH8J81FehpskRmhLq/3VlwQvb7TvwOceQ2tbs= +cloud.google.com/go/documentai v1.20.0/go.mod h1:yJkInoMcK0qNAEdRnqY/D5asy73tnPe88I1YTZT+a8E= +cloud.google.com/go/documentai v1.22.0/go.mod h1:yJkInoMcK0qNAEdRnqY/D5asy73tnPe88I1YTZT+a8E= +cloud.google.com/go/domains v0.6.0/go.mod h1:T9Rz3GasrpYk6mEGHh4rymIhjlnIuB4ofT1wTxDeT4Y= +cloud.google.com/go/domains v0.7.0/go.mod h1:PtZeqS1xjnXuRPKE/88Iru/LdfoRyEHYA9nFQf4UKpg= +cloud.google.com/go/domains v0.8.0/go.mod h1:M9i3MMDzGFXsydri9/vW+EWz9sWb4I6WyHqdlAk0idE= +cloud.google.com/go/domains v0.9.1/go.mod h1:aOp1c0MbejQQ2Pjf1iJvnVyT+z6R6s8pX66KaCSDYfE= +cloud.google.com/go/edgecontainer v0.1.0/go.mod h1:WgkZ9tp10bFxqO8BLPqv2LlfmQF1X8lZqwW4r1BTajk= +cloud.google.com/go/edgecontainer v0.2.0/go.mod h1:RTmLijy+lGpQ7BXuTDa4C4ssxyXT34NIuHIgKuP4s5w= +cloud.google.com/go/edgecontainer v0.3.0/go.mod h1:FLDpP4nykgwwIfcLt6zInhprzw0lEi2P1fjO6Ie0qbc= +cloud.google.com/go/edgecontainer v1.0.0/go.mod h1:cttArqZpBB2q58W/upSG++ooo6EsblxDIolxa3jSjbY= +cloud.google.com/go/edgecontainer v1.1.1/go.mod h1:O5bYcS//7MELQZs3+7mabRqoWQhXCzenBu0R8bz2rwk= +cloud.google.com/go/errorreporting v0.3.0/go.mod h1:xsP2yaAp+OAW4OIm60An2bbLpqIhKXdWR/tawvl7QzU= +cloud.google.com/go/essentialcontacts v1.3.0/go.mod h1:r+OnHa5jfj90qIfZDO/VztSFqbQan7HV75p8sA+mdGI= +cloud.google.com/go/essentialcontacts v1.4.0/go.mod h1:8tRldvHYsmnBCHdFpvU+GL75oWiBKl80BiqlFh9tp+8= +cloud.google.com/go/essentialcontacts v1.5.0/go.mod h1:ay29Z4zODTuwliK7SnX8E86aUF2CTzdNtvv42niCX0M= +cloud.google.com/go/essentialcontacts v1.6.2/go.mod h1:T2tB6tX+TRak7i88Fb2N9Ok3PvY3UNbUsMag9/BARh4= +cloud.google.com/go/eventarc v1.7.0/go.mod h1:6ctpF3zTnaQCxUjHUdcfgcA1A2T309+omHZth7gDfmc= +cloud.google.com/go/eventarc v1.8.0/go.mod h1:imbzxkyAU4ubfsaKYdQg04WS1NvncblHEup4kvF+4gw= +cloud.google.com/go/eventarc v1.10.0/go.mod h1:u3R35tmZ9HvswGRBnF48IlYgYeBcPUCjkr4BTdem2Kw= +cloud.google.com/go/eventarc v1.11.0/go.mod h1:PyUjsUKPWoRBCHeOxZd/lbOOjahV41icXyUY5kSTvVY= +cloud.google.com/go/eventarc v1.12.1/go.mod h1:mAFCW6lukH5+IZjkvrEss+jmt2kOdYlN8aMx3sRJiAI= +cloud.google.com/go/eventarc v1.13.0/go.mod h1:mAFCW6lukH5+IZjkvrEss+jmt2kOdYlN8aMx3sRJiAI= +cloud.google.com/go/filestore v1.3.0/go.mod h1:+qbvHGvXU1HaKX2nD0WEPo92TP/8AQuCVEBXNY9z0+w= +cloud.google.com/go/filestore v1.4.0/go.mod h1:PaG5oDfo9r224f8OYXURtAsY+Fbyq/bLYoINEK8XQAI= +cloud.google.com/go/filestore v1.5.0/go.mod h1:FqBXDWBp4YLHqRnVGveOkHDf8svj9r5+mUDLupOWEDs= +cloud.google.com/go/filestore v1.6.0/go.mod h1:di5unNuss/qfZTw2U9nhFqo8/ZDSc466dre85Kydllg= +cloud.google.com/go/filestore v1.7.1/go.mod h1:y10jsorq40JJnjR/lQ8AfFbbcGlw3g+Dp8oN7i7FjV4= +cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= +cloud.google.com/go/firestore v1.9.0/go.mod h1:HMkjKHNTtRyZNiMzu7YAsLr9K3X2udY2AMwDaMEQiiE= +cloud.google.com/go/firestore v1.11.0/go.mod h1:b38dKhgzlmNNGTNZZwe7ZRFEuRab1Hay3/DBsIGKKy4= +cloud.google.com/go/functions v1.6.0/go.mod h1:3H1UA3qiIPRWD7PeZKLvHZ9SaQhR26XIJcC0A5GbvAk= +cloud.google.com/go/functions v1.7.0/go.mod h1:+d+QBcWM+RsrgZfV9xo6KfA1GlzJfxcfZcRPEhDDfzg= +cloud.google.com/go/functions v1.8.0/go.mod h1:RTZ4/HsQjIqIYP9a9YPbU+QFoQsAlYgrwOXJWHn1POY= +cloud.google.com/go/functions v1.9.0/go.mod h1:Y+Dz8yGguzO3PpIjhLTbnqV1CWmgQ5UwtlpzoyquQ08= +cloud.google.com/go/functions v1.10.0/go.mod h1:0D3hEOe3DbEvCXtYOZHQZmD+SzYsi1YbI7dGvHfldXw= +cloud.google.com/go/functions v1.12.0/go.mod h1:AXWGrF3e2C/5ehvwYo/GH6O5s09tOPksiKhz+hH8WkA= +cloud.google.com/go/functions v1.13.0/go.mod h1:EU4O007sQm6Ef/PwRsI8N2umygGqPBS/IZQKBQBcJ3c= +cloud.google.com/go/functions v1.15.1/go.mod h1:P5yNWUTkyU+LvW/S9O6V+V423VZooALQlqoXdoPz5AE= +cloud.google.com/go/gaming v1.5.0/go.mod h1:ol7rGcxP/qHTRQE/RO4bxkXq+Fix0j6D4LFPzYTIrDM= +cloud.google.com/go/gaming v1.6.0/go.mod h1:YMU1GEvA39Qt3zWGyAVA9bpYz/yAhTvaQ1t2sK4KPUA= +cloud.google.com/go/gaming v1.7.0/go.mod h1:LrB8U7MHdGgFG851iHAfqUdLcKBdQ55hzXy9xBJz0+w= +cloud.google.com/go/gaming v1.8.0/go.mod h1:xAqjS8b7jAVW0KFYeRUxngo9My3f33kFmua++Pi+ggM= +cloud.google.com/go/gaming v1.9.0/go.mod h1:Fc7kEmCObylSWLO334NcO+O9QMDyz+TKC4v1D7X+Bc0= +cloud.google.com/go/gaming v1.10.1/go.mod h1:XQQvtfP8Rb9Rxnxm5wFVpAp9zCQkJi2bLIb7iHGwB3s= +cloud.google.com/go/gkebackup v0.2.0/go.mod h1:XKvv/4LfG829/B8B7xRkk8zRrOEbKtEam6yNfuQNH60= +cloud.google.com/go/gkebackup v0.3.0/go.mod h1:n/E671i1aOQvUxT541aTkCwExO/bTer2HDlj4TsBRAo= +cloud.google.com/go/gkebackup v0.4.0/go.mod h1:byAyBGUwYGEEww7xsbnUTBHIYcOPy/PgUWUtOeRm9Vg= +cloud.google.com/go/gkebackup v1.3.0/go.mod h1:vUDOu++N0U5qs4IhG1pcOnD1Mac79xWy6GoBFlWCWBU= +cloud.google.com/go/gkeconnect v0.5.0/go.mod h1:c5lsNAg5EwAy7fkqX/+goqFsU1Da/jQFqArp+wGNr/o= +cloud.google.com/go/gkeconnect v0.6.0/go.mod h1:Mln67KyU/sHJEBY8kFZ0xTeyPtzbq9StAVvEULYK16A= +cloud.google.com/go/gkeconnect v0.7.0/go.mod h1:SNfmVqPkaEi3bF/B3CNZOAYPYdg7sU+obZ+QTky2Myw= +cloud.google.com/go/gkeconnect v0.8.1/go.mod h1:KWiK1g9sDLZqhxB2xEuPV8V9NYzrqTUmQR9shJHpOZw= +cloud.google.com/go/gkehub v0.9.0/go.mod h1:WYHN6WG8w9bXU0hqNxt8rm5uxnk8IH+lPY9J2TV7BK0= +cloud.google.com/go/gkehub v0.10.0/go.mod h1:UIPwxI0DsrpsVoWpLB0stwKCP+WFVG9+y977wO+hBH0= +cloud.google.com/go/gkehub v0.11.0/go.mod h1:JOWHlmN+GHyIbuWQPl47/C2RFhnFKH38jH9Ascu3n0E= +cloud.google.com/go/gkehub v0.12.0/go.mod h1:djiIwwzTTBrF5NaXCGv3mf7klpEMcST17VBTVVDcuaw= +cloud.google.com/go/gkehub v0.14.1/go.mod h1:VEXKIJZ2avzrbd7u+zeMtW00Y8ddk/4V9511C9CQGTY= +cloud.google.com/go/gkemulticloud v0.3.0/go.mod h1:7orzy7O0S+5kq95e4Hpn7RysVA7dPs8W/GgfUtsPbrA= +cloud.google.com/go/gkemulticloud v0.4.0/go.mod h1:E9gxVBnseLWCk24ch+P9+B2CoDFJZTyIgLKSalC7tuI= +cloud.google.com/go/gkemulticloud v0.5.0/go.mod h1:W0JDkiyi3Tqh0TJr//y19wyb1yf8llHVto2Htf2Ja3Y= +cloud.google.com/go/gkemulticloud v0.6.1/go.mod h1:kbZ3HKyTsiwqKX7Yw56+wUGwwNZViRnxWK2DVknXWfw= +cloud.google.com/go/gkemulticloud v1.0.0/go.mod h1:kbZ3HKyTsiwqKX7Yw56+wUGwwNZViRnxWK2DVknXWfw= +cloud.google.com/go/grafeas v0.2.0/go.mod h1:KhxgtF2hb0P191HlY5besjYm6MqTSTj3LSI+M+ByZHc= +cloud.google.com/go/grafeas v0.3.0/go.mod h1:P7hgN24EyONOTMyeJH6DxG4zD7fwiYa5Q6GUgyFSOU8= +cloud.google.com/go/gsuiteaddons v1.3.0/go.mod h1:EUNK/J1lZEZO8yPtykKxLXI6JSVN2rg9bN8SXOa0bgM= +cloud.google.com/go/gsuiteaddons v1.4.0/go.mod h1:rZK5I8hht7u7HxFQcFei0+AtfS9uSushomRlg+3ua1o= +cloud.google.com/go/gsuiteaddons v1.5.0/go.mod h1:TFCClYLd64Eaa12sFVmUyG62tk4mdIsI7pAnSXRkcFo= +cloud.google.com/go/gsuiteaddons v1.6.1/go.mod h1:CodrdOqRZcLp5WOwejHWYBjZvfY0kOphkAKpF/3qdZY= +cloud.google.com/go/iam v0.1.0/go.mod h1:vcUNEa0pEm0qRVpmWepWaFMIAI8/hjB9mO8rNCJtF6c= +cloud.google.com/go/iam v0.3.0/go.mod h1:XzJPvDayI+9zsASAFO68Hk07u3z+f+JrT2xXNdp4bnY= +cloud.google.com/go/iam v0.5.0/go.mod h1:wPU9Vt0P4UmCux7mqtRu6jcpPAb74cP1fh50J3QpkUc= +cloud.google.com/go/iam v0.6.0/go.mod h1:+1AH33ueBne5MzYccyMHtEKqLE4/kJOibtffMHDMFMc= +cloud.google.com/go/iam v0.7.0/go.mod h1:H5Br8wRaDGNc8XP3keLc4unfUUZeyH3Sfl9XpQEYOeg= +cloud.google.com/go/iam v0.8.0/go.mod h1:lga0/y3iH6CX7sYqypWJ33hf7kkfXJag67naqGESjkE= +cloud.google.com/go/iam v0.11.0/go.mod h1:9PiLDanza5D+oWFZiH1uG+RnRCfEGKoyl6yo4cgWZGY= +cloud.google.com/go/iam v0.12.0/go.mod h1:knyHGviacl11zrtZUoDuYpDgLjvr28sLQaG0YB2GYAY= +cloud.google.com/go/iam v0.13.0/go.mod h1:ljOg+rcNfzZ5d6f1nAUJ8ZIxOaZUVoS14bKCtaLZ/D0= +cloud.google.com/go/iam v1.0.1/go.mod h1:yR3tmSL8BcZB4bxByRv2jkSIahVmCtfKZwLYGBalRE8= +cloud.google.com/go/iam v1.1.0/go.mod h1:nxdHjaKfCr7fNYx/HJMM8LgiMugmveWlkatear5gVyk= +cloud.google.com/go/iam v1.1.1/go.mod h1:A5avdyVL2tCppe4unb0951eI9jreack+RJ0/d+KUZOU= +cloud.google.com/go/iap v1.4.0/go.mod h1:RGFwRJdihTINIe4wZ2iCP0zF/qu18ZwyKxrhMhygBEc= +cloud.google.com/go/iap v1.5.0/go.mod h1:UH/CGgKd4KyohZL5Pt0jSKE4m3FR51qg6FKQ/z/Ix9A= +cloud.google.com/go/iap v1.6.0/go.mod h1:NSuvI9C/j7UdjGjIde7t7HBz+QTwBcapPE07+sSRcLk= +cloud.google.com/go/iap v1.7.0/go.mod h1:beqQx56T9O1G1yNPph+spKpNibDlYIiIixiqsQXxLIo= +cloud.google.com/go/iap v1.7.1/go.mod h1:WapEwPc7ZxGt2jFGB/C/bm+hP0Y6NXzOYGjpPnmMS74= +cloud.google.com/go/iap v1.8.1/go.mod h1:sJCbeqg3mvWLqjZNsI6dfAtbbV1DL2Rl7e1mTyXYREQ= +cloud.google.com/go/ids v1.1.0/go.mod h1:WIuwCaYVOzHIj2OhN9HAwvW+DBdmUAdcWlFxRl+KubM= +cloud.google.com/go/ids v1.2.0/go.mod h1:5WXvp4n25S0rA/mQWAg1YEEBBq6/s+7ml1RDCW1IrcY= +cloud.google.com/go/ids v1.3.0/go.mod h1:JBdTYwANikFKaDP6LtW5JAi4gubs57SVNQjemdt6xV4= +cloud.google.com/go/ids v1.4.1/go.mod h1:np41ed8YMU8zOgv53MMMoCntLTn2lF+SUzlM+O3u/jw= +cloud.google.com/go/iot v1.3.0/go.mod h1:r7RGh2B61+B8oz0AGE+J72AhA0G7tdXItODWsaA2oLs= +cloud.google.com/go/iot v1.4.0/go.mod h1:dIDxPOn0UvNDUMD8Ger7FIaTuvMkj+aGk94RPP0iV+g= +cloud.google.com/go/iot v1.5.0/go.mod h1:mpz5259PDl3XJthEmh9+ap0affn/MqNSP4My77Qql9o= +cloud.google.com/go/iot v1.6.0/go.mod h1:IqdAsmE2cTYYNO1Fvjfzo9po179rAtJeVGUvkLN3rLE= +cloud.google.com/go/iot v1.7.1/go.mod h1:46Mgw7ev1k9KqK1ao0ayW9h0lI+3hxeanz+L1zmbbbk= +cloud.google.com/go/kms v1.4.0/go.mod h1:fajBHndQ+6ubNw6Ss2sSd+SWvjL26RNo/dr7uxsnnOA= +cloud.google.com/go/kms v1.5.0/go.mod h1:QJS2YY0eJGBg3mnDfuaCyLauWwBJiHRboYxJ++1xJNg= +cloud.google.com/go/kms v1.6.0/go.mod h1:Jjy850yySiasBUDi6KFUwUv2n1+o7QZFyuUJg6OgjA0= +cloud.google.com/go/kms v1.8.0/go.mod h1:4xFEhYFqvW+4VMELtZyxomGSYtSQKzM178ylFW4jMAg= +cloud.google.com/go/kms v1.9.0/go.mod h1:qb1tPTgfF9RQP8e1wq4cLFErVuTJv7UsSC915J8dh3w= +cloud.google.com/go/kms v1.10.0/go.mod h1:ng3KTUtQQU9bPX3+QGLsflZIHlkbn8amFAMY63m8d24= +cloud.google.com/go/kms v1.10.1/go.mod h1:rIWk/TryCkR59GMC3YtHtXeLzd634lBbKenvyySAyYI= +cloud.google.com/go/kms v1.11.0/go.mod h1:hwdiYC0xjnWsKQQCQQmIQnS9asjYVSK6jtXm+zFqXLM= +cloud.google.com/go/kms v1.12.1/go.mod h1:c9J991h5DTl+kg7gi3MYomh12YEENGrf48ee/N/2CDM= +cloud.google.com/go/kms v1.15.0/go.mod h1:c9J991h5DTl+kg7gi3MYomh12YEENGrf48ee/N/2CDM= +cloud.google.com/go/language v1.4.0/go.mod h1:F9dRpNFQmJbkaop6g0JhSBXCNlO90e1KWx5iDdxbWic= +cloud.google.com/go/language v1.6.0/go.mod h1:6dJ8t3B+lUYfStgls25GusK04NLh3eDLQnWM3mdEbhI= +cloud.google.com/go/language v1.7.0/go.mod h1:DJ6dYN/W+SQOjF8e1hLQXMF21AkH2w9wiPzPCJa2MIE= +cloud.google.com/go/language v1.8.0/go.mod h1:qYPVHf7SPoNNiCL2Dr0FfEFNil1qi3pQEyygwpgVKB8= +cloud.google.com/go/language v1.9.0/go.mod h1:Ns15WooPM5Ad/5no/0n81yUetis74g3zrbeJBE+ptUY= +cloud.google.com/go/language v1.10.1/go.mod h1:CPp94nsdVNiQEt1CNjF5WkTcisLiHPyIbMhvR8H2AW0= +cloud.google.com/go/lifesciences v0.5.0/go.mod h1:3oIKy8ycWGPUyZDR/8RNnTOYevhaMLqh5vLUXs9zvT8= +cloud.google.com/go/lifesciences v0.6.0/go.mod h1:ddj6tSX/7BOnhxCSd3ZcETvtNr8NZ6t/iPhY2Tyfu08= +cloud.google.com/go/lifesciences v0.8.0/go.mod h1:lFxiEOMqII6XggGbOnKiyZ7IBwoIqA84ClvoezaA/bo= +cloud.google.com/go/lifesciences v0.9.1/go.mod h1:hACAOd1fFbCGLr/+weUKRAJas82Y4vrL3O5326N//Wc= +cloud.google.com/go/logging v1.6.1/go.mod h1:5ZO0mHHbvm8gEmeEUHrmDlTDSu5imF6MUP9OfilNXBw= +cloud.google.com/go/logging v1.7.0/go.mod h1:3xjP2CjkM3ZkO73aj4ASA5wRPGGCRrPIAeNqVNkzY8M= +cloud.google.com/go/longrunning v0.1.1/go.mod h1:UUFxuDWkv22EuY93jjmDMFT5GPQKeFVJBIF6QlTqdsE= +cloud.google.com/go/longrunning v0.3.0/go.mod h1:qth9Y41RRSUE69rDcOn6DdK3HfQfsUI0YSmW3iIlLJc= +cloud.google.com/go/longrunning v0.4.1/go.mod h1:4iWDqhBZ70CvZ6BfETbvam3T8FMvLK+eFj0E6AaRQTo= +cloud.google.com/go/longrunning v0.4.2/go.mod h1:OHrnaYyLUV6oqwh0xiS7e5sLQhP1m0QU9R+WhGDMgIQ= +cloud.google.com/go/longrunning v0.5.0/go.mod h1:0JNuqRShmscVAhIACGtskSAWtqtOoPkwP0YF1oVEchc= +cloud.google.com/go/longrunning v0.5.1/go.mod h1:spvimkwdz6SPWKEt/XBij79E9fiTkHSQl/fRUUQJYJc= +cloud.google.com/go/managedidentities v1.3.0/go.mod h1:UzlW3cBOiPrzucO5qWkNkh0w33KFtBJU281hacNvsdE= +cloud.google.com/go/managedidentities v1.4.0/go.mod h1:NWSBYbEMgqmbZsLIyKvxrYbtqOsxY1ZrGM+9RgDqInM= +cloud.google.com/go/managedidentities v1.5.0/go.mod h1:+dWcZ0JlUmpuxpIDfyP5pP5y0bLdRwOS4Lp7gMni/LA= +cloud.google.com/go/managedidentities v1.6.1/go.mod h1:h/irGhTN2SkZ64F43tfGPMbHnypMbu4RB3yl8YcuEak= +cloud.google.com/go/maps v0.1.0/go.mod h1:BQM97WGyfw9FWEmQMpZ5T6cpovXXSd1cGmFma94eubI= +cloud.google.com/go/maps v0.6.0/go.mod h1:o6DAMMfb+aINHz/p/jbcY+mYeXBoZoxTfdSQ8VAJaCw= +cloud.google.com/go/maps v0.7.0/go.mod h1:3GnvVl3cqeSvgMcpRlQidXsPYuDGQ8naBis7MVzpXsY= +cloud.google.com/go/maps v1.3.0/go.mod h1:6mWTUv+WhnOwAgjVsSW2QPPECmW+s3PcRyOa9vgG/5s= +cloud.google.com/go/maps v1.4.0/go.mod h1:6mWTUv+WhnOwAgjVsSW2QPPECmW+s3PcRyOa9vgG/5s= +cloud.google.com/go/mediatranslation v0.5.0/go.mod h1:jGPUhGTybqsPQn91pNXw0xVHfuJ3leR1wj37oU3y1f4= +cloud.google.com/go/mediatranslation v0.6.0/go.mod h1:hHdBCTYNigsBxshbznuIMFNe5QXEowAuNmmC7h8pu5w= +cloud.google.com/go/mediatranslation v0.7.0/go.mod h1:LCnB/gZr90ONOIQLgSXagp8XUW1ODs2UmUMvcgMfI2I= +cloud.google.com/go/mediatranslation v0.8.1/go.mod h1:L/7hBdEYbYHQJhX2sldtTO5SZZ1C1vkapubj0T2aGig= +cloud.google.com/go/memcache v1.4.0/go.mod h1:rTOfiGZtJX1AaFUrOgsMHX5kAzaTQ8azHiuDoTPzNsE= +cloud.google.com/go/memcache v1.5.0/go.mod h1:dk3fCK7dVo0cUU2c36jKb4VqKPS22BTkf81Xq617aWM= +cloud.google.com/go/memcache v1.6.0/go.mod h1:XS5xB0eQZdHtTuTF9Hf8eJkKtR3pVRCcvJwtm68T3rA= +cloud.google.com/go/memcache v1.7.0/go.mod h1:ywMKfjWhNtkQTxrWxCkCFkoPjLHPW6A7WOTVI8xy3LY= +cloud.google.com/go/memcache v1.9.0/go.mod h1:8oEyzXCu+zo9RzlEaEjHl4KkgjlNDaXbCQeQWlzNFJM= +cloud.google.com/go/memcache v1.10.1/go.mod h1:47YRQIarv4I3QS5+hoETgKO40InqzLP6kpNLvyXuyaA= +cloud.google.com/go/metastore v1.5.0/go.mod h1:2ZNrDcQwghfdtCwJ33nM0+GrBGlVuh8rakL3vdPY3XY= +cloud.google.com/go/metastore v1.6.0/go.mod h1:6cyQTls8CWXzk45G55x57DVQ9gWg7RiH65+YgPsNh9s= +cloud.google.com/go/metastore v1.7.0/go.mod h1:s45D0B4IlsINu87/AsWiEVYbLaIMeUSoxlKKDqBGFS8= +cloud.google.com/go/metastore v1.8.0/go.mod h1:zHiMc4ZUpBiM7twCIFQmJ9JMEkDSyZS9U12uf7wHqSI= +cloud.google.com/go/metastore v1.10.0/go.mod h1:fPEnH3g4JJAk+gMRnrAnoqyv2lpUCqJPWOodSaf45Eo= +cloud.google.com/go/metastore v1.11.1/go.mod h1:uZuSo80U3Wd4zi6C22ZZliOUJ3XeM/MlYi/z5OAOWRA= +cloud.google.com/go/metastore v1.12.0/go.mod h1:uZuSo80U3Wd4zi6C22ZZliOUJ3XeM/MlYi/z5OAOWRA= +cloud.google.com/go/monitoring v1.7.0/go.mod h1:HpYse6kkGo//7p6sT0wsIC6IBDET0RhIsnmlA53dvEk= +cloud.google.com/go/monitoring v1.8.0/go.mod h1:E7PtoMJ1kQXWxPjB6mv2fhC5/15jInuulFdYYtlcvT4= +cloud.google.com/go/monitoring v1.12.0/go.mod h1:yx8Jj2fZNEkL/GYZyTLS4ZtZEZN8WtDEiEqG4kLK50w= +cloud.google.com/go/monitoring v1.13.0/go.mod h1:k2yMBAB1H9JT/QETjNkgdCGD9bPF712XiLTVr+cBrpw= +cloud.google.com/go/monitoring v1.15.1/go.mod h1:lADlSAlFdbqQuwwpaImhsJXu1QSdd3ojypXrFSMr2rM= +cloud.google.com/go/networkconnectivity v1.4.0/go.mod h1:nOl7YL8odKyAOtzNX73/M5/mGZgqqMeryi6UPZTk/rA= +cloud.google.com/go/networkconnectivity v1.5.0/go.mod h1:3GzqJx7uhtlM3kln0+x5wyFvuVH1pIBJjhCpjzSt75o= +cloud.google.com/go/networkconnectivity v1.6.0/go.mod h1:OJOoEXW+0LAxHh89nXd64uGG+FbQoeH8DtxCHVOMlaM= +cloud.google.com/go/networkconnectivity v1.7.0/go.mod h1:RMuSbkdbPwNMQjB5HBWD5MpTBnNm39iAVpC3TmsExt8= +cloud.google.com/go/networkconnectivity v1.10.0/go.mod h1:UP4O4sWXJG13AqrTdQCD9TnLGEbtNRqjuaaA7bNjF5E= +cloud.google.com/go/networkconnectivity v1.11.0/go.mod h1:iWmDD4QF16VCDLXUqvyspJjIEtBR/4zq5hwnY2X3scM= +cloud.google.com/go/networkconnectivity v1.12.1/go.mod h1:PelxSWYM7Sh9/guf8CFhi6vIqf19Ir/sbfZRUwXh92E= +cloud.google.com/go/networkmanagement v1.4.0/go.mod h1:Q9mdLLRn60AsOrPc8rs8iNV6OHXaGcDdsIQe1ohekq8= +cloud.google.com/go/networkmanagement v1.5.0/go.mod h1:ZnOeZ/evzUdUsnvRt792H0uYEnHQEMaz+REhhzJRcf4= +cloud.google.com/go/networkmanagement v1.6.0/go.mod h1:5pKPqyXjB/sgtvB5xqOemumoQNB7y95Q7S+4rjSOPYY= +cloud.google.com/go/networkmanagement v1.8.0/go.mod h1:Ho/BUGmtyEqrttTgWEe7m+8vDdK74ibQc+Be0q7Fof0= +cloud.google.com/go/networksecurity v0.5.0/go.mod h1:xS6fOCoqpVC5zx15Z/MqkfDwH4+m/61A3ODiDV1xmiQ= +cloud.google.com/go/networksecurity v0.6.0/go.mod h1:Q5fjhTr9WMI5mbpRYEbiexTzROf7ZbDzvzCrNl14nyU= +cloud.google.com/go/networksecurity v0.7.0/go.mod h1:mAnzoxx/8TBSyXEeESMy9OOYwo1v+gZ5eMRnsT5bC8k= +cloud.google.com/go/networksecurity v0.8.0/go.mod h1:B78DkqsxFG5zRSVuwYFRZ9Xz8IcQ5iECsNrPn74hKHU= +cloud.google.com/go/networksecurity v0.9.1/go.mod h1:MCMdxOKQ30wsBI1eI659f9kEp4wuuAueoC9AJKSPWZQ= +cloud.google.com/go/notebooks v1.2.0/go.mod h1:9+wtppMfVPUeJ8fIWPOq1UnATHISkGXGqTkxeieQ6UY= +cloud.google.com/go/notebooks v1.3.0/go.mod h1:bFR5lj07DtCPC7YAAJ//vHskFBxA5JzYlH68kXVdk34= +cloud.google.com/go/notebooks v1.4.0/go.mod h1:4QPMngcwmgb6uw7Po99B2xv5ufVoIQ7nOGDyL4P8AgA= +cloud.google.com/go/notebooks v1.5.0/go.mod h1:q8mwhnP9aR8Hpfnrc5iN5IBhrXUy8S2vuYs+kBJ/gu0= +cloud.google.com/go/notebooks v1.7.0/go.mod h1:PVlaDGfJgj1fl1S3dUwhFMXFgfYGhYQt2164xOMONmE= +cloud.google.com/go/notebooks v1.8.0/go.mod h1:Lq6dYKOYOWUCTvw5t2q1gp1lAp0zxAxRycayS0iJcqQ= +cloud.google.com/go/notebooks v1.9.1/go.mod h1:zqG9/gk05JrzgBt4ghLzEepPHNwE5jgPcHZRKhlC1A8= +cloud.google.com/go/optimization v1.1.0/go.mod h1:5po+wfvX5AQlPznyVEZjGJTMr4+CAkJf2XSTQOOl9l4= +cloud.google.com/go/optimization v1.2.0/go.mod h1:Lr7SOHdRDENsh+WXVmQhQTrzdu9ybg0NecjHidBq6xs= +cloud.google.com/go/optimization v1.3.1/go.mod h1:IvUSefKiwd1a5p0RgHDbWCIbDFgKuEdB+fPPuP0IDLI= +cloud.google.com/go/optimization v1.4.1/go.mod h1:j64vZQP7h9bO49m2rVaTVoNM0vEBEN5eKPUPbZyXOrk= +cloud.google.com/go/orchestration v1.3.0/go.mod h1:Sj5tq/JpWiB//X/q3Ngwdl5K7B7Y0KZ7bfv0wL6fqVA= +cloud.google.com/go/orchestration v1.4.0/go.mod h1:6W5NLFWs2TlniBphAViZEVhrXRSMgUGDfW7vrWKvsBk= +cloud.google.com/go/orchestration v1.6.0/go.mod h1:M62Bevp7pkxStDfFfTuCOaXgaaqRAga1yKyoMtEoWPQ= +cloud.google.com/go/orchestration v1.8.1/go.mod h1:4sluRF3wgbYVRqz7zJ1/EUNc90TTprliq9477fGobD8= +cloud.google.com/go/orgpolicy v1.4.0/go.mod h1:xrSLIV4RePWmP9P3tBl8S93lTmlAxjm06NSm2UTmKvE= +cloud.google.com/go/orgpolicy v1.5.0/go.mod h1:hZEc5q3wzwXJaKrsx5+Ewg0u1LxJ51nNFlext7Tanwc= +cloud.google.com/go/orgpolicy v1.10.0/go.mod h1:w1fo8b7rRqlXlIJbVhOMPrwVljyuW5mqssvBtU18ONc= +cloud.google.com/go/orgpolicy v1.11.0/go.mod h1:2RK748+FtVvnfuynxBzdnyu7sygtoZa1za/0ZfpOs1M= +cloud.google.com/go/orgpolicy v1.11.1/go.mod h1:8+E3jQcpZJQliP+zaFfayC2Pg5bmhuLK755wKhIIUCE= +cloud.google.com/go/osconfig v1.7.0/go.mod h1:oVHeCeZELfJP7XLxcBGTMBvRO+1nQ5tFG9VQTmYS2Fs= +cloud.google.com/go/osconfig v1.8.0/go.mod h1:EQqZLu5w5XA7eKizepumcvWx+m8mJUhEwiPqWiZeEdg= +cloud.google.com/go/osconfig v1.9.0/go.mod h1:Yx+IeIZJ3bdWmzbQU4fxNl8xsZ4amB+dygAwFPlvnNo= +cloud.google.com/go/osconfig v1.10.0/go.mod h1:uMhCzqC5I8zfD9zDEAfvgVhDS8oIjySWh+l4WK6GnWw= +cloud.google.com/go/osconfig v1.11.0/go.mod h1:aDICxrur2ogRd9zY5ytBLV89KEgT2MKB2L/n6x1ooPw= +cloud.google.com/go/osconfig v1.12.0/go.mod h1:8f/PaYzoS3JMVfdfTubkowZYGmAhUCjjwnjqWI7NVBc= +cloud.google.com/go/osconfig v1.12.1/go.mod h1:4CjBxND0gswz2gfYRCUoUzCm9zCABp91EeTtWXyz0tE= +cloud.google.com/go/oslogin v1.4.0/go.mod h1:YdgMXWRaElXz/lDk1Na6Fh5orF7gvmJ0FGLIs9LId4E= +cloud.google.com/go/oslogin v1.5.0/go.mod h1:D260Qj11W2qx/HVF29zBg+0fd6YCSjSqLUkY/qEenQU= +cloud.google.com/go/oslogin v1.6.0/go.mod h1:zOJ1O3+dTU8WPlGEkFSh7qeHPPSoxrcMbbK1Nm2iX70= +cloud.google.com/go/oslogin v1.7.0/go.mod h1:e04SN0xO1UNJ1M5GP0vzVBFicIe4O53FOfcixIqTyXo= +cloud.google.com/go/oslogin v1.9.0/go.mod h1:HNavntnH8nzrn8JCTT5fj18FuJLFJc4NaZJtBnQtKFs= +cloud.google.com/go/oslogin v1.10.1/go.mod h1:x692z7yAue5nE7CsSnoG0aaMbNoRJRXO4sn73R+ZqAs= +cloud.google.com/go/phishingprotection v0.5.0/go.mod h1:Y3HZknsK9bc9dMi+oE8Bim0lczMU6hrX0UpADuMefr0= +cloud.google.com/go/phishingprotection v0.6.0/go.mod h1:9Y3LBLgy0kDTcYET8ZH3bq/7qni15yVUoAxiFxnlSUA= +cloud.google.com/go/phishingprotection v0.7.0/go.mod h1:8qJI4QKHoda/sb/7/YmMQ2omRLSLYSu9bU0EKCNI+Lk= +cloud.google.com/go/phishingprotection v0.8.1/go.mod h1:AxonW7GovcA8qdEk13NfHq9hNx5KPtfxXNeUxTDxB6I= +cloud.google.com/go/policytroubleshooter v1.3.0/go.mod h1:qy0+VwANja+kKrjlQuOzmlvscn4RNsAc0e15GGqfMxg= +cloud.google.com/go/policytroubleshooter v1.4.0/go.mod h1:DZT4BcRw3QoO8ota9xw/LKtPa8lKeCByYeKTIf/vxdE= +cloud.google.com/go/policytroubleshooter v1.5.0/go.mod h1:Rz1WfV+1oIpPdN2VvvuboLVRsB1Hclg3CKQ53j9l8vw= +cloud.google.com/go/policytroubleshooter v1.6.0/go.mod h1:zYqaPTsmfvpjm5ULxAyD/lINQxJ0DDsnWOP/GZ7xzBc= +cloud.google.com/go/policytroubleshooter v1.7.1/go.mod h1:0NaT5v3Ag1M7U5r0GfDCpUFkWd9YqpubBWsQlhanRv0= +cloud.google.com/go/policytroubleshooter v1.8.0/go.mod h1:tmn5Ir5EToWe384EuboTcVQT7nTag2+DuH3uHmKd1HU= +cloud.google.com/go/privatecatalog v0.5.0/go.mod h1:XgosMUvvPyxDjAVNDYxJ7wBW8//hLDDYmnsNcMGq1K0= +cloud.google.com/go/privatecatalog v0.6.0/go.mod h1:i/fbkZR0hLN29eEWiiwue8Pb+GforiEIBnV9yrRUOKI= +cloud.google.com/go/privatecatalog v0.7.0/go.mod h1:2s5ssIFO69F5csTXcwBP7NPFTZvps26xGzvQ2PQaBYg= +cloud.google.com/go/privatecatalog v0.8.0/go.mod h1:nQ6pfaegeDAq/Q5lrfCQzQLhubPiZhSaNhIgfJlnIXs= +cloud.google.com/go/privatecatalog v0.9.1/go.mod h1:0XlDXW2unJXdf9zFz968Hp35gl/bhF4twwpXZAW50JA= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= +cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= +cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= +cloud.google.com/go/pubsub v1.26.0/go.mod h1:QgBH3U/jdJy/ftjPhTkyXNj543Tin1pRYcdcPRnFIRI= +cloud.google.com/go/pubsub v1.27.1/go.mod h1:hQN39ymbV9geqBnfQq6Xf63yNhUAhv9CZhzp5O6qsW0= +cloud.google.com/go/pubsub v1.28.0/go.mod h1:vuXFpwaVoIPQMGXqRyUQigu/AX1S3IWugR9xznmcXX8= +cloud.google.com/go/pubsub v1.30.0/go.mod h1:qWi1OPS0B+b5L+Sg6Gmc9zD1Y+HaM0MdUr7LsupY1P4= +cloud.google.com/go/pubsub v1.32.0/go.mod h1:f+w71I33OMyxf9VpMVcZbnG5KSUkCOUHYpFd5U1GdRc= +cloud.google.com/go/pubsub v1.33.0/go.mod h1:f+w71I33OMyxf9VpMVcZbnG5KSUkCOUHYpFd5U1GdRc= +cloud.google.com/go/pubsublite v1.5.0/go.mod h1:xapqNQ1CuLfGi23Yda/9l4bBCKz/wC3KIJ5gKcxveZg= +cloud.google.com/go/pubsublite v1.6.0/go.mod h1:1eFCS0U11xlOuMFV/0iBqw3zP12kddMeCbj/F3FSj9k= +cloud.google.com/go/pubsublite v1.7.0/go.mod h1:8hVMwRXfDfvGm3fahVbtDbiLePT3gpoiJYJY+vxWxVM= +cloud.google.com/go/pubsublite v1.8.1/go.mod h1:fOLdU4f5xldK4RGJrBMm+J7zMWNj/k4PxwEZXy39QS0= +cloud.google.com/go/recaptchaenterprise v1.3.1/go.mod h1:OdD+q+y4XGeAlxRaMn1Y7/GveP6zmq76byL6tjPE7d4= +cloud.google.com/go/recaptchaenterprise/v2 v2.1.0/go.mod h1:w9yVqajwroDNTfGuhmOjPDN//rZGySaf6PtFVcSCa7o= +cloud.google.com/go/recaptchaenterprise/v2 v2.2.0/go.mod h1:/Zu5jisWGeERrd5HnlS3EUGb/D335f9k51B/FVil0jk= +cloud.google.com/go/recaptchaenterprise/v2 v2.3.0/go.mod h1:O9LwGCjrhGHBQET5CA7dd5NwwNQUErSgEDit1DLNTdo= +cloud.google.com/go/recaptchaenterprise/v2 v2.4.0/go.mod h1:Am3LHfOuBstrLrNCBrlI5sbwx9LBg3te2N6hGvHn2mE= +cloud.google.com/go/recaptchaenterprise/v2 v2.5.0/go.mod h1:O8LzcHXN3rz0j+LBC91jrwI3R+1ZSZEWrfL7XHgNo9U= +cloud.google.com/go/recaptchaenterprise/v2 v2.6.0/go.mod h1:RPauz9jeLtB3JVzg6nCbe12qNoaa8pXc4d/YukAmcnA= +cloud.google.com/go/recaptchaenterprise/v2 v2.7.0/go.mod h1:19wVj/fs5RtYtynAPJdDTb69oW0vNHYDBTbB4NvMD9c= +cloud.google.com/go/recaptchaenterprise/v2 v2.7.2/go.mod h1:kR0KjsJS7Jt1YSyWFkseQ756D45kaYNTlDPPaRAvDBU= +cloud.google.com/go/recommendationengine v0.5.0/go.mod h1:E5756pJcVFeVgaQv3WNpImkFP8a+RptV6dDLGPILjvg= +cloud.google.com/go/recommendationengine v0.6.0/go.mod h1:08mq2umu9oIqc7tDy8sx+MNJdLG0fUi3vaSVbztHgJ4= +cloud.google.com/go/recommendationengine v0.7.0/go.mod h1:1reUcE3GIu6MeBz/h5xZJqNLuuVjNg1lmWMPyjatzac= +cloud.google.com/go/recommendationengine v0.8.1/go.mod h1:MrZihWwtFYWDzE6Hz5nKcNz3gLizXVIDI/o3G1DLcrE= +cloud.google.com/go/recommender v1.5.0/go.mod h1:jdoeiBIVrJe9gQjwd759ecLJbxCDED4A6p+mqoqDvTg= +cloud.google.com/go/recommender v1.6.0/go.mod h1:+yETpm25mcoiECKh9DEScGzIRyDKpZ0cEhWGo+8bo+c= +cloud.google.com/go/recommender v1.7.0/go.mod h1:XLHs/W+T8olwlGOgfQenXBTbIseGclClff6lhFVe9Bs= +cloud.google.com/go/recommender v1.8.0/go.mod h1:PkjXrTT05BFKwxaUxQmtIlrtj0kph108r02ZZQ5FE70= +cloud.google.com/go/recommender v1.9.0/go.mod h1:PnSsnZY7q+VL1uax2JWkt/UegHssxjUVVCrX52CuEmQ= +cloud.google.com/go/recommender v1.10.1/go.mod h1:XFvrE4Suqn5Cq0Lf+mCP6oBHD/yRMA8XxP5sb7Q7gpA= +cloud.google.com/go/redis v1.7.0/go.mod h1:V3x5Jq1jzUcg+UNsRvdmsfuFnit1cfe3Z/PGyq/lm4Y= +cloud.google.com/go/redis v1.8.0/go.mod h1:Fm2szCDavWzBk2cDKxrkmWBqoCiL1+Ctwq7EyqBCA/A= +cloud.google.com/go/redis v1.9.0/go.mod h1:HMYQuajvb2D0LvMgZmLDZW8V5aOC/WxstZHiy4g8OiA= +cloud.google.com/go/redis v1.10.0/go.mod h1:ThJf3mMBQtW18JzGgh41/Wld6vnDDc/F/F35UolRZPM= +cloud.google.com/go/redis v1.11.0/go.mod h1:/X6eicana+BWcUda5PpwZC48o37SiFVTFSs0fWAJ7uQ= +cloud.google.com/go/redis v1.13.1/go.mod h1:VP7DGLpE91M6bcsDdMuyCm2hIpB6Vp2hI090Mfd1tcg= +cloud.google.com/go/resourcemanager v1.3.0/go.mod h1:bAtrTjZQFJkiWTPDb1WBjzvc6/kifjj4QBYuKCCoqKA= +cloud.google.com/go/resourcemanager v1.4.0/go.mod h1:MwxuzkumyTX7/a3n37gmsT3py7LIXwrShilPh3P1tR0= +cloud.google.com/go/resourcemanager v1.5.0/go.mod h1:eQoXNAiAvCf5PXxWxXjhKQoTMaUSNrEfg+6qdf/wots= +cloud.google.com/go/resourcemanager v1.6.0/go.mod h1:YcpXGRs8fDzcUl1Xw8uOVmI8JEadvhRIkoXXUNVYcVo= +cloud.google.com/go/resourcemanager v1.7.0/go.mod h1:HlD3m6+bwhzj9XCouqmeiGuni95NTrExfhoSrkC/3EI= +cloud.google.com/go/resourcemanager v1.9.1/go.mod h1:dVCuosgrh1tINZ/RwBufr8lULmWGOkPS8gL5gqyjdT8= +cloud.google.com/go/resourcesettings v1.3.0/go.mod h1:lzew8VfESA5DQ8gdlHwMrqZs1S9V87v3oCnKCWoOuQU= +cloud.google.com/go/resourcesettings v1.4.0/go.mod h1:ldiH9IJpcrlC3VSuCGvjR5of/ezRrOxFtpJoJo5SmXg= +cloud.google.com/go/resourcesettings v1.5.0/go.mod h1:+xJF7QSG6undsQDfsCJyqWXyBwUoJLhetkRMDRnIoXA= +cloud.google.com/go/resourcesettings v1.6.1/go.mod h1:M7mk9PIZrC5Fgsu1kZJci6mpgN8o0IUzVx3eJU3y4Jw= +cloud.google.com/go/retail v1.8.0/go.mod h1:QblKS8waDmNUhghY2TI9O3JLlFk8jybHeV4BF19FrE4= +cloud.google.com/go/retail v1.9.0/go.mod h1:g6jb6mKuCS1QKnH/dpu7isX253absFl6iE92nHwlBUY= +cloud.google.com/go/retail v1.10.0/go.mod h1:2gDk9HsL4HMS4oZwz6daui2/jmKvqShXKQuB2RZ+cCc= +cloud.google.com/go/retail v1.11.0/go.mod h1:MBLk1NaWPmh6iVFSz9MeKG/Psyd7TAgm6y/9L2B4x9Y= +cloud.google.com/go/retail v1.12.0/go.mod h1:UMkelN/0Z8XvKymXFbD4EhFJlYKRx1FGhQkVPU5kF14= +cloud.google.com/go/retail v1.14.1/go.mod h1:y3Wv3Vr2k54dLNIrCzenyKG8g8dhvhncT2NcNjb/6gE= +cloud.google.com/go/run v0.2.0/go.mod h1:CNtKsTA1sDcnqqIFR3Pb5Tq0usWxJJvsWOCPldRU3Do= +cloud.google.com/go/run v0.3.0/go.mod h1:TuyY1+taHxTjrD0ZFk2iAR+xyOXEA0ztb7U3UNA0zBo= +cloud.google.com/go/run v0.8.0/go.mod h1:VniEnuBwqjigv0A7ONfQUaEItaiCRVujlMqerPPiktM= +cloud.google.com/go/run v0.9.0/go.mod h1:Wwu+/vvg8Y+JUApMwEDfVfhetv30hCG4ZwDR/IXl2Qg= +cloud.google.com/go/run v1.2.0/go.mod h1:36V1IlDzQ0XxbQjUx6IYbw8H3TJnWvhii963WW3B/bo= +cloud.google.com/go/scheduler v1.4.0/go.mod h1:drcJBmxF3aqZJRhmkHQ9b3uSSpQoltBPGPxGAWROx6s= +cloud.google.com/go/scheduler v1.5.0/go.mod h1:ri073ym49NW3AfT6DZi21vLZrG07GXr5p3H1KxN5QlI= +cloud.google.com/go/scheduler v1.6.0/go.mod h1:SgeKVM7MIwPn3BqtcBntpLyrIJftQISRrYB5ZtT+KOk= +cloud.google.com/go/scheduler v1.7.0/go.mod h1:jyCiBqWW956uBjjPMMuX09n3x37mtyPJegEWKxRsn44= +cloud.google.com/go/scheduler v1.8.0/go.mod h1:TCET+Y5Gp1YgHT8py4nlg2Sew8nUHMqcpousDgXJVQc= +cloud.google.com/go/scheduler v1.9.0/go.mod h1:yexg5t+KSmqu+njTIh3b7oYPheFtBWGcbVUYF1GGMIc= +cloud.google.com/go/scheduler v1.10.1/go.mod h1:R63Ldltd47Bs4gnhQkmNDse5w8gBRrhObZ54PxgR2Oo= +cloud.google.com/go/secretmanager v1.6.0/go.mod h1:awVa/OXF6IiyaU1wQ34inzQNc4ISIDIrId8qE5QGgKA= +cloud.google.com/go/secretmanager v1.8.0/go.mod h1:hnVgi/bN5MYHd3Gt0SPuTPPp5ENina1/LxM+2W9U9J4= +cloud.google.com/go/secretmanager v1.9.0/go.mod h1:b71qH2l1yHmWQHt9LC80akm86mX8AL6X1MA01dW8ht4= +cloud.google.com/go/secretmanager v1.10.0/go.mod h1:MfnrdvKMPNra9aZtQFvBcvRU54hbPD8/HayQdlUgJpU= +cloud.google.com/go/secretmanager v1.11.1/go.mod h1:znq9JlXgTNdBeQk9TBW/FnR/W4uChEKGeqQWAJ8SXFw= +cloud.google.com/go/security v1.5.0/go.mod h1:lgxGdyOKKjHL4YG3/YwIL2zLqMFCKs0UbQwgyZmfJl4= +cloud.google.com/go/security v1.7.0/go.mod h1:mZklORHl6Bg7CNnnjLH//0UlAlaXqiG7Lb9PsPXLfD0= +cloud.google.com/go/security v1.8.0/go.mod h1:hAQOwgmaHhztFhiQ41CjDODdWP0+AE1B3sX4OFlq+GU= +cloud.google.com/go/security v1.9.0/go.mod h1:6Ta1bO8LXI89nZnmnsZGp9lVoVWXqsVbIq/t9dzI+2Q= +cloud.google.com/go/security v1.10.0/go.mod h1:QtOMZByJVlibUT2h9afNDWRZ1G96gVywH8T5GUSb9IA= +cloud.google.com/go/security v1.12.0/go.mod h1:rV6EhrpbNHrrxqlvW0BWAIawFWq3X90SduMJdFwtLB8= +cloud.google.com/go/security v1.13.0/go.mod h1:Q1Nvxl1PAgmeW0y3HTt54JYIvUdtcpYKVfIB8AOMZ+0= +cloud.google.com/go/security v1.15.1/go.mod h1:MvTnnbsWnehoizHi09zoiZob0iCHVcL4AUBj76h9fXA= +cloud.google.com/go/securitycenter v1.13.0/go.mod h1:cv5qNAqjY84FCN6Y9z28WlkKXyWsgLO832YiWwkCWcU= +cloud.google.com/go/securitycenter v1.14.0/go.mod h1:gZLAhtyKv85n52XYWt6RmeBdydyxfPeTrpToDPw4Auc= +cloud.google.com/go/securitycenter v1.15.0/go.mod h1:PeKJ0t8MoFmmXLXWm41JidyzI3PJjd8sXWaVqg43WWk= +cloud.google.com/go/securitycenter v1.16.0/go.mod h1:Q9GMaLQFUD+5ZTabrbujNWLtSLZIZF7SAR0wWECrjdk= +cloud.google.com/go/securitycenter v1.18.1/go.mod h1:0/25gAzCM/9OL9vVx4ChPeM/+DlfGQJDwBy/UC8AKK0= +cloud.google.com/go/securitycenter v1.19.0/go.mod h1:LVLmSg8ZkkyaNy4u7HCIshAngSQ8EcIRREP3xBnyfag= +cloud.google.com/go/securitycenter v1.23.0/go.mod h1:8pwQ4n+Y9WCWM278R8W3nF65QtY172h4S8aXyI9/hsQ= +cloud.google.com/go/servicecontrol v1.4.0/go.mod h1:o0hUSJ1TXJAmi/7fLJAedOovnujSEvjKCAFNXPQ1RaU= +cloud.google.com/go/servicecontrol v1.5.0/go.mod h1:qM0CnXHhyqKVuiZnGKrIurvVImCs8gmqWsDoqe9sU1s= +cloud.google.com/go/servicecontrol v1.10.0/go.mod h1:pQvyvSRh7YzUF2efw7H87V92mxU8FnFDawMClGCNuAA= +cloud.google.com/go/servicecontrol v1.11.0/go.mod h1:kFmTzYzTUIuZs0ycVqRHNaNhgR+UMUpw9n02l/pY+mc= +cloud.google.com/go/servicecontrol v1.11.1/go.mod h1:aSnNNlwEFBY+PWGQ2DoM0JJ/QUXqV5/ZD9DOLB7SnUk= +cloud.google.com/go/servicedirectory v1.4.0/go.mod h1:gH1MUaZCgtP7qQiI+F+A+OpeKF/HQWgtAddhTbhL2bs= +cloud.google.com/go/servicedirectory v1.5.0/go.mod h1:QMKFL0NUySbpZJ1UZs3oFAmdvVxhhxB6eJ/Vlp73dfg= +cloud.google.com/go/servicedirectory v1.6.0/go.mod h1:pUlbnWsLH9c13yGkxCmfumWEPjsRs1RlmJ4pqiNjVL4= +cloud.google.com/go/servicedirectory v1.7.0/go.mod h1:5p/U5oyvgYGYejufvxhgwjL8UVXjkuw7q5XcG10wx1U= +cloud.google.com/go/servicedirectory v1.8.0/go.mod h1:srXodfhY1GFIPvltunswqXpVxFPpZjf8nkKQT7XcXaY= +cloud.google.com/go/servicedirectory v1.9.0/go.mod h1:29je5JjiygNYlmsGz8k6o+OZ8vd4f//bQLtvzkPPT/s= +cloud.google.com/go/servicedirectory v1.10.1/go.mod h1:Xv0YVH8s4pVOwfM/1eMTl0XJ6bzIOSLDt8f8eLaGOxQ= +cloud.google.com/go/servicedirectory v1.11.0/go.mod h1:Xv0YVH8s4pVOwfM/1eMTl0XJ6bzIOSLDt8f8eLaGOxQ= +cloud.google.com/go/servicemanagement v1.4.0/go.mod h1:d8t8MDbezI7Z2R1O/wu8oTggo3BI2GKYbdG4y/SJTco= +cloud.google.com/go/servicemanagement v1.5.0/go.mod h1:XGaCRe57kfqu4+lRxaFEAuqmjzF0r+gWHjWqKqBvKFo= +cloud.google.com/go/servicemanagement v1.6.0/go.mod h1:aWns7EeeCOtGEX4OvZUWCCJONRZeFKiptqKf1D0l/Jc= +cloud.google.com/go/servicemanagement v1.8.0/go.mod h1:MSS2TDlIEQD/fzsSGfCdJItQveu9NXnUniTrq/L8LK4= +cloud.google.com/go/serviceusage v1.3.0/go.mod h1:Hya1cozXM4SeSKTAgGXgj97GlqUvF5JaoXacR1JTP/E= +cloud.google.com/go/serviceusage v1.4.0/go.mod h1:SB4yxXSaYVuUBYUml6qklyONXNLt83U0Rb+CXyhjEeU= +cloud.google.com/go/serviceusage v1.5.0/go.mod h1:w8U1JvqUqwJNPEOTQjrMHkw3IaIFLoLsPLvsE3xueec= +cloud.google.com/go/serviceusage v1.6.0/go.mod h1:R5wwQcbOWsyuOfbP9tGdAnCAc6B9DRwPG1xtWMDeuPA= +cloud.google.com/go/shell v1.3.0/go.mod h1:VZ9HmRjZBsjLGXusm7K5Q5lzzByZmJHf1d0IWHEN5X4= +cloud.google.com/go/shell v1.4.0/go.mod h1:HDxPzZf3GkDdhExzD/gs8Grqk+dmYcEjGShZgYa9URw= +cloud.google.com/go/shell v1.6.0/go.mod h1:oHO8QACS90luWgxP3N9iZVuEiSF84zNyLytb+qE2f9A= +cloud.google.com/go/shell v1.7.1/go.mod h1:u1RaM+huXFaTojTbW4g9P5emOrrmLE69KrxqQahKn4g= +cloud.google.com/go/spanner v1.41.0/go.mod h1:MLYDBJR/dY4Wt7ZaMIQ7rXOTLjYrmxLE/5ve9vFfWos= +cloud.google.com/go/spanner v1.44.0/go.mod h1:G8XIgYdOK+Fbcpbs7p2fiprDw4CaZX63whnSMLVBxjk= +cloud.google.com/go/spanner v1.45.0/go.mod h1:FIws5LowYz8YAE1J8fOS7DJup8ff7xJeetWEo5REA2M= +cloud.google.com/go/spanner v1.47.0/go.mod h1:IXsJwVW2j4UKs0eYDqodab6HgGuA1bViSqW4uH9lfUI= +cloud.google.com/go/speech v1.6.0/go.mod h1:79tcr4FHCimOp56lwC01xnt/WPJZc4v3gzyT7FoBkCM= +cloud.google.com/go/speech v1.7.0/go.mod h1:KptqL+BAQIhMsj1kOP2la5DSEEerPDuOP/2mmkhHhZQ= +cloud.google.com/go/speech v1.8.0/go.mod h1:9bYIl1/tjsAnMgKGHKmBZzXKEkGgtU+MpdDPTE9f7y0= +cloud.google.com/go/speech v1.9.0/go.mod h1:xQ0jTcmnRFFM2RfX/U+rk6FQNUF6DQlydUSyoooSpco= +cloud.google.com/go/speech v1.14.1/go.mod h1:gEosVRPJ9waG7zqqnsHpYTOoAS4KouMRLDFMekpJ0J0= +cloud.google.com/go/speech v1.15.0/go.mod h1:y6oH7GhqCaZANH7+Oe0BhgIogsNInLlz542tg3VqeYI= +cloud.google.com/go/speech v1.17.1/go.mod h1:8rVNzU43tQvxDaGvqOhpDqgkJTFowBpDvCJ14kGlJYo= +cloud.google.com/go/speech v1.19.0/go.mod h1:8rVNzU43tQvxDaGvqOhpDqgkJTFowBpDvCJ14kGlJYo= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= +cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= +cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= +cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= +cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= +cloud.google.com/go/storage v1.22.1/go.mod h1:S8N1cAStu7BOeFfE8KAQzmyyLkK8p/vmRq6kuBTW58Y= +cloud.google.com/go/storage v1.23.0/go.mod h1:vOEEDNFnciUMhBeT6hsJIn3ieU5cFRmzeLgDvXzfIXc= +cloud.google.com/go/storage v1.27.0/go.mod h1:x9DOL8TK/ygDUMieqwfhdpQryTeEkhGKMi80i/iqR2s= +cloud.google.com/go/storage v1.28.1/go.mod h1:Qnisd4CqDdo6BGs2AD5LLnEsmSQ80wQ5ogcBBKhU86Y= +cloud.google.com/go/storage v1.29.0/go.mod h1:4puEjyTKnku6gfKoTfNOU/W+a9JyuVNxjpS5GBrB8h4= +cloud.google.com/go/storage v1.30.1/go.mod h1:NfxhC0UJE1aXSx7CIIbCf7y9HKT7BiccwkR7+P7gN8E= +cloud.google.com/go/storage v1.32.0/go.mod h1:Hhh/dogNRGca7IWv1RC2YqEn0c0G77ctA/OxflYkiD8= +cloud.google.com/go/storagetransfer v1.5.0/go.mod h1:dxNzUopWy7RQevYFHewchb29POFv3/AaBgnhqzqiK0w= +cloud.google.com/go/storagetransfer v1.6.0/go.mod h1:y77xm4CQV/ZhFZH75PLEXY0ROiS7Gh6pSKrM8dJyg6I= +cloud.google.com/go/storagetransfer v1.7.0/go.mod h1:8Giuj1QNb1kfLAiWM1bN6dHzfdlDAVC9rv9abHot2W4= +cloud.google.com/go/storagetransfer v1.8.0/go.mod h1:JpegsHHU1eXg7lMHkvf+KE5XDJ7EQu0GwNJbbVGanEw= +cloud.google.com/go/storagetransfer v1.10.0/go.mod h1:DM4sTlSmGiNczmV6iZyceIh2dbs+7z2Ayg6YAiQlYfA= +cloud.google.com/go/talent v1.1.0/go.mod h1:Vl4pt9jiHKvOgF9KoZo6Kob9oV4lwd/ZD5Cto54zDRw= +cloud.google.com/go/talent v1.2.0/go.mod h1:MoNF9bhFQbiJ6eFD3uSsg0uBALw4n4gaCaEjBw9zo8g= +cloud.google.com/go/talent v1.3.0/go.mod h1:CmcxwJ/PKfRgd1pBjQgU6W3YBwiewmUzQYH5HHmSCmM= +cloud.google.com/go/talent v1.4.0/go.mod h1:ezFtAgVuRf8jRsvyE6EwmbTK5LKciD4KVnHuDEFmOOA= +cloud.google.com/go/talent v1.5.0/go.mod h1:G+ODMj9bsasAEJkQSzO2uHQWXHHXUomArjWQQYkqK6c= +cloud.google.com/go/talent v1.6.2/go.mod h1:CbGvmKCG61mkdjcqTcLOkb2ZN1SrQI8MDyma2l7VD24= +cloud.google.com/go/texttospeech v1.4.0/go.mod h1:FX8HQHA6sEpJ7rCMSfXuzBcysDAuWusNNNvN9FELDd8= +cloud.google.com/go/texttospeech v1.5.0/go.mod h1:oKPLhR4n4ZdQqWKURdwxMy0uiTS1xU161C8W57Wkea4= +cloud.google.com/go/texttospeech v1.6.0/go.mod h1:YmwmFT8pj1aBblQOI3TfKmwibnsfvhIBzPXcW4EBovc= +cloud.google.com/go/texttospeech v1.7.1/go.mod h1:m7QfG5IXxeneGqTapXNxv2ItxP/FS0hCZBwXYqucgSk= +cloud.google.com/go/tpu v1.3.0/go.mod h1:aJIManG0o20tfDQlRIej44FcwGGl/cD0oiRyMKG19IQ= +cloud.google.com/go/tpu v1.4.0/go.mod h1:mjZaX8p0VBgllCzF6wcU2ovUXN9TONFLd7iz227X2Xg= +cloud.google.com/go/tpu v1.5.0/go.mod h1:8zVo1rYDFuW2l4yZVY0R0fb/v44xLh3llq7RuV61fPM= +cloud.google.com/go/tpu v1.6.1/go.mod h1:sOdcHVIgDEEOKuqUoi6Fq53MKHJAtOwtz0GuKsWSH3E= +cloud.google.com/go/trace v1.3.0/go.mod h1:FFUE83d9Ca57C+K8rDl/Ih8LwOzWIV1krKgxg6N0G28= +cloud.google.com/go/trace v1.4.0/go.mod h1:UG0v8UBqzusp+z63o7FK74SdFE+AXpCLdFb1rshXG+Y= +cloud.google.com/go/trace v1.8.0/go.mod h1:zH7vcsbAhklH8hWFig58HvxcxyQbaIqMarMg9hn5ECA= +cloud.google.com/go/trace v1.9.0/go.mod h1:lOQqpE5IaWY0Ixg7/r2SjixMuc6lfTFeO4QGM4dQWOk= +cloud.google.com/go/trace v1.10.1/go.mod h1:gbtL94KE5AJLH3y+WVpfWILmqgc6dXcqgNXdOPAQTYk= +cloud.google.com/go/translate v1.3.0/go.mod h1:gzMUwRjvOqj5i69y/LYLd8RrNQk+hOmIXTi9+nb3Djs= +cloud.google.com/go/translate v1.4.0/go.mod h1:06Dn/ppvLD6WvA5Rhdp029IX2Mi3Mn7fpMRLPvXT5Wg= +cloud.google.com/go/translate v1.5.0/go.mod h1:29YDSYveqqpA1CQFD7NQuP49xymq17RXNaUDdc0mNu0= +cloud.google.com/go/translate v1.6.0/go.mod h1:lMGRudH1pu7I3n3PETiOB2507gf3HnfLV8qlkHZEyos= +cloud.google.com/go/translate v1.7.0/go.mod h1:lMGRudH1pu7I3n3PETiOB2507gf3HnfLV8qlkHZEyos= +cloud.google.com/go/translate v1.8.1/go.mod h1:d1ZH5aaOA0CNhWeXeC8ujd4tdCFw8XoNWRljklu5RHs= +cloud.google.com/go/translate v1.8.2/go.mod h1:d1ZH5aaOA0CNhWeXeC8ujd4tdCFw8XoNWRljklu5RHs= +cloud.google.com/go/video v1.8.0/go.mod h1:sTzKFc0bUSByE8Yoh8X0mn8bMymItVGPfTuUBUyRgxk= +cloud.google.com/go/video v1.9.0/go.mod h1:0RhNKFRF5v92f8dQt0yhaHrEuH95m068JYOvLZYnJSw= +cloud.google.com/go/video v1.12.0/go.mod h1:MLQew95eTuaNDEGriQdcYn0dTwf9oWiA4uYebxM5kdg= +cloud.google.com/go/video v1.13.0/go.mod h1:ulzkYlYgCp15N2AokzKjy7MQ9ejuynOJdf1tR5lGthk= +cloud.google.com/go/video v1.14.0/go.mod h1:SkgaXwT+lIIAKqWAJfktHT/RbgjSuY6DobxEp0C5yTQ= +cloud.google.com/go/video v1.15.0/go.mod h1:SkgaXwT+lIIAKqWAJfktHT/RbgjSuY6DobxEp0C5yTQ= +cloud.google.com/go/video v1.17.1/go.mod h1:9qmqPqw/Ib2tLqaeHgtakU+l5TcJxCJbhFXM7UJjVzU= +cloud.google.com/go/video v1.19.0/go.mod h1:9qmqPqw/Ib2tLqaeHgtakU+l5TcJxCJbhFXM7UJjVzU= +cloud.google.com/go/videointelligence v1.6.0/go.mod h1:w0DIDlVRKtwPCn/C4iwZIJdvC69yInhW0cfi+p546uU= +cloud.google.com/go/videointelligence v1.7.0/go.mod h1:k8pI/1wAhjznARtVT9U1llUaFNPh7muw8QyOUpavru4= +cloud.google.com/go/videointelligence v1.8.0/go.mod h1:dIcCn4gVDdS7yte/w+koiXn5dWVplOZkE+xwG9FgK+M= +cloud.google.com/go/videointelligence v1.9.0/go.mod h1:29lVRMPDYHikk3v8EdPSaL8Ku+eMzDljjuvRs105XoU= +cloud.google.com/go/videointelligence v1.10.0/go.mod h1:LHZngX1liVtUhZvi2uNS0VQuOzNi2TkY1OakiuoUOjU= +cloud.google.com/go/videointelligence v1.11.1/go.mod h1:76xn/8InyQHarjTWsBR058SmlPCwQjgcvoW0aZykOvo= +cloud.google.com/go/vision v1.2.0/go.mod h1:SmNwgObm5DpFBme2xpyOyasvBc1aPdjvMk2bBk0tKD0= +cloud.google.com/go/vision/v2 v2.2.0/go.mod h1:uCdV4PpN1S0jyCyq8sIM42v2Y6zOLkZs+4R9LrGYwFo= +cloud.google.com/go/vision/v2 v2.3.0/go.mod h1:UO61abBx9QRMFkNBbf1D8B1LXdS2cGiiCRx0vSpZoUo= +cloud.google.com/go/vision/v2 v2.4.0/go.mod h1:VtI579ll9RpVTrdKdkMzckdnwMyX2JILb+MhPqRbPsY= +cloud.google.com/go/vision/v2 v2.5.0/go.mod h1:MmaezXOOE+IWa+cS7OhRRLK2cNv1ZL98zhqFFZaaH2E= +cloud.google.com/go/vision/v2 v2.6.0/go.mod h1:158Hes0MvOS9Z/bDMSFpjwsUrZ5fPrdwuyyvKSGAGMY= +cloud.google.com/go/vision/v2 v2.7.0/go.mod h1:H89VysHy21avemp6xcf9b9JvZHVehWbET0uT/bcuY/0= +cloud.google.com/go/vision/v2 v2.7.2/go.mod h1:jKa8oSYBWhYiXarHPvP4USxYANYUEdEsQrloLjrSwJU= +cloud.google.com/go/vmmigration v1.2.0/go.mod h1:IRf0o7myyWFSmVR1ItrBSFLFD/rJkfDCUTO4vLlJvsE= +cloud.google.com/go/vmmigration v1.3.0/go.mod h1:oGJ6ZgGPQOFdjHuocGcLqX4lc98YQ7Ygq8YQwHh9A7g= +cloud.google.com/go/vmmigration v1.5.0/go.mod h1:E4YQ8q7/4W9gobHjQg4JJSgXXSgY21nA5r8swQV+Xxc= +cloud.google.com/go/vmmigration v1.6.0/go.mod h1:bopQ/g4z+8qXzichC7GW1w2MjbErL54rk3/C843CjfY= +cloud.google.com/go/vmmigration v1.7.1/go.mod h1:WD+5z7a/IpZ5bKK//YmT9E047AD+rjycCAvyMxGJbro= +cloud.google.com/go/vmwareengine v0.1.0/go.mod h1:RsdNEf/8UDvKllXhMz5J40XxDrNJNN4sagiox+OI208= +cloud.google.com/go/vmwareengine v0.2.2/go.mod h1:sKdctNJxb3KLZkE/6Oui94iw/xs9PRNC2wnNLXsHvH8= +cloud.google.com/go/vmwareengine v0.3.0/go.mod h1:wvoyMvNWdIzxMYSpH/R7y2h5h3WFkx6d+1TIsP39WGY= +cloud.google.com/go/vmwareengine v0.4.1/go.mod h1:Px64x+BvjPZwWuc4HdmVhoygcXqEkGHXoa7uyfTgSI0= +cloud.google.com/go/vmwareengine v1.0.0/go.mod h1:Px64x+BvjPZwWuc4HdmVhoygcXqEkGHXoa7uyfTgSI0= +cloud.google.com/go/vpcaccess v1.4.0/go.mod h1:aQHVbTWDYUR1EbTApSVvMq1EnT57ppDmQzZ3imqIk4w= +cloud.google.com/go/vpcaccess v1.5.0/go.mod h1:drmg4HLk9NkZpGfCmZ3Tz0Bwnm2+DKqViEpeEpOq0m8= +cloud.google.com/go/vpcaccess v1.6.0/go.mod h1:wX2ILaNhe7TlVa4vC5xce1bCnqE3AeH27RV31lnmZes= +cloud.google.com/go/vpcaccess v1.7.1/go.mod h1:FogoD46/ZU+JUBX9D606X21EnxiszYi2tArQwLY4SXs= +cloud.google.com/go/webrisk v1.4.0/go.mod h1:Hn8X6Zr+ziE2aNd8SliSDWpEnSS1u4R9+xXZmFiHmGE= +cloud.google.com/go/webrisk v1.5.0/go.mod h1:iPG6fr52Tv7sGk0H6qUFzmL3HHZev1htXuWDEEsqMTg= +cloud.google.com/go/webrisk v1.6.0/go.mod h1:65sW9V9rOosnc9ZY7A7jsy1zoHS5W9IAXv6dGqhMQMc= +cloud.google.com/go/webrisk v1.7.0/go.mod h1:mVMHgEYH0r337nmt1JyLthzMr6YxwN1aAIEc2fTcq7A= +cloud.google.com/go/webrisk v1.8.0/go.mod h1:oJPDuamzHXgUc+b8SiHRcVInZQuybnvEW72PqTc7sSg= +cloud.google.com/go/webrisk v1.9.1/go.mod h1:4GCmXKcOa2BZcZPn6DCEvE7HypmEJcJkr4mtM+sqYPc= +cloud.google.com/go/websecurityscanner v1.3.0/go.mod h1:uImdKm2wyeXQevQJXeh8Uun/Ym1VqworNDlBXQevGMo= +cloud.google.com/go/websecurityscanner v1.4.0/go.mod h1:ebit/Fp0a+FWu5j4JOmJEV8S8CzdTkAS77oDsiSqYWQ= +cloud.google.com/go/websecurityscanner v1.5.0/go.mod h1:Y6xdCPy81yi0SQnDY1xdNTNpfY1oAgXUlcfN3B3eSng= +cloud.google.com/go/websecurityscanner v1.6.1/go.mod h1:Njgaw3rttgRHXzwCB8kgCYqv5/rGpFCsBOvPbYgszpg= +cloud.google.com/go/workflows v1.6.0/go.mod h1:6t9F5h/unJz41YqfBmqSASJSXccBLtD1Vwf+KmJENM0= +cloud.google.com/go/workflows v1.7.0/go.mod h1:JhSrZuVZWuiDfKEFxU0/F1PQjmpnpcoISEXH2bcHC3M= +cloud.google.com/go/workflows v1.8.0/go.mod h1:ysGhmEajwZxGn1OhGOGKsTXc5PyxOc0vfKf5Af+to4M= +cloud.google.com/go/workflows v1.9.0/go.mod h1:ZGkj1aFIOd9c8Gerkjjq7OW7I5+l6cSvT3ujaO/WwSA= +cloud.google.com/go/workflows v1.10.0/go.mod h1:fZ8LmRmZQWacon9UCX1r/g/DfAXx5VcPALq2CxzdePw= +cloud.google.com/go/workflows v1.11.1/go.mod h1:Z+t10G1wF7h8LgdY/EmRcQY8ptBD/nvofaL6FqlET6g= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +gioui.org v0.0.0-20210308172011-57750fc8a0a6/go.mod h1:RSH6KIUZ0p2xy5zHDxgAM4zumjgTw83q2ge/PI+yyw8= +git.sr.ht/~sbinet/gg v0.3.1/go.mod h1:KGYtlADtqsqANL9ueOFkWymvzUvLMQllU5Ixo+8v3pc= +github.com/Azure/azure-pipeline-go v0.2.3/go.mod h1:x841ezTBIMG6O3lAcl8ATHnsOPVl2bqk7S3ta6S6u4k= +github.com/Azure/azure-storage-blob-go v0.14.0/go.mod h1:SMqIBi+SuiQH32bvyjngEewEeXoPfKMgWlBDaYf6fck= +github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= +github.com/Azure/go-autorest/autorest/adal v0.9.13/go.mod h1:W/MM4U6nLxnIskrw4UwWzlHfGjwUS50aOsc/I3yuU8M= +github.com/Azure/go-autorest/autorest/date v0.3.0/go.mod h1:BI0uouVdmngYNUzGWeSYnokU+TrmwEsOqdt8Y6sso74= +github.com/Azure/go-autorest/autorest/mocks v0.4.1/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k= +github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= +github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8= +github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/JohnCGriffin/overflow v0.0.0-20211019200055-46fa312c352c/go.mod h1:X0CRv0ky0k6m906ixxpzmDRLvX58TFUKS2eePweuyxk= +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/agiledragon/gomonkey/v2 v2.11.0 h1:5oxSgA+tC1xuGsrIorR+sYiziYltmJyEZ9qA25b6l5U= +github.com/agiledragon/gomonkey/v2 v2.11.0/go.mod h1:ap1AmDzcVOAz1YpeJ3TCzIgstoaWLA6jbbgxfB4w2iY= +github.com/ajstarks/deck v0.0.0-20200831202436-30c9fc6549a9/go.mod h1:JynElWSGnm/4RlzPXRlREEwqTHAN3T56Bv2ITsFT3gY= +github.com/ajstarks/deck/generate v0.0.0-20210309230005-c3f852c02e19/go.mod h1:T13YZdzov6OU0A1+RfKZiZN9ca6VeKdBdyDV+BY97Tk= +github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw= +github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b/go.mod h1:1KcenG0jGWcpt8ov532z81sp/kMMUG485J2InIOyADM= +github.com/alecthomas/kingpin/v2 v2.3.1/go.mod h1:oYL5vtsvEHZGHxU7DMp32Dvx+qL+ptGn6lWaot2vCNE= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= +github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE= +github.com/alex-ant/gomath v0.0.0-20160516115720-89013a210a82 h1:7dONQ3WNZ1zy960TmkxJPuwoolZwL7xKtpcM04MBnt4= +github.com/alex-ant/gomath v0.0.0-20160516115720-89013a210a82/go.mod h1:nLnM0KdK1CmygvjpDUO6m1TjSsiQtL61juhNsvV/JVI= +github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= +github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= +github.com/apache/arrow/go/v10 v10.0.1/go.mod h1:YvhnlEePVnBS4+0z3fhPfUy7W1Ikj0Ih0vcRo/gZ1M0= +github.com/apache/arrow/go/v11 v11.0.0/go.mod h1:Eg5OsL5H+e299f7u5ssuXsuHQVEGC4xei5aX110hRiI= +github.com/apache/arrow/go/v12 v12.0.0/go.mod h1:d+tV/eHZZ7Dz7RPrFKtPK02tpr+c9/PEd/zm8mDS9Vg= +github.com/apache/thrift v0.16.0/go.mod h1:PHK3hniurgQaNMZYaCLEqXKsYK8upmhPbmdP2FXSqgU= +github.com/apolloconfig/agollo/v4 v4.4.0 h1:bIIRTEN4f7HgLx97/cNpduEvP9qQ7BkCyDOI2j800VM= +github.com/apolloconfig/agollo/v4 v4.4.0/go.mod h1:6WjI68IzqMk/Y6ghMtrj5AX6Uewo20ZnncvRhTceQqg= +github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= +github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= +github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/aws/aws-sdk-go v1.45.1/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI= +github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= +github.com/bketelsen/crypt v0.0.4/go.mod h1:aI6NrJ0pMGgvZKL1iVgXLnfIFJtfV+bKCoqOes/6LfM= +github.com/bmizerany/pat v0.0.0-20170815010413-6226ea591a40 h1:y4B3+GPxKlrigF1ha5FFErxK+sr6sWxQovRMzwMhejo= +github.com/bmizerany/pat v0.0.0-20170815010413-6226ea591a40/go.mod h1:8rLXio+WjiTceGBHIoTvn60HIbs7Hm7bcHjyrSqYB9c= +github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= +github.com/boombuler/barcode v1.0.1/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= +github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= +github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= +github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= +github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= +github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0= +github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4= +github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM= +github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/census-instrumentation/opencensus-proto v0.3.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/census-instrumentation/opencensus-proto v0.4.1/go.mod h1:4T9NM4+4Vw91VeyqjLS6ao50K5bOcLKN6Q42XnYaRYw= +github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= +github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y= +github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= +github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= +github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= +github.com/cncf/udpa/go v0.0.0-20220112060539-c52dc94e7fbe/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= +github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20220314180256-7f1daf1720fc/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20230105202645-06c439db220b/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20230310173818-32f1caf87195/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20230607035331-e9ce68804cb4/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/dave/jennifer v1.6.1/go.mod h1:nXbxhEmQfOZhWml3D1cDK5M1FLnMSozpbFN/m3RmGZc= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= +github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= +github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= +github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= +github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= +github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE= +github.com/envoyproxy/go-control-plane v0.10.3/go.mod h1:fJJn/j26vwOu972OllsvAgJJM//w9BV6Fxbg2LuVd34= +github.com/envoyproxy/go-control-plane v0.11.0/go.mod h1:VnHyVMpzcLvCFt9yUz1UnCwHLhwx1WguiVDV7pTG/tI= +github.com/envoyproxy/go-control-plane v0.11.1-0.20230524094728-9239064ad72f/go.mod h1:sfYdkwUW4BA3PbKjySwjJy+O4Pu0h62rlqCMHNk+K+Q= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/envoyproxy/protoc-gen-validate v0.6.7/go.mod h1:dyJXwwfPK2VSqiB9Klm1J6romD608Ba7Hij42vrOBCo= +github.com/envoyproxy/protoc-gen-validate v0.9.1/go.mod h1:OKNgG7TCp5pF4d6XftA0++PMirau2/yoOwVac3AbF2w= +github.com/envoyproxy/protoc-gen-validate v0.10.0/go.mod h1:DRjgyB0I43LtJapqN6NiRwroiAU2PaFuvk/vjgh61ss= +github.com/envoyproxy/protoc-gen-validate v0.10.1/go.mod h1:DRjgyB0I43LtJapqN6NiRwroiAU2PaFuvk/vjgh61ss= +github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= +github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= +github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= +github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= +github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= +github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/gin-contrib/pprof v1.5.0 h1:E/Oy7g+kNw94KfdCy3bZxQFtyDnAX2V7axRS7sNYVrU= +github.com/gin-contrib/pprof v1.5.0/go.mod h1:GqFL6LerKoCQ/RSWnkYczkTJ+tOAUVN/8sbnEtaqOKs= +github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= +github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= +github.com/go-fonts/dejavu v0.1.0/go.mod h1:4Wt4I4OU2Nq9asgDCteaAaWZOV24E+0/Pwo0gppep4g= +github.com/go-fonts/latin-modern v0.2.0/go.mod h1:rQVLdDMK+mK1xscDwsqM5J8U2jrRa3T0ecnM9pNujks= +github.com/go-fonts/liberation v0.1.1/go.mod h1:K6qoJYypsmfVjWg8KOVDQhLc8UDgIK2HYqyqAO9z7GY= +github.com/go-fonts/liberation v0.2.0/go.mod h1:K6qoJYypsmfVjWg8KOVDQhLc8UDgIK2HYqyqAO9z7GY= +github.com/go-fonts/stix v0.1.0/go.mod h1:w/c1f0ldAUlJmLBvlbkvVXLAD+tAMqobIIQpmnUIzUY= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= +github.com/go-kit/log v0.2.0/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0= +github.com/go-kit/log v0.2.1/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0= +github.com/go-latex/latex v0.0.0-20210118124228-b3d85cf34e07/go.mod h1:CO1AlKB2CSIqUrmQPqA0gdRIlnLEY0gK5JGjh37zN5U= +github.com/go-latex/latex v0.0.0-20210823091927-c0d11ff05a81/go.mod h1:SX0U8uGpxhq9o2S/CELCSUxEWWAuoCUcVCQWv7G2OCk= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= +github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= +github.com/go-pdf/fpdf v0.5.0/go.mod h1:HzcnA+A23uwogo0tp9yU+l3V+KXhiESpt1PMayhOh5M= +github.com/go-pdf/fpdf v0.6.0/go.mod h1:HzcnA+A23uwogo0tp9yU+l3V+KXhiESpt1PMayhOh5M= +github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= +github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= +github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.8.0/go.mod h1:9JhgTzTaE31GZDpH/HSvHiRJrJ3iKAgqqH0Bl/Ocjdk= +github.com/go-playground/validator/v10 v10.22.0 h1:k6HsTZ0sTnROkhS//R0O+55JgM8C4Bx7ia+JlgcnOao= +github.com/go-playground/validator/v10 v10.22.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= +github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc= +github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/goccy/go-json v0.9.11/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= +github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw= +github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg= +github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g= +github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/glog v1.0.0/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4= +github.com/golang/glog v1.1.0/go.mod h1:pfYeQZ3JWZoXTV5sFc986z3HTpwQs9At6P4ImfuP3NQ= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= +github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8= +github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= +github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= +github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/flatbuffers v2.0.8+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= +github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-pkcs11 v0.2.0/go.mod h1:6eQoGcuNJpa7jnd5pMGdkSaQpNDYvPlXWMcjXXThLlY= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/martian/v3 v3.2.1/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk= +github.com/google/martian/v3 v3.3.2/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/s2a-go v0.1.0/go.mod h1:OJpEgntRZo8ugHpF9hkoLJbS5dSI20XZeXJ9JVywLlM= +github.com/google/s2a-go v0.1.3/go.mod h1:Ej+mSEMGRnqRzjc7VtF+jdBwYG5fuJfiZ8ELkjEwM0A= +github.com/google/s2a-go v0.1.4/go.mod h1:Ej+mSEMGRnqRzjc7VtF+jdBwYG5fuJfiZ8ELkjEwM0A= +github.com/google/s2a-go v0.1.5/go.mod h1:Ej+mSEMGRnqRzjc7VtF+jdBwYG5fuJfiZ8ELkjEwM0A= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +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/googleapis/enterprise-certificate-proxy v0.0.0-20220520183353-fd19c99a87aa/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8= +github.com/googleapis/enterprise-certificate-proxy v0.1.0/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8= +github.com/googleapis/enterprise-certificate-proxy v0.2.0/go.mod h1:8C0jb7/mgJe/9KK8Lm7X9ctZC2t60YyIpYEI16jx0Qg= +github.com/googleapis/enterprise-certificate-proxy v0.2.1/go.mod h1:AwSRAtLfXpU5Nm3pW+v7rGDHp09LsPtGY9MduiEsR9k= +github.com/googleapis/enterprise-certificate-proxy v0.2.3/go.mod h1:AwSRAtLfXpU5Nm3pW+v7rGDHp09LsPtGY9MduiEsR9k= +github.com/googleapis/enterprise-certificate-proxy v0.2.4/go.mod h1:AwSRAtLfXpU5Nm3pW+v7rGDHp09LsPtGY9MduiEsR9k= +github.com/googleapis/enterprise-certificate-proxy v0.2.5/go.mod h1:RxW0N9901Cko1VOCW3SXCpWP+mlIEkk2tP7jnHy9a3w= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0= +github.com/googleapis/gax-go/v2 v2.1.1/go.mod h1:hddJymUZASv3XPyGkUpKj8pPO47Rmb0eJc8R6ouapiM= +github.com/googleapis/gax-go/v2 v2.2.0/go.mod h1:as02EH8zWkzwUoLbBaFeQ+arQaj/OthfcblKl4IGNaM= +github.com/googleapis/gax-go/v2 v2.3.0/go.mod h1:b8LNqSzNabLiUpXKkY7HAR5jr6bIT99EXz9pXxye9YM= +github.com/googleapis/gax-go/v2 v2.4.0/go.mod h1:XOTVJ59hdnfJLIP/dh8n5CGryZR2LxK9wbMD5+iXC6c= +github.com/googleapis/gax-go/v2 v2.5.1/go.mod h1:h6B0KMMFNtI2ddbGJn3T3ZbwkeT6yqEF02fYlzkUCyo= +github.com/googleapis/gax-go/v2 v2.6.0/go.mod h1:1mjbznJAPHFpesgE5ucqfYEscaz5kMdcIDwU/6+DDoY= +github.com/googleapis/gax-go/v2 v2.7.0/go.mod h1:TEop28CZZQ2y+c0VxMUmu1lV+fQx57QpBWsYpwqHJx8= +github.com/googleapis/gax-go/v2 v2.7.1/go.mod h1:4orTrqY6hXxxaUL4LHIPl6lGo8vAE38/qKbhSAKP6QI= +github.com/googleapis/gax-go/v2 v2.8.0/go.mod h1:4orTrqY6hXxxaUL4LHIPl6lGo8vAE38/qKbhSAKP6QI= +github.com/googleapis/gax-go/v2 v2.10.0/go.mod h1:4UOEnMCrxsSqQ940WnTiD6qJ63le2ev3xfyagutxiPw= +github.com/googleapis/gax-go/v2 v2.11.0/go.mod h1:DxmR61SGKkGLa2xigwuZIQpkCI2S5iydzRfb3peWZJI= +github.com/googleapis/gax-go/v2 v2.12.0/go.mod h1:y+aIqrI5eb1YGMVJfuV3185Ts/D7qKpsEkdD5+I6QGU= +github.com/googleapis/go-type-adapters v1.0.0/go.mod h1:zHW75FOG2aur7gAO2B+MLby+cLsWGBF62rFAi7WjWO4= +github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/grpc-ecosystem/go-grpc-middleware v1.4.0/go.mod h1:g5qyo/la0ALbONm6Vbp88Yd8NsDy6rZz+RcrMPxvld8= +github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0/go.mod h1:hgWBS7lorOAVIJEQMi4ZsPv9hVvWI6+ch50m39Pf2Ks= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.11.3/go.mod h1:o//XUCC/F+yRGJoPO/VU0GSB0f8Nhgmxx0VIRUvaC0w= +github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI= +github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= +github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= +github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= +github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= +github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= +github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= +github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= +github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= +github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= +github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= +github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= +github.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= +github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= +github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= +github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= +github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= +github.com/jung-kurt/gofpdf v1.0.0/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= +github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= +github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/asmfmt v1.3.2/go.mod h1:AG8TuvYojzulgDAMCnYn50l/5QV3Bs/tp6j0HLHbNSE= +github.com/klauspost/compress v1.15.9/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU= +github.com/klauspost/compress v1.17.2 h1:RlWWUY/Dr4fL8qk9YG7DTZ7PDgME2V4csBXA8L/ixi4= +github.com/klauspost/compress v1.17.2/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM= +github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= +github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/lyft/protoc-gen-star v0.6.0/go.mod h1:TGAoBVkt8w7MPG72TrKIu85MIdXwDuzJYeZuUPFPNwA= +github.com/lyft/protoc-gen-star v0.6.1/go.mod h1:TGAoBVkt8w7MPG72TrKIu85MIdXwDuzJYeZuUPFPNwA= +github.com/lyft/protoc-gen-star/v2 v2.0.1/go.mod h1:RcCdONR2ScXaYnQC5tUzxzlpA3WVYF7/opLeUgcQs/o= +github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= +github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= +github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/matishsiao/goInfo v0.0.0-20210923090445-da2e3fa8d45f h1:B0OD7nYl2FPQEVrw8g2uyc1lGEzNbvrKh7fspGZcbvY= +github.com/matishsiao/goInfo v0.0.0-20210923090445-da2e3fa8d45f/go.mod h1:aEt7p9Rvh67BYApmZwNDPpgircTO2kgdmDUoF/1QmwA= +github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-ieproxy v0.0.1/go.mod h1:pYabZ6IHcRpFh7vIaLfK7rdcWgFEb3SFJ6/gNWuh88E= +github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.17/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-sqlite3 v1.14.14/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= +github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= +github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= +github.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8/go.mod h1:mC1jAcsrzbxHt8iiaC+zU4b1ylILSosueou12R++wfY= +github.com/minio/c2goasm v0.0.0-20190812172519-36a3d3bbc4f3/go.mod h1:RagcQ7I8IeTMnF8JTXieKnO4Z6JCsikNEzj0DwauVzE= +github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= +github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= +github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= +github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= +github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= +github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= +github.com/mojocn/base64Captcha v1.3.6 h1:gZEKu1nsKpttuIAQgWHO+4Mhhls8cAKyiV2Ew03H+Tw= +github.com/mojocn/base64Captcha v1.3.6/go.mod h1:i5CtHvm+oMbj1UzEPXaA8IH/xHFZ3DGY3Wh3dBpZ28E= +github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe h1:iruDEfMl2E6fbMZ9s0scYfZQ84/6SPL6zC8ACM2oIL0= +github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= +github.com/mritd/chinaid v1.0.4 h1:jneCudUdOVZTgeVCezL/MeJZHoMLRpnav3STJp59ySU= +github.com/mritd/chinaid v1.0.4/go.mod h1:oEIvdY3QZd08MxyfEtdQ71DdQWOcALYqkiK0aHgAPIU= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uYLpLIr5fm8diHn0JbqRycJi6w0Ms= +github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= +github.com/panjf2000/ants/v2 v2.10.0 h1:zhRg1pQUtkyRiOFo2Sbqwjp0GfBNo9cUY2/Grpx1p+8= +github.com/panjf2000/ants/v2 v2.10.0/go.mod h1:7ZxyxsqE4vvW0M7LSD8aI3cKwgFhBHbxnlN8mDqHa1I= +github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= +github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= +github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= +github.com/phpdave11/gofpdf v1.4.2/go.mod h1:zpO6xFn9yxo3YLyMvW8HcKWVdbNqgIfOOp2dXMnm1mY= +github.com/phpdave11/gofpdi v1.0.12/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI= +github.com/phpdave11/gofpdi v1.0.13/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI= +github.com/pierrec/lz4/v4 v4.1.15/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= +github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= +github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= +github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= +github.com/prometheus/client_golang v1.12.1/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY= +github.com/prometheus/client_golang v1.14.0/go.mod h1:8vpkKitgIVNcqrRBWh1C4TIUQgYNtG/XQE4E/Zae36Y= +github.com/prometheus/client_golang v1.16.0/go.mod h1:Zsulrv/L9oM40tJ7T815tM89lFEugiJ9HzIqaAx4LKc= +github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE= +github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w= +github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw= +github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI= +github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= +github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= +github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= +github.com/prometheus/common v0.37.0/go.mod h1:phzohg0JFMnBEFGxTDbfu3QyL5GI8gTQJFhYO5B3mfA= +github.com/prometheus/common v0.42.0/go.mod h1:xBwqVerjNdUDjgODMpudtOMwlOwf2SaTr1yjz4b7Zbc= +github.com/prometheus/common v0.48.0 h1:QO8U2CdOzSn1BBsmXJXduaaW+dY/5QLjfB8svtSzKKE= +github.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= +github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= +github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= +github.com/prometheus/procfs v0.8.0/go.mod h1:z7EfXMXOkbkqb9IINtpCn86r/to3BnA0uaxHdg830/4= +github.com/prometheus/procfs v0.10.1/go.mod h1:nwNm2aOCAYw8uTR/9bWRREkZFxAUcWzPHWJq+XBB/FM= +github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= +github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= +github.com/qiniu/dyn v1.3.0/go.mod h1:E8oERcm8TtwJiZvkQPbcAh0RL8jO1G0VXJMW3FAWdkk= +github.com/qiniu/go-sdk/v7 v7.21.1 h1:D/IjVOlg5pTw0jeDjqTo6H5QM73Obb1AYfPOHmIFN+Q= +github.com/qiniu/go-sdk/v7 v7.21.1/go.mod h1:8EM2awITynlem2VML2dXGHkMYP2UyECsGLOdp6yMpco= +github.com/qiniu/x v1.10.5/go.mod h1:03Ni9tj+N2h2aKnAz+6N0Xfl8FwMEDRC2PAlxekASDs= +github.com/redis/go-redis/v9 v9.5.3 h1:fOAp1/uJG+ZtcITgZOfYFmTKPE7n4Vclj1wZFgRciUU= +github.com/redis/go-redis/v9 v9.5.3/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M= +github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/richardlehane/mscfb v1.0.4 h1:WULscsljNPConisD5hR0+OyZjwK46Pfyr6mPu5ZawpM= +github.com/richardlehane/mscfb v1.0.4/go.mod h1:YzVpcZg9czvAuhk9T+a3avCpcFPMUWm7gK3DypaEsUk= +github.com/richardlehane/msoleps v1.0.1/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg= +github.com/richardlehane/msoleps v1.0.3 h1:aznSZzrwYRl3rLKRT3gUk9am7T/mLNSnJINvN0AQoVM= +github.com/richardlehane/msoleps v1.0.3/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg= +github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= +github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= +github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= +github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= +github.com/rs/cors v1.11.0 h1:0B9GE/r9Bc2UxRMMtymBkHTenPkHDv0CW4Y98GBY+po= +github.com/rs/cors v1.11.0/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= +github.com/rs/cors/wrapper/gin v0.0.0-20240515105523-1562b1715b35 h1:YI8KKdUmi/l2NWArtFPEY6qFM7h6+V2kYj5kz81WSHs= +github.com/rs/cors/wrapper/gin v0.0.0-20240515105523-1562b1715b35/go.mod h1:742Ialb8SOs5yB2PqRDzFcyND3280PoaS5/wcKQUQKE= +github.com/ruudk/golang-pdf417 v0.0.0-20181029194003-1af4ab5afa58/go.mod h1:6lfFZQK844Gfx8o5WFuvpxWRwnSoipWe/p622j1v06w= +github.com/ruudk/golang-pdf417 v0.0.0-20201230142125-a7e3863a1245/go.mod h1:pQAZKsJ8yyVxGRWYNEm9oFB8ieLgKFnamEyDmSA0BRk= +github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ= +github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= +github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= +github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= +github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= +github.com/sethgrid/pester v1.2.0/go.mod h1:hEUINb4RqvDxtoCaU0BNT/HV4ig5kfgOasrf1xcvr0A= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= +github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/speps/go-hashids v2.0.0+incompatible h1:kSfxGfESueJKTx0mpER9Y/1XHl+FVQjtCqRyYcviFbw= +github.com/speps/go-hashids v2.0.0+incompatible/go.mod h1:P7hqPzMdnZOfyIk+xrlG1QaSMw+gCBdHKsBDnhpaZvc= +github.com/spf13/afero v1.3.3/go.mod h1:5KUK8ByomD5Ti5Artl0RtHeI5pTF7MIDuXL3yY520V4= +github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= +github.com/spf13/afero v1.9.2/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y= +github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= +github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= +github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= +github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= +github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.8.1/go.mod h1:o0Pch8wJ9BVSWGQMbra6iw0oQ5oktSIBaujf1rJH9Ns= +github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI= +github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= +github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= +github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +github.com/tevid/gohamcrest v1.1.1 h1:ou+xSqlIw1xfGTg1uq1nif/htZ2S3EzRqLm2BP+tYU0= +github.com/tevid/gohamcrest v1.1.1/go.mod h1:3UvtWlqm8j5JbwYZh80D/PVBt0mJ1eJiYgZMibh0H/k= +github.com/tidwall/assert v0.1.0 h1:aWcKyRBUAdLoVebxo95N7+YZVTFF/ASTr7BN4sLP6XI= +github.com/tidwall/assert v0.1.0/go.mod h1:QLYtGyeqse53vuELQheYl9dngGCJQ+mTtlxcktb+Kj8= +github.com/tidwall/btree v1.4.2 h1:PpkaieETJMUxYNADsjgtNRcERX7mGc/GP2zp/r5FM3g= +github.com/tidwall/btree v1.4.2/go.mod h1:LGm8L/DZjPLmeWGjv5kFrY8dL4uVhMmzmmLYmsObdKE= +github.com/tidwall/buntdb v1.3.1 h1:HKoDF01/aBhl9RjYtbaLnvX9/OuenwvQiC3OP1CcL4o= +github.com/tidwall/buntdb v1.3.1/go.mod h1:lZZrZUWzlyDJKlLQ6DKAy53LnG7m5kHyrEHvvcDmBpU= +github.com/tidwall/gjson v1.12.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/gjson v1.17.1 h1:wlYEnwqAHgzmhNUFfw7Xalt2JzQvsMx2Se4PcoFCT/U= +github.com/tidwall/gjson v1.17.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/grect v0.1.4 h1:dA3oIgNgWdSspFzn1kS4S/RDpZFLrIxAZOdJKjYapOg= +github.com/tidwall/grect v0.1.4/go.mod h1:9FBsaYRaR0Tcy4UwefBX/UDcDcDy9V5jUcxHzv2jd5Q= +github.com/tidwall/lotsa v1.0.2 h1:dNVBH5MErdaQ/xd9s769R31/n2dXavsQ0Yf4TMEHHw8= +github.com/tidwall/lotsa v1.0.2/go.mod h1:X6NiU+4yHA3fE3Puvpnn1XMDrFZrE9JO2/w+UMuqgR8= +github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= +github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/rtred v0.1.2 h1:exmoQtOLvDoO8ud++6LwVsAMTu0KPzLTUrMln8u1yu8= +github.com/tidwall/rtred v0.1.2/go.mod h1:hd69WNXQ5RP9vHd7dqekAz+RIdtfBogmglkZSRxCHFQ= +github.com/tidwall/tinyqueue v0.1.1 h1:SpNEvEggbpyN5DIReaJ2/1ndroY8iyEGxPYxoSaymYE= +github.com/tidwall/tinyqueue v0.1.1/go.mod h1:O/QNHwrnjqr6IHItYrzoHAKYhBkLI67Q096fQP5zMYw= +github.com/tus/tusd v1.13.0 h1:W7rtb1XPSpde/GPZAgdfUS3vus2Jt2KmckS6OUd3CU8= +github.com/tus/tusd v1.13.0/go.mod h1:1tX4CDGlx8koHGFJdSaJ5ybUIm2NeVloJgZEPSKRcQA= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= +github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +github.com/vimeo/go-util v1.4.1/go.mod h1:r+yspV//C48HeMXV8nEvtUeNiIiGfVv3bbEHzOgudwE= +github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c= +github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= +github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY= +github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4= +github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8= +github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM= +github.com/xhit/go-str2duration v1.2.0/go.mod h1:3cPSlfZlUHVlneIVfePFWcJZsuwf+P1v2SRTV4cUmp4= +github.com/xuri/efp v0.0.0-20231025114914-d1ff6096ae53 h1:Chd9DkqERQQuHpXjR/HSV1jLZA6uaoiwwH3vSuF3IW0= +github.com/xuri/efp v0.0.0-20231025114914-d1ff6096ae53/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI= +github.com/xuri/excelize/v2 v2.8.1 h1:pZLMEwK8ep+CLIUWpWmvW8IWE/yxqG0I1xcN6cVMGuQ= +github.com/xuri/excelize/v2 v2.8.1/go.mod h1:oli1E4C3Pa5RXg1TBXn4ENCXDV5JUMlBluUhG7c+CEE= +github.com/xuri/nfp v0.0.0-20230919160717-d98342af3f05 h1:qhbILQo1K3mphbwKh1vNm4oGezE1eF9fQWmNiIpSfI4= +github.com/xuri/nfp v0.0.0-20230919160717-d98342af3f05/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ= +github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d h1:splanxYIlg+5LfHAM6xpdFEAYOk8iySO56hMFq6uLyA= +github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= +github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= +github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA= +go.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs= +go.etcd.io/etcd/client/pkg/v3 v3.5.0/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= +go.etcd.io/etcd/client/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsXlzd7alYQ= +go.mongodb.org/mongo-driver v1.15.1 h1:l+RvoUOoMXFmADTLfYDm7On9dRm7p4T80/lEQM+r7HU= +go.mongodb.org/mongo-driver v1.15.1/go.mod h1:Vzb0Mk/pa7e6cWw85R4F/endUC3u0U9jGcNU603k65c= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= +go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= +go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= +go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= +go.opentelemetry.io/proto/otlp v0.15.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U= +go.opentelemetry.io/proto/otlp v0.19.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= +go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= +go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= +go.uber.org/zap v1.18.1/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= +golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc= +golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20220314234659-1baeb1ce4c0b/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= +golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= +golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0= +golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I= +golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio= +golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= +golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= +golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= +golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191002040644-a1355ae1e2c3/go.mod h1:NOZ3BPKG0ec/BKJQgnvsSFpcKLM5xXVWnvZS97DWHgE= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= +golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/exp v0.0.0-20220827204233-334a2380cb91/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= +golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g= +golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= +golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.0.0-20190910094157-69e4b8554b2a/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.0.0-20200119044424-58c23975cae1/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.0.0-20200430140353-33d19683fad8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.0.0-20200618115811-c13761719519/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.0.0-20201208152932-35266b937fa6/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.0.0-20210216034530-4410531fe030/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.0.0-20210607152325-775e3b0c77b9/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM= +golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM= +golang.org/x/image v0.0.0-20211028202545-6944b10bf410/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM= +golang.org/x/image v0.0.0-20220302094943-723b81ca9867/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM= +golang.org/x/image v0.13.0/go.mod h1:6mmbMOeV28HuMTgA6OSRkdXKYw/t5W9Uwn2Yv1r3Yxk= +golang.org/x/image v0.14.0 h1:tNgSxAFe3jC4uYqvZdTr84SZoM1KfwdC9SKIFrLjFn4= +golang.org/x/image v0.14.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= +golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.5.0/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= +golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.10.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191112182307-2180aed22343/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220325170049-de3da57026de/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220412020605-290c469a71a5/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220607020251-c690dde0001d/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.0.0-20220617184016-355a448f1bc9/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.0.0-20220909164309-bea034e7d591/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= +golang.org/x/net v0.0.0-20221012135044-0b7e1fb9d458/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= +golang.org/x/net v0.0.0-20221014081412-f15817d10f9b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= +golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= +golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= +golang.org/x/net v0.4.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= +golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= +golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA= +golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= +golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= +golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210402161424-2e8d93401602/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= +golang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= +golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= +golang.org/x/oauth2 v0.0.0-20220608161450-d0670ef3b1eb/go.mod h1:jaDAt6Dkxork7LmZnYtzbRWj0W47D86a3TGe0YHBvmE= +golang.org/x/oauth2 v0.0.0-20220622183110-fd043fe589d2/go.mod h1:jaDAt6Dkxork7LmZnYtzbRWj0W47D86a3TGe0YHBvmE= +golang.org/x/oauth2 v0.0.0-20220822191816-0ebed06d0094/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= +golang.org/x/oauth2 v0.0.0-20220909003341-f21342109be1/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= +golang.org/x/oauth2 v0.0.0-20221006150949-b44042a4b9c1/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= +golang.org/x/oauth2 v0.0.0-20221014153046-6fdb5e3db783/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= +golang.org/x/oauth2 v0.4.0/go.mod h1:RznEsdpjGAINPTOF0UH/t+xJ75L18YO3Ho6Pyn+uRec= +golang.org/x/oauth2 v0.5.0/go.mod h1:9/XBHVqLaWO3/BRHs5jbpYCnOZVjj5V0ndyaAM7KB4I= +golang.org/x/oauth2 v0.6.0/go.mod h1:ycmewcwgD4Rpr3eZJLSB4Kyyljb3qDh40vJ8STE5HKw= +golang.org/x/oauth2 v0.7.0/go.mod h1:hPLQkd9LyjfXTiRohC/41GhcFqxisoUQ99sCUOHO9x4= +golang.org/x/oauth2 v0.8.0/go.mod h1:yr7u4HXZRm1R1kBWqr/xKNqewf0plRYoB7sla+BCIXE= +golang.org/x/oauth2 v0.10.0/go.mod h1:kTpgurOux7LqtuxjuyZa4Gj2gdezIt/jQtGnNFfypQI= +golang.org/x/oauth2 v0.11.0/go.mod h1:LdF7O/8bLR/qWK9DrpXmbHLTouvRHK0SgJl0GmDBchk= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220929204114-8fcdb60fdcc0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.2.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191112214154-59a1497f0cea/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200828194041-157a740278f4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210304124612-50617c2ba197/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210603125802-9665404d3644/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210816183151-1e6c022a8912/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211210111614-af8b64212486/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220328115105-d36c6a25d886/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220502124256-b6088ccd6cba/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220610221304-9f5ed59c137d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220624220833-87e55d714810/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220829200755-d48e67d00261/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= +golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= +golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA= +golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= +golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.9.0/go.mod h1:M6DEAAIenWoTxdKrOltXcmDY3rSplQUkrvaDU5FcQyo= +golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o= +golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU= +golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= +golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= +golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20220922220347-f3bd1da661af/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.1.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= +golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190206041539-40960b6deb8e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190927191325-030b2cf1153e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= +golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= +golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201124115921-2c860bdd6e78/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= +golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.9/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.3.0/go.mod h1:/rWhSS2+zyEVwoJf8YAX6L2f0ntZ7Kn/mGgAWcipA5k= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s= +golang.org/x/tools v0.9.1/go.mod h1:owI94Op576fPu3cIGQeHs3joujW/2Oc6MtlxbF5dfNc= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= +golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= +golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= +gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo= +gonum.org/v1/gonum v0.8.2/go.mod h1:oe/vMfY3deqTw+1EZJhuvEW2iwGF1bW9wwu7XCu0+v0= +gonum.org/v1/gonum v0.9.3/go.mod h1:TZumC3NeyVQskjXqmyWt4S3bINhy7B4eYwW69EbyX+0= +gonum.org/v1/gonum v0.11.0/go.mod h1:fSG4YDCxxUZQJ7rKsQrj0gMOg00Il0Z96/qMA4bVQhA= +gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw= +gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b/go.mod h1:Wt8AAjI+ypCyYX3nZBvf6cAIx93T+c/OS2HFAYskSZc= +gonum.org/v1/plot v0.9.0/go.mod h1:3Pcqqmp6RHvJI72kgb8fThyUnav364FOsdDo2aGW5lY= +gonum.org/v1/plot v0.10.1/go.mod h1:VZW5OlhkL1mysU9vaqNHnsy86inf6Ot+jB3r+BczCEo= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= +google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= +google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= +google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= +google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= +google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU= +google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94= +google.golang.org/api v0.44.0/go.mod h1:EBOGZqzyhtvMDoxwS97ctnh0zUmYY6CxqXsc1AvkYD8= +google.golang.org/api v0.47.0/go.mod h1:Wbvgpq1HddcWVtzsVLyfLp8lDg6AA241LmgIL59tHXo= +google.golang.org/api v0.48.0/go.mod h1:71Pr1vy+TAZRPkPs/xlCf5SsU8WjuAWv1Pfjbtukyy4= +google.golang.org/api v0.50.0/go.mod h1:4bNT5pAuq5ji4SRZm+5QIkjny9JAyVD/3gaSihNefaw= +google.golang.org/api v0.51.0/go.mod h1:t4HdrdoNgyN5cbEfm7Lum0lcLDLiise1F8qDKX00sOU= +google.golang.org/api v0.54.0/go.mod h1:7C4bFFOvVDGXjfDTAsgGwDgAxRDeQ4X8NvUedIt6z3k= +google.golang.org/api v0.55.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE= +google.golang.org/api v0.56.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE= +google.golang.org/api v0.57.0/go.mod h1:dVPlbZyBo2/OjBpmvNdpn2GRm6rPy75jyU7bmhdrMgI= +google.golang.org/api v0.61.0/go.mod h1:xQRti5UdCmoCEqFxcz93fTl338AVqDgyaDRuOZ3hg9I= +google.golang.org/api v0.63.0/go.mod h1:gs4ij2ffTRXwuzzgJl/56BdwJaA194ijkfn++9tDuPo= +google.golang.org/api v0.67.0/go.mod h1:ShHKP8E60yPsKNw/w8w+VYaj9H6buA5UqDp8dhbQZ6g= +google.golang.org/api v0.70.0/go.mod h1:Bs4ZM2HGifEvXwd50TtW70ovgJffJYw2oRCOFU/SkfA= +google.golang.org/api v0.71.0/go.mod h1:4PyU6e6JogV1f9eA4voyrTY2batOLdgZ5qZ5HOCc4j8= +google.golang.org/api v0.74.0/go.mod h1:ZpfMZOVRMywNyvJFeqL9HRWBgAuRfSjJFpe9QtRRyDs= +google.golang.org/api v0.75.0/go.mod h1:pU9QmyHLnzlpar1Mjt4IbapUCy8J+6HD6GeELN69ljA= +google.golang.org/api v0.77.0/go.mod h1:pU9QmyHLnzlpar1Mjt4IbapUCy8J+6HD6GeELN69ljA= +google.golang.org/api v0.78.0/go.mod h1:1Sg78yoMLOhlQTeF+ARBoytAcH1NNyyl390YMy6rKmw= +google.golang.org/api v0.80.0/go.mod h1:xY3nI94gbvBrE0J6NHXhxOmW97HG7Khjkku6AFB3Hyg= +google.golang.org/api v0.84.0/go.mod h1:NTsGnUFJMYROtiquksZHBWtHfeMC7iYthki7Eq3pa8o= +google.golang.org/api v0.85.0/go.mod h1:AqZf8Ep9uZ2pyTvgL+x0D3Zt0eoT9b5E8fmzfu6FO2g= +google.golang.org/api v0.90.0/go.mod h1:+Sem1dnrKlrXMR/X0bPnMWyluQe4RsNoYfmNLhOIkzw= +google.golang.org/api v0.93.0/go.mod h1:+Sem1dnrKlrXMR/X0bPnMWyluQe4RsNoYfmNLhOIkzw= +google.golang.org/api v0.95.0/go.mod h1:eADj+UBuxkh5zlrSntJghuNeg8HwQ1w5lTKkuqaETEI= +google.golang.org/api v0.96.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ13s= +google.golang.org/api v0.97.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ13s= +google.golang.org/api v0.98.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ13s= +google.golang.org/api v0.99.0/go.mod h1:1YOf74vkVndF7pG6hIHuINsM7eWwpVTAfNMNiL91A08= +google.golang.org/api v0.100.0/go.mod h1:ZE3Z2+ZOr87Rx7dqFsdRQkRBk36kDtp/h+QpHbB7a70= +google.golang.org/api v0.102.0/go.mod h1:3VFl6/fzoA+qNuS1N1/VfXY4LjoXN/wzeIp7TweWwGo= +google.golang.org/api v0.103.0/go.mod h1:hGtW6nK1AC+d9si/UBhw8Xli+QMOf6xyNAyJw4qU9w0= +google.golang.org/api v0.106.0/go.mod h1:2Ts0XTHNVWxypznxWOYUeI4g3WdP9Pk2Qk58+a/O9MY= +google.golang.org/api v0.107.0/go.mod h1:2Ts0XTHNVWxypznxWOYUeI4g3WdP9Pk2Qk58+a/O9MY= +google.golang.org/api v0.108.0/go.mod h1:2Ts0XTHNVWxypznxWOYUeI4g3WdP9Pk2Qk58+a/O9MY= +google.golang.org/api v0.110.0/go.mod h1:7FC4Vvx1Mooxh8C5HWjzZHcavuS2f6pmJpZx60ca7iI= +google.golang.org/api v0.111.0/go.mod h1:qtFHvU9mhgTJegR31csQ+rwxyUTHOKFqCKWp1J0fdw0= +google.golang.org/api v0.114.0/go.mod h1:ifYI2ZsFK6/uGddGfAD5BMxlnkBqCmqHSDUVi45N5Yg= +google.golang.org/api v0.118.0/go.mod h1:76TtD3vkgmZ66zZzp72bUUklpmQmKlhh6sYtIjYK+5E= +google.golang.org/api v0.122.0/go.mod h1:gcitW0lvnyWjSp9nKxAbdHKIZ6vF4aajGueeslZOyms= +google.golang.org/api v0.124.0/go.mod h1:xu2HQurE5gi/3t1aFCvhPD781p0a3p11sdunTJ2BlP4= +google.golang.org/api v0.125.0/go.mod h1:mBwVAtz+87bEN6CbA1GtZPDOqY2R5ONPqJeIlvyo4Aw= +google.golang.org/api v0.126.0/go.mod h1:mBwVAtz+87bEN6CbA1GtZPDOqY2R5ONPqJeIlvyo4Aw= +google.golang.org/api v0.128.0/go.mod h1:Y611qgqaE92On/7g65MQgxYul3c0rEB894kniWLY750= +google.golang.org/api v0.132.0/go.mod h1:AeTBC6GpJnJSRJjktDcPX0QwtS8pGYZOV6MSuSCusw0= +google.golang.org/api v0.138.0/go.mod h1:4xyob8CxC+0GChNBvEUAk8VBKNvYOTWM9T3v3UfRxuY= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= +google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200423170343-7949de9c1215/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= +google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210329143202-679c6ae281ee/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= +google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= +google.golang.org/genproto v0.0.0-20210513213006-bf773b8c8384/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A= +google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= +google.golang.org/genproto v0.0.0-20210604141403-392c879c8b08/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= +google.golang.org/genproto v0.0.0-20210608205507-b6d2f5bf0d7d/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= +google.golang.org/genproto v0.0.0-20210624195500-8bfb893ecb84/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24= +google.golang.org/genproto v0.0.0-20210713002101-d411969a0d9a/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k= +google.golang.org/genproto v0.0.0-20210716133855-ce7ef5c701ea/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k= +google.golang.org/genproto v0.0.0-20210728212813-7823e685a01f/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= +google.golang.org/genproto v0.0.0-20210805201207-89edb61ffb67/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= +google.golang.org/genproto v0.0.0-20210813162853-db860fec028c/go.mod h1:cFeNkxwySK631ADgubI+/XFU/xp8FD5KIVV4rj8UC5w= +google.golang.org/genproto v0.0.0-20210821163610-241b8fcbd6c8/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210828152312-66f60bf46e71/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210831024726-fe130286e0e2/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210903162649-d08c68adba83/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210909211513-a8c4777a87af/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210924002016-3dee208752a0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211206160659-862468c7d6e0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211221195035-429b39de9b1c/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20220126215142-9970aeb2e350/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20220207164111-0872dc986b00/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20220218161850-94dd64e39d7c/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= +google.golang.org/genproto v0.0.0-20220222213610-43724f9ea8cf/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= +google.golang.org/genproto v0.0.0-20220304144024-325a89244dc8/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= +google.golang.org/genproto v0.0.0-20220310185008-1973136f34c6/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= +google.golang.org/genproto v0.0.0-20220324131243-acbaeb5b85eb/go.mod h1:hAL49I2IFola2sVEjAn7MEwsja0xp51I0tlGAf9hz4E= +google.golang.org/genproto v0.0.0-20220329172620-7be39ac1afc7/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220407144326-9054f6ed7bac/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220413183235-5e96e2839df9/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220414192740-2d67ff6cf2b4/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220421151946-72621c1f0bd3/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220429170224-98d788798c3e/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220502173005-c8bf987b8c21/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= +google.golang.org/genproto v0.0.0-20220505152158-f39f71e6c8f3/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= +google.golang.org/genproto v0.0.0-20220518221133-4f43b3371335/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= +google.golang.org/genproto v0.0.0-20220523171625-347a074981d8/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= +google.golang.org/genproto v0.0.0-20220608133413-ed9918b62aac/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= +google.golang.org/genproto v0.0.0-20220616135557-88e70c0c3a90/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= +google.golang.org/genproto v0.0.0-20220617124728-180714bec0ad/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= +google.golang.org/genproto v0.0.0-20220624142145-8cd45d7dbd1f/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= +google.golang.org/genproto v0.0.0-20220628213854-d9e0b6570c03/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= +google.golang.org/genproto v0.0.0-20220722212130-b98a9ff5e252/go.mod h1:GkXuJDJ6aQ7lnJcRF+SJVgFdQhypqgl3LB1C9vabdRE= +google.golang.org/genproto v0.0.0-20220801145646-83ce21fca29f/go.mod h1:iHe1svFLAZg9VWz891+QbRMwUv9O/1Ww+/mngYeThbc= +google.golang.org/genproto v0.0.0-20220815135757-37a418bb8959/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= +google.golang.org/genproto v0.0.0-20220817144833-d7fd3f11b9b1/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= +google.golang.org/genproto v0.0.0-20220822174746-9e6da59bd2fc/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= +google.golang.org/genproto v0.0.0-20220829144015-23454907ede3/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= +google.golang.org/genproto v0.0.0-20220829175752-36a9c930ecbf/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= +google.golang.org/genproto v0.0.0-20220913154956-18f8339a66a5/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= +google.golang.org/genproto v0.0.0-20220914142337-ca0e39ece12f/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= +google.golang.org/genproto v0.0.0-20220915135415-7fd63a7952de/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= +google.golang.org/genproto v0.0.0-20220916172020-2692e8806bfa/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= +google.golang.org/genproto v0.0.0-20220919141832-68c03719ef51/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= +google.golang.org/genproto v0.0.0-20220920201722-2b89144ce006/go.mod h1:ht8XFiar2npT/g4vkk7O0WYS1sHOHbdujxbEp7CJWbw= +google.golang.org/genproto v0.0.0-20220926165614-551eb538f295/go.mod h1:woMGP53BroOrRY3xTxlbr8Y3eB/nzAvvFM83q7kG2OI= +google.golang.org/genproto v0.0.0-20220926220553-6981cbe3cfce/go.mod h1:woMGP53BroOrRY3xTxlbr8Y3eB/nzAvvFM83q7kG2OI= +google.golang.org/genproto v0.0.0-20221010155953-15ba04fc1c0e/go.mod h1:3526vdqwhZAwq4wsRUaVG555sVgsNmIjRtO7t/JH29U= +google.golang.org/genproto v0.0.0-20221014173430-6e2ab493f96b/go.mod h1:1vXfmgAz9N9Jx0QA82PqRVauvCz1SGSz739p0f183jM= +google.golang.org/genproto v0.0.0-20221014213838-99cd37c6964a/go.mod h1:1vXfmgAz9N9Jx0QA82PqRVauvCz1SGSz739p0f183jM= +google.golang.org/genproto v0.0.0-20221024153911-1573dae28c9c/go.mod h1:9qHF0xnpdSfF6knlcsnpzUu5y+rpwgbvsyGAZPBMg4s= +google.golang.org/genproto v0.0.0-20221024183307-1bc688fe9f3e/go.mod h1:9qHF0xnpdSfF6knlcsnpzUu5y+rpwgbvsyGAZPBMg4s= +google.golang.org/genproto v0.0.0-20221027153422-115e99e71e1c/go.mod h1:CGI5F/G+E5bKwmfYo09AXuVN4dD894kIKUFmVbP2/Fo= +google.golang.org/genproto v0.0.0-20221109142239-94d6d90a7d66/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg= +google.golang.org/genproto v0.0.0-20221114212237-e4508ebdbee1/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg= +google.golang.org/genproto v0.0.0-20221117204609-8f9c96812029/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg= +google.golang.org/genproto v0.0.0-20221118155620-16455021b5e6/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg= +google.golang.org/genproto v0.0.0-20221201164419-0e50fba7f41c/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg= +google.golang.org/genproto v0.0.0-20221201204527-e3fa12d562f3/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg= +google.golang.org/genproto v0.0.0-20221202195650-67e5cbc046fd/go.mod h1:cTsE614GARnxrLsqKREzmNYJACSWWpAWdNMwnD7c2BE= +google.golang.org/genproto v0.0.0-20221227171554-f9683d7f8bef/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= +google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= +google.golang.org/genproto v0.0.0-20230112194545-e10362b5ecf9/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= +google.golang.org/genproto v0.0.0-20230113154510-dbe35b8444a5/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= +google.golang.org/genproto v0.0.0-20230123190316-2c411cf9d197/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= +google.golang.org/genproto v0.0.0-20230124163310-31e0e69b6fc2/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= +google.golang.org/genproto v0.0.0-20230125152338-dcaf20b6aeaa/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= +google.golang.org/genproto v0.0.0-20230127162408-596548ed4efa/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= +google.golang.org/genproto v0.0.0-20230209215440-0dfe4f8abfcc/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= +google.golang.org/genproto v0.0.0-20230216225411-c8e22ba71e44/go.mod h1:8B0gmkoRebU8ukX6HP+4wrVQUY1+6PkQ44BSyIlflHA= +google.golang.org/genproto v0.0.0-20230222225845-10f96fb3dbec/go.mod h1:3Dl5ZL0q0isWJt+FVcfpQyirqemEuLAK/iFvg1UP1Hw= +google.golang.org/genproto v0.0.0-20230223222841-637eb2293923/go.mod h1:3Dl5ZL0q0isWJt+FVcfpQyirqemEuLAK/iFvg1UP1Hw= +google.golang.org/genproto v0.0.0-20230303212802-e74f57abe488/go.mod h1:TvhZT5f700eVlTNwND1xoEZQeWTB2RY/65kplwl/bFA= +google.golang.org/genproto v0.0.0-20230306155012-7f2fa6fef1f4/go.mod h1:NWraEVixdDnqcqQ30jipen1STv2r/n24Wb7twVTGR4s= +google.golang.org/genproto v0.0.0-20230320184635-7606e756e683/go.mod h1:NWraEVixdDnqcqQ30jipen1STv2r/n24Wb7twVTGR4s= +google.golang.org/genproto v0.0.0-20230323212658-478b75c54725/go.mod h1:UUQDJDOlWu4KYeJZffbWgBkS1YFobzKbLVfK69pe0Ak= +google.golang.org/genproto v0.0.0-20230330154414-c0448cd141ea/go.mod h1:UUQDJDOlWu4KYeJZffbWgBkS1YFobzKbLVfK69pe0Ak= +google.golang.org/genproto v0.0.0-20230331144136-dcfb400f0633/go.mod h1:UUQDJDOlWu4KYeJZffbWgBkS1YFobzKbLVfK69pe0Ak= +google.golang.org/genproto v0.0.0-20230403163135-c38d8f061ccd/go.mod h1:UUQDJDOlWu4KYeJZffbWgBkS1YFobzKbLVfK69pe0Ak= +google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1/go.mod h1:nKE/iIaLqn2bQwXBg8f1g2Ylh6r5MN5CmZvuzZCgsCU= +google.golang.org/genproto v0.0.0-20230525234025-438c736192d0/go.mod h1:9ExIQyXL5hZrHzQceCwuSYwZZ5QZBazOcprJ5rgs3lY= +google.golang.org/genproto v0.0.0-20230526161137-0005af68ea54/go.mod h1:zqTuNwFlFRsw5zIts5VnzLQxSRqh+CGOTVMlYbY0Eyk= +google.golang.org/genproto v0.0.0-20230530153820-e85fd2cbaebc/go.mod h1:xZnkP7mREFX5MORlOPEzLMr+90PPZQ2QWzrVTWfAq64= +google.golang.org/genproto v0.0.0-20230629202037-9506855d4529/go.mod h1:xZnkP7mREFX5MORlOPEzLMr+90PPZQ2QWzrVTWfAq64= +google.golang.org/genproto v0.0.0-20230706204954-ccb25ca9f130/go.mod h1:O9kGHb51iE/nOGvQaDUuadVYqovW56s5emA88lQnj6Y= +google.golang.org/genproto v0.0.0-20230726155614-23370e0ffb3e/go.mod h1:0ggbjUrZYpy1q+ANUS30SEoGZ53cdfwtbuG7Ptgy108= +google.golang.org/genproto v0.0.0-20230803162519-f966b187b2e5/go.mod h1:oH/ZOT02u4kWEp7oYBGYFFkCdKS/uYR9Z7+0/xuuFp8= +google.golang.org/genproto/googleapis/api v0.0.0-20230525234020-1aefcd67740a/go.mod h1:ts19tUU+Z0ZShN1y3aPyq2+O3d5FUNNgT6FtOzmrNn8= +google.golang.org/genproto/googleapis/api v0.0.0-20230525234035-dd9d682886f9/go.mod h1:vHYtlOoi6TsQ3Uk2yxR7NI5z8uoV+3pZtR4jmHIkRig= +google.golang.org/genproto/googleapis/api v0.0.0-20230526203410-71b5a4ffd15e/go.mod h1:vHYtlOoi6TsQ3Uk2yxR7NI5z8uoV+3pZtR4jmHIkRig= +google.golang.org/genproto/googleapis/api v0.0.0-20230530153820-e85fd2cbaebc/go.mod h1:vHYtlOoi6TsQ3Uk2yxR7NI5z8uoV+3pZtR4jmHIkRig= +google.golang.org/genproto/googleapis/api v0.0.0-20230629202037-9506855d4529/go.mod h1:vHYtlOoi6TsQ3Uk2yxR7NI5z8uoV+3pZtR4jmHIkRig= +google.golang.org/genproto/googleapis/api v0.0.0-20230706204954-ccb25ca9f130/go.mod h1:mPBs5jNgx2GuQGvFwUvVKqtn6HsUw9nP64BedgvqEsQ= +google.golang.org/genproto/googleapis/api v0.0.0-20230726155614-23370e0ffb3e/go.mod h1:rsr7RhLuwsDKL7RmgDDCUc6yaGr1iqceVb5Wv6f6YvQ= +google.golang.org/genproto/googleapis/api v0.0.0-20230803162519-f966b187b2e5/go.mod h1:5DZzOUPCLYL3mNkQ0ms0F3EuUNZ7py1Bqeq6sxzI7/Q= +google.golang.org/genproto/googleapis/bytestream v0.0.0-20230530153820-e85fd2cbaebc/go.mod h1:ylj+BE99M198VPbBh6A8d9n3w8fChvyLK3wwBOjXBFA= +google.golang.org/genproto/googleapis/bytestream v0.0.0-20230711160842-782d3b101e98/go.mod h1:3QoBVwTHkXbY1oRGzlhwhOykfcATQN43LJ6iT8Wy8kE= +google.golang.org/genproto/googleapis/bytestream v0.0.0-20230807174057-1744710a1577/go.mod h1:NjCQG/D8JandXxM57PZbAJL1DCNL6EypA0vPPwfsc7c= +google.golang.org/genproto/googleapis/rpc v0.0.0-20230525234015-3fc162c6f38a/go.mod h1:xURIpW9ES5+/GZhnV6beoEtxQrnkRGIfP5VQG2tCBLc= +google.golang.org/genproto/googleapis/rpc v0.0.0-20230525234030-28d5490b6b19/go.mod h1:66JfowdXAEgad5O9NnYcsNPLCPZJD++2L9X0PCMODrA= +google.golang.org/genproto/googleapis/rpc v0.0.0-20230526203410-71b5a4ffd15e/go.mod h1:66JfowdXAEgad5O9NnYcsNPLCPZJD++2L9X0PCMODrA= +google.golang.org/genproto/googleapis/rpc v0.0.0-20230530153820-e85fd2cbaebc/go.mod h1:66JfowdXAEgad5O9NnYcsNPLCPZJD++2L9X0PCMODrA= +google.golang.org/genproto/googleapis/rpc v0.0.0-20230629202037-9506855d4529/go.mod h1:66JfowdXAEgad5O9NnYcsNPLCPZJD++2L9X0PCMODrA= +google.golang.org/genproto/googleapis/rpc v0.0.0-20230706204954-ccb25ca9f130/go.mod h1:8mL13HKkDa+IuJ8yruA3ci0q+0vsUz4m//+ottjwS5o= +google.golang.org/genproto/googleapis/rpc v0.0.0-20230711160842-782d3b101e98/go.mod h1:TUfxEVdsvPg18p6AslUXFoLdpED4oBnGwyqk3dV1XzM= +google.golang.org/genproto/googleapis/rpc v0.0.0-20230731190214-cbb8c96f2d6d/go.mod h1:TUfxEVdsvPg18p6AslUXFoLdpED4oBnGwyqk3dV1XzM= +google.golang.org/genproto/googleapis/rpc v0.0.0-20230807174057-1744710a1577/go.mod h1:+Bk1OCOj40wS2hwAMA+aCW9ypzm63QTBBHp6lQ3p+9M= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237 h1:NnYq6UN9ReLM9/Y01KWNOWyI5xQ9kbIms5GGJVwS/Yc= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= +google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= +google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= +google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= +google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= +google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/grpc v1.37.1/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= +google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= +google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= +google.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= +google.golang.org/grpc v1.42.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= +google.golang.org/grpc v1.44.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= +google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ= +google.golang.org/grpc v1.46.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= +google.golang.org/grpc v1.46.2/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= +google.golang.org/grpc v1.47.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= +google.golang.org/grpc v1.48.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= +google.golang.org/grpc v1.49.0/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI= +google.golang.org/grpc v1.50.0/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI= +google.golang.org/grpc v1.50.1/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI= +google.golang.org/grpc v1.51.0/go.mod h1:wgNDFcnuBGmxLKI/qn4T+m5BtEBYXJPvibbUPsAIPww= +google.golang.org/grpc v1.52.0/go.mod h1:pu6fVzoFb+NBYNAvQL08ic+lvB2IojljRYuun5vorUY= +google.golang.org/grpc v1.52.3/go.mod h1:pu6fVzoFb+NBYNAvQL08ic+lvB2IojljRYuun5vorUY= +google.golang.org/grpc v1.53.0/go.mod h1:OnIrk0ipVdj4N5d9IUoFUx72/VlD7+jUsHwZgwSMQpw= +google.golang.org/grpc v1.54.0/go.mod h1:PUSEXI6iWghWaB6lXM4knEgpJNu2qUcKfDtNci3EC2g= +google.golang.org/grpc v1.55.0/go.mod h1:iYEXKGkEBhg1PjZQvoYEVPTDkHo1/bjTnfwTeGONTY8= +google.golang.org/grpc v1.56.1/go.mod h1:I9bI3vqKfayGqPUAwGdOSu7kt6oIJLixfffKrpXqQ9s= +google.golang.org/grpc v1.56.2/go.mod h1:I9bI3vqKfayGqPUAwGdOSu7kt6oIJLixfffKrpXqQ9s= +google.golang.org/grpc v1.57.0/go.mod h1:Sd+9RMTACXwmub0zcNY2c4arhtrbBYD1AUHI/dt16Mo= +google.golang.org/grpc v1.64.0 h1:KH3VH9y/MgNQg1dE7b3XfVK0GsPSIzJwdF617gUSbvY= +google.golang.org/grpc v1.64.0/go.mod h1:oxjF8E3FBnjp+/gVFYdWacaLDx9na1aqy9oovLpxQYg= +google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.29.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= +google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= +gopkg.in/Acconut/lockfile.v1 v1.1.0/go.mod h1:6UCz3wJ8tSFUsPR6uP/j8uegEtDuEEqFxlpi0JI4Umw= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk= +gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df h1:n7WqCuqOuCbNr617RXOY0AWRXxgwEyPp2z+p0+hgMuE= +gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df/go.mod h1:LRQQ+SO6ZHR7tOkpBDuZnXENFzX8qRjMDMyPD6BRkCw= +gopkg.in/h2non/gock.v1 v1.1.2/go.mod h1:n7UGz/ckNChHiK05rDoiC4MYSunEC/lyaUm2WWaDva0= +gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= +gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gorm.io/driver/mysql v1.5.7 h1:MndhOPYOfEp2rHKgkZIhJ16eVUIRf2HmzgoPmh7FCWo= +gorm.io/driver/mysql v1.5.7/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM= +gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= +gorm.io/gorm v1.25.10 h1:dQpO+33KalOA+aFYGlK+EfxcI5MbO7EP2yYygwh9h+s= +gorm.io/gorm v1.25.10/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +honnef.co/go/tools v0.1.3/go.mod h1:NgwopIslSNH47DimFoV78dnkksY2EFtX0ajyb3K/las= +lukechampine.com/uint128 v1.1.1/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk= +lukechampine.com/uint128 v1.2.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk= +modernc.org/cc/v3 v3.36.0/go.mod h1:NFUHyPn4ekoC/JHeZFfZurN6ixxawE1BnVonP/oahEI= +modernc.org/cc/v3 v3.36.2/go.mod h1:NFUHyPn4ekoC/JHeZFfZurN6ixxawE1BnVonP/oahEI= +modernc.org/cc/v3 v3.36.3/go.mod h1:NFUHyPn4ekoC/JHeZFfZurN6ixxawE1BnVonP/oahEI= +modernc.org/cc/v3 v3.37.0/go.mod h1:vtL+3mdHx/wcj3iEGz84rQa8vEqR6XM84v5Lcvfph20= +modernc.org/cc/v3 v3.40.0/go.mod h1:/bTg4dnWkSXowUO6ssQKnOV0yMVxDYNIsIrzqTFDGH0= +modernc.org/ccgo/v3 v3.0.0-20220428102840-41399a37e894/go.mod h1:eI31LL8EwEBKPpNpA4bU1/i+sKOwOrQy8D87zWUcRZc= +modernc.org/ccgo/v3 v3.0.0-20220430103911-bc99d88307be/go.mod h1:bwdAnOoaIt8Ax9YdWGjxWsdkPcZyRPHqrOvJxaKAKGw= +modernc.org/ccgo/v3 v3.0.0-20220904174949-82d86e1b6d56/go.mod h1:YSXjPL62P2AMSxBphRHPn7IkzhVHqkvOnRKAKh+W6ZI= +modernc.org/ccgo/v3 v3.16.4/go.mod h1:tGtX0gE9Jn7hdZFeU88slbTh1UtCYKusWOoCJuvkWsQ= +modernc.org/ccgo/v3 v3.16.6/go.mod h1:tGtX0gE9Jn7hdZFeU88slbTh1UtCYKusWOoCJuvkWsQ= +modernc.org/ccgo/v3 v3.16.8/go.mod h1:zNjwkizS+fIFDrDjIAgBSCLkWbJuHF+ar3QRn+Z9aws= +modernc.org/ccgo/v3 v3.16.9/go.mod h1:zNMzC9A9xeNUepy6KuZBbugn3c0Mc9TeiJO4lgvkJDo= +modernc.org/ccgo/v3 v3.16.13-0.20221017192402-261537637ce8/go.mod h1:fUB3Vn0nVPReA+7IG7yZDfjv1TMWjhQP8gCxrFAtL5g= +modernc.org/ccgo/v3 v3.16.13/go.mod h1:2Quk+5YgpImhPjv2Qsob1DnZ/4som1lJTodubIcoUkY= +modernc.org/ccorpus v1.11.6/go.mod h1:2gEUTrWqdpH2pXsmTM1ZkjeSrUWDpjMu2T6m29L/ErQ= +modernc.org/httpfs v1.0.6/go.mod h1:7dosgurJGp0sPaRanU53W4xZYKh14wfzX420oZADeHM= +modernc.org/libc v0.0.0-20220428101251-2d5f3daf273b/go.mod h1:p7Mg4+koNjc8jkqwcoFBJx7tXkpj00G77X7A72jXPXA= +modernc.org/libc v1.16.0/go.mod h1:N4LD6DBE9cf+Dzf9buBlzVJndKr/iJHG97vGLHYnb5A= +modernc.org/libc v1.16.1/go.mod h1:JjJE0eu4yeK7tab2n4S1w8tlWd9MxXLRzheaRnAKymU= +modernc.org/libc v1.16.17/go.mod h1:hYIV5VZczAmGZAnG15Vdngn5HSF5cSkbvfz2B7GRuVU= +modernc.org/libc v1.16.19/go.mod h1:p7Mg4+koNjc8jkqwcoFBJx7tXkpj00G77X7A72jXPXA= +modernc.org/libc v1.17.0/go.mod h1:XsgLldpP4aWlPlsjqKRdHPqCxCjISdHfM/yeWC5GyW0= +modernc.org/libc v1.17.1/go.mod h1:FZ23b+8LjxZs7XtFMbSzL/EhPxNbfZbErxEHc7cbD9s= +modernc.org/libc v1.17.4/go.mod h1:WNg2ZH56rDEwdropAJeZPQkXmDwh+JCA1s/htl6r2fA= +modernc.org/libc v1.18.0/go.mod h1:vj6zehR5bfc98ipowQOM2nIDUZnVew/wNC/2tOGS+q0= +modernc.org/libc v1.20.3/go.mod h1:ZRfIaEkgrYgZDl6pa4W39HgN5G/yDW+NRmNKZBDFrk0= +modernc.org/libc v1.21.4/go.mod h1:przBsL5RDOZajTVslkugzLBj1evTue36jEomFQOoYuI= +modernc.org/libc v1.22.2/go.mod h1:uvQavJ1pZ0hIoC/jfqNoMLURIMhKzINIWypNM17puug= +modernc.org/mathutil v1.2.2/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= +modernc.org/mathutil v1.4.1/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= +modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= +modernc.org/memory v1.1.1/go.mod h1:/0wo5ibyrQiaoUoH7f9D8dnglAmILJ5/cxZlRECf+Nw= +modernc.org/memory v1.2.0/go.mod h1:/0wo5ibyrQiaoUoH7f9D8dnglAmILJ5/cxZlRECf+Nw= +modernc.org/memory v1.2.1/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU= +modernc.org/memory v1.3.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU= +modernc.org/memory v1.4.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU= +modernc.org/memory v1.5.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU= +modernc.org/opt v0.1.1/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= +modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= +modernc.org/sqlite v1.18.1/go.mod h1:6ho+Gow7oX5V+OiOQ6Tr4xeqbx13UZ6t+Fw9IRUG4d4= +modernc.org/sqlite v1.18.2/go.mod h1:kvrTLEWgxUcHa2GfHBQtanR1H9ht3hTJNtKpzH9k1u0= +modernc.org/strutil v1.1.1/go.mod h1:DE+MQQ/hjKBZS2zNInV5hhcipt5rLPWkmpbGeW5mmdw= +modernc.org/strutil v1.1.3/go.mod h1:MEHNA7PdEnEwLvspRMtWTNnp2nnyvMfkimT1NKNAGbw= +modernc.org/tcl v1.13.1/go.mod h1:XOLfOwzhkljL4itZkK6T72ckMgvj0BDsnKNdZVUOecw= +modernc.org/tcl v1.13.2/go.mod h1:7CLiGIPo1M8Rv1Mitpv5akc2+8fxUd2y2UzC/MfMzy0= +modernc.org/token v1.0.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= +modernc.org/token v1.0.1/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= +modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= +modernc.org/z v1.5.1/go.mod h1:eWFB510QWW5Th9YGZT81s+LwvaAs3Q2yr4sP0rmLkv8= +nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= diff --git a/pkg/aes/aes.go b/pkg/aes/aes.go new file mode 100644 index 0000000..ab80e4f --- /dev/null +++ b/pkg/aes/aes.go @@ -0,0 +1,120 @@ +package aes + +import ( + cryptoAes "crypto/aes" + "crypto/cipher" + "encoding/base64" +) + +var _ Aes = (*aes)(nil) + +type Aes interface { + i() + + EncryptCBC(encryptStr string, urlEncode bool) (string, error) + DecryptCBC(decryptStr string, urlEncode bool) (string, error) + + EncryptCFB(plain string, urlEncode bool) (string, error) + DecryptCFB(encrypted string, urlEncode bool) (string, error) +} + +type aes struct { + key string + iv string +} + +func New(key, iv string) Aes { + return &aes{ + key: key, + iv: iv, + } +} + +func (a *aes) i() {} + +func (a *aes) EncryptCBC(encryptStr string, urlEncode bool) (string, error) { + encoder := base64.StdEncoding + if urlEncode { + encoder = base64.URLEncoding + } + + encryptBytes := []byte(encryptStr) + block, err := cryptoAes.NewCipher([]byte(a.key)) + if err != nil { + return "", err + } + + blockSize := block.BlockSize() + encryptBytes = pkcsPadding(encryptBytes, blockSize) + blockMode := cipher.NewCBCEncrypter(block, []byte(a.iv)) + encrypted := make([]byte, len(encryptBytes)) + blockMode.CryptBlocks(encrypted, encryptBytes) + + return encoder.EncodeToString(encrypted), nil +} + +func (a *aes) DecryptCBC(decryptStr string, urlEncode bool) (string, error) { + encoder := base64.StdEncoding + if urlEncode { + encoder = base64.URLEncoding + } + + decryptBytes, err := encoder.DecodeString(decryptStr) + if err != nil { + return "", err + } + + block, err := cryptoAes.NewCipher([]byte(a.key)) + if err != nil { + return "", err + } + + blockMode := cipher.NewCBCDecrypter(block, []byte(a.iv)) + decrypted := make([]byte, len(decryptBytes)) + blockMode.CryptBlocks(decrypted, decryptBytes) + decrypted = pkcsUnPadding(decrypted) + + return string(decrypted), nil +} + +func (a *aes) EncryptCFB(plain string, urlEncode bool) (string, error) { + encoder := base64.StdEncoding + if urlEncode { + encoder = base64.URLEncoding + } + + block, err := cryptoAes.NewCipher([]byte(a.key)) + if err != nil { + return "", err + } + + encrypted := make([]byte, len(plain)) + stream := cipher.NewCFBEncrypter(block, []byte(a.iv)) + stream.XORKeyStream(encrypted, []byte(plain)) + + return encoder.EncodeToString(encrypted), nil + +} + +func (a *aes) DecryptCFB(encrypted string, urlEncode bool) (string, error) { + encoder := base64.StdEncoding + if urlEncode { + encoder = base64.URLEncoding + } + + decryptBytes, err := encoder.DecodeString(encrypted) + if err != nil { + return "", err + } + + block, err := cryptoAes.NewCipher([]byte(a.key)) + if err != nil { + return "", err + } + + plain := make([]byte, len(decryptBytes)) + stream := cipher.NewCFBDecrypter(block, []byte(a.iv)) + stream.XORKeyStream(plain, decryptBytes) + + return string(plain), nil +} diff --git a/pkg/aes/padding.go b/pkg/aes/padding.go new file mode 100644 index 0000000..9f9d137 --- /dev/null +++ b/pkg/aes/padding.go @@ -0,0 +1,25 @@ +package aes + +import ( + "bytes" +) + +/** + * 填充有六种:NoPadding, PKCS#5, PKCS#7, ISO 10126, ANSI X9.23和ZerosPadding + * 对于AES来说PKCS5Padding和PKCS7Padding是完全一样的,不同在于PKCS5限定了块大小为8bytes而PKCS7没有限定 + * 因此对于AES来说两者完全相同,但是对于Rijndael就不一样了 + * AES是Rijndael在块大小为8bytes时的特例,对于使用其他信息块大小的Rijndael算法只能使用PKCS7 + * 在AES加密当中严格来说是不能使用pkcs5的,因为AES的块大小是16bytes而pkcs5只能用于8bytes,通常我们在AES加密中所说的pkcs5指的就是pkcs7 + */ + +func pkcsPadding(cipherText []byte, blockSize int) []byte { + p := blockSize - len(cipherText)%blockSize + padText := bytes.Repeat([]byte{byte(p)}, p) + return append(cipherText, padText...) +} + +func pkcsUnPadding(decrypted []byte) []byte { + length := len(decrypted) + u := int(decrypted[length-1]) + return decrypted[:(length - u)] +} diff --git a/pkg/android_binary/apk/apk.go b/pkg/android_binary/apk/apk.go new file mode 100644 index 0000000..2724fbb --- /dev/null +++ b/pkg/android_binary/apk/apk.go @@ -0,0 +1,196 @@ +package apk + +import ( + "archive/zip" + "bytes" + "fmt" + "git.bvbej.com/bvbej/base-golang/pkg/android_binary" + "image" + "io" + "os" + "strconv" + + _ "image/jpeg" // handle jpeg format + _ "image/png" // handle png format +) + +// Apk is an application package file for android. +type Apk struct { + f *os.File + zipreader *zip.Reader + manifest Manifest + table *android_binary.TableFile +} + +// OpenFile will open the file specified by filename and return Apk +func OpenFile(filename string) (apk *Apk, err error) { + f, err := os.Open(filename) + if err != nil { + return nil, err + } + defer func() { + if err != nil { + f.Close() + } + }() + fi, err := f.Stat() + if err != nil { + return nil, err + } + apk, err = OpenZipReader(f, fi.Size()) + if err != nil { + return nil, err + } + apk.f = f + return +} + +// OpenZipReader has same arguments like zip.NewReader +func OpenZipReader(r io.ReaderAt, size int64) (*Apk, error) { + zipreader, err := zip.NewReader(r, size) + if err != nil { + return nil, err + } + apk := &Apk{ + zipreader: zipreader, + } + if err = apk.parseResources(); err != nil { + return nil, err + } + if err = apk.parseManifest(); err != nil { + return nil, errorf("parse-manifest: %w", err) + } + return apk, nil +} + +// Close is avaliable only if apk is created with OpenFile +func (k *Apk) Close() error { + if k.f == nil { + return nil + } + return k.f.Close() +} + +// Icon returns the icon image of the APK. +func (k *Apk) Icon(resConfig *android_binary.ResTableConfig) (image.Image, error) { + iconPath, err := k.manifest.App.Icon.WithResTableConfig(resConfig).String() + if err != nil { + return nil, err + } + if android_binary.IsResID(iconPath) { + return nil, newError("unable to convert icon-id to icon path") + } + imgData, err := k.readZipFile(iconPath) + if err != nil { + return nil, err + } + m, _, err := image.Decode(bytes.NewReader(imgData)) + return m, err +} + +// Label returns the label of the APK. +func (k *Apk) Label(resConfig *android_binary.ResTableConfig) (s string, err error) { + s, err = k.manifest.App.Label.WithResTableConfig(resConfig).String() + if err != nil { + return + } + if android_binary.IsResID(s) { + err = newError("unable to convert label-id to string") + } + return +} + +// Manifest returns the manifest of the APK. +func (k *Apk) Manifest() Manifest { + return k.manifest +} + +// PackageName returns the package name of the APK. +func (k *Apk) PackageName() string { + return k.manifest.Package.MustString() +} + +func isMainIntentFilter(intent ActivityIntentFilter) bool { + ok := false + for _, action := range intent.Actions { + s, err := action.Name.String() + if err == nil && s == "android.intent.action.MAIN" { + ok = true + break + } + } + if !ok { + return false + } + ok = false + for _, category := range intent.Categories { + s, err := category.Name.String() + if err == nil && s == "android.intent.category.LAUNCHER" { + ok = true + break + } + } + return ok +} + +// MainActivity returns the name of the main activity. +func (k *Apk) MainActivity() (activity string, err error) { + for _, act := range k.manifest.App.Activities { + for _, intent := range act.IntentFilters { + if isMainIntentFilter(intent) { + return act.Name.String() + } + } + } + for _, act := range k.manifest.App.ActivityAliases { + for _, intent := range act.IntentFilters { + if isMainIntentFilter(intent) { + return act.TargetActivity.String() + } + } + } + + return "", newError("No main activity found") +} + +func (k *Apk) parseManifest() error { + xmlData, err := k.readZipFile("AndroidManifest.xml") + if err != nil { + return errorf("failed to read AndroidManifest.xml: %w", err) + } + xmlfile, err := android_binary.NewXMLFile(bytes.NewReader(xmlData)) + if err != nil { + return errorf("failed to parse AndroidManifest.xml: %w", err) + } + return xmlfile.Decode(&k.manifest, k.table, nil) +} + +func (k *Apk) parseResources() (err error) { + resData, err := k.readZipFile("resources.arsc") + if err != nil { + return + } + k.table, err = android_binary.NewTableFile(bytes.NewReader(resData)) + return +} + +func (k *Apk) readZipFile(name string) (data []byte, err error) { + buf := bytes.NewBuffer(nil) + for _, file := range k.zipreader.File { + if file.Name != name { + continue + } + rc, er := file.Open() + if er != nil { + err = er + return + } + defer rc.Close() + _, err = io.Copy(buf, rc) + if err != nil { + return + } + return buf.Bytes(), nil + } + return nil, fmt.Errorf("File %s not found", strconv.Quote(name)) +} diff --git a/pkg/android_binary/apk/apkxml.go b/pkg/android_binary/apk/apkxml.go new file mode 100644 index 0000000..dbdb282 --- /dev/null +++ b/pkg/android_binary/apk/apkxml.go @@ -0,0 +1,108 @@ +package apk + +import "git.bvbej.com/bvbej/base-golang/pkg/android_binary" + +// Instrumentation is an application instrumentation code. +type Instrumentation struct { + Name android_binary.String `xml:"http://schemas.android.com/apk/res/android name,attr"` + Target android_binary.String `xml:"http://schemas.android.com/apk/res/android targetPackage,attr"` + HandleProfiling android_binary.Bool `xml:"http://schemas.android.com/apk/res/android handleProfiling,attr"` + FunctionalTest android_binary.Bool `xml:"http://schemas.android.com/apk/res/android functionalTest,attr"` +} + +// ActivityAction is an action of an activity. +type ActivityAction struct { + Name android_binary.String `xml:"http://schemas.android.com/apk/res/android name,attr"` +} + +// ActivityCategory is a category of an activity. +type ActivityCategory struct { + Name android_binary.String `xml:"http://schemas.android.com/apk/res/android name,attr"` +} + +// ActivityIntentFilter is an androidbinary.Int32ent filter of an activity. +type ActivityIntentFilter struct { + Actions []ActivityAction `xml:"action"` + Categories []ActivityCategory `xml:"category"` +} + +// AppActivity is an activity in an application. +type AppActivity struct { + Theme android_binary.String `xml:"http://schemas.android.com/apk/res/android theme,attr"` + Name android_binary.String `xml:"http://schemas.android.com/apk/res/android name,attr"` + Label android_binary.String `xml:"http://schemas.android.com/apk/res/android label,attr"` + ScreenOrientation android_binary.String `xml:"http://schemas.android.com/apk/res/android screenOrientation,attr"` + IntentFilters []ActivityIntentFilter `xml:"intent-filter"` +} + +// AppActivityAlias https://developer.android.com/guide/topics/manifest/activity-alias-element +type AppActivityAlias struct { + Name android_binary.String `xml:"http://schemas.android.com/apk/res/android name,attr"` + Label android_binary.String `xml:"http://schemas.android.com/apk/res/android label,attr"` + TargetActivity android_binary.String `xml:"http://schemas.android.com/apk/res/android targetActivity,attr"` + IntentFilters []ActivityIntentFilter `xml:"intent-filter"` +} + +// MetaData is a metadata in an application. +type MetaData struct { + Name android_binary.String `xml:"http://schemas.android.com/apk/res/android name,attr"` + Value android_binary.String `xml:"http://schemas.android.com/apk/res/android value,attr"` +} + +// Application is an application in an APK. +type Application struct { + AllowTaskReparenting android_binary.Bool `xml:"http://schemas.android.com/apk/res/android allowTaskReparenting,attr"` + AllowBackup android_binary.Bool `xml:"http://schemas.android.com/apk/res/android allowBackup,attr"` + BackupAgent android_binary.String `xml:"http://schemas.android.com/apk/res/android backupAgent,attr"` + Debuggable android_binary.Bool `xml:"http://schemas.android.com/apk/res/android debuggable,attr"` + Description android_binary.String `xml:"http://schemas.android.com/apk/res/android description,attr"` + Enabled android_binary.Bool `xml:"http://schemas.android.com/apk/res/android enabled,attr"` + HasCode android_binary.Bool `xml:"http://schemas.android.com/apk/res/android hasCode,attr"` + HardwareAccelerated android_binary.Bool `xml:"http://schemas.android.com/apk/res/android hardwareAccelerated,attr"` + Icon android_binary.String `xml:"http://schemas.android.com/apk/res/android icon,attr"` + KillAfterRestore android_binary.Bool `xml:"http://schemas.android.com/apk/res/android killAfterRestore,attr"` + LargeHeap android_binary.Bool `xml:"http://schemas.android.com/apk/res/android largeHeap,attr"` + Label android_binary.String `xml:"http://schemas.android.com/apk/res/android label,attr"` + Logo android_binary.String `xml:"http://schemas.android.com/apk/res/android logo,attr"` + ManageSpaceActivity android_binary.String `xml:"http://schemas.android.com/apk/res/android manageSpaceActivity,attr"` + Name android_binary.String `xml:"http://schemas.android.com/apk/res/android name,attr"` + Permission android_binary.String `xml:"http://schemas.android.com/apk/res/android permission,attr"` + Persistent android_binary.Bool `xml:"http://schemas.android.com/apk/res/android persistent,attr"` + Process android_binary.String `xml:"http://schemas.android.com/apk/res/android process,attr"` + RestoreAnyVersion android_binary.Bool `xml:"http://schemas.android.com/apk/res/android restoreAnyVersion,attr"` + RequiredAccountType android_binary.String `xml:"http://schemas.android.com/apk/res/android requiredAccountType,attr"` + RestrictedAccountType android_binary.String `xml:"http://schemas.android.com/apk/res/android restrictedAccountType,attr"` + SupportsRtl android_binary.Bool `xml:"http://schemas.android.com/apk/res/android supportsRtl,attr"` + TaskAffinity android_binary.String `xml:"http://schemas.android.com/apk/res/android taskAffinity,attr"` + TestOnly android_binary.Bool `xml:"http://schemas.android.com/apk/res/android testOnly,attr"` + Theme android_binary.String `xml:"http://schemas.android.com/apk/res/android theme,attr"` + UIOptions android_binary.String `xml:"http://schemas.android.com/apk/res/android uiOptions,attr"` + VMSafeMode android_binary.Bool `xml:"http://schemas.android.com/apk/res/android vmSafeMode,attr"` + Activities []AppActivity `xml:"activity"` + ActivityAliases []AppActivityAlias `xml:"activity-alias"` + MetaData []MetaData `xml:"meta-data"` +} + +// UsesSDK is target SDK version. +type UsesSDK struct { + Min android_binary.Int32 `xml:"http://schemas.android.com/apk/res/android minSdkVersion,attr"` + Target android_binary.Int32 `xml:"http://schemas.android.com/apk/res/android targetSdkVersion,attr"` + Max android_binary.Int32 `xml:"http://schemas.android.com/apk/res/android maxSdkVersion,attr"` +} + +// UsesPermission is user grant the system permission. +type UsesPermission struct { + Name android_binary.String `xml:"http://schemas.android.com/apk/res/android name,attr"` + Max android_binary.Int32 `xml:"http://schemas.android.com/apk/res/android maxSdkVersion,attr"` +} + +// Manifest is a manifest of an APK. +type Manifest struct { + Package android_binary.String `xml:"package,attr"` + VersionCode android_binary.Int32 `xml:"http://schemas.android.com/apk/res/android versionCode,attr"` + VersionName android_binary.String `xml:"http://schemas.android.com/apk/res/android versionName,attr"` + App Application `xml:"application"` + Instrument Instrumentation `xml:"instrumentation"` + SDK UsesSDK `xml:"uses-sdk"` + UsesPermissions []UsesPermission `xml:"uses-permission"` +} diff --git a/pkg/android_binary/apk/errors.go b/pkg/android_binary/apk/errors.go new file mode 100644 index 0000000..1f428f4 --- /dev/null +++ b/pkg/android_binary/apk/errors.go @@ -0,0 +1,9 @@ +package apk + +import ( + "errors" + "fmt" +) + +var newError = errors.New +var errorf = fmt.Errorf diff --git a/pkg/android_binary/common.go b/pkg/android_binary/common.go new file mode 100644 index 0000000..cf3eb73 --- /dev/null +++ b/pkg/android_binary/common.go @@ -0,0 +1,258 @@ +package android_binary + +import ( + "bytes" + "encoding/binary" + "io" + "unicode/utf16" +) + +// ChunkType is a type of a resource chunk. +type ChunkType uint16 + +// Chunk types. +const ( + ResNullChunkType ChunkType = 0x0000 + ResStringPoolChunkType ChunkType = 0x0001 + ResTableChunkType ChunkType = 0x0002 + ResXMLChunkType ChunkType = 0x0003 + + // Chunk types in RES_XML_TYPE + ResXMLFirstChunkType ChunkType = 0x0100 + ResXMLStartNamespaceType ChunkType = 0x0100 + ResXMLEndNamespaceType ChunkType = 0x0101 + ResXMLStartElementType ChunkType = 0x0102 + ResXMLEndElementType ChunkType = 0x0103 + ResXMLCDataType ChunkType = 0x0104 + ResXMLLastChunkType ChunkType = 0x017f + + // This contains a uint32_t array mapping strings in the string + // pool back to resource identifiers. It is optional. + ResXMLResourceMapType ChunkType = 0x0180 + + // Chunk types in RES_TABLE_TYPE + ResTablePackageType ChunkType = 0x0200 + ResTableTypeType ChunkType = 0x0201 + ResTableTypeSpecType ChunkType = 0x0202 +) + +// ResChunkHeader is a header of a resource chunk. +type ResChunkHeader struct { + Type ChunkType + HeaderSize uint16 + Size uint32 +} + +// Flags are flags for string pool header. +type Flags uint32 + +// the values of Flags. +const ( + SortedFlag Flags = 1 << 0 + UTF8Flag Flags = 1 << 8 +) + +// ResStringPoolHeader is a chunk header of string pool. +type ResStringPoolHeader struct { + Header ResChunkHeader + StringCount uint32 + StyleCount uint32 + Flags Flags + StringStart uint32 + StylesStart uint32 +} + +// ResStringPoolSpan is a span of style information associated with +// a string in the pool. +type ResStringPoolSpan struct { + FirstChar, LastChar uint32 +} + +// ResStringPool is a string pool resource. +type ResStringPool struct { + Header ResStringPoolHeader + Strings []string + Styles []ResStringPoolSpan +} + +// NilResStringPoolRef is nil reference for string pool. +const NilResStringPoolRef = ResStringPoolRef(0xFFFFFFFF) + +// ResStringPoolRef is a type representing a reference to a string. +type ResStringPoolRef uint32 + +// DataType is a type of the data value. +type DataType uint8 + +// The constants for DataType +const ( + TypeNull DataType = 0x00 + TypeReference DataType = 0x01 + TypeAttribute DataType = 0x02 + TypeString DataType = 0x03 + TypeFloat DataType = 0x04 + TypeDemention DataType = 0x05 + TypeFraction DataType = 0x06 + TypeFirstInt DataType = 0x10 + TypeIntDec DataType = 0x10 + TypeIntHex DataType = 0x11 + TypeIntBoolean DataType = 0x12 + TypeFirstColorInt DataType = 0x1c + TypeIntColorARGB8 DataType = 0x1c + TypeIntColorRGB8 DataType = 0x1d + TypeIntColorARGB4 DataType = 0x1e + TypeIntColorRGB4 DataType = 0x1f + TypeLastColorInt DataType = 0x1f + TypeLastInt DataType = 0x1f +) + +// ResValue is a representation of a value in a resource +type ResValue struct { + Size uint16 + Res0 uint8 + DataType DataType + Data uint32 +} + +// GetString returns a string referenced by ref. +func (pool *ResStringPool) GetString(ref ResStringPoolRef) string { + return pool.Strings[int(ref)] +} + +func readStringPool(sr *io.SectionReader) (*ResStringPool, error) { + sp := new(ResStringPool) + if err := binary.Read(sr, binary.LittleEndian, &sp.Header); err != nil { + return nil, err + } + + stringStarts := make([]uint32, sp.Header.StringCount) + if err := binary.Read(sr, binary.LittleEndian, stringStarts); err != nil { + return nil, err + } + + styleStarts := make([]uint32, sp.Header.StyleCount) + if err := binary.Read(sr, binary.LittleEndian, styleStarts); err != nil { + return nil, err + } + + sp.Strings = make([]string, sp.Header.StringCount) + for i, start := range stringStarts { + var str string + var err error + if _, err := sr.Seek(int64(sp.Header.StringStart+start), io.SeekStart); err != nil { + return nil, err + } + if (sp.Header.Flags & UTF8Flag) == 0 { + str, err = readUTF16(sr) + } else { + str, err = readUTF8(sr) + } + if err != nil { + return nil, err + } + sp.Strings[i] = str + } + + sp.Styles = make([]ResStringPoolSpan, sp.Header.StyleCount) + for i, start := range styleStarts { + if _, err := sr.Seek(int64(sp.Header.StylesStart+start), io.SeekStart); err != nil { + return nil, err + } + if err := binary.Read(sr, binary.LittleEndian, &sp.Styles[i]); err != nil { + return nil, err + } + } + + return sp, nil +} + +func readUTF16(sr *io.SectionReader) (string, error) { + // read length of string + size, err := readUTF16length(sr) + if err != nil { + return "", err + } + + // read string value + buf := make([]uint16, size) + if err := binary.Read(sr, binary.LittleEndian, buf); err != nil { + return "", err + } + return string(utf16.Decode(buf)), nil +} + +func readUTF16length(sr *io.SectionReader) (int, error) { + var size int + var first, second uint16 + if err := binary.Read(sr, binary.LittleEndian, &first); err != nil { + return 0, err + } + if (first & 0x8000) != 0 { + if err := binary.Read(sr, binary.LittleEndian, &second); err != nil { + return 0, err + } + size = (int(first&0x7FFF) << 16) + int(second) + } else { + size = int(first) + } + return size, nil +} + +func readUTF8(sr *io.SectionReader) (string, error) { + // skip utf16 length + _, err := readUTF8length(sr) + if err != nil { + return "", err + } + + // read lenth of string + size, err := readUTF8length(sr) + if err != nil { + return "", err + } + + buf := make([]uint8, size) + if err := binary.Read(sr, binary.LittleEndian, buf); err != nil { + return "", err + } + return string(buf), nil +} + +func readUTF8length(sr *io.SectionReader) (int, error) { + var size int + var first, second uint8 + if err := binary.Read(sr, binary.LittleEndian, &first); err != nil { + return 0, err + } + if (first & 0x80) != 0 { + if err := binary.Read(sr, binary.LittleEndian, &second); err != nil { + return 0, err + } + size = (int(first&0x7F) << 8) + int(second) + } else { + size = int(first) + } + return size, nil +} + +func newZeroFilledReader(r io.Reader, actual int64, expected int64) (io.Reader, error) { + if actual >= expected { + // no need to fill + return r, nil + } + + // read `actual' bytes from r, and + buf := new(bytes.Buffer) + if _, err := io.CopyN(buf, r, actual); err != nil { + return nil, err + } + + // fill zero until `expected' bytes + for i := actual; i < expected; i++ { + if err := buf.WriteByte(0x00); err != nil { + return nil, err + } + } + + return buf, nil +} diff --git a/pkg/android_binary/table.go b/pkg/android_binary/table.go new file mode 100644 index 0000000..c1bac03 --- /dev/null +++ b/pkg/android_binary/table.go @@ -0,0 +1,1093 @@ +package android_binary + +import ( + "encoding/binary" + "fmt" + "io" + "strconv" + "strings" + "unsafe" +) + +// ResID is ID for resources. +type ResID uint32 + +// TableFile is a resource table file. +type TableFile struct { + stringPool *ResStringPool + tablePackages map[uint32]*TablePackage +} + +// ResTableHeader is a header of TableFile. +type ResTableHeader struct { + Header ResChunkHeader + PackageCount uint32 +} + +// ResTablePackage is a header of table packages. +type ResTablePackage struct { + Header ResChunkHeader + ID uint32 + Name [128]uint16 + TypeStrings uint32 + LastPublicType uint32 + KeyStrings uint32 + LastPublicKey uint32 +} + +// TablePackage is a table package. +type TablePackage struct { + Header ResTablePackage + TypeStrings *ResStringPool + KeyStrings *ResStringPool + TableTypes []*TableType +} + +// ResTableType is a type of a table. +type ResTableType struct { + Header ResChunkHeader + ID uint8 + Res0 uint8 + Res1 uint16 + EntryCount uint32 + EntriesStart uint32 + Config ResTableConfig +} + +// ScreenLayout describes screen layout. +type ScreenLayout uint8 + +// ScreenLayout bits +const ( + MaskScreenSize ScreenLayout = 0x0f + ScreenSizeAny ScreenLayout = 0x01 + ScreenSizeSmall ScreenLayout = 0x02 + ScreenSizeNormal ScreenLayout = 0x03 + ScreenSizeLarge ScreenLayout = 0x04 + ScreenSizeXLarge ScreenLayout = 0x05 + + MaskScreenLong ScreenLayout = 0x30 + ShiftScreenLong = 4 + ScreenLongAny ScreenLayout = 0x00 + ScreenLongNo ScreenLayout = 0x10 + ScreenLongYes ScreenLayout = 0x20 + + MaskLayoutDir ScreenLayout = 0xC0 + ShiftLayoutDir = 6 + LayoutDirAny ScreenLayout = 0x00 + LayoutDirLTR ScreenLayout = 0x40 + LayoutDirRTL ScreenLayout = 0x80 +) + +// UIMode describes UI mode. +type UIMode uint8 + +// UIMode bits +const ( + MaskUIModeType UIMode = 0x0f + UIModeTypeAny UIMode = 0x01 + UIModeTypeNormal UIMode = 0x02 + UIModeTypeDesk UIMode = 0x03 + UIModeTypeCar UIMode = 0x04 + + MaskUIModeNight UIMode = 0x30 + ShiftUIModeNight = 4 + UIModeNightAny UIMode = 0x00 + UIModeNightNo UIMode = 0x10 + UIModeNightYes UIMode = 0x20 +) + +// InputFlags are input flags. +type InputFlags uint8 + +// input flags +const ( + MaskKeysHidden InputFlags = 0x03 + KeysHiddenAny InputFlags = 0x00 + KeysHiddenNo InputFlags = 0x01 + KeysHiddenYes InputFlags = 0x02 + KeysHiddenSoft InputFlags = 0x03 + + MaskNavHidden InputFlags = 0x0c + NavHiddenAny InputFlags = 0x00 + NavHiddenNo InputFlags = 0x04 + NavHiddenYes InputFlags = 0x08 +) + +// ResTableConfig is a configuration of a table. +type ResTableConfig struct { + Size uint32 + // imsi + Mcc uint16 + Mnc uint16 + + // locale + Language [2]uint8 + Country [2]uint8 + + // screen type + Orientation uint8 + Touchscreen uint8 + Density uint16 + + // inout + Keyboard uint8 + Navigation uint8 + InputFlags InputFlags + InputPad0 uint8 + + // screen size + ScreenWidth uint16 + ScreenHeight uint16 + + // version + SDKVersion uint16 + MinorVersion uint16 + + // screen config + ScreenLayout ScreenLayout + UIMode UIMode + SmallestScreenWidthDp uint16 + + // screen size dp + ScreenWidthDp uint16 + ScreenHeightDp uint16 +} + +// TableType is a collection of resource entries for a particular resource data type. +type TableType struct { + Header *ResTableType + Entries []TableEntry +} + +// ResTableEntry is the beginning of information about an entry in the resource table. +type ResTableEntry struct { + Size uint16 + Flags uint16 + Key ResStringPoolRef +} + +// TableEntry is a entry in a resource table. +type TableEntry struct { + Key *ResTableEntry + Value *ResValue + Flags uint32 +} + +// ResTableTypeSpec is specification of the resources defined by a particular type. +type ResTableTypeSpec struct { + Header ResChunkHeader + ID uint8 + Res0 uint8 + Res1 uint16 + EntryCount uint32 +} + +// IsResID returns whether s is ResId. +func IsResID(s string) bool { + return strings.HasPrefix(s, "@0x") +} + +// ParseResID parses ResId. +func ParseResID(s string) (ResID, error) { + if !IsResID(s) { + return 0, fmt.Errorf("androidbinary: %s is not ResID", s) + } + id, err := strconv.ParseUint(s[3:], 16, 32) + if err != nil { + return 0, err + } + return ResID(id), nil +} + +func (id ResID) String() string { + return fmt.Sprintf("@0x%08X", uint32(id)) +} + +// Package returns the package index of id. +func (id ResID) Package() uint32 { + return uint32(id) >> 24 +} + +// Type returns the type index of id. +func (id ResID) Type() int { + return (int(id) >> 16) & 0xFF +} + +// Entry returns the entry index of id. +func (id ResID) Entry() int { + return int(id) & 0xFFFF +} + +// NewTableFile returns new TableFile. +func NewTableFile(r io.ReaderAt) (*TableFile, error) { + f := new(TableFile) + sr := io.NewSectionReader(r, 0, 1<<63-1) + + header := new(ResTableHeader) + binary.Read(sr, binary.LittleEndian, header) + f.tablePackages = make(map[uint32]*TablePackage) + + offset := int64(header.Header.HeaderSize) + for offset < int64(header.Header.Size) { + chunkHeader, err := f.readChunk(sr, offset) + if err != nil { + return nil, err + } + offset += int64(chunkHeader.Size) + } + return f, nil +} + +func (f *TableFile) findPackage(id uint32) *TablePackage { + if f == nil { + return nil + } + return f.tablePackages[id] +} + +func (p *TablePackage) findEntry(typeIndex, entryIndex int, config *ResTableConfig) TableEntry { + var best *TableType + for _, t := range p.TableTypes { + switch { + case int(t.Header.ID) != typeIndex: + // nothing to do + case !t.Header.Config.Match(config): + // nothing to do + case entryIndex >= len(t.Entries): + // nothing to do + case t.Entries[entryIndex].Value == nil: + // nothing to do + case best == nil || t.Header.Config.IsBetterThan(&best.Header.Config, config): + best = t + } + } + if best == nil || entryIndex >= len(best.Entries) { + return TableEntry{} + } + return best.Entries[entryIndex] +} + +// GetResource returns a resource referenced by id. +func (f *TableFile) GetResource(id ResID, config *ResTableConfig) (any, error) { + p := f.findPackage(id.Package()) + if p == nil { + return nil, fmt.Errorf("androidbinary: package 0x%02X not found", id.Package()) + } + e := p.findEntry(id.Type(), id.Entry(), config) + v := e.Value + if v == nil { + return nil, fmt.Errorf("androidbinary: entry 0x%04X not found", id.Entry()) + } + switch v.DataType { + case TypeNull: + return nil, nil + case TypeString: + return f.GetString(ResStringPoolRef(v.Data)), nil + case TypeIntDec: + return v.Data, nil + case TypeIntHex: + return v.Data, nil + case TypeIntBoolean: + return v.Data != 0, nil + } + return v.Data, nil +} + +// GetString returns a string referenced by ref. +func (f *TableFile) GetString(ref ResStringPoolRef) string { + return f.stringPool.GetString(ref) +} + +func (f *TableFile) readChunk(r io.ReaderAt, offset int64) (*ResChunkHeader, error) { + sr := io.NewSectionReader(r, offset, 1<<63-1-offset) + chunkHeader := &ResChunkHeader{} + if _, err := sr.Seek(0, io.SeekStart); err != nil { + return nil, err + } + if err := binary.Read(sr, binary.LittleEndian, chunkHeader); err != nil { + return nil, err + } + + var err error + if _, err := sr.Seek(0, io.SeekStart); err != nil { + return nil, err + } + switch chunkHeader.Type { + case ResStringPoolChunkType: + f.stringPool, err = readStringPool(sr) + case ResTablePackageType: + var tablePackage *TablePackage + tablePackage, err = readTablePackage(sr) + f.tablePackages[tablePackage.Header.ID] = tablePackage + } + if err != nil { + return nil, err + } + + return chunkHeader, nil +} + +func readTablePackage(sr *io.SectionReader) (*TablePackage, error) { + tablePackage := new(TablePackage) + header := new(ResTablePackage) + if err := binary.Read(sr, binary.LittleEndian, header); err != nil { + return nil, err + } + tablePackage.Header = *header + + srTypes := io.NewSectionReader(sr, int64(header.TypeStrings), int64(header.Header.Size-header.TypeStrings)) + if typeStrings, err := readStringPool(srTypes); err == nil { + tablePackage.TypeStrings = typeStrings + } else { + return nil, err + } + + srKeys := io.NewSectionReader(sr, int64(header.KeyStrings), int64(header.Header.Size-header.KeyStrings)) + if keyStrings, err := readStringPool(srKeys); err == nil { + tablePackage.KeyStrings = keyStrings + } else { + return nil, err + } + + offset := int64(header.Header.HeaderSize) + for offset < int64(header.Header.Size) { + chunkHeader := &ResChunkHeader{} + if _, err := sr.Seek(offset, io.SeekStart); err != nil { + return nil, err + } + if err := binary.Read(sr, binary.LittleEndian, chunkHeader); err != nil { + return nil, err + } + + var err error + chunkReader := io.NewSectionReader(sr, offset, int64(chunkHeader.Size)) + if _, err := sr.Seek(offset, io.SeekStart); err != nil { + return nil, err + } + switch chunkHeader.Type { + case ResTableTypeType: + var tableType *TableType + tableType, err = readTableType(chunkHeader, chunkReader) + tablePackage.TableTypes = append(tablePackage.TableTypes, tableType) + case ResTableTypeSpecType: + _, err = readTableTypeSpec(chunkReader) + } + if err != nil { + return nil, err + } + offset += int64(chunkHeader.Size) + } + + return tablePackage, nil +} + +func readTableType(chunkHeader *ResChunkHeader, sr *io.SectionReader) (*TableType, error) { + // TableType header may be omitted + header := new(ResTableType) + if _, err := sr.Seek(0, io.SeekStart); err != nil { + return nil, err + } + buf, err := newZeroFilledReader(sr, int64(chunkHeader.HeaderSize), int64(unsafe.Sizeof(*header))) + if err != nil { + return nil, err + } + if err := binary.Read(buf, binary.LittleEndian, header); err != nil { + return nil, err + } + + entryIndexes := make([]uint32, header.EntryCount) + if _, err := sr.Seek(int64(header.Header.HeaderSize), io.SeekStart); err != nil { + return nil, err + } + if err := binary.Read(sr, binary.LittleEndian, entryIndexes); err != nil { + return nil, err + } + + entries := make([]TableEntry, header.EntryCount) + for i, index := range entryIndexes { + if index == 0xFFFFFFFF { + continue + } + if _, err := sr.Seek(int64(header.EntriesStart+index), io.SeekStart); err != nil { + return nil, err + } + var key ResTableEntry + binary.Read(sr, binary.LittleEndian, &key) + entries[i].Key = &key + + var val ResValue + binary.Read(sr, binary.LittleEndian, &val) + entries[i].Value = &val + } + return &TableType{ + header, + entries, + }, nil +} + +func readTableTypeSpec(sr *io.SectionReader) ([]uint32, error) { + header := new(ResTableTypeSpec) + if err := binary.Read(sr, binary.LittleEndian, header); err != nil { + return nil, err + } + + flags := make([]uint32, header.EntryCount) + if _, err := sr.Seek(int64(header.Header.HeaderSize), io.SeekStart); err != nil { + return nil, err + } + if err := binary.Read(sr, binary.LittleEndian, flags); err != nil { + return nil, err + } + return flags, nil +} + +// IsMoreSpecificThan returns true if c is more specific than o. +func (c *ResTableConfig) IsMoreSpecificThan(o *ResTableConfig) bool { + // nil ResTableConfig is never more specific than any ResTableConfig + if c == nil { + return false + } + if o == nil { + return false + } + + // imsi + if c.Mcc != o.Mcc { + if c.Mcc == 0 { + return false + } + if o.Mnc == 0 { + return true + } + } + if c.Mnc != o.Mnc { + if c.Mnc == 0 { + return false + } + if o.Mnc == 0 { + return true + } + } + + // locale + if diff := c.IsLocaleMoreSpecificThan(o); diff < 0 { + return false + } else if diff > 0 { + return true + } + + // screen layout + if c.ScreenLayout != 0 || o.ScreenLayout != 0 { + if ((c.ScreenLayout ^ o.ScreenLayout) & MaskLayoutDir) != 0 { + if (c.ScreenLayout & MaskLayoutDir) == 0 { + return false + } + if (o.ScreenLayout & MaskLayoutDir) == 0 { + return true + } + } + } + + // smallest screen width dp + if c.SmallestScreenWidthDp != 0 || o.SmallestScreenWidthDp != 0 { + if c.SmallestScreenWidthDp != o.SmallestScreenWidthDp { + if c.SmallestScreenWidthDp == 0 { + return false + } + if o.SmallestScreenWidthDp == 0 { + return true + } + } + } + + // screen size dp + if c.ScreenWidthDp != 0 || o.ScreenWidthDp != 0 || + c.ScreenHeightDp != 0 || o.ScreenHeightDp != 0 { + if c.ScreenWidthDp != o.ScreenWidthDp { + if c.ScreenWidthDp == 0 { + return false + } + if o.ScreenWidthDp == 0 { + return true + } + } + if c.ScreenHeightDp != o.ScreenHeightDp { + if c.ScreenHeightDp == 0 { + return false + } + if o.ScreenHeightDp == 0 { + return true + } + } + } + + // screen layout + if c.ScreenLayout != 0 || o.ScreenLayout != 0 { + if ((c.ScreenLayout ^ o.ScreenLayout) & MaskScreenSize) != 0 { + if (c.ScreenLayout & MaskScreenSize) == 0 { + return false + } + if (o.ScreenLayout & MaskScreenSize) == 0 { + return true + } + } + if ((c.ScreenLayout ^ o.ScreenLayout) & MaskScreenLong) != 0 { + if (c.ScreenLayout & MaskScreenLong) == 0 { + return false + } + if (o.ScreenLayout & MaskScreenLong) == 0 { + return true + } + } + } + + // orientation + if c.Orientation != o.Orientation { + if c.Orientation == 0 { + return false + } + if o.Orientation == 0 { + return true + } + } + + // uimode + if c.UIMode != 0 || o.UIMode != 0 { + diff := c.UIMode ^ o.UIMode + if (diff & MaskUIModeType) != 0 { + if (c.UIMode & MaskUIModeType) == 0 { + return false + } + if (o.UIMode & MaskUIModeType) == 0 { + return true + } + } + if (diff & MaskUIModeNight) != 0 { + if (c.UIMode & MaskUIModeNight) == 0 { + return false + } + if (o.UIMode & MaskUIModeNight) == 0 { + return true + } + } + } + + // touchscreen + if c.Touchscreen != o.Touchscreen { + if c.Touchscreen == 0 { + return false + } + if o.Touchscreen == 0 { + return true + } + } + + // input + if c.InputFlags != 0 || o.InputFlags != 0 { + myKeysHidden := c.InputFlags & MaskKeysHidden + oKeysHidden := o.InputFlags & MaskKeysHidden + if (myKeysHidden ^ oKeysHidden) != 0 { + if myKeysHidden == 0 { + return false + } + if oKeysHidden == 0 { + return true + } + } + myNavHidden := c.InputFlags & MaskNavHidden + oNavHidden := o.InputFlags & MaskNavHidden + if (myNavHidden ^ oNavHidden) != 0 { + if myNavHidden == 0 { + return false + } + if oNavHidden == 0 { + return true + } + } + } + + if c.Keyboard != o.Keyboard { + if c.Keyboard == 0 { + return false + } + if o.Keyboard == 0 { + return true + } + } + + if c.Navigation != o.Navigation { + if c.Navigation == 0 { + return false + } + if o.Navigation == 0 { + return true + } + } + + // screen size + if c.ScreenWidth != 0 || o.ScreenWidth != 0 || + c.ScreenHeight != 0 || o.ScreenHeight != 0 { + if c.ScreenWidth != o.ScreenWidth { + if c.ScreenWidth == 0 { + return false + } + if o.ScreenWidth == 0 { + return true + } + } + if c.ScreenHeight != o.ScreenHeight { + if c.ScreenHeight == 0 { + return false + } + if o.ScreenHeight == 0 { + return true + } + } + } + + //version + if c.SDKVersion != o.SDKVersion { + if c.SDKVersion == 0 { + return false + } + if o.SDKVersion == 0 { + return true + } + } + if c.MinorVersion != o.MinorVersion { + if c.MinorVersion == 0 { + return false + } + if o.MinorVersion == 0 { + return true + } + } + + return false +} + +// IsBetterThan returns true if c is better than o for the r configuration. +func (c *ResTableConfig) IsBetterThan(o *ResTableConfig, r *ResTableConfig) bool { + if r == nil { + return c.IsMoreSpecificThan(o) + } + + // nil ResTableConfig is never better than any ResTableConfig + if c == nil { + return false + } + if o == nil { + return false + } + + // imsi + if c.Mcc != 0 || c.Mnc != 0 || o.Mcc != 0 || o.Mnc != 0 { + if c.Mcc != o.Mcc && r.Mcc != 0 { + return c.Mcc != 0 + } + if c.Mnc != o.Mnc && r.Mnc != 0 { + return c.Mnc != 0 + } + } + + // locale + if c.IsLocaleBetterThan(o, r) { + return true + } + + // screen layout + if c.ScreenLayout != 0 || o.ScreenLayout != 0 { + myLayoutdir := c.ScreenLayout & MaskLayoutDir + oLayoutdir := o.ScreenLayout & MaskLayoutDir + if (myLayoutdir^oLayoutdir) != 0 && (r.ScreenLayout&MaskLayoutDir) != 0 { + return myLayoutdir > oLayoutdir + } + } + + // smallest screen width dp + if c.SmallestScreenWidthDp != 0 || o.SmallestScreenWidthDp != 0 { + if c.SmallestScreenWidthDp != o.SmallestScreenWidthDp { + return c.SmallestScreenWidthDp > o.SmallestScreenWidthDp + } + } + + // screen size dp + if c.ScreenWidthDp != 0 || c.ScreenHeightDp != 0 || o.ScreenWidthDp != 0 || o.ScreenHeightDp != 0 { + myDelta := 0 + otherDelta := 0 + if r.ScreenWidthDp != 0 { + myDelta += int(r.ScreenWidthDp) - int(c.ScreenWidthDp) + otherDelta += int(r.ScreenWidthDp) - int(o.ScreenWidthDp) + } + if r.ScreenHeightDp != 0 { + myDelta += int(r.ScreenHeightDp) - int(c.ScreenHeightDp) + otherDelta += int(r.ScreenHeightDp) - int(o.ScreenHeightDp) + } + if myDelta != otherDelta { + return myDelta < otherDelta + } + } + + // screen layout + if c.ScreenLayout != 0 || o.ScreenLayout != 0 { + mySL := c.ScreenLayout & MaskScreenSize + oSL := o.ScreenLayout & MaskScreenSize + if (mySL^oSL) != 0 && (r.ScreenLayout&MaskScreenSize) != 0 { + fixedMySL := mySL + fixedOSL := oSL + if (r.ScreenLayout & MaskScreenSize) >= ScreenSizeNormal { + if fixedMySL == 0 { + fixedMySL = ScreenSizeNormal + } + if fixedOSL == 0 { + fixedOSL = ScreenSizeNormal + } + } + if fixedMySL == fixedOSL { + return mySL != 0 + } + return fixedMySL > fixedOSL + } + + if ((c.ScreenLayout^o.ScreenLayout)&MaskScreenLong) != 0 && + (r.ScreenLayout&MaskScreenLong) != 0 { + return (c.ScreenLayout & MaskScreenLong) != 0 + } + } + + // orientation + if c.Orientation != o.Orientation && r.Orientation != 0 { + return c.Orientation != 0 + } + + // uimode + if c.UIMode != 0 || o.UIMode != 0 { + diff := c.UIMode ^ o.UIMode + if (diff&MaskUIModeType) != 0 && (r.UIMode&MaskUIModeType) != 0 { + return (c.UIMode & MaskUIModeType) != 0 + } + if (diff&MaskUIModeNight) != 0 && (r.UIMode&MaskUIModeNight) != 0 { + return (c.UIMode & MaskUIModeNight) != 0 + } + } + + // screen type + if c.Density != o.Density { + h := int(c.Density) + if h == 0 { + h = 160 + } + l := int(o.Density) + if l == 0 { + l = 160 + } + blmBigger := true + if l > h { + h, l = l, h + blmBigger = false + } + + reqValue := int(r.Density) + if reqValue == 0 { + reqValue = 160 + } + if reqValue >= h { + return blmBigger + } + if l >= reqValue { + return !blmBigger + } + if (2*l-reqValue)*h > reqValue*reqValue { + return !blmBigger + } + return blmBigger + } + if c.Touchscreen != o.Touchscreen && r.Touchscreen != 0 { + return c.Touchscreen != 0 + } + + // input + if c.InputFlags != 0 || o.InputFlags != 0 { + myKeysHidden := c.InputFlags & MaskKeysHidden + oKeysHidden := o.InputFlags & MaskKeysHidden + reqKeysHidden := r.InputFlags & MaskKeysHidden + if myKeysHidden != oKeysHidden && reqKeysHidden != 0 { + switch { + case myKeysHidden == 0: + return false + case oKeysHidden == 0: + return true + case reqKeysHidden == myKeysHidden: + return true + case reqKeysHidden == oKeysHidden: + return false + } + } + myNavHidden := c.InputFlags & MaskNavHidden + oNavHidden := o.InputFlags & MaskNavHidden + reqNavHidden := r.InputFlags & MaskNavHidden + if myNavHidden != oNavHidden && reqNavHidden != 0 { + switch { + case myNavHidden == 0: + return false + case oNavHidden == 0: + return true + } + } + } + if c.Keyboard != o.Keyboard && r.Keyboard != 0 { + return c.Keyboard != 0 + } + if c.Navigation != o.Navigation && r.Navigation != 0 { + return c.Navigation != 0 + } + + // screen size + if c.ScreenWidth != 0 || c.ScreenHeight != 0 || o.ScreenWidth != 0 || o.ScreenHeight != 0 { + myDelta := 0 + otherDelta := 0 + if r.ScreenWidth != 0 { + myDelta += int(r.ScreenWidth) - int(c.ScreenWidth) + otherDelta += int(r.ScreenWidth) - int(o.ScreenWidth) + } + if r.ScreenHeight != 0 { + myDelta += int(r.ScreenHeight) - int(c.ScreenHeight) + otherDelta += int(r.ScreenHeight) - int(o.ScreenHeight) + } + if myDelta != otherDelta { + return myDelta < otherDelta + } + } + + // version + if c.SDKVersion != 0 || o.MinorVersion != 0 { + if c.SDKVersion != o.SDKVersion && r.SDKVersion != 0 { + return c.SDKVersion > o.SDKVersion + } + if c.MinorVersion != o.MinorVersion && r.MinorVersion != 0 { + return c.MinorVersion != 0 + } + } + + return false +} + +// IsLocaleMoreSpecificThan a positive integer if this config is more specific than o, +// a negative integer if |o| is more specific +// and 0 if they're equally specific. +func (c *ResTableConfig) IsLocaleMoreSpecificThan(o *ResTableConfig) int { + if (c.Language != [2]uint8{} || c.Country != [2]uint8{}) || (o.Language != [2]uint8{} || o.Country != [2]uint8{}) { + if c.Language != o.Language { + if c.Language == [2]uint8{} { + return -1 + } + if o.Language == [2]uint8{} { + return 1 + } + } + + if c.Country != o.Country { + if c.Country == [2]uint8{} { + return -1 + } + if o.Country == [2]uint8{} { + return 1 + } + } + } + return 0 +} + +// IsLocaleBetterThan returns true if c is a better locale match than o for the r configuration. +func (c *ResTableConfig) IsLocaleBetterThan(o *ResTableConfig, r *ResTableConfig) bool { + if r.Language == [2]uint8{} && r.Country == [2]uint8{} { + // The request doesn't have a locale, so no resource is better + // than the other. + return false + } + + if c.Language == [2]uint8{} && c.Country == [2]uint8{} && o.Language == [2]uint8{} && o.Country == [2]uint8{} { + // The locales parts of both resources are empty, so no one is better + // than the other. + return false + } + + if c.Language != o.Language { + // The languages of the two resources are not the same. + + // the US English resource have traditionally lived for most apps. + if r.Language == [2]uint8{'e', 'n'} { + if r.Country == [2]uint8{'U', 'S'} { + if c.Language != [2]uint8{} { + return c.Country == [2]uint8{} || c.Country == [2]uint8{'U', 'S'} + } + return !(c.Country == [2]uint8{} || c.Country == [2]uint8{'U', 'S'}) + } + } + return c.Language != [2]uint8{} + } + + if c.Country != o.Country { + return c.Country != [2]uint8{} + } + + return false +} + +// Match returns true if c can be considered a match for the parameters in settings. +func (c *ResTableConfig) Match(settings *ResTableConfig) bool { + // nil ResTableConfig always matches. + if settings == nil { + return true + } else if c == nil { + return *settings == ResTableConfig{} + } + + // match imsi + if settings.Mcc == 0 { + if c.Mcc != 0 { + return false + } + } else { + if c.Mcc != 0 && c.Mcc != settings.Mcc { + return false + } + } + if settings.Mnc == 0 { + if c.Mnc != 0 { + return false + } + } else { + if c.Mnc != 0 && c.Mnc != settings.Mnc { + return false + } + } + + // match locale + if c.Language != [2]uint8{0, 0} { + // Don't consider country and variants when deciding matches. + // If two configs differ only in their country and variant, + // they can be weeded out in the isMoreSpecificThan test. + if c.Language != settings.Language { + return false + } + + if c.Country != [2]uint8{0, 0} { + if c.Country != settings.Country { + return false + } + } + } + + // screen layout + layoutDir := c.ScreenLayout & MaskLayoutDir + setLayoutDir := settings.ScreenLayout & MaskLayoutDir + if layoutDir != 0 && layoutDir != setLayoutDir { + return false + } + + screenSize := c.ScreenLayout & MaskScreenSize + setScreenSize := settings.ScreenLayout & MaskScreenSize + if screenSize != 0 && screenSize > setScreenSize { + return false + } + + screenLong := c.ScreenLayout & MaskScreenLong + setScreenLong := settings.ScreenLayout & MaskScreenLong + if screenLong != 0 && screenLong != setScreenLong { + return false + } + + // ui mode + uiModeType := c.UIMode & MaskUIModeType + setUIModeType := settings.UIMode & MaskUIModeType + if uiModeType != 0 && uiModeType != setUIModeType { + return false + } + + uiModeNight := c.UIMode & MaskUIModeNight + setUIModeNight := settings.UIMode & MaskUIModeNight + if uiModeNight != 0 && uiModeNight != setUIModeNight { + return false + } + + // smallest screen width dp + if c.SmallestScreenWidthDp != 0 && + c.SmallestScreenWidthDp > settings.SmallestScreenWidthDp { + return false + } + + // screen size dp + if c.ScreenWidthDp != 0 && + c.ScreenWidthDp > settings.ScreenWidthDp { + return false + } + if c.ScreenHeightDp != 0 && + c.ScreenHeightDp > settings.ScreenHeightDp { + return false + } + + // screen type + if c.Orientation != 0 && c.Orientation != settings.Orientation { + return false + } + if c.Touchscreen != 0 && c.Touchscreen != settings.Touchscreen { + return false + } + + // input + if c.InputFlags != 0 { + myKeysHidden := c.InputFlags & MaskKeysHidden + oKeysHidden := settings.InputFlags & MaskKeysHidden + if myKeysHidden != 0 && myKeysHidden != oKeysHidden { + if myKeysHidden != KeysHiddenNo || oKeysHidden != KeysHiddenSoft { + return false + } + } + myNavHidden := c.InputFlags & MaskNavHidden + oNavHidden := settings.InputFlags & MaskNavHidden + if myNavHidden != 0 && myNavHidden != oNavHidden { + return false + } + } + if c.Keyboard != 0 && c.Keyboard != settings.Keyboard { + return false + } + if c.Navigation != 0 && c.Navigation != settings.Navigation { + return false + } + + // screen size + if c.ScreenWidth != 0 && + c.ScreenWidth > settings.ScreenWidth { + return false + } + if c.ScreenHeight != 0 && + c.ScreenHeight > settings.ScreenHeight { + return false + } + + // version + if settings.SDKVersion != 0 && c.SDKVersion != 0 && + c.SDKVersion > settings.SDKVersion { + return false + } + if settings.MinorVersion != 0 && c.MinorVersion != 0 && + c.MinorVersion != settings.MinorVersion { + return false + } + + return true +} + +// Locale returns the locale of the configuration. +func (c *ResTableConfig) Locale() string { + if c.Language[0] == 0 { + return "" + } + if c.Country[0] == 0 { + return fmt.Sprintf("%c%c", c.Language[0], c.Language[1]) + } + return fmt.Sprintf("%c%c-%c%c", c.Language[0], c.Language[1], c.Country[0], c.Country[1]) +} diff --git a/pkg/android_binary/type.go b/pkg/android_binary/type.go new file mode 100644 index 0000000..3f0116e --- /dev/null +++ b/pkg/android_binary/type.go @@ -0,0 +1,333 @@ +package android_binary + +import ( + "encoding/xml" + "fmt" + "reflect" + "strconv" +) + +type injector interface { + inject(table *TableFile, config *ResTableConfig) +} + +var injectorType = reflect.TypeOf((*injector)(nil)).Elem() + +func inject(val reflect.Value, table *TableFile, config *ResTableConfig) { + if val.Kind() == reflect.Ptr { + if val.IsNil() { + return + } + val = val.Elem() + } + if val.CanInterface() && val.Type().Implements(injectorType) { + val.Interface().(injector).inject(table, config) + return + } + if val.CanAddr() { + pv := val.Addr() + if pv.CanInterface() && pv.Type().Implements(injectorType) { + pv.Interface().(injector).inject(table, config) + return + } + } + + switch val.Kind() { + default: + // ignore other types + return + case reflect.Slice, reflect.Array: + l := val.Len() + for i := 0; i < l; i++ { + inject(val.Index(i), table, config) + } + return + case reflect.Struct: + l := val.NumField() + for i := 0; i < l; i++ { + inject(val.Field(i), table, config) + } + } +} + +// Bool is a boolean value in XML file. +// It may be an immediate value or a reference. +type Bool struct { + value string + table *TableFile + config *ResTableConfig +} + +// WithTableFile ties TableFile to the Bool. +func (v Bool) WithTableFile(table *TableFile) Bool { + return Bool{ + value: v.value, + table: table, + config: v.config, + } +} + +// WithResTableConfig ties ResTableConfig to the Bool. +func (v Bool) WithResTableConfig(config *ResTableConfig) Bool { + return Bool{ + value: v.value, + table: v.table, + config: config, + } +} + +func (v *Bool) inject(table *TableFile, config *ResTableConfig) { + v.table = table + v.config = config +} + +// SetBool sets a boolean value. +func (v *Bool) SetBool(value bool) { + v.value = strconv.FormatBool(value) +} + +// SetResID sets a boolean value with the resource id. +func (v *Bool) SetResID(resID ResID) { + v.value = resID.String() +} + +// UnmarshalXMLAttr implements xml.UnmarshalerAttr. +func (v *Bool) UnmarshalXMLAttr(attr xml.Attr) error { + v.value = attr.Value + return nil +} + +// MarshalXMLAttr implements xml.MarshalerAttr. +func (v Bool) MarshalXMLAttr(name xml.Name) (xml.Attr, error) { + if v.value == "" { + // return the zero value of bool + return xml.Attr{ + Name: name, + Value: "false", + }, nil + } + return xml.Attr{ + Name: name, + Value: v.value, + }, nil +} + +// Bool returns the boolean value. +// It resolves the reference if needed. +func (v Bool) Bool() (bool, error) { + if v.value == "" { + return false, nil + } + if !IsResID(v.value) { + return strconv.ParseBool(v.value) + } + id, err := ParseResID(v.value) + if err != nil { + return false, err + } + value, err := v.table.GetResource(id, v.config) + if err != nil { + return false, err + } + ret, ok := value.(bool) + if !ok { + return false, fmt.Errorf("invalid type: %T", value) + } + return ret, nil +} + +// MustBool is same as Bool, but it panics if it fails to parse the value. +func (v Bool) MustBool() bool { + ret, err := v.Bool() + if err != nil { + panic(err) + } + return ret +} + +// Int32 is an integer value in XML file. +// It may be an immediate value or a reference. +type Int32 struct { + value string + table *TableFile + config *ResTableConfig +} + +// WithTableFile ties TableFile to the Bool. +func (v Int32) WithTableFile(table *TableFile) Int32 { + return Int32{ + value: v.value, + table: table, + config: v.config, + } +} + +// WithResTableConfig ties ResTableConfig to the Bool. +func (v Int32) WithResTableConfig(config *ResTableConfig) Bool { + return Bool{ + value: v.value, + table: v.table, + config: config, + } +} + +func (v *Int32) inject(table *TableFile, config *ResTableConfig) { + v.table = table + v.config = config +} + +// SetInt32 sets an integer value. +func (v *Int32) SetInt32(value int32) { + v.value = strconv.FormatInt(int64(value), 10) +} + +// SetResID sets a boolean value with the resource id. +func (v *Int32) SetResID(resID ResID) { + v.value = resID.String() +} + +// UnmarshalXMLAttr implements xml.UnmarshalerAttr. +func (v *Int32) UnmarshalXMLAttr(attr xml.Attr) error { + v.value = attr.Value + return nil +} + +// MarshalXMLAttr implements xml.MarshalerAttr. +func (v Int32) MarshalXMLAttr(name xml.Name) (xml.Attr, error) { + if v.value == "" { + // return the zero value of int32 + return xml.Attr{ + Name: name, + Value: "0", + }, nil + } + return xml.Attr{ + Name: name, + Value: v.value, + }, nil +} + +// Int32 returns the integer value. +// It resolves the reference if needed. +func (v Int32) Int32() (int32, error) { + if v.value == "" { + return 0, nil + } + if !IsResID(v.value) { + v, err := strconv.ParseInt(v.value, 10, 32) + return int32(v), err + } + id, err := ParseResID(v.value) + if err != nil { + return 0, err + } + value, err := v.table.GetResource(id, v.config) + if err != nil { + return 0, err + } + ret, ok := value.(uint32) + if !ok { + return 0, fmt.Errorf("invalid type: %T", value) + } + return int32(ret), nil +} + +// MustInt32 is same as Int32, but it panics if it fails to parse the value. +func (v Int32) MustInt32() int32 { + ret, err := v.Int32() + if err != nil { + panic(err) + } + return ret +} + +// String is a boolean value in XML file. +// It may be an immediate value or a reference. +type String struct { + value string + table *TableFile + config *ResTableConfig +} + +// WithTableFile ties TableFile to the Bool. +func (v String) WithTableFile(table *TableFile) String { + return String{ + value: v.value, + table: table, + config: v.config, + } +} + +// WithResTableConfig ties ResTableConfig to the Bool. +func (v String) WithResTableConfig(config *ResTableConfig) String { + return String{ + value: v.value, + table: v.table, + config: config, + } +} + +func (v *String) inject(table *TableFile, config *ResTableConfig) { + v.table = table + v.config = config +} + +// SetString sets a string value. +func (v *String) SetString(value string) { + v.value = value +} + +// SetResID sets a boolean value with the resource id. +func (v *String) SetResID(resID ResID) { + v.value = resID.String() +} + +// UnmarshalXMLAttr implements xml.UnmarshalerAttr. +func (v *String) UnmarshalXMLAttr(attr xml.Attr) error { + v.value = attr.Value + return nil +} + +// MarshalXMLAttr implements xml.MarshalerAttr. +func (v String) MarshalXMLAttr(name xml.Name) (xml.Attr, error) { + return xml.Attr{ + Name: name, + Value: v.value, + }, nil +} + +// String returns the string value. +// It resolves the reference if needed. +func (v String) String() (string, error) { + if !IsResID(v.value) { + return v.value, nil + } + id, err := ParseResID(v.value) + if err != nil { + return "", err + } + + value, err := v.table.GetResource(id, v.config) + if err != nil { + return "", err + } + + //todo 读取套娃 + switch value.(type) { + case string: + return value.(string), nil + case uint32: + return fmt.Sprintf("%d", value.(uint32)), nil + default: + return "", nil + } +} + +// MustString is same as String, but it panics if it fails to parse the value. +func (v String) MustString() string { + ret, err := v.String() + if err != nil { + panic(err) + } + return ret +} diff --git a/pkg/android_binary/xml.go b/pkg/android_binary/xml.go new file mode 100644 index 0000000..26c3654 --- /dev/null +++ b/pkg/android_binary/xml.go @@ -0,0 +1,271 @@ +package android_binary + +import ( + "bytes" + "encoding/binary" + "encoding/xml" + "fmt" + "io" + "reflect" +) + +// XMLFile is an XML file expressed in binary format. +type XMLFile struct { + stringPool *ResStringPool + resourceMap []uint32 + notPrecessedNS map[ResStringPoolRef]ResStringPoolRef + namespaces map[ResStringPoolRef]ResStringPoolRef + xmlBuffer bytes.Buffer +} + +// ResXMLTreeNode is basic XML tree node. +type ResXMLTreeNode struct { + Header ResChunkHeader + LineNumber uint32 + Comment ResStringPoolRef +} + +// ResXMLTreeNamespaceExt is extended XML tree node for namespace start/end nodes. +type ResXMLTreeNamespaceExt struct { + Prefix ResStringPoolRef + URI ResStringPoolRef +} + +// ResXMLTreeAttrExt is extended XML tree node for start tags -- includes attribute. +type ResXMLTreeAttrExt struct { + NS ResStringPoolRef + Name ResStringPoolRef + AttributeStart uint16 + AttributeSize uint16 + AttributeCount uint16 + IDIndex uint16 + ClassIndex uint16 + StyleIndex uint16 +} + +// ResXMLTreeAttribute is an attribute of start tags. +type ResXMLTreeAttribute struct { + NS ResStringPoolRef + Name ResStringPoolRef + RawValue ResStringPoolRef + TypedValue ResValue +} + +// ResXMLTreeEndElementExt is extended XML tree node for element start/end nodes. +type ResXMLTreeEndElementExt struct { + NS ResStringPoolRef + Name ResStringPoolRef +} + +// NewXMLFile returns a new XMLFile. +func NewXMLFile(r io.ReaderAt) (*XMLFile, error) { + f := new(XMLFile) + sr := io.NewSectionReader(r, 0, 1<<63-1) + + fmt.Fprintf(&f.xmlBuffer, xml.Header) + + header := new(ResChunkHeader) + if err := binary.Read(sr, binary.LittleEndian, header); err != nil { + return nil, err + } + offset := int64(header.HeaderSize) + for offset < int64(header.Size) { + chunkHeader, err := f.readChunk(r, offset) + if err != nil { + return nil, err + } + offset += int64(chunkHeader.Size) + } + return f, nil +} + +// Reader returns a reader of XML file expressed in text format. +func (f *XMLFile) Reader() *bytes.Reader { + return bytes.NewReader(f.xmlBuffer.Bytes()) +} + +// Decode decodes XML file and stores the result in the value pointed to by v. +// To resolve the resource references, Decode also stores default TableFile and ResTableConfig in the value pointed to by v. +func (f *XMLFile) Decode(v any, table *TableFile, config *ResTableConfig) error { + decoder := xml.NewDecoder(f.Reader()) + if err := decoder.Decode(v); err != nil { + return err + } + inject(reflect.ValueOf(v), table, config) + return nil +} + +func (f *XMLFile) readChunk(r io.ReaderAt, offset int64) (*ResChunkHeader, error) { + sr := io.NewSectionReader(r, offset, 1<<63-1-offset) + chunkHeader := &ResChunkHeader{} + if _, err := sr.Seek(0, io.SeekStart); err != nil { + return nil, err + } + if err := binary.Read(sr, binary.LittleEndian, chunkHeader); err != nil { + return nil, err + } + + var err error + if _, err := sr.Seek(0, io.SeekStart); err != nil { + return nil, err + } + switch chunkHeader.Type { + case ResStringPoolChunkType: + f.stringPool, err = readStringPool(sr) + case ResXMLStartNamespaceType: + err = f.readStartNamespace(sr) + case ResXMLEndNamespaceType: + err = f.readEndNamespace(sr) + case ResXMLStartElementType: + err = f.readStartElement(sr) + case ResXMLEndElementType: + err = f.readEndElement(sr) + } + if err != nil { + return nil, err + } + + return chunkHeader, nil +} + +// GetString returns a string referenced by ref. +func (f *XMLFile) GetString(ref ResStringPoolRef) string { + return f.stringPool.GetString(ref) +} + +func (f *XMLFile) readStartNamespace(sr *io.SectionReader) error { + header := new(ResXMLTreeNode) + if err := binary.Read(sr, binary.LittleEndian, header); err != nil { + return err + } + + if _, err := sr.Seek(int64(header.Header.HeaderSize), io.SeekStart); err != nil { + return err + } + namespace := new(ResXMLTreeNamespaceExt) + if err := binary.Read(sr, binary.LittleEndian, namespace); err != nil { + return err + } + + if f.notPrecessedNS == nil { + f.notPrecessedNS = make(map[ResStringPoolRef]ResStringPoolRef) + } + f.notPrecessedNS[namespace.URI] = namespace.Prefix + + if f.namespaces == nil { + f.namespaces = make(map[ResStringPoolRef]ResStringPoolRef) + } + f.namespaces[namespace.URI] = namespace.Prefix + + return nil +} + +func (f *XMLFile) readEndNamespace(sr *io.SectionReader) error { + header := new(ResXMLTreeNode) + if err := binary.Read(sr, binary.LittleEndian, header); err != nil { + return err + } + + if _, err := sr.Seek(int64(header.Header.HeaderSize), io.SeekStart); err != nil { + return err + } + namespace := new(ResXMLTreeNamespaceExt) + if err := binary.Read(sr, binary.LittleEndian, namespace); err != nil { + return err + } + delete(f.namespaces, namespace.URI) + return nil +} + +func (f *XMLFile) addNamespacePrefix(ns, name ResStringPoolRef) string { + if ns != NilResStringPoolRef { + prefix := f.GetString(f.namespaces[ns]) + return fmt.Sprintf("%s:%s", prefix, f.GetString(name)) + } + return f.GetString(name) +} + +func (f *XMLFile) readStartElement(sr *io.SectionReader) error { + header := new(ResXMLTreeNode) + if err := binary.Read(sr, binary.LittleEndian, header); err != nil { + return err + } + + if _, err := sr.Seek(int64(header.Header.HeaderSize), io.SeekStart); err != nil { + return err + } + ext := new(ResXMLTreeAttrExt) + if err := binary.Read(sr, binary.LittleEndian, ext); err != nil { + return nil + } + + fmt.Fprintf(&f.xmlBuffer, "<%s", f.addNamespacePrefix(ext.NS, ext.Name)) + + // output XML namespaces + if f.notPrecessedNS != nil { + for uri, prefix := range f.notPrecessedNS { + fmt.Fprintf(&f.xmlBuffer, " xmlns:%s=\"", f.GetString(prefix)) + xml.Escape(&f.xmlBuffer, []byte(f.GetString(uri))) + fmt.Fprint(&f.xmlBuffer, "\"") + } + f.notPrecessedNS = nil + } + + // process attributes + offset := int64(ext.AttributeStart + header.Header.HeaderSize) + for i := 0; i < int(ext.AttributeCount); i++ { + if _, err := sr.Seek(offset, io.SeekStart); err != nil { + return err + } + attr := new(ResXMLTreeAttribute) + binary.Read(sr, binary.LittleEndian, attr) + + var value string + if attr.RawValue != NilResStringPoolRef { + value = f.GetString(attr.RawValue) + } else { + data := attr.TypedValue.Data + switch attr.TypedValue.DataType { + case TypeNull: + value = "" + case TypeReference: + value = fmt.Sprintf("@0x%08X", data) + case TypeIntDec: + value = fmt.Sprintf("%d", data) + case TypeIntHex: + value = fmt.Sprintf("0x%08X", data) + case TypeIntBoolean: + if data != 0 { + value = "true" + } else { + value = "false" + } + default: + value = fmt.Sprintf("@0x%08X", data) + } + } + + fmt.Fprintf(&f.xmlBuffer, " %s=\"", f.addNamespacePrefix(attr.NS, attr.Name)) + xml.Escape(&f.xmlBuffer, []byte(value)) + fmt.Fprint(&f.xmlBuffer, "\"") + offset += int64(ext.AttributeSize) + } + fmt.Fprint(&f.xmlBuffer, ">") + return nil +} + +func (f *XMLFile) readEndElement(sr *io.SectionReader) error { + header := new(ResXMLTreeNode) + if err := binary.Read(sr, binary.LittleEndian, header); err != nil { + return err + } + if _, err := sr.Seek(int64(header.Header.HeaderSize), io.SeekStart); err != nil { + return err + } + ext := new(ResXMLTreeEndElementExt) + if err := binary.Read(sr, binary.LittleEndian, ext); err != nil { + return err + } + fmt.Fprintf(&f.xmlBuffer, "", f.addNamespacePrefix(ext.NS, ext.Name)) + return nil +} diff --git a/pkg/ants/pool.go b/pkg/ants/pool.go new file mode 100644 index 0000000..cfc65da --- /dev/null +++ b/pkg/ants/pool.go @@ -0,0 +1,108 @@ +package ants + +import ( + "errors" + "fmt" + "git.bvbej.com/bvbej/base-golang/pkg/ticker" + "github.com/panjf2000/ants/v2" + "go.uber.org/zap" + "runtime/debug" + "time" +) + +var _ GoroutinePool = (*goroutinePool)(nil) + +type GoroutinePool interface { + run() + + Submit(task func()) + Stop() + + Size() int + Running() int + Free() int +} + +type goroutinePool struct { + pool *ants.Pool + logger *zap.Logger + ticker ticker.Ticker + step int +} + +type poolLogger struct { + zap *zap.Logger +} + +func (l *poolLogger) Printf(format string, args ...any) { + l.zap.Sugar().Infof(format, args) +} + +func NewPool(zapLogger *zap.Logger, step int) (GoroutinePool, error) { + ttl := time.Minute * 5 + + options := ants.Options{ + Nonblocking: true, + ExpiryDuration: ttl, + PanicHandler: func(err any) { + zapLogger.Sugar().Error( + "GoroutinePool panic", + zap.String("error", fmt.Sprintf("%+v", err)), + zap.String("stack", string(debug.Stack())), + ) + }, + Logger: &poolLogger{zap: zapLogger}, + } + + antsPool, err := ants.NewPool(step, ants.WithOptions(options)) + if err != nil { + return nil, err + } + + pool := &goroutinePool{ + pool: antsPool, + logger: zapLogger, + ticker: ticker.New(ttl), + step: step, + } + pool.run() + + return pool, nil +} + +func (p *goroutinePool) run() { + p.ticker.Process(func() { + if p.Free() > p.step { + mul := p.Free() / p.step + p.pool.Tune(p.Size() - p.step*mul) + } + }) +} + +func (p *goroutinePool) Submit(task func()) { + if p.pool.IsClosed() { + return + } + err := p.pool.Submit(task) + if errors.Is(err, ants.ErrPoolOverload) { + p.pool.Tune(p.Size() + p.step) + p.Submit(task) + } +} + +func (p *goroutinePool) Size() int { + return p.pool.Cap() +} + +func (p *goroutinePool) Running() int { + return p.pool.Running() +} + +func (p *goroutinePool) Free() int { + return p.pool.Free() +} + +func (p *goroutinePool) Stop() { + p.ticker.Stop() + p.pool.Release() +} diff --git a/pkg/apollo/config.go b/pkg/apollo/config.go new file mode 100644 index 0000000..6cf5601 --- /dev/null +++ b/pkg/apollo/config.go @@ -0,0 +1,97 @@ +package apollo + +import ( + "fmt" + + "git.bvbej.com/bvbej/base-golang/pkg/env" + "github.com/apolloconfig/agollo/v4" + "github.com/apolloconfig/agollo/v4/component/log" + apolloConfig "github.com/apolloconfig/agollo/v4/env/config" + "github.com/apolloconfig/agollo/v4/storage" + "github.com/spf13/viper" +) + +type clientConfig struct { + client agollo.Client + ac *apolloConfig.AppConfig + conf any + + onChange func(event *storage.ChangeEvent) + onNewestChange func(*storage.FullChangeEvent) +} + +type Option func(*clientConfig) + +func WithOnChangeEvent(event func(event *storage.ChangeEvent)) Option { + return func(conf *clientConfig) { + conf.onChange = event + } +} + +func WithOnNewestChangeEvent(event func(event *storage.FullChangeEvent)) Option { + return func(conf *clientConfig) { + conf.onNewestChange = event + } +} + +func GetApolloConfig(appId, secret string, config any, opt ...Option) error { + var err error + namespace := env.Active().Value() + ".yaml" + + c := new(clientConfig) + c.conf = config + c.ac = &apolloConfig.AppConfig{ + AppID: appId, + Cluster: "dev", + IP: "https://config.bvbej.com", + NamespaceName: namespace, + IsBackupConfig: false, + Secret: secret, + MustStart: true, + } + for _, option := range opt { + option(c) + } + + agollo.SetLogger(&log.DefaultLogger{}) + + c.client, err = agollo.StartWithConfig(func() (*apolloConfig.AppConfig, error) { + return c.ac, nil + }) + if err != nil { + return fmt.Errorf("get config error:[%s]", err) + } + c.client.AddChangeListener(c) + + err = c.serialization() + if err != nil { + return fmt.Errorf("unmarshal config error:[%s]", err) + } + + return nil +} + +func (c *clientConfig) serialization() error { + parser := viper.New() + + parser.SetConfigType("yaml") + c.client.GetConfigCache(c.ac.NamespaceName).Range(func(key, value any) bool { + parser.Set(key.(string), value) + return true + }) + + return parser.Unmarshal(c.conf) +} + +func (c *clientConfig) OnChange(event *storage.ChangeEvent) { + _ = c.serialization() + if c.onChange != nil { + c.onChange(event) + } +} + +func (c *clientConfig) OnNewestChange(event *storage.FullChangeEvent) { + if c.onNewestChange != nil { + c.onNewestChange(event) + } +} diff --git a/pkg/auth/config.go b/pkg/auth/config.go new file mode 100644 index 0000000..13d8e94 --- /dev/null +++ b/pkg/auth/config.go @@ -0,0 +1,29 @@ +package auth + +import "time" + +// Config authorization configuration parameters +type Config struct { + // access token expiration time, 0 means it doesn't expire + AccessTokenExp time.Duration + // refresh token expiration time, 0 means it doesn't expire + RefreshTokenExp time.Duration + // whether to generate the refreshing token + IsGenerateRefresh bool +} + +// RefreshConfig refreshing token config +type RefreshConfig struct { + // whether to reset the refreshing creation time + IsResetRefreshTime bool + // whether to remove access token + IsRemoveAccess bool + // whether to remove refreshing token + IsRemoveRefreshing bool +} + +// default configs +var ( + DefaultAccessTokenCfg = &Config{AccessTokenExp: time.Hour * 24, RefreshTokenExp: time.Hour * 24 * 7, IsGenerateRefresh: true} + DefaultRefreshTokenCfg = &RefreshConfig{IsResetRefreshTime: true, IsRemoveAccess: true, IsRemoveRefreshing: true} +) diff --git a/pkg/auth/error.go b/pkg/auth/error.go new file mode 100644 index 0000000..e392bee --- /dev/null +++ b/pkg/auth/error.go @@ -0,0 +1,12 @@ +package auth + +import "errors" + +var New = errors.New + +var ( + ErrInvalidAccessToken = errors.New("invalid access token") + ErrInvalidRefreshToken = errors.New("invalid refresh token") + ErrExpiredAccessToken = errors.New("expired access token") + ErrExpiredRefreshToken = errors.New("expired refresh token") +) diff --git a/pkg/auth/generate.go b/pkg/auth/generate.go new file mode 100644 index 0000000..cfe5db2 --- /dev/null +++ b/pkg/auth/generate.go @@ -0,0 +1,17 @@ +package auth + +import ( + "time" +) + +type ( + GenerateBasic struct { + UserID string + CreateAt time.Time + TokenInfo TokenInfo + } + + AccessGenerate interface { + Token(data *GenerateBasic, isGenRefresh bool) (access, refresh string, err error) + } +) diff --git a/pkg/auth/jwt_access.go b/pkg/auth/jwt_access.go new file mode 100644 index 0000000..a0a42c8 --- /dev/null +++ b/pkg/auth/jwt_access.go @@ -0,0 +1,97 @@ +package auth + +import ( + "encoding/base64" + "errors" + "strings" + "time" + + "github.com/golang-jwt/jwt/v4" + "github.com/google/uuid" +) + +// JWTAccessClaims jwt claims +type JWTAccessClaims struct { + jwt.RegisteredClaims +} + +// Valid claims verification +func (a *JWTAccessClaims) Valid() error { + if a.ExpiresAt.Before(time.Now()) { + return ErrInvalidAccessToken + } + return nil +} + +// NewJWTAccessGenerate create to generate the jwt access token instance +func NewJWTAccessGenerate(key []byte, method jwt.SigningMethod) *JWTAccessGenerate { + return &JWTAccessGenerate{ + SignedKey: key, + SignedMethod: method, + } +} + +// JWTAccessGenerate generate the jwt access token +type JWTAccessGenerate struct { + SignedKey []byte + SignedMethod jwt.SigningMethod +} + +// Token based on the UUID generated token +func (a *JWTAccessGenerate) Token(data *GenerateBasic, isGenRefresh bool) (string, string, error) { + claims := &JWTAccessClaims{ + RegisteredClaims: jwt.RegisteredClaims{ + Issuer: "BvBeJ", + Subject: data.UserID, + ExpiresAt: jwt.NewNumericDate(data.TokenInfo.GetAccessCreateAt().Add(data.TokenInfo.GetAccessExpiresIn())), + }, + } + + token := jwt.NewWithClaims(a.SignedMethod, claims) + var key any + if a.isEs() { + v, err := jwt.ParseECPrivateKeyFromPEM(a.SignedKey) + if err != nil { + return "", "", err + } + key = v + } else if a.isRsOrPS() { + v, err := jwt.ParseRSAPrivateKeyFromPEM(a.SignedKey) + if err != nil { + return "", "", err + } + key = v + } else if a.isHs() { + key = a.SignedKey + } else { + return "", "", errors.New("unsupported sign method") + } + + access, err := token.SignedString(key) + if err != nil { + return "", "", err + } + refresh := "" + + if isGenRefresh { + t := uuid.NewSHA1(uuid.Must(uuid.NewRandom()), []byte(access)).String() + refresh = base64.URLEncoding.EncodeToString([]byte(t)) + refresh = strings.ToUpper(strings.TrimRight(refresh, "=")) + } + + return access, refresh, nil +} + +func (a *JWTAccessGenerate) isEs() bool { + return strings.HasPrefix(a.SignedMethod.Alg(), "ES") +} + +func (a *JWTAccessGenerate) isRsOrPS() bool { + isRs := strings.HasPrefix(a.SignedMethod.Alg(), "RS") + isPs := strings.HasPrefix(a.SignedMethod.Alg(), "PS") + return isRs || isPs +} + +func (a *JWTAccessGenerate) isHs() bool { + return strings.HasPrefix(a.SignedMethod.Alg(), "HS") +} diff --git a/pkg/auth/manager.go b/pkg/auth/manager.go new file mode 100644 index 0000000..c30dff5 --- /dev/null +++ b/pkg/auth/manager.go @@ -0,0 +1,194 @@ +package auth + +import ( + "time" +) + +// NewManager create to authorization management instance +func NewManager(ag AccessGenerate, ts TokenStore) *Manager { + return &Manager{ + cfg: DefaultAccessTokenCfg, + rCfg: DefaultRefreshTokenCfg, + accessGenerate: ag, + tokenStore: ts, + } +} + +// SetConfig mapping the access token generate config +func (m *Manager) SetConfig(cfg *Config) { + m.cfg = cfg +} + +// SetRefreshTokenConfig mapping the token refresh config +func (m *Manager) SetRefreshTokenConfig(store *RefreshConfig) { + m.rCfg = store +} + +// Manager provide authorization management +type Manager struct { + cfg *Config + rCfg *RefreshConfig + accessGenerate AccessGenerate + tokenStore TokenStore +} + +// GenerateAccessToken generate the access token +func (m *Manager) GenerateAccessToken(userID string) (TokenInfo, error) { + ti := NewToken() + ti.SetUserID(userID) + + createAt := time.Now() + ti.SetAccessCreateAt(createAt) + + // set access token expires + ti.SetAccessExpiresIn(m.cfg.AccessTokenExp) + if m.cfg.IsGenerateRefresh { + ti.SetRefreshCreateAt(createAt) + ti.SetRefreshExpiresIn(m.cfg.RefreshTokenExp) + } + + td := &GenerateBasic{ + UserID: userID, + CreateAt: createAt, + TokenInfo: ti, + } + + av, rv, err := m.accessGenerate.Token(td, m.cfg.IsGenerateRefresh) + if err != nil { + return nil, err + } + ti.SetAccess(av) + + if rv != "" { + ti.SetRefresh(rv) + } + + err = m.tokenStore.Create(ti) + if err != nil { + return nil, err + } + + return ti, nil +} + +// RefreshAccessToken refreshing an access token +func (m *Manager) RefreshAccessToken(refresh string) (TokenInfo, error) { + ti, err := m.LoadRefreshToken(refresh) + if err != nil { + return nil, err + } + + oldAccess, oldRefresh := ti.GetAccess(), ti.GetRefresh() + + td := &GenerateBasic{ + UserID: ti.GetUserID(), + CreateAt: time.Now(), + TokenInfo: ti, + } + + ti.SetAccessCreateAt(td.CreateAt) + if v := m.cfg.AccessTokenExp; v > 0 { + ti.SetAccessExpiresIn(v) + } + + if v := m.cfg.RefreshTokenExp; v > 0 { + ti.SetRefreshExpiresIn(v) + } + + if m.rCfg.IsResetRefreshTime { + ti.SetRefreshCreateAt(td.CreateAt) + } + + tv, rv, err := m.accessGenerate.Token(td, m.cfg.IsGenerateRefresh) + if err != nil { + return nil, err + } + + ti.SetAccess(tv) + if rv != "" { + ti.SetRefresh(rv) + } + + if err = m.tokenStore.Create(ti); err != nil { + return nil, err + } + + if m.rCfg.IsRemoveAccess { + // remove the old access token + if err = m.tokenStore.RemoveByAccess(oldAccess); err != nil { + return nil, err + } + } + + if m.rCfg.IsRemoveRefreshing && rv != "" { + // remove the old refresh token + if err = m.tokenStore.RemoveByRefresh(oldRefresh); err != nil { + return nil, err + } + } + + if rv == "" { + ti.SetRefresh("") + ti.SetRefreshCreateAt(time.Now()) + ti.SetRefreshExpiresIn(0) + } + + return ti, nil +} + +// RemoveAccessToken use the access token to delete the token information +func (m *Manager) RemoveAccessToken(access string) error { + if access == "" { + return ErrInvalidAccessToken + } + return m.tokenStore.RemoveByAccess(access) +} + +// RemoveRefreshToken use the refresh token to delete the token information +func (m *Manager) RemoveRefreshToken(refresh string) error { + if refresh == "" { + return ErrInvalidAccessToken + } + return m.tokenStore.RemoveByRefresh(refresh) +} + +// LoadAccessToken according to the access token for corresponding token information +func (m *Manager) LoadAccessToken(access string) (TokenInfo, error) { + if access == "" { + return nil, ErrInvalidAccessToken + } + + ct := time.Now() + ti, err := m.tokenStore.GetByAccess(access) + if err != nil { + return nil, err + } else if ti == nil || ti.GetAccess() != access { + return nil, ErrInvalidAccessToken + } else if ti.GetRefresh() != "" && ti.GetRefreshExpiresIn() != 0 && + ti.GetRefreshCreateAt().Add(ti.GetRefreshExpiresIn()).Before(ct) { + return nil, ErrExpiredRefreshToken + } else if ti.GetAccessExpiresIn() != 0 && + ti.GetAccessCreateAt().Add(ti.GetAccessExpiresIn()).Before(ct) { + return nil, ErrExpiredAccessToken + } + return ti, nil +} + +// LoadRefreshToken according to the refresh token for corresponding token information +func (m *Manager) LoadRefreshToken(refresh string) (TokenInfo, error) { + if refresh == "" { + return nil, ErrInvalidRefreshToken + } + + ti, err := m.tokenStore.GetByRefresh(refresh) + if err != nil { + return nil, err + } else if ti == nil || ti.GetRefresh() != refresh { + return nil, ErrInvalidRefreshToken + } else if ti.GetRefreshExpiresIn() != 0 && // refresh token set to not expire + ti.GetRefreshCreateAt().Add(ti.GetRefreshExpiresIn()).Before(time.Now()) { + return nil, ErrExpiredRefreshToken + } + + return ti, nil +} diff --git a/pkg/auth/store.go b/pkg/auth/store.go new file mode 100644 index 0000000..877a1d5 --- /dev/null +++ b/pkg/auth/store.go @@ -0,0 +1,334 @@ +package auth + +import ( + "context" + "errors" + "fmt" + "github.com/google/uuid" + jsonIterator "github.com/json-iterator/go" + "github.com/redis/go-redis/v9" + "github.com/tidwall/buntdb" + "time" +) + +var ( + jsonMarshal = jsonIterator.Marshal + jsonUnmarshal = jsonIterator.Unmarshal +) + +type TokenStore interface { + Create(info TokenInfo) error + RemoveByAccess(access string) error + RemoveByRefresh(refresh string) error + GetByAccess(access string) (TokenInfo, error) + GetByRefresh(refresh string) (TokenInfo, error) +} + +// NewMemoryTokenStore create a token buntStore instance based on memory +func NewMemoryTokenStore() (TokenStore, error) { + return NewFileTokenStore(":memory:") +} + +// NewFileTokenStore create a token buntStore instance based on file +func NewFileTokenStore(filename string) (TokenStore, error) { + db, err := buntdb.Open(filename) + if err != nil { + return nil, err + } + return &buntStore{db: db}, nil +} + +// buntStore token storage based on buntdb(https://github.com/tidwall/buntdb) +type buntStore struct { + db *buntdb.DB +} + +func (ts *buntStore) remove(key string) error { + err := ts.db.Update(func(tx *buntdb.Tx) error { + _, err := tx.Delete(key) + return err + }) + if errors.Is(err, buntdb.ErrNotFound) { + return nil + } + return err +} + +func (ts *buntStore) getData(key string) (TokenInfo, error) { + var ti TokenInfo + err := ts.db.View(func(tx *buntdb.Tx) error { + jv, err := tx.Get(key) + if err != nil { + return err + } + + var tm Token + err = jsonUnmarshal([]byte(jv), &tm) + if err != nil { + return err + } + ti = &tm + return nil + }) + if err != nil { + if err == buntdb.ErrNotFound { + return nil, nil + } + return nil, err + } + return ti, nil +} + +func (ts *buntStore) getBasicID(key string) (string, error) { + var basicID string + err := ts.db.View(func(tx *buntdb.Tx) error { + v, err := tx.Get(key) + if err != nil { + return err + } + basicID = v + return nil + }) + if err != nil { + if err == buntdb.ErrNotFound { + return "", nil + } + return "", err + } + return basicID, nil +} + +// Create and buntStore the new token information +func (ts *buntStore) Create(info TokenInfo) error { + ct := time.Now() + jv, err := jsonMarshal(info) + if err != nil { + return err + } + + return ts.db.Update(func(tx *buntdb.Tx) error { + basicID := uuid.Must(uuid.NewRandom()).String() + aexp := info.GetAccessExpiresIn() + rexp := aexp + expires := true + if refresh := info.GetRefresh(); refresh != "" { + rexp = info.GetRefreshCreateAt().Add(info.GetRefreshExpiresIn()).Sub(ct) + if aexp.Seconds() > rexp.Seconds() { + aexp = rexp + } + expires = info.GetRefreshExpiresIn() != 0 + _, _, err = tx.Set(refresh, basicID, &buntdb.SetOptions{Expires: expires, TTL: rexp}) + if err != nil { + return err + } + } + + _, _, err = tx.Set(basicID, string(jv), &buntdb.SetOptions{Expires: expires, TTL: rexp}) + if err != nil { + return err + } + + _, _, err = tx.Set(info.GetAccess(), basicID, &buntdb.SetOptions{Expires: expires, TTL: aexp}) + return err + }) +} + +// RemoveByAccess use the access token to delete the token information +func (ts *buntStore) RemoveByAccess(access string) error { + return ts.remove(access) +} + +// RemoveByRefresh use the refresh token to delete the token information +func (ts *buntStore) RemoveByRefresh(refresh string) error { + return ts.remove(refresh) +} + +// GetByAccess use the access token for token information data +func (ts *buntStore) GetByAccess(access string) (TokenInfo, error) { + basicID, err := ts.getBasicID(access) + if err != nil { + return nil, err + } + return ts.getData(basicID) +} + +// GetByRefresh use the refresh token for token information data +func (ts *buntStore) GetByRefresh(refresh string) (TokenInfo, error) { + basicID, err := ts.getBasicID(refresh) + if err != nil { + return nil, err + } + return ts.getData(basicID) +} + +/*------------------------------------------------------------------------------------*/ + +// NewRedisStoreWithCli create an instance of a redis store +func NewRedisStoreWithCli(cli *redis.Client, keyNamespace string) TokenStore { + store := &redisStore{ + cli: cli, + ctx: context.TODO(), + ns: keyNamespace, + } + return store +} + +// TokenStore redis token store +type redisStore struct { + cli *redis.Client + ctx context.Context + ns string +} + +func (s *redisStore) wrapperKey(key string) string { + return fmt.Sprintf("%s%s", s.ns, key) +} + +func (s *redisStore) checkError(result redis.Cmder) (bool, error) { + if err := result.Err(); err != nil { + if err == redis.Nil { + return true, nil + } + return false, err + } + return false, nil +} + +func (s *redisStore) remove(key string) error { + result := s.cli.Del(s.ctx, s.wrapperKey(key)) + _, err := s.checkError(result) + return err +} + +func (s *redisStore) removeToken(tokenString string, isRefresh bool) error { + basicID, err := s.getBasicID(tokenString) + if err != nil { + return err + } else if basicID == "" { + return nil + } + + err = s.remove(tokenString) + if err != nil { + return err + } + + token, err := s.getToken(basicID) + if err != nil { + return err + } else if token == nil { + return nil + } + + checkToken := token.GetRefresh() + if isRefresh { + checkToken = token.GetAccess() + } + result := s.cli.Exists(s.ctx, s.wrapperKey(checkToken)) + if err = result.Err(); err != nil && err != redis.Nil { + return err + } else if result.Val() == 0 { + return s.remove(basicID) + } + + return nil +} + +func (s *redisStore) parseToken(result *redis.StringCmd) (TokenInfo, error) { + if ok, err := s.checkError(result); err != nil { + return nil, err + } else if ok { + return nil, nil + } + + buf, err := result.Bytes() + if err != nil { + if err == redis.Nil { + return nil, nil + } + return nil, err + } + + var token Token + if err = jsonUnmarshal(buf, &token); err != nil { + return nil, err + } + return &token, nil +} + +func (s *redisStore) getToken(key string) (TokenInfo, error) { + result := s.cli.Get(s.ctx, s.wrapperKey(key)) + return s.parseToken(result) +} + +func (s *redisStore) parseBasicID(result *redis.StringCmd) (string, error) { + if ok, err := s.checkError(result); err != nil { + return "", err + } else if ok { + return "", nil + } + return result.Val(), nil +} + +func (s *redisStore) getBasicID(token string) (string, error) { + result := s.cli.Get(s.ctx, s.wrapperKey(token)) + return s.parseBasicID(result) +} + +// Create and store the new token information +func (s *redisStore) Create(info TokenInfo) error { + ct := time.Now() + jv, err := jsonMarshal(info) + if err != nil { + return err + } + + pipe := s.cli.TxPipeline() + basicID := uuid.Must(uuid.NewRandom()).String() + aexp := info.GetAccessExpiresIn() + rexp := aexp + + if refresh := info.GetRefresh(); refresh != "" { + rexp = info.GetRefreshCreateAt().Add(info.GetRefreshExpiresIn()).Sub(ct) + if aexp.Seconds() > rexp.Seconds() { + aexp = rexp + } + pipe.Set(s.ctx, s.wrapperKey(refresh), basicID, rexp) + } + + pipe.Set(s.ctx, s.wrapperKey(info.GetAccess()), basicID, aexp) + pipe.Set(s.ctx, s.wrapperKey(basicID), jv, rexp) + + if _, err = pipe.Exec(s.ctx); err != nil { + return err + } + return nil +} + +// RemoveByAccess Use the access token to delete the token information +func (s *redisStore) RemoveByAccess(access string) error { + return s.removeToken(access, false) +} + +// RemoveByRefresh Use the refresh token to delete the token information +func (s *redisStore) RemoveByRefresh(refresh string) error { + return s.removeToken(refresh, true) +} + +// GetByAccess Use the access token for token information data +func (s *redisStore) GetByAccess(access string) (TokenInfo, error) { + basicID, err := s.getBasicID(access) + if err != nil || basicID == "" { + return nil, err + } + return s.getToken(basicID) +} + +// GetByRefresh Use the refresh token for token information data +func (s *redisStore) GetByRefresh(refresh string) (TokenInfo, error) { + basicID, err := s.getBasicID(refresh) + if err != nil || basicID == "" { + return nil, err + } + return s.getToken(basicID) +} diff --git a/pkg/auth/token.go b/pkg/auth/token.go new file mode 100644 index 0000000..7354d4a --- /dev/null +++ b/pkg/auth/token.go @@ -0,0 +1,118 @@ +package auth + +import ( + "time" +) + +// TokenInfo the token information model interface +type TokenInfo interface { + New() TokenInfo + + GetUserID() string + SetUserID(string) + + GetAccess() string + SetAccess(string) + GetAccessCreateAt() time.Time + SetAccessCreateAt(time.Time) + GetAccessExpiresIn() time.Duration + SetAccessExpiresIn(time.Duration) + + GetRefresh() string + SetRefresh(string) + GetRefreshCreateAt() time.Time + SetRefreshCreateAt(time.Time) + GetRefreshExpiresIn() time.Duration + SetRefreshExpiresIn(time.Duration) +} + +// NewToken create to token model instance +func NewToken() *Token { + return &Token{} +} + +// Token token model +type Token struct { + UserID string `bson:"UserID"` + Access string `bson:"Access"` + AccessCreateAt time.Time `bson:"AccessCreateAt"` + AccessExpiresIn time.Duration `bson:"AccessExpiresIn"` + Refresh string `bson:"Refresh"` + RefreshCreateAt time.Time `bson:"RefreshCreateAt"` + RefreshExpiresIn time.Duration `bson:"RefreshExpiresIn"` +} + +// New create to token model instance +func (t *Token) New() TokenInfo { + return NewToken() +} + +// GetUserID the user id +func (t *Token) GetUserID() string { + return t.UserID +} + +// SetUserID the user id +func (t *Token) SetUserID(userID string) { + t.UserID = userID +} + +// GetAccess access Token +func (t *Token) GetAccess() string { + return t.Access +} + +// SetAccess access Token +func (t *Token) SetAccess(access string) { + t.Access = access +} + +// GetAccessCreateAt create Time +func (t *Token) GetAccessCreateAt() time.Time { + return t.AccessCreateAt +} + +// SetAccessCreateAt create Time +func (t *Token) SetAccessCreateAt(createAt time.Time) { + t.AccessCreateAt = createAt +} + +// GetAccessExpiresIn the lifetime in seconds of the access token +func (t *Token) GetAccessExpiresIn() time.Duration { + return t.AccessExpiresIn +} + +// SetAccessExpiresIn the lifetime in seconds of the access token +func (t *Token) SetAccessExpiresIn(exp time.Duration) { + t.AccessExpiresIn = exp +} + +// GetRefresh refresh Token +func (t *Token) GetRefresh() string { + return t.Refresh +} + +// SetRefresh refresh Token +func (t *Token) SetRefresh(refresh string) { + t.Refresh = refresh +} + +// GetRefreshCreateAt create Time +func (t *Token) GetRefreshCreateAt() time.Time { + return t.RefreshCreateAt +} + +// SetRefreshCreateAt create Time +func (t *Token) SetRefreshCreateAt(createAt time.Time) { + t.RefreshCreateAt = createAt +} + +// GetRefreshExpiresIn the lifetime in seconds of the refresh token +func (t *Token) GetRefreshExpiresIn() time.Duration { + return t.RefreshExpiresIn +} + +// SetRefreshExpiresIn the lifetime in seconds of the refresh token +func (t *Token) SetRefreshExpiresIn(exp time.Duration) { + t.RefreshExpiresIn = exp +} diff --git a/pkg/bcrypt/password.go b/pkg/bcrypt/password.go new file mode 100644 index 0000000..3b035ce --- /dev/null +++ b/pkg/bcrypt/password.go @@ -0,0 +1,39 @@ +package bcrypt + +import ( + "fmt" + "golang.org/x/crypto/bcrypt" +) + +var _ Password = (*password)(nil) + +type Password interface { + i() + Generate(pwd string) string + Validate(pwd, hash string) bool +} + +type password struct { + cost int +} + +func (p *password) i() {} + +func NewPassword(cost int) (Password, error) { + if cost < bcrypt.MinCost || cost > bcrypt.MaxCost { + return nil, fmt.Errorf("cost out of range") + } + return &password{ + cost: cost, + }, nil +} + +func (p *password) Generate(pwd string) string { + hash, _ := bcrypt.GenerateFromPassword([]byte(pwd), p.cost) + return string(hash) +} + +func (p *password) Validate(pwd, hash string) bool { + err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(pwd)) + return err == nil +} diff --git a/pkg/browser/browser.go b/pkg/browser/browser.go new file mode 100644 index 0000000..7089f7e --- /dev/null +++ b/pkg/browser/browser.go @@ -0,0 +1,23 @@ +package browser + +import ( + "fmt" + "os/exec" + "runtime" +) + +var commands = map[string]string{ + "windows": "start", + "darwin": "open", + "linux": "xdg-open", +} + +func Open(uri string) error { + run, ok := commands[runtime.GOOS] + if !ok { + return fmt.Errorf("don't know how to open things on %s platform", runtime.GOOS) + } + + cmd := exec.Command(run, uri) + return cmd.Start() +} diff --git a/pkg/cache/redis.go b/pkg/cache/redis.go new file mode 100644 index 0000000..a45e7b4 --- /dev/null +++ b/pkg/cache/redis.go @@ -0,0 +1,482 @@ +package cache + +import ( + "context" + "errors" + "fmt" + "time" + + "git.bvbej.com/bvbej/base-golang/pkg/time_parse" + "git.bvbej.com/bvbej/base-golang/pkg/trace" + "github.com/redis/go-redis/v9" +) + +type Option func(*option) + +type Trace = trace.T + +type option struct { + Trace *trace.Trace + Redis *trace.Redis +} + +type RedisConfig struct { + Addr string `yaml:"addr"` + Pass string `yaml:"pass"` + DB int `yaml:"db"` + MaxRetries int `yaml:"maxRetries"` // 最大重试次数 + PoolSize int `yaml:"poolSize"` // Redis连接池大小 + MinIdleConn int `yaml:"minIdleConn"` // 最小空闲连接数 +} + +func newOption() *option { + return &option{} +} + +var _ Repo = (*cacheRepo)(nil) + +type Repo interface { + i() + Client() *redis.Client + Set(key, value string, ttl time.Duration, options ...Option) error + Get(key string, options ...Option) (string, error) + TTL(key string) (time.Duration, error) + Expire(key string, ttl time.Duration) bool + ExpireAt(key string, ttl time.Time) bool + Del(key string, options ...Option) bool + Exists(keys ...string) bool + Incr(key string, options ...Option) (int64, error) + Decr(key string, options ...Option) (int64, error) + HGet(key, field string, options ...Option) (string, error) + HSet(key, field, value string, options ...Option) error + HDel(key, field string, options ...Option) error + HGetAll(key string, options ...Option) (map[string]string, error) + HIncrBy(key, field string, incr int64, options ...Option) (int64, error) + HIncrByFloat(key, field string, incr float64, options ...Option) (float64, error) + LPush(key, value string, options ...Option) error + LLen(key string, options ...Option) (int64, error) + BRPop(key string, timeout time.Duration, options ...Option) (string, error) + Close() error +} + +type cacheRepo struct { + client *redis.Client + ctx context.Context +} + +func New(cfg RedisConfig) (Repo, error) { + client := redis.NewClient(&redis.Options{ + Addr: cfg.Addr, + Password: cfg.Pass, + DB: cfg.DB, + MaxRetries: cfg.MaxRetries, + PoolSize: cfg.PoolSize, + MinIdleConns: cfg.MinIdleConn, + }) + ctx := context.TODO() + if err := client.Ping(ctx).Err(); err != nil { + return nil, errors.Join(err, errors.New("ping redis err")) + } + return &cacheRepo{ + client: client, + ctx: ctx, + }, nil +} + +func WithTrace(t Trace) Option { + return func(opt *option) { + if t != nil { + opt.Trace = t.(*trace.Trace) + opt.Redis = new(trace.Redis) + } + } +} + +func (c *cacheRepo) i() {} + +func (c *cacheRepo) Client() *redis.Client { + return c.client +} + +func (c *cacheRepo) Set(key, value string, ttl time.Duration, options ...Option) error { + ts := time.Now() + opt := newOption() + defer func() { + if opt.Trace != nil { + opt.Redis.Timestamp = time_parse.CSTLayoutString() + opt.Redis.Handle = "set" + opt.Redis.Key = key + opt.Redis.Value = value + opt.Redis.TTL = ttl.Minutes() + opt.Redis.CostSeconds = time.Since(ts).Seconds() + opt.Trace.AppendRedis(opt.Redis) + } + }() + + for _, f := range options { + f(opt) + } + + if err := c.client.Set(c.ctx, key, value, ttl).Err(); err != nil { + return errors.Join(err, fmt.Errorf("redis set key: %s err", key)) + } + + return nil +} + +func (c *cacheRepo) Get(key string, options ...Option) (string, error) { + ts := time.Now() + opt := newOption() + defer func() { + if opt.Trace != nil { + opt.Redis.Timestamp = time_parse.CSTLayoutString() + opt.Redis.Handle = "get" + opt.Redis.Key = key + opt.Redis.CostSeconds = time.Since(ts).Seconds() + opt.Trace.AppendRedis(opt.Redis) + } + }() + + for _, f := range options { + f(opt) + } + + value, err := c.client.Get(c.ctx, key).Result() + if err != nil { + return "", errors.Join(err, fmt.Errorf("redis get key: %s err", key)) + } + + return value, nil +} + +func (c *cacheRepo) TTL(key string) (time.Duration, error) { + ttl, err := c.client.TTL(c.ctx, key).Result() + if err != nil { + return -1, errors.Join(err, fmt.Errorf("redis get key: %s err", key)) + } + + return ttl, nil +} + +func (c *cacheRepo) Expire(key string, ttl time.Duration) bool { + ok, _ := c.client.Expire(c.ctx, key, ttl).Result() + return ok +} + +func (c *cacheRepo) ExpireAt(key string, ttl time.Time) bool { + ok, _ := c.client.ExpireAt(c.ctx, key, ttl).Result() + return ok +} + +func (c *cacheRepo) Exists(keys ...string) bool { + if len(keys) == 0 { + return true + } + value, _ := c.client.Exists(c.ctx, keys...).Result() + return value > 0 +} + +func (c *cacheRepo) Del(key string, options ...Option) bool { + ts := time.Now() + opt := newOption() + defer func() { + if opt.Trace != nil { + opt.Redis.Timestamp = time_parse.CSTLayoutString() + opt.Redis.Handle = "del" + opt.Redis.Key = key + opt.Redis.CostSeconds = time.Since(ts).Seconds() + opt.Trace.AppendRedis(opt.Redis) + } + }() + + for _, f := range options { + f(opt) + } + + if key == "" { + return true + } + + value, _ := c.client.Del(c.ctx, key).Result() + return value > 0 +} + +func (c *cacheRepo) Incr(key string, options ...Option) (int64, error) { + ts := time.Now() + opt := newOption() + defer func() { + if opt.Trace != nil { + opt.Redis.Timestamp = time_parse.CSTLayoutString() + opt.Redis.Handle = "incr" + opt.Redis.Key = key + opt.Redis.CostSeconds = time.Since(ts).Seconds() + opt.Trace.AppendRedis(opt.Redis) + } + }() + + for _, f := range options { + f(opt) + } + value, err := c.client.Incr(c.ctx, key).Result() + if err != nil { + return 0, errors.Join(err, fmt.Errorf("redis incr key: %s err", key)) + } + return value, nil +} + +func (c *cacheRepo) Decr(key string, options ...Option) (int64, error) { + ts := time.Now() + opt := newOption() + defer func() { + if opt.Trace != nil { + opt.Redis.Timestamp = time_parse.CSTLayoutString() + opt.Redis.Handle = "decr" + opt.Redis.Key = key + opt.Redis.CostSeconds = time.Since(ts).Seconds() + opt.Trace.AppendRedis(opt.Redis) + } + }() + + for _, f := range options { + f(opt) + } + value, err := c.client.Decr(c.ctx, key).Result() + if err != nil { + return 0, errors.Join(err, fmt.Errorf("redis decr key: %s err", key)) + } + return value, nil +} + +func (c *cacheRepo) HGet(key, field string, options ...Option) (string, error) { + ts := time.Now() + opt := newOption() + defer func() { + if opt.Trace != nil { + opt.Redis.Timestamp = time_parse.CSTLayoutString() + opt.Redis.Handle = "hash get" + opt.Redis.Key = key + opt.Redis.Value = field + opt.Redis.CostSeconds = time.Since(ts).Seconds() + opt.Trace.AppendRedis(opt.Redis) + } + }() + + for _, f := range options { + f(opt) + } + + value, err := c.client.HGet(c.ctx, key, field).Result() + if err != nil { + return "", errors.Join(err, fmt.Errorf("redis hget key: %s field: %s err", key, field)) + } + + return value, nil +} + +func (c *cacheRepo) HSet(key, field, value string, options ...Option) error { + ts := time.Now() + opt := newOption() + defer func() { + if opt.Trace != nil { + opt.Redis.Timestamp = time_parse.CSTLayoutString() + opt.Redis.Handle = "hash set" + opt.Redis.Key = key + opt.Redis.Value = field + "/" + value + opt.Redis.CostSeconds = time.Since(ts).Seconds() + opt.Trace.AppendRedis(opt.Redis) + } + }() + + for _, f := range options { + f(opt) + } + + if err := c.client.HSet(c.ctx, key, field, value).Err(); err != nil { + return errors.Join(err, fmt.Errorf("redis hset key: %s field: %s err", key, field)) + } + + return nil +} + +func (c *cacheRepo) HDel(key, field string, options ...Option) error { + ts := time.Now() + opt := newOption() + defer func() { + if opt.Trace != nil { + opt.Redis.Timestamp = time_parse.CSTLayoutString() + opt.Redis.Handle = "hash del" + opt.Redis.Key = key + opt.Redis.Value = field + opt.Redis.CostSeconds = time.Since(ts).Seconds() + opt.Trace.AppendRedis(opt.Redis) + } + }() + + for _, f := range options { + f(opt) + } + + if err := c.client.HDel(c.ctx, key, field).Err(); err != nil { + return errors.Join(err, fmt.Errorf("redis hdel key: %s field: %s err", key, field)) + } + + return nil +} + +func (c *cacheRepo) HGetAll(key string, options ...Option) (map[string]string, error) { + ts := time.Now() + opt := newOption() + defer func() { + if opt.Trace != nil { + opt.Redis.Timestamp = time_parse.CSTLayoutString() + opt.Redis.Handle = "hash get all" + opt.Redis.Key = key + opt.Redis.CostSeconds = time.Since(ts).Seconds() + opt.Trace.AppendRedis(opt.Redis) + } + }() + + for _, f := range options { + f(opt) + } + + value, err := c.client.HGetAll(c.ctx, key).Result() + if err != nil { + return nil, errors.Join(err, fmt.Errorf("redis hget all key: %s err", key)) + } + + return value, nil +} + +func (c *cacheRepo) HIncrBy(key, field string, incr int64, options ...Option) (int64, error) { + ts := time.Now() + opt := newOption() + defer func() { + if opt.Trace != nil { + opt.Redis.Timestamp = time_parse.CSTLayoutString() + opt.Redis.Handle = "hash incr int64" + opt.Redis.Key = key + opt.Redis.Value = fmt.Sprintf("field:%s incr:%d", field, incr) + opt.Redis.CostSeconds = time.Since(ts).Seconds() + opt.Trace.AppendRedis(opt.Redis) + } + }() + + for _, f := range options { + f(opt) + } + + value, err := c.client.HIncrBy(c.ctx, key, field, incr).Result() + if err != nil { + return 0, errors.Join(err, fmt.Errorf("redis hash incr int64 key: %s err", key)) + } + + return value, nil +} + +func (c *cacheRepo) HIncrByFloat(key, field string, incr float64, options ...Option) (float64, error) { + ts := time.Now() + opt := newOption() + defer func() { + if opt.Trace != nil { + opt.Redis.Timestamp = time_parse.CSTLayoutString() + opt.Redis.Handle = "hash incr float64" + opt.Redis.Key = key + opt.Redis.Value = fmt.Sprintf("field:%s incr:%d", field, incr) + opt.Redis.CostSeconds = time.Since(ts).Seconds() + opt.Trace.AppendRedis(opt.Redis) + } + }() + + for _, f := range options { + f(opt) + } + + value, err := c.client.HIncrByFloat(c.ctx, key, field, incr).Result() + if err != nil { + return 0, errors.Join(err, fmt.Errorf("redis hash incr float64 key: %s err", key)) + } + + return value, nil +} + +func (c *cacheRepo) LPush(key, value string, options ...Option) error { + ts := time.Now() + opt := newOption() + defer func() { + if opt.Trace != nil { + opt.Redis.Timestamp = time_parse.CSTLayoutString() + opt.Redis.Handle = "list push" + opt.Redis.Key = key + opt.Redis.Value = value + opt.Redis.CostSeconds = time.Since(ts).Seconds() + opt.Trace.AppendRedis(opt.Redis) + } + }() + + for _, f := range options { + f(opt) + } + + _, err := c.client.LPush(c.ctx, key, value).Result() + if err != nil { + return errors.Join(err, fmt.Errorf("redis list push key: %s value: %s err", key, value)) + } + + return nil +} + +func (c *cacheRepo) LLen(key string, options ...Option) (int64, error) { + ts := time.Now() + opt := newOption() + defer func() { + if opt.Trace != nil { + opt.Redis.Timestamp = time_parse.CSTLayoutString() + opt.Redis.Handle = "list len" + opt.Redis.Key = key + opt.Redis.CostSeconds = time.Since(ts).Seconds() + opt.Trace.AppendRedis(opt.Redis) + } + }() + + for _, f := range options { + f(opt) + } + + value, err := c.client.LLen(c.ctx, key).Result() + if err != nil { + return 0, errors.Join(err, fmt.Errorf("redis list len key: %s err", key)) + } + + return value, nil +} + +func (c *cacheRepo) BRPop(key string, timeout time.Duration, options ...Option) (string, error) { + ts := time.Now() + opt := newOption() + defer func() { + if opt.Trace != nil { + opt.Redis.Timestamp = time_parse.CSTLayoutString() + opt.Redis.Handle = "list brpop" + opt.Redis.Key = key + opt.Redis.TTL = timeout.Seconds() + opt.Redis.CostSeconds = time.Since(ts).Seconds() + opt.Trace.AppendRedis(opt.Redis) + } + }() + + for _, f := range options { + f(opt) + } + + value, err := c.client.BRPop(c.ctx, timeout, key).Result() + if err != nil { + return "", errors.Join(err, fmt.Errorf("redis list len key: %s err", key)) + } + + return value[1], nil +} + +func (c *cacheRepo) Close() error { + return c.client.Close() +} diff --git a/pkg/captcha/base64.go b/pkg/captcha/base64.go new file mode 100644 index 0000000..d951c37 --- /dev/null +++ b/pkg/captcha/base64.go @@ -0,0 +1,97 @@ +package captcha + +import ( + "fmt" + "git.bvbej.com/bvbej/base-golang/pkg/cache" + "github.com/mojocn/base64Captcha" + "go.uber.org/zap" + "strings" + "time" +) + +var _ base64Captcha.Store = (*store)(nil) + +type store struct { + cache cache.Repo + ttl time.Duration + logger *zap.Logger + ns string + prefix string +} + +func (s *store) Set(id string, value string) error { + err := s.cache.Set(fmt.Sprintf("%s%s%s", s.ns, s.prefix, id), value, s.ttl) + if err != nil { + return err + } + return nil +} + +func (s *store) Get(id string, clear bool) string { + value, err := s.cache.Get(fmt.Sprintf("%s%s%s", s.ns, s.prefix, id)) + if err == nil && clear { + s.cache.Del(fmt.Sprintf("%s%s%s", s.ns, s.prefix, id)) + } + return value +} + +func (s *store) Verify(id, answer string, clear bool) bool { + value := s.Get(id, clear) + if value == "" || answer == "" { + return false + } + return strings.ToLower(value) == strings.ToLower(answer) +} + +func NewStore(cache cache.Repo, ttl time.Duration, namespace string) base64Captcha.Store { + return &store{ + cache: cache, + ttl: ttl, + ns: namespace, + prefix: "captcha:base64:", + } +} + +var _ Captcha = (*captcha)(nil) + +type Captcha interface { + Generate() (id, b64s, answer string, err error) + Verify(id, value string) bool +} + +type captcha struct { + driver base64Captcha.Driver + store base64Captcha.Store +} + +func NewStringCaptcha(store base64Captcha.Store, height, width, length int) Captcha { + conf := &base64Captcha.DriverString{ + Height: height, + Width: width, + NoiseCount: length, + ShowLineOptions: base64Captcha.OptionShowHollowLine, + Length: length, + Source: "ABCDEFGHIJKMNPQRSTUVWXYZabcdefghijkmnpqrstuvwxyz0123456789", + } + return &captcha{ + driver: conf.ConvertFonts(), + store: store, + } +} + +func NewDigitCaptcha(store base64Captcha.Store, height, width, length int) Captcha { + conf := base64Captcha.NewDriverDigit(height, width, length, 0.7, height) + return &captcha{ + driver: conf, + store: store, + } +} + +func (c *captcha) Generate() (id, b64s, answer string, err error) { + newCaptcha := base64Captcha.NewCaptcha(c.driver, c.store) + return newCaptcha.Generate() +} + +func (c *captcha) Verify(id, value string) bool { + return c.store.Verify(id, value, true) +} diff --git a/pkg/cidr/calc.go b/pkg/cidr/calc.go new file mode 100644 index 0000000..568a5f5 --- /dev/null +++ b/pkg/cidr/calc.go @@ -0,0 +1,119 @@ +package cidr + +import ( + "bytes" + "fmt" + "math" + "net" + "sort" +) + +// SuperNetting 合并网段 +func SuperNetting(ns []string) (*cidr, error) { + num := len(ns) + if num < 1 || (num&(num-1)) != 0 { + return nil, fmt.Errorf("子网数量必须是2的次方") + } + + mask := "" + var cidrs []*cidr + for _, n := range ns { + // 检查子网CIDR有效性 + c, err := ParseCIDR(n) + if err != nil { + return nil, fmt.Errorf("网段%v格式错误", n) + } + cidrs = append(cidrs, c) + + // TODO 暂只考虑相同子网掩码的网段合并 + if len(mask) == 0 { + mask = c.Mask() + } else if c.Mask() != mask { + return nil, fmt.Errorf("子网掩码不一致") + } + } + AscSortCIDRs(cidrs) + + // 检查网段是否连续 + var network net.IP + for _, c := range cidrs { + if len(network) > 0 { + if !network.Equal(c.ipNet.IP) { + return nil, fmt.Errorf("必须是连续的网段") + } + } + network = net.ParseIP(c.Broadcast()) + IncrIP(network) + } + + // 子网掩码左移,得到共同的父网段 + c := cidrs[0] + ones, bits := c.MaskSize() + ones = ones - int(math.Log2(float64(num))) + c.ipNet.Mask = net.CIDRMask(ones, bits) + c.ipNet.IP.Mask(c.ipNet.Mask) + + return c, nil +} + +// IncrIP IP地址自增 +func IncrIP(ip net.IP) { + for i := len(ip) - 1; i >= 0; i-- { + ip[i]++ + if ip[i] > 0 { + break + } + } +} + +// DecrIP IP地址自减 +func DecrIP(ip net.IP) { + length := len(ip) + for i := length - 1; i >= 0; i-- { + ip[length-1]-- + if ip[length-1] < 0xFF { + break + } + for j := 1; j < length; j++ { + ip[length-j-1]-- + if ip[length-j-1] < 0xFF { + return + } + } + } +} + +// Compare 比较IP大小 a等于b,返回0; a大于b,返回+1; a小于b,返回-1 +func Compare(a, b net.IP) int { + return bytes.Compare(a, b) +} + +// AscSortCIDRs 升序 +func AscSortCIDRs(cs []*cidr) { + sort.Slice(cs, func(i, j int) bool { + if n := bytes.Compare(cs[i].ipNet.IP, cs[j].ipNet.IP); n != 0 { + return n < 0 + } + + if n := bytes.Compare(cs[i].ipNet.Mask, cs[j].ipNet.Mask); n != 0 { + return n < 0 + } + + return false + }) +} + +// DescSortCIDRs 降序 +func DescSortCIDRs(cs []*cidr) { + sort.Slice(cs, func(i, j int) bool { + if n := bytes.Compare(cs[i].ipNet.IP, cs[j].ipNet.IP); n != 0 { + return n >= 0 + } + + if n := bytes.Compare(cs[i].ipNet.Mask, cs[j].ipNet.Mask); n != 0 { + return n >= 0 + } + + return false + }) +} diff --git a/pkg/cidr/ip.go b/pkg/cidr/ip.go new file mode 100644 index 0000000..9818a87 --- /dev/null +++ b/pkg/cidr/ip.go @@ -0,0 +1,194 @@ +package cidr + +import ( + "encoding/hex" + "fmt" + "math" + "math/big" + "net" +) + +// 裂解子网的方式 +const ( + MethodSubnetNum = 0 // 基于子网数量 + MethodHostNum = 1 // 基于主机数量 +) + +var _ CIDR = (*cidr)(nil) + +type CIDR interface { + CIDR() string + IP() string + Network() string + Broadcast() string + Mask() string + MaskSize() (int, int) + IPRange() (string, string) + IPCount() *big.Int + + IsIPv4() bool + IsIPv6() bool + + Equal(string) bool + Contains(string) bool + ForEachIP(func(string) error) error + ForEachIPBeginWith(string, func(string) error) error + SubNetting(method, num int) ([]*cidr, error) +} + +type cidr struct { + ip net.IP + ipNet *net.IPNet +} + +// ParseCIDR 解析CIDR网段 +func ParseCIDR(s string) (*cidr, error) { + i, n, err := net.ParseCIDR(s) + if err != nil { + return nil, err + } + return &cidr{ip: i, ipNet: n}, nil +} + +// Equal 判断网段是否相等 +func (c *cidr) Equal(ns string) bool { + c2, err := ParseCIDR(ns) + if err != nil { + return false + } + return c.ipNet.IP.Equal(c2.ipNet.IP) /* && c.ipNet.IP.Equal(c2.ip) */ +} + +// IsIPv4 判断是否IPv4 +func (c *cidr) IsIPv4() bool { + _, bits := c.ipNet.Mask.Size() + return bits/8 == net.IPv4len +} + +// IsIPv6 判断是否IPv6 +func (c *cidr) IsIPv6() bool { + _, bits := c.ipNet.Mask.Size() + return bits/8 == net.IPv6len +} + +// Contains 判断IP是否包含在网段中 +func (c *cidr) Contains(ip string) bool { + return c.ipNet.Contains(net.ParseIP(ip)) +} + +// CIDR 根据子网掩码长度校准后的CIDR +func (c *cidr) CIDR() string { + return c.ipNet.String() +} + +// IP CIDR字符串中的IP部分 +func (c *cidr) IP() string { + return c.ip.String() +} + +// Network 网络号 +func (c *cidr) Network() string { + return c.ipNet.IP.String() +} + +// MaskSize 子网掩码位数 +func (c *cidr) MaskSize() (ones, bits int) { + ones, bits = c.ipNet.Mask.Size() + return +} + +// Mask 子网掩码 +func (c *cidr) Mask() string { + mask, _ := hex.DecodeString(c.ipNet.Mask.String()) + return net.IP([]byte(mask)).String() +} + +// Broadcast 广播地址(网段最后一个IP) +func (c *cidr) Broadcast() string { + mask := c.ipNet.Mask + bcst := make(net.IP, len(c.ipNet.IP)) + copy(bcst, c.ipNet.IP) + for i := 0; i < len(mask); i++ { + ipIdx := len(bcst) - i - 1 + bcst[ipIdx] = c.ipNet.IP[ipIdx] | ^mask[len(mask)-i-1] + } + return bcst.String() +} + +// IPRange 起始IP、结束IP +func (c *cidr) IPRange() (start, end string) { + return c.Network(), c.Broadcast() +} + +// IPCount IP数量 +func (c *cidr) IPCount() *big.Int { + ones, bits := c.ipNet.Mask.Size() + return big.NewInt(0).Lsh(big.NewInt(1), uint(bits-ones)) +} + +// ForEachIP 遍历网段下所有IP +func (c *cidr) ForEachIP(iterator func(ip string) error) error { + next := make(net.IP, len(c.ipNet.IP)) + copy(next, c.ipNet.IP) + for c.ipNet.Contains(next) { + if err := iterator(next.String()); err != nil { + return err + } + IncrIP(next) + } + return nil +} + +// ForEachIPBeginWith 从指定IP开始遍历网段下后续的IP +func (c *cidr) ForEachIPBeginWith(beginIP string, iterator func(ip string) error) error { + next := net.ParseIP(beginIP) + for c.ipNet.Contains(next) { + if err := iterator(next.String()); err != nil { + return err + } + IncrIP(next) + } + return nil +} + +// SubNetting 裂解网段 +func (c *cidr) SubNetting(method, num int) ([]*cidr, error) { + if num < 1 || (num&(num-1)) != 0 { + return nil, fmt.Errorf("裂解数量必须是2的次方") + } + + newOnes := int(math.Log2(float64(num))) + ones, bits := c.MaskSize() + switch method { + default: + return nil, fmt.Errorf("不支持的裂解方式") + case MethodSubnetNum: + newOnes = ones + newOnes + // 如果子网的掩码长度大于父网段的长度,则无法裂解 + if newOnes > bits { + return nil, nil + } + case MethodHostNum: + newOnes = bits - newOnes + // 如果子网的掩码长度小于等于父网段的掩码长度,则无法裂解 + if newOnes <= ones { + return nil, nil + } + // 主机数量转换为子网数量 + num = int(math.Pow(float64(2), float64(newOnes-ones))) + } + + var cidrs []*cidr + network := make(net.IP, len(c.ipNet.IP)) + copy(network, c.ipNet.IP) + for i := 0; i < num; i++ { + cidr, _ := ParseCIDR(fmt.Sprintf("%v/%v", network.String(), newOnes)) + cidrs = append(cidrs, cidr) + + // 广播地址的下一个IP即为下一段的网络号 + network = net.ParseIP(cidr.Broadcast()) + IncrIP(network) + } + + return cidrs, nil +} diff --git a/pkg/cmap/cmap.go b/pkg/cmap/cmap.go new file mode 100644 index 0000000..efc6687 --- /dev/null +++ b/pkg/cmap/cmap.go @@ -0,0 +1,360 @@ +package cmap + +import ( + "encoding/json" + "fmt" + "sync" +) + +var ShardCount = 32 + +type Stringer interface { + fmt.Stringer + comparable +} + +// ConcurrentMap A "thread" safe map of type string:Anything. +// To avoid lock bottlenecks this map is dived to several (ShardCount) map shards. +type ConcurrentMap[K comparable, V any] struct { + shards []*ConcurrentMapShared[K, V] + sharding func(key K) uint32 +} + +// ConcurrentMapShared A "thread" safe string to anything map. +type ConcurrentMapShared[K comparable, V any] struct { + items map[K]V + sync.RWMutex // Read Write mutex, guards access to internal map. +} + +func create[K comparable, V any](sharding func(key K) uint32) ConcurrentMap[K, V] { + m := ConcurrentMap[K, V]{ + sharding: sharding, + shards: make([]*ConcurrentMapShared[K, V], ShardCount), + } + for i := 0; i < ShardCount; i++ { + m.shards[i] = &ConcurrentMapShared[K, V]{items: make(map[K]V)} + } + return m +} + +// New Creates a new concurrent map. +func New[V any]() ConcurrentMap[string, V] { + return create[string, V](fnv32) +} + +// NewStringer Creates a new concurrent map. +func NewStringer[K Stringer, V any]() ConcurrentMap[K, V] { + return create[K, V](strfnv32[K]) +} + +// NewWithCustomShardingFunction Creates a new concurrent map. +func NewWithCustomShardingFunction[K comparable, V any](sharding func(key K) uint32) ConcurrentMap[K, V] { + return create[K, V](sharding) +} + +// GetShard returns shard under given key +func (m ConcurrentMap[K, V]) GetShard(key K) *ConcurrentMapShared[K, V] { + return m.shards[uint(m.sharding(key))%uint(ShardCount)] +} + +func (m ConcurrentMap[K, V]) MSet(data map[K]V) { + for key, value := range data { + shard := m.GetShard(key) + shard.Lock() + shard.items[key] = value + shard.Unlock() + } +} + +// Set Sets the given value under the specified key. +func (m ConcurrentMap[K, V]) Set(key K, value V) { + // Get map shard. + shard := m.GetShard(key) + shard.Lock() + shard.items[key] = value + shard.Unlock() +} + +// UpsertCb Callback to return new element to be inserted into the map +// It is called while lock is held, therefore it MUST NOT +// try to access other keys in same map, as it can lead to deadlock since +// Go sync.RWLock is not reentrant +type UpsertCb[V any] func(exist bool, valueInMap V, newValue V) V + +// Upsert Insert or Update - updates existing element or inserts a new one using UpsertCb +func (m ConcurrentMap[K, V]) Upsert(key K, value V, cb UpsertCb[V]) (res V) { + shard := m.GetShard(key) + shard.Lock() + v, ok := shard.items[key] + res = cb(ok, v, value) + shard.items[key] = res + shard.Unlock() + return res +} + +// SetIfAbsent Sets the given value under the specified key if no value was associated with it. +func (m ConcurrentMap[K, V]) SetIfAbsent(key K, value V) bool { + // Get map shard. + shard := m.GetShard(key) + shard.Lock() + _, ok := shard.items[key] + if !ok { + shard.items[key] = value + } + shard.Unlock() + return !ok +} + +// Get retrieves an element from map under given key. +func (m ConcurrentMap[K, V]) Get(key K) (V, bool) { + // Get shard + shard := m.GetShard(key) + shard.RLock() + // Get item from shard. + val, ok := shard.items[key] + shard.RUnlock() + return val, ok +} + +// Count returns the number of elements within the map. +func (m ConcurrentMap[K, V]) Count() int { + count := 0 + for i := 0; i < ShardCount; i++ { + shard := m.shards[i] + shard.RLock() + count += len(shard.items) + shard.RUnlock() + } + return count +} + +// Has Looks up an item under specified key +func (m ConcurrentMap[K, V]) Has(key K) bool { + // Get shard + shard := m.GetShard(key) + shard.RLock() + // See if element is within shard. + _, ok := shard.items[key] + shard.RUnlock() + return ok +} + +// Remove removes an element from the map. +func (m ConcurrentMap[K, V]) Remove(key K) { + // Try to get shard. + shard := m.GetShard(key) + shard.Lock() + delete(shard.items, key) + shard.Unlock() +} + +// RemoveCb is a callback executed in a map.RemoveCb() call, while Lock is held +// If returns true, the element will be removed from the map +type RemoveCb[K any, V any] func(key K, v V, exists bool) bool + +// RemoveCb locks the shard containing the key, retrieves its current value and calls the callback with those params +// If callback returns true and element exists, it will remove it from the map +// Returns the value returned by the callback (even if element was not present in the map) +func (m ConcurrentMap[K, V]) RemoveCb(key K, cb RemoveCb[K, V]) bool { + // Try to get shard. + shard := m.GetShard(key) + shard.Lock() + v, ok := shard.items[key] + remove := cb(key, v, ok) + if remove && ok { + delete(shard.items, key) + } + shard.Unlock() + return remove +} + +// Pop removes an element from the map and returns it +func (m ConcurrentMap[K, V]) Pop(key K) (v V, exists bool) { + // Try to get shard. + shard := m.GetShard(key) + shard.Lock() + v, exists = shard.items[key] + delete(shard.items, key) + shard.Unlock() + return v, exists +} + +// IsEmpty checks if map is empty. +func (m ConcurrentMap[K, V]) IsEmpty() bool { + return m.Count() == 0 +} + +// Tuple Used by the Iter & IterBuffered functions to wrap two variables together over a channel, +type Tuple[K comparable, V any] struct { + Key K + Val V +} + +// IterBuffered returns a buffered iterator which could be used in a for range loop. +func (m ConcurrentMap[K, V]) IterBuffered() <-chan Tuple[K, V] { + chans := snapshot(m) + total := 0 + for _, c := range chans { + total += cap(c) + } + ch := make(chan Tuple[K, V], total) + go fanIn(chans, ch) + return ch +} + +// Clear removes all items from map. +func (m ConcurrentMap[K, V]) Clear() { + for item := range m.IterBuffered() { + m.Remove(item.Key) + } +} + +// Returns an array of channels that contains elements in each shard, +// which likely takes a snapshot of `m`. +// It returns once the size of each buffered channel is determined, +// before all the channels are populated using goroutines. +func snapshot[K comparable, V any](m ConcurrentMap[K, V]) (chans []chan Tuple[K, V]) { + //When you access map items before initializing. + if len(m.shards) == 0 { + panic(`cmap.ConcurrentMap is not initialized. Should run New() before usage.`) + } + chans = make([]chan Tuple[K, V], ShardCount) + wg := sync.WaitGroup{} + wg.Add(ShardCount) + // Foreach shard. + for index, shard := range m.shards { + go func(index int, shard *ConcurrentMapShared[K, V]) { + // Foreach key, value pair. + shard.RLock() + chans[index] = make(chan Tuple[K, V], len(shard.items)) + wg.Done() + for key, val := range shard.items { + chans[index] <- Tuple[K, V]{key, val} + } + shard.RUnlock() + close(chans[index]) + }(index, shard) + } + wg.Wait() + return chans +} + +// fanIn reads elements from channels `chans` into channel `out` +func fanIn[K comparable, V any](chans []chan Tuple[K, V], out chan Tuple[K, V]) { + wg := sync.WaitGroup{} + wg.Add(len(chans)) + for _, ch := range chans { + go func(ch chan Tuple[K, V]) { + for t := range ch { + out <- t + } + wg.Done() + }(ch) + } + wg.Wait() + close(out) +} + +// Items returns all items as map[string]V +func (m ConcurrentMap[K, V]) Items() map[K]V { + tmp := make(map[K]V) + + // Insert items to temporary map. + for item := range m.IterBuffered() { + tmp[item.Key] = item.Val + } + + return tmp +} + +// IterCb Iterator callbacalled for every key,value found in +// maps. RLock is held for all calls for a given shard +// therefore callback sess consistent view of a shard, +// but not across the shards +type IterCb[K comparable, V any] func(key K, v V) + +// IterCb Callback based iterator, cheapest way to read +// all elements in a map. +func (m ConcurrentMap[K, V]) IterCb(fn IterCb[K, V]) { + for idx := range m.shards { + shard := (m.shards)[idx] + shard.RLock() + for key, value := range shard.items { + fn(key, value) + } + shard.RUnlock() + } +} + +// Keys returns all keys as []string +func (m ConcurrentMap[K, V]) Keys() []K { + count := m.Count() + ch := make(chan K, count) + go func() { + // Foreach shard. + wg := sync.WaitGroup{} + wg.Add(ShardCount) + for _, shard := range m.shards { + go func(shard *ConcurrentMapShared[K, V]) { + // Foreach key, value pair. + shard.RLock() + for key := range shard.items { + ch <- key + } + shard.RUnlock() + wg.Done() + }(shard) + } + wg.Wait() + close(ch) + }() + + // Generate keys + keys := make([]K, 0, count) + for k := range ch { + keys = append(keys, k) + } + return keys +} + +// MarshalJSON Reviles ConcurrentMap "private" variables to json marshal. +func (m ConcurrentMap[K, V]) MarshalJSON() ([]byte, error) { + // Create a temporary map, which will hold all item spread across shards. + tmp := make(map[K]V) + + // Insert items to temporary map. + for item := range m.IterBuffered() { + tmp[item.Key] = item.Val + } + return json.Marshal(tmp) +} +func strfnv32[K fmt.Stringer](key K) uint32 { + return fnv32(key.String()) +} + +func fnv32(key string) uint32 { + hash := uint32(2166136261) + const prime32 = uint32(16777619) + keyLength := len(key) + for i := 0; i < keyLength; i++ { + hash *= prime32 + hash ^= uint32(key[i]) + } + return hash +} + +// UnmarshalJSON Reverse process of Marshal. +func (m *ConcurrentMap[K, V]) UnmarshalJSON(b []byte) (err error) { + tmp := make(map[K]V) + + // Unmarshal into a single map. + if err := json.Unmarshal(b, &tmp); err != nil { + return err + } + + // foreach key,value pair in temporary map insert into our concurrent map. + for key, val := range tmp { + m.Set(key, val) + } + return nil +} diff --git a/pkg/color/string_darwin.go b/pkg/color/string_darwin.go new file mode 100644 index 0000000..b2097d1 --- /dev/null +++ b/pkg/color/string_darwin.go @@ -0,0 +1,47 @@ +//go:build darwin +// +build darwin + +package color + +import ( + "fmt" + "math/rand" + "strconv" +) + +var _ = RandomColor() + +// RandomColor generates a random color. +func RandomColor() string { + return fmt.Sprintf("#%s", strconv.FormatInt(int64(rand.Intn(16777216)), 16)) +} + +// Yellow ... +func Yellow(msg string) string { + return fmt.Sprintf("\x1b[33m%s\x1b[0m", msg) +} + +// Red ... +func Red(msg string) string { + return fmt.Sprintf("\x1b[31m%s\x1b[0m", msg) +} + +// Redf ... +func Redf(msg string, arg any) string { + return fmt.Sprintf("\x1b[31m%s\x1b[0m %+v\n", msg, arg) +} + +// Blue ... +func Blue(msg string) string { + return fmt.Sprintf("\x1b[34m%s\x1b[0m", msg) +} + +// Green ... +func Green(msg string) string { + return fmt.Sprintf("\x1b[32m%s\x1b[0m", msg) +} + +// Greenf ... +func Greenf(msg string, arg any) string { + return fmt.Sprintf("\x1b[32m%s\x1b[0m %+v\n", msg, arg) +} diff --git a/pkg/color/string_linux.go b/pkg/color/string_linux.go new file mode 100644 index 0000000..c4081f1 --- /dev/null +++ b/pkg/color/string_linux.go @@ -0,0 +1,47 @@ +//go:build linux +// +build linux + +package color + +import ( + "fmt" + "math/rand" + "strconv" +) + +var _ = RandomColor() + +// RandomColor generates a random color. +func RandomColor() string { + return fmt.Sprintf("#%s", strconv.FormatInt(int64(rand.Intn(16777216)), 16)) +} + +// Yellow ... +func Yellow(msg string) string { + return fmt.Sprintf("\x1b[33m%s\x1b[0m", msg) +} + +// Red ... +func Red(msg string) string { + return fmt.Sprintf("\x1b[31m%s\x1b[0m", msg) +} + +// Redf ... +func Redf(msg string, arg any) string { + return fmt.Sprintf("\x1b[31m%s\x1b[0m %+v\n", msg, arg) +} + +// Blue ... +func Blue(msg string) string { + return fmt.Sprintf("\x1b[34m%s\x1b[0m", msg) +} + +// Green ... +func Green(msg string) string { + return fmt.Sprintf("\x1b[32m%s\x1b[0m", msg) +} + +// Greenf ... +func Greenf(msg string, arg any) string { + return fmt.Sprintf("\x1b[32m%s\x1b[0m %+v\n", msg, arg) +} diff --git a/pkg/color/string_windows.go b/pkg/color/string_windows.go new file mode 100644 index 0000000..db8050d --- /dev/null +++ b/pkg/color/string_windows.go @@ -0,0 +1,47 @@ +//go:build windows +// +build windows + +package color + +import ( + "fmt" + "math/rand" + "strconv" +) + +var _ = RandomColor() + +// RandomColor generates a random color. +func RandomColor() string { + return fmt.Sprintf("#%s", strconv.FormatInt(int64(rand.Intn(16777216)), 16)) +} + +// Yellow ... +func Yellow(msg string) string { + return fmt.Sprintf("\033[33m%s\033[0m", msg) +} + +// Red ... +func Red(msg string) string { + return fmt.Sprintf("\033[31m%s\033[0m", msg) +} + +// Redf ... +func Redf(msg string, arg any) string { + return fmt.Sprintf("\033[31m%s\033[0m %+v\n", msg, arg) +} + +// Blue ... +func Blue(msg string) string { + return fmt.Sprintf("\033[34m%s\033[0m", msg) +} + +// Green ... +func Green(msg string) string { + return fmt.Sprintf("\033[32m%s\033[0m", msg) +} + +// Greenf ... +func Greenf(msg string, arg any) string { + return fmt.Sprintf("\033[32m%s\033[0m %+v\n", msg, arg) +} diff --git a/pkg/compress/compress.go b/pkg/compress/compress.go new file mode 100644 index 0000000..5b01d00 --- /dev/null +++ b/pkg/compress/compress.go @@ -0,0 +1,38 @@ +package compress + +import ( + "bytes" + "compress/zlib" + "io" +) + +var _ Compress = (*compress)(nil) + +type Compress interface { + DoZlibCompress(src []byte) []byte + DoZlibUnCompress(compressSrc []byte) []byte +} + +type compress struct{} + +func New() Compress { + return &compress{} +} + +// DoZlibCompress 进行zlib压缩 +func (c *compress) DoZlibCompress(src []byte) []byte { + var in bytes.Buffer + w := zlib.NewWriter(&in) + _, _ = w.Write(src) + _ = w.Close() + return in.Bytes() +} + +// DoZlibUnCompress 进行zlib解压缩 +func (c *compress) DoZlibUnCompress(compressSrc []byte) []byte { + b := bytes.NewReader(compressSrc) + var out bytes.Buffer + r, _ := zlib.NewReader(b) + _, _ = io.Copy(&out, r) + return out.Bytes() +} diff --git a/pkg/crontab/crontab.go b/pkg/crontab/crontab.go new file mode 100644 index 0000000..25a876b --- /dev/null +++ b/pkg/crontab/crontab.go @@ -0,0 +1,40 @@ +package crontab + +import ( + "github.com/robfig/cron/v3" +) + +var _ Crontab = (*crontab)(nil) + +type Crontab interface { + i() + AddFunc(spec string, cmd func()) (entryID cron.EntryID, err error) + Entries() []cron.Entry + Stop() +} + +type crontab struct { + cron *cron.Cron +} + +func New() Crontab { + return &crontab{ + cron: cron.New(), + } +} + +func (c *crontab) i() {} + +func (c *crontab) AddFunc(spec string, cmd func()) (entryID cron.EntryID, err error) { + entryID, err = c.cron.AddFunc(spec, cmd) + c.cron.Start() + return +} + +func (c *crontab) Stop() { + c.cron.Stop() +} + +func (c *crontab) Entries() []cron.Entry { + return c.cron.Entries() +} diff --git a/pkg/database/mongo.go b/pkg/database/mongo.go new file mode 100644 index 0000000..873d5ee --- /dev/null +++ b/pkg/database/mongo.go @@ -0,0 +1,77 @@ +package database + +import ( + "context" + "fmt" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" + "go.mongodb.org/mongo-driver/mongo/readpref" + "time" +) + +var _ MongoDB = (*mongoDB)(nil) + +type MongoDB interface { + i() + GetDB() *mongo.Database + Close() error +} + +type MongoDBConfig struct { + Addr string `yaml:"addr"` + User string `yaml:"user"` + Pass string `yaml:"pass"` + Name string `yaml:"name"` + Timeout time.Duration `yaml:"timeout"` +} + +type mongoDB struct { + client *mongo.Client + db *mongo.Database + timeout time.Duration +} + +func (m *mongoDB) i() {} + +func NewMongoDB(cfg MongoDBConfig) (MongoDB, error) { + timeout := cfg.Timeout * time.Second + connectCtx, connectCancelFunc := context.WithTimeout(context.Background(), timeout) + defer connectCancelFunc() + var auth string + if len(cfg.User) > 0 && len(cfg.Pass) > 0 { + auth = fmt.Sprintf("%s:%s@", cfg.User, cfg.Pass) + } + client, err := mongo.Connect(connectCtx, options.Client().ApplyURI( + fmt.Sprintf("mongodb://%s%s", auth, cfg.Addr), + )) + if err != nil { + return nil, err + } + + pingCtx, pingCancelFunc := context.WithTimeout(context.Background(), timeout) + defer pingCancelFunc() + err = client.Ping(pingCtx, readpref.Primary()) + if err != nil { + return nil, err + } + + return &mongoDB{ + client: client, + db: client.Database(cfg.Name), + timeout: timeout, + }, nil +} + +func (m *mongoDB) GetDB() *mongo.Database { + return m.db +} + +func (m *mongoDB) Close() error { + disconnectCtx, disconnectCancelFunc := context.WithTimeout(context.Background(), m.timeout) + defer disconnectCancelFunc() + err := m.client.Disconnect(disconnectCtx) + if err != nil { + return err + } + return nil +} diff --git a/pkg/database/mysql.go b/pkg/database/mysql.go new file mode 100644 index 0000000..8e731fa --- /dev/null +++ b/pkg/database/mysql.go @@ -0,0 +1,247 @@ +package database + +import ( + "errors" + "fmt" + "log" + "os" + "time" + + "git.bvbej.com/bvbej/base-golang/pkg/time_parse" + "git.bvbej.com/bvbej/base-golang/pkg/trace" + "gorm.io/driver/mysql" + "gorm.io/gorm" + "gorm.io/gorm/logger" + "gorm.io/gorm/schema" + "gorm.io/gorm/utils" +) + +const ( + callBackBeforeName = "core:before" + callBackAfterName = "core:after" + startTime = "_start_time" + traceCtxName = "_trace_ctx_name" +) + +var _ MysqlRepo = (*mysqlRepo)(nil) + +type MysqlRepo interface { + i() + GetRead(options ...Option) *gorm.DB + GetWrite(options ...Option) *gorm.DB + Close() error +} + +type MySQLConfig struct { + Read struct { + Addr string `yaml:"addr"` + User string `yaml:"user"` + Pass string `yaml:"pass"` + Name string `yaml:"name"` + } `yaml:"read"` + Write struct { + Addr string `yaml:"addr"` + User string `yaml:"user"` + Pass string `yaml:"pass"` + Name string `yaml:"name"` + } `yaml:"write"` + Base struct { + MaxOpenConn int `yaml:"maxOpenConn"` //最大连接数 + MaxIdleConn int `yaml:"maxIdleConn"` //最大空闲连接数 + ConnMaxLifeTime time.Duration `yaml:"connMaxLifeTime"` //最大连接超时(分钟) + } `yaml:"base"` +} + +type mysqlRepo struct { + read *gorm.DB + write *gorm.DB +} + +func NewMysql(cfg MySQLConfig) (MysqlRepo, error) { + dbr, err := dbConnect(cfg.Read.User, cfg.Read.Pass, cfg.Read.Addr, cfg.Read.Name, + cfg.Base.MaxOpenConn, cfg.Base.MaxIdleConn, cfg.Base.ConnMaxLifeTime) + if err != nil { + return nil, err + } + + dbw, err := dbConnect(cfg.Write.User, cfg.Write.Pass, cfg.Write.Addr, cfg.Write.Name, + cfg.Base.MaxOpenConn, cfg.Base.MaxIdleConn, cfg.Base.ConnMaxLifeTime) + if err != nil { + return nil, err + } + + return &mysqlRepo{ + read: dbr, + write: dbw, + }, nil +} + +func (d *mysqlRepo) i() {} + +func (d *mysqlRepo) GetRead(options ...Option) *gorm.DB { + opt := newOption() + for _, f := range options { + f(opt) + } + + db := d.read + if opt.Trace != nil { + db.InstanceSet(traceCtxName, opt.Trace) + } + + return db +} + +func (d *mysqlRepo) GetWrite(options ...Option) *gorm.DB { + opt := newOption() + for _, f := range options { + f(opt) + } + + db := d.write + if opt.Trace != nil { + db.InstanceSet(traceCtxName, opt.Trace) + } + + return db +} + +func (d *mysqlRepo) Close() (err error) { + rdb, err1 := d.read.DB() + if err1 != nil { + err = errors.Join(err1) + } + err2 := rdb.Close() + if err2 != nil { + err = errors.Join(err2) + } + + wdb, err3 := d.write.DB() + if err3 != nil { + err = errors.Join(err3) + } + err4 := wdb.Close() + if err4 != nil { + err = errors.Join(err4) + } + + return err +} + +func dbConnect(user, pass, addr, dbName string, maxOpenConn, maxIdleConn int, connMaxLifeTime time.Duration) (*gorm.DB, error) { + dsn := fmt.Sprintf("%s:%s@tcp(%s)/%s?charset=utf8mb4&parseTime=%t&loc=%s", + user, + pass, + addr, + dbName, + true, + "Local") + + // 日志配置 + newLogger := logger.New( + log.New(os.Stdout, "\r\n", log.LstdFlags), + logger.Config{ + SlowThreshold: time.Second, // 慢SQL阈值 + Colorful: true, // 彩色打印 + IgnoreRecordNotFoundError: true, // 忽略记录未找到错误 + LogLevel: logger.Error, // 日志级别 + }, + ) + + db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{ + NamingStrategy: schema.NamingStrategy{ + SingularTable: true, + }, + Logger: newLogger, + }) + + if err != nil { + return nil, errors.Join(err, fmt.Errorf("[db connection failed] Database name: %s", dbName)) + } + + db.Set("gorm:table_options", "CHARSET=utf8mb4") + + sqlDB, err := db.DB() + if err != nil { + return nil, err + } + + // 设置连接池 用于设置最大打开的连接数,默认值为0表示不限制.设置最大的连接数,可以避免并发太高导致连接mysql出现too many connections的错误。 + sqlDB.SetMaxOpenConns(maxOpenConn) + + // 设置最大连接数 用于设置闲置的连接数.设置闲置的连接数则当开启的一个连接使用完成后可以放在池里等候下一次使用。 + sqlDB.SetMaxIdleConns(maxIdleConn) + + // 设置最大连接超时 + sqlDB.SetConnMaxLifetime(time.Minute * connMaxLifeTime) + + // 使用插件 + err = db.Use(&TracePlugin{}) + if err != nil { + return nil, err + } + + return db, nil +} + +/***************************************************************/ + +type TracePlugin struct{} + +func (op *TracePlugin) Name() string { + return "TracePlugin" +} + +func (op *TracePlugin) Initialize(db *gorm.DB) (err error) { + // 开始前 + _ = db.Callback().Create().Before("gorm:before_create").Register(callBackBeforeName, before) + _ = db.Callback().Query().Before("gorm:query").Register(callBackBeforeName, before) + _ = db.Callback().Delete().Before("gorm:before_delete").Register(callBackBeforeName, before) + _ = db.Callback().Update().Before("gorm:setup_reflect_value").Register(callBackBeforeName, before) + _ = db.Callback().Row().Before("gorm:row").Register(callBackBeforeName, before) + _ = db.Callback().Raw().Before("gorm:raw").Register(callBackBeforeName, before) + + // 结束后 + _ = db.Callback().Create().After("gorm:after_create").Register(callBackAfterName, after) + _ = db.Callback().Query().After("gorm:after_query").Register(callBackAfterName, after) + _ = db.Callback().Delete().After("gorm:after_delete").Register(callBackAfterName, after) + _ = db.Callback().Update().After("gorm:after_update").Register(callBackAfterName, after) + _ = db.Callback().Row().After("gorm:row").Register(callBackAfterName, after) + _ = db.Callback().Raw().After("gorm:raw").Register(callBackAfterName, after) + return +} + +func before(db *gorm.DB) { + db.InstanceSet(startTime, time.Now()) +} + +func after(db *gorm.DB) { + _traceCtx, isExist := db.InstanceGet(traceCtxName) + if !isExist { + return + } + _trace, ok := _traceCtx.(trace.T) + if !ok { + return + } + + _ts, isExist := db.InstanceGet(startTime) + if !isExist { + return + } + + ts, ok := _ts.(time.Time) + if !ok { + return + } + + sql := db.Dialector.Explain(db.Statement.SQL.String(), db.Statement.Vars...) + + sqlInfo := new(trace.SQL) + sqlInfo.Timestamp = time_parse.CSTLayoutString() + sqlInfo.SQL = sql + sqlInfo.Stack = utils.FileWithLineNum() + sqlInfo.Rows = db.Statement.RowsAffected + sqlInfo.CostSeconds = time.Since(ts).Seconds() + _trace.AppendSQL(sqlInfo) +} diff --git a/pkg/database/tool.go b/pkg/database/tool.go new file mode 100644 index 0000000..dd0336b --- /dev/null +++ b/pkg/database/tool.go @@ -0,0 +1,88 @@ +package database + +import ( + "database/sql" + "database/sql/driver" + "encoding/json" + "fmt" + "gorm.io/gorm" + "reflect" +) + +type NullTime sql.NullTime + +func (n *NullTime) Scan(value any) error { + return (*sql.NullTime)(n).Scan(value) +} + +func (n NullTime) Value() (driver.Value, error) { + if !n.Valid { + return nil, nil + } + return n.Time, nil +} + +func (n NullTime) MarshalJSON() ([]byte, error) { + if n.Valid { + return json.Marshal(n.Time) + } + return json.Marshal(nil) +} + +func (n *NullTime) UnmarshalJSON(b []byte) error { + if string(b) == "null" { + n.Valid = false + return nil + } + err := json.Unmarshal(b, &n.Time) + if err == nil { + n.Valid = true + } + return err +} + +/*-----------------------------------------------------------*/ + +type PaginateList struct { + Page int64 `json:"page"` + Size int64 `json:"size"` + Total int64 `json:"total"` + List any `json:"list"` +} + +func Paginate(db *gorm.DB, model any, page, size int64) (*PaginateList, error) { + ptr := reflect.ValueOf(model) + if ptr.Kind() != reflect.Ptr { + return nil, fmt.Errorf("model must be pointer") + } + + var total int64 + err := db.Model(model).Count(&total).Error + if err != nil { + return &PaginateList{ + Page: page, + Size: size, + Total: total, + List: make([]any, 0), + }, err + } + + offset := size * (page - 1) + err = db.Limit(int(size)).Offset(int(offset)).Find(model).Error + + if err != nil { + return &PaginateList{ + Page: page, + Size: size, + Total: total, + List: make([]any, 0), + }, err + } + + return &PaginateList{ + Page: page, + Size: size, + Total: total, + List: model, + }, nil +} diff --git a/pkg/database/trace.go b/pkg/database/trace.go new file mode 100644 index 0000000..93b3467 --- /dev/null +++ b/pkg/database/trace.go @@ -0,0 +1,23 @@ +package database + +import "git.bvbej.com/bvbej/base-golang/pkg/trace" + +type Trace = trace.T + +type Option func(*option) + +func WithTrace(t Trace) Option { + return func(opt *option) { + if t != nil { + opt.Trace = t.(*trace.Trace) + } + } +} + +func newOption() *option { + return &option{} +} + +type option struct { + Trace *trace.Trace +} diff --git a/pkg/ddm/README.md b/pkg/ddm/README.md new file mode 100644 index 0000000..640c15b --- /dev/null +++ b/pkg/ddm/README.md @@ -0,0 +1,26 @@ +## DDM + +动态数据掩码(Dynamic Data Masking,简称为DDM)能够防止把敏感数据暴露给未经授权的用户。 + +| 类型 | 要求 | 示例 | 说明 +| ---- | ---- | ---- | ---- +| 手机号 | 前 3 后 4 | 132****7986 | 定长 11 位数字 +| 邮箱地址 | 前 1 后 1 | l**w@gmail.com | 仅对 @ 之前的邮箱名称进行掩码 +| 姓名 | 隐姓 | *鸿章 | 将姓氏隐藏 +| 密码 | 不输出 | ****** | +| 银行卡卡号 | 前 6 后 4 | 622888******5676 | 银行卡卡号最多 19 位数字 +| 身份证号 | 前 1 后 1 | 1******7 | 定长 18 位 + +#### 代码示例 + +``` +// 返回值 +type message struct { + Email ddm.Email `json:"email"` +} + +msg := new(message) +msg.Email = ddm.Email("xinliangnote@163.com") +... + +``` diff --git a/pkg/ddm/benchmark.go b/pkg/ddm/benchmark.go new file mode 100644 index 0000000..41ae16a --- /dev/null +++ b/pkg/ddm/benchmark.go @@ -0,0 +1,35 @@ +package ddm + +import ( + "github.com/mritd/chinaid" +) + +type BType uint8 + +const ( + BMobile BType = iota + BIDNo + BName + BBankNo + BEmail + BAddress +) + +func Benchmark(bType BType) string { + switch bType { + case BMobile: + return chinaid.Mobile() + case BIDNo: + return chinaid.IDNo() + case BEmail: + return chinaid.Email() + case BAddress: + return chinaid.Address() + case BName: + return chinaid.Name() + case BBankNo: + return chinaid.BankNo() + default: + return "" + } +} diff --git a/pkg/ddm/mark.go b/pkg/ddm/mark.go new file mode 100644 index 0000000..3d95e00 --- /dev/null +++ b/pkg/ddm/mark.go @@ -0,0 +1,62 @@ +package ddm + +import ( + "fmt" + "strings" +) + +func (m Mobile) MarshalJSON() ([]byte, error) { + if len(m) != 11 { + return []byte(`"` + m + `"`), nil + } + + v := fmt.Sprintf("%s****%s", m[:3], m[len(m)-4:]) + return []byte(`"` + v + `"`), nil +} + +func (bc BankCard) MarshalJSON() ([]byte, error) { + if len(bc) > 19 || len(bc) < 16 { + return []byte(`"` + bc + `"`), nil + } + + v := fmt.Sprintf("%s******%s", bc[:6], bc[len(bc)-4:]) + return []byte(`"` + v + `"`), nil +} + +func (card IDCard) MarshalJSON() ([]byte, error) { + if len(card) != 18 { + return []byte(`"` + card + `"`), nil + } + + v := fmt.Sprintf("%s******%s", card[:1], card[len(card)-1:]) + return []byte(`"` + v + `"`), nil +} + +func (name IDName) MarshalJSON() ([]byte, error) { + if len(name) < 1 { + return []byte(`""`), nil + } + + nameRune := []rune(name) + v := fmt.Sprintf("*%s", string(nameRune[1:])) + return []byte(`"` + v + `"`), nil +} + +func (pw PassWord) MarshalJSON() ([]byte, error) { + v := "******" + return []byte(`"` + v + `"`), nil +} + +func (e Email) MarshalJSON() ([]byte, error) { + if !strings.Contains(string(e), "@") { + return []byte(`"` + e + `"`), nil + } + + split := strings.Split(string(e), "@") + if len(split[0]) < 1 || len(split[1]) < 1 { + return []byte(`"` + e + `"`), nil + } + + v := fmt.Sprintf("%s***%s", split[0][:1], split[0][len(split[0])-1:]) + return []byte(`"` + v + "@" + split[1] + `"`), nil +} diff --git a/pkg/ddm/type.go b/pkg/ddm/type.go new file mode 100644 index 0000000..47de14c --- /dev/null +++ b/pkg/ddm/type.go @@ -0,0 +1,19 @@ +package ddm + +// 手机号 132****7986 +type Mobile string + +// 银行卡号 622888******5676 +type BankCard string + +// 身份证号 1******7 +type IDCard string + +// 姓名 *鸿章 +type IDName string + +// 密码 ****** +type PassWord string + +// 邮箱 l***w@gmail.com +type Email string diff --git a/pkg/downloader/downloader.go b/pkg/downloader/downloader.go new file mode 100644 index 0000000..5a07d91 --- /dev/null +++ b/pkg/downloader/downloader.go @@ -0,0 +1,264 @@ +package downloader + +import ( + "errors" + "git.bvbej.com/bvbej/base-golang/pkg/downloader/base" + "git.bvbej.com/bvbej/base-golang/pkg/downloader/controller" + "git.bvbej.com/bvbej/base-golang/pkg/downloader/fetcher" + "git.bvbej.com/bvbej/base-golang/pkg/downloader/protocol/http" + "git.bvbej.com/bvbej/base-golang/pkg/downloader/util" + "github.com/google/uuid" + "net/url" + "strings" + "sync" + "time" +) + +type Listener func(event *Event) + +type TaskInfo struct { + ID string + Res *base.Resource + Opts *base.Options + Status base.Status + Progress *Progress + + fetcher fetcher.Fetcher + timer *util.Timer + locker *sync.Mutex +} + +type Progress struct { + // 下载耗时(纳秒) + Used int64 + // 每秒下载字节数 + Speed int64 + // 已下载的字节数 + Downloaded int64 +} + +type downloader struct { + *controller.DefaultController + fetchBuilders map[string]func() fetcher.Fetcher + task *TaskInfo + listener Listener + finished bool + finishedCh chan error +} + +func newDownloader(f func() (protocols []string, builder func() fetcher.Fetcher), options ...controller.Option) *downloader { + d := &downloader{ + DefaultController: controller.NewController(options...), + finishedCh: make(chan error, 1), + } + + d.fetchBuilders = make(map[string]func() fetcher.Fetcher) + protocols, builder := f() + for _, p := range protocols { + d.fetchBuilders[strings.ToUpper(p)] = builder + } + + return d +} + +func (d *downloader) buildFetcher(URL string) (fetcher.Fetcher, error) { + parseURL, err := url.Parse(URL) + if err != nil { + return nil, err + } + if fetchBuilder, ok := d.fetchBuilders[strings.ToUpper(parseURL.Scheme)]; ok { + fetched := fetchBuilder() + fetched.Setup(d.DefaultController) + return fetched, nil + } + return nil, errors.New("unsupported protocol") +} + +func (d *downloader) Resolve(req *base.Request) (*base.Resource, error) { + fetched, err := d.buildFetcher(req.URL) + if err != nil { + return nil, err + } + return fetched.Resolve(req) +} + +func (d *downloader) Create(res *base.Resource, opts *base.Options) (err error) { + fetched, err := d.buildFetcher(res.Req.URL) + if err != nil { + return + } + if !res.Range || opts.Connections < 1 { + opts.Connections = 1 + } + err = fetched.Create(res, opts) + if err != nil { + return + } + + task := &TaskInfo{ + ID: uuid.New().String(), + Res: res, + Opts: opts, + Status: base.DownloadStatusStart, + Progress: &Progress{}, + fetcher: fetched, + timer: &util.Timer{}, + locker: new(sync.Mutex), + } + d.task = task + task.timer.Start() + d.emit(EventKeyStart) + err = fetched.Start() + if err != nil { + return + } + + go func() { + err = fetched.Wait() + if err != nil { + d.emit(EventKeyError, err) + task.Status = base.DownloadStatusError + } else { + task.Progress.Used = task.timer.Used() + if task.Res.TotalSize == 0 { + task.Res.TotalSize = task.fetcher.Progress().TotalDownloaded() + } + used := task.Progress.Used / int64(time.Second) + if used == 0 { + used = 1 + } + task.Progress.Speed = task.Res.TotalSize / used + task.Progress.Downloaded = task.Res.TotalSize + d.emit(EventKeyDone) + task.Status = base.DownloadStatusDone + } + d.finished = true + d.emit(EventKeyFinally, err) + d.finishedCh <- err + }() + + // 每秒统计一次下载速度 + go func() { + for !d.finished { + if d.task.Status == base.DownloadStatusPause { + continue + } + + current := d.task.fetcher.Progress().TotalDownloaded() + d.task.Progress.Used = d.task.timer.Used() + d.task.Progress.Speed = current - d.task.Progress.Downloaded + d.task.Progress.Downloaded = current + d.emit(EventKeyProgress) + + time.Sleep(time.Second) + } + }() + + return +} + +func (d *downloader) Pause() error { + d.task.locker.Lock() + defer d.task.locker.Unlock() + d.task.timer.Pause() + err := d.task.fetcher.Pause() + if err != nil { + return err + } + d.emit(EventKeyPause) + d.task.Status = base.DownloadStatusPause + return nil +} + +func (d *downloader) Continue() error { + d.task.locker.Lock() + defer d.task.locker.Unlock() + d.task.timer.Continue() + err := d.task.fetcher.Continue() + if err != nil { + return err + } + d.emit(EventKeyContinue) + d.task.Status = base.DownloadStatusStart + return nil +} + +func (d *downloader) Listener(fn Listener) { + d.listener = fn +} + +func (d *downloader) emit(eventKey EventKey, errs ...error) { + if d.listener != nil { + var err error + if len(errs) > 0 { + err = errs[0] + } + d.listener(&Event{ + Key: eventKey, + Task: d.task, + Err: err, + }) + } +} + +var _ Boot = (*boot)(nil) + +type Boot interface { + URL(url string) Boot + Extra(extra any) Boot + Listener(listener Listener) Boot + Create(opts *base.Options) <-chan error +} + +type boot struct { + url string + extra any + listener Listener + downloader *downloader +} + +func (b *boot) resolve() (*base.Resource, error) { + return b.downloader.Resolve(&base.Request{ + URL: b.url, + Extra: b.extra, + }) +} + +func (b *boot) URL(url string) Boot { + b.url = url + return b +} + +func (b *boot) Extra(extra any) Boot { + b.extra = extra + return b +} + +func (b *boot) Listener(listener Listener) Boot { + b.listener = listener + return b +} + +func (b *boot) Create(opts *base.Options) <-chan error { + res, err := b.resolve() + if err != nil { + b.downloader.finishedCh <- err + return b.downloader.finishedCh + } + b.downloader.Listener(b.listener) + + err = b.downloader.Create(res, opts) + if err != nil { + b.downloader.finishedCh <- err + return b.downloader.finishedCh + } + + return b.downloader.finishedCh +} + +// New 一个文件对应一个实例 +func New(options ...controller.Option) Boot { + return &boot{ + downloader: newDownloader(http.FetcherBuilder, options...), + } +} diff --git a/pkg/downloader/downloader_test.go b/pkg/downloader/downloader_test.go new file mode 100644 index 0000000..4ac1c85 --- /dev/null +++ b/pkg/downloader/downloader_test.go @@ -0,0 +1,45 @@ +package downloader + +import ( + "fmt" + "git.bvbej.com/bvbej/base-golang/pkg/downloader/base" + "git.bvbej.com/bvbej/base-golang/pkg/downloader/controller" + "git.bvbej.com/bvbej/base-golang/tool" + "golang.org/x/net/proxy" + "net/http" + "net/url" + "runtime" + "testing" + "time" +) + +func TestNewDownloader(t *testing.T) { + dialer, err := proxy.SOCKS5("tcp", "127.0.0.1:1080", nil, proxy.Direct) + parse, _ := url.Parse(`socks5://127.0.0.1:1080`) + if err != nil { + t.Fatal(err, dialer) + } + err = <-New( + controller.WithDialer(dialer), // todo: use dialer proxy + controller.WithCookie(nil), + controller.WithProxy(http.ProxyURL(parse)), // todo: use http client proxy + controller.WithTimeout(time.Second*3), + ).URL("http://10.0.1.34/com.tencent.tmgp.jxqy.apk"). + Listener(func(event *Event) { + if event.Key == EventKeyFinally { + fmt.Println("下载完成!") + } + if event.Key == EventKeyProgress { + fmt.Printf("下载速度:%s/s 已下载:%s 已用时:%s \n", + tool.ByteFmt(event.Task.Progress.Speed), + tool.ByteFmt(event.Task.Progress.Downloaded), + time.Duration(event.Task.Progress.Used), + ) + } + }). + Create(&base.Options{ + Connections: runtime.NumCPU(), + }) + + t.Log(err) +} diff --git a/pkg/downloader/event.go b/pkg/downloader/event.go new file mode 100644 index 0000000..12b393d --- /dev/null +++ b/pkg/downloader/event.go @@ -0,0 +1,19 @@ +package downloader + +type EventKey string + +const ( + EventKeyStart EventKey = "start" + EventKeyPause EventKey = "pause" + EventKeyContinue EventKey = "continue" + EventKeyProgress EventKey = "progress" + EventKeyError EventKey = "error" + EventKeyDone EventKey = "done" + EventKeyFinally EventKey = "finally" +) + +type Event struct { + Key EventKey + Task *TaskInfo + Err error +} diff --git a/pkg/duration_fmt/fmt.go b/pkg/duration_fmt/fmt.go new file mode 100644 index 0000000..5bd6575 --- /dev/null +++ b/pkg/duration_fmt/fmt.go @@ -0,0 +1,340 @@ +package duration_fmt + +import ( + "errors" + "fmt" + "regexp" + "strconv" + "strings" + "time" +) + +var ( + //units, _ = DefaultUnitsCoder.Decode("year,week,day,hour,minute,second,millisecond,microsecond") + units, _ = DefaultUnitsCoder.Decode("年,星期,天,小时,分钟,秒,毫秒,微秒") + unitsShort = []string{"y", "w", "d", "h", "m", "s", "ms", "µs"} +) + +// Durafmt holds the parsed duration and the original input duration. +type Durafmt struct { + duration time.Duration + input string // Used as reference. + limitN int // Non-zero to limit only first N elements to output. + limitUnit string // Non-empty to limit max unit +} + +// LimitToUnit sets the output format, you will not have unit bigger than the UNIT specified. UNIT = "" means no restriction. +func (d *Durafmt) LimitToUnit(unit string) *Durafmt { + d.limitUnit = unit + return d +} + +// LimitFirstN sets the output format, outputing only first N elements. n == 0 means no limit. +func (d *Durafmt) LimitFirstN(n int) *Durafmt { + d.limitN = n + return d +} + +func (d *Durafmt) Duration() time.Duration { + return d.duration +} + +// Truncate sets precision +func (d *Durafmt) Truncate(unit time.Duration) *Durafmt { + d.duration = d.duration.Truncate(unit) + return d +} + +// Parse creates a new *Durafmt struct, returns error if input is invalid. +func Parse(dinput time.Duration) *Durafmt { + input := dinput.String() + return &Durafmt{dinput, input, 0, ""} +} + +// ParseShort creates a new *Durafmt struct, short form, returns error if input is invalid. +// It's shortcut for `Parse(dur).LimitFirstN(1)` +func ParseShort(dinput time.Duration) *Durafmt { + input := dinput.String() + return &Durafmt{dinput, input, 1, ""} +} + +// ParseString creates a new *Durafmt struct from a string. +// returns an error if input is invalid. +func ParseString(input string) (*Durafmt, error) { + if input == "0" || input == "-0" { + return nil, errors.New("durafmt: missing unit in duration " + input) + } + duration, err := time.ParseDuration(input) + if err != nil { + return nil, err + } + return &Durafmt{duration, input, 0, ""}, nil +} + +// ParseStringShort creates a new *Durafmt struct from a string, short form +// returns an error if input is invalid. +// It's shortcut for `ParseString(durStr)` and then calling `LimitFirstN(1)` +func ParseStringShort(input string) (*Durafmt, error) { + if input == "0" || input == "-0" { + return nil, errors.New("durafmt: missing unit in duration " + input) + } + duration, err := time.ParseDuration(input) + if err != nil { + return nil, err + } + return &Durafmt{duration, input, 1, ""}, nil +} + +// String parses d *Durafmt into a human readable duration with default units. +func (d *Durafmt) String() string { + return d.Format(units) +} + +// Format parses d *Durafmt into a human readable duration with units. +func (d *Durafmt) Format(units Units) string { + var duration string + + // Check for minus durations. + if string(d.input[0]) == "-" { + duration += "-" + d.duration = -d.duration + } + + var microseconds int64 + var milliseconds int64 + var seconds int64 + var minutes int64 + var hours int64 + var days int64 + var weeks int64 + var years int64 + var shouldConvert = false + + remainingSecondsToConvert := int64(d.duration / time.Microsecond) + + // Convert duration. + if d.limitUnit == "" { + shouldConvert = true + } + + if d.limitUnit == "years" || shouldConvert { + years = remainingSecondsToConvert / (365 * 24 * 3600 * 1000000) + remainingSecondsToConvert -= years * 365 * 24 * 3600 * 1000000 + shouldConvert = true + } + + if d.limitUnit == "weeks" || shouldConvert { + weeks = remainingSecondsToConvert / (7 * 24 * 3600 * 1000000) + remainingSecondsToConvert -= weeks * 7 * 24 * 3600 * 1000000 + shouldConvert = true + } + + if d.limitUnit == "days" || shouldConvert { + days = remainingSecondsToConvert / (24 * 3600 * 1000000) + remainingSecondsToConvert -= days * 24 * 3600 * 1000000 + shouldConvert = true + } + + if d.limitUnit == "hours" || shouldConvert { + hours = remainingSecondsToConvert / (3600 * 1000000) + remainingSecondsToConvert -= hours * 3600 * 1000000 + shouldConvert = true + } + + if d.limitUnit == "minutes" || shouldConvert { + minutes = remainingSecondsToConvert / (60 * 1000000) + remainingSecondsToConvert -= minutes * 60 * 1000000 + shouldConvert = true + } + + if d.limitUnit == "seconds" || shouldConvert { + seconds = remainingSecondsToConvert / 1000000 + remainingSecondsToConvert -= seconds * 1000000 + shouldConvert = true + } + + if d.limitUnit == "milliseconds" || shouldConvert { + milliseconds = remainingSecondsToConvert / 1000 + remainingSecondsToConvert -= milliseconds * 1000 + } + + microseconds = remainingSecondsToConvert + + // Create a map of the converted duration time. + durationMap := []int64{ + microseconds, + milliseconds, + seconds, + minutes, + hours, + days, + weeks, + years, + } + + // Construct duration string. + for i, u := range units.Units() { + v := durationMap[7-i] + strval := strconv.FormatInt(v, 10) + switch { + // add to the duration string if v > 1. + case v > 1: + duration += strval + " " + u.Plural + " " + // remove the plural 's', if v is 1. + case v == 1: + duration += strval + " " + u.Singular + " " + // omit any value with 0s or 0. + case d.duration.String() == "0" || d.duration.String() == "0s": + pattern := fmt.Sprintf("^-?0%s$", unitsShort[i]) + isMatch, err := regexp.MatchString(pattern, d.input) + if err != nil { + return "" + } + if isMatch { + duration += strval + " " + u.Plural + } + + // omit any value with 0. + case v == 0: + continue + } + } + // trim any remaining spaces. + duration = strings.TrimSpace(duration) + + // if more than 2 spaces present return the first 2 strings + // if short version is requested + if d.limitN > 0 { + parts := strings.Split(duration, " ") + if len(parts) > d.limitN*2 { + duration = strings.Join(parts[:d.limitN*2], " ") + } + } + + return duration +} + +func (d *Durafmt) InternationalString() string { + var duration string + + // Check for minus durations. + if string(d.input[0]) == "-" { + duration += "-" + d.duration = -d.duration + } + + var microseconds int64 + var milliseconds int64 + var seconds int64 + var minutes int64 + var hours int64 + var days int64 + var weeks int64 + var years int64 + var shouldConvert = false + + remainingSecondsToConvert := int64(d.duration / time.Microsecond) + + // Convert duration. + if d.limitUnit == "" { + shouldConvert = true + } + + if d.limitUnit == "years" || shouldConvert { + years = remainingSecondsToConvert / (365 * 24 * 3600 * 1000000) + remainingSecondsToConvert -= years * 365 * 24 * 3600 * 1000000 + shouldConvert = true + } + + if d.limitUnit == "weeks" || shouldConvert { + weeks = remainingSecondsToConvert / (7 * 24 * 3600 * 1000000) + remainingSecondsToConvert -= weeks * 7 * 24 * 3600 * 1000000 + shouldConvert = true + } + + if d.limitUnit == "days" || shouldConvert { + days = remainingSecondsToConvert / (24 * 3600 * 1000000) + remainingSecondsToConvert -= days * 24 * 3600 * 1000000 + shouldConvert = true + } + + if d.limitUnit == "hours" || shouldConvert { + hours = remainingSecondsToConvert / (3600 * 1000000) + remainingSecondsToConvert -= hours * 3600 * 1000000 + shouldConvert = true + } + + if d.limitUnit == "minutes" || shouldConvert { + minutes = remainingSecondsToConvert / (60 * 1000000) + remainingSecondsToConvert -= minutes * 60 * 1000000 + shouldConvert = true + } + + if d.limitUnit == "seconds" || shouldConvert { + seconds = remainingSecondsToConvert / 1000000 + remainingSecondsToConvert -= seconds * 1000000 + shouldConvert = true + } + + if d.limitUnit == "milliseconds" || shouldConvert { + milliseconds = remainingSecondsToConvert / 1000 + remainingSecondsToConvert -= milliseconds * 1000 + } + + microseconds = remainingSecondsToConvert + + // Create a map of the converted duration time. + durationMap := map[string]int64{ + "µs": microseconds, + "ms": milliseconds, + "s": seconds, + "m": minutes, + "h": hours, + "d": days, + "w": weeks, + "y": years, + } + + // Construct duration string. + for i := range units.Units() { + u := unitsShort[i] + v := durationMap[u] + strval := strconv.FormatInt(v, 10) + switch { + // add to the duration string if v > 0. + case v > 0: + duration += strval + " " + u + " " + // omit any value with 0. + case d.duration.String() == "0": + pattern := fmt.Sprintf("^-?0%s$", unitsShort[i]) + isMatch, err := regexp.MatchString(pattern, d.input) + if err != nil { + return "" + } + if isMatch { + duration += strval + " " + u + } + + // omit any value with 0. + case v == 0: + continue + } + } + // trim any remaining spaces. + duration = strings.TrimSpace(duration) + + // if more than 2 spaces present return the first 2 strings + // if short version is requested + if d.limitN > 0 { + parts := strings.Split(duration, " ") + if len(parts) > d.limitN*2 { + duration = strings.Join(parts[:d.limitN*2], " ") + } + } + + return duration +} + +func (d *Durafmt) TrimSpace() string { + return strings.Replace(d.String(), " ", "", -1) +} diff --git a/pkg/duration_fmt/units.go b/pkg/duration_fmt/units.go new file mode 100644 index 0000000..0781cc5 --- /dev/null +++ b/pkg/duration_fmt/units.go @@ -0,0 +1,107 @@ +package duration_fmt + +import ( + "fmt" + "strings" +) + +// DefaultUnitsCoder default units coder using `":"` as PluralSep and `","` as UnitsSep +var DefaultUnitsCoder = UnitsCoder{":", ","} + +// Unit the pair of singular and plural units +type Unit struct { + Singular, Plural string +} + +// Units duration units +type Units struct { + Year, Week, Day, Hour, Minute, + Second, Millisecond, Microsecond Unit +} + +// Units return a slice of units +func (u Units) Units() []Unit { + return []Unit{u.Year, u.Week, u.Day, u.Hour, u.Minute, + u.Second, u.Millisecond, u.Microsecond} +} + +// UnitsCoder the units encoder and decoder +type UnitsCoder struct { + // PluralSep char to sep singular and plural pair. + // Example with char `":"`: `"year:year"` (english) or `"mês:meses"` (portuguese) + PluralSep, + // UnitsSep char to sep units (singular and plural pairs). + // Example with char `","`: `"year:year,week:weeks"` (english) or `"mês:meses,semana:semanas"` (portuguese) + UnitsSep string +} + +// Encode encodes input Units to string +// Examples with `UnitsCoder{PluralSep: ":", UnitsSep = ","}` +// - singular and plural pair units: `"year:wers,week:weeks,day:days,hour:hours,minute:minutes,second:seconds,millisecond:millliseconds,microsecond:microsseconds"` +func (coder UnitsCoder) Encode(units Units) string { + var pairs = make([]string, 8) + for i, u := range units.Units() { + pairs[i] = u.Singular + coder.PluralSep + u.Plural + } + return strings.Join(pairs, coder.UnitsSep) +} + +// Decode decodes input string to Units. +// The input must follow the following formats: +// - Unit format (singular and plural pair) +// - must singular (the plural receives 's' character as suffix) +// - singular and plural: separated by `PluralSep` char +// Example with char `":"`: `"year:year"` (english) or `"mês:meses"` (portuguese) +// - Units format (pairs of Year, Week, Day, Hour, Minute, +// Second, Millisecond and Microsecond units) separated by `UnitsSep` char +// - Examples with `UnitsCoder{PluralSep: ":", UnitsSep = ","}` +// - must singular units: `"year,week,day,hour,minute,second,millisecond,microsecond"` +// - mixed units: `"year,week:weeks,day,hour,minute:minutes,second,millisecond,microsecond"` +// - singular and plural pair units: `"year:wers,week:weeks,day:days,hour:hours,minute:minutes,second:seconds,millisecond:millliseconds,microsecond:microsseconds"` +func (coder UnitsCoder) Decode(s string) (units Units, err error) { + parts := strings.Split(s, coder.UnitsSep) + if len(parts) != 8 { + err = fmt.Errorf("bad parts length") + return units, err + } + + var parse = func(name, part string, u *Unit) bool { + ps := strings.Split(part, coder.PluralSep) + switch len(ps) { + case 1: + u.Singular, u.Plural = ps[0], ps[0] + case 2: + u.Singular, u.Plural = ps[0], ps[1] + default: + err = fmt.Errorf("bad unit %q pair length", name) + return false + } + return true + } + + if !parse("Year", parts[0], &units.Year) { + return units, err + } + if !parse("Week", parts[1], &units.Week) { + return units, err + } + if !parse("Day", parts[2], &units.Day) { + return units, err + } + if !parse("Hour", parts[3], &units.Hour) { + return units, err + } + if !parse("Minute", parts[4], &units.Minute) { + return units, err + } + if !parse("Second", parts[5], &units.Second) { + return units, err + } + if !parse("Millisecond", parts[6], &units.Millisecond) { + return units, err + } + if !parse("Microsecond", parts[7], &units.Microsecond) { + return units, err + } + return units, err +} diff --git a/pkg/env/env.go b/pkg/env/env.go new file mode 100644 index 0000000..a82cea7 --- /dev/null +++ b/pkg/env/env.go @@ -0,0 +1,80 @@ +package env + +import ( + "fmt" + "strings" +) + +var ( + active Environment + dev Environment = &environment{value: "dev"} //开发环境 + fat Environment = &environment{value: "fat"} //测试环境 + uat Environment = &environment{value: "uat"} //预上线环境 + pro Environment = &environment{value: "pro"} //正式环境 +) + +var _ Environment = (*environment)(nil) + +// Environment 环境配置 +type Environment interface { + Value() string + IsDev() bool + IsFat() bool + IsUat() bool + IsPro() bool + t() +} + +type environment struct { + value string +} + +func (e *environment) Value() string { + return e.value +} + +func (e *environment) IsDev() bool { + return e.value == "dev" +} + +func (e *environment) IsFat() bool { + return e.value == "fat" +} + +func (e *environment) IsUat() bool { + return e.value == "uat" +} + +func (e *environment) IsPro() bool { + return e.value == "pro" +} + +func (e *environment) t() {} + +func Set(env string) error { + var err error + + switch strings.ToLower(strings.TrimSpace(env)) { + case "dev": + active = dev + case "fat": + active = fat + case "uat": + active = uat + case "pro": + active = pro + default: + err = fmt.Errorf("'%s' cannot be found, or it is illegal. enum:dev(开发环境),fat(测试环境),uat(预上线环境),pro(正式环境)", env) + } + + return err +} + +// Active 当前配置的env +func Active() Environment { + if active == nil { + fmt.Println("Warning: environment not set. The default 'dev' will be used.") + active = dev + } + return active +} diff --git a/pkg/errno/errno.go b/pkg/errno/errno.go new file mode 100644 index 0000000..f524076 --- /dev/null +++ b/pkg/errno/errno.go @@ -0,0 +1,74 @@ +package errno + +import ( + "encoding/json" +) + +var _ Error = (*err)(nil) + +type Error interface { + // WithErr 设置错误信息 + WithErr(err error) Error + // GetBusinessCode 获取 Business Code + GetBusinessCode() int + // GetHttpCode 获取 HTTP Code + GetHttpCode() int + // GetMsg 获取 Msg + GetMsg() string + // GetErr 获取错误信息 + GetErr() error + // ToString 返回 JSON 格式的错误详情 + ToString() string +} + +type err struct { + HttpCode int // HTTP Code + BusinessCode int // Business Code + Message string // 描述信息 + Err error // 错误信息 +} + +func NewError(httpCode, businessCode int, msg string) Error { + return &err{ + HttpCode: httpCode, + BusinessCode: businessCode, + Message: msg, + } +} + +func (e *err) WithErr(err error) Error { + e.Err = err + return e +} + +func (e *err) GetHttpCode() int { + return e.HttpCode +} + +func (e *err) GetBusinessCode() int { + return e.BusinessCode +} + +func (e *err) GetMsg() string { + return e.Message +} + +func (e *err) GetErr() error { + return e.Err +} + +// ToString 返回 JSON 格式的错误详情 +func (e *err) ToString() string { + err := &struct { + HttpCode int `json:"http_code"` + BusinessCode int `json:"business_code"` + Message string `json:"message"` + }{ + HttpCode: e.HttpCode, + BusinessCode: e.BusinessCode, + Message: e.Message, + } + + raw, _ := json.Marshal(err) + return string(raw) +} diff --git a/pkg/excel/export.go b/pkg/excel/export.go new file mode 100644 index 0000000..751a02c --- /dev/null +++ b/pkg/excel/export.go @@ -0,0 +1,111 @@ +package excel + +import ( + "github.com/xuri/excelize/v2" + "strconv" +) + +const maxCharCount = 26 + +var DefaultColumnWidth float64 = 20 + +type ColumnOption struct { + Field string + Comment string + Width float64 +} + +func Export(sheetName, filepath string, columns []ColumnOption, rows []map[string]any) error { + f := excelize.NewFile() + sheetIndex, err := f.NewSheet(sheetName) + if err != nil { + return err + } + _ = f.DeleteSheet("Sheet1") + _ = f.SetColWidth(sheetName, "A", string(byte('A'+len(columns)-1)), DefaultColumnWidth) + contentStyle, _ := f.NewStyle(&excelize.Style{ + Alignment: &excelize.Alignment{ + Horizontal: "center", + Vertical: "center", + WrapText: true, + }, + }) + titleStyle, _ := f.NewStyle(&excelize.Style{ + Alignment: &excelize.Alignment{ + Horizontal: "center", + Vertical: "center", + WrapText: true, + }, + Font: &excelize.Font{ + Bold: true, + Size: 14, + }, + }) + + maxColumnRowNameLen := 1 + len(strconv.Itoa(len(rows))) + columnCount := len(columns) + if columnCount > maxCharCount { + maxColumnRowNameLen++ + } else if columnCount > maxCharCount*maxCharCount { + maxColumnRowNameLen += 2 + } + + //标题 + type columnItem struct { + RowName []byte + FieldName string + } + columnNames := make([]columnItem, 0) + for index, column := range columns { + columnName := getColumnName(index, maxColumnRowNameLen) + if column.Width > 0 { + _ = f.SetColWidth(sheetName, string(columnName), string(columnName), column.Width) + } + columnNames = append(columnNames, columnItem{FieldName: column.Field, RowName: columnName}) + rowName := getColumnRowName(columnName, 1) + err := f.SetCellValue(sheetName, rowName, column.Comment) + if err != nil { + return err + } + _ = f.SetCellStyle(sheetName, rowName, rowName, titleStyle) + } + + //正文 + for rowIndex, row := range rows { + for _, item := range columnNames { + rowName := getColumnRowName(item.RowName, rowIndex+2) + err := f.SetCellValue(sheetName, rowName, row[item.FieldName]) + if err != nil { + return err + } + _ = f.SetCellStyle(sheetName, rowName, rowName, contentStyle) + } + } + + f.SetActiveSheet(sheetIndex) + + err = f.SaveAs(filepath) + if err != nil { + return err + } + + return nil +} + +func getColumnName(column, maxColumnRowNameLen int) []byte { + const A = 'A' + if column < maxCharCount { + slice := make([]byte, 0, maxColumnRowNameLen) + return append(slice, byte(A+column)) + } else { + return append(getColumnName(column/maxCharCount-1, maxColumnRowNameLen), byte(A+column%maxCharCount)) + } +} + +func getColumnRowName(columnName []byte, rowIndex int) (columnRowName string) { + l := len(columnName) + columnName = strconv.AppendInt(columnName, int64(rowIndex), 10) + columnRowName = string(columnName) + columnName = columnName[:l] + return +} diff --git a/pkg/file/file.go b/pkg/file/file.go new file mode 100644 index 0000000..9bee285 --- /dev/null +++ b/pkg/file/file.go @@ -0,0 +1,189 @@ +package file + +import ( + "bytes" + "fmt" + "io" + "os" +) + +var ( + buffSize = 1 << 20 +) + +// ReadLineFromEnd -- +type ReadLineFromEnd struct { + f *os.File + + fileSize int + bwr *bytes.Buffer + lineBuff []byte + swapBuff []byte + + isFirst bool +} + +// Exists +func IsExists(path string) (os.FileInfo, bool) { + f, err := os.Stat(path) + return f, err == nil || os.IsExist(err) +} + +// NewReadLineFromEnd +func NewReadLineFromEnd(filename string) (rd *ReadLineFromEnd, err error) { + f, err := os.Open(filename) + if err != nil { + return nil, err + } + + info, err := f.Stat() + if err != nil { + return nil, err + } + + if info.IsDir() { + return nil, fmt.Errorf("not file") + } + + fileSize := int(info.Size()) + + rd = &ReadLineFromEnd{ + f: f, + fileSize: fileSize, + bwr: bytes.NewBuffer([]byte{}), + lineBuff: make([]byte, 0), + swapBuff: make([]byte, buffSize), + isFirst: true, + } + return rd, nil +} + +// ReadLine 结尾包含'\n' +func (c *ReadLineFromEnd) ReadLine() (line []byte, err error) { + var ok bool + for { + ok, err = c.buff() + if err != nil { + return nil, err + } + if ok { + break + } + } + line, err = c.bwr.ReadBytes('\n') + if err == io.EOF && c.fileSize > 0 { + err = nil + } + return line, err +} + +// Close -- +func (c *ReadLineFromEnd) Close() (err error) { + return c.f.Close() +} + +func (c *ReadLineFromEnd) buff() (ok bool, err error) { + if c.fileSize == 0 { + return true, nil + } + + if c.bwr.Len() >= buffSize { + return true, nil + } + + offset := 0 + if c.fileSize > buffSize { + offset = c.fileSize - buffSize + } + _, err = c.f.Seek(int64(offset), 0) + if err != nil { + return false, err + } + + n, err := c.f.Read(c.swapBuff) + if err != nil && err != io.EOF { + return false, err + } + if c.fileSize < n { + n = c.fileSize + } + if n == 0 { + return true, nil + } + + for { + m := bytes.LastIndex(c.swapBuff[:n], []byte{'\n'}) + if m == -1 { + break + } + if m < n-1 { + err = c.writeLine(c.swapBuff[m+1 : n]) + if err != nil { + return false, err + } + ok = true + } else if m == n-1 && !c.isFirst { + err = c.writeLine(nil) + if err != nil { + return false, err + } + ok = true + } + n = m + if n == 0 { + break + } + } + if n > 0 { + reverseBytes(c.swapBuff[:n]) + c.lineBuff = append(c.lineBuff, c.swapBuff[:n]...) + } + if offset == 0 { + err = c.writeLine(nil) + if err != nil { + return false, err + } + ok = true + } + c.fileSize = offset + if c.isFirst { + c.isFirst = false + } + return ok, nil +} + +func (c *ReadLineFromEnd) writeLine(b []byte) (err error) { + if len(b) > 0 { + _, err = c.bwr.Write(b) + if err != nil { + return err + } + } + if len(c.lineBuff) > 0 { + reverseBytes(c.lineBuff) + _, err = c.bwr.Write(c.lineBuff) + if err != nil { + return err + } + c.lineBuff = c.lineBuff[:0] + } + _, err = c.bwr.Write([]byte{'\n'}) + if err != nil { + return err + } + return nil +} + +func reverseBytes(b []byte) { + n := len(b) + if n <= 1 { + return + } + for i := 0; i < n; i++ { + k := n - 1 + if k != i { + b[i], b[k] = b[k], b[i] + } + n-- + } +} diff --git a/pkg/grpclient/client.go b/pkg/grpclient/client.go new file mode 100644 index 0000000..0723fd5 --- /dev/null +++ b/pkg/grpclient/client.go @@ -0,0 +1,98 @@ +package grpclient + +import ( + "context" + "errors" + "time" + + "google.golang.org/grpc" + "google.golang.org/grpc/credentials" + "google.golang.org/grpc/credentials/insecure" + "google.golang.org/grpc/keepalive" +) + +var ( + defaultDialTimeout = time.Second * 2 +) + +type Option func(*option) + +type option struct { + credential credentials.TransportCredentials + keepalive *keepalive.ClientParameters + dialTimeout time.Duration + unaryInterceptor grpc.UnaryClientInterceptor +} + +// WithCredential setup credential for tls +func WithCredential(credential credentials.TransportCredentials) Option { + return func(opt *option) { + opt.credential = credential + } +} + +// WithKeepAlive setup keepalive parameters +func WithKeepAlive(keepalive *keepalive.ClientParameters) Option { + return func(opt *option) { + opt.keepalive = keepalive + } +} + +// WithDialTimeout set up the dial timeout +func WithDialTimeout(timeout time.Duration) Option { + return func(opt *option) { + opt.dialTimeout = timeout + } +} + +func WithUnaryInterceptor(unaryInterceptor grpc.UnaryClientInterceptor) Option { + return func(opt *option) { + opt.unaryInterceptor = unaryInterceptor + } +} + +func New(target string, options ...Option) (*grpc.ClientConn, error) { + if target == "" { + return nil, errors.New("target required") + } + + opt := new(option) + for _, f := range options { + f(opt) + } + + kacp := defaultKeepAlive + if opt.keepalive != nil { + kacp = opt.keepalive + } + + dialTimeout := defaultDialTimeout + if opt.dialTimeout > 0 { + dialTimeout = opt.dialTimeout + } + + dialOptions := []grpc.DialOption{ + grpc.WithBlock(), + grpc.WithKeepaliveParams(*kacp), + } + + if opt.unaryInterceptor != nil { + dialOptions = append(dialOptions, grpc.WithUnaryInterceptor(opt.unaryInterceptor)) + } + + if opt.credential == nil { + dialOptions = append(dialOptions, grpc.WithTransportCredentials(insecure.NewCredentials())) + } else { + dialOptions = append(dialOptions, grpc.WithTransportCredentials(opt.credential)) + } + + ctx, cancel := context.WithTimeout(context.Background(), dialTimeout) + defer cancel() + + conn, err := grpc.DialContext(ctx, target, dialOptions...) + if err != nil { + return nil, err + } + + return conn, nil +} diff --git a/pkg/grpclient/keepalive.go b/pkg/grpclient/keepalive.go new file mode 100644 index 0000000..70fc6b0 --- /dev/null +++ b/pkg/grpclient/keepalive.go @@ -0,0 +1,15 @@ +package grpclient + +import ( + "time" + + "google.golang.org/grpc/keepalive" +) + +var ( + defaultKeepAlive = &keepalive.ClientParameters{ + Time: 10 * time.Second, + Timeout: time.Second, + PermitWithoutStream: true, + } +) diff --git a/pkg/hash/hash.go b/pkg/hash/hash.go new file mode 100644 index 0000000..e708b76 --- /dev/null +++ b/pkg/hash/hash.go @@ -0,0 +1,25 @@ +package hash + +var _ Hash = (*hash)(nil) + +type Hash interface { + i() + + // hashids + HashidsEncode(params []int) (string, error) + HashidsDecode(hash string) ([]int, error) +} + +type hash struct { + secret string + length int +} + +func New(secret string, length int) Hash { + return &hash{ + secret: secret, + length: length, + } +} + +func (h *hash) i() {} diff --git a/pkg/hash/hash_hashids.go b/pkg/hash/hash_hashids.go new file mode 100644 index 0000000..e84296f --- /dev/null +++ b/pkg/hash/hash_hashids.go @@ -0,0 +1,41 @@ +package hash + +import ( + "github.com/speps/go-hashids" +) + +func (h *hash) HashidsEncode(params []int) (string, error) { + hd := hashids.NewData() + hd.Salt = h.secret + hd.MinLength = h.length + + hashID, err := hashids.NewWithData(hd) + if err != nil { + return "", err + } + + hashStr, err := hashID.Encode(params) + if err != nil { + return "", err + } + + return hashStr, nil +} + +func (h *hash) HashidsDecode(hash string) ([]int, error) { + hd := hashids.NewData() + hd.Salt = h.secret + hd.MinLength = h.length + + hashID, err := hashids.NewWithData(hd) + if err != nil { + return nil, err + } + + ids, err := hashID.DecodeWithError(hash) + if err != nil { + return nil, err + } + + return ids, nil +} diff --git a/pkg/hmac/hmac.go b/pkg/hmac/hmac.go new file mode 100644 index 0000000..f9d6beb --- /dev/null +++ b/pkg/hmac/hmac.go @@ -0,0 +1,55 @@ +package hmac + +import ( + baseHmac "crypto/hmac" + "crypto/sha1" + "crypto/sha256" + "encoding/base64" + "encoding/hex" +) + +var _ HMAC = (*hmac)(nil) + +type HMAC interface { + i() + + Sha1ToString(data string) string + Sha1ToBase64String(data string) string + + Sha256ToString(data string) string + Sha256ToBase64String(data string) string +} + +type hmac struct { + secret string +} + +func New(secret string) HMAC { + return &hmac{secret: secret} +} + +func (m *hmac) i() {} + +func (m *hmac) Sha256ToString(data string) string { + h := baseHmac.New(sha256.New, []byte(m.secret)) + h.Write([]byte(data)) + return hex.EncodeToString(h.Sum(nil)) +} + +func (m *hmac) Sha256ToBase64String(data string) string { + h := baseHmac.New(sha256.New, []byte(m.secret)) + h.Write([]byte(data)) + return base64.StdEncoding.EncodeToString(h.Sum(nil)) +} + +func (m *hmac) Sha1ToString(data string) string { + h := baseHmac.New(sha1.New, []byte(m.secret)) + h.Write([]byte(data)) + return hex.EncodeToString(h.Sum(nil)) +} + +func (m *hmac) Sha1ToBase64String(data string) string { + h := baseHmac.New(sha1.New, []byte(m.secret)) + h.Write([]byte(data)) + return base64.StdEncoding.EncodeToString(h.Sum(nil)) +} diff --git a/pkg/httpclient/alarm.go b/pkg/httpclient/alarm.go new file mode 100644 index 0000000..8a08eb7 --- /dev/null +++ b/pkg/httpclient/alarm.go @@ -0,0 +1,29 @@ +package httpclient + +import ( + "bufio" + "bytes" + + "go.uber.org/zap" +) + +// Verify parse the body and verify that it is correct +type AlarmVerify func(body []byte) (shouldAlarm bool) + +type AlarmObject interface { + Send(subject, body string) error +} + +func onFailedAlarm(title string, raw []byte, logger *zap.Logger, alarmObject AlarmObject) { + buf := bytes.NewBuffer(nil) + + scanner := bufio.NewScanner(bytes.NewReader(raw)) + for scanner.Scan() { + buf.WriteString(scanner.Text()) + buf.WriteString("
") + } + + if err := alarmObject.Send(title, buf.String()); err != nil && logger != nil { + logger.Error("calls failed alarm err", zap.Error(err)) + } +} diff --git a/pkg/httpclient/client.go b/pkg/httpclient/client.go new file mode 100644 index 0000000..4d3e51b --- /dev/null +++ b/pkg/httpclient/client.go @@ -0,0 +1,395 @@ +package httpclient + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "github.com/spf13/cast" + "net/http" + httpURL "net/url" + "time" + + "git.bvbej.com/bvbej/base-golang/pkg/trace" +) + +const ( + // DefaultTTL 一次http请求最长执行1分钟 + DefaultTTL = time.Minute +) + +// Get 请求 +func Get(url string, form httpURL.Values, options ...Option) (body []byte, err error) { + return withoutBody(http.MethodGet, url, form, options...) +} + +// Delete delete 请求 +func Delete(url string, form httpURL.Values, options ...Option) (body []byte, err error) { + return withoutBody(http.MethodDelete, url, form, options...) +} + +func withoutBody(method, url string, form httpURL.Values, options ...Option) (body []byte, err error) { + if url == "" { + return nil, errors.New("url required") + } + + if len(form) > 0 { + if url, err = addFormValuesIntoURL(url, form); err != nil { + return + } + } + + ts := time.Now() + + opt := getOption() + defer func() { + if opt.trace != nil { + opt.dialog.Success = err == nil + opt.dialog.CostSeconds = time.Since(ts).Seconds() + opt.trace.AppendDialog(opt.dialog) + } + + releaseOption(opt) + }() + + for _, f := range options { + f(opt) + } + opt.header["Content-Type"] = []string{"application/x-www-form-urlencoded; charset=utf-8"} + if opt.trace != nil { + opt.header[trace.Header] = []string{opt.trace.ID()} + } + + ttl := opt.ttl + if ttl <= 0 { + ttl = DefaultTTL + } + + ctx, cancel := context.WithTimeout(context.Background(), ttl) + defer cancel() + + if opt.dialog != nil { + decodedURL, _ := httpURL.QueryUnescape(url) + opt.dialog.Request = &trace.Request{ + TTL: ttl.String(), + Method: method, + DecodedURL: decodedURL, + Header: opt.header, + } + } + + retryTimes := opt.retryTimes + if retryTimes <= 0 { + retryTimes = DefaultRetryTimes + } + + retryDelay := opt.retryDelay + if retryDelay <= 0 { + retryDelay = DefaultRetryDelay + } + + var httpCode int + + defer func() { + if opt.alarmObject == nil { + return + } + + if opt.alarmVerify != nil && !opt.alarmVerify(body) && err == nil { + return + } + + info := &struct { + TraceID string `json:"trace_id"` + Request struct { + Method string `json:"method"` + URL string `json:"url"` + } `json:"request"` + Response struct { + HTTPCode int `json:"http_code"` + Body string `json:"body"` + } `json:"response"` + Error string `json:"error"` + }{} + + if opt.trace != nil { + info.TraceID = opt.trace.ID() + } + info.Request.Method = method + info.Request.URL = url + info.Response.HTTPCode = httpCode + info.Response.Body = string(body) + info.Error = "" + if err != nil { + info.Error = fmt.Sprintf("%+v", err) + } + + raw, _ := json.MarshalIndent(info, "", " ") + onFailedAlarm(opt.alarmTitle, raw, opt.logger, opt.alarmObject) + + }() + + for k := 0; k < retryTimes; k++ { + body, httpCode, err = doHTTP(ctx, method, url, nil, opt) + if shouldRetry(ctx, httpCode) || (opt.retryVerify != nil && opt.retryVerify(body)) { + time.Sleep(retryDelay) + continue + } + + return + } + return +} + +// PostForm post form 请求 +func PostForm(url string, form httpURL.Values, options ...Option) (body []byte, err error) { + return withFormBody(http.MethodPost, url, form, options...) +} + +// PostJSON post json 请求 +func PostJSON(url string, raw json.RawMessage, options ...Option) (body []byte, err error) { + return withJSONBody(http.MethodPost, url, raw, options...) +} + +// PutForm put form 请求 +func PutForm(url string, form httpURL.Values, options ...Option) (body []byte, err error) { + return withFormBody(http.MethodPut, url, form, options...) +} + +// PutJSON put json 请求 +func PutJSON(url string, raw json.RawMessage, options ...Option) (body []byte, err error) { + return withJSONBody(http.MethodPut, url, raw, options...) +} + +// PatchFrom patch form 请求 +func PatchFrom(url string, form httpURL.Values, options ...Option) (body []byte, err error) { + return withFormBody(http.MethodPatch, url, form, options...) +} + +// PatchJSON patch json 请求 +func PatchJSON(url string, raw json.RawMessage, options ...Option) (body []byte, err error) { + return withJSONBody(http.MethodPatch, url, raw, options...) +} + +func withFormBody(method, url string, form httpURL.Values, options ...Option) (body []byte, err error) { + if url == "" { + return nil, errors.New("url required") + } + if len(form) == 0 { + return nil, errors.New("form required") + } + + ts := time.Now() + + opt := getOption() + defer func() { + if opt.trace != nil { + opt.dialog.Success = err == nil + opt.dialog.CostSeconds = time.Since(ts).Seconds() + opt.trace.AppendDialog(opt.dialog) + } + + releaseOption(opt) + }() + + for _, f := range options { + f(opt) + } + opt.header["Content-Type"] = []string{"application/x-www-form-urlencoded; charset=utf-8"} + if opt.trace != nil { + opt.header[trace.Header] = []string{opt.trace.ID()} + } + + ttl := opt.ttl + if ttl <= 0 { + ttl = DefaultTTL + } + + ctx, cancel := context.WithTimeout(context.Background(), ttl) + defer cancel() + + formValue := form.Encode() + if opt.dialog != nil { + decodedURL, _ := httpURL.QueryUnescape(url) + opt.dialog.Request = &trace.Request{ + TTL: ttl.String(), + Method: method, + DecodedURL: decodedURL, + Header: opt.header, + Body: formValue, + } + } + + retryTimes := opt.retryTimes + if retryTimes <= 0 { + retryTimes = DefaultRetryTimes + } + + retryDelay := opt.retryDelay + if retryDelay <= 0 { + retryDelay = DefaultRetryDelay + } + + var httpCode int + + defer func() { + if opt.alarmObject == nil { + return + } + + if opt.alarmVerify != nil && !opt.alarmVerify(body) && err == nil { + return + } + + info := &struct { + TraceID string `json:"trace_id"` + Request struct { + Method string `json:"method"` + URL string `json:"url"` + } `json:"request"` + Response struct { + HTTPCode int `json:"http_code"` + Body string `json:"body"` + } `json:"response"` + Error string `json:"error"` + }{} + + if opt.trace != nil { + info.TraceID = opt.trace.ID() + } + info.Request.Method = method + info.Request.URL = url + info.Response.HTTPCode = httpCode + info.Response.Body = string(body) + info.Error = "" + if err != nil { + info.Error = fmt.Sprintf("%+v", err) + } + + raw, _ := json.MarshalIndent(info, "", " ") + onFailedAlarm(opt.alarmTitle, raw, opt.logger, opt.alarmObject) + + }() + + for k := 0; k < retryTimes; k++ { + body, httpCode, err = doHTTP(ctx, method, url, []byte(formValue), opt) + if shouldRetry(ctx, httpCode) || (opt.retryVerify != nil && opt.retryVerify(body)) { + time.Sleep(retryDelay) + continue + } + + return + } + return +} + +func withJSONBody(method, url string, raw json.RawMessage, options ...Option) (body []byte, err error) { + if url == "" { + return nil, errors.New("url required") + } + if len(raw) == 0 { + return nil, errors.New("raw required") + } + + ts := time.Now() + + opt := getOption() + defer func() { + if opt.trace != nil { + opt.dialog.Success = err == nil + opt.dialog.CostSeconds = time.Since(ts).Seconds() + opt.trace.AppendDialog(opt.dialog) + } + + releaseOption(opt) + }() + + for _, f := range options { + f(opt) + } + opt.header["Content-Type"] = []string{"application/json; charset=utf-8"} + if opt.trace != nil { + opt.header[trace.Header] = []string{opt.trace.ID()} + } + + ttl := opt.ttl + if ttl <= 0 { + ttl = DefaultTTL + } + + ctx, cancel := context.WithTimeout(context.Background(), ttl) + defer cancel() + + if opt.dialog != nil { + decodedURL, _ := httpURL.QueryUnescape(url) + opt.dialog.Request = &trace.Request{ + TTL: ttl.String(), + Method: method, + DecodedURL: decodedURL, + Header: opt.header, + Body: cast.ToString(raw), + } + } + + retryTimes := opt.retryTimes + if retryTimes <= 0 { + retryTimes = DefaultRetryTimes + } + + retryDelay := opt.retryDelay + if retryDelay <= 0 { + retryDelay = DefaultRetryDelay + } + + var httpCode int + + defer func() { + if opt.alarmObject == nil { + return + } + + if opt.alarmVerify != nil && !opt.alarmVerify(body) && err == nil { + return + } + + info := &struct { + TraceID string `json:"trace_id"` + Request struct { + Method string `json:"method"` + URL string `json:"url"` + } `json:"request"` + Response struct { + HTTPCode int `json:"http_code"` + Body string `json:"body"` + } `json:"response"` + Error string `json:"error"` + }{} + + if opt.trace != nil { + info.TraceID = opt.trace.ID() + } + info.Request.Method = method + info.Request.URL = url + info.Response.HTTPCode = httpCode + info.Response.Body = string(body) + info.Error = "" + if err != nil { + info.Error = fmt.Sprintf("%+v", err) + } + + raw, _ := json.MarshalIndent(info, "", " ") + onFailedAlarm(opt.alarmTitle, raw, opt.logger, opt.alarmObject) + + }() + + for k := 0; k < retryTimes; k++ { + body, httpCode, err = doHTTP(ctx, method, url, raw, opt) + if shouldRetry(ctx, httpCode) || (opt.retryVerify != nil && opt.retryVerify(body)) { + time.Sleep(retryDelay) + continue + } + + return + } + return +} diff --git a/pkg/httpclient/error.go b/pkg/httpclient/error.go new file mode 100644 index 0000000..647f1fd --- /dev/null +++ b/pkg/httpclient/error.go @@ -0,0 +1,46 @@ +package httpclient + +var _ ReplyErr = (*replyErr)(nil) + +// ReplyErr 错误响应,当 resp.StatusCode != http.StatusOK 时用来包装返回的 httpcode 和 body 。 +type ReplyErr interface { + error + StatusCode() int + Body() []byte +} + +type replyErr struct { + err error + statusCode int + body []byte +} + +func (r *replyErr) Error() string { + return r.err.Error() +} + +func (r *replyErr) StatusCode() int { + return r.statusCode +} + +func (r *replyErr) Body() []byte { + return r.body +} + +func newReplyErr(statusCode int, body []byte, err error) ReplyErr { + return &replyErr{ + statusCode: statusCode, + body: body, + err: err, + } +} + +// ToReplyErr 尝试将 err 转换为 ReplyErr +func ToReplyErr(err error) (ReplyErr, bool) { + if err == nil { + return nil, false + } + + e, ok := err.(ReplyErr) + return e, ok +} diff --git a/pkg/httpclient/option.go b/pkg/httpclient/option.go new file mode 100644 index 0000000..3153b36 --- /dev/null +++ b/pkg/httpclient/option.go @@ -0,0 +1,138 @@ +package httpclient + +import ( + "sync" + "time" + + "git.bvbej.com/bvbej/base-golang/pkg/trace" + + "go.uber.org/zap" +) + +var ( + cache = &sync.Pool{ + New: func() any { + return &option{ + header: make(map[string][]string), + } + }, + } +) + +// Mock 定义接口Mock数据 +type Mock func() (body []byte) + +// Option 自定义设置http请求 +type Option func(*option) + +type basicAuth struct { + username string + password string +} + +type option struct { + ttl time.Duration + basicAuth *basicAuth + header map[string][]string + trace *trace.Trace + dialog *trace.Dialog + logger *zap.Logger + retryTimes int + retryDelay time.Duration + retryVerify RetryVerify + alarmTitle string + alarmObject AlarmObject + alarmVerify AlarmVerify + mock Mock +} + +func (o *option) reset() { + o.ttl = 0 + o.basicAuth = nil + o.header = make(map[string][]string) + o.trace = nil + o.dialog = nil + o.logger = nil + o.retryTimes = 0 + o.retryDelay = 0 + o.retryVerify = nil + o.alarmTitle = "" + o.alarmObject = nil + o.alarmVerify = nil + o.mock = nil +} + +func getOption() *option { + return cache.Get().(*option) +} + +func releaseOption(opt *option) { + opt.reset() + cache.Put(opt) +} + +// WithTTL 本次http请求最长执行时间 +func WithTTL(ttl time.Duration) Option { + return func(opt *option) { + opt.ttl = ttl + } +} + +// WithHeader 设置http header,可以调用多次设置多对key-value +func WithHeader(key, value string) Option { + return func(opt *option) { + opt.header[key] = []string{value} + } +} + +// WithBasicAuth 设置基础认证权限 +func WithBasicAuth(username, password string) Option { + return func(opt *option) { + opt.basicAuth = &basicAuth{ + username: username, + password: password, + } + } +} + +// WithTrace 设置trace信息 +func WithTrace(t trace.T) Option { + return func(opt *option) { + if t != nil { + opt.trace = t.(*trace.Trace) + opt.dialog = new(trace.Dialog) + } + } +} + +// WithLogger 设置logger以便打印关键日志 +func WithLogger(logger *zap.Logger) Option { + return func(opt *option) { + opt.logger = logger + } +} + +// WithMock 设置 mock 数据 +func WithMock(m Mock) Option { + return func(opt *option) { + opt.mock = m + } +} + +// WithOnFailedAlarm 设置告警通知 +func WithOnFailedAlarm(alarmTitle string, alarmObject AlarmObject, alarmVerify AlarmVerify) Option { + return func(opt *option) { + opt.alarmTitle = alarmTitle + opt.alarmObject = alarmObject + opt.alarmVerify = alarmVerify + } +} + +// WithOnFailedRetry 设置失败重试 +func WithOnFailedRetry(retryTimes int, retryDelay time.Duration, retryVerify RetryVerify) Option { + return func(opt *option) { + opt.retryTimes = retryTimes + opt.retryDelay = retryDelay + opt.retryVerify = retryVerify + } +} diff --git a/pkg/httpclient/retry.go b/pkg/httpclient/retry.go new file mode 100644 index 0000000..4883f00 --- /dev/null +++ b/pkg/httpclient/retry.go @@ -0,0 +1,44 @@ +package httpclient + +import ( + "context" + "net/http" + "time" +) + +const ( + // DefaultRetryTimes 如果请求失败,最多重试3次 + DefaultRetryTimes = 3 + // DefaultRetryDelay 在重试前,延迟等待100毫秒 + DefaultRetryDelay = time.Millisecond * 100 +) + +// Verify parse the body and verify that it is correct +type RetryVerify func(body []byte) (shouldRetry bool) + +func shouldRetry(ctx context.Context, httpCode int) bool { + select { + case <-ctx.Done(): + return false + default: + } + + switch httpCode { + case + _StatusReadRespErr, + _StatusDoReqErr, + + http.StatusRequestTimeout, + http.StatusLocked, + http.StatusTooEarly, + http.StatusTooManyRequests, + + http.StatusServiceUnavailable, + http.StatusGatewayTimeout: + + return true + + default: + return false + } +} diff --git a/pkg/httpclient/util.go b/pkg/httpclient/util.go new file mode 100644 index 0000000..2df2edc --- /dev/null +++ b/pkg/httpclient/util.go @@ -0,0 +1,147 @@ +package httpclient + +import ( + "bytes" + "context" + "crypto/tls" + "errors" + "fmt" + "io" + "net/http" + "net/url" + "time" + + "git.bvbej.com/bvbej/base-golang/pkg/trace" + "go.uber.org/zap" +) + +const ( + // _StatusReadRespErr read resp body err, should re-call doHTTP again. + _StatusReadRespErr = -204 + // _StatusDoReqErr do req err, should re-call doHTTP again. + _StatusDoReqErr = -500 +) + +var defaultClient = &http.Client{ + Transport: &http.Transport{ + DisableKeepAlives: true, + DisableCompression: true, + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: true, + }, + MaxIdleConns: 100, + MaxConnsPerHost: 100, + MaxIdleConnsPerHost: 100, + }, +} + +func doHTTP(ctx context.Context, method, url string, payload []byte, opt *option) ([]byte, int, error) { + ts := time.Now() + + if mock := opt.mock; mock != nil { + if opt.dialog != nil { + opt.dialog.AppendResponse(&trace.Response{ + HttpCode: http.StatusOK, + HttpCodeMsg: http.StatusText(http.StatusOK), + Body: string(mock()), + CostSeconds: time.Since(ts).Seconds(), + }) + } + return mock(), http.StatusOK, nil + } + + req, err := http.NewRequestWithContext(ctx, method, url, bytes.NewReader(payload)) + if err != nil { + return nil, -1, errors.Join(err, fmt.Errorf("new request [%s %s] err", method, url)) + } + + for key, value := range opt.header { + req.Header.Set(key, value[0]) + } + + if opt.basicAuth != nil { + req.SetBasicAuth(opt.basicAuth.username, opt.basicAuth.password) + } + + resp, err := defaultClient.Do(req) + if err != nil { + err = errors.Join(err, fmt.Errorf("do request [%s %s] err", method, url)) + if opt.dialog != nil { + opt.dialog.AppendResponse(&trace.Response{ + Body: err.Error(), + CostSeconds: time.Since(ts).Seconds(), + }) + } + + if opt.logger != nil { + opt.logger.Warn("doHTTP got err", zap.Error(err)) + } + return nil, _StatusDoReqErr, err + } + defer func() { + _ = resp.Body.Close() + }() + + body, err := io.ReadAll(resp.Body) + if err != nil { + err = errors.Join(err, fmt.Errorf("read resp body from [%s %s] err", method, url)) + if opt.dialog != nil { + opt.dialog.AppendResponse(&trace.Response{ + Body: err.Error(), + CostSeconds: time.Since(ts).Seconds(), + }) + } + + if opt.logger != nil { + opt.logger.Warn("doHTTP got err", zap.Error(err)) + } + return nil, _StatusReadRespErr, err + } + + defer func() { + if opt.dialog != nil { + opt.dialog.AppendResponse(&trace.Response{ + Header: resp.Header, + HttpCode: resp.StatusCode, + HttpCodeMsg: resp.Status, + Body: string(body), // unsafe + CostSeconds: time.Since(ts).Seconds(), + }) + } + }() + + if resp.StatusCode != http.StatusOK { + return nil, resp.StatusCode, newReplyErr( + resp.StatusCode, + body, + fmt.Errorf("do [%s %s] return code: %d message: %s", method, url, resp.StatusCode, string(body)), + ) + } + + return body, http.StatusOK, nil +} + +// addFormValuesIntoURL append url.Values into url string +func addFormValuesIntoURL(rawURL string, form url.Values) (string, error) { + if rawURL == "" { + return "", errors.New("rawURL required") + } + if len(form) == 0 { + return "", errors.New("form required") + } + + target, err := url.Parse(rawURL) + if err != nil { + return "", errors.Join(err, fmt.Errorf("parse rawURL `%s` err", rawURL)) + } + + urlValues := target.Query() + for key, values := range form { + for _, value := range values { + urlValues.Add(key, value) + } + } + + target.RawQuery = urlValues.Encode() + return target.String(), nil +} diff --git a/pkg/limiter/rate.go b/pkg/limiter/rate.go new file mode 100644 index 0000000..f35fa65 --- /dev/null +++ b/pkg/limiter/rate.go @@ -0,0 +1,71 @@ +package limiter + +import ( + "git.bvbej.com/bvbej/base-golang/pkg/ticker" + "golang.org/x/time/rate" + "sync" + "time" +) + +var _ RateLimiter = (*rateLimiter)(nil) + +type item struct { + lastTime time.Time + limiter *rate.Limiter +} + +type RateLimiter interface { + set(key string) *rate.Limiter + get(key string) *rate.Limiter + Allow(key string) bool +} + +type rateLimiter struct { + limit rate.Limit + burst int + list *sync.Map + recycle ticker.Ticker +} + +func NewRateLimiter(limit rate.Limit, burst int) RateLimiter { + list := new(sync.Map) + t := ticker.New(time.Minute) + t.Process(func() { + list.Range(func(key, value any) bool { + if value.(*item).lastTime.Before(time.Now().Add(-time.Hour)) { + list.Delete(key) + } + return true + }) + }) + return &rateLimiter{ + list: list, + limit: limit, + recycle: t, + burst: burst, + } +} + +func (i *rateLimiter) set(key string) *rate.Limiter { + store := &item{ + lastTime: time.Now(), + limiter: rate.NewLimiter(i.limit, i.burst), + } + i.list.Store(key, store) + return store.limiter +} + +func (i *rateLimiter) get(key string) *rate.Limiter { + value, ok := i.list.Load(key) + if !ok { + return i.set(key) + } + value.(*item).lastTime = time.Now() + i.list.Store(key, value) + return value.(*item).limiter +} + +func (i *rateLimiter) Allow(key string) bool { + limiter := i.get(key) + return limiter.Allow() +} diff --git a/pkg/lock/lock.go b/pkg/lock/lock.go new file mode 100644 index 0000000..498874c --- /dev/null +++ b/pkg/lock/lock.go @@ -0,0 +1,47 @@ +package lock + +import ( + "sync" + "sync/atomic" +) + +var _ Locker = (*locker)(nil) + +type Locker interface { + condition() bool + Lock() + Unlock() +} + +type locker struct { + lock *sync.Mutex + cond *sync.Cond + v *atomic.Bool +} + +func NewLocker() Locker { + lock := new(sync.Mutex) + return &locker{ + lock: lock, + cond: sync.NewCond(lock), + v: new(atomic.Bool), + } +} + +func (l *locker) condition() bool { + return l.v.Load() +} + +func (l *locker) Lock() { + l.cond.L.Lock() + for l.condition() { + l.cond.Wait() + } + l.v.Store(true) +} + +func (l *locker) Unlock() { + l.v.Store(false) + l.cond.L.Unlock() + l.cond.Signal() +} diff --git a/pkg/logger/logger.go b/pkg/logger/logger.go new file mode 100644 index 0000000..3f3920e --- /dev/null +++ b/pkg/logger/logger.go @@ -0,0 +1,275 @@ +package logger + +import ( + "io" + "os" + "path/filepath" + "time" + + "go.uber.org/zap" + "go.uber.org/zap/zapcore" + "gopkg.in/natefinch/lumberjack.v2" +) + +const ( + // DefaultLevel the default log level + DefaultLevel = zapcore.InfoLevel + + // DefaultTimeLayout the default time layout; + DefaultTimeLayout = time.DateTime +) + +// Option custom setup config +type Option func(*option) + +type option struct { + level zapcore.Level + fields map[string]string + file io.Writer + timeLayout string + disableConsole bool + disableCaller bool +} + +// WithDebugLevel only greater than 'level' will output +func WithDebugLevel() Option { + return func(opt *option) { + opt.level = zapcore.DebugLevel + } +} + +// WithInfoLevel only greater than 'level' will output +func WithInfoLevel() Option { + return func(opt *option) { + opt.level = zapcore.InfoLevel + } +} + +// WithWarnLevel only greater than 'level' will output +func WithWarnLevel() Option { + return func(opt *option) { + opt.level = zapcore.WarnLevel + } +} + +// WithErrorLevel only greater than 'level' will output +func WithErrorLevel() Option { + return func(opt *option) { + opt.level = zapcore.ErrorLevel + } +} + +// WithField add some field(s) to log +func WithField(key, value string) Option { + return func(opt *option) { + opt.fields[key] = value + } +} + +// WithFileP write log to some file +func WithFileP(file string) Option { + dir := filepath.Dir(file) + if err := os.MkdirAll(dir, 0766); err != nil { + panic(err) + } + + f, err := os.OpenFile(file, os.O_CREATE|os.O_APPEND|os.O_RDWR, 0766) + if err != nil { + panic(err) + } + + return func(opt *option) { + opt.file = zapcore.Lock(f) + } +} + +// WithFileRotationP write log to some file with rotation +func WithFileRotationP(file string) Option { + dir := filepath.Dir(file) + if err := os.MkdirAll(dir, 0766); err != nil { + panic(err) + } + + return func(opt *option) { + opt.file = &lumberjack.Logger{ // concurrent-safed + Filename: file, // 文件路径 + MaxSize: 128, // 单个文件最大尺寸,默认单位 M + MaxBackups: 300, // 最多保留 300 个备份 + MaxAge: 30, // 最大时间,默认单位 day + LocalTime: true, // 使用本地时间 + Compress: true, // 是否压缩 disabled by default + } + } +} + +// WithTimeLayout custom time format +func WithTimeLayout(timeLayout string) Option { + return func(opt *option) { + opt.timeLayout = timeLayout + } +} + +// WithDisableConsole WithEnableConsole write log to os.Stdout or os.Stderr +func WithDisableConsole() Option { + return func(opt *option) { + opt.disableConsole = true + } +} + +// WithDisableCaller disable caller field +func WithDisableCaller() Option { + return func(opt *option) { + opt.disableCaller = true + } +} + +// NewJSONLogger return a json-encoder zap logger, +func NewJSONLogger(opts ...Option) (*zap.Logger, error) { + opt := &option{level: DefaultLevel, fields: make(map[string]string)} + for _, f := range opts { + f(opt) + } + + timeLayout := DefaultTimeLayout + if opt.timeLayout != "" { + timeLayout = opt.timeLayout + } + + // similar to zap.NewProductionEncoderConfig() + encoderConfig := zapcore.EncoderConfig{ + TimeKey: "time", + LevelKey: "level", + NameKey: "logger", // used by logger.Named(key); optional; useless + CallerKey: "caller", + MessageKey: "msg", + StacktraceKey: "stacktrace", // use by zap.AddStacktrace; optional; useless + LineEnding: zapcore.DefaultLineEnding, + EncodeLevel: zapcore.LowercaseLevelEncoder, // 小写编码器 + EncodeTime: func(t time.Time, enc zapcore.PrimitiveArrayEncoder) { + enc.AppendString(t.Format(timeLayout)) + }, + EncodeDuration: zapcore.MillisDurationEncoder, + EncodeCaller: zapcore.ShortCallerEncoder, // 全路径编码器 + } + + jsonEncoder := zapcore.NewJSONEncoder(encoderConfig) + + // lowPriority usd by info\debug\warn + lowPriority := zap.LevelEnablerFunc(func(lvl zapcore.Level) bool { + return lvl >= opt.level && lvl < zapcore.ErrorLevel + }) + + // highPriority usd by error\panic\fatal + highPriority := zap.LevelEnablerFunc(func(lvl zapcore.Level) bool { + return lvl >= opt.level && lvl >= zapcore.ErrorLevel + }) + + stdout := zapcore.Lock(os.Stdout) // lock for concurrent safe + stderr := zapcore.Lock(os.Stderr) // lock for concurrent safe + + core := zapcore.NewTee() + + if !opt.disableConsole { + core = zapcore.NewTee( + zapcore.NewCore(jsonEncoder, + zapcore.NewMultiWriteSyncer(stdout), + lowPriority, + ), + zapcore.NewCore(jsonEncoder, + zapcore.NewMultiWriteSyncer(stderr), + highPriority, + ), + ) + } + + if opt.file != nil { + core = zapcore.NewTee(core, + zapcore.NewCore(jsonEncoder, + zapcore.AddSync(opt.file), + zap.LevelEnablerFunc(func(lvl zapcore.Level) bool { + return lvl >= opt.level + }), + ), + ) + } + + logger := zap.New(core, + zap.WithCaller(!opt.disableCaller), + zap.ErrorOutput(stderr), + ) + + for key, value := range opt.fields { + logger = logger.WithOptions(zap.Fields(zapcore.Field{Key: key, Type: zapcore.StringType, String: value})) + } + return logger, nil +} + +var _ Meta = (*meta)(nil) + +// Meta key-value +type Meta interface { + Key() string + Value() any + meta() +} + +type meta struct { + key string + value any +} + +func (m *meta) Key() string { + return m.key +} + +func (m *meta) Value() any { + return m.value +} + +func (m *meta) meta() {} + +// NewMeta create meat +func NewMeta(key string, value any) Meta { + return &meta{key: key, value: value} +} + +// WrapMeta wrap meta to zap fields +func WrapMeta(err error, metas ...Meta) (fields []zap.Field) { + capacity := len(metas) + 1 // namespace meta + if err != nil { + capacity++ + } + + fields = make([]zap.Field, 0, capacity) + if err != nil { + fields = append(fields, zap.Error(err)) + } + + fields = append(fields, zap.Namespace("meta")) + for _, meta := range metas { + fields = append(fields, zap.Any(meta.Key(), meta.Value())) + } + + return +} + +// RestyClientLogger use by resty.Client +func RestyClientLogger(logger *zap.Logger) *RestyClientLog { + return &RestyClientLog{logger: logger} +} + +type RestyClientLog struct { + logger *zap.Logger +} + +func (r *RestyClientLog) Errorf(format string, v ...any) { + r.logger.Sugar().Errorf(format, v) +} + +func (r *RestyClientLog) Warnf(format string, v ...any) { + r.logger.Sugar().Warnf(format, v) +} + +func (r *RestyClientLog) Debugf(format string, v ...any) { + r.logger.Sugar().Debugf(format, v) +} diff --git a/pkg/mail/mail.go b/pkg/mail/mail.go new file mode 100644 index 0000000..08f2959 --- /dev/null +++ b/pkg/mail/mail.go @@ -0,0 +1,36 @@ +package mail + +import ( + "gopkg.in/gomail.v2" +) + +type Options struct { + MailHost string + MailPort int + MailUser string // 发件人 + MailPass string // 发件人密码 + MailTo []string // 多个收件人 + Subject string // 邮件主题 + Body string // 邮件内容 +} + +func Send(o *Options) error { + + m := gomail.NewMessage() + + //设置发件人 + m.SetHeader("From", o.MailUser) + + //设置发送给多个用户 + m.SetHeader("To", o.MailTo...) + + //设置邮件主题 + m.SetHeader("Subject", o.Subject) + + //设置邮件正文 + m.SetBody("text/html", o.Body) + + d := gomail.NewDialer(o.MailHost, o.MailPort, o.MailUser, o.MailPass) + + return d.DialAndSend(m) +} diff --git a/pkg/md5/md5.go b/pkg/md5/md5.go new file mode 100644 index 0000000..04518df --- /dev/null +++ b/pkg/md5/md5.go @@ -0,0 +1,28 @@ +package md5 + +import ( + cryptoMD5 "crypto/md5" + "encoding/hex" +) + +var _ MD5 = (*md5)(nil) + +type MD5 interface { + i() + // Encrypt 加密 + Encrypt(encryptStr string) string +} + +type md5 struct{} + +func New() MD5 { + return &md5{} +} + +func (m *md5) i() {} + +func (m *md5) Encrypt(encryptStr string) string { + s := cryptoMD5.New() + s.Write([]byte(encryptStr)) + return hex.EncodeToString(s.Sum(nil)) +} diff --git a/pkg/metrics/metrics.go b/pkg/metrics/metrics.go new file mode 100644 index 0000000..ebc0a40 --- /dev/null +++ b/pkg/metrics/metrics.go @@ -0,0 +1,51 @@ +package metrics + +import ( + "github.com/prometheus/client_golang/prometheus" + "github.com/spf13/cast" +) + +const ( + namespace = "bvbej" + subsystem = "api" +) + +// metricsRequestsTotal metrics for request total 计数器(Counter) +var metricsRequestsTotal = prometheus.NewCounterVec( + prometheus.CounterOpts{ + Namespace: namespace, + Subsystem: subsystem, + Name: "requests_total", + Help: "request(ms) total", + }, + []string{"method", "path"}, +) + +// metricsRequestsCost metrics for requests cost 累积直方图(Histogram) +var metricsRequestsCost = prometheus.NewHistogramVec( + prometheus.HistogramOpts{ + Namespace: namespace, + Subsystem: subsystem, + Name: "requests_cost", + Help: "request(ms) cost milliseconds", + }, + []string{"method", "path", "success"}, +) + +func init() { + prometheus.MustRegister(metricsRequestsTotal, metricsRequestsCost) +} + +// RecordMetrics 记录指标 +func RecordMetrics(method, uri string, success bool, costSeconds float64) { + metricsRequestsTotal.With(prometheus.Labels{ + "method": method, + "path": uri, + }).Inc() + + metricsRequestsCost.With(prometheus.Labels{ + "method": method, + "path": uri, + "success": cast.ToString(success), + }).Observe(costSeconds) +} diff --git a/pkg/mux/context.go b/pkg/mux/context.go new file mode 100644 index 0000000..db76867 --- /dev/null +++ b/pkg/mux/context.go @@ -0,0 +1,406 @@ +package mux + +import ( + "bytes" + stdCtx "context" + "io" + "net/http" + "net/url" + "strings" + "sync" + + "git.bvbej.com/bvbej/base-golang/pkg/errno" + "git.bvbej.com/bvbej/base-golang/pkg/trace" + + "github.com/gin-gonic/gin" + "github.com/gin-gonic/gin/binding" + "go.uber.org/zap" +) + +type HandlerFunc func(c Context) + +type Trace = trace.T + +const ( + _Alias = "_alias_" + _TraceName = "_trace_" + _LoggerName = "_logger_" + _BodyName = "_body_" + _PayloadName = "_payload_" + _GraphPayloadName = "_graph_payload_" + _AbortErrorName = "_abort_error_" + _Auth = "_auth_" +) + +var contextPool = &sync.Pool{ + New: func() any { + return new(context) + }, +} + +func newContext(ctx *gin.Context) Context { + getContext := contextPool.Get().(*context) + getContext.ctx = ctx + return getContext +} + +func releaseContext(ctx Context) { + c := ctx.(*context) + c.ctx = nil + contextPool.Put(c) +} + +var _ Context = (*context)(nil) + +type Context interface { + init() + + Context() *gin.Context + + // ShouldBindQuery 反序列化 query + // tag: `form:"xxx"` (注:不要写成 query) + ShouldBindQuery(obj any) error + + // ShouldBindPostForm 反序列化 x-www-from-urlencoded + // tag: `form:"xxx"` + ShouldBindPostForm(obj any) error + + // ShouldBindForm 同时反序列化 form-data; + // tag: `form:"xxx"` + ShouldBindForm(obj any) error + + // ShouldBindJSON 反序列化 post-json + // tag: `json:"xxx"` + ShouldBindJSON(obj any) error + + // ShouldBindURI 反序列化 path 参数(如路由路径为 /user/:name) + // tag: `uri:"xxx"` + ShouldBindURI(obj any) error + + // Redirect 重定向 + Redirect(code int, location string) + + // Trace 获取 Trace 对象 + Trace() Trace + setTrace(trace Trace) + disableTrace() + + // Logger 获取 Logger 对象 + Logger() *zap.Logger + setLogger(logger *zap.Logger) + + // Payload 正确返回 + Payload(payload any) + getPayload() any + + // GraphPayload GraphQL返回值 与 api 返回结构不同 + GraphPayload(payload any) + getGraphPayload() any + + // HTML 返回界面 + HTML(name string, obj any) + + // AbortWithError 错误返回 + AbortWithError(err errno.Error) + abortError() errno.Error + + // Header 获取 Header 对象 + Header() http.Header + // GetHeader 获取 Header + GetHeader(key string) string + // SetHeader 设置 Header + SetHeader(key, value string) + + Auth() any + SetAuth(auth any) + + // Authorization 获取请求认证信息 + Authorization() string + + // Platform 平台标识 + Platform() string + + // Alias 设置路由别名 for metrics uri + Alias() string + setAlias(path string) + + // RequestInputParams 获取所有参数 + RequestInputParams() url.Values + // RequestQueryParams 获取 Query 参数 + RequestQueryParams() url.Values + // RequestPostFormParams 获取 PostForm 参数 + RequestPostFormParams() url.Values + // Request 获取 Request 对象 + Request() *http.Request + // RawData 获取 Request.Body + RawData() []byte + // Method 获取 Request.Method + Method() string + // Host 获取 Request.Host + Host() string + // Path 获取 请求的路径 Request.URL.Path (不附带 querystring) + Path() string + // URI 获取 unescape 后的 Request.URL.RequestURI() + URI() string + // RequestContext 获取请求的 context (当 client 关闭后,会自动 canceled) + RequestContext() StdContext + // ResponseWriter 获取 ResponseWriter 对象 + ResponseWriter() gin.ResponseWriter +} + +type context struct { + ctx *gin.Context +} + +type StdContext struct { + stdCtx.Context + Trace + *zap.Logger +} + +func (c *context) init() { + body, err := c.ctx.GetRawData() + if err != nil { + panic(err) + } + + c.ctx.Set(_BodyName, body) // cache body是为了trace使用 + c.ctx.Request.Body = io.NopCloser(bytes.NewBuffer(body)) // re-construct req body +} + +func (c *context) Context() *gin.Context { + return c.ctx +} + +// ShouldBindQuery 反序列化querystring +// tag: `form:"xxx"` (注:不要写成query) +func (c *context) ShouldBindQuery(obj any) error { + return c.ctx.ShouldBindWith(obj, binding.Query) +} + +// ShouldBindPostForm 反序列化 postform (querystring 会被忽略) +// tag: `form:"xxx"` +func (c *context) ShouldBindPostForm(obj any) error { + return c.ctx.ShouldBindWith(obj, binding.FormPost) +} + +// ShouldBindForm 同时反序列化querystring和postform; +// 当querystring和postform存在相同字段时,postform优先使用。 +// tag: `form:"xxx"` +func (c *context) ShouldBindForm(obj any) error { + return c.ctx.ShouldBindWith(obj, binding.Form) +} + +// ShouldBindJSON 反序列化postjson +// tag: `json:"xxx"` +func (c *context) ShouldBindJSON(obj any) error { + return c.ctx.ShouldBindWith(obj, binding.JSON) +} + +// ShouldBindURI 反序列化path参数(如路由路径为 /user/:name) +// tag: `uri:"xxx"` +func (c *context) ShouldBindURI(obj any) error { + return c.ctx.ShouldBindUri(obj) +} + +// Redirect 重定向 +func (c *context) Redirect(code int, location string) { + c.ctx.Redirect(code, location) +} + +func (c *context) Trace() Trace { + t, ok := c.ctx.Get(_TraceName) + if !ok || t == nil { + return nil + } + + return t.(Trace) +} + +func (c *context) setTrace(trace Trace) { + c.ctx.Set(_TraceName, trace) +} + +func (c *context) disableTrace() { + c.setTrace(nil) +} + +func (c *context) Logger() *zap.Logger { + logger, ok := c.ctx.Get(_LoggerName) + if !ok { + return nil + } + + return logger.(*zap.Logger) +} + +func (c *context) setLogger(logger *zap.Logger) { + c.ctx.Set(_LoggerName, logger) +} + +func (c *context) getPayload() any { + if payload, ok := c.ctx.Get(_PayloadName); ok != false { + return payload + } + return nil +} + +func (c *context) Payload(payload any) { + c.ctx.Set(_PayloadName, payload) +} + +func (c *context) getGraphPayload() any { + if payload, ok := c.ctx.Get(_GraphPayloadName); ok != false { + return payload + } + return nil +} + +func (c *context) GraphPayload(payload any) { + c.ctx.Set(_GraphPayloadName, payload) +} + +func (c *context) HTML(name string, obj any) { + c.ctx.HTML(200, name+".html", obj) +} + +func (c *context) Header() http.Header { + header := c.ctx.Request.Header + + clone := make(http.Header, len(header)) + for k, v := range header { + value := make([]string, len(v)) + copy(value, v) + + clone[k] = value + } + return clone +} + +func (c *context) GetHeader(key string) string { + return c.ctx.GetHeader(key) +} + +func (c *context) SetHeader(key, value string) { + c.ctx.Header(key, value) +} + +func (c *context) Auth() any { + val, ok := c.ctx.Get(_Auth) + if !ok { + return nil + } + return val +} + +func (c *context) SetAuth(auth any) { + c.ctx.Set(_Auth, auth) +} + +func (c *context) Authorization() string { + return c.ctx.GetHeader("Authorization") +} + +func (c *context) Platform() string { + return c.ctx.GetHeader("X-Platform") +} + +func (c *context) AbortWithError(err errno.Error) { + if err != nil { + httpCode := err.GetHttpCode() + if httpCode == 0 { + httpCode = http.StatusInternalServerError + } + + c.ctx.AbortWithStatus(httpCode) + c.ctx.Set(_AbortErrorName, err) + } +} + +func (c *context) abortError() errno.Error { + err, _ := c.ctx.Get(_AbortErrorName) + return err.(errno.Error) +} + +func (c *context) Alias() string { + path, ok := c.ctx.Get(_Alias) + if !ok { + return "" + } + + return path.(string) +} + +func (c *context) setAlias(path string) { + if path = strings.TrimSpace(path); path != "" { + c.ctx.Set(_Alias, path) + } +} + +// RequestInputParams 获取所有参数 +func (c *context) RequestInputParams() url.Values { + _ = c.ctx.Request.ParseForm() + return c.ctx.Request.Form +} + +// RequestQueryParams 获取Query参数 +func (c *context) RequestQueryParams() url.Values { + query, _ := url.ParseQuery(c.ctx.Request.URL.RawQuery) + return query +} + +// RequestPostFormParams 获取 PostForm 参数 +func (c *context) RequestPostFormParams() url.Values { + _ = c.ctx.Request.ParseForm() + return c.ctx.Request.PostForm +} + +// Request 获取 Request +func (c *context) Request() *http.Request { + return c.ctx.Request +} + +func (c *context) RawData() []byte { + body, ok := c.ctx.Get(_BodyName) + if !ok { + return nil + } + + return body.([]byte) +} + +// Method 请求的method +func (c *context) Method() string { + return c.ctx.Request.Method +} + +// Host 请求的host +func (c *context) Host() string { + return c.ctx.Request.Host +} + +// Path 请求的路径(不附带querystring) +func (c *context) Path() string { + return c.ctx.Request.URL.Path +} + +// URI unescape后的uri +func (c *context) URI() string { + uri, _ := url.QueryUnescape(c.ctx.Request.URL.RequestURI()) + return uri +} + +// RequestContext (包装 Trace + Logger) 获取请求的 context (当client关闭后,会自动canceled) +func (c *context) RequestContext() StdContext { + return StdContext{ + //c.ctx.Request.Context(), + stdCtx.Background(), + c.Trace(), + c.Logger(), + } +} + +// ResponseWriter 获取 ResponseWriter +func (c *context) ResponseWriter() gin.ResponseWriter { + return c.ctx.Writer +} diff --git a/pkg/mux/core.go b/pkg/mux/core.go new file mode 100644 index 0000000..9fb8927 --- /dev/null +++ b/pkg/mux/core.go @@ -0,0 +1,466 @@ +package mux + +import ( + "errors" + "fmt" + "net/http" + "net/url" + "runtime/debug" + "time" + + "git.bvbej.com/bvbej/base-golang/pkg/color" + "git.bvbej.com/bvbej/base-golang/pkg/env" + "git.bvbej.com/bvbej/base-golang/pkg/errno" + "git.bvbej.com/bvbej/base-golang/pkg/limiter" + "git.bvbej.com/bvbej/base-golang/pkg/trace" + "git.bvbej.com/bvbej/base-golang/pkg/validator" + "github.com/gin-contrib/pprof" + "github.com/gin-gonic/gin" + "github.com/gin-gonic/gin/binding" + "github.com/prometheus/client_golang/prometheus/promhttp" + cors "github.com/rs/cors/wrapper/gin" + "go.uber.org/multierr" + "go.uber.org/zap" + "golang.org/x/time/rate" +) + +type Option func(*option) + +type option struct { + enableCors bool + enablePProf bool + enablePrometheus bool + enableOpenBrowser string + staticDirs []string + panicNotify OnPanicNotify + recordMetrics RecordMetrics + rateLimiter limiter.RateLimiter +} + +const SuccessCode = 0 + +type Failure struct { + ResultCode int `json:"result_code"` // 业务码 + ResultInfo string `json:"result_info"` // 描述信息 + +} + +type Success struct { + ResultCode int `json:"result_code"` // 业务码 + ResultData any `json:"result_data"` //返回数据 +} + +/******************************************************************************/ + +// OnPanicNotify 发生panic时通知用 +type OnPanicNotify func(ctx Context, err any, stackInfo string) + +// RecordMetrics 记录prometheus指标用 +// 如果使用AliasForRecordMetrics配置了别名,uri将被替换为别名。 +type RecordMetrics func(method, uri string, success bool, costSeconds float64) + +// DisableTrace 禁用追踪链 +func DisableTrace(ctx Context) { + ctx.disableTrace() +} + +// WithPanicNotify 设置panic时的通知回调 +func WithPanicNotify(notify OnPanicNotify) Option { + return func(opt *option) { + opt.panicNotify = notify + fmt.Println(color.Green("* [register panic notify]")) + } +} + +// WithRecordMetrics 设置记录prometheus记录指标回调 +func WithRecordMetrics(record RecordMetrics) Option { + return func(opt *option) { + opt.recordMetrics = record + } +} + +// WithEnableCors 开启CORS +func WithEnableCors() Option { + return func(opt *option) { + opt.enableCors = true + fmt.Println(color.Green("* [register cors]")) + } +} + +// WithEnableRate 开启限流 +func WithEnableRate(limit rate.Limit, burst int) Option { + return func(opt *option) { + opt.rateLimiter = limiter.NewRateLimiter(limit, burst) + fmt.Println(color.Green("* [register rate]")) + } +} + +// WithStaticDir 设置静态文件目录 +func WithStaticDir(dirs []string) Option { + return func(opt *option) { + opt.staticDirs = dirs + fmt.Println(color.Green("* [register rate]")) + } +} + +// AliasForRecordMetrics 对请求uri起个别名,用于prometheus记录指标。 +// 如:Get /user/:username 这样的uri,因为username会有非常多的情况,这样记录prometheus指标会非常的不有好。 +func AliasForRecordMetrics(path string) HandlerFunc { + return func(ctx Context) { + ctx.setAlias(path) + } +} + +/******************************************************************************/ + +// RouterGroup 包装gin的RouterGroup +type RouterGroup interface { + Group(string, ...HandlerFunc) RouterGroup + IRoutes +} + +var _ IRoutes = (*router)(nil) + +// IRoutes 包装gin的IRoutes +type IRoutes interface { + Any(string, ...HandlerFunc) + GET(string, ...HandlerFunc) + POST(string, ...HandlerFunc) + DELETE(string, ...HandlerFunc) + PATCH(string, ...HandlerFunc) + PUT(string, ...HandlerFunc) + OPTIONS(string, ...HandlerFunc) + HEAD(string, ...HandlerFunc) +} + +type router struct { + group *gin.RouterGroup +} + +func (r *router) Group(relativePath string, handlers ...HandlerFunc) RouterGroup { + group := r.group.Group(relativePath, wrapHandlers(handlers...)...) + return &router{group: group} +} + +func (r *router) Any(relativePath string, handlers ...HandlerFunc) { + r.group.Any(relativePath, wrapHandlers(handlers...)...) +} + +func (r *router) GET(relativePath string, handlers ...HandlerFunc) { + r.group.GET(relativePath, wrapHandlers(handlers...)...) +} + +func (r *router) POST(relativePath string, handlers ...HandlerFunc) { + r.group.POST(relativePath, wrapHandlers(handlers...)...) +} + +func (r *router) DELETE(relativePath string, handlers ...HandlerFunc) { + r.group.DELETE(relativePath, wrapHandlers(handlers...)...) +} + +func (r *router) PATCH(relativePath string, handlers ...HandlerFunc) { + r.group.PATCH(relativePath, wrapHandlers(handlers...)...) +} + +func (r *router) PUT(relativePath string, handlers ...HandlerFunc) { + r.group.PUT(relativePath, wrapHandlers(handlers...)...) +} + +func (r *router) OPTIONS(relativePath string, handlers ...HandlerFunc) { + r.group.OPTIONS(relativePath, wrapHandlers(handlers...)...) +} + +func (r *router) HEAD(relativePath string, handlers ...HandlerFunc) { + r.group.HEAD(relativePath, wrapHandlers(handlers...)...) +} + +func wrapHandlers(handlers ...HandlerFunc) []gin.HandlerFunc { + list := make([]gin.HandlerFunc, len(handlers)) + for i, handler := range handlers { + fn := handler + list[i] = func(c *gin.Context) { + ctx := newContext(c) + defer releaseContext(ctx) + + fn(ctx) + } + } + return list +} + +/******************************************************************************/ + +var _ Mux = (*mux)(nil) + +type Mux interface { + http.Handler + Group(relativePath string, handlers ...HandlerFunc) RouterGroup + Routes() gin.RoutesInfo + HandlerFunc(relativePath string, handlerFunc gin.HandlerFunc) +} + +type mux struct { + engine *gin.Engine +} + +func (m *mux) ServeHTTP(w http.ResponseWriter, req *http.Request) { + m.engine.ServeHTTP(w, req) +} + +func (m *mux) Group(relativePath string, handlers ...HandlerFunc) RouterGroup { + return &router{ + group: m.engine.Group(relativePath, wrapHandlers(handlers...)...), + } +} + +func (m *mux) Routes() gin.RoutesInfo { + return m.engine.Routes() +} + +func (m *mux) HandlerFunc(relativePath string, handlerFunc gin.HandlerFunc) { + m.engine.GET(relativePath, handlerFunc) +} + +func New(logger *zap.Logger, options ...Option) (Mux, error) { + if logger == nil { + return nil, errors.New("logger required") + } + + gin.SetMode(gin.ReleaseMode) + binding.Validator = validator.Validator + newMux := &mux{ + engine: gin.New(), + } + + fmt.Println(color.Green(fmt.Sprintf("* [register env %s]", env.Active().Value()))) + + // withoutLogPaths 这些请求,默认不记录日志 + withoutTracePaths := map[string]bool{ + "/metrics": true, + "/favicon.ico": true, + "/system/health": true, + } + + opt := new(option) + for _, f := range options { + f(opt) + } + + if opt.enablePProf { + pprof.Register(newMux.engine) + fmt.Println(color.Green("* [register pprof]")) + } + + if opt.enablePrometheus { + newMux.engine.GET("/metrics", gin.WrapH(promhttp.Handler())) + fmt.Println(color.Green("* [register prometheus]")) + } + + if opt.enableCors { + newMux.engine.Use(cors.AllowAll()) + } + + if opt.staticDirs != nil { + for _, dir := range opt.staticDirs { + newMux.engine.StaticFS(dir, gin.Dir(dir, false)) + } + } + + // recover两次,防止处理时发生panic,尤其是在OnPanicNotify中。 + newMux.engine.Use(func(ctx *gin.Context) { + defer func() { + if err := recover(); err != nil { + logger.Error("got panic", zap.String("panic", fmt.Sprintf("%+v", err)), zap.String("stack", string(debug.Stack()))) + } + }() + + ctx.Next() + }) + + newMux.engine.Use(func(ctx *gin.Context) { + ts := time.Now() + + newCtx := newContext(ctx) + defer releaseContext(newCtx) + + newCtx.init() + newCtx.setLogger(logger) + + if !withoutTracePaths[ctx.Request.URL.Path] { + if traceId := newCtx.GetHeader(trace.Header); traceId != "" { + newCtx.setTrace(trace.New(traceId)) + } else { + newCtx.setTrace(trace.New("")) + } + } + + defer func() { + if err := recover(); err != nil { + stackInfo := string(debug.Stack()) + logger.Error("got panic", zap.String("panic", fmt.Sprintf("%+v", err)), zap.String("stack", stackInfo)) + newCtx.AbortWithError(errno.NewError( + http.StatusInternalServerError, + http.StatusInternalServerError, + http.StatusText(http.StatusInternalServerError)), + ) + + if notify := opt.panicNotify; notify != nil { + notify(newCtx, err, stackInfo) + } + } + + if ctx.Writer.Status() == http.StatusNotFound { + return + } + + var ( + response any + businessCode int + businessCodeMsg string + abortErr error + graphResponse any + ) + + if ctx.IsAborted() { + for i := range ctx.Errors { // gin error + multierr.AppendInto(&abortErr, ctx.Errors[i]) + } + + if err := newCtx.abortError(); err != nil { // customer err + multierr.AppendInto(&abortErr, err.GetErr()) + response = err + businessCode = err.GetBusinessCode() + businessCodeMsg = err.GetMsg() + + if x := newCtx.Trace(); x != nil { + newCtx.SetHeader(trace.Header, x.ID()) + } + + ctx.JSON(err.GetHttpCode(), &Failure{ + ResultCode: businessCode, + ResultInfo: businessCodeMsg, + }) + } + } else { + response = newCtx.getPayload() + if response != nil { + if x := newCtx.Trace(); x != nil { + newCtx.SetHeader(trace.Header, x.ID()) + } + ctx.JSON(http.StatusOK, response) + } + } + + graphResponse = newCtx.getGraphPayload() + + if opt.recordMetrics != nil { + uri := newCtx.Path() + if alias := newCtx.Alias(); alias != "" { + uri = alias + } + + opt.recordMetrics( + newCtx.Method(), + uri, + !ctx.IsAborted() && ctx.Writer.Status() == http.StatusOK, + time.Since(ts).Seconds(), + ) + } + + var t *trace.Trace + if x := newCtx.Trace(); x != nil { + t = x.(*trace.Trace) + } else { + return + } + + decodedURL, _ := url.QueryUnescape(ctx.Request.URL.RequestURI()) + + t.WithRequest(&trace.Request{ + TTL: "un-limit", + Method: ctx.Request.Method, + DecodedURL: decodedURL, + Header: ctx.Request.Header, + Body: string(newCtx.RawData()), + }) + + var responseBody any + + if response != nil { + responseBody = response + } + + if graphResponse != nil { + responseBody = graphResponse + } + + t.WithResponse(&trace.Response{ + Header: ctx.Writer.Header(), + HttpCode: ctx.Writer.Status(), + HttpCodeMsg: http.StatusText(ctx.Writer.Status()), + BusinessCode: businessCode, + BusinessCodeMsg: businessCodeMsg, + Body: responseBody, + CostSeconds: time.Since(ts).Seconds(), + }) + + t.Success = !ctx.IsAborted() && ctx.Writer.Status() == http.StatusOK + t.CostSeconds = time.Since(ts).Seconds() + + logger.Info("core-interceptor", + zap.Any("method", ctx.Request.Method), + zap.Any("path", decodedURL), + zap.Any("http_code", ctx.Writer.Status()), + zap.Any("business_code", businessCode), + zap.Any("success", t.Success), + zap.Any("cost_seconds", t.CostSeconds), + zap.Any("trace_id", t.Identifier), + zap.Any("trace_info", t), + zap.Error(abortErr), + ) + }() + + ctx.Next() + }) + + if opt.rateLimiter != nil { + newMux.engine.Use(func(ctx *gin.Context) { + newCtx := newContext(ctx) + defer releaseContext(newCtx) + + if !opt.rateLimiter.Allow(ctx.ClientIP()) { + newCtx.AbortWithError(errno.NewError( + http.StatusTooManyRequests, + http.StatusTooManyRequests, + http.StatusText(http.StatusTooManyRequests)), + ) + return + } + + ctx.Next() + }) + } + + newMux.engine.NoMethod(wrapHandlers(DisableTrace)...) + newMux.engine.NoRoute(wrapHandlers(DisableTrace)...) + system := newMux.Group("/system") + { + // 健康检查 + system.GET("/health", func(ctx Context) { + resp := &struct { + Timestamp time.Time `json:"timestamp"` + Environment string `json:"environment"` + Host string `json:"host"` + Status string `json:"status"` + }{ + Timestamp: time.Now(), + Environment: env.Active().Value(), + Host: ctx.Host(), + Status: "ok", + } + ctx.Payload(resp) + }) + } + + return newMux, nil +} diff --git a/pkg/observable/iterable.go b/pkg/observable/iterable.go new file mode 100644 index 0000000..2ac38b4 --- /dev/null +++ b/pkg/observable/iterable.go @@ -0,0 +1,3 @@ +package observable + +type Iterable <-chan any diff --git a/pkg/observable/observable.go b/pkg/observable/observable.go new file mode 100644 index 0000000..bf0a1e9 --- /dev/null +++ b/pkg/observable/observable.go @@ -0,0 +1,67 @@ +package observable + +// Ref: github.com/Dreamacro/clash/common/observable + +import ( + "errors" + "sync" +) + +type Observable struct { + iterable Iterable + listener map[Subscription]*Subscriber + mux sync.Mutex + done bool +} + +func (o *Observable) process() { + for item := range o.iterable { + o.mux.Lock() + for _, sub := range o.listener { + sub.Emit(item) + } + o.mux.Unlock() + } + o.close() +} + +func (o *Observable) close() { + o.mux.Lock() + defer o.mux.Unlock() + + o.done = true + for _, sub := range o.listener { + sub.Close() + } +} + +func (o *Observable) Subscribe() (Subscription, error) { + o.mux.Lock() + defer o.mux.Unlock() + if o.done { + return nil, errors.New("observable is closed") + } + subscriber := newSubscriber() + o.listener[subscriber.Out()] = subscriber + return subscriber.Out(), nil +} + +func (o *Observable) UnSubscribe(sub Subscription) { + o.mux.Lock() + defer o.mux.Unlock() + subscriber, exist := o.listener[sub] + if !exist { + return + } + delete(o.listener, sub) + subscriber.Close() +} + +func NewObservable(any Iterable) *Observable { + observable := &Observable{ + iterable: any, + listener: map[Subscription]*Subscriber{}, + } + go observable.process() + return observable +} diff --git a/pkg/observable/subscriber.go b/pkg/observable/subscriber.go new file mode 100644 index 0000000..0d8559b --- /dev/null +++ b/pkg/observable/subscriber.go @@ -0,0 +1,33 @@ +package observable + +import ( + "sync" +) + +type Subscription <-chan any + +type Subscriber struct { + buffer chan any + once sync.Once +} + +func (s *Subscriber) Emit(item any) { + s.buffer <- item +} + +func (s *Subscriber) Out() Subscription { + return s.buffer +} + +func (s *Subscriber) Close() { + s.once.Do(func() { + close(s.buffer) + }) +} + +func newSubscriber() *Subscriber { + sub := &Subscriber{ + buffer: make(chan any, 200), + } + return sub +} diff --git a/pkg/p/print.go b/pkg/p/print.go new file mode 100644 index 0000000..273884b --- /dev/null +++ b/pkg/p/print.go @@ -0,0 +1,50 @@ +package p + +import ( + "fmt" + "time" + + "git.bvbej.com/bvbej/base-golang/pkg/trace" +) + +type Option func(*option) + +type Trace = trace.T + +type option struct { + Trace *trace.Trace + Debug *trace.Debug +} + +func newOption() *option { + return &option{} +} + +func Println(key string, value any, options ...Option) { + ts := time.Now() + opt := newOption() + defer func() { + if opt.Trace != nil { + opt.Debug.Key = key + opt.Debug.Value = value + opt.Debug.CostSeconds = time.Since(ts).Seconds() + opt.Trace.AppendDebug(opt.Debug) + } + }() + + for _, f := range options { + f(opt) + } + + fmt.Println(fmt.Sprintf("KEY: %s | VALUE: %v", key, value)) +} + +// WithTrace 设置trace信息 +func WithTrace(t Trace) Option { + return func(opt *option) { + if t != nil { + opt.Trace = t.(*trace.Trace) + opt.Debug = new(trace.Debug) + } + } +} diff --git a/pkg/proxy/io.go b/pkg/proxy/io.go new file mode 100644 index 0000000..efd5d95 --- /dev/null +++ b/pkg/proxy/io.go @@ -0,0 +1,142 @@ +package proxy + +import ( + "context" + "golang.org/x/time/rate" + "io" + "net" + "runtime/debug" + "sync" + "time" +) + +const burstLimit = 1000 * 1000 * 1000 + +type Reader struct { + r io.Reader + limiter *rate.Limiter + ctx context.Context +} + +func NewReader(r io.Reader) *Reader { + return &Reader{ + r: r, + ctx: context.Background(), + } +} + +func (s *Reader) SetRateLimit(bytesPerSec float64) { + s.limiter = rate.NewLimiter(rate.Limit(bytesPerSec), burstLimit) + s.limiter.AllowN(time.Now(), burstLimit) // spend initial burst +} + +func (s *Reader) Read(p []byte) (int, error) { + if s.limiter == nil { + return s.r.Read(p) + } + n, err := s.r.Read(p) + if err != nil { + return n, err + } + if err := s.limiter.WaitN(s.ctx, n); err != nil { + return n, err + } + return n, nil +} + +func ConnectHost(hostAndPort string, timeout int) (conn net.Conn, err error) { + conn, err = net.DialTimeout("tcp", hostAndPort, time.Duration(timeout)*time.Millisecond) + return +} + +func CloseConn(conn net.Conn) { + if conn != nil { + _ = conn.SetDeadline(time.Now().Add(time.Millisecond)) + _ = conn.Close() + } +} + +func IoBind(dst io.ReadWriter, src io.ReadWriter, fn func(isSrcErr bool, err error), cfn func(count int, isPositive bool), bytesPreSec float64) { + var one = &sync.Once{} + go func() { + defer func() { + if e := recover(); e != nil { + logger.Sugar().Errorf("IoBind crashed , err : %s , \ntrace:%s", e, string(debug.Stack())) + } + }() + var err error + var isSrcErr bool + if bytesPreSec > 0 { + newReader := NewReader(src) + newReader.SetRateLimit(bytesPreSec) + _, isSrcErr, err = IoCopy(dst, newReader, func(c int) { + cfn(c, false) + }) + + } else { + _, isSrcErr, err = IoCopy(dst, src, func(c int) { + cfn(c, false) + }) + } + if err != nil { + one.Do(func() { + fn(isSrcErr, err) + }) + } + }() + go func() { + defer func() { + if e := recover(); e != nil { + logger.Sugar().Errorf("IoBind crashed , err : %s , \ntrace:%s", e, string(debug.Stack())) + } + }() + var err error + var isSrcErr bool + if bytesPreSec > 0 { + newReader := NewReader(dst) + newReader.SetRateLimit(bytesPreSec) + _, isSrcErr, err = IoCopy(src, newReader, func(c int) { + cfn(c, true) + }) + } else { + _, isSrcErr, err = IoCopy(src, dst, func(c int) { + cfn(c, true) + }) + } + if err != nil { + one.Do(func() { + fn(isSrcErr, err) + }) + } + }() +} + +func IoCopy(dst io.Writer, src io.Reader, fn ...func(count int)) (written int64, isSrcErr bool, err error) { + buf := make([]byte, 32*1024) + for { + nr, er := src.Read(buf) + if nr > 0 { + nw, ew := dst.Write(buf[0:nr]) + if nw > 0 { + written += int64(nw) + if len(fn) == 1 { + fn[0](nw) + } + } + if ew != nil { + err = ew + break + } + if nr != nw { + err = io.ErrShortWrite + break + } + } + if er != nil { + err = er + isSrcErr = true + break + } + } + return written, isSrcErr, err +} diff --git a/pkg/proxy/service.go b/pkg/proxy/service.go new file mode 100644 index 0000000..e015341 --- /dev/null +++ b/pkg/proxy/service.go @@ -0,0 +1,105 @@ +package proxy + +import ( + "fmt" + "go.uber.org/zap" + "net" + "runtime/debug" + "sync" +) + +var ( + servicesMap = new(sync.Map) + logger *zap.Logger +) + +type Service struct { + TCPConn TCP + Name string +} + +func (s *Service) Stop() { + servicesMap.Delete(s.Name) + s.TCPConn.Close() +} + +func Run(name string, args TCPArgs, zapLogger *zap.Logger) *Service { + logger = zapLogger + service := &Service{ + TCPConn: &tcp{cfg: args}, + Name: name, + } + store, loaded := servicesMap.LoadOrStore(name, service) + if loaded { + service = store.(*Service) + } + + go func() { + defer func() { + recoverErr := recover() + if recoverErr != nil { + logger.Sugar().Errorf("%s servcie crashed, ERR: %s\ntrace:%s", name, recoverErr, string(debug.Stack())) + } + }() + startErr := service.TCPConn.Start() + if startErr != nil { + logger.Sugar().Errorf("%s servcie fail, ERR: %s", name, startErr) + } + }() + + return service +} + +/////////////////////////////////////////////////////////////////////////// + +type Listener struct { + ip string + port int + Listener net.Listener + errAcceptHandler func(err error) +} + +func NewListener(ip string, port int) Listener { + return Listener{ + ip: ip, + port: port, + errAcceptHandler: func(err error) { + logger.Sugar().Errorf("accept error , ERR:%s", err) + }, + } +} + +func (sc *Listener) ListenTCP(fn func(conn net.Conn)) (err error) { + sc.Listener, err = net.Listen("tcp", fmt.Sprintf("%s:%d", sc.ip, sc.port)) + if err == nil { + go func() { + defer func() { + if e := recover(); e != nil { + logger.Sugar().Infof("ListenTCP crashed , err : %s , \ntrace:%s", e, string(debug.Stack())) + } + }() + for { + var conn net.Conn + conn, err = sc.Listener.Accept() + if err == nil { + go func() { + defer func() { + if e := recover(); e != nil { + logger.Sugar().Infof("connection handler crashed , err : %s , \ntrace:%s", e, string(debug.Stack())) + } + }() + fn(conn) + }() + } else { + sc.errAcceptHandler(err) + break + } + } + }() + } + return +} + +func (sc *Listener) CloseListen() error { + return sc.Listener.Close() +} diff --git a/pkg/proxy/tcp.go b/pkg/proxy/tcp.go new file mode 100644 index 0000000..54b4860 --- /dev/null +++ b/pkg/proxy/tcp.go @@ -0,0 +1,94 @@ +package proxy + +import ( + "net" + "strconv" +) + +var _ TCP = (*tcp)(nil) + +type TCPArgs struct { + Local string //监听地址 + Parent string //被代理地址 + Timeout int //拨号超时(毫秒) + OutCallback func() bool //回调 //是否允许代理 +} + +type TCP interface { + Start() (err error) + Close() + + callback(inConn net.Conn) + outToTCP(inConn net.Conn) (err error) +} + +type tcp struct { + inConn net.Conn + outConn net.Conn + listen Listener + cfg TCPArgs +} + +func (s *tcp) Start() (err error) { + host, port, _ := net.SplitHostPort(s.cfg.Local) + p, _ := strconv.Atoi(port) + s.listen = NewListener(host, p) + err = s.listen.ListenTCP(s.callback) + if err != nil { + return + } + return +} + +func (s *tcp) Close() { + if s.inConn != nil { + CloseConn(s.inConn) + } + if s.outConn != nil { + CloseConn(s.outConn) + } + _ = s.listen.CloseListen() +} + +func (s *tcp) callback(inConn net.Conn) { + defer func() { + if err := recover(); err != nil { + logger.Sugar().Infof("conn handler crashed with err : %s", err) + } + }() + + if s.cfg.OutCallback != nil { + if !s.cfg.OutCallback() { + CloseConn(inConn) + return + } + } + + err := s.outToTCP(inConn) + if err != nil { + CloseConn(inConn) + } + + s.inConn = inConn +} + +func (s *tcp) outToTCP(inConn net.Conn) error { + outConn, err := ConnectHost(s.cfg.Parent, s.cfg.Timeout) + if err != nil { + return err + } + + inAddr := inConn.RemoteAddr().String() + inLocalAddr := inConn.LocalAddr().String() + outAddr := outConn.RemoteAddr().String() + outLocalAddr := outConn.LocalAddr().String() + IoBind(inConn, outConn, func(isSrcErr bool, err error) { + CloseConn(inConn) + CloseConn(outConn) + logger.Sugar().Infof("conn %s - %s - %s -%s released", inAddr, inLocalAddr, outLocalAddr, outAddr) + }, func(n int, d bool) {}, 0) + + logger.Sugar().Infof("conn %s - %s - %s -%s connected", inAddr, inLocalAddr, outLocalAddr, outAddr) + + return nil +} diff --git a/pkg/qiniu/storage.go b/pkg/qiniu/storage.go new file mode 100644 index 0000000..9868979 --- /dev/null +++ b/pkg/qiniu/storage.go @@ -0,0 +1,236 @@ +package qiniu + +import ( + "context" + "errors" + "fmt" + "git.bvbej.com/bvbej/base-golang/pkg/md5" + "git.bvbej.com/bvbej/base-golang/tool" + "github.com/qiniu/go-sdk/v7/auth/qbox" + "github.com/qiniu/go-sdk/v7/storage" + "github.com/tidwall/gjson" + "io" + "net/http" + "net/url" + "path" + "strings" + "time" +) + +var _ QiNiu = (*qiNiu)(nil) + +type QiNiu interface { + i() + SetDefaultUploadTokenTTL(ttl uint64) + GetCallbackUploadToken(ttl uint64, callbackURL string) string + GetUploadToken(ttl uint64) string + GetPrivateURL(key string, ttl uint64) string + VerifyCallback(req *http.Request) (bool, error) + UploadFile(key, localFile string) (*PutRet, error) + ResumeUploadFile(key, localFile string) (*PutRet, error) + DelFile(key string) error + TimestampSecuritySign(path string, ttl time.Duration) string + GetFileInfo(key string) *storage.FileInfo + ListFiles(prefix, delimiter, marker string, limit int) (entries []storage.ListItem, commonPrefixes []string, nextMarker string, hasNext bool, err error) + GetFileHash(path, qhash string) (hash string, err error) +} + +type qiNiu struct { + mac *qbox.Mac + bucketManager *storage.BucketManager + conf *storage.Config + bucket string + domain string + securityKey string + md5 md5.MD5 + uploadTokenTTL uint64 +} + +type PutRet struct { + Key string `json:"key"` + Hash string `json:"hash"` + Fsize string `json:"fsize"` + Fname string `json:"fname"` + Ext string `json:"ext"` + Unique string `json:"unique"` + User string `json:"user"` +} + +func New(accessKey, secretKey, bucket, domain, securityKey string) QiNiu { + region, _ := storage.GetRegion(accessKey, bucket) + mac := qbox.NewMac(accessKey, secretKey) + conf := &storage.Config{ + Region: region, //空间所在的存储区域 + UseHTTPS: true, //是否使用https域名 + UseCdnDomains: true, //上传是否使用CDN上传加速 + } + return &qiNiu{ + mac: mac, + bucketManager: storage.NewBucketManager(mac, conf), + bucket: bucket, + domain: domain, + securityKey: securityKey, + conf: conf, + md5: md5.New(), + uploadTokenTTL: 3600, + } +} + +func (q *qiNiu) i() {} + +func (q *qiNiu) SetDefaultUploadTokenTTL(ttl uint64) { + q.uploadTokenTTL = ttl +} + +func (q *qiNiu) GetUploadToken(ttl uint64) string { + putPolicy := storage.PutPolicy{ + Scope: q.bucket, + Expires: ttl, + } + return putPolicy.UploadToken(q.mac) +} + +func (q *qiNiu) GetCallbackUploadToken(ttl uint64, callbackURL string) string { + putPolicy := storage.PutPolicy{ + Scope: q.bucket, + CallbackURL: callbackURL, + CallbackBody: `{"key":"$(key)","hash":"$(etag)","fname":"$(fname)","fsize":"$(fsize)","ext":"$(ext)","unique":"$(x:unique)","user":"$(x:user)"}`, + CallbackBodyType: "application/json", + Expires: ttl, + } + return putPolicy.UploadToken(q.mac) +} + +func (q *qiNiu) GetPrivateURL(key string, ttl uint64) string { + deadline := time.Now().Add(time.Second * time.Duration(ttl)).Unix() + return storage.MakePrivateURL(q.mac, q.domain, key, deadline) +} + +func (q *qiNiu) VerifyCallback(req *http.Request) (bool, error) { + return q.mac.VerifyCallback(req) +} + +func (q *qiNiu) UploadFile(key, localFile string) (*PutRet, error) { + upToken := q.GetUploadToken(q.uploadTokenTTL) + + //构建表单上传的对象 + formUploader := storage.NewFormUploader(q.conf) + + //请求参数 + filename := path.Base(key) + fileSuffix := path.Ext(key) + filePrefix := filename[0 : len(filename)-len(fileSuffix)] + putExtra := &storage.PutExtra{ + Params: map[string]string{ + "x:unique": filePrefix, + "x:user": "-", + }, + } + + //自定义返回body + ret := new(PutRet) + + err := formUploader.PutFile(context.Background(), ret, upToken, key, localFile, putExtra) + if err != nil { + return nil, err + } + + return ret, nil +} + +func (q *qiNiu) ResumeUploadFile(key, localFile string) (*PutRet, error) { + upToken := q.GetUploadToken(q.uploadTokenTTL) + + //构建分片上传的对象 + resumeUploader := storage.NewResumeUploaderV2(q.conf) + + //请求参数 + filename := path.Base(key) + fileSuffix := path.Ext(key) + filePrefix := filename[0 : len(filename)-len(fileSuffix)] + putExtra := &storage.RputV2Extra{ + CustomVars: map[string]string{ + "x:unique": filePrefix, + "x:user": "-", + }, + } + + //自定义返回body + ret := new(PutRet) + + err := resumeUploader.PutFile(context.Background(), ret, upToken, key, localFile, putExtra) + if err != nil { + return nil, err + } + + return ret, nil +} + +func (q *qiNiu) DelFile(key string) error { + err := q.bucketManager.Delete(q.bucket, key) + if err != nil { + return err + } + + return nil +} + +func (q *qiNiu) TimestampSecuritySign(path string, ttl time.Duration) string { + sep := "/" + path = strings.Trim(path, sep) + splits := strings.Split(path, sep) + for i, split := range splits { + splits[i] = url.QueryEscape(split) + } + path = sep + strings.Join(splits, sep) + + unix := time.Now().Add(ttl).Unix() + hex := fmt.Sprintf("%x", unix) + + encrypt := q.md5.Encrypt(q.securityKey + path + hex) + + param := make(url.Values) + param.Set("sign", encrypt) + param.Set("t", hex) + + return param.Encode() +} + +func (q *qiNiu) GetFileInfo(key string) *storage.FileInfo { + fileInfo, sErr := q.bucketManager.Stat(q.bucket, key) + if sErr != nil { + return nil + } + + return &fileInfo +} + +func (q *qiNiu) ListFiles(prefix, delimiter, marker string, limit int) (entries []storage.ListItem, + commonPrefixes []string, nextMarker string, hasNext bool, err error) { + return q.bucketManager.ListFiles(q.bucket, prefix, delimiter, marker, limit) +} + +func (q *qiNiu) GetFileHash(path, qhash string) (hash string, err error) { + if !tool.InArray(qhash, []string{"sha1", "md5", "sha256"}) { + return "", errors.New("qhash invalid") + } + + sign := q.TimestampSecuritySign(path, time.Second*5) + addr := fmt.Sprintf("https://cdn.mogume.com/%s?%s&qhash/%s", path, sign, qhash) + + resp, err := http.Get(addr) + if err != nil { + return "", err + } + defer func() { _ = resp.Body.Close() }() + if resp.StatusCode != http.StatusOK { + return "", errors.New(resp.Status) + } + + body, err := io.ReadAll(resp.Body) + if err != nil { + return "", err + } + + return gjson.GetBytes(body, "hash").String(), nil +} diff --git a/pkg/rsa/rsa.go b/pkg/rsa/rsa.go new file mode 100644 index 0000000..928b98b --- /dev/null +++ b/pkg/rsa/rsa.go @@ -0,0 +1,131 @@ +package rsa + +import ( + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "encoding/base64" + "encoding/pem" +) + +var _ Public = (*rsaPub)(nil) +var _ Private = (*rsaPri)(nil) + +type Public interface { + i() + EncryptURLEncoding(encryptStr string) (string, error) + Encrypt(encryptStr string) (string, error) +} + +type Private interface { + i() + + Decrypt(decryptStr string) (string, error) + DecryptURLEncoding(decryptStr string) (string, error) +} + +type rsaPub struct { + PublicKey string +} + +type rsaPri struct { + PrivateKey string +} + +func NewPublic(publicKey string) Public { + return &rsaPub{ + PublicKey: publicKey, + } +} + +func NewPrivate(privateKey string) Private { + return &rsaPri{ + PrivateKey: privateKey, + } +} + +func (pub *rsaPub) i() {} + +func (pub *rsaPub) Encrypt(encryptStr string) (string, error) { + // pem 解码 + block, _ := pem.Decode([]byte(pub.PublicKey)) + + // x509 解码 + publicKeyInterface, err := x509.ParsePKIXPublicKey(block.Bytes) + if err != nil { + return "", err + } + + // 类型断言 + publicKey := publicKeyInterface.(*rsa.PublicKey) + + //对明文进行加密 + encryptedStr, err := rsa.EncryptPKCS1v15(rand.Reader, publicKey, []byte(encryptStr)) + if err != nil { + return "", err + } + + //返回密文 + return base64.StdEncoding.EncodeToString(encryptedStr), nil +} + +func (pub *rsaPub) EncryptURLEncoding(encryptStr string) (string, error) { + // pem 解码 + block, _ := pem.Decode([]byte(pub.PublicKey)) + + // x509 解码 + publicKeyInterface, err := x509.ParsePKIXPublicKey(block.Bytes) + if err != nil { + return "", err + } + + // 类型断言 + publicKey := publicKeyInterface.(*rsa.PublicKey) + + //对明文进行加密 + encryptedStr, err := rsa.EncryptPKCS1v15(rand.Reader, publicKey, []byte(encryptStr)) + if err != nil { + return "", err + } + + //返回密文 + return base64.URLEncoding.EncodeToString(encryptedStr), nil +} + +func (pri *rsaPri) i() {} + +func (pri *rsaPri) Decrypt(decryptStr string) (string, error) { + // pem 解码 + block, _ := pem.Decode([]byte(pri.PrivateKey)) + + // X509 解码 + privateKey, err := x509.ParsePKCS1PrivateKey(block.Bytes) + if err != nil { + return "", err + } + decryptBytes, err := base64.StdEncoding.DecodeString(decryptStr) + + //对密文进行解密 + decrypted, _ := rsa.DecryptPKCS1v15(rand.Reader, privateKey, decryptBytes) + + //返回明文 + return string(decrypted), nil +} + +func (pri *rsaPri) DecryptURLEncoding(decryptStr string) (string, error) { + // pem 解码 + block, _ := pem.Decode([]byte(pri.PrivateKey)) + + // X509 解码 + privateKey, err := x509.ParsePKCS1PrivateKey(block.Bytes) + if err != nil { + return "", err + } + decryptBytes, err := base64.URLEncoding.DecodeString(decryptStr) + + //对密文进行解密 + decrypted, _ := rsa.DecryptPKCS1v15(rand.Reader, privateKey, decryptBytes) + + //返回明文 + return string(decrypted), nil +} diff --git a/pkg/shutdown/shutdown.go b/pkg/shutdown/shutdown.go new file mode 100644 index 0000000..040bc8e --- /dev/null +++ b/pkg/shutdown/shutdown.go @@ -0,0 +1,50 @@ +package shutdown + +import ( + "os" + "os/signal" + "syscall" +) + +var _ Hook = (*hook)(nil) + +// Hook a graceful shutdown hook, default with signals of SIGINT and SIGTERM +type Hook interface { + // WithSignals add more signals into hook + WithSignals(signals ...syscall.Signal) Hook + + // Close register shutdown handles + Close(funcs ...func()) +} + +type hook struct { + ctx chan os.Signal +} + +// NewHook create a Hook instance +func NewHook() Hook { + hook := &hook{ + ctx: make(chan os.Signal, 1), + } + + return hook.WithSignals(syscall.SIGINT, syscall.SIGTERM, syscall.SIGKILL, syscall.SIGQUIT) +} + +func (h *hook) WithSignals(signals ...syscall.Signal) Hook { + for _, s := range signals { + signal.Notify(h.ctx, s) + } + + return h +} + +func (h *hook) Close(funcs ...func()) { + select { + case <-h.ctx: + } + signal.Stop(h.ctx) + + for _, f := range funcs { + f() + } +} diff --git a/pkg/signature/signature.go b/pkg/signature/signature.go new file mode 100644 index 0000000..f3d5058 --- /dev/null +++ b/pkg/signature/signature.go @@ -0,0 +1,52 @@ +package signature + +import ( + "net/http" + "net/url" + "time" +) + +var _ Signature = (*signature)(nil) + +const ( + delimiter = "|" +) + +// 合法的 Methods +var methods = map[string]bool{ + http.MethodGet: true, + http.MethodPost: true, + http.MethodHead: true, + http.MethodPut: true, + http.MethodPatch: true, + http.MethodDelete: true, + http.MethodConnect: true, + http.MethodOptions: true, + http.MethodTrace: true, +} + +type Signature interface { + i() + + // Generate 生成签名 + Generate(path string, method string, params url.Values) (authorization, date string, err error) + + // Verify 验证签名 + Verify(authorization, date string, path string, method string, params url.Values) (ok bool, err error) +} + +type signature struct { + key string + secret string + ttl time.Duration +} + +func New(key, secret string, ttl time.Duration) Signature { + return &signature{ + key: key, + secret: secret, + ttl: ttl, + } +} + +func (s *signature) i() {} diff --git a/pkg/signature/signature_generate.go b/pkg/signature/signature_generate.go new file mode 100644 index 0000000..54033a9 --- /dev/null +++ b/pkg/signature/signature_generate.go @@ -0,0 +1,62 @@ +package signature + +import ( + "bytes" + "crypto/hmac" + "crypto/sha256" + "encoding/base64" + "errors" + "fmt" + "net/url" + "strings" + + "git.bvbej.com/bvbej/base-golang/pkg/time_parse" +) + +// Generate +// path 请求的路径 (不附带 querystring) +func (s *signature) Generate(path string, method string, params url.Values) (authorization, date string, err error) { + if path == "" { + err = errors.New("path required") + return + } + + if method == "" { + err = errors.New("method required") + return + } + + methodName := strings.ToUpper(method) + if !methods[methodName] { + err = errors.New("method param error") + return + } + + // Date + date = time_parse.CSTLayoutString() + + // Encode() 方法中自带 sorted by key + sortParamsEncode, err := url.QueryUnescape(params.Encode()) + if err != nil { + err = fmt.Errorf("url QueryUnescape error [%v]", err) + return + } + + // 加密字符串规则 + buffer := bytes.NewBuffer(nil) + buffer.WriteString(path) + buffer.WriteString(delimiter) + buffer.WriteString(methodName) + buffer.WriteString(delimiter) + buffer.WriteString(sortParamsEncode) + buffer.WriteString(delimiter) + buffer.WriteString(date) + + // 对数据进行 sha256 加密,并进行 base64 encode + hash := hmac.New(sha256.New, []byte(s.secret)) + hash.Write(buffer.Bytes()) + digest := base64.StdEncoding.EncodeToString(hash.Sum(nil)) + + authorization = fmt.Sprintf("%s %s", s.key, digest) + return +} diff --git a/pkg/signature/signature_verify.go b/pkg/signature/signature_verify.go new file mode 100644 index 0000000..fe7c500 --- /dev/null +++ b/pkg/signature/signature_verify.go @@ -0,0 +1,73 @@ +package signature + +import ( + "bytes" + "crypto/hmac" + "crypto/sha256" + "encoding/base64" + "errors" + "fmt" + "net/url" + "strings" + "time" + + "git.bvbej.com/bvbej/base-golang/pkg/time_parse" +) + +func (s *signature) Verify(authorization, date string, path string, method string, params url.Values) (ok bool, err error) { + if date == "" { + err = errors.New("date required") + return + } + + if path == "" { + err = errors.New("path required") + return + } + + if method == "" { + err = errors.New("method required") + return + } + + methodName := strings.ToUpper(method) + if !methods[methodName] { + err = errors.New("method param error") + return + } + + ts, err := time_parse.ParseCSTInLocation(date) + if err != nil { + err = errors.New("date must follow '2006-01-02 15:04:05'") + return + } + + if time_parse.SubInLocation(ts) > float64(s.ttl/time.Second) { + err = fmt.Errorf("date exceeds limit [%v]", s.ttl) + return + } + + // Encode() 方法中自带 sorted by key + sortParamsEncode, err := url.QueryUnescape(params.Encode()) + if err != nil { + err = fmt.Errorf("url QueryUnescape error [%v]", err) + return + } + + buffer := bytes.NewBuffer(nil) + buffer.WriteString(path) + buffer.WriteString(delimiter) + buffer.WriteString(methodName) + buffer.WriteString(delimiter) + buffer.WriteString(sortParamsEncode) + buffer.WriteString(delimiter) + buffer.WriteString(date) + + // 对数据进行 hmac 加密,并进行 base64 encode + hash := hmac.New(sha256.New, []byte(s.secret)) + hash.Write(buffer.Bytes()) + digest := base64.StdEncoding.EncodeToString(hash.Sum(nil)) + + ok = authorization == fmt.Sprintf("%s %s", s.key, digest) + return +} diff --git a/pkg/sse/client.html b/pkg/sse/client.html new file mode 100644 index 0000000..fbe5be2 --- /dev/null +++ b/pkg/sse/client.html @@ -0,0 +1,26 @@ + + + + + + Server Sent Event + + + +
+ + + + + \ No newline at end of file diff --git a/pkg/sse/server.go b/pkg/sse/server.go new file mode 100644 index 0000000..01e3d61 --- /dev/null +++ b/pkg/sse/server.go @@ -0,0 +1,118 @@ +package sse + +import ( + "github.com/gin-gonic/gin" + "io" + "net/http" + "sync" + "sync/atomic" +) + +var _ Server = (*event)(nil) + +type Server interface { + HandlerFunc(auth func(c *gin.Context) (string, error)) gin.HandlerFunc + Push(user, name, msg string) bool + Broadcast(name, msg string) +} + +type clientChan struct { + User string + Chan chan msgChan +} + +type msgChan struct { + Name string + Message string +} + +type event struct { + SessionList sync.Map + Count atomic.Int32 + + Register chan clientChan + Unregister chan string +} + +func NewServer() Server { + e := &event{ + SessionList: sync.Map{}, + Count: atomic.Int32{}, + Register: make(chan clientChan), + Unregister: make(chan string), + } + + go e.listen() + + return e +} + +func (stream *event) listen() { + for { + select { + case client := <-stream.Register: + stream.SessionList.Store(client.User, client.Chan) + stream.Count.Add(1) + case user := <-stream.Unregister: + value, ok := stream.SessionList.Load(user) + if ok { + event := value.(chan msgChan) + close(event) + stream.SessionList.Delete(user) + stream.Count.Add(-1) + } + } + } +} + +func (stream *event) HandlerFunc(auth func(c *gin.Context) (string, error)) gin.HandlerFunc { + return func(c *gin.Context) { + user, err := auth(c) + if err != nil { + c.AbortWithStatus(http.StatusBadRequest) + return + } + + e := make(chan msgChan) + client := clientChan{ + User: user, + Chan: e, + } + stream.Register <- client + defer func() { + stream.Unregister <- user + }() + + c.Writer.Header().Set("Content-Type", "text/event-stream") + c.Writer.Header().Set("Cache-Control", "no-cache") + c.Writer.Header().Set("Connection", "keep-alive") + c.Writer.Header().Set("Transfer-Encoding", "chunked") + + c.Stream(func(w io.Writer) bool { + if msg, ok := <-e; ok { + c.SSEvent(msg.Name, msg.Message) + return true + } + return false + }) + + c.Next() + } +} + +func (stream *event) Push(user, name, msg string) bool { + value, ok := stream.SessionList.Load(user) + if ok { + val := value.(chan msgChan) + val <- msgChan{Name: name, Message: msg} + } + return false +} + +func (stream *event) Broadcast(name, msg string) { + stream.SessionList.Range(func(user, value any) bool { + val := value.(chan msgChan) + val <- msgChan{Name: name, Message: msg} + return true + }) +} diff --git a/pkg/ticker/ticker.go b/pkg/ticker/ticker.go new file mode 100644 index 0000000..6f4d2b5 --- /dev/null +++ b/pkg/ticker/ticker.go @@ -0,0 +1,52 @@ +package ticker + +import ( + "context" + "time" +) + +var _ Ticker = (*ticker)(nil) + +type Ticker interface { + worker() + + Process(fun func()) + Stop() +} + +type ticker struct { + ticker *time.Ticker + ctx context.Context + cancel context.CancelFunc + f func() +} + +func New(d time.Duration) Ticker { + ctx, cancelFunc := context.WithCancel(context.Background()) + return &ticker{ + ticker: time.NewTicker(d), + ctx: ctx, + cancel: cancelFunc, + } +} + +func (t *ticker) worker() { + for { + select { + case <-t.ticker.C: + t.f() + case <-t.ctx.Done(): + return + } + } +} + +func (t *ticker) Process(fun func()) { + t.f = fun + go t.worker() +} + +func (t *ticker) Stop() { + t.ticker.Stop() + t.cancel() +} diff --git a/pkg/time_parse/time_parse.go b/pkg/time_parse/time_parse.go new file mode 100644 index 0000000..e9f2ccb --- /dev/null +++ b/pkg/time_parse/time_parse.go @@ -0,0 +1,124 @@ +package time_parse + +import ( + "github.com/jinzhu/now" + "math" + "net/http" + "time" +) + +var ( + cst = time.Local +) + +func SetPRCLocation() error { + var err error + if cst, err = time.LoadLocation("Asia/Shanghai"); err != nil { + return err + } + return nil +} + +func Now() *now.Now { + timeFormats := append(append(now.TimeFormats, time.DateTime), time.DateOnly) + + c := &now.Config{ + WeekStartDay: time.Monday, + TimeLocation: cst, + TimeFormats: timeFormats, + } + + return c.With(time.Now().In(cst)) +} + +// RFC3339ToCSTLayout convert rfc3339 value to China standard time layout +// 2020-11-08T08:18:46+08:00 => 2020-11-08 08:18:46 +func RFC3339ToCSTLayout(value string) (string, error) { + ts, err := time.Parse(time.RFC3339, value) + if err != nil { + return "", err + } + + return ts.In(cst).Format(time.DateTime), nil +} + +// GetMilliTimestamp 获取CST毫秒时间戳 +func GetMilliTimestamp() int64 { + return time.Now().In(cst).UnixMilli() +} + +// GetTimestamp 获取CST时间戳 +func GetTimestamp() int64 { + return time.Now().In(cst).Unix() +} + +// CSTLayoutString 格式化时间 +// 返回 "2006-01-02 15:04:05" 格式的时间 +func CSTLayoutString() string { + return time.Now().In(cst).Format(time.DateTime) +} + +// ParseCSTInLocation 格式化时间 +func ParseCSTInLocation(date string) (time.Time, error) { + return time.ParseInLocation(time.DateTime, date, cst) +} + +// CSTLayoutStringToUnix 返回 unix 时间戳 +// 2020-01-24 21:11:11 => 1579871471 +func CSTLayoutStringToUnix(cstLayoutString string) (int64, error) { + stamp, err := time.ParseInLocation(time.DateTime, cstLayoutString, cst) + if err != nil { + return 0, err + } + return stamp.Unix(), nil +} + +// GMTLayoutString 格式化时间 +// 返回 "Mon, 02 Jan 2006 15:04:05 GMT" 格式的时间 +func GMTLayoutString() string { + return time.Now().In(cst).Format(http.TimeFormat) +} + +// ParseGMTInLocation 格式化时间 +func ParseGMTInLocation(date string) (time.Time, error) { + return time.ParseInLocation(http.TimeFormat, date, cst) +} + +// SubInLocation 计算时间差 +func SubInLocation(ts time.Time) float64 { + return math.Abs(time.Now().In(cst).Sub(ts).Seconds()) +} + +// GetFirstDateOfMonth 获取传入的时间所在月份的第一天,即某月第一天的0点。如传入time.Now(), 返回当前月份的第一天0点时间。 +func GetFirstDateOfMonth(d time.Time) time.Time { + d = d.AddDate(0, 0, -d.Day()+1) + return GetStartDayTime(d) +} + +// GetLastDateOfMonth 获取传入的时间所在月份的最后一天,即某月最后一天的0点。如传入time.Now(), 返回当前月份的最后一天0点时间。 +func GetLastDateOfMonth(d time.Time) time.Time { + return GetEndDayTime(GetFirstDateOfMonth(d).AddDate(0, 1, -1)) +} + +// GetStartDayTime 获取某一天的开始时间 +func GetStartDayTime(d time.Time) time.Time { + return time.Date(d.Year(), d.Month(), d.Day(), 0, 0, 0, 0, cst) +} + +// GetEndDayTime 获取某一天的结束时间 +func GetEndDayTime(d time.Time) time.Time { + return GetStartDayTime(d).Add(24 * time.Hour).Add(-time.Second) +} + +// GetStartYesterdayTime 获取昨天的起始时间 +func GetStartYesterdayTime() time.Time { + d, _ := time.ParseDuration("-24h") + return GetStartDayTime(time.Now().Add(d)) +} + +// SubInLocationDays 计算两个日期相差的天数 +func SubInLocationDays(t1, t2 time.Time) float64 { + t1 = time.Date(t1.Year(), t1.Month(), t1.Day(), 0, 0, 0, 0, cst) + t2 = time.Date(t2.Year(), t2.Month(), t2.Day(), 0, 0, 0, 0, cst) + return math.Abs(t1.Sub(t2).Hours() / 24) +} diff --git a/pkg/token/README.md b/pkg/token/README.md new file mode 100644 index 0000000..0b25585 --- /dev/null +++ b/pkg/token/README.md @@ -0,0 +1,20 @@ +## 与 UrlSign 对应的 PHP 加密算法 + +```php +// 对 params key 进行排序 +ksort($params); + +// 对 sortParams 进行 Encode +$sortParamsEncode = http_build_query($params); + +// 加密字符串规则 path + method + sortParamsEncode + secret +$encryptStr = $path . $method . $sortParamsEncode . $secret + +// 对加密字符串进行 md5 +$md5Str = md5($encryptStr); + +// 对 md5Str 进行 base64 encode +$tokenString = base64_encode($md5Str); + +echo $tokenString; +``` \ No newline at end of file diff --git a/pkg/token/token.go b/pkg/token/token.go new file mode 100644 index 0000000..a2c4ffd --- /dev/null +++ b/pkg/token/token.go @@ -0,0 +1,36 @@ +package token + +import ( + "net/url" + "time" + + "github.com/golang-jwt/jwt/v4" +) + +var _ Token = (*token)(nil) + +type Token interface { + // i 为了避免被其他包实现 + i() + + // JwtSign JWT 签名方式 + JwtSign(userId, subject string, expireDuration time.Duration) (tokenString string, err error) + JwtParse(tokenString string) (*jwt.RegisteredClaims, error) + + // UrlSign URL 签名方式,不支持解密 + UrlSign(path string, method string, params url.Values) (tokenString string, err error) +} + +type token struct { + secret string + domain []string +} + +func New(secret string, domain ...string) Token { + return &token{ + secret: secret, + domain: domain, + } +} + +func (t *token) i() {} diff --git a/pkg/token/token_jwt.go b/pkg/token/token_jwt.go new file mode 100644 index 0000000..2065d0d --- /dev/null +++ b/pkg/token/token_jwt.go @@ -0,0 +1,43 @@ +package token + +import ( + "github.com/golang-jwt/jwt/v4" + "time" +) + +func (t *token) JwtSign(userId, subject string, expireDuration time.Duration) (tokenString string, err error) { + // The token content. + // iss: (Issuer)签发者 + // iat: (Issued At)签发时间,用Unix时间戳表示 + // exp: (Expiration Time)过期时间,用Unix时间戳表示 + // aud: (Audience)接收该JWT的一方 + // sub: (Subject)该JWT的主题 + // nbf: (Not Before)不要早于这个时间 + // jti: (JWT ID)用于标识JWT的唯一ID + c := &jwt.RegisteredClaims{ + Issuer: "BvBeJ", + Subject: subject, + Audience: jwt.ClaimStrings(t.domain), + ExpiresAt: jwt.NewNumericDate(time.Now().Add(expireDuration)), + NotBefore: jwt.NewNumericDate(time.Now()), + IssuedAt: jwt.NewNumericDate(time.Now()), + ID: userId, + } + + tokenString, err = jwt.NewWithClaims(jwt.SigningMethodHS256, c).SignedString([]byte(t.secret)) + return +} + +func (t *token) JwtParse(tokenString string) (*jwt.RegisteredClaims, error) { + tokenClaims, err := jwt.ParseWithClaims(tokenString, &jwt.RegisteredClaims{}, func(token *jwt.Token) (any, error) { + return []byte(t.secret), nil + }) + + if tokenClaims != nil { + if c, ok := tokenClaims.Claims.(*jwt.RegisteredClaims); ok && tokenClaims.Valid { + return c, nil + } + } + + return nil, err +} diff --git a/pkg/token/token_url.go b/pkg/token/token_url.go new file mode 100644 index 0000000..de1ffca --- /dev/null +++ b/pkg/token/token_url.go @@ -0,0 +1,48 @@ +package token + +import ( + "crypto/md5" + "encoding/base64" + "encoding/hex" + "errors" + "fmt" + "net/url" + "strings" +) + +// UrlSign +// path 请求的路径 (不附带 querystring) +func (t *token) UrlSign(path string, method string, params url.Values) (tokenString string, err error) { + // 合法的 Methods + methods := map[string]bool{ + "get": true, + "post": true, + "put": true, + "path": true, + "delete": true, + "head": true, + "options": true, + } + + methodName := strings.ToLower(method) + if !methods[methodName] { + err = errors.New("method param error") + return + } + + // Encode() 方法中自带 sorted by key + sortParamsEncode := params.Encode() + + // 加密字符串规则 path + method + sortParamsEncode + secret + encryptStr := fmt.Sprintf("%s%s%s%s", path, methodName, sortParamsEncode, t.secret) + + // 对加密字符串进行 md5 + s := md5.New() + s.Write([]byte(encryptStr)) + md5Str := hex.EncodeToString(s.Sum(nil)) + + // 对 md5Str 进行 base64 encode + tokenString = base64.StdEncoding.EncodeToString([]byte(md5Str)) + + return +} diff --git a/pkg/trace/README.md b/pkg/trace/README.md new file mode 100644 index 0000000..1dae1db --- /dev/null +++ b/pkg/trace/README.md @@ -0,0 +1,81 @@ +## trace + +一个用于开发调试的辅助工具。 + +可以实时显示当前页面的操作的请求信息、运行情况、SQL执行、错误提示等。 + +- `trace.go` 主入口文件; +- `dialog.go` 处理 third_party_requests 记录; +- `debug.go` 处理 debug 记录; + +#### 数据格式 + +##### trace_id + +当前 trace 的 ID,例如:938ff86be98439c6c1a7,便于搜索使用。 + +##### request + +请求信息,会包括: + +- ttl 请求超时时间,例如:2s 或 un-limit +- method 请求方式,例如:GET 或 POST +- decoded_url 请求地址 +- header 请求头信息 +- body 请求体信息 + +##### response + +- header 响应头信息 +- body 响应提信息 +- business_code 业务码,例如:10010 +- business_code_msg 业务码信息,例如:签名错误 +- http_code HTTP 状态码,例如:200 +- http_code_msg HTTP 状态码信息,例如:OK +- cost_seconds 耗费时长:单位秒,比如 0.001105661 + +##### third_party_requests + +每一个第三方 http 请求都会生成如下的一组数据,多个请求会生成多组数据。 + +- request,同上 request 结构一致 +- response,同上 response 结构一致 +- success,是否成功,true 或 false +- cost_seconds,耗费时长:单位秒 + +注意:response 中的 business_code、business_code_msg 为空,因为各个第三方返回结构不同,这两个字段为空。 + +##### sqls + +执行的 SQL 信息,多个 SQL 会记录多组数据。 + +- timestamp,时间,格式:2006-01-02 15:04:05 +- stack,文件地址和行号 +- cost_seconds,执行时长,单位:秒 +- sql,SQL 语句 +- rows_affected,影响行数 + +##### debugs + +- key 打印的标示 +- value 打印的值 + +```cassandraql +// 调试时,使用这个方法: +p.Print("key", "value", p.WithTrace(c.Trace())) +``` + +只有参数中增加了 `p.WithTrace(c.Trace())`,才会记录到 `debugs` 中。 + +##### success + +是否成功,true 或 false + +```cassandraql +success = !ctx.IsAborted() && ctx.Writer.Status() == http.StatusOK +``` + +##### cost_seconds + +耗费时长:单位秒,比如 0.001105661 + diff --git a/pkg/trace/debug.go b/pkg/trace/debug.go new file mode 100644 index 0000000..d84c388 --- /dev/null +++ b/pkg/trace/debug.go @@ -0,0 +1,7 @@ +package trace + +type Debug struct { + Key string `json:"key"` // 标示 + Value any `json:"value"` // 值 + CostSeconds float64 `json:"cost_seconds"` // 执行时间(单位秒) +} diff --git a/pkg/trace/dialog.go b/pkg/trace/dialog.go new file mode 100644 index 0000000..e5c9740 --- /dev/null +++ b/pkg/trace/dialog.go @@ -0,0 +1,32 @@ +package trace + +import "sync" + +var _ D = (*Dialog)(nil) + +type D interface { + i() + AppendResponse(resp *Response) +} + +// Dialog 内部调用其它方接口的会话信息;失败时会有retry操作,所以 response 会有多次。 +type Dialog struct { + mux sync.Mutex + Request *Request `json:"request"` // 请求信息 + Responses []*Response `json:"responses"` // 返回信息 + Success bool `json:"success"` // 是否成功,true 或 false + CostSeconds float64 `json:"cost_seconds"` // 执行时长(单位秒) +} + +func (d *Dialog) i() {} + +// AppendResponse 按转的追加response信息 +func (d *Dialog) AppendResponse(resp *Response) { + if resp == nil { + return + } + + d.mux.Lock() + d.Responses = append(d.Responses, resp) + d.mux.Unlock() +} diff --git a/pkg/trace/grpc.go b/pkg/trace/grpc.go new file mode 100644 index 0000000..e1bff9f --- /dev/null +++ b/pkg/trace/grpc.go @@ -0,0 +1,17 @@ +package trace + +import ( + "google.golang.org/grpc/metadata" +) + +type Grpc struct { + Timestamp string `json:"timestamp"` // 时间,格式:2006-01-02 15:04:05 + Addr string `json:"addr"` // 地址 + Method string `json:"method"` // 操作方法 + Meta metadata.MD `json:"meta"` // Mate + Request map[string]any `json:"request"` // 请求信息 + Response map[string]any `json:"response"` // 返回信息 + CostSeconds float64 `json:"cost_seconds"` // 执行时间(单位秒) + Code string `json:"err_code,omitempty"` // 错误码 + Message string `json:"err_message,omitempty"` // 错误信息 +} diff --git a/pkg/trace/redis.go b/pkg/trace/redis.go new file mode 100644 index 0000000..87df256 --- /dev/null +++ b/pkg/trace/redis.go @@ -0,0 +1,10 @@ +package trace + +type Redis struct { + Timestamp string `json:"timestamp"` // 时间,格式:2006-01-02 15:04:05 + Handle string `json:"handle"` // 操作,SET/GET 等 + Key string `json:"key"` // Key + Value string `json:"value,omitempty"` // Value + TTL float64 `json:"ttl,omitempty"` // 超时时长(单位分) + CostSeconds float64 `json:"cost_seconds"` // 执行时间(单位秒) +} diff --git a/pkg/trace/sql.go b/pkg/trace/sql.go new file mode 100644 index 0000000..d27cde3 --- /dev/null +++ b/pkg/trace/sql.go @@ -0,0 +1,9 @@ +package trace + +type SQL struct { + Timestamp string `json:"timestamp"` // 时间,格式:2006-01-02 15:04:05 + Stack string `json:"stack"` // 文件地址和行号 + SQL string `json:"sql"` // SQL 语句 + Rows int64 `json:"rows_affected"` // 影响行数 + CostSeconds float64 `json:"cost_seconds"` // 执行时长(单位秒) +} diff --git a/pkg/trace/trace.go b/pkg/trace/trace.go new file mode 100644 index 0000000..45278f8 --- /dev/null +++ b/pkg/trace/trace.go @@ -0,0 +1,154 @@ +package trace + +import ( + "crypto/rand" + "encoding/hex" + "sync" +) + +const Header = "X-TRACE-ID" + +var _ T = (*Trace)(nil) + +type T interface { + i() + ID() string + WithRequest(req *Request) *Trace + WithResponse(resp *Response) *Trace + AppendDialog(dialog *Dialog) *Trace + AppendDebug(debug *Debug) *Trace + AppendSQL(sql *SQL) *Trace + AppendRedis(redis *Redis) *Trace + AppendGRPC(grpc *Grpc) *Trace +} + +// Trace 记录的参数 +type Trace struct { + mux sync.Mutex + Identifier string `json:"trace_id"` // 链路ID + Request *Request `json:"request"` // 请求信息 + Response *Response `json:"response"` // 返回信息 + ThirdPartyRequests []*Dialog `json:"third_party_requests,omitempty"` // 调用第三方接口的信息 + Debugs []*Debug `json:"debugs,omitempty"` // 调试信息 + SQLs []*SQL `json:"sqls,omitempty"` // 执行的 SQL 信息 + Redis []*Redis `json:"redis,omitempty"` // 执行的 Redis 信息 + GRPCs []*Grpc `json:"grpc,omitempty"` // 执行的 gRPC 信息 + Success bool `json:"success"` // 请求结果 true or false + CostSeconds float64 `json:"cost_seconds"` // 执行时长(单位秒) +} + +// Request 请求信息 +type Request struct { + TTL string `json:"ttl,omitempty"` // 请求超时时间 + Method string `json:"method"` // 请求方式 + DecodedURL string `json:"decoded_url"` // 请求地址 + Header any `json:"header"` // 请求 Header 信息 + Body any `json:"body"` // 请求 Body 信息 +} + +// Response 响应信息 +type Response struct { + Header any `json:"header"` // Header 信息 + Body any `json:"body"` // Body 信息 + BusinessCode int `json:"business_code,omitempty"` // 业务码 + BusinessCodeMsg string `json:"business_code_msg,omitempty"` // 提示信息 + HttpCode int `json:"http_code"` // HTTP 状态码 + HttpCodeMsg string `json:"http_code_msg"` // HTTP 状态码信息 + CostSeconds float64 `json:"cost_seconds"` // 执行时间(单位秒) +} + +func New(id string) *Trace { + if id == "" { + buf := make([]byte, 10) + _, _ = rand.Read(buf) + id = hex.EncodeToString(buf) + } + + return &Trace{ + Identifier: id, + } +} + +func (t *Trace) i() {} + +// ID 唯一标识符 +func (t *Trace) ID() string { + return t.Identifier +} + +// WithRequest 设置request +func (t *Trace) WithRequest(req *Request) *Trace { + t.Request = req + return t +} + +// WithResponse 设置response +func (t *Trace) WithResponse(resp *Response) *Trace { + t.Response = resp + return t +} + +// AppendDialog 安全的追加内部调用过程dialog +func (t *Trace) AppendDialog(dialog *Dialog) *Trace { + if dialog == nil { + return t + } + + t.mux.Lock() + defer t.mux.Unlock() + + t.ThirdPartyRequests = append(t.ThirdPartyRequests, dialog) + return t +} + +// AppendDebug 追加 debug +func (t *Trace) AppendDebug(debug *Debug) *Trace { + if debug == nil { + return t + } + + t.mux.Lock() + defer t.mux.Unlock() + + t.Debugs = append(t.Debugs, debug) + return t +} + +// AppendSQL 追加 SQL +func (t *Trace) AppendSQL(sql *SQL) *Trace { + if sql == nil { + return t + } + + t.mux.Lock() + defer t.mux.Unlock() + + t.SQLs = append(t.SQLs, sql) + return t +} + +// AppendRedis 追加 Redis +func (t *Trace) AppendRedis(redis *Redis) *Trace { + if redis == nil { + return t + } + + t.mux.Lock() + defer t.mux.Unlock() + + t.Redis = append(t.Redis, redis) + return t +} + +// AppendGRPC 追加 gRPC 调用信息 +func (t *Trace) AppendGRPC(grpc *Grpc) *Trace { + if grpc == nil { + return t + } + + t.mux.Lock() + defer t.mux.Unlock() + + t.GRPCs = append(t.GRPCs, grpc) + return t +} diff --git a/pkg/upload/tus.go b/pkg/upload/tus.go new file mode 100644 index 0000000..939df71 --- /dev/null +++ b/pkg/upload/tus.go @@ -0,0 +1,228 @@ +package upload + +import ( + "context" + "crypto/sha256" + "errors" + "fmt" + "git.bvbej.com/bvbej/base-golang/pkg/color" + "git.bvbej.com/bvbej/base-golang/pkg/ticker" + "git.bvbej.com/bvbej/base-golang/pkg/token" + "github.com/rs/cors" + "github.com/tus/tusd/pkg/filestore" + tus "github.com/tus/tusd/pkg/handler" + "go.uber.org/zap" + "net/http" + "os" + "strings" + "sync" + "time" +) + +var _ Server = (*server)(nil) + +type Server interface { + GetUploadToken(string, string, time.Duration) string + GetFileInfo(string) (*tus.FileInfo, error) + + Start(func(string, string, tus.FileInfo)) error + Stop() error +} + +type server struct { + headerTokenKey string + uploading sync.Map + config Config + token token.Token + store filestore.FileStore + logger *zap.Logger + httpServer *http.Server + ctx context.Context + done context.CancelFunc + checker ticker.Ticker + completedEvent func(sha256, param string, info tus.FileInfo) +} + +type Config struct { + ListenAddr string + Path string + Dir string + Secret string + DisableDownload bool + Debug bool +} + +func New(conf Config, logger *zap.Logger) Server { + ctx, cancelFunc := context.WithCancel(context.Background()) + return &server{ + config: conf, + uploading: sync.Map{}, + headerTokenKey: "Authorization", + logger: logger, + token: token.New(conf.Secret), + ctx: ctx, + done: cancelFunc, + checker: ticker.New(time.Minute), + } +} + +func (s *server) GetUploadToken(sha256, param string, ttl time.Duration) string { + sign, _ := s.token.JwtSign(sha256, param, ttl) + return sign +} + +func (s *server) GetFileInfo(id string) (*tus.FileInfo, error) { + upload, err := s.store.GetUpload(context.Background(), id) + if err != nil { + return nil, err + } + info, err := upload.GetInfo(context.Background()) + if err != nil { + return nil, err + } + return &info, nil +} + +func (s *server) Start(completedEvent func(sha256, param string, info tus.FileInfo)) error { + s.completedEvent = completedEvent + + composer := tus.NewStoreComposer() + if err := os.MkdirAll(s.config.Dir, os.ModePerm); err != nil { + return err + } + s.store = filestore.New(s.config.Dir) + s.store.UseIn(composer) + + handler, err := tus.NewHandler(tus.Config{ + StoreComposer: composer, + BasePath: s.config.Path, + Logger: zap.NewStdLog(s.logger), + NotifyCompleteUploads: true, + NotifyTerminatedUploads: true, + DisableTermination: true, + DisableDownload: s.config.DisableDownload, + RespectForwardedHeaders: strings.Contains(s.config.ListenAddr, "127.0.0.1"), + PreUploadCreateCallback: func(hook tus.HookEvent) error { + authStr := hook.HTTPRequest.Header.Get(s.headerTokenKey) + jwtClaims, err := s.token.JwtParse(authStr) + if err == nil { + _, ok := s.uploading.Load(authStr) + if !ok { + s.uploading.Store(authStr, jwtClaims.ExpiresAt.Time) + return nil + } + return errors.New("repeated") + } + return errors.New("unauthorized") + }, + PreFinishResponseCallback: func(hook tus.HookEvent) error { + authStr := hook.HTTPRequest.Header.Get(s.headerTokenKey) + jwtParse, err := s.token.JwtParse(authStr) + if err != nil { + return errors.New("token expired") + } + _, ok := s.uploading.Load(authStr) + if ok { + s.uploading.Delete(authStr) + } + + upload, err := s.store.GetUpload(context.Background(), hook.Upload.ID) + if err != nil { + return err + } + + info, err := upload.GetInfo(context.Background()) + path, exist := info.Storage["Path"] + if err != nil || !exist { + return errors.New("file not found") + } + content, err := os.ReadFile(path) + if err != nil { + return err + } + + hash := sha256.New() + hash.Write(content) + sha256Byte := hash.Sum(nil) + sha256String := fmt.Sprintf("%x", sha256Byte) + if !s.config.Debug && sha256String != strings.ToLower(jwtParse.ID) { + _ = os.Remove(path) + _ = os.Remove(path + ".info") + return errors.New("file check error") + } + + return nil + }, + }) + if err != nil { + return err + } + + go func() { + for { + select { + case event := <-handler.CompleteUploads: + authStr := event.HTTPRequest.Header.Get(s.headerTokenKey) + jwtParse, _ := s.token.JwtParse(authStr) + if s.completedEvent != nil { + go func() { + s.completedEvent(jwtParse.ID, jwtParse.Subject, event.Upload) + }() + } + case <-s.ctx.Done(): + return + } + } + }() + + go func() { + for { + select { + case event := <-handler.TerminatedUploads: + upload, _ := s.store.GetUpload(context.Background(), event.Upload.ID) + if upload != nil { + info, _ := upload.GetInfo(context.Background()) + path, exist := info.Storage["Path"] + if exist { + _ = os.Remove(path) + _ = os.Remove(path + ".info") + } + } + case <-s.ctx.Done(): + return + } + } + }() + + s.checker.Process(func() { + s.uploading.Range(func(key, value any) bool { + t := value.(time.Time) + if t.Before(time.Now()) { + s.uploading.Delete(key) + } + return true + }) + }) + + //监听服务 + addr := s.config.ListenAddr + mux := http.NewServeMux() + mux.Handle(s.config.Path, http.StripPrefix(s.config.Path, handler)) + s.httpServer = &http.Server{ + Addr: addr, + Handler: cors.AllowAll().Handler(mux), + } + go func() { + if err = s.httpServer.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { + s.logger.Sugar().Fatal("upload server startup err", zap.Error(err)) + } + }() + fmt.Println(color.Green(fmt.Sprintf("* [register tusd listen %s]", addr))) + + return nil +} + +func (s *server) Stop() error { + s.done() + return s.httpServer.Close() +} diff --git a/pkg/urltable/urltable.go b/pkg/urltable/urltable.go new file mode 100644 index 0000000..fd1f984 --- /dev/null +++ b/pkg/urltable/urltable.go @@ -0,0 +1,169 @@ +package urltable + +import ( + "errors" + "fmt" + "net/http" + "strings" +) + +const ( + empty = "" + fuzzy = "*" + omitted = "**" + delimiter = "/" + methodView = "VIEW" +) + +// parse and validate pattern +func parse(pattern string) ([]string, error) { + const format = "[get, post, put, patch, delete, view]/{a-Z}+/{*}+/{**}" + + if pattern = strings.TrimLeft(strings.TrimSpace(pattern), delimiter); pattern == "" { + return nil, fmt.Errorf("pattern illegal, should in format of %s", format) + } + + paths := strings.Split(pattern, delimiter) + if len(paths) < 2 { + return nil, fmt.Errorf("pattern illegal, should in format of %s", format) + } + + for i := range paths { + paths[i] = strings.TrimSpace(paths[i]) + } + + // likes get/ get/* get/** + if len(paths) == 2 && (paths[1] == empty || paths[1] == fuzzy || paths[1] == omitted) { + return nil, errors.New("illegal wildcard") + } + + switch paths[0] = strings.ToUpper(paths[0]); paths[0] { + case http.MethodGet, + http.MethodPost, + http.MethodPut, + http.MethodPatch, + http.MethodDelete, + methodView: + default: + return nil, fmt.Errorf("only supports [%s %s %s %s %s %s]", + http.MethodGet, http.MethodPost, http.MethodPut, http.MethodPatch, http.MethodDelete, methodView) + } + + for k := 1; k < len(paths); k++ { + if paths[k] == empty && k+1 != len(paths) { + return nil, errors.New("pattern contains illegal empty path") + } + + if paths[k] == omitted && k+1 != len(paths) { + return nil, errors.New("pattern contains illegal omitted path") + } + } + + return paths, nil +} + +// Format pattern +func Format(pattern string) (string, error) { + paths, err := parse(pattern) + if err != nil { + return "", err + } + + return strings.Join(paths, delimiter), nil +} + +type section struct { + leaf bool + mapping map[string]*section +} + +func newSection() *section { + return §ion{mapping: make(map[string]*section)} +} + +// Table a (thread unsafe) table to store urls +type Table struct { + size int + root *section +} + +// NewTable create a table +func NewTable() *Table { + return &Table{root: newSection()} +} + +// Size contains how many urls +func (t *Table) Size() int { + return t.size +} + +// Append pattern +func (t *Table) Append(pattern string) error { + paths, err := parse(pattern) + if err != nil { + return err + } + + insert := false + root := t.root + for i, path := range paths { + if (path == fuzzy && root.mapping[omitted] != nil) || + (path == omitted && root.mapping[fuzzy] != nil) || + (path != omitted && root.mapping[omitted] != nil) { + return fmt.Errorf("conflict at %s", strings.Join(paths[:i], delimiter)) + } + + next := root.mapping[path] + if next == nil { + next = newSection() + root.mapping[path] = next + insert = true + } + root = next + } + + if insert { + t.size++ + } + + root.leaf = true + return nil +} + +// Mapping url to pattern +func (t *Table) Mapping(url string) (string, error) { + paths, err := parse(url) + if err != nil { + return "", err + } + + pattern := make([]string, 0, len(paths)) + + root := t.root + for _, path := range paths { + next := root.mapping[path] + if next == nil { + nextFuzzy := root.mapping[fuzzy] + nextOmitted := root.mapping[omitted] + if nextFuzzy == nil && nextOmitted == nil { + return "", nil + } + + if nextOmitted != nil { + pattern = append(pattern, omitted) + return strings.Join(pattern, delimiter), nil + } + + next = nextFuzzy + path = fuzzy + } + + root = next + pattern = append(pattern, path) + } + + if root.leaf { + return strings.Join(pattern, delimiter), nil + } + return "", nil +} diff --git a/pkg/validator/rule.go b/pkg/validator/rule.go new file mode 100644 index 0000000..035ba52 --- /dev/null +++ b/pkg/validator/rule.go @@ -0,0 +1,32 @@ +package validator + +import ( + "github.com/go-playground/validator/v10" + "regexp" + "time" +) + +func isoPhone(fl validator.FieldLevel) bool { + ok, _ := regexp.MatchString(`^\+(9[976]\d|8[987530]\d|6[987]\d|5[90]\d|42\d|3[875]\d|2[98654321]\d|9[8543210]|8[6421]|6[6543210]|5[87654321]|4[987654310]|3[9643210]|2[70]|7|1)\d{1,14}$`, fl.Field().String()) + return ok +} + +func chinesePhone(fl validator.FieldLevel) bool { + ok, _ := regexp.MatchString(`^1[3-9]\d{9}$`, fl.Field().String()) + return ok +} + +func idCard(fl validator.FieldLevel) bool { + isIdCard, _ := regexp.MatchString(`^[1-9]\d{7}((0\d)|(1[0-2]))(([0-2]\d)|3[0-1])\d{3}$|^[1-9]\d{5}[1-9]\d{3}((0\d)|(1[0-2]))(([0-2]\d)|3[0-1])\d{3}(\d|X)$`, fl.Field().String()) + return isIdCard +} + +func chineseName(fl validator.FieldLevel) bool { + var hzRegexp = regexp.MustCompile("^[\u4e00-\u9fa5]{2,8}$") + return hzRegexp.MatchString(fl.Field().String()) +} + +func year(fl validator.FieldLevel) bool { + _, err := time.Parse("2006", fl.Field().String()) + return err == nil +} diff --git a/pkg/validator/validator.go b/pkg/validator/validator.go new file mode 100644 index 0000000..fcbfa05 --- /dev/null +++ b/pkg/validator/validator.go @@ -0,0 +1,55 @@ +package validator + +import ( + "reflect" + "sync" + + "github.com/gin-gonic/gin/binding" + "github.com/go-playground/validator/v10" +) + +type DefaultValidator struct { + once sync.Once + validate *validator.Validate +} + +var Validator binding.StructValidator = &DefaultValidator{} + +func (v *DefaultValidator) ValidateStruct(obj any) error { + if kindOfData(obj) == reflect.Struct { + + v.lazyinit() + + if err := v.validate.Struct(obj); err != nil { + return err + } + } + return nil +} + +func (v *DefaultValidator) Engine() any { + v.lazyinit() + return v.validate +} + +func (v *DefaultValidator) lazyinit() { + v.once.Do(func() { + v.validate = validator.New() + v.validate.SetTagName("validate") + _ = v.validate.RegisterValidation("chinesePhone", chinesePhone) + _ = v.validate.RegisterValidation("isoPhone", isoPhone) + _ = v.validate.RegisterValidation("idCard", idCard) + _ = v.validate.RegisterValidation("chineseName", chineseName) + _ = v.validate.RegisterValidation("year", year) + }) +} + +func kindOfData(data any) reflect.Kind { + value := reflect.ValueOf(data) + valueType := value.Kind() + + if valueType == reflect.Ptr { + valueType = value.Elem().Kind() + } + return valueType +} diff --git a/pkg/websocket/client/client.go b/pkg/websocket/client/client.go new file mode 100644 index 0000000..f466cbf --- /dev/null +++ b/pkg/websocket/client/client.go @@ -0,0 +1,242 @@ +package client + +import ( + "errors" + "fmt" + "git.bvbej.com/bvbej/base-golang/pkg/ticker" + "git.bvbej.com/bvbej/base-golang/pkg/websocket/client/service" + "git.bvbej.com/bvbej/base-golang/pkg/websocket/codec" + _ "git.bvbej.com/bvbej/base-golang/pkg/websocket/codec/json" + _ "git.bvbej.com/bvbej/base-golang/pkg/websocket/codec/protobuf" + "git.bvbej.com/bvbej/base-golang/tool" + "github.com/gorilla/websocket" + "go.uber.org/zap" + "net/http" + "net/url" + "reflect" + "sync/atomic" + "time" +) + +const ( + writeWait = 20 * time.Second + pongWait = 60 * time.Second + reconnectWait = 3 * time.Second + pingPeriod = (pongWait * 9) / 10 + maxFrameMessageLen = 16 * 1024 + maxSendBuffer = 32 +) + +var ( + _ Client = (*client)(nil) + ErrBrokenPipe = errors.New("send to broken pipe") + ErrBufferPoolExceed = errors.New("send buffer exceed") +) + +type Client interface { + readLoop() + writeLoop() + ping() + reconnect() + onReceive(msg []byte) error + onSend(msg []byte) error + connect() error + + Send(router string, data any) error + Connect(requestHeader http.Header) error + OnReceiveError(f func(error)) + OnReconnected(f func(error)) + Close() +} + +type client struct { + url url.URL + requestHeader http.Header + logger *zap.Logger + session *service.Session + isConnected atomic.Bool + routerCodec codec.Codec + send chan []byte + handlers map[string]*service.Handler // 注册的方法列表 + onReceiveErr func(error) + pingTicker ticker.Ticker + checkConnTicker ticker.Ticker + onReconnect func(error) +} + +func New(logger *zap.Logger, url url.URL, decoder string, handlers any) (Client, error) { + if !tool.InArray(url.Scheme, []string{"ws", "wss"}) { + return nil, errors.New(`param: scheme not supported`) + } + + routerCodec := codec.GetCodec(decoder) + if routerCodec == nil { + return nil, errors.New(`param: codec not supported`) + } + + components := service.RegisterHandler(handlers) + if len(components) == 0 { + return nil, errors.New(`param: handlers unqualified`) + } + + c := &client{ + logger: logger, + isConnected: atomic.Bool{}, + routerCodec: routerCodec, + url: url, + send: make(chan []byte, maxSendBuffer), + handlers: components, + pingTicker: ticker.New(pingPeriod), + checkConnTicker: ticker.New(reconnectWait), + } + return c, nil +} + +func (c *client) readLoop() { + _ = c.session.Conn.SetReadDeadline(time.Now().Add(pongWait)) + c.session.Conn.SetPongHandler(func(string) error { + _ = c.session.Conn.SetReadDeadline(time.Now().Add(pongWait)) + return nil + }) + + for { + _, data, err := c.session.Conn.ReadMessage() + if err != nil { + c.isConnected.Store(false) + break + } + err = c.onReceive(data) + if err != nil && c.onReceiveErr != nil { + c.onReceiveErr(err) + } + } +} + +func (c *client) writeLoop() { + for msg := range c.send { + _ = c.session.Conn.SetWriteDeadline(time.Now().Add(writeWait)) + err := c.session.Conn.WriteMessage(websocket.BinaryMessage, msg) + if err != nil { + c.logger.Sugar().Errorf("writeLoop err: %s", err) + } + } +} + +func (c *client) ping() { + c.pingTicker.Process(func() { + _ = c.session.Conn.SetWriteDeadline(time.Now().Add(writeWait)) + _ = c.session.Conn.WriteMessage(websocket.PingMessage, nil) + }) +} + +func (c *client) reconnect() { + c.checkConnTicker.Process(func() { + if c.isConnected.Load() { + return + } + err := c.connect() + if c.onReconnect != nil { + c.onReconnect(err) + } + }) +} + +func (c *client) connect() error { + conn, _, err := websocket.DefaultDialer.Dial(c.url.String(), c.requestHeader) + if err != nil { + return fmt.Errorf("dial: %s", err) + } + + c.session = service.NewSession(conn) + c.isConnected.Store(true) + + go c.readLoop() + + return nil +} + +func (c *client) onReceive(msg []byte) error { + _, msgPack, err := c.routerCodec.Unmarshal(msg) + if err != nil { + return fmt.Errorf("onreceive: %v", err) + } + router, ok := msgPack.Router.(string) + if !ok { + return fmt.Errorf("onreceive: invalid router:%v", msgPack.Router) + } + s, ok := c.handlers[router] + if !ok { + return fmt.Errorf("onreceive: function not registed router:%s err:%v", msgPack.Router, err) + } + + if msgPack.Err != nil { + return fmt.Errorf("%s:%s", router, msgPack.Err) + } + + var args = []reflect.Value{s.Receiver, reflect.ValueOf(c.session), reflect.ValueOf(msgPack.DataPtr)} + s.Method.Func.Call(args) + + return nil +} + +func (c *client) onSend(msg []byte) (err error) { + defer func() { + if e := recover(); e != nil { + err = ErrBrokenPipe + } + }() + + if !c.isConnected.Load() { + return ErrBrokenPipe + } + + if len(c.send) >= maxSendBuffer { + return ErrBufferPoolExceed + } + + if len(msg) > maxFrameMessageLen { + return + } + + c.send <- msg + + return nil +} + +func (c *client) Connect(requestHeader http.Header) error { + c.requestHeader = requestHeader + + err := c.connect() + if err != nil { + return err + } + + go c.ping() + go c.writeLoop() + go c.reconnect() + + return nil +} + +func (c *client) Send(router string, data any) error { + rb, err := c.routerCodec.Marshal(router, data, nil) + if err != nil { + return fmt.Errorf("service: %v", err) + } + return c.onSend(rb) +} + +func (c *client) OnReceiveError(f func(error)) { + c.onReceiveErr = f +} + +func (c *client) OnReconnected(f func(error)) { + c.onReconnect = f +} + +func (c *client) Close() { + close(c.send) + c.pingTicker.Stop() + c.checkConnTicker.Stop() + _ = c.session.Conn.Close() +} diff --git a/pkg/websocket/client/service/method.go b/pkg/websocket/client/service/method.go new file mode 100644 index 0000000..bab0dec --- /dev/null +++ b/pkg/websocket/client/service/method.go @@ -0,0 +1,28 @@ +package service + +import ( + "reflect" +) + +var ( + typeOfBytes = reflect.TypeOf(([]byte)(nil)) + typeOfSession = reflect.TypeOf(NewSession(nil)) +) + +// 方法检测 +func isHandlerMethod(method reflect.Method) bool { + mt := method.Type + if method.PkgPath != "" { + return false + } + if mt.NumIn() != 3 { + return false + } + if t1 := mt.In(1); t1.Kind() != reflect.Ptr || t1 != typeOfSession { + return false + } + if mt.In(2).Kind() != reflect.Ptr && mt.In(2) != typeOfBytes { + return false + } + return true +} diff --git a/pkg/websocket/client/service/peer.go b/pkg/websocket/client/service/peer.go new file mode 100644 index 0000000..6896ca8 --- /dev/null +++ b/pkg/websocket/client/service/peer.go @@ -0,0 +1,19 @@ +package service + +import ( + "github.com/gorilla/websocket" + "time" +) + +type Session struct { + Conn *websocket.Conn + Time time.Time +} + +func NewSession(conn *websocket.Conn) *Session { + s := &Session{ + Conn: conn, + Time: time.Now(), + } + return s +} diff --git a/pkg/websocket/client/service/receiver.go b/pkg/websocket/client/service/receiver.go new file mode 100644 index 0000000..b3f8f3b --- /dev/null +++ b/pkg/websocket/client/service/receiver.go @@ -0,0 +1,57 @@ +package service + +import ( + "fmt" + "git.bvbej.com/bvbej/base-golang/pkg/websocket/codec" + "git.bvbej.com/bvbej/base-golang/pkg/websocket/util" + "reflect" + "strings" +) + +type Handler struct { + Receiver reflect.Value // 值 + Method reflect.Method // 方法 + Type reflect.Type // 类型 + IsRawArg bool // 数据是否需要序列化 +} + +func RegisterHandler(components ...any) map[string]*Handler { + methods := make(map[string]*Handler) + for _, component := range components { + rt := reflect.TypeOf(component) + rv := reflect.ValueOf(component) + + typeName := reflect.Indirect(rv).Type().Name() + if typeName == "" { + continue + } + if !util.IsExported(typeName) { + continue + } + + for m := 0; m < rt.NumMethod(); m++ { + method := rt.Method(m) + mt := method.Type + mn := method.Name + if isHandlerMethod(method) { + raw := false + if mt.In(2) == typeOfBytes { + raw = true + } + router := fmt.Sprintf("%s.%s", strings.ToLower(typeName), strings.ToLower(mn)) + methods[router] = &Handler{ + Receiver: rv, + Method: method, + Type: mt.In(2), + IsRawArg: raw, + } + } + } + } + + for router, handler := range methods { + codec.RegisterMessage(router, handler.Type) + } + + return methods +} diff --git a/pkg/websocket/codec/codec.go b/pkg/websocket/codec/codec.go new file mode 100644 index 0000000..e819338 --- /dev/null +++ b/pkg/websocket/codec/codec.go @@ -0,0 +1,26 @@ +package codec + +type Codec interface { + Marshal(router string, dataPtr any, err error) ([]byte, error) + Unmarshal([]byte) (int, *MsgPack, error) + ToString(any) string +} + +var codecsList = make(map[string]Codec) + +func RegisterCodec(name string, codec Codec) { + if codec == nil { + panic("codec: Register provide is nil") + } + if _, dup := codecsList[name]; dup { + panic("codec: Register called twice for provide " + name) + } + codecsList[name] = codec +} + +func GetCodec(name string) Codec { + if v, ok := codecsList[name]; ok { + return v + } + return nil +} diff --git a/pkg/websocket/codec/json/json.go b/pkg/websocket/codec/json/json.go new file mode 100644 index 0000000..3c575c3 --- /dev/null +++ b/pkg/websocket/codec/json/json.go @@ -0,0 +1,97 @@ +package json + +import ( + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "git.bvbej.com/bvbej/base-golang/pkg/websocket/codec" +) + +type jsonCodec struct{} + +type jsonReq struct { + Router string `json:"router"` + Data []byte `json:"data"` + Error string `json:"error,omitempty"` +} + +type jsonAck struct { + Router string `json:"router"` + Data string `json:"data"` + Error string `json:"error,omitempty"` +} + +func init() { + codec.RegisterCodec("json_codec", new(jsonCodec)) +} + +func (*jsonCodec) Marshal(router string, dataPtr any, retErr error) ([]byte, error) { + if router == "" { + return nil, fmt.Errorf("marshal: router is empty") + } + + if dataPtr == nil && retErr == nil { + return nil, fmt.Errorf("marshal data in package is nil. router:%s dt:%T", router, dataPtr) + } + + ack := &jsonAck{ + Router: router, + } + + if dataPtr != nil { + data, err := json.Marshal(dataPtr) + if err != nil { + return nil, fmt.Errorf("marshal json marshal failed. routerr:%s dt:%T err:%v", router, dataPtr, err) + } + ack.Data = base64.StdEncoding.EncodeToString(data) + } + + if retErr != nil { + ack.Error = retErr.Error() + } + + ackByte, err := json.Marshal(ack) + if err != nil { + return nil, fmt.Errorf("marshal json marshal failed. routerr:%s dt:%T err:%v", router, dataPtr, err) + } + return ackByte, nil +} + +func (*jsonCodec) Unmarshal(msg []byte) (int, *codec.MsgPack, error) { + var i = len(msg) + + req := &jsonReq{} + err := json.Unmarshal(msg, req) + if err != nil { + return i, nil, errors.New("unmarshal split message id failed") + } + + var router = req.Router + msgPack := &codec.MsgPack{Router: router} + dt := codec.GetMessage(router) + if dt == nil { + return i, nil, fmt.Errorf("unmarshal message not registed. router:%s", router) + } + + if req.Data != nil { + err = json.Unmarshal(req.Data, dt) + if err != nil { + return i, nil, fmt.Errorf("unmarshal json unmarshal failed. dt:%T msg:%s err:%v", dt, string(msg), err) + } + } + msgPack.DataPtr = dt + if req.Error != "" { + msgPack.Err = errors.New(req.Error) + } + + return i, msgPack, nil +} + +func (*jsonCodec) ToString(data any) string { + ab, err := json.Marshal(data) + if err != nil { + return fmt.Sprintf("invalid type %T", data) + } + return string(ab) +} diff --git a/pkg/websocket/codec/meta.go b/pkg/websocket/codec/meta.go new file mode 100644 index 0000000..7435f91 --- /dev/null +++ b/pkg/websocket/codec/meta.go @@ -0,0 +1,43 @@ +package codec + +import ( + "fmt" + "reflect" + "sync" +) + +type MsgPack struct { + Router any + DataPtr any + Err error +} + +var modelMap = make(map[any]reflect.Type) +var modelMapLock sync.RWMutex + +func RegisterMessage(router any, datePtr any) { + modelMapLock.Lock() + defer modelMapLock.Unlock() + if _, ok := modelMap[router]; ok { + fmt.Println(fmt.Sprintf("codec: repeat registration. router:%s ", router)) + return + } + if t, ok := datePtr.(reflect.Type); ok { + modelMap[router] = t.Elem() + } else { + t := reflect.TypeOf(datePtr) + if t.Kind() != reflect.Ptr { + panic(fmt.Errorf("codec: cannot use non-ptr message struct `%s`", t)) + } + modelMap[router] = t.Elem() + } +} + +func GetMessage(router any) any { + modelMapLock.RLock() + defer modelMapLock.RUnlock() + if ptr, ok := modelMap[router]; ok { + return reflect.New(ptr).Interface() + } + return nil +} diff --git a/pkg/websocket/codec/protobuf/protobuf.go b/pkg/websocket/codec/protobuf/protobuf.go new file mode 100644 index 0000000..7e8a664 --- /dev/null +++ b/pkg/websocket/codec/protobuf/protobuf.go @@ -0,0 +1,85 @@ +package protobuf + +import ( + "errors" + "fmt" + + "git.bvbej.com/bvbej/base-golang/pkg/websocket/codec" + "git.bvbej.com/bvbej/base-golang/pkg/websocket/codec/protobuf/protocol" + "google.golang.org/protobuf/proto" +) + +type protobufCodec struct{} + +func init() { + codec.RegisterCodec("protobuf_codec", new(protobufCodec)) +} + +func (*protobufCodec) Marshal(router string, dataPtr any, retErr error) ([]byte, error) { + if router == "" { + return nil, fmt.Errorf("marshal: empty router") + } + if dataPtr == nil && retErr == nil { + return nil, fmt.Errorf("marshal: empty data") + } + ack := &protocol.TransPack{ + Router: router, + } + if dataPtr != nil { + pbMsg, ok := dataPtr.(proto.Message) + if !ok { + return nil, fmt.Errorf("marshal: dataptr only support proto.Message type. router:%s dt:%T ", + router, dataPtr) + } + data, err := proto.Marshal(pbMsg) + if err != nil { + return nil, fmt.Errorf("marshal:protocol buffer marshal failed. router:%s dt:%T err:%v", + router, dataPtr, err) + } + ack.Data = data + } else { + ack.Error = retErr.Error() + } + ackByte, err := proto.Marshal(ack) + if err != nil { + return nil, fmt.Errorf("marshal:protocol buffer marshal failed. router:%s dt:%T err:%v", + router, ack, err) + } + return ackByte, nil +} + +func (*protobufCodec) Unmarshal(msg []byte) (int, *codec.MsgPack, error) { + var l = len(msg) + req := &protocol.TransPack{} + err := proto.Unmarshal(msg, req) + if err != nil { + return l, nil, errors.New("unmarshal split message id failed.") + } + var router = req.Router + msgPack := &codec.MsgPack{Router: router} + dt := codec.GetMessage(router) + if dt == nil { + return l, nil, fmt.Errorf("unmarshal message not registed. router:%s", + router) + } + if req.Data != nil { + err = proto.Unmarshal(req.Data, dt.(proto.Message)) + if err != nil { + return l, nil, fmt.Errorf("unmarshal failed. router:%s", router) + } + } + msgPack.DataPtr = dt + if req.Error != "" { + msgPack.Err = errors.New(req.Error) + } + return l, msgPack, nil +} + +func (*protobufCodec) ToString(data any) string { + pbMsg, ok := data.(proto.Message) + if !ok { + return fmt.Sprintf("invalid type %T", data) + } + marshal, _ := proto.Marshal(pbMsg) + return string(marshal) +} diff --git a/pkg/websocket/codec/protobuf/protocol/base.pb.go b/pkg/websocket/codec/protobuf/protocol/base.pb.go new file mode 100644 index 0000000..0f1fdd3 --- /dev/null +++ b/pkg/websocket/codec/protobuf/protocol/base.pb.go @@ -0,0 +1,226 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.26.0 +// protoc v3.21.1 +// source: base.proto + +package protocol + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// 通信包装 +type TransPack struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Router string `protobuf:"bytes,1,opt,name=router,proto3" json:"router,omitempty"` + Data []byte `protobuf:"bytes,2,opt,name=data,proto3" json:"data,omitempty"` + Error string `protobuf:"bytes,3,opt,name=error,proto3" json:"error,omitempty"` +} + +func (x *TransPack) Reset() { + *x = TransPack{} + if protoimpl.UnsafeEnabled { + mi := &file_base_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *TransPack) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TransPack) ProtoMessage() {} + +func (x *TransPack) ProtoReflect() protoreflect.Message { + mi := &file_base_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use TransPack.ProtoReflect.Descriptor instead. +func (*TransPack) Descriptor() ([]byte, []int) { + return file_base_proto_rawDescGZIP(), []int{0} +} + +func (x *TransPack) GetRouter() string { + if x != nil { + return x.Router + } + return "" +} + +func (x *TransPack) GetData() []byte { + if x != nil { + return x.Data + } + return nil +} + +func (x *TransPack) GetError() string { + if x != nil { + return x.Error + } + return "" +} + +// 连接检测 +type PingPang struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Timestamp int64 `protobuf:"varint,1,opt,name=timestamp,proto3" json:"timestamp,omitempty"` //时间戳 +} + +func (x *PingPang) Reset() { + *x = PingPang{} + if protoimpl.UnsafeEnabled { + mi := &file_base_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *PingPang) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PingPang) ProtoMessage() {} + +func (x *PingPang) ProtoReflect() protoreflect.Message { + mi := &file_base_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PingPang.ProtoReflect.Descriptor instead. +func (*PingPang) Descriptor() ([]byte, []int) { + return file_base_proto_rawDescGZIP(), []int{1} +} + +func (x *PingPang) GetTimestamp() int64 { + if x != nil { + return x.Timestamp + } + return 0 +} + +var File_base_proto protoreflect.FileDescriptor + +var file_base_proto_rawDesc = []byte{ + 0x0a, 0x0a, 0x62, 0x61, 0x73, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x08, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x22, 0x4d, 0x0a, 0x09, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x50, + 0x61, 0x63, 0x6b, 0x12, 0x16, 0x0a, 0x06, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x06, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x72, 0x12, 0x12, 0x0a, 0x04, 0x64, + 0x61, 0x74, 0x61, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x64, 0x61, 0x74, 0x61, 0x12, + 0x14, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, + 0x65, 0x72, 0x72, 0x6f, 0x72, 0x22, 0x28, 0x0a, 0x08, 0x50, 0x69, 0x6e, 0x67, 0x50, 0x61, 0x6e, + 0x67, 0x12, 0x1c, 0x0a, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x42, + 0x27, 0x5a, 0x25, 0x70, 0x6b, 0x67, 0x2f, 0x77, 0x65, 0x62, 0x73, 0x6f, 0x63, 0x6b, 0x65, 0x74, + 0x2f, 0x63, 0x6f, 0x64, 0x65, 0x63, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_base_proto_rawDescOnce sync.Once + file_base_proto_rawDescData = file_base_proto_rawDesc +) + +func file_base_proto_rawDescGZIP() []byte { + file_base_proto_rawDescOnce.Do(func() { + file_base_proto_rawDescData = protoimpl.X.CompressGZIP(file_base_proto_rawDescData) + }) + return file_base_proto_rawDescData +} + +var file_base_proto_msgTypes = make([]protoimpl.MessageInfo, 2) +var file_base_proto_goTypes = []any{ + (*TransPack)(nil), // 0: protocol.TransPack + (*PingPang)(nil), // 1: protocol.PingPang +} +var file_base_proto_depIdxs = []int32{ + 0, // [0:0] is the sub-list for method output_type + 0, // [0:0] is the sub-list for method input_type + 0, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name +} + +func init() { file_base_proto_init() } +func file_base_proto_init() { + if File_base_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_base_proto_msgTypes[0].Exporter = func(v any, i int) any { + switch v := v.(*TransPack); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_base_proto_msgTypes[1].Exporter = func(v any, i int) any { + switch v := v.(*PingPang); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_base_proto_rawDesc, + NumEnums: 0, + NumMessages: 2, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_base_proto_goTypes, + DependencyIndexes: file_base_proto_depIdxs, + MessageInfos: file_base_proto_msgTypes, + }.Build() + File_base_proto = out.File + file_base_proto_rawDesc = nil + file_base_proto_goTypes = nil + file_base_proto_depIdxs = nil +} diff --git a/pkg/websocket/codec/protobuf/protocol/base.proto b/pkg/websocket/codec/protobuf/protocol/base.proto new file mode 100644 index 0000000..bedf617 --- /dev/null +++ b/pkg/websocket/codec/protobuf/protocol/base.proto @@ -0,0 +1,15 @@ +syntax = "proto3"; +package protocol; +option go_package = "pkg/websocket/codec/protobuf/protocol"; + +//通信包装 +message TransPack { + string router = 1; + bytes data = 2; + string error = 3; +} + +//连接检测 +message PingPang { + int64 timestamp = 1; //时间戳 +} \ No newline at end of file diff --git a/pkg/websocket/peer/callback.go b/pkg/websocket/peer/callback.go new file mode 100644 index 0000000..d205767 --- /dev/null +++ b/pkg/websocket/peer/callback.go @@ -0,0 +1,6 @@ +package peer + +type ConnectionCallBack interface { + OnClosed(*Session) + OnReceive(*Session, []byte) error +} diff --git a/pkg/websocket/peer/connect/acceptor.go b/pkg/websocket/peer/connect/acceptor.go new file mode 100644 index 0000000..ba30860 --- /dev/null +++ b/pkg/websocket/peer/connect/acceptor.go @@ -0,0 +1,98 @@ +package connect + +import ( + "context" + "errors" + "fmt" + "git.bvbej.com/bvbej/base-golang/pkg/mux" + "github.com/gin-gonic/gin" + "go.uber.org/zap" + "net/http" + "net/url" + "time" + + "git.bvbej.com/bvbej/base-golang/pkg/websocket/peer" + "github.com/gorilla/websocket" +) + +var _ WsAcceptor = (*wsAcceptor)(nil) + +var upgrader = websocket.Upgrader{ + CheckOrigin: func(r *http.Request) bool { + return true + }, +} + +type WsAcceptor interface { + Start(addr string) error + Stop() + GinHandle(ctx *gin.Context) + HandlerFunc() mux.HandlerFunc +} + +type wsAcceptor struct { + server *http.Server + sessMgr *peer.SessionManager + logger *zap.Logger +} + +func NewWsAcceptor(sessMgr *peer.SessionManager, loggers *zap.Logger) WsAcceptor { + return &wsAcceptor{ + sessMgr: sessMgr, + logger: loggers, + } +} + +func (ws *wsAcceptor) Start(addr string) error { + urlObj, err := url.Parse(addr) + if err != nil { + return fmt.Errorf("websocket urlparse failed. url(%s) %v", addr, err) + } + if urlObj.Path == "" { + return fmt.Errorf("websocket start failed. expect path in url to listen addr:%s", addr) + } + + http.HandleFunc(urlObj.Path, func(w http.ResponseWriter, r *http.Request) { + c, upgradeErr := upgrader.Upgrade(w, r, nil) + if upgradeErr != nil { + ws.logger.Sugar().Errorf("upgrade http failed: %s", upgradeErr) + return + } + ws.sessMgr.Register <- peer.NewSession(NewConnection(c, ws.sessMgr)) + }) + + ws.server = &http.Server{Addr: urlObj.Host} + err = ws.server.ListenAndServe() + if err != nil && !errors.Is(err, http.ErrServerClosed) { + return fmt.Errorf("websocket ListenAndServe addr:%s failed:%v", addr, err) + } + + return nil +} + +func (ws *wsAcceptor) Stop() { + ws.sessMgr.CloseAllSession() + + if ws.server != nil { + ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) + defer cancel() + if err := ws.server.Shutdown(ctx); err != nil { + ws.logger.Sugar().Errorf("server shutdown err:[%s]", err) + } + } +} + +func (ws *wsAcceptor) GinHandle(ctx *gin.Context) { + c, upgradeErr := upgrader.Upgrade(ctx.Writer, ctx.Request, nil) + if upgradeErr != nil { + ws.logger.Sugar().Errorf("upgrade http failed: %s", upgradeErr) + return + } + ws.sessMgr.Register <- peer.NewSession(NewConnection(c, ws.sessMgr)) +} + +func (ws *wsAcceptor) HandlerFunc() mux.HandlerFunc { + return func(c mux.Context) { + ws.GinHandle(c.Context()) + } +} diff --git a/pkg/websocket/peer/connect/connection.go b/pkg/websocket/peer/connect/connection.go new file mode 100644 index 0000000..0a51496 --- /dev/null +++ b/pkg/websocket/peer/connect/connection.go @@ -0,0 +1,134 @@ +package connect + +import ( + "errors" + "time" + + "git.bvbej.com/bvbej/base-golang/pkg/websocket/peer" + "github.com/gorilla/websocket" +) + +const ( + writeWait = 20 * time.Second + pongWait = 60 * time.Second + pingPeriod = (pongWait * 9) / 10 + maxFrameMessageLen = 16 * 1024 //4 * 4096 + maxSendBuffer = 16 +) + +var ( + ErrBrokenPipe = errors.New("send to broken pipe") + ErrBufferPoolExceed = errors.New("send buffer exceed") +) + +type wsConnection struct { + peer.ConnectionIdentify + pm *peer.SessionManager + conn *websocket.Conn + send chan []byte + running bool +} + +func NewConnection(conn *websocket.Conn, p *peer.SessionManager) *wsConnection { + wsc := &wsConnection{ + conn: conn, + pm: p, + send: make(chan []byte, maxSendBuffer), + running: true, + } + + go wsc.acceptLoop() + go wsc.sendLoop() + + return wsc +} + +func (ws *wsConnection) Peer() *peer.SessionManager { + return ws.pm +} + +func (ws *wsConnection) Raw() any { + if ws.conn == nil { + return nil + } + return ws.conn +} + +func (ws *wsConnection) RemoteAddr() string { + if ws.conn == nil { + return "" + } + return ws.conn.RemoteAddr().String() +} + +func (ws *wsConnection) Close() { + _ = ws.conn.Close() + ws.running = false +} + +func (ws *wsConnection) Send(msg []byte) (err error) { + defer func() { + if e := recover(); e != nil { + err = ErrBrokenPipe + } + }() + if !ws.running { + return ErrBrokenPipe + } + if len(ws.send) >= maxSendBuffer { + return ErrBufferPoolExceed + } + if len(msg) > maxFrameMessageLen { + return + } + ws.send <- msg + return nil +} + +func (ws *wsConnection) acceptLoop() { + defer func() { + ws.pm.Unregister <- ws.ID() + _ = ws.conn.Close() + ws.running = false + }() + _ = ws.conn.SetReadDeadline(time.Now().Add(pongWait)) + ws.conn.SetPongHandler(func(string) error { + _ = ws.conn.SetReadDeadline(time.Now().Add(pongWait)) + return nil + }) + for ws.conn != nil { + _, data, err := ws.conn.ReadMessage() + if err != nil { + break + } + ws.pm.ProcessMessage(ws.ID(), data) + } +} + +func (ws *wsConnection) sendLoop() { + ticker := time.NewTicker(pingPeriod) + defer func() { + ticker.Stop() + _ = ws.conn.Close() + ws.running = false + close(ws.send) + }() + for { + select { + case msg := <-ws.send: + _ = ws.conn.SetWriteDeadline(time.Now().Add(writeWait)) + if err := ws.conn.WriteMessage(websocket.BinaryMessage, msg); err != nil { + return + } + case <-ticker.C: + _ = ws.conn.SetWriteDeadline(time.Now().Add(writeWait)) + if err := ws.conn.WriteMessage(websocket.PingMessage, nil); err != nil { + return + } + } + } +} + +func (ws *wsConnection) IsClosed() bool { + return !ws.running +} diff --git a/pkg/websocket/peer/connection.go b/pkg/websocket/peer/connection.go new file mode 100644 index 0000000..e21b0cf --- /dev/null +++ b/pkg/websocket/peer/connection.go @@ -0,0 +1,23 @@ +package peer + +type Connection interface { + Raw() any + Peer() *SessionManager + Send(msg []byte) error + Close() + ID() int64 + RemoteAddr() string + IsClosed() bool +} + +type ConnectionIdentify struct { + id int64 +} + +func (ci *ConnectionIdentify) ID() int64 { + return ci.id +} + +func (ci *ConnectionIdentify) SetID(id int64) { + ci.id = id +} diff --git a/pkg/websocket/peer/session.go b/pkg/websocket/peer/session.go new file mode 100644 index 0000000..3b55113 --- /dev/null +++ b/pkg/websocket/peer/session.go @@ -0,0 +1,16 @@ +package peer + +import "time" + +type Session struct { + Conn Connection + Time time.Time +} + +func NewSession(conn Connection) *Session { + s := &Session{ + Conn: conn, + Time: time.Now(), + } + return s +} diff --git a/pkg/websocket/peer/session_manager.go b/pkg/websocket/peer/session_manager.go new file mode 100644 index 0000000..67da7f3 --- /dev/null +++ b/pkg/websocket/peer/session_manager.go @@ -0,0 +1,119 @@ +package peer + +import ( + "sync" + "sync/atomic" +) + +type SessionManager struct { + sessionList sync.Map // 使用Id关联会话 + + connIDGen int64 // 记录已经生成的会话ID流水号 + + count int64 // 记录当前在使用的会话数量 + + callback ConnectionCallBack + Register chan *Session + Unregister chan int64 +} + +func (mgr *SessionManager) SetIDBase(base int64) { + + atomic.StoreInt64(&mgr.connIDGen, base) +} + +func (mgr *SessionManager) Count() int { + return int(atomic.LoadInt64(&mgr.count)) +} + +func (mgr *SessionManager) Add(sess *Session) { + + id := atomic.AddInt64(&mgr.connIDGen, 1) + + atomic.AddInt64(&mgr.count, 1) + + sess.Conn.(interface { + SetID(int64) + }).SetID(id) + + mgr.sessionList.Store(id, sess) +} + +func (mgr *SessionManager) Close(id int64) { + if v, ok := mgr.sessionList.Load(id); ok { + if mgr.callback != nil { + go mgr.callback.OnClosed(v.(*Session)) + } + } + mgr.sessionList.Delete(id) + atomic.AddInt64(&mgr.count, -1) +} + +func (mgr *SessionManager) ProcessMessage(id int64, msg []byte) { + if v, ok := mgr.sessionList.Load(id); ok { + if mgr.callback != nil { + go func() { + err := mgr.callback.OnReceive(v.(*Session), msg) + if err != nil { + v.(*Session).Conn.Close() + } + }() + } + } +} + +func (mgr *SessionManager) run() { + for { + select { + case client := <-mgr.Register: + mgr.connIDGen++ + mgr.count++ + client.Conn.(interface { + SetID(int64) + }).SetID(mgr.connIDGen) + mgr.sessionList.Store(mgr.connIDGen, client) + case clientID := <-mgr.Unregister: + if v, ok := mgr.sessionList.Load(clientID); ok { + if mgr.callback != nil { + go mgr.callback.OnClosed(v.(*Session)) + } + } + mgr.sessionList.Delete(clientID) + mgr.count-- + } + } +} + +func (mgr *SessionManager) GetSession(id int64) *Session { + if v, ok := mgr.sessionList.Load(id); ok { + return v.(*Session) + } + return nil +} + +func (mgr *SessionManager) VisitSession(callback func(*Session) bool) { + mgr.sessionList.Range(func(key, value any) bool { + return callback(value.(*Session)) + }) +} + +func (mgr *SessionManager) CloseAllSession() { + mgr.VisitSession(func(sess *Session) bool { + sess.Conn.Close() + return true + }) +} + +func (mgr *SessionManager) SessionCount() int64 { + return atomic.LoadInt64(&mgr.count) +} + +func NewSessionMgr(callback ConnectionCallBack) *SessionManager { + s := &SessionManager{ + callback: callback, + Register: make(chan *Session), + Unregister: make(chan int64), + } + go s.run() + return s +} diff --git a/pkg/websocket/service/component.go b/pkg/websocket/service/component.go new file mode 100644 index 0000000..8acf9b1 --- /dev/null +++ b/pkg/websocket/service/component.go @@ -0,0 +1,27 @@ +package service + +import ( + "time" + + "git.bvbej.com/bvbej/base-golang/pkg/websocket/peer" +) + +type Component interface { + Init() + OnSessionClose(*peer.Session) bool + OnRequestFinished(*peer.Session, string, any, string, time.Duration) +} + +type ComponentBase struct{} + +func (c *ComponentBase) Init() { + +} + +func (c *ComponentBase) OnSessionClose(session *peer.Session) bool { + return false +} + +func (c *ComponentBase) OnRequestFinished(session *peer.Session, router string, req any, errMsg string, delta time.Duration) { + +} diff --git a/pkg/websocket/service/method.go b/pkg/websocket/service/method.go new file mode 100644 index 0000000..99c17b9 --- /dev/null +++ b/pkg/websocket/service/method.go @@ -0,0 +1,33 @@ +package service + +import ( + "git.bvbej.com/bvbej/base-golang/pkg/websocket/peer" + "reflect" +) + +var ( + typeOfError = reflect.TypeOf((*error)(nil)).Elem() + typeOfBytes = reflect.TypeOf(([]byte)(nil)) + typeOfSession = reflect.TypeOf(peer.NewSession(nil)) +) + +// 方法检测 +func isHandlerMethod(method reflect.Method) bool { + mt := method.Type + if method.PkgPath != "" { + return false + } + if mt.NumIn() != 3 { + return false + } + if mt.NumOut() != 2 { + return false + } + if t1 := mt.In(1); t1.Kind() != reflect.Ptr || t1 != typeOfSession { + return false + } + if (mt.In(2).Kind() != reflect.Ptr && mt.In(2) != typeOfBytes) || mt.Out(1) != typeOfError || mt.Out(0).Kind() != reflect.Ptr { + return false + } + return true +} diff --git a/pkg/websocket/service/service.go b/pkg/websocket/service/service.go new file mode 100644 index 0000000..069acac --- /dev/null +++ b/pkg/websocket/service/service.go @@ -0,0 +1,98 @@ +package service + +import ( + "errors" + "git.bvbej.com/bvbej/base-golang/pkg/websocket/util" + "reflect" + "strings" + + "git.bvbej.com/bvbej/base-golang/pkg/websocket/peer" +) + +type Handler struct { + Receiver reflect.Value // 值 + Method reflect.Method // 方法 + Type reflect.Type // 类型 + IsRawArg bool // 数据是否需要序列化 +} + +type Service struct { + Name string // 服务名 + Type reflect.Type // 服务类型 + Receiver reflect.Value // 服务值 + Handlers map[string]*Handler // 注册的方法列表 + Component Component +} + +func NewService(comp Component) *Service { + s := &Service{ + Type: reflect.TypeOf(comp), + Receiver: reflect.ValueOf(comp), + Component: comp, + } + s.Name = strings.ToLower(reflect.Indirect(s.Receiver).Type().Name()) + //调用初始化方法 + s.Component.Init() + return s +} + +func (s *Service) SuitableHandlerMethods(typ reflect.Type) map[string]*Handler { + methods := make(map[string]*Handler) + for m := 0; m < typ.NumMethod(); m++ { + method := typ.Method(m) + mt := method.Type + mn := method.Name + if isHandlerMethod(method) { + raw := false + if mt.In(2) == typeOfBytes { + raw = true + } + mn = strings.ToLower(mn) + methods[mn] = &Handler{Method: method, Type: mt.In(2), IsRawArg: raw} + } + } + return methods +} + +func (s *Service) ExtractHandler() error { + typeName := reflect.Indirect(s.Receiver).Type().Name() + if typeName == "" { + return errors.New("no service name for type " + s.Type.String()) + } + if !util.IsExported(typeName) { + return errors.New("type " + typeName + " is not exported") + } + s.Handlers = s.SuitableHandlerMethods(s.Type) + for i := range s.Handlers { + s.Handlers[i].Receiver = s.Receiver + } + if reflect.Indirect(s.Receiver).NumField() > 0 { + filedNum := reflect.Indirect(s.Receiver).NumField() + for i := 0; i < filedNum; i++ { + ty := reflect.Indirect(s.Receiver).Field(i).Type().Name() + if ty == ChildName { + h := s.SuitableHandlerMethods(reflect.Indirect(s.Receiver).Field(i).Elem().Type()) + for ih, v := range h { + s.Handlers[ih] = v + s.Handlers[ih].Receiver = reflect.Indirect(s.Receiver).Field(i).Elem() + } + } + } + } + if len(s.Handlers) == 0 { + str := "service: " + method := s.SuitableHandlerMethods(reflect.PtrTo(s.Type)) + if len(method) != 0 { + str = "type " + s.Name + " has no exported methods of suitable type (hint: pass a pointer to value of that type)" + } else { + str = "type " + s.Name + " has no exported methods of suitable type" + } + return errors.New(str) + } + + return nil +} + +func (s *Service) OnSessionClose(session *peer.Session) bool { + return s.Component.OnSessionClose(session) +} diff --git a/pkg/websocket/service/service_manager.go b/pkg/websocket/service/service_manager.go new file mode 100644 index 0000000..9b2cb09 --- /dev/null +++ b/pkg/websocket/service/service_manager.go @@ -0,0 +1,59 @@ +package service + +import ( + "fmt" + "git.bvbej.com/bvbej/base-golang/pkg/websocket/codec" + _ "git.bvbej.com/bvbej/base-golang/pkg/websocket/codec/json" + "git.bvbej.com/bvbej/base-golang/pkg/websocket/peer" + "go.uber.org/zap" +) + +var ( + serviceLogger *zap.Logger + + RegisteredServiceList = make(map[string]*Service) // all registered service + RouterCodec codec.Codec +) + +const ChildName = "Hbase" + +type Hbase any + +func RegisterService(logger *zap.Logger, comp ...Component) { + serviceLogger = logger + for _, v := range comp { + s := NewService(v) + if _, ok := RegisteredServiceList[s.Name]; ok { + serviceLogger.Sugar().Errorf("service: service already defined: %s", s.Name) + } + if err := s.ExtractHandler(); err != nil { + serviceLogger.Sugar().Errorf("service: extract handler function failed: %v", err) + } + RegisteredServiceList[s.Name] = s + for name, handler := range s.Handlers { + router := fmt.Sprintf("%s.%s", s.Name, name) + //注册消息 用于解码 + codec.RegisterMessage(router, handler.Type) + serviceLogger.Sugar().Debugf("service: router %s param %s registed", router, handler.Type) + } + } +} + +func SetCodec(name string) error { + RouterCodec = codec.GetCodec(name) + if RouterCodec == nil { + return fmt.Errorf("service: codec %s not registered", name) + } + return nil +} + +func Send(session *peer.Session, router string, data any) error { + if RouterCodec == nil { + return fmt.Errorf("service: codec not set") + } + rb, err := RouterCodec.Marshal(router, data, nil) + if err != nil { + return fmt.Errorf("service: %v", err) + } + return session.Conn.Send(rb) +} diff --git a/pkg/websocket/service/session_callback.go b/pkg/websocket/service/session_callback.go new file mode 100644 index 0000000..300885a --- /dev/null +++ b/pkg/websocket/service/session_callback.go @@ -0,0 +1,102 @@ +package service + +import ( + "fmt" + "reflect" + "strings" + "time" + + "git.bvbej.com/bvbej/base-golang/pkg/websocket/peer" +) + +type callBackEntity struct{} + +func GetSessionManager() *peer.SessionManager { + return peer.NewSessionMgr(&callBackEntity{}) +} + +func (cb *callBackEntity) OnClosed(session *peer.Session) { + defer func() { + if err := recover(); err != nil { + fmt.Println(fmt.Sprintf("OnClosed: session:%d err:%v", session.Conn.ID(), err)) + } + }() + for _, v := range RegisteredServiceList { + if ok := v.OnSessionClose(session); ok { + return + } + } +} + +func (cb *callBackEntity) OnReceive(session *peer.Session, msg []byte) error { + _, msgPack, err := RouterCodec.Unmarshal(msg) + if err != nil { + return fmt.Errorf("onreceive: %v", err) + } + router, ok := msgPack.Router.(string) + if !ok { + return fmt.Errorf("onreceive: invalid router:%v", msgPack.Router) + } + routerArr := strings.Split(router, ".") + if len(routerArr) != 2 { + return fmt.Errorf("onreceive: invalid router:%s", msgPack.Router) + } + s, ok := RegisteredServiceList[routerArr[0]] + if !ok { + return fmt.Errorf("onreceive: function not registed router:%s err:%v", msgPack.Router, err) + } + h, ok := s.Handlers[routerArr[1]] + if !ok { + return fmt.Errorf("onreceive: function not registed router:%s err:%v", msgPack.Router, err) + } + t1 := time.Now() + + var args = []reflect.Value{h.Receiver, reflect.ValueOf(session), reflect.ValueOf(msgPack.DataPtr)} + var res any + var rb []byte + res, err = CallHandlerFunc(h.Method, args) + if res != nil && !reflect.ValueOf(res).IsNil() { + rb, err = RouterCodec.Marshal(router, res, nil) + if err != nil { + return fmt.Errorf("service: %v", err) + } + err = session.Conn.Send(rb) + if err != nil { + serviceLogger.Sugar().Warnf("warn! service send msg failed router:%s err:%v", router, err) + } + } else { + rb, err = RouterCodec.Marshal(router, nil, err) + if err != nil { + return fmt.Errorf("service: %v", err) + } + err = session.Conn.Send(rb) + if err != nil { + serviceLogger.Sugar().Warnf("warn! service send msg failed router:%s err:%v", router, err) + } + } + var errs string + if err != nil { + errs = err.Error() + } + dt := time.Since(t1) + go s.Component.OnRequestFinished(session, router, RouterCodec.ToString(msgPack.DataPtr), errs, dt) + return nil +} + +func CallHandlerFunc(foo reflect.Method, args []reflect.Value) (retValue any, retErr error) { + defer func() { + if err := recover(); err != nil { + fmt.Println(fmt.Sprintf("CallHandlerFunc: %v", err)) + retValue = nil + retErr = fmt.Errorf("CallHandlerFunc: call method pkg:%s method:%s err:%v", foo.PkgPath, foo.Name, err) + } + }() + if ret := foo.Func.Call(args); len(ret) > 0 { + var err error = nil + if r1 := ret[1].Interface(); r1 != nil { + err = r1.(error) + } + return ret[0].Interface(), err + } + return nil, fmt.Errorf("CallHandlerFunc: call method pkg:%s method:%s", foo.PkgPath, foo.Name) +} diff --git a/pkg/websocket/util/util.go b/pkg/websocket/util/util.go new file mode 100644 index 0000000..b3c5790 --- /dev/null +++ b/pkg/websocket/util/util.go @@ -0,0 +1,11 @@ +package util + +import ( + "unicode" + "unicode/utf8" +) + +func IsExported(name string) bool { + w, _ := utf8.DecodeRuneInString(name) + return unicode.IsUpper(w) +} diff --git a/tool/helper.go b/tool/helper.go new file mode 100644 index 0000000..2b5935d --- /dev/null +++ b/tool/helper.go @@ -0,0 +1,328 @@ +package tool + +import ( + "bufio" + "bytes" + "errors" + "fmt" + "io" + "math" + "math/rand" + "net" + "net/http" + "net/url" + "os" + "regexp" + "strconv" + "strings" + "time" + "unicode" +) + +// GetOrderNumber 获取订单号 +func GetOrderNumber() string { + parse, _ := time.Parse(time.DateTime, "2021-04-27 00:00:00") + hours := time.Now().Sub(parse).Hours() + day := math.Floor(hours / 24) + prefix := fmt.Sprintf("%05d", int64(day)) + format := time.Now().Format("") + "%0" + strconv.Itoa(10) + "d" + n := math.Pow10(10) + return prefix + fmt.Sprintf(format, rand.New(rand.NewSource(time.Now().UnixNano())).Int63n(int64(n))) +} + +// ByteFmt 格式化显示文件大小 +func ByteFmt(size int64) string { + var unitArr = []string{"B", "KB", "MB", "GB", "TB", "EB"} + if size == 0 { + return "unknown" + } + fs := float64(size) + p := int(math.Log(fs) / math.Log(1024)) + val := fs / math.Pow(1024, float64(p)) + _, frac := math.Modf(val) + if frac > 0 { + return fmt.Sprintf("%.1f%s", math.Floor(val*10)/10, unitArr[p]) + } else { + return fmt.Sprintf("%d%s", int(val), unitArr[p]) + } +} + +// UniqueArray 切片唯一值 +func UniqueArray[T comparable](array []T) []T { + result := make([]T, 0, len(array)) + temp := map[T]struct{}{} + for _, item := range array { + if _, ok := temp[item]; !ok { + temp[item] = struct{}{} + result = append(result, item) + } + } + return result +} + +// InArray 是否在切片 +func InArray[T comparable](item T, array []T) bool { + for _, s := range array { + if item == s { + return true + } + } + return false +} + +// ChunkArray 分割切片 +func ChunkArray[T comparable](array []T, size int) (ss [][]T) { + if size <= 0 || len(array) <= size { + return [][]T{array} + } + mod := len(array) % size + k := len(array) / size + var end int + if mod == 0 { + end = k + } else { + end = k + 1 + } + for i := 0; i < end; i++ { + if i != k { + ss = append(ss, array[i*size:(i+1)*size]) + } else { + ss = append(ss, array[i*size:]) + } + } + return +} + +// IsChinese 判断字符串是否包含中文 +func IsChinese(str string) bool { + var count int + for _, v := range str { + if unicode.Is(unicode.Han, v) { + count++ + break + } + } + return count > 0 +} + +// RandInt 随机数 +func RandInt(max int) int { + if max <= 0 { + return 0 + } + return rand.Intn(max) +} + +// FindIPv4 字符串中查找IP4 +func FindIPv4(input string) string { + partIp := "(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])" + must := partIp + "\\." + partIp + "\\." + partIp + "\\." + partIp + matchMe := regexp.MustCompile(must) + return matchMe.FindString(input) +} + +// RandString 随机字符串 +func RandString(n int) string { + var letterRunes = []rune("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") + b := make([]rune, n) + for i := range b { + b[i] = letterRunes[RandInt(len(letterRunes))] + } + return string(b) +} + +// IP4ToInt ip4地址转Int类型 +func IP4ToInt(ip net.IP) int { + ipSplit := strings.Split(ip.To4().String(), ".") + var ipInt = 0 + var pos uint = 24 + for _, ipSeg := range ipSplit { + tempInt, _ := strconv.Atoi(ipSeg) + tempInt = tempInt << pos + ipInt = ipInt | tempInt + pos -= 8 + } + return ipInt +} + +// IP4IntToString 反序列化Int为IP4地址 +func IP4IntToString(ipInt int) string { + ipSplit := make([]string, 4) + var length = len(ipSplit) + buffer := bytes.NewBufferString("") + for i := 0; i < length; i++ { + tempInt := ipInt & 0xFF + ipSplit[length-i-1] = strconv.Itoa(tempInt) + ipInt = ipInt >> 8 + } + for i := 0; i < length; i++ { + buffer.WriteString(ipSplit[i]) + if i < length-1 { + buffer.WriteString(".") + } + } + return buffer.String() +} + +// CompareVersion 比较`x.y.z`类型版本号大小 +func CompareVersion(verCurrent, verUpdate string) int { + versionA := strings.Split(verCurrent, ".") + versionB := strings.Split(verUpdate, ".") + + for i := len(versionA); i < 4; i++ { + versionA = append(versionA, "0") + } + for i := len(versionB); i < 4; i++ { + versionB = append(versionB, "0") + } + + for i := 0; i < 4; i++ { + version1, _ := strconv.Atoi(versionA[i]) + version2, _ := strconv.Atoi(versionB[i]) + if version1 == version2 { + continue + } else if version1 > version2 { + return 1 + } else { + return -1 + } + } + + return 0 +} + +// GetLocalIP 获取内网本机ipv4地址 +func GetLocalIP() (string, error) { + var address []net.Addr + eth0Interface, err := net.InterfaceByName("eth0") + if err == nil { + address, err = eth0Interface.Addrs() + } else { + address, err = net.InterfaceAddrs() + } + if err != nil { + return "", err + } + for _, addr := range address { + if it, ok := addr.(*net.IPNet); ok && !it.IP.IsLoopback() { + if it.IP.To4() != nil { + return it.IP.String(), nil + } + } + } + return "", err +} + +// GetInternetIP 获取外网本机ipv4地址 +func GetInternetIP() (string, error) { + resp, err := http.Get("https://api.ipify.org") + if err != nil { + resp, err = http.Get("https://icanhazip.com") + if err != nil { + return "", err + } + } + defer func() { + _ = resp.Body.Close() + }() + s, readErr := io.ReadAll(resp.Body) + if readErr != nil { + return "", readErr + } + return string(s), nil +} + +// IsExists 检测指定路径文件或者文件夹是否存在 +func IsExists(path string) bool { + _, err := os.Stat(path) //os.Stat获取文件信息 + if err != nil { + if os.IsExist(err) { + return true + } + return false + } + return true +} + +// CheckWebsiteAddr 检查域名是否正确 +func CheckWebsiteAddr(addr string) error { + if 0 == len(addr) { + return errors.New("url is empty") + } + + parse, err := url.Parse(addr) + if err != nil { + return err + } + + if "http" != parse.Scheme && "https" != parse.Scheme { + return errors.New("url Scheme illegal. err:" + parse.Scheme) + } + + re := regexp.MustCompile(`^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$|^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)+([A-Za-z]|[A-Za-z][A-Za-z0-9\-]*[A-Za-z0-9])$`) + u := strings.Split(parse.Host, ":") + if 2 == len(u) && 0 != len(u[1]) { + result := re.FindAllStringSubmatch(u[0], -1) + if result == nil { + return errors.New("url illegal. err:" + u[0]) + } + return nil + } + + result := re.FindAllStringSubmatch(parse.Host, -1) + if result == nil { + return errors.New("url illegal. err:" + parse.Host) + } + + return nil +} + +// LoadKeyValConfig 获取本地`a=c\n`类型文件的键值对 +func LoadKeyValConfig(text string) map[string]string { + config := make(map[string]string) + r := bufio.NewReader(bytes.NewBufferString(text)) + for { + b, _, err := r.ReadLine() + if err != nil { + if err == io.EOF { + break + } + panic(err) + } + s := strings.TrimSpace(string(b)) + index := strings.Index(s, "=") + if index < 0 || strings.HasPrefix(s, "#") { + continue + } + key := strings.TrimSpace(s[:index]) + if len(key) == 0 { + continue + } + value := strings.TrimSpace(s[index+1:]) + if len(value) == 0 { + continue + } + config[key] = value + } + return config +} + +// PortIsUse 判断端口是否占用 +func PortIsUse(port int) bool { + _, tcpError := net.DialTimeout("tcp", fmt.Sprintf(":%d", port), time.Millisecond*50) + udpAddr, _ := net.ResolveUDPAddr("udp4", fmt.Sprintf(":%d", port)) + udpConn, udpError := net.ListenUDP("udp", udpAddr) + if udpConn != nil { + defer func() { + _ = udpConn.Close() + }() + } + return tcpError == nil || udpError != nil +} + +// ReplaceInvalidFilenameChars 用下划线替换不合法文件名的字符 +func ReplaceInvalidFilenameChars(filename string) string { + pattern := regexp.MustCompile(`[\\/:*?"<>|{}'()!@#$%^&\[\];,+=~·!¥…()—{}、:“”‘’;,《》。?]`) + filename = pattern.ReplaceAllString(filename, "_") + return strings.ReplaceAll(filename, " ", "") +} diff --git a/tool/http.go b/tool/http.go new file mode 100644 index 0000000..d71688f --- /dev/null +++ b/tool/http.go @@ -0,0 +1,133 @@ +package tool + +import ( + "encoding/json" + "git.bvbej.com/bvbej/base-golang/pkg/aes" + "git.bvbej.com/bvbej/base-golang/pkg/hmac" + "git.bvbej.com/bvbej/base-golang/pkg/httpclient" + "git.bvbej.com/bvbej/base-golang/pkg/time_parse" + "github.com/tidwall/gjson" + netUrl "net/url" + "strconv" + "time" +) + +func HttpPostByAes(url, productID, key, vi string, params netUrl.Values) (body []byte, err error) { + var signatureDatetime = time_parse.CSTLayoutString() + unix, _ := time_parse.CSTLayoutStringToUnix(signatureDatetime) + params.Set("t", strconv.FormatInt(unix, 10)) + var sign string + sign, err = aes.New(key, vi).EncryptCBC(params.Encode(), false) + if err != nil { + return body, err + } + body, err = httpclient.PostForm( + url, + params, + httpclient.WithTTL(time.Second*30), + httpclient.WithHeader("Signature", sign), + httpclient.WithHeader("Signature-Datetime", signatureDatetime), + httpclient.WithHeader("Product-ID", productID), + httpclient.WithOnFailedRetry(1, time.Millisecond*200, + func(body []byte) (shouldRetry bool) { + return len(body) == 0 + }, + ), + ) + return body, err +} + +func HttpPostByAesWithTimeout(url, productID, key, vi string, params netUrl.Values, timeout time.Duration) (body []byte, err error) { + var signatureDatetime = time_parse.CSTLayoutString() + unix, _ := time_parse.CSTLayoutStringToUnix(signatureDatetime) + params.Set("t", strconv.FormatInt(unix, 10)) + var sign string + sign, err = aes.New(key, vi).EncryptCBC(params.Encode(), false) + if err != nil { + return body, err + } + body, err = httpclient.PostForm( + url, + params, + httpclient.WithTTL(timeout), + httpclient.WithHeader("Signature", sign), + httpclient.WithHeader("Signature-Datetime", signatureDatetime), + httpclient.WithHeader("Product-ID", productID), + httpclient.WithOnFailedRetry(3, time.Second*1, + func(body []byte) (shouldRetry bool) { + return len(body) == 0 + }, + ), + ) + return body, err +} + +func DingTalkAlertMarkdown(accessToken, secret, title, text string) bool { + params := netUrl.Values{} + timestamp := strconv.FormatInt(time_parse.GetMilliTimestamp(), 10) + signData := timestamp + "\n" + secret + sign := netUrl.QueryEscape(hmac.New(secret).Sha256ToBase64String(signData)) + params.Set("access_token", accessToken) + params.Set("sign", sign) + params.Set("timestamp", timestamp) + var content struct { + MsgType string `json:"msgtype"` + Markdown struct { + Title string `json:"title"` + Text string `json:"text"` + } `json:"markdown"` + } + content.MsgType = "markdown" + content.Markdown.Title = title + content.Markdown.Text = text + body, _ := json.Marshal(content) + response, err := httpclient.PostJSON( + "https://oapi.dingtalk.com/robot/send?"+params.Encode(), + body, + httpclient.WithTTL(time.Second*5), + httpclient.WithHeader("Content-Type", "application/json;charset=utf-8"), + httpclient.WithOnFailedRetry(3, time.Second*1, + func(body []byte) (shouldRetry bool) { + return len(body) == 0 + }, + ), + ) + if err != nil { + return false + } + return gjson.Get(string(response), "errcode").Int() == 0 +} + +func DingTalkAlertText(accessToken, secret string, text string) bool { + params := netUrl.Values{} + timestamp := strconv.FormatInt(time_parse.GetMilliTimestamp(), 10) + signData := timestamp + "\n" + secret + sign := netUrl.QueryEscape(hmac.New(secret).Sha256ToBase64String(signData)) + params.Set("access_token", accessToken) + params.Set("sign", sign) + params.Set("timestamp", timestamp) + var content struct { + MsgType string `json:"msgtype"` + Text struct { + Content string `json:"content"` + } `json:"text"` + } + content.MsgType = "text" + content.Text.Content = text + body, _ := json.Marshal(content) + response, err := httpclient.PostJSON( + "https://oapi.dingtalk.com/robot/send?"+params.Encode(), + body, + httpclient.WithTTL(time.Second*5), + httpclient.WithHeader("Content-Type", "application/json;charset=utf-8"), + httpclient.WithOnFailedRetry(3, time.Second*1, + func(body []byte) (shouldRetry bool) { + return len(body) == 0 + }, + ), + ) + if err != nil { + return false + } + return gjson.Get(string(response), "errcode").Int() == 0 +}