transfer.sh/cmd/cmd.go
2023-12-04 14:36:57 +09:00

550 lines
14 KiB
Go

package cmd
import (
"errors"
"fmt"
"log"
"os"
"strings"
"github.com/dutchcoders/transfer.sh/server/storage"
"github.com/dutchcoders/transfer.sh/server"
"github.com/fatih/color"
"github.com/urfave/cli/v2"
"google.golang.org/api/googleapi"
)
// Version is inject at build time
var Version = "0.0.0"
var helpTemplate = `NAME:
{{.Name}} - {{.Usage}}
DESCRIPTION:
{{.Description}}
USAGE:
{{.Name}} {{if .Flags}}[flags] {{end}}command{{if .Flags}}{{end}} [arguments...]
COMMANDS:
{{range .Commands}}{{join .Names ", "}}{{ "\t" }}{{.Usage}}
{{end}}{{if .Flags}}
FLAGS:
{{range .Flags}}{{.}}
{{end}}{{end}}
VERSION:
` + Version +
`{{ "\n"}}`
var globalFlags = []cli.Flag{
&cli.StringFlag{
Name: "listener",
Usage: "127.0.0.1:8080",
Value: "127.0.0.1:8080",
EnvVars: []string{"LISTENER"},
},
// redirect to https?
// hostnames
&cli.StringFlag{
Name: "profile-listener",
Usage: "127.0.0.1:6060",
Value: "",
EnvVars: []string{"PROFILE_LISTENER"},
},
&cli.BoolFlag{
Name: "force-https",
Usage: "",
EnvVars: []string{"FORCE_HTTPS"},
},
&cli.StringFlag{
Name: "tls-listener",
Usage: "127.0.0.1:8443",
Value: "",
EnvVars: []string{"TLS_LISTENER"},
},
&cli.BoolFlag{
Name: "tls-listener-only",
Usage: "",
EnvVars: []string{"TLS_LISTENER_ONLY"},
},
&cli.StringFlag{
Name: "tls-cert-file",
Value: "",
EnvVars: []string{"TLS_CERT_FILE"},
},
&cli.StringFlag{
Name: "tls-private-key",
Value: "",
EnvVars: []string{"TLS_PRIVATE_KEY"},
},
&cli.StringFlag{
Name: "temp-path",
Usage: "path to temp files",
Value: os.TempDir(),
EnvVars: []string{"TEMP_PATH"},
},
&cli.StringFlag{
Name: "web-path",
Usage: "path to static web files",
Value: "",
EnvVars: []string{"WEB_PATH"},
},
&cli.StringFlag{
Name: "proxy-path",
Usage: "path prefix when service is run behind a proxy",
Value: "",
EnvVars: []string{"PROXY_PATH"},
},
&cli.StringFlag{
Name: "proxy-port",
Usage: "port of the proxy when the service is run behind a proxy",
Value: "",
EnvVars: []string{"PROXY_PORT"},
},
&cli.StringFlag{
Name: "email-contact",
Usage: "email address to link in Contact Us (front end)",
Value: "",
EnvVars: []string{"EMAIL_CONTACT"},
},
&cli.StringFlag{
Name: "ga-key",
Usage: "key for google analytics (front end)",
Value: "",
EnvVars: []string{"GA_KEY"},
},
&cli.StringFlag{
Name: "uservoice-key",
Usage: "key for user voice (front end)",
Value: "",
EnvVars: []string{"USERVOICE_KEY"},
},
&cli.StringFlag{
Name: "provider",
Usage: "s3|gdrive|local",
Value: "",
EnvVars: []string{"PROVIDER"},
},
&cli.StringFlag{
Name: "s3-endpoint",
Usage: "",
Value: "",
EnvVars: []string{"S3_ENDPOINT"},
},
&cli.StringFlag{
Name: "s3-region",
Usage: "",
Value: "eu-west-1",
EnvVars: []string{"S3_REGION"},
},
&cli.StringFlag{
Name: "aws-access-key",
Usage: "",
Value: "",
EnvVars: []string{"AWS_ACCESS_KEY"},
},
&cli.StringFlag{
Name: "aws-secret-key",
Usage: "",
Value: "",
EnvVars: []string{"AWS_SECRET_KEY"},
},
&cli.StringFlag{
Name: "bucket",
Usage: "",
Value: "",
EnvVars: []string{"BUCKET"},
},
&cli.BoolFlag{
Name: "s3-no-multipart",
Usage: "Disables S3 Multipart Puts",
EnvVars: []string{"S3_NO_MULTIPART"},
},
&cli.BoolFlag{
Name: "s3-path-style",
Usage: "Forces path style URLs, required for Minio.",
EnvVars: []string{"S3_PATH_STYLE"},
},
&cli.StringFlag{
Name: "gdrive-client-json-filepath",
Usage: "",
Value: "",
EnvVars: []string{"GDRIVE_CLIENT_JSON_FILEPATH"},
},
&cli.StringFlag{
Name: "gdrive-local-config-path",
Usage: "",
Value: "",
EnvVars: []string{"GDRIVE_LOCAL_CONFIG_PATH"},
},
&cli.IntFlag{
Name: "gdrive-chunk-size",
Usage: "",
Value: googleapi.DefaultUploadChunkSize / 1024 / 1024,
EnvVars: []string{"GDRIVE_CHUNK_SIZE"},
},
&cli.StringFlag{
Name: "storj-access",
Usage: "Access for the project",
Value: "",
EnvVars: []string{"STORJ_ACCESS"},
},
&cli.StringFlag{
Name: "storj-bucket",
Usage: "Bucket to use within the project",
Value: "",
EnvVars: []string{"STORJ_BUCKET"},
},
&cli.IntFlag{
Name: "rate-limit",
Usage: "requests per minute",
Value: 0,
EnvVars: []string{"RATE_LIMIT"},
},
&cli.IntFlag{
Name: "purge-days",
Usage: "number of days after uploads are purged automatically",
Value: 0,
EnvVars: []string{"PURGE_DAYS"},
},
&cli.IntFlag{
Name: "purge-interval",
Usage: "interval in hours to run the automatic purge for",
Value: 0,
EnvVars: []string{"PURGE_INTERVAL"},
},
&cli.Int64Flag{
Name: "max-upload-size",
Usage: "max limit for upload, in kilobytes",
Value: 0,
EnvVars: []string{"MAX_UPLOAD_SIZE"},
},
&cli.StringFlag{
Name: "lets-encrypt-hosts",
Usage: "host1, host2",
Value: "",
EnvVars: []string{"HOSTS"},
},
&cli.StringFlag{
Name: "log",
Usage: "/var/log/transfersh.log",
Value: "",
EnvVars: []string{"LOG"},
},
&cli.StringFlag{
Name: "basedir",
Usage: "path to storage",
Value: "",
EnvVars: []string{"BASEDIR"},
},
&cli.StringFlag{
Name: "clamav-host",
Usage: "clamav-host",
Value: "",
EnvVars: []string{"CLAMAV_HOST"},
},
&cli.BoolFlag{
Name: "perform-clamav-prescan",
Usage: "perform-clamav-prescan",
EnvVars: []string{"PERFORM_CLAMAV_PRESCAN"},
},
&cli.StringFlag{
Name: "virustotal-key",
Usage: "virustotal-key",
Value: "",
EnvVars: []string{"VIRUSTOTAL_KEY"},
},
&cli.BoolFlag{
Name: "profiler",
Usage: "enable profiling",
EnvVars: []string{"PROFILER"},
},
&cli.StringFlag{
Name: "http-auth-user",
Usage: "user for http basic auth",
Value: "",
EnvVars: []string{"HTTP_AUTH_USER"},
},
&cli.StringFlag{
Name: "http-auth-pass",
Usage: "pass for http basic auth",
Value: "",
EnvVars: []string{"HTTP_AUTH_PASS"},
},
&cli.StringFlag{
Name: "http-auth-htpasswd",
Usage: "htpasswd file http basic auth",
Value: "",
EnvVars: []string{"HTTP_AUTH_HTPASSWD"},
},
&cli.StringFlag{
Name: "http-auth-ip-whitelist",
Usage: "comma separated list of ips allowed to upload without being challenged an http auth",
Value: "",
EnvVars: []string{"HTTP_AUTH_IP_WHITELIST"},
},
&cli.StringFlag{
Name: "ip-whitelist",
Usage: "comma separated list of ips allowed to connect to the service",
Value: "",
EnvVars: []string{"IP_WHITELIST"},
},
&cli.StringFlag{
Name: "ip-blacklist",
Usage: "comma separated list of ips not allowed to connect to the service",
Value: "",
EnvVars: []string{"IP_BLACKLIST"},
},
&cli.StringFlag{
Name: "cors-domains",
Usage: "comma separated list of domains allowed for CORS requests",
Value: "",
EnvVars: []string{"CORS_DOMAINS"},
},
&cli.IntFlag{
Name: "random-token-length",
Usage: "",
Value: 10,
EnvVars: []string{"RANDOM_TOKEN_LENGTH"},
},
}
// Cmd wraps cli.app
type Cmd struct {
*cli.App
}
func versionCommand(_ *cli.Context) error {
fmt.Println(color.YellowString("transfer.sh %s: Easy file sharing from the command line", Version))
return nil
}
// New is the factory for transfer.sh
func New() *Cmd {
logger := log.New(os.Stdout, "[transfer.sh]", log.LstdFlags)
app := cli.NewApp()
app.Name = "transfer.sh"
app.Authors = []*cli.Author{}
app.Usage = "transfer.sh"
app.Description = `Easy file sharing from the command line`
app.Version = Version
app.Flags = globalFlags
app.CustomAppHelpTemplate = helpTemplate
app.Commands = []*cli.Command{
{
Name: "version",
Action: versionCommand,
},
}
app.Before = func(c *cli.Context) error {
return nil
}
app.Action = func(c *cli.Context) error {
var options []server.OptionFn
if v := c.String("listener"); v != "" {
options = append(options, server.Listener(v))
}
if v := c.String("cors-domains"); v != "" {
options = append(options, server.CorsDomains(v))
}
if v := c.String("tls-listener"); v == "" {
} else if c.Bool("tls-listener-only") {
options = append(options, server.TLSListener(v, true))
} else {
options = append(options, server.TLSListener(v, false))
}
if v := c.String("profile-listener"); v != "" {
options = append(options, server.ProfileListener(v))
}
if v := c.String("web-path"); v != "" {
options = append(options, server.WebPath(v))
}
if v := c.String("proxy-path"); v != "" {
options = append(options, server.ProxyPath(v))
}
if v := c.String("proxy-port"); v != "" {
options = append(options, server.ProxyPort(v))
}
if v := c.String("email-contact"); v != "" {
options = append(options, server.EmailContact(v))
}
if v := c.String("ga-key"); v != "" {
options = append(options, server.GoogleAnalytics(v))
}
if v := c.String("uservoice-key"); v != "" {
options = append(options, server.UserVoice(v))
}
if v := c.String("temp-path"); v != "" {
options = append(options, server.TempPath(v))
}
if v := c.String("log"); v != "" {
options = append(options, server.LogFile(logger, v))
} else {
options = append(options, server.Logger(logger))
}
if v := c.String("lets-encrypt-hosts"); v != "" {
options = append(options, server.UseLetsEncrypt(strings.Split(v, ",")))
}
if v := c.String("virustotal-key"); v != "" {
options = append(options, server.VirustotalKey(v))
}
if v := c.String("clamav-host"); v != "" {
options = append(options, server.ClamavHost(v))
}
if v := c.Bool("perform-clamav-prescan"); v {
if c.String("clamav-host") == "" {
return errors.New("clamav-host not set")
}
options = append(options, server.PerformClamavPrescan(v))
}
if v := c.Int64("max-upload-size"); v > 0 {
options = append(options, server.MaxUploadSize(v))
}
if v := c.Int("rate-limit"); v > 0 {
options = append(options, server.RateLimit(v))
}
v := c.Int("random-token-length")
options = append(options, server.RandomTokenLength(v))
purgeDays := c.Int("purge-days")
purgeInterval := c.Int("purge-interval")
if purgeDays > 0 && purgeInterval > 0 {
options = append(options, server.Purge(purgeDays, purgeInterval))
}
if cert := c.String("tls-cert-file"); cert == "" {
} else if pk := c.String("tls-private-key"); pk == "" {
} else {
options = append(options, server.TLSConfig(cert, pk))
}
if c.Bool("profiler") {
options = append(options, server.EnableProfiler())
}
if c.Bool("force-https") {
options = append(options, server.ForceHTTPS())
}
if httpAuthUser := c.String("http-auth-user"); httpAuthUser == "" {
} else if httpAuthPass := c.String("http-auth-pass"); httpAuthPass == "" {
} else {
options = append(options, server.HTTPAuthCredentials(httpAuthUser, httpAuthPass))
}
if httpAuthHtpasswd := c.String("http-auth-htpasswd"); httpAuthHtpasswd != "" {
options = append(options, server.HTTPAuthHtpasswd(httpAuthHtpasswd))
}
if httpAuthIPWhitelist := c.String("http-auth-ip-whitelist"); httpAuthIPWhitelist != "" {
ipFilterOptions := server.IPFilterOptions{}
ipFilterOptions.AllowedIPs = strings.Split(httpAuthIPWhitelist, ",")
ipFilterOptions.BlockByDefault = true
options = append(options, server.HTTPAUTHFilterOptions(ipFilterOptions))
}
applyIPFilter := false
ipFilterOptions := server.IPFilterOptions{}
if ipWhitelist := c.String("ip-whitelist"); ipWhitelist != "" {
applyIPFilter = true
ipFilterOptions.AllowedIPs = strings.Split(ipWhitelist, ",")
ipFilterOptions.BlockByDefault = true
}
if ipBlacklist := c.String("ip-blacklist"); ipBlacklist != "" {
applyIPFilter = true
ipFilterOptions.BlockedIPs = strings.Split(ipBlacklist, ",")
}
if applyIPFilter {
options = append(options, server.FilterOptions(ipFilterOptions))
}
switch provider := c.String("provider"); provider {
case "s3":
if accessKey := c.String("aws-access-key"); accessKey == "" {
return errors.New("access-key not set.")
} else if secretKey := c.String("aws-secret-key"); secretKey == "" {
return errors.New("secret-key not set.")
} else if bucket := c.String("bucket"); bucket == "" {
return errors.New("bucket not set.")
} else if store, err := storage.NewS3Storage(c.Context, accessKey, secretKey, bucket, purgeDays, c.String("s3-region"), c.String("s3-endpoint"), c.Bool("s3-no-multipart"), c.Bool("s3-path-style"), logger); err != nil {
return err
} else {
options = append(options, server.UseStorage(store))
}
case "gdrive":
chunkSize := c.Int("gdrive-chunk-size") * 1024 * 1024
if clientJSONFilepath := c.String("gdrive-client-json-filepath"); clientJSONFilepath == "" {
return errors.New("gdrive-client-json-filepath not set.")
} else if localConfigPath := c.String("gdrive-local-config-path"); localConfigPath == "" {
return errors.New("gdrive-local-config-path not set.")
} else if basedir := c.String("basedir"); basedir == "" {
return errors.New("basedir not set.")
} else if store, err := storage.NewGDriveStorage(c.Context, clientJSONFilepath, localConfigPath, basedir, chunkSize, logger); err != nil {
return err
} else {
options = append(options, server.UseStorage(store))
}
case "storj":
if access := c.String("storj-access"); access == "" {
return errors.New("storj-access not set.")
} else if bucket := c.String("storj-bucket"); bucket == "" {
return errors.New("storj-bucket not set.")
} else if store, err := storage.NewStorjStorage(c.Context, access, bucket, purgeDays, logger); err != nil {
return err
} else {
options = append(options, server.UseStorage(store))
}
case "local":
if v := c.String("basedir"); v == "" {
return errors.New("basedir not set.")
} else if store, err := storage.NewLocalStorage(v, logger); err != nil {
return err
} else {
options = append(options, server.UseStorage(store))
}
default:
return errors.New("Provider not set or invalid.")
}
srvr, err := server.New(
options...,
)
if err != nil {
logger.Println(color.RedString("Error starting server: %s", err.Error()))
return err
}
srvr.Run()
return nil
}
return &Cmd{
App: app,
}
}