/
Java11HttpTransportFactory.java
303 lines (269 loc) · 9.25 KB
/
Java11HttpTransportFactory.java
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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
/*******************************************************************************
* Copyright (c) 2022 Christoph Läubrich and others.
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License 2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Christoph Läubrich - initial API and implementation
*******************************************************************************/
package org.eclipse.tycho.p2maven.transport;
import java.io.IOException;
import java.io.InputStream;
import java.io.InterruptedIOException;
import java.io.OutputStream;
import java.net.Proxy;
import java.net.ProxySelector;
import java.net.SocketAddress;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpClient.Redirect;
import java.net.http.HttpClient.Version;
import java.net.http.HttpRequest;
import java.net.http.HttpRequest.BodyPublishers;
import java.net.http.HttpRequest.Builder;
import java.net.http.HttpResponse;
import java.net.http.HttpResponse.BodyHandlers;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.time.Duration;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import org.codehaus.plexus.component.annotations.Component;
import org.codehaus.plexus.component.annotations.Requirement;
import org.codehaus.plexus.logging.Logger;
import org.codehaus.plexus.personality.plexus.lifecycle.phase.Initializable;
import org.codehaus.plexus.personality.plexus.lifecycle.phase.InitializationException;
import org.eclipse.tycho.p2maven.helper.ProxyHelper;
import org.eclipse.tycho.p2maven.transport.Response.ResponseConsumer;
/**
* A transport using Java11 HttpClient
*/
@Component(role = HttpTransportFactory.class, hint = Java11HttpTransportFactory.HINT)
public class Java11HttpTransportFactory implements HttpTransportFactory, Initializable {
private static final int MAX_DISCARD = 1024 * 10;
private static final byte[] DUMMY_BUFFER = new byte[MAX_DISCARD];
private static final String LAST_MODIFIED_HEADER = "Last-Modified";
// see https://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.3
// per RFC there are three different formats:
private static final List<ThreadLocal<DateFormat>> DATE_PATTERNS = List.of(//
// RFC 1123
ThreadLocal.withInitial(() -> new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz", Locale.ENGLISH)),
// RFC 1036
ThreadLocal.withInitial(() -> new SimpleDateFormat("EEE, dd-MMM-yy HH:mm:ss zzz", Locale.ENGLISH)),
// ANSI C's asctime() format
ThreadLocal.withInitial(() -> new SimpleDateFormat("EEE MMMd HH:mm:ss yyyy", Locale.ENGLISH)));
static final String HINT = "Java11Client";
@Requirement
ProxyHelper proxyHelper;
@Requirement
MavenAuthenticator authenticator;
@Requirement
Logger logger;
private HttpClient client;
private HttpClient clientHttp1;
@Override
public HttpTransport createTransport(URI uri) {
Java11HttpTransport transport = new Java11HttpTransport(client, clientHttp1, HttpRequest.newBuilder().uri(uri),
uri, logger);
authenticator.preemtiveAuth((k, v) -> transport.setHeader(k, v), uri);
return transport;
}
private static final class Java11HttpTransport implements HttpTransport {
private Builder builder;
private HttpClient client;
private Logger logger;
private HttpClient clientHttp1;
private URI uri;
public Java11HttpTransport(HttpClient client, HttpClient clientHttp1, Builder builder, URI uri, Logger logger) {
this.client = client;
this.clientHttp1 = clientHttp1;
this.builder = builder;
this.uri = uri;
this.logger = logger;
}
@Override
public void setHeader(String key, String value) {
builder.setHeader(key, value);
}
@Override
public <T> T get(ResponseConsumer<T> consumer) throws IOException {
try {
try {
return performGet(consumer, client);
} catch (IOException e) {
if (isGoaway(e)) {
logger.info("Received GOAWAY from server " + uri.getHost() + " will retry with Http/1...");
TimeUnit.SECONDS.sleep(1);
return performGet(consumer, clientHttp1);
}
throw e;
}
} catch (InterruptedException e) {
throw new InterruptedIOException();
}
}
private <T> T performGet(ResponseConsumer<T> consumer, HttpClient httpClient)
throws IOException, InterruptedException {
HttpRequest request = builder.GET().timeout(Duration.ofSeconds(TIMEOUT_SECONDS)).build();
HttpResponse<InputStream> response = httpClient.send(request, BodyHandlers.ofInputStream());
try (ResponseImplementation<InputStream> implementation = new ResponseImplementation<>(response) {
@Override
public void close() {
if (response.version() == Version.HTTP_1_1) {
// discard any remaining data and close the stream to return the connection to
// the pool..
try (InputStream stream = response.body()) {
int discarded = 0;
while (discarded < MAX_DISCARD) {
int read = stream.read(DUMMY_BUFFER);
if (read < 0) {
break;
}
discarded += read;
}
} catch (IOException e) {
// don't care...
}
} else {
// just closing should be enough to signal to the framework...
try (InputStream stream = response.body()) {
} catch (IOException e) {
// don't care...
}
}
}
@Override
public void transferTo(OutputStream outputStream, ContentEncoding transportEncoding)
throws IOException {
transportEncoding.decode(response.body()).transferTo(outputStream);
}
}) {
return consumer.handleResponse(implementation);
}
}
@Override
public Response head() throws IOException {
try {
try {
return doHead(client);
} catch (IOException e) {
if (isGoaway(e)) {
logger.debug("Received GOAWAY from server " + uri.getHost()
+ " will retry with Http/1...");
TimeUnit.SECONDS.sleep(1);
return doHead(clientHttp1);
}
throw e;
}
} catch (InterruptedException e) {
throw new InterruptedIOException();
}
}
private Response doHead(HttpClient httpClient) throws IOException, InterruptedException {
HttpResponse<Void> response = httpClient.send(
builder.method("HEAD", BodyPublishers.noBody()).timeout(Duration.ofSeconds(TIMEOUT_SECONDS))
.build(),
BodyHandlers.discarding());
return new ResponseImplementation<>(response) {
@Override
public void close() {
// nothing...
}
@Override
public void transferTo(OutputStream outputStream, ContentEncoding transportEncoding)
throws IOException {
throw new IOException("HEAD returns no body");
}
};
}
}
private static abstract class ResponseImplementation<T> implements Response {
private final HttpResponse<T> response;
private ResponseImplementation(HttpResponse<T> response) {
this.response = response;
}
@Override
public int statusCode() {
return response.statusCode();
}
@Override
public Map<String, List<String>> headers() {
return response.headers().map();
}
@Override
public String getHeader(String header) {
return response.headers().firstValue(header).orElse(null);
}
@Override
public URI getURI() {
return response.uri();
}
@Override
public long getLastModified() {
String lastModifiedHeader = getHeader(LAST_MODIFIED_HEADER);
if (lastModifiedHeader == null)
return 0L;
// first check if there are any quotes around and remove them
if (lastModifiedHeader.length() > 1 && lastModifiedHeader.startsWith("'")
&& lastModifiedHeader.endsWith("'")) {
lastModifiedHeader = lastModifiedHeader.substring(1, lastModifiedHeader.length() - 1);
}
// no check all date formats
for (final ThreadLocal<DateFormat> dateFormat : DATE_PATTERNS) {
try {
return dateFormat.get().parse(lastModifiedHeader).getTime();
} catch (ParseException e) {
// try next one...
}
}
return 0L;
}
}
@Override
public void initialize() throws InitializationException {
ProxySelector proxySelector = new ProxySelector() {
@Override
public List<Proxy> select(URI uri) {
Proxy proxy = proxyHelper.getProxy(uri);
return List.of(proxy);
}
@Override
public void connectFailed(URI uri, SocketAddress sa, IOException ioe) {
// anything useful we can do here?
}
};
client = HttpClient.newBuilder().connectTimeout(Duration.ofMinutes(TIMEOUT_SECONDS))
.followRedirects(Redirect.NEVER)
.proxy(proxySelector).build();
clientHttp1 = HttpClient.newBuilder().connectTimeout(Duration.ofMinutes(TIMEOUT_SECONDS))
.version(Version.HTTP_1_1).followRedirects(Redirect.NEVER)
.proxy(proxySelector).build();
}
private static boolean isGoaway(Throwable e) {
if (e == null) {
return false;
}
if (e instanceof IOException) {
// first check the message
String message = e.getMessage();
if (message != null && message.contains("GOAWAY received")) {
return true;
}
// maybe it is in the stack?!?
for (StackTraceElement stack : e.getStackTrace()) {
if ("jdk.internal.net.http.Http2Connection.handleGoAway".equals(stack.getMethodName())) {
return true;
}
}
}
// look further in the chain...
return isGoaway(e.getCause());
}
}