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.http;
018
019import static org.apache.juneau.commons.utils.Shorts.*;
020
021import java.util.*;
022
023/**
024 * Utility for masking the values of query-string parameters that commonly carry secrets (passwords, tokens, API
025 * keys) before a URI is written somewhere observable, such as a log line.
026 *
027 * <p>
028 * Parameter-name matching is case-insensitive.  Only the value is masked; the parameter name and the rest of the URI
029 * are preserved.
030 *
031 * <p class='bjava'>
032 *    String <jv>masked</jv> = RedactedQueryParams.<jsm>redactUri</jsm>(<js>"/a?user=bob&amp;token=abc"</js>);
033 *    <jc>// masked == "/a?user=bob&amp;token=[REDACTED]"</jc>
034 * </p>
035 *
036 * @since 10.0.0
037 */
038public final class RedactedQueryParams {
039
040   /** Canonical set of query parameter names commonly used to carry secrets, matched case-insensitively. */
041   public static final Set<String> DEFAULT = Set.of(
042      "password", "passwd", "secret", "token", "access_token", "api_key", "apikey", "key", "authorization", "auth",
043      "session", "sessionid", "code", "client_secret");
044
045   private RedactedQueryParams() {}
046
047   /**
048    * Returns {@code true} if the given query parameter name is in {@link #DEFAULT} (case-insensitive).
049    *
050    * @param name The parameter name to test. Can be {@code null} (returns {@code false}).
051    * @return {@code true} if the name is sensitive.
052    */
053   public static boolean isSensitive(String name) {
054      return isSensitive(name, DEFAULT);
055   }
056
057   /**
058    * Returns {@code true} if the given query parameter name matches one of {@code names}, case-insensitively.
059    *
060    * @param name The parameter name to test. Can be {@code null} (returns {@code false}).
061    * @param names The candidate names. Can be {@code null} (returns {@code false}).  {@code null} elements are ignored.
062    * @return {@code true} if the name is sensitive.
063    */
064   public static boolean isSensitive(String name, Collection<String> names) {
065      if (name == null || names == null)
066         return false;
067      for (var n : names)
068         if (n != null && eqic(name, n))
069            return true;
070      return false;
071   }
072
073   /**
074    * Masks the values of sensitive query parameters in a URI using {@link #DEFAULT}.
075    *
076    * @param uri The URI or URI reference. Can be {@code null} (returned as-is).
077    * @return The URI with sensitive parameter values replaced by {@link RedactedHeaders#REDACTED}.
078    */
079   public static String redactUri(String uri) {
080      return redactUri(uri, DEFAULT);
081   }
082
083   /**
084    * Masks the values of query parameters whose names match {@code names} in a URI.
085    *
086    * <p>
087    * The parameter name is compared after percent-decoding.  A fragment, if present, is left untouched.
088    *
089    * @param uri The URI or URI reference. Can be {@code null} (returned as-is).
090    * @param names The sensitive parameter names. Can be {@code null} (nothing is masked).
091    * @return The URI with matching parameter values replaced by {@link RedactedHeaders#REDACTED}.
092    */
093   public static String redactUri(String uri, Collection<String> names) {
094      if (uri == null || names == null)
095         return uri;
096      var q = uri.indexOf('?');
097      if (q < 0)
098         return uri;
099      var hash = uri.indexOf('#', q);
100      var end = hash < 0 ? uri.length() : hash;
101      var sb = new StringBuilder(uri.length()).append(uri, 0, q + 1);
102      var pairs = uri.substring(q + 1, end).split("&", -1);
103      for (var i = 0; i < pairs.length; i++) {
104         if (i > 0)
105            sb.append('&');
106         var pair = pairs[i];
107         var eq = pair.indexOf('=');
108         if (eq < 0) {
109            sb.append(pair);
110            continue;
111         }
112         var name = pair.substring(0, eq);
113         sb.append(name).append('=').append(isSensitive(decode(name), names) ? RedactedHeaders.REDACTED : pair.substring(eq + 1));
114      }
115      return sb.append(uri, end, uri.length()).toString();
116   }
117
118   private static String decode(String s) {
119      try {
120         return java.net.URLDecoder.decode(s, java.nio.charset.StandardCharsets.UTF_8);
121      } catch (@SuppressWarnings("unused") IllegalArgumentException e) {
122         return s;
123      }
124   }
125}