1e10381143
Fixes #139
269 lines
9.6 KiB
Go
269 lines
9.6 KiB
Go
// +build !noportable
|
|
|
|
package cmd
|
|
|
|
import (
|
|
"encoding/base64"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"os"
|
|
"path"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/spf13/cobra"
|
|
|
|
"github.com/drakkan/sftpgo/dataprovider"
|
|
"github.com/drakkan/sftpgo/service"
|
|
"github.com/drakkan/sftpgo/sftpd"
|
|
"github.com/drakkan/sftpgo/version"
|
|
"github.com/drakkan/sftpgo/vfs"
|
|
)
|
|
|
|
var (
|
|
directoryToServe string
|
|
portableSFTPDPort int
|
|
portableAdvertiseService bool
|
|
portableAdvertiseCredentials bool
|
|
portableUsername string
|
|
portablePassword string
|
|
portableLogFile string
|
|
portableLogVerbose bool
|
|
portablePublicKeys []string
|
|
portablePermissions []string
|
|
portableSSHCommands []string
|
|
portableAllowedExtensions []string
|
|
portableDeniedExtensions []string
|
|
portableFsProvider int
|
|
portableS3Bucket string
|
|
portableS3Region string
|
|
portableS3AccessKey string
|
|
portableS3AccessSecret string
|
|
portableS3Endpoint string
|
|
portableS3StorageClass string
|
|
portableS3KeyPrefix string
|
|
portableS3ULPartSize int
|
|
portableS3ULConcurrency int
|
|
portableGCSBucket string
|
|
portableGCSCredentialsFile string
|
|
portableGCSAutoCredentials int
|
|
portableGCSStorageClass string
|
|
portableGCSKeyPrefix string
|
|
portableCmd = &cobra.Command{
|
|
Use: "portable",
|
|
Short: "Serve a single directory",
|
|
Long: `To serve the current working directory with auto generated credentials simply
|
|
use:
|
|
|
|
$ sftpgo portable
|
|
|
|
Please take a look at the usage below to customize the serving parameters`,
|
|
Run: func(cmd *cobra.Command, args []string) {
|
|
portableDir := directoryToServe
|
|
if !filepath.IsAbs(portableDir) {
|
|
if portableFsProvider == 0 {
|
|
portableDir, _ = filepath.Abs(portableDir)
|
|
} else {
|
|
portableDir = os.TempDir()
|
|
}
|
|
}
|
|
permissions := make(map[string][]string)
|
|
permissions["/"] = portablePermissions
|
|
portableGCSCredentials := ""
|
|
if portableFsProvider == 2 && len(portableGCSCredentialsFile) > 0 {
|
|
fi, err := os.Stat(portableGCSCredentialsFile)
|
|
if err != nil {
|
|
fmt.Printf("Invalid GCS credentials file: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
if fi.Size() > 1048576 {
|
|
fmt.Printf("Invalid GCS credentials file: %#v is too big %v/1048576 bytes\n", portableGCSCredentialsFile,
|
|
fi.Size())
|
|
os.Exit(1)
|
|
}
|
|
creds, err := ioutil.ReadFile(portableGCSCredentialsFile)
|
|
if err != nil {
|
|
fmt.Printf("Unable to read credentials file: %v\n", err)
|
|
}
|
|
portableGCSCredentials = base64.StdEncoding.EncodeToString(creds)
|
|
portableGCSAutoCredentials = 0
|
|
}
|
|
service := service.Service{
|
|
ConfigDir: filepath.Clean(defaultConfigDir),
|
|
ConfigFile: defaultConfigName,
|
|
LogFilePath: portableLogFile,
|
|
LogMaxSize: defaultLogMaxSize,
|
|
LogMaxBackups: defaultLogMaxBackup,
|
|
LogMaxAge: defaultLogMaxAge,
|
|
LogCompress: defaultLogCompress,
|
|
LogVerbose: portableLogVerbose,
|
|
Profiler: defaultProfiler,
|
|
Shutdown: make(chan bool),
|
|
PortableMode: 1,
|
|
PortableUser: dataprovider.User{
|
|
Username: portableUsername,
|
|
Password: portablePassword,
|
|
PublicKeys: portablePublicKeys,
|
|
Permissions: permissions,
|
|
HomeDir: portableDir,
|
|
Status: 1,
|
|
FsConfig: dataprovider.Filesystem{
|
|
Provider: portableFsProvider,
|
|
S3Config: vfs.S3FsConfig{
|
|
Bucket: portableS3Bucket,
|
|
Region: portableS3Region,
|
|
AccessKey: portableS3AccessKey,
|
|
AccessSecret: portableS3AccessSecret,
|
|
Endpoint: portableS3Endpoint,
|
|
StorageClass: portableS3StorageClass,
|
|
KeyPrefix: portableS3KeyPrefix,
|
|
UploadPartSize: int64(portableS3ULPartSize),
|
|
UploadConcurrency: portableS3ULConcurrency,
|
|
},
|
|
GCSConfig: vfs.GCSFsConfig{
|
|
Bucket: portableGCSBucket,
|
|
Credentials: portableGCSCredentials,
|
|
AutomaticCredentials: portableGCSAutoCredentials,
|
|
StorageClass: portableGCSStorageClass,
|
|
KeyPrefix: portableGCSKeyPrefix,
|
|
},
|
|
},
|
|
Filters: dataprovider.UserFilters{
|
|
FileExtensions: parseFileExtensionsFilters(),
|
|
},
|
|
},
|
|
}
|
|
if err := service.StartPortableMode(portableSFTPDPort, portableSSHCommands, portableAdvertiseService,
|
|
portableAdvertiseCredentials); err == nil {
|
|
service.Wait()
|
|
os.Exit(0)
|
|
}
|
|
os.Exit(1)
|
|
},
|
|
}
|
|
)
|
|
|
|
func init() {
|
|
version.AddFeature("+portable")
|
|
|
|
portableCmd.Flags().StringVarP(&directoryToServe, "directory", "d", ".", `Path to the directory to serve.
|
|
This can be an absolute path or a path
|
|
relative to the current directory
|
|
`)
|
|
portableCmd.Flags().IntVarP(&portableSFTPDPort, "sftpd-port", "s", 0, "0 means a random unprivileged port")
|
|
portableCmd.Flags().StringSliceVarP(&portableSSHCommands, "ssh-commands", "c", sftpd.GetDefaultSSHCommands(),
|
|
`SSH commands to enable.
|
|
"*" means any supported SSH command
|
|
including scp
|
|
`)
|
|
portableCmd.Flags().StringVarP(&portableUsername, "username", "u", "", `Leave empty to use an auto generated
|
|
value`)
|
|
portableCmd.Flags().StringVarP(&portablePassword, "password", "p", "", `Leave empty to use an auto generated
|
|
value`)
|
|
portableCmd.Flags().StringVarP(&portableLogFile, logFilePathFlag, "l", "", "Leave empty to disable logging")
|
|
portableCmd.Flags().BoolVarP(&portableLogVerbose, logVerboseFlag, "v", false, "Enable verbose logs")
|
|
portableCmd.Flags().StringSliceVarP(&portablePublicKeys, "public-key", "k", []string{}, "")
|
|
portableCmd.Flags().StringSliceVarP(&portablePermissions, "permissions", "g", []string{"list", "download"},
|
|
`User's permissions. "*" means any
|
|
permission`)
|
|
portableCmd.Flags().StringArrayVar(&portableAllowedExtensions, "allowed-extensions", []string{},
|
|
`Allowed file extensions case
|
|
insensitive. The format is
|
|
/dir::ext1,ext2.
|
|
For example: "/somedir::.jpg,.png"`)
|
|
portableCmd.Flags().StringArrayVar(&portableDeniedExtensions, "denied-extensions", []string{},
|
|
`Denied file extensions case
|
|
insensitive. The format is
|
|
/dir::ext1,ext2.
|
|
For example: "/somedir::.jpg,.png"`)
|
|
portableCmd.Flags().BoolVarP(&portableAdvertiseService, "advertise-service", "S", false,
|
|
`Advertise SFTP service using multicast
|
|
DNS`)
|
|
portableCmd.Flags().BoolVarP(&portableAdvertiseCredentials, "advertise-credentials", "C", false,
|
|
`If the SFTP service is advertised via
|
|
multicast DNS, this flag allows to put
|
|
username/password inside the advertised
|
|
TXT record`)
|
|
portableCmd.Flags().IntVarP(&portableFsProvider, "fs-provider", "f", 0, `0 means local filesystem,
|
|
1 Amazon S3 compatible,
|
|
2 Google Cloud Storage`)
|
|
portableCmd.Flags().StringVar(&portableS3Bucket, "s3-bucket", "", "")
|
|
portableCmd.Flags().StringVar(&portableS3Region, "s3-region", "", "")
|
|
portableCmd.Flags().StringVar(&portableS3AccessKey, "s3-access-key", "", "")
|
|
portableCmd.Flags().StringVar(&portableS3AccessSecret, "s3-access-secret", "", "")
|
|
portableCmd.Flags().StringVar(&portableS3Endpoint, "s3-endpoint", "", "")
|
|
portableCmd.Flags().StringVar(&portableS3StorageClass, "s3-storage-class", "", "")
|
|
portableCmd.Flags().StringVar(&portableS3KeyPrefix, "s3-key-prefix", "", `Allows to restrict access to the
|
|
virtual folder identified by this
|
|
prefix and its contents`)
|
|
portableCmd.Flags().IntVar(&portableS3ULPartSize, "s3-upload-part-size", 5, `The buffer size for multipart uploads
|
|
(MB)`)
|
|
portableCmd.Flags().IntVar(&portableS3ULConcurrency, "s3-upload-concurrency", 2, `How many parts are uploaded in
|
|
parallel`)
|
|
portableCmd.Flags().StringVar(&portableGCSBucket, "gcs-bucket", "", "")
|
|
portableCmd.Flags().StringVar(&portableGCSStorageClass, "gcs-storage-class", "", "")
|
|
portableCmd.Flags().StringVar(&portableGCSKeyPrefix, "gcs-key-prefix", "", `Allows to restrict access to the
|
|
virtual folder identified by this
|
|
prefix and its contents`)
|
|
portableCmd.Flags().StringVar(&portableGCSCredentialsFile, "gcs-credentials-file", "", `Google Cloud Storage JSON credentials
|
|
file`)
|
|
portableCmd.Flags().IntVar(&portableGCSAutoCredentials, "gcs-automatic-credentials", 1, `0 means explicit credentials using
|
|
a JSON credentials file, 1 automatic
|
|
`)
|
|
rootCmd.AddCommand(portableCmd)
|
|
}
|
|
|
|
func parseFileExtensionsFilters() []dataprovider.ExtensionsFilter {
|
|
var extensions []dataprovider.ExtensionsFilter
|
|
for _, val := range portableAllowedExtensions {
|
|
p, exts := getExtensionsFilterValues(strings.TrimSpace(val))
|
|
if len(p) > 0 {
|
|
extensions = append(extensions, dataprovider.ExtensionsFilter{
|
|
Path: path.Clean(p),
|
|
AllowedExtensions: exts,
|
|
DeniedExtensions: []string{},
|
|
})
|
|
}
|
|
}
|
|
for _, val := range portableDeniedExtensions {
|
|
p, exts := getExtensionsFilterValues(strings.TrimSpace(val))
|
|
if len(p) > 0 {
|
|
found := false
|
|
for index, e := range extensions {
|
|
if path.Clean(e.Path) == path.Clean(p) {
|
|
extensions[index].DeniedExtensions = append(extensions[index].DeniedExtensions, exts...)
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
extensions = append(extensions, dataprovider.ExtensionsFilter{
|
|
Path: path.Clean(p),
|
|
AllowedExtensions: []string{},
|
|
DeniedExtensions: exts,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
return extensions
|
|
}
|
|
|
|
func getExtensionsFilterValues(value string) (string, []string) {
|
|
if strings.Contains(value, "::") {
|
|
dirExts := strings.Split(value, "::")
|
|
if len(dirExts) > 1 {
|
|
dir := strings.TrimSpace(dirExts[0])
|
|
exts := []string{}
|
|
for _, e := range strings.Split(dirExts[1], ",") {
|
|
cleanedExt := strings.TrimSpace(e)
|
|
if len(cleanedExt) > 0 {
|
|
exts = append(exts, cleanedExt)
|
|
}
|
|
}
|
|
if len(dir) > 0 && len(exts) > 0 {
|
|
return dir, exts
|
|
}
|
|
}
|
|
}
|
|
return "", nil
|
|
}
|