View Javadoc
1   /*
2    * ====================================================================
3    * Licensed to the Apache Software Foundation (ASF) under one
4    * or more contributor license agreements.  See the NOTICE file
5    * distributed with this work for additional information
6    * regarding copyright ownership.  The ASF licenses this file
7    * to you under the Apache License, Version 2.0 (the
8    * "License"); you may not use this file except in compliance
9    * with the License.  You may obtain a copy of the License at
10   *
11   *   http://www.apache.org/licenses/LICENSE-2.0
12   *
13   * Unless required by applicable law or agreed to in writing,
14   * software distributed under the License is distributed on an
15   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16   * KIND, either express or implied.  See the License for the
17   * specific language governing permissions and limitations
18   * under the License.
19   * ====================================================================
20   *
21   * This software consists of voluntary contributions made by many
22   * individuals on behalf of the Apache Software Foundation.  For more
23   * information on the Apache Software Foundation, please see
24   * <http://www.apache.org/>.
25   *
26   */
27  
28  package org.apache.hc.core5.http.protocol;
29  
30  import java.io.IOException;
31  import java.net.Inet6Address;
32  import java.net.InetSocketAddress;
33  import java.net.SocketAddress;
34  
35  import org.apache.hc.core5.annotation.Contract;
36  import org.apache.hc.core5.annotation.ThreadingBehavior;
37  import org.apache.hc.core5.http.EndpointDetails;
38  import org.apache.hc.core5.http.EntityDetails;
39  import org.apache.hc.core5.http.Header;
40  import org.apache.hc.core5.http.HttpException;
41  import org.apache.hc.core5.http.HttpRequest;
42  import org.apache.hc.core5.http.HttpRequestInterceptor;
43  import org.apache.hc.core5.http.HttpVersion;
44  import org.apache.hc.core5.http.ProtocolException;
45  import org.apache.hc.core5.http.ProtocolVersion;
46  import org.apache.hc.core5.net.URIAuthority;
47  import org.apache.hc.core5.util.Args;
48  
49  /**
50   * This request interceptor can be used by an HTTP proxy or an intermediary to add a Forwarded header
51   * to outgoing request messages.
52   * The Forwarded header is used to capture information about the intermediate nodes that a request
53   * has passed through. This information can be useful for security purposes or for debugging purposes.
54   * <p>
55   * The Forwarded header consists of a list of key-value pairs separated by semicolons. The keys that
56   * can be used in the Forwarded header include "host", "port", "proto", "for", and "by". The host
57   * key is used to specify the host name or IP address of the request authority. The port key is used
58   * to specify the port number of the request authority. The proto key is used to specify the
59   * protocol version of the request. The for key is used to specify the IP address of the client
60   * making the request. The by key is used to specify the IP address of the node adding the Forwarded
61   * header.
62   * <p>
63   * When multiple proxy servers are involved in forwarding a request, each proxy can add its own
64   * Forwarded header to the request. This allows for the capture of information about each
65   * intermediate node that the request passes through.
66   * <p>
67   * The ForwardedRequest class adds the Forwarded header to the request by implementing the process()
68   * method of the HttpRequestInterceptor interface. The method retrieves the ProtocolVersion and
69   * {@link URIAuthority} from the {@link HttpContext}. The ProtocolVersion is used to determine the
70   * proto key value and the {@link URIAuthority} is used to determine the host and port key values.
71   * The method also retrieves the {@link EndpointDetails} from the {@link HttpContext}, if it exists.
72   * The {@link EndpointDetails} is used to determine the by and for key values. If a Forwarded header
73   * already exists in the request, the existing header is not overwritten; instead, the new header
74   * value is appended to the existing header value, with a comma separator.
75   * <p>
76   *
77   * @since 5.3
78   */
79  @Contract(threading = ThreadingBehavior.IMMUTABLE)
80  public class ForwardedRequest implements HttpRequestInterceptor {
81  
82      /**
83       * The name of the header to set in the HTTP request.
84       */
85      private static final String FORWARDED_HEADER_NAME = "Forwarded";
86  
87      /**
88       * Singleton instance.
89       */
90      public static final HttpRequestInterceptor INSTANCE = new ForwardedRequest();
91  
92  
93      /**
94       * The process method adds a Forwarded header to an HTTP request containing information about
95       * the intermediate nodes that the request has passed through. If a Forwarded header already
96       * exists in the request, the new header value is appended to the existing header value, with a
97       * comma separator.
98       * <p>
99       * The method retrieves the {@link ProtocolVersion} and {@link URIAuthority} from the
100      * HttpContext. The ProtocolVersion is used to determine the proto key value and the
101      * URIAuthority is used to determine the host and port key values. The method also retrieves the
102      * EndpointDetails from the {@link HttpContext}, if it exists. The {@link EndpointDetails} is
103      * used to determine the by key value.
104      *
105      * @param request the HTTP request to which the Forwarded header is to be added (not
106      *                {@code null})
107      * @param entity  the entity details of the request (can be {@code null})
108      * @param context the HTTP context in which the request is being executed (not {@code null})
109      * @throws HttpException     if there is an error processing the request
110      * @throws IOException       if an IO error occurs while processing the request
111      * @throws ProtocolException if the request authority is not specified
112      */
113     @Override
114     public void process(final HttpRequest request, final EntityDetails entity, final HttpContext context) throws HttpException, IOException {
115         Args.notNull(request, "HTTP request");
116         Args.notNull(context, "HTTP context");
117 
118         final ProtocolVersion ver = context.getProtocolVersion() != null ? context.getProtocolVersion() : HttpVersion.HTTP_1_1;
119 
120         final URIAuthority authority = request.getAuthority();
121         if (authority == null) {
122             throw new ProtocolException("Request authority not specified");
123         }
124 
125         final int port = authority.getPort();
126         final StringBuilder valueBuilder = new StringBuilder();
127 
128         final Header forwardedHeader = request.getFirstHeader(FORWARDED_HEADER_NAME);
129         final boolean forwardedHeaderExists = forwardedHeader != null;
130         if (forwardedHeaderExists) {
131             // Forwarded header already exists, add current hop details to the end of the existing value
132             valueBuilder.append(forwardedHeader.getValue());
133             valueBuilder.append(", ");
134         }
135 
136         // Add the current hop details
137         final EndpointDetails endpointDetails = (EndpointDetails) context.getAttribute(HttpCoreContext.CONNECTION_ENDPOINT);
138 
139         // Add the "by" parameter
140         if (endpointDetails != null) {
141             final SocketAddress remoteAddress = endpointDetails.getRemoteAddress();
142             if (remoteAddress instanceof InetSocketAddress) {
143                 final InetSocketAddress inetAddress = (InetSocketAddress) remoteAddress;
144                 final String byValue = inetAddress.getHostString() + ":" + inetAddress.getPort();
145                 if (inetAddress.getAddress() instanceof Inet6Address) {
146                     valueBuilder.append("by=\"").append(byValue).append("\"");
147                 } else {
148                     valueBuilder.append("by=").append(byValue);
149                 }
150             }
151             // Add the "for" parameter
152             final SocketAddress localAddress = endpointDetails.getLocalAddress();
153             if (localAddress instanceof InetSocketAddress) {
154                 final InetSocketAddress inetAddress = (InetSocketAddress) localAddress;
155                 final String forValue = inetAddress.getHostString() + ":" + inetAddress.getPort();
156                 if (inetAddress.getAddress() instanceof Inet6Address) {
157                     valueBuilder.append(";for=\"").append(forValue).append("\"");
158                 } else {
159                     valueBuilder.append(";for=").append(forValue);
160                 }
161             }
162 
163         }
164         // Add the "host" parameter
165         if (valueBuilder.length() > 0 && !forwardedHeaderExists) {
166             valueBuilder.append(";");
167         }
168         valueBuilder.append("host=\"").append(authority.getHostName()).append("\"");
169 
170         // Add the "port" parameter
171         if (port != -1) {
172             valueBuilder.append(";port=").append(port);
173         }
174 
175         // Add the "proto" parameter
176         final String protoValue = ver.getProtocol();
177         if (protoValue != null) {
178             valueBuilder.append(";proto=").append(protoValue);
179         }
180 
181         final String value = valueBuilder.toString();
182         request.setHeader(FORWARDED_HEADER_NAME, value);
183     }
184 
185 }