-
Notifications
You must be signed in to change notification settings - Fork 6
/
HeadRequestFileResolver.java
170 lines (150 loc) · 7.72 KB
/
HeadRequestFileResolver.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
/*
* Made with all the love in the world
* by scireum in Remshalden, Germany
*
* Copyright by scireum GmbH
* http://www.scireum.de - info@scireum.de
*/
package sirius.biz.storage.layer3;
import io.netty.handler.codec.http.HttpHeaderNames;
import io.netty.handler.codec.http.HttpMethod;
import io.netty.handler.codec.http.HttpResponseStatus;
import sirius.kernel.commons.Files;
import sirius.kernel.commons.Streams;
import sirius.kernel.commons.Strings;
import sirius.kernel.commons.Tuple;
import sirius.kernel.di.std.Register;
import sirius.kernel.health.Exceptions;
import sirius.kernel.xml.Outcall;
import java.io.IOException;
import java.net.CookieManager;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.http.HttpResponse;
import java.net.http.HttpTimeoutException;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.time.LocalDateTime;
import java.util.Set;
import java.util.function.Predicate;
/**
* Resolves files by performing a HEAD request to the given URI.
* <p>
* If the request is successful, the path is extracted from the <tt>Content-Disposition</tt> header. If none was
* returned, but redirects were followed, we attempt to extract the path from the new URL. If the server doesn't support
* HEAD requests, we directly perform a GET request and hope the <tt>Content-Disposition</tt> header is set.
*/
@Register
public class HeadRequestFileResolver extends RemoteFileResolver {
@Override
public boolean requiresRequestForPathResolve() {
return true;
}
@Override
public Tuple<VirtualFile, Boolean> resolve(VirtualFile parent,
URI uri,
FetchFromUrlMode mode,
Predicate<String> fileExtensionVerifier,
Set<Options> options) throws IOException {
try {
Outcall headRequest = new Outcall(uri);
headRequest.markAsHeadRequest();
headRequest.alwaysFollowRedirects();
headRequest.modifyClient().connectTimeout(Duration.ofSeconds(10));
CookieManager cookieManager = new CookieManager();
headRequest.modifyClient().cookieHandler(cookieManager);
String path = headRequest.parseFileNameFromContentDisposition()
.filter(filename -> fileExtensionVerifier.test(Files.getFileExtension(filename)))
.orElse(null);
URI lastConnectedURI = headRequest.getResponse().request().uri();
if (Strings.isEmpty(path) && !uri.getPath().equals(lastConnectedURI.getPath())) {
// We don't have a path yet, but we followed redirects, so we check the new URI
if (headRequest.getResponseCode() == HttpResponseStatus.NOT_FOUND.code() && lastConnectedURI.toString()
.contains(
"Ã")) {
// We followed a redirect header in UTF-8 that was interpreted as ISO-8859-1, indicated by 'Ã' in the url
// as the starting byte of two byte characters in UTF-8 will always be interpreted as 'Ã' in ISO-8859-1
lastConnectedURI =
new URI(new String(lastConnectedURI.toString().getBytes(StandardCharsets.ISO_8859_1),
StandardCharsets.UTF_8));
}
path = parsePathFromUri(lastConnectedURI, fileExtensionVerifier);
}
if (Strings.isFilled(path)) {
VirtualFile file = resolveVirtualFile(parent, path, uri.getHost(), options);
LocalDateTime lastModifiedHeader =
headRequest.getHeaderFieldDate(HttpHeaderNames.LAST_MODIFIED.toString()).orElse(null);
if (lastModifiedHeader == null
|| !file.exists()
|| mode == FetchFromUrlMode.ALWAYS_FETCH
|| file.lastModifiedDate().isBefore(lastModifiedHeader)) {
return Tuple.create(file, file.performLoadFromUri(lastConnectedURI, mode));
} else {
return Tuple.create(file, false);
}
}
if (!shouldRetryWithGet(headRequest.getResponse())) {
return null;
}
} catch (HttpTimeoutException | URISyntaxException exception) {
Exceptions.ignore(exception);
}
// We either ran into a timeout or the server doesn't support HEAD requests -> re-attempt with a GET
return resolveViaGetRequest(parent, uri, mode, options);
}
private boolean shouldRetryWithGet(HttpResponse<?> response) {
if (response.statusCode() == HttpResponseStatus.METHOD_NOT_ALLOWED.code() && allowsGet(response)) {
// server disallows HEAD request and indicates GET is allowed
return true;
}
// some servers will improperly respond with 503 or 501 if HEAD requests are not allowed
// - we want to retry anyway
return response.statusCode() == HttpResponseStatus.NOT_IMPLEMENTED.code()
|| response.statusCode() == HttpResponseStatus.SERVICE_UNAVAILABLE.code();
}
private boolean allowsGet(HttpResponse<?> response) {
return response.headers()
.firstValue(HttpHeaderNames.ALLOW.toString())
.filter(header -> header.toUpperCase().contains(HttpMethod.GET.name()))
.isPresent();
}
private Tuple<VirtualFile, Boolean> resolveViaGetRequest(VirtualFile parent,
URI uri,
FetchFromUrlMode mode,
Set<Options> options) throws IOException {
Outcall request = new Outcall(uri);
request.alwaysFollowRedirects();
String path = request.parseFileNameFromContentDisposition().orElse(null);
if (Strings.isEmpty(path)) {
// Drain any content, the server sent, as we have no way of processing it...
Streams.exhaust(request.getResponse().body());
return null;
}
VirtualFile file = resolveVirtualFile(parent, path, uri.getHost(), options);
if (file.exists() && mode == FetchFromUrlMode.NON_EXISTENT) {
// Drain any content, as the mode dictates not to update the file (which might require another upload,
// so discarding the data is faster).
Streams.exhaust(request.getResponse().body());
return Tuple.create(file, false);
}
LocalDateTime lastModifiedHeader =
request.getHeaderFieldDate(HttpHeaderNames.LAST_MODIFIED.toString()).orElse(null);
if (lastModifiedHeader == null
|| !file.exists()
|| mode == FetchFromUrlMode.ALWAYS_FETCH
|| file.lastModifiedDate().isBefore(lastModifiedHeader)) {
// Directly load the file from the response, we don't need another request.
file.loadFromResponse(request.getResponse());
return Tuple.create(file, true);
} else {
// Drain any content, as the mode dictates not to update the file (which might require another upload,
// so discarding the data is faster).
Streams.exhaust(request.getResponse().body());
return Tuple.create(file, false);
}
}
@Override
public int getPriority() {
return 300;
}
}