Support reading and writing cookies.
The git config entries "http.cookieFile" and "http.saveCookies" are correctly evaluated. Bug: 488572 Change-Id: Icfeeea95e1a5bac3fa4438849d4ac2306d7d5562 Signed-off-by: Konrad Windszus <konrad_w@gmx.de> Signed-off-by: Matthias Sohn <matthias.sohn@sap.com>
This commit is contained in:
parent
8cd07cb815
commit
d7bd2e700c
|
@ -35,6 +35,7 @@ Import-Package: com.googlecode.javaewah;version="[1.1.6,2.0.0)",
|
|||
org.eclipse.jgit.internal.storage.pack;version="[5.4.0,5.5.0)",
|
||||
org.eclipse.jgit.internal.storage.reftable;version="[5.4.0,5.5.0)",
|
||||
org.eclipse.jgit.internal.storage.reftree;version="[5.4.0,5.5.0)",
|
||||
org.eclipse.jgit.internal.transport.http;version="[5.4.0,5.5.0)",
|
||||
org.eclipse.jgit.internal.transport.parser;version="[5.4.0,5.5.0)",
|
||||
org.eclipse.jgit.junit;version="[5.4.0,5.5.0)",
|
||||
org.eclipse.jgit.junit.ssh;version="[5.4.0,5.5.0)",
|
||||
|
|
|
@ -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()));
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
|
|
|
@ -86,6 +86,7 @@ Export-Package: org.eclipse.jgit.annotations;version="5.4.0",
|
|||
org.eclipse.jgit.pgm",
|
||||
org.eclipse.jgit.internal.storage.reftree;version="5.4.0";x-friends:="org.eclipse.jgit.junit,org.eclipse.jgit.test,org.eclipse.jgit.pgm",
|
||||
org.eclipse.jgit.internal.submodule;version="5.4.0";x-internal:=true,
|
||||
org.eclipse.jgit.internal.transport.http;version="5.4.0";x-friends:="org.eclipse.jgit.test",
|
||||
org.eclipse.jgit.internal.transport.parser;version="5.4.0";x-friends:="org.eclipse.jgit.http.server,org.eclipse.jgit.test",
|
||||
org.eclipse.jgit.internal.transport.ssh;version="5.4.0";x-friends:="org.eclipse.jgit.ssh.apache",
|
||||
org.eclipse.jgit.lib;version="5.4.0";
|
||||
|
|
|
@ -208,6 +208,10 @@ couldNotDeleteTemporaryIndexFileShouldNotHappen=Could not delete temporary index
|
|||
couldNotGetAdvertisedRef=Remote {0} did not advertise Ref for branch {1}. This Ref may not exist in the remote or may be hidden by permission settings.
|
||||
couldNotGetRepoStatistics=Could not get repository statistics
|
||||
couldNotLockHEAD=Could not lock HEAD
|
||||
couldNotFindTabInLine=Could not find tab in line {0}. Tab is the mandatory separator for the Netscape Cookie File Format.
|
||||
couldNotFindSixTabsInLine=Could not find 6 tabs but only {0} in line '{1}'. 7 tab separated columns per line are mandatory for the Netscape Cookie File Format.
|
||||
couldNotPersistCookies=Could not persist received cookies in file ''{0}''
|
||||
couldNotReadCookieFile=Could not read cookie file ''{0}''
|
||||
couldNotReadIndexInOneGo=Could not read index in one go, only {0} out of {1} read
|
||||
couldNotReadObjectWhileParsingCommit=Could not read an object while parsing commit {0}
|
||||
couldNotRenameDeleteOldIndex=Could not rename delete old index
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -89,6 +89,30 @@ public class HttpConfig {
|
|||
/** git config key for the "sslVerify" setting. */
|
||||
public static final String SSL_VERIFY_KEY = "sslVerify"; //$NON-NLS-1$
|
||||
|
||||
/**
|
||||
* git config key for the "cookieFile" setting.
|
||||
*
|
||||
* @since 5.4
|
||||
*/
|
||||
public static final String COOKIE_FILE_KEY = "cookieFile"; //$NON-NLS-1$
|
||||
|
||||
/**
|
||||
* git config key for the "saveCookies" setting.
|
||||
*
|
||||
* @since 5.4
|
||||
*/
|
||||
public static final String SAVE_COOKIES_KEY = "saveCookies"; //$NON-NLS-1$
|
||||
|
||||
/**
|
||||
* Custom JGit config key which holds the maximum number of cookie files to
|
||||
* keep in the cache.
|
||||
*
|
||||
* @since 5.4
|
||||
*/
|
||||
public static final String COOKIE_FILE_CACHE_LIMIT_KEY = "cookieFileCacheLimit"; //$NON-NLS-1$
|
||||
|
||||
private static final int DEFAULT_COOKIE_FILE_CACHE_LIMIT = 10;
|
||||
|
||||
private static final String MAX_REDIRECT_SYSTEM_PROPERTY = "http.maxRedirects"; //$NON-NLS-1$
|
||||
|
||||
private static final int DEFAULT_MAX_REDIRECTS = 5;
|
||||
|
@ -153,6 +177,12 @@ public boolean matchConfigValue(String s) {
|
|||
|
||||
private int maxRedirects;
|
||||
|
||||
private String cookieFile;
|
||||
|
||||
private boolean saveCookies;
|
||||
|
||||
private int cookieFileCacheLimit;
|
||||
|
||||
/**
|
||||
* Get the "http.postBuffer" setting
|
||||
*
|
||||
|
@ -189,6 +219,40 @@ public int getMaxRedirects() {
|
|||
return maxRedirects;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the "http.cookieFile" setting
|
||||
*
|
||||
* @return the value of the "http.cookieFile" setting
|
||||
*
|
||||
* @since 5.4
|
||||
*/
|
||||
public String getCookieFile() {
|
||||
return cookieFile;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the "http.saveCookies" setting
|
||||
*
|
||||
* @return the value of the "http.saveCookies" setting
|
||||
*
|
||||
* @since 5.4
|
||||
*/
|
||||
public boolean getSaveCookies() {
|
||||
return saveCookies;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the "http.cookieFileCacheLimit" setting (gives the maximum number of
|
||||
* cookie files to keep in the LRU cache)
|
||||
*
|
||||
* @return the value of the "http.cookieFileCacheLimit" setting
|
||||
*
|
||||
* @since 5.4
|
||||
*/
|
||||
public int getCookieFileCacheLimit() {
|
||||
return cookieFileCacheLimit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new {@link org.eclipse.jgit.transport.HttpConfig} tailored to
|
||||
* the given {@link org.eclipse.jgit.transport.URIish}.
|
||||
|
@ -237,6 +301,10 @@ private void init(Config config, URIish uri) {
|
|||
if (redirectLimit < 0) {
|
||||
redirectLimit = MAX_REDIRECTS;
|
||||
}
|
||||
cookieFile = config.getString(HTTP, null, COOKIE_FILE_KEY);
|
||||
saveCookies = config.getBoolean(HTTP, SAVE_COOKIES_KEY, false);
|
||||
cookieFileCacheLimit = config.getInt(HTTP, COOKIE_FILE_CACHE_LIMIT_KEY,
|
||||
DEFAULT_COOKIE_FILE_CACHE_LIMIT);
|
||||
String match = findMatch(config.getSubsections(HTTP), uri);
|
||||
if (match != null) {
|
||||
// Override with more specific items
|
||||
|
@ -251,6 +319,13 @@ private void init(Config config, URIish uri) {
|
|||
if (newMaxRedirects >= 0) {
|
||||
redirectLimit = newMaxRedirects;
|
||||
}
|
||||
String urlSpecificCookieFile = config.getString(HTTP, match,
|
||||
COOKIE_FILE_KEY);
|
||||
if (urlSpecificCookieFile != null) {
|
||||
cookieFile = urlSpecificCookieFile;
|
||||
}
|
||||
saveCookies = config.getBoolean(HTTP, match, SAVE_COOKIES_KEY,
|
||||
saveCookies);
|
||||
}
|
||||
postBuffer = postBufferSize;
|
||||
sslVerify = sslVerifyFlag;
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -169,6 +169,27 @@ public class HttpSupport {
|
|||
/** The {@code WWW-Authenticate} header. */
|
||||
public static final String HDR_WWW_AUTHENTICATE = "WWW-Authenticate"; //$NON-NLS-1$
|
||||
|
||||
/**
|
||||
* The {@code Cookie} header.
|
||||
*
|
||||
* @since 5.4
|
||||
*/
|
||||
public static final String HDR_COOKIE = "Cookie"; //$NON-NLS-1$
|
||||
|
||||
/**
|
||||
* The {@code Set-Cookie} header.
|
||||
*
|
||||
* @since 5.4
|
||||
*/
|
||||
public static final String HDR_SET_COOKIE = "Set-Cookie"; //$NON-NLS-1$
|
||||
|
||||
/**
|
||||
* The {@code Set-Cookie2} header.
|
||||
*
|
||||
* @since 5.4
|
||||
*/
|
||||
public static final String HDR_SET_COOKIE2 = "Set-Cookie2"; //$NON-NLS-1$
|
||||
|
||||
/**
|
||||
* URL encode a value string into an output buffer.
|
||||
*
|
||||
|
|
|
@ -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