portable.go 13 KB

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