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.logging;
018
019import static org.apache.juneau.commons.utils.Shorts.*;
020
021import java.util.*;
022
023import org.apache.juneau.http.*;
024import org.apache.juneau.rest.server.*;
025
026import jakarta.servlet.http.*;
027
028/**
029 * Per-tier formatter for JUL-level-driven REST debug logging.
030 *
031 * <p>
032 * This is the single public extension point for customizing how request/response debug records are rendered.
033 * The internal two-phase pipeline invokes the tier methods <b>cumulatively</b> based on the resolved logger level:
034 * <ul>
035 *    <li>{@link #formatBasic(RestRequest,RestResponse) formatBasic} &mdash; always (tier {@code INFO}).
036 *    <li>{@link #formatHeaders(RestRequest,RestResponse) formatHeaders} &mdash; added at tier {@code FINE}.
037 *    <li>{@link #formatBody(RestRequest,RestResponse) formatBody} &mdash; added at tier {@code FINEST}.
038 * </ul>
039 *
040 * <p>
041 * The two additive tiers default to the empty string so that a bare implementation supplying only
042 * {@link #formatBasic(RestRequest,RestResponse) formatBasic} is additive-safe. Most implementations instead extend
043 * {@link BasicRestDebugFormatter} and override only the tier(s) they wish to change.
044 *
045 * <h5 class='section'>Secure by default</h5>
046 * <p>
047 * {@link BasicRestDebugFormatter} is secure by default: request/response <b>bodies are never dumped</b> unless the
048 * operator has deliberately set the {@code JUNEAU_REST_DEBUG_ALLOW_DUMP_BODIES} environment variable (and even then only
049 * at {@code FINEST} on renderable content). Credential-bearing header values are masked, every client-controlled string
050 * is escaped against log forging, and all output is length-bounded. See
051 * {@link BasicRestDebugFormatter} for the full contract.
052 *
053 * <p>
054 * The cached request/response bytes, the thrown exception, and the request execution time are reachable through the
055 * {@link RestRequest}/{@link RestResponse} accessors ({@link RestRequest#getCachedContent()},
056 * {@link RestRequest#getCachedContentLength()}, {@link RestRequest#getException()}, {@link RestRequest#getExecTime()},
057 * {@link RestResponse#getCachedContent()}, {@link RestResponse#getCachedContentLength()}).
058 *
059 * <h5 class='section'>See Also:</h5><ul>
060 *    <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/RestServerLoggingAndDebugging">Logging / Debugging</a>
061 * </ul>
062 *
063 * @since 10.0.0
064 */
065public interface RestDebugFormatter {
066
067   /** Default cap on the rendered URI length in the status line. */
068   int DEFAULT_MAX_URI_LENGTH = 2048;
069
070   /**
071    * Renders the basic ({@code INFO}-tier) portion of the debug record.
072    *
073    * <p>
074    * Always invoked. Typically a single status line such as {@code [200] HTTP GET /foo}.
075    *
076    * @param req The current REST request. Never <jk>null</jk>.
077    * @param res The current REST response. Never <jk>null</jk>.
078    * @return The rendered basic portion. Never <jk>null</jk>.
079    */
080   String formatBasic(RestRequest req, RestResponse res);
081
082   /**
083    * Renders the headers ({@code FINE}-tier) portion of the debug record.
084    *
085    * <p>
086    * Appended to {@link #formatBasic(RestRequest,RestResponse)} when the resolved logger is at {@code FINE}-or-finer.
087    * The default returns the empty string.
088    *
089    * @param req The current REST request. Never <jk>null</jk>.
090    * @param res The current REST response. Never <jk>null</jk>.
091    * @return The rendered headers portion. Never <jk>null</jk>.
092    */
093   default String formatHeaders(RestRequest req, RestResponse res) {
094      return "";
095   }
096
097   /**
098    * Renders the body ({@code FINEST}-tier) portion of the debug record.
099    *
100    * <p>
101    * Appended when the resolved logger is at {@code FINEST}. Reads the cached request/response bytes. The default
102    * returns the empty string.
103    *
104    * @param req The current REST request. Never <jk>null</jk>.
105    * @param res The current REST response. Never <jk>null</jk>.
106    * @return The rendered body portion. Never <jk>null</jk>.
107    */
108   default String formatBody(RestRequest req, RestResponse res) {
109      return "";
110   }
111
112   /**
113    * Renders the basic status line ({@code [status] HTTP method uri}) directly from servlet objects.
114    *
115    * <p>
116    * Used both by {@link #formatBasic(RestRequest,RestResponse)} and by the pipeline's no-operation (404/405) path,
117    * which has no {@link RestRequest}/{@link RestResponse} to render through. The method and URI are always
118    * {@link DebugTextSanitizer#sanitize(String,int) sanitized} (CR/LF and control characters escaped) so a
119    * client-controlled URI cannot forge a log line, and the rendered URI is length-capped. This default applies
120    * {@link #DEFAULT_MAX_URI_LENGTH}; {@link BasicRestDebugFormatter} overrides it to apply its configurable
121    * {@code maxUriLength}.
122    *
123    * <p>
124    * The query string is intentionally excluded (matching {@link HttpServletRequest#getRequestURI()}), so plaintext
125    * {@code ?token=…} disclosure is never re-introduced through this path.
126    *
127    * @param req The servlet request. Never <jk>null</jk>.
128    * @param res The servlet response. Never <jk>null</jk>.
129    * @return The rendered, sanitized status line.
130    */
131   default String statusLine(HttpServletRequest req, HttpServletResponse res) {
132      var method = DebugTextSanitizer.sanitize(req.getMethod(), DEFAULT_MAX_URI_LENGTH);
133      var uri = DebugTextSanitizer.sanitize(req.getRequestURI(), DEFAULT_MAX_URI_LENGTH);
134      return new StringBuilder().append('[').append(res.getStatus()).append("] HTTP ").append(method).append(' ').append(uri).toString();
135   }
136
137   /**
138    * Returns <jk>true</jk> if a body with the given content type is worth rendering as text (as opposed to being
139    * binary/unknown).
140    *
141    * <p>
142    * This is a text-vs-binary renderability predicate, <b>not</b> a redaction allowlist. It parses only the media type
143    * (type/subtype, ignoring parameters such as {@code ; charset=utf-8}), case-folds, and treats the following as
144    * renderable: {@code text/*}, {@code application/json}, {@code application/xml}, any {@code +json}/{@code +xml}
145    * suffix, and {@code application/x-www-form-urlencoded}. {@code multipart/form-data} is explicitly non-renderable.
146    * An absent or blank content type is conservatively treated as non-renderable.
147    *
148    * <p>
149    * {@code Content-Encoding} is not visible to this one-argument predicate; a caller that dumps bodies must separately
150    * treat any non-identity encoding (e.g. {@code gzip}) as non-renderable.
151    *
152    * @param contentType The body's content type. Can be <jk>null</jk> (returns <jk>false</jk>).
153    * @return <jk>true</jk> if the content type is renderable as text.
154    */
155   default boolean isBodyRenderable(String contentType) {
156      if (contentType == null)
157         return false;
158      var ct = contentType.trim();
159      var semi = ct.indexOf(';');
160      if (semi >= 0)
161         ct = ct.substring(0, semi).trim();
162      ct = ct.toLowerCase(Locale.ROOT);
163      if (ct.isEmpty())
164         return false;
165      if (ct.startsWith("text/"))
166         return true;
167      if (eq(ct, "multipart/form-data"))
168         return false;
169      if (eqa(ct, "application/json", "application/xml", "application/x-www-form-urlencoded"))
170         return true;
171      return ct.endsWith("+json") || ct.endsWith("+xml");
172   }
173
174   /**
175    * Returns the maximum number of request/response body bytes to capture for the {@code FINEST}-tier body rendering.
176    *
177    * <p>
178    * This cap is enforced at <i>capture</i> time by the two-phase pipeline, so memory stays bounded even for
179    * large uploads/downloads. Defaults to 8&nbsp;KB.
180    *
181    * @return The body capture cap in bytes.
182    */
183   default int bodyCap() {
184      return 8 * 1024;
185   }
186}