diff --git a/org.eclipse.jgit.test/META-INF/MANIFEST.MF b/org.eclipse.jgit.test/META-INF/MANIFEST.MF index 7ec5e6175..1c6211d53 100644 --- a/org.eclipse.jgit.test/META-INF/MANIFEST.MF +++ b/org.eclipse.jgit.test/META-INF/MANIFEST.MF @@ -35,6 +35,7 @@ Import-Package: com.googlecode.javaewah;version="[1.1.6,2.0.0)", org.eclipse.jgit.internal.storage.pack;version="[5.4.0,5.5.0)", org.eclipse.jgit.internal.storage.reftable;version="[5.4.0,5.5.0)", org.eclipse.jgit.internal.storage.reftree;version="[5.4.0,5.5.0)", + org.eclipse.jgit.internal.transport.http;version="[5.4.0,5.5.0)", org.eclipse.jgit.internal.transport.parser;version="[5.4.0,5.5.0)", org.eclipse.jgit.junit;version="[5.4.0,5.5.0)", org.eclipse.jgit.junit.ssh;version="[5.4.0,5.5.0)", diff --git a/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/internal/transport/http/cookies-invalid.txt b/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/internal/transport/http/cookies-invalid.txt new file mode 100644 index 000000000..bbc6a7329 --- /dev/null +++ b/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/internal/transport/http/cookies-invalid.txt @@ -0,0 +1 @@ +some-domain /some/path1 FALSE 0 key1 value1 \ No newline at end of file diff --git a/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/internal/transport/http/cookies-simple1.txt b/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/internal/transport/http/cookies-simple1.txt new file mode 100644 index 000000000..e06b38c71 --- /dev/null +++ b/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/internal/transport/http/cookies-simple1.txt @@ -0,0 +1,2 @@ +some-domain1 TRUE /some/path1 FALSE 1893499200000 key1 valueFromSimple1 +some-domain1 TRUE /some/path1 FALSE 1893499200000 key2 valueFromSimple1 \ No newline at end of file diff --git a/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/internal/transport/http/cookies-simple2.txt b/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/internal/transport/http/cookies-simple2.txt new file mode 100644 index 000000000..4bf6723fd --- /dev/null +++ b/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/internal/transport/http/cookies-simple2.txt @@ -0,0 +1,2 @@ +some-domain1 TRUE /some/path1 FALSE 1893499200000 key1 valueFromSimple2 +some-domain1 TRUE /some/path1 FALSE 1893499200000 key3 valueFromSimple2 \ No newline at end of file diff --git a/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/internal/transport/http/cookies-with-empty-and-comment-lines.txt b/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/internal/transport/http/cookies-with-empty-and-comment-lines.txt new file mode 100644 index 000000000..a9b8a2815 --- /dev/null +++ b/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/internal/transport/http/cookies-with-empty-and-comment-lines.txt @@ -0,0 +1,8 @@ +# first line is a comment +# the next cookie is supposed to be removed, because it has expired already +some-domain1 TRUE /some/path1 FALSE 0 key1 value1 + +# expires date is 01/01/2030 @ 12:00am (UTC) +#HttpOnly_.some-domain2 TRUE /some/path2 TRUE 1893499200000 key2 value2 + +some-domain3 TRUE /some/path3 FALSE 1893499200000 key3 value3 \ No newline at end of file diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/transport/http/NetscapeCookieFileTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/transport/http/NetscapeCookieFileTest.java new file mode 100644 index 000000000..8f6cd3aae --- /dev/null +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/transport/http/NetscapeCookieFileTest.java @@ -0,0 +1,441 @@ +/* + * Copyright (C) 2018, Konrad Windszus + * and other copyright owners as documented in the project's IP log. + * + * This program and the accompanying materials are made available + * under the terms of the Eclipse Distribution License v1.0 which + * accompanies this distribution, is reproduced below, and is + * available at http://www.eclipse.org/org/documents/edl-v10.php + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or + * without modification, are permitted provided that the following + * conditions are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * + * - Neither the name of the Eclipse Foundation, Inc. nor the + * names of its contributors may be used to endorse or promote + * products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.eclipse.jgit.internal.transport.http; + +import java.io.IOException; +import java.io.InputStream; +import java.io.Writer; +import java.net.HttpCookie; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.time.Instant; +import java.util.Arrays; +import java.util.Date; +import java.util.LinkedHashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Set; +import java.util.regex.Pattern; + +import org.eclipse.jgit.internal.storage.file.LockFile; +import org.eclipse.jgit.internal.transport.http.NetscapeCookieFile; +import org.hamcrest.CoreMatchers; +import org.hamcrest.Description; +import org.hamcrest.Matcher; +import org.hamcrest.TypeSafeMatcher; +import org.hamcrest.collection.IsIterableContainingInOrder; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +public class NetscapeCookieFileTest { + + @Rule + public TemporaryFolder folder = new TemporaryFolder(); + + private Path tmpFile; + + private URL baseUrl; + + /** + * This is the expiration date that is used in the test cookie files + */ + private static long JAN_01_2030_NOON = Instant + .parse("2030-01-01T12:00:00.000Z").toEpochMilli(); + + @Before + public void setUp() throws IOException { + // this will not only return a new file name but also create new empty + // file! + tmpFile = folder.newFile().toPath(); + baseUrl = new URL("http://domain.com/my/path"); + } + + @Test + public void testMergeCookies() { + Set cookieSet1 = new LinkedHashSet<>(); + HttpCookie cookie = new HttpCookie("key1", "valueFromSet1"); + cookieSet1.add(cookie); + cookie = new HttpCookie("key2", "valueFromSet1"); + cookieSet1.add(cookie); + + Set cookieSet2 = new LinkedHashSet<>(); + cookie = new HttpCookie("key1", "valueFromSet2"); + cookieSet2.add(cookie); + cookie = new HttpCookie("key3", "valueFromSet2"); + cookieSet2.add(cookie); + + Set cookiesExpectedMergedSet = new LinkedHashSet<>(); + cookie = new HttpCookie("key1", "valueFromSet1"); + cookiesExpectedMergedSet.add(cookie); + cookie = new HttpCookie("key2", "valueFromSet1"); + cookiesExpectedMergedSet.add(cookie); + cookie = new HttpCookie("key3", "valueFromSet2"); + cookiesExpectedMergedSet.add(cookie); + + Assert.assertThat( + NetscapeCookieFile.mergeCookies(cookieSet1, cookieSet2), + HttpCookiesMatcher.containsInOrder(cookiesExpectedMergedSet)); + + Assert.assertThat(NetscapeCookieFile.mergeCookies(cookieSet1, null), + HttpCookiesMatcher.containsInOrder(cookieSet1)); + } + + @Test + public void testWriteToNewFile() throws IOException { + Set cookies = new LinkedHashSet<>(); + cookies.add(new HttpCookie("key1", "value")); + // first cookie is a session cookie (and should be ignored) + + HttpCookie cookie = new HttpCookie("key2", "value"); + cookie.setSecure(true); + cookie.setDomain("mydomain.com"); + cookie.setPath("/"); + cookie.setMaxAge(1000); + cookies.add(cookie); + Date creationDate = new Date(); + try (Writer writer = Files.newBufferedWriter(tmpFile, + StandardCharsets.US_ASCII)) { + NetscapeCookieFile.write(writer, cookies, baseUrl, creationDate); + } + + String expectedExpiration = String + .valueOf(creationDate.getTime() + (cookie.getMaxAge() * 1000)); + + Assert.assertThat( + Files.readAllLines(tmpFile, StandardCharsets.US_ASCII), + CoreMatchers + .equalTo(Arrays.asList("mydomain.com\tTRUE\t/\tTRUE\t" + + expectedExpiration + "\tkey2\tvalue"))); + } + + @Test + public void testWriteToExistingFile() throws IOException { + try (InputStream input = this.getClass() + .getResourceAsStream("cookies-simple1.txt")) { + Files.copy(input, tmpFile, StandardCopyOption.REPLACE_EXISTING); + } + + Set cookies = new LinkedHashSet<>(); + HttpCookie cookie = new HttpCookie("key2", "value2"); + cookie.setMaxAge(1000); + cookies.add(cookie); + Date creationDate = new Date(); + try (Writer writer = Files.newBufferedWriter(tmpFile, + StandardCharsets.US_ASCII)) { + NetscapeCookieFile.write(writer, cookies, baseUrl, creationDate); + } + String expectedExpiration = String + .valueOf(creationDate.getTime() + (cookie.getMaxAge() * 1000)); + + Assert.assertThat( + Files.readAllLines(tmpFile, StandardCharsets.US_ASCII), + CoreMatchers.equalTo( + Arrays.asList("domain.com\tTRUE\t/my/path\tFALSE\t" + + expectedExpiration + "\tkey2\tvalue2"))); + } + + @Test(expected = IOException.class) + public void testWriteWhileSomeoneIsHoldingTheLock() + throws IllegalArgumentException, IOException, InterruptedException { + try (InputStream input = this.getClass() + .getResourceAsStream("cookies-simple1.txt")) { + Files.copy(input, tmpFile, StandardCopyOption.REPLACE_EXISTING); + } + NetscapeCookieFile cookieFile = new NetscapeCookieFile(tmpFile); + // now imitate another process/thread holding the lock file + LockFile lockFile = new LockFile(tmpFile.toFile()); + try { + Assert.assertTrue("Could not acquire lock", lockFile.lock()); + cookieFile.write(baseUrl); + } finally { + lockFile.unlock(); + } + } + + @Test + public void testWriteAfterAnotherJgitProcessModifiedTheFile() + throws IOException, InterruptedException { + try (InputStream input = this.getClass() + .getResourceAsStream("cookies-simple1.txt")) { + Files.copy(input, tmpFile, StandardCopyOption.REPLACE_EXISTING); + } + NetscapeCookieFile cookieFile = new NetscapeCookieFile(tmpFile); + cookieFile.getCookies(true); + // now modify file externally + try (InputStream input = this.getClass() + .getResourceAsStream("cookies-simple2.txt")) { + Files.copy(input, tmpFile, StandardCopyOption.REPLACE_EXISTING); + } + // now try to write + cookieFile.write(baseUrl); + + // validate that the external changes are there as well + // due to rounding errors (conversion from ms to sec to ms) + // the expiration date might not be exact + List lines = Files.readAllLines(tmpFile, + StandardCharsets.US_ASCII); + + Assert.assertEquals("Expected 3 lines", 3, lines.size()); + assertStringMatchesPatternWithInexactNumber(lines.get(0), + "some-domain1\tTRUE\t/some/path1\tFALSE\t(\\d*)\tkey1\tvalueFromSimple2", + JAN_01_2030_NOON, 1000); + assertStringMatchesPatternWithInexactNumber(lines.get(1), + "some-domain1\tTRUE\t/some/path1\tFALSE\t(\\d*)\tkey3\tvalueFromSimple2", + JAN_01_2030_NOON, 1000); + assertStringMatchesPatternWithInexactNumber(lines.get(2), + "some-domain1\tTRUE\t/some/path1\tFALSE\t(\\d*)\tkey2\tvalueFromSimple1", + JAN_01_2030_NOON, 1000); + } + + @SuppressWarnings("boxing") + private static final void assertStringMatchesPatternWithInexactNumber( + String string, String pattern, long expectedNumericValue, + long delta) { + java.util.regex.Matcher matcher = Pattern.compile(pattern) + .matcher(string); + Assert.assertTrue("Given string '" + string + "' does not match '" + + pattern + "'", matcher.matches()); + // extract numeric value + Long actualNumericValue = Long.decode(matcher.group(1)); + + Assert.assertTrue( + "Value is supposed to be close to " + expectedNumericValue + + " but is " + actualNumericValue + ".", + Math.abs(expectedNumericValue - actualNumericValue) <= delta); + } + + @Test + public void testWriteAndReadCycle() throws IOException { + Set cookies = new LinkedHashSet<>(); + + HttpCookie cookie = new HttpCookie("key1", "value1"); + cookie.setPath("/some/path1"); + cookie.setDomain("some-domain1"); + cookie.setMaxAge(1000); + cookies.add(cookie); + cookie = new HttpCookie("key2", "value2"); + cookie.setSecure(true); + cookie.setPath("/some/path2"); + cookie.setDomain("some-domain2"); + cookie.setMaxAge(1000); + cookie.setHttpOnly(true); + cookies.add(cookie); + + Date creationDate = new Date(); + + try (Writer writer = Files.newBufferedWriter(tmpFile, + StandardCharsets.US_ASCII)) { + NetscapeCookieFile.write(writer, cookies, baseUrl, creationDate); + } + Set actualCookies = new NetscapeCookieFile(tmpFile, + creationDate).getCookies(true); + Assert.assertThat(actualCookies, + HttpCookiesMatcher.containsInOrder(cookies)); + } + + @Test + public void testReadAndWriteCycle() throws IOException { + try (InputStream input = this.getClass() + .getResourceAsStream("cookies-simple1.txt")) { + Files.copy(input, tmpFile, StandardCopyOption.REPLACE_EXISTING); + } + // round up to the next second (to prevent rounding errors) + Date creationDate = new Date( + (System.currentTimeMillis() / 1000) * 1000); + Set cookies = new NetscapeCookieFile(tmpFile, creationDate) + .getCookies(true); + Path tmpFile2 = folder.newFile().toPath(); + try (Writer writer = Files.newBufferedWriter(tmpFile2, + StandardCharsets.US_ASCII)) { + NetscapeCookieFile.write(writer, cookies, baseUrl, creationDate); + } + // compare original file with newly written one, they should not differ + Assert.assertEquals(Files.readAllLines(tmpFile), + Files.readAllLines(tmpFile2)); + } + + @Test + public void testReadWithEmptyAndCommentLines() throws IOException { + try (InputStream input = this.getClass().getResourceAsStream( + "cookies-with-empty-and-comment-lines.txt")) { + Files.copy(input, tmpFile, StandardCopyOption.REPLACE_EXISTING); + } + + Date creationDate = new Date(); + Set cookies = new LinkedHashSet<>(); + + HttpCookie cookie = new HttpCookie("key2", "value2"); + cookie.setDomain("some-domain2"); + cookie.setPath("/some/path2"); + cookie.setMaxAge((JAN_01_2030_NOON - creationDate.getTime()) / 1000); + cookie.setSecure(true); + cookie.setHttpOnly(true); + cookies.add(cookie); + + cookie = new HttpCookie("key3", "value3"); + cookie.setDomain("some-domain3"); + cookie.setPath("/some/path3"); + cookie.setMaxAge((JAN_01_2030_NOON - creationDate.getTime()) / 1000); + cookies.add(cookie); + + Set actualCookies = new NetscapeCookieFile(tmpFile, creationDate) + .getCookies(true); + Assert.assertThat(actualCookies, + HttpCookiesMatcher.containsInOrder(cookies)); + } + + @Test + public void testReadInvalidFile() throws IOException { + try (InputStream input = this.getClass() + .getResourceAsStream("cookies-invalid.txt")) { + Files.copy(input, tmpFile, StandardCopyOption.REPLACE_EXISTING); + } + + new NetscapeCookieFile(tmpFile) + .getCookies(true); + } + + public final static class HttpCookiesMatcher { + public static Matcher> containsInOrder( + Iterable expectedCookies) { + return containsInOrder(expectedCookies, 0); + } + + public static Matcher> containsInOrder( + Iterable expectedCookies, int allowedMaxAgeDelta) { + final List> cookieMatchers = new LinkedList<>(); + for (HttpCookie cookie : expectedCookies) { + cookieMatchers + .add(new HttpCookieMatcher(cookie, allowedMaxAgeDelta)); + } + return new IsIterableContainingInOrder<>(cookieMatchers); + } + } + + /** + * The default {@link HttpCookie#equals(Object)} is not good enough for + * testing purposes. Also the {@link HttpCookie#toString()} only emits some + * of the cookie attributes. For testing a dedicated matcher is needed which + * takes into account all attributes. + */ + private final static class HttpCookieMatcher + extends TypeSafeMatcher { + + private final HttpCookie cookie; + + private final int allowedMaxAgeDelta; + + public HttpCookieMatcher(HttpCookie cookie, int allowedMaxAgeDelta) { + this.cookie = cookie; + this.allowedMaxAgeDelta = allowedMaxAgeDelta; + } + + @Override + public void describeTo(Description description) { + describeCookie(description, cookie); + } + + @Override + protected void describeMismatchSafely(HttpCookie item, + Description mismatchDescription) { + mismatchDescription.appendText("was "); + describeCookie(mismatchDescription, item); + } + + @Override + protected boolean matchesSafely(HttpCookie otherCookie) { + // the equals method in HttpCookie is not specific enough, we want + // to consider all attributes! + return (equals(cookie.getName(), otherCookie.getName()) + && equals(cookie.getValue(), otherCookie.getValue()) + && equals(cookie.getDomain(), otherCookie.getDomain()) + && equals(cookie.getPath(), otherCookie.getPath()) + && (cookie.getMaxAge() >= otherCookie.getMaxAge() + - allowedMaxAgeDelta) + && (cookie.getMaxAge() <= otherCookie.getMaxAge() + + allowedMaxAgeDelta) + && cookie.isHttpOnly() == otherCookie.isHttpOnly() + && cookie.getSecure() == otherCookie.getSecure() + && cookie.getVersion() == otherCookie.getVersion()); + } + + private static boolean equals(String value1, String value2) { + if (value1 == null && value2 == null) { + return true; + } + if (value1 == null || value2 == null) { + return false; + } + return value1.equals(value2); + } + + @SuppressWarnings("boxing") + protected static void describeCookie(Description description, + HttpCookie cookie) { + description.appendText("HttpCookie["); + description.appendText("name: ").appendValue(cookie.getName()) + .appendText(", "); + description.appendText("value: ").appendValue(cookie.getValue()) + .appendText(", "); + description.appendText("domain: ").appendValue(cookie.getDomain()) + .appendText(", "); + description.appendText("path: ").appendValue(cookie.getPath()) + .appendText(", "); + description.appendText("maxAge: ").appendValue(cookie.getMaxAge()) + .appendText(", "); + description.appendText("httpOnly: ") + .appendValue(cookie.isHttpOnly()).appendText(", "); + description.appendText("secure: ").appendValue(cookie.getSecure()) + .appendText(", "); + description.appendText("version: ").appendValue(cookie.getVersion()) + .appendText(", "); + description.appendText("]"); + } + } +} diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/TransportHttpTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/TransportHttpTest.java new file mode 100644 index 000000000..111c92523 --- /dev/null +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/TransportHttpTest.java @@ -0,0 +1,189 @@ +/* + * Copyright (C) 2018, Konrad Windszus + * and other copyright owners as documented in the project's IP log. + * + * This program and the accompanying materials are made available + * under the terms of the Eclipse Distribution License v1.0 which + * accompanies this distribution, is reproduced below, and is + * available at http://www.eclipse.org/org/documents/edl-v10.php + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or + * without modification, are permitted provided that the following + * conditions are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * + * - Neither the name of the Eclipse Foundation, Inc. nor the + * names of its contributors may be used to endorse or promote + * products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.eclipse.jgit.transport; + +import java.io.File; +import java.io.IOException; +import java.net.HttpCookie; +import java.time.Instant; +import java.util.Arrays; +import java.util.Collections; +import java.util.Date; +import java.util.LinkedHashSet; +import java.util.Set; + +import org.eclipse.jgit.internal.transport.http.NetscapeCookieFile; +import org.eclipse.jgit.internal.transport.http.NetscapeCookieFileTest.HttpCookiesMatcher; +import org.eclipse.jgit.lib.Config; +import org.eclipse.jgit.test.resources.SampleDataRepositoryTestCase; +import org.eclipse.jgit.transport.http.HttpConnection; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.mockito.ArgumentMatchers; +import org.mockito.Mockito; + +public class TransportHttpTest extends SampleDataRepositoryTestCase { + private URIish uri; + private File cookieFile; + + @Override + @Before + public void setUp() throws Exception { + super.setUp(); + uri = new URIish("https://everyones.loves.git/u/2"); + + final Config config = db.getConfig(); + config.setBoolean("http", null, "saveCookies", true); + cookieFile = createTempFile(); + config.setString("http", null, "cookieFile", + cookieFile.getAbsolutePath()); + } + + @Test + public void testMatchesCookieDomain() { + Assert.assertTrue(TransportHttp.matchesCookieDomain("example.com", + "example.com")); + Assert.assertTrue(TransportHttp.matchesCookieDomain("Example.Com", + "example.cOM")); + Assert.assertTrue(TransportHttp.matchesCookieDomain( + "some.subdomain.example.com", "example.com")); + Assert.assertFalse(TransportHttp + .matchesCookieDomain("someotherexample.com", "example.com")); + Assert.assertFalse(TransportHttp.matchesCookieDomain("example.com", + "example1.com")); + Assert.assertFalse(TransportHttp + .matchesCookieDomain("sub.sub.example.com", ".example.com")); + Assert.assertTrue(TransportHttp.matchesCookieDomain("host.example.com", + "example.com")); + Assert.assertTrue(TransportHttp.matchesCookieDomain( + "something.example.com", "something.example.com")); + Assert.assertTrue(TransportHttp.matchesCookieDomain( + "host.something.example.com", "something.example.com")); + } + + @Test + public void testMatchesCookiePath() { + Assert.assertTrue( + TransportHttp.matchesCookiePath("/some/path", "/some/path")); + Assert.assertTrue(TransportHttp.matchesCookiePath("/some/path/child", + "/some/path")); + Assert.assertTrue(TransportHttp.matchesCookiePath("/some/path/child", + "/some/path/")); + Assert.assertFalse(TransportHttp.matchesCookiePath("/some/pathother", + "/some/path")); + Assert.assertFalse( + TransportHttp.matchesCookiePath("otherpath", "/some/path")); + } + + @Test + public void testProcessResponseCookies() throws IOException { + HttpConnection connection = Mockito.mock(HttpConnection.class); + Mockito.when( + connection.getHeaderFields(ArgumentMatchers.eq("Set-Cookie"))) + .thenReturn(Arrays.asList( + "id=a3fWa; Expires=Fri, 01 Jan 2100 11:00:00 GMT; Secure; HttpOnly", + "sessionid=38afes7a8; HttpOnly; Path=/")); + Mockito.when( + connection.getHeaderFields(ArgumentMatchers.eq("Set-Cookie2"))) + .thenReturn(Collections + .singletonList("cookie2=some value; Max-Age=1234; Path=/")); + + try (TransportHttp transportHttp = new TransportHttp(db, uri)) { + Date creationDate = new Date(); + transportHttp.processResponseCookies(connection); + + // evaluate written cookie file + Set expectedCookies = new LinkedHashSet<>(); + + HttpCookie cookie = new HttpCookie("id", "a3fWa"); + cookie.setDomain("everyones.loves.git"); + cookie.setPath("/u/2/"); + + cookie.setMaxAge( + (Instant.parse("2100-01-01T11:00:00.000Z").toEpochMilli() + - creationDate.getTime()) / 1000); + cookie.setSecure(true); + cookie.setHttpOnly(true); + expectedCookies.add(cookie); + + cookie = new HttpCookie("cookie2", "some value"); + cookie.setDomain("everyones.loves.git"); + cookie.setPath("/"); + cookie.setMaxAge(1234); + expectedCookies.add(cookie); + + Assert.assertThat( + new NetscapeCookieFile(cookieFile.toPath()) + .getCookies(true), + HttpCookiesMatcher.containsInOrder(expectedCookies, 5)); + } + } + + @Test + public void testProcessResponseCookiesNotPersistingWithSaveCookiesFalse() + throws IOException { + HttpConnection connection = Mockito.mock(HttpConnection.class); + Mockito.when( + connection.getHeaderFields(ArgumentMatchers.eq("Set-Cookie"))) + .thenReturn(Arrays.asList( + "id=a3fWa; Expires=Thu, 21 Oct 2100 11:00:00 GMT; Secure; HttpOnly", + "sessionid=38afes7a8; HttpOnly; Path=/")); + Mockito.when( + connection.getHeaderFields(ArgumentMatchers.eq("Set-Cookie2"))) + .thenReturn(Collections.singletonList( + "cookie2=some value; Max-Age=1234; Path=/")); + + // tweak config + final Config config = db.getConfig(); + config.setBoolean("http", null, "saveCookies", false); + + try (TransportHttp transportHttp = new TransportHttp(db, uri)) { + transportHttp.processResponseCookies(connection); + + // evaluate written cookie file + Assert.assertFalse("Cookie file was not supposed to be written!", + cookieFile.exists()); + } + } +} diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/LRUMapTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/LRUMapTest.java new file mode 100644 index 000000000..da59533ae --- /dev/null +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/LRUMapTest.java @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2018, Konrad Windszus + * and other copyright owners as documented in the project's IP log. + * + * This program and the accompanying materials are made available + * under the terms of the Eclipse Distribution License v1.0 which + * accompanies this distribution, is reproduced below, and is + * available at http://www.eclipse.org/org/documents/edl-v10.php + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or + * without modification, are permitted provided that the following + * conditions are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * + * - Neither the name of the Eclipse Foundation, Inc. nor the + * names of its contributors may be used to endorse or promote + * products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.eclipse.jgit.util; + +import java.util.LinkedHashMap; +import java.util.Map; + +import org.hamcrest.collection.IsIterableContainingInOrder; +import org.junit.Assert; +import org.junit.Test; + +public class LRUMapTest { + + @SuppressWarnings("boxing") + @Test + public void testLRUEntriesAreEvicted() { + Map map = new LRUMap<>(3, 3); + for (int i = 0; i < 3; i++) { + map.put(i, i); + } + // access the last ones + map.get(2); + map.get(0); + + // put another one which exceeds the limit (entry with key "1" is + // evicted) + map.put(3, 3); + + Map expectedMap = new LinkedHashMap<>(); + expectedMap.put(2, 2); + expectedMap.put(0, 0); + expectedMap.put(3, 3); + + Assert.assertThat(map.entrySet(), + IsIterableContainingInOrder + .contains(expectedMap.entrySet().toArray())); + } +} diff --git a/org.eclipse.jgit/.settings/.api_filters b/org.eclipse.jgit/.settings/.api_filters index 7f93191ca..ed36dde8b 100644 --- a/org.eclipse.jgit/.settings/.api_filters +++ b/org.eclipse.jgit/.settings/.api_filters @@ -68,4 +68,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/org.eclipse.jgit/META-INF/MANIFEST.MF b/org.eclipse.jgit/META-INF/MANIFEST.MF index bd3161b43..893f0d430 100644 --- a/org.eclipse.jgit/META-INF/MANIFEST.MF +++ b/org.eclipse.jgit/META-INF/MANIFEST.MF @@ -86,6 +86,7 @@ Export-Package: org.eclipse.jgit.annotations;version="5.4.0", org.eclipse.jgit.pgm", org.eclipse.jgit.internal.storage.reftree;version="5.4.0";x-friends:="org.eclipse.jgit.junit,org.eclipse.jgit.test,org.eclipse.jgit.pgm", org.eclipse.jgit.internal.submodule;version="5.4.0";x-internal:=true, + org.eclipse.jgit.internal.transport.http;version="5.4.0";x-friends:="org.eclipse.jgit.test", org.eclipse.jgit.internal.transport.parser;version="5.4.0";x-friends:="org.eclipse.jgit.http.server,org.eclipse.jgit.test", org.eclipse.jgit.internal.transport.ssh;version="5.4.0";x-friends:="org.eclipse.jgit.ssh.apache", org.eclipse.jgit.lib;version="5.4.0"; diff --git a/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties b/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties index 6da6fee38..88fdc3d81 100644 --- a/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties +++ b/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties @@ -208,6 +208,10 @@ couldNotDeleteTemporaryIndexFileShouldNotHappen=Could not delete temporary index couldNotGetAdvertisedRef=Remote {0} did not advertise Ref for branch {1}. This Ref may not exist in the remote or may be hidden by permission settings. couldNotGetRepoStatistics=Could not get repository statistics couldNotLockHEAD=Could not lock HEAD +couldNotFindTabInLine=Could not find tab in line {0}. Tab is the mandatory separator for the Netscape Cookie File Format. +couldNotFindSixTabsInLine=Could not find 6 tabs but only {0} in line '{1}'. 7 tab separated columns per line are mandatory for the Netscape Cookie File Format. +couldNotPersistCookies=Could not persist received cookies in file ''{0}'' +couldNotReadCookieFile=Could not read cookie file ''{0}'' couldNotReadIndexInOneGo=Could not read index in one go, only {0} out of {1} read couldNotReadObjectWhileParsingCommit=Could not read an object while parsing commit {0} couldNotRenameDeleteOldIndex=Could not rename delete old index diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java index 7a5ef4b7e..88b3fc850 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java @@ -267,9 +267,13 @@ public class JGitText extends TranslationBundle { /***/ public String couldNotCheckOutBecauseOfConflicts; /***/ public String couldNotDeleteLockFileShouldNotHappen; /***/ public String couldNotDeleteTemporaryIndexFileShouldNotHappen; + /***/ public String couldNotFindTabInLine; + /***/ public String couldNotFindSixTabsInLine; /***/ public String couldNotGetAdvertisedRef; /***/ public String couldNotGetRepoStatistics; /***/ public String couldNotLockHEAD; + /***/ public String couldNotPersistCookies; + /***/ public String couldNotReadCookieFile; /***/ public String couldNotReadIndexInOneGo; /***/ public String couldNotReadObjectWhileParsingCommit; /***/ public String couldNotRenameDeleteOldIndex; diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/transport/http/NetscapeCookieFile.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/transport/http/NetscapeCookieFile.java new file mode 100644 index 000000000..93be5c69e --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/transport/http/NetscapeCookieFile.java @@ -0,0 +1,471 @@ +/* + * Copyright (C) 2018, Konrad Windszus + * and other copyright owners as documented in the project's IP log. + * + * This program and the accompanying materials are made available + * under the terms of the Eclipse Distribution License v1.0 which + * accompanies this distribution, is reproduced below, and is + * available at http://www.eclipse.org/org/documents/edl-v10.php + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or + * without modification, are permitted provided that the following + * conditions are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * + * - Neither the name of the Eclipse Foundation, Inc. nor the + * names of its contributors may be used to endorse or promote + * products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.eclipse.jgit.internal.transport.http; + +import java.io.BufferedReader; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.io.StringReader; +import java.io.Writer; +import java.net.HttpCookie; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.text.MessageFormat; +import java.util.Arrays; +import java.util.Collection; +import java.util.Date; +import java.util.LinkedHashSet; +import java.util.Set; + +import org.eclipse.jgit.annotations.NonNull; +import org.eclipse.jgit.annotations.Nullable; +import org.eclipse.jgit.internal.JGitText; +import org.eclipse.jgit.internal.storage.file.FileSnapshot; +import org.eclipse.jgit.internal.storage.file.LockFile; +import org.eclipse.jgit.lib.Constants; +import org.eclipse.jgit.storage.file.FileBasedConfig; +import org.eclipse.jgit.util.FileUtils; +import org.eclipse.jgit.util.IO; +import org.eclipse.jgit.util.RawParseUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Wraps all cookies persisted in a Netscape Cookie File Format + * being referenced via the git config http.cookieFile. + * + * It will only load the cookies lazily, i.e. before calling + * {@link #getCookies(boolean)} the file is not evaluated. This class also + * allows persisting cookies in that file format. + *

