ForwardedRequest.java

/*
 * ====================================================================
 * 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 org.apache.hc.core5.http.protocol;

import java.io.IOException;
import java.net.Inet6Address;
import java.net.InetSocketAddress;
import java.net.SocketAddress;

import org.apache.hc.core5.annotation.Contract;
import org.apache.hc.core5.annotation.ThreadingBehavior;
import org.apache.hc.core5.http.EndpointDetails;
import org.apache.hc.core5.http.EntityDetails;
import org.apache.hc.core5.http.Header;
import org.apache.hc.core5.http.HttpException;
import org.apache.hc.core5.http.HttpRequest;
import org.apache.hc.core5.http.HttpRequestInterceptor;
import org.apache.hc.core5.http.HttpVersion;
import org.apache.hc.core5.http.ProtocolException;
import org.apache.hc.core5.http.ProtocolVersion;
import org.apache.hc.core5.net.URIAuthority;
import org.apache.hc.core5.util.Args;

/**
 * This request interceptor can be used by an HTTP proxy or an intermediary to add a Forwarded header
 * to outgoing request messages.
 * The Forwarded header is used to capture information about the intermediate nodes that a request
 * has passed through. This information can be useful for security purposes or for debugging purposes.
 * <p>
 * The Forwarded header consists of a list of key-value pairs separated by semicolons. The keys that
 * can be used in the Forwarded header include "host", "port", "proto", "for", and "by". The host
 * key is used to specify the host name or IP address of the request authority. The port key is used
 * to specify the port number of the request authority. The proto key is used to specify the
 * protocol version of the request. The for key is used to specify the IP address of the client
 * making the request. The by key is used to specify the IP address of the node adding the Forwarded
 * header.
 * <p>
 * When multiple proxy servers are involved in forwarding a request, each proxy can add its own
 * Forwarded header to the request. This allows for the capture of information about each
 * intermediate node that the request passes through.
 * <p>
 * The ForwardedRequest class adds the Forwarded header to the request by implementing the process()
 * method of the HttpRequestInterceptor interface. The method retrieves the ProtocolVersion and
 * {@link URIAuthority} from the {@link HttpContext}. The ProtocolVersion is used to determine the
 * proto key value and the {@link URIAuthority} is used to determine the host and port key values.
 * The method also retrieves the {@link EndpointDetails} from the {@link HttpContext}, if it exists.
 * The {@link EndpointDetails} is used to determine the by and for key values. If a Forwarded header
 * already exists in the request, the existing header is not overwritten; instead, the new header
 * value is appended to the existing header value, with a comma separator.
 * <p>
 *
 * @since 5.3
 */
@Contract(threading = ThreadingBehavior.IMMUTABLE)
public class ForwardedRequest implements HttpRequestInterceptor {

    /**
     * The name of the header to set in the HTTP request.
     */
    private static final String FORWARDED_HEADER_NAME = "Forwarded";

    /**
     * Singleton instance.
     */
    public static final HttpRequestInterceptor INSTANCE = new ForwardedRequest();


    /**
     * The process method adds a Forwarded header to an HTTP request containing information about
     * the intermediate nodes that the request has passed through. If a Forwarded header already
     * exists in the request, the new header value is appended to the existing header value, with a
     * comma separator.
     * <p>
     * The method retrieves the {@link ProtocolVersion} and {@link URIAuthority} from the
     * HttpContext. The ProtocolVersion is used to determine the proto key value and the
     * URIAuthority is used to determine the host and port key values. The method also retrieves the
     * EndpointDetails from the {@link HttpContext}, if it exists. The {@link EndpointDetails} is
     * used to determine the by key value.
     *
     * @param request the HTTP request to which the Forwarded header is to be added (not
     *                {@code null})
     * @param entity  the entity details of the request (can be {@code null})
     * @param localContext the HTTP context in which the request is being executed (not {@code null})
     * @throws HttpException     if there is an error processing the request
     * @throws IOException       if an IO error occurs while processing the request
     * @throws ProtocolException if the request authority is not specified
     */
    @Override
    public void process(final HttpRequest request, final EntityDetails entity, final HttpContext localContext) throws HttpException, IOException {
        Args.notNull(request, "HTTP request");
        Args.notNull(localContext, "HTTP context");

        final HttpCoreContext context = HttpCoreContext.cast(localContext);
        final ProtocolVersion ver = context.getProtocolVersion() != null ? context.getProtocolVersion() : HttpVersion.HTTP_1_1;

        final URIAuthority authority = request.getAuthority();
        if (authority == null) {
            throw new ProtocolException("Request authority not specified");
        }

        final int port = authority.getPort();
        final StringBuilder valueBuilder = new StringBuilder();

        final Header forwardedHeader = request.getFirstHeader(FORWARDED_HEADER_NAME);
        final boolean forwardedHeaderExists = forwardedHeader != null;
        if (forwardedHeaderExists) {
            // Forwarded header already exists, add current hop details to the end of the existing value
            valueBuilder.append(forwardedHeader.getValue());
            valueBuilder.append(", ");
        }

        // Add the current hop details
        final EndpointDetails endpointDetails = context.getEndpointDetails();

        // Add the "by" parameter
        if (endpointDetails != null) {
            final SocketAddress remoteAddress = endpointDetails.getRemoteAddress();
            if (remoteAddress instanceof InetSocketAddress) {
                final InetSocketAddress inetAddress = (InetSocketAddress) remoteAddress;
                final String byValue = inetAddress.getHostString() + ":" + inetAddress.getPort();
                if (inetAddress.getAddress() instanceof Inet6Address) {
                    valueBuilder.append("by=\"").append(byValue).append("\"");
                } else {
                    valueBuilder.append("by=").append(byValue);
                }
            }
            // Add the "for" parameter
            final SocketAddress localAddress = endpointDetails.getLocalAddress();
            if (localAddress instanceof InetSocketAddress) {
                final InetSocketAddress inetAddress = (InetSocketAddress) localAddress;
                final String forValue = inetAddress.getHostString() + ":" + inetAddress.getPort();
                if (inetAddress.getAddress() instanceof Inet6Address) {
                    valueBuilder.append(";for=\"").append(forValue).append("\"");
                } else {
                    valueBuilder.append(";for=").append(forValue);
                }
            }

        }
        // Add the "host" parameter
        if (valueBuilder.length() > 0 && !forwardedHeaderExists) {
            valueBuilder.append(";");
        }
        valueBuilder.append("host=\"").append(authority.getHostName()).append("\"");

        // Add the "port" parameter
        if (port != -1) {
            valueBuilder.append(";port=").append(port);
        }

        // Add the "proto" parameter
        final String protoValue = ver.getProtocol();
        if (protoValue != null) {
            valueBuilder.append(";proto=").append(protoValue);
        }

        final String value = valueBuilder.toString();
        request.setHeader(FORWARDED_HEADER_NAME, value);
    }

}