Support reading and writing cookies.

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

Bug: 488572
Change-Id: Icfeeea95e1a5bac3fa4438849d4ac2306d7d5562
Signed-off-by: Konrad Windszus <konrad_w@gmx.de>
Signed-off-by: Matthias Sohn <matthias.sohn@sap.com>
diff --git a/org.eclipse.jgit.test/META-INF/MANIFEST.MF b/org.eclipse.jgit.test/META-INF/MANIFEST.MF
index 7ec5e61..1c6211d 100644
--- a/org.eclipse.jgit.test/META-INF/MANIFEST.MF
+++ b/org.eclipse.jgit.test/META-INF/MANIFEST.MF
@@ -35,6 +35,7 @@
  org.eclipse.jgit.internal.storage.pack;version="[5.4.0,5.5.0)",
  org.eclipse.jgit.internal.storage.reftable;version="[5.4.0,5.5.0)",
  org.eclipse.jgit.internal.storage.reftree;version="[5.4.0,5.5.0)",
+ org.eclipse.jgit.internal.transport.http;version="[5.4.0,5.5.0)",
  org.eclipse.jgit.internal.transport.parser;version="[5.4.0,5.5.0)",
  org.eclipse.jgit.junit;version="[5.4.0,5.5.0)",
  org.eclipse.jgit.junit.ssh;version="[5.4.0,5.5.0)",
