diff --git a/include/libssh/sftp.h b/include/libssh/sftp.h index ef069a37..a1aa783a 100644 --- a/include/libssh/sftp.h +++ b/include/libssh/sftp.h @@ -897,6 +897,19 @@ LIBSSH_API char *sftp_canonicalize_path(sftp_session sftp, const char *path); */ LIBSSH_API int sftp_server_version(sftp_session sftp); +/** + * @brief Canonicalize path using expand-path@openssh.com extension + * + * @param sftp The sftp session handle. + * + * @param path The path to be canonicalized. + * + * @return A pointer to the newly allocated canonicalized path, + * NULL on error. The caller needs to free the memory + * using ssh_string_free_char(). + */ +LIBSSH_API char *sftp_expand_path(sftp_session sftp, const char *path); + #ifdef WITH_SERVER /** * @brief Create a new sftp server session. diff --git a/src/sftp.c b/src/sftp.c index 14a07bca..83abf6f2 100644 --- a/src/sftp.c +++ b/src/sftp.c @@ -3744,4 +3744,98 @@ sftp_attributes sftp_fstat(sftp_file file) return NULL; } +char *sftp_expand_path(sftp_session sftp, const char *path) +{ + sftp_status_message status = NULL; + sftp_message msg = NULL; + ssh_buffer buffer = NULL; + uint32_t id; + int rc; + + if (sftp == NULL) { + return NULL; + } + + if (path == NULL) { + ssh_set_error(sftp->session, + SSH_FATAL, + "NULL received as an argument instead of the path to expand"); + sftp_set_error(sftp, SSH_FX_FAILURE); + return NULL; + } + + buffer = ssh_buffer_new(); + if (buffer == NULL) { + ssh_set_error_oom(sftp->session); + sftp_set_error(sftp, SSH_FX_FAILURE); + return NULL; + } + + id = sftp_get_new_id(sftp); + + rc = ssh_buffer_pack(buffer, + "dss", + id, + "expand-path@openssh.com", + path); + if (rc != SSH_OK) { + ssh_set_error_oom(sftp->session); + SSH_BUFFER_FREE(buffer); + sftp_set_error(sftp, SSH_FX_FAILURE); + return NULL; + } + + rc = sftp_packet_write(sftp, SSH_FXP_EXTENDED, buffer); + SSH_BUFFER_FREE(buffer); + if (rc < 0) { + return NULL; + } + + while (msg == NULL) { + rc = sftp_read_and_dispatch(sftp); + if (rc < 0) { + return NULL; + } + msg = sftp_dequeue(sftp, id); + } + + if (msg->packet_type == SSH_FXP_NAME) { + uint32_t ignored = 0; + char *cname = NULL; + + rc = ssh_buffer_unpack(msg->payload, + "ds", + &ignored, + &cname); + sftp_message_free(msg); + if (rc != SSH_OK) { + ssh_set_error(sftp->session, + SSH_ERROR, + "Failed to parse expanded path"); + sftp_set_error(sftp, SSH_FX_FAILURE); + return NULL; + } + + return cname; + } else if (msg->packet_type == SSH_FXP_STATUS) { + status = parse_status_msg(msg); + sftp_message_free(msg); + if (status == NULL) { + return NULL; + } + sftp_set_error(sftp, status->status); + ssh_set_error(sftp->session, SSH_REQUEST_DENIED, + "SFTP server: %s", status->errormsg); + status_msg_free(status); + } else { + ssh_set_error(sftp->session, SSH_FATAL, + "Received message %d when attempting to expand path", + msg->packet_type); + sftp_message_free(msg); + sftp_set_error(sftp, SSH_FX_BAD_MESSAGE); + } + + return NULL; +} + #endif /* WITH_SFTP */ diff --git a/tests/client/CMakeLists.txt b/tests/client/CMakeLists.txt index f977b35d..4c4b1d8e 100644 --- a/tests/client/CMakeLists.txt +++ b/tests/client/CMakeLists.txt @@ -53,6 +53,7 @@ if (WITH_SFTP) torture_sftp_hardlink torture_sftp_limits torture_sftp_rename + torture_sftp_expand_path ${SFTP_BENCHMARK_TESTS}) endif (WITH_SFTP) diff --git a/tests/client/torture_sftp_expand_path.c b/tests/client/torture_sftp_expand_path.c new file mode 100644 index 00000000..85ef0108 --- /dev/null +++ b/tests/client/torture_sftp_expand_path.c @@ -0,0 +1,125 @@ +#include "config.h" + +#define LIBSSH_STATIC + +#include "torture.h" +#include "sftp.c" + +#include +#include +#include + +static int sshd_setup(void **state) +{ + torture_setup_sshd_server(state, false); + + return 0; +} + +static int sshd_teardown(void **state) +{ + torture_teardown_sshd_server(state); + + return 0; +} + +static int session_setup(void **state) +{ + struct torture_state *s = *state; + struct passwd *pwd = NULL; + int rc; + + pwd = getpwnam("bob"); + assert_non_null(pwd); + + rc = setuid(pwd->pw_uid); + assert_return_code(rc, errno); + + s->ssh.session = torture_ssh_session(s, + TORTURE_SSH_SERVER, + NULL, + TORTURE_SSH_USER_ALICE, + NULL); + assert_non_null(s->ssh.session); + + s->ssh.tsftp = torture_sftp_session(s->ssh.session); + assert_non_null(s->ssh.tsftp); + + return 0; +} + +static int session_teardown(void **state) +{ + struct torture_state *s = *state; + + torture_rmdirs(s->ssh.tsftp->testdir); + torture_sftp_close(s->ssh.tsftp); + ssh_disconnect(s->ssh.session); + ssh_free(s->ssh.session); + + return 0; +} + +static void torture_sftp_expand_path(void **state) +{ + struct torture_state *s = *state; + struct torture_sftp *t = s->ssh.tsftp; + struct passwd *pwd = NULL; + char *expanded_path = NULL; + int rc; + + rc = sftp_extension_supported(t->sftp, "expand-path@openssh.com", "1"); + if (rc == 0) { + skip(); + } + + pwd = getpwnam(TORTURE_SSH_USER_ALICE); + assert_non_null(pwd); + + /* testing for a absolute path */ + expanded_path = sftp_expand_path(t->sftp, "~/."); + assert_non_null(expanded_path); + + assert_string_equal(expanded_path, pwd->pw_dir); + + SSH_STRING_FREE_CHAR(expanded_path); + + /* testing for a relative path */ + expanded_path = sftp_expand_path(t->sftp, "."); + assert_non_null(expanded_path); + + assert_string_equal(expanded_path, pwd->pw_dir); + + SSH_STRING_FREE_CHAR(expanded_path); + + /* passing a NULL sftp session */ + expanded_path = sftp_expand_path(NULL, "~/."); + assert_null(expanded_path); + + /* passing an invalid path */ + expanded_path = sftp_expand_path(t->sftp, "/...//"); + assert_null(expanded_path); + + /* passing null path */ + expanded_path = sftp_expand_path(t->sftp, NULL); + assert_null(expanded_path); +} + +int torture_run_tests(void) +{ + int rc; + struct CMUnitTest tests[] = { + cmocka_unit_test_setup_teardown(torture_sftp_expand_path, + session_setup, + session_teardown) + }; + + ssh_init(); + + torture_filter_tests(tests); + rc = cmocka_run_group_tests(tests, sshd_setup, sshd_teardown); + + ssh_finalize(); + + return rc; +}