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}