diff --git a/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/internal/transport/http/cookies-invalid.txt b/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/internal/transport/http/cookies-invalid.txt
new file mode 100644
index 0000000..bbc6a73
--- /dev/null
+++ b/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/internal/transport/http/cookies-invalid.txt
@@ -0,0 +1 @@
+some-domain	/some/path1	FALSE	0	key1	value1
\ No newline at end of file
diff --git a/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/internal/transport/http/cookies-simple1.txt b/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/internal/transport/http/cookies-simple1.txt
new file mode 100644
index 0000000..e06b38c
--- /dev/null
+++ b/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/internal/transport/http/cookies-simple1.txt
@@ -0,0 +1,2 @@
+some-domain1	TRUE	/some/path1	FALSE	1893499200000	key1	valueFromSimple1
+some-domain1	TRUE	/some/path1	FALSE	1893499200000	key2	valueFromSimple1
\ No newline at end of file
diff --git a/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/internal/transport/http/cookies-simple2.txt b/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/internal/transport/http/cookies-simple2.txt
new file mode 100644
index 0000000..4bf6723f
--- /dev/null
+++ b/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/internal/transport/http/cookies-simple2.txt
@@ -0,0 +1,2 @@
+some-domain1	TRUE	/some/path1	FALSE	1893499200000	key1	valueFromSimple2
+some-domain1	TRUE	/some/path1	FALSE	1893499200000	key3	valueFromSimple2
\ No newline at end of file
diff --git a/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/internal/transport/http/cookies-with-empty-and-comment-lines.txt b/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/internal/transport/http/cookies-with-empty-and-comment-lines.txt
new file mode 100644
index 0000000..a9b8a28
--- /dev/null
+++ b/org.eclipse.jgit.test/tst-rsrc/org/eclipse/jgit/internal/transport/http/cookies-with-empty-and-comment-lines.txt
@@ -0,0 +1,8 @@
+# first line is a comment
+# the next cookie is supposed to be removed, because it has expired already
+some-domain1	TRUE	/some/path1	FALSE	0	key1	value1
+
+# expires date is 01/01/2030 @ 12:00am (UTC)
+#HttpOnly_.some-domain2	TRUE	/some/path2	TRUE	1893499200000	key2	value2
+
+some-domain3	TRUE	/some/path3	FALSE	1893499200000	key3	value3
\ No newline at end of file
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/transport/http/NetscapeCookieFileTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/transport/http/NetscapeCookieFileTest.java
new file mode 100644
index 0000000..8f6cd3a
--- /dev/null
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/internal/transport/http/NetscapeCookieFileTest.java
@@ -0,0 +1,441 @@
+/*
+ * Copyright (C) 2018, Konrad Windszus <konrad_w@gmx.de>
+ * and other copyright owners as documented in the project's IP log.
+ *
+ * This program and the accompanying materials are made available
+ * under the terms of the Eclipse Distribution License v1.0 which
+ * accompanies this distribution, is reproduced below, and is
+ * available at http://www.eclipse.org/org/documents/edl-v10.php
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above
+ *   copyright notice, this list of conditions and the following
+ *   disclaimer in the documentation and/or other materials provided
+ *   with the distribution.
+ *
+ * - Neither the name of the Eclipse Foundation, Inc. nor the
+ *   names of its contributors may be used to endorse or promote
+ *   products derived from this software without specific prior
+ *   written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+ * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.eclipse.jgit.internal.transport.http;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.Writer;
+import java.net.HttpCookie;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardCopyOption;
+import java.time.Instant;
+import java.util.Arrays;
+import java.util.Date;
+import java.util.LinkedHashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Set;
+import java.util.regex.Pattern;
+
+import org.eclipse.jgit.internal.storage.file.LockFile;
+import org.eclipse.jgit.internal.transport.http.NetscapeCookieFile;
+import org.hamcrest.CoreMatchers;
+import org.hamcrest.Description;
+import org.hamcrest.Matcher;
+import org.hamcrest.TypeSafeMatcher;
+import org.hamcrest.collection.IsIterableContainingInOrder;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+public class NetscapeCookieFileTest {
+
+	@Rule
+	public TemporaryFolder folder = new TemporaryFolder();
+
+	private Path tmpFile;
+
+	private URL baseUrl;
+
+	/**
+	 * This is the expiration date that is used in the test cookie files
+	 */
+	private static long JAN_01_2030_NOON = Instant
+			.parse("2030-01-01T12:00:00.000Z").toEpochMilli();
+
+	@Before
+	public void setUp() throws IOException {
+		// this will not only return a new file name but also create new empty
+		// file!
+		tmpFile = folder.newFile().toPath();
+		baseUrl = new URL("http://domain.com/my/path");
+	}
+
+	@Test
+	public void testMergeCookies() {
+		Set<HttpCookie> cookieSet1 = new LinkedHashSet<>();
+		HttpCookie cookie = new HttpCookie("key1", "valueFromSet1");
+		cookieSet1.add(cookie);
+		cookie = new HttpCookie("key2", "valueFromSet1");
+		cookieSet1.add(cookie);
+
+		Set<HttpCookie> cookieSet2 = new LinkedHashSet<>();
+		cookie = new HttpCookie("key1", "valueFromSet2");
+		cookieSet2.add(cookie);
+		cookie = new HttpCookie("key3", "valueFromSet2");
+		cookieSet2.add(cookie);
+
+		Set<HttpCookie> cookiesExpectedMergedSet = new LinkedHashSet<>();
+		cookie = new HttpCookie("key1", "valueFromSet1");
+		cookiesExpectedMergedSet.add(cookie);
+		cookie = new HttpCookie("key2", "valueFromSet1");
+		cookiesExpectedMergedSet.add(cookie);
+		cookie = new HttpCookie("key3", "valueFromSet2");
+		cookiesExpectedMergedSet.add(cookie);
+
+		Assert.assertThat(
+				NetscapeCookieFile.mergeCookies(cookieSet1, cookieSet2),
+				HttpCookiesMatcher.containsInOrder(cookiesExpectedMergedSet));
+
+		Assert.assertThat(NetscapeCookieFile.mergeCookies(cookieSet1, null),
+				HttpCookiesMatcher.containsInOrder(cookieSet1));
+	}
+
+	@Test
+	public void testWriteToNewFile() throws IOException {
+		Set<HttpCookie> cookies = new LinkedHashSet<>();
+		cookies.add(new HttpCookie("key1", "value"));
+		// first cookie is a session cookie (and should be ignored)
+
+		HttpCookie cookie = new HttpCookie("key2", "value");
+		cookie.setSecure(true);
+		cookie.setDomain("mydomain.com");
+		cookie.setPath("/");
+		cookie.setMaxAge(1000);
+		cookies.add(cookie);
+		Date creationDate = new Date();
+		try (Writer writer = Files.newBufferedWriter(tmpFile,
+				StandardCharsets.US_ASCII)) {
+			NetscapeCookieFile.write(writer, cookies, baseUrl, creationDate);
+		}
+
+		String expectedExpiration = String
+				.valueOf(creationDate.getTime() + (cookie.getMaxAge() * 1000));
+
+		Assert.assertThat(
+				Files.readAllLines(tmpFile, StandardCharsets.US_ASCII),
+				CoreMatchers
+						.equalTo(Arrays.asList("mydomain.com\tTRUE\t/\tTRUE\t"
+								+ expectedExpiration + "\tkey2\tvalue")));
+	}
+
+	@Test
+	public void testWriteToExistingFile() throws IOException {
+		try (InputStream input = this.getClass()
+				.getResourceAsStream("cookies-simple1.txt")) {
+			Files.copy(input, tmpFile, StandardCopyOption.REPLACE_EXISTING);
+		}
+
+		Set<HttpCookie> cookies = new LinkedHashSet<>();
+		HttpCookie cookie = new HttpCookie("key2", "value2");
+		cookie.setMaxAge(1000);
+		cookies.add(cookie);
+		Date creationDate = new Date();
+		try (Writer writer = Files.newBufferedWriter(tmpFile,
+				StandardCharsets.US_ASCII)) {
+			NetscapeCookieFile.write(writer, cookies, baseUrl, creationDate);
+		}
+		String expectedExpiration = String
+				.valueOf(creationDate.getTime() + (cookie.getMaxAge() * 1000));
+
+		Assert.assertThat(
+				Files.readAllLines(tmpFile, StandardCharsets.US_ASCII),
+				CoreMatchers.equalTo(
+						Arrays.asList("domain.com\tTRUE\t/my/path\tFALSE\t"
+								+ expectedExpiration + "\tkey2\tvalue2")));
+	}
+
+	@Test(expected = IOException.class)
+	public void testWriteWhileSomeoneIsHoldingTheLock()
+			throws IllegalArgumentException, IOException, InterruptedException {
+		try (InputStream input = this.getClass()
+				.getResourceAsStream("cookies-simple1.txt")) {
+			Files.copy(input, tmpFile, StandardCopyOption.REPLACE_EXISTING);
+		}
+		NetscapeCookieFile cookieFile = new NetscapeCookieFile(tmpFile);
+		// now imitate another process/thread holding the lock file
+		LockFile lockFile = new LockFile(tmpFile.toFile());
+		try {
+			Assert.assertTrue("Could not acquire lock", lockFile.lock());
+			cookieFile.write(baseUrl);
+		} finally {
+			lockFile.unlock();
+		}
+	}
+
+	@Test
+	public void testWriteAfterAnotherJgitProcessModifiedTheFile()
+			throws IOException, InterruptedException {
+		try (InputStream input = this.getClass()
+				.getResourceAsStream("cookies-simple1.txt")) {
+			Files.copy(input, tmpFile, StandardCopyOption.REPLACE_EXISTING);
+		}
+		NetscapeCookieFile cookieFile = new NetscapeCookieFile(tmpFile);
+		cookieFile.getCookies(true);
+		// now modify file externally
+		try (InputStream input = this.getClass()
+				.getResourceAsStream("cookies-simple2.txt")) {
+			Files.copy(input, tmpFile, StandardCopyOption.REPLACE_EXISTING);
+		}
+		// now try to write
+		cookieFile.write(baseUrl);
+
+		// validate that the external changes are there as well
+		// due to rounding errors (conversion from ms to sec to ms)
+		// the expiration date might not be exact
+		List<String> lines = Files.readAllLines(tmpFile,
+				StandardCharsets.US_ASCII);
+
+		Assert.assertEquals("Expected 3 lines", 3, lines.size());
+		assertStringMatchesPatternWithInexactNumber(lines.get(0),
+				"some-domain1\tTRUE\t/some/path1\tFALSE\t(\\d*)\tkey1\tvalueFromSimple2",
+				JAN_01_2030_NOON, 1000);
+		assertStringMatchesPatternWithInexactNumber(lines.get(1),
+				"some-domain1\tTRUE\t/some/path1\tFALSE\t(\\d*)\tkey3\tvalueFromSimple2",
+				JAN_01_2030_NOON, 1000);
+		assertStringMatchesPatternWithInexactNumber(lines.get(2),
+				"some-domain1\tTRUE\t/some/path1\tFALSE\t(\\d*)\tkey2\tvalueFromSimple1",
+				JAN_01_2030_NOON, 1000);
+	}
+
+	@SuppressWarnings("boxing")
+	private static final void assertStringMatchesPatternWithInexactNumber(
+			String string, String pattern, long expectedNumericValue,
+			long delta) {
+		java.util.regex.Matcher matcher = Pattern.compile(pattern)
+				.matcher(string);
+		Assert.assertTrue("Given string '" + string + "' does not match '"
+				+ pattern + "'", matcher.matches());
+		// extract numeric value
+		Long actualNumericValue = Long.decode(matcher.group(1));
+
+		Assert.assertTrue(
+				"Value is supposed to be close to " + expectedNumericValue
+						+ " but is " + actualNumericValue + ".",
+				Math.abs(expectedNumericValue - actualNumericValue) <= delta);
+	}
+
+	@Test
+	public void testWriteAndReadCycle() throws IOException {
+		Set<HttpCookie> cookies = new LinkedHashSet<>();
+
+		HttpCookie cookie = new HttpCookie("key1", "value1");
+		cookie.setPath("/some/path1");
+		cookie.setDomain("some-domain1");
+		cookie.setMaxAge(1000);
+		cookies.add(cookie);
+		cookie = new HttpCookie("key2", "value2");
+		cookie.setSecure(true);
+		cookie.setPath("/some/path2");
+		cookie.setDomain("some-domain2");
+		cookie.setMaxAge(1000);
+		cookie.setHttpOnly(true);
+		cookies.add(cookie);
+
+		Date creationDate = new Date();
+
+		try (Writer writer = Files.newBufferedWriter(tmpFile,
+				StandardCharsets.US_ASCII)) {
+			NetscapeCookieFile.write(writer, cookies, baseUrl, creationDate);
+		}
+		Set<HttpCookie> actualCookies = new NetscapeCookieFile(tmpFile,
+				creationDate).getCookies(true);
+		Assert.assertThat(actualCookies,
+				HttpCookiesMatcher.containsInOrder(cookies));
+	}
+
+	@Test
+	public void testReadAndWriteCycle() throws IOException {
+		try (InputStream input = this.getClass()
+				.getResourceAsStream("cookies-simple1.txt")) {
+			Files.copy(input, tmpFile, StandardCopyOption.REPLACE_EXISTING);
+		}
+		// round up to the next second (to prevent rounding errors)
+		Date creationDate = new Date(
+				(System.currentTimeMillis() / 1000) * 1000);
+		Set<HttpCookie> cookies = new NetscapeCookieFile(tmpFile, creationDate)
+				.getCookies(true);
+		Path tmpFile2 = folder.newFile().toPath();
+		try (Writer writer = Files.newBufferedWriter(tmpFile2,
+				StandardCharsets.US_ASCII)) {
+			NetscapeCookieFile.write(writer, cookies, baseUrl, creationDate);
+		}
+		// compare original file with newly written one, they should not differ
+		Assert.assertEquals(Files.readAllLines(tmpFile),
+				Files.readAllLines(tmpFile2));
+	}
+
+	@Test
+	public void testReadWithEmptyAndCommentLines() throws IOException {
+		try (InputStream input = this.getClass().getResourceAsStream(
+				"cookies-with-empty-and-comment-lines.txt")) {
+			Files.copy(input, tmpFile, StandardCopyOption.REPLACE_EXISTING);
+		}
+
+		Date creationDate = new Date();
+		Set<HttpCookie> cookies = new LinkedHashSet<>();
+
+		HttpCookie cookie = new HttpCookie("key2", "value2");
+		cookie.setDomain("some-domain2");
+		cookie.setPath("/some/path2");
+		cookie.setMaxAge((JAN_01_2030_NOON - creationDate.getTime()) / 1000);
+		cookie.setSecure(true);
+		cookie.setHttpOnly(true);
+		cookies.add(cookie);
+
+		cookie = new HttpCookie("key3", "value3");
+		cookie.setDomain("some-domain3");
+		cookie.setPath("/some/path3");
+		cookie.setMaxAge((JAN_01_2030_NOON - creationDate.getTime()) / 1000);
+		cookies.add(cookie);
+
+		Set<HttpCookie> actualCookies = new NetscapeCookieFile(tmpFile, creationDate)
+				.getCookies(true);
+		Assert.assertThat(actualCookies,
+				HttpCookiesMatcher.containsInOrder(cookies));
+	}
+
+	@Test
+	public void testReadInvalidFile() throws IOException {
+		try (InputStream input = this.getClass()
+				.getResourceAsStream("cookies-invalid.txt")) {
+			Files.copy(input, tmpFile, StandardCopyOption.REPLACE_EXISTING);
+		}
+
+		new NetscapeCookieFile(tmpFile)
+				.getCookies(true);
+	}
+
+	public final static class HttpCookiesMatcher {
+		public static Matcher<Iterable<? extends HttpCookie>> containsInOrder(
+				Iterable<HttpCookie> expectedCookies) {
+			return containsInOrder(expectedCookies, 0);
+		}
+
+		public static Matcher<Iterable<? extends HttpCookie>> containsInOrder(
+				Iterable<HttpCookie> expectedCookies, int allowedMaxAgeDelta) {
+			final List<Matcher<? super HttpCookie>> cookieMatchers = new LinkedList<>();
+			for (HttpCookie cookie : expectedCookies) {
+				cookieMatchers
+						.add(new HttpCookieMatcher(cookie, allowedMaxAgeDelta));
+			}
+			return new IsIterableContainingInOrder<>(cookieMatchers);
+		}
+	}
+
+	/**
+	 * The default {@link HttpCookie#equals(Object)} is not good enough for
+	 * testing purposes. Also the {@link HttpCookie#toString()} only emits some
+	 * of the cookie attributes. For testing a dedicated matcher is needed which
+	 * takes into account all attributes.
+	 */
+	private final static class HttpCookieMatcher
+			extends TypeSafeMatcher<HttpCookie> {
+
+		private final HttpCookie cookie;
+
+		private final int allowedMaxAgeDelta;
+
+		public HttpCookieMatcher(HttpCookie cookie, int allowedMaxAgeDelta) {
+			this.cookie = cookie;
+			this.allowedMaxAgeDelta = allowedMaxAgeDelta;
+		}
+
+		@Override
+		public void describeTo(Description description) {
+			describeCookie(description, cookie);
+		}
+
+		@Override
+		protected void describeMismatchSafely(HttpCookie item,
+				Description mismatchDescription) {
+			mismatchDescription.appendText("was ");
+			describeCookie(mismatchDescription, item);
+		}
+
+		@Override
+		protected boolean matchesSafely(HttpCookie otherCookie) {
+			// the equals method in HttpCookie is not specific enough, we want
+			// to consider all attributes!
+			return (equals(cookie.getName(), otherCookie.getName())
+					&& equals(cookie.getValue(), otherCookie.getValue())
+					&& equals(cookie.getDomain(), otherCookie.getDomain())
+					&& equals(cookie.getPath(), otherCookie.getPath())
+					&& (cookie.getMaxAge() >= otherCookie.getMaxAge()
+							- allowedMaxAgeDelta)
+					&& (cookie.getMaxAge() <= otherCookie.getMaxAge()
+							+ allowedMaxAgeDelta)
+					&& cookie.isHttpOnly() == otherCookie.isHttpOnly()
+					&& cookie.getSecure() == otherCookie.getSecure()
+					&& cookie.getVersion() == otherCookie.getVersion());
+		}
+
+		private static boolean equals(String value1, String value2) {
+			if (value1 == null && value2 == null) {
+				return true;
+			}
+			if (value1 == null || value2 == null) {
+				return false;
+			}
+			return value1.equals(value2);
+		}
+
+		@SuppressWarnings("boxing")
+		protected static void describeCookie(Description description,
+				HttpCookie cookie) {
+			description.appendText("HttpCookie[");
+			description.appendText("name: ").appendValue(cookie.getName())
+					.appendText(", ");
+			description.appendText("value: ").appendValue(cookie.getValue())
+					.appendText(", ");
+			description.appendText("domain: ").appendValue(cookie.getDomain())
+					.appendText(", ");
+			description.appendText("path: ").appendValue(cookie.getPath())
+					.appendText(", ");
+			description.appendText("maxAge: ").appendValue(cookie.getMaxAge())
+					.appendText(", ");
+			description.appendText("httpOnly: ")
+					.appendValue(cookie.isHttpOnly()).appendText(", ");
+			description.appendText("secure: ").appendValue(cookie.getSecure())
+					.appendText(", ");
+			description.appendText("version: ").appendValue(cookie.getVersion())
+					.appendText(", ");
+			description.appendText("]");
+		}
+	}
+}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/TransportHttpTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/TransportHttpTest.java
new file mode 100644
index 0000000..111c925
--- /dev/null
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/TransportHttpTest.java
@@ -0,0 +1,189 @@
+/*
+ * Copyright (C) 2018, Konrad Windszus <konrad_w@gmx.de>
+ * and other copyright owners as documented in the project's IP log.
+ *
+ * This program and the accompanying materials are made available
+ * under the terms of the Eclipse Distribution License v1.0 which
+ * accompanies this distribution, is reproduced below, and is
+ * available at http://www.eclipse.org/org/documents/edl-v10.php
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above
+ *   copyright notice, this list of conditions and the following
+ *   disclaimer in the documentation and/or other materials provided
+ *   with the distribution.
+ *
+ * - Neither the name of the Eclipse Foundation, Inc. nor the
+ *   names of its contributors may be used to endorse or promote
+ *   products derived from this software without specific prior
+ *   written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+ * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.eclipse.jgit.transport;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.HttpCookie;
+import java.time.Instant;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Date;
+import java.util.LinkedHashSet;
+import java.util.Set;
+
+import org.eclipse.jgit.internal.transport.http.NetscapeCookieFile;
+import org.eclipse.jgit.internal.transport.http.NetscapeCookieFileTest.HttpCookiesMatcher;
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.test.resources.SampleDataRepositoryTestCase;
+import org.eclipse.jgit.transport.http.HttpConnection;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.ArgumentMatchers;
+import org.mockito.Mockito;
+
+public class TransportHttpTest extends SampleDataRepositoryTestCase {
+	private URIish uri;
+	private File cookieFile;
+
+	@Override
+	@Before
+	public void setUp() throws Exception {
+		super.setUp();
+		uri = new URIish("https://everyones.loves.git/u/2");
+
+		final Config config = db.getConfig();
+		config.setBoolean("http", null, "saveCookies", true);
+		cookieFile = createTempFile();
+		config.setString("http", null, "cookieFile",
+				cookieFile.getAbsolutePath());
+	}
+
+	@Test
+	public void testMatchesCookieDomain() {
+		Assert.assertTrue(TransportHttp.matchesCookieDomain("example.com",
+				"example.com"));
+		Assert.assertTrue(TransportHttp.matchesCookieDomain("Example.Com",
+				"example.cOM"));
+		Assert.assertTrue(TransportHttp.matchesCookieDomain(
+				"some.subdomain.example.com", "example.com"));
+		Assert.assertFalse(TransportHttp
+				.matchesCookieDomain("someotherexample.com", "example.com"));
+		Assert.assertFalse(TransportHttp.matchesCookieDomain("example.com",
+				"example1.com"));
+		Assert.assertFalse(TransportHttp
+				.matchesCookieDomain("sub.sub.example.com", ".example.com"));
+		Assert.assertTrue(TransportHttp.matchesCookieDomain("host.example.com",
+				"example.com"));
+		Assert.assertTrue(TransportHttp.matchesCookieDomain(
+				"something.example.com", "something.example.com"));
+		Assert.assertTrue(TransportHttp.matchesCookieDomain(
+				"host.something.example.com", "something.example.com"));
+	}
+
+	@Test
+	public void testMatchesCookiePath() {
+		Assert.assertTrue(
+				TransportHttp.matchesCookiePath("/some/path", "/some/path"));
+		Assert.assertTrue(TransportHttp.matchesCookiePath("/some/path/child",
+				"/some/path"));
+		Assert.assertTrue(TransportHttp.matchesCookiePath("/some/path/child",
+				"/some/path/"));
+		Assert.assertFalse(TransportHttp.matchesCookiePath("/some/pathother",
+				"/some/path"));
+		Assert.assertFalse(
+				TransportHttp.matchesCookiePath("otherpath", "/some/path"));
+	}
+
+	@Test
+	public void testProcessResponseCookies() throws IOException {
+		HttpConnection connection = Mockito.mock(HttpConnection.class);
+		Mockito.when(
+				connection.getHeaderFields(ArgumentMatchers.eq("Set-Cookie")))
+				.thenReturn(Arrays.asList(
+						"id=a3fWa; Expires=Fri, 01 Jan 2100 11:00:00 GMT; Secure; HttpOnly",
+						"sessionid=38afes7a8; HttpOnly; Path=/"));
+		Mockito.when(
+				connection.getHeaderFields(ArgumentMatchers.eq("Set-Cookie2")))
+				.thenReturn(Collections
+						.singletonList("cookie2=some value; Max-Age=1234; Path=/"));
+
+		try (TransportHttp transportHttp = new TransportHttp(db, uri)) {
+			Date creationDate = new Date();
+			transportHttp.processResponseCookies(connection);
+
+			// evaluate written cookie file
+			Set<HttpCookie> expectedCookies = new LinkedHashSet<>();
+
+			HttpCookie cookie = new HttpCookie("id", "a3fWa");
+			cookie.setDomain("everyones.loves.git");
+			cookie.setPath("/u/2/");
+
+			cookie.setMaxAge(
+					(Instant.parse("2100-01-01T11:00:00.000Z").toEpochMilli()
+							- creationDate.getTime()) / 1000);
+			cookie.setSecure(true);
+			cookie.setHttpOnly(true);
+			expectedCookies.add(cookie);
+
+			cookie = new HttpCookie("cookie2", "some value");
+			cookie.setDomain("everyones.loves.git");
+			cookie.setPath("/");
+			cookie.setMaxAge(1234);
+			expectedCookies.add(cookie);
+
+			Assert.assertThat(
+					new NetscapeCookieFile(cookieFile.toPath())
+							.getCookies(true),
+					HttpCookiesMatcher.containsInOrder(expectedCookies, 5));
+		}
+	}
+
+	@Test
+	public void testProcessResponseCookiesNotPersistingWithSaveCookiesFalse()
+			throws IOException {
+		HttpConnection connection = Mockito.mock(HttpConnection.class);
+		Mockito.when(
+				connection.getHeaderFields(ArgumentMatchers.eq("Set-Cookie")))
+				.thenReturn(Arrays.asList(
+						"id=a3fWa; Expires=Thu, 21 Oct 2100 11:00:00 GMT; Secure; HttpOnly",
+						"sessionid=38afes7a8; HttpOnly; Path=/"));
+		Mockito.when(
+				connection.getHeaderFields(ArgumentMatchers.eq("Set-Cookie2")))
+				.thenReturn(Collections.singletonList(
+						"cookie2=some value; Max-Age=1234; Path=/"));
+
+		// tweak config
+		final Config config = db.getConfig();
+		config.setBoolean("http", null, "saveCookies", false);
+
+		try (TransportHttp transportHttp = new TransportHttp(db, uri)) {
+			transportHttp.processResponseCookies(connection);
+
+			// evaluate written cookie file
+			Assert.assertFalse("Cookie file was not supposed to be written!",
+					cookieFile.exists());
+		}
+	}
+}
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/LRUMapTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/LRUMapTest.java
new file mode 100644
index 0000000..da59533
--- /dev/null
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/util/LRUMapTest.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2018, Konrad Windszus <konrad_w@gmx.de>
+ * and other copyright owners as documented in the project's IP log.
+ *
+ * This program and the accompanying materials are made available
+ * under the terms of the Eclipse Distribution License v1.0 which
+ * accompanies this distribution, is reproduced below, and is
+ * available at http://www.eclipse.org/org/documents/edl-v10.php
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above
+ *   copyright notice, this list of conditions and the following
+ *   disclaimer in the documentation and/or other materials provided
+ *   with the distribution.
+ *
+ * - Neither the name of the Eclipse Foundation, Inc. nor the
+ *   names of its contributors may be used to endorse or promote
+ *   products derived from this software without specific prior
+ *   written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+ * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.eclipse.jgit.util;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+import org.hamcrest.collection.IsIterableContainingInOrder;
+import org.junit.Assert;
+import org.junit.Test;
+
+public class LRUMapTest {
+
+	@SuppressWarnings("boxing")
+	@Test
+	public void testLRUEntriesAreEvicted() {
+		Map<Integer, Integer> map = new LRUMap<>(3, 3);
+		for (int i = 0; i < 3; i++) {
+			map.put(i, i);
+		}
+		// access the last ones
+		map.get(2);
+		map.get(0);
+
+		// put another one which exceeds the limit (entry with key "1" is
+		// evicted)
+		map.put(3, 3);
+
+		Map<Integer, Integer> expectedMap = new LinkedHashMap<>();
+		expectedMap.put(2, 2);
+		expectedMap.put(0, 0);
+		expectedMap.put(3, 3);
+
+		Assert.assertThat(map.entrySet(),
+				IsIterableContainingInOrder
+						.contains(expectedMap.entrySet().toArray()));
+	}
+}
diff --git a/org.eclipse.jgit/.settings/.api_filters b/org.eclipse.jgit/.settings/.api_filters
index 7f93191..ed36dde 100644
--- a/org.eclipse.jgit/.settings/.api_filters
+++ b/org.eclipse.jgit/.settings/.api_filters
@@ -68,4 +68,44 @@
             </message_arguments>
         </filter>
     </resource>
+    <resource path="src/org/eclipse/jgit/transport/HttpConfig.java" type="org.eclipse.jgit.transport.HttpConfig">
+        <filter id="336658481">
+            <message_arguments>
+                <message_argument value="org.eclipse.jgit.transport.HttpConfig"/>
+                <message_argument value="COOKIE_FILE_CACHE_LIMIT_KEY"/>
+            </message_arguments>
+        </filter>
+        <filter id="336658481">
+            <message_arguments>
+                <message_argument value="org.eclipse.jgit.transport.HttpConfig"/>
+                <message_argument value="COOKIE_FILE_KEY"/>
+            </message_arguments>
+        </filter>
+        <filter id="336658481">
+            <message_arguments>
+                <message_argument value="org.eclipse.jgit.transport.HttpConfig"/>
+                <message_argument value="SAVE_COOKIES_KEY"/>
+            </message_arguments>
+        </filter>
+    </resource>
+    <resource path="src/org/eclipse/jgit/util/HttpSupport.java" type="org.eclipse.jgit.util.HttpSupport">
+        <filter id="336658481">
+            <message_arguments>
+                <message_argument value="org.eclipse.jgit.util.HttpSupport"/>
+                <message_argument value="HDR_COOKIE"/>
+            </message_arguments>
+        </filter>
+        <filter id="336658481">
+            <message_arguments>
+                <message_argument value="org.eclipse.jgit.util.HttpSupport"/>
+                <message_argument value="HDR_SET_COOKIE"/>
+            </message_arguments>
+        </filter>
+        <filter id="336658481">
+            <message_arguments>
+                <message_argument value="org.eclipse.jgit.util.HttpSupport"/>
+                <message_argument value="HDR_SET_COOKIE2"/>
+            </message_arguments>
+        </filter>
+    </resource>
 </component>
diff --git a/org.eclipse.jgit/META-INF/MANIFEST.MF b/org.eclipse.jgit/META-INF/MANIFEST.MF
index bd3161b..893f0d4 100644
--- a/org.eclipse.jgit/META-INF/MANIFEST.MF
+++ b/org.eclipse.jgit/META-INF/MANIFEST.MF
@@ -86,6 +86,7 @@
    org.eclipse.jgit.pgm",
  org.eclipse.jgit.internal.storage.reftree;version="5.4.0";x-friends:="org.eclipse.jgit.junit,org.eclipse.jgit.test,org.eclipse.jgit.pgm",
  org.eclipse.jgit.internal.submodule;version="5.4.0";x-internal:=true,
+ org.eclipse.jgit.internal.transport.http;version="5.4.0";x-friends:="org.eclipse.jgit.test",
  org.eclipse.jgit.internal.transport.parser;version="5.4.0";x-friends:="org.eclipse.jgit.http.server,org.eclipse.jgit.test",
  org.eclipse.jgit.internal.transport.ssh;version="5.4.0";x-friends:="org.eclipse.jgit.ssh.apache",
  org.eclipse.jgit.lib;version="5.4.0";
diff --git a/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties b/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties
index 6da6fee..88fdc3d 100644
--- a/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties
+++ b/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties
@@ -208,6 +208,10 @@
 couldNotGetAdvertisedRef=Remote {0} did not advertise Ref for branch {1}. This Ref may not exist in the remote or may be hidden by permission settings.
 couldNotGetRepoStatistics=Could not get repository statistics
 couldNotLockHEAD=Could not lock HEAD
+couldNotFindTabInLine=Could not find tab in line {0}. Tab is the mandatory separator for the Netscape Cookie File Format.
+couldNotFindSixTabsInLine=Could not find 6 tabs but only {0} in line '{1}'. 7 tab separated columns per line are mandatory for the Netscape Cookie File Format.
+couldNotPersistCookies=Could not persist received cookies in file ''{0}''
+couldNotReadCookieFile=Could not read cookie file ''{0}''
 couldNotReadIndexInOneGo=Could not read index in one go, only {0} out of {1} read
 couldNotReadObjectWhileParsingCommit=Could not read an object while parsing commit {0}
 couldNotRenameDeleteOldIndex=Could not rename delete old index
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java
index 7a5ef4b..88b3fc8 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java
@@ -267,9 +267,13 @@ public static JGitText get() {
 	/***/ public String couldNotCheckOutBecauseOfConflicts;
 	/***/ public String couldNotDeleteLockFileShouldNotHappen;
 	/***/ public String couldNotDeleteTemporaryIndexFileShouldNotHappen;
