Browse Source
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>stable-5.5
Konrad Windszus
6 years ago
committed by
Matthias Sohn
18 changed files with 1774 additions and 1 deletions
@ -0,0 +1 @@
|
||||
some-domain /some/path1 FALSE 0 key1 value1 |
@ -0,0 +1,2 @@
|
||||
some-domain1 TRUE /some/path1 FALSE 1893499200000 key1 valueFromSimple1 |
||||
some-domain1 TRUE /some/path1 FALSE 1893499200000 key2 valueFromSimple1 |
@ -0,0 +1,2 @@
|
||||
some-domain1 TRUE /some/path1 FALSE 1893499200000 key1 valueFromSimple2 |
||||
some-domain1 TRUE /some/path1 FALSE 1893499200000 key3 valueFromSimple2 |
@ -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 |
@ -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("]"); |
||||
} |
||||
} |
||||
} |
@ -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()); |
||||
} |
||||
} |
||||
} |
@ -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())); |
||||
} |
||||
} |
@ -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; |
||||
} |
||||
} |
@ -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); |
||||
} |
||||
|
||||
} |
@ -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; |
||||
} |
||||
} |
Loading…
Reference in new issue