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