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 ForgeRock AS. 015 */ 016 017package org.forgerock.http.apache; 018 019import java.io.IOException; 020import java.util.Arrays; 021import java.util.List; 022 023import org.apache.http.Header; 024import org.apache.http.HeaderIterator; 025import org.apache.http.HttpEntity; 026import org.apache.http.HttpResponse; 027import org.apache.http.StatusLine; 028import org.apache.http.client.methods.HttpEntityEnclosingRequestBase; 029import org.apache.http.client.methods.HttpRequestBase; 030import org.apache.http.client.methods.HttpUriRequest; 031import org.apache.http.entity.InputStreamEntity; 032import org.forgerock.http.header.ConnectionHeader; 033import org.forgerock.http.header.ContentEncodingHeader; 034import org.forgerock.http.header.ContentLengthHeader; 035import org.forgerock.http.header.ContentTypeHeader; 036import org.forgerock.http.io.Buffer; 037import org.forgerock.http.io.IO; 038import org.forgerock.http.protocol.Request; 039import org.forgerock.http.protocol.Response; 040import org.forgerock.http.protocol.Status; 041import org.forgerock.http.spi.HttpClient; 042import org.forgerock.http.util.CaseInsensitiveSet; 043import org.forgerock.util.Factory; 044 045/** 046 * This abstract client is used to share commonly used constants and methods 047 * in both synchronous and asynchronous Apache HTTP Client libraries. 048 */ 049public abstract class AbstractHttpClient implements HttpClient { 050 051 /** Headers that are suppressed in request. */ 052 // FIXME: How should the the "Expect" header be handled? 053 private static final CaseInsensitiveSet SUPPRESS_REQUEST_HEADERS = new CaseInsensitiveSet( 054 Arrays.asList( 055 // populated in outgoing request by EntityRequest (HttpEntityEnclosingRequestBase): 056 "Content-Encoding", "Content-Length", "Content-Type", 057 // hop-by-hop headers, not forwarded by proxies, per RFC 2616 13.5.1: 058 "Connection", "Keep-Alive", "Proxy-Authenticate", "Proxy-Authorization", "TE", 059 "Trailers", "Transfer-Encoding", "Upgrade")); 060 061 /** Headers that are suppressed in response. */ 062 private static final CaseInsensitiveSet SUPPRESS_RESPONSE_HEADERS = new CaseInsensitiveSet( 063 Arrays.asList( 064 // hop-by-hop headers, not forwarded by proxies, per RFC 2616 13.5.1: 065 "Connection", "Keep-Alive", "Proxy-Authenticate", "Proxy-Authorization", "TE", 066 "Trailers", "Transfer-Encoding", "Upgrade")); 067 068 private final Factory<Buffer> storage; 069 070 /** 071 * Base constructor for AHC {@link HttpClient} drivers. 072 * 073 * @param storage 074 * temporary storage area 075 */ 076 protected AbstractHttpClient(final Factory<Buffer> storage) { 077 this.storage = storage; 078 } 079 080 /** A request that encloses an entity. */ 081 private static class EntityRequest extends HttpEntityEnclosingRequestBase { 082 private final String method; 083 084 public EntityRequest(final Request request) { 085 this.method = request.getMethod(); 086 final InputStreamEntity entity = 087 new InputStreamEntity(request.getEntity().getRawContentInputStream(), 088 ContentLengthHeader.valueOf(request).getLength()); 089 final List<String> contentType = ContentTypeHeader.valueOf(request).getValues(); 090 if (contentType != null && contentType.size() > 1) { 091 throw new IllegalArgumentException("Content-Type configured with multiple values"); 092 } 093 entity.setContentType(contentType == null || contentType.size() == 0 ? null : contentType.get(0)); 094 final List<String> encoding = ContentEncodingHeader.valueOf(request).getValues(); 095 if (encoding != null && encoding.size() > 1) { 096 throw new IllegalArgumentException("Content-Encoding configured with multiple values"); 097 } 098 entity.setContentEncoding(encoding == null || encoding.size() == 0 ? null : encoding.get(0)); 099 setEntity(entity); 100 } 101 102 @Override 103 public String getMethod() { 104 return method; 105 } 106 } 107 108 /** A request that does not enclose an entity. */ 109 private static class NonEntityRequest extends HttpRequestBase { 110 private final String method; 111 112 public NonEntityRequest(final Request request) { 113 this.method = request.getMethod(); 114 final Header[] contentLengthHeader = getHeaders(ContentLengthHeader.NAME); 115 if ((contentLengthHeader == null || contentLengthHeader.length == 0) 116 && ("PUT".equals(method) || "POST".equals(method) || "PROPFIND".equals(method))) { 117 setHeader(ContentLengthHeader.NAME, "0"); 118 } 119 } 120 121 @Override 122 public String getMethod() { 123 return method; 124 } 125 } 126 127 /** 128 * Creates a new {@link HttpUriRequest} populated from the given {@code request}. 129 * The returned message has some of its headers filtered/ignored (proxy behaviour). 130 * 131 * @param request OpenIG request structure 132 * @return AHC request structure 133 */ 134 protected HttpUriRequest createHttpUriRequest(final Request request) { 135 // Create the Http request depending if there is an entity or not 136 HttpRequestBase clientRequest = request.getEntity().isRawContentEmpty() 137 ? new NonEntityRequest(request) : new EntityRequest(request); 138 clientRequest.setURI(request.getUri().asURI()); 139 140 // Parse request Connection headers to be suppressed in message 141 CaseInsensitiveSet removableHeaderNames = new CaseInsensitiveSet(); 142 removableHeaderNames.addAll(ConnectionHeader.valueOf(request).getTokens()); 143 144 // Populates request headers 145 for (String name : request.getHeaders().keySet()) { 146 if (!SUPPRESS_REQUEST_HEADERS.contains(name) && !removableHeaderNames.contains(name)) { 147 for (final String value : request.getHeaders().get(name).getValues()) { 148 clientRequest.addHeader(name, value); 149 } 150 } 151 } 152 return clientRequest; 153 } 154 155 /** 156 * Creates a new {@link Response} populated from the given AHC {@code result}. 157 * The returned message has some of its headers filtered/ignored (proxy behaviour). 158 * 159 * @param result AHC response structure 160 * @return openIG response structure 161 */ 162 protected Response createResponse(final HttpResponse result) { 163 Response response = new Response(); 164 // Response entity 165 HttpEntity entity = result.getEntity(); 166 if (entity != null) { 167 try { 168 response.setEntity(IO.newBranchingInputStream(entity.getContent(), storage)); 169 } catch (IOException e) { 170 response.setStatus(Status.INTERNAL_SERVER_ERROR); 171 response.setCause(e); 172 return response; 173 } 174 } 175 176 // Response status line 177 StatusLine statusLine = result.getStatusLine(); 178 response.setVersion(statusLine.getProtocolVersion().toString()); 179 response.setStatus(Status.valueOf(statusLine.getStatusCode(), statusLine.getReasonPhrase())); 180 181 // Parse response Connection headers to be suppressed in message 182 CaseInsensitiveSet removableHeaderNames = new CaseInsensitiveSet(); 183 removableHeaderNames.addAll(ConnectionHeader.valueOf(response).getTokens()); 184 185 // Response headers 186 for (HeaderIterator i = result.headerIterator(); i.hasNext();) { 187 Header header = i.nextHeader(); 188 String name = header.getName(); 189 if (!SUPPRESS_RESPONSE_HEADERS.contains(name) && !removableHeaderNames.contains(name)) { 190 response.getHeaders().add(name, header.getValue()); 191 } 192 } 193 194 return response; 195 } 196}