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:
parent
f2bf06a419
commit
6ac6bca7a1
5 changed files with 60 additions and 22 deletions
|
@ -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
|
want to specify a path relative to the user's home directory, pass a
|
||||||
relative path to the sftp backend.
|
relative path to the sftp backend.
|
||||||
|
|
||||||
The backend config string does not allow specifying a port. If you need
|
If you need to specify a port number or IPv6 address, you'll need to use
|
||||||
to contact an sftp server on a different port, you can create an entry
|
URL syntax. E.g., the repository ``/srv/restic-repo`` on ``[::1]`` (localhost)
|
||||||
in the ``ssh`` file, usually located in your user's home directory at
|
at port 2222 with username ``user`` can be specified as
|
||||||
``~/.ssh/config`` or in ``/etc/ssh/ssh_config``:
|
|
||||||
|
::
|
||||||
|
|
||||||
|
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``:
|
||||||
|
|
||||||
::
|
::
|
||||||
|
|
||||||
|
|
|
@ -11,7 +11,8 @@ import (
|
||||||
|
|
||||||
// Config collects all information required to connect to an sftp server.
|
// Config collects all information required to connect to an sftp server.
|
||||||
type Config struct {
|
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)"`
|
Layout string `option:"layout" help:"use this backend directory layout (default: auto-detect)"`
|
||||||
Command string `option:"command" help:"specify command to create sftp connection"`
|
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
|
// 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
|
// and sftp:user@host:directory. The directory will be path Cleaned and can
|
||||||
// be an absolute path if it starts with a '/' (e.g.
|
// be an absolute path if it starts with a '/' (e.g.
|
||||||
// sftp://user@host//absolute and sftp:user@host:/absolute).
|
// sftp://user@host//absolute and sftp:user@host:/absolute).
|
||||||
func ParseConfig(s string) (interface{}, error) {
|
func ParseConfig(s string) (interface{}, error) {
|
||||||
var user, host, dir string
|
var user, host, port, dir string
|
||||||
switch {
|
switch {
|
||||||
case strings.HasPrefix(s, "sftp://"):
|
case strings.HasPrefix(s, "sftp://"):
|
||||||
// parse the "sftp://user@host/path" url format
|
// parse the "sftp://user@host/path" url format
|
||||||
|
@ -37,7 +38,8 @@ func ParseConfig(s string) (interface{}, error) {
|
||||||
if url.User != nil {
|
if url.User != nil {
|
||||||
user = url.User.Username()
|
user = url.User.Username()
|
||||||
}
|
}
|
||||||
host = url.Host
|
host = url.Hostname()
|
||||||
|
port = url.Port()
|
||||||
dir = url.Path
|
dir = url.Path
|
||||||
if dir == "" {
|
if dir == "" {
|
||||||
return nil, errors.Errorf("invalid backend %q, no directory specified", s)
|
return nil, errors.Errorf("invalid backend %q, no directory specified", s)
|
||||||
|
@ -76,6 +78,7 @@ func ParseConfig(s string) (interface{}, error) {
|
||||||
return Config{
|
return Config{
|
||||||
User: user,
|
User: user,
|
||||||
Host: host,
|
Host: host,
|
||||||
|
Port: port,
|
||||||
Path: p,
|
Path: p,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,11 +23,11 @@ var configTests = []struct {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"sftp://host:10022//dir/subdir",
|
"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",
|
"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",
|
"sftp://user@host/dir/subdir/../other",
|
||||||
|
@ -38,6 +38,17 @@ var configTests = []struct {
|
||||||
Config{User: "user", Host: "host", Path: "dir/subdir"},
|
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
|
// second form, user specified sftp:user@host:/dir
|
||||||
{
|
{
|
||||||
"sftp:user@host:/dir/subdir",
|
"sftp:user@host:/dir/subdir",
|
||||||
|
|
|
@ -8,7 +8,6 @@ import (
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path"
|
"path"
|
||||||
"strings"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/restic/restic/internal/errors"
|
"github.com/restic/restic/internal/errors"
|
||||||
|
@ -190,10 +189,11 @@ func buildSSHCommand(cfg Config) (cmd string, args []string, err error) {
|
||||||
|
|
||||||
cmd = "ssh"
|
cmd = "ssh"
|
||||||
|
|
||||||
hostport := strings.Split(cfg.Host, ":")
|
host, port := cfg.Host, cfg.Port
|
||||||
args = []string{hostport[0]}
|
|
||||||
if len(hostport) > 1 {
|
args = []string{host}
|
||||||
args = append(args, "-p", hostport[1])
|
if port != "" {
|
||||||
|
args = append(args, "-p", port)
|
||||||
}
|
}
|
||||||
if cfg.User != "" {
|
if cfg.User != "" {
|
||||||
args = append(args, "-l")
|
args = append(args, "-l")
|
||||||
|
|
|
@ -21,23 +21,35 @@ var sshcmdTests = []struct {
|
||||||
[]string{"host", "-s", "sftp"},
|
[]string{"host", "-s", "sftp"},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Config{Host: "host:10022", Path: "/dir/subdir"},
|
Config{Host: "host", Port: "10022", Path: "/dir/subdir"},
|
||||||
"ssh",
|
"ssh",
|
||||||
[]string{"host", "-p", "10022", "-s", "sftp"},
|
[]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",
|
"ssh",
|
||||||
[]string{"host", "-p", "10022", "-l", "user", "-s", "sftp"},
|
[]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) {
|
func TestBuildSSHCommand(t *testing.T) {
|
||||||
for _, test := range sshcmdTests {
|
for i, test := range sshcmdTests {
|
||||||
t.Run("", func(t *testing.T) {
|
t.Run("", func(t *testing.T) {
|
||||||
cmd, args, err := buildSSHCommand(test.cfg)
|
cmd, args, err := buildSSHCommand(test.cfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatalf("%v in test %d", err, i)
|
||||||
}
|
}
|
||||||
|
|
||||||
if cmd != test.cmd {
|
if cmd != test.cmd {
|
||||||
|
@ -45,7 +57,8 @@ func TestBuildSSHCommand(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if !reflect.DeepEqual(test.args, args) {
|
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)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue