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}