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}