portable.go 19 KB

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