diff --git a/README.md b/README.md index bc8dbbe..8e18b0f 100644 --- a/README.md +++ b/README.md @@ -119,6 +119,16 @@ ls /tmp/ | grep -i distribusi distribusi-go-20-25-492637114356 ``` +If you have SSH access to a server, you can publish your files with `-P/--publish`. + +It works just like [scp] does: + +``` +./distribusi -p -P : +``` + +See [this short guide](#ssh-guide-for-the-publishing) for help with SSH issues. + :v: ## Hacking @@ -154,6 +164,42 @@ Binaries are in `dist/`. [`LICENSE`](./LICENSE) +## Tips + +### SSH guide for `-P/--publish` + +If you already know & use `scp` then `-P/--publish` should Just Work :tm: If +not, read on for some tips on how to configure your SSH client to help +`distribusi-go` make SSH connections successfully. + +The simplest way to make sure `distribusi` can make an SSH connection with your +server is to match a `~/.ssh/config` entry with the `-P :...` server +value you pass on the command-line. + +If you want to run `./distribusi -p -P varia.zone:/var/www/`, then a +matching `~/.ssh/config` entry might look like this: + +```ssh +Host varia.zone + Hostname varia.zone + User decentral1se + Port 22 + IdentityFile ~/.ssh/ +``` + +This tells `distribusi-go` everything it needs to know to make a successful SSH +connection. Run with `-d/--debug` for extra help figuring out what connection +details are being used. + +If you have a secret protected SSH key then make sure you've got a running +`ssh-agent` and have added the key with the following: + +``` +eval $(ssh-agent -k) +ssh-add ~/.ssh/ +ssh-add -L # see loaded keys +``` + [Go]: https://go.dev [Pillow]: https://pillow.readthedocs.io/en/stable/installation.html#external-libraries [distribusi]: https://git.vvvvvvaria.org/varia/distribusi @@ -165,3 +211,4 @@ Binaries are in `dist/`. [the entire listing]: https://vvvvvvaria.org/~decentral1se/distribusi-go/ [this book]: https://www.gopl.io/ [this ticket]: https://git.vvvvvvaria.org/decentral1se/distribusi-go/issues/1 +[scp]: https://linux.die.net/man/1/scp diff --git a/distribusi.go b/distribusi.go index f5d5229..1ed8c15 100644 --- a/distribusi.go +++ b/distribusi.go @@ -11,6 +11,7 @@ import ( "io" "io/fs" "io/ioutil" + "net" "net/http" "os" "os/exec" @@ -25,9 +26,13 @@ import ( "github.com/disintegration/imaging" "github.com/gabriel-vasile/mimetype" "github.com/k0kubun/go-ansi" + "github.com/kevinburke/ssh_config" + "github.com/povsister/scp" "github.com/schollz/progressbar/v3" "github.com/sirupsen/logrus" "github.com/urfave/cli/v2" + "golang.org/x/crypto/ssh" + "golang.org/x/crypto/ssh/agent" ) // Version is the current version of distribusi-go. @@ -211,6 +216,12 @@ Example: Usage: "path to distribusify", Required: true, }, + &cli.StringFlag{ + Name: "publish", + Aliases: []string{"P"}, + Usage: `publish to a server using scp (e.g. "varia.zone:public_html")`, + Required: false, + }, &cli.BoolFlag{ Name: "serve", Aliases: []string{"s"}, @@ -253,6 +264,10 @@ Example: return nil }, Action: func(c *cli.Context) error { + if c.Bool("serve") && c.String("publish") != "" { + logrus.Fatal("woops, can't publish & serve at the same time?") + } + root, err := filepath.Abs(c.String("path")) if err != nil { logrus.Fatal(err) @@ -300,6 +315,16 @@ Example: fmt.Println("done!") } + pubTarget := c.String("publish") + if pubTarget != "" { + logrus.Debugf("attempting to publish files to %s", pubTarget) + + if err := scpPublish(c, root, pubTarget); err != nil { + ch <- err + return + } + } + ch <- nil return }() @@ -879,3 +904,67 @@ func mkProgressBar(c *cli.Context) *progressbar.ProgressBar { return bar } + +// scpPublish initates a scp-like publishing interface. We do our best here to +// simply work with existing local work station SSH client configurations. It +// is mostly up to folks to configure their own shit. We don't do anything +// fancy here. +func scpPublish(c *cli.Context, root, pubTarget string) error { + split := strings.Split(pubTarget, ":") + server, remotePath := split[0], split[1] + + logrus.Debugf("parsed server: %s remotePath: %s from %s", server, remotePath, pubTarget) + + if hostname := ssh_config.Get(server, "Hostname"); hostname == "" { + return fmt.Errorf("missing Hostname entry for %s in ~/.ssh/config, cannot continue", server) + } + + user := ssh_config.Get(server, "User") + port := ssh_config.Get(server, "Port") + + logrus.Debugf("read user: %s, port: %s for %s in ~/.ssh/config", user, port, server) + + sshConf := &ssh.ClientConfig{ + User: user, + HostKeyCallback: ssh.InsecureIgnoreHostKey(), // awful, i know + Timeout: 5 * time.Second, + } + + identityFile := ssh_config.Get(server, "IdentityFile") + if identityFile != "" && identityFile != "~/.ssh/identity" { + sshAgent, err := net.Dial("unix", os.Getenv("SSH_AUTH_SOCK")) + if err != nil { + return err + } + + agentCl := agent.NewClient(sshAgent) + authMethod := ssh.PublicKeysCallback(agentCl.Signers) + sshConf.Auth = []ssh.AuthMethod{authMethod} + + logrus.Debugf("read identityFile: %s for %s in ~/.ssh/config, using ssh-agent for auth", identityFile, server) + } + + logrus.Debug("attempting to construct SSH client for publishing logic") + + serverAndPort := fmt.Sprintf("%s:%s", server, port) + scpClient, err := scp.NewClient(serverAndPort, sshConf, &scp.ClientOption{}) + if err != nil { + return err + } + defer scpClient.Close() + + opts := &scp.DirTransferOption{ + Context: c.Context, + Timeout: 10 * time.Minute, + } + + fmt.Printf(fmt.Sprintf("publishing %s to %s...", filepath.Base(root), server)) + + if err := scpClient.CopyDirToRemote(root, remotePath, opts); err != nil { + return err + } + + fmt.Println(" done!") + + return nil +} diff --git a/go.mod b/go.mod index 9343a46..f445217 100644 --- a/go.mod +++ b/go.mod @@ -16,9 +16,11 @@ require ( require ( github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d // indirect github.com/facebookgo/stack v0.0.0-20160209184415-751773369052 // indirect + github.com/kevinburke/ssh_config v1.1.0 // indirect github.com/mattn/go-isatty v0.0.14 // indirect github.com/mattn/go-runewidth v0.0.13 // indirect github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect + github.com/povsister/scp v0.0.0-20210427074412-33febfd9f13e // indirect github.com/rivo/uniseg v0.2.0 // indirect github.com/russross/blackfriday/v2 v2.0.1 // indirect github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect diff --git a/go.sum b/go.sum index 44491cd..cd786b2 100644 --- a/go.sum +++ b/go.sum @@ -16,6 +16,8 @@ github.com/gabriel-vasile/mimetype v1.4.0 h1:Cn9dkdYsMIu56tGho+fqzh7XmvY2YyGU0Fn github.com/gabriel-vasile/mimetype v1.4.0/go.mod h1:fA8fi6KUiG7MgQQ+mEWotXoEOvmxRtOJlERCzSmRvr8= github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213 h1:qGQQKEcAR99REcMpsXCp3lJ03zYT1PkRd3kQGPn9GVg= github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213/go.mod h1:vNUNkEQ1e29fT/6vq2aBdFsgNPmy8qMdSay1npru+Sw= +github.com/kevinburke/ssh_config v1.1.0 h1:pH/t1WS9NzT8go394IqZeJTMHVm6Cr6ZJ6AQ+mdNo/o= +github.com/kevinburke/ssh_config v1.1.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= @@ -24,6 +26,8 @@ github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db h1:62I3jR2Em github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db/go.mod h1:l0dey0ia/Uv7NcFFVbCLtqEBQbrT4OCwCSKTEv6enCw= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/povsister/scp v0.0.0-20210427074412-33febfd9f13e h1:VtsDti2SgX7M7jy0QAyGgb162PeHLrOaNxmcYOtaGsY= +github.com/povsister/scp v0.0.0-20210427074412-33febfd9f13e/go.mod h1:i1Au86ZXK0ZalQNyBp2njCcyhSCR/QP/AMfILip+zNI= github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q= @@ -40,10 +44,12 @@ github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0 github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/urfave/cli/v2 v2.3.0 h1:qph92Y649prgesehzOrQjdWyxFOp/QVM+6imKHad91M= github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI= +golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20220131195533-30dcbda58838 h1:71vQrMauZZhcTVK6KdYM+rklehEEwb3E+ZhaE5jrPrE= golang.org/x/crypto v0.0.0-20220131195533-30dcbda58838/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8 h1:hVwzHzIUGRjiF7EcUjqNxk3NCfkPxbDKRdnNE1Rpg0U= golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2 h1:CIJ76btIcR3eFI5EgSo6k1qKw9KJexJuRLI9G7Hp5wE= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= @@ -58,6 +64,7 @@ golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9sn golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=