View Javadoc
1   /*
2    * Copyright (C) 2018, Konrad Windszus <konrad_w@gmx.de> and others
3    *
4    * This program and the accompanying materials are made available under the
5    * terms of the Eclipse Distribution License v. 1.0 which is available at
6    * https://www.eclipse.org/org/documents/edl-v10.php.
7    *
8    * SPDX-License-Identifier: BSD-3-Clause
9    */
10  package org.eclipse.jgit.internal.transport.http;
11  
12  import static org.hamcrest.MatcherAssert.assertThat;
13  import static org.junit.Assert.assertEquals;
14  import static org.junit.Assert.assertTrue;
15  
16  import java.io.IOException;
17  import java.io.InputStream;
18  import java.io.Writer;
19  import java.net.HttpCookie;
20  import java.net.URL;
21  import java.nio.charset.StandardCharsets;
22  import java.nio.file.Files;
23  import java.nio.file.Path;
24  import java.nio.file.StandardCopyOption;
25  import java.time.Instant;
26  import java.util.Arrays;
27  import java.util.Date;
28  import java.util.LinkedHashSet;
29  import java.util.List;
30  import java.util.Set;
31  import java.util.regex.Pattern;
32  
33  import org.eclipse.jgit.internal.storage.file.LockFile;
34  import org.eclipse.jgit.util.http.HttpCookiesMatcher;
35  import org.hamcrest.CoreMatchers;
36  import org.junit.Before;
37  import org.junit.Rule;
38  import org.junit.Test;
39  import org.junit.rules.TemporaryFolder;
40  
41  public class NetscapeCookieFileTest {
42  
43  	@Rule
44  	public TemporaryFolder folder = new TemporaryFolder();
45  
46  	private Path tmpFile;
47  
48  	private URL baseUrl;
49  
50  	/**
51  	 * This is the expiration date that is used in the test cookie files
52  	 */
53  	private static long JAN_01_2030_NOON = Instant
54  			.parse("2030-01-01T12:00:00.000Z").toEpochMilli();
55  
56  	@Before
57  	public void setUp() throws IOException {
58  		// this will not only return a new file name but also create new empty
59  		// file!
60  		tmpFile = folder.newFile().toPath();
61  		baseUrl = new URL("http://domain.com/my/path");
62  	}
63  
64  	@Test
65  	public void testMergeCookies() {
66  		Set<HttpCookie> cookieSet1 = new LinkedHashSet<>();
67  		HttpCookie cookie = new HttpCookie("key1", "valueFromSet1");
68  		cookieSet1.add(cookie);
69  		cookie = new HttpCookie("key2", "valueFromSet1");
70  		cookieSet1.add(cookie);
71  
72  		Set<HttpCookie> cookieSet2 = new LinkedHashSet<>();
73  		cookie = new HttpCookie("key1", "valueFromSet2");
74  		cookieSet2.add(cookie);
75  		cookie = new HttpCookie("key3", "valueFromSet2");
76  		cookieSet2.add(cookie);
77  
78  		Set<HttpCookie> cookiesExpectedMergedSet = new LinkedHashSet<>();
79  		cookie = new HttpCookie("key1", "valueFromSet1");
80  		cookiesExpectedMergedSet.add(cookie);
81  		cookie = new HttpCookie("key2", "valueFromSet1");
82  		cookiesExpectedMergedSet.add(cookie);
83  		cookie = new HttpCookie("key3", "valueFromSet2");
84  		cookiesExpectedMergedSet.add(cookie);
85  
86  		assertThat(NetscapeCookieFile.mergeCookies(cookieSet1, cookieSet2),
87  				HttpCookiesMatcher.containsInOrder(cookiesExpectedMergedSet));
88  
89  		assertThat(NetscapeCookieFile.mergeCookies(cookieSet1, null),
90  				HttpCookiesMatcher.containsInOrder(cookieSet1));
91  	}
92  
93  	@Test
94  	public void testWriteToNewFile() throws IOException {
95  		Set<HttpCookie> cookies = new LinkedHashSet<>();
96  		cookies.add(new HttpCookie("key1", "value"));
97  		// first cookie is a session cookie (and should be ignored)
98  
99  		HttpCookie cookie = new HttpCookie("key2", "value");
100 		cookie.setSecure(true);
101 		cookie.setDomain("mydomain.com");
102 		cookie.setPath("/");
103 		cookie.setMaxAge(1000);
104 		cookies.add(cookie);
105 		Date creationDate = new Date();
106 		try (Writer writer = Files.newBufferedWriter(tmpFile,
107 				StandardCharsets.US_ASCII)) {
108 			NetscapeCookieFile.write(writer, cookies, baseUrl, creationDate);
109 		}
110 
111 		String expectedExpiration = String
112 				.valueOf(creationDate.getTime() + (cookie.getMaxAge() * 1000));
113 
114 		assertThat(Files.readAllLines(tmpFile, StandardCharsets.US_ASCII),
115 				CoreMatchers
116 						.equalTo(Arrays.asList("mydomain.com\tTRUE\t/\tTRUE\t"
117 								+ expectedExpiration + "\tkey2\tvalue")));
118 	}
119 
120 	@Test
121 	public void testWriteToExistingFile() throws IOException {
122 		try (InputStream input = this.getClass()
123 				.getResourceAsStream("cookies-simple1.txt")) {
124 			Files.copy(input, tmpFile, StandardCopyOption.REPLACE_EXISTING);
125 		}
126 
127 		Set<HttpCookie> cookies = new LinkedHashSet<>();
128 		HttpCookie cookie = new HttpCookie("key2", "value2");
129 		cookie.setMaxAge(1000);
130 		cookies.add(cookie);
131 		Date creationDate = new Date();
132 		try (Writer writer = Files.newBufferedWriter(tmpFile,
133 				StandardCharsets.US_ASCII)) {
134 			NetscapeCookieFile.write(writer, cookies, baseUrl, creationDate);
135 		}
136 		String expectedExpiration = String
137 				.valueOf(creationDate.getTime() + (cookie.getMaxAge() * 1000));
138 
139 		assertThat(Files.readAllLines(tmpFile, StandardCharsets.US_ASCII),
140 				CoreMatchers.equalTo(
141 						Arrays.asList("domain.com\tTRUE\t/my/path\tFALSE\t"
142 								+ expectedExpiration + "\tkey2\tvalue2")));
143 	}
144 
145 	@Test(expected = IOException.class)
146 	public void testWriteWhileSomeoneIsHoldingTheLock()
147 			throws IllegalArgumentException, IOException, InterruptedException {
148 		try (InputStream input = this.getClass()
149 				.getResourceAsStream("cookies-simple1.txt")) {
150 			Files.copy(input, tmpFile, StandardCopyOption.REPLACE_EXISTING);
151 		}
152 		NetscapeCookieFile cookieFile = new NetscapeCookieFile(tmpFile);
153 		// now imitate another process/thread holding the lock file
154 		LockFile lockFile = new LockFile(tmpFile.toFile());
155 		try {
156 			assertTrue("Could not acquire lock", lockFile.lock());
157 			cookieFile.write(baseUrl);
158 		} finally {
159 			lockFile.unlock();
160 		}
161 	}
162 
163 	@Test
164 	public void testWriteAfterAnotherJgitProcessModifiedTheFile()
165 			throws IOException, InterruptedException {
166 		try (InputStream input = this.getClass()
167 				.getResourceAsStream("cookies-simple1.txt")) {
168 			Files.copy(input, tmpFile, StandardCopyOption.REPLACE_EXISTING);
169 		}
170 		NetscapeCookieFile cookieFile = new NetscapeCookieFile(tmpFile);
171 		cookieFile.getCookies(true);
172 		// now modify file externally
173 		try (InputStream input = this.getClass()
174 				.getResourceAsStream("cookies-simple2.txt")) {
175 			Files.copy(input, tmpFile, StandardCopyOption.REPLACE_EXISTING);
176 		}
177 		// now try to write
178 		cookieFile.write(baseUrl);
179 
180 		// validate that the external changes are there as well
181 		// due to rounding errors (conversion from ms to sec to ms)
182 		// the expiration date might not be exact
183 		List<String> lines = Files.readAllLines(tmpFile,
184 				StandardCharsets.US_ASCII);
185 
186 		assertEquals("Expected 3 lines", 3, lines.size());
187 		assertStringMatchesPatternWithInexactNumber(lines.get(0),
188 				"some-domain1\tTRUE\t/some/path1\tFALSE\t(\\d*)\tkey1\tvalueFromSimple2",
189 				JAN_01_2030_NOON, 1000);
190 		assertStringMatchesPatternWithInexactNumber(lines.get(1),
191 				"some-domain1\tTRUE\t/some/path1\tFALSE\t(\\d*)\tkey3\tvalueFromSimple2",
192 				JAN_01_2030_NOON, 1000);
193 		assertStringMatchesPatternWithInexactNumber(lines.get(2),
194 				"some-domain1\tTRUE\t/some/path1\tFALSE\t(\\d*)\tkey2\tvalueFromSimple1",
195 				JAN_01_2030_NOON, 1000);
196 	}
197 
198 	@SuppressWarnings("boxing")
199 	private static final void assertStringMatchesPatternWithInexactNumber(
200 			String string, String pattern, long expectedNumericValue,
201 			long delta) {
202 		java.util.regex.Matcher matcher = Pattern.compile(pattern)
203 				.matcher(string);
204 		assertTrue("Given string '" + string + "' does not match '" + pattern
205 				+ "'", matcher.matches());
206 		// extract numeric value
207 		Long actualNumericValue = Long.decode(matcher.group(1));
208 
209 		assertTrue(
210 				"Value is supposed to be close to " + expectedNumericValue
211 						+ " but is " + actualNumericValue + ".",
212 				Math.abs(expectedNumericValue - actualNumericValue) <= delta);
213 	}
214 
215 	@Test
216 	public void testWriteAndReadCycle() throws IOException {
217 		Set<HttpCookie> cookies = new LinkedHashSet<>();
218 
219 		HttpCookie cookie = new HttpCookie("key1", "value1");
220 		cookie.setPath("/some/path1");
221 		cookie.setDomain("some-domain1");
222 		cookie.setMaxAge(1000);
223 		cookies.add(cookie);
224 		cookie = new HttpCookie("key2", "value2");
225 		cookie.setSecure(true);
226 		cookie.setPath("/some/path2");
227 		cookie.setDomain("some-domain2");
228 		cookie.setMaxAge(1000);
229 		cookie.setHttpOnly(true);
230 		cookies.add(cookie);
231 
232 		Date creationDate = new Date();
233 
234 		try (Writer writer = Files.newBufferedWriter(tmpFile,
235 				StandardCharsets.US_ASCII)) {
236 			NetscapeCookieFile.write(writer, cookies, baseUrl, creationDate);
237 		}
238 		Set<HttpCookie> actualCookies = new NetscapeCookieFile(tmpFile,
239 				creationDate).getCookies(true);
240 		assertThat(actualCookies, HttpCookiesMatcher.containsInOrder(cookies));
241 	}
242 
243 	@Test
244 	public void testReadAndWriteCycle() throws IOException {
245 		try (InputStream input = this.getClass()
246 				.getResourceAsStream("cookies-simple1.txt")) {
247 			Files.copy(input, tmpFile, StandardCopyOption.REPLACE_EXISTING);
248 		}
249 		// round up to the next second (to prevent rounding errors)
250 		Date creationDate = new Date(
251 				(System.currentTimeMillis() / 1000) * 1000);
252 		Set<HttpCookie> cookies = new NetscapeCookieFile(tmpFile, creationDate)
253 				.getCookies(true);
254 		Path tmpFile2 = folder.newFile().toPath();
255 		try (Writer writer = Files.newBufferedWriter(tmpFile2,
256 				StandardCharsets.US_ASCII)) {
257 			NetscapeCookieFile.write(writer, cookies, baseUrl, creationDate);
258 		}
259 		// compare original file with newly written one, they should not differ
260 		assertEquals(Files.readAllLines(tmpFile), Files.readAllLines(tmpFile2));
261 	}
262 
263 	@Test
264 	public void testReadWithEmptyAndCommentLines() throws IOException {
265 		try (InputStream input = this.getClass().getResourceAsStream(
266 				"cookies-with-empty-and-comment-lines.txt")) {
267 			Files.copy(input, tmpFile, StandardCopyOption.REPLACE_EXISTING);
268 		}
269 
270 		Date creationDate = new Date();
271 		Set<HttpCookie> cookies = new LinkedHashSet<>();
272 
273 		HttpCookie cookie = new HttpCookie("key2", "value2");
274 		cookie.setDomain("some-domain2");
275 		cookie.setPath("/some/path2");
276 		cookie.setMaxAge((JAN_01_2030_NOON - creationDate.getTime()) / 1000);
277 		cookie.setSecure(true);
278 		cookie.setHttpOnly(true);
279 		cookies.add(cookie);
280 
281 		cookie = new HttpCookie("key3", "value3");
282 		cookie.setDomain("some-domain3");
283 		cookie.setPath("/some/path3");
284 		cookie.setMaxAge((JAN_01_2030_NOON - creationDate.getTime()) / 1000);
285 		cookies.add(cookie);
286 
287 		Set<HttpCookie> actualCookies = new NetscapeCookieFile(tmpFile, creationDate)
288 				.getCookies(true);
289 		assertThat(actualCookies, HttpCookiesMatcher.containsInOrder(cookies));
290 	}
291 
292 	@Test
293 	public void testReadInvalidFile() throws IOException {
294 		try (InputStream input = this.getClass()
295 				.getResourceAsStream("cookies-invalid.txt")) {
296 			Files.copy(input, tmpFile, StandardCopyOption.REPLACE_EXISTING);
297 		}
298 
299 		new NetscapeCookieFile(tmpFile)
300 				.getCookies(true);
301 	}
302 }