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.http2.impl;
29  
30  import java.net.URISyntaxException;
31  import java.util.ArrayList;
32  import java.util.Iterator;
33  import java.util.List;
34  
35  import org.apache.hc.core5.http.Header;
36  import org.apache.hc.core5.http.HttpException;
37  import org.apache.hc.core5.http.HttpHeaders;
38  import org.apache.hc.core5.http.HttpRequest;
39  import org.apache.hc.core5.http.HttpVersion;
40  import org.apache.hc.core5.http.Method;
41  import org.apache.hc.core5.http.ProtocolException;
42  import org.apache.hc.core5.http.URIScheme;
43  import org.apache.hc.core5.http.message.BasicHeader;
44  import org.apache.hc.core5.http.message.BasicHttpRequest;
45  import org.apache.hc.core5.http2.H2MessageConverter;
46  import org.apache.hc.core5.http2.H2PseudoRequestHeaders;
47  import org.apache.hc.core5.net.URIAuthority;
48  import org.apache.hc.core5.util.TextUtils;
49  
50  /**
51   * HTTP/2 request converter.
52   *
53   * @since 5.0
54   */
55  public final class DefaultH2RequestConverter implements H2MessageConverter<HttpRequest> {
56  
57      public final static DefaultH2RequestConverter INSTANCE = new DefaultH2RequestConverter();
58  
59      @Override
60      public HttpRequest convert(final List<Header> headers) throws HttpException {
61          String method = null;
62          String scheme = null;
63          String authority = null;
64          String path = null;
65          final List<Header> messageHeaders = new ArrayList<>();
66  
67          for (int i = 0; i < headers.size(); i++) {
68              final Header header = headers.get(i);
69              final String name = header.getName();
70              final String value = header.getValue();
71  
72              for (int n = 0; n < name.length(); n++) {
73                  final char ch = name.charAt(n);
74                  if (Character.isAlphabetic(ch) && !Character.isLowerCase(ch)) {
75                      throw new ProtocolException("Header name '%s' is invalid (header name contains uppercase characters)", name);
76                  }
77              }
78  
79              if (name.startsWith(":")) {
80                  if (!messageHeaders.isEmpty()) {
81                      throw new ProtocolException("Invalid sequence of headers (pseudo-headers must precede message headers)");
82                  }
83  
84                  switch (name) {
85                      case H2PseudoRequestHeaders.METHOD:
86                          if (method != null) {
87                              throw new ProtocolException("Multiple '%s' request headers are illegal", name);
88                          }
89                          method = value;
90                          break;
91                      case H2PseudoRequestHeaders.SCHEME:
92                          if (scheme != null) {
93                              throw new ProtocolException("Multiple '%s' request headers are illegal", name);
94                          }
95                          scheme = value;
96                          break;
97                      case H2PseudoRequestHeaders.PATH:
98                          if (path != null) {
99                              throw new ProtocolException("Multiple '%s' request headers are illegal", name);
100                         }
101                         path = value;
102                         break;
103                     case H2PseudoRequestHeaders.AUTHORITY:
104                         authority = value;
105                         break;
106                     default:
107                         throw new ProtocolException("Unsupported request header '%s'", name);
108                 }
109             } else {
110                 if (name.equalsIgnoreCase(HttpHeaders.CONNECTION) || name.equalsIgnoreCase(HttpHeaders.KEEP_ALIVE)
111                     || name.equalsIgnoreCase(HttpHeaders.PROXY_CONNECTION) || name.equalsIgnoreCase(HttpHeaders.TRANSFER_ENCODING)
112                     || name.equalsIgnoreCase(HttpHeaders.HOST) || name.equalsIgnoreCase(HttpHeaders.UPGRADE)) {
113                     throw new ProtocolException("Header '%s: %s' is illegal for HTTP/2 messages", header.getName(), header.getValue());
114                 }
115                 if (name.equalsIgnoreCase(HttpHeaders.TE) && !value.equalsIgnoreCase("trailers")) {
116                     throw new ProtocolException("Header '%s: %s' is illegal for HTTP/2 messages", header.getName(), header.getValue());
117                 }
118                 messageHeaders.add(header);
119             }
120         }
121         if (method == null) {
122             throw new ProtocolException("Mandatory request header '%s' not found", H2PseudoRequestHeaders.METHOD);
123         }
124         if (Method.CONNECT.isSame(method)) {
125             if (authority == null) {
126                 throw new ProtocolException("Header '%s' is mandatory for CONNECT request", H2PseudoRequestHeaders.AUTHORITY);
127             }
128             if (scheme != null) {
129                 throw new ProtocolException("Header '%s' must not be set for CONNECT request", H2PseudoRequestHeaders.SCHEME);
130             }
131             if (path != null) {
132                 throw new ProtocolException("Header '%s' must not be set for CONNECT request", H2PseudoRequestHeaders.PATH);
133             }
134         } else {
135             if (scheme == null) {
136                 throw new ProtocolException("Mandatory request header '%s' not found", H2PseudoRequestHeaders.SCHEME);
137             }
138             if (path == null) {
139                 throw new ProtocolException("Mandatory request header '%s' not found", H2PseudoRequestHeaders.PATH);
140             }
141             validatePathPseudoHeader(method, scheme, path);
142         }
143 
144         final HttpRequest httpRequest = new BasicHttpRequest(method, path);
145         httpRequest.setVersion(HttpVersion.HTTP_2);
146         httpRequest.setScheme(scheme);
147         try {
148             httpRequest.setAuthority(URIAuthority.create(authority));
149         } catch (final URISyntaxException ex) {
150             throw new ProtocolException(ex.getMessage(), ex);
151         }
152         httpRequest.setPath(path);
153         for (int i = 0; i < messageHeaders.size(); i++) {
154             httpRequest.addHeader(messageHeaders.get(i));
155         }
156         return httpRequest;
157     }
158 
159     @Override
160     public List<Header> convert(final HttpRequest message) throws HttpException {
161         if (TextUtils.isBlank(message.getMethod())) {
162             throw new ProtocolException("Request method is empty");
163         }
164         final boolean optionMethod = Method.CONNECT.name().equalsIgnoreCase(message.getMethod());
165         if (optionMethod) {
166             if (message.getAuthority() == null) {
167                 throw new ProtocolException("CONNECT request authority is not set");
168             }
169             if (message.getPath() != null) {
170                 throw new ProtocolException("CONNECT request path must be null");
171             }
172         } else {
173             if (TextUtils.isBlank(message.getScheme())) {
174                 throw new ProtocolException("Request scheme is not set");
175             }
176             if (TextUtils.isBlank(message.getPath())) {
177                 throw new ProtocolException("Request path is not set");
178             }
179         }
180         final List<Header> headers = new ArrayList<>();
181         headers.add(new BasicHeader(H2PseudoRequestHeaders.METHOD, message.getMethod(), false));
182         if (optionMethod) {
183             headers.add(new BasicHeader(H2PseudoRequestHeaders.AUTHORITY, message.getAuthority(), false));
184         }  else {
185             headers.add(new BasicHeader(H2PseudoRequestHeaders.SCHEME, message.getScheme(), false));
186             if (message.getAuthority() != null) {
187                 headers.add(new BasicHeader(H2PseudoRequestHeaders.AUTHORITY, message.getAuthority(), false));
188             }
189             headers.add(new BasicHeader(H2PseudoRequestHeaders.PATH, message.getPath(), false));
190         }
191 
192         for (final Iterator<Header> it = message.headerIterator(); it.hasNext(); ) {
193             final Header header = it.next();
194             final String name = header.getName();
195             final String value = header.getValue();
196             if (name.startsWith(":")) {
197                 throw new ProtocolException("Header name '%s' is invalid", name);
198             }
199             if (name.equalsIgnoreCase(HttpHeaders.CONNECTION) || name.equalsIgnoreCase(HttpHeaders.KEEP_ALIVE)
200                 || name.equalsIgnoreCase(HttpHeaders.PROXY_CONNECTION) || name.equalsIgnoreCase(HttpHeaders.TRANSFER_ENCODING)
201                 || name.equalsIgnoreCase(HttpHeaders.HOST) || name.equalsIgnoreCase(HttpHeaders.UPGRADE)) {
202                 throw new ProtocolException("Header '%s: %s' is illegal for HTTP/2 messages", header.getName(), header.getValue());
203             }
204             if (name.equalsIgnoreCase(HttpHeaders.TE) && !value.equalsIgnoreCase("trailers")) {
205                 throw new ProtocolException("Header '%s: %s' is illegal for HTTP/2 messages", header.getName(), header.getValue());
206             }
207             headers.add(new BasicHeader(TextUtils.toLowerCase(name), value));
208         }
209 
210         return headers;
211     }
212 
213     /**
214      * Validates the {@code :path} pseudo-header field based on the provided HTTP method and scheme.
215      * <p>
216      * This method performs the following validations:
217      * </p>
218      * <ul>
219      *     <li><strong>Non-Empty Path:</strong> For 'http' or 'https' URIs, the {@code :path} pseudo-header field must not be empty.</li>
220      *     <li><strong>OPTIONS Method:</strong> If the HTTP method is OPTIONS and the URI does not contain a path component,
221      *         the {@code :path} pseudo-header field must have a value of '*'. </li>
222      *     <li><strong>Path Starting with '/':</strong> For 'http' or 'https' URIs, the {@code :path} pseudo-header field must either start with '/' or be '*'. </li>
223      * </ul>
224      *
225      * @param method The HTTP method of the request, e.g., GET, POST, OPTIONS, etc.
226      * @param scheme The scheme of the request, e.g., http or https.
227      * @param path The value of the {@code :path} pseudo-header field.
228      * @throws ProtocolException if any of the validations fail.
229      */
230     private void validatePathPseudoHeader(final String method, final String scheme, final String path) throws ProtocolException {
231         if (URIScheme.HTTP.name().equalsIgnoreCase(scheme) || URIScheme.HTTPS.name().equalsIgnoreCase(scheme)) {
232             if (TextUtils.isBlank(path)) {
233                 throw new ProtocolException("':path' pseudo-header field must not be empty for 'http' or 'https' URIs");
234             } else {
235                 final boolean isRoot = path.startsWith("/");
236                 if (Method.OPTIONS.isSame(method)) {
237                     if (!"*".equals(path) && !isRoot) {
238                         throw new ProtocolException("OPTIONS request for an 'http' or 'https' URI must have a ':path' pseudo-header field with a value of '*' or '/'");
239                     }
240                 } else {
241                     if (!isRoot) {
242                         throw new ProtocolException("':path' pseudo-header field for 'http' or 'https' URIs must start with '/'");
243                     }
244                 }
245             }
246         }
247     }
248 }