aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorKonrad Windszus <konrad_w@gmx.de>2018-11-19 18:10:07 +0100
committerMatthias Sohn <matthias.sohn@sap.com>2019-06-06 19:02:17 +0200
commitd7bd2e700c7eb1ea4f3b4a65087331c4ee96a9e5 (patch)
tree009f1b9bada4f63357a0995a041877bbfae6bf99
parent8cd07cb8157eec75099cb93c25d6daa9d5e6e0bc (diff)
downloadjgit-d7bd2e700c7eb1ea4f3b4a65087331c4ee96a9e5.tar.gz
jgit-d7bd2e700c7eb1ea4f3b4a65087331c4ee96a9e5.zip
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>
-rw-r--r--org.eclipse.jgit.test/META-INF/MANIFEST.MF1
-rw-r--r--org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/internal/transport/http/cookies-invalid.txt1
-rw-r--r--org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/internal/transport/http/cookies-simple1.txt2
-rw-r--r--org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/internal/transport/http/cookies-simple2.txt2
-rw-r--r--org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/internal/transport/http/cookies-with-empty-and-comment-lines.txt8
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/transport/http/NetscapeCookieFileTest.java441
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/TransportHttpTest.java189
-rw-r--r--org.eclipse.jgit.test/tst/org/eclipse/jgit/util/LRUMapTest.java78
-rw-r--r--org.eclipse.jgit/.settings/.api_filters40
-rw-r--r--org.eclipse.jgit/META-INF/MANIFEST.MF1
-rw-r--r--org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties4
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java4
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/internal/transport/http/NetscapeCookieFile.java471
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/internal/transport/http/NetscapeCookieFileCache.java106
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/transport/HttpConfig.java75
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportHttp.java248
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/util/HttpSupport.java21
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/util/LRUMap.java83
18 files changed, 1774 insertions, 1 deletions
diff --git a/org.eclipse.jgit.test/META-INF/MANIFEST.MF b/org.eclipse.jgit.test/META-INF/MANIFEST.MF
index 7ec5e6175f..1c6211d53d 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 0000000000..bbc6a7329a
--- /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 0000000000..e06b38c712
--- /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 0000000000..4bf6723fda
--- /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 0000000000..a9b8a28153
--- /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 0000000000..8f6cd3aae4
--- /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 <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("]");
+ }
+ }
+}
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 0000000000..111c92523c
--- /dev/null
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/TransportHttpTest.java
@@ -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());
+ }
+ }
+}
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 0000000000..da59533aeb
--- /dev/null
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/LRUMapTest.java
@@ -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()));
+ }
+}
diff --git a/org.eclipse.jgit/.settings/.api_filters b/org.eclipse.jgit/.settings/.api_filters
index 7f93191ca6..ed36dde8b3 100644
--- a/org.eclipse.jgit/.settings/.api_filters
+++ b/org.eclipse.jgit/.settings/.api_filters
@@ -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>
diff --git a/org.eclipse.jgit/META-INF/MANIFEST.MF b/org.eclipse.jgit/META-INF/MANIFEST.MF
index bd3161b438..893f0d4305 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 6da6fee387..88fdc3d818 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 7a5ef4b7e2..88b3fc8505 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 0000000000..93be5c69e3
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/transport/http/NetscapeCookieFile.java
@@ -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;
+ }
+}
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 0000000000..882b2d055b
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/transport/http/NetscapeCookieFileCache.java
@@ -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);
+ }
+
+}
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 101ce35685..54c21cbc8c 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
*
@@ -190,6 +220,40 @@ public class HttpConfig {
}
/**
+ * 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 42aa80ea48..f44a99b8b4 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<HttpCookie> 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<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 @@ 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<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 @@ 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<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();
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 54e4ee01fd..640670debc 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/util/HttpSupport.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/HttpSupport.java
@@ -170,6 +170,27 @@ public class HttpSupport {
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.
*
* @param urlstr
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 0000000000..41c15363f8
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/LRUMap.java
@@ -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;
+ }
+}