package sftp import ( "io" "os" "regexp" "sync" "syscall" "testing" "time" "github.com/pkg/errors" "github.com/stretchr/testify/assert" ) const ( typeDirectory = "d" typeFile = "[^d]" ) func TestRunLsWithExamplesDirectory(t *testing.T) { path := "examples" item, _ := os.Stat(path) result := runLs(path, item) runLsTestHelper(t, result, typeDirectory, path) } func TestRunLsWithLicensesFile(t *testing.T) { path := "LICENSE" item, _ := os.Stat(path) result := runLs(path, item) runLsTestHelper(t, result, typeFile, path) } /* The format of the `longname' field is unspecified by this protocol. It MUST be suitable for use in the output of a directory listing command (in fact, the recommended operation for a directory listing command is to simply display this data). However, clients SHOULD NOT attempt to parse the longname field for file attributes; they SHOULD use the attrs field instead. The recommended format for the longname field is as follows: -rwxr-xr-x 1 mjos staff 348911 Mar 25 14:29 t-filexfer 1234567890 123 12345678 12345678 12345678 123456789012 Here, the first line is sample output, and the second field indicates widths of the various fields. Fields are separated by spaces. The first field lists file permissions for user, group, and others; the second field is link count; the third field is the name of the user who owns the file; the fourth field is the name of the group that owns the file; the fifth field is the size of the file in bytes; the sixth field (which actually may contain spaces, but is fixed to 12 characters) is the file modification time, and the seventh field is the file name. Each field is specified to be a minimum of certain number of character positions (indicated by the second line above), but may also be longer if the data does not fit in the specified length. The SSH_FXP_ATTRS response has the following format: uint32 id ATTRS attrs where `id' is the request identifier, and `attrs' is the returned file attributes as described in Section ``File Attributes''. */ func runLsTestHelper(t *testing.T, result, expectedType, path string) { // using regular expressions to make tests work on all systems // a virtual file system (like afero) would be needed to mock valid filesystem checks // expected layout is: // drwxr-xr-x 8 501 20 272 Aug 9 19:46 examples // permissions (len 10, "drwxr-xr-x") got := result[0:10] if ok, err := regexp.MatchString("^"+expectedType+"[rwx-]{9}$", got); !ok { t.Errorf("runLs(%#v, *FileInfo): permission field mismatch, expected dir, got: %#v, err: %#v", path, got, err) } // space got = result[10:11] if ok, err := regexp.MatchString("^\\s$", got); !ok { t.Errorf("runLs(%#v, *FileInfo): spacer 1 mismatch, expected whitespace, got: %#v, err: %#v", path, got, err) } // link count (len 3, number) got = result[12:15] if ok, err := regexp.MatchString("^\\s*[0-9]+$", got); !ok { t.Errorf("runLs(%#v, *FileInfo): link count field mismatch, got: %#v, err: %#v", path, got, err) } // spacer got = result[15:16] if ok, err := regexp.MatchString("^\\s$", got); !ok { t.Errorf("runLs(%#v, *FileInfo): spacer 2 mismatch, expected whitespace, got: %#v, err: %#v", path, got, err) } // username / uid (len 8, number or string) got = result[16:24] if ok, err := regexp.MatchString("^[^\\s]{1,8}\\s*$", got); !ok { t.Errorf("runLs(%#v, *FileInfo): username / uid mismatch, expected user, got: %#v, err: %#v", path, got, err) } // spacer got = result[24:25] if ok, err := regexp.MatchString("^\\s$", got); !ok { t.Errorf("runLs(%#v, *FileInfo): spacer 3 mismatch, expected whitespace, got: %#v, err: %#v", path, got, err) } // groupname / gid (len 8, number or string) got = result[25:33] if ok, err := regexp.MatchString("^[^\\s]{1,8}\\s*$", got); !ok { t.Errorf("runLs(%#v, *FileInfo): groupname / gid mismatch, expected group, got: %#v, err: %#v", path, got, err) } // spacer got = result[33:34] if ok, err := regexp.MatchString("^\\s$", got); !ok { t.Errorf("runLs(%#v, *FileInfo): spacer 4 mismatch, expected whitespace, got: %#v, err: %#v", path, got, err) } // filesize (len 8) got = result[34:42] if ok, err := regexp.MatchString("^\\s*[0-9]+$", got); !ok { t.Errorf("runLs(%#v, *FileInfo): filesize field mismatch, expected size in bytes, got: %#v, err: %#v", path, got, err) } // spacer got = result[42:43] if ok, err := regexp.MatchString("^\\s$", got); !ok { t.Errorf("runLs(%#v, *FileInfo): spacer 5 mismatch, expected whitespace, got: %#v, err: %#v", path, got, err) } // mod time (len 12, e.g. Aug 9 19:46) got = result[43:55] layout := "Jan 2 15:04" _, err := time.Parse(layout, got) if err != nil { layout = "Jan 2 2006" _, err = time.Parse(layout, got) } if err != nil { t.Errorf("runLs(%#v, *FileInfo): mod time field mismatch, expected date layout %s, got: %#v, err: %#v", path, layout, got, err) } // spacer got = result[55:56] if ok, err := regexp.MatchString("^\\s$", got); !ok { t.Errorf("runLs(%#v, *FileInfo): spacer 6 mismatch, expected whitespace, got: %#v, err: %#v", path, got, err) } // filename got = result[56:] if ok, err := regexp.MatchString("^"+path+"$", got); !ok { t.Errorf("runLs(%#v, *FileInfo): name field mismatch, expected examples, got: %#v, err: %#v", path, got, err) } } func clientServerPair(t *testing.T) (*Client, *Server) { cr, sw := io.Pipe() sr, cw := io.Pipe() server, err := NewServer(struct { io.Reader io.WriteCloser }{sr, sw}) if err != nil { t.Fatal(err) } go server.Serve() client, err := NewClientPipe(cr, cw) if err != nil { t.Fatalf("%+v\n", err) } return client, server } type sshFxpTestBadExtendedPacket struct { ID uint32 Extension string Data string } func (p sshFxpTestBadExtendedPacket) id() uint32 { return p.ID } func (p sshFxpTestBadExtendedPacket) MarshalBinary() ([]byte, error) { l := 1 + 4 + 4 + // type(byte) + uint32 + uint32 len(p.Extension) + len(p.Data) b := make([]byte, 0, l) b = append(b, ssh_FXP_EXTENDED) b = marshalUint32(b, p.ID) b = marshalString(b, p.Extension) b = marshalString(b, p.Data) return b, nil } // test that errors are sent back when we request an invalid extended packet operation func TestInvalidExtendedPacket(t *testing.T) { client, server := clientServerPair(t) defer client.Close() defer server.Close() badPacket := sshFxpTestBadExtendedPacket{client.nextID(), "thisDoesn'tExist", "foobar"} _, _, err := client.clientConn.sendPacket(badPacket) if err == nil { t.Fatal("expected error from bad packet") } // try to stat a file; the client should have shut down. filePath := "/etc/passwd" _, err = client.Stat(filePath) if err == nil { t.Fatal("expected error from closed connection") } } // test that server handles concurrent requests correctly func TestConcurrentRequests(t *testing.T) { client, server := clientServerPair(t) defer client.Close() defer server.Close() concurrency := 2 var wg sync.WaitGroup wg.Add(concurrency) for i := 0; i < concurrency; i++ { go func() { defer wg.Done() for j := 0; j < 1024; j++ { f, err := client.Open("/etc/passwd") if err != nil { t.Errorf("failed to open file: %v", err) } if err := f.Close(); err != nil { t.Errorf("failed t close file: %v", err) } } }() } wg.Wait() } // Test error conversion func TestStatusFromError(t *testing.T) { type test struct { err error pkt sshFxpStatusPacket } tpkt := func(id, code uint32) sshFxpStatusPacket { return sshFxpStatusPacket{ ID: id, StatusError: StatusError{Code: code}, } } test_cases := []test{ test{syscall.ENOENT, tpkt(1, ssh_FX_NO_SUCH_FILE)}, test{&os.PathError{Err: syscall.ENOENT}, tpkt(2, ssh_FX_NO_SUCH_FILE)}, test{&os.PathError{Err: errors.New("foo")}, tpkt(3, ssh_FX_FAILURE)}, test{ErrSshFxEof, tpkt(4, ssh_FX_EOF)}, test{ErrSshFxOpUnsupported, tpkt(5, ssh_FX_OP_UNSUPPORTED)}, test{io.EOF, tpkt(6, ssh_FX_EOF)}, test{os.ErrNotExist, tpkt(7, ssh_FX_NO_SUCH_FILE)}, } for _, tc := range test_cases { tc.pkt.StatusError.msg = tc.err.Error() assert.Equal(t, tc.pkt, statusFromError(tc.pkt, tc.err)) } }