WIP: HTTP-01 solver that stores challenge tokens in FrostFS #4

Draft
potyarkin wants to merge 9 commits from potyarkin/lego:feature/http-01-frostfs-solver into tcl/http-01-frostfs-solver
9 changed files with 692 additions and 11 deletions

View file

@ -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.",

View file

@ -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, ":") {

View file

@ -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.

24
go.mod
View file

@ -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
@ -56,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
@ -90,6 +92,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 +106,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 +125,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 +152,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 +164,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/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
github.com/pelletier/go-toml/v2 v2.1.0 // indirect
@ -181,6 +197,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 +205,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 +216,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

59
go.sum
View file

@ -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=
@ -268,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=
@ -291,8 +310,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=
@ -315,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=
@ -347,6 +369,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 +394,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 +440,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 +487,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 +514,12 @@ 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=
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=
@ -642,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=
@ -651,6 +689,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=
@ -681,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=
@ -700,11 +742,13 @@ 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=
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=
@ -733,8 +777,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 +888,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 +1010,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=

View file

@ -0,0 +1,275 @@
package frostfs
import (
"context"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"errors"
"fmt"
"math"
"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"
"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"
)
// Storage provides barebones API client for a single FrostFS container.
type Storage struct {
endpoint string
container containerid.ID
key *ecdsa.PrivateKey
user user.ID
epoch epochCalc
}
const storageRequestTimeout = 10 * time.Second
// 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)
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)
}
if !obj.VerifyIDSignature() {
return "", errors.New("signing object: invalid signature was generated")
}
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 id.String(), 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 (%s): %w", oid, err)
}
stat := res.Status()
if !status.IsSuccessful(stat) {
return fmt.Errorf("delete object (%s): %w", oid, stat.(error))
}
return nil
}
// Open new connection to FrostFS storage node.
func (s *Storage) dial(ctx context.Context) (*client.Client, error) {
var w client.PrmInit
w.Key = *s.key
var c client.Client
c.Init(w)
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
}
// Load private key from wallet file.
func getKey(walletPath, walletAccount, walletPassword string) (*ecdsa.PrivateKey, error) { //nolint:gocyclo
if walletPath == "" {
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
}
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) //nolint:govet // err shadow declaration
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)
}
account = w.GetAccount(hash)
if account == nil {
return nil, fmt.Errorf("account not found: %s", walletAccount)
}
}
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
}
// 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
}

View file

@ -0,0 +1,211 @@
package frostfs
// Tests for our FrostFS client code
import (
"bytes"
"context"
"crypto/ecdsa"
"crypto/sha256"
"os"
"sync"
"testing"
"time"
"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_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_CONTAINER")
}
key, err := getKey(
os.Getenv("FROSTFS_WALLET"),
os.Getenv("FROSTFS_WALLET_ACCOUNT"),
os.Getenv("FROSTFS_WALLET_PASSWORD"),
)
if err != nil {
t.Fatal(err)
}
storage, err := Open(endpoint, cid, key)
if err != nil {
t.Fatal(err)
}
return storage
}
// Save some bytes to FrostFS and clean up after.
func TestObjectPutDelete(t *testing.T) {
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) {
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.Errorf("non-identical object id: %s != %s", baseline, obj)
return
}
}
}
}()
const testCount = 5
storage := openStorage(t)
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.Error(err)
return
}
err = storage.Delete(ctx, oid)
if err != nil {
t.Error(err)
return
}
select {
case objects <- oid:
case <-ctx.Done():
return
}
t.Log(oid)
}()
}
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 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))
}
}
// 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)
}
}

View file

@ -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}}

View file

@ -0,0 +1,85 @@
// 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"
"strconv"
"strings"
"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
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 {
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)
}
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)
}
w.oid, err = w.frostfs.Save(
ctx,
[]byte(keyAuth),
"FileName", token,
"ACME", token,
"__SYSTEM__EXPIRATION_EPOCH", strconv.FormatUint(expires, 10),
)
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
}