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}