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