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(new File(home, "foo/bar").getPath(),
376 				c.getValue("UserKnownHostsFile"));
377 	}
378 
379 	@Test
380 	public void testListValueMultiple() throws Exception {
381 		// Tilde expansion occurs within the parser
382 		config("Host orcz\nUserKnownHostsFile \"~/foo/ba z\" ~/foo/bar \n");
383 		final HostConfig c = lookup("orcz");
384 		assertNotNull(c);
385 		assertArrayEquals(new Object[] { new File(home, "foo/ba z").getPath(),
386 				new File(home, "foo/bar").getPath() },
387 				c.getValues("UserKnownHostsFile").toArray());
388 	}
389 
390 	@Test
391 	public void testRepeatedLookupsWithModification() throws Exception {
392 		config("Host orcz\n" + "\tConnectionAttempts -1\n");
393 		final HostConfig h1 = lookup("orcz");
394 		assertNotNull(h1);
395 		assertAttempts(1, h1);
396 		config("Host orcz\n" + "\tConnectionAttempts 5\n");
397 		final HostConfig h2 = lookup("orcz");
398 		assertNotNull(h2);
399 		assertNotSame(h1, h2);
400 		assertAttempts(5, h2);
401 		assertAttempts(1, h1);
402 		assertNotSame(h1, h2);
403 	}
404 
405 	@Test
406 	public void testIdentityFile() throws Exception {
407 		config("Host orcz\nIdentityFile \"~/foo/ba z\"\nIdentityFile ~/foo/bar");
408 		final HostConfig h = lookup("orcz");
409 		assertNotNull(h);
410 		// Does tilde replacement
411 		assertArrayEquals(new Object[] { new File(home, "foo/ba z").getPath(),
412 				new File(home, "foo/bar").getPath() },
413 				h.getValues(SshConstants.IDENTITY_FILE).toArray());
414 	}
415 
416 	@Test
417 	public void testMultiIdentityFile() throws Exception {
418 		config("IdentityFile \"~/foo/ba z\"\nHost orcz\nIdentityFile ~/foo/bar\nHOST *\nIdentityFile ~/foo/baz");
419 		final HostConfig h = lookup("orcz");
420 		assertNotNull(h);
421 		assertArrayEquals(new Object[] { new File(home, "foo/ba z").getPath(),
422 				new File(home, "foo/bar").getPath(),
423 				new File(home, "foo/baz").getPath() },
424 				h.getValues(SshConstants.IDENTITY_FILE).toArray());
425 	}
426 
427 	@Test
428 	public void testNegatedPattern() throws Exception {
429 		config("Host repo.or.cz\nIdentityFile ~/foo/bar\nHOST !*.or.cz\nIdentityFile /foo/baz");
430 		final HostConfig h = lookup("repo.or.cz");
431 		assertNotNull(h);
432 		assertIdentity(new File(home, "foo/bar"), h);
433 		assertArrayEquals(new Object[] { new File(home, "foo/bar").getPath() },
434 				h.getValues(SshConstants.IDENTITY_FILE).toArray());
435 	}
436 
437 	@Test
438 	public void testPattern() throws Exception {
439 		config("Host repo.or.cz\nIdentityFile ~/foo/bar\nHOST *.or.cz\nIdentityFile ~/foo/baz");
440 		final HostConfig h = lookup("repo.or.cz");
441 		assertNotNull(h);
442 		assertIdentity(new File(home, "foo/bar"), h);
443 		assertArrayEquals(new Object[] { new File(home, "foo/bar").getPath(),
444 				new File(home, "foo/baz").getPath() },
445 				h.getValues(SshConstants.IDENTITY_FILE).toArray());
446 	}
447 
448 	@Test
449 	public void testMultiHost() throws Exception {
450 		config("Host orcz *.or.cz\nIdentityFile ~/foo/bar\nHOST *.or.cz\nIdentityFile ~/foo/baz");
451 		final HostConfig h1 = lookup("repo.or.cz");
452 		assertNotNull(h1);
453 		assertIdentity(new File(home, "foo/bar"), h1);
454 		assertArrayEquals(new Object[] { new File(home, "foo/bar").getPath(),
455 				new File(home, "foo/baz").getPath() },
456 				h1.getValues(SshConstants.IDENTITY_FILE).toArray());
457 		final HostConfig h2 = lookup("orcz");
458 		assertNotNull(h2);
459 		assertIdentity(new File(home, "foo/bar"), h2);
460 		assertArrayEquals(new Object[] { new File(home, "foo/bar").getPath() },
461 				h2.getValues(SshConstants.IDENTITY_FILE).toArray());
462 	}
463 
464 	@Test
465 	public void testEqualsSign() throws Exception {
466 		config("Host=orcz\n\tConnectionAttempts = 5\n\tUser=\t  foobar\t\n");
467 		final HostConfig h = lookup("orcz");
468 		assertNotNull(h);
469 		assertAttempts(5, h);
470 		assertUser("foobar", h);
471 	}
472 
473 	@Test
474 	public void testMissingArgument() throws Exception {
475 		config("Host=orcz\n\tSendEnv\nIdentityFile\t\nForwardX11\n\tUser=\t  foobar\t\n");
476 		final HostConfig h = lookup("orcz");
477 		assertNotNull(h);
478 		assertUser("foobar", h);
479 		assertEquals("[]", h.getValues("SendEnv").toString());
480 		assertIdentity(null, h);
481 		assertNull(h.getValue("ForwardX11"));
482 	}
483 
484 	@Test
485 	public void testHomeDirUserReplacement() throws Exception {
486 		config("Host=orcz\n\tIdentityFile %d/.ssh/%u_id_dsa");
487 		final HostConfig h = lookup("orcz");
488 		assertNotNull(h);
489 		assertIdentity(new File(new File(home, ".ssh"), "jex_junit_id_dsa"), h);
490 	}
491 
492 	@Test
493 	public void testHostnameReplacement() throws Exception {
494 		config("Host=orcz\nHost *.*\n\tHostname %h\nHost *\n\tHostname %h.example.org");
495 		final HostConfig h = lookup("orcz");
496 		assertNotNull(h);
497 		assertHost("orcz.example.org", h);
498 	}
499 
500 	@Test
501 	public void testRemoteUserReplacement() throws Exception {
502 		config("Host=orcz\n\tUser foo\n" + "Host *.*\n\tHostname %h\n"
503 				+ "Host *\n\tHostname %h.ex%%20ample.org\n\tIdentityFile ~/.ssh/%h_%r_id_dsa");
504 		final HostConfig h = lookup("orcz");
505 		assertNotNull(h);
506 		assertIdentity(
507 				new File(new File(home, ".ssh"),
508 						"orcz.ex%20ample.org_foo_id_dsa"),
509 				h);
510 	}
511 
512 	@Test
513 	public void testLocalhostFQDNReplacement() throws Exception {
514 		String localhost = SystemReader.getInstance().getHostname();
515 		config("Host=orcz\n\tIdentityFile ~/.ssh/%l_id_dsa");
516 		final HostConfig h = lookup("orcz");
517 		assertNotNull(h);
518 		assertIdentity(
519 				new File(new File(home, ".ssh"), localhost + "_id_dsa"),
520 				h);
521 	}
522 
523 	@Test
524 	public void testPubKeyAcceptedAlgorithms() throws Exception {
525 		config("Host=orcz\n\tPubkeyAcceptedAlgorithms ^ssh-rsa");
526 		HostConfig h = lookup("orcz");
527 		assertEquals("^ssh-rsa",
528 				h.getValue(SshConstants.PUBKEY_ACCEPTED_ALGORITHMS));
529 		assertEquals("^ssh-rsa", h.getValue("PubkeyAcceptedKeyTypes"));
530 	}
531 
532 	@Test
533 	public void testPubKeyAcceptedKeyTypes() throws Exception {
534 		config("Host=orcz\n\tPubkeyAcceptedKeyTypes ^ssh-rsa");
535 		HostConfig h = lookup("orcz");
536 		assertEquals("^ssh-rsa",
537 				h.getValue(SshConstants.PUBKEY_ACCEPTED_ALGORITHMS));
538 		assertEquals("^ssh-rsa", h.getValue("PubkeyAcceptedKeyTypes"));
539 	}
540 
541 	@Test
542 	public void testEolComments() throws Exception {
543 		config("#Comment\nHost=orcz #Comment\n\tPubkeyAcceptedAlgorithms ^ssh-rsa # Comment\n#Comment");
544 		HostConfig h = lookup("orcz");
545 		assertNotNull(h);
546 		assertEquals("^ssh-rsa",
547 				h.getValue(SshConstants.PUBKEY_ACCEPTED_ALGORITHMS));
548 	}
549 
550 	@Test
551 	public void testEnVarSubstitution() throws Exception {
552 		config("Host orcz\nIdentityFile ~/tmp/${TST_VAR}\n"
553 				+ "CertificateFile ~/tmp/${}/foo\nUser ${TST_VAR}\nIdentityAgent ~/tmp/${TST_VAR/bar");
554 		HostConfig h = lookup("orcz");
555 		assertNotNull(h);
556 		File tmp = new File(home, "tmp");
557 		assertEquals(new File(tmp, "TEST").getPath(),
558 				h.getValue(SshConstants.IDENTITY_FILE));
559 		// No variable name
560 		assertEquals(new File(new File(tmp, "${}"), "foo").getPath(),
561 				h.getValue(SshConstants.CERTIFICATE_FILE));
562 		// User doesn't get env var substitution:
563 		assertUser("${TST_VAR}", h);
564 		// Unterminated:
565 		assertEquals(new File(new File(tmp, "${TST_VAR"), "bar").getPath(),
566 				h.getValue(SshConstants.IDENTITY_AGENT));
567 	}
568 
569 	@Test
570 	public void testIdentityAgentNone() throws Exception {
571 		config("Host orcz\nIdentityAgent none\n");
572 		HostConfig h = lookup("orcz");
573 		assertEquals(SshConstants.NONE,
574 				h.getValue(SshConstants.IDENTITY_AGENT));
575 	}
576 
577 	@Test
578 	public void testIdentityAgentSshAuthSock() throws Exception {
579 		config("Host orcz\nIdentityAgent SSH_AUTH_SOCK\n");
580 		HostConfig h = lookup("orcz");
581 		assertEquals(SshConstants.ENV_SSH_AUTH_SOCKET,
582 				h.getValue(SshConstants.IDENTITY_AGENT));
583 	}
584 
585 	@Test
586 	public void testNegativeMatch() throws Exception {
587 		config("Host foo.bar !foobar.baz *.baz\n" + "Port 29418\n");
588 		HostConfig h = lookup("foo.bar");
589 		assertNotNull(h);
590 		assertPort(29418, h);
591 		h = lookup("foobar.baz");
592 		assertNotNull(h);
593 		assertPort(22, h);
594 		h = lookup("foo.baz");
595 		assertNotNull(h);
596 		assertPort(29418, h);
597 	}
598 
599 	@Test
600 	public void testNegativeMatch2() throws Exception {
601 		// Negative match after the positive match.
602 		config("Host foo.bar *.baz !foobar.baz\n" + "Port 29418\n");
603 		HostConfig h = lookup("foo.bar");
604 		assertNotNull(h);
605 		assertPort(29418, h);
606 		h = lookup("foobar.baz");
607 		assertNotNull(h);
608 		assertPort(22, h);
609 		h = lookup("foo.baz");
610 		assertNotNull(h);
611 		assertPort(29418, h);
612 	}
613 
614 	@Test
615 	public void testNoMatch() throws Exception {
616 		config("Host !host1 !host2\n" + "Port 29418\n");
617 		HostConfig h = lookup("host1");
618 		assertNotNull(h);
619 		assertPort(22, h);
620 		h = lookup("host2");
621 		assertNotNull(h);
622 		assertPort(22, h);
623 		h = lookup("host3");
624 		assertNotNull(h);
625 		assertPort(22, h);
626 	}
627 
628 	@Test
629 	public void testMultipleMatch() throws Exception {
630 		config("Host foo.bar\nPort 29418\nIdentityFile ~/foo\n\n"
631 				+ "Host *.bar\nPort 22\nIdentityFile ~/bar\n"
632 				+ "Host foo.bar\nPort 47\nIdentityFile ~/baz\n");
633 		HostConfig h = lookup("foo.bar");
634 		assertNotNull(h);
635 		assertPort(29418, h);
636 		assertArrayEquals(
637 				new Object[] { new File(home, "foo").getPath(),
638 						new File(home, "bar").getPath(),
639 						new File(home, "baz").getPath() },
640 				h.getValues(SshConstants.IDENTITY_FILE).toArray());
641 	}
642 
643 	@Test
644 	public void testWhitespace() throws Exception {
645 		config("Host foo \tbar   baz\nPort 29418\n");
646 		HostConfig h = lookup("foo");
647 		assertNotNull(h);
648 		assertPort(29418, h);
649 		h = lookup("bar");
650 		assertNotNull(h);
651 		assertPort(29418, h);
652 		h = lookup("baz");
653 		assertNotNull(h);
654 		assertPort(29418, h);
655 		h = lookup("\tbar");
656 		assertNotNull(h);
657 		assertPort(22, h);
658 	}
659 
660 	@Test
661 	public void testTimeSpec() throws Exception {
662 		assertEquals(-1, OpenSshConfigFile.timeSpec(null));
663 		assertEquals(-1, OpenSshConfigFile.timeSpec(""));
664 		assertEquals(-1, OpenSshConfigFile.timeSpec("  "));
665 		assertEquals(-1, OpenSshConfigFile.timeSpec("s"));
666 		assertEquals(-1, OpenSshConfigFile.timeSpec("  s"));
667 		assertEquals(-1, OpenSshConfigFile.timeSpec(" +s"));
668 		assertEquals(-1, OpenSshConfigFile.timeSpec(" -s"));
669 		assertEquals(-1, OpenSshConfigFile.timeSpec("1ms"));
670 		assertEquals(600, OpenSshConfigFile.timeSpec("600"));
671 		assertEquals(600, OpenSshConfigFile.timeSpec("600s"));
672 		assertEquals(600, OpenSshConfigFile.timeSpec("  600s"));
673 		assertEquals(600, OpenSshConfigFile.timeSpec("  600s  "));
674 		assertEquals(600, OpenSshConfigFile.timeSpec("\t600s"));
675 		assertEquals(600, OpenSshConfigFile.timeSpec(" \t600  "));
676 		assertEquals(-1, OpenSshConfigFile.timeSpec("  600 s  "));
677 		assertEquals(-1, OpenSshConfigFile.timeSpec("600 s"));
678 		assertEquals(600, OpenSshConfigFile.timeSpec("10m"));
679 		assertEquals(5400, OpenSshConfigFile.timeSpec("1h30m"));
680 		assertEquals(5400, OpenSshConfigFile.timeSpec("1h 30m"));
681 		assertEquals(5400, OpenSshConfigFile.timeSpec("1h \t30m"));
682 		assertEquals(5400, OpenSshConfigFile.timeSpec("1h+30m"));
683 		assertEquals(5400, OpenSshConfigFile.timeSpec("1h +30m"));
684 		assertEquals(-1, OpenSshConfigFile.timeSpec("1h + 30m"));
685 		assertEquals(-1, OpenSshConfigFile.timeSpec("1h -30m"));
686 		assertEquals(3630, OpenSshConfigFile.timeSpec("1h30s"));
687 		assertEquals(5400, OpenSshConfigFile.timeSpec("30m 1h"));
688 		assertEquals(3600, OpenSshConfigFile.timeSpec("30m 30m"));
689 		assertEquals(60, OpenSshConfigFile.timeSpec("30 30"));
690 		assertEquals(0, OpenSshConfigFile.timeSpec("0"));
691 		assertEquals(1, OpenSshConfigFile.timeSpec("1"));
692 		assertEquals(1, OpenSshConfigFile.timeSpec("1S"));
693 		assertEquals(1, OpenSshConfigFile.timeSpec("1s"));
694 		assertEquals(60, OpenSshConfigFile.timeSpec("1M"));
695 		assertEquals(60, OpenSshConfigFile.timeSpec("1m"));
696 		assertEquals(3600, OpenSshConfigFile.timeSpec("1H"));
697 		assertEquals(3600, OpenSshConfigFile.timeSpec("1h"));
698 		assertEquals(86400, OpenSshConfigFile.timeSpec("1D"));
699 		assertEquals(86400, OpenSshConfigFile.timeSpec("1d"));
700 		assertEquals(604800, OpenSshConfigFile.timeSpec("1W"));
701 		assertEquals(604800, OpenSshConfigFile.timeSpec("1w"));
702 		assertEquals(172800, OpenSshConfigFile.timeSpec("2d"));
703 		assertEquals(604800, OpenSshConfigFile.timeSpec("1w"));
704 		assertEquals(604800 + 172800 + 3 * 3600 + 30 * 60 + 10,
705 				OpenSshConfigFile.timeSpec("1w2d3h30m10s"));
706 		assertEquals(-1, OpenSshConfigFile.timeSpec("-7"));
707 		assertEquals(-1, OpenSshConfigFile.timeSpec("-9d"));
708 		assertEquals(Integer.MAX_VALUE, OpenSshConfigFile
709 				.timeSpec(Integer.toString(Integer.MAX_VALUE)));
710 		assertEquals(-1, OpenSshConfigFile
711 				.timeSpec(Long.toString(Integer.MAX_VALUE + 1L)));
712 		assertEquals(-1, OpenSshConfigFile
713 				.timeSpec(Integer.toString(Integer.MAX_VALUE / 60 + 1) + 'M'));
714 		assertEquals(-1, OpenSshConfigFile.timeSpec("1000000000000000000000w"));
715 	}
716 }