From d157f13b27712fa9730a40b80a0e959f27bb71ee Mon Sep 17 00:00:00 2001 From: Nuhiat-Arefin Date: Tue, 14 Apr 2026 20:06:23 +0600 Subject: [PATCH] config: support ConnectTimeout time values Signed-off-by: Nuhiat-Arefin Reviewed-by: Jakub Jelen Merge-Request: --- src/config.c | 87 +++++++++++++++++++++++++++++--- tests/unittests/torture_config.c | 70 +++++++++++++++++++++++++ 2 files changed, 150 insertions(+), 7 deletions(-) diff --git a/src/config.c b/src/config.c index 5bcada0b..84681bd3 100644 --- a/src/config.c +++ b/src/config.c @@ -894,6 +894,69 @@ ssh_config_get_auth_option(enum ssh_config_opcode_e opcode) return -1; } +static long ssh_config_convtime(const char *p, long notfound) +{ + char *endp = NULL; + long total = 0; + long value; + long multiplier; + + if (p == NULL || *p == '\0') { + return notfound; + } + + while (*p != '\0') { + errno = 0; + value = strtol(p, &endp, 10); + if (p == endp || errno != 0 || value < 0) { + return notfound; + } + + switch (*endp) { + case '\0': + case 's': + case 'S': + multiplier = 1; + break; + case 'm': + case 'M': + multiplier = 60; + break; + case 'h': + case 'H': + multiplier = 60 * 60; + break; + case 'd': + case 'D': + multiplier = 24 * 60 * 60; + break; + case 'w': + case 'W': + multiplier = 7 * 24 * 60 * 60; + break; + default: + return notfound; + } + + /* + * OpenSSH accepts ConnectTimeout values only up to INT_MAX + * seconds: see openssh-portable/misc.c:convtime(). + */ + if (value > (INT_MAX - total) / multiplier) { + return notfound; + } + total += value * multiplier; + + if (*endp == '\0') { + p = endp; + } else { + p = endp + 1; + } + } + + return total; +} + #define CHECK_COND_OR_FAIL(cond, error_message) \ if ((cond)) { \ SSH_LOG(SSH_LOG_DEBUG, \ @@ -930,7 +993,7 @@ static int ssh_config_parse_line_internal(ssh_session session, char *keyword = NULL; char *lowerhost = NULL; size_t len; - int i, rv; + int i, rv, cmp; uint8_t *seen = session->opts.options_seen; long l; int64_t ll; @@ -1418,12 +1481,22 @@ static int ssh_config_parse_line_internal(ssh_session session, } break; case SOC_TIMEOUT: - l = ssh_config_get_long(&s, -1); - CHECK_COND_OR_FAIL(l < 0, "Invalid argument"); - if (*parsing) { - ssh_options_set(session, SSH_OPTIONS_TIMEOUT, &l); - } - break; + p = ssh_config_get_str_tok(&s, NULL); + CHECK_COND_OR_FAIL(p == NULL, "Missing argument"); + cmp = strcmp(p, "none"); + if (cmp == 0) { + if (*parsing) { + session->opts.timeout = (unsigned long)SSH_TIMEOUT_INFINITE; + session->opts.timeout_usec = 0; + } + break; + } + l = ssh_config_convtime(p, -1); + CHECK_COND_OR_FAIL(l < 0, "Invalid argument"); + if (*parsing) { + ssh_options_set(session, SSH_OPTIONS_TIMEOUT, &l); + } + break; case SOC_STRICTHOSTKEYCHECK: i = ssh_config_get_yesno(&s, -1); CHECK_COND_OR_FAIL(i < 0, "Invalid argument"); diff --git a/tests/unittests/torture_config.c b/tests/unittests/torture_config.c index 71c11b88..09af7a46 100644 --- a/tests/unittests/torture_config.c +++ b/tests/unittests/torture_config.c @@ -1,5 +1,7 @@ #include "config.h" +#include + #define LIBSSH_STATIC #ifndef _WIN32 @@ -58,6 +60,7 @@ extern LIBSSH_THREAD int ssh_log_level; #define LIBSSH_TESTCONFIG_LOGLEVEL_MISSING "libssh_test_loglevel_missing.tmp" #define LIBSSH_TESTCONFIG_JUMP "libssh_test_jump.tmp" #define LIBSSH_TESTCONFIG_NUMERIC_INVALID "libssh_test_numeric_invalid.tmp" +#define LIBSSH_TESTCONFIG_TIMEOUT_SUFFIX "libssh_test_timeout_suffix.tmp" #define LIBSSH_TESTCONFIG_STRING1 \ "User "USERNAME"\nInclude "LIBSSH_TESTCONFIG2"\n\n" @@ -344,6 +347,8 @@ static int setup_config_files(void **state) unlink(LIBSSH_TESTCONFIG_MATCH_COMPLEX); unlink(LIBSSH_TESTCONFIG_LOGLEVEL_MISSING); unlink(LIBSSH_TESTCONFIG_JUMP); + unlink(LIBSSH_TESTCONFIG_NUMERIC_INVALID); + unlink(LIBSSH_TESTCONFIG_TIMEOUT_SUFFIX); torture_write_file(LIBSSH_TESTCONFIG1, LIBSSH_TESTCONFIG_STRING1); @@ -455,6 +460,8 @@ static int teardown_config_files(void **state) unlink(LIBSSH_TESTCONFIG_MATCH_COMPLEX); unlink(LIBSSH_TESTCONFIG_LOGLEVEL_MISSING); unlink(LIBSSH_TESTCONFIG_JUMP); + unlink(LIBSSH_TESTCONFIG_NUMERIC_INVALID); + unlink(LIBSSH_TESTCONFIG_TIMEOUT_SUFFIX); return 0; } @@ -860,6 +867,63 @@ static void torture_config_numeric_invalid_string(void **state) torture_config_numeric_invalid(state, NULL); } +static void torture_config_timeout_suffix(void **state, const char *file) +{ + ssh_session session = *state; + struct timeout_case { + const char *host; + const char *value; + long expected; + bool infinite; + } cases[] = { + {"seconds", "30s", 30, false}, + {"seconds_upper", "30S", 30, false}, + {"minutes", "1m", 60, false}, + {"minutes_upper", "1M", 60, false}, + {"days", "30d", 30L * 24 * 60 * 60, false}, + {"days_upper", "1D", 24L * 60 * 60, false}, + {"weeks_upper", "1W", 7L * 24 * 60 * 60, false}, + {"int_max", "3550w5d3h14m7s", INT_MAX, false}, + {"repeat_s", "30s30s", 60, false}, + {"repeat_h", "1h1h", 7200, false}, + {"compound", "1h30m", 5400, false}, + {"compound_upper", "1H30M", 5400, false}, + {"none", "none", 0, true}, + }; + char config[256]; + size_t i; + + for (i = 0; i < ARRAY_SIZE(cases); i++) { + torture_reset_config(session); + ssh_options_set(session, SSH_OPTIONS_HOST, cases[i].host); + snprintf(config, + sizeof(config), + "Host %s\n\tConnectTimeout %s\n", + cases[i].host, + cases[i].value); + if (file != NULL) { + torture_write_file(file, config); + } + _parse_config(session, file, file != NULL ? NULL : config, SSH_OK); + if (cases[i].infinite) { + assert_int_equal(session->opts.timeout, + (unsigned long)SSH_TIMEOUT_INFINITE); + assert_int_equal(session->opts.timeout_usec, 0); + } else { + assert_int_equal(session->opts.timeout, cases[i].expected); + } + } +} + +static void torture_config_timeout_suffix_file(void **state) +{ + torture_config_timeout_suffix(state, LIBSSH_TESTCONFIG_TIMEOUT_SUFFIX); +} + +static void torture_config_timeout_suffix_string(void **state) +{ + torture_config_timeout_suffix(state, NULL); +} /** * @brief Helper for checking hostname, username and port of ssh_jump_info_struct */ @@ -3716,6 +3780,12 @@ int torture_run_tests(void) cmocka_unit_test_setup_teardown(torture_config_numeric_invalid_string, setup, teardown), + cmocka_unit_test_setup_teardown(torture_config_timeout_suffix_file, + setup, + teardown), + cmocka_unit_test_setup_teardown(torture_config_timeout_suffix_string, + setup, + teardown), cmocka_unit_test_setup_teardown(torture_config_unknown_file, setup, teardown),