001/* 002 * The contents of this file are subject to the terms of the Common Development and 003 * Distribution License (the License). You may not use this file except in compliance with the 004 * License. 005 * 006 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the 007 * specific language governing permission and limitations under the License. 008 * 009 * When distributing Covered Software, include this CDDL Header Notice in each file and include 010 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL 011 * Header, with the fields enclosed by brackets [] replaced by your own identifying 012 * information: "Portions copyright [year] [name of copyright owner]". 013 * 014 * Copyright 2015-2016 ForgeRock AS. 015 */ 016 017package org.forgerock.http.apache; 018 019import java.util.Arrays; 020import java.util.List; 021 022import org.apache.http.Header; 023import org.apache.http.HeaderIterator; 024import org.apache.http.HttpResponse; 025import org.apache.http.StatusLine; 026import org.apache.http.client.methods.HttpEntityEnclosingRequestBase; 027import org.apache.http.client.methods.HttpRequestBase; 028import org.apache.http.client.methods.HttpUriRequest; 029import org.apache.http.entity.InputStreamEntity; 030import org.forgerock.http.header.ConnectionHeader; 031import org.forgerock.http.header.ContentEncodingHeader; 032import org.forgerock.http.header.ContentLengthHeader; 033import org.forgerock.http.header.ContentTypeHeader; 034import org.forgerock.http.protocol.Request; 035import org.forgerock.http.protocol.Response; 036import org.forgerock.http.protocol.Status; 037import org.forgerock.http.spi.HttpClient; 038import org.forgerock.http.util.CaseInsensitiveSet; 039 040/** 041 * This abstract client is used to share commonly used constants and methods 042 * in both synchronous and asynchronous Apache HTTP Client libraries. 043 */ 044public abstract class AbstractHttpClient implements HttpClient { 045 046 /** Headers that are suppressed in request. */ 047 // FIXME: How should the the "Expect" header be handled? 048 private static final CaseInsensitiveSet SUPPRESS_REQUEST_HEADERS = new CaseInsensitiveSet( 049 Arrays.asList( 050 // populated in outgoing request by EntityRequest (HttpEntityEnclosingRequestBase): 051 "Content-Encoding", "Content-Length", "Content-Type", 052 // hop-by-hop headers, not forwarded by proxies, per RFC 2616 13.5.1: 053 "Connection", "Keep-Alive", "Proxy-Authenticate", "Proxy-Authorization", "TE", 054 "Trailers", "Transfer-Encoding", "Upgrade")); 055 056 /** Headers that are suppressed in response. */ 057 private static final CaseInsensitiveSet SUPPRESS_RESPONSE_HEADERS = new CaseInsensitiveSet( 058 Arrays.asList( 059 // hop-by-hop headers, not forwarded by proxies, per RFC 2616 13.5.1: 060 "Connection", "Keep-Alive", "Proxy-Authenticate", "Proxy-Authorization", "TE", 061 "Trailers", "Transfer-Encoding", "Upgrade")); 062 063 /** A request that encloses an entity. */ 064 private static class EntityRequest extends HttpEntityEnclosingRequestBase { 065 private final String method; 066 067 public EntityRequest(final Request request) { 068 this.method = request.getMethod(); 069 final InputStreamEntity entity = 070 new InputStreamEntity(request.getEntity().getRawContentInputStream(), 071 ContentLengthHeader.valueOf(request).getLength()); 072 final List<String> contentType = ContentTypeHeader.valueOf(request).getValues(); 073 if (contentType != null && contentType.size() > 1) { 074 throw new IllegalArgumentException("Content-Type configured with multiple values"); 075 } 076 entity.setContentType(contentType == null || contentType.size() == 0 ? null : contentType.get(0)); 077 final List<String> encoding = ContentEncodingHeader.valueOf(request).getValues(); 078 if (encoding != null && encoding.size() > 1) { 079 throw new IllegalArgumentException("Content-Encoding configured with multiple values"); 080 } 081 entity.setContentEncoding(encoding == null || encoding.size() == 0 ? null : encoding.get(0)); 082 setEntity(entity); 083 } 084 085 @Override 086 public String getMethod() { 087 return method; 088 } 089 } 090 091 /** A request that does not enclose an entity. */ 092 private static class NonEntityRequest extends HttpRequestBase { 093 private final String method; 094 095 public NonEntityRequest(final Request request) { 096 this.method = request.getMethod(); 097 final Header[] contentLengthHeader = getHeaders(ContentLengthHeader.NAME); 098 if ((contentLengthHeader == null || contentLengthHeader.length == 0) 099 && ("PUT".equals(method) || "POST".equals(method) || "PROPFIND".equals(method))) { 100 setHeader(ContentLengthHeader.NAME, "0"); 101 } 102 } 103 104 @Override 105 public String getMethod() { 106 return method; 107 } 108 } 109 110 /** 111 * Creates a new {@link HttpUriRequest} populated from the given {@code request}. 112 * The returned message has some of its headers filtered/ignored (proxy behaviour). 113 * 114 * @param request OpenIG request structure 115 * @return AHC request structure 116 */ 117 protected HttpUriRequest createHttpUriRequest(final Request request) { 118 // Create the Http request depending if there is an entity or not 119 HttpRequestBase clientRequest = request.getEntity().isRawContentEmpty() 120 ? new NonEntityRequest(request) : new EntityRequest(request); 121 clientRequest.setURI(request.getUri().asURI()); 122 123 // Parse request Connection headers to be suppressed in message 124 CaseInsensitiveSet removableHeaderNames = new CaseInsensitiveSet(); 125 removableHeaderNames.addAll(ConnectionHeader.valueOf(request).getTokens()); 126 127 // Populates request headers 128 for (String name : request.getHeaders().keySet()) { 129 if (!SUPPRESS_REQUEST_HEADERS.contains(name) && !removableHeaderNames.contains(name)) { 130 for (final String value : request.getHeaders().get(name).getValues()) { 131 clientRequest.addHeader(name, value); 132 } 133 } 134 } 135 return clientRequest; 136 } 137 138 /** 139 * Creates a new {@link Response} populated from the given AHC {@code result}. 140 * The returned message has some of its headers filtered/ignored (proxy behaviour). 141 * 142 * @param result AHC response structure 143 * @return CHF response structure 144 */ 145 protected static Response createResponseWithoutEntity(final HttpResponse result) { 146 // Response status line 147 StatusLine statusLine = result.getStatusLine(); 148 Response response = new Response(Status.valueOf(statusLine.getStatusCode(), statusLine.getReasonPhrase())); 149 response.setVersion(statusLine.getProtocolVersion().toString()); 150 151 // Parse response Connection headers to be suppressed in message 152 CaseInsensitiveSet removableHeaderNames = new CaseInsensitiveSet(); 153 removableHeaderNames.addAll(ConnectionHeader.valueOf(response).getTokens()); 154 155 // Response headers 156 for (HeaderIterator i = result.headerIterator(); i.hasNext();) { 157 Header header = i.nextHeader(); 158 String name = header.getName(); 159 if (!SUPPRESS_RESPONSE_HEADERS.contains(name) && !removableHeaderNames.contains(name)) { 160 response.getHeaders().add(name, header.getValue()); 161 } 162 } 163 164 return response; 165 } 166}