001/* 002 * Licensed to DuraSpace under one or more contributor license agreements. 003 * See the NOTICE file distributed with this work for additional information 004 * regarding copyright ownership. 005 * 006 * DuraSpace licenses this file to you under the Apache License, 007 * Version 2.0 (the "License"); you may not use this file except in 008 * compliance with the License. You may obtain a copy of the License at 009 * 010 * http://www.apache.org/licenses/LICENSE-2.0 011 * 012 * Unless required by applicable law or agreed to in writing, software 013 * distributed under the License is distributed on an "AS IS" BASIS, 014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 015 * See the License for the specific language governing permissions and 016 * limitations under the License. 017 */ 018package org.fcrepo.http.api; 019 020import static java.nio.file.StandardOpenOption.APPEND; 021import static org.junit.Assert.assertTrue; 022import static org.junit.Assert.fail; 023 024import java.io.BufferedWriter; 025import java.io.File; 026import java.io.IOException; 027import java.nio.file.Files; 028import org.fcrepo.kernel.api.exception.ExternalMessageBodyException; 029import org.junit.After; 030import org.junit.Before; 031import org.junit.Ignore; 032import org.junit.Rule; 033import org.junit.Test; 034import org.junit.rules.TemporaryFolder; 035 036/** 037 * @author bbpennel 038 */ 039public class ExternalContentPathValidatorTest { 040 041 private ExternalContentPathValidator validator; 042 043 @Rule 044 public TemporaryFolder tmpDir = new TemporaryFolder(); 045 046 private File dataDir; 047 048 private String dataUri; 049 050 private File allowListFile; 051 052 private File goodFile; 053 054 private String goodFileUri; 055 056 @Before 057 public void init() throws Exception { 058 allowListFile = tmpDir.newFile(); 059 060 validator = new ExternalContentPathValidator(); 061 validator.setConfigPath(allowListFile.getAbsolutePath()); 062 063 dataDir = tmpDir.newFolder(); 064 dataUri = dataDir.toURI().toString(); 065 066 goodFile = new File(dataDir, "file.txt"); 067 goodFile.createNewFile(); 068 goodFileUri = goodFile.toURI().toString(); 069 } 070 071 @After 072 public void after() { 073 validator.shutdown(); 074 } 075 076 @Test(expected = ExternalMessageBodyException.class) 077 public void testValidateWithNoAllowList() throws Exception { 078 validator.setConfigPath(null); 079 validator.init(); 080 081 validator.validate(goodFileUri); 082 } 083 084 @Test 085 public void testValidFileUri() throws Exception { 086 addAllowedPath(dataUri); 087 088 validator.validate(goodFileUri); 089 } 090 091 @Test 092 public void testValidHttpUri() throws Exception { 093 final String goodPath = "http://example.com/"; 094 final String extPath = goodPath + "file.txt"; 095 096 addAllowedPath(goodPath); 097 098 validator.validate(extPath); 099 } 100 101 @Test 102 public void testExactFileUri() throws Exception { 103 addAllowedPath(goodFileUri); 104 105 validator.validate(goodFileUri); 106 } 107 108 @Test 109 public void testMultipleMatches() throws Exception { 110 new File(dataDir, "file.txt").createNewFile(); 111 final String extPath = goodFileUri; 112 final String anotherPath = tmpDir.getRoot().toURI().toString(); 113 114 addAllowedPath(anotherPath); 115 addAllowedPath(dataUri); 116 117 validator.validate(extPath); 118 } 119 120 @Test 121 public void testMultipleSchemes() throws Exception { 122 final String httpPath = "http://example.com/"; 123 124 addAllowedPath(httpPath); 125 addAllowedPath(dataUri); 126 127 validator.validate(goodFileUri); 128 } 129 130 @Test(expected = ExternalMessageBodyException.class) 131 public void testInvalidFileUri() throws Exception { 132 final String extPath = tmpDir.newFile("file.txt").toURI().toString(); 133 134 addAllowedPath(dataUri); 135 136 validator.validate(extPath); 137 } 138 139 @Test(expected = ExternalMessageBodyException.class) 140 public void testMalformedUri() throws Exception { 141 final String goodPath = "http://example.com/"; 142 final String extPath = ".bad://example.com/file.txt"; 143 144 addAllowedPath(goodPath); 145 146 validator.validate(extPath); 147 } 148 149 @Test(expected = ExternalMessageBodyException.class) 150 public void testNonabsoluteUri() throws Exception { 151 final String goodPath = "http://example.com/"; 152 final String extPath = "/example.com/file.txt"; 153 154 addAllowedPath(goodPath); 155 156 validator.validate(extPath); 157 } 158 159 @Test(expected = ExternalMessageBodyException.class) 160 public void testInvalidHttpUri() throws Exception { 161 final String goodPath = "http://good.example.com/"; 162 final String extPath = "http://bad.example.com/file.txt"; 163 164 addAllowedPath(goodPath); 165 166 validator.validate(extPath); 167 } 168 169 @Test(expected = ExternalMessageBodyException.class) 170 public void testHttpUriMissingSlash() throws Exception { 171 // Slash after domain is required 172 final String goodPath = "http://good.example.com"; 173 final String extPath = "http://good.example.com:8080/offlimits"; 174 175 addAllowedPath(goodPath); 176 177 validator.validate(extPath); 178 } 179 180 @Test(expected = ExternalMessageBodyException.class) 181 public void testRelativeModifier() throws Exception { 182 final String extPath = dataUri + "../sneaky.txt"; 183 184 addAllowedPath(dataUri); 185 186 validator.validate(extPath); 187 } 188 189 @Test(expected = ExternalMessageBodyException.class) 190 public void testNoScheme() throws Exception { 191 final String extPath = dataDir.getAbsolutePath() + "file.txt"; 192 193 addAllowedPath(dataUri); 194 195 validator.validate(extPath); 196 } 197 198 @Test(expected = ExternalMessageBodyException.class) 199 public void testEmptyPath() throws Exception { 200 addAllowedPath(dataUri); 201 202 validator.validate(""); 203 } 204 205 @Test(expected = ExternalMessageBodyException.class) 206 public void testNullPath() throws Exception { 207 addAllowedPath(dataUri); 208 209 validator.validate(null); 210 } 211 212 @Test(expected = IOException.class) 213 public void testListFileDoesNotExist() throws Exception { 214 allowListFile.delete(); 215 216 validator.init(); 217 } 218 219 @Test 220 public void testAllowAny() throws Exception { 221 addAllowedPath("file:///"); 222 addAllowedPath("http://"); 223 addAllowedPath("https://"); 224 225 final String path1 = goodFileUri; 226 validator.validate(path1); 227 final String path2 = "http://example.com/file"; 228 validator.validate(path2); 229 final String path3 = "https://example.com/file"; 230 validator.validate(path3); 231 } 232 233 @Test 234 public void testCaseInsensitiveFileScheme() throws Exception { 235 final String goodPath = "FILE://" + dataDir.toURI().getPath(); 236 final String extPath = dataUri + "file.txt"; 237 238 addAllowedPath(goodPath); 239 240 validator.validate(extPath); 241 } 242 243 @Test 244 public void testVaryingSensitiveFilePath() throws Exception { 245 final File subfolder = tmpDir.newFolder("CAPSLOCK"); 246 final File file = new File(subfolder, "OoOoOooOOooo.txt"); 247 file.createNewFile(); 248 249 final String goodPath = subfolder.toURI().toString(); 250 final String extPath = file.toURI().toString(); 251 252 addAllowedPath(goodPath); 253 254 validator.validate(extPath); 255 } 256 257 @Test 258 public void testVaryingCaseHttpUri() throws Exception { 259 final String goodPath = "http://sensitive.example.com/PATH/to/"; 260 final String extPath = "http://sensitive.example.com/PATH/to/stuff"; 261 262 addAllowedPath(goodPath); 263 264 validator.validate(extPath); 265 } 266 267 @Test 268 public void testFileSlashes() throws Exception { 269 final File oneFolder = tmpDir.newFolder("one"); 270 final File twoFolder = tmpDir.newFolder("two"); 271 final File threeFolder = tmpDir.newFolder("three"); 272 final File manyFolder = tmpDir.newFolder("toomany"); 273 new File(oneFolder, "file").createNewFile(); 274 new File(twoFolder, "file").createNewFile(); 275 new File(threeFolder, "file").createNewFile(); 276 new File(manyFolder, "file").createNewFile(); 277 278 addAllowedPath("file:" + oneFolder.toURI().getPath() + "/"); 279 addAllowedPath("file:/" + twoFolder.toURI().getPath() + "/"); 280 addAllowedPath("file://" + threeFolder.toURI().getPath() + "/"); 281 addAllowedPath("file:///" + manyFolder.toURI().getPath() + "/"); 282 283 validator.validate("file:" + oneFolder.toURI().getPath() + "/file"); 284 validator.validate("file:/" + oneFolder.toURI().getPath() + "/file"); 285 validator.validate("file://" + oneFolder.toURI().getPath() + "/file"); 286 287 validator.validate("file:" + twoFolder.toURI().getPath() + "/file"); 288 validator.validate("file:/" + twoFolder.toURI().getPath() + "/file"); 289 validator.validate("file://" + twoFolder.toURI().getPath() + "/file"); 290 291 validator.validate("file:" + threeFolder.toURI().getPath() + "/file"); 292 validator.validate("file:/" + threeFolder.toURI().getPath() + "/file"); 293 validator.validate("file://" + threeFolder.toURI().getPath() + "/file"); 294 295 try { 296 validator.validate("file:" + manyFolder.toURI().getPath() + "file"); 297 fail(); 298 } catch (final ExternalMessageBodyException e) { 299 // expected 300 } 301 } 302 303 @Test(expected = ExternalMessageBodyException.class) 304 public void testDisallowDirectoryUriWithoutSlash() throws Exception { 305 final String allowDir = dataUri.substring(0, dataUri.length() - 1); 306 addAllowedPath(allowDir); 307 308 final String extPath = dataUri + "file.txt"; 309 validator.validate(extPath); 310 } 311 312 @Test(expected = ExternalMessageBodyException.class) 313 public void testDisallowFileUriWithSlash() throws Exception { 314 new File(dataDir, "file").createNewFile(); 315 final String extPath = dataUri + "file/"; 316 final String allowExactPath = extPath + "/"; 317 318 addAllowedPath(allowExactPath); 319 320 validator.validate(extPath); 321 } 322 323 /* 324 * Test ignored because it takes around 10+ seconds to poll for events on MacOS: 325 * https://bugs.openjdk.java.net/browse/JDK-7133447 Can be enabled for one off testing 326 */ 327 @Ignore("Test is ignored due to file event timing") 328 @Test 329 public void testDetectModification() throws Exception { 330 validator.setMonitorForChanges(true); 331 332 final String anotherPath = tmpDir.newFolder("other").toURI().toString(); 333 addAllowedPath(anotherPath); 334 335 final String path = dataUri + "file"; 336 try { 337 validator.validate(path); 338 fail(); 339 } catch (final ExternalMessageBodyException e) { 340 // Expected 341 } 342 343 // Wait to ensure that the watch service is watching... 344 Thread.sleep(5000); 345 346 // Add a new allowed path 347 try (final BufferedWriter writer = Files.newBufferedWriter(allowListFile.toPath(), APPEND)) { 348 writer.write(dataUri + System.lineSeparator()); 349 } 350 351 // Check that the new allowed path was detected 352 boolean pass = false; 353 // Polling to see if change occurred for 20 seconds 354 final long endTimes = System.nanoTime() + 20000000000L; 355 while (System.nanoTime() < endTimes) { 356 Thread.sleep(50); 357 try { 358 validator.validate(path); 359 pass = true; 360 break; 361 } catch (final ExternalMessageBodyException e) { 362 // Still not passing, retry 363 } 364 } 365 366 assertTrue("Validator did not update with new path", pass); 367 } 368 369 private void addAllowedPath(final String allowed) throws Exception { 370 try (final BufferedWriter writer = Files.newBufferedWriter(allowListFile.toPath(), APPEND)) { 371 writer.write(allowed + System.lineSeparator()); 372 } 373 validator.init(); 374 } 375}