001/*
002 * Copyright 2002-2019 the original author or authors.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *      https://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016
017package org.springframework.web.server.adapter;
018
019import java.security.Principal;
020import java.time.Instant;
021import java.time.temporal.ChronoUnit;
022import java.util.Arrays;
023import java.util.List;
024import java.util.Map;
025import java.util.concurrent.ConcurrentHashMap;
026import java.util.function.Function;
027
028import reactor.core.publisher.Mono;
029
030import org.springframework.context.ApplicationContext;
031import org.springframework.context.i18n.LocaleContext;
032import org.springframework.core.ResolvableType;
033import org.springframework.core.codec.Hints;
034import org.springframework.http.HttpHeaders;
035import org.springframework.http.HttpMethod;
036import org.springframework.http.HttpStatus;
037import org.springframework.http.InvalidMediaTypeException;
038import org.springframework.http.MediaType;
039import org.springframework.http.codec.HttpMessageReader;
040import org.springframework.http.codec.ServerCodecConfigurer;
041import org.springframework.http.codec.multipart.Part;
042import org.springframework.http.server.reactive.ServerHttpRequest;
043import org.springframework.http.server.reactive.ServerHttpResponse;
044import org.springframework.lang.Nullable;
045import org.springframework.util.Assert;
046import org.springframework.util.CollectionUtils;
047import org.springframework.util.LinkedMultiValueMap;
048import org.springframework.util.MultiValueMap;
049import org.springframework.util.StringUtils;
050import org.springframework.web.server.ServerWebExchange;
051import org.springframework.web.server.WebSession;
052import org.springframework.web.server.i18n.LocaleContextResolver;
053import org.springframework.web.server.session.WebSessionManager;
054
055/**
056 * Default implementation of {@link ServerWebExchange}.
057 *
058 * @author Rossen Stoyanchev
059 * @since 5.0
060 */
061public class DefaultServerWebExchange implements ServerWebExchange {
062
063        private static final List<HttpMethod> SAFE_METHODS = Arrays.asList(HttpMethod.GET, HttpMethod.HEAD);
064
065        private static final ResolvableType FORM_DATA_TYPE =
066                        ResolvableType.forClassWithGenerics(MultiValueMap.class, String.class, String.class);
067
068        private static final ResolvableType MULTIPART_DATA_TYPE = ResolvableType.forClassWithGenerics(
069                        MultiValueMap.class, String.class, Part.class);
070
071        private static final Mono<MultiValueMap<String, String>> EMPTY_FORM_DATA =
072                        Mono.just(CollectionUtils.unmodifiableMultiValueMap(new LinkedMultiValueMap<String, String>(0)))
073                                        .cache();
074
075        private static final Mono<MultiValueMap<String, Part>> EMPTY_MULTIPART_DATA =
076                        Mono.just(CollectionUtils.unmodifiableMultiValueMap(new LinkedMultiValueMap<String, Part>(0)))
077                                        .cache();
078
079
080        private final ServerHttpRequest request;
081
082        private final ServerHttpResponse response;
083
084        private final Map<String, Object> attributes = new ConcurrentHashMap<>();
085
086        private final Mono<WebSession> sessionMono;
087
088        private final LocaleContextResolver localeContextResolver;
089
090        private final Mono<MultiValueMap<String, String>> formDataMono;
091
092        private final Mono<MultiValueMap<String, Part>> multipartDataMono;
093
094        @Nullable
095        private final ApplicationContext applicationContext;
096
097        private volatile boolean notModified;
098
099        private Function<String, String> urlTransformer = url -> url;
100
101        @Nullable
102        private Object logId;
103
104        private String logPrefix = "";
105
106
107        public DefaultServerWebExchange(ServerHttpRequest request, ServerHttpResponse response,
108                        WebSessionManager sessionManager, ServerCodecConfigurer codecConfigurer,
109                        LocaleContextResolver localeContextResolver) {
110
111                this(request, response, sessionManager, codecConfigurer, localeContextResolver, null);
112        }
113
114        DefaultServerWebExchange(ServerHttpRequest request, ServerHttpResponse response,
115                        WebSessionManager sessionManager, ServerCodecConfigurer codecConfigurer,
116                        LocaleContextResolver localeContextResolver, @Nullable ApplicationContext applicationContext) {
117
118                Assert.notNull(request, "'request' is required");
119                Assert.notNull(response, "'response' is required");
120                Assert.notNull(sessionManager, "'sessionManager' is required");
121                Assert.notNull(codecConfigurer, "'codecConfigurer' is required");
122                Assert.notNull(localeContextResolver, "'localeContextResolver' is required");
123
124                // Initialize before first call to getLogPrefix()
125                this.attributes.put(ServerWebExchange.LOG_ID_ATTRIBUTE, request.getId());
126
127                this.request = request;
128                this.response = response;
129                this.sessionMono = sessionManager.getSession(this).cache();
130                this.localeContextResolver = localeContextResolver;
131                this.formDataMono = initFormData(request, codecConfigurer, getLogPrefix());
132                this.multipartDataMono = initMultipartData(request, codecConfigurer, getLogPrefix());
133                this.applicationContext = applicationContext;
134        }
135
136        @SuppressWarnings("unchecked")
137        private static Mono<MultiValueMap<String, String>> initFormData(ServerHttpRequest request,
138                        ServerCodecConfigurer configurer, String logPrefix) {
139
140                try {
141                        MediaType contentType = request.getHeaders().getContentType();
142                        if (MediaType.APPLICATION_FORM_URLENCODED.isCompatibleWith(contentType)) {
143                                return ((HttpMessageReader<MultiValueMap<String, String>>) configurer.getReaders().stream()
144                                                .filter(reader -> reader.canRead(FORM_DATA_TYPE, MediaType.APPLICATION_FORM_URLENCODED))
145                                                .findFirst()
146                                                .orElseThrow(() -> new IllegalStateException("No form data HttpMessageReader.")))
147                                                .readMono(FORM_DATA_TYPE, request, Hints.from(Hints.LOG_PREFIX_HINT, logPrefix))
148                                                .switchIfEmpty(EMPTY_FORM_DATA)
149                                                .cache();
150                        }
151                }
152                catch (InvalidMediaTypeException ex) {
153                        // Ignore
154                }
155                return EMPTY_FORM_DATA;
156        }
157
158        @SuppressWarnings("unchecked")
159        private static Mono<MultiValueMap<String, Part>> initMultipartData(ServerHttpRequest request,
160                        ServerCodecConfigurer configurer, String logPrefix) {
161
162                try {
163                        MediaType contentType = request.getHeaders().getContentType();
164                        if (MediaType.MULTIPART_FORM_DATA.isCompatibleWith(contentType)) {
165                                return ((HttpMessageReader<MultiValueMap<String, Part>>) configurer.getReaders().stream()
166                                                .filter(reader -> reader.canRead(MULTIPART_DATA_TYPE, MediaType.MULTIPART_FORM_DATA))
167                                                .findFirst()
168                                                .orElseThrow(() -> new IllegalStateException("No multipart HttpMessageReader.")))
169                                                .readMono(MULTIPART_DATA_TYPE, request, Hints.from(Hints.LOG_PREFIX_HINT, logPrefix))
170                                                .switchIfEmpty(EMPTY_MULTIPART_DATA)
171                                                .cache();
172                        }
173                }
174                catch (InvalidMediaTypeException ex) {
175                        // Ignore
176                }
177                return EMPTY_MULTIPART_DATA;
178        }
179
180
181        @Override
182        public ServerHttpRequest getRequest() {
183                return this.request;
184        }
185
186        private HttpHeaders getRequestHeaders() {
187                return getRequest().getHeaders();
188        }
189
190        @Override
191        public ServerHttpResponse getResponse() {
192                return this.response;
193        }
194
195        private HttpHeaders getResponseHeaders() {
196                return getResponse().getHeaders();
197        }
198
199        @Override
200        public Map<String, Object> getAttributes() {
201                return this.attributes;
202        }
203
204        @Override
205        public Mono<WebSession> getSession() {
206                return this.sessionMono;
207        }
208
209        @Override
210        public <T extends Principal> Mono<T> getPrincipal() {
211                return Mono.empty();
212        }
213
214        @Override
215        public Mono<MultiValueMap<String, String>> getFormData() {
216                return this.formDataMono;
217        }
218
219        @Override
220        public Mono<MultiValueMap<String, Part>> getMultipartData() {
221                return this.multipartDataMono;
222        }
223
224        @Override
225        public LocaleContext getLocaleContext() {
226                return this.localeContextResolver.resolveLocaleContext(this);
227        }
228
229        @Override
230        @Nullable
231        public ApplicationContext getApplicationContext() {
232                return this.applicationContext;
233        }
234
235        @Override
236        public boolean isNotModified() {
237                return this.notModified;
238        }
239
240        @Override
241        public boolean checkNotModified(Instant lastModified) {
242                return checkNotModified(null, lastModified);
243        }
244
245        @Override
246        public boolean checkNotModified(String etag) {
247                return checkNotModified(etag, Instant.MIN);
248        }
249
250        @Override
251        public boolean checkNotModified(@Nullable String etag, Instant lastModified) {
252                HttpStatus status = getResponse().getStatusCode();
253                if (this.notModified || (status != null && !HttpStatus.OK.equals(status))) {
254                        return this.notModified;
255                }
256
257                // Evaluate conditions in order of precedence.
258                // See https://tools.ietf.org/html/rfc7232#section-6
259
260                if (validateIfUnmodifiedSince(lastModified)) {
261                        if (this.notModified) {
262                                getResponse().setStatusCode(HttpStatus.PRECONDITION_FAILED);
263                        }
264                        return this.notModified;
265                }
266
267                boolean validated = validateIfNoneMatch(etag);
268                if (!validated) {
269                        validateIfModifiedSince(lastModified);
270                }
271
272                // Update response
273
274                boolean isHttpGetOrHead = SAFE_METHODS.contains(getRequest().getMethod());
275                if (this.notModified) {
276                        getResponse().setStatusCode(isHttpGetOrHead ?
277                                        HttpStatus.NOT_MODIFIED : HttpStatus.PRECONDITION_FAILED);
278                }
279                if (isHttpGetOrHead) {
280                        if (lastModified.isAfter(Instant.EPOCH) && getResponseHeaders().getLastModified() == -1) {
281                                getResponseHeaders().setLastModified(lastModified.toEpochMilli());
282                        }
283                        if (StringUtils.hasLength(etag) && getResponseHeaders().getETag() == null) {
284                                getResponseHeaders().setETag(padEtagIfNecessary(etag));
285                        }
286                }
287
288                return this.notModified;
289        }
290
291        private boolean validateIfUnmodifiedSince(Instant lastModified) {
292                if (lastModified.isBefore(Instant.EPOCH)) {
293                        return false;
294                }
295                long ifUnmodifiedSince = getRequestHeaders().getIfUnmodifiedSince();
296                if (ifUnmodifiedSince == -1) {
297                        return false;
298                }
299                // We will perform this validation...
300                Instant sinceInstant = Instant.ofEpochMilli(ifUnmodifiedSince);
301                this.notModified = sinceInstant.isBefore(lastModified.truncatedTo(ChronoUnit.SECONDS));
302                return true;
303        }
304
305        private boolean validateIfNoneMatch(@Nullable String etag) {
306                if (!StringUtils.hasLength(etag)) {
307                        return false;
308                }
309                List<String> ifNoneMatch;
310                try {
311                        ifNoneMatch = getRequestHeaders().getIfNoneMatch();
312                }
313                catch (IllegalArgumentException ex) {
314                        return false;
315                }
316                if (ifNoneMatch.isEmpty()) {
317                        return false;
318                }
319                // We will perform this validation...
320                etag = padEtagIfNecessary(etag);
321                if (etag.startsWith("W/")) {
322                        etag = etag.substring(2);
323                }
324                for (String clientEtag : ifNoneMatch) {
325                        // Compare weak/strong ETags as per https://tools.ietf.org/html/rfc7232#section-2.3
326                        if (StringUtils.hasLength(clientEtag)) {
327                                if (clientEtag.startsWith("W/")) {
328                                        clientEtag = clientEtag.substring(2);
329                                }
330                                if (clientEtag.equals(etag)) {
331                                        this.notModified = true;
332                                        break;
333                                }
334                        }
335                }
336                return true;
337        }
338
339        private String padEtagIfNecessary(String etag) {
340                if (!StringUtils.hasLength(etag)) {
341                        return etag;
342                }
343                if ((etag.startsWith("\"") || etag.startsWith("W/\"")) && etag.endsWith("\"")) {
344                        return etag;
345                }
346                return "\"" + etag + "\"";
347        }
348
349        private boolean validateIfModifiedSince(Instant lastModified) {
350                if (lastModified.isBefore(Instant.EPOCH)) {
351                        return false;
352                }
353                long ifModifiedSince = getRequestHeaders().getIfModifiedSince();
354                if (ifModifiedSince == -1) {
355                        return false;
356                }
357                // We will perform this validation...
358                this.notModified = ChronoUnit.SECONDS.between(lastModified, Instant.ofEpochMilli(ifModifiedSince)) >= 0;
359                return true;
360        }
361
362        @Override
363        public String transformUrl(String url) {
364                return this.urlTransformer.apply(url);
365        }
366
367        @Override
368        public void addUrlTransformer(Function<String, String> transformer) {
369                Assert.notNull(transformer, "'encoder' must not be null");
370                this.urlTransformer = this.urlTransformer.andThen(transformer);
371        }
372
373        @Override
374        public String getLogPrefix() {
375                Object value = getAttribute(LOG_ID_ATTRIBUTE);
376                if (this.logId != value) {
377                        this.logId = value;
378                        this.logPrefix = value != null ? "[" + value + "] " : "";
379                }
380                return this.logPrefix;
381        }
382
383}