View Javadoc
1   /*
2    * Copyright (C) 2008, 2021 Google Inc. and others
3    *
4    * This program and the accompanying materials are made available under the
5    * terms of the Eclipse Distribution License v. 1.0 which is available at
6    * https://www.eclipse.org/org/documents/edl-v10.php.
7    *
8    * SPDX-License-Identifier: BSD-3-Clause
9    */
10  
11  package org.eclipse.jgit.internal.transport.ssh;
12  
13  import static java.nio.charset.StandardCharsets.UTF_8;
14  import static org.junit.Assert.assertArrayEquals;
15  import static org.junit.Assert.assertEquals;
16  import static org.junit.Assert.assertNotNull;
17  import static org.junit.Assert.assertNotSame;
18  import static org.junit.Assert.assertNull;
19  import static org.junit.Assert.assertTrue;
20  
21  import java.io.File;
22  import java.io.FileOutputStream;
23  import java.io.IOException;
24  import java.io.OutputStreamWriter;
25  import java.time.Instant;
26  import java.util.concurrent.TimeUnit;
27  
28  import org.eclipse.jgit.junit.RepositoryTestCase;
29  import org.eclipse.jgit.lib.Constants;
30  import org.eclipse.jgit.transport.SshConfigStore.HostConfig;
31  import org.eclipse.jgit.transport.SshConstants;
32  import org.eclipse.jgit.util.FS;
33  import org.eclipse.jgit.util.FileUtils;
34  import org.eclipse.jgit.util.SystemReader;
35  import org.junit.Before;
36  import org.junit.Test;
37  
38  public class OpenSshConfigFileTest extends RepositoryTestCase {
39  
40  	private File home;
41  
42  	private File configFile;
43  
44  	private OpenSshConfigFile osc;
45  
46  	@Override
47  	@Before
48  	public void setUp() throws Exception {
49  		super.setUp();
50  
51  		home = new File(trash, "home");
52  		FileUtils.mkdir(home);
53  
54  		configFile = new File(new File(home, ".ssh"), Constants.CONFIG);
55  		FileUtils.mkdir(configFile.getParentFile());
56  
57  		mockSystemReader.setProperty(Constants.OS_USER_NAME_KEY, "jex_junit");
58  		mockSystemReader.setProperty("TST_VAR", "TEST");
59  		osc = new OpenSshConfigFile(home, configFile, "jex_junit");
60  	}
61  
62  	private void config(String data) throws IOException {
63  		FS fs = FS.DETECTED;
64  		long resolution = FS.getFileStoreAttributes(configFile.toPath())
65  				.getFsTimestampResolution().toNanos();
66  		Instant lastMtime = fs.lastModifiedInstant(configFile);
67  		do {
68  			try (final OutputStreamWriter fw = new OutputStreamWriter(
69  					new FileOutputStream(configFile), UTF_8)) {
70  				fw.write(data);
71  				TimeUnit.NANOSECONDS.sleep(resolution);
72  			} catch (InterruptedException e) {
73  				Thread.interrupted();
74  			}
75  		} while (lastMtime.equals(fs.lastModifiedInstant(configFile)));
76  	}
77  
78  	private HostConfig lookup(String hostname) {
79  		return osc.lookupDefault(hostname, 0, null);
80  	}
81  
82  	private void assertHost(String expected, HostConfig h) {
83  		assertEquals(expected, h.getValue(SshConstants.HOST_NAME));
84  	}
85  
86  	private void assertUser(String expected, HostConfig h) {
87  		assertEquals(expected, h.getValue(SshConstants.USER));
88  	}
89  
90  	private void assertPort(int expected, HostConfig h) {
91  		assertEquals(expected,
92  				OpenSshConfigFile.positive(h.getValue(SshConstants.PORT)));
93  	}
94  
95  	private void assertIdentity(File expected, HostConfig h) {
96  		String actual = h.getValue(SshConstants.IDENTITY_FILE);
97  		if (expected == null) {
98  			assertNull(actual);
99  		} else {
100 			assertEquals(expected, new File(actual));
101 		}
102 	}
103 
104 	private void assertAttempts(int expected, HostConfig h) {
105 		assertEquals(expected, OpenSshConfigFile
106 				.positive(h.getValue(SshConstants.CONNECTION_ATTEMPTS)));
107 	}
108 
109 	@Test
110 	public void testNoConfig() {
111 		final HostConfig h = lookup("repo.or.cz");
112 		assertNotNull(h);
113 		assertHost("repo.or.cz", h);
114 		assertUser("jex_junit", h);
115 		assertPort(22, h);
116 		assertAttempts(1, h);
117 		assertIdentity(null, h);
118 	}
119 
120 	@Test
121 	public void testSeparatorParsing() throws Exception {
122 		config("Host\tfirst\n" +
123 		       "\tHostName\tfirst.tld\n" +
124 		       "\n" +
125 				"Host second\n" +
126 		       " HostName\tsecond.tld\n" +
127 		       "Host=third\n" +
128 		       "HostName=third.tld\n\n\n" +
129 				"\t Host = fourth\n\n\n" +
130 		       " \t HostName\t=fourth.tld\n" +
131 		       "Host\t =     last\n" +
132 		       "HostName  \t    last.tld");
133 		assertNotNull(lookup("first"));
134 		assertHost("first.tld", lookup("first"));
135 		assertNotNull(lookup("second"));
136 		assertHost("second.tld", lookup("second"));
137 		assertNotNull(lookup("third"));
138 		assertHost("third.tld", lookup("third"));
139 		assertNotNull(lookup("fourth"));
140 		assertHost("fourth.tld", lookup("fourth"));
141 		assertNotNull(lookup("last"));
142 		assertHost("last.tld", lookup("last"));
143 	}
144 
145 	@Test
146 	public void testQuoteParsing() throws Exception {
147 		config("Host \"good\"\n" +
148 			" HostName=\"good.tld\"\n" +
149 			" Port=\"6007\"\n" +
150 			" User=\"gooduser\"\n" +
151 				"Host multiple unquoted and \"quoted\" \"hosts\"\n" +
152 			" Port=\"2222\"\n" +
153 				"Host \"spaced\"\n" +
154 			"# Bad host name, but testing preservation of spaces\n" +
155 			" HostName=\" spaced\ttld \"\n" +
156 			"# Misbalanced quotes\n" +
157 				"Host \"bad\"\n" +
158 			"# OpenSSH doesn't allow this but ...\n" +
159 			" HostName=bad.tld\"\n");
160 		assertHost("good.tld", lookup("good"));
161 		assertUser("gooduser", lookup("good"));
162 		assertPort(6007, lookup("good"));
163 		assertPort(2222, lookup("multiple"));
164 		assertPort(2222, lookup("quoted"));
165 		assertPort(2222, lookup("and"));
166 		assertPort(2222, lookup("unquoted"));
167 		assertPort(2222, lookup("hosts"));
168 		assertHost(" spaced\ttld ", lookup("spaced"));
169 		assertHost("bad.tld", lookup("bad"));
170 	}
171 
172 	@Test
173 	public void testAdvancedParsing() throws Exception {
174 		// Escaped quotes, and line comments
175 		config("Host foo\n"
176 				+ " HostName=\"foo\\\"d.tld\"\n"
177 				+ " User= someone#foo\n"
178 				+ "Host bar\n"
179 				+ " User ' some one#two' # Comment\n"
180 				+ " GlobalKnownHostsFile '/a folder/with spaces/hosts' '/other/more hosts' # Comment\n"
181 				+ "Host foobar\n"
182 				+ " User a\\ u\\ thor\n"
183 				+ "Host backslash\n"
184 				+ " User some\\one\\\\\\ foo\n"
185 				+ "Host backslash_before_quote\n"
186 				+ " User \\\"someone#\"el#se\" #Comment\n"
187 				+ "Host backslash_in_quote\n"
188 				+ " User 'some\\one\\\\\\ foo'\n");
189 		assertHost("foo\"d.tld", lookup("foo"));
190 		assertUser("someone#foo", lookup("foo"));
191 		HostConfig c = lookup("bar");
192 		assertUser(" some one#two", c);
193 		assertArrayEquals(
194 				new Object[] { "/a folder/with spaces/hosts",
195 						"/other/more hosts" },
196 				c.getValues("GlobalKnownHostsFile").toArray());
197 		assertUser("a u thor", lookup("foobar"));
198 		assertUser("some\\one\\ foo", lookup("backslash"));
199 		assertUser("\"someone#el#se", lookup("backslash_before_quote"));
200 		assertUser("some\\one\\\\ foo", lookup("backslash_in_quote"));
201 	}
202 
203 	@Test
204 	public void testCaseInsensitiveKeyLookup() throws Exception {
205 		config("Host orcz\n" + "Port 29418\n"
206 				+ "\tHostName repo.or.cz\nStrictHostKeyChecking yes\n");
207 		final HostConfig c = lookup("orcz");
208 		String exactCase = c.getValue("StrictHostKeyChecking");
209 		assertEquals("yes", exactCase);
210 		assertEquals(exactCase, c.getValue("stricthostkeychecking"));
211 		assertEquals(exactCase, c.getValue("STRICTHOSTKEYCHECKING"));
212 		assertEquals(exactCase, c.getValue("sTrIcThostKEYcheckING"));
213 		assertNull(c.getValue("sTrIcThostKEYcheckIN"));
214 	}
215 
216 	@Test
217 	public void testAlias_DoesNotMatch() throws Exception {
218 		config("Host orcz\n" + "Port 29418\n"
219 				+ "\tHostName repo.or.cz\n");
220 		final HostConfig h = lookup("repo.or.cz");
221 		assertNotNull(h);
222 		assertHost("repo.or.cz", h);
223 		assertUser("jex_junit", h);
224 		assertPort(22, h);
225 		assertIdentity(null, h);
226 		final HostConfig h2 = lookup("orcz");
227 		assertHost("repo.or.cz", h);
228 		assertUser("jex_junit", h);
229 		assertPort(29418, h2);
230 		assertIdentity(null, h);
231 	}
232 
233 	@Test
234 	public void testAlias_OptionsSet() throws Exception {
235 		config("Host orcz\n" + "\tHostName repo.or.cz\n" + "\tPort 2222\n"
236 				+ "\tUser jex\n" + "\tIdentityFile .ssh/id_jex\n"
237 				+ "\tForwardX11 no\n");
238 		final HostConfig h = lookup("orcz");
239 		assertNotNull(h);
240 		assertHost("repo.or.cz", h);
241 		assertUser("jex", h);
242 		assertPort(2222, h);
243 		assertIdentity(new File(home, ".ssh/id_jex"), h);
244 	}
245 
246 	@Test
247 	public void testAlias_OptionsKeywordCaseInsensitive() throws Exception {
248 		config("hOsT orcz\n" + "\thOsTnAmE repo.or.cz\n" + "\tPORT 2222\n"
249 				+ "\tuser jex\n" + "\tidentityfile .ssh/id_jex\n"
250 				+ "\tForwardX11 no\n");
251 		final HostConfig h = lookup("orcz");
252 		assertNotNull(h);
253 		assertHost("repo.or.cz", h);
254 		assertUser("jex", h);
255 		assertPort(2222, h);
256 		assertIdentity(new File(home, ".ssh/id_jex"), h);
257 	}
258 
259 	@Test
260 	public void testAlias_OptionsInherit() throws Exception {
261 		config("Host orcz\n" + "\tHostName repo.or.cz\n" + "\n" + "Host *\n"
262 				+ "\tHostName not.a.host.example.com\n" + "\tPort 2222\n"
263 				+ "\tUser jex\n" + "\tIdentityFile .ssh/id_jex\n"
264 				+ "\tForwardX11 no\n");
265 		final HostConfig h = lookup("orcz");
266 		assertNotNull(h);
267 		assertHost("repo.or.cz", h);
268 		assertUser("jex", h);
269 		assertPort(2222, h);
270 		assertIdentity(new File(home, ".ssh/id_jex"), h);
271 	}
272 
273 	@Test
274 	public void testAlias_PreferredAuthenticationsDefault() throws Exception {
275 		final HostConfig h = lookup("orcz");
276 		assertNotNull(h);
277 		assertNull(h.getValue(SshConstants.PREFERRED_AUTHENTICATIONS));
278 	}
279 
280 	@Test
281 	public void testAlias_PreferredAuthentications() throws Exception {
282 		config("Host orcz\n" + "\tPreferredAuthentications publickey\n");
283 		final HostConfig h = lookup("orcz");
284 		assertNotNull(h);
285 		assertEquals("publickey",
286 				h.getValue(SshConstants.PREFERRED_AUTHENTICATIONS));
287 	}
288 
289 	@Test
290 	public void testAlias_InheritPreferredAuthentications() throws Exception {
291 		config("Host orcz\n" + "\tHostName repo.or.cz\n" + "\n" + "Host *\n"
292 				+ "\tPreferredAuthentications 'publickey, hostbased'\n");
293 		final HostConfig h = lookup("orcz");
294 		assertNotNull(h);
295 		assertEquals("publickey,hostbased",
296 				h.getValue(SshConstants.PREFERRED_AUTHENTICATIONS));
297 	}
298 
299 	@Test
300 	public void testAlias_BatchModeDefault() throws Exception {
301 		final HostConfig h = lookup("orcz");
302 		assertNotNull(h);
303 		assertNull(h.getValue(SshConstants.BATCH_MODE));
304 	}
305 
306 	@Test
307 	public void testAlias_BatchModeYes() throws Exception {
308 		config("Host orcz\n" + "\tBatchMode yes\n");
309 		final HostConfig h = lookup("orcz");
310 		assertNotNull(h);
311 		assertTrue(OpenSshConfigFile.flag(h.getValue(SshConstants.BATCH_MODE)));
312 	}
313 
314 	@Test
315 	public void testAlias_InheritBatchMode() throws Exception {
316 		config("Host orcz\n" + "\tHostName repo.or.cz\n" + "\n" + "Host *\n"
317 				+ "\tBatchMode yes\n");
318 		final HostConfig h = lookup("orcz");
319 		assertNotNull(h);
320 		assertTrue(OpenSshConfigFile.flag(h.getValue(SshConstants.BATCH_MODE)));
321 	}
322 
323 	@Test
324 	public void testAlias_ConnectionAttemptsDefault() throws Exception {
325 		final HostConfig h = lookup("orcz");
326 		assertNotNull(h);
327 		assertAttempts(1, h);
328 	}
329 
330 	@Test
331 	public void testAlias_ConnectionAttempts() throws Exception {
332 		config("Host orcz\n" + "\tConnectionAttempts 5\n");
333 		final HostConfig h = lookup("orcz");
334 		assertNotNull(h);
335 		assertAttempts(5, h);
336 	}
337 
338 	@Test
339 	public void testAlias_invalidConnectionAttempts() throws Exception {
340 		config("Host orcz\n" + "\tConnectionAttempts -1\n");
341 		final HostConfig h = lookup("orcz");
342 		assertNotNull(h);
343 		assertAttempts(1, h);
344 	}
345 
346 	@Test
347 	public void testAlias_badConnectionAttempts() throws Exception {
348 		config("Host orcz\n" + "\tConnectionAttempts xxx\n");
349 		final HostConfig h = lookup("orcz");
350 		assertNotNull(h);
351 		assertAttempts(1, h);
352 	}
353 
354 	@Test
355 	public void testDefaultBlock() throws Exception {
356 		config("ConnectionAttempts 5\n\nHost orcz\nConnectionAttempts 3\n");
357 		final HostConfig h = lookup("orcz");
358 		assertNotNull(h);
359 		assertAttempts(5, h);
360 	}
361 
362 	@Test
363 	public void testHostCaseInsensitive() throws Exception {
364 		config("hOsT orcz\nConnectionAttempts 3\n");
365 		final HostConfig h = lookup("orcz");
366 		assertNotNull(h);
367 		assertAttempts(3, h);
368 	}
369 
370 	@Test
371 	public void testListValueSingle() throws Exception {
372 		config("Host orcz\nUserKnownHostsFile /foo/bar\n");
373 		final HostConfig c = lookup("orcz");
374 		assertNotNull(c);
375 		assertEquals("/foo/bar", c.getValue("UserKnownHostsFile"));
376 	}
377 
378 	@Test
379 	public void testListValueMultiple() throws Exception {
380 		// Tilde expansion occurs within the parser
381 		config("Host orcz\nUserKnownHostsFile \"~/foo/ba z\" /foo/bar \n");
382 		final HostConfig c = lookup("orcz");
383 		assertNotNull(c);
384 		assertArrayEquals(new Object[] { new File(home, "foo/ba z").getPath(),
385 				"/foo/bar" },
386 				c.getValues("UserKnownHostsFile").toArray());
387 	}
388 
389 	@Test
390 	public void testRepeatedLookupsWithModification() throws Exception {
391 		config("Host orcz\n" + "\tConnectionAttempts -1\n");
392 		final HostConfig h1 = lookup("orcz");
393 		assertNotNull(h1);
394 		assertAttempts(1, h1);
395 		config("Host orcz\n" + "\tConnectionAttempts 5\n");
396 		final HostConfig h2 = lookup("orcz");
397 		assertNotNull(h2);
398 		assertNotSame(h1, h2);
399 		assertAttempts(5, h2);
400 		assertAttempts(1, h1);
401 		assertNotSame(h1, h2);
402 	}
403 
404 	@Test
405 	public void testIdentityFile() throws Exception {
406 		config("Host orcz\nIdentityFile \"~/foo/ba z\"\nIdentityFile /foo/bar");
407 		final HostConfig h = lookup("orcz");
408 		assertNotNull(h);
409 		// Does tilde replacement
410 		assertArrayEquals(new Object[] { new File(home, "foo/ba z").getPath(),
411 				"/foo/bar" },
412 				h.getValues(SshConstants.IDENTITY_FILE).toArray());
413 	}
414 
415 	@Test
416 	public void testMultiIdentityFile() throws Exception {
417 		config("IdentityFile \"~/foo/ba z\"\nHost orcz\nIdentityFile /foo/bar\nHOST *\nIdentityFile /foo/baz");
418 		final HostConfig h = lookup("orcz");
419 		assertNotNull(h);
420 		assertArrayEquals(new Object[] { new File(home, "foo/ba z").getPath(),
421 				"/foo/bar", "/foo/baz" },
422 				h.getValues(SshConstants.IDENTITY_FILE).toArray());
423 	}
424 
425 	@Test
426 	public void testNegatedPattern() throws Exception {
427 		config("Host repo.or.cz\nIdentityFile ~/foo/bar\nHOST !*.or.cz\nIdentityFile /foo/baz");
428 		final HostConfig h = lookup("repo.or.cz");
429 		assertNotNull(h);
430 		assertIdentity(new File(home, "foo/bar"), h);
431 		assertArrayEquals(new Object[] { new File(home, "foo/bar").getPath() },
432 				h.getValues(SshConstants.IDENTITY_FILE).toArray());
433 	}
434 
435 	@Test
436 	public void testPattern() throws Exception {
437 		config("Host repo.or.cz\nIdentityFile ~/foo/bar\nHOST *.or.cz\nIdentityFile /foo/baz");
438 		final HostConfig h = lookup("repo.or.cz");
439 		assertNotNull(h);
440 		assertIdentity(new File(home, "foo/bar"), h);
441 		assertArrayEquals(new Object[] { new File(home, "foo/bar").getPath(),
442 				"/foo/baz" },
443 				h.getValues(SshConstants.IDENTITY_FILE).toArray());
444 	}
445 
446 	@Test
447 	public void testMultiHost() throws Exception {
448 		config("Host orcz *.or.cz\nIdentityFile ~/foo/bar\nHOST *.or.cz\nIdentityFile /foo/baz");
449 		final HostConfig h1 = lookup("repo.or.cz");
450 		assertNotNull(h1);
451 		assertIdentity(new File(home, "foo/bar"), h1);
452 		assertArrayEquals(new Object[] { new File(home, "foo/bar").getPath(),
453 				"/foo/baz" },
454 				h1.getValues(SshConstants.IDENTITY_FILE).toArray());
455 		final HostConfig h2 = lookup("orcz");
456 		assertNotNull(h2);
457 		assertIdentity(new File(home, "foo/bar"), h2);
458 		assertArrayEquals(new Object[] { new File(home, "foo/bar").getPath() },
459 				h2.getValues(SshConstants.IDENTITY_FILE).toArray());
460 	}
461 
462 	@Test
463 	public void testEqualsSign() throws Exception {
464 		config("Host=orcz\n\tConnectionAttempts = 5\n\tUser=\t  foobar\t\n");
465 		final HostConfig h = lookup("orcz");
466 		assertNotNull(h);
467 		assertAttempts(5, h);
468 		assertUser("foobar", h);
469 	}
470 
471 	@Test
472 	public void testMissingArgument() throws Exception {
473 		config("Host=orcz\n\tSendEnv\nIdentityFile\t\nForwardX11\n\tUser=\t  foobar\t\n");
474 		final HostConfig h = lookup("orcz");
475 		assertNotNull(h);
476 		assertUser("foobar", h);
477 		assertEquals("[]", h.getValues("SendEnv").toString());
478 		assertIdentity(null, h);
479 		assertNull(h.getValue("ForwardX11"));
480 	}
481 
482 	@Test
483 	public void testHomeDirUserReplacement() throws Exception {
484 		config("Host=orcz\n\tIdentityFile %d/.ssh/%u_id_dsa");
485 		final HostConfig h = lookup("orcz");
486 		assertNotNull(h);
487 		assertIdentity(new File(new File(home, ".ssh"), "jex_junit_id_dsa"), h);
488 	}
489 
490 	@Test
491 	public void testHostnameReplacement() throws Exception {
492 		config("Host=orcz\nHost *.*\n\tHostname %h\nHost *\n\tHostname %h.example.org");
493 		final HostConfig h = lookup("orcz");
494 		assertNotNull(h);
495 		assertHost("orcz.example.org", h);
496 	}
497 
498 	@Test
499 	public void testRemoteUserReplacement() throws Exception {
500 		config("Host=orcz\n\tUser foo\n" + "Host *.*\n\tHostname %h\n"
501 				+ "Host *\n\tHostname %h.ex%%20ample.org\n\tIdentityFile ~/.ssh/%h_%r_id_dsa");
502 		final HostConfig h = lookup("orcz");
503 		assertNotNull(h);
504 		assertIdentity(
505 				new File(new File(home, ".ssh"),
506 						"orcz.ex%20ample.org_foo_id_dsa"),
507 				h);
508 	}
509 
510 	@Test
511 	public void testLocalhostFQDNReplacement() throws Exception {
512 		String localhost = SystemReader.getInstance().getHostname();
513 		config("Host=orcz\n\tIdentityFile ~/.ssh/%l_id_dsa");
514 		final HostConfig h = lookup("orcz");
515 		assertNotNull(h);
516 		assertIdentity(
517 				new File(new File(home, ".ssh"), localhost + "_id_dsa"),
518 				h);
519 	}
520 
521 	@Test
522 	public void testPubKeyAcceptedAlgorithms() throws Exception {
523 		config("Host=orcz\n\tPubkeyAcceptedAlgorithms ^ssh-rsa");
524 		HostConfig h = lookup("orcz");
525 		assertEquals("^ssh-rsa",
526 				h.getValue(SshConstants.PUBKEY_ACCEPTED_ALGORITHMS));
527 		assertEquals("^ssh-rsa", h.getValue("PubkeyAcceptedKeyTypes"));
528 	}
529 
530 	@Test
531 	public void testPubKeyAcceptedKeyTypes() throws Exception {
532 		config("Host=orcz\n\tPubkeyAcceptedKeyTypes ^ssh-rsa");
533 		HostConfig h = lookup("orcz");
534 		assertEquals("^ssh-rsa",
535 				h.getValue(SshConstants.PUBKEY_ACCEPTED_ALGORITHMS));
536 		assertEquals("^ssh-rsa", h.getValue("PubkeyAcceptedKeyTypes"));
537 	}
538 
539 	@Test
540 	public void testEolComments() throws Exception {
541 		config("#Comment\nHost=orcz #Comment\n\tPubkeyAcceptedAlgorithms ^ssh-rsa # Comment\n#Comment");
542 		HostConfig h = lookup("orcz");
543 		assertNotNull(h);
544 		assertEquals("^ssh-rsa",
545 				h.getValue(SshConstants.PUBKEY_ACCEPTED_ALGORITHMS));
546 	}
547 
548 	@Test
549 	public void testEnVarSubstitution() throws Exception {
550 		config("Host orcz\nIdentityFile /tmp/${TST_VAR}\n"
551 				+ "CertificateFile /tmp/${}/foo\nUser ${TST_VAR}\nIdentityAgent /tmp/${TST_VAR/bar");
552 		HostConfig h = lookup("orcz");
553 		assertNotNull(h);
554 		assertEquals("/tmp/TEST",
555 				h.getValue(SshConstants.IDENTITY_FILE));
556 		// No variable name
557 		assertEquals("/tmp/${}/foo", h.getValue(SshConstants.CERTIFICATE_FILE));
558 		// User doesn't get env var substitution:
559 		assertUser("${TST_VAR}", h);
560 		// Unterminated:
561 		assertEquals("/tmp/${TST_VAR/bar",
562 				h.getValue(SshConstants.IDENTITY_AGENT));
563 	}
564 
565 	@Test
566 	public void testNegativeMatch() throws Exception {
567 		config("Host foo.bar !foobar.baz *.baz\n" + "Port 29418\n");
568 		HostConfig h = lookup("foo.bar");
569 		assertNotNull(h);
570 		assertPort(29418, h);
571 		h = lookup("foobar.baz");
572 		assertNotNull(h);
573 		assertPort(22, h);
574 		h = lookup("foo.baz");
575 		assertNotNull(h);
576 		assertPort(29418, h);
577 	}
578 
579 	@Test
580 	public void testNegativeMatch2() throws Exception {
581 		// Negative match after the positive match.
582 		config("Host foo.bar *.baz !foobar.baz\n" + "Port 29418\n");
583 		HostConfig h = lookup("foo.bar");
584 		assertNotNull(h);
585 		assertPort(29418, h);
586 		h = lookup("foobar.baz");
587 		assertNotNull(h);
588 		assertPort(22, h);
589 		h = lookup("foo.baz");
590 		assertNotNull(h);
591 		assertPort(29418, h);
592 	}
593 
594 	@Test
595 	public void testNoMatch() throws Exception {
596 		config("Host !host1 !host2\n" + "Port 29418\n");
597 		HostConfig h = lookup("host1");
598 		assertNotNull(h);
599 		assertPort(22, h);
600 		h = lookup("host2");
601 		assertNotNull(h);
602 		assertPort(22, h);
603 		h = lookup("host3");
604 		assertNotNull(h);
605 		assertPort(22, h);
606 	}
607 
608 	@Test
609 	public void testMultipleMatch() throws Exception {
610 		config("Host foo.bar\nPort 29418\nIdentityFile /foo\n\n"
611 				+ "Host *.bar\nPort 22\nIdentityFile /bar\n"
612 				+ "Host foo.bar\nPort 47\nIdentityFile /baz\n");
613 		HostConfig h = lookup("foo.bar");
614 		assertNotNull(h);
615 		assertPort(29418, h);
616 		assertArrayEquals(new Object[] { "/foo", "/bar", "/baz" },
617 				h.getValues(SshConstants.IDENTITY_FILE).toArray());
618 	}
619 
620 	@Test
621 	public void testWhitespace() throws Exception {
622 		config("Host foo \tbar   baz\nPort 29418\n");
623 		HostConfig h = lookup("foo");
624 		assertNotNull(h);
625 		assertPort(29418, h);
626 		h = lookup("bar");
627 		assertNotNull(h);
628 		assertPort(29418, h);
629 		h = lookup("baz");
630 		assertNotNull(h);
631 		assertPort(29418, h);
632 		h = lookup("\tbar");
633 		assertNotNull(h);
634 		assertPort(22, h);
635 	}
636 }