diff --git a/broker-core/src/main/java/org/apache/qpid/server/util/StringUtil.java b/broker-core/src/main/java/org/apache/qpid/server/util/StringUtil.java index 0ac09da802..159178eafd 100644 --- a/broker-core/src/main/java/org/apache/qpid/server/util/StringUtil.java +++ b/broker-core/src/main/java/org/apache/qpid/server/util/StringUtil.java @@ -20,9 +20,15 @@ */ package org.apache.qpid.server.util; +import java.io.IOException; +import java.io.StringWriter; +import java.io.Writer; import java.nio.charset.StandardCharsets; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; +import java.util.BitSet; +import java.util.Comparator; +import java.util.Map; import java.util.Random; import java.security.SecureRandom; @@ -33,9 +39,22 @@ public class StringUtil private static final String OTHERS = "_-"; private static final char[] CHARACTERS = (NUMBERS + LETTERS + LETTERS.toUpperCase() + OTHERS).toCharArray(); private static final char[] HEX = "0123456789ABCDEF".toCharArray(); + private static final Map HTML_ESCAPE = Map.of("\"", """, + "&", "&", + "<", "<", + ">", ">", + "'", "'"); + private static final BitSet HTML_ESCAPE_PREFIX_SET = new BitSet(); + private static final int LONGEST_HTML_ESCAPE_ENTRY = HTML_ESCAPE.values().stream() + .max(Comparator.comparingInt(CharSequence::length)) + .get().length(); + private static final Random RANDOM = new SecureRandom(); - - private final Random _random = new SecureRandom(); + static + { + HTML_ESCAPE.keySet().forEach(key -> HTML_ESCAPE_PREFIX_SET.set(key.charAt(0))); + + } public static String elideDataUrl(final String path) { @@ -57,7 +76,7 @@ public String randomAlphaNumericString(int maxLength) char[] result = new char[maxLength]; for (int i = 0; i < maxLength; i++) { - result[i] = (char) CHARACTERS[_random.nextInt(CHARACTERS.length)]; + result[i] = CHARACTERS[RANDOM.nextInt(CHARACTERS.length)]; } return new String(result); } @@ -69,9 +88,9 @@ public String randomAlphaNumericString(int maxLength) * @param managerName * @return unique java name */ - public String createUniqueJavaName(String managerName) + public String createUniqueJavaName(final String managerName) { - StringBuilder builder = new StringBuilder(); + final StringBuilder builder = new StringBuilder(); boolean initialChar = true; for (int i = 0; i < managerName.length(); i++) { @@ -89,7 +108,7 @@ public String createUniqueJavaName(String managerName) } try { - byte[] digest = MessageDigest.getInstance("MD5").digest(managerName.getBytes(StandardCharsets.UTF_8)); + final byte[] digest = MessageDigest.getInstance("MD5").digest(managerName.getBytes(StandardCharsets.UTF_8)); builder.append(toHex(digest).toLowerCase()); } catch (NoSuchAlgorithmException e) @@ -99,4 +118,78 @@ public String createUniqueJavaName(String managerName) return builder.toString(); } + public static String escapeHtml4(final String input) + { + if (input == null) + { + return null; + } + try + { + final StringWriter writer = new StringWriter(input.length() * 2); + translate(input, writer); + return writer.toString(); + } + catch (IOException e) + { + throw new ConnectionScopedRuntimeException(e); + } + } + + private static void translate(final CharSequence input, final Writer writer) throws IOException + { + int pos = 0; + int len = input.length(); + + while (pos < len) + { + int consumed = translate(input, pos, writer); + if (consumed == 0) + { + char c1 = input.charAt(pos); + writer.write(c1); + ++pos; + if (Character.isHighSurrogate(c1) && pos < len) + { + char c2 = input.charAt(pos); + if (Character.isLowSurrogate(c2)) + { + writer.write(c2); + ++pos; + } + } + } + else + { + for (int pt = 0; pt < consumed; ++pt) + { + pos += Character.charCount(Character.codePointAt(input, pos)); + } + } + } + } + + private static int translate(final CharSequence input, final int index, final Writer writer) throws IOException + { + if (HTML_ESCAPE_PREFIX_SET.get(input.charAt(index))) + { + int max = LONGEST_HTML_ESCAPE_ENTRY; + if (index + LONGEST_HTML_ESCAPE_ENTRY > input.length()) + { + max = input.length() - index; + } + + for (int i = max; i >= 1; --i) + { + final CharSequence subSeq = input.subSequence(index, index + i); + final String result = (String) HTML_ESCAPE.get(subSeq.toString()); + if (result != null) + { + writer.write(result); + return Character.codePointCount(subSeq, 0, subSeq.length()); + } + } + } + return 0; + } } diff --git a/broker-core/src/test/java/org/apache/qpid/server/util/StringUtilTest.java b/broker-core/src/test/java/org/apache/qpid/server/util/StringUtilTest.java index 1c32d6823c..4136855d62 100644 --- a/broker-core/src/test/java/org/apache/qpid/server/util/StringUtilTest.java +++ b/broker-core/src/test/java/org/apache/qpid/server/util/StringUtilTest.java @@ -21,6 +21,7 @@ package org.apache.qpid.server.util; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; import org.junit.jupiter.api.BeforeEach; @@ -70,4 +71,19 @@ public void testCreateUniqueJavaName() assertEquals("97b247ba19ff869340d3797cc73ca065", _util.createUniqueJavaName("1++++----")); assertEquals("d41d8cd98f00b204e9800998ecf8427e", _util.createUniqueJavaName("")); } + + @Test + public void escapeHtml4() + { + assertNull(StringUtil.escapeHtml4(null)); + assertEquals("", StringUtil.escapeHtml4("")); + assertEquals("test", StringUtil.escapeHtml4("test")); + assertEquals("<test>", StringUtil.escapeHtml4("")); + assertEquals(""test"", StringUtil.escapeHtml4("\"test\"")); + assertEquals("&", StringUtil.escapeHtml4("&")); + assertEquals("& & < " > & &", StringUtil.escapeHtml4("& & < \" > & &")); + assertEquals(">>>", StringUtil.escapeHtml4(">>>")); + assertEquals("<<<", StringUtil.escapeHtml4("<<<")); + assertEquals("'''", StringUtil.escapeHtml4("'''")); + } } diff --git a/broker-plugins/management-http/src/main/java/org/apache/qpid/server/management/plugin/servlet/DefinedFileServlet.java b/broker-plugins/management-http/src/main/java/org/apache/qpid/server/management/plugin/servlet/DefinedFileServlet.java deleted file mode 100644 index c23c5c392c..0000000000 --- a/broker-plugins/management-http/src/main/java/org/apache/qpid/server/management/plugin/servlet/DefinedFileServlet.java +++ /dev/null @@ -1,113 +0,0 @@ -/* - * 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.qpid.server.management.plugin.servlet; - -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.nio.charset.StandardCharsets; - -import jakarta.servlet.ServletConfig; -import jakarta.servlet.ServletException; -import jakarta.servlet.http.HttpServlet; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; - -import org.apache.qpid.server.management.plugin.HttpManagementUtil; - -public class DefinedFileServlet extends HttpServlet -{ - private static final long serialVersionUID = 1L; - - private static final String FILENAME_INIT_PARAMETER = "filename"; - private final String _expectedPath; - private final String _apiDocsPath; - - private String _filename; - - public DefinedFileServlet() - { - this(null); - } - - public DefinedFileServlet(String filename) - { - this(filename, null, null); - } - - - public DefinedFileServlet(String filename, String expectedPath, String apiDocsPath) - { - _filename = filename; - _expectedPath = expectedPath; - _apiDocsPath = apiDocsPath; - } - - @Override - public void init() throws ServletException - { - ServletConfig config = getServletConfig(); - String fileName = config.getInitParameter(FILENAME_INIT_PARAMETER); - if (fileName != null && !"".equals(fileName)) - { - _filename = fileName; - } - } - - @Override - protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException - { - final String path = request.getServletPath(); - if(_expectedPath == null || _expectedPath.equals(path == null ? "" : path)) - { - try (OutputStream output = HttpManagementUtil.getOutputStream(request, response)) - { - try (InputStream fileInput = getClass().getResourceAsStream("/resources/" + _filename)) - { - if (fileInput != null) - { - byte[] buffer = new byte[1024]; - response.setStatus(HttpServletResponse.SC_OK); - int read = 0; - - while ((read = fileInput.read(buffer)) > 0) - { - output.write(buffer, 0, read); - } - } - else - { - response.sendError(HttpServletResponse.SC_NOT_FOUND, "unknown file: " + _filename); - } - } - } - } - else - { - response.setStatus(HttpServletResponse.SC_NOT_FOUND); - try (OutputStream output = HttpManagementUtil.getOutputStream(request, response)) - { - final String notFoundMessage = "Unknown path '" - + request.getServletPath() - + "'. Please read the api docs at " - + request.getScheme() - + "://" + request.getServerName() + ":" + request.getServerPort() + _apiDocsPath + "\n"; - output.write(notFoundMessage.getBytes(StandardCharsets.UTF_8)); - } - } - } -} diff --git a/broker-plugins/management-http/src/main/java/org/apache/qpid/server/management/plugin/servlet/RootServlet.java b/broker-plugins/management-http/src/main/java/org/apache/qpid/server/management/plugin/servlet/RootServlet.java index 00e726fcdb..9ec7ecd728 100644 --- a/broker-plugins/management-http/src/main/java/org/apache/qpid/server/management/plugin/servlet/RootServlet.java +++ b/broker-plugins/management-http/src/main/java/org/apache/qpid/server/management/plugin/servlet/RootServlet.java @@ -27,6 +27,7 @@ import jakarta.servlet.http.HttpServletResponse; import org.apache.qpid.server.management.plugin.HttpManagementUtil; +import org.apache.qpid.server.util.StringUtil; public class RootServlet extends HttpServlet { @@ -37,7 +38,7 @@ public class RootServlet extends HttpServlet private final String _filename; - public RootServlet(String expectedPath, String apiDocsPath, String filename) + public RootServlet(final String expectedPath, final String apiDocsPath, final String filename) { _expectedPath = expectedPath; _apiDocsPath = apiDocsPath; @@ -47,48 +48,45 @@ public RootServlet(String expectedPath, String apiDocsPath, String filename) @Override public void init() throws ServletException { - + // currently no initialization logic needed } @Override - protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException + protected void doGet(final HttpServletRequest request, final HttpServletResponse response) + throws ServletException, IOException { final String path = request.getServletPath(); - if(_expectedPath == null || _expectedPath.equals(path == null ? "" : path)) + if (_expectedPath == null || _expectedPath.equals(path == null ? "" : path)) { - try (OutputStream output = HttpManagementUtil.getOutputStream(request, response)) + try (final OutputStream output = HttpManagementUtil.getOutputStream(request, response); + final InputStream fileInput = getClass().getResourceAsStream("/resources/" + _filename)) { - try (InputStream fileInput = getClass().getResourceAsStream("/resources/" + _filename)) + if (fileInput == null) { - if (fileInput != null) - { - byte[] buffer = new byte[1024]; - response.setStatus(HttpServletResponse.SC_OK); - int read = 0; + final String fileName = StringUtil.escapeHtml4(_filename); + response.sendError(HttpServletResponse.SC_NOT_FOUND, "unknown file: " + fileName); + return; + } + + final byte[] buffer = new byte[1024]; + response.setStatus(HttpServletResponse.SC_OK); + int read; - while ((read = fileInput.read(buffer)) > 0) - { - output.write(buffer, 0, read); - } - } - else - { - response.sendError(HttpServletResponse.SC_NOT_FOUND, "unknown file: " + _filename); - } + while ((read = fileInput.read(buffer)) > 0) + { + output.write(buffer, 0, read); } } } else { - response.setStatus(HttpServletResponse.SC_NOT_FOUND); - try (OutputStream output = HttpManagementUtil.getOutputStream(request, response)) + try (final OutputStream output = HttpManagementUtil.getOutputStream(request, response)) { - final String notFoundMessage = "Unknown path '" - + request.getServletPath() - + "'. Please read the api docs at " - + request.getScheme() - + "://" + request.getServerName() + ":" + request.getServerPort() + _apiDocsPath + "\n"; + final String servletPath = StringUtil.escapeHtml4(request.getServletPath()); + final String notFoundMessage = "Unknown path \"" + servletPath + + "\". Please read the api docs at " + request.getScheme() + "://" + request.getServerName() + + ":" + request.getServerPort() + "/" + _apiDocsPath + "\n"; output.write(notFoundMessage.getBytes(StandardCharsets.UTF_8)); } } diff --git a/broker-plugins/management-http/src/test/java/org/apache/qpid/server/management/plugin/servlet/RootServletTest.java b/broker-plugins/management-http/src/test/java/org/apache/qpid/server/management/plugin/servlet/RootServletTest.java new file mode 100644 index 0000000000..c2e15f6ed2 --- /dev/null +++ b/broker-plugins/management-http/src/test/java/org/apache/qpid/server/management/plugin/servlet/RootServletTest.java @@ -0,0 +1,190 @@ +/* + * 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.qpid.server.management.plugin.servlet; + +import static org.apache.qpid.server.management.plugin.HttpManagementUtil.ATTR_MANAGEMENT_CONFIGURATION; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.io.OutputStream; +import java.io.StringWriter; +import java.nio.charset.StandardCharsets; + +import jakarta.servlet.ServletContext; +import jakarta.servlet.ServletOutputStream; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.apache.commons.io.output.WriterOutputStream; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.apache.qpid.server.management.plugin.HttpManagementConfiguration; + +class RootServletTest +{ + private final HttpManagementConfiguration httpManagementConfiguration = mock(HttpManagementConfiguration.class); + private final ServletContext servletContext = mock(ServletContext.class); + private final HttpServletRequest request = mock(HttpServletRequest.class); + private final HttpServletResponse response = mock(HttpServletResponse.class); + + private final RootServlet _rootServlet = new RootServlet("expectedPath", "apiDocsPath", "file.txt"); + + @BeforeEach + void beforeEach() + { + when(servletContext.getAttribute(ATTR_MANAGEMENT_CONFIGURATION)).thenReturn(httpManagementConfiguration); + when(request.getScheme()).thenReturn("http"); + when(request.getServerName()).thenReturn("localhost"); + when(request.getServerPort()).thenReturn(8080); + when(request.getServletContext()).thenReturn(servletContext); + } + + @Test + void expectedPathUnknownFile() throws Exception + { + when(request.getServletPath()).thenReturn("expectedPath"); + + try (final StringWriter stringWriter = new StringWriter(); + final OutputStream outputStream = new WriterOutputStream(stringWriter, StandardCharsets.UTF_8)) + { + final ServletOutputStream servletOutputStream = mock(ServletOutputStream.class); + doAnswer(invocationOnMock -> + { + outputStream.write(invocationOnMock.getArgument(0)); + return null; + }).when(servletOutputStream).write(any(byte[].class)); + when(response.getOutputStream()).thenReturn(servletOutputStream); + + doAnswer(invocationOnMock -> + { + final String arg = invocationOnMock.getArgument(1); + outputStream.write(arg.getBytes(StandardCharsets.UTF_8)); + return null; + }).when(response).sendError(any(int.class), any(String.class)); + + new RootServlet("expectedPath", "apiDocsPath", "unknown-file.txt").doGet(request, response); + + outputStream.flush(); + assertEquals("unknown file: unknown-file.txt", stringWriter.toString()); + } + } + + @Test + void expectedPathKnownFile() throws Exception + { + when(request.getServletPath()).thenReturn("expectedPath"); + + try (final StringWriter stringWriter = new StringWriter(); + final OutputStream outputStream = new WriterOutputStream(stringWriter, StandardCharsets.UTF_8)) + { + final ServletOutputStream servletOutputStream = mock(ServletOutputStream.class); + doAnswer(invocationOnMock -> + { + outputStream.write(invocationOnMock.getArgument(0), invocationOnMock.getArgument(1), invocationOnMock.getArgument(2)); + return null; + }).when(servletOutputStream).write(any(byte[].class), any(int.class), any(int.class)); + when(response.getOutputStream()).thenReturn(servletOutputStream); + + _rootServlet.doGet(request, response); + + outputStream.flush(); + assertTrue(stringWriter.toString().contains("result")); + } + } + + @Test + void unknownPath () throws Exception + { + when(request.getServletPath()).thenReturn("unknown"); + + try (final StringWriter stringWriter = new StringWriter(); + final OutputStream outputStream = new WriterOutputStream(stringWriter, StandardCharsets.UTF_8)) + { + final ServletOutputStream servletOutputStream = mock(ServletOutputStream.class); + doAnswer(invocationOnMock -> + { + outputStream.write(invocationOnMock.getArgument(0)); + return null; + }).when(servletOutputStream).write(any(byte[].class)); + when(response.getOutputStream()).thenReturn(servletOutputStream); + + _rootServlet.doGet(request, response); + + outputStream.flush(); + assertEquals("Unknown path \"unknown\". Please read the api docs at " + + "http://localhost:8080/apiDocsPath\n", stringWriter.toString()); + } + } + + @Test + void escapedUnknownPath () throws Exception + { + when(request.getServletPath()).thenReturn(" & \"test\" 'test' "); + + try (final StringWriter stringWriter = new StringWriter(); + final OutputStream outputStream = new WriterOutputStream(stringWriter, StandardCharsets.UTF_8)) + { + final ServletOutputStream servletOutputStream = mock(ServletOutputStream.class); + doAnswer(invocationOnMock -> + { + outputStream.write(invocationOnMock.getArgument(0)); + return null; + }).when(servletOutputStream).write(any(byte[].class)); + when(response.getOutputStream()).thenReturn(servletOutputStream); + + _rootServlet.doGet(request, response); + + outputStream.flush(); + assertEquals("Unknown path \"<unknown> & "test" 'test' \". " + + "Please read the api docs at http://localhost:8080/apiDocsPath\n", stringWriter.toString()); + } + } + + @Test + void escapedUnknownFile () throws Exception + { + when(request.getServletPath()).thenReturn("expectedPath"); + + try (final StringWriter stringWriter = new StringWriter(); + final OutputStream outputStream = new WriterOutputStream(stringWriter, StandardCharsets.UTF_8)) + { + final ServletOutputStream servletOutputStream = mock(ServletOutputStream.class); + doAnswer(invocationOnMock -> + { + outputStream.write(invocationOnMock.getArgument(0)); + return null; + }).when(servletOutputStream).write(any(byte[].class)); + when(response.getOutputStream()).thenReturn(servletOutputStream); + + doAnswer(invocationOnMock -> + { + final String arg = invocationOnMock.getArgument(1); + outputStream.write(arg.getBytes(StandardCharsets.UTF_8)); + return null; + }).when(response).sendError(any(int.class), any(String.class)); + + new RootServlet("expectedPath", "apiDocsPath", " & \"test\" 'test'.txt").doGet(request, response); + + outputStream.flush(); + assertEquals("unknown file: <unknown> & "test" 'test'.txt", stringWriter.toString()); + } + } +} diff --git a/broker-plugins/management-http/src/test/resources/resources/file.txt b/broker-plugins/management-http/src/test/resources/resources/file.txt new file mode 100644 index 0000000000..fbeadb9d49 --- /dev/null +++ b/broker-plugins/management-http/src/test/resources/resources/file.txt @@ -0,0 +1,17 @@ +/* + * 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. + */ +result \ No newline at end of file