From bb87c097ba60f48d98e62cbcfc0e719710b3e7e3 Mon Sep 17 00:00:00 2001 From: Vitaliy Potyarkin Date: Tue, 15 Oct 2024 12:31:18 +0300 Subject: [PATCH 1/9] feat: FrostFS client for future HTTP-01 solver Signed-off-by: Vitaliy Potyarkin --- go.mod | 23 +++- go.sum | 48 ++++++-- providers/http/frostfs/client.go | 158 ++++++++++++++++++++++++++ providers/http/frostfs/client_test.go | 100 ++++++++++++++++ 4 files changed, 318 insertions(+), 11 deletions(-) create mode 100644 providers/http/frostfs/client.go create mode 100644 providers/http/frostfs/client_test.go diff --git a/go.mod b/go.mod index 43960c16..10fdceb4 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.22.0 require ( cloud.google.com/go/compute/metadata v0.5.1 + git.frostfs.info/TrueCloudLab/frostfs-sdk-go v0.0.0-20241011121723-d7872061f859 github.com/Azure/azure-sdk-for-go v68.0.0+incompatible github.com/Azure/azure-sdk-for-go/sdk/azcore v1.14.0 github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.7.0 @@ -90,6 +91,11 @@ require ( require ( cloud.google.com/go/auth v0.9.3 // indirect cloud.google.com/go/auth/oauth2adapt v0.2.4 // indirect + git.frostfs.info/TrueCloudLab/frostfs-api-go/v2 v2.16.1-0.20241011114054-f0fc40e116d1 // indirect + git.frostfs.info/TrueCloudLab/frostfs-crypto v0.6.0 // indirect + git.frostfs.info/TrueCloudLab/hrw v1.2.1 // indirect + git.frostfs.info/TrueCloudLab/rfc6979 v0.4.0 // indirect + git.frostfs.info/TrueCloudLab/tzhash v1.8.0 // indirect github.com/AdamSLevy/jsonrpc2/v14 v14.1.0 // indirect github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 // indirect github.com/Azure/go-autorest v14.2.0+incompatible // indirect @@ -99,6 +105,8 @@ require ( github.com/Azure/go-autorest/logger v0.2.1 // indirect github.com/Azure/go-autorest/tracing v0.6.0 // indirect github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 // indirect + github.com/VictoriaMetrics/easyproto v0.1.4 // indirect + github.com/antlr4-go/antlr/v4 v4.13.1 // indirect github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.4 // indirect github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.13 // indirect github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.17 // indirect @@ -116,6 +124,7 @@ require ( github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect github.com/dimchansky/utfbom v1.1.1 // indirect github.com/fatih/structs v1.1.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect @@ -142,8 +151,10 @@ require ( github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/hashicorp/go-uuid v1.0.3 // indirect + github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect + github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213 // indirect github.com/kolo/xmlrpc v0.0.0-20220921171641-a4b6fa1dd06b // indirect @@ -152,10 +163,14 @@ require ( github.com/leodido/go-urn v1.2.4 // indirect github.com/liquidweb/liquidweb-cli v0.6.9 // indirect github.com/magiconair/properties v1.8.7 // indirect + github.com/mailru/easyjson v0.7.7 // indirect github.com/mitchellh/go-homedir v1.1.0 // 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/mr-tron/base58 v1.2.0 // indirect + github.com/nspcc-dev/neo-go v0.106.2 // indirect + github.com/nspcc-dev/rfc6979 v0.2.1 // indirect github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b // indirect github.com/patrickmn/go-cache v2.1.0+incompatible // indirect github.com/pelletier/go-toml/v2 v2.1.0 // indirect @@ -181,6 +196,7 @@ require ( github.com/stretchr/objx v0.5.2 // indirect github.com/subosito/gotenv v1.6.0 // indirect github.com/tjfoc/gmsm v1.4.1 // indirect + github.com/twmb/murmur3 v1.1.8 // indirect github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect go.mongodb.org/mongo-driver v1.12.0 // indirect go.opencensus.io v0.24.0 // indirect @@ -188,10 +204,9 @@ require ( go.opentelemetry.io/otel v1.29.0 // indirect go.opentelemetry.io/otel/metric v1.29.0 // indirect go.opentelemetry.io/otel/trace v1.29.0 // indirect - go.uber.org/atomic v1.9.0 // indirect - go.uber.org/multierr v1.9.0 // indirect + go.uber.org/multierr v1.11.0 // indirect go.uber.org/ratelimit v0.3.0 // indirect - golang.org/x/exp v0.0.0-20240213143201-ec583247a57a // indirect + golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 // indirect golang.org/x/mod v0.21.0 // indirect golang.org/x/sync v0.8.0 // indirect golang.org/x/sys v0.25.0 // indirect @@ -200,7 +215,7 @@ require ( google.golang.org/genproto v0.0.0-20240903143218-8af14fe29dc1 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20240827150818-7e3bb234dfed // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 // indirect - google.golang.org/grpc v1.66.1 // indirect + google.golang.org/grpc v1.66.2 // indirect google.golang.org/protobuf v1.34.2 // 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 index 8072e33e..939bb1dc 100644 --- a/go.sum +++ b/go.sum @@ -17,6 +17,18 @@ cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqCl cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +git.frostfs.info/TrueCloudLab/frostfs-api-go/v2 v2.16.1-0.20241011114054-f0fc40e116d1 h1:ivcdxQeQDnx4srF2ezoaeVlF0FAycSAztwfIUJnUI4s= +git.frostfs.info/TrueCloudLab/frostfs-api-go/v2 v2.16.1-0.20241011114054-f0fc40e116d1/go.mod h1:F5GS7hRb62PUy5sTYDC4ajVdeffoAfjHSSHTKUJEaYU= +git.frostfs.info/TrueCloudLab/frostfs-crypto v0.6.0 h1:FxqFDhQYYgpe41qsIHVOcdzSVCB8JNSfPG7Uk4r2oSk= +git.frostfs.info/TrueCloudLab/frostfs-crypto v0.6.0/go.mod h1:RUIKZATQLJ+TaYQa60X2fTDwfuhMfm8Ar60bQ5fr+vU= +git.frostfs.info/TrueCloudLab/frostfs-sdk-go v0.0.0-20241011121723-d7872061f859 h1:XXlKY9bseTzggafcgP3LTryCSCZ4chCUqHuNApRhsi0= +git.frostfs.info/TrueCloudLab/frostfs-sdk-go v0.0.0-20241011121723-d7872061f859/go.mod h1:3txOjFJ8M/JFs01h7xOrnQHVn6hZgDNA16ivyUlu1iU= +git.frostfs.info/TrueCloudLab/hrw v1.2.1 h1:ccBRK21rFvY5R1WotI6LNoPlizk7qSvdfD8lNIRudVc= +git.frostfs.info/TrueCloudLab/hrw v1.2.1/go.mod h1:C1Ygde2n843yTZEQ0FP69jYiuaYV0kriLvP4zm8JuvM= +git.frostfs.info/TrueCloudLab/rfc6979 v0.4.0 h1:M2KR3iBj7WpY3hP10IevfIB9MURr4O9mwVfJ+SjT3HA= +git.frostfs.info/TrueCloudLab/rfc6979 v0.4.0/go.mod h1:okpbKfVYf/BpejtfFTfhZqFP+sZ8rsHrP8Rr/jYPNRc= +git.frostfs.info/TrueCloudLab/tzhash v1.8.0 h1:UFMnUIk0Zh17m8rjGHJMqku2hCgaXDqjqZzS4gsb4UA= +git.frostfs.info/TrueCloudLab/tzhash v1.8.0/go.mod h1:dhY+oy274hV8wGvGL4MwwMpdL3GYvaX1a8GQZQHvlF8= github.com/AdamSLevy/jsonrpc2/v14 v14.1.0 h1:Dy3M9aegiI7d7PF1LUdjbVigJReo+QOceYsMyFh9qoE= github.com/AdamSLevy/jsonrpc2/v14 v14.1.0/go.mod h1:ZakZtbCXxCz82NJvq7MoREtiQesnDfrtF6RFUGzQfLo= github.com/Azure/azure-sdk-for-go v68.0.0+incompatible h1:fcYLmCpyNYRnvJbPerq7U0hS+6+I79yEDJBqVNcqUzU= @@ -70,6 +82,8 @@ github.com/HdrHistogram/hdrhistogram-go v1.1.2/go.mod h1:yDgFjdqOqDEKOvasDdhWNXY github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/OpenDNS/vegadns2client v0.0.0-20180418235048-a3fa4a771d87 h1:xPMsUicZ3iosVPSIP7bW5EcGUzjiiMl1OYTe14y/R24= github.com/OpenDNS/vegadns2client v0.0.0-20180418235048-a3fa4a771d87/go.mod h1:iGLljf5n9GjT6kc0HBvyI1nOKnGQbNB66VzSNbK5iks= +github.com/VictoriaMetrics/easyproto v0.1.4 h1:r8cNvo8o6sR4QShBXQd1bKw/VVLSQma/V2KhTBPf+Sc= +github.com/VictoriaMetrics/easyproto v0.1.4/go.mod h1:QlGlzaJnDfFd8Lk6Ci/fuLxfTo3/GThPs2KH23mv710= github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw= github.com/akamai/AkamaiOPEN-edgegrid-golang v1.2.2 h1:F1j7z+/DKEsYqZNoxC6wvfmaiDneLsQOFQmuq9NADSY= github.com/akamai/AkamaiOPEN-edgegrid-golang v1.2.2/go.mod h1:QlXr/TrICfQ/ANa76sLeQyhAJyNR9sEcfNuZBkY9jgY= @@ -78,6 +92,8 @@ github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRF github.com/aliyun/alibaba-cloud-sdk-go v1.63.15 h1:r2uwBUQhLhcPzaWz9tRJqc8MjYwHb+oF2+Q6467BF14= github.com/aliyun/alibaba-cloud-sdk-go v1.63.15/go.mod h1:SOSDHfe1kX91v3W5QiBsWSLqeLxImobbMX1mxrFHsVQ= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= +github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ= +github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw= 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= @@ -164,6 +180,8 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs 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/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 h1:8UrgZ3GkP4i/CLijOJx79Yu+etlyjdBU4sfcs2WYQMs= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= github.com/dimchansky/utfbom v1.1.1 h1:vV6w1AhK4VMnhBno/TPVCoK9U/LP0PkLCS9tbxHdi/U= @@ -291,8 +309,9 @@ github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/ github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= 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-20210407192527-94a9f03dee38 h1:yAJXTCF9TqKcTiHJAE8dj7HMvPfh66eeA2JYW7eFpSE= github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20230817174616-7a8ec2ada47b h1:h9U78+dx9a4BKdQkBBos92HalKpaGKHrp+3Uo6yTodo= +github.com/google/pprof v0.0.0-20230817174616-7a8ec2ada47b/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/s2a-go v0.1.8 h1:zZDs9gcbt9ZPLV0ndSyQk6Kacx2g/X+SKYovpnz3SMM= github.com/google/s2a-go v0.1.8/go.mod h1:6iNWHTpQ+nfNRN5E00MSdfDwVesa8hhS32PhPO8deJA= @@ -347,6 +366,8 @@ github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/b 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/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= +github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= 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= @@ -370,6 +391,8 @@ github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHW github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= @@ -414,6 +437,8 @@ github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czP github.com/magiconair/properties v1.8.4/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/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.7/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= @@ -459,6 +484,8 @@ github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3Rllmb 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/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= +github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o= +github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/namedotcom/go v0.0.0-20180403034216-08470befbe04 h1:o6uBwrhM5C8Ll3MAAxrQxRHEu7FkapwTuI2WmL1rw4g= github.com/namedotcom/go v0.0.0-20180403034216-08470befbe04/go.mod h1:5sN+Lt1CaY4wsPvgQH/jsuJi4XO2ssZbdsIizr4CVC8= @@ -484,6 +511,10 @@ github.com/nrdcg/nodion v0.1.0 h1:zLKaqTn2X0aDuBHHfyA1zFgeZfiCpmu/O9DM73okavw= github.com/nrdcg/nodion v0.1.0/go.mod h1:inbuh3neCtIWlMPZHtEpe43TmRXxHV6+hk97iCZicms= github.com/nrdcg/porkbun v0.4.0 h1:rWweKlwo1PToQ3H+tEO9gPRW0wzzgmI/Ob3n2Guticw= github.com/nrdcg/porkbun v0.4.0/go.mod h1:/QMskrHEIM0IhC/wY7iTCUgINsxdT2WcOphktJ9+Q54= +github.com/nspcc-dev/neo-go v0.106.2 h1:KXSJ2J5Oacc7LrX3r4jvnC8ihKqHs5NB21q4f2S3r9o= +github.com/nspcc-dev/neo-go v0.106.2/go.mod h1:Ojwfx3/lv0VTeEHMpQ17g0wTnXcCSoFQVq5GEeCZmGo= +github.com/nspcc-dev/rfc6979 v0.2.1 h1:8wWxkamHWFmO790GsewSoKUSJjVnL1fmdRpokU/RgRM= +github.com/nspcc-dev/rfc6979 v0.2.1/go.mod h1:Tk7h5kyUWkhjyO3zUgFFhy1v2vQv3BvQEntakdtqrWc= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= @@ -651,6 +682,8 @@ github.com/tjfoc/gmsm v1.4.1/go.mod h1:j4INPkHWMrhJb38G+J6W4Tw0AbuN8Thu3PbdVYhVc github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/transip/gotransip/v6 v6.26.0 h1:Aejfvh8rSp8Mj2GX/RpdBjMCv+Iy/DmgfNgczPDP550= github.com/transip/gotransip/v6 v6.26.0/go.mod h1:x0/RWGRK/zob817O3tfO2xhFoP1vu8YOHORx6Jpk80s= +github.com/twmb/murmur3 v1.1.8 h1:8Yt9taO/WN3l08xErzjeschgZU2QSrwm1kclYq+0aRg= +github.com/twmb/murmur3 v1.1.8/go.mod h1:Qq/R7NUyOfr65zD+6Q5IHKsJLwP7exErjN6lyyq3OSQ= github.com/uber/jaeger-client-go v2.30.0+incompatible h1:D6wyKGCecFaSRUpo8lCVbaOOb6ThwMmTEbhRwtKR97o= github.com/uber/jaeger-client-go v2.30.0+incompatible/go.mod h1:WVhlPFC8FDjOFMMWRy2pZqQJSXxYSwNYOkTr/Z6d3Kk= github.com/uber/jaeger-lib v2.4.1+incompatible h1:td4jdvLcExb4cBISKIpHuGoVXh+dVKhn2Um6rjCsSsg= @@ -700,8 +733,8 @@ go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= -go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI= -go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ= +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/ratelimit v0.3.0 h1:IdZd9wqvFXnvLvSEBo0KPcGfkoBGNkpTHlrE3Rcjkjw= go.uber.org/ratelimit v0.3.0/go.mod h1:So5LG7CV1zWpY1sHe+DXTJqQvOx+FFPFaAs2SnoyBaI= go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= @@ -733,8 +766,8 @@ golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL 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-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= -golang.org/x/exp v0.0.0-20240213143201-ec583247a57a h1:HinSgX1tJRX3KsL//Gxynpw5CTOAIPhgL4W8PNiIpVE= -golang.org/x/exp v0.0.0-20240213143201-ec583247a57a/go.mod h1:CxmFvTBINI24O/j8iY7H1xHzx2i4OsyguNBmN/uPtqc= +golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM= +golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc= 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= @@ -844,6 +877,7 @@ golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/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-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-20220209214540-3681064d5158/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-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -965,8 +999,8 @@ google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= google.golang.org/grpc v1.41.0/go.mod h1:U3l9uK9J0sini8mHphKoXyaqDA/8VyGnDee1zzIUK6k= -google.golang.org/grpc v1.66.1 h1:hO5qAXR19+/Z44hmvIM4dQFMSYX9XcWsByfoxutBpAM= -google.golang.org/grpc v1.66.1/go.mod h1:s3/l6xSSCURdVfAnL+TqCNMyTDAGN6+lZeVxnZR128Y= +google.golang.org/grpc v1.66.2 h1:3QdXkuq3Bkh7w+ywLdLvM56cmGvQHUMZpiCzt6Rqaoo= +google.golang.org/grpc v1.66.2/go.mod h1:s3/l6xSSCURdVfAnL+TqCNMyTDAGN6+lZeVxnZR128Y= 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= diff --git a/providers/http/frostfs/client.go b/providers/http/frostfs/client.go new file mode 100644 index 00000000..8677943a --- /dev/null +++ b/providers/http/frostfs/client.go @@ -0,0 +1,158 @@ +package frostfs + +import ( + "context" + "crypto/ecdsa" + "fmt" + "time" + + "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/client" + status "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/client/status" + containerid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id" + "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object" + objectid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id" + "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/user" +) + +// Barebones API client for a single FrostFS container +type Storage struct { + endpoint string + container containerid.ID + key *ecdsa.PrivateKey + user user.ID +} + +const storageRequestTimeout = 10 * time.Second + +// Create FrostFS client for working with a single storage container +func Open(endpoint, containerID string, key *ecdsa.PrivateKey) (*Storage, error) { + var container containerid.ID + err := container.DecodeString(containerID) + if err != nil { + return nil, fmt.Errorf("invalid container ID: %w", err) + } + var owner user.ID + user.IDFromKey(&owner, key.PublicKey) + return &Storage{ + endpoint: endpoint, + key: key, + user: owner, + container: container, + }, nil +} + +// Save byte slice to FrostFS object +// +// Large objects must not be created this way. Depending on FrostFS network configuration, +// attempting to send byte slices larger than ~64MB will always return an error. +func (s *Storage) Save(ctx context.Context, data []byte, attr ...string) (oid string, err error) { + attributes, err := keyval2attrs(attr...) + if err != nil { + return "", fmt.Errorf("invalid attributes: %w", err) + } + + obj := object.New() + object.InitCreation(obj, object.RequiredFields{ + Container: s.container, + Owner: s.user, + }) + obj.SetAttributes(attributes...) + obj.SetPayload(data) + obj.SetPayloadSize(uint64(len(data))) + object.CalculateAndSetPayloadChecksum(obj) + err = object.CalculateAndSetID(obj) + if err != nil { + return "", fmt.Errorf("object ID: %w", err) + } + err = object.CalculateAndSetSignature(*s.key, obj) + if err != nil { + return "", fmt.Errorf("signing object: %w", err) + } + + ctx, cancel := context.WithTimeout(ctx, storageRequestTimeout) + defer cancel() + + c, err := s.dial(ctx) + if err != nil { + return "", fmt.Errorf("connecting to storage node: %w", err) + } + put := client.PrmObjectPutSingle{ + Key: s.key, + Object: obj, + } + res, err := c.ObjectPutSingle(ctx, put) + if err != nil { + return "", fmt.Errorf("sending object to storage: %w", err) + } + stat := res.Status() + if !status.IsSuccessful(stat) { + return "", fmt.Errorf("saving object to storage: %w", stat.(error)) + } + id, _ := obj.ID() + return fmt.Sprint(id), nil +} + +// Delete object from container +func (s *Storage) Delete(ctx context.Context, oid string) error { + var obj objectid.ID + err := obj.DecodeString(oid) + if err != nil { + return fmt.Errorf("invalid object id: %w", err) + } + c, err := s.dial(ctx) + if err != nil { + return fmt.Errorf("connecting to storage node: %w", err) + } + ctx, cancel := context.WithTimeout(ctx, storageRequestTimeout) + defer cancel() + res, err := c.ObjectDelete(ctx, client.PrmObjectDelete{ + ContainerID: &s.container, + ObjectID: &obj, + Key: s.key, + }) + if err != nil { + return fmt.Errorf("delete request: %w", err) + } + stat := res.Status() + if !status.IsSuccessful(stat) { + return fmt.Errorf("delete object: %w", stat.(error)) + } + return nil +} + +// Open new connection to FrostFS storage node +func (s *Storage) dial(ctx context.Context) (*client.Client, error) { + var wallet client.PrmInit + wallet.Key = *s.key + + var c client.Client + c.Init(wallet) + + var endpoint client.PrmDial + endpoint.Endpoint = s.endpoint + + err := c.Dial(ctx, endpoint) + if err != nil { + return nil, err + } + return &c, nil +} + +// Compose object attributes slice +func keyval2attrs(attr ...string) ([]object.Attribute, error) { + if len(attr)%2 != 0 { + return nil, fmt.Errorf("odd number of key-value strings: %d (must be even)", len(attr)) + } + attributes := make([]object.Attribute, 0, len(attr)/2) + var current *object.Attribute + for index, text := range attr { + if index%2 == 0 { + current = object.NewAttribute() + current.SetKey(text) + } else { + current.SetValue(text) + attributes = append(attributes, *current) + } + } + return attributes, nil +} diff --git a/providers/http/frostfs/client_test.go b/providers/http/frostfs/client_test.go new file mode 100644 index 00000000..7217aff4 --- /dev/null +++ b/providers/http/frostfs/client_test.go @@ -0,0 +1,100 @@ +package frostfs + +// Tests for our FrostFS client code + +import ( + "testing" + + "context" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "sync" +) + +func openStorage(t *testing.T) *Storage { + const ( + cid_hardcoded = "348WWfBKbS79Wbmm38MRE3oBoEDM5Ga1XXbGKGNyisDM" + endpoint_hardcoded = "grpc://localhost:8802" + ) + + // We will use an ephemeral key to write to publicly writable container + key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + t.Fatal(err) + } + + storage, err := Open(endpoint_hardcoded, cid_hardcoded, key) + if err != nil { + t.Fatal(err) + } + return storage +} + +// Save some bytes to FrostFS and clean up after +func TestObjectPutDelete(t *testing.T) { + var payload = []byte("Hello FrostFS!\n") + storage := openStorage(t) + ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) + oid, err := storage.Save(ctx, payload, "FileName", "hello_from_sdk.txt", "Tag", "foobar") + if err != nil { + t.Fatal(err) + } + t.Logf("saved object: %s", oid) + err = storage.Delete(ctx, oid) + if err != nil { + t.Fatal(err) + } + t.Logf("deleted object: %s", oid) +} + +// Check that FrostFS is in fact a content addressable storage +func TestMulipleObjects(t *testing.T) { + var payload = []byte("Multiple objects with same content should get the same object ID") + + ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) + var wg sync.WaitGroup + + objects := make(chan string) + go func() { + baseline := "" + for { + select { + case <-ctx.Done(): + return + case obj, ok := <-objects: + if !ok { + return + } + if baseline == "" { + baseline = obj + continue + } + if obj != baseline { + t.Fatalf("non-identical object id: %s != %s", baseline, obj) + } + } + } + }() + + const testCount = 5 + storage := openStorage(t) + for i := 0; i < testCount; i++ { + wg.Add(1) + go func() { + defer wg.Done() + oid, err := storage.Save(ctx, payload, "FileName", "CAS.txt", "Tag", "test") + if err != nil { + t.Fatal(err) + } + err = storage.Delete(ctx, oid) + if err != nil { + t.Fatal(err) + } + t.Log(oid) + }() + } + wg.Wait() +} -- 2.45.2 From 23e60f1e98f67d1621a0f352a613ad5fc3cec057 Mon Sep 17 00:00:00 2001 From: Vitaliy Potyarkin Date: Tue, 15 Oct 2024 12:47:54 +0300 Subject: [PATCH 2/9] Read test connection credentials from env vars Signed-off-by: Vitaliy Potyarkin --- providers/http/frostfs/client_test.go | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/providers/http/frostfs/client_test.go b/providers/http/frostfs/client_test.go index 7217aff4..495f7b27 100644 --- a/providers/http/frostfs/client_test.go +++ b/providers/http/frostfs/client_test.go @@ -9,22 +9,22 @@ import ( "crypto/ecdsa" "crypto/elliptic" "crypto/rand" + "os" "sync" ) +// Initialize storage backend for tests func openStorage(t *testing.T) *Storage { - const ( - cid_hardcoded = "348WWfBKbS79Wbmm38MRE3oBoEDM5Ga1XXbGKGNyisDM" - endpoint_hardcoded = "grpc://localhost:8802" - ) - - // We will use an ephemeral key to write to publicly writable container - key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + cid := os.Getenv("FROSTFS_CID") // example: 348WWfBKbS79Wbmm38MRE3oBoEDM5Ga1XXbGKGNyisDM + endpoint := os.Getenv("FROSTFS_ENDPOINT") // example: grpc://localhost:8802 + if cid == "" || endpoint == "" { + t.Skipf("one or more environment variables not set: FROSTFS_ENDPOINT, FROSTFS_CID") + } + key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) // TODO: using ephemeral keys for now, later read from env vars if err != nil { t.Fatal(err) } - - storage, err := Open(endpoint_hardcoded, cid_hardcoded, key) + storage, err := Open(endpoint, cid, key) if err != nil { t.Fatal(err) } -- 2.45.2 From 30563a0fb18eb8c69c09871547a34e310807dd12 Mon Sep 17 00:00:00 2001 From: Vitaliy Potyarkin Date: Tue, 15 Oct 2024 15:42:46 +0300 Subject: [PATCH 3/9] Open NEO wallet from file system Signed-off-by: Vitaliy Potyarkin --- go.mod | 3 +- go.sum | 11 +++++ providers/http/frostfs/client.go | 48 +++++++++++++++++++ providers/http/frostfs/client_test.go | 31 ++++++++++++ .../http/frostfs/client_test_wallet.json | 1 + 5 files changed, 93 insertions(+), 1 deletion(-) create mode 100644 providers/http/frostfs/client_test_wallet.json diff --git a/go.mod b/go.mod index 10fdceb4..f4fbfd53 100644 --- a/go.mod +++ b/go.mod @@ -57,6 +57,7 @@ require ( github.com/nrdcg/namesilo v0.2.1 github.com/nrdcg/nodion v0.1.0 github.com/nrdcg/porkbun v0.4.0 + github.com/nspcc-dev/neo-go v0.106.2 github.com/nzdjb/go-metaname v1.0.0 github.com/oracle/oci-go-sdk/v65 v65.73.0 github.com/ovh/go-ovh v1.6.0 @@ -169,7 +170,7 @@ require ( github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/mr-tron/base58 v1.2.0 // indirect - github.com/nspcc-dev/neo-go v0.106.2 // indirect + github.com/nspcc-dev/go-ordered-json v0.0.0-20240301084351-0246b013f8b2 // indirect github.com/nspcc-dev/rfc6979 v0.2.1 // indirect github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b // indirect github.com/patrickmn/go-cache v2.1.0+incompatible // indirect diff --git a/go.sum b/go.sum index 939bb1dc..8ea943dd 100644 --- a/go.sum +++ b/go.sum @@ -286,6 +286,7 @@ github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw 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.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4= github.com/golang/snappy v0.0.1/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= @@ -334,6 +335,8 @@ github.com/gophercloud/utils v0.0.0-20231010081019-80377eca5d56/go.mod h1:VSalo4 github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY= +github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY= github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= @@ -511,6 +514,8 @@ github.com/nrdcg/nodion v0.1.0 h1:zLKaqTn2X0aDuBHHfyA1zFgeZfiCpmu/O9DM73okavw= github.com/nrdcg/nodion v0.1.0/go.mod h1:inbuh3neCtIWlMPZHtEpe43TmRXxHV6+hk97iCZicms= github.com/nrdcg/porkbun v0.4.0 h1:rWweKlwo1PToQ3H+tEO9gPRW0wzzgmI/Ob3n2Guticw= github.com/nrdcg/porkbun v0.4.0/go.mod h1:/QMskrHEIM0IhC/wY7iTCUgINsxdT2WcOphktJ9+Q54= +github.com/nspcc-dev/go-ordered-json v0.0.0-20240301084351-0246b013f8b2 h1:mD9hU3v+zJcnHAVmHnZKt3I++tvn30gBj2rP2PocZMk= +github.com/nspcc-dev/go-ordered-json v0.0.0-20240301084351-0246b013f8b2/go.mod h1:U5VfmPNM88P4RORFb6KSUVBdJBDhlqggJZYGXGPxOcc= github.com/nspcc-dev/neo-go v0.106.2 h1:KXSJ2J5Oacc7LrX3r4jvnC8ihKqHs5NB21q4f2S3r9o= github.com/nspcc-dev/neo-go v0.106.2/go.mod h1:Ojwfx3/lv0VTeEHMpQ17g0wTnXcCSoFQVq5GEeCZmGo= github.com/nspcc-dev/rfc6979 v0.2.1 h1:8wWxkamHWFmO790GsewSoKUSJjVnL1fmdRpokU/RgRM= @@ -673,6 +678,8 @@ github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8 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/syndtr/goleveldb v1.0.1-0.20210305035536-64b5b1c73954 h1:xQdMZ1WLrgkkvOZ/LDQxjVxMLdby7osSh4ZEVa5sIjs= +github.com/syndtr/goleveldb v1.0.1-0.20210305035536-64b5b1c73954/go.mod h1:u2MKkTVTVJWe5D1rCvame8WqhBd88EuIwODJZ1VHCPM= github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1002 h1:RE84sHFFx6t24DJvSnF9fS1DzBNv9OpctzHK3t7AY+I= github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1002/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0= github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.1002 h1:QwE0dRkAAbdf+eACnkNULgDn9ZKUJpPWRyXdqJolP5E= @@ -714,6 +721,8 @@ github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9dec github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= +go.etcd.io/bbolt v1.3.9 h1:8x7aARPEXiXbHmtUwAIv7eV2fQFHrLLavdiJ3uzJXoI= +go.etcd.io/bbolt v1.3.9/go.mod h1:zaO32+Ti0PK1ivdPtgMESzuzL2VPoIG1PCQNvOdo/dE= go.mongodb.org/mongo-driver v1.12.0 h1:aPx33jmn/rQuJXPQLZQ8NtfPQG8CaqgLThFtqRb0PiE= go.mongodb.org/mongo-driver v1.12.0/go.mod h1:AZkxhPnFJUoH7kZlFkVKucV20K387miPfm7oimrSmK0= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= @@ -738,6 +747,8 @@ go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN8 go.uber.org/ratelimit v0.3.0 h1:IdZd9wqvFXnvLvSEBo0KPcGfkoBGNkpTHlrE3Rcjkjw= go.uber.org/ratelimit v0.3.0/go.mod h1:So5LG7CV1zWpY1sHe+DXTJqQvOx+FFPFaAs2SnoyBaI= go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +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/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= diff --git a/providers/http/frostfs/client.go b/providers/http/frostfs/client.go index 8677943a..a3825ff9 100644 --- a/providers/http/frostfs/client.go +++ b/providers/http/frostfs/client.go @@ -3,9 +3,15 @@ package frostfs import ( "context" "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" "fmt" "time" + "github.com/nspcc-dev/neo-go/pkg/encoding/base58" + "github.com/nspcc-dev/neo-go/pkg/util" + "github.com/nspcc-dev/neo-go/pkg/wallet" + "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/client" status "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/client/status" containerid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id" @@ -156,3 +162,45 @@ func keyval2attrs(attr ...string) ([]object.Attribute, error) { } return attributes, nil } + +// Load private key from wallet file +func getKey(walletPath, walletAccount, walletPassword string) (*ecdsa.PrivateKey, error) { + var err error + if walletPath == "" { + key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) // TODO: using ephemeral keys for now, later read from env vars + if err != nil { + return nil, fmt.Errorf("generating ephemeral key: %w", err) + } + return key, nil + } + w, err := wallet.NewWalletFromFile(walletPath) + if err != nil { + return nil, err + } + defer w.Close() + if len(w.Accounts) == 0 { + return nil, fmt.Errorf("no accounts in wallet: %s", walletPath) + } + account := w.Accounts[0] + if walletAccount != "" { + decode, err := base58.CheckDecode(walletAccount) + if err != nil { + return nil, fmt.Errorf("invalid account address: %w", err) + } + hash, err := util.Uint160DecodeBytesBE(decode[1:]) + if err != nil { + return nil, fmt.Errorf("invalid account hash: %w", err) + } + account = w.GetAccount(hash) + if account == nil { + return nil, fmt.Errorf("account not found: %s", walletAccount) + } + } + defer account.Close() + err = account.Decrypt(walletPassword, w.Scrypt) + if err != nil { + return nil, fmt.Errorf("failed to decrypt wallet: %w", err) + } + key := account.PrivateKey().PrivateKey + return &key, nil +} diff --git a/providers/http/frostfs/client_test.go b/providers/http/frostfs/client_test.go index 495f7b27..fd0c5601 100644 --- a/providers/http/frostfs/client_test.go +++ b/providers/http/frostfs/client_test.go @@ -9,8 +9,11 @@ import ( "crypto/ecdsa" "crypto/elliptic" "crypto/rand" + "fmt" "os" "sync" + + "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/user" ) // Initialize storage backend for tests @@ -98,3 +101,31 @@ func TestMulipleObjects(t *testing.T) { } wg.Wait() } + +// Check opening wallet from file system +func TestLoadWallet(t *testing.T) { + const ( + walletPath = "client_test_wallet.json" + walletAccount = "NWZnjbTKbzwtX6w1q5R3kbEKrnJ5bp1kn7" + ) + key, err := getKey(walletPath, "", "") + if err != nil { + t.Fatal(err) + } + if key2addr(key) != walletAccount { + t.Fatalf("incorrect address for default account: want %s, got %s", walletAccount, key2addr(key)) + } + key, err = getKey(walletPath, walletAccount, "") + if err != nil { + t.Fatal(err) + } + if key2addr(key) != walletAccount { + t.Fatalf("incorrect address for specific account: want %s, got %s", walletAccount, key2addr(key)) + } +} + +func key2addr(k *ecdsa.PrivateKey) string { + var owner user.ID + user.IDFromKey(&owner, k.PublicKey) + return fmt.Sprint(owner) +} diff --git a/providers/http/frostfs/client_test_wallet.json b/providers/http/frostfs/client_test_wallet.json new file mode 100644 index 00000000..4d4c28cc --- /dev/null +++ b/providers/http/frostfs/client_test_wallet.json @@ -0,0 +1 @@ +{"version":"1.0","accounts":[{"address":"NWZnjbTKbzwtX6w1q5R3kbEKrnJ5bp1kn7","key":"6PYNiUNC8ss4jfvhfTeXkboUaw8rF1Fycvi8sGfuKWkApJyxJwwQhJYUMS","label":"notused","contract":{"script":"DCECqsn8tWyTP/7yDX76x5POd+QJPma2QaEaUK5FMo+1K+NBVuezJw==","parameters":[{"name":"parameter0","type":"Signature"}],"deployed":false},"lock":false,"isDefault":false}],"scrypt":{"n":16384,"r":8,"p":8},"extra":{"Tokens":null}} \ No newline at end of file -- 2.45.2 From d8c8aba31256c90294c7c43d018db2baf5f22374 Mon Sep 17 00:00:00 2001 From: Vitaliy Potyarkin Date: Tue, 15 Oct 2024 16:52:34 +0300 Subject: [PATCH 4/9] Add HTTP-01 solver with FrostFS backend Signed-off-by: Vitaliy Potyarkin --- cmd/flags.go | 30 +++++++++++++++ cmd/setup_challenges.go | 13 +++++++ docs/data/zz_cli_help.toml | 5 +++ providers/http/frostfs/frostfs.go | 64 +++++++++++++++++++++++++++++++ 4 files changed, 112 insertions(+) create mode 100644 providers/http/frostfs/frostfs.go diff --git a/cmd/flags.go b/cmd/flags.go index d119df84..5998071f 100644 --- a/cmd/flags.go +++ b/cmd/flags.go @@ -29,6 +29,11 @@ const ( flgHTTPWebroot = "http.webroot" flgHTTPMemcachedHost = "http.memcached-host" flgHTTPS3Bucket = "http.s3-bucket" + flgHTTPFrostFSEndpoint = "http.frostfs-endpoint" + flgHTTPFrostFSContainer = "http.frostfs-container" + flgHTTPFrostFSWallet = "http.frostfs-wallet" + flgHTTPFrostFSWalletAccount = "http.frostfs-wallet-account" + flgHTTPFrostFSWalletPass = "http.frostfs-wallet-password" flgTLS = "tls" flgTLSPort = "tls.port" flgDNS = "dns" @@ -135,6 +140,31 @@ func CreateFlags(defaultPath string) []cli.Flag { Name: flgHTTPS3Bucket, Usage: "Set the S3 bucket name to use for HTTP-01 based challenges. Challenges will be written to the S3 bucket.", }, + &cli.StringFlag{ + Name: flgHTTPFrostFSEndpoint, + Usage: "Set FrostFS endpoint to use for HTTP-01 based challenges. Challenges will be written to FrostFS container", + EnvVars: []string{"FROSTFS_ENDPOINT"}, + }, + &cli.StringFlag{ + Name: flgHTTPFrostFSContainer, + Usage: "Set FrostFS container ID to use for HTTP-01 based challenges. Challenges will be written to FrostFS container", + EnvVars: []string{"FROSTFS_CONTAINER"}, + }, + &cli.StringFlag{ + Name: flgHTTPFrostFSWallet, + Usage: "Path to NEO wallet to use for interaction with FrostFS. If no wallet is provided an ephemeral one will be generated. Such key will only work for publicly writable containers and will significantly reduce security: any attacker with knowledge of FrostFS endpoint and CID will be able to obtain certificates for your domain", + EnvVars: []string{"FROSTFS_WALLET"}, + }, + &cli.StringFlag{ + Name: flgHTTPFrostFSWalletAccount, + Usage: "Wallet account to use for interaction with FrostFS. If not set, the first account from wallet will be used", + EnvVars: []string{"FROSTFS_WALLET_ACCOUNT"}, + }, + &cli.StringFlag{ + Name: flgHTTPFrostFSWalletPass, + Usage: "Account password to decrypt the wallet. If not set, an empty password is assumed", + EnvVars: []string{"FROSTFS_WALLET_PASSWORD"}, + }, &cli.BoolFlag{ Name: flgTLS, Usage: "Use the TLS-ALPN-01 challenge to solve challenges. Can be mixed with other types of challenges.", diff --git a/cmd/setup_challenges.go b/cmd/setup_challenges.go index 2ec38198..442f0364 100644 --- a/cmd/setup_challenges.go +++ b/cmd/setup_challenges.go @@ -13,6 +13,7 @@ import ( "github.com/go-acme/lego/v4/lego" "github.com/go-acme/lego/v4/log" "github.com/go-acme/lego/v4/providers/dns" + "github.com/go-acme/lego/v4/providers/http/frostfs" "github.com/go-acme/lego/v4/providers/http/memcached" "github.com/go-acme/lego/v4/providers/http/s3" "github.com/go-acme/lego/v4/providers/http/webroot" @@ -67,6 +68,18 @@ func setupHTTPProvider(ctx *cli.Context) challenge.Provider { log.Fatal(err) } return ps + case ctx.IsSet(flgHTTPFrostFSEndpoint): + ps, err := frostfs.NewHTTPProvider( + ctx.String(flgHTTPFrostFSEndpoint), + ctx.String(flgHTTPFrostFSContainer), + ctx.String(flgHTTPFrostFSWallet), + ctx.String(flgHTTPFrostFSWalletAccount), + ctx.String(flgHTTPFrostFSWalletPass), + ) + if err != nil { + log.Fatal(err) + } + return ps case ctx.IsSet(flgHTTPPort): iface := ctx.String(flgHTTPPort) if !strings.Contains(iface, ":") { diff --git a/docs/data/zz_cli_help.toml b/docs/data/zz_cli_help.toml index 94275c06..6b8b1f27 100644 --- a/docs/data/zz_cli_help.toml +++ b/docs/data/zz_cli_help.toml @@ -36,6 +36,11 @@ GLOBAL OPTIONS: --http.webroot value Set the webroot folder to use for HTTP-01 based challenges to write directly to the .well-known/acme-challenge file. This disables the built-in server and expects the given directory to be publicly served with access to .well-known/acme-challenge --http.memcached-host value [ --http.memcached-host value ] Set the memcached host(s) to use for HTTP-01 based challenges. Challenges will be written to all specified hosts. --http.s3-bucket value Set the S3 bucket name to use for HTTP-01 based challenges. Challenges will be written to the S3 bucket. + --http.frostfs-endpoint value Set FrostFS endpoint to use for HTTP-01 based challenges. Challenges will be written to FrostFS container [$FROSTFS_ENDPOINT] + --http.frostfs-container value Set FrostFS container ID to use for HTTP-01 based challenges. Challenges will be written to FrostFS container [$FROSTFS_CONTAINER] + --http.frostfs-wallet value Path to NEO wallet to use for interaction with FrostFS. If no wallet is provided an ephemeral one will be generated. Such key will only work for publicly writable containers and will significantly reduce security: any attacker with knowledge of FrostFS endpoint and CID will be able to obtain certificates for your domain [$FROSTFS_WALLET] + --http.frostfs-wallet-account value Wallet account to use for interaction with FrostFS. If not set, the first account from wallet will be used [$FROSTFS_WALLET_ACCOUNT] + --http.frostfs-wallet-password value Account password to decrypt the wallet. If not set, an empty password is assumed [$FROSTFS_WALLET_PASSWORD] --tls Use the TLS-ALPN-01 challenge to solve challenges. Can be mixed with other types of challenges. (default: false) --tls.port value Set the port and interface to use for TLS-ALPN-01 based challenges to listen on. Supported: interface:port or :port. (default: ":443") --dns value Solve a DNS-01 challenge using the specified provider. Can be mixed with other types of challenges. Run 'lego dnshelp' for help on usage. diff --git a/providers/http/frostfs/frostfs.go b/providers/http/frostfs/frostfs.go new file mode 100644 index 00000000..40b2b7a0 --- /dev/null +++ b/providers/http/frostfs/frostfs.go @@ -0,0 +1,64 @@ +// Package frostfs provides HTTP-01 solver that saves challenge token to +// FrostFS to make it available to multiple hosts at once. +// Useful for deploying FrostFS gateways (HTTP or S3) +package frostfs + +import ( + "context" + "errors" + "fmt" + + "github.com/go-acme/lego/v4/challenge" +) + +// HTTPProvider is a custom solver for HTTP-01 challenge that saves token to FrostFS. +type HTTPProvider struct { + frostfs *Storage + oid string +} + +var _ challenge.Provider = new(HTTPProvider) + +func NewHTTPProvider(endpoint, cid, walletPath, walletAccount, walletPassword string) (*HTTPProvider, error) { + if endpoint == "" { + return nil, errors.New("empty endpoint") + } + if cid == "" { + return nil, errors.New("empty container id") + } + key, err := getKey(walletPath, walletAccount, walletPassword) + if err != nil { + return nil, err + } + storage, err := Open(endpoint, cid, key) + if err != nil { + return nil, err + } + return &HTTPProvider{frostfs: storage}, nil +} + +func (w *HTTPProvider) Present(domain, token, keyAuth string) error { + var err error + if w.oid != "" { + return fmt.Errorf("%T is not safe to re-enter: object was saved and not yet cleaned up: %s", w, w.oid) + } + w.oid, err = w.frostfs.Save( + context.TODO(), + []byte(keyAuth), + "FileName", token, + "ACME", token, + ) + return err +} + +func (w *HTTPProvider) CleanUp(domain, token, keyAuth string) error { + if w.oid == "" { + panic("Cleanup() called before Present()") + } + err := w.frostfs.Delete(context.TODO(), w.oid) + if err != nil { + return err + } + w.oid = "" + return nil +} -- 2.45.2 From 254983fbe2b61984ef649b6bac89c135dc1eec3c Mon Sep 17 00:00:00 2001 From: Vitaliy Potyarkin Date: Tue, 15 Oct 2024 16:52:53 +0300 Subject: [PATCH 5/9] Fix linter errors Signed-off-by: Vitaliy Potyarkin --- providers/http/frostfs/client.go | 30 +++++++++++++-------------- providers/http/frostfs/client_test.go | 30 +++++++++++++-------------- 2 files changed, 29 insertions(+), 31 deletions(-) diff --git a/providers/http/frostfs/client.go b/providers/http/frostfs/client.go index a3825ff9..fc063792 100644 --- a/providers/http/frostfs/client.go +++ b/providers/http/frostfs/client.go @@ -8,19 +8,18 @@ import ( "fmt" "time" - "github.com/nspcc-dev/neo-go/pkg/encoding/base58" - "github.com/nspcc-dev/neo-go/pkg/util" - "github.com/nspcc-dev/neo-go/pkg/wallet" - "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/client" status "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/client/status" containerid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id" "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object" objectid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id" "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/user" + "github.com/nspcc-dev/neo-go/pkg/encoding/base58" + "github.com/nspcc-dev/neo-go/pkg/util" + "github.com/nspcc-dev/neo-go/pkg/wallet" ) -// Barebones API client for a single FrostFS container +// Storage provides barebones API client for a single FrostFS container. type Storage struct { endpoint string container containerid.ID @@ -30,7 +29,7 @@ type Storage struct { const storageRequestTimeout = 10 * time.Second -// Create FrostFS client for working with a single storage container +// Open FrostFS client for working with a single storage container. func Open(endpoint, containerID string, key *ecdsa.PrivateKey) (*Storage, error) { var container containerid.ID err := container.DecodeString(containerID) @@ -95,10 +94,10 @@ func (s *Storage) Save(ctx context.Context, data []byte, attr ...string) (oid st return "", fmt.Errorf("saving object to storage: %w", stat.(error)) } id, _ := obj.ID() - return fmt.Sprint(id), nil + return id.String(), nil } -// Delete object from container +// Delete object from container. func (s *Storage) Delete(ctx context.Context, oid string) error { var obj objectid.ID err := obj.DecodeString(oid) @@ -126,13 +125,13 @@ func (s *Storage) Delete(ctx context.Context, oid string) error { return nil } -// Open new connection to FrostFS storage node +// Open new connection to FrostFS storage node. func (s *Storage) dial(ctx context.Context) (*client.Client, error) { - var wallet client.PrmInit - wallet.Key = *s.key + var w client.PrmInit + w.Key = *s.key var c client.Client - c.Init(wallet) + c.Init(w) var endpoint client.PrmDial endpoint.Endpoint = s.endpoint @@ -144,7 +143,7 @@ func (s *Storage) dial(ctx context.Context) (*client.Client, error) { return &c, nil } -// Compose object attributes slice +// Compose object attributes slice. func keyval2attrs(attr ...string) ([]object.Attribute, error) { if len(attr)%2 != 0 { return nil, fmt.Errorf("odd number of key-value strings: %d (must be even)", len(attr)) @@ -163,9 +162,8 @@ func keyval2attrs(attr ...string) ([]object.Attribute, error) { return attributes, nil } -// Load private key from wallet file +// Load private key from wallet file. func getKey(walletPath, walletAccount, walletPassword string) (*ecdsa.PrivateKey, error) { - var err error if walletPath == "" { key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) // TODO: using ephemeral keys for now, later read from env vars if err != nil { @@ -183,7 +181,7 @@ func getKey(walletPath, walletAccount, walletPassword string) (*ecdsa.PrivateKey } account := w.Accounts[0] if walletAccount != "" { - decode, err := base58.CheckDecode(walletAccount) + decode, err := base58.CheckDecode(walletAccount) //nolint:govet // err shadow declaration if err != nil { return nil, fmt.Errorf("invalid account address: %w", err) } diff --git a/providers/http/frostfs/client_test.go b/providers/http/frostfs/client_test.go index fd0c5601..1334bc76 100644 --- a/providers/http/frostfs/client_test.go +++ b/providers/http/frostfs/client_test.go @@ -3,8 +3,6 @@ package frostfs // Tests for our FrostFS client code import ( - "testing" - "context" "crypto/ecdsa" "crypto/elliptic" @@ -12,14 +10,16 @@ import ( "fmt" "os" "sync" + "testing" "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/user" ) -// Initialize storage backend for tests +// Initialize storage backend for tests. func openStorage(t *testing.T) *Storage { - cid := os.Getenv("FROSTFS_CID") // example: 348WWfBKbS79Wbmm38MRE3oBoEDM5Ga1XXbGKGNyisDM - endpoint := os.Getenv("FROSTFS_ENDPOINT") // example: grpc://localhost:8802 + t.Helper() + cid := os.Getenv("FROSTFS_CID") // sample: "348WWfBKbS79Wbmm38MRE3oBoEDM5Ga1XXbGKGNyisDM" + endpoint := os.Getenv("FROSTFS_ENDPOINT") // sample: "grpc://localhost:8802" if cid == "" || endpoint == "" { t.Skipf("one or more environment variables not set: FROSTFS_ENDPOINT, FROSTFS_CID") } @@ -34,9 +34,9 @@ func openStorage(t *testing.T) *Storage { return storage } -// Save some bytes to FrostFS and clean up after +// Save some bytes to FrostFS and clean up after. func TestObjectPutDelete(t *testing.T) { - var payload = []byte("Hello FrostFS!\n") + payload := []byte("Hello FrostFS!\n") storage := openStorage(t) ctx, cancel := context.WithCancel(context.Background()) t.Cleanup(cancel) @@ -52,9 +52,9 @@ func TestObjectPutDelete(t *testing.T) { t.Logf("deleted object: %s", oid) } -// Check that FrostFS is in fact a content addressable storage +// Check that FrostFS is in fact a content addressable storage. func TestMulipleObjects(t *testing.T) { - var payload = []byte("Multiple objects with same content should get the same object ID") + payload := []byte("Multiple objects with same content should get the same object ID") ctx, cancel := context.WithCancel(context.Background()) t.Cleanup(cancel) @@ -76,7 +76,7 @@ func TestMulipleObjects(t *testing.T) { continue } if obj != baseline { - t.Fatalf("non-identical object id: %s != %s", baseline, obj) + panic(fmt.Errorf("non-identical object id: %s != %s", baseline, obj)) } } } @@ -84,17 +84,17 @@ func TestMulipleObjects(t *testing.T) { const testCount = 5 storage := openStorage(t) - for i := 0; i < testCount; i++ { + for i := 0; i < testCount; i++ { //nolint:intrange wg.Add(1) go func() { defer wg.Done() oid, err := storage.Save(ctx, payload, "FileName", "CAS.txt", "Tag", "test") if err != nil { - t.Fatal(err) + panic(err) } err = storage.Delete(ctx, oid) if err != nil { - t.Fatal(err) + panic(err) } t.Log(oid) }() @@ -102,7 +102,7 @@ func TestMulipleObjects(t *testing.T) { wg.Wait() } -// Check opening wallet from file system +// Check opening wallet from file system. func TestLoadWallet(t *testing.T) { const ( walletPath = "client_test_wallet.json" @@ -127,5 +127,5 @@ func TestLoadWallet(t *testing.T) { func key2addr(k *ecdsa.PrivateKey) string { var owner user.ID user.IDFromKey(&owner, k.PublicKey) - return fmt.Sprint(owner) + return owner.String() } -- 2.45.2 From 9ff9d5be257d2992fed71f2d32eed8053d6b2c60 Mon Sep 17 00:00:00 2001 From: Vitaliy Potyarkin Date: Wed, 16 Oct 2024 12:05:26 +0300 Subject: [PATCH 6/9] frostfs: Fix invalid signatures issued by key from json Ephemeral keys worked fine while keys loaded from filesystem would generate invalid signatures. This was caused by destroying private key material during calls to Wallet.Close() and Account.Close(). Since these calls do nothing except wiping the private key, we omit them now. Responsibility for private key security is delegated to caller of getKey() Signed-off-by: Vitaliy Potyarkin --- providers/http/frostfs/client.go | 27 +++++++--- providers/http/frostfs/client_test.go | 77 +++++++++++++++++++++++---- 2 files changed, 88 insertions(+), 16 deletions(-) diff --git a/providers/http/frostfs/client.go b/providers/http/frostfs/client.go index fc063792..e24ceca6 100644 --- a/providers/http/frostfs/client.go +++ b/providers/http/frostfs/client.go @@ -5,6 +5,7 @@ import ( "crypto/ecdsa" "crypto/elliptic" "crypto/rand" + "errors" "fmt" "time" @@ -73,6 +74,9 @@ func (s *Storage) Save(ctx context.Context, data []byte, attr ...string) (oid st if err != nil { return "", fmt.Errorf("signing object: %w", err) } + if !obj.VerifyIDSignature() { + return "", errors.New("signing object: invalid signature was generated") + } ctx, cancel := context.WithTimeout(ctx, storageRequestTimeout) defer cancel() @@ -163,19 +167,21 @@ func keyval2attrs(attr ...string) ([]object.Attribute, error) { } // Load private key from wallet file. -func getKey(walletPath, walletAccount, walletPassword string) (*ecdsa.PrivateKey, error) { +func getKey(walletPath, walletAccount, walletPassword string) (*ecdsa.PrivateKey, error) { //nolint:gocyclo if walletPath == "" { - key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) // TODO: using ephemeral keys for now, later read from env vars + key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) if err != nil { return nil, fmt.Errorf("generating ephemeral key: %w", err) } return key, nil } + + // This function intentionally omits calls to w.Close() and account.Close() + // because that would destroy the underlying ecdsa.PrivateKey. w, err := wallet.NewWalletFromFile(walletPath) if err != nil { return nil, err } - defer w.Close() if len(w.Accounts) == 0 { return nil, fmt.Errorf("no accounts in wallet: %s", walletPath) } @@ -185,6 +191,12 @@ func getKey(walletPath, walletAccount, walletPassword string) (*ecdsa.PrivateKey if err != nil { return nil, fmt.Errorf("invalid account address: %w", err) } + if len(decode) != 21 { + return nil, fmt.Errorf("invalid account address length: %d bytes", len(decode)) + } + if decode[0] != 0x35 { + return nil, fmt.Errorf("invalid account address first byte: %s -> %#x", walletAccount, decode[0]) + } hash, err := util.Uint160DecodeBytesBE(decode[1:]) if err != nil { return nil, fmt.Errorf("invalid account hash: %w", err) @@ -194,10 +206,11 @@ func getKey(walletPath, walletAccount, walletPassword string) (*ecdsa.PrivateKey return nil, fmt.Errorf("account not found: %s", walletAccount) } } - defer account.Close() - err = account.Decrypt(walletPassword, w.Scrypt) - if err != nil { - return nil, fmt.Errorf("failed to decrypt wallet: %w", err) + if account.PrivateKey() == nil { + err = account.Decrypt(walletPassword, w.Scrypt) + if err != nil { + return nil, fmt.Errorf("failed to decrypt wallet: %w", err) + } } key := account.PrivateKey().PrivateKey return &key, nil diff --git a/providers/http/frostfs/client_test.go b/providers/http/frostfs/client_test.go index 1334bc76..42dd3587 100644 --- a/providers/http/frostfs/client_test.go +++ b/providers/http/frostfs/client_test.go @@ -3,27 +3,32 @@ package frostfs // Tests for our FrostFS client code import ( + "bytes" "context" "crypto/ecdsa" - "crypto/elliptic" - "crypto/rand" - "fmt" + "crypto/sha256" "os" "sync" "testing" "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/user" + "github.com/nspcc-dev/neo-go/pkg/crypto/keys" + "github.com/nspcc-dev/neo-go/pkg/wallet" ) // Initialize storage backend for tests. func openStorage(t *testing.T) *Storage { t.Helper() - cid := os.Getenv("FROSTFS_CID") // sample: "348WWfBKbS79Wbmm38MRE3oBoEDM5Ga1XXbGKGNyisDM" + cid := os.Getenv("FROSTFS_CONTAINER") // sample: "348WWfBKbS79Wbmm38MRE3oBoEDM5Ga1XXbGKGNyisDM" endpoint := os.Getenv("FROSTFS_ENDPOINT") // sample: "grpc://localhost:8802" if cid == "" || endpoint == "" { - t.Skipf("one or more environment variables not set: FROSTFS_ENDPOINT, FROSTFS_CID") + t.Skipf("one or more environment variables not set: FROSTFS_ENDPOINT, FROSTFS_CONTAINER") } - key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) // TODO: using ephemeral keys for now, later read from env vars + key, err := getKey( + os.Getenv("FROSTFS_WALLET"), + os.Getenv("FROSTFS_WALLET_ACCOUNT"), + os.Getenv("FROSTFS_WALLET_PASSWORD"), + ) if err != nil { t.Fatal(err) } @@ -76,7 +81,8 @@ func TestMulipleObjects(t *testing.T) { continue } if obj != baseline { - panic(fmt.Errorf("non-identical object id: %s != %s", baseline, obj)) + t.Errorf("non-identical object id: %s != %s", baseline, obj) + return } } } @@ -90,11 +96,18 @@ func TestMulipleObjects(t *testing.T) { defer wg.Done() oid, err := storage.Save(ctx, payload, "FileName", "CAS.txt", "Tag", "test") if err != nil { - panic(err) + t.Error(err) + return } err = storage.Delete(ctx, oid) if err != nil { - panic(err) + t.Error(err) + return + } + select { + case objects <- oid: + case <-ctx.Done(): + return } t.Log(oid) }() @@ -129,3 +142,49 @@ func key2addr(k *ecdsa.PrivateKey) string { user.IDFromKey(&owner, k.PublicKey) return owner.String() } + +// Check that loaded wallet key generates valid signature. +func TestLoadWalletAndSign(t *testing.T) { + const ( + walletPath = "client_test_wallet.json" + walletAccount = "NWZnjbTKbzwtX6w1q5R3kbEKrnJ5bp1kn7" + walletAccountPassword = "" + walletAccountIndex = 0 + dataString = "some data to sign in this test" + ) + w, err := wallet.NewWalletFromFile(walletPath) + if err != nil { + t.Fatalf("wallet from file: %v", err) + } + accountFromWallet := w.Accounts[walletAccountIndex] + err = accountFromWallet.Decrypt(walletAccountPassword, w.Scrypt) + if err != nil { + t.Fatalf("wallet decrypt: %v", err) + } + + key, err := getKey(walletPath, walletAccount, walletAccountPassword) + if err != nil { + t.Fatal(err) + } + wrappedKey := &keys.PrivateKey{PrivateKey: *key} + accountFromKey := wallet.NewAccountFromPrivateKey(wrappedKey) + + if !accountFromKey.PublicKey().Equal(accountFromWallet.PublicKey()) { + t.Fatalf("corrupted key: want %s, got %s", accountFromWallet.PublicKey().Address(), accountFromKey.PublicKey().Address()) + } + data := []byte(dataString) + sig := accountFromKey.PrivateKey().Sign(data) + hash := sha256.Sum256(data) + hashSig := accountFromKey.PrivateKey().SignHash(hash) + if !bytes.Equal(sig, hashSig) { + t.Fatalf("different signatures for data (%x) and for hash (%x)", sig, hashSig) + } + ok := accountFromKey.PublicKey().Verify(sig, hash[:]) + if !ok { + t.Errorf("signature %x (%d bytes): check failed: signing key", sig, len(sig)) + } + ok = accountFromWallet.PublicKey().Verify(sig, hash[:]) + if !ok { + t.Errorf("signature %x (%d bytes): check failed: wallet key", sig, len(sig)) + } +} -- 2.45.2 From 61ce76f6486ac129126899db79607ae005669baa Mon Sep 17 00:00:00 2001 From: Vitaliy Potyarkin Date: Wed, 16 Oct 2024 16:18:20 +0300 Subject: [PATCH 7/9] frostfs: Expire saved tokens automatically Signed-off-by: Vitaliy Potyarkin --- providers/http/frostfs/client.go | 58 +++++++++++++++++++++++++++ providers/http/frostfs/client_test.go | 21 ++++++++++ providers/http/frostfs/frostfs.go | 19 ++++++++- 3 files changed, 97 insertions(+), 1 deletion(-) diff --git a/providers/http/frostfs/client.go b/providers/http/frostfs/client.go index e24ceca6..0fa3a619 100644 --- a/providers/http/frostfs/client.go +++ b/providers/http/frostfs/client.go @@ -7,6 +7,7 @@ import ( "crypto/rand" "errors" "fmt" + "math" "time" "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/client" @@ -26,6 +27,7 @@ type Storage struct { container containerid.ID key *ecdsa.PrivateKey user user.ID + epoch epochCalc } const storageRequestTimeout = 10 * time.Second @@ -215,3 +217,59 @@ func getKey(walletPath, walletAccount, walletPassword string) (*ecdsa.PrivateKey key := account.PrivateKey().PrivateKey return &key, nil } + +// Epoch converts human time value into FrostFS epoch that is expected to be +// current at that time. +// +// Due to nonlinear nature of FrostFS time these calculations are approximate +// for the future and are likely wrong for the past. +func (s *Storage) Epoch(ctx context.Context, t time.Time) (epoch uint64, err error) { + if !s.epoch.Ready() { + var cancel context.CancelFunc + ctx, cancel = context.WithTimeout(ctx, storageRequestTimeout) + defer cancel() + c, err := s.dial(ctx) + if err != nil { + return 0, fmt.Errorf("connecting to storage node: %w", err) + } + res, err := c.NetworkInfo(ctx, client.PrmNetworkInfo{}) + if err != nil { + return 0, fmt.Errorf("network info request: %w", err) + } + stat := res.Status() + if !status.IsSuccessful(stat) { + return 0, fmt.Errorf("network info: %w", stat.(error)) + } + info := res.Info() + s.epoch = epochCalc{ + timestamp: time.Now(), + epoch: info.CurrentEpoch(), + blockPerEpoch: info.EpochDuration(), + msPerBlock: info.MsPerBlock(), + } + } + if !s.epoch.Ready() { + return 0, errors.New("failed to initialize epoch calculator") + } + return s.epoch.At(t), nil +} + +type epochCalc struct { + timestamp time.Time + epoch uint64 + msPerBlock int64 + blockPerEpoch uint64 +} + +func (e epochCalc) At(t time.Time) uint64 { + return uint64( + int64(e.epoch) + + int64(math.Ceil( + float64(t.Sub(e.timestamp).Milliseconds())/ + float64(e.msPerBlock)/ + float64(e.blockPerEpoch)))) +} + +func (e epochCalc) Ready() bool { + return !e.timestamp.IsZero() && e.epoch != 0 && e.msPerBlock != 0 && e.blockPerEpoch != 0 +} diff --git a/providers/http/frostfs/client_test.go b/providers/http/frostfs/client_test.go index 42dd3587..8ca45199 100644 --- a/providers/http/frostfs/client_test.go +++ b/providers/http/frostfs/client_test.go @@ -10,6 +10,7 @@ import ( "os" "sync" "testing" + "time" "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/user" "github.com/nspcc-dev/neo-go/pkg/crypto/keys" @@ -188,3 +189,23 @@ func TestLoadWalletAndSign(t *testing.T) { t.Errorf("signature %x (%d bytes): check failed: wallet key", sig, len(sig)) } } + +// Show epoch values for nearby times. +// +// This test is barely useful when run unattended: it will only validate "no +// errors, no panic" result. +// Human operator is required to check if epoch calculations actually make +// sense for network configuration being used. +func TestEpoch(t *testing.T) { + storage := openStorage(t) + now := time.Now() + var i time.Duration + for i = -10; i < 10; i++ { + timestamp := now.Add(i * time.Minute) //nolint:durationcheck + epoch, err := storage.Epoch(context.Background(), timestamp) + if err != nil { + t.Fatal(err) + } + t.Logf("%s: %d", timestamp, epoch) + } +} diff --git a/providers/http/frostfs/frostfs.go b/providers/http/frostfs/frostfs.go index 40b2b7a0..81d79724 100644 --- a/providers/http/frostfs/frostfs.go +++ b/providers/http/frostfs/frostfs.go @@ -7,10 +7,18 @@ import ( "context" "errors" "fmt" + "strconv" + "time" "github.com/go-acme/lego/v4/challenge" ) +const ( + // Challenge token will be garbage collected sometime after this interval + // even if Cleanup() call fails for whatever reason. + tokenLifetime = 1 * time.Hour +) + // HTTPProvider is a custom solver for HTTP-01 challenge that saves token to FrostFS. type HTTPProvider struct { frostfs *Storage @@ -42,11 +50,20 @@ func (w *HTTPProvider) Present(domain, token, keyAuth string) error { if w.oid != "" { return fmt.Errorf("%T is not safe to re-enter: object was saved and not yet cleaned up: %s", w, w.oid) } + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + expires, err := w.frostfs.Epoch(ctx, time.Now().Add(tokenLifetime)) + if err != nil { + return fmt.Errorf("failed to calculate token expiration: %w", err) + } w.oid, err = w.frostfs.Save( - context.TODO(), + ctx, []byte(keyAuth), "FileName", token, "ACME", token, + "__SYSTEM__EXPIRATION_EPOCH", strconv.FormatUint(expires, 10), ) return err } -- 2.45.2 From 597d147c7dddd5fe0d679138055df9cf8dc16b12 Mon Sep 17 00:00:00 2001 From: Vitaliy Potyarkin Date: Wed, 16 Oct 2024 17:16:35 +0300 Subject: [PATCH 8/9] frostfs: Reject tokens with slash character Current reverse proxy configs assume that token is a valid filename with no nesting levels. It's better to reject unsupported tokens early Signed-off-by: Vitaliy Potyarkin --- providers/http/frostfs/frostfs.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/providers/http/frostfs/frostfs.go b/providers/http/frostfs/frostfs.go index 81d79724..813fa455 100644 --- a/providers/http/frostfs/frostfs.go +++ b/providers/http/frostfs/frostfs.go @@ -8,6 +8,7 @@ import ( "errors" "fmt" "strconv" + "strings" "time" "github.com/go-acme/lego/v4/challenge" @@ -46,7 +47,9 @@ func NewHTTPProvider(endpoint, cid, walletPath, walletAccount, walletPassword st } func (w *HTTPProvider) Present(domain, token, keyAuth string) error { - var err error + if strings.Contains(token, "/") { + return fmt.Errorf("token with slash character is not supported: %s", token) + } if w.oid != "" { return fmt.Errorf("%T is not safe to re-enter: object was saved and not yet cleaned up: %s", w, w.oid) } @@ -54,6 +57,7 @@ func (w *HTTPProvider) Present(domain, token, keyAuth string) error { ctx, cancel := context.WithCancel(context.Background()) defer cancel() + var err error expires, err := w.frostfs.Epoch(ctx, time.Now().Add(tokenLifetime)) if err != nil { return fmt.Errorf("failed to calculate token expiration: %w", err) -- 2.45.2 From 10ccc575874dcf68dbc9de8794b5a7b65b72d622 Mon Sep 17 00:00:00 2001 From: Vitaliy Potyarkin Date: Thu, 17 Oct 2024 11:21:50 +0300 Subject: [PATCH 9/9] frostfs: Add object ID to error messages Signed-off-by: Vitaliy Potyarkin --- providers/http/frostfs/client.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/providers/http/frostfs/client.go b/providers/http/frostfs/client.go index 0fa3a619..3c3f69a1 100644 --- a/providers/http/frostfs/client.go +++ b/providers/http/frostfs/client.go @@ -122,11 +122,11 @@ func (s *Storage) Delete(ctx context.Context, oid string) error { Key: s.key, }) if err != nil { - return fmt.Errorf("delete request: %w", err) + return fmt.Errorf("delete request (%s): %w", oid, err) } stat := res.Status() if !status.IsSuccessful(stat) { - return fmt.Errorf("delete object: %w", stat.(error)) + return fmt.Errorf("delete object (%s): %w", oid, stat.(error)) } return nil } -- 2.45.2