| /* |
| * 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.ssi; |
| |
| |
| import java.io.IOException; |
| import java.net.URL; |
| import java.net.URLConnection; |
| import java.nio.charset.Charset; |
| import java.nio.charset.StandardCharsets; |
| import java.util.Collection; |
| import java.util.Date; |
| import java.util.Enumeration; |
| import java.util.Locale; |
| import java.util.Objects; |
| |
| import jakarta.servlet.RequestDispatcher; |
| import jakarta.servlet.ServletContext; |
| import jakarta.servlet.ServletException; |
| import jakarta.servlet.http.HttpServletRequest; |
| import jakarta.servlet.http.HttpServletResponse; |
| |
| import org.apache.catalina.connector.Connector; |
| import org.apache.catalina.connector.Request; |
| import org.apache.tomcat.util.buf.B2CConverter; |
| import org.apache.tomcat.util.buf.UDecoder; |
| import org.apache.tomcat.util.http.Method; |
| import org.apache.tomcat.util.http.RequestUtil; |
| import org.apache.tomcat.util.res.StringManager; |
| |
| /** |
| * An implementation of SSIExternalResolver that is used with servlets. |
| */ |
| public class SSIServletExternalResolver implements SSIExternalResolver { |
| private static final StringManager sm = StringManager.getManager(SSIServletExternalResolver.class); |
| protected final String[] VARIABLE_NAMES = { "AUTH_TYPE", "CONTENT_LENGTH", "CONTENT_TYPE", "DOCUMENT_NAME", |
| "DOCUMENT_URI", "GATEWAY_INTERFACE", "HTTP_ACCEPT", "HTTP_ACCEPT_ENCODING", "HTTP_ACCEPT_LANGUAGE", |
| "HTTP_CONNECTION", "HTTP_HOST", "HTTP_REFERER", "HTTP_USER_AGENT", "PATH_INFO", "PATH_TRANSLATED", |
| "QUERY_STRING", "QUERY_STRING_UNESCAPED", "REMOTE_ADDR", "REMOTE_HOST", "REMOTE_PORT", "REMOTE_USER", |
| "REQUEST_METHOD", "REQUEST_URI", "SCRIPT_FILENAME", "SCRIPT_NAME", "SERVER_ADDR", "SERVER_NAME", |
| "SERVER_PORT", "SERVER_PROTOCOL", "SERVER_SOFTWARE", "UNIQUE_ID" }; |
| protected final ServletContext context; |
| protected final HttpServletRequest req; |
| protected final HttpServletResponse res; |
| protected final boolean isVirtualWebappRelative; |
| protected final int debug; |
| protected final String inputEncoding; |
| |
| public SSIServletExternalResolver(ServletContext context, HttpServletRequest req, HttpServletResponse res, |
| boolean isVirtualWebappRelative, int debug, String inputEncoding) { |
| this.context = context; |
| this.req = req; |
| this.res = res; |
| this.isVirtualWebappRelative = isVirtualWebappRelative; |
| this.debug = debug; |
| this.inputEncoding = inputEncoding; |
| } |
| |
| |
| @Override |
| public void log(String message, Throwable throwable) { |
| /* |
| * We can't assume that Servlet.log(message, null) is the same as Servlet.log( message ), since API doesn't seem |
| * to say so. |
| */ |
| if (throwable != null) { |
| context.log(message, throwable); |
| } else { |
| context.log(message); |
| } |
| } |
| |
| |
| @Override |
| public void addVariableNames(Collection<String> variableNames) { |
| for (String variableName : VARIABLE_NAMES) { |
| String variableValue = getVariableValue(variableName); |
| if (variableValue != null) { |
| variableNames.add(variableName); |
| } |
| } |
| Enumeration<String> e = req.getAttributeNames(); |
| while (e.hasMoreElements()) { |
| String name = e.nextElement(); |
| if (!isNameReserved(name)) { |
| variableNames.add(name); |
| } |
| } |
| } |
| |
| |
| protected Object getReqAttributeIgnoreCase(String targetName) { |
| Object object = null; |
| if (!isNameReserved(targetName)) { |
| object = req.getAttribute(targetName); |
| if (object == null) { |
| Enumeration<String> e = req.getAttributeNames(); |
| while (e.hasMoreElements()) { |
| String name = e.nextElement(); |
| if (targetName.equalsIgnoreCase(name) && !isNameReserved(name)) { |
| object = req.getAttribute(name); |
| if (object != null) { |
| break; |
| } |
| } |
| } |
| } |
| } |
| return object; |
| } |
| |
| |
| protected boolean isNameReserved(String name) { |
| return name.startsWith("java.") || name.startsWith("javax.") || name.startsWith("sun."); |
| } |
| |
| |
| @Override |
| public void setVariableValue(String name, String value) { |
| if (!isNameReserved(name)) { |
| req.setAttribute(name, value); |
| } |
| } |
| |
| |
| @Override |
| public String getVariableValue(String name) { |
| String retVal; |
| Object object = getReqAttributeIgnoreCase(name); |
| if (object != null) { |
| retVal = object.toString(); |
| } else { |
| retVal = getCGIVariable(name); |
| } |
| return retVal; |
| } |
| |
| |
| protected String getCGIVariable(String name) { |
| String retVal = null; |
| String[] nameParts = name.toUpperCase(Locale.ENGLISH).split("_"); |
| int requiredParts = 2; |
| if (nameParts.length == 1) { |
| if (nameParts[0].equals("PATH")) { |
| requiredParts = 1; |
| } |
| } else if (nameParts[0].equals("AUTH")) { |
| if (nameParts[1].equals("TYPE")) { |
| retVal = req.getAuthType(); |
| } |
| } else if (nameParts[0].equals("CONTENT")) { |
| if (nameParts[1].equals("LENGTH")) { |
| long contentLength = req.getContentLengthLong(); |
| if (contentLength >= 0) { |
| retVal = Long.toString(contentLength); |
| } |
| } else if (nameParts[1].equals("TYPE")) { |
| retVal = req.getContentType(); |
| } |
| } else if (nameParts[0].equals("DOCUMENT")) { |
| if (nameParts[1].equals("NAME")) { |
| String requestURI = req.getRequestURI(); |
| retVal = requestURI.substring(requestURI.lastIndexOf('/') + 1); |
| } else if (nameParts[1].equals("URI")) { |
| retVal = req.getRequestURI(); |
| } |
| } else if (name.equalsIgnoreCase("GATEWAY_INTERFACE")) { |
| retVal = "CGI/1.1"; |
| } else if (nameParts[0].equals("HTTP")) { |
| switch (nameParts[1]) { |
| case "ACCEPT" -> { |
| String accept = null; |
| if (nameParts.length == 2) { |
| accept = "Accept"; |
| } else if (nameParts[2].equals("ENCODING")) { |
| requiredParts = 3; |
| accept = "Accept-Encoding"; |
| } else if (nameParts[2].equals("LANGUAGE")) { |
| requiredParts = 3; |
| accept = "Accept-Language"; |
| } |
| if (accept != null) { |
| Enumeration<String> acceptHeaders = req.getHeaders(accept); |
| if (acceptHeaders != null) { |
| if (acceptHeaders.hasMoreElements()) { |
| StringBuilder rv = new StringBuilder(acceptHeaders.nextElement()); |
| while (acceptHeaders.hasMoreElements()) { |
| rv.append(", "); |
| rv.append(acceptHeaders.nextElement()); |
| } |
| retVal = rv.toString(); |
| } |
| } |
| } |
| } |
| case "CONNECTION" -> retVal = req.getHeader("Connection"); |
| case "HOST" -> retVal = req.getHeader("Host"); |
| case "REFERER" -> retVal = req.getHeader("Referer"); |
| case "USER" -> { |
| if (nameParts.length == 3) { |
| if (nameParts[2].equals("AGENT")) { |
| requiredParts = 3; |
| retVal = req.getHeader("User-Agent"); |
| } |
| } |
| } |
| } |
| |
| } else if (nameParts[0].equals("PATH")) { |
| if (nameParts[1].equals("INFO")) { |
| retVal = req.getPathInfo(); |
| } else if (nameParts[1].equals("TRANSLATED")) { |
| retVal = req.getPathTranslated(); |
| } |
| } else if (nameParts[0].equals("QUERY")) { |
| if (nameParts[1].equals("STRING")) { |
| String queryString = req.getQueryString(); |
| if (nameParts.length == 2) { |
| // apache displays this as an empty string rather than (none) |
| retVal = nullToEmptyString(queryString); |
| } else if (nameParts[2].equals("UNESCAPED")) { |
| requiredParts = 3; |
| if (queryString != null) { |
| Charset uriCharset = null; |
| Charset requestCharset = null; |
| boolean useBodyEncodingForURI = false; |
| |
| // Get encoding settings from request / connector if possible |
| if (req instanceof Request) { |
| requestCharset = ((Request) req).getCoyoteRequest().getCharsetHolder().getCharset(); |
| Connector connector = ((Request) req).getConnector(); |
| uriCharset = connector.getURICharset(); |
| useBodyEncodingForURI = connector.getUseBodyEncodingForURI(); |
| } |
| |
| Charset queryStringCharset; |
| |
| // If valid, apply settings from request / connector |
| // Use default as a last resort |
| if (useBodyEncodingForURI && requestCharset != null) { |
| queryStringCharset = requestCharset; |
| } else { |
| queryStringCharset = Objects.requireNonNullElse(uriCharset, StandardCharsets.UTF_8); |
| } |
| |
| retVal = UDecoder.URLDecode(queryString, queryStringCharset); |
| } |
| } |
| } |
| } else if (nameParts[0].equals("REMOTE")) { |
| switch (nameParts[1]) { |
| case "ADDR" -> retVal = req.getRemoteAddr(); |
| case "HOST" -> retVal = req.getRemoteHost(); |
| case "IDENT" -> { |
| // Not implemented |
| } |
| case "PORT" -> retVal = Integer.toString(req.getRemotePort()); |
| case "USER" -> retVal = req.getRemoteUser(); |
| } |
| } else if (nameParts[0].equals("REQUEST")) { |
| if (nameParts[1].equals("METHOD")) { |
| retVal = req.getMethod(); |
| } else if (nameParts[1].equals("URI")) { |
| // If this is an error page, get the original URI |
| retVal = (String) req.getAttribute(RequestDispatcher.FORWARD_REQUEST_URI); |
| if (retVal == null) { |
| retVal = req.getRequestURI(); |
| } |
| } |
| } else if (nameParts[0].equals("SCRIPT")) { |
| String scriptName = req.getServletPath(); |
| if (nameParts[1].equals("FILENAME")) { |
| retVal = context.getRealPath(scriptName); |
| } else if (nameParts[1].equals("NAME")) { |
| retVal = scriptName; |
| } |
| } else if (nameParts[0].equals("SERVER")) { |
| if (nameParts[1].equals("ADDR")) { |
| retVal = req.getLocalAddr(); |
| } |
| if (nameParts[1].equals("NAME")) { |
| retVal = req.getServerName(); |
| } else if (nameParts[1].equals("PORT")) { |
| retVal = Integer.toString(req.getServerPort()); |
| } else if (nameParts[1].equals("PROTOCOL")) { |
| retVal = req.getProtocol(); |
| } else if (nameParts[1].equals("SOFTWARE")) { |
| retVal = context.getServerInfo() + ' ' + System.getProperty("java.vm.name") + '/' + |
| System.getProperty("java.vm.version") + ' ' + System.getProperty("os.name"); |
| } |
| } else if (name.equalsIgnoreCase("UNIQUE_ID")) { |
| retVal = req.getRequestedSessionId(); |
| } |
| if (requiredParts != nameParts.length) { |
| return null; |
| } |
| return retVal; |
| } |
| |
| @Override |
| public Date getCurrentDate() { |
| return new Date(); |
| } |
| |
| |
| protected String nullToEmptyString(String string) { |
| String retVal = string; |
| if (retVal == null) { |
| retVal = ""; |
| } |
| return retVal; |
| } |
| |
| |
| protected String getPathWithoutFileName(String servletPath) { |
| String retVal = null; |
| int lastSlash = servletPath.lastIndexOf('/'); |
| if (lastSlash >= 0) { |
| // cut off file name |
| retVal = servletPath.substring(0, lastSlash + 1); |
| } |
| return retVal; |
| } |
| |
| |
| protected String getPathWithoutContext(final String contextPath, final String servletPath) { |
| if (servletPath.startsWith(contextPath)) { |
| return servletPath.substring(contextPath.length()); |
| } |
| return servletPath; |
| } |
| |
| |
| protected String getAbsolutePath(String path) throws IOException { |
| String pathWithoutContext = SSIServletRequestUtil.getRelativePath(req); |
| String prefix = getPathWithoutFileName(pathWithoutContext); |
| if (prefix == null) { |
| throw new IOException(sm.getString("ssiServletExternalResolver.removeFilenameError", pathWithoutContext)); |
| } |
| String fullPath = prefix + path; |
| String retVal = RequestUtil.normalize(fullPath); |
| if (retVal == null) { |
| throw new IOException(sm.getString("ssiServletExternalResolver.normalizationError", fullPath)); |
| } |
| return retVal; |
| } |
| |
| |
| protected ServletContextAndPath getServletContextAndPathFromNonVirtualPath(String nonVirtualPath) |
| throws IOException { |
| if (nonVirtualPath.startsWith("/") || nonVirtualPath.startsWith("\\")) { |
| throw new IOException(sm.getString("ssiServletExternalResolver.absoluteNonVirtualPath", nonVirtualPath)); |
| } |
| if (nonVirtualPath.contains("../")) { |
| throw new IOException( |
| sm.getString("ssiServletExternalResolver.pathTraversalNonVirtualPath", nonVirtualPath)); |
| } |
| return new ServletContextAndPath(context, getAbsolutePath(nonVirtualPath)); |
| } |
| |
| |
| protected ServletContextAndPath getServletContextAndPathFromVirtualPath(String virtualPath) throws IOException { |
| |
| if (!virtualPath.startsWith("/") && !virtualPath.startsWith("\\")) { |
| return new ServletContextAndPath(context, getAbsolutePath(virtualPath)); |
| } |
| |
| String normalized = RequestUtil.normalize(virtualPath); |
| if (isVirtualWebappRelative) { |
| return new ServletContextAndPath(context, normalized); |
| } |
| |
| ServletContext normContext = context.getContext(normalized); |
| if (normContext == null) { |
| throw new IOException(sm.getString("ssiServletExternalResolver.noContext", normalized)); |
| } |
| // If it's the root context, then there is no context element to remove. |
| // ie: '/file1.shtml' vs '/appName1/file1.shtml' |
| if (!isRootContext(normContext)) { |
| String noContext = getPathWithoutContext(normContext.getContextPath(), normalized); |
| return new ServletContextAndPath(normContext, noContext); |
| } |
| |
| return new ServletContextAndPath(normContext, normalized); |
| } |
| |
| |
| // Assumes servletContext is not-null |
| // Assumes that identity comparison will be true for the same context |
| // Assuming the above, getContext("/") will be non-null as long as the root context is accessible. |
| // If it isn't, then servletContext can't be the root context anyway, hence they will not match. |
| protected boolean isRootContext(ServletContext servletContext) { |
| return servletContext == servletContext.getContext("/"); |
| } |
| |
| |
| protected ServletContextAndPath getServletContextAndPath(String originalPath, boolean virtual) throws IOException { |
| if (debug > 0) { |
| log("SSIServletExternalResolver.getServletContextAndPath( " + originalPath + ", " + virtual + ")", null); |
| } |
| if (virtual) { |
| return getServletContextAndPathFromVirtualPath(originalPath); |
| } else { |
| return getServletContextAndPathFromNonVirtualPath(originalPath); |
| } |
| } |
| |
| |
| protected URLConnection getURLConnection(String originalPath, boolean virtual) throws IOException { |
| ServletContextAndPath csAndP = getServletContextAndPath(originalPath, virtual); |
| ServletContext context = csAndP.servletContext(); |
| String path = csAndP.path(); |
| URL url = context.getResource(path); |
| if (url == null) { |
| throw new IOException(sm.getString("ssiServletExternalResolver.noResource", path)); |
| } |
| return url.openConnection(); |
| } |
| |
| |
| @Override |
| public long getFileLastModified(String path, boolean virtual) throws IOException { |
| long lastModified = 0; |
| try { |
| URLConnection urlConnection = getURLConnection(path, virtual); |
| lastModified = urlConnection.getLastModified(); |
| } catch (IOException ignore) { |
| // Ignore this. It will always fail for non-file based includes |
| } |
| return lastModified; |
| } |
| |
| |
| @Override |
| public long getFileSize(String path, boolean virtual) throws IOException { |
| long fileSize = -1; |
| try { |
| URLConnection urlConnection = getURLConnection(path, virtual); |
| fileSize = urlConnection.getContentLengthLong(); |
| } catch (IOException ignore) { |
| // Ignore this. It will always fail for non-file based includes |
| } |
| return fileSize; |
| } |
| |
| |
| /* |
| * We are making lots of unnecessary copies of the included data here. If someone ever complains that this is slow, |
| * we should connect the included stream to the print writer that SSICommand uses. |
| */ |
| @Override |
| public String getFileText(String originalPath, boolean virtual) throws IOException { |
| try { |
| ServletContextAndPath csAndP = getServletContextAndPath(originalPath, virtual); |
| ServletContext context = csAndP.servletContext(); |
| String path = csAndP.path(); |
| RequestDispatcher rd = context.getRequestDispatcher(path); |
| if (rd == null) { |
| throw new IOException(sm.getString("ssiServletExternalResolver.requestDispatcherError", path)); |
| } |
| ByteArrayServletOutputStream basos = new ByteArrayServletOutputStream(); |
| ResponseIncludeWrapper responseIncludeWrapper = new ResponseIncludeWrapper(res, basos); |
| rd.include(req, responseIncludeWrapper); |
| // We can't assume the included servlet flushed its output |
| responseIncludeWrapper.flushOutputStreamOrWriter(); |
| byte[] bytes = basos.toByteArray(); |
| |
| // Assume platform default encoding unless otherwise specified |
| String retVal; |
| if (inputEncoding == null) { |
| retVal = new String(bytes); |
| } else { |
| retVal = new String(bytes, B2CConverter.getCharset(inputEncoding)); |
| } |
| |
| /* |
| * Make an assumption that an empty response is a failure. This is a problem if a truly empty file were |
| * included, but not sure how else to tell. |
| */ |
| if (retVal.isEmpty() && !Method.HEAD.equals(req.getMethod())) { |
| throw new IOException(sm.getString("ssiServletExternalResolver.noFile", path)); |
| } |
| return retVal; |
| } catch (ServletException e) { |
| throw new IOException(sm.getString("ssiServletExternalResolver.noIncludeFile", originalPath), e); |
| } |
| } |
| |
| protected record ServletContextAndPath(ServletContext servletContext, String path) { |
| public ServletContext getServletContext() { |
| return servletContext; |
| } |
| |
| public String getPath() { |
| return path; |
| } |
| } |
| } |