blob: 0293aedba2869808509dbf0ab7eb585b8310d9a5 [file] [log] [blame]
/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
~ 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.sling.xss.impl.webconsole;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
import javax.json.Json;
import javax.json.JsonArrayBuilder;
import javax.json.JsonObject;
import javax.json.JsonWriter;
import javax.servlet.Servlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringEscapeUtils;
import org.apache.sling.xss.XSSFilter;
import org.apache.sling.xss.impl.XSSFilterImpl;
import org.apache.sling.xss.impl.status.XSSStatusService;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@Component(
service = Servlet.class,
property = {
XSSProtectionAPIWebConsolePlugin.REG_PROP_LABEL + "=" + XSSProtectionAPIWebConsolePlugin.LABEL,
XSSProtectionAPIWebConsolePlugin.REG_PROP_TITLE + "=" + XSSProtectionAPIWebConsolePlugin.TITLE,
XSSProtectionAPIWebConsolePlugin.REG_PROP_CATEGORY + "=Sling"
}
)
public class XSSProtectionAPIWebConsolePlugin extends HttpServlet {
private static final Logger LOGGER = LoggerFactory.getLogger(XSSProtectionAPIWebConsolePlugin.class);
/*
do not replace the following constants with the ones from org.apache.felix, since you'll create a wiring to those APIs; the
current way this plugin is written allows it to optionally be available, if the Felix Web Console is installed on the OSGi
platform where this bundle will be deployed
*/
static final String REG_PROP_LABEL = "felix.webconsole.label";
static final String REG_PROP_TITLE = "felix.webconsole.title";
static final String REG_PROP_CATEGORY = "felix.webconsole.category";
static final String LABEL = "xssprotection";
static final String TITLE= "XSS Protection";
private static final String PLUGIN_ROOT_PATH = "/" + LABEL;
private static final String URI_CONFIG_XHR = PLUGIN_ROOT_PATH + "/config.xhr";
private static final String URI_BLOCKED_XHR = PLUGIN_ROOT_PATH + "/blocked.json";
private static final String URI_CONFIG_XML = PLUGIN_ROOT_PATH + "/config.xml";
private static final String INTERNAL_RESOURCES_FOLDER = "/webconsole";
private static final String RES_ROOT = PLUGIN_ROOT_PATH + INTERNAL_RESOURCES_FOLDER;
private static final String RES_URI_PRETTIFY_CSS = RES_ROOT + "/prettify.css";
private static final String RES_URI_PRETTIFY_JS = RES_ROOT + "/prettify.js";
private static final String RES_URI_XSS_CSS = RES_ROOT + "/xss.css";
private static final String RES_URI_XSS_JS = RES_ROOT + "/xss.js";
private static final String RES_URI_BLOCKED_JS = RES_ROOT + "/blocked.js";
private static final String RES_URI_CONFIG_JS = RES_ROOT + "/config.js";
public static final String SCRIPT_TAG = "<script src='%s'></script>\n";
public static final String LINK_TAG = "<link rel='stylesheet' type='text/css' href='%s'>";
@Reference(target = "(component.name=org.apache.sling.xss.impl.XSSFilterImpl)")
private XSSFilter xssFilter;
@Reference
private XSSStatusService statusService;
private static final Set<String> CSS_RESOURCES = new HashSet<>(Arrays.asList(RES_URI_PRETTIFY_CSS, RES_URI_XSS_CSS));
private static final Set<String> JS_RESOURCES = new HashSet<>(Arrays.asList(RES_URI_PRETTIFY_JS, RES_URI_XSS_JS, RES_URI_BLOCKED_JS,
RES_URI_CONFIG_JS));
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) {
String pluginResource = request.getPathInfo();
String consoleRoot = request.getRequestURI().substring(0, request.getRequestURI().indexOf(pluginResource));
if (CSS_RESOURCES.contains(pluginResource)) {
streamResource(response, FilenameUtils.getName(pluginResource), "text/css");
} else if (JS_RESOURCES.contains(pluginResource)) {
streamResource(response, FilenameUtils.getName(pluginResource), "application/javascript");
} else if (URI_CONFIG_XHR.equalsIgnoreCase(pluginResource) && xssFilter != null) {
writeAntiSamyConfiguration(consoleRoot, response);
} else if (URI_CONFIG_XML.equalsIgnoreCase(pluginResource) && xssFilter != null) {
streamAntiSamyConfiguration(response);
} else if (URI_BLOCKED_XHR.equalsIgnoreCase(pluginResource)) {
generateInvalidUrlsJSONReport(response);
} else {
try {
PrintWriter printWriter = response.getWriter();
printWriter.printf(LINK_TAG, consoleRoot + RES_URI_XSS_CSS);
printWriter.printf(SCRIPT_TAG, consoleRoot + RES_URI_XSS_JS);
printWriter.println("<div id='xss-tabs'>");
printWriter.println("<ul>");
printWriter.println("<li id='blocked-tab'><a href='#blocked'><span>Status</span></a></li>");
if (xssFilter != null) {
printWriter.println(
String.format("<li id='config-tab'><a href='%s'><span>Active Configuration</span></a></li>",
consoleRoot + URI_CONFIG_XHR));
}
printWriter.println("</ul>");
printWriter.println("<div id='blocked'>");
printWriter.println("<div class='table'>");
printWriter.println("<div class='ui-widget-header ui-corner-top buttonGroup'>Blocked URLs</div>");
printWriter.println("<table class='nicetable tablesorter' id='invalid-urls'>");
printWriter.println("<thead>");
printWriter.println("<tr>");
printWriter.println("<th class='header'>URL</th>");
printWriter.println("<th class='header'>Times Blocked</th>");
printWriter.println("</tr>");
printWriter.println("</thead>");
printWriter.println("<tbody id='invalid-urls-rows'>");
printWriter.println("</tbody>");
printWriter.println("</table>");
printWriter.println("</div></div></div>");
} catch (IOException e) {
LOGGER.error("Unable to generate scaffold for the webconsole plugin output.", e);
}
}
}
private void streamAntiSamyConfiguration(HttpServletResponse response) {
try {
response.setContentType("application/xml");
response.setHeader("Content-Disposition", "attachment; filename=config.xml");
XSSFilterImpl xssFilterImpl = (XSSFilterImpl) xssFilter;
IOUtils.copy(xssFilterImpl.getActivePolicy().read(), response.getOutputStream());
} catch (IOException e) {
LOGGER.error("Unable to stream AntiSamy configuration.", e);
}
}
private void generateInvalidUrlsJSONReport(HttpServletResponse response) {
JsonArrayBuilder hrefs = Json.createArrayBuilder();
for (Map.Entry<String, AtomicInteger> entry : statusService.getInvalidUrls().entrySet()) {
JsonObject href =
Json.createObjectBuilder().add("href", entry.getKey()).add("times", entry.getValue().intValue()).build();
hrefs.add(href);
}
try (JsonWriter writer = Json.createWriter(response.getWriter())) {
response.setContentType("application/json");
writer.writeObject(Json.createObjectBuilder().add("hrefs", hrefs.build()).build());
} catch (IOException e) {
LOGGER.error("Unable to write JSON report for invalid URLs.", e);
}
}
private void writeAntiSamyConfiguration(String consoleRoot, HttpServletResponse response) {
response.setContentType("text/html");
XSSFilterImpl xssFilterImpl = (XSSFilterImpl) xssFilter;
XSSFilterImpl.AntiSamyPolicy antiSamyPolicy = xssFilterImpl.getActivePolicy();
if (antiSamyPolicy != null) {
try {
PrintWriter printWriter = response.getWriter();
printWriter.printf(SCRIPT_TAG, consoleRoot + RES_URI_CONFIG_JS);
printWriter.write("<div id='config'>");
printWriter.printf(LINK_TAG, consoleRoot + RES_URI_PRETTIFY_CSS);
printWriter.printf(SCRIPT_TAG, consoleRoot + RES_URI_PRETTIFY_JS);
printWriter.write("<p class='statline ui-state-highlight'>The current AntiSamy configuration ");
if (antiSamyPolicy.isEmbedded()) {
printWriter.write("is the default one embedded in the org.apache.sling.xss bundle.");
} else {
printWriter.printf("is loaded from %s.", antiSamyPolicy.getPath());
}
printWriter.write("<button style='float:right' type='button' id='download-config'>Download</button></p>");
String contents = "";
try (InputStream configurationStream = antiSamyPolicy.read()) {
contents = IOUtils.toString(configurationStream, StandardCharsets.UTF_8);
}
printWriter.write("<pre class='prettyprint linenums'>");
printWriter.write(StringEscapeUtils.escapeHtml4(contents));
printWriter.write("</pre>");
printWriter.write("</div>");
} catch (IOException e) {
LOGGER.error("Unable to write the AntiSamy configuration tab.", e);
}
}
}
/**
* Streams a resource embedded in the bundle.
* @param response the response
* @param file the file name
* @param contentType the content type of the resource
*/
private void streamResource(HttpServletResponse response, String file, String contentType) {
try (InputStream cssStream =
getClass().getClassLoader().getResourceAsStream(INTERNAL_RESOURCES_FOLDER + "/" + file)) {
if (cssStream != null) {
response.setContentType(contentType);
IOUtils.copy(cssStream, response.getOutputStream());
}
} catch (IOException e) {
LOGGER.error(String.format("Unable to stream bundled resource %s.", file), e);
}
}
}