]> source.dussan.org Git - jgit.git/commitdiff
Support reading and writing cookies. 04/132704/25
authorKonrad Windszus <konrad_w@gmx.de>
Mon, 19 Nov 2018 17:10:07 +0000 (18:10 +0100)
committerMatthias Sohn <matthias.sohn@sap.com>
Thu, 6 Jun 2019 17:02:17 +0000 (19:02 +0200)
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>
18 files changed:
org.eclipse.jgit.test/META-INF/MANIFEST.MF
org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/internal/transport/http/cookies-invalid.txt [new file with mode: 0644]
org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/internal/transport/http/cookies-simple1.txt [new file with mode: 0644]
org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/internal/transport/http/cookies-simple2.txt [new file with mode: 0644]
org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/internal/transport/http/cookies-with-empty-and-comment-lines.txt [new file with mode: 0644]
org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/transport/http/NetscapeCookieFileTest.java [new file with mode: 0644]
org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/TransportHttpTest.java [new file with mode: 0644]
org.eclipse.jgit.test/tst/org/eclipse/jgit/util/LRUMapTest.java [new file with mode: 0644]
org.eclipse.jgit/.settings/.api_filters
org.eclipse.jgit/META-INF/MANIFEST.MF
org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties
org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java
org.eclipse.jgit/src/org/eclipse/jgit/internal/transport/http/NetscapeCookieFile.java [new file with mode: 0644]
org.eclipse.jgit/src/org/eclipse/jgit/internal/transport/http/NetscapeCookieFileCache.java [new file with mode: 0644]
org.eclipse.jgit/src/org/eclipse/jgit/transport/HttpConfig.java
org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportHttp.java
org.eclipse.jgit/src/org/eclipse/jgit/util/HttpSupport.java
org.eclipse.jgit/src/org/eclipse/jgit/util/LRUMap.java [new file with mode: 0644]

index 7ec5e6175f396cac388f755c0b925095123cdc3e..1c6211d53d0403b347947a2c0605fc81cfdcb981 100644 (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)",
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 (file)
index 0000000..bbc6a73
--- /dev/null
@@ -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 (file)
index 0000000..e06b38c
--- /dev/null
@@ -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 (file)
index 0000000..4bf6723
--- /dev/null
@@ -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 (file)
index 0000000..a9b8a28
--- /dev/null
@@ -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 (file)
index 0000000..8f6cd3a
--- /dev/null
@@ -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 (file)
index 0000000..111c925
--- /dev/null
@@ -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 (file)
index 0000000..da59533
--- /dev/null
@@ -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()));
+       }
+}
index 7f93191ca6ba6141573e922abf586011c3872a34..ed36dde8b3c3027778731604fd86c7e6c60c9cd9 100644 (file)
             </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>
index bd3161b438b998c4e888e14c737112e966f625a6..893f0d4305457d96815194f1dc9b4308640ca734 100644 (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";
index 6da6fee387c87262e1a00f4ea181b55a3b786996..88fdc3d818eea22ca6cd37198d40540fbfb5da35 100644 (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
index 7a5ef4b7e2554dc7dd850958816345e8b78913ca..88b3fc8505d805f167e1632c8d1b7f3f9b69c8d8 100644 (file)
@@ -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 (file)
index 0000000..93be5c6
--- /dev/null
@@ -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 (file)
index 0000000..882b2d0
--- /dev/null
@@ -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);
+       }
+
+}
index 101ce35685fba9f967a16a3f210789abbf5d8309..54c21cbc8cbf09804efa9f64c199c694cbc50ceb 100644 (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 class HttpConfig {
 
        private int maxRedirects;
 
+       private String cookieFile;
+
+       private boolean saveCookies;
+
+       private int cookieFileCacheLimit;
+
        /**
         * Get the "http.postBuffer" setting
         *
@@ -189,6 +219,40 @@ public class HttpConfig {
                return maxRedirects;
        }
 
+       /**
+        * Get the "http.cookieFile" setting
+        *
+        * @return the value of the "http.cookieFile" setting
+        *
+        * @since 5.4
+        */
+       public String getCookieFile() {
+               return cookieFile;
+       }
+
+       /**
+        * Get the "http.saveCookies" setting
+        *
+        * @return the value of the "http.saveCookies" setting
+        *
+        * @since 5.4
+        */
+       public boolean getSaveCookies() {
+               return saveCookies;
+       }
+
+       /**
+        * Get the "http.cookieFileCacheLimit" setting (gives the maximum number of
+        * cookie files to keep in the LRU cache)
+        *
+        * @return the value of the "http.cookieFileCacheLimit" setting
+        *
+        * @since 5.4
+        */
+       public int getCookieFileCacheLimit() {
+               return cookieFileCacheLimit;
+       }
+
        /**
         * Creates a new {@link org.eclipse.jgit.transport.HttpConfig} tailored to
         * the given {@link org.eclipse.jgit.transport.URIish}.
@@ -237,6 +301,10 @@ public class HttpConfig {
                if (redirectLimit < 0) {
                        redirectLimit = MAX_REDIRECTS;
                }
+               cookieFile = config.getString(HTTP, null, COOKIE_FILE_KEY);
+               saveCookies = config.getBoolean(HTTP, SAVE_COOKIES_KEY, false);
+               cookieFileCacheLimit = config.getInt(HTTP, COOKIE_FILE_CACHE_LIMIT_KEY,
+                               DEFAULT_COOKIE_FILE_CACHE_LIMIT);
                String match = findMatch(config.getSubsections(HTTP), uri);
                if (match != null) {
                        // Override with more specific items
@@ -251,6 +319,13 @@ public class HttpConfig {
                        if (newMaxRedirects >= 0) {
                                redirectLimit = newMaxRedirects;
                        }
+                       String urlSpecificCookieFile = config.getString(HTTP, match,
+                                       COOKIE_FILE_KEY);
+                       if (urlSpecificCookieFile != null) {
+                               cookieFile = urlSpecificCookieFile;
+                       }
+                       saveCookies = config.getBoolean(HTTP, match, SAVE_COOKIES_KEY,
+                                       saveCookies);
                }
                postBuffer = postBufferSize;
                sslVerify = sslVerifyFlag;
index 42aa80ea48cda14bff47c214e5aba1e514e3398c..f44a99b8b411bf4e5b0a6222715fb59652de48ea 100644 (file)
@@ -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();
index 54e4ee01fd8844fd27aabf4ac85747ef14f1c780..640670debca5edced47f9503e162f4790fdeacde 100644 (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.
         *
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 (file)
index 0000000..41c1536
--- /dev/null
@@ -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;
+       }
+}