001/*
002 * Copyright 2002-2018 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.context.request;
018
019import java.security.Principal;
020import java.text.ParseException;
021import java.text.SimpleDateFormat;
022import java.util.Arrays;
023import java.util.Enumeration;
024import java.util.Iterator;
025import java.util.List;
026import java.util.Locale;
027import java.util.Map;
028import java.util.TimeZone;
029import java.util.regex.Matcher;
030import java.util.regex.Pattern;
031
032import javax.servlet.http.HttpServletRequest;
033import javax.servlet.http.HttpServletResponse;
034import javax.servlet.http.HttpSession;
035
036import org.springframework.http.HttpHeaders;
037import org.springframework.http.HttpMethod;
038import org.springframework.http.HttpStatus;
039import org.springframework.lang.Nullable;
040import org.springframework.util.CollectionUtils;
041import org.springframework.util.ObjectUtils;
042import org.springframework.util.StringUtils;
043import org.springframework.web.util.WebUtils;
044
045/**
046 * {@link WebRequest} adapter for an {@link javax.servlet.http.HttpServletRequest}.
047 *
048 * @author Juergen Hoeller
049 * @author Brian Clozel
050 * @author Markus Malkusch
051 * @since 2.0
052 */
053public class ServletWebRequest extends ServletRequestAttributes implements NativeWebRequest {
054
055        private static final List<String> SAFE_METHODS = Arrays.asList("GET", "HEAD");
056
057        /**
058         * Pattern matching ETag multiple field values in headers such as "If-Match", "If-None-Match".
059         * @see <a href="https://tools.ietf.org/html/rfc7232#section-2.3">Section 2.3 of RFC 7232</a>
060         */
061        private static final Pattern ETAG_HEADER_VALUE_PATTERN = Pattern.compile("\\*|\\s*((W\\/)?(\"[^\"]*\"))\\s*,?");
062
063        /**
064         * Date formats as specified in the HTTP RFC.
065         * @see <a href="https://tools.ietf.org/html/rfc7231#section-7.1.1.1">Section 7.1.1.1 of RFC 7231</a>
066         */
067        private static final String[] DATE_FORMATS = new String[] {
068                        "EEE, dd MMM yyyy HH:mm:ss zzz",
069                        "EEE, dd-MMM-yy HH:mm:ss zzz",
070                        "EEE MMM dd HH:mm:ss yyyy"
071        };
072
073        private static final TimeZone GMT = TimeZone.getTimeZone("GMT");
074
075        private boolean notModified = false;
076
077
078        /**
079         * Create a new ServletWebRequest instance for the given request.
080         * @param request current HTTP request
081         */
082        public ServletWebRequest(HttpServletRequest request) {
083                super(request);
084        }
085
086        /**
087         * Create a new ServletWebRequest instance for the given request/response pair.
088         * @param request current HTTP request
089         * @param response current HTTP response (for automatic last-modified handling)
090         */
091        public ServletWebRequest(HttpServletRequest request, @Nullable HttpServletResponse response) {
092                super(request, response);
093        }
094
095
096        @Override
097        public Object getNativeRequest() {
098                return getRequest();
099        }
100
101        @Override
102        public Object getNativeResponse() {
103                return getResponse();
104        }
105
106        @Override
107        public <T> T getNativeRequest(@Nullable Class<T> requiredType) {
108                return WebUtils.getNativeRequest(getRequest(), requiredType);
109        }
110
111        @Override
112        public <T> T getNativeResponse(@Nullable Class<T> requiredType) {
113                HttpServletResponse response = getResponse();
114                return (response != null ? WebUtils.getNativeResponse(response, requiredType) : null);
115        }
116
117        /**
118         * Return the HTTP method of the request.
119         * @since 4.0.2
120         */
121        @Nullable
122        public HttpMethod getHttpMethod() {
123                return HttpMethod.resolve(getRequest().getMethod());
124        }
125
126        @Override
127        @Nullable
128        public String getHeader(String headerName) {
129                return getRequest().getHeader(headerName);
130        }
131
132        @Override
133        @Nullable
134        public String[] getHeaderValues(String headerName) {
135                String[] headerValues = StringUtils.toStringArray(getRequest().getHeaders(headerName));
136                return (!ObjectUtils.isEmpty(headerValues) ? headerValues : null);
137        }
138
139        @Override
140        public Iterator<String> getHeaderNames() {
141                return CollectionUtils.toIterator(getRequest().getHeaderNames());
142        }
143
144        @Override
145        @Nullable
146        public String getParameter(String paramName) {
147                return getRequest().getParameter(paramName);
148        }
149
150        @Override
151        @Nullable
152        public String[] getParameterValues(String paramName) {
153                return getRequest().getParameterValues(paramName);
154        }
155
156        @Override
157        public Iterator<String> getParameterNames() {
158                return CollectionUtils.toIterator(getRequest().getParameterNames());
159        }
160
161        @Override
162        public Map<String, String[]> getParameterMap() {
163                return getRequest().getParameterMap();
164        }
165
166        @Override
167        public Locale getLocale() {
168                return getRequest().getLocale();
169        }
170
171        @Override
172        public String getContextPath() {
173                return getRequest().getContextPath();
174        }
175
176        @Override
177        @Nullable
178        public String getRemoteUser() {
179                return getRequest().getRemoteUser();
180        }
181
182        @Override
183        @Nullable
184        public Principal getUserPrincipal() {
185                return getRequest().getUserPrincipal();
186        }
187
188        @Override
189        public boolean isUserInRole(String role) {
190                return getRequest().isUserInRole(role);
191        }
192
193        @Override
194        public boolean isSecure() {
195                return getRequest().isSecure();
196        }
197
198
199        @Override
200        public boolean checkNotModified(long lastModifiedTimestamp) {
201                return checkNotModified(null, lastModifiedTimestamp);
202        }
203
204        @Override
205        public boolean checkNotModified(String etag) {
206                return checkNotModified(etag, -1);
207        }
208
209        @Override
210        public boolean checkNotModified(@Nullable String etag, long lastModifiedTimestamp) {
211                HttpServletResponse response = getResponse();
212                if (this.notModified || (response != null && HttpStatus.OK.value() != response.getStatus())) {
213                        return this.notModified;
214                }
215
216                // Evaluate conditions in order of precedence.
217                // See https://tools.ietf.org/html/rfc7232#section-6
218
219                if (validateIfUnmodifiedSince(lastModifiedTimestamp)) {
220                        if (this.notModified && response != null) {
221                                response.setStatus(HttpStatus.PRECONDITION_FAILED.value());
222                        }
223                        return this.notModified;
224                }
225
226                boolean validated = validateIfNoneMatch(etag);
227                if (!validated) {
228                        validateIfModifiedSince(lastModifiedTimestamp);
229                }
230
231                // Update response
232                if (response != null) {
233                        boolean isHttpGetOrHead = SAFE_METHODS.contains(getRequest().getMethod());
234                        if (this.notModified) {
235                                response.setStatus(isHttpGetOrHead ?
236                                                HttpStatus.NOT_MODIFIED.value() : HttpStatus.PRECONDITION_FAILED.value());
237                        }
238                        if (isHttpGetOrHead) {
239                                if (lastModifiedTimestamp > 0 && parseDateValue(response.getHeader(HttpHeaders.LAST_MODIFIED)) == -1) {
240                                        response.setDateHeader(HttpHeaders.LAST_MODIFIED, lastModifiedTimestamp);
241                                }
242                                if (StringUtils.hasLength(etag) && response.getHeader(HttpHeaders.ETAG) == null) {
243                                        response.setHeader(HttpHeaders.ETAG, padEtagIfNecessary(etag));
244                                }
245                        }
246                }
247
248                return this.notModified;
249        }
250
251        private boolean validateIfUnmodifiedSince(long lastModifiedTimestamp) {
252                if (lastModifiedTimestamp < 0) {
253                        return false;
254                }
255                long ifUnmodifiedSince = parseDateHeader(HttpHeaders.IF_UNMODIFIED_SINCE);
256                if (ifUnmodifiedSince == -1) {
257                        return false;
258                }
259                // We will perform this validation...
260                this.notModified = (ifUnmodifiedSince < (lastModifiedTimestamp / 1000 * 1000));
261                return true;
262        }
263
264        private boolean validateIfNoneMatch(@Nullable String etag) {
265                if (!StringUtils.hasLength(etag)) {
266                        return false;
267                }
268
269                Enumeration<String> ifNoneMatch;
270                try {
271                        ifNoneMatch = getRequest().getHeaders(HttpHeaders.IF_NONE_MATCH);
272                }
273                catch (IllegalArgumentException ex) {
274                        return false;
275                }
276                if (!ifNoneMatch.hasMoreElements()) {
277                        return false;
278                }
279
280                // We will perform this validation...
281                etag = padEtagIfNecessary(etag);
282                if (etag.startsWith("W/")) {
283                        etag = etag.substring(2);
284                }
285                while (ifNoneMatch.hasMoreElements()) {
286                        String clientETags = ifNoneMatch.nextElement();
287                        Matcher etagMatcher = ETAG_HEADER_VALUE_PATTERN.matcher(clientETags);
288                        // Compare weak/strong ETags as per https://tools.ietf.org/html/rfc7232#section-2.3
289                        while (etagMatcher.find()) {
290                                if (StringUtils.hasLength(etagMatcher.group()) && etag.equals(etagMatcher.group(3))) {
291                                        this.notModified = true;
292                                        break;
293                                }
294                        }
295                }
296
297                return true;
298        }
299
300        private String padEtagIfNecessary(String etag) {
301                if (!StringUtils.hasLength(etag)) {
302                        return etag;
303                }
304                if ((etag.startsWith("\"") || etag.startsWith("W/\"")) && etag.endsWith("\"")) {
305                        return etag;
306                }
307                return "\"" + etag + "\"";
308        }
309
310        private boolean validateIfModifiedSince(long lastModifiedTimestamp) {
311                if (lastModifiedTimestamp < 0) {
312                        return false;
313                }
314                long ifModifiedSince = parseDateHeader(HttpHeaders.IF_MODIFIED_SINCE);
315                if (ifModifiedSince == -1) {
316                        return false;
317                }
318                // We will perform this validation...
319                this.notModified = ifModifiedSince >= (lastModifiedTimestamp / 1000 * 1000);
320                return true;
321        }
322
323        public boolean isNotModified() {
324                return this.notModified;
325        }
326
327        private long parseDateHeader(String headerName) {
328                long dateValue = -1;
329                try {
330                        dateValue = getRequest().getDateHeader(headerName);
331                }
332                catch (IllegalArgumentException ex) {
333                        String headerValue = getHeader(headerName);
334                        // Possibly an IE 10 style value: "Wed, 09 Apr 2014 09:57:42 GMT; length=13774"
335                        if (headerValue != null) {
336                                int separatorIndex = headerValue.indexOf(';');
337                                if (separatorIndex != -1) {
338                                        String datePart = headerValue.substring(0, separatorIndex);
339                                        dateValue = parseDateValue(datePart);
340                                }
341                        }
342                }
343                return dateValue;
344        }
345
346        private long parseDateValue(@Nullable String headerValue) {
347                if (headerValue == null) {
348                        // No header value sent at all
349                        return -1;
350                }
351                if (headerValue.length() >= 3) {
352                        // Short "0" or "-1" like values are never valid HTTP date headers...
353                        // Let's only bother with SimpleDateFormat parsing for long enough values.
354                        for (String dateFormat : DATE_FORMATS) {
355                                SimpleDateFormat simpleDateFormat = new SimpleDateFormat(dateFormat, Locale.US);
356                                simpleDateFormat.setTimeZone(GMT);
357                                try {
358                                        return simpleDateFormat.parse(headerValue).getTime();
359                                }
360                                catch (ParseException ex) {
361                                        // ignore
362                                }
363                        }
364                }
365                return -1;
366        }
367
368        @Override
369        public String getDescription(boolean includeClientInfo) {
370                HttpServletRequest request = getRequest();
371                StringBuilder sb = new StringBuilder();
372                sb.append("uri=").append(request.getRequestURI());
373                if (includeClientInfo) {
374                        String client = request.getRemoteAddr();
375                        if (StringUtils.hasLength(client)) {
376                                sb.append(";client=").append(client);
377                        }
378                        HttpSession session = request.getSession(false);
379                        if (session != null) {
380                                sb.append(";session=").append(session.getId());
381                        }
382                        String user = request.getRemoteUser();
383                        if (StringUtils.hasLength(user)) {
384                                sb.append(";user=").append(user);
385                        }
386                }
387                return sb.toString();
388        }
389
390
391        @Override
392        public String toString() {
393                return "ServletWebRequest: " + getDescription(true);
394        }
395
396}