344 lines
9.0 KiB
Go
344 lines
9.0 KiB
Go
/*
|
|
Copyright 2021 The Dapr Authors
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
*/
|
|
|
|
package localstorage
|
|
|
|
import (
|
|
"context"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"reflect"
|
|
"strconv"
|
|
"strings"
|
|
|
|
securejoin "github.com/cyphar/filepath-securejoin"
|
|
"github.com/google/uuid"
|
|
|
|
"github.com/dapr/components-contrib/bindings"
|
|
"github.com/dapr/components-contrib/metadata"
|
|
"github.com/dapr/kit/logger"
|
|
)
|
|
|
|
const (
|
|
fileNameMetadataKey = "fileName"
|
|
)
|
|
|
|
// List of root paths that are disallowed
|
|
var disallowedRootPaths = []string{
|
|
filepath.Clean("/proc"),
|
|
filepath.Clean("/sys"),
|
|
filepath.Clean("/boot"),
|
|
// See: https://github.com/dapr/components-contrib/issues/2444
|
|
filepath.Clean("/var/run/secrets"),
|
|
}
|
|
|
|
// LocalStorage allows saving files to disk.
|
|
type LocalStorage struct {
|
|
metadata *Metadata
|
|
logger logger.Logger
|
|
}
|
|
|
|
// Metadata defines the metadata.
|
|
type Metadata struct {
|
|
RootPath string `json:"rootPath"`
|
|
}
|
|
|
|
type createResponse struct {
|
|
FileName string `json:"fileName"`
|
|
}
|
|
|
|
// NewLocalStorage returns a new LocalStorage instance.
|
|
func NewLocalStorage(logger logger.Logger) bindings.OutputBinding {
|
|
return &LocalStorage{logger: logger}
|
|
}
|
|
|
|
// Init performs metadata parsing.
|
|
func (ls *LocalStorage) Init(_ context.Context, metadata bindings.Metadata) error {
|
|
m, err := ls.parseMetadata(metadata)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to parse metadata: %w", err)
|
|
}
|
|
ls.metadata = m
|
|
|
|
err = os.MkdirAll(ls.metadata.RootPath, 0o777)
|
|
if err != nil {
|
|
return fmt.Errorf("unable to create directory specified by 'rootPath' %s: %w", ls.metadata.RootPath, err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (ls *LocalStorage) parseMetadata(meta bindings.Metadata) (*Metadata, error) {
|
|
var m Metadata
|
|
err := metadata.DecodeMetadata(meta.Properties, &m)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
m.RootPath, err = validateRootPath(m.RootPath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &m, nil
|
|
}
|
|
|
|
func validateRootPath(rootPath string) (string, error) {
|
|
var err error
|
|
|
|
if rootPath == "" {
|
|
return "", errors.New("property rootPath must not be empty")
|
|
}
|
|
|
|
// If the root path is relative, resolve it as absolute
|
|
rootPath = filepath.Clean(rootPath)
|
|
if !filepath.IsAbs(rootPath) {
|
|
var cwd string
|
|
cwd, err = os.Getwd()
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to obtain current working directory: %w", err)
|
|
}
|
|
rootPath = filepath.Join(cwd, rootPath)
|
|
}
|
|
|
|
// Resolve symlinks
|
|
// If rootPath doesn't (yet) exist, we need to check its parent folders
|
|
checkPath := rootPath
|
|
sep := string(os.PathSeparator)
|
|
// Per docs of filepath.Dir and filepath.Clean (invoked earlier): "The returned path does not end in a separator unless it is the root directory."
|
|
var fi os.FileInfo
|
|
for !strings.HasSuffix(checkPath, sep) {
|
|
fi, err = os.Stat(checkPath)
|
|
if err == nil {
|
|
if !fi.IsDir() {
|
|
return "", errors.New("property rootPath represents a file and not a directory")
|
|
}
|
|
break
|
|
} else if os.IsNotExist(err) {
|
|
checkPath = filepath.Dir(checkPath)
|
|
continue
|
|
} else {
|
|
return "", fmt.Errorf("error getting stat for path %s: %w", checkPath, err)
|
|
}
|
|
}
|
|
resolvedCheckPath, err := filepath.EvalSymlinks(checkPath)
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to evaluate symlinks in rootPath: %w", err)
|
|
}
|
|
rootPath = filepath.Join(resolvedCheckPath, rootPath[len(checkPath):])
|
|
|
|
// Check if the path is in a disallowed location
|
|
for _, p := range disallowedRootPaths {
|
|
if rootPath == p || strings.HasPrefix(rootPath, p+sep) {
|
|
return "", errors.New("property rootPath points to a disallowed location")
|
|
}
|
|
}
|
|
|
|
return rootPath, nil
|
|
}
|
|
|
|
// Operations enumerates supported binding operations.
|
|
func (ls *LocalStorage) Operations() []bindings.OperationKind {
|
|
return []bindings.OperationKind{
|
|
bindings.CreateOperation,
|
|
bindings.GetOperation,
|
|
bindings.ListOperation,
|
|
bindings.DeleteOperation,
|
|
}
|
|
}
|
|
|
|
func (ls *LocalStorage) create(filename string, req *bindings.InvokeRequest) (*bindings.InvokeResponse, error) {
|
|
d, err := strconv.Unquote(string(req.Data))
|
|
if err == nil {
|
|
req.Data = []byte(d)
|
|
}
|
|
|
|
decoded, err := base64.StdEncoding.DecodeString(string(req.Data))
|
|
if err == nil {
|
|
req.Data = decoded
|
|
}
|
|
|
|
absPath, relPath, err := getSecureAbsRelPath(ls.metadata.RootPath, filename)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error getting absolute path for file %s: %w", filename, err)
|
|
}
|
|
|
|
dir := filepath.Dir(absPath)
|
|
err = os.MkdirAll(dir, 0o777)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error creating directory %s: %w", dir, err)
|
|
}
|
|
|
|
f, err := os.Create(absPath)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error creating file %s: %w", absPath, err)
|
|
}
|
|
defer f.Close()
|
|
|
|
numBytes, err := f.Write(req.Data)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error writing to file %s: %w", absPath, err)
|
|
}
|
|
|
|
ls.logger.Debugf("wrote file: %s. numBytes: %d", absPath, numBytes)
|
|
|
|
resp := createResponse{
|
|
FileName: relPath,
|
|
}
|
|
|
|
b, err := json.Marshal(resp)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error encoding response as JSON: %w", err)
|
|
}
|
|
|
|
return &bindings.InvokeResponse{
|
|
Data: b,
|
|
}, nil
|
|
}
|
|
|
|
func (ls *LocalStorage) get(filename string, req *bindings.InvokeRequest) (*bindings.InvokeResponse, error) {
|
|
absPath, _, err := getSecureAbsRelPath(ls.metadata.RootPath, filename)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error getting absolute path for file %s: %w", filename, err)
|
|
}
|
|
|
|
f, err := os.Open(absPath)
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
return nil, fmt.Errorf("file not found: %s", absPath)
|
|
}
|
|
return nil, fmt.Errorf("error opening path %s: %w", absPath, err)
|
|
}
|
|
defer f.Close()
|
|
|
|
b, err := io.ReadAll(f)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error reading file %s: %w", absPath, err)
|
|
}
|
|
|
|
ls.logger.Debugf("read file: %s. size: %d bytes", absPath, len(b))
|
|
|
|
return &bindings.InvokeResponse{
|
|
Data: b,
|
|
}, nil
|
|
}
|
|
|
|
func (ls *LocalStorage) delete(filename string, req *bindings.InvokeRequest) (*bindings.InvokeResponse, error) {
|
|
absPath, _, err := getSecureAbsRelPath(ls.metadata.RootPath, filename)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error getting absolute path for file %s: %w", filename, err)
|
|
}
|
|
|
|
err = os.Remove(absPath)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error deleting file %s: %w", absPath, err)
|
|
}
|
|
|
|
ls.logger.Debugf("removed file: %s", absPath)
|
|
|
|
return nil, nil
|
|
}
|
|
|
|
func (ls *LocalStorage) list(filename string, req *bindings.InvokeRequest) (*bindings.InvokeResponse, error) {
|
|
absPath, _, err := getSecureAbsRelPath(ls.metadata.RootPath, filename)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error getting absolute path for file %s: %w", filename, err)
|
|
}
|
|
|
|
fi, err := os.Stat(absPath)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error getting stats for path %s: %w", absPath, err)
|
|
}
|
|
|
|
if !fi.IsDir() {
|
|
return nil, fmt.Errorf("unable to list files as the file specified is not a directory: %s", absPath)
|
|
}
|
|
|
|
files, err := walkPath(absPath)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error listing files in the directory %s: %w", absPath, err)
|
|
}
|
|
|
|
b, err := json.Marshal(files)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error encoding response as JSON: %w", err)
|
|
}
|
|
|
|
return &bindings.InvokeResponse{
|
|
Data: b,
|
|
}, nil
|
|
}
|
|
|
|
func getSecureAbsRelPath(rootPath string, filename string) (absPath string, relPath string, err error) {
|
|
absPath, err = securejoin.SecureJoin(rootPath, filename)
|
|
if err != nil {
|
|
return
|
|
}
|
|
relPath, err = filepath.Rel(rootPath, absPath)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
func walkPath(root string) ([]string, error) {
|
|
var files []string
|
|
err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
|
|
if !info.IsDir() {
|
|
files = append(files, path)
|
|
}
|
|
|
|
return nil
|
|
})
|
|
|
|
return files, err
|
|
}
|
|
|
|
// Invoke is called for output bindings.
|
|
func (ls *LocalStorage) Invoke(_ context.Context, req *bindings.InvokeRequest) (*bindings.InvokeResponse, error) {
|
|
filename := req.Metadata[fileNameMetadataKey]
|
|
if filename == "" && req.Operation == bindings.CreateOperation {
|
|
u, err := uuid.NewRandom()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to generate UUID: %w", err)
|
|
}
|
|
filename = u.String()
|
|
}
|
|
|
|
switch req.Operation {
|
|
case bindings.CreateOperation:
|
|
return ls.create(filename, req)
|
|
case bindings.GetOperation:
|
|
return ls.get(filename, req)
|
|
case bindings.DeleteOperation:
|
|
return ls.delete(filename, req)
|
|
case bindings.ListOperation:
|
|
return ls.list(filename, req)
|
|
default:
|
|
return nil, fmt.Errorf("unsupported operation %s", req.Operation)
|
|
}
|
|
}
|
|
|
|
// GetComponentMetadata returns the metadata of the component.
|
|
func (ls *LocalStorage) GetComponentMetadata() (metadataInfo metadata.MetadataMap) {
|
|
metadataStruct := Metadata{}
|
|
metadata.GetMetadataInfoFromStructType(reflect.TypeOf(metadataStruct), &metadataInfo, metadata.BindingType)
|
|
return
|
|
}
|