/******************************************************************************* * Copyright French Prime minister Office/SGMAP/DINSIC/Vitam Program (2015-2019) * * contact.vitam@culture.gouv.fr * * This software is a computer program whose purpose is to implement a digital archiving back-office system managing * high volumetry securely and efficiently. * * This software is governed by the CeCILL 2.1 license under French law and abiding by the rules of distribution of free * software. You can use, modify and/ or redistribute the software under the terms of the CeCILL 2.1 license as * circulated by CEA, CNRS and INRIA at the following URL "http://www.cecill.info". * * As a counterpart to the access to the source code and rights to copy, modify and redistribute granted by the license, * users are provided only with a limited warranty and the software's author, the holder of the economic rights, and the * successive licensors have only limited liability. * * In this respect, the user's attention is drawn to the risks associated with loading, using, modifying and/or * developing or reproducing the software by the user in light of its specific status of free software, that may mean * that it is complicated to manipulate, and that also therefore means that it is reserved for developers and * experienced professionals having in-depth computer knowledge. Users are therefore encouraged to load and test the * software's suitability as regards their requirements in conditions enabling the security of their systems and/or data * to be ensured and, more generally, to use and operate it in the same conditions as regards security. * * The fact that you are presently reading this means that you have had knowledge of the CeCILL 2.1 license and that you * accept its terms. *******************************************************************************/ package fr.gouv.vitam.common.server.application; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.stream.Collectors; import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.MultivaluedHashMap; import javax.ws.rs.core.MultivaluedMap; import com.google.common.base.Joiner; import fr.gouv.vitam.common.ParametersChecker; import fr.gouv.vitam.common.logging.VitamLogger; import fr.gouv.vitam.common.logging.VitamLoggerFactory; /** * Class helper to manage specifics Vitam headers */ // TODO P1: This is a copy of the api-design module header management. In another item we should refactor both to make // this // http header management common vitam. public final class HttpHeaderHelper { private static final VitamLogger LOGGER = VitamLoggerFactory.getInstance(HttpHeaderHelper.class); private static final String CASE_INSENSITIVE = "(?i)"; private HttpHeaderHelper() { // Nothing } /** * Get header values from {@link HttpHeaders} for {@link VitamHttpHeader} * * @param headers the headers list * @param name the {@link VitamHttpHeader} with wanted header name * @return the list of values for specified header name * @throws IllegalArgumentException if headers or name is null */ public static List<String> getHeaderValues(HttpHeaders headers, VitamHttpHeader name) { ParametersChecker.checkParameter("Name cannot be null", name); return getHeaderValues(headers, name.getName()); } /** * Retrieve header values from {@link HttpHeaders} for {@link VitamHttpHeader} * * @param headers the headers list * @param name the header name * @return the list of values for specified header name * @throws IllegalArgumentException if headers is null or name is null or empty */ public static List<String> getHeaderValues(HttpHeaders headers, String name) { ParametersChecker.checkParameter("Name cannot be null", name); ParametersChecker.checkParameter("Headers cannot be null", headers); return headers.getRequestHeader(name); } /** * Check if headers are declared * * @param headers the headers list to check * @param vitamHeader the header to retreive * @return true if the header is defined, false otherwise * @throws IllegalArgumentException if headers or vitamHeader is null */ public static boolean hasValuesFor(HttpHeaders headers, VitamHttpHeader vitamHeader) { ParametersChecker.checkParameter("Header name cannot be null", vitamHeader); ParametersChecker.checkParameter("Headers cannot be null", headers); final List<String> values = headers.getRequestHeader(vitamHeader.getName()); return values != null && !values.isEmpty(); } /** * Check specific vitam headers values with regular expression from the (define in {@link VitamHttpHeader}) At the * first wrong value, treatment stops and throws an {@link IllegalStateException} Note that, the regular expression * is case insensitive. * * @param headers HTTP headers list to check * @throws IllegalStateException when a header value doesn't match with teh defined regular expression * @throws IllegalArgumentException if headers is null */ public static void checkVitamHeaders(HttpHeaders headers) { ParametersChecker.checkParameter("Headers cannot be null", headers); final MultivaluedMap<String, String> requestHeaders = headers.getRequestHeaders(); checkVitamHeadersMap(requestHeaders); } /** * Check specific vitam headers values with regular expression from the (define in {@link VitamHttpHeader}) At the * first wrong value, treatment stops and throws an {@link IllegalStateException} Note that, the regular expression * is case insensitive. * * @param requestHeaders HTTP headers list to check * @throws IllegalStateException when a header value doesn't match with the defined regular expression */ public static void checkVitamHeadersMap(MultivaluedMap<String, String> requestHeaders) { if (requestHeaders != null && !requestHeaders.isEmpty()) { for (final VitamHttpHeader vitamHttpHeader : VitamHttpHeader.values()) { final List<String> values = requestHeaders.get(vitamHttpHeader.getName()); if (values != null && !values.stream() .anyMatch(value -> value.matches(CASE_INSENSITIVE + vitamHttpHeader.getRegExp()))) { throw new IllegalStateException(String.format("%s header has wrong value", vitamHttpHeader .getName())); } } } } /** * Validate HTTP header values. If header is known by the {@link VitamHttpHeader} then check if format matches with * the defined regular expression. Also check the wantedHeaders map values with {@link VitamHttpHeader}. Values of * headers (from HTTP or from wanted list) can not be null (throw an exception). * * In case of error, the exception contains all errors in its message. * * @param headers the headers list to validate * @param wantedHeaders the map representing wanted header values with the key for header name and the value (list) * for the wanted values for this specific header name * @throws IllegalArgumentException if headers is null * @throws IllegalStateException if one or more header values does not equal to wanted values, header values do not * match with the defined regular expression, wanted values for a header are null, header values are null. * This exception contains all errors in its message. */ public static void validateHeaderValue(HttpHeaders headers, MultivaluedHashMap<String, String> wantedHeaders) { ParametersChecker.checkParameter("Headers cannot be null", headers); final List<String> errorDetails = new ArrayList<>(); for (final String wantedHeaderName : wantedHeaders.keySet()) { final List<String> wantedValues = toLowerCaseList(wantedHeaders.get(wantedHeaderName)); final VitamHttpHeader vitamHeader = VitamHttpHeader.get(wantedHeaderName); final List<String> headersValues = toLowerCaseList(headers.getRequestHeader(wantedHeaderName)); List<String> tmpHeadersValues; final List<String> tmpWantedValues = toLowerCaseList(wantedValues); if (headersValues.isEmpty()) { errorDetails.add(String.format("Header %s values are null", wantedHeaderName)); } else { tmpHeadersValues = toLowerCaseList(headersValues); if (vitamHeader != null) { errorDetails .addAll(collectNonMatchingItems(vitamHeader, wantedValues, wantedHeaderName, "Wanted " + "value %s for header %s does not match with define regular %s")); errorDetails .addAll(collectNonMatchingItems(vitamHeader, headersValues, wantedHeaderName, "Found " + "value %s for header %s does not match with define regular %s")); } tmpHeadersValues.removeAll(wantedValues); tmpWantedValues.removeAll(headersValues); if (!tmpHeadersValues.isEmpty()) { // error ? LOGGER.warn("Some values ({}) from header {} were not check (not asked)", Joiner.on(", ").join(tmpHeadersValues), wantedHeaderName); } } if (!tmpWantedValues.isEmpty()) { errorDetails.add(String.format("Some WANTED values from header %s were not found (%s)", wantedHeaderName, Joiner.on(", ").join(tmpWantedValues))); } } if (!errorDetails.isEmpty()) { throw new IllegalArgumentException( String.format("There are %d errors %n%s", errorDetails.size(), Joiner.on("\n").join(errorDetails))); } } /** * Transform string in input list to their lowercase value * * @param list a list of strings to be lowercase * @return a list of string containing all elements of the input list but each elements is in lowercase */ private static List<String> toLowerCaseList(List<String> list) { if (list == null) { return Collections.emptyList(); } return list.stream().map(String::toLowerCase).collect(Collectors.toList()); } private static List<String> collectNonMatchingItems(VitamHttpHeader vitamHeader, List<String> valuesTocheck, String wantedHeaderName, String messageTemplate) { final List<String> result = new ArrayList<>(); if (vitamHeader != null && valuesTocheck != null) { result.addAll(valuesTocheck.stream() .filter(headerValue -> !headerValue.matches(CASE_INSENSITIVE + vitamHeader.getRegExp())) .map(headerValue -> String.format(messageTemplate, headerValue, wantedHeaderName, vitamHeader.getRegExp())) .collect(Collectors.toList())); } return result; } }