diff --git a/include/libssh/sftp.h b/include/libssh/sftp.h index 4b4f8481..ef069a37 100644 --- a/include/libssh/sftp.h +++ b/include/libssh/sftp.h @@ -77,6 +77,7 @@ typedef struct sftp_request_queue_struct* sftp_request_queue; typedef struct sftp_session_struct* sftp_session; typedef struct sftp_status_message_struct* sftp_status_message; typedef struct sftp_statvfs_struct* sftp_statvfs_t; +typedef struct sftp_limits_struct* sftp_limits_t; struct sftp_session_struct { ssh_session session; @@ -200,6 +201,16 @@ struct sftp_statvfs_struct { uint64_t f_namemax; /** maximum filename length */ }; +/** + * @brief SFTP limits structure. + */ +struct sftp_limits_struct { + uint64_t max_packet_length; /** maximum number of bytes in a single sftp packet */ + uint64_t max_read_length; /** maximum length in a SSH_FXP_READ packet */ + uint64_t max_write_length; /** maximum length in a SSH_FXP_WRITE packet */ + uint64_t max_open_handles; /** maximum number of active handles allowed by server */ +}; + /** * @brief Creates a new sftp session. * @@ -846,6 +857,24 @@ LIBSSH_API void sftp_statvfs_free(sftp_statvfs_t statvfs_o); */ LIBSSH_API int sftp_fsync(sftp_file file); +/** + * @brief Get information about the various limits the server might impose. + * + * @param sftp The sftp session handle. + * + * @return A limits structure or NULL on error. + * + * @see sftp_get_error() + */ +LIBSSH_API sftp_limits_t sftp_limits(sftp_session sftp); + +/** + * @brief Free the memory of an allocated limits. + * + * @param limits The limits to free. + */ +LIBSSH_API void sftp_limits_free(sftp_limits_t limits); + /** * @brief Canonicalize a sftp path. * diff --git a/src/sftp.c b/src/sftp.c index c1f29449..14a07bca 100644 --- a/src/sftp.c +++ b/src/sftp.c @@ -3383,6 +3383,122 @@ void sftp_statvfs_free(sftp_statvfs_t statvfs) { SAFE_FREE(statvfs); } +static sftp_limits_t +sftp_parse_limits(sftp_session sftp, ssh_buffer buf) +{ + sftp_limits_t limits = NULL; + int rc; + + limits = calloc(1, sizeof(struct sftp_limits_struct)); + if (limits == NULL) { + ssh_set_error_oom(sftp->session); + sftp_set_error(sftp, SSH_FX_FAILURE); + return NULL; + } + + rc = ssh_buffer_unpack(buf, "qqqq", + &limits->max_packet_length, /** maximum number of bytes in a single sftp packet */ + &limits->max_read_length, /** maximum length in a SSH_FXP_READ packet */ + &limits->max_write_length, /** maximum length in a SSH_FXP_WRITE packet */ + &limits->max_open_handles /** maximum number of active handles allowed by server */ + ); + if (rc != SSH_OK) { + SAFE_FREE(limits); + ssh_set_error(sftp->session, SSH_FATAL, "Invalid limits structure"); + sftp_set_error(sftp, SSH_FX_FAILURE); + return NULL; + } + + return limits; +} + +sftp_limits_t +sftp_limits(sftp_session sftp) +{ + sftp_status_message status = NULL; + sftp_message msg = NULL; + ssh_buffer buffer; + uint32_t id; + int rc; + + if (sftp == NULL) + 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, + "ds", + id, + "limits@openssh.com"); + 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) { + if (sftp_read_and_dispatch(sftp) < 0) { + return NULL; + } + msg = sftp_dequeue(sftp, id); + } + + if (msg->packet_type == SSH_FXP_EXTENDED_REPLY) { + sftp_limits_t limits = sftp_parse_limits(sftp, msg->payload); + sftp_message_free(msg); + if (limits == NULL) { + return NULL; + } + + return limits; + } else if (msg->packet_type == SSH_FXP_STATUS) { /* bad response (error) */ + 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 { /* this shouldn't happen */ + ssh_set_error(sftp->session, + SSH_FATAL, + "Received message %d when attempting to get limits", + msg->packet_type); + sftp_message_free(msg); + sftp_set_error(sftp, SSH_FX_BAD_MESSAGE); + } + + return NULL; +} + +void +sftp_limits_free(sftp_limits_t limits) +{ + if (limits == NULL) { + return; + } + + SAFE_FREE(limits); +} + /* another code written by Nick */ char *sftp_canonicalize_path(sftp_session sftp, const char *path) { diff --git a/tests/client/CMakeLists.txt b/tests/client/CMakeLists.txt index 6bb1762a..f977b35d 100644 --- a/tests/client/CMakeLists.txt +++ b/tests/client/CMakeLists.txt @@ -51,6 +51,7 @@ if (WITH_SFTP) torture_sftp_read torture_sftp_fsync torture_sftp_hardlink + torture_sftp_limits torture_sftp_rename ${SFTP_BENCHMARK_TESTS}) endif (WITH_SFTP) diff --git a/tests/client/torture_sftp_limits.c b/tests/client/torture_sftp_limits.c new file mode 100644 index 00000000..f8b682f7 --- /dev/null +++ b/tests/client/torture_sftp_limits.c @@ -0,0 +1,97 @@ +#define LIBSSH_STATIC + +#include "config.h" + +#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_limits(void **state) +{ + struct torture_state *s = *state; + struct torture_sftp *t = s->ssh.tsftp; + sftp_limits_t li; + + if (!sftp_extension_supported(t->sftp, "limits@openssh.com", "1")) + skip(); + + li = sftp_limits(t->sftp); + assert_non_null(li); + + assert_int_not_equal(li->max_packet_length, 0); + assert_int_not_equal(li->max_read_length, 0); + assert_int_not_equal(li->max_write_length, 0); + assert_int_not_equal(li->max_open_handles, 0); + + sftp_limits_free(li); +} + +int torture_run_tests(void) +{ + int rc; + struct CMUnitTest tests[] = { + cmocka_unit_test_setup_teardown(torture_sftp_limits, + session_setup, + session_teardown) + }; + + ssh_init(); + + torture_filter_tests(tests); + rc = cmocka_run_group_tests(tests, sshd_setup, sshd_teardown); + ssh_finalize(); + + return rc; +}