package repo import ( "encoding/json" "io" "io/ioutil" "os" "path/filepath" "strings" "sync" "github.com/ipfs/go-datastore" "github.com/ipfs/go-datastore/namespace" badger "github.com/ipfs/go-ds-badger" fslock "github.com/ipfs/go-fs-lock" logging "github.com/ipfs/go-log" "github.com/libp2p/go-libp2p-core/crypto" "github.com/mitchellh/go-homedir" "github.com/multiformats/go-base32" "github.com/multiformats/go-multiaddr" "github.com/pkg/errors" "golang.org/x/xerrors" "github.com/filecoin-project/go-lotus/chain/types" "github.com/filecoin-project/go-lotus/node/config" ) const ( fsAPI = "api" fsAPIToken = "token" fsConfig = "config.toml" fsDatastore = "datastore" fsLibp2pKey = "libp2p.priv" fsLock = "repo.lock" fsKeystore = "keystore" ) var log = logging.Logger("repo") var ErrRepoExists = errors.New("repo exists") // FsRepo is struct for repo, use NewFS to create type FsRepo struct { path string } var _ Repo = &FsRepo{} // NewFS creates a repo instance based on a path on file system func NewFS(path string) (*FsRepo, error) { path, err := homedir.Expand(path) if err != nil { return nil, err } return &FsRepo{ path: path, }, nil } func (fsr *FsRepo) Exists() (bool, error) { _, err := os.Stat(filepath.Join(fsr.path, fsConfig)) notexist := os.IsNotExist(err) if notexist { err = nil } return !notexist, err } func (fsr *FsRepo) Init() error { if _, err := os.Stat(fsr.path); err == nil { return fsr.initKeystore() } else if !os.IsNotExist(err) { return err } log.Infof("Initializing repo at '%s'", fsr.path) err := os.Mkdir(fsr.path, 0755) //nolint: gosec if err != nil { return err } c, err := os.Create(filepath.Join(fsr.path, fsConfig)) if err != nil { return err } if err := c.Close(); err != nil { return err } return fsr.initKeystore() } func (fsr *FsRepo) initKeystore() error { kstorePath := filepath.Join(fsr.path, fsKeystore) if _, err := os.Stat(kstorePath); err == nil { return ErrRepoExists } else if !os.IsNotExist(err) { return err } return os.Mkdir(kstorePath, 0700) } // APIEndpoint returns endpoint of API in this repo func (fsr *FsRepo) APIEndpoint() (multiaddr.Multiaddr, error) { p := filepath.Join(fsr.path, fsAPI) f, err := os.Open(p) if os.IsNotExist(err) { return nil, ErrNoAPIEndpoint } else if err != nil { return nil, err } defer f.Close() //nolint: errcheck // Read only op data, err := ioutil.ReadAll(f) if err != nil { return nil, err } strma := string(data) strma = strings.TrimSpace(strma) apima, err := multiaddr.NewMultiaddr(strma) if err != nil { return nil, err } return apima, nil } func (fsr *FsRepo) APIToken() ([]byte, error) { p := filepath.Join(fsr.path, fsAPIToken) f, err := os.Open(p) if os.IsNotExist(err) { return nil, ErrNoAPIEndpoint } else if err != nil { return nil, err } defer f.Close() //nolint: errcheck // Read only op return ioutil.ReadAll(f) } // Lock acquires exclusive lock on this repo func (fsr *FsRepo) Lock() (LockedRepo, error) { locked, err := fslock.Locked(fsr.path, fsLock) if err != nil { return nil, xerrors.Errorf("could not check lock status: %w", err) } if locked { return nil, ErrRepoAlreadyLocked } closer, err := fslock.Lock(fsr.path, fsLock) if err != nil { return nil, xerrors.Errorf("could not lock the repo: %w", err) } return &fsLockedRepo{ path: fsr.path, closer: closer, }, nil } type fsLockedRepo struct { path string closer io.Closer ds datastore.Batching dsErr error dsOnce sync.Once } func (fsr *fsLockedRepo) Path() string { return fsr.path } func (fsr *fsLockedRepo) Close() error { err := os.Remove(fsr.join(fsAPI)) if err != nil && !os.IsNotExist(err) { return xerrors.Errorf("could not remove API file: %w", err) } err = fsr.closer.Close() fsr.closer = nil return err } // join joins path elements with fsr.path func (fsr *fsLockedRepo) join(paths ...string) string { return filepath.Join(append([]string{fsr.path}, paths...)...) } func (fsr *fsLockedRepo) stillValid() error { if fsr.closer == nil { return ErrClosedRepo } return nil } func (fsr *fsLockedRepo) Datastore(ns string) (datastore.Batching, error) { fsr.dsOnce.Do(func() { fsr.ds, fsr.dsErr = badger.NewDatastore(fsr.join(fsDatastore), nil) /*if fsr.dsErr == nil { fsr.ds = datastore.NewLogDatastore(fsr.ds, "fsrepo") }*/ }) if fsr.dsErr != nil { return nil, fsr.dsErr } return namespace.Wrap(fsr.ds, datastore.NewKey(ns)), nil } func (fsr *fsLockedRepo) Config() (*config.Root, error) { if err := fsr.stillValid(); err != nil { return nil, err } return config.FromFile(fsr.join(fsConfig)) } func (fsr *fsLockedRepo) Libp2pIdentity() (crypto.PrivKey, error) { if err := fsr.stillValid(); err != nil { return nil, err } kpath := fsr.join(fsLibp2pKey) stat, err := os.Stat(kpath) if os.IsNotExist(err) { pk, err := genLibp2pKey() if err != nil { return nil, xerrors.Errorf("could not generate private key: %w", err) } pkb, err := pk.Bytes() if err != nil { return nil, xerrors.Errorf("could not serialize private key: %w", err) } err = ioutil.WriteFile(kpath, pkb, 0600) if err != nil { return nil, xerrors.Errorf("could not write private key: %w", err) } } else if err != nil { return nil, err } stat, err = os.Stat(kpath) if err != nil { return nil, err } if stat.Mode()&0066 != 0 { return nil, xerrors.New("libp2p identity has too wide access permissions, " + fsLibp2pKey + " should have permission 0600") } f, err := os.Open(kpath) if err != nil { return nil, xerrors.Errorf("could not open private key file: %w", err) } defer f.Close() //nolint: errcheck // read-only op pkbytes, err := ioutil.ReadAll(f) if err != nil { return nil, xerrors.Errorf("could not read private key file: %w", err) } pk, err := crypto.UnmarshalPrivateKey(pkbytes) if err != nil { return nil, xerrors.Errorf("could not unmarshal private key: %w", err) } return pk, nil } func (fsr *fsLockedRepo) SetAPIEndpoint(ma multiaddr.Multiaddr) error { if err := fsr.stillValid(); err != nil { return err } return ioutil.WriteFile(fsr.join(fsAPI), []byte(ma.String()), 0644) } func (fsr *fsLockedRepo) SetAPIToken(token []byte) error { if err := fsr.stillValid(); err != nil { return err } return ioutil.WriteFile(fsr.join(fsAPIToken), token, 0600) } func (fsr *fsLockedRepo) KeyStore() (types.KeyStore, error) { if err := fsr.stillValid(); err != nil { return nil, err } return fsr, nil } var kstrPermissionMsg = "permissions of key: '%s' are too relaxed, " + "required: 0600, got: %#o" // List lists all the keys stored in the KeyStore func (fsr *fsLockedRepo) List() ([]string, error) { if err := fsr.stillValid(); err != nil { return nil, err } kstorePath := fsr.join(fsKeystore) dir, err := os.Open(kstorePath) if err != nil { return nil, xerrors.Errorf("opening dir to list keystore: %w", err) } files, err := dir.Readdir(-1) if err != nil { return nil, xerrors.Errorf("reading keystore dir: %w", err) } keys := make([]string, 0, len(files)) for _, f := range files { if f.Mode()&0077 != 0 { return nil, xerrors.Errorf(kstrPermissionMsg, f.Name(), f.Mode()) } name, err := base32.RawStdEncoding.DecodeString(f.Name()) if err != nil { return nil, xerrors.Errorf("decoding key: '%s': %w", f.Name(), err) } keys = append(keys, string(name)) } return keys, nil } // Get gets a key out of keystore and returns types.KeyInfo coresponding to named key func (fsr *fsLockedRepo) Get(name string) (types.KeyInfo, error) { if err := fsr.stillValid(); err != nil { return types.KeyInfo{}, err } encName := base32.RawStdEncoding.EncodeToString([]byte(name)) keyPath := fsr.join(fsKeystore, encName) fstat, err := os.Stat(keyPath) if os.IsNotExist(err) { return types.KeyInfo{}, xerrors.Errorf("opening key '%s': %w", name, ErrKeyNotFound) } else if err != nil { return types.KeyInfo{}, xerrors.Errorf("opening key '%s': %w", name, err) } if fstat.Mode()&0077 != 0 { return types.KeyInfo{}, xerrors.Errorf(kstrPermissionMsg, name, err) } file, err := os.Open(keyPath) if err != nil { return types.KeyInfo{}, xerrors.Errorf("opening key '%s': %w", name, err) } defer file.Close() //nolint: errcheck // read only op data, err := ioutil.ReadAll(file) if err != nil { return types.KeyInfo{}, xerrors.Errorf("reading key '%s': %w", name, err) } var res types.KeyInfo err = json.Unmarshal(data, &res) if err != nil { return types.KeyInfo{}, xerrors.Errorf("decoding key '%s': %w", name, err) } return res, nil } // Put saves key info under given name func (fsr *fsLockedRepo) Put(name string, info types.KeyInfo) error { if err := fsr.stillValid(); err != nil { return err } encName := base32.RawStdEncoding.EncodeToString([]byte(name)) keyPath := fsr.join(fsKeystore, encName) _, err := os.Stat(keyPath) if err == nil { return xerrors.Errorf("checking key before put '%s': %w", name, ErrKeyExists) } else if !os.IsNotExist(err) { return xerrors.Errorf("checking key before put '%s': %w", name, err) } keyData, err := json.Marshal(info) if err != nil { return xerrors.Errorf("encoding key '%s': %w", name, err) } err = ioutil.WriteFile(keyPath, keyData, 0600) if err != nil { return xerrors.Errorf("writing key '%s': %w", name, err) } return nil } func (fsr *fsLockedRepo) Delete(name string) error { if err := fsr.stillValid(); err != nil { return err } encName := base32.RawStdEncoding.EncodeToString([]byte(name)) keyPath := fsr.join(fsKeystore, encName) _, err := os.Stat(keyPath) if os.IsNotExist(err) { return xerrors.Errorf("checking key before delete '%s': %w", name, ErrKeyNotFound) } else if err != nil { return xerrors.Errorf("checking key before delete '%s': %w", name, err) } err = os.Remove(keyPath) if err != nil { return xerrors.Errorf("deleting key '%s': %w", name, err) } return nil }