/* * JBoss, Home of Professional Open Source. * Copyright 2014 Red Hat, Inc., and individual contributors * as indicated by the @author tags. * * Licensed 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 io.undertow.util; import java.util.ArrayList; import java.util.Collections; import java.util.List; import io.undertow.server.HttpServerExchange; /** * @author Stuart Douglas */ public class ETagUtils { private static final char COMMA = ','; private static final char QUOTE = '"'; private static final char W = 'W'; private static final char SLASH = '/'; /** * Handles the if-match header. returns true if the request should proceed, false otherwise * * @param exchange the exchange * @param etag The etags * @return */ public static boolean handleIfMatch(final HttpServerExchange exchange, final ETag etag, boolean allowWeak) { return handleIfMatch(exchange, Collections.singletonList(etag), allowWeak); } /** * Handles the if-match header. returns true if the request should proceed, false otherwise * * @param exchange the exchange * @param etags The etags * @return */ public static boolean handleIfMatch(final HttpServerExchange exchange, final List<ETag> etags, boolean allowWeak) { return handleIfMatch(exchange.getRequestHeaders().getFirst(Headers.IF_MATCH), etags, allowWeak); } /** * Handles the if-match header. returns true if the request should proceed, false otherwise * * @param ifMatch The if match header * @param etag The etags * @return */ public static boolean handleIfMatch(final String ifMatch, final ETag etag, boolean allowWeak) { return handleIfMatch(ifMatch, Collections.singletonList(etag), allowWeak); } /** * Handles the if-match header. returns true if the request should proceed, false otherwise * * @param ifMatch The ifMatch header * @param etags The etags * @return */ public static boolean handleIfMatch(final String ifMatch, final List<ETag> etags, boolean allowWeak) { if (ifMatch == null) { return true; } List<ETag> parts = parseETagList(ifMatch); for (ETag part : parts) { if (part.getTag().equals("*")) { return true; //todo: how to tell if there is a current entity for the request } if (part.isWeak() && !allowWeak) { continue; } for (ETag tag : etags) { if (tag != null) { if (tag.isWeak() && !allowWeak) { continue; } if (tag.getTag().equals(part.getTag())) { return true; } } } } return false; } /** * Handles the if-none-match header. returns true if the request should proceed, false otherwise * * @param exchange the exchange * @param etag The etags * @return */ public static boolean handleIfNoneMatch(final HttpServerExchange exchange, final ETag etag, boolean allowWeak) { return handleIfNoneMatch(exchange, Collections.singletonList(etag), allowWeak); } /** * Handles the if-none-match header. returns true if the request should proceed, false otherwise * * @param exchange the exchange * @param etags The etags * @return */ public static boolean handleIfNoneMatch(final HttpServerExchange exchange, final List<ETag> etags, boolean allowWeak) { return handleIfNoneMatch(exchange.getRequestHeaders().getFirst(Headers.IF_NONE_MATCH), etags, allowWeak); } /** * Handles the if-none-match header. returns true if the request should proceed, false otherwise * * @param ifNoneMatch the header * @param etag The etags * @return */ public static boolean handleIfNoneMatch(final String ifNoneMatch, final ETag etag, boolean allowWeak) { return handleIfNoneMatch(ifNoneMatch, Collections.singletonList(etag), allowWeak); } /** * Handles the if-none-match header. returns true if the request should proceed, false otherwise * * @param ifNoneMatch the header * @param etags The etags * @return */ public static boolean handleIfNoneMatch(final String ifNoneMatch, final List<ETag> etags, boolean allowWeak) { if (ifNoneMatch == null) { return true; } List<ETag> parts = parseETagList(ifNoneMatch); for (ETag part : parts) { if (part.getTag().equals("*")) { return false; } if (part.isWeak() && !allowWeak) { continue; } for (ETag tag : etags) { if (tag != null) { if (tag.isWeak() && !allowWeak) { continue; } if (tag.getTag().equals(part.getTag())) { return false; } } } } return true; } public static List<ETag> parseETagList(final String header) { char[] headerChars = header.toCharArray(); // The LinkedHashMap is used so that the parameter order can also be retained. List<ETag> response = new ArrayList<>(); SearchingFor searchingFor = SearchingFor.START_OF_VALUE; String currentToken = null; int valueStart = 0; boolean weak = false; boolean malformed = false; for (int i = 0; i < headerChars.length; i++) { switch (searchingFor) { case START_OF_VALUE: if (headerChars[i] != COMMA && !Character.isWhitespace(headerChars[i])) { if (headerChars[i] == QUOTE) { valueStart = i + 1; searchingFor = SearchingFor.LAST_QUOTE; weak = false; malformed = false; } else if (headerChars[i] == W) { searchingFor = SearchingFor.WEAK_SLASH; } } break; case WEAK_SLASH: if (headerChars[i] == QUOTE) { valueStart = i + 1; searchingFor = SearchingFor.LAST_QUOTE; weak = true; malformed = false; } else if (headerChars[i] != SLASH) { malformed = true; searchingFor = SearchingFor.END_OF_VALUE; } break; case LAST_QUOTE: if (headerChars[i] == QUOTE) { String value = String.valueOf(headerChars, valueStart, i - valueStart); response.add(new ETag(weak, value.trim())); searchingFor = SearchingFor.START_OF_VALUE; } break; case END_OF_VALUE: if (headerChars[i] == COMMA || Character.isWhitespace(headerChars[i])) { if (!malformed) { String value = String.valueOf(headerChars, valueStart, i - valueStart); response.add(new ETag(weak, value.trim())); searchingFor = SearchingFor.START_OF_VALUE; } } break; } } if (searchingFor == SearchingFor.END_OF_VALUE || searchingFor == SearchingFor.LAST_QUOTE) { if (!malformed) { // Special case where we reached the end of the array containing the header values. String value = String.valueOf(headerChars, valueStart, headerChars.length - valueStart); response.add(new ETag(weak, value.trim())); } } return response; } /** * @param exchange The exchange * @return The ETag for the exchange, or null if the etag is not set */ public static ETag getETag(final HttpServerExchange exchange) { final String tag = exchange.getResponseHeaders().getFirst(Headers.ETAG); if (tag == null) { return null; } char[] headerChars = tag.toCharArray(); SearchingFor searchingFor = SearchingFor.START_OF_VALUE; int valueStart = 0; boolean weak = false; boolean malformed = false; for (int i = 0; i < headerChars.length; i++) { switch (searchingFor) { case START_OF_VALUE: if (headerChars[i] != COMMA && !Character.isWhitespace(headerChars[i])) { if (headerChars[i] == QUOTE) { valueStart = i + 1; searchingFor = SearchingFor.LAST_QUOTE; weak = false; malformed = false; } else if (headerChars[i] == W) { searchingFor = SearchingFor.WEAK_SLASH; } } break; case WEAK_SLASH: if (headerChars[i] == QUOTE) { valueStart = i + 1; searchingFor = SearchingFor.LAST_QUOTE; weak = true; malformed = false; } else if (headerChars[i] != SLASH) { return null; //malformed } break; case LAST_QUOTE: if (headerChars[i] == QUOTE) { String value = String.valueOf(headerChars, valueStart, i - valueStart); return new ETag(weak, value.trim()); } break; case END_OF_VALUE: if (headerChars[i] == COMMA || Character.isWhitespace(headerChars[i])) { if (!malformed) { String value = String.valueOf(headerChars, valueStart, i - valueStart); return new ETag(weak, value.trim()); } } break; } } if (searchingFor == SearchingFor.END_OF_VALUE || searchingFor == SearchingFor.LAST_QUOTE) { if (!malformed) { // Special case where we reached the end of the array containing the header values. String value = String.valueOf(headerChars, valueStart, headerChars.length - valueStart); return new ETag(weak, value.trim()); } } return null; } enum SearchingFor { START_OF_VALUE, LAST_QUOTE, END_OF_VALUE, WEAK_SLASH; } }