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 }