/* * 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.catalina.servlets; import java.io.File; import java.io.IOException; import java.io.PrintWriter; import java.io.StringReader; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; import javax.servlet.RequestDispatcher; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.junit.Assert; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; import org.junit.runners.Parameterized.Parameter; import org.apache.catalina.Context; import org.apache.catalina.Wrapper; import org.apache.catalina.startup.Tomcat; import org.apache.catalina.startup.TomcatBaseTest; import org.apache.tomcat.util.buf.B2CConverter; import org.apache.tomcat.util.buf.ByteChunk; import org.apache.tomcat.util.http.parser.MediaType; /* * Note: This test is split using two base classes. This is because, as a single * test, it takes so long to run it dominates the time taken to run the * tests when running tests using multiple threads. For example, on a * system with 12 cores, the tests take ~5 minutes per connector with this * test as a single test and ~3.5 minutes per connector with this test * split in two. */ @RunWith(Parameterized.class) public abstract class DefaultServletEncodingBaseTest extends TomcatBaseTest { @Parameterized.Parameters(name = "{index}: contextEnc[{0}], fileEnc[{1}], target[{2}]," + " useInclude[{3}], outputEnc[{4}], callSetCharacterEnc[{5}], useWriter[{6}]") public static Collection parameters() { String[] encodings = new String[] { "utf-8", "ibm850", "cp1252", "iso-8859-1" }; String[] targetFiles = new String[] { "cp1252", "ibm850", "iso-8859-1", "utf-8-bom", "utf-8" }; List parameterSets = new ArrayList<>(); for (String contextResponseEncoding : encodings) { for (String fileEncoding : encodings) { for (String targetFile : targetFiles) { for (Boolean useInclude : booleans) { if (useInclude.booleanValue()) { for (String outputEncoding : encodings) { for (Boolean callSetCharacterEncoding : booleans) { for (Boolean useWriter : booleans) { parameterSets.add(new Object[] { contextResponseEncoding, fileEncoding, targetFile, useInclude, outputEncoding, callSetCharacterEncoding, useWriter }); } } } } else { /* * Not using include so ignore outputEncoding, * callSetCharacterEncoding and useWriter * * Tests that do not use include are always expected to * pass. */ String encoding = targetFile; if (encoding.endsWith("-bom")) { encoding = encoding.substring(0, encoding.length() - 4); } parameterSets.add(new Object[] { contextResponseEncoding, fileEncoding, targetFile, useInclude, encoding, Boolean.FALSE, Boolean.FALSE }); } } } } } return parameterSets; } private static boolean getExpected(String fileEncoding, boolean useBom, String targetFile, String outputEncoding, boolean callSetCharacterEncoding, boolean useWriter) { if (useWriter || callSetCharacterEncoding) { /* * Using a writer or setting the output character encoding means the * response will specify a character set. These cases therefore * reduce to can the file be read with the correct encoding. * (Assuming any BOM is always skipped in the included output.) */ if (targetFile.endsWith("-bom") && useBom || targetFile.startsWith(fileEncoding) || targetFile.equals("cp1252") && fileEncoding.equals("iso-8859-1") || targetFile.equals("iso-8859-1") && fileEncoding.equals("cp1252")) { return true; } else { return false; } } else if (!(targetFile.startsWith(outputEncoding) || targetFile.equals("cp1252") && outputEncoding.equals("iso-8859-1") || targetFile.equals("iso-8859-1") && outputEncoding.equals("cp1252"))) { /* * The non-writer use cases read the target file as bytes. These * cases therefore reduce to can the bytes from the target file be * included in the output without corruption? The character used in * the tests has been chosen so that, apart from iso-8859-1 and * cp1252, the bytes vary by character set. * (Assuming any BOM is always skipped in the included output.) */ return false; } else { return true; } } @Parameter(0) public String contextResponseEncoding; @Parameter(1) public String fileEncoding; @Parameter(2) public String targetFile; @Parameter(3) public boolean useInclude; @Parameter(4) public String outputEncoding; @Parameter(5) public boolean callSetCharacterEncoding; @Parameter(6) public boolean useWriter; protected abstract boolean getUseBom(); @Test public void testEncoding() throws Exception { boolean expectedPass = getExpected(fileEncoding, getUseBom(), targetFile, outputEncoding, callSetCharacterEncoding, useWriter); Tomcat tomcat = getTomcatInstance(); File appDir = new File("test/webapp"); Context ctxt = tomcat.addContext("", appDir.getAbsolutePath()); ctxt.setResponseCharacterEncoding(contextResponseEncoding); Wrapper defaultServlet = Tomcat.addServlet(ctxt, "default", DefaultServlet.class.getName()); defaultServlet.addInitParameter("fileEncoding", fileEncoding); defaultServlet.addInitParameter("useBomIfPresent", Boolean.toString(getUseBom())); ctxt.addServletMappingDecoded("/", "default"); if (useInclude) { Tomcat.addServlet(ctxt, "include", new EncodingServlet( outputEncoding, callSetCharacterEncoding, targetFile, useWriter)); ctxt.addServletMappingDecoded("/include", "include"); } tomcat.start(); final ByteChunk res = new ByteChunk(); Map> headers = new HashMap<>(); String target; if (useInclude) { target = "http://localhost:" + getPort() + "/include"; } else { target = "http://localhost:" + getPort() + "/bug49nnn/bug49464-" + targetFile + ".txt"; } int rc = getUrl(target, res, headers); Assert.assertEquals(HttpServletResponse.SC_OK, rc); String contentType = getSingleHeader("Content-Type", headers); if (contentType != null) { MediaType mediaType = MediaType.parseMediaType(new StringReader(contentType)); String charset = mediaType.getCharset(); if (charset == null) { res.setCharset(B2CConverter.getCharset(outputEncoding)); } else { res.setCharset(B2CConverter.getCharset(charset)); } } else { res.setCharset(B2CConverter.getCharset(outputEncoding)); } String body = res.toString(); /* * Remove BOM before checking content * BOM (should be) removed by Tomcat when file is included */ if (!useInclude && targetFile.endsWith("-bom")) { body = body.substring(1); } if (expectedPass) { if (useInclude) { Assert.assertEquals("\u00bd-\u00bd-\u00bd", body); } else { Assert.assertEquals("\u00bd", body); } } else { if (useInclude) { Assert.assertNotEquals("\u00bd-\u00bd-\u00bd", body); } else { Assert.assertNotEquals("\u00bd", body); } } } private static class EncodingServlet extends HttpServlet { private static final long serialVersionUID = 1L; private final String outputEncoding; private final boolean callSetCharacterEncoding; private final String includeTarget; private final boolean useWriter; public EncodingServlet(String outputEncoding, boolean callSetCharacterEncoding, String includeTarget, boolean useWriter) { this.outputEncoding = outputEncoding; this.callSetCharacterEncoding = callSetCharacterEncoding; this.includeTarget = includeTarget; this.useWriter = useWriter; } @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { resp.setContentType("text/plain"); if (callSetCharacterEncoding) { resp.setCharacterEncoding(outputEncoding); } if (useWriter) { PrintWriter pw = resp.getWriter(); pw.print("\u00bd-"); } else { resp.getOutputStream().write("\u00bd-".getBytes(outputEncoding)); } resp.flushBuffer(); RequestDispatcher rd = req.getRequestDispatcher("/bug49nnn/bug49464-" + includeTarget + ".txt"); rd.include(req, resp); if (useWriter) { PrintWriter pw = resp.getWriter(); pw.print("-\u00bd"); } else { resp.getOutputStream().write("-\u00bd".getBytes(outputEncoding)); } } } }