portable.go 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305
  1. // +build !noportable
  2. package cmd
  3. import (
  4. "encoding/base64"
  5. "fmt"
  6. "io/ioutil"
  7. "os"
  8. "path"
  9. "path/filepath"
  10. "strings"
  11. "github.com/spf13/cobra"
  12. "github.com/drakkan/sftpgo/common"
  13. "github.com/drakkan/sftpgo/dataprovider"
  14. "github.com/drakkan/sftpgo/service"
  15. "github.com/drakkan/sftpgo/sftpd"
  16. "github.com/drakkan/sftpgo/version"
  17. "github.com/drakkan/sftpgo/vfs"
  18. )
  19. var (
  20. directoryToServe string
  21. portableSFTPDPort int
  22. portableAdvertiseService bool
  23. portableAdvertiseCredentials bool
  24. portableUsername string
  25. portablePassword string
  26. portableLogFile string
  27. portableLogVerbose bool
  28. portablePublicKeys []string
  29. portablePermissions []string
  30. portableSSHCommands []string
  31. portableAllowedExtensions []string
  32. portableDeniedExtensions []string
  33. portableFsProvider int
  34. portableS3Bucket string
  35. portableS3Region string
  36. portableS3AccessKey string
  37. portableS3AccessSecret string
  38. portableS3Endpoint string
  39. portableS3StorageClass string
  40. portableS3KeyPrefix string
  41. portableS3ULPartSize int
  42. portableS3ULConcurrency int
  43. portableGCSBucket string
  44. portableGCSCredentialsFile string
  45. portableGCSAutoCredentials int
  46. portableGCSStorageClass string
  47. portableGCSKeyPrefix string
  48. portableFTPDPort int
  49. portableFTPSCert string
  50. portableFTPSKey string
  51. portableWebDAVPort int
  52. portableWebDAVCert string
  53. portableWebDAVKey string
  54. portableCmd = &cobra.Command{
  55. Use: "portable",
  56. Short: "Serve a single directory",
  57. Long: `To serve the current working directory with auto generated credentials simply
  58. use:
  59. $ sftpgo portable
  60. Please take a look at the usage below to customize the serving parameters`,
  61. Run: func(cmd *cobra.Command, args []string) {
  62. portableDir := directoryToServe
  63. fsProvider := dataprovider.FilesystemProvider(portableFsProvider)
  64. if !filepath.IsAbs(portableDir) {
  65. if fsProvider == dataprovider.LocalFilesystemProvider {
  66. portableDir, _ = filepath.Abs(portableDir)
  67. } else {
  68. portableDir = os.TempDir()
  69. }
  70. }
  71. permissions := make(map[string][]string)
  72. permissions["/"] = portablePermissions
  73. portableGCSCredentials := ""
  74. if fsProvider == dataprovider.GCSFilesystemProvider && len(portableGCSCredentialsFile) > 0 {
  75. fi, err := os.Stat(portableGCSCredentialsFile)
  76. if err != nil {
  77. fmt.Printf("Invalid GCS credentials file: %v\n", err)
  78. os.Exit(1)
  79. }
  80. if fi.Size() > 1048576 {
  81. fmt.Printf("Invalid GCS credentials file: %#v is too big %v/1048576 bytes\n", portableGCSCredentialsFile,
  82. fi.Size())
  83. os.Exit(1)
  84. }
  85. creds, err := ioutil.ReadFile(portableGCSCredentialsFile)
  86. if err != nil {
  87. fmt.Printf("Unable to read credentials file: %v\n", err)
  88. }
  89. portableGCSCredentials = base64.StdEncoding.EncodeToString(creds)
  90. portableGCSAutoCredentials = 0
  91. }
  92. if portableFTPDPort >= 0 && len(portableFTPSCert) > 0 && len(portableFTPSKey) > 0 {
  93. _, err := common.NewCertManager(portableFTPSCert, portableFTPSKey, "FTP portable")
  94. if err != nil {
  95. fmt.Printf("Unable to load FTPS key pair, cert file %#v key file %#v error: %v\n",
  96. portableFTPSCert, portableFTPSKey, err)
  97. os.Exit(1)
  98. }
  99. }
  100. if portableWebDAVPort > 0 && len(portableWebDAVCert) > 0 && len(portableWebDAVKey) > 0 {
  101. _, err := common.NewCertManager(portableWebDAVCert, portableWebDAVKey, "WebDAV portable")
  102. if err != nil {
  103. fmt.Printf("Unable to load WebDAV key pair, cert file %#v key file %#v error: %v\n",
  104. portableWebDAVCert, portableWebDAVKey, err)
  105. os.Exit(1)
  106. }
  107. }
  108. service := service.Service{
  109. ConfigDir: filepath.Clean(defaultConfigDir),
  110. ConfigFile: defaultConfigName,
  111. LogFilePath: portableLogFile,
  112. LogMaxSize: defaultLogMaxSize,
  113. LogMaxBackups: defaultLogMaxBackup,
  114. LogMaxAge: defaultLogMaxAge,
  115. LogCompress: defaultLogCompress,
  116. LogVerbose: portableLogVerbose,
  117. Profiler: defaultProfiler,
  118. Shutdown: make(chan bool),
  119. PortableMode: 1,
  120. PortableUser: dataprovider.User{
  121. Username: portableUsername,
  122. Password: portablePassword,
  123. PublicKeys: portablePublicKeys,
  124. Permissions: permissions,
  125. HomeDir: portableDir,
  126. Status: 1,
  127. FsConfig: dataprovider.Filesystem{
  128. Provider: dataprovider.FilesystemProvider(portableFsProvider),
  129. S3Config: vfs.S3FsConfig{
  130. Bucket: portableS3Bucket,
  131. Region: portableS3Region,
  132. AccessKey: portableS3AccessKey,
  133. AccessSecret: portableS3AccessSecret,
  134. Endpoint: portableS3Endpoint,
  135. StorageClass: portableS3StorageClass,
  136. KeyPrefix: portableS3KeyPrefix,
  137. UploadPartSize: int64(portableS3ULPartSize),
  138. UploadConcurrency: portableS3ULConcurrency,
  139. },
  140. GCSConfig: vfs.GCSFsConfig{
  141. Bucket: portableGCSBucket,
  142. Credentials: portableGCSCredentials,
  143. AutomaticCredentials: portableGCSAutoCredentials,
  144. StorageClass: portableGCSStorageClass,
  145. KeyPrefix: portableGCSKeyPrefix,
  146. },
  147. },
  148. Filters: dataprovider.UserFilters{
  149. FileExtensions: parseFileExtensionsFilters(),
  150. },
  151. },
  152. }
  153. if err := service.StartPortableMode(portableSFTPDPort, portableFTPDPort, portableWebDAVPort, portableSSHCommands, portableAdvertiseService,
  154. portableAdvertiseCredentials, portableFTPSCert, portableFTPSKey, portableWebDAVCert, portableWebDAVKey); err == nil {
  155. service.Wait()
  156. if service.Error == nil {
  157. os.Exit(0)
  158. }
  159. }
  160. os.Exit(1)
  161. },
  162. }
  163. )
  164. func init() {
  165. version.AddFeature("+portable")
  166. portableCmd.Flags().StringVarP(&directoryToServe, "directory", "d", ".", `Path to the directory to serve.
  167. This can be an absolute path or a path
  168. relative to the current directory
  169. `)
  170. portableCmd.Flags().IntVarP(&portableSFTPDPort, "sftpd-port", "s", 0, "0 means a random unprivileged port")
  171. portableCmd.Flags().IntVar(&portableFTPDPort, "ftpd-port", -1, `0 means a random unprivileged port,
  172. < 0 disabled`)
  173. portableCmd.Flags().IntVar(&portableWebDAVPort, "webdav-port", -1, `0 means a random unprivileged port,
  174. < 0 disabled`)
  175. portableCmd.Flags().StringSliceVarP(&portableSSHCommands, "ssh-commands", "c", sftpd.GetDefaultSSHCommands(),
  176. `SSH commands to enable.
  177. "*" means any supported SSH command
  178. including scp
  179. `)
  180. portableCmd.Flags().StringVarP(&portableUsername, "username", "u", "", `Leave empty to use an auto generated
  181. value`)
  182. portableCmd.Flags().StringVarP(&portablePassword, "password", "p", "", `Leave empty to use an auto generated
  183. value`)
  184. portableCmd.Flags().StringVarP(&portableLogFile, logFilePathFlag, "l", "", "Leave empty to disable logging")
  185. portableCmd.Flags().BoolVarP(&portableLogVerbose, logVerboseFlag, "v", false, "Enable verbose logs")
  186. portableCmd.Flags().StringSliceVarP(&portablePublicKeys, "public-key", "k", []string{}, "")
  187. portableCmd.Flags().StringSliceVarP(&portablePermissions, "permissions", "g", []string{"list", "download"},
  188. `User's permissions. "*" means any
  189. permission`)
  190. portableCmd.Flags().StringArrayVar(&portableAllowedExtensions, "allowed-extensions", []string{},
  191. `Allowed file extensions case
  192. insensitive. The format is
  193. /dir::ext1,ext2.
  194. For example: "/somedir::.jpg,.png"`)
  195. portableCmd.Flags().StringArrayVar(&portableDeniedExtensions, "denied-extensions", []string{},
  196. `Denied file extensions case
  197. insensitive. The format is
  198. /dir::ext1,ext2.
  199. For example: "/somedir::.jpg,.png"`)
  200. portableCmd.Flags().BoolVarP(&portableAdvertiseService, "advertise-service", "S", false,
  201. `Advertise SFTP/FTP service using
  202. multicast DNS`)
  203. portableCmd.Flags().BoolVarP(&portableAdvertiseCredentials, "advertise-credentials", "C", false,
  204. `If the SFTP/FTP service is
  205. advertised via multicast DNS, this
  206. flag allows to put username/password
  207. inside the advertised TXT record`)
  208. portableCmd.Flags().IntVarP(&portableFsProvider, "fs-provider", "f", int(dataprovider.LocalFilesystemProvider), `0 means local filesystem,
  209. 1 Amazon S3 compatible,
  210. 2 Google Cloud Storage`)
  211. portableCmd.Flags().StringVar(&portableS3Bucket, "s3-bucket", "", "")
  212. portableCmd.Flags().StringVar(&portableS3Region, "s3-region", "", "")
  213. portableCmd.Flags().StringVar(&portableS3AccessKey, "s3-access-key", "", "")
  214. portableCmd.Flags().StringVar(&portableS3AccessSecret, "s3-access-secret", "", "")
  215. portableCmd.Flags().StringVar(&portableS3Endpoint, "s3-endpoint", "", "")
  216. portableCmd.Flags().StringVar(&portableS3StorageClass, "s3-storage-class", "", "")
  217. portableCmd.Flags().StringVar(&portableS3KeyPrefix, "s3-key-prefix", "", `Allows to restrict access to the
  218. virtual folder identified by this
  219. prefix and its contents`)
  220. portableCmd.Flags().IntVar(&portableS3ULPartSize, "s3-upload-part-size", 5, `The buffer size for multipart uploads
  221. (MB)`)
  222. portableCmd.Flags().IntVar(&portableS3ULConcurrency, "s3-upload-concurrency", 2, `How many parts are uploaded in
  223. parallel`)
  224. portableCmd.Flags().StringVar(&portableGCSBucket, "gcs-bucket", "", "")
  225. portableCmd.Flags().StringVar(&portableGCSStorageClass, "gcs-storage-class", "", "")
  226. portableCmd.Flags().StringVar(&portableGCSKeyPrefix, "gcs-key-prefix", "", `Allows to restrict access to the
  227. virtual folder identified by this
  228. prefix and its contents`)
  229. portableCmd.Flags().StringVar(&portableGCSCredentialsFile, "gcs-credentials-file", "", `Google Cloud Storage JSON credentials
  230. file`)
  231. portableCmd.Flags().IntVar(&portableGCSAutoCredentials, "gcs-automatic-credentials", 1, `0 means explicit credentials using
  232. a JSON credentials file, 1 automatic
  233. `)
  234. portableCmd.Flags().StringVar(&portableFTPSCert, "ftpd-cert", "", "Path to the certificate file for FTPS")
  235. portableCmd.Flags().StringVar(&portableFTPSKey, "ftpd-key", "", "Path to the key file for FTPS")
  236. portableCmd.Flags().StringVar(&portableWebDAVCert, "webdav-cert", "", `Path to the certificate file for WebDAV
  237. over HTTPS`)
  238. portableCmd.Flags().StringVar(&portableWebDAVKey, "webdav-key", "", `Path to the key file for WebDAV over
  239. HTTPS`)
  240. rootCmd.AddCommand(portableCmd)
  241. }
  242. func parseFileExtensionsFilters() []dataprovider.ExtensionsFilter {
  243. var extensions []dataprovider.ExtensionsFilter
  244. for _, val := range portableAllowedExtensions {
  245. p, exts := getExtensionsFilterValues(strings.TrimSpace(val))
  246. if len(p) > 0 {
  247. extensions = append(extensions, dataprovider.ExtensionsFilter{
  248. Path: path.Clean(p),
  249. AllowedExtensions: exts,
  250. DeniedExtensions: []string{},
  251. })
  252. }
  253. }
  254. for _, val := range portableDeniedExtensions {
  255. p, exts := getExtensionsFilterValues(strings.TrimSpace(val))
  256. if len(p) > 0 {
  257. found := false
  258. for index, e := range extensions {
  259. if path.Clean(e.Path) == path.Clean(p) {
  260. extensions[index].DeniedExtensions = append(extensions[index].DeniedExtensions, exts...)
  261. found = true
  262. break
  263. }
  264. }
  265. if !found {
  266. extensions = append(extensions, dataprovider.ExtensionsFilter{
  267. Path: path.Clean(p),
  268. AllowedExtensions: []string{},
  269. DeniedExtensions: exts,
  270. })
  271. }
  272. }
  273. }
  274. return extensions
  275. }
  276. func getExtensionsFilterValues(value string) (string, []string) {
  277. if strings.Contains(value, "::") {
  278. dirExts := strings.Split(value, "::")
  279. if len(dirExts) > 1 {
  280. dir := strings.TrimSpace(dirExts[0])
  281. exts := []string{}
  282. for _, e := range strings.Split(dirExts[1], ",") {
  283. cleanedExt := strings.TrimSpace(e)
  284. if len(cleanedExt) > 0 {
  285. exts = append(exts, cleanedExt)
  286. }
  287. }
  288. if len(dir) > 0 && len(exts) > 0 {
  289. return dir, exts
  290. }
  291. }
  292. }
  293. return "", nil
  294. }