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.util;
018
019import java.io.BufferedReader;
020import java.io.ByteArrayOutputStream;
021import java.io.IOException;
022import java.io.InputStreamReader;
023import java.net.URLEncoder;
024import java.util.Arrays;
025import java.util.Enumeration;
026import java.util.Iterator;
027import java.util.List;
028import java.util.Map;
029
030import javax.servlet.ReadListener;
031import javax.servlet.ServletInputStream;
032import javax.servlet.http.HttpServletRequest;
033import javax.servlet.http.HttpServletRequestWrapper;
034
035import org.springframework.http.HttpMethod;
036import org.springframework.lang.Nullable;
037
038/**
039 * {@link javax.servlet.http.HttpServletRequest} wrapper that caches all content read from
040 * the {@linkplain #getInputStream() input stream} and {@linkplain #getReader() reader},
041 * and allows this content to be retrieved via a {@link #getContentAsByteArray() byte array}.
042 *
043 * <p>Used e.g. by {@link org.springframework.web.filter.AbstractRequestLoggingFilter}.
044 * Note: As of Spring Framework 5.0, this wrapper is built on the Servlet 3.1 API.
045 *
046 * @author Juergen Hoeller
047 * @author Brian Clozel
048 * @since 4.1.3
049 * @see ContentCachingResponseWrapper
050 */
051public class ContentCachingRequestWrapper extends HttpServletRequestWrapper {
052
053        private static final String FORM_CONTENT_TYPE = "application/x-www-form-urlencoded";
054
055
056        private final ByteArrayOutputStream cachedContent;
057
058        @Nullable
059        private final Integer contentCacheLimit;
060
061        @Nullable
062        private ServletInputStream inputStream;
063
064        @Nullable
065        private BufferedReader reader;
066
067
068        /**
069         * Create a new ContentCachingRequestWrapper for the given servlet request.
070         * @param request the original servlet request
071         */
072        public ContentCachingRequestWrapper(HttpServletRequest request) {
073                super(request);
074                int contentLength = request.getContentLength();
075                this.cachedContent = new ByteArrayOutputStream(contentLength >= 0 ? contentLength : 1024);
076                this.contentCacheLimit = null;
077        }
078
079        /**
080         * Create a new ContentCachingRequestWrapper for the given servlet request.
081         * @param request the original servlet request
082         * @param contentCacheLimit the maximum number of bytes to cache per request
083         * @since 4.3.6
084         * @see #handleContentOverflow(int)
085         */
086        public ContentCachingRequestWrapper(HttpServletRequest request, int contentCacheLimit) {
087                super(request);
088                this.cachedContent = new ByteArrayOutputStream(contentCacheLimit);
089                this.contentCacheLimit = contentCacheLimit;
090        }
091
092
093        @Override
094        public ServletInputStream getInputStream() throws IOException {
095                if (this.inputStream == null) {
096                        this.inputStream = new ContentCachingInputStream(getRequest().getInputStream());
097                }
098                return this.inputStream;
099        }
100
101        @Override
102        public String getCharacterEncoding() {
103                String enc = super.getCharacterEncoding();
104                return (enc != null ? enc : WebUtils.DEFAULT_CHARACTER_ENCODING);
105        }
106
107        @Override
108        public BufferedReader getReader() throws IOException {
109                if (this.reader == null) {
110                        this.reader = new BufferedReader(new InputStreamReader(getInputStream(), getCharacterEncoding()));
111                }
112                return this.reader;
113        }
114
115        @Override
116        public String getParameter(String name) {
117                if (this.cachedContent.size() == 0 && isFormPost()) {
118                        writeRequestParametersToCachedContent();
119                }
120                return super.getParameter(name);
121        }
122
123        @Override
124        public Map<String, String[]> getParameterMap() {
125                if (this.cachedContent.size() == 0 && isFormPost()) {
126                        writeRequestParametersToCachedContent();
127                }
128                return super.getParameterMap();
129        }
130
131        @Override
132        public Enumeration<String> getParameterNames() {
133                if (this.cachedContent.size() == 0 && isFormPost()) {
134                        writeRequestParametersToCachedContent();
135                }
136                return super.getParameterNames();
137        }
138
139        @Override
140        public String[] getParameterValues(String name) {
141                if (this.cachedContent.size() == 0 && isFormPost()) {
142                        writeRequestParametersToCachedContent();
143                }
144                return super.getParameterValues(name);
145        }
146
147
148        private boolean isFormPost() {
149                String contentType = getContentType();
150                return (contentType != null && contentType.contains(FORM_CONTENT_TYPE) &&
151                                HttpMethod.POST.matches(getMethod()));
152        }
153
154        private void writeRequestParametersToCachedContent() {
155                try {
156                        if (this.cachedContent.size() == 0) {
157                                String requestEncoding = getCharacterEncoding();
158                                Map<String, String[]> form = super.getParameterMap();
159                                for (Iterator<String> nameIterator = form.keySet().iterator(); nameIterator.hasNext(); ) {
160                                        String name = nameIterator.next();
161                                        List<String> values = Arrays.asList(form.get(name));
162                                        for (Iterator<String> valueIterator = values.iterator(); valueIterator.hasNext(); ) {
163                                                String value = valueIterator.next();
164                                                this.cachedContent.write(URLEncoder.encode(name, requestEncoding).getBytes());
165                                                if (value != null) {
166                                                        this.cachedContent.write('=');
167                                                        this.cachedContent.write(URLEncoder.encode(value, requestEncoding).getBytes());
168                                                        if (valueIterator.hasNext()) {
169                                                                this.cachedContent.write('&');
170                                                        }
171                                                }
172                                        }
173                                        if (nameIterator.hasNext()) {
174                                                this.cachedContent.write('&');
175                                        }
176                                }
177                        }
178                }
179                catch (IOException ex) {
180                        throw new IllegalStateException("Failed to write request parameters to cached content", ex);
181                }
182        }
183
184        /**
185         * Return the cached request content as a byte array.
186         * <p>The returned array will never be larger than the content cache limit.
187         * @see #ContentCachingRequestWrapper(HttpServletRequest, int)
188         */
189        public byte[] getContentAsByteArray() {
190                return this.cachedContent.toByteArray();
191        }
192
193        /**
194         * Template method for handling a content overflow: specifically, a request
195         * body being read that exceeds the specified content cache limit.
196         * <p>The default implementation is empty. Subclasses may override this to
197         * throw a payload-too-large exception or the like.
198         * @param contentCacheLimit the maximum number of bytes to cache per request
199         * which has just been exceeded
200         * @since 4.3.6
201         * @see #ContentCachingRequestWrapper(HttpServletRequest, int)
202         */
203        protected void handleContentOverflow(int contentCacheLimit) {
204        }
205
206
207        private class ContentCachingInputStream extends ServletInputStream {
208
209                private final ServletInputStream is;
210
211                private boolean overflow = false;
212
213                public ContentCachingInputStream(ServletInputStream is) {
214                        this.is = is;
215                }
216
217                @Override
218                public int read() throws IOException {
219                        int ch = this.is.read();
220                        if (ch != -1 && !this.overflow) {
221                                if (contentCacheLimit != null && cachedContent.size() == contentCacheLimit) {
222                                        this.overflow = true;
223                                        handleContentOverflow(contentCacheLimit);
224                                }
225                                else {
226                                        cachedContent.write(ch);
227                                }
228                        }
229                        return ch;
230                }
231
232                @Override
233                public int read(byte[] b) throws IOException {
234                        int count = this.is.read(b);
235                        writeToCache(b, 0, count);
236                        return count;
237                }
238
239                private void writeToCache(final byte[] b, final int off, int count) {
240                        if (!this.overflow && count > 0) {
241                                if (contentCacheLimit != null &&
242                                                count + cachedContent.size() > contentCacheLimit) {
243                                        this.overflow = true;
244                                        cachedContent.write(b, off, contentCacheLimit - cachedContent.size());
245                                        handleContentOverflow(contentCacheLimit);
246                                        return;
247                                }
248                                cachedContent.write(b, off, count);
249                        }
250                }
251
252                @Override
253                public int read(final byte[] b, final int off, final int len) throws IOException {
254                        int count = this.is.read(b, off, len);
255                        writeToCache(b, off, count);
256                        return count;
257                }
258
259                @Override
260                public int readLine(final byte[] b, final int off, final int len) throws IOException {
261                        int count = this.is.readLine(b, off, len);
262                        writeToCache(b, off, count);
263                        return count;
264                }
265
266                @Override
267                public boolean isFinished() {
268                        return this.is.isFinished();
269                }
270
271                @Override
272                public boolean isReady() {
273                        return this.is.isReady();
274                }
275
276                @Override
277                public void setReadListener(ReadListener readListener) {
278                        this.is.setReadListener(readListener);
279                }
280        }
281
282}