+ * In general this class is not thread-safe. So any consumer needs to take care + * of synchronization! + * + * @see Netscape Cookie File + * Format + * @see Cookie + * format for wget + * @see libcurl + * Cookie file parsing + * @see libcurl + * Cookie file writing + * @see NetscapeCookieFileCache + */ +public final class NetscapeCookieFile { + + private static final String HTTP_ONLY_PREAMBLE = "#HttpOnly_"; //$NON-NLS-1$ + + private static final String COLUMN_SEPARATOR = "\t"; //$NON-NLS-1$ + + private static final String LINE_SEPARATOR = "\n"; //$NON-NLS-1$ + + /** + * Maximum number of retries to acquire the lock for writing to the + * underlying file. + */ + private static final int LOCK_ACQUIRE_MAX_RETRY_COUNT = 4; + + /** + * Sleep time in milliseconds between retries to acquire the lock for + * writing to the underlying file. + */ + private static final int LOCK_ACQUIRE_RETRY_SLEEP = 500; + + private final Path path; + + private FileSnapshot snapshot; + + private byte[] hash; + + final Date creationDate; + + private Set cookies = null; + + private static final Logger LOG = LoggerFactory + .getLogger(NetscapeCookieFile.class); + + /** + * @param path + */ + public NetscapeCookieFile(Path path) { + this(path, new Date()); + } + + NetscapeCookieFile(Path path, Date creationDate) { + this.path = path; + this.snapshot = FileSnapshot.DIRTY; + this.creationDate = creationDate; + } + + /** + * @return the path to the underlying cookie file + */ + public Path getPath() { + return path; + } + + /** + * @param refresh + * if {@code true} updates the list from the underlying cookie + * file if it has been modified since the last read otherwise + * returns the current transient state. In case the cookie file + * has never been read before will always read from the + * underlying file disregarding the value of this parameter. + * @return all cookies (may contain session cookies as well). This does not + * return a copy of the list but rather the original one. Every + * addition to the returned list can afterwards be persisted via + * {@link #write(URL)}. Errors in the underlying file will not lead + * to exceptions but rather to an empty set being returned and the + * underlying error being logged. + */ + public Set getCookies(boolean refresh) { + if (cookies == null || refresh) { + try { + byte[] in = getFileContentIfModified(); + Set newCookies = parseCookieFile(in, creationDate); + if (cookies != null) { + cookies = mergeCookies(newCookies, cookies); + } else { + cookies = newCookies; + } + return cookies; + } catch (IOException | IllegalArgumentException e) { + LOG.warn( + MessageFormat.format( + JGitText.get().couldNotReadCookieFile, path), + e); + if (cookies == null) { + cookies = new LinkedHashSet<>(); + } + } + } + return cookies; + + } + + /** + * Parses the given file and extracts all cookie information from it. + * + * @param input + * the file content to parse + * @param creationDate + * the date for the creation of the cookies (used to calculate + * the maxAge based on the expiration date given within the file) + * @return the set of parsed cookies from the given file (even expired + * ones). If there is more than one cookie with the same name in + * this file the last one overwrites the first one! + * @throws IOException + * if the given file could not be read for some reason + * @throws IllegalArgumentException + * if the given file does not have a proper format. + */ + private static Set parseCookieFile(@NonNull byte[] input, + @NonNull Date creationDate) + throws IOException, IllegalArgumentException { + + String decoded = RawParseUtils.decode(StandardCharsets.US_ASCII, input); + + Set cookies = new LinkedHashSet<>(); + try (BufferedReader reader = new BufferedReader( + new StringReader(decoded))) { + String line; + while ((line = reader.readLine()) != null) { + HttpCookie cookie = parseLine(line, creationDate); + if (cookie != null) { + cookies.add(cookie); + } + } + } + return cookies; + } + + private static HttpCookie parseLine(@NonNull String line, + @NonNull Date creationDate) { + if (line.isEmpty() || (line.startsWith("#") //$NON-NLS-1$ + && !line.startsWith(HTTP_ONLY_PREAMBLE))) { + return null; + } + String[] cookieLineParts = line.split(COLUMN_SEPARATOR, 7); + if (cookieLineParts == null) { + throw new IllegalArgumentException(MessageFormat + .format(JGitText.get().couldNotFindTabInLine, line)); + } + if (cookieLineParts.length < 7) { + throw new IllegalArgumentException(MessageFormat.format( + JGitText.get().couldNotFindSixTabsInLine, + Integer.valueOf(cookieLineParts.length), line)); + } + String name = cookieLineParts[5]; + String value = cookieLineParts[6]; + HttpCookie cookie = new HttpCookie(name, value); + + String domain = cookieLineParts[0]; + if (domain.startsWith(HTTP_ONLY_PREAMBLE)) { + cookie.setHttpOnly(true); + domain = domain.substring(HTTP_ONLY_PREAMBLE.length()); + } + // strip off leading "." + // (https://tools.ietf.org/html/rfc6265#section-5.2.3) + if (domain.startsWith(".")) { //$NON-NLS-1$ + domain = domain.substring(1); + } + cookie.setDomain(domain); + // domain evaluation as boolean flag not considered (i.e. always assumed + // to be true) + cookie.setPath(cookieLineParts[2]); + cookie.setSecure(Boolean.parseBoolean(cookieLineParts[3])); + + long expires = Long.parseLong(cookieLineParts[4]); + long maxAge = (expires - creationDate.getTime()) / 1000; + if (maxAge <= 0) { + return null; // skip expired cookies + } + cookie.setMaxAge(maxAge); + return cookie; + } + + /** + * Writes all the cookies being maintained in the set being returned by + * {@link #getCookies(boolean)} to the underlying file. + * + * Session-cookies will not be persisted. + * + * @param url + * url for which to write the cookies (important to derive + * default values for non-explicitly set attributes) + * @throws IOException + * @throws IllegalArgumentException + * @throws InterruptedException + */ + public void write(URL url) + throws IllegalArgumentException, IOException, InterruptedException { + try { + byte[] cookieFileContent = getFileContentIfModified(); + if (cookieFileContent != null) { + LOG.debug( + "Reading the underlying cookie file '{}' as it has been modified since the last access", //$NON-NLS-1$ + path); + // reread new changes if necessary + Set cookiesFromFile = NetscapeCookieFile + .parseCookieFile(cookieFileContent, creationDate); + this.cookies = mergeCookies(cookiesFromFile, cookies); + } + } catch (FileNotFoundException e) { + // ignore if file previously did not exist yet! + } + + ByteArrayOutputStream output = new ByteArrayOutputStream(); + try (Writer writer = new OutputStreamWriter(output, + StandardCharsets.US_ASCII)) { + write(writer, cookies, url, creationDate); + } + LockFile lockFile = new LockFile(path.toFile()); + for (int retryCount = 0; retryCount < LOCK_ACQUIRE_MAX_RETRY_COUNT; retryCount++) { + if (lockFile.lock()) { + try { + lockFile.setNeedSnapshot(true); + lockFile.write(output.toByteArray()); + if (!lockFile.commit()) { + throw new IOException(MessageFormat.format( + JGitText.get().cannotCommitWriteTo, path)); + } + } finally { + lockFile.unlock(); + } + return; + } + Thread.sleep(LOCK_ACQUIRE_RETRY_SLEEP); + } + throw new IOException( + MessageFormat.format(JGitText.get().cannotLock, lockFile)); + + } + + /** + * Read the underying file and return its content but only in case it has + * been modified since the last access. Internally calculates the hash and + * maintains {@link FileSnapshot}s to prevent issues described as "Racy + * Git problem". Inspired by {@link FileBasedConfig#load()}. + * + * @return the file contents in case the file has been modified since the + * last access, otherwise {@code null} + * @throws IOException + */ + private byte[] getFileContentIfModified() throws IOException { + final int maxStaleRetries = 5; + int retries = 0; + File file = getPath().toFile(); + while (true) { + final FileSnapshot oldSnapshot = snapshot; + final FileSnapshot newSnapshot = FileSnapshot.save(file); + try { + final byte[] in = IO.readFully(file); + byte[] newHash = hash(in); + if (Arrays.equals(hash, newHash)) { + if (oldSnapshot.equals(newSnapshot)) { + oldSnapshot.setClean(newSnapshot); + } else { + snapshot = newSnapshot; + } + } else { + snapshot = newSnapshot; + hash = newHash; + } + return in; + } catch (FileNotFoundException e) { + throw e; + } catch (IOException e) { + if (FileUtils.isStaleFileHandle(e) + && retries < maxStaleRetries) { + if (LOG.isDebugEnabled()) { + LOG.debug(MessageFormat.format( + JGitText.get().configHandleIsStale, + Integer.valueOf(retries)), e); + } + retries++; + continue; + } + throw new IOException(MessageFormat + .format(JGitText.get().cannotReadFile, getPath()), e); + } + } + + } + + private byte[] hash(final byte[] in) { + return Constants.newMessageDigest().digest(in); + } + + /** + * Writes the given cookies to the file in the Netscape Cookie File Format + * (also used by curl) + * + * @param writer + * the writer to use to persist the cookies. + * @param cookies + * the cookies to write into the file + * @param url + * the url for which to write the cookie (to derive the default + * values for certain cookie attributes) + * @param creationDate + * the date when the cookie has been created. Important for + * calculation the cookie expiration time (calculated from + * cookie's maxAge and this creation time). + * @throws IOException + */ + static void write(@NonNull Writer writer, + @NonNull Collection cookies, @NonNull URL url, + @NonNull Date creationDate) throws IOException { + for (HttpCookie cookie : cookies) { + writeCookie(writer, cookie, url, creationDate); + } + } + + private static void writeCookie(@NonNull Writer writer, + @NonNull HttpCookie cookie, @NonNull URL url, + @NonNull Date creationDate) throws IOException { + if (cookie.getMaxAge() <= 0) { + return; // skip expired cookies + } + String domain = ""; //$NON-NLS-1$ + if (cookie.isHttpOnly()) { + domain = HTTP_ONLY_PREAMBLE; + } + if (cookie.getDomain() != null) { + domain += cookie.getDomain(); + } else { + domain += url.getHost(); + } + writer.write(domain); + writer.write(COLUMN_SEPARATOR); + writer.write("TRUE"); //$NON-NLS-1$ + writer.write(COLUMN_SEPARATOR); + String path = cookie.getPath(); + if (path == null) { + path = url.getPath(); + } + writer.write(path); + writer.write(COLUMN_SEPARATOR); + writer.write(Boolean.toString(cookie.getSecure()).toUpperCase()); + writer.write(COLUMN_SEPARATOR); + final String expirationDate; + // whenCreated field is not accessible in HttpCookie + expirationDate = String + .valueOf(creationDate.getTime() + (cookie.getMaxAge() * 1000)); + writer.write(expirationDate); + writer.write(COLUMN_SEPARATOR); + writer.write(cookie.getName()); + writer.write(COLUMN_SEPARATOR); + writer.write(cookie.getValue()); + writer.write(LINE_SEPARATOR); + } + + /** + * Merge the given sets in the following way. All cookies from + * {@code cookies1} and {@code cookies2} are contained in the resulting set + * which have unique names. If there is a duplicate entry for one name only + * the entry from set {@code cookies1} ends up in the resulting set. + * + * @param cookies1 + * @param cookies2 + * + * @return the merged cookies + */ + static Set mergeCookies(Set cookies1, + @Nullable Set cookies2) { + Set mergedCookies = new LinkedHashSet<>(cookies1); + if (cookies2 != null) { + mergedCookies.addAll(cookies2); + } + return mergedCookies; + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/transport/http/NetscapeCookieFileCache.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/transport/http/NetscapeCookieFileCache.java new file mode 100644 index 000000000..882b2d055 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/transport/http/NetscapeCookieFileCache.java @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2018, Konrad Windszus + * and other copyright owners as documented in the project's IP log. + * + * This program and the accompanying materials are made available + * under the terms of the Eclipse Distribution License v1.0 which + * accompanies this distribution, is reproduced below, and is + * available at http://www.eclipse.org/org/documents/edl-v10.php + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or + * without modification, are permitted provided that the following + * conditions are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * + * - Neither the name of the Eclipse Foundation, Inc. nor the + * names of its contributors may be used to endorse or promote + * products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.eclipse.jgit.internal.transport.http; + +import java.nio.file.Path; + +import org.eclipse.jgit.transport.HttpConfig; +import org.eclipse.jgit.util.LRUMap; + +/** + * A cache of all known cookie files ({@link NetscapeCookieFile}). May contain + * at most {@code n} entries, where the least-recently used one is evicted as + * soon as more entries are added. The maximum number of entries (={@code n}) + * can be set via the git config key {@code http.cookieFileCacheLimit}. By + * default it is set to 10. + *

+ * The cache is global, i.e. it is shared among all consumers within the same + * Java process. + * + * @see NetscapeCookieFile + * + */ +public class NetscapeCookieFileCache { + + private final LRUMap cookieFileMap; + + private static NetscapeCookieFileCache instance; + + private NetscapeCookieFileCache(HttpConfig config) { + cookieFileMap = new LRUMap<>(config.getCookieFileCacheLimit(), + config.getCookieFileCacheLimit()); + } + + /** + * @param config + * the config which defines the limit for this cache + * @return the singleton instance of the cookie file cache. If the cache has + * already been created the given config is ignored (even if it + * differs from the config, with which the cache has originally been + * created) + */ + public static NetscapeCookieFileCache getInstance(HttpConfig config) { + if (instance == null) { + return new NetscapeCookieFileCache(config); + } else { + return instance; + } + } + + /** + * @param path + * the path of the cookie file to retrieve + * @return the cache entry belonging to the requested file + */ + public NetscapeCookieFile getEntry(Path path) { + if (!cookieFileMap.containsKey(path)) { + synchronized (NetscapeCookieFileCache.class) { + if (!cookieFileMap.containsKey(path)) { + cookieFileMap.put(path, new NetscapeCookieFile(path)); + } + } + } + return cookieFileMap.get(path); + } + +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/HttpConfig.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/HttpConfig.java index 101ce3568..54c21cbc8 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/HttpConfig.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/HttpConfig.java @@ -89,6 +89,30 @@ public class HttpConfig { /** git config key for the "sslVerify" setting. */ public static final String SSL_VERIFY_KEY = "sslVerify"; //$NON-NLS-1$ + /** + * git config key for the "cookieFile" setting. + * + * @since 5.4 + */ + public static final String COOKIE_FILE_KEY = "cookieFile"; //$NON-NLS-1$ + + /** + * git config key for the "saveCookies" setting. + * + * @since 5.4 + */ + public static final String SAVE_COOKIES_KEY = "saveCookies"; //$NON-NLS-1$ + + /** + * Custom JGit config key which holds the maximum number of cookie files to + * keep in the cache. + * + * @since 5.4 + */ + public static final String COOKIE_FILE_CACHE_LIMIT_KEY = "cookieFileCacheLimit"; //$NON-NLS-1$ + + private static final int DEFAULT_COOKIE_FILE_CACHE_LIMIT = 10; + private static final String MAX_REDIRECT_SYSTEM_PROPERTY = "http.maxRedirects"; //$NON-NLS-1$ private static final int DEFAULT_MAX_REDIRECTS = 5; @@ -153,6 +177,12 @@ public class HttpConfig { private int maxRedirects; + private String cookieFile; + + private boolean saveCookies; + + private int cookieFileCacheLimit; + /** * Get the "http.postBuffer" setting * @@ -189,6 +219,40 @@ public class HttpConfig { return maxRedirects; } + /** + * Get the "http.cookieFile" setting + * + * @return the value of the "http.cookieFile" setting + * + * @since 5.4 + */ + public String getCookieFile() { + return cookieFile; + } + + /** + * Get the "http.saveCookies" setting + * + * @return the value of the "http.saveCookies" setting + * + * @since 5.4 + */ + public boolean getSaveCookies() { + return saveCookies; + } + + /** + * Get the "http.cookieFileCacheLimit" setting (gives the maximum number of + * cookie files to keep in the LRU cache) + * + * @return the value of the "http.cookieFileCacheLimit" setting + * + * @since 5.4 + */ + public int getCookieFileCacheLimit() { + return cookieFileCacheLimit; + } + /** * Creates a new {@link org.eclipse.jgit.transport.HttpConfig} tailored to * the given {@link org.eclipse.jgit.transport.URIish}. @@ -237,6 +301,10 @@ public class HttpConfig { if (redirectLimit < 0) { redirectLimit = MAX_REDIRECTS; } + cookieFile = config.getString(HTTP, null, COOKIE_FILE_KEY); + saveCookies = config.getBoolean(HTTP, SAVE_COOKIES_KEY, false); + cookieFileCacheLimit = config.getInt(HTTP, COOKIE_FILE_CACHE_LIMIT_KEY, + DEFAULT_COOKIE_FILE_CACHE_LIMIT); String match = findMatch(config.getSubsections(HTTP), uri); if (match != null) { // Override with more specific items @@ -251,6 +319,13 @@ public class HttpConfig { if (newMaxRedirects >= 0) { redirectLimit = newMaxRedirects; } + String urlSpecificCookieFile = config.getString(HTTP, match, + COOKIE_FILE_KEY); + if (urlSpecificCookieFile != null) { + cookieFile = urlSpecificCookieFile; + } + saveCookies = config.getBoolean(HTTP, match, SAVE_COOKIES_KEY, + saveCookies); } postBuffer = postBufferSize; sslVerify = sslVerifyFlag; diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportHttp.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportHttp.java index 42aa80ea4..f44a99b8b 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportHttp.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportHttp.java @@ -54,8 +54,11 @@ import static org.eclipse.jgit.util.HttpSupport.HDR_ACCEPT; import static org.eclipse.jgit.util.HttpSupport.HDR_ACCEPT_ENCODING; import static org.eclipse.jgit.util.HttpSupport.HDR_CONTENT_ENCODING; import static org.eclipse.jgit.util.HttpSupport.HDR_CONTENT_TYPE; +import static org.eclipse.jgit.util.HttpSupport.HDR_COOKIE; import static org.eclipse.jgit.util.HttpSupport.HDR_LOCATION; import static org.eclipse.jgit.util.HttpSupport.HDR_PRAGMA; +import static org.eclipse.jgit.util.HttpSupport.HDR_SET_COOKIE; +import static org.eclipse.jgit.util.HttpSupport.HDR_SET_COOKIE2; import static org.eclipse.jgit.util.HttpSupport.HDR_USER_AGENT; import static org.eclipse.jgit.util.HttpSupport.HDR_WWW_AUTHENTICATE; import static org.eclipse.jgit.util.HttpSupport.METHOD_GET; @@ -68,11 +71,15 @@ import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; +import java.net.HttpCookie; import java.net.MalformedURLException; import java.net.Proxy; import java.net.ProxySelector; import java.net.URISyntaxException; import java.net.URL; +import java.nio.file.InvalidPathException; +import java.nio.file.Path; +import java.nio.file.Paths; import java.security.cert.CertPathBuilderException; import java.security.cert.CertPathValidatorException; import java.security.cert.CertificateException; @@ -84,6 +91,8 @@ import java.util.Collections; import java.util.EnumSet; import java.util.HashSet; import java.util.LinkedHashSet; +import java.util.LinkedList; +import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Set; @@ -100,6 +109,8 @@ import org.eclipse.jgit.errors.PackProtocolException; import org.eclipse.jgit.errors.TransportException; import org.eclipse.jgit.internal.JGitText; import org.eclipse.jgit.internal.storage.file.RefDirectory; +import org.eclipse.jgit.internal.transport.http.NetscapeCookieFile; +import org.eclipse.jgit.internal.transport.http.NetscapeCookieFileCache; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.ObjectIdRef; @@ -116,6 +127,7 @@ import org.eclipse.jgit.util.FS; import org.eclipse.jgit.util.HttpSupport; import org.eclipse.jgit.util.IO; import org.eclipse.jgit.util.RawParseUtils; +import org.eclipse.jgit.util.StringUtils; import org.eclipse.jgit.util.SystemReader; import org.eclipse.jgit.util.TemporaryBuffer; import org.eclipse.jgit.util.io.DisabledOutputStream; @@ -274,6 +286,19 @@ public class TransportHttp extends HttpTransport implements WalkTransport, private boolean sslFailure = false; + /** + * All stored cookies bound to this repo (independent of the baseUrl) + */ + private final NetscapeCookieFile cookieFile; + + /** + * The cookies to be sent with each request to the given {@link #baseUrl}. + * Filtered view on top of {@link #cookieFile} where only cookies which + * apply to the current url are left. This set needs to be filtered for + * expired entries each time prior to sending them. + */ + private final Set relevantCookies; + TransportHttp(Repository local, URIish uri) throws NotSupportedException { super(local, uri); @@ -281,6 +306,8 @@ public class TransportHttp extends HttpTransport implements WalkTransport, http = new HttpConfig(local.getConfig(), uri); proxySelector = ProxySelector.getDefault(); sslVerify = http.isSslVerify(); + cookieFile = getCookieFileFromConfig(http); + relevantCookies = filterCookies(cookieFile, baseUrl); } private URL toURL(URIish urish) throws MalformedURLException { @@ -321,6 +348,8 @@ public class TransportHttp extends HttpTransport implements WalkTransport, http = new HttpConfig(uri); proxySelector = ProxySelector.getDefault(); sslVerify = http.isSslVerify(); + cookieFile = getCookieFileFromConfig(http); + relevantCookies = filterCookies(cookieFile, baseUrl); } /** @@ -508,6 +537,7 @@ public class TransportHttp extends HttpTransport implements WalkTransport, conn.setRequestProperty(HDR_ACCEPT, "*/*"); //$NON-NLS-1$ } final int status = HttpSupport.response(conn); + processResponseCookies(conn); switch (status) { case HttpConnection.HTTP_OK: // Check if HttpConnection did some authentication in the @@ -596,6 +626,57 @@ public class TransportHttp extends HttpTransport implements WalkTransport, } } + void processResponseCookies(HttpConnection conn) { + if (cookieFile != null && http.getSaveCookies()) { + List foundCookies = new LinkedList<>(); + + List cookieHeaderValues = conn + .getHeaderFields(HDR_SET_COOKIE); + if (!cookieHeaderValues.isEmpty()) { + foundCookies.addAll( + extractCookies(HDR_SET_COOKIE, cookieHeaderValues)); + } + cookieHeaderValues = conn.getHeaderFields(HDR_SET_COOKIE2); + if (!cookieHeaderValues.isEmpty()) { + foundCookies.addAll( + extractCookies(HDR_SET_COOKIE2, cookieHeaderValues)); + } + if (foundCookies.size() > 0) { + try { + // update cookie lists with the newly received cookies! + Set cookies = cookieFile.getCookies(false); + cookies.addAll(foundCookies); + cookieFile.write(baseUrl); + relevantCookies.addAll(foundCookies); + } catch (IOException | IllegalArgumentException + | InterruptedException e) { + LOG.warn(MessageFormat.format( + JGitText.get().couldNotPersistCookies, + cookieFile.getPath()), e); + } + } + } + } + + private List extractCookies(String headerKey, + List headerValues) { + List foundCookies = new LinkedList<>(); + for (String headerValue : headerValues) { + foundCookies + .addAll(HttpCookie.parse(headerKey + ':' + headerValue)); + } + // HttpCookies.parse(...) is only compliant with RFC 2965. Make it RFC + // 6265 compliant by applying the logic from + // https://tools.ietf.org/html/rfc6265#section-5.2.3 + for (HttpCookie foundCookie : foundCookies) { + String domain = foundCookie.getDomain(); + if (domain != null && domain.startsWith(".")) { //$NON-NLS-1$ + foundCookie.setDomain(domain.substring(1)); + } + } + return foundCookies; + } + private static class CredentialItems { CredentialItem.InformationalMessage message; @@ -847,14 +928,35 @@ public class TransportHttp extends HttpTransport implements WalkTransport, conn.setConnectTimeout(effTimeOut); conn.setReadTimeout(effTimeOut); } + // set cookie header if necessary + if (relevantCookies.size() > 0) { + setCookieHeader(conn); + } + if (this.headers != null && !this.headers.isEmpty()) { - for (Map.Entry entry : this.headers.entrySet()) + for (Map.Entry entry : this.headers.entrySet()) { conn.setRequestProperty(entry.getKey(), entry.getValue()); + } } authMethod.configureRequest(conn); return conn; } + private void setCookieHeader(HttpConnection conn) { + StringBuilder cookieHeaderValue = new StringBuilder(); + for (HttpCookie cookie : relevantCookies) { + if (!cookie.hasExpired()) { + if (cookieHeaderValue.length() > 0) { + cookieHeaderValue.append(';'); + } + cookieHeaderValue.append(cookie.toString()); + } + } + if (cookieHeaderValue.length() >= 0) { + conn.setRequestProperty(HDR_COOKIE, cookieHeaderValue.toString()); + } + } + final InputStream openInputStream(HttpConnection conn) throws IOException { InputStream input = conn.getInputStream(); @@ -868,6 +970,150 @@ public class TransportHttp extends HttpTransport implements WalkTransport, return new TransportException(uri, why); } + private static NetscapeCookieFile getCookieFileFromConfig( + HttpConfig config) { + if (!StringUtils.isEmptyOrNull(config.getCookieFile())) { + try { + Path cookieFilePath = Paths.get(config.getCookieFile()); + return NetscapeCookieFileCache.getInstance(config) + .getEntry(cookieFilePath); + } catch (InvalidPathException e) { + LOG.warn(MessageFormat.format( + JGitText.get().couldNotReadCookieFile, + config.getCookieFile()), e); + } + } + return null; + } + + private static Set filterCookies(NetscapeCookieFile cookieFile, + URL url) { + if (cookieFile != null) { + return filterCookies(cookieFile.getCookies(true), url); + } + return Collections.emptySet(); + } + + /** + * + * @param allCookies + * a list of cookies. + * @param url + * the url for which to filter the list of cookies. + * @return only the cookies from {@code allCookies} which are relevant (i.e. + * are not expired, have a matching domain, have a matching path and + * have a matching secure attribute) + */ + private static Set filterCookies(Set allCookies, + URL url) { + Set filteredCookies = new HashSet<>(); + for (HttpCookie cookie : allCookies) { + if (cookie.hasExpired()) { + continue; + } + if (!matchesCookieDomain(url.getHost(), cookie.getDomain())) { + continue; + } + if (!matchesCookiePath(url.getPath(), cookie.getPath())) { + continue; + } + if (cookie.getSecure() && !"https".equals(url.getProtocol())) { //$NON-NLS-1$ + continue; + } + filteredCookies.add(cookie); + } + return filteredCookies; + } + + /** + * + * The utility method to check whether a host name is in a cookie's domain + * or not. Similar to {@link HttpCookie#domainMatches(String, String)} but + * implements domain matching rules according to + * RFC 6265, + * section 5.1.3 instead of the rules from + * RFC 2965, + * section 3.3.1. + *

+ * The former rules are also used by libcurl internally. + *

+ * The rules are as follows + * + * A string matches another domain string if at least one of the following + * conditions holds: + *

    + *
  • The domain string and the string are identical. (Note that both the + * domain string and the string will have been canonicalized to lower case + * at this point.)
  • + *
  • All of the following conditions hold + *
      + *
    • The domain string is a suffix of the string.
    • + *
    • The last character of the string that is not included in the domain + * string is a %x2E (".") character.
    • + *
    • The string is a host name (i.e., not an IP address).
    • + *
    + *
  • + *
+ * + * @param host + * the host to compare against the cookieDomain + * @param cookieDomain + * the domain to compare against + * @return {@code true} if they domain-match; {@code false} if not + * + * @see RFC + * 6265, section 5.1.3 (Domain Matching) + * @see JDK-8206092 + * : HttpCookie.domainMatches() does not match to sub-sub-domain + */ + static boolean matchesCookieDomain(String host, String cookieDomain) { + cookieDomain = cookieDomain.toLowerCase(Locale.ROOT); + host = host.toLowerCase(Locale.ROOT); + if (host.equals(cookieDomain)) { + return true; + } else { + if (!host.endsWith(cookieDomain)) { + return false; + } + return host + .charAt(host.length() - cookieDomain.length() - 1) == '.'; + } + } + + /** + * The utility method to check whether a path is matching a cookie path + * domain or not. The rules are defined by + * RFC 6265, + * section 5.1.4: + * + * A request-path path-matches a given cookie-path if at least one of the + * following conditions holds: + *
    + *
  • The cookie-path and the request-path are identical.
  • + *
  • The cookie-path is a prefix of the request-path, and the last + * character of the cookie-path is %x2F ("/").
  • + *
  • The cookie-path is a prefix of the request-path, and the first + * character of the request-path that is not included in the cookie- path is + * a %x2F ("/") character.
  • + *
+ * @param path + * the path to check + * @param cookiePath + * the cookie's path + * + * @return {@code true} if they path-match; {@code false} if not + */ + static boolean matchesCookiePath(String path, String cookiePath) { + if (cookiePath.equals(path)) { + return true; + } + if (!cookiePath.endsWith("/")) { //$NON-NLS-1$ + cookiePath += "/"; //$NON-NLS-1$ + } + return path.startsWith(cookiePath); + } + private boolean isSmartHttp(HttpConnection c, String service) { final String expType = "application/x-" + service + "-advertisement"; //$NON-NLS-1$ //$NON-NLS-2$ final String actType = c.getContentType(); diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/HttpSupport.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/HttpSupport.java index 54e4ee01f..640670deb 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/util/HttpSupport.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/HttpSupport.java @@ -169,6 +169,27 @@ public class HttpSupport { /** The {@code WWW-Authenticate} header. */ public static final String HDR_WWW_AUTHENTICATE = "WWW-Authenticate"; //$NON-NLS-1$ + /** + * The {@code Cookie} header. + * + * @since 5.4 + */ + public static final String HDR_COOKIE = "Cookie"; //$NON-NLS-1$ + + /** + * The {@code Set-Cookie} header. + * + * @since 5.4 + */ + public static final String HDR_SET_COOKIE = "Set-Cookie"; //$NON-NLS-1$ + + /** + * The {@code Set-Cookie2} header. + * + * @since 5.4 + */ + public static final String HDR_SET_COOKIE2 = "Set-Cookie2"; //$NON-NLS-1$ + /** * URL encode a value string into an output buffer. * diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/LRUMap.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/LRUMap.java new file mode 100644 index 000000000..41c15363f --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/LRUMap.java @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2018, Konrad Windszus + * and other copyright owners as documented in the project's IP log. + * + * This program and the accompanying materials are made available + * under the terms of the Eclipse Distribution License v1.0 which + * accompanies this distribution, is reproduced below, and is + * available at http://www.eclipse.org/org/documents/edl-v10.php + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or + * without modification, are permitted provided that the following + * conditions are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * + * - Neither the name of the Eclipse Foundation, Inc. nor the + * names of its contributors may be used to endorse or promote + * products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.eclipse.jgit.util; + +import java.util.LinkedHashMap; + +/** + * Map with only up to n entries. If a new entry is added so that the map + * contains more than those n entries the least-recently used entry is removed + * from the map. + * + * @param + * the type of keys maintained by this map + * @param + * the type of mapped values + * + * @since 5.4 + */ +public class LRUMap extends LinkedHashMap { + + private static final long serialVersionUID = 4329609127403759486L; + + private final int limit; + + /** + * Constructs an empty map which may contain at most the given amount of + * entries. + * + * @param initialCapacity + * the initial capacity + * @param limit + * the number of entries the map should have at most + */ + public LRUMap(int initialCapacity, int limit) { + super(initialCapacity, 0.75f, true); + this.limit = limit; + } + + @Override + protected boolean removeEldestEntry(java.util.Map.Entry eldest) { + return size() > limit; + } +}