001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.juneau.rest.server.util; 018 019import java.io.*; 020import java.nio.charset.*; 021 022import jakarta.servlet.*; 023import jakarta.servlet.http.*; 024 025/** 026 * Wraps an {@link HttpServletRequest} and tees a <b>bounded</b> copy of the request body into memory for debug logging. 027 * 028 * <p> 029 * Unlike a fully-buffering wrapper, this captures at most a configured cap (default 8 KB) of the body while passing 030 * the full stream through to the handler untouched. The total number of bytes read is tracked so a truncation marker can 031 * be rendered. Memory stays bounded even for large uploads. 032 * 033 */ 034@SuppressWarnings({ 035 "resource" // The tee'd request stream is owned by the underlying request/servlet container, which closes it when the request completes; closing it here would break body-caching. Eclipse JDT @Owning warning is by design. 036}) 037public class CachingHttpServletRequest extends HttpServletRequestWrapper { 038 039 /** Default body capture cap, in bytes (8 KB). */ 040 public static final int DEFAULT_CAP = 8 * 1024; 041 042 /** 043 * Wraps the specified request inside a {@link CachingHttpServletRequest} if it isn't already, using the default cap. 044 * 045 * @param req The request to wrap. Must not be <jk>null</jk>. 046 * @return The wrapped request. 047 */ 048 public static CachingHttpServletRequest wrap(HttpServletRequest req) { 049 return wrap(req, DEFAULT_CAP); 050 } 051 052 /** 053 * Wraps the specified request inside a {@link CachingHttpServletRequest} if it isn't already. 054 * 055 * @param req The request to wrap. Must not be <jk>null</jk>. 056 * @param cap The maximum number of body bytes to capture. 057 * @return The wrapped request. 058 */ 059 public static CachingHttpServletRequest wrap(HttpServletRequest req, int cap) { 060 if (req instanceof CachingHttpServletRequest req2) 061 return req2; 062 return new CachingHttpServletRequest(req, cap); 063 } 064 065 private final int cap; 066 private final ByteArrayOutputStream buffer = new ByteArrayOutputStream(); 067 private long totalLength = 0; 068 private TeeServletInputStream stream; 069 private BufferedReader reader; 070 071 /** 072 * Constructor. 073 * 074 * @param req The request being wrapped. Must not be <jk>null</jk>. 075 * @param cap The maximum number of body bytes to capture. 076 */ 077 protected CachingHttpServletRequest(HttpServletRequest req, int cap) { 078 super(req); 079 this.cap = cap; 080 } 081 082 /** 083 * Returns the captured (possibly truncated) request body bytes. 084 * 085 * @return The captured body bytes (at most the configured cap). Never <jk>null</jk>; empty if the body was not read. 086 */ 087 public byte[] getContent() { return buffer.toByteArray(); } 088 089 /** 090 * Returns the total number of body bytes read so far, including any bytes beyond the capture cap. 091 * 092 * @return The total body length in bytes. 093 */ 094 public long getTotalLength() { return totalLength; } 095 096 @Override 097 public ServletInputStream getInputStream() throws IOException { 098 if (stream == null) 099 stream = new TeeServletInputStream(getRequest().getInputStream()); 100 return stream; 101 } 102 103 /** 104 * Returns a character-stream tee over the same underlying byte stream captured by {@link #getInputStream()}. 105 * 106 * <p> 107 * A handler that reads the request body through {@code getReader()} instead of {@code getInputStream()} would 108 * otherwise bypass capture entirely — the default {@link HttpServletRequestWrapper#getReader()} delegates 109 * straight to the wrapped request. This override routes the reader through the tee'd stream so both access 110 * styles are captured identically. 111 */ 112 @Override 113 public BufferedReader getReader() throws IOException { 114 if (reader == null) { 115 var enc = getCharacterEncoding(); 116 var cs = enc == null ? StandardCharsets.ISO_8859_1 : Charset.forName(enc); 117 reader = new BufferedReader(new InputStreamReader(getInputStream(), cs)); 118 } 119 return reader; 120 } 121 122 private final class TeeServletInputStream extends ServletInputStream { 123 124 private final ServletInputStream delegate; 125 126 TeeServletInputStream(ServletInputStream delegate) { 127 this.delegate = delegate; 128 } 129 130 @Override 131 public int read() throws IOException { 132 var b = delegate.read(); 133 if (b != -1) 134 capture(b); 135 return b; 136 } 137 138 @Override 139 public int read(byte[] b, int off, int len) throws IOException { 140 var n = delegate.read(b, off, len); 141 if (n > 0) 142 capture(b, off, n); 143 return n; 144 } 145 146 @Override 147 public boolean isFinished() { return delegate.isFinished(); } 148 149 @Override 150 public boolean isReady() { return delegate.isReady(); } 151 152 @Override 153 public void setReadListener(ReadListener readListener) { delegate.setReadListener(readListener); } 154 155 // Only this tee touches the outer request's capture buffer, so these helpers live here rather than on the 156 // outer class. 157 private void capture(int b) { 158 totalLength++; 159 if (buffer.size() < cap) 160 buffer.write(b); 161 } 162 163 private void capture(byte[] b, int off, int len) { 164 totalLength += len; 165 var room = cap - buffer.size(); 166 if (room > 0) 167 buffer.write(b, off, Math.min(room, len)); 168 } 169 } 170}