+	/***/ public String couldNotFindTabInLine;
+	/***/ public String couldNotFindSixTabsInLine;
 	/***/ public String couldNotGetAdvertisedRef;
 	/***/ public String couldNotGetRepoStatistics;
 	/***/ public String couldNotLockHEAD;
+	/***/ public String couldNotPersistCookies;
+	/***/ public String couldNotReadCookieFile;
 	/***/ public String couldNotReadIndexInOneGo;
 	/***/ public String couldNotReadObjectWhileParsingCommit;
 	/***/ public String couldNotRenameDeleteOldIndex;
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/transport/http/NetscapeCookieFile.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/transport/http/NetscapeCookieFile.java
new file mode 100644
index 0000000..93be5c6
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/transport/http/NetscapeCookieFile.java
@@ -0,0 +1,471 @@
+/*
+ * Copyright (C) 2018, Konrad Windszus <konrad_w@gmx.de>
+ * and other copyright owners as documented in the project's IP log.
+ *
+ * This program and the accompanying materials are made available
+ * under the terms of the Eclipse Distribution License v1.0 which
+ * accompanies this distribution, is reproduced below, and is
+ * available at http://www.eclipse.org/org/documents/edl-v10.php
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above
+ *   copyright notice, this list of conditions and the following
+ *   disclaimer in the documentation and/or other materials provided
+ *   with the distribution.
+ *
+ * - Neither the name of the Eclipse Foundation, Inc. nor the
+ *   names of its contributors may be used to endorse or promote
+ *   products derived from this software without specific prior
+ *   written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+ * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.eclipse.jgit.internal.transport.http;
+
+import java.io.BufferedReader;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.OutputStreamWriter;
+import java.io.StringReader;
+import java.io.Writer;
+import java.net.HttpCookie;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Path;
+import java.text.MessageFormat;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Date;
+import java.util.LinkedHashSet;
+import java.util.Set;
+
+import org.eclipse.jgit.annotations.NonNull;
+import org.eclipse.jgit.annotations.Nullable;
+import org.eclipse.jgit.internal.JGitText;
+import org.eclipse.jgit.internal.storage.file.FileSnapshot;
+import org.eclipse.jgit.internal.storage.file.LockFile;
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.storage.file.FileBasedConfig;
+import org.eclipse.jgit.util.FileUtils;
+import org.eclipse.jgit.util.IO;
+import org.eclipse.jgit.util.RawParseUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Wraps all cookies persisted in a <strong>Netscape Cookie File Format</strong>
+ * being referenced via the git config <a href=
+ * "https://git-scm.com/docs/git-config#git-config-httpcookieFile">http.cookieFile</a>.
+ *
+ * It will only load the cookies lazily, i.e. before calling
+ * {@link #getCookies(boolean)} the file is not evaluated. This class also
+ * allows persisting cookies in that file format.
+ * <p>
+ * In general this class is not thread-safe. So any consumer needs to take care
+ * of synchronization!
+ *
+ * @see <a href="http://www.cookiecentral.com/faq/#3.5">Netscape Cookie File
+ *      Format</a>
+ * @see <a href=
+ *      "https://unix.stackexchange.com/questions/36531/format-of-cookies-when-using-wget">Cookie
+ *      format for wget</a>
+ * @see <a href=
+ *      "https://github.com/curl/curl/blob/07ebaf837843124ee670e5b8c218b80b92e06e47/lib/cookie.c#L745">libcurl
+ *      Cookie file parsing</a>
+ * @see <a href=
+ *      "https://github.com/curl/curl/blob/07ebaf837843124ee670e5b8c218b80b92e06e47/lib/cookie.c#L1417">libcurl
+ *      Cookie file writing</a>
+ * @see NetscapeCookieFileCache
+ */
+public final class NetscapeCookieFile {
+
+	private static final String HTTP_ONLY_PREAMBLE = "#HttpOnly_"; //$NON-NLS-1$
+
+	private static final String COLUMN_SEPARATOR = "\t"; //$NON-NLS-1$
+
+	private static final String LINE_SEPARATOR = "\n"; //$NON-NLS-1$
+
+	/**
+	 * Maximum number of retries to acquire the lock for writing to the
+	 * underlying file.
+	 */
+	private static final int LOCK_ACQUIRE_MAX_RETRY_COUNT = 4;
+
+	/**
+	 * Sleep time in milliseconds between retries to acquire the lock for
+	 * writing to the underlying file.
+	 */
+	private static final int LOCK_ACQUIRE_RETRY_SLEEP = 500;
+
+	private final Path path;
+
+	private FileSnapshot snapshot;
+
+	private byte[] hash;
+
+	final Date creationDate;
+
+	private Set<HttpCookie> cookies = null;
+
+	private static final Logger LOG = LoggerFactory
+			.getLogger(NetscapeCookieFile.class);
+
+	/**
+	 * @param path
+	 */
+	public NetscapeCookieFile(Path path) {
+		this(path, new Date());
+	}
+
+	NetscapeCookieFile(Path path, Date creationDate) {
+		this.path = path;
+		this.snapshot = FileSnapshot.DIRTY;
+		this.creationDate = creationDate;
+	}
+
+	/**
+	 * @return the path to the underlying cookie file
+	 */
+	public Path getPath() {
+		return path;
+	}
+
+	/**
+	 * @param refresh
+	 *            if {@code true} updates the list from the underlying cookie
+	 *            file if it has been modified since the last read otherwise
+	 *            returns the current transient state. In case the cookie file
+	 *            has never been read before will always read from the
+	 *            underlying file disregarding the value of this parameter.
+	 * @return all cookies (may contain session cookies as well). This does not
+	 *         return a copy of the list but rather the original one. Every
+	 *         addition to the returned list can afterwards be persisted via
+	 *         {@link #write(URL)}. Errors in the underlying file will not lead
+	 *         to exceptions but rather to an empty set being returned and the
+	 *         underlying error being logged.
+	 */
+	public Set<HttpCookie> getCookies(boolean refresh) {
+		if (cookies == null || refresh) {
+			try {
+				byte[] in = getFileContentIfModified();
+				Set<HttpCookie> newCookies = parseCookieFile(in, creationDate);
+				if (cookies != null) {
+					cookies = mergeCookies(newCookies, cookies);
+				} else {
+					cookies = newCookies;
+				}
+				return cookies;
+			} catch (IOException | IllegalArgumentException e) {
+				LOG.warn(
+						MessageFormat.format(
+								JGitText.get().couldNotReadCookieFile, path),
+						e);
+				if (cookies == null) {
+					cookies = new LinkedHashSet<>();
+				}
+			}
+		}
+		return cookies;
+
+	}
+
+	/**
+	 * Parses the given file and extracts all cookie information from it.
+	 *
+	 * @param input
+	 *            the file content to parse
+	 * @param creationDate
+	 *            the date for the creation of the cookies (used to calculate
+	 *            the maxAge based on the expiration date given within the file)
+	 * @return the set of parsed cookies from the given file (even expired
+	 *         ones). If there is more than one cookie with the same name in
+	 *         this file the last one overwrites the first one!
+	 * @throws IOException
+	 *             if the given file could not be read for some reason
+	 * @throws IllegalArgumentException
+	 *             if the given file does not have a proper format.
+	 */
+	private static Set<HttpCookie> parseCookieFile(@NonNull byte[] input,
+			@NonNull Date creationDate)
+			throws IOException, IllegalArgumentException {
+
+		String decoded = RawParseUtils.decode(StandardCharsets.US_ASCII, input);
+
+		Set<HttpCookie> cookies = new LinkedHashSet<>();
+		try (BufferedReader reader = new BufferedReader(
+				new StringReader(decoded))) {
+			String line;
+			while ((line = reader.readLine()) != null) {
+				HttpCookie cookie = parseLine(line, creationDate);
+				if (cookie != null) {
+					cookies.add(cookie);
+				}
+			}
+		}
+		return cookies;
+	}
+
+	private static HttpCookie parseLine(@NonNull String line,
+			@NonNull Date creationDate) {
+		if (line.isEmpty() || (line.startsWith("#") //$NON-NLS-1$
+				&& !line.startsWith(HTTP_ONLY_PREAMBLE))) {
+			return null;
+		}
+		String[] cookieLineParts = line.split(COLUMN_SEPARATOR, 7);
+		if (cookieLineParts == null) {
+			throw new IllegalArgumentException(MessageFormat
+					.format(JGitText.get().couldNotFindTabInLine, line));
+		}
+		if (cookieLineParts.length < 7) {
+			throw new IllegalArgumentException(MessageFormat.format(
+					JGitText.get().couldNotFindSixTabsInLine,
+					Integer.valueOf(cookieLineParts.length), line));
+		}
+		String name = cookieLineParts[5];
+		String value = cookieLineParts[6];
+		HttpCookie cookie = new HttpCookie(name, value);
+
+		String domain = cookieLineParts[0];
+		if (domain.startsWith(HTTP_ONLY_PREAMBLE)) {
+			cookie.setHttpOnly(true);
+			domain = domain.substring(HTTP_ONLY_PREAMBLE.length());
+		}
+		// strip off leading "."
+		// (https://tools.ietf.org/html/rfc6265#section-5.2.3)
+		if (domain.startsWith(".")) { //$NON-NLS-1$
+			domain = domain.substring(1);
+		}
+		cookie.setDomain(domain);
+		// domain evaluation as boolean flag not considered (i.e. always assumed
+		// to be true)
+		cookie.setPath(cookieLineParts[2]);
+		cookie.setSecure(Boolean.parseBoolean(cookieLineParts[3]));
+
+		long expires = Long.parseLong(cookieLineParts[4]);
+		long maxAge = (expires - creationDate.getTime()) / 1000;
+		if (maxAge <= 0) {
+			return null; // skip expired cookies
+		}
+		cookie.setMaxAge(maxAge);
+		return cookie;
+	}
+
+	/**
+	 * Writes all the cookies being maintained in the set being returned by
+	 * {@link #getCookies(boolean)} to the underlying file.
+	 *
+	 * Session-cookies will not be persisted.
+	 *
+	 * @param url
+	 *            url for which to write the cookies (important to derive
+	 *            default values for non-explicitly set attributes)
+	 * @throws IOException
+	 * @throws IllegalArgumentException
+	 * @throws InterruptedException
+	 */
+	public void write(URL url)
+			throws IllegalArgumentException, IOException, InterruptedException {
+		try {
+			byte[] cookieFileContent = getFileContentIfModified();
+			if (cookieFileContent != null) {
+				LOG.debug(
+						"Reading the underlying cookie file '{}' as it has been modified since the last access", //$NON-NLS-1$
+						path);
+				// reread new changes if necessary
+				Set<HttpCookie> cookiesFromFile = NetscapeCookieFile
+						.parseCookieFile(cookieFileContent, creationDate);
+				this.cookies = mergeCookies(cookiesFromFile, cookies);
+			}
+		} catch (FileNotFoundException e) {
+			// ignore if file previously did not exist yet!
+		}
+
+		ByteArrayOutputStream output = new ByteArrayOutputStream();
+		try (Writer writer = new OutputStreamWriter(output,
+				StandardCharsets.US_ASCII)) {
+			write(writer, cookies, url, creationDate);
+		}
+		LockFile lockFile = new LockFile(path.toFile());
+		for (int retryCount = 0; retryCount < LOCK_ACQUIRE_MAX_RETRY_COUNT; retryCount++) {
+			if (lockFile.lock()) {
+				try {
+					lockFile.setNeedSnapshot(true);
+					lockFile.write(output.toByteArray());
+					if (!lockFile.commit()) {
+						throw new IOException(MessageFormat.format(
+								JGitText.get().cannotCommitWriteTo, path));
+					}
+				} finally {
+					lockFile.unlock();
+				}
+				return;
+			}
+			Thread.sleep(LOCK_ACQUIRE_RETRY_SLEEP);
+		}
+		throw new IOException(
+				MessageFormat.format(JGitText.get().cannotLock, lockFile));
+
+	}
+
+	/**
+	 * Read the underying file and return its content but only in case it has
+	 * been modified since the last access. Internally calculates the hash and
+	 * maintains {@link FileSnapshot}s to prevent issues described as <a href=
+	 * "https://github.com/git/git/blob/master/Documentation/technical/racy-git.txt">"Racy
+	 * Git problem"</a>. Inspired by {@link FileBasedConfig#load()}.
+	 *
+	 * @return the file contents in case the file has been modified since the
+	 *         last access, otherwise {@code null}
+	 * @throws IOException
+	 */
+	private byte[] getFileContentIfModified() throws IOException {
+		final int maxStaleRetries = 5;
+		int retries = 0;
+		File file = getPath().toFile();
+		while (true) {
+			final FileSnapshot oldSnapshot = snapshot;
+			final FileSnapshot newSnapshot = FileSnapshot.save(file);
+			try {
+				final byte[] in = IO.readFully(file);
+				byte[] newHash = hash(in);
+				if (Arrays.equals(hash, newHash)) {
+					if (oldSnapshot.equals(newSnapshot)) {
+						oldSnapshot.setClean(newSnapshot);
+					} else {
+						snapshot = newSnapshot;
+					}
+				} else {
+					snapshot = newSnapshot;
+					hash = newHash;
+				}
+				return in;
+			} catch (FileNotFoundException e) {
+				throw e;
+			} catch (IOException e) {
+				if (FileUtils.isStaleFileHandle(e)
+						&& retries < maxStaleRetries) {
+					if (LOG.isDebugEnabled()) {
+						LOG.debug(MessageFormat.format(
+								JGitText.get().configHandleIsStale,
+								Integer.valueOf(retries)), e);
+					}
+					retries++;
+					continue;
+				}
+				throw new IOException(MessageFormat
+						.format(JGitText.get().cannotReadFile, getPath()), e);
+			}
+		}
+
+	}
+
+	private byte[] hash(final byte[] in) {
+		return Constants.newMessageDigest().digest(in);
+	}
+
+	/**
+	 * Writes the given cookies to the file in the Netscape Cookie File Format
+	 * (also used by curl)
+	 *
+	 * @param writer
+	 *            the writer to use to persist the cookies.
+	 * @param cookies
+	 *            the cookies to write into the file
+	 * @param url
+	 *            the url for which to write the cookie (to derive the default
+	 *            values for certain cookie attributes)
+	 * @param creationDate
+	 *            the date when the cookie has been created. Important for
+	 *            calculation the cookie expiration time (calculated from
+	 *            cookie's maxAge and this creation time).
+	 * @throws IOException
+	 */
+	static void write(@NonNull Writer writer,
+			@NonNull Collection<HttpCookie> cookies, @NonNull URL url,
+			@NonNull Date creationDate) throws IOException {
+		for (HttpCookie cookie : cookies) {
+			writeCookie(writer, cookie, url, creationDate);
+		}
+	}
+
+	private static void writeCookie(@NonNull Writer writer,
+			@NonNull HttpCookie cookie, @NonNull URL url,
+			@NonNull Date creationDate) throws IOException {
+		if (cookie.getMaxAge() <= 0) {
+			return; // skip expired cookies
+		}
+		String domain = ""; //$NON-NLS-1$
+		if (cookie.isHttpOnly()) {
+			domain = HTTP_ONLY_PREAMBLE;
+		}
+		if (cookie.getDomain() != null) {
+			domain += cookie.getDomain();
+		} else {
+			domain += url.getHost();
+		}
+		writer.write(domain);
+		writer.write(COLUMN_SEPARATOR);
+		writer.write("TRUE"); //$NON-NLS-1$
+		writer.write(COLUMN_SEPARATOR);
+		String path = cookie.getPath();
+		if (path == null) {
+			path = url.getPath();
+		}
+		writer.write(path);
+		writer.write(COLUMN_SEPARATOR);
+		writer.write(Boolean.toString(cookie.getSecure()).toUpperCase());
+		writer.write(COLUMN_SEPARATOR);
+		final String expirationDate;
+		// whenCreated field is not accessible in HttpCookie
+		expirationDate = String
+				.valueOf(creationDate.getTime() + (cookie.getMaxAge() * 1000));
+		writer.write(expirationDate);
+		writer.write(COLUMN_SEPARATOR);
+		writer.write(cookie.getName());
+		writer.write(COLUMN_SEPARATOR);
+		writer.write(cookie.getValue());
+		writer.write(LINE_SEPARATOR);
+	}
+
+	/**
+	 * Merge the given sets in the following way. All cookies from
+	 * {@code cookies1} and {@code cookies2} are contained in the resulting set
+	 * which have unique names. If there is a duplicate entry for one name only
+	 * the entry from set {@code cookies1} ends up in the resulting set.
+	 *
+	 * @param cookies1
+	 * @param cookies2
+	 *
+	 * @return the merged cookies
+	 */
+	static Set<HttpCookie> mergeCookies(Set<HttpCookie> cookies1,
+			@Nullable Set<HttpCookie> cookies2) {
+		Set<HttpCookie> mergedCookies = new LinkedHashSet<>(cookies1);
+		if (cookies2 != null) {
+			mergedCookies.addAll(cookies2);
+		}
+		return mergedCookies;
+	}
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/transport/http/NetscapeCookieFileCache.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/transport/http/NetscapeCookieFileCache.java
new file mode 100644
index 0000000..882b2d0
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/transport/http/NetscapeCookieFileCache.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright (C) 2018, Konrad Windszus <konrad_w@gmx.de>
+ * and other copyright owners as documented in the project's IP log.
+ *
+ * This program and the accompanying materials are made available
+ * under the terms of the Eclipse Distribution License v1.0 which
+ * accompanies this distribution, is reproduced below, and is
+ * available at http://www.eclipse.org/org/documents/edl-v10.php
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above
+ *   copyright notice, this list of conditions and the following
+ *   disclaimer in the documentation and/or other materials provided
+ *   with the distribution.
+ *
+ * - Neither the name of the Eclipse Foundation, Inc. nor the
+ *   names of its contributors may be used to endorse or promote
+ *   products derived from this software without specific prior
+ *   written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+ * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.eclipse.jgit.internal.transport.http;
+
+import java.nio.file.Path;
+
+import org.eclipse.jgit.transport.HttpConfig;
+import org.eclipse.jgit.util.LRUMap;
+
+/**
+ * A cache of all known cookie files ({@link NetscapeCookieFile}). May contain
+ * at most {@code n} entries, where the least-recently used one is evicted as
+ * soon as more entries are added. The maximum number of entries (={@code n})
+ * can be set via the git config key {@code http.cookieFileCacheLimit}. By
+ * default it is set to 10.
+ * <p>
+ * The cache is global, i.e. it is shared among all consumers within the same
+ * Java process.
+ *
+ * @see NetscapeCookieFile
+ *
+ */
+public class NetscapeCookieFileCache {
+
+	private final LRUMap<Path, NetscapeCookieFile> cookieFileMap;
+
+	private static NetscapeCookieFileCache instance;
+
+	private NetscapeCookieFileCache(HttpConfig config) {
+		cookieFileMap = new LRUMap<>(config.getCookieFileCacheLimit(),
+				config.getCookieFileCacheLimit());
+	}
+
+	/**
+	 * @param config
+	 *            the config which defines the limit for this cache
+	 * @return the singleton instance of the cookie file cache. If the cache has
+	 *         already been created the given config is ignored (even if it
+	 *         differs from the config, with which the cache has originally been
+	 *         created)
+	 */
+	public static NetscapeCookieFileCache getInstance(HttpConfig config) {
+		if (instance == null) {
+			return new NetscapeCookieFileCache(config);
+		} else {
+			return instance;
+		}
+	}
+
+	/**
+	 * @param path
+	 *            the path of the cookie file to retrieve
+	 * @return the cache entry belonging to the requested file
+	 */
+	public NetscapeCookieFile getEntry(Path path) {
+		if (!cookieFileMap.containsKey(path)) {
+			synchronized (NetscapeCookieFileCache.class) {
+				if (!cookieFileMap.containsKey(path)) {
+					cookieFileMap.put(path, new NetscapeCookieFile(path));
+				}
+			}
+		}
+		return cookieFileMap.get(path);
+	}
+
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/HttpConfig.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/HttpConfig.java
index 101ce35..54c21cb 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/HttpConfig.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/HttpConfig.java
@@ -89,6 +89,30 @@ public class HttpConfig {
 	/** git config key for the "sslVerify" setting. */
 	public static final String SSL_VERIFY_KEY = "sslVerify"; //$NON-NLS-1$
 
+	/**
+	 * git config key for the "cookieFile" setting.
+	 *
+	 * @since 5.4
+	 */
+	public static final String COOKIE_FILE_KEY = "cookieFile"; //$NON-NLS-1$
+
+	/**
+	 * git config key for the "saveCookies" setting.
+	 *
+	 * @since 5.4
+	 */
+	public static final String SAVE_COOKIES_KEY = "saveCookies"; //$NON-NLS-1$
+
+	/**
+	 * Custom JGit config key which holds the maximum number of cookie files to
+	 * keep in the cache.
+	 *
+	 * @since 5.4
+	 */
+	public static final String COOKIE_FILE_CACHE_LIMIT_KEY = "cookieFileCacheLimit"; //$NON-NLS-1$
+
+	private static final int DEFAULT_COOKIE_FILE_CACHE_LIMIT = 10;
+
 	private static final String MAX_REDIRECT_SYSTEM_PROPERTY = "http.maxRedirects"; //$NON-NLS-1$
 
 	private static final int DEFAULT_MAX_REDIRECTS = 5;
@@ -153,6 +177,12 @@ public boolean matchConfigValue(String s) {
 
 	private int maxRedirects;
 
+	private String cookieFile;
+
+	private boolean saveCookies;
+
+	private int cookieFileCacheLimit;
+
 	/**
 	 * Get the "http.postBuffer" setting
 	 *
@@ -190,6 +220,40 @@ public int getMaxRedirects() {
 	}
 
 	/**
+	 * Get the "http.cookieFile" setting
+	 *
+	 * @return the value of the "http.cookieFile" setting
+	 *
+	 * @since 5.4
+	 */
+	public String getCookieFile() {
+		return cookieFile;
+	}
+
+	/**
+	 * Get the "http.saveCookies" setting
+	 *
+	 * @return the value of the "http.saveCookies" setting
+	 *
+	 * @since 5.4
+	 */
+	public boolean getSaveCookies() {
+		return saveCookies;
+	}
+
+	/**
+	 * Get the "http.cookieFileCacheLimit" setting (gives the maximum number of
+	 * cookie files to keep in the LRU cache)
+	 *
+	 * @return the value of the "http.cookieFileCacheLimit" setting
+	 *
+	 * @since 5.4
+	 */
+	public int getCookieFileCacheLimit() {
+		return cookieFileCacheLimit;
+	}
+
+	/**
 	 * Creates a new {@link org.eclipse.jgit.transport.HttpConfig} tailored to
 	 * the given {@link org.eclipse.jgit.transport.URIish}.
 	 *
@@ -237,6 +301,10 @@ private void init(Config config, URIish uri) {
 		if (redirectLimit < 0) {
 			redirectLimit = MAX_REDIRECTS;
 		}
+		cookieFile = config.getString(HTTP, null, COOKIE_FILE_KEY);
+		saveCookies = config.getBoolean(HTTP, SAVE_COOKIES_KEY, false);
+		cookieFileCacheLimit = config.getInt(HTTP, COOKIE_FILE_CACHE_LIMIT_KEY,
+				DEFAULT_COOKIE_FILE_CACHE_LIMIT);
 		String match = findMatch(config.getSubsections(HTTP), uri);
 		if (match != null) {
 			// Override with more specific items
@@ -251,6 +319,13 @@ private void init(Config config, URIish uri) {
 			if (newMaxRedirects >= 0) {
 				redirectLimit = newMaxRedirects;
 			}
+			String urlSpecificCookieFile = config.getString(HTTP, match,
+					COOKIE_FILE_KEY);
+			if (urlSpecificCookieFile != null) {
+				cookieFile = urlSpecificCookieFile;
+			}
+			saveCookies = config.getBoolean(HTTP, match, SAVE_COOKIES_KEY,
+					saveCookies);
 		}
 		postBuffer = postBufferSize;
 		sslVerify = sslVerifyFlag;
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportHttp.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportHttp.java
index 42aa80e..f44a99b 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportHttp.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/TransportHttp.java
@@ -54,8 +54,11 @@
 import static org.eclipse.jgit.util.HttpSupport.HDR_ACCEPT_ENCODING;
 import static org.eclipse.jgit.util.HttpSupport.HDR_CONTENT_ENCODING;
 import static org.eclipse.jgit.util.HttpSupport.HDR_CONTENT_TYPE;
+import static org.eclipse.jgit.util.HttpSupport.HDR_COOKIE;
 import static org.eclipse.jgit.util.HttpSupport.HDR_LOCATION;
 import static org.eclipse.jgit.util.HttpSupport.HDR_PRAGMA;
+import static org.eclipse.jgit.util.HttpSupport.HDR_SET_COOKIE;
+import static org.eclipse.jgit.util.HttpSupport.HDR_SET_COOKIE2;
 import static org.eclipse.jgit.util.HttpSupport.HDR_USER_AGENT;
 import static org.eclipse.jgit.util.HttpSupport.HDR_WWW_AUTHENTICATE;
 import static org.eclipse.jgit.util.HttpSupport.METHOD_GET;
@@ -68,11 +71,15 @@
 import java.io.InputStream;
 import java.io.InputStreamReader;
 import java.io.OutputStream;
+import java.net.HttpCookie;
 import java.net.MalformedURLException;
 import java.net.Proxy;
 import java.net.ProxySelector;
 import java.net.URISyntaxException;
 import java.net.URL;
+import java.nio.file.InvalidPathException;
+import java.nio.file.Path;
+import java.nio.file.Paths;
 import java.security.cert.CertPathBuilderException;
 import java.security.cert.CertPathValidatorException;
 import java.security.cert.CertificateException;
@@ -84,6 +91,8 @@
 import java.util.EnumSet;
 import java.util.HashSet;
 import java.util.LinkedHashSet;
+import java.util.LinkedList;
+import java.util.List;
 import java.util.Locale;
 import java.util.Map;
 import java.util.Set;
@@ -100,6 +109,8 @@
 import org.eclipse.jgit.errors.TransportException;
 import org.eclipse.jgit.internal.JGitText;
 import org.eclipse.jgit.internal.storage.file.RefDirectory;
+import org.eclipse.jgit.internal.transport.http.NetscapeCookieFile;
+import org.eclipse.jgit.internal.transport.http.NetscapeCookieFileCache;
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.ObjectIdRef;
@@ -116,6 +127,7 @@
 import org.eclipse.jgit.util.HttpSupport;
 import org.eclipse.jgit.util.IO;
 import org.eclipse.jgit.util.RawParseUtils;
+import org.eclipse.jgit.util.StringUtils;
 import org.eclipse.jgit.util.SystemReader;
 import org.eclipse.jgit.util.TemporaryBuffer;
 import org.eclipse.jgit.util.io.DisabledOutputStream;
@@ -274,6 +286,19 @@ public Transport open(URIish uri, Repository local, String remoteName)
 
 	private boolean sslFailure = false;
 
+	/**
+	 * All stored cookies bound to this repo (independent of the baseUrl)
+	 */
+	private final NetscapeCookieFile cookieFile;
+
+	/**
+	 * The cookies to be sent with each request to the given {@link #baseUrl}.
+	 * Filtered view on top of {@link #cookieFile} where only cookies which
+	 * apply to the current url are left. This set needs to be filtered for
+	 * expired entries each time prior to sending them.
+	 */
+	private final Set<HttpCookie> relevantCookies;
+
 	TransportHttp(Repository local, URIish uri)
 			throws NotSupportedException {
 		super(local, uri);
@@ -281,6 +306,8 @@ public Transport open(URIish uri, Repository local, String remoteName)
 		http = new HttpConfig(local.getConfig(), uri);
 		proxySelector = ProxySelector.getDefault();
 		sslVerify = http.isSslVerify();
+		cookieFile = getCookieFileFromConfig(http);
+		relevantCookies = filterCookies(cookieFile, baseUrl);
 	}
 
 	private URL toURL(URIish urish) throws MalformedURLException {
@@ -321,6 +348,8 @@ protected void setURI(URIish uri) throws NotSupportedException {
 		http = new HttpConfig(uri);
 		proxySelector = ProxySelector.getDefault();
 		sslVerify = http.isSslVerify();
+		cookieFile = getCookieFileFromConfig(http);
+		relevantCookies = filterCookies(cookieFile, baseUrl);
 	}
 
 	/**
@@ -508,6 +537,7 @@ private HttpConnection connect(String service)
 					conn.setRequestProperty(HDR_ACCEPT, "*/*"); //$NON-NLS-1$
 				}
 				final int status = HttpSupport.response(conn);
+				processResponseCookies(conn);
 				switch (status) {
 				case HttpConnection.HTTP_OK:
 					// Check if HttpConnection did some authentication in the
@@ -596,6 +626,57 @@ private HttpConnection connect(String service)
 		}
 	}
 
+	void processResponseCookies(HttpConnection conn) {
+		if (cookieFile != null && http.getSaveCookies()) {
+			List<HttpCookie> foundCookies = new LinkedList<>();
+
+			List<String> cookieHeaderValues = conn
+					.getHeaderFields(HDR_SET_COOKIE);
+			if (!cookieHeaderValues.isEmpty()) {
+				foundCookies.addAll(
+						extractCookies(HDR_SET_COOKIE, cookieHeaderValues));
+			}
+			cookieHeaderValues = conn.getHeaderFields(HDR_SET_COOKIE2);
+			if (!cookieHeaderValues.isEmpty()) {
+				foundCookies.addAll(
+						extractCookies(HDR_SET_COOKIE2, cookieHeaderValues));
+			}
+			if (foundCookies.size() > 0) {
+				try {
+					// update cookie lists with the newly received cookies!
+					Set<HttpCookie> cookies = cookieFile.getCookies(false);
+					cookies.addAll(foundCookies);
+					cookieFile.write(baseUrl);
+					relevantCookies.addAll(foundCookies);
+				} catch (IOException | IllegalArgumentException
+						| InterruptedException e) {
+					LOG.warn(MessageFormat.format(
+							JGitText.get().couldNotPersistCookies,
+							cookieFile.getPath()), e);
+				}
+			}
+		}
+	}
+
+	private List<HttpCookie> extractCookies(String headerKey,
+			List<String> headerValues) {
+		List<HttpCookie> foundCookies = new LinkedList<>();
+		for (String headerValue : headerValues) {
+			foundCookies
+					.addAll(HttpCookie.parse(headerKey + ':' + headerValue));
+		}
+		// HttpCookies.parse(...) is only compliant with RFC 2965. Make it RFC
+		// 6265 compliant by applying the logic from
+		// https://tools.ietf.org/html/rfc6265#section-5.2.3
+		for (HttpCookie foundCookie : foundCookies) {
+			String domain = foundCookie.getDomain();
+			if (domain != null && domain.startsWith(".")) { //$NON-NLS-1$
+				foundCookie.setDomain(domain.substring(1));
+			}
+		}
+		return foundCookies;
+	}
+
 	private static class CredentialItems {
 		CredentialItem.InformationalMessage message;
 
@@ -847,14 +928,35 @@ protected HttpConnection httpOpen(String method, URL u,
 			conn.setConnectTimeout(effTimeOut);
 			conn.setReadTimeout(effTimeOut);
 		}
+		// set cookie header if necessary
+		if (relevantCookies.size() > 0) {
+			setCookieHeader(conn);
+		}
+
 		if (this.headers != null && !this.headers.isEmpty()) {
-			for (Map.Entry<String, String> entry : this.headers.entrySet())
+			for (Map.Entry<String, String> entry : this.headers.entrySet()) {
 				conn.setRequestProperty(entry.getKey(), entry.getValue());
+			}
 		}
 		authMethod.configureRequest(conn);
 		return conn;
 	}
 
+	private void setCookieHeader(HttpConnection conn) {
+		StringBuilder cookieHeaderValue = new StringBuilder();
+		for (HttpCookie cookie : relevantCookies) {
+			if (!cookie.hasExpired()) {
+				if (cookieHeaderValue.length() > 0) {
+					cookieHeaderValue.append(';');
+				}
+				cookieHeaderValue.append(cookie.toString());
+			}
+		}
+		if (cookieHeaderValue.length() >= 0) {
+			conn.setRequestProperty(HDR_COOKIE, cookieHeaderValue.toString());
+		}
+	}
+
 	final InputStream openInputStream(HttpConnection conn)
 			throws IOException {
 		InputStream input = conn.getInputStream();
@@ -868,6 +970,150 @@ IOException wrongContentType(String expType, String actType) {
 		return new TransportException(uri, why);
 	}
 
+	private static NetscapeCookieFile getCookieFileFromConfig(
+			HttpConfig config) {
+		if (!StringUtils.isEmptyOrNull(config.getCookieFile())) {
+			try {
+				Path cookieFilePath = Paths.get(config.getCookieFile());
+				return NetscapeCookieFileCache.getInstance(config)
+						.getEntry(cookieFilePath);
+			} catch (InvalidPathException e) {
+				LOG.warn(MessageFormat.format(
+						JGitText.get().couldNotReadCookieFile,
+						config.getCookieFile()), e);
+			}
+		}
+		return null;
+	}
+
+	private static Set<HttpCookie> filterCookies(NetscapeCookieFile cookieFile,
+			URL url) {
+		if (cookieFile != null) {
+			return filterCookies(cookieFile.getCookies(true), url);
+		}
+		return Collections.emptySet();
+	}
+
+	/**
+	 *
+	 * @param allCookies
+	 *            a list of cookies.
+	 * @param url
+	 *            the url for which to filter the list of cookies.
+	 * @return only the cookies from {@code allCookies} which are relevant (i.e.
+	 *         are not expired, have a matching domain, have a matching path and
+	 *         have a matching secure attribute)
+	 */
+	private static Set<HttpCookie> filterCookies(Set<HttpCookie> allCookies,
+			URL url) {
+		Set<HttpCookie> filteredCookies = new HashSet<>();
+		for (HttpCookie cookie : allCookies) {
+			if (cookie.hasExpired()) {
+				continue;
+			}
+			if (!matchesCookieDomain(url.getHost(), cookie.getDomain())) {
+				continue;
+			}
+			if (!matchesCookiePath(url.getPath(), cookie.getPath())) {
+				continue;
+			}
+			if (cookie.getSecure() && !"https".equals(url.getProtocol())) { //$NON-NLS-1$
+				continue;
+			}
+			filteredCookies.add(cookie);
+		}
+		return filteredCookies;
+	}
+
+	/**
+	 *
+	 * The utility method to check whether a host name is in a cookie's domain
+	 * or not. Similar to {@link HttpCookie#domainMatches(String, String)} but
+	 * implements domain matching rules according to
+	 * <a href="https://tools.ietf.org/html/rfc6265#section-5.1.3">RFC 6265,
+	 * section 5.1.3</a> instead of the rules from
+	 * <a href="https://tools.ietf.org/html/rfc2965#section-3.3">RFC 2965,
+	 * section 3.3.1</a>.
+	 * <p>
+	 * The former rules are also used by libcurl internally.
+	 * <p>
+	 * The rules are as follows
+	 *
+	 * A string matches another domain string if at least one of the following
+	 * conditions holds:
+	 * <ul>
+	 * <li>The domain string and the string are identical. (Note that both the
+	 * domain string and the string will have been canonicalized to lower case
+	 * at this point.)</li>
+	 * <li>All of the following conditions hold
+	 * <ul>
+	 * <li>The domain string is a suffix of the string.</li>
+	 * <li>The last character of the string that is not included in the domain
+	 * string is a %x2E (".") character.</li>
+	 * <li>The string is a host name (i.e., not an IP address).</li>
+	 * </ul>
+	 * </li>
+	 * </ul>
+	 *
+	 * @param host
+	 *            the host to compare against the cookieDomain
+	 * @param cookieDomain
+	 *            the domain to compare against
+	 * @return {@code true} if they domain-match; {@code false} if not
+	 *
+	 * @see <a href= "https://tools.ietf.org/html/rfc6265#section-5.1.3">RFC
+	 *      6265, section 5.1.3 (Domain Matching)</a>
+	 * @see <a href=
+	 *      "https://bugs.java.com/bugdatabase/view_bug.do?bug_id=8206092">JDK-8206092
+	 *      : HttpCookie.domainMatches() does not match to sub-sub-domain</a>
+	 */
+	static boolean matchesCookieDomain(String host, String cookieDomain) {
+		cookieDomain = cookieDomain.toLowerCase(Locale.ROOT);
+		host = host.toLowerCase(Locale.ROOT);
+		if (host.equals(cookieDomain)) {
+			return true;
+		} else {
+			if (!host.endsWith(cookieDomain)) {
+				return false;
+			}
+			return host
+					.charAt(host.length() - cookieDomain.length() - 1) == '.';
+		}
+	}
+
+	/**
+	 * The utility method to check whether a path is matching a cookie path
+	 * domain or not. The rules are defined by
+	 * <a href="https://tools.ietf.org/html/rfc6265#section-5.1.4">RFC 6265,
+	 * section 5.1.4</a>:
+	 *
+	 * A request-path path-matches a given cookie-path if at least one of the
+	 * following conditions holds:
+	 * <ul>
+	 * <li>The cookie-path and the request-path are identical.</li>
+	 * <li>The cookie-path is a prefix of the request-path, and the last
+	 * character of the cookie-path is %x2F ("/").</li>
+	 * <li>The cookie-path is a prefix of the request-path, and the first
+	 * character of the request-path that is not included in the cookie- path is
+	 * a %x2F ("/") character.</li>
+	 * </ul>
+	 * @param path
+	 *            the path to check
+	 * @param cookiePath
+	 *            the cookie's path
+	 *
+	 * @return {@code true} if they path-match; {@code false} if not
+	 */
+	static boolean matchesCookiePath(String path, String cookiePath) {
+		if (cookiePath.equals(path)) {
+			return true;
+		}
+		if (!cookiePath.endsWith("/")) { //$NON-NLS-1$
+			cookiePath += "/"; //$NON-NLS-1$
+		}
+		return path.startsWith(cookiePath);
+	}
+
 	private boolean isSmartHttp(HttpConnection c, String service) {
 		final String expType = "application/x-" + service + "-advertisement"; //$NON-NLS-1$ //$NON-NLS-2$
 		final String actType = c.getContentType();
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/HttpSupport.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/HttpSupport.java
index 54e4ee0..640670d 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/util/HttpSupport.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/HttpSupport.java
@@ -170,6 +170,27 @@ public class HttpSupport {
 	public static final String HDR_WWW_AUTHENTICATE = "WWW-Authenticate"; //$NON-NLS-1$
 
 	/**
+	 * The {@code Cookie} header.
+	 *
+	 * @since 5.4
+	 */
+	public static final String HDR_COOKIE = "Cookie"; //$NON-NLS-1$
+
+	/**
+	 * The {@code Set-Cookie} header.
+	 *
+	 * @since 5.4
+	 */
+	public static final String HDR_SET_COOKIE = "Set-Cookie"; //$NON-NLS-1$
+
+	/**
+	 * The {@code Set-Cookie2} header.
+	 *
+	 * @since 5.4
+	 */
+	public static final String HDR_SET_COOKIE2 = "Set-Cookie2"; //$NON-NLS-1$
+
+	/**
 	 * URL encode a value string into an output buffer.
 	 *
 	 * @param urlstr
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/LRUMap.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/LRUMap.java
new file mode 100644
index 0000000..41c1536
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/LRUMap.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2018, Konrad Windszus <konrad_w@gmx.de>
+ * and other copyright owners as documented in the project's IP log.
+ *
+ * This program and the accompanying materials are made available
+ * under the terms of the Eclipse Distribution License v1.0 which
+ * accompanies this distribution, is reproduced below, and is
+ * available at http://www.eclipse.org/org/documents/edl-v10.php
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above
+ *   copyright notice, this list of conditions and the following
+ *   disclaimer in the documentation and/or other materials provided
+ *   with the distribution.
+ *
+ * - Neither the name of the Eclipse Foundation, Inc. nor the
+ *   names of its contributors may be used to endorse or promote
+ *   products derived from this software without specific prior
+ *   written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+ * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.eclipse.jgit.util;
+
+import java.util.LinkedHashMap;
+
+/**
+ * Map with only up to n entries. If a new entry is added so that the map
+ * contains more than those n entries the least-recently used entry is removed
+ * from the map.
+ *
+ * @param <K>
+ *            the type of keys maintained by this map
+ * @param <V>
+ *            the type of mapped values
+ *
+ * @since 5.4
+ */
+public class LRUMap<K, V> extends LinkedHashMap<K, V> {
+
+	private static final long serialVersionUID = 4329609127403759486L;
+
+	private final int limit;
+
+	/**
+	 * Constructs an empty map which may contain at most the given amount of
+	 * entries.
+	 *
+	 * @param initialCapacity
+	 *            the initial capacity
+	 * @param limit
+	 *            the number of entries the map should have at most
+	 */
+	public LRUMap(int initialCapacity, int limit) {
+		super(initialCapacity, 0.75f, true);
+		this.limit = limit;
+	}
+
+	@Override
+	protected boolean removeEldestEntry(java.util.Map.Entry<K, V> eldest) {
+		return size() > limit;
+	}
+}