diff --git a/client/src/main/java/org/apache/cloudstack/ACSRequestLog.java b/client/src/main/java/org/apache/cloudstack/ACSRequestLog.java new file mode 100644 index 00000000000..123d2761e00 --- /dev/null +++ b/client/src/main/java/org/apache/cloudstack/ACSRequestLog.java @@ -0,0 +1,84 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +// +package org.apache.cloudstack; + +import com.cloud.utils.StringUtils; +import org.eclipse.jetty.server.NCSARequestLog; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.Response; +import org.eclipse.jetty.util.DateCache; +import org.eclipse.jetty.util.component.LifeCycle; + +import java.util.Locale; +import java.util.TimeZone; + +import static org.apache.commons.configuration.DataConfiguration.DEFAULT_DATE_FORMAT; + +public class ACSRequestLog extends NCSARequestLog { + private static final ThreadLocal buffers = + ThreadLocal.withInitial(() -> new StringBuilder(256)); + + private final DateCache dateCache; + + public ACSRequestLog() { + super(); + + TimeZone timeZone = TimeZone.getTimeZone("GMT"); + Locale locale = Locale.getDefault(); + dateCache = new DateCache(DEFAULT_DATE_FORMAT, locale, timeZone); + } + + @Override + public void log(Request request, Response response) { + String requestURI = StringUtils.cleanString(request.getOriginalURI()); + try { + StringBuilder sb = buffers.get(); + sb.setLength(0); + + sb.append(request.getHttpChannel().getEndPoint() + .getRemoteAddress().getAddress() + .getHostAddress()) + .append(" - - [") + .append(dateCache.format(request.getTimeStamp())) + .append("] \"") + .append(request.getMethod()) + .append(" ") + .append(requestURI) + .append(" ") + .append(request.getProtocol()) + .append("\" ") + .append(response.getStatus()) + .append(" ") + .append(response.getHttpChannel().getBytesWritten()) // apply filter here? + .append(" \"-\" \"") + .append(request.getHeader("User-Agent")) + .append("\""); + + write(sb.toString()); + } catch (Exception e) { + LOG.warn("Unable to log request", e); + } + } + + @Override + protected void stop(LifeCycle lifeCycle) throws Exception { + buffers.remove(); + super.stop(lifeCycle); + } +} diff --git a/client/src/main/java/org/apache/cloudstack/ServerDaemon.java b/client/src/main/java/org/apache/cloudstack/ServerDaemon.java index c6fd2ff24dc..259a99330df 100644 --- a/client/src/main/java/org/apache/cloudstack/ServerDaemon.java +++ b/client/src/main/java/org/apache/cloudstack/ServerDaemon.java @@ -32,7 +32,6 @@ import org.apache.commons.lang3.StringUtils; import org.eclipse.jetty.jmx.MBeanContainer; import org.eclipse.jetty.server.HttpConfiguration; import org.eclipse.jetty.server.HttpConnectionFactory; -import org.eclipse.jetty.server.NCSARequestLog; import org.eclipse.jetty.server.RequestLog; import org.eclipse.jetty.server.SecureRequestCustomizer; import org.eclipse.jetty.server.Server; @@ -299,7 +298,7 @@ public class ServerDaemon implements Daemon { } private RequestLog createRequestLog() { - final NCSARequestLog log = new NCSARequestLog(); + final ACSRequestLog log = new ACSRequestLog(); final File logPath = new File(accessLogFile); final File parentFile = logPath.getParentFile(); if (parentFile != null) { diff --git a/utils/src/main/java/com/cloud/utils/StringUtils.java b/utils/src/main/java/com/cloud/utils/StringUtils.java index 94295e3bb2e..73b8d04bf00 100644 --- a/utils/src/main/java/com/cloud/utils/StringUtils.java +++ b/utils/src/main/java/com/cloud/utils/StringUtils.java @@ -25,6 +25,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import java.nio.charset.Charset; import java.util.Arrays; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -39,12 +40,12 @@ import java.util.stream.Collectors; public class StringUtils extends org.apache.commons.lang3.StringUtils { private static final char[] hexChar = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'}; - private static Charset preferredACSCharset; + private static final Charset preferredACSCharset; private static final String UTF8 = "UTF-8"; static { if (isUtf8Supported()) { - preferredACSCharset = Charset.forName(UTF8); + preferredACSCharset = StandardCharsets.UTF_8; } else { preferredACSCharset = Charset.defaultCharset(); } @@ -66,8 +67,8 @@ public class StringUtils extends org.apache.commons.lang3.StringUtils { if (tags != null) { final String[] tokens = tags.split(","); final StringBuilder t = new StringBuilder(); - for (int i = 0; i < tokens.length; i++) { - t.append(tokens[i].trim()).append(","); + for (String token : tokens) { + t.append(token.trim()).append(","); } t.delete(t.length() - 1, t.length()); tags = t.toString(); @@ -77,16 +78,16 @@ public class StringUtils extends org.apache.commons.lang3.StringUtils { } /** - * @param tags + * @param tags a {code}String{code} containing a list of comma separated tags * @return List of tags */ public static List csvTagsToList(final String tags) { - final List tagsList = new ArrayList(); + final List tagsList = new ArrayList<>(); if (tags != null) { final String[] tokens = tags.split(","); - for (int i = 0; i < tokens.length; i++) { - tagsList.add(tokens[i].trim()); + for (String token : tokens) { + tagsList.add(token.trim()); } } @@ -95,13 +96,13 @@ public class StringUtils extends org.apache.commons.lang3.StringUtils { /** * Converts a List of tags to a comma separated list - * @param tagsList + * @param tagsList List of tags to convert to a comma separated list in a {code}String{code} * @return String containing a comma separated list of tags */ public static String listToCsvTags(final List tagsList) { final StringBuilder tags = new StringBuilder(); - if (tagsList.size() > 0) { + if (!tagsList.isEmpty()) { for (int i = 0; i < tagsList.size(); i++) { tags.append(tagsList.get(i)); if (i != tagsList.size() - 1) { @@ -113,22 +114,6 @@ public class StringUtils extends org.apache.commons.lang3.StringUtils { return tags.toString(); } - public static String getExceptionStackInfo(final Throwable e) { - final StringBuffer sb = new StringBuffer(); - - sb.append(e.toString()).append("\n"); - final StackTraceElement[] elemnents = e.getStackTrace(); - for (final StackTraceElement element : elemnents) { - sb.append(element.getClassName()).append("."); - sb.append(element.getMethodName()).append("("); - sb.append(element.getFileName()).append(":"); - sb.append(element.getLineNumber()).append(")"); - sb.append("\n"); - } - - return sb.toString(); - } - public static String unicodeEscape(final String s) { final StringBuilder sb = new StringBuilder(); for (int i = 0; i < s.length(); i++) { @@ -151,25 +136,22 @@ public class StringUtils extends org.apache.commons.lang3.StringUtils { return "*"; } - final StringBuffer sb = new StringBuffer(); - sb.append(password.charAt(0)); - for (int i = 1; i < password.length(); i++) { - sb.append("*"); - } - - return sb.toString(); + return password.charAt(0) + + "*".repeat(password.length() - 1); } // removes a password request param and it's value, also considering password is in query parameter value which has been url encoded - private static final Pattern REGEX_PASSWORD_QUERYSTRING = Pattern.compile("(&|%26)?[^(&|%26)]*((p|P)assword|accesskey|secretkey)(=|%3D).*?(?=(%26|[&'\"]|$))"); + private static final Pattern REGEX_PASSWORD_QUERYSTRING = Pattern.compile("(&|%26)?[^(&|%26)]*(([pP])assword|accesskey|secretkey)(=|%3D).*?(?=(%26|[&'\"]|$))"); // removes a password/accesskey/ property from a response json object - private static final Pattern REGEX_PASSWORD_JSON = Pattern.compile("\"((p|P)assword|privatekey|accesskey|secretkey)\":\\s?\".*?\",?"); + private static final Pattern REGEX_PASSWORD_JSON = Pattern.compile("\"(([pP])assword|privatekey|accesskey|secretkey)\":\\s?\".*?\",?"); - private static final Pattern REGEX_PASSWORD_DETAILS = Pattern.compile("(&|%26)?details(\\[|%5B)\\d*(\\]|%5D)\\.key(=|%3D)((p|P)assword|accesskey|secretkey)(?=(%26|[&'\"]))"); + private static final Pattern REGEX_PASSWORD_DETAILS = Pattern.compile("(&|%26)?details(\\[|%5B)\\d*(\\]|%5D)\\.key(=|%3D)(([pP])assword|accesskey|secretkey)(?=(%26|[&'\"]))"); private static final Pattern REGEX_PASSWORD_DETAILS_INDEX = Pattern.compile("details(\\[|%5B)\\d*(\\]|%5D)"); + private static final Pattern REGEX_SESSION_KEY = Pattern.compile("sessionkey=[A-Za-z0-9_-]+"); + private static final Pattern REGEX_REDUNDANT_AND = Pattern.compile("(&|%26)(&|%26)+"); // Responsible for stripping sensitive content from request and response strings @@ -178,6 +160,7 @@ public class StringUtils extends org.apache.commons.lang3.StringUtils { if (stringToClean != null) { cleanResult = REGEX_PASSWORD_QUERYSTRING.matcher(stringToClean).replaceAll(""); cleanResult = REGEX_PASSWORD_JSON.matcher(cleanResult).replaceAll(""); + cleanResult = REGEX_SESSION_KEY.matcher(cleanResult).replaceAll(""); final Matcher detailsMatcher = REGEX_PASSWORD_DETAILS.matcher(cleanResult); while (detailsMatcher.find()) { final Matcher detailsIndexMatcher = REGEX_PASSWORD_DETAILS_INDEX.matcher(detailsMatcher.group()); @@ -205,24 +188,20 @@ public class StringUtils extends org.apache.commons.lang3.StringUtils { return true; } - if (tags1 != null && tags2 == null) { - return false; - } - - if (tags1 == null && tags2 != null) { + if (tags1 == null ^ tags2 == null) { return false; } final String delimiter = ","; - final List lstTags1 = new ArrayList(); + final List lstTags1 = new ArrayList<>(); final String[] aTags1 = tags1.split(delimiter); for (final String tag1 : aTags1) { lstTags1.add(tag1.toLowerCase()); } - final List lstTags2 = new ArrayList(); + final List lstTags2 = new ArrayList<>(); final String[] aTags2 = tags2.split(delimiter); for (final String tag2 : aTags2) { @@ -233,7 +212,7 @@ public class StringUtils extends org.apache.commons.lang3.StringUtils { } public static Map stringToMap(final String s) { - final Map map = new HashMap(); + final Map map = new HashMap<>(); final String[] elements = s.split(";"); for (final String parts : elements) { final String[] keyValue = parts.split(":"); @@ -243,14 +222,14 @@ public class StringUtils extends org.apache.commons.lang3.StringUtils { } public static String mapToString(final Map map) { - String s = ""; + StringBuilder s = new StringBuilder(); for (final Map.Entry entry : map.entrySet()) { - s += entry.getKey() + ":" + entry.getValue() + ";"; + s.append(entry.getKey()).append(":").append(entry.getValue()).append(";"); } if (s.length() > 0) { - s = s.substring(0, s.length() - 1); + s = new StringBuilder(s.substring(0, s.length() - 1)); } - return s; + return s.toString(); } public static List applyPagination(final List originalList, final Long startIndex, final Long pageSizeVal) { @@ -271,7 +250,7 @@ public class StringUtils extends org.apache.commons.lang3.StringUtils { } private static List> partitionList(final List originalList, final int chunkSize) { - final List> listOfChunks = new ArrayList>(); + final List> listOfChunks = new ArrayList<>(); for (int i = 0; i < originalList.size() / chunkSize; i++) { listOfChunks.add(originalList.subList(i * chunkSize, i * chunkSize + chunkSize)); } @@ -299,9 +278,7 @@ public class StringUtils extends org.apache.commons.lang3.StringUtils { if (org.apache.commons.lang3.StringUtils.isNotBlank(jsonString)) { try { JsonNode jsonNode = objectMapper.readTree(jsonString); - jsonNode.fields().forEachRemaining(entry -> { - mapResult.put(entry.getKey(), entry.getValue().asText()); - }); + jsonNode.fields().forEachRemaining(entry -> mapResult.put(entry.getKey(), entry.getValue().asText())); } catch (Exception e) { throw new CloudRuntimeException("Error while parsing json to convert it to map " + e.getMessage()); } diff --git a/utils/src/test/java/com/cloud/utils/StringUtilsTest.java b/utils/src/test/java/com/cloud/utils/StringUtilsTest.java index 8aab74f1134..8a41f956043 100644 --- a/utils/src/test/java/com/cloud/utils/StringUtilsTest.java +++ b/utils/src/test/java/com/cloud/utils/StringUtilsTest.java @@ -240,6 +240,21 @@ public class StringUtilsTest { assertEquals(result, expected); } + @Test + public void testCleanSessionkeyFromAccessLogString() { + final String input = "GET /client/api/?managementserverid=cad7010f-216f-48cb-af11-280588863c4e&command=readyForShutdown&response=json&sessionkey=-FrgnKy6pj-JB4BI2sXqo HTTP/1.1\" 200 180 \"-\" \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.5 Safari/605.1.15 Ddg/18.5"; + final String expected = "GET /client/api/?managementserverid=cad7010f-216f-48cb-af11-280588863c4e&command=readyForShutdown&response=json& HTTP/1.1\" 200 180 \"-\" \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.5 Safari/605.1.15 Ddg/18.5"; + final String result = StringUtils.cleanString(input); + assertEquals(expected, result); + } + @Test + public void testCleanSessionkeyFromRequestJsonString() { + final String input = "{id=64b5e71d-2ae8-11ef-9466-1e00c400042b, showicon=true, command=listUsers, response=json, sessionkey=lXfAicKQXPBzt7KjLx6DwVfcOuA}"; + final String expected = "{id=64b5e71d-2ae8-11ef-9466-1e00c400042b, showicon=true, command=listUsers, response=json, }"; + final String result = StringUtils.cleanString(input); + assertEquals(expected, result); + } + @Test public void listToCsvTags() { assertEquals("a,b,c", StringUtils.listToCsvTags(Arrays.asList("a","b", "c")));