1 /* 2 * Copyright (C) 2004-2008 Jive Software. All rights reserved. 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package org.jivesoftware.openfire.container; 18 19 import java.io.BufferedInputStream; 20 import java.io.File; 21 import java.io.FileInputStream; 22 import java.io.IOException; 23 import java.io.InputStream; 24 import java.nio.file.Path; 25 import java.util.Collections; 26 import java.util.Enumeration; 27 import java.util.HashSet; 28 import java.util.List; 29 import java.util.Map; 30 import java.util.Set; 31 import java.util.concurrent.ConcurrentHashMap; 32 33 import org.dom4j.Document; 34 import org.jivesoftware.admin.FlashMessageTag; 35 import org.jivesoftware.admin.PluginFilter; 36 import org.jivesoftware.openfire.XMPPServer; 37 import org.jivesoftware.util.JiveGlobals; 38 import org.jivesoftware.util.LocaleUtils; 39 import org.jivesoftware.util.StringUtils; 40 import org.jivesoftware.util.SystemProperty; 41 import org.jivesoftware.util.WebXmlUtils; 42 import org.slf4j.Logger; 43 import org.slf4j.LoggerFactory; 44 45 import javax.servlet.Filter; 46 import javax.servlet.FilterConfig; 47 import javax.servlet.GenericServlet; 48 import javax.servlet.Servlet; 49 import javax.servlet.ServletConfig; 50 import javax.servlet.ServletContext; 51 import javax.servlet.ServletException; 52 import javax.servlet.ServletOutputStream; 53 import javax.servlet.http.HttpServlet; 54 import javax.servlet.http.HttpServletRequest; 55 import javax.servlet.http.HttpServletResponse; 56 57 /** 58 * The plugin servlet acts as a proxy for web requests (in the admin console) 59 * to plugins. Since plugins can be dynamically loaded and live in a different place 60 * than normal Openfire admin console files, it's not possible to have them 61 * added to the normal Openfire admin console web app directory. 62 * <p> 63 * The servlet listens for requests in the form {@code /plugins/[pluginName]/[JSP File]} 64 * (e.g. {@code /plugins/foo/example.jsp}). It also listens for non JSP requests in the 65 * form like {@code /plugins/[pluginName]/images/*.png|gif}, 66 * {@code /plugins/[pluginName]/scripts/*.js|css} or 67 * {@code /plugins/[pluginName]/styles/*.css} (e.g. 68 * {@code /plugins/foo/images/example.gif}). 69 * </p> 70 * JSP files must be compiled and available via the plugin's class loader. The mapping 71 * between JSP name and servlet class files is defined in [pluginName]/web/web.xml. 72 * Typically, this file is auto-generated by the JSP compiler when packaging the plugin. 73 * 74 * @author Matt Tucker 75 */ 76 public class PluginServlet extends HttpServlet { 77 78 private static final Logger Log = LoggerFactory.getLogger(PluginServlet.class); 79 private static final String CSRF_ATTRIBUTE = "csrf"; 80 81 public static final SystemProperty<Boolean> ALLOW_LOCAL_FILE_READING = SystemProperty.Builder.ofType( Boolean.class ) 82 .setKey( "plugins.servlet.allowLocalFileReading" ) 83 .setDynamic( true ) 84 .setDefaultValue( false ) 85 .build(); 86 87 private static final Map<String, GenericServlet> servlets; // mapped using lowercase path (OF-1105) 88 private static PluginManager pluginManager; 89 private static ServletConfig servletConfig; 90 91 static { 92 servlets = new ConcurrentHashMap<>(); 93 } 94 95 private static final String PLUGINS_WEBROOT = "/plugins/"; 96 97 @Override init(ServletConfig config)98 public void init(ServletConfig config) throws ServletException { 99 super.init(config); 100 servletConfig = config; 101 } 102 103 @Override service(HttpServletRequest request, HttpServletResponse response)104 public void service(HttpServletRequest request, HttpServletResponse response) { 105 String pathInfo = request.getPathInfo(); 106 if (pathInfo == null) { 107 response.setStatus(HttpServletResponse.SC_NOT_FOUND); 108 } 109 else { 110 try { 111 final PluginMetadata pluginMetadata = getPluginMetadataFromPath(pathInfo); 112 if (pluginMetadata.isCsrfProtectionEnabled()) { 113 if (!passesCsrf(request)) { 114 request.getSession().setAttribute(FlashMessageTag.ERROR_MESSAGE_KEY, LocaleUtils.getLocalizedString("global.csrf.failed")); 115 response.sendRedirect(request.getRequestURI()); 116 return; 117 } 118 } 119 // Handle JSP requests. 120 if (pathInfo.endsWith(".jsp")) { 121 setCSRF(request); 122 handleJSP(pathInfo, request, response); 123 } 124 // Handle servlet requests. 125 else if (getServlet(pathInfo) != null) { 126 setCSRF(request); 127 handleServlet(pathInfo, request, response); 128 } 129 // Handle image/other requests. 130 else { 131 handleOtherRequest(pathInfo, response); 132 } 133 } 134 catch (Exception e) { 135 Log.error(e.getMessage(), e); 136 response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); 137 } 138 } 139 } 140 setCSRF(final HttpServletRequest request)141 private static void setCSRF(final HttpServletRequest request) { 142 final String csrf = StringUtils.randomString(32); 143 request.getSession().setAttribute(CSRF_ATTRIBUTE, csrf); 144 request.setAttribute(CSRF_ATTRIBUTE, csrf); 145 } 146 passesCsrf(final HttpServletRequest request)147 private boolean passesCsrf(final HttpServletRequest request) { 148 if (request.getMethod().equals("GET")) { 149 // No CSRF's for GET requests 150 return true; 151 } 152 153 final String sessionCsrf = (String) request.getSession().getAttribute(CSRF_ATTRIBUTE); 154 return sessionCsrf != null && sessionCsrf.equals(request.getParameter(CSRF_ATTRIBUTE)); 155 } 156 getPluginMetadataFromPath(final String pathInfo)157 private PluginMetadata getPluginMetadataFromPath(final String pathInfo) { 158 final String pluginName = pathInfo.split("/")[1]; 159 return XMPPServer.getInstance().getPluginManager().getMetadata(pluginName); 160 } 161 162 /** 163 * Registers all JSP page servlets for a plugin. 164 * 165 * @param manager the plugin manager. 166 * @param plugin the plugin. 167 * @param webXML the web.xml file containing JSP page names to servlet class file 168 * mappings. 169 */ registerServlets( PluginManager manager, final Plugin plugin, File webXML)170 public static void registerServlets( PluginManager manager, final Plugin plugin, File webXML) 171 { 172 pluginManager = manager; 173 174 if ( !webXML.exists() ) 175 { 176 Log.error("Could not register plugin servlets, file " + webXML.getAbsolutePath() + " does not exist."); 177 return; 178 } 179 180 // Find the name of the plugin directory given that the webXML file lives in plugins/[pluginName]/web/web.xml 181 final String pluginName = webXML.getParentFile().getParentFile().getParentFile().getName(); 182 try 183 { 184 final Document webXmlDoc = WebXmlUtils.asDocument( webXML ); 185 186 final List<String> servletNames = WebXmlUtils.getServletNames( webXmlDoc ); 187 for ( final String servletName : servletNames ) 188 { 189 Log.debug( "Loading servlet '{}' of plugin '{}'...", servletName, pluginName ); 190 191 final String className = WebXmlUtils.getServletClassName( webXmlDoc, servletName ); 192 if ( className == null || className.isEmpty() ) 193 { 194 Log.warn( "Could not load servlet '{}' of plugin '{}'. web-xml does not define a class name for this servlet.", servletName, pluginName ); 195 continue; 196 } 197 198 try { 199 final Class<?> theClass = manager.loadClass(plugin, className); 200 201 final Object instance = theClass.newInstance(); 202 if (!(instance instanceof GenericServlet)) { 203 Log.warn("Could not load servlet '{}' of plugin '{}'. Its class ({}) is not an instance of javax.servlet.GenericServlet.", servletName, pluginName, className); 204 continue; 205 } 206 207 Log.debug("Initializing servlet '{}' of plugin '{}'...", servletName, pluginName); 208 ((GenericServlet) instance).init(new ServletConfig() { 209 @Override 210 public String getServletName() { 211 return servletName; 212 } 213 214 @Override 215 public ServletContext getServletContext() { 216 return new PluginServletContext( servletConfig.getServletContext(), pluginManager, plugin ); 217 } 218 219 @Override 220 public String getInitParameter( String s ) 221 { 222 final Map<String, String> params = WebXmlUtils.getServletInitParams( webXmlDoc, servletName ); 223 if (params.isEmpty()) { 224 return null; 225 } 226 return params.get( s ); 227 } 228 229 @Override 230 public Enumeration<String> getInitParameterNames() 231 { 232 final Map<String, String> params = WebXmlUtils.getServletInitParams( webXmlDoc, servletName ); 233 if (params.isEmpty()) { 234 return Collections.emptyEnumeration(); 235 } 236 return Collections.enumeration( params.keySet() ); 237 } 238 }); 239 240 Log.debug("Registering servlet '{}' of plugin '{}' URL patterns.", servletName, pluginName); 241 final Set<String> urlPatterns = WebXmlUtils.getServletUrlPatterns(webXmlDoc, servletName); 242 for (final String urlPattern : urlPatterns) { 243 final String path = (pluginName + urlPattern).toLowerCase(); 244 servlets.put(path, (GenericServlet) instance); 245 Log.debug("Servlet '{}' registered on path: {}", servletName, path); 246 } 247 Log.debug("Servlet '{}' of plugin '{}' loaded successfully.", servletName, pluginName); 248 } catch (final Exception e) { 249 Log.warn("Exception attempting to load servlet '{}' ({}) of plugin '{}'", servletName, className, pluginName, e); 250 } 251 } 252 253 254 final List<String> filterNames = WebXmlUtils.getFilterNames( webXmlDoc ); 255 for ( final String filterName : filterNames ) 256 { 257 Log.debug( "Loading filter '{}' of plugin '{}'...", filterName, pluginName ); 258 final String className = WebXmlUtils.getFilterClassName( webXmlDoc, filterName ); 259 if ( className == null || className.isEmpty() ) 260 { 261 Log.warn( "Could not load filter '{}' of plugin '{}'. web-xml does not define a class name for this filter.", filterName, pluginName ); 262 continue; 263 } 264 final Class<?> theClass = manager.loadClass( plugin, className ); 265 266 final Object instance = theClass.newInstance(); 267 if ( !(instance instanceof Filter) ) 268 { 269 Log.warn( "Could not load filter '{}' of plugin '{}'. Its class ({}) is not an instance of javax.servlet.Filter.", filterName, pluginName, className ); 270 continue; 271 } 272 273 Log.debug( "Initializing filter '{}' of plugin '{}'...", filterName, pluginName ); 274 ( (Filter) instance ).init( new FilterConfig() 275 { 276 @Override 277 public String getFilterName() 278 { 279 return filterName; 280 } 281 282 @Override 283 public ServletContext getServletContext() 284 { 285 return new PluginServletContext( servletConfig.getServletContext(), pluginManager, plugin ); 286 } 287 288 @Override 289 public String getInitParameter( String s ) 290 { 291 final Map<String, String> params = WebXmlUtils.getFilterInitParams( webXmlDoc, filterName ); 292 if (params.isEmpty()) { 293 return null; 294 } 295 return params.get( s ); 296 } 297 298 @Override 299 public Enumeration<String> getInitParameterNames() 300 { 301 final Map<String, String> params = WebXmlUtils.getFilterInitParams( webXmlDoc, filterName ); 302 if (params.isEmpty()) { 303 return Collections.emptyEnumeration(); 304 } 305 return Collections.enumeration( params.keySet() ); 306 } 307 } ); 308 309 Log.debug( "Registering filter '{}' of plugin '{}' URL patterns.", filterName, pluginName ); 310 final Set<String> urlPatterns = WebXmlUtils.getFilterUrlPatterns( webXmlDoc, filterName ); 311 for ( final String urlPattern : urlPatterns ) 312 { 313 PluginFilter.addPluginFilter( urlPattern, ( (Filter) instance ) ); 314 } 315 Log.debug( "Filter '{}' of plugin '{}' loaded successfully.", filterName, pluginName ); 316 } 317 } 318 catch (Throwable e) 319 { 320 Log.error( "An unexpected problem occurred while attempting to register servlets for plugin '{}'.", plugin, e); 321 } 322 } 323 324 /** 325 * Unregisters all JSP page servlets for a plugin. 326 * 327 * @param webXML the web.xml file containing JSP page names to servlet class file 328 * mappings. 329 */ unregisterServlets(File webXML)330 public static void unregisterServlets(File webXML) 331 { 332 if ( !webXML.exists() ) 333 { 334 Log.error("Could not unregister plugin servlets, file " + webXML.getAbsolutePath() + " does not exist."); 335 return; 336 } 337 338 // Find the name of the plugin directory given that the webXML file lives in plugins/[pluginName]/web/web.xml 339 final String pluginName = webXML.getParentFile().getParentFile().getParentFile().getName(); 340 try 341 { 342 final Document webXmlDoc = WebXmlUtils.asDocument( webXML ); 343 344 // Un-register and destroy all servlets. 345 final List<String> servletNames = WebXmlUtils.getServletNames( webXmlDoc ); 346 for ( final String servletName : servletNames ) 347 { 348 Log.debug( "Unregistering servlet '{}' of plugin '{}'", servletName, pluginName ); 349 final Set<Servlet> toDestroy = new HashSet<>(); 350 final Set<String> urlPatterns = WebXmlUtils.getServletUrlPatterns( webXmlDoc, servletName ); 351 for ( final String urlPattern : urlPatterns ) 352 { 353 final GenericServlet servlet = servlets.remove( ( pluginName + urlPattern ).toLowerCase() ); 354 if (servlet != null) 355 { 356 toDestroy.add( servlet ); 357 } 358 } 359 360 for ( final Servlet servlet : toDestroy ) 361 { 362 servlet.destroy(); 363 } 364 365 Log.debug( "Servlet '{}' of plugin '{}' unregistered and destroyed successfully.", servletName, pluginName ); 366 367 } 368 369 // Un-register and destroy all servlet filters. 370 final List<String> filterNames = WebXmlUtils.getFilterNames( webXmlDoc ); 371 for ( final String filterName : filterNames ) 372 { 373 Log.debug( "Unregistering filter '{}' of plugin '{}'", filterName, pluginName ); 374 final Set<Filter> toDestroy = new HashSet<>(); 375 final String className = WebXmlUtils.getFilterClassName( webXmlDoc, filterName ); 376 final Set<String> urlPatterns = WebXmlUtils.getFilterUrlPatterns( webXmlDoc, filterName ); 377 for ( final String urlPattern : urlPatterns ) 378 { 379 final Filter filter = PluginFilter.removePluginFilter( urlPattern, className ); 380 if (filter != null) 381 { 382 toDestroy.add( filter ); 383 } 384 } 385 386 for ( final Filter filter : toDestroy ) 387 { 388 filter.destroy(); 389 } 390 391 Log.debug( "Filter '{}' of plugin '{}' unregistered and destroyesd successfully.", filterName, pluginName ); 392 } 393 } 394 catch (Throwable e) { 395 Log.error( "An unexpected problem occurred while attempting to unregister servlets.", e); 396 } 397 } 398 399 /** 400 * Registers a live servlet for a plugin programmatically, does not 401 * initialize the servlet. 402 * 403 * @param pluginManager the plugin manager 404 * @param plugin the owner of the servlet 405 * @param servlet the servlet. 406 * @param relativeUrl the relative url where the servlet should be bound 407 * @return the effective url that can be used to initialize the servlet 408 * @throws ServletException if the servlet is null 409 */ registerServlet(PluginManager pluginManager, Plugin plugin, GenericServlet servlet, String relativeUrl)410 public static String registerServlet(PluginManager pluginManager, 411 Plugin plugin, GenericServlet servlet, String relativeUrl) 412 throws ServletException { 413 414 String pluginName = pluginManager.getPluginPath(plugin).getFileName().toString(); 415 PluginServlet.pluginManager = pluginManager; 416 if (servlet == null) { 417 throw new ServletException("Servlet is missing"); 418 } 419 String pluginServletUrl = pluginName + relativeUrl; 420 servlets.put((pluginName + relativeUrl).toLowerCase(), servlet); 421 return PLUGINS_WEBROOT + pluginServletUrl; 422 423 } 424 425 /** 426 * Unregister a live servlet for a plugin programmatically. Does not call 427 * the servlet destroy method. 428 * 429 * @param plugin the owner of the servlet 430 * @param url the relative url where servlet has been bound 431 * @return the unregistered servlet, so that it can be destroyed 432 * @throws ServletException if the URL is missing 433 */ unregisterServlet(Plugin plugin, String url)434 public static GenericServlet unregisterServlet(Plugin plugin, String url) 435 throws ServletException { 436 String pluginName = pluginManager.getPluginPath(plugin).getFileName().toString(); 437 if (url == null) { 438 throw new ServletException("Servlet URL is missing"); 439 } 440 String fullUrl = pluginName + url; 441 return servlets.remove(fullUrl.toLowerCase()); 442 } 443 444 /** 445 * Handles a request for a JSP page. It checks to see if a servlet is mapped 446 * for the JSP URL. If one is found, request handling is passed to it. If no 447 * servlet is found, a 404 error is returned. 448 * 449 * @param pathInfo the extra path info. 450 * @param request the request object. 451 * @param response the response object. 452 * @throws ServletException if a servlet exception occurs while handling the request. 453 * @throws IOException if an IOException occurs while handling the request. 454 */ handleJSP(String pathInfo, HttpServletRequest request, HttpServletResponse response)455 private void handleJSP(String pathInfo, HttpServletRequest request, 456 HttpServletResponse response) throws ServletException, IOException { 457 // Strip the starting "/" from the path to find the JSP URL. 458 String jspURL = pathInfo.substring(1); 459 GenericServlet servlet = servlets.get(jspURL.toLowerCase()); 460 if (servlet != null) { 461 Log.trace("Handling JSP at: {}", jspURL); 462 servlet.service(request, response); 463 } 464 else { 465 Log.trace("Unable to handle JSP. No registration for: {}", jspURL); 466 response.setStatus(HttpServletResponse.SC_NOT_FOUND); 467 } 468 } 469 470 /** 471 * Handles a request for a Servlet. If one is found, request handling is passed to it. 472 * If no servlet is found, a 404 error is returned. 473 * 474 * @param pathInfo the extra path info. 475 * @param request the request object. 476 * @param response the response object. 477 * @throws ServletException if a servlet exception occurs while handling the request. 478 * @throws IOException if an IOException occurs while handling the request. 479 */ handleServlet(String pathInfo, HttpServletRequest request, HttpServletResponse response)480 private void handleServlet(String pathInfo, HttpServletRequest request, 481 HttpServletResponse response) throws ServletException, IOException { 482 // Strip the starting "/" from the path to find the JSP URL. 483 GenericServlet servlet = getServlet(pathInfo); 484 if (servlet != null) { 485 Log.trace("Handling servlet at: {}", pathInfo); 486 servlet.service(request, response); 487 } 488 else { 489 Log.trace("Unable to handle servlet. No registration for: {}", pathInfo); 490 response.setStatus(HttpServletResponse.SC_NOT_FOUND); 491 } 492 } 493 494 /** 495 * Returns the correct servlet with mapping checks. 496 * 497 * @param pathInfo the pathinfo to map to the servlet. 498 * @return the mapped servlet, or null if no servlet was found. 499 */ getServlet(String pathInfo)500 private GenericServlet getServlet(String pathInfo) { 501 pathInfo = pathInfo.substring(1).toLowerCase(); 502 503 GenericServlet servlet = servlets.get(pathInfo); 504 if (servlet == null) { 505 for (String key : servlets.keySet()) { 506 int index = key.indexOf("/*"); 507 String searchkey = key; 508 if (index != -1) { 509 searchkey = key.substring(0, index); 510 } 511 if (searchkey.startsWith(pathInfo) || pathInfo.startsWith(searchkey)) { 512 servlet = servlets.get(key); 513 break; 514 } 515 } 516 } 517 Log.trace("Found servlet {} for path {}", servlet != null ? servlet.getServletName() : "(none)", pathInfo); 518 return servlet; 519 } 520 521 522 /** 523 * Handles a request for other web items (images, etc.) 524 * 525 * @param pathInfo the extra path info. 526 * @param response the response object. 527 * @throws IOException if an IOException occurs while handling the request. 528 */ handleOtherRequest(String pathInfo, HttpServletResponse response)529 private void handleOtherRequest(String pathInfo, HttpServletResponse response) throws IOException { 530 String[] parts = pathInfo.split("/"); 531 // Image request must be in correct format. 532 if (parts.length < 3) { 533 Log.trace("Unable to handle 'other' request (not enough path parts): {}", pathInfo); 534 response.setStatus(HttpServletResponse.SC_NOT_FOUND); 535 return; 536 } 537 538 String contextPath = ""; 539 int index = pathInfo.indexOf(parts[1]); 540 if (index != -1) { 541 contextPath = pathInfo.substring(index + parts[1].length()); 542 } 543 544 File pluginDirectory = new File(JiveGlobals.getHomeDirectory(), "plugins"); 545 File file = new File(pluginDirectory, parts[1] + File.separator + "web" + contextPath); 546 547 if ( !ALLOW_LOCAL_FILE_READING.getValue() ) { 548 // Ensure that the file that's being served is a file that is part of Openfire. This guards against 549 // accessing files from the operating system, or other files that shouldn't be accessible via the web (OF-1886). 550 final Path absoluteHome = new File( JiveGlobals.getHomeDirectory() ).toPath().normalize().toAbsolutePath(); 551 final Path absoluteLookup = file.toPath().normalize().toAbsolutePath(); 552 if ( !absoluteLookup.startsWith( absoluteHome ) ) 553 { 554 Log.trace("Unable to handle 'other' request (forbidden path): {}", pathInfo); 555 response.setStatus( HttpServletResponse.SC_FORBIDDEN ); 556 return; 557 } 558 } 559 560 if (!file.exists()) { 561 Log.trace("Unable to handle 'other' request (not found): {}", pathInfo); 562 response.setStatus(HttpServletResponse.SC_NOT_FOUND); 563 return; 564 } 565 566 String contentType = getServletContext().getMimeType(pathInfo); 567 if (contentType == null) { 568 contentType = "text/plain"; 569 } 570 response.setContentType(contentType); 571 // Write out the resource to the user. 572 Log.trace("Handling 'other' request with a response content type of {}: {}", contentType, pathInfo); 573 try (InputStream in = new BufferedInputStream(new FileInputStream(file))) { 574 try (ServletOutputStream out = response.getOutputStream()) { 575 576 // Set the size of the file. 577 response.setContentLength((int) file.length()); 578 579 // Use a 1K buffer. 580 byte[] buf = new byte[1024]; 581 int len; 582 while ((len = in.read(buf)) != -1) { 583 out.write(buf, 0, len); 584 } 585 } 586 } 587 } 588 } 589