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&nbsp;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&nbsp;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 &mdash; 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}