View Javadoc
1   /*
2    * Copyright (C) 2009-2010, 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.http.test;
12  
13  import static org.hamcrest.MatcherAssert.assertThat;
14  import static org.hamcrest.Matchers.is;
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.assertNull;
19  import static org.junit.Assert.assertTrue;
20  import static org.junit.Assert.fail;
21  
22  import java.io.File;
23  import java.io.OutputStream;
24  import java.net.URI;
25  import java.net.URL;
26  import java.text.MessageFormat;
27  import java.time.Instant;
28  import java.util.List;
29  import java.util.Set;
30  
31  import javax.servlet.http.HttpServletRequest;
32  
33  import org.eclipse.jetty.servlet.DefaultServlet;
34  import org.eclipse.jetty.servlet.ServletContextHandler;
35  import org.eclipse.jetty.servlet.ServletHolder;
36  import org.eclipse.jgit.api.Git;
37  import org.eclipse.jgit.errors.NoRemoteRepositoryException;
38  import org.eclipse.jgit.errors.RepositoryNotFoundException;
39  import org.eclipse.jgit.errors.TransportException;
40  import org.eclipse.jgit.http.server.GitServlet;
41  import org.eclipse.jgit.internal.JGitText;
42  import org.eclipse.jgit.junit.TestRepository;
43  import org.eclipse.jgit.junit.http.AccessEvent;
44  import org.eclipse.jgit.junit.http.AppServer;
45  import org.eclipse.jgit.lib.Constants;
46  import org.eclipse.jgit.lib.Ref;
47  import org.eclipse.jgit.lib.RefUpdate;
48  import org.eclipse.jgit.lib.Repository;
49  import org.eclipse.jgit.lib.StoredConfig;
50  import org.eclipse.jgit.revwalk.RevCommit;
51  import org.eclipse.jgit.transport.FetchConnection;
52  import org.eclipse.jgit.transport.HttpTransport;
53  import org.eclipse.jgit.transport.PacketLineIn;
54  import org.eclipse.jgit.transport.PacketLineOut;
55  import org.eclipse.jgit.transport.Transport;
56  import org.eclipse.jgit.transport.URIish;
57  import org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider;
58  import org.eclipse.jgit.transport.http.HttpConnection;
59  import org.eclipse.jgit.transport.http.HttpConnectionFactory;
60  import org.junit.Before;
61  import org.junit.Test;
62  
63  public class HttpClientTests extends AllFactoriesHttpTestCase {
64  
65  	private TestRepository<Repository> remoteRepository;
66  
67  	private URIish dumbAuthNoneURI;
68  
69  	private URIish dumbAuthBasicURI;
70  
71  	private URIish smartAuthNoneURI;
72  
73  	private URIish smartAuthBasicURI;
74  
75  	public HttpClientTests(HttpConnectionFactory cf) {
76  		super(cf);
77  	}
78  
79  	@Override
80  	@Before
81  	public void setUp() throws Exception {
82  		super.setUp();
83  
84  		remoteRepository = createTestRepository();
85  		remoteRepository.update(master, remoteRepository.commit().create());
86  
87  		ServletContextHandler dNone = dumb("/dnone");
88  		ServletContextHandler dBasic = server.authBasic(dumb("/dbasic"));
89  
90  		ServletContextHandler sNone = smart("/snone");
91  		ServletContextHandler sBasic = server.authBasic(smart("/sbasic"));
92  
93  		server.setUp();
94  
95  		final String srcName = nameOf(remoteRepository.getRepository());
96  		dumbAuthNoneURI = toURIish(dNone, srcName);
97  		dumbAuthBasicURI = toURIish(dBasic, srcName);
98  
99  		smartAuthNoneURI = toURIish(sNone, srcName);
100 		smartAuthBasicURI = toURIish(sBasic, srcName);
101 	}
102 
103 	private ServletContextHandler dumb(String path) {
104 		final File srcGit = remoteRepository.getRepository().getDirectory();
105 		final URI base = srcGit.getParentFile().toURI();
106 
107 		ServletContextHandler ctx = server.addContext(path);
108 		ctx.setResourceBase(base.toString());
109 		ServletHolder holder = ctx.addServlet(DefaultServlet.class, "/");
110 		// The tmp directory is symlinked on OS X
111 		holder.setInitParameter("aliases", "true");
112 		return ctx;
113 	}
114 
115 	private ServletContextHandler smart(String path) {
116 		GitServlet gs = new GitServlet();
117 		gs.setRepositoryResolver((HttpServletRequest req, String name) -> {
118 			final Repository db = remoteRepository.getRepository();
119 			if (!name.equals(nameOf(db))) {
120 				throw new RepositoryNotFoundException(name);
121 			}
122 			db.incrementOpen();
123 			return db;
124 		});
125 
126 		ServletContextHandler ctx = server.addContext(path);
127 		ctx.addServlet(new ServletHolder(gs), "/*");
128 		return ctx;
129 	}
130 
131 	private static String nameOf(Repository db) {
132 		return db.getDirectory().getName();
133 	}
134 
135 	@Test
136 	public void testRepositoryNotFound_Dumb() throws Exception {
137 		URIish uri = toURIish("/dumb.none/not-found");
138 		Repository dst = createBareRepository();
139 		try (Transport t = Transport.open(dst, uri)) {
140 			try {
141 				t.openFetch();
142 				fail("connection opened to not found repository");
143 			} catch (NoRemoteRepositoryException err) {
144 				String exp = uri + ": " + uri
145 						+ "/info/refs?service=git-upload-pack not found";
146 				assertNotNull(err.getMessage());
147 				assertTrue("Unexpected error message",
148 						err.getMessage().startsWith(exp));
149 			}
150 		}
151 	}
152 
153 	@Test
154 	public void testRepositoryNotFound_Smart() throws Exception {
155 		URIish uri = toURIish("/smart.none/not-found");
156 		Repository dst = createBareRepository();
157 		try (Transport t = Transport.open(dst, uri)) {
158 			try {
159 				t.openFetch();
160 				fail("connection opened to not found repository");
161 			} catch (NoRemoteRepositoryException err) {
162 				String exp = uri + ": " + uri
163 						+ "/info/refs?service=git-upload-pack not found";
164 				assertNotNull(err.getMessage());
165 				assertTrue("Unexpected error message",
166 						err.getMessage().startsWith(exp));
167 			}
168 		}
169 	}
170 
171 	@Test
172 	public void testListRemote_Dumb_DetachedHEAD() throws Exception {
173 		Repository src = remoteRepository.getRepository();
174 		RefUpdate u = src.updateRef(Constants.HEAD, true);
175 		RevCommit Q = remoteRepository.commit().message("Q").create();
176 		u.setNewObjectId(Q);
177 		assertEquals(RefUpdate.Result.FORCED, u.forceUpdate());
178 
179 		Repository dst = createBareRepository();
180 		Ref head;
181 		try (Transport t = Transport.open(dst, dumbAuthNoneURI);
182 				FetchConnection c = t.openFetch()) {
183 			head = c.getRef(Constants.HEAD);
184 		}
185 		assertNotNull("has " + Constants.HEAD, head);
186 		assertEquals(Q, head.getObjectId());
187 	}
188 
189 	@Test
190 	public void testListRemote_Dumb_NoHEAD() throws Exception {
191 		Repository src = remoteRepository.getRepository();
192 		File headref = new File(src.getDirectory(), Constants.HEAD);
193 		assertTrue("HEAD used to be present", headref.delete());
194 		assertFalse("HEAD is gone", headref.exists());
195 
196 		Repository dst = createBareRepository();
197 		Ref head;
198 		try (Transport t = Transport.open(dst, dumbAuthNoneURI);
199 				FetchConnection c = t.openFetch()) {
200 			head = c.getRef(Constants.HEAD);
201 		}
202 		assertNull("has no " + Constants.HEAD, head);
203 	}
204 
205 	@Test
206 	public void testListRemote_Smart_DetachedHEAD() throws Exception {
207 		Repository src = remoteRepository.getRepository();
208 		RefUpdate u = src.updateRef(Constants.HEAD, true);
209 		RevCommit Q = remoteRepository.commit().message("Q").create();
210 		u.setNewObjectId(Q);
211 		assertEquals(RefUpdate.Result.FORCED, u.forceUpdate());
212 
213 		Repository dst = createBareRepository();
214 		Ref head;
215 		try (Transport t = Transport.open(dst, smartAuthNoneURI);
216 				FetchConnection c = t.openFetch()) {
217 			head = c.getRef(Constants.HEAD);
218 		}
219 		assertNotNull("has " + Constants.HEAD, head);
220 		assertEquals(Q, head.getObjectId());
221 	}
222 
223 	@Test
224 	public void testListRemote_Smart_WithQueryParameters() throws Exception {
225 		URIish myURI = toURIish("/snone/do?r=1&p=test.git");
226 		Repository dst = createBareRepository();
227 		try (Transport t = Transport.open(dst, myURI)) {
228 			try {
229 				t.openFetch();
230 				fail("test did not fail to find repository as expected");
231 			} catch (NoRemoteRepositoryException err) {
232 				// expected
233 			}
234 		}
235 
236 		List<AccessEvent> requests = getRequests();
237 		assertEquals(1, requests.size());
238 
239 		AccessEvent info = requests.get(0);
240 		assertEquals("GET", info.getMethod());
241 		assertEquals("/snone/do", info.getPath());
242 		assertEquals(3, info.getParameters().size());
243 		assertEquals("1", info.getParameter("r"));
244 		assertEquals("test.git/info/refs", info.getParameter("p"));
245 		assertEquals("git-upload-pack", info.getParameter("service"));
246 		assertEquals(404, info.getStatus());
247 	}
248 
249 	@Test
250 	public void testListRemote_Dumb_NeedsAuth() throws Exception {
251 		Repository dst = createBareRepository();
252 		try (Transport t = Transport.open(dst, dumbAuthBasicURI)) {
253 			try {
254 				t.openFetch();
255 				fail("connection opened even info/refs needs auth basic");
256 			} catch (TransportException err) {
257 				String exp = dumbAuthBasicURI + ": "
258 						+ JGitText.get().noCredentialsProvider;
259 				assertEquals(exp, err.getMessage());
260 			}
261 		}
262 	}
263 
264 	@Test
265 	public void testListRemote_Dumb_Auth() throws Exception {
266 		Repository dst = createBareRepository();
267 		try (Transport t = Transport.open(dst, dumbAuthBasicURI)) {
268 			t.setCredentialsProvider(new UsernamePasswordCredentialsProvider(
269 					AppServer.username, AppServer.password));
270 			t.openFetch().close();
271 		}
272 		try (Transport t = Transport.open(dst, dumbAuthBasicURI)) {
273 			t.setCredentialsProvider(new UsernamePasswordCredentialsProvider(
274 					AppServer.username, ""));
275 			try {
276 				t.openFetch();
277 				fail("connection opened even info/refs needs auth basic and we provide wrong password");
278 			} catch (TransportException err) {
279 				String exp = dumbAuthBasicURI + ": "
280 						+ JGitText.get().notAuthorized;
281 				assertEquals(exp, err.getMessage());
282 			}
283 		}
284 	}
285 
286 	@Test
287 	public void testListRemote_Smart_UploadPackNeedsAuth() throws Exception {
288 		Repository dst = createBareRepository();
289 		try (Transport t = Transport.open(dst, smartAuthBasicURI)) {
290 			try {
291 				t.openFetch();
292 				fail("connection opened even though service disabled");
293 			} catch (TransportException err) {
294 				String exp = smartAuthBasicURI + ": "
295 						+ JGitText.get().noCredentialsProvider;
296 				assertEquals(exp, err.getMessage());
297 			}
298 		}
299 	}
300 
301 	@Test
302 	public void testListRemote_Smart_UploadPackDisabled() throws Exception {
303 		Repository src = remoteRepository.getRepository();
304 		final StoredConfig cfg = src.getConfig();
305 		cfg.setBoolean("http", null, "uploadpack", false);
306 		cfg.save();
307 
308 		Repository dst = createBareRepository();
309 		try (Transport t = Transport.open(dst, smartAuthNoneURI)) {
310 			try {
311 				t.openFetch();
312 				fail("connection opened even though service disabled");
313 			} catch (TransportException err) {
314 				String exp = smartAuthNoneURI + ": "
315 						+ MessageFormat.format(
316 								JGitText.get().serviceNotPermitted,
317 								smartAuthNoneURI.toString() + "/",
318 								"git-upload-pack");
319 				assertEquals(exp, err.getMessage());
320 			}
321 		}
322 	}
323 
324 	@Test
325 	public void testListRemoteWithoutLocalRepository() throws Exception {
326 		try (Transport t = Transport.open(smartAuthNoneURI);
327 				FetchConnection c = t.openFetch()) {
328 			Ref head = c.getRef(Constants.HEAD);
329 			assertNotNull(head);
330 		}
331 	}
332 
333 	@Test
334 	public void testHttpClientWantsV2AndServerNotConfigured() throws Exception {
335 		String url = smartAuthNoneURI.toString() + "/info/refs?service=git-upload-pack";
336 		HttpConnection c = HttpTransport.getConnectionFactory()
337 				.create(new URL(url));
338 		c.setRequestMethod("GET");
339 		c.setRequestProperty("Git-Protocol", "version=2");
340 		assertEquals(200, c.getResponseCode());
341 
342 		PacketLineIn pckIn = new PacketLineIn(c.getInputStream());
343 		assertThat(pckIn.readString(), is("version 2"));
344 	}
345 
346 	@Test
347 	public void testHttpServerConfiguredToV0() throws Exception {
348 		remoteRepository.getRepository().getConfig().setInt(
349 			"protocol", null, "version", 0);
350 		String url = smartAuthNoneURI.toString() + "/info/refs?service=git-upload-pack";
351 		HttpConnection c = HttpTransport.getConnectionFactory()
352 				.create(new URL(url));
353 		c.setRequestMethod("GET");
354 		c.setRequestProperty("Git-Protocol", "version=2");
355 		assertEquals(200, c.getResponseCode());
356 
357 		PacketLineIn pckIn = new PacketLineIn(c.getInputStream());
358 
359 		// Check that we get a v0 response.
360 		assertThat(pckIn.readString(), is("# service=git-upload-pack"));
361 		assertTrue(PacketLineIn.isEnd(pckIn.readString()));
362 		assertTrue(pckIn.readString().matches("[0-9a-f]{40} HEAD.*"));
363 	}
364 
365 	@Test
366 	public void testV2HttpFirstResponse() throws Exception {
367 		String url = smartAuthNoneURI.toString() + "/info/refs?service=git-upload-pack";
368 		HttpConnection c = HttpTransport.getConnectionFactory()
369 				.create(new URL(url));
370 		c.setRequestMethod("GET");
371 		c.setRequestProperty("Git-Protocol", "version=2");
372 		assertEquals(200, c.getResponseCode());
373 
374 		PacketLineIn pckIn = new PacketLineIn(c.getInputStream());
375 		assertThat(pckIn.readString(), is("version 2"));
376 
377 		// What remains are capabilities - ensure that all of them are
378 		// non-empty strings, and that we see END at the end.
379 		for (String s : pckIn.readStrings()) {
380 			assertTrue(!s.isEmpty());
381 		}
382 	}
383 
384 	@Test
385 	public void testV2HttpSubsequentResponse() throws Exception {
386 		String url = smartAuthNoneURI.toString() + "/git-upload-pack";
387 		HttpConnection c = HttpTransport.getConnectionFactory()
388 				.create(new URL(url));
389 		c.setRequestMethod("POST");
390 		c.setRequestProperty("Content-Type", "application/x-git-upload-pack-request");
391 		c.setRequestProperty("Git-Protocol", "version=2");
392 		c.setDoOutput(true);
393 
394 		// Test ls-refs to verify that everything is connected
395 		// properly. Tests for other commands go in
396 		// UploadPackTest.java.
397 
398 		try (OutputStream os = c.getOutputStream()) {
399 			PacketLineOut pckOut = new PacketLineOut(os);
400 			pckOut.writeString("command=ls-refs");
401 			pckOut.writeDelim();
402 			pckOut.end();
403 		}
404 
405 		PacketLineIn pckIn = new PacketLineIn(c.getInputStream());
406 
407 		// Just check that we get what looks like a ref advertisement.
408 		for (String s : pckIn.readStrings()) {
409 			assertTrue(s.matches("[0-9a-f]{40} [A-Za-z/]*"));
410 		}
411 
412 		assertEquals(200, c.getResponseCode());
413 	}
414 
415 	@Test
416 	public void testCloneWithDepth() throws Exception {
417 		remoteRepository.getRepository().getConfig().setInt(
418 				"protocol", null, "version", 0);
419 		File directory = createTempDirectory("testCloneWithDepth");
420 		Git git = Git.cloneRepository()
421 					 .setDirectory(directory)
422 					 .setDepth(1)
423 					 .setURI(smartAuthNoneURI.toString())
424 					 .call();
425 
426 		assertEquals(Set.of(git.getRepository().resolve(Constants.HEAD)), git.getRepository().getObjectDatabase().getShallowCommits());
427 	}
428 
429 	@Test
430 	public void testCloneWithDeepenSince() throws Exception {
431 		remoteRepository.getRepository().getConfig().setInt(
432 				"protocol", null, "version", 0);
433 		RevCommit commit = remoteRepository.commit()
434 										   .parent(remoteRepository.git().log().call().iterator().next())
435 										   .message("Test")
436 										   .add("test.txt", "Hello world")
437 										   .create();
438 		remoteRepository.update(master, commit);
439 
440 		File directory = createTempDirectory("testCloneWithDeepenSince");
441 		Git git = Git.cloneRepository()
442 					 .setDirectory(directory)
443 					 .setShallowSince(Instant.ofEpochSecond(commit.getCommitTime()))
444 					 .setURI(smartAuthNoneURI.toString())
445 					 .call();
446 
447 		assertEquals(Set.of(git.getRepository().resolve(Constants.HEAD)), git.getRepository().getObjectDatabase().getShallowCommits());
448 	}
449 
450 	@Test
451 	public void testCloneWithDeepenNot() throws Exception {
452 		remoteRepository.getRepository().getConfig().setInt(
453 				"protocol", null, "version", 0);
454 		RevCommit commit = remoteRepository.git().log().call().iterator().next();
455 		remoteRepository.update(master, remoteRepository.commit()
456 														.parent(commit)
457 														.message("Test")
458 														.add("test.txt", "Hello world")
459 														.create());
460 
461 		File directory = createTempDirectory("testCloneWithDeepenNot");
462 		Git git = Git.cloneRepository()
463 					 .setDirectory(directory)
464 					 .addShallowExclude(commit.getId())
465 					 .setURI(smartAuthNoneURI.toString())
466 					 .call();
467 
468 		assertEquals(Set.of(git.getRepository().resolve(Constants.HEAD)), git.getRepository().getObjectDatabase().getShallowCommits());
469 	}
470 
471     @Test
472 	public void testV2CloneWithDepth() throws Exception {
473         File directory = createTempDirectory("testV2CloneWithDepth");
474         Git git = Git.cloneRepository()
475                      .setDirectory(directory)
476                      .setDepth(1)
477                      .setURI(smartAuthNoneURI.toString())
478                      .call();
479 
480         assertEquals(Set.of(git.getRepository().resolve(Constants.HEAD)), git.getRepository().getObjectDatabase().getShallowCommits());
481     }
482 
483     @Test
484     public void testV2CloneWithDeepenSince() throws Exception {
485         RevCommit commit = remoteRepository.commit()
486                                            .parent(remoteRepository.git().log().call().iterator().next())
487                                            .message("Test")
488                                            .add("test.txt", "Hello world")
489                                            .create();
490         remoteRepository.update(master, commit);
491 
492         File directory = createTempDirectory("testV2CloneWithDeepenSince");
493         Git git = Git.cloneRepository()
494                      .setDirectory(directory)
495                      .setShallowSince(Instant.ofEpochSecond(commit.getCommitTime()))
496                      .setURI(smartAuthNoneURI.toString())
497                      .call();
498 
499 		assertEquals(Set.of(git.getRepository().resolve(Constants.HEAD)), git.getRepository().getObjectDatabase().getShallowCommits());
500     }
501 
502     @Test
503     public void testV2CloneWithDeepenNot() throws Exception {
504         RevCommit commit = remoteRepository.git().log().call().iterator().next();
505         remoteRepository.update(master, remoteRepository.commit()
506                                                         .parent(commit)
507                                                         .message("Test")
508                                                         .add("test.txt", "Hello world")
509                                                         .create());
510 
511         File directory = createTempDirectory("testV2CloneWithDeepenNot");
512         Git git = Git.cloneRepository()
513                      .setDirectory(directory)
514                      .addShallowExclude(commit.getId())
515                      .setURI(smartAuthNoneURI.toString())
516                      .call();
517 
518 		assertEquals(Set.of(git.getRepository().resolve(Constants.HEAD)), git.getRepository().getObjectDatabase().getShallowCommits());
519     }
520 }