ssh install refuses stray arguments before -- and a symlinked authorized_keys (closes #61)
check / check (push) Failing after 2s
check / check (push) Failing after 2s
Anything but the host before --, or a second argument when there is no --, used to reach sftp in front of the host; it is now refused before the mnemonic is read or any connection is made. That includes "install -- host", which names no host before --. The first listing of ~/.ssh is now a long one, ls -n, which the client formats itself whatever the server, so an authorized_keys that is a symlink shows as one. It is refused before any upload, since the rename would have replaced the link and left the file it points at without the key. The README says so. Model: opus-5-5
This commit is contained in:
@@ -49,6 +49,20 @@ var ErrCannotEnter = errors.New(
|
||||
"~/.ssh is there on the host but cannot be entered",
|
||||
)
|
||||
|
||||
// ErrSymlink is the refusal of a host whose authorized_keys is a
|
||||
// symlink: the rename that puts the new file in place would replace the
|
||||
// link itself, and the file it points at would never get the key.
|
||||
var ErrSymlink = errors.New(
|
||||
"~/.ssh/authorized_keys on the host is a symlink, which the tool " +
|
||||
"leaves alone",
|
||||
)
|
||||
|
||||
// ErrStrayArgument is the refusal of anything but the host before --,
|
||||
// which would otherwise be handed to sftp in front of the host.
|
||||
var ErrStrayArgument = errors.New(
|
||||
"only the host goes before --; options for sftp go after --",
|
||||
)
|
||||
|
||||
// install returns the command that adds the public key to a host.
|
||||
func install() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
@@ -60,7 +74,22 @@ func install() *cobra.Command {
|
||||
"beside it which is then renamed over it. Nothing is run " +
|
||||
"on the host. Anything after -- is given to sftp " +
|
||||
"unchanged, which is where the port goes (-P).",
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
Args: cobra.MatchAll(
|
||||
cobra.MinimumNArgs(1),
|
||||
func(cmd *cobra.Command, args []string) error {
|
||||
// ArgsLenAtDash is -1 when there is no --.
|
||||
before := cmd.ArgsLenAtDash()
|
||||
if before == -1 {
|
||||
before = len(args)
|
||||
}
|
||||
|
||||
if before != 1 {
|
||||
return ErrStrayArgument
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
key, comment, err := derived(cmd)
|
||||
if err != nil {
|
||||
@@ -256,14 +285,23 @@ func merge(content, line string) (string, bool) {
|
||||
// it can be looked up, not even ".". The first listing of such a
|
||||
// directory comes up empty, as the server leaves out every name it
|
||||
// cannot look up.
|
||||
//
|
||||
// The first listing is a long one, which shows an authorized_keys that
|
||||
// is a symlink as one. That is refused before anything else sftp said
|
||||
// is read, so a link the get could not follow is refused in the same
|
||||
// words.
|
||||
func fetch(
|
||||
cmd *cobra.Command, host string, options []string, into string,
|
||||
) (string, bool, error) {
|
||||
said, err := session(cmd, host, options, []string{
|
||||
"ls -1 " + directory,
|
||||
"ls -n " + directory,
|
||||
"ls -1 " + directory + "/.",
|
||||
"get " + authorized + " " + quoted(into),
|
||||
})
|
||||
if symlinked(said) {
|
||||
return "", false, ErrSymlink
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
if listingNotFound(said, directory) {
|
||||
return "", false, nil
|
||||
@@ -326,6 +364,22 @@ func reportedCannotList(line string) (string, bool) {
|
||||
return strings.TrimSuffix(strings.TrimPrefix(line, before), after), true
|
||||
}
|
||||
|
||||
// symlinked says whether the long listing of .ssh shows authorized_keys
|
||||
// as a symlink. With -n the client writes each line itself, as ls -l
|
||||
// does, whatever the server: the type comes first, "l" for a symlink,
|
||||
// and the path as the listing named it comes last.
|
||||
func symlinked(said string) bool {
|
||||
for line := range strings.Lines(said) {
|
||||
fields := strings.Fields(line)
|
||||
if len(fields) > 0 && strings.HasPrefix(fields[0], "l") &&
|
||||
fields[len(fields)-1] == authorized {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// absent says whether sftp reported the file that was asked for as
|
||||
// not being there, which is the one failure of the fetch that is read
|
||||
// as an empty authorized_keys. The reading is taken only from the
|
||||
|
||||
@@ -10,7 +10,7 @@ import "testing"
|
||||
const (
|
||||
echoed = `sftp> get .ssh/authorized_keys "/tmp/keyfunc/authorized_keys"
|
||||
`
|
||||
listed = "sftp> ls -1 .ssh\n"
|
||||
listed = "sftp> ls -n .ssh\n"
|
||||
warning = `Warning: Identity file /gone not accessible: ` +
|
||||
"No such file or directory.\n"
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user