Support reading and writing cookies.

The git config entries "http.cookieFile" and
"http.saveCookies" are correctly evaluated.

Bug: 488572
Change-Id: Icfeeea95e1a5bac3fa4438849d4ac2306d7d5562
Signed-off-by: Konrad Windszus <konrad_w@gmx.de>
Signed-off-by: Matthias Sohn <matthias.sohn@sap.com>
This commit is contained in:
Konrad Windszus 2018-11-19 18:10:07 +01:00 committed by Matthias Sohn
parent 8cd07cb815
commit d7bd2e700c
18 changed files with 1774 additions and 1 deletions

View File

@ -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)",

View File

@ -0,0 +1 @@
some-domain /some/path1 FALSE 0 key1 value1

View File

@ -0,0 +1,2 @@
some-domain1 TRUE /some/path1 FALSE 1893499200000 key1 valueFromSimple1
some-domain1 TRUE /some/path1 FALSE 1893499200000 key2 valueFromSimple1

View File

@ -0,0 +1,2 @@
some-domain1 TRUE /some/path1 FALSE 1893499200000 key1 valueFromSimple2
some-domain1 TRUE /some/path1 FALSE 1893499200000 key3 valueFromSimple2

View File

@ -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

View File

@ -0,0 +1,441 @@
/*
* Copyright (C) 2018, Konrad Windszus <konrad_w@gmx.de>
* 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<HttpCookie> cookieSet1 = new LinkedHashSet<>();
HttpCookie cookie = new HttpCookie("key1", "valueFromSet1");
cookieSet1.add(cookie);
cookie = new HttpCookie("key2", "valueFromSet1");
cookieSet1.add(cookie);
Set<HttpCookie> cookieSet2 = new LinkedHashSet<>();
cookie = new HttpCookie("key1", "valueFromSet2");
cookieSet2.add(cookie);
cookie = new HttpCookie("key3", "valueFromSet2");
cookieSet2.add(cookie);
Set<HttpCookie> 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<HttpCookie> 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<HttpCookie> 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<String> 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<HttpCookie> 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<HttpCookie> 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<HttpCookie> 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<HttpCookie> 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<HttpCookie> 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<Iterable<? extends HttpCookie>> containsInOrder(
Iterable<HttpCookie> expectedCookies) {
return containsInOrder(expectedCookies, 0);
}
public static Matcher<Iterable<? extends HttpCookie>> containsInOrder(
Iterable<HttpCookie> expectedCookies, int allowedMaxAgeDelta) {
final List<Matcher<? super HttpCookie>> 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<HttpCookie> {
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("]");
}
}
}

View File

@ -0,0 +1,189 @@
/*
* Copyright (C) 2018, Konrad Windszus <konrad_w@gmx.de>
* 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<HttpCookie> 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());
}
}
}

View File

@ -0,0 +1,78 @@
/*
* Copyright (C) 2018, Konrad Windszus <konrad_w@gmx.de>
* 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<Integer, Integer> 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<Integer, Integer> expectedMap = new LinkedHashMap<>();
expectedMap.put(2, 2);
expectedMap.put(0, 0);
expectedMap.put(3, 3);
Assert.assertThat(map.entrySet(),
IsIterableContainingInOrder
.contains(expectedMap.entrySet().toArray()));
}
}

View File

@ -68,4 +68,44 @@
</message_arguments>
</filter>
</resource>
<resource path="src/org/eclipse/jgit/transport/HttpConfig.java" type="org.eclipse.jgit.transport.HttpConfig">
<filter id="336658481">
<message_arguments>
<message_argument value="org.eclipse.jgit.transport.HttpConfig"/>
<message_argument value="COOKIE_FILE_CACHE_LIMIT_KEY"/>
</message_arguments>
</filter>
<filter id="336658481">
<message_arguments>
<message_argument value="org.eclipse.jgit.transport.HttpConfig"/>
<message_argument value="COOKIE_FILE_KEY"/>
</message_arguments>
</filter>
<filter id="336658481">
<message_arguments>
<message_argument value="org.eclipse.jgit.transport.HttpConfig"/>
<message_argument value="SAVE_COOKIES_KEY"/>
</message_arguments>
</filter>
</resource>
<resource path="src/org/eclipse/jgit/util/HttpSupport.java" type="org.eclipse.jgit.util.HttpSupport">
<filter id="336658481">
<message_arguments>
<message_argument value="org.eclipse.jgit.util.HttpSupport"/>
<message_argument value="HDR_COOKIE"/>
</message_arguments>
</filter>
<filter id="336658481">
<message_arguments>
<message_argument value="org.eclipse.jgit.util.HttpSupport"/>
<message_argument value="HDR_SET_COOKIE"/>
</message_arguments>
</filter>
<filter id="336658481">
<message_arguments>
<message_argument value="org.eclipse.jgit.util.HttpSupport"/>
<message_argument value="HDR_SET_COOKIE2"/>
</message_arguments>
</filter>
</resource>
</component>

View File

@ -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";

View File

@ -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

View File

@ -267,9 +267,13 @@ public static JGitText get() {
/***/ 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;

View File

