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&token=abc"</js>); 033 * <jc>// masked == "/a?user=bob&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}