Support IPv6 in SFTP backend

The previous code was doing its own hostname:port splitting, which
caused IPv6 addresses to be misinterpreted.
This commit is contained in:
greatroar 2020-02-19 15:33:52 +01:00
parent f2bf06a419
commit 6ac6bca7a1
5 changed files with 60 additions and 22 deletions

View file

@ -86,10 +86,21 @@ specify the user this way: ``user@domain@host``.
want to specify a path relative to the user's home directory, pass a
relative path to the sftp backend.
The backend config string does not allow specifying a port. If you need
to contact an sftp server on a different port, you can create an entry
in the ``ssh`` file, usually located in your user's home directory at
``~/.ssh/config`` or in ``/etc/ssh/ssh_config``:
If you need to specify a port number or IPv6 address, you'll need to use
URL syntax. E.g., the repository ``/srv/restic-repo`` on ``[::1]`` (localhost)
at port 2222 with username ``user`` can be specified as
::
sftp://user@[::1]:2222//srv/restic-repo
Note the double slash: the first slash separates the connection settings from
the path, while the second is the start of the path. To specify a relative
path, use one slash.
Alternatively, you can create an entry in the ``ssh`` configuration file,
usually located in your home directory at ``~/.ssh/config`` or in
``/etc/ssh/ssh_config``:
::

View file

@ -11,7 +11,8 @@ import (
// Config collects all information required to connect to an sftp server.
type Config struct {
User, Host, Path string
User, Host, Port, Path string
Layout string `option:"layout" help:"use this backend directory layout (default: auto-detect)"`
Command string `option:"command" help:"specify command to create sftp connection"`
}
@ -21,12 +22,12 @@ func init() {
}
// ParseConfig parses the string s and extracts the sftp config. The
// supported configuration formats are sftp://user@host/directory
// supported configuration formats are sftp://user@host[:port]/directory
// and sftp:user@host:directory. The directory will be path Cleaned and can
// be an absolute path if it starts with a '/' (e.g.
// sftp://user@host//absolute and sftp:user@host:/absolute).
func ParseConfig(s string) (interface{}, error) {
var user, host, dir string
var user, host, port, dir string
switch {
case strings.HasPrefix(s, "sftp://"):
// parse the "sftp://user@host/path" url format
@ -37,7 +38,8 @@ func ParseConfig(s string) (interface{}, error) {
if url.User != nil {
user = url.User.Username()
}
host = url.Host
host = url.Hostname()
port = url.Port()
dir = url.Path
if dir == "" {
return nil, errors.Errorf("invalid backend %q, no directory specified", s)
@ -76,6 +78,7 @@ func ParseConfig(s string) (interface{}, error) {
return Config{
User: user,
Host: host,
Port: port,
Path: p,
}, nil
}

View file

@ -23,11 +23,11 @@ var configTests = []struct {
},
{
"sftp://host:10022//dir/subdir",
Config{Host: "host:10022", Path: "/dir/subdir"},
Config{Host: "host", Port: "10022", Path: "/dir/subdir"},
},
{
"sftp://user@host:10022//dir/subdir",
Config{User: "user", Host: "host:10022", Path: "/dir/subdir"},
Config{User: "user", Host: "host", Port: "10022", Path: "/dir/subdir"},
},
{
"sftp://user@host/dir/subdir/../other",
@ -38,6 +38,17 @@ var configTests = []struct {
Config{User: "user", Host: "host", Path: "dir/subdir"},
},
// IPv6 address.
{
"sftp://user@[::1]/dir",
Config{User: "user", Host: "::1", Path: "dir"},
},
// IPv6 address with port.
{
"sftp://user@[::1]:22/dir",
Config{User: "user", Host: "::1", Port: "22", Path: "dir"},
},
// second form, user specified sftp:user@host:/dir
{
"sftp:user@host:/dir/subdir",

View file

@ -8,7 +8,6 @@ import (
"os"
"os/exec"
"path"
"strings"
"time"
"github.com/restic/restic/internal/errors"
@ -190,10 +189,11 @@ func buildSSHCommand(cfg Config) (cmd string, args []string, err error) {
cmd = "ssh"
hostport := strings.Split(cfg.Host, ":")
args = []string{hostport[0]}
if len(hostport) > 1 {
args = append(args, "-p", hostport[1])
host, port := cfg.Host, cfg.Port
args = []string{host}
if port != "" {
args = append(args, "-p", port)
}
if cfg.User != "" {
args = append(args, "-l")

View file

@ -21,23 +21,35 @@ var sshcmdTests = []struct {
[]string{"host", "-s", "sftp"},
},
{
Config{Host: "host:10022", Path: "/dir/subdir"},
Config{Host: "host", Port: "10022", Path: "/dir/subdir"},
"ssh",
[]string{"host", "-p", "10022", "-s", "sftp"},
},
{
Config{User: "user", Host: "host:10022", Path: "/dir/subdir"},
Config{User: "user", Host: "host", Port: "10022", Path: "/dir/subdir"},
"ssh",
[]string{"host", "-p", "10022", "-l", "user", "-s", "sftp"},
},
{
// IPv6 address.
Config{User: "user", Host: "::1", Path: "dir"},
"ssh",
[]string{"::1", "-l", "user", "-s", "sftp"},
},
{
// IPv6 address with zone and port.
Config{User: "user", Host: "::1%lo0", Port: "22", Path: "dir"},
"ssh",
[]string{"::1%lo0", "-p", "22", "-l", "user", "-s", "sftp"},
},
}
func TestBuildSSHCommand(t *testing.T) {
for _, test := range sshcmdTests {
for i, test := range sshcmdTests {
t.Run("", func(t *testing.T) {
cmd, args, err := buildSSHCommand(test.cfg)
if err != nil {
t.Fatal(err)
t.Fatalf("%v in test %d", err, i)
}
if cmd != test.cmd {
@ -45,7 +57,8 @@ func TestBuildSSHCommand(t *testing.T) {
}
if !reflect.DeepEqual(test.args, args) {
t.Fatalf("wrong args, want:\n %v\ngot:\n %v", test.args, args)
t.Fatalf("wrong args in test %d, want:\n %v\ngot:\n %v",
i, test.args, args)
}
})
}