@ -0,0 +1,471 @@
/*
* Copyright (C) 2018, Konrad Windszus <konrad_w@gmx.de>
* 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 <strong>Netscape Cookie File Format</strong>
* being referenced via the git config <a href=
* "https://git-scm.com/docs/git-config#git-config-httpcookieFile">http.cookieFile</a>.
*
* 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.
* <p>
* In general this class is not thread-safe. So any consumer needs to take care
* of synchronization!
*
* @see <a href="http://www.cookiecentral.com/faq/#3.5">Netscape Cookie File
* Format</a>
* @see <a href=
* "https://unix.stackexchange.com/questions/36531/format-of-cookies-when-using-wget">Cookie
* format for wget</a>
* @see <a href=
* "https://github.com/curl/curl/blob/07ebaf837843124ee670e5b8c218b80b92e06e47/lib/cookie.c#L745">libcurl
* Cookie file parsing</a>
* @see <a href=
* "https://github.com/curl/curl/blob/07ebaf837843124ee670e5b8c218b80b92e06e47/lib/cookie.c#L1417">libcurl
* Cookie file writing</a>
* @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<HttpCookie> 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<HttpCookie> getCookies(boolean refresh) {
if (cookies == null || refresh) {
try {
byte[] in = getFileContentIfModified();
Set<HttpCookie> 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<HttpCookie> parseCookieFile(@NonNull byte[] input,
@NonNull Date creationDate)
throws IOException, IllegalArgumentException {
String decoded = RawParseUtils.decode(StandardCharsets.US_ASCII, input);
Set<HttpCookie> 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<HttpCookie> 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 <a href=
* "https://github.com/git/git/blob/master/Documentation/technical/racy-git.txt">"Racy
* Git problem"</a>. 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<HttpCookie> 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<HttpCookie> mergeCookies(Set<HttpCookie> cookies1,
@Nullable Set<HttpCookie> cookies2) {
Set<HttpCookie> mergedCookies = new LinkedHashSet<>(cookies1);
if (cookies2 != null) {
mergedCookies.addAll(cookies2);
}
return mergedCookies;
}
}

View File

@ -0,0 +1,106 @@
/*
* Copyright (C) 2018, Konrad Windszus <konrad_w@gmx.de>
* 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.
* <p>
* 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<Path, NetscapeCookieFile> 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);
}
}

View File

@ -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 boolean matchConfigValue(String s) {
private int maxRedirects;
private String cookieFile;
private boolean saveCookies;
private int cookieFileCacheLimit;
/**
* Get the "http.postBuffer" setting
*
@ -189,6 +219,40 @@ public int getMaxRedirects() {
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 @@ private void init(Config config, URIish uri) {
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 @@ private void init(Config config, URIish uri) {
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;

View File

@ -54,8 +54,11 @@
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.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.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.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.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 Transport open(URIish uri, Repository local, String remoteName)
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<HttpCookie> relevantCookies;
TransportHttp(Repository local, URIish uri)
throws NotSupportedException {
super(local, uri);
@ -281,6 +306,8 @@ public Transport open(URIish uri, Repository local, String remoteName)
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 @@ protected void setURI(URIish uri) throws NotSupportedException {
http = new HttpConfig(uri);
proxySelector = ProxySelector.getDefault();
sslVerify = http.isSslVerify();
cookieFile = getCookieFileFromConfig(http);
relevantCookies = filterCookies(cookieFile, baseUrl);
}
/**
@ -508,6 +537,7 @@ private HttpConnection connect(String service)
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 @@ private HttpConnection connect(String service)
}
}
void processResponseCookies(HttpConnection conn) {
if (cookieFile != null && http.getSaveCookies()) {
List<HttpCookie> foundCookies = new LinkedList<>();
List<String> 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<HttpCookie> 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<HttpCookie> extractCookies(String headerKey,
List<String> headerValues) {
List<HttpCookie> 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 @@ protected HttpConnection httpOpen(String method, URL u,
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<String, String> entry : this.headers.entrySet())
for (Map.Entry<String, String> 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 @@ IOException wrongContentType(String expType, String actType) {
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<HttpCookie> 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<HttpCookie> filterCookies(Set<HttpCookie> allCookies,
URL url) {
Set<HttpCookie> 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
* <a href="https://tools.ietf.org/html/rfc6265#section-5.1.3">RFC 6265,
* section 5.1.3</a> instead of the rules from
* <a href="https://tools.ietf.org/html/rfc2965#section-3.3">RFC 2965,
* section 3.3.1</a>.
* <p>
* The former rules are also used by libcurl internally.
* <p>
* The rules are as follows
*
* A string matches another domain string if at least one of the following
* conditions holds:
* <ul>
* <li>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.)</li>
* <li>All of the following conditions hold
* <ul>
* <li>The domain string is a suffix of the string.</li>
* <li>The last character of the string that is not included in the domain
* string is a %x2E (".") character.</li>
* <li>The string is a host name (i.e., not an IP address).</li>
* </ul>
* </li>
* </ul>
*
* @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 <a href= "https://tools.ietf.org/html/rfc6265#section-5.1.3">RFC
* 6265, section 5.1.3 (Domain Matching)</a>
* @see <a href=
* "https://bugs.java.com/bugdatabase/view_bug.do?bug_id=8206092">JDK-8206092
* : HttpCookie.domainMatches() does not match to sub-sub-domain</a>
*/
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
* <a href="https://tools.ietf.org/html/rfc6265#section-5.1.4">RFC 6265,
* section 5.1.4</a>:
*
* A request-path path-matches a given cookie-path if at least one of the
* following conditions holds:
* <ul>
* <li>The cookie-path and the request-path are identical.</li>
* <li>The cookie-path is a prefix of the request-path, and the last
* character of the cookie-path is %x2F ("/").</li>
* <li>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.</li>
* </ul>
* @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();

View File

@ -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.
*

View File

@ -0,0 +1,83 @@
/*
* Copyright (C) 2018, Konrad Windszus <konrad_w@gmx.de>
* 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 <K>
* the type of keys maintained by this map
* @param <V>
* the type of mapped values
*
* @since 5.4
*/
public class LRUMap<K, V> extends LinkedHashMap<K, V> {
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<K, V> eldest) {
return size() > limit;
}
}