1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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
52
53
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
215
216
217
218
219
220
221
222
223
224
225
226
227
228
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 }