/* * ==================================================================== * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF 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. * ==================================================================== * * This software consists of voluntary contributions made by many * individuals on behalf of the Apache Software Foundation. For more * information on the Apache Software Foundation, please see * <http://www.apache.org/>. * */ package com.apigee.sdk.apm.http.impl.client.cache; import java.util.ArrayList; import java.util.List; import org.apache.http.Header; import org.apache.http.HeaderElement; import org.apache.http.HttpEntityEnclosingRequest; import org.apache.http.HttpRequest; import org.apache.http.HttpResponse; import org.apache.http.HttpStatus; import org.apache.http.HttpVersion; import org.apache.http.ProtocolException; import org.apache.http.ProtocolVersion; import org.apache.http.entity.AbstractHttpEntity; import org.apache.http.impl.client.RequestWrapper; import org.apache.http.message.BasicHeader; import org.apache.http.message.BasicHttpResponse; import org.apache.http.message.BasicStatusLine; import org.apache.http.protocol.HTTP; import com.apigee.sdk.apm.http.annotation.Immutable; import com.apigee.sdk.apm.http.client.cache.HeaderConstants; /** * @since 4.1 */ @Immutable class RequestProtocolCompliance { /** * Test to see if the {@link HttpRequest} is HTTP1.1 compliant or not and if * not, we can not continue. * * @param request * the HttpRequest Object * @return list of {@link RequestProtocolError} */ public List<RequestProtocolError> requestIsFatallyNonCompliant( HttpRequest request) { List<RequestProtocolError> theErrors = new ArrayList<RequestProtocolError>(); // RequestProtocolError anError = // requestContainsBodyButNoLength(request); // if (anError != null) { // theErrors.add(anError); // } RequestProtocolError anError = requestHasWeakETagAndRange(request); if (anError != null) { theErrors.add(anError); } anError = requestHasWeekETagForPUTOrDELETEIfMatch(request); if (anError != null) { theErrors.add(anError); } anError = requestContainsNoCacheDirectiveWithFieldName(request); if (anError != null) { theErrors.add(anError); } return theErrors; } /** * If the {@link HttpRequest} is non-compliant but 'fixable' we go ahead and * fix the request here. Returning the updated one. * * @param request * the request to check for compliance * @return the updated request * @throws ProtocolException * when we have trouble making the request compliant */ public HttpRequest makeRequestCompliant(HttpRequest request) throws ProtocolException { if (requestMustNotHaveEntity(request)) { ((HttpEntityEnclosingRequest) request).setEntity(null); } verifyRequestWithExpectContinueFlagHas100continueHeader(request); verifyOPTIONSRequestWithBodyHasContentType(request); decrementOPTIONSMaxForwardsIfGreaterThen0(request); if (requestVersionIsTooLow(request)) { return upgradeRequestTo(request, HttpVersion.HTTP_1_1); } if (requestMinorVersionIsTooHighMajorVersionsMatch(request)) { return downgradeRequestTo(request, HttpVersion.HTTP_1_1); } return request; } private boolean requestMustNotHaveEntity(HttpRequest request) { return HeaderConstants.TRACE_METHOD.equals(request.getRequestLine() .getMethod()) && request instanceof HttpEntityEnclosingRequest; } private void decrementOPTIONSMaxForwardsIfGreaterThen0(HttpRequest request) { if (!HeaderConstants.OPTIONS_METHOD.equals(request.getRequestLine() .getMethod())) { return; } Header maxForwards = request .getFirstHeader(HeaderConstants.MAX_FORWARDS); if (maxForwards == null) { return; } request.removeHeaders(HeaderConstants.MAX_FORWARDS); int currentMaxForwards = Integer.parseInt(maxForwards.getValue()); request.setHeader(HeaderConstants.MAX_FORWARDS, Integer.toString(currentMaxForwards - 1)); } private void verifyOPTIONSRequestWithBodyHasContentType(HttpRequest request) { if (!HeaderConstants.OPTIONS_METHOD.equals(request.getRequestLine() .getMethod())) { return; } if (!(request instanceof HttpEntityEnclosingRequest)) { return; } addContentTypeHeaderIfMissing((HttpEntityEnclosingRequest) request); } private void addContentTypeHeaderIfMissing( HttpEntityEnclosingRequest request) { if (request.getEntity().getContentType() == null) { ((AbstractHttpEntity) request.getEntity()) .setContentType(HTTP.OCTET_STREAM_TYPE); } } private void verifyRequestWithExpectContinueFlagHas100continueHeader( HttpRequest request) { if (request instanceof HttpEntityEnclosingRequest) { if (((HttpEntityEnclosingRequest) request).expectContinue() && ((HttpEntityEnclosingRequest) request).getEntity() != null) { add100ContinueHeaderIfMissing(request); } else { remove100ContinueHeaderIfExists(request); } } else { remove100ContinueHeaderIfExists(request); } } private void remove100ContinueHeaderIfExists(HttpRequest request) { boolean hasHeader = false; Header[] expectHeaders = request.getHeaders(HTTP.EXPECT_DIRECTIVE); List<HeaderElement> expectElementsThatAreNot100Continue = new ArrayList<HeaderElement>(); for (Header h : expectHeaders) { for (HeaderElement elt : h.getElements()) { if (!(HTTP.EXPECT_CONTINUE.equalsIgnoreCase(elt.getName()))) { expectElementsThatAreNot100Continue.add(elt); } else { hasHeader = true; } } if (hasHeader) { request.removeHeader(h); for (HeaderElement elt : expectElementsThatAreNot100Continue) { BasicHeader newHeader = new BasicHeader( HTTP.EXPECT_DIRECTIVE, elt.getName()); request.addHeader(newHeader); } return; } else { expectElementsThatAreNot100Continue = new ArrayList<HeaderElement>(); } } } private void add100ContinueHeaderIfMissing(HttpRequest request) { boolean hasHeader = false; for (Header h : request.getHeaders(HTTP.EXPECT_DIRECTIVE)) { for (HeaderElement elt : h.getElements()) { if (HTTP.EXPECT_CONTINUE.equalsIgnoreCase(elt.getName())) { hasHeader = true; } } } if (!hasHeader) { request.addHeader(HTTP.EXPECT_DIRECTIVE, HTTP.EXPECT_CONTINUE); } } private HttpRequest upgradeRequestTo(HttpRequest request, ProtocolVersion version) throws ProtocolException { RequestWrapper newRequest = new RequestWrapper(request); newRequest.setProtocolVersion(version); return newRequest; } private HttpRequest downgradeRequestTo(HttpRequest request, ProtocolVersion version) throws ProtocolException { RequestWrapper newRequest = new RequestWrapper(request); newRequest.setProtocolVersion(version); return newRequest; } protected boolean requestMinorVersionIsTooHighMajorVersionsMatch( HttpRequest request) { ProtocolVersion requestProtocol = request.getProtocolVersion(); if (requestProtocol.getMajor() != HttpVersion.HTTP_1_1.getMajor()) { return false; } if (requestProtocol.getMinor() > HttpVersion.HTTP_1_1.getMinor()) { return true; } return false; } protected boolean requestVersionIsTooLow(HttpRequest request) { return request.getProtocolVersion().compareToVersion( HttpVersion.HTTP_1_1) < 0; } /** * Extract error information about the {@link HttpRequest} telling the * 'caller' that a problem occured. * * @param errorCheck * What type of error should I get * @return The {@link HttpResponse} that is the error generated */ public HttpResponse getErrorForRequest(RequestProtocolError errorCheck) { switch (errorCheck) { case BODY_BUT_NO_LENGTH_ERROR: return new BasicHttpResponse(new BasicStatusLine( HttpVersion.HTTP_1_1, HttpStatus.SC_LENGTH_REQUIRED, "")); case WEAK_ETAG_AND_RANGE_ERROR: return new BasicHttpResponse(new BasicStatusLine( HttpVersion.HTTP_1_1, HttpStatus.SC_BAD_REQUEST, "Weak eTag not compatible with byte range")); case WEAK_ETAG_ON_PUTDELETE_METHOD_ERROR: return new BasicHttpResponse(new BasicStatusLine( HttpVersion.HTTP_1_1, HttpStatus.SC_BAD_REQUEST, "Weak eTag not compatible with PUT or DELETE requests")); case NO_CACHE_DIRECTIVE_WITH_FIELD_NAME: return new BasicHttpResponse(new BasicStatusLine( HttpVersion.HTTP_1_1, HttpStatus.SC_BAD_REQUEST, "No-Cache directive MUST NOT include a field name")); default: throw new IllegalStateException( "The request was compliant, therefore no error can be generated for it."); } } private RequestProtocolError requestHasWeakETagAndRange(HttpRequest request) { // TODO: Should these be looking at all the headers marked as Range? String method = request.getRequestLine().getMethod(); if (!(HeaderConstants.GET_METHOD.equals(method))) { return null; } Header range = request.getFirstHeader(HeaderConstants.RANGE); if (range == null) return null; Header ifRange = request.getFirstHeader(HeaderConstants.IF_RANGE); if (ifRange == null) return null; String val = ifRange.getValue(); if (val.startsWith("W/")) { return RequestProtocolError.WEAK_ETAG_AND_RANGE_ERROR; } return null; } private RequestProtocolError requestHasWeekETagForPUTOrDELETEIfMatch( HttpRequest request) { // TODO: Should these be looking at all the headers marked as // If-Match/If-None-Match? String method = request.getRequestLine().getMethod(); if (!(HeaderConstants.PUT_METHOD.equals(method) || HeaderConstants.DELETE_METHOD .equals(method))) { return null; } Header ifMatch = request.getFirstHeader(HeaderConstants.IF_MATCH); if (ifMatch != null) { String val = ifMatch.getValue(); if (val.startsWith("W/")) { return RequestProtocolError.WEAK_ETAG_ON_PUTDELETE_METHOD_ERROR; } } else { Header ifNoneMatch = request .getFirstHeader(HeaderConstants.IF_NONE_MATCH); if (ifNoneMatch == null) return null; String val2 = ifNoneMatch.getValue(); if (val2.startsWith("W/")) { return RequestProtocolError.WEAK_ETAG_ON_PUTDELETE_METHOD_ERROR; } } return null; } private RequestProtocolError requestContainsNoCacheDirectiveWithFieldName( HttpRequest request) { for (Header h : request.getHeaders("Cache-Control")) { for (HeaderElement elt : h.getElements()) { if ("no-cache".equalsIgnoreCase(elt.getName()) && elt.getValue() != null) { return RequestProtocolError.NO_CACHE_DIRECTIVE_WITH_FIELD_NAME; } } } return null; } }