mirror of
https://git.libssh.org/projects/libssh.git
synced 2026-06-11 12:56:21 +09:00
config: add %n expansion and defer HostName normalization
Add %n support to path expansion. Defer HostName handling so the expanded value is computed even when the final host cannot be applied yet. HostName specific expansion lowercases literal hostname text and %h expansions, while unsupported HostName %X tokens are preserved literally and normal host validation still applies when the result is passed to SSH_OPTIONS_HOST. This keeps the expansion logic correct now and leaves room for future HostName token support without changing the deferred path again. Signed-off-by: Nuhiat-Arefin <nuhiatarefin@gmail.com> Reviewed-by: Jakub Jelen <jjelen@redhat.com> Merge-Request: <https://gitlab.com/libssh/libssh-mirror/-/merge_requests/811>
This commit is contained in:
committed by
Jakub Jelen
parent
77ef6379a5
commit
fc9963d29e
@@ -53,7 +53,10 @@ int ssh_file_readaccess_ok(const char *file);
|
||||
int ssh_dir_writeable(const char *path);
|
||||
|
||||
char *ssh_path_expand_tilde(const char *d);
|
||||
/* Expand a string using session option values. */
|
||||
char *ssh_path_expand_escape(ssh_session session, const char *s);
|
||||
/* Expand HostName tokens. */
|
||||
char *ssh_path_expand_hostname(ssh_session session, const char *s);
|
||||
int ssh_analyze_banner(ssh_session session, int server);
|
||||
int ssh_is_ipaddr_v4(const char *str);
|
||||
int ssh_is_ipaddr(const char *str);
|
||||
|
||||
@@ -290,6 +290,7 @@ struct ssh_session_struct {
|
||||
int address_family;
|
||||
bool batch_mode;
|
||||
char *originalhost; /* user-supplied host for config matching */
|
||||
char *config_hostname; /* normalized HostName pattern, applied later */
|
||||
bool config_hostname_only; /* config hostname path: update host only,
|
||||
not originalhost */
|
||||
char *tag; /* configuration tag for Match tagged */
|
||||
|
||||
142
src/config.c
142
src/config.c
@@ -534,6 +534,91 @@ ssh_match_exec(ssh_session session, const char *command, bool negate)
|
||||
}
|
||||
#endif /* WITH_EXEC */
|
||||
|
||||
/*
|
||||
* HostName recognizes %% and %h during config parsing. Other %X sequences are
|
||||
* left for deferred HostName expansion, and only a trailing bare '%' is
|
||||
* rejected here.
|
||||
*/
|
||||
static int ssh_config_scan_hostname_tokens(ssh_session session,
|
||||
const char *hostname,
|
||||
bool *needs_host,
|
||||
bool *has_unknown)
|
||||
{
|
||||
const char *p = NULL;
|
||||
|
||||
if (needs_host != NULL) {
|
||||
*needs_host = false;
|
||||
}
|
||||
if (has_unknown != NULL) {
|
||||
*has_unknown = false;
|
||||
}
|
||||
|
||||
if (hostname == NULL) {
|
||||
ssh_set_error(session,
|
||||
SSH_FATAL,
|
||||
"Cannot scan HostName tokens from NULL input");
|
||||
return -1;
|
||||
}
|
||||
|
||||
for (p = hostname; *p != '\0'; p++) {
|
||||
if (*p == '%') {
|
||||
if (p[1] == '\0') {
|
||||
ssh_set_error(session, SSH_FATAL, "Incomplete Hostname token");
|
||||
return -1;
|
||||
}
|
||||
switch (p[1]) {
|
||||
case '%':
|
||||
p++;
|
||||
continue;
|
||||
case 'h':
|
||||
if (needs_host != NULL) {
|
||||
*needs_host = true;
|
||||
}
|
||||
p++;
|
||||
continue;
|
||||
default:
|
||||
if (has_unknown != NULL) {
|
||||
*has_unknown = true;
|
||||
}
|
||||
p++;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static char *ssh_config_lowercase_hostname_pattern(const char *hostname)
|
||||
{
|
||||
char *pattern = NULL;
|
||||
char *p = NULL;
|
||||
bool escape = false;
|
||||
|
||||
if (hostname == NULL) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
pattern = strdup(hostname);
|
||||
if (pattern == NULL) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
for (p = pattern; *p != '\0'; p++) {
|
||||
if (escape) {
|
||||
escape = false;
|
||||
continue;
|
||||
}
|
||||
if (*p == '%') {
|
||||
escape = true;
|
||||
continue;
|
||||
}
|
||||
*p = tolower((unsigned char)*p);
|
||||
}
|
||||
|
||||
return pattern;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief: Parse the ProxyJump configuration line and if parsing,
|
||||
* stores the result in the configuration option
|
||||
@@ -1409,25 +1494,60 @@ static int ssh_config_parse_line_internal(ssh_session session,
|
||||
p = ssh_config_get_str_tok(&s, NULL);
|
||||
CHECK_COND_OR_FAIL(p == NULL, "Missing argument");
|
||||
if (*parsing) {
|
||||
char *z = NULL;
|
||||
char *lower = NULL;
|
||||
z = ssh_path_expand_escape(session, p);
|
||||
if (z == NULL) {
|
||||
z = strdup(p);
|
||||
int rc;
|
||||
bool had_expansion = strchr(p, '%') != NULL;
|
||||
bool needs_host = false;
|
||||
bool has_unknown = false;
|
||||
rc = ssh_config_scan_hostname_tokens(session,
|
||||
p,
|
||||
&needs_host,
|
||||
&has_unknown);
|
||||
if (rc < 0) {
|
||||
SAFE_FREE(x);
|
||||
return -1;
|
||||
}
|
||||
if (z != NULL) {
|
||||
/* Always lowercase hostname */
|
||||
lower = ssh_lowercase(z);
|
||||
if (had_expansion) {
|
||||
if (!has_unknown &&
|
||||
(!needs_host || session->opts.host != NULL ||
|
||||
session->opts.originalhost != NULL)) {
|
||||
char *expanded = ssh_path_expand_hostname(session, p);
|
||||
if (expanded == NULL) {
|
||||
SAFE_FREE(x);
|
||||
return -1;
|
||||
}
|
||||
session->opts.config_hostname_only = true;
|
||||
rv = ssh_options_set(session, SSH_OPTIONS_HOST, expanded);
|
||||
session->opts.config_hostname_only = false;
|
||||
free(expanded);
|
||||
if (rv != SSH_OK) {
|
||||
/* Expanded HostName values remain fatal if host
|
||||
* validation rejects the resulting hostname.
|
||||
*/
|
||||
SAFE_FREE(x);
|
||||
return -1;
|
||||
}
|
||||
} else {
|
||||
char *hostname_pattern =
|
||||
ssh_config_lowercase_hostname_pattern(p);
|
||||
if (hostname_pattern == NULL) {
|
||||
ssh_set_error_oom(session);
|
||||
SAFE_FREE(x);
|
||||
return -1;
|
||||
}
|
||||
SAFE_FREE(session->opts.config_hostname);
|
||||
session->opts.config_hostname = hostname_pattern;
|
||||
}
|
||||
} else {
|
||||
char *lower = ssh_lowercase(p);
|
||||
if (lower == NULL) {
|
||||
SAFE_FREE(z);
|
||||
ssh_set_error_oom(session);
|
||||
SAFE_FREE(x);
|
||||
return -1;
|
||||
}
|
||||
session->opts.config_hostname_only = true;
|
||||
ssh_options_set(session, SSH_OPTIONS_HOST, lower);
|
||||
free(lower);
|
||||
session->opts.config_hostname_only = false;
|
||||
free(z);
|
||||
free(lower);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
131
src/misc.c
131
src/misc.c
@@ -1469,26 +1469,12 @@ err:
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/** @internal
|
||||
* @brief expands a string in function of session options
|
||||
*
|
||||
* @param[in] session The SSH session providing option values for expansion.
|
||||
* @param[in] s Format string to expand. Known parameters:
|
||||
* - %d user home directory (~)
|
||||
* - %h target host name
|
||||
* - %u local username
|
||||
* - %l local hostname
|
||||
* - %r remote username
|
||||
* - %p remote port
|
||||
* - %j proxyjump string
|
||||
* - %C Hash of %l%h%p%r%j
|
||||
*
|
||||
* @returns Expanded string. The caller needs to free the memory using
|
||||
* ssh_string_free_char().
|
||||
*
|
||||
* @see ssh_string_free_char()
|
||||
/* Internal expansion helper. hostname_lenient preserves unknown %X tokens
|
||||
* literally for HostName handling.
|
||||
*/
|
||||
char *ssh_path_expand_escape(ssh_session session, const char *s)
|
||||
static char *ssh_path_expand_internal(ssh_session session,
|
||||
const char *s,
|
||||
bool hostname_lenient)
|
||||
{
|
||||
char *buf = NULL;
|
||||
char *r = NULL;
|
||||
@@ -1520,8 +1506,7 @@ char *ssh_path_expand_escape(ssh_session session, const char *s)
|
||||
|
||||
for (i = 0; *p != '\0'; p++) {
|
||||
if (*p != '%') {
|
||||
escape:
|
||||
buf[i] = *p;
|
||||
buf[i] = hostname_lenient ? tolower((unsigned char)*p) : *p;
|
||||
i++;
|
||||
if (i >= MAX_BUF_SIZE) {
|
||||
free(buf);
|
||||
@@ -1534,12 +1519,51 @@ char *ssh_path_expand_escape(ssh_session session, const char *s)
|
||||
|
||||
p++;
|
||||
if (*p == '\0') {
|
||||
/* HostName expansion rejects trailing '%' to match the parse-time
|
||||
* scan. Keep the general expansion path unchanged, where a
|
||||
* trailing '%' is truncated.
|
||||
*/
|
||||
if (hostname_lenient) {
|
||||
ssh_set_error(session, SSH_FATAL, "Incomplete Hostname token");
|
||||
free(buf);
|
||||
free(r);
|
||||
return NULL;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (hostname_lenient && *p != '%' && *p != 'h') {
|
||||
buf[i] = '%';
|
||||
i++;
|
||||
if (i >= MAX_BUF_SIZE) {
|
||||
ssh_set_error(session, SSH_FATAL, "String too long");
|
||||
free(buf);
|
||||
free(r);
|
||||
return NULL;
|
||||
}
|
||||
buf[i] = *p;
|
||||
i++;
|
||||
if (i >= MAX_BUF_SIZE) {
|
||||
ssh_set_error(session, SSH_FATAL, "String too long");
|
||||
free(buf);
|
||||
free(r);
|
||||
return NULL;
|
||||
}
|
||||
buf[i] = '\0';
|
||||
continue;
|
||||
}
|
||||
|
||||
switch (*p) {
|
||||
case '%':
|
||||
goto escape;
|
||||
buf[i] = '%';
|
||||
i++;
|
||||
if (i >= MAX_BUF_SIZE) {
|
||||
free(buf);
|
||||
free(r);
|
||||
return NULL;
|
||||
}
|
||||
buf[i] = '\0';
|
||||
continue;
|
||||
case 'd':
|
||||
x = ssh_get_user_home_dir(session);
|
||||
if (x == NULL) {
|
||||
@@ -1557,9 +1581,11 @@ char *ssh_path_expand_escape(ssh_session session, const char *s)
|
||||
break;
|
||||
case 'h':
|
||||
if (session->opts.host) {
|
||||
x = strdup(session->opts.host);
|
||||
x = hostname_lenient ? ssh_lowercase(session->opts.host)
|
||||
: strdup(session->opts.host);
|
||||
} else if (session->opts.originalhost) {
|
||||
x = strdup(session->opts.originalhost);
|
||||
x = hostname_lenient ? ssh_lowercase(session->opts.originalhost)
|
||||
: strdup(session->opts.originalhost);
|
||||
} else {
|
||||
ssh_set_error(session, SSH_FATAL, "Cannot expand host");
|
||||
free(buf);
|
||||
@@ -1567,6 +1593,18 @@ char *ssh_path_expand_escape(ssh_session session, const char *s)
|
||||
return NULL;
|
||||
}
|
||||
break;
|
||||
case 'n':
|
||||
if (session->opts.originalhost) {
|
||||
x = strdup(session->opts.originalhost);
|
||||
} else {
|
||||
ssh_set_error(session,
|
||||
SSH_FATAL,
|
||||
"Cannot expand original host");
|
||||
free(buf);
|
||||
free(r);
|
||||
return NULL;
|
||||
}
|
||||
break;
|
||||
case 'r':
|
||||
if (session->opts.username) {
|
||||
x = strdup(session->opts.username);
|
||||
@@ -1635,6 +1673,51 @@ char *ssh_path_expand_escape(ssh_session session, const char *s)
|
||||
return x;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Expand a string using session option values.
|
||||
*
|
||||
* @param[in] session The SSH session providing option values for expansion.
|
||||
* @param[in] s Format string to expand.
|
||||
*
|
||||
* Supported tokens:
|
||||
* - %d user home directory (~)
|
||||
* - %h target host name
|
||||
* - %u local username
|
||||
* - %l local hostname
|
||||
* - %r remote username
|
||||
* - %p remote port
|
||||
* - %j proxyjump string
|
||||
* - %C Hash of %l%h%p%r%j
|
||||
* - %n original target host name
|
||||
*
|
||||
* @returns Expanded string. The caller needs to free the memory using
|
||||
* ssh_string_free_char().
|
||||
*
|
||||
* @see ssh_string_free_char()
|
||||
*/
|
||||
char *ssh_path_expand_escape(ssh_session session, const char *s)
|
||||
{
|
||||
return ssh_path_expand_internal(session, s, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Expand HostName tokens.
|
||||
*
|
||||
* @param[in] session The SSH session providing option values for expansion.
|
||||
* @param[in] s HostName pattern to expand.
|
||||
*
|
||||
* Like ssh_path_expand_escape(), but only interprets %% and %h. Other %X
|
||||
* tokens are preserved literally. Literal hostname text and %h expansions are
|
||||
* normalized to lowercase.
|
||||
*
|
||||
* @returns Expanded string. The caller needs to free the memory using
|
||||
* ssh_string_free_char().
|
||||
*/
|
||||
char *ssh_path_expand_hostname(ssh_session session, const char *s)
|
||||
{
|
||||
return ssh_path_expand_internal(session, s, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
|
||||
@@ -124,6 +124,14 @@ int ssh_options_copy(ssh_session src, ssh_session *dest)
|
||||
}
|
||||
}
|
||||
|
||||
if (src->opts.config_hostname != NULL) {
|
||||
new->opts.config_hostname = strdup(src->opts.config_hostname);
|
||||
if (new->opts.config_hostname == NULL) {
|
||||
ssh_free(new);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
if (src->opts.bindaddr != NULL) {
|
||||
new->opts.bindaddr = strdup(src->opts.bindaddr);
|
||||
if (new->opts.bindaddr == NULL) {
|
||||
@@ -782,6 +790,7 @@ int ssh_options_set(ssh_session session,
|
||||
session->opts.username = username;
|
||||
}
|
||||
if (!session->opts.config_hostname_only) {
|
||||
SAFE_FREE(session->opts.config_hostname);
|
||||
SAFE_FREE(session->opts.originalhost);
|
||||
session->opts.originalhost = hostname;
|
||||
} else {
|
||||
@@ -2270,6 +2279,46 @@ int ssh_options_apply(ssh_session session)
|
||||
}
|
||||
}
|
||||
|
||||
if (session->opts.config_hostname != NULL) {
|
||||
char *saved_host = NULL;
|
||||
|
||||
tmp = ssh_path_expand_hostname(session, session->opts.config_hostname);
|
||||
if (tmp == NULL) {
|
||||
return -1;
|
||||
}
|
||||
if (session->opts.host != NULL) {
|
||||
saved_host = strdup(session->opts.host);
|
||||
if (saved_host == NULL) {
|
||||
free(tmp);
|
||||
ssh_set_error_oom(session);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
session->opts.config_hostname_only = true;
|
||||
rc = ssh_options_set(session, SSH_OPTIONS_HOST, tmp);
|
||||
session->opts.config_hostname_only = false;
|
||||
if (rc != SSH_OK) {
|
||||
/* If HostName expansion leaves a literal '%', keep the current
|
||||
* host instead of treating the deferred HostName as fatal.
|
||||
*/
|
||||
if (strchr(tmp, '%') == NULL) {
|
||||
SAFE_FREE(saved_host);
|
||||
free(tmp);
|
||||
return -1;
|
||||
}
|
||||
SSH_LOG(SSH_LOG_WARN,
|
||||
"HostName %s contains unknown expansion tokens and could "
|
||||
"not be applied; falling back to current host",
|
||||
tmp);
|
||||
SAFE_FREE(session->opts.host);
|
||||
session->opts.host = saved_host;
|
||||
saved_host = NULL;
|
||||
}
|
||||
SAFE_FREE(saved_host);
|
||||
free(tmp);
|
||||
SAFE_FREE(session->opts.config_hostname);
|
||||
}
|
||||
|
||||
if ((session->opts.exp_flags & SSH_OPT_EXP_FLAG_KNOWNHOSTS) == 0) {
|
||||
if (session->opts.knownhosts == NULL) {
|
||||
tmp = ssh_path_expand_escape(session, "%d/.ssh/known_hosts");
|
||||
|
||||
@@ -408,6 +408,7 @@ void ssh_free(ssh_session session)
|
||||
SAFE_FREE(session->opts.username);
|
||||
SAFE_FREE(session->opts.host);
|
||||
SAFE_FREE(session->opts.originalhost);
|
||||
SAFE_FREE(session->opts.config_hostname);
|
||||
SAFE_FREE(session->opts.tag);
|
||||
SAFE_FREE(session->opts.homedir);
|
||||
SAFE_FREE(session->opts.sshdir);
|
||||
|
||||
Reference in New Issue
Block a user