This is an automated email from the ASF dual-hosted git repository.
twolf pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/mina-sshd.git
The following commit(s) were added to refs/heads/master by this push:
new c66429a4b GH-865: replace %h in HostName SSH config
c66429a4b is described below
commit c66429a4b2269ec6cfcd8a632e7d124dce655411
Author: Thomas Wolf <[email protected]>
AuthorDate: Wed Jan 7 20:28:12 2026 +0100
GH-865: replace %h in HostName SSH config
OpenSSH does so. It enables for instance to write an SSH host entry
Host foo*
HostName %h.example.com
to resolve "foobar" to "foobar.example.com" and "foobaz" to
"foobaz.example.com".
---
CHANGES.md | 1 +
.../sshd/client/config/hosts/HostConfigEntry.java | 34 ++++++++++++++++++++++
.../client/config/hosts/HostConfigEntryTest.java | 14 +++++----
3 files changed, 43 insertions(+), 6 deletions(-)
diff --git a/CHANGES.md b/CHANGES.md
index 7a085eaf6..59d7a19b9 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -44,6 +44,7 @@
* [GH-814](https://github.com/apache/mina-sshd/pull/814) Include a fix for
CVE-2020-36843 in optional dependency net.i2p.crypto:eddsa:0.3.0: perform the
missing range check in Apache MINA SSHD before delegating to the signature
verification in net.i2p.crypto:eddsa:0.3.0. This means that using
net.i2p.crypto:eddsa:0.3.0 in Apache MINA SSHD is
safe despite that CVE in the dependency.
+* [GH-865](https://github.com/apache/mina-sshd/issues/865) replace `%h` in
`HostName` SSH config
## Potential Compatibility Issues
diff --git
a/sshd-common/src/main/java/org/apache/sshd/client/config/hosts/HostConfigEntry.java
b/sshd-common/src/main/java/org/apache/sshd/client/config/hosts/HostConfigEntry.java
index 6e3608b83..205dedce6 100644
---
a/sshd-common/src/main/java/org/apache/sshd/client/config/hosts/HostConfigEntry.java
+++
b/sshd-common/src/main/java/org/apache/sshd/client/config/hosts/HostConfigEntry.java
@@ -647,6 +647,8 @@ public class HostConfigEntry extends HostPatternsHolder
implements MutableUserHo
String temp = entry.getHostName(); // Remember that this was
null above.
if (temp == null || temp.isEmpty()) {
entry.setHostName(host);
+ } else {
+ entry.setHostName(substituteHostName(temp, host));
}
temp = entry.getUsername();
if (temp == null || temp.isEmpty()) {
@@ -919,6 +921,38 @@ public class HostConfigEntry extends HostPatternsHolder
implements MutableUserHo
return sb.toString();
}
+ private static String substituteHostName(String host, String
originalHostName) {
+ int len = host.length();
+ int j = host.indexOf(PATH_MACRO_CHAR);
+ if (j < 0) {
+ return host;
+ }
+ StringBuilder result = new StringBuilder(len);
+ result.append(host.substring(0, j));
+ for (int i = j; i < len; i++) {
+ char ch = host.charAt(i);
+ if (ch == PATH_MACRO_CHAR) {
+ i++;
+ ValidateUtils.checkTrue(i < len, "Missing macro modifier in
hostname %s", host);
+ ch = host.charAt(i);
+ switch (ch) {
+ case PATH_MACRO_CHAR:
+ result.append(ch);
+ break;
+ case REMOTE_HOST_MACRO:
+ result.append(originalHostName);
+ break;
+ default:
+ ValidateUtils.throwIllegalArgumentException("Bad
modifier '%s' in hostname %s", String.valueOf(ch),
+ host);
+ }
+ } else {
+ result.append(ch);
+ }
+ }
+ return result.toString();
+ }
+
/**
* @return The default {@link Path} location of the OpenSSH hosts entries
configuration file
*/
diff --git
a/sshd-common/src/test/java/org/apache/sshd/client/config/hosts/HostConfigEntryTest.java
b/sshd-common/src/test/java/org/apache/sshd/client/config/hosts/HostConfigEntryTest.java
index c53fd518d..16c123785 100644
---
a/sshd-common/src/test/java/org/apache/sshd/client/config/hosts/HostConfigEntryTest.java
+++
b/sshd-common/src/test/java/org/apache/sshd/client/config/hosts/HostConfigEntryTest.java
@@ -36,12 +36,6 @@ import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertFalse;
-import static org.junit.jupiter.api.Assertions.assertNotNull;
-import static org.junit.jupiter.api.Assertions.assertNull;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-
/**
* @author <a href="mailto:[email protected]">Apache MINA SSHD Project</a>
*/
@@ -95,6 +89,14 @@ public class HostConfigEntryTest extends JUnitTestSupport {
expect("foo.example.com", 2022, "testuser", resolved);
}
+ @Test
+ void substituteHostname() throws Exception {
+ HostConfigEntry entry = new HostConfigEntry("foo*", "b%%ar%h.org", -1,
"test");
+ HostConfigEntry resolved =
HostConfigEntry.toHostConfigEntryResolver(Collections.singleton(entry))
+ .resolveEffectiveHost("fooby", 0, null, "", null, null);
+ expect("b%arfooby.org", 22, "test", resolved);
+ }
+
@Test
void defaults() throws Exception {
HostConfigEntry entry = new HostConfigEntry("foo*", "bar.example.com",
22, "test");