portable.go 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429
  1. //go:build !noportable
  2. // +build !noportable
  3. package cmd
  4. import (
  5. "fmt"
  6. "os"
  7. "path"
  8. "path/filepath"
  9. "strings"
  10. "github.com/spf13/cobra"
  11. "github.com/drakkan/sftpgo/v2/common"
  12. "github.com/drakkan/sftpgo/v2/dataprovider"
  13. "github.com/drakkan/sftpgo/v2/sdk"
  14. "github.com/drakkan/sftpgo/v2/sdk/kms"
  15. "github.com/drakkan/sftpgo/v2/service"
  16. "github.com/drakkan/sftpgo/v2/sftpd"
  17. "github.com/drakkan/sftpgo/v2/version"
  18. "github.com/drakkan/sftpgo/v2/vfs"
  19. )
  20. var (
  21. directoryToServe string
  22. portableSFTPDPort int
  23. portableAdvertiseService bool
  24. portableAdvertiseCredentials bool
  25. portableUsername string
  26. portablePassword string
  27. portableLogFile string
  28. portableLogVerbose bool
  29. portableLogUTCTime bool
  30. portablePublicKeys []string
  31. portablePermissions []string
  32. portableSSHCommands []string
  33. portableAllowedPatterns []string
  34. portableDeniedPatterns []string
  35. portableFsProvider string
  36. portableS3Bucket string
  37. portableS3Region string
  38. portableS3AccessKey string
  39. portableS3AccessSecret string
  40. portableS3Endpoint string
  41. portableS3StorageClass string
  42. portableS3ACL string
  43. portableS3KeyPrefix string
  44. portableS3ULPartSize int
  45. portableS3ULConcurrency int
  46. portableS3ForcePathStyle bool
  47. portableGCSBucket string
  48. portableGCSCredentialsFile string
  49. portableGCSAutoCredentials int
  50. portableGCSStorageClass string
  51. portableGCSKeyPrefix string
  52. portableFTPDPort int
  53. portableFTPSCert string
  54. portableFTPSKey string
  55. portableWebDAVPort int
  56. portableWebDAVCert string
  57. portableWebDAVKey string
  58. portableAzContainer string
  59. portableAzAccountName string
  60. portableAzAccountKey string
  61. portableAzEndpoint string
  62. portableAzAccessTier string
  63. portableAzSASURL string
  64. portableAzKeyPrefix string
  65. portableAzULPartSize int
  66. portableAzULConcurrency int
  67. portableAzUseEmulator bool
  68. portableCryptPassphrase string
  69. portableSFTPEndpoint string
  70. portableSFTPUsername string
  71. portableSFTPPassword string
  72. portableSFTPPrivateKeyPath string
  73. portableSFTPFingerprints []string
  74. portableSFTPPrefix string
  75. portableSFTPDisableConcurrentReads bool
  76. portableSFTPDBufferSize int64
  77. portableCmd = &cobra.Command{
  78. Use: "portable",
  79. Short: "Serve a single directory/account",
  80. Long: `To serve the current working directory with auto generated credentials simply
  81. use:
  82. $ sftpgo portable
  83. Please take a look at the usage below to customize the serving parameters`,
  84. Run: func(cmd *cobra.Command, args []string) {
  85. portableDir := directoryToServe
  86. fsProvider := sdk.GetProviderByName(portableFsProvider)
  87. if !filepath.IsAbs(portableDir) {
  88. if fsProvider == sdk.LocalFilesystemProvider {
  89. portableDir, _ = filepath.Abs(portableDir)
  90. } else {
  91. portableDir = os.TempDir()
  92. }
  93. }
  94. permissions := make(map[string][]string)
  95. permissions["/"] = portablePermissions
  96. portableGCSCredentials := ""
  97. if fsProvider == sdk.GCSFilesystemProvider && portableGCSCredentialsFile != "" {
  98. contents, err := getFileContents(portableGCSCredentialsFile)
  99. if err != nil {
  100. fmt.Printf("Unable to get GCS credentials: %v\n", err)
  101. os.Exit(1)
  102. }
  103. portableGCSCredentials = contents
  104. portableGCSAutoCredentials = 0
  105. }
  106. portableSFTPPrivateKey := ""
  107. if fsProvider == sdk.SFTPFilesystemProvider && portableSFTPPrivateKeyPath != "" {
  108. contents, err := getFileContents(portableSFTPPrivateKeyPath)
  109. if err != nil {
  110. fmt.Printf("Unable to get SFTP private key: %v\n", err)
  111. os.Exit(1)
  112. }
  113. portableSFTPPrivateKey = contents
  114. }
  115. if portableFTPDPort >= 0 && len(portableFTPSCert) > 0 && len(portableFTPSKey) > 0 {
  116. _, err := common.NewCertManager(portableFTPSCert, portableFTPSKey, filepath.Clean(defaultConfigDir),
  117. "FTP portable")
  118. if err != nil {
  119. fmt.Printf("Unable to load FTPS key pair, cert file %#v key file %#v error: %v\n",
  120. portableFTPSCert, portableFTPSKey, err)
  121. os.Exit(1)
  122. }
  123. }
  124. if portableWebDAVPort > 0 && len(portableWebDAVCert) > 0 && len(portableWebDAVKey) > 0 {
  125. _, err := common.NewCertManager(portableWebDAVCert, portableWebDAVKey, filepath.Clean(defaultConfigDir),
  126. "WebDAV portable")
  127. if err != nil {
  128. fmt.Printf("Unable to load WebDAV key pair, cert file %#v key file %#v error: %v\n",
  129. portableWebDAVCert, portableWebDAVKey, err)
  130. os.Exit(1)
  131. }
  132. }
  133. service := service.Service{
  134. ConfigDir: filepath.Clean(defaultConfigDir),
  135. ConfigFile: defaultConfigFile,
  136. LogFilePath: portableLogFile,
  137. LogMaxSize: defaultLogMaxSize,
  138. LogMaxBackups: defaultLogMaxBackup,
  139. LogMaxAge: defaultLogMaxAge,
  140. LogCompress: defaultLogCompress,
  141. LogVerbose: portableLogVerbose,
  142. LogUTCTime: portableLogUTCTime,
  143. Shutdown: make(chan bool),
  144. PortableMode: 1,
  145. PortableUser: dataprovider.User{
  146. BaseUser: sdk.BaseUser{
  147. Username: portableUsername,
  148. Password: portablePassword,
  149. PublicKeys: portablePublicKeys,
  150. Permissions: permissions,
  151. HomeDir: portableDir,
  152. Status: 1,
  153. Filters: sdk.UserFilters{
  154. FilePatterns: parsePatternsFilesFilters(),
  155. },
  156. },
  157. FsConfig: vfs.Filesystem{
  158. Provider: sdk.GetProviderByName(portableFsProvider),
  159. S3Config: vfs.S3FsConfig{
  160. S3FsConfig: sdk.S3FsConfig{
  161. Bucket: portableS3Bucket,
  162. Region: portableS3Region,
  163. AccessKey: portableS3AccessKey,
  164. AccessSecret: kms.NewPlainSecret(portableS3AccessSecret),
  165. Endpoint: portableS3Endpoint,
  166. StorageClass: portableS3StorageClass,
  167. ACL: portableS3ACL,
  168. KeyPrefix: portableS3KeyPrefix,
  169. UploadPartSize: int64(portableS3ULPartSize),
  170. UploadConcurrency: portableS3ULConcurrency,
  171. ForcePathStyle: portableS3ForcePathStyle,
  172. },
  173. },
  174. GCSConfig: vfs.GCSFsConfig{
  175. GCSFsConfig: sdk.GCSFsConfig{
  176. Bucket: portableGCSBucket,
  177. Credentials: kms.NewPlainSecret(portableGCSCredentials),
  178. AutomaticCredentials: portableGCSAutoCredentials,
  179. StorageClass: portableGCSStorageClass,
  180. KeyPrefix: portableGCSKeyPrefix,
  181. },
  182. },
  183. AzBlobConfig: vfs.AzBlobFsConfig{
  184. AzBlobFsConfig: sdk.AzBlobFsConfig{
  185. Container: portableAzContainer,
  186. AccountName: portableAzAccountName,
  187. AccountKey: kms.NewPlainSecret(portableAzAccountKey),
  188. Endpoint: portableAzEndpoint,
  189. AccessTier: portableAzAccessTier,
  190. SASURL: kms.NewPlainSecret(portableAzSASURL),
  191. KeyPrefix: portableAzKeyPrefix,
  192. UseEmulator: portableAzUseEmulator,
  193. UploadPartSize: int64(portableAzULPartSize),
  194. UploadConcurrency: portableAzULConcurrency,
  195. },
  196. },
  197. CryptConfig: vfs.CryptFsConfig{
  198. CryptFsConfig: sdk.CryptFsConfig{
  199. Passphrase: kms.NewPlainSecret(portableCryptPassphrase),
  200. },
  201. },
  202. SFTPConfig: vfs.SFTPFsConfig{
  203. SFTPFsConfig: sdk.SFTPFsConfig{
  204. Endpoint: portableSFTPEndpoint,
  205. Username: portableSFTPUsername,
  206. Password: kms.NewPlainSecret(portableSFTPPassword),
  207. PrivateKey: kms.NewPlainSecret(portableSFTPPrivateKey),
  208. Fingerprints: portableSFTPFingerprints,
  209. Prefix: portableSFTPPrefix,
  210. DisableCouncurrentReads: portableSFTPDisableConcurrentReads,
  211. BufferSize: portableSFTPDBufferSize,
  212. },
  213. },
  214. },
  215. },
  216. }
  217. if err := service.StartPortableMode(portableSFTPDPort, portableFTPDPort, portableWebDAVPort, portableSSHCommands, portableAdvertiseService,
  218. portableAdvertiseCredentials, portableFTPSCert, portableFTPSKey, portableWebDAVCert, portableWebDAVKey); err == nil {
  219. service.Wait()
  220. if service.Error == nil {
  221. os.Exit(0)
  222. }
  223. }
  224. os.Exit(1)
  225. },
  226. }
  227. )
  228. func init() {
  229. version.AddFeature("+portable")
  230. portableCmd.Flags().StringVarP(&directoryToServe, "directory", "d", ".", `Path to the directory to serve.
  231. This can be an absolute path or a path
  232. relative to the current directory
  233. `)
  234. portableCmd.Flags().IntVarP(&portableSFTPDPort, "sftpd-port", "s", 0, `0 means a random unprivileged port,
  235. < 0 disabled`)
  236. portableCmd.Flags().IntVar(&portableFTPDPort, "ftpd-port", -1, `0 means a random unprivileged port,
  237. < 0 disabled`)
  238. portableCmd.Flags().IntVar(&portableWebDAVPort, "webdav-port", -1, `0 means a random unprivileged port,
  239. < 0 disabled`)
  240. portableCmd.Flags().StringSliceVarP(&portableSSHCommands, "ssh-commands", "c", sftpd.GetDefaultSSHCommands(),
  241. `SSH commands to enable.
  242. "*" means any supported SSH command
  243. including scp
  244. `)
  245. portableCmd.Flags().StringVarP(&portableUsername, "username", "u", "", `Leave empty to use an auto generated
  246. value`)
  247. portableCmd.Flags().StringVarP(&portablePassword, "password", "p", "", `Leave empty to use an auto generated
  248. value`)
  249. portableCmd.Flags().StringVarP(&portableLogFile, logFilePathFlag, "l", "", "Leave empty to disable logging")
  250. portableCmd.Flags().BoolVarP(&portableLogVerbose, logVerboseFlag, "v", false, "Enable verbose logs")
  251. portableCmd.Flags().BoolVar(&portableLogUTCTime, logUTCTimeFlag, false, "Use UTC time for logging")
  252. portableCmd.Flags().StringSliceVarP(&portablePublicKeys, "public-key", "k", []string{}, "")
  253. portableCmd.Flags().StringSliceVarP(&portablePermissions, "permissions", "g", []string{"list", "download"},
  254. `User's permissions. "*" means any
  255. permission`)
  256. portableCmd.Flags().StringArrayVar(&portableAllowedPatterns, "allowed-patterns", []string{},
  257. `Allowed file patterns case insensitive.
  258. The format is:
  259. /dir::pattern1,pattern2.
  260. For example: "/somedir::*.jpg,a*b?.png"`)
  261. portableCmd.Flags().StringArrayVar(&portableDeniedPatterns, "denied-patterns", []string{},
  262. `Denied file patterns case insensitive.
  263. The format is:
  264. /dir::pattern1,pattern2.
  265. For example: "/somedir::*.jpg,a*b?.png"`)
  266. portableCmd.Flags().BoolVarP(&portableAdvertiseService, "advertise-service", "S", false,
  267. `Advertise configured services using
  268. multicast DNS`)
  269. portableCmd.Flags().BoolVarP(&portableAdvertiseCredentials, "advertise-credentials", "C", false,
  270. `If the SFTP/FTP service is
  271. advertised via multicast DNS, this
  272. flag allows to put username/password
  273. inside the advertised TXT record`)
  274. portableCmd.Flags().StringVarP(&portableFsProvider, "fs-provider", "f", "osfs", `osfs => local filesystem (legacy value: 0)
  275. s3fs => AWS S3 compatible (legacy: 1)
  276. gcsfs => Google Cloud Storage (legacy: 2)
  277. azblobfs => Azure Blob Storage (legacy: 3)
  278. cryptfs => Encrypted local filesystem (legacy: 4)
  279. sftpfs => SFTP (legacy: 5)`)
  280. portableCmd.Flags().StringVar(&portableS3Bucket, "s3-bucket", "", "")
  281. portableCmd.Flags().StringVar(&portableS3Region, "s3-region", "", "")
  282. portableCmd.Flags().StringVar(&portableS3AccessKey, "s3-access-key", "", "")
  283. portableCmd.Flags().StringVar(&portableS3AccessSecret, "s3-access-secret", "", "")
  284. portableCmd.Flags().StringVar(&portableS3Endpoint, "s3-endpoint", "", "")
  285. portableCmd.Flags().StringVar(&portableS3StorageClass, "s3-storage-class", "", "")
  286. portableCmd.Flags().StringVar(&portableS3ACL, "s3-acl", "", "")
  287. portableCmd.Flags().StringVar(&portableS3KeyPrefix, "s3-key-prefix", "", `Allows to restrict access to the
  288. virtual folder identified by this
  289. prefix and its contents`)
  290. portableCmd.Flags().IntVar(&portableS3ULPartSize, "s3-upload-part-size", 5, `The buffer size for multipart uploads
  291. (MB)`)
  292. portableCmd.Flags().IntVar(&portableS3ULConcurrency, "s3-upload-concurrency", 2, `How many parts are uploaded in
  293. parallel`)
  294. portableCmd.Flags().BoolVar(&portableS3ForcePathStyle, "s3-force-path-style", false, `Force path style bucket URL`)
  295. portableCmd.Flags().StringVar(&portableGCSBucket, "gcs-bucket", "", "")
  296. portableCmd.Flags().StringVar(&portableGCSStorageClass, "gcs-storage-class", "", "")
  297. portableCmd.Flags().StringVar(&portableGCSKeyPrefix, "gcs-key-prefix", "", `Allows to restrict access to the
  298. virtual folder identified by this
  299. prefix and its contents`)
  300. portableCmd.Flags().StringVar(&portableGCSCredentialsFile, "gcs-credentials-file", "", `Google Cloud Storage JSON credentials
  301. file`)
  302. portableCmd.Flags().IntVar(&portableGCSAutoCredentials, "gcs-automatic-credentials", 1, `0 means explicit credentials using
  303. a JSON credentials file, 1 automatic
  304. `)
  305. portableCmd.Flags().StringVar(&portableFTPSCert, "ftpd-cert", "", "Path to the certificate file for FTPS")
  306. portableCmd.Flags().StringVar(&portableFTPSKey, "ftpd-key", "", "Path to the key file for FTPS")
  307. portableCmd.Flags().StringVar(&portableWebDAVCert, "webdav-cert", "", `Path to the certificate file for WebDAV
  308. over HTTPS`)
  309. portableCmd.Flags().StringVar(&portableWebDAVKey, "webdav-key", "", `Path to the key file for WebDAV over
  310. HTTPS`)
  311. portableCmd.Flags().StringVar(&portableAzContainer, "az-container", "", "")
  312. portableCmd.Flags().StringVar(&portableAzAccountName, "az-account-name", "", "")
  313. portableCmd.Flags().StringVar(&portableAzAccountKey, "az-account-key", "", "")
  314. portableCmd.Flags().StringVar(&portableAzSASURL, "az-sas-url", "", `Shared access signature URL`)
  315. portableCmd.Flags().StringVar(&portableAzEndpoint, "az-endpoint", "", `Leave empty to use the default:
  316. "blob.core.windows.net"`)
  317. portableCmd.Flags().StringVar(&portableAzAccessTier, "az-access-tier", "", `Leave empty to use the default
  318. container setting`)
  319. portableCmd.Flags().StringVar(&portableAzKeyPrefix, "az-key-prefix", "", `Allows to restrict access to the
  320. virtual folder identified by this
  321. prefix and its contents`)
  322. portableCmd.Flags().IntVar(&portableAzULPartSize, "az-upload-part-size", 4, `The buffer size for multipart uploads
  323. (MB)`)
  324. portableCmd.Flags().IntVar(&portableAzULConcurrency, "az-upload-concurrency", 2, `How many parts are uploaded in
  325. parallel`)
  326. portableCmd.Flags().BoolVar(&portableAzUseEmulator, "az-use-emulator", false, "")
  327. portableCmd.Flags().StringVar(&portableCryptPassphrase, "crypto-passphrase", "", `Passphrase for encryption/decryption`)
  328. portableCmd.Flags().StringVar(&portableSFTPEndpoint, "sftp-endpoint", "", `SFTP endpoint as host:port for SFTP
  329. provider`)
  330. portableCmd.Flags().StringVar(&portableSFTPUsername, "sftp-username", "", `SFTP user for SFTP provider`)
  331. portableCmd.Flags().StringVar(&portableSFTPPassword, "sftp-password", "", `SFTP password for SFTP provider`)
  332. portableCmd.Flags().StringVar(&portableSFTPPrivateKeyPath, "sftp-key-path", "", `SFTP private key path for SFTP provider`)
  333. portableCmd.Flags().StringSliceVar(&portableSFTPFingerprints, "sftp-fingerprints", []string{}, `SFTP fingerprints to verify remote host
  334. key for SFTP provider`)
  335. portableCmd.Flags().StringVar(&portableSFTPPrefix, "sftp-prefix", "", `SFTP prefix allows restrict all
  336. operations to a given path within the
  337. remote SFTP server`)
  338. portableCmd.Flags().BoolVar(&portableSFTPDisableConcurrentReads, "sftp-disable-concurrent-reads", false, `Concurrent reads are safe to use and
  339. disabling them will degrade performance.
  340. Disable for read once servers`)
  341. portableCmd.Flags().Int64Var(&portableSFTPDBufferSize, "sftp-buffer-size", 0, `The size of the buffer (in MB) to use
  342. for transfers. By enabling buffering,
  343. the reads and writes, from/to the
  344. remote SFTP server, are split in
  345. multiple concurrent requests and this
  346. allows data to be transferred at a
  347. faster rate, over high latency networks,
  348. by overlapping round-trip times`)
  349. rootCmd.AddCommand(portableCmd)
  350. }
  351. func parsePatternsFilesFilters() []sdk.PatternsFilter {
  352. var patterns []sdk.PatternsFilter
  353. for _, val := range portableAllowedPatterns {
  354. p, exts := getPatternsFilterValues(strings.TrimSpace(val))
  355. if p != "" {
  356. patterns = append(patterns, sdk.PatternsFilter{
  357. Path: path.Clean(p),
  358. AllowedPatterns: exts,
  359. DeniedPatterns: []string{},
  360. })
  361. }
  362. }
  363. for _, val := range portableDeniedPatterns {
  364. p, exts := getPatternsFilterValues(strings.TrimSpace(val))
  365. if p != "" {
  366. found := false
  367. for index, e := range patterns {
  368. if path.Clean(e.Path) == path.Clean(p) {
  369. patterns[index].DeniedPatterns = append(patterns[index].DeniedPatterns, exts...)
  370. found = true
  371. break
  372. }
  373. }
  374. if !found {
  375. patterns = append(patterns, sdk.PatternsFilter{
  376. Path: path.Clean(p),
  377. AllowedPatterns: []string{},
  378. DeniedPatterns: exts,
  379. })
  380. }
  381. }
  382. }
  383. return patterns
  384. }
  385. func getPatternsFilterValues(value string) (string, []string) {
  386. if strings.Contains(value, "::") {
  387. dirExts := strings.Split(value, "::")
  388. if len(dirExts) > 1 {
  389. dir := strings.TrimSpace(dirExts[0])
  390. exts := []string{}
  391. for _, e := range strings.Split(dirExts[1], ",") {
  392. cleanedExt := strings.TrimSpace(e)
  393. if cleanedExt != "" {
  394. exts = append(exts, cleanedExt)
  395. }
  396. }
  397. if dir != "" && len(exts) > 0 {
  398. return dir, exts
  399. }
  400. }
  401. }
  402. return "", nil
  403. }
  404. func getFileContents(name string) (string, error) {
  405. fi, err := os.Stat(name)
  406. if err != nil {
  407. return "", err
  408. }
  409. if fi.Size() > 1048576 {
  410. return "", fmt.Errorf("%#v is too big %v/1048576 bytes", name, fi.Size())
  411. }
  412. contents, err := os.ReadFile(name)
  413. if err != nil {
  414. return "", err
  415. }
  416. return string(contents), nil
  417. }