org.fcrepo.integration.http.api.ExternalContentPathValidatorIT.java Source code

Java tutorial

Introduction

Here is the source code for org.fcrepo.integration.http.api.ExternalContentPathValidatorIT.java

Source

/*
 * Licensed to DuraSpace under one or more contributor license agreements.
 * See the NOTICE file distributed with this work for additional information
 * regarding copyright ownership.
 *
 * DuraSpace licenses this file to you under the Apache License,
 * Version 2.0 (the "License"); you may not use this file except in
 * compliance with the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.fcrepo.integration.http.api;

import static java.nio.file.StandardOpenOption.APPEND;
import static javax.ws.rs.core.HttpHeaders.CONTENT_LOCATION;
import static javax.ws.rs.core.HttpHeaders.CONTENT_TYPE;
import static javax.ws.rs.core.HttpHeaders.LINK;
import static org.apache.http.HttpStatus.SC_CREATED;
import static org.apache.http.HttpStatus.SC_OK;
import static org.apache.http.HttpStatus.SC_BAD_REQUEST;
import static org.fcrepo.kernel.api.RdfLexicon.NON_RDF_SOURCE;
import static org.junit.Assert.assertEquals;
import static org.slf4j.LoggerFactory.getLogger;

import java.io.BufferedWriter;
import java.io.File;
import java.net.ConnectException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.List;

import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.apache.http.NoHttpResponseException;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpPut;
import org.apache.http.entity.StringEntity;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.slf4j.Logger;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.annotation.DirtiesContext.ClassMode;
import org.springframework.test.context.TestExecutionListeners;
import org.springframework.test.context.TestExecutionListeners.MergeMode;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.support.DependencyInjectionTestExecutionListener;

/**
 * @author bbpennel
 */
@RunWith(SpringJUnit4ClassRunner.class)
@DirtiesContext(classMode = ClassMode.BEFORE_CLASS)
@TestExecutionListeners(listeners = { DependencyInjectionTestExecutionListener.class,
        DirtyContextBeforeAndAfterClassTestExecutionListener.class }, mergeMode = MergeMode.MERGE_WITH_DEFAULTS)
public class ExternalContentPathValidatorIT extends AbstractResourceIT {

    private static final Logger LOGGER = getLogger(ExternalContentPathValidatorIT.class);

    private static final String NON_RDF_SOURCE_LINK_HEADER = "<" + NON_RDF_SOURCE.getURI() + ">;rel=\"type\"";

    private static File disallowedDir;
    private static File allowedDir;

    static {
        try {
            final File allowedFile = File.createTempFile("allowed", ".txt");
            allowedFile.deleteOnExit();
            addAllowedPath(allowedFile, serverAddress);

            final Path disallowedPath = Files.createTempDirectory("disallowed");
            disallowedPath.toFile().deleteOnExit();
            disallowedDir = disallowedPath.toFile();
            allowedDir = Files.createTempDirectory(disallowedPath, "data").toFile();
            addAllowedPath(allowedFile, allowedDir.toURI().toString());

            System.setProperty("fcrepo.external.content.allowed", allowedFile.getAbsolutePath());
            LOGGER.warn("fcrepo.external.content.allowed = {}", allowedFile.getAbsolutePath());
        } catch (final Exception e) {
            LOGGER.error("Failed to setup allowed configuration file", e);
        }
    }

    private static void addAllowedPath(final File allowedFile, final String allowed) throws Exception {
        try (BufferedWriter writer = Files.newBufferedWriter(allowedFile.toPath(), APPEND)) {
            writer.write(allowed + System.lineSeparator());
        }
    }

    @Before
    public void init() throws Exception {
        // Because of the dirtied context, need to wait for fedora to restart before testing
        int triesRemaining = 50;
        while (true) {
            final HttpGet get = new HttpGet(serverAddress);
            try (final CloseableHttpResponse response = execute(get)) {
                assertEquals(SC_OK, getStatus(response));
                break;
            } catch (final NoHttpResponseException | ConnectException e) {
                if (triesRemaining-- > 0) {
                    LOGGER.debug("Waiting for fedora to become available");
                    Thread.sleep(50);
                } else {
                    throw new Exception("Fedora instance did not become available in allowed time");
                }
            }
        }
        // Now that fedora has started, clear the property so it won't impact other tests
        System.clearProperty("fcrepo.external.content.allowed");
    }

    @Test
    public void testAllowedPath() throws Exception {
        final HttpPost method = postObjMethod();
        method.addHeader(CONTENT_TYPE, "text/plain");
        method.addHeader(LINK, NON_RDF_SOURCE_LINK_HEADER);
        method.setEntity(new StringEntity("xyz"));
        final String externalLocation;

        // Make an external remote URI.
        try (final CloseableHttpResponse response = execute(method)) {
            assertEquals(SC_CREATED, getStatus(response));
            externalLocation = getLocation(response);
        }

        final String id = getRandomUniqueId();

        final HttpPut put = putObjMethod(id);
        put.addHeader(LINK, getExternalContentLinkHeader(externalLocation, "proxy", null));
        try (final CloseableHttpResponse response = execute(put)) {
            assertEquals(SC_CREATED, getStatus(response));
        }
        // Get the external content proxy resource.
        try (final CloseableHttpResponse response = execute(getObjMethod(id))) {
            assertEquals(SC_OK, getStatus(response));
            assertEquals("text/plain", response.getFirstHeader(CONTENT_TYPE).getValue());
            assertEquals(externalLocation, response.getFirstHeader(CONTENT_LOCATION).getValue());
        }
    }

    @Test
    public void testDisallowedPath() throws Exception {
        final String externalLocation = "http://example.com/";

        final String id = getRandomUniqueId();

        final HttpPut put = putObjMethod(id);
        put.addHeader(LINK, getExternalContentLinkHeader(externalLocation, "proxy", null));
        try (final CloseableHttpResponse response = execute(put)) {
            assertEquals(SC_BAD_REQUEST, getStatus(response));
        }
    }

    @Test
    public void testAllowedFilePath() throws Exception {
        final String fileContent = "content";
        final File permittedFile = new File(allowedDir, "test.txt");
        FileUtils.writeStringToFile(permittedFile, fileContent, "UTF-8");
        final String fileUri = permittedFile.toURI().toString();

        final String id = getRandomUniqueId();
        final HttpPut put = putObjMethod(id);
        put.addHeader(LINK, getExternalContentLinkHeader(fileUri, "proxy", "text/plain"));
        try (final CloseableHttpResponse response = execute(put)) {
            assertEquals(SC_CREATED, getStatus(response));
        }
        // Get the external content proxy resource.
        try (final CloseableHttpResponse response = execute(getObjMethod(id))) {
            assertEquals(SC_OK, getStatus(response));
            assertEquals("text/plain", response.getFirstHeader(CONTENT_TYPE).getValue());
            assertEquals(fileUri, response.getFirstHeader(CONTENT_LOCATION).getValue());
            assertEquals(fileContent, IOUtils.toString(response.getEntity().getContent(), "UTF-8"));
        }
    }

    @Test
    public void testDisallowedFilePath() throws Exception {
        final String fileContent = "content";
        final File disallowedFile = new File(disallowedDir, "test.txt");
        FileUtils.writeStringToFile(disallowedFile, fileContent, "UTF-8");
        final String fileUri = disallowedFile.toURI().toString();

        final String id = getRandomUniqueId();
        final HttpPut put = putObjMethod(id);
        put.addHeader(LINK, getExternalContentLinkHeader(fileUri, "proxy", "text/plain"));
        try (final CloseableHttpResponse response = execute(put)) {
            assertEquals(SC_BAD_REQUEST, getStatus(response));
        }
    }

    @Test
    public void testPathModifiers() throws Exception {
        // Creating file in disallowed path
        final String fileContent = "content";
        final File disallowedFile = new File(disallowedDir, "test.txt");
        FileUtils.writeStringToFile(disallowedFile, fileContent, "UTF-8");

        // Variations of path modifiers that should be rejected or fail to find file.
        final List<String> modifiers = Arrays.asList("../", "%2e%2e%2f", "%2e%2e/", "..%2f", "%252e%252e%255c",
                "%2e%2e%5c", "%2e%2e%5c%2f", "..%c0%af");

        for (final String modifier : modifiers) {
            // Attempt to address file with escaped uri modifiers
            final String externalLocation = allowedDir.toURI().toString() + modifier + "test.txt";

            final String id = getRandomUniqueId();
            final HttpPut put = putObjMethod(id);
            put.addHeader(LINK, getExternalContentLinkHeader(externalLocation, "proxy", "text/plain"));
            try (final CloseableHttpResponse response = execute(put)) {
                assertEquals("Path " + externalLocation + " must be rejected", SC_BAD_REQUEST, getStatus(response));
            }
        }
    }
}