001/* 002 * This library is part of OpenCms - 003 * the Open Source Content Management System 004 * 005 * Copyright (c) Alkacon Software GmbH & Co. KG (http://www.alkacon.com) 006 * 007 * This library is free software; you can redistribute it and/or 008 * modify it under the terms of the GNU Lesser General Public 009 * License as published by the Free Software Foundation; either 010 * version 2.1 of the License, or (at your option) any later version. 011 * 012 * This library is distributed in the hope that it will be useful, 013 * but WITHOUT ANY WARRANTY; without even the implied warranty of 014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 015 * Lesser General Public License for more details. 016 * 017 * For further information about Alkacon Software GmbH & Co. KG, please see the 018 * company website: http://www.alkacon.com 019 * 020 * For further information about OpenCms, please see the 021 * project website: http://www.opencms.org 022 * 023 * You should have received a copy of the GNU Lesser General Public 024 * License along with this library; if not, write to the Free Software 025 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 026 */ 027 028package org.opencms.main; 029 030import org.opencms.ade.configuration.CmsADEConfigData; 031import org.opencms.file.CmsFile; 032import org.opencms.file.CmsObject; 033import org.opencms.file.CmsResource; 034import org.opencms.file.CmsResourceFilter; 035import org.opencms.i18n.CmsMessageContainer; 036import org.opencms.site.CmsSite; 037import org.opencms.staticexport.CmsStaticExportData; 038import org.opencms.staticexport.CmsStaticExportRequest; 039import org.opencms.util.CmsRequestUtil; 040import org.opencms.util.CmsStringUtil; 041import org.opencms.util.CmsThreadLocalStack; 042import org.opencms.util.CmsUUID; 043 044import java.io.IOException; 045import java.util.HashMap; 046import java.util.Map; 047import java.util.concurrent.ConcurrentHashMap; 048import java.util.function.Consumer; 049import java.util.function.Function; 050 051import javax.servlet.ServletConfig; 052import javax.servlet.ServletException; 053import javax.servlet.http.HttpServlet; 054import javax.servlet.http.HttpServletRequest; 055import javax.servlet.http.HttpServletResponse; 056 057import org.apache.commons.logging.Log; 058 059/** 060 * This the main servlet of the OpenCms system.<p> 061 * 062 * From here, all operations that are results of HTTP requests are invoked. 063 * Any incoming request is handled in multiple steps: 064 * 065 * <ol><li>The requesting <code>{@link org.opencms.file.CmsUser}</code> is authenticated 066 * and a <code>{@link org.opencms.file.CmsObject}</code> with this users context information 067 * is created. This <code>{@link org.opencms.file.CmsObject}</code> is used to access all functions of OpenCms, limited by 068 * the authenticated users permissions. If the user is not identified, it is set to the default user, usually named "Guest".</li> 069 * 070 * <li>The requested <code>{@link org.opencms.file.CmsResource}</code> is loaded into OpenCms and depending on its type 071 * (and the users persmissions to display or modify it), 072 * it is send to one of the OpenCms <code>{@link org.opencms.loader.I_CmsResourceLoader}</code> implementations 073 * do be processed.</li> 074 * 075 * <li> 076 * The <code>{@link org.opencms.loader.I_CmsResourceLoader}</code> will then decide what to do with the 077 * contents of the requested <code>{@link org.opencms.file.CmsResource}</code>. 078 * In case of a JSP resource the JSP handling mechanism is invoked with the <code>{@link org.opencms.loader.CmsJspLoader}</code>, 079 * in case of an image (or another static resource) this will be returned by the <code>{@link org.opencms.loader.CmsDumpLoader}</code> 080 * etc. 081 * </li></ol> 082 * 083 * @since 6.0.0 084 * 085 * @see org.opencms.main.CmsShell 086 * @see org.opencms.file.CmsObject 087 * @see org.opencms.main.OpenCms 088 */ 089public class OpenCmsServlet extends HttpServlet implements I_CmsRequestHandler { 090 091 /** 092 * Context class for storing request-dependent caches etc. 093 */ 094 public static class RequestCache { 095 096 /** Cache for sitemap configurations. */ 097 private Map<String, CmsADEConfigData> m_configCache = new HashMap<>(); 098 099 /** Map of attributes. */ 100 private Map<String, Object> m_attributes = new HashMap<>(); 101 102 /** Buffer for log messages to write at the end of the request. */ 103 private CmsDuplicateRemovingLogBuffer m_logBuffer = new CmsDuplicateRemovingLogBuffer(); 104 105 /** 106 * Adds a log message to the log buffer. 107 * 108 * @param channel the channel 109 * @param level the log level 110 * @param message the message 111 */ 112 public void addLog(String channel, String level, String message) { 113 114 m_logBuffer.add(channel, level, message); 115 } 116 117 /** 118 * Called at the end of the request. 119 */ 120 public void close() { 121 122 m_logBuffer.flush(); 123 } 124 125 /** 126 * Gets the attribute for the given key. 127 * 128 * @param key the key 129 * @return the attribute 130 */ 131 public Object getAttribute(String key) { 132 133 return m_attributes.get(key); 134 } 135 136 /** 137 * Gets the stored attribute for the given key, or lazily initializes it with the given provider function if it doesn't exist yet. 138 * 139 * @param key the key 140 * @param provider the function to lazily initialize the entry 141 * @return the attribute 142 */ 143 public Object getAttribute(String key, Function<String, Object> provider) { 144 145 return m_attributes.computeIfAbsent(key, provider); 146 } 147 148 /** 149 * Gets the cached sitemap configuration data. 150 * 151 * @param key the key 152 * @return the sitemap config from the cache 153 */ 154 public CmsADEConfigData getCachedConfig(String key) { 155 156 return m_configCache.get(key); 157 } 158 159 /** 160 * Sets the attribute for the given key. 161 * 162 * @param key the key 163 * @param value the attribute value 164 */ 165 public void setAttribute(String key, Object value) { 166 167 m_attributes.put(key, value); 168 } 169 170 /** 171 * Sets the cached sitemap configuration data. 172 * 173 * @param key the key 174 * @param config the sitemap configuration to cache 175 */ 176 public void setCachedConfig(String key, CmsADEConfigData config) { 177 178 m_configCache.put(key, config); 179 } 180 181 } 182 183 /** 184 * Debugging information about currently running requests. 185 */ 186 public static class RequestInfo { 187 188 /** Start time of the request. */ 189 private long m_startTime; 190 191 /** URI of the request. */ 192 private String m_uri; 193 194 /** Thread id of the thread handling the request. */ 195 private long m_threadId; 196 197 /** 198 * Creates a new instance. 199 * 200 * @param uri the URI 201 * @param startTime the start time 202 */ 203 public RequestInfo(String uri, long startTime) { 204 205 m_startTime = startTime; 206 m_uri = uri; 207 m_threadId = Thread.currentThread().getId(); 208 } 209 210 /** 211 * Gets the request start time. 212 * 213 * @return the start time 214 */ 215 public long getStartTime() { 216 217 return m_startTime; 218 } 219 220 /** 221 * Gets the thread id 222 * 223 * @return the thread id 224 */ 225 public long getThreadId() { 226 227 return m_threadId; 228 } 229 230 /** 231 * Gets the URI of the request. 232 * 233 * @return the URI of the request 234 */ 235 public String getUri() { 236 237 return m_uri; 238 } 239 } 240 241 /** The current request in a threadlocal. */ 242 public static final CmsThreadLocalStack<HttpServletRequest> currentRequestStack = new CmsThreadLocalStack<HttpServletRequest>(); 243 244 /** Map containing beans with information about currently running requests. */ 245 public static final ConcurrentHashMap<CmsUUID, RequestInfo> activeRequests = new ConcurrentHashMap<>(); 246 247 /** The current thread context for the request. */ 248 private static final CmsThreadLocalStack<RequestCache> requestCacheStack = new CmsThreadLocalStack<RequestCache>(); 249 250 /** GWT RPC services suffix. */ 251 public static final String HANDLE_GWT = ".gwt"; 252 253 /** Handler prefix. */ 254 public static final String HANDLE_PATH = "/handle"; 255 256 /** Name of the <code>DefaultWebApplication</code> parameter in the <code>web.xml</code> OpenCms servlet configuration. */ 257 public static final String SERVLET_PARAM_DEFAULT_WEB_APPLICATION = "DefaultWebApplication"; 258 259 /** Name of the <code>OpenCmsHome</code> parameter in the <code>web.xml</code> OpenCms servlet configuration. */ 260 public static final String SERVLET_PARAM_OPEN_CMS_HOME = "OpenCmsHome"; 261 262 /** Name of the <code>OpenCmsServlet</code> parameter in the <code>web.xml</code> OpenCms servlet configuration. */ 263 public static final String SERVLET_PARAM_OPEN_CMS_SERVLET = "OpenCmsServlet"; 264 265 /** Name of the <code>WebApplicationContext</code> parameter in the <code>web.xml</code> OpenCms servlet configuration. */ 266 public static final String SERVLET_PARAM_WEB_APPLICATION_CONTEXT = "WebApplicationContext"; 267 268 /** Path to handler "error page" files in the VFS. */ 269 private static final String HANDLE_VFS_PATH = "/system/handler" + HANDLE_PATH; 270 271 /** Handler "error page" file suffix. */ 272 private static final String HANDLE_VFS_SUFFIX = ".html"; 273 274 /** Handler implementation names. */ 275 private static final String[] HANDLER_NAMES = {"404"}; 276 277 /** The log object for this class. */ 278 private static final Log LOG = CmsLog.getLog(OpenCmsServlet.class); 279 280 /** Serial version UID required for safe serialization. */ 281 private static final long serialVersionUID = 4729951599966070050L; 282 283 /** URL prefix for the built-in service handler. */ 284 public static final String HANDLE_BUILTIN_SERVICE = "/handleBuiltinService/"; 285 286 /** 287 * Gets a thread-local, request-specific context object for requests made to the OpenCms servlet. 288 * 289 * @return the thread context 290 */ 291 public static RequestCache getRequestCache() { 292 293 RequestCache result = requestCacheStack.top(); 294 return result; 295 } 296 297 /** 298 * Helper method used to conveniently access the request cache, does nothing if the request cache isn't set. 299 * 300 * @param handler the handler to pass the request cache to 301 */ 302 public static void withRequestCache(Consumer<RequestCache> handler) { 303 304 RequestCache cache = getRequestCache(); 305 if (cache != null) { 306 handler.accept(cache); 307 } 308 } 309 310 /** 311 * OpenCms servlet main request handling method.<p> 312 * 313 * @see javax.servlet.http.HttpServlet#doGet(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse) 314 */ 315 @Override 316 public void doGet(HttpServletRequest req, HttpServletResponse res) throws IOException, ServletException { 317 318 // we are using stacks for these because the doGet method may be called reentrantly, e.g. when using servlet forwarding. 319 currentRequestStack.push(req); 320 requestCacheStack.push(new RequestCache()); 321 final CmsUUID requestId = new CmsUUID(); 322 RequestInfo reqInfo = new RequestInfo( 323 req.getRequestURL().toString() + (req.getQueryString() != null ? "?" + req.getQueryString() : ""), 324 System.currentTimeMillis()); 325 activeRequests.put(requestId, reqInfo); 326 try { 327 328 // check to OpenCms runlevel 329 int runlevel = OpenCmsCore.getInstance().getRunLevel(); 330 331 // write OpenCms server identification in the response header 332 res.setHeader(CmsRequestUtil.HEADER_SERVER, OpenCmsCore.getInstance().getSystemInfo().getVersion()); 333 334 if (runlevel != OpenCms.RUNLEVEL_4_SERVLET_ACCESS) { 335 // not the "normal" servlet runlevel 336 if (runlevel == OpenCms.RUNLEVEL_3_SHELL_ACCESS) { 337 // we have shell runlevel only, upgrade to servlet runlevel (required after setup wizard) 338 init(getServletConfig()); 339 } else { 340 // illegal runlevel, we can't process requests 341 // sending status code 403, indicating the server understood the request but refused to fulfill it 342 res.sendError(HttpServletResponse.SC_FORBIDDEN); 343 // goodbye 344 return; 345 } 346 } 347 348 String path = OpenCmsCore.getPathInfo(req); 349 if (path.startsWith(HANDLE_BUILTIN_SERVICE)) { 350 // built-in services are for small AJAX-related functionality in the core that doesn't need to be configurable 351 String remainder = path.substring(HANDLE_BUILTIN_SERVICE.length() - 1); // we want a leading slash in remainder 352 OpenCmsCore.getInstance().invokeBuiltinService(remainder, req, res); 353 } else if (path.startsWith(HANDLE_PATH)) { 354 // this is a request to an OpenCms handler URI 355 invokeHandler(req, res); 356 } else if (path.endsWith(HANDLE_GWT)) { 357 // handle GWT rpc services 358 String serviceName = CmsResource.getName(path); 359 serviceName = serviceName.substring(0, serviceName.length() - HANDLE_GWT.length()); 360 OpenCmsCore.getInstance().invokeGwtService(serviceName, req, res, getServletConfig()); 361 } else { 362 // standard request to a URI in the OpenCms VFS 363 OpenCmsCore.getInstance().showResource(req, res); 364 } 365 } finally { 366 currentRequestStack.pop(); 367 requestCacheStack.pop().close(); 368 activeRequests.remove(requestId); 369 } 370 } 371 372 /** 373 * OpenCms servlet POST request handling method, 374 * will just call {@link #doGet(HttpServletRequest, HttpServletResponse)}.<p> 375 * 376 * @see javax.servlet.http.HttpServlet#doPost(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse) 377 */ 378 @Override 379 public void doPost(HttpServletRequest req, HttpServletResponse res) throws IOException, ServletException { 380 381 doGet(req, res); 382 } 383 384 /** 385 * @see org.opencms.main.I_CmsRequestHandler#getHandlerNames() 386 */ 387 public String[] getHandlerNames() { 388 389 return HANDLER_NAMES; 390 } 391 392 /** 393 * @see org.opencms.main.I_CmsRequestHandler#handle(HttpServletRequest, HttpServletResponse, String) 394 */ 395 public void handle(HttpServletRequest req, HttpServletResponse res, String name) 396 throws IOException, ServletException { 397 398 int errorCode; 399 try { 400 errorCode = Integer.valueOf(name).intValue(); 401 } catch (NumberFormatException nf) { 402 res.sendError(HttpServletResponse.SC_FORBIDDEN); 403 LOG.debug("Error parsing handler name.", nf); 404 return; 405 } 406 switch (errorCode) { 407 case 404: 408 CmsObject cms = null; 409 CmsStaticExportData exportData = null; 410 try { 411 // this will be set in the root site 412 cms = OpenCms.initCmsObject(OpenCms.getDefaultUsers().getUserExport()); 413 exportData = OpenCms.getStaticExportManager().getExportData(req, cms); 414 } catch (CmsException e) { 415 // unlikely to happen 416 if (LOG.isWarnEnabled()) { 417 LOG.warn( 418 Messages.get().getBundle().key( 419 Messages.LOG_INIT_CMSOBJECT_IN_HANDLER_2, 420 name, 421 OpenCmsCore.getPathInfo(req)), 422 e); 423 } 424 } 425 if (exportData != null) { 426 try { 427 // generate a static export request wrapper 428 CmsStaticExportRequest exportReq = new CmsStaticExportRequest(req, exportData); 429 // export the resource and set the response status according to the result 430 res.setStatus(OpenCms.getStaticExportManager().export(exportReq, res, cms, exportData)); 431 } catch (Throwable t) { 432 if (LOG.isWarnEnabled()) { 433 LOG.warn(Messages.get().getBundle().key(Messages.LOG_ERROR_EXPORT_1, exportData), t); 434 } 435 openErrorHandler(req, res, errorCode); 436 } 437 } else { 438 openErrorHandler(req, res, errorCode); 439 } 440 break; 441 default: 442 openErrorHandler(req, res, errorCode); 443 } 444 } 445 446 /** 447 * @see javax.servlet.Servlet#init(javax.servlet.ServletConfig) 448 */ 449 @Override 450 public synchronized void init(ServletConfig config) throws ServletException { 451 452 super.init(config); 453 try { 454 // upgrade the runlevel 455 // usually this should have already been done by the context listener 456 // however, after a fresh install / setup this will be done from here 457 OpenCmsCore.getInstance().upgradeRunlevel(config.getServletContext()); 458 // finalize OpenCms initialization 459 OpenCmsCore.getInstance().initServlet(this); 460 } catch (CmsInitException e) { 461 if (Messages.ERR_CRITICAL_INIT_WIZARD_0.equals(e.getMessageContainer().getKey())) { 462 // if wizard is still enabled - allow retry of initialization (required for setup wizard) 463 // this means the servlet init() call must be terminated by an exception 464 if (CmsServletContainerSettings.isServletThrowsException()) { 465 throw new ServletException(e.getMessage()); 466 } else { 467 // this is needed since some servlet containers does not like the servlet to throw exceptions, 468 // like BEA WLS 9.x and Resin 469 LOG.error(Messages.get().getBundle().key(Messages.LOG_ERROR_GENERIC_0), e); 470 } 471 } 472 } catch (Throwable t) { 473 LOG.error(Messages.get().getBundle().key(Messages.LOG_ERROR_GENERIC_0), t); 474 } 475 } 476 477 /** 478 * Manages requests to internal OpenCms request handlers.<p> 479 * 480 * @param req the current request 481 * @param res the current response 482 * @throws ServletException in case an error occurs 483 * @throws ServletException in case an error occurs 484 * @throws IOException in case an error occurs 485 */ 486 protected void invokeHandler(HttpServletRequest req, HttpServletResponse res) throws IOException, ServletException { 487 488 String pathInfo = OpenCmsCore.getPathInfo(req); 489 String name = pathInfo.substring(HANDLE_PATH.length()); 490 I_CmsRequestHandler handler = OpenCmsCore.getInstance().getRequestHandler(name); 491 if ((handler == null) && name.contains("/")) { 492 // if the name contains a '/', also check for handlers matching the first path fragment only 493 name = name.substring(0, name.indexOf("/")); 494 handler = OpenCmsCore.getInstance().getRequestHandler(name); 495 } 496 if (handler != null) { 497 handler.handle(req, res, name); 498 } else { 499 LOG.warn("Invalid request handler call: " + pathInfo); 500 openErrorHandler(req, res, HttpServletResponse.SC_INTERNAL_SERVER_ERROR); 501 } 502 } 503 504 /** 505 * Displays an error code handler loaded from the OpenCms VFS, 506 * or if such a page does not exist, 507 * displays the default servlet container error code.<p> 508 * 509 * @param req the current request 510 * @param res the current response 511 * @param errorCode the error code to display 512 * @throws IOException if something goes wrong 513 * @throws ServletException if something goes wrong 514 */ 515 protected void openErrorHandler(HttpServletRequest req, HttpServletResponse res, int errorCode) 516 throws IOException, ServletException { 517 518 String handlerUri = (new StringBuffer(64)).append(HANDLE_VFS_PATH).append(errorCode).append( 519 HANDLE_VFS_SUFFIX).toString(); 520 // provide the original error code in a request attribute 521 req.setAttribute(CmsRequestUtil.ATTRIBUTE_ERRORCODE, Integer.valueOf(errorCode)); 522 CmsObject cms; 523 CmsFile file; 524 try { 525 // create OpenCms context, this will be set in the root site 526 cms = OpenCms.initCmsObject(OpenCms.getDefaultUsers().getUserGuest()); 527 cms.getRequestContext().setSecureRequest(OpenCms.getSiteManager().usesSecureSite(req)); 528 } catch (CmsException e) { 529 // unlikely to happen as the OpenCms "Guest" context can always be initialized 530 CmsMessageContainer container = Messages.get().container( 531 Messages.LOG_INIT_CMSOBJECT_IN_HANDLER_2, 532 Integer.valueOf(errorCode), 533 handlerUri); 534 if (LOG.isWarnEnabled()) { 535 LOG.warn(org.opencms.jsp.Messages.getLocalizedMessage(container, req), e); 536 } 537 // however, if it _does_ happen, then we really can't continue here 538 if (!res.isCommitted()) { 539 // since the handler file is not accessible, display the default error page 540 res.sendError(errorCode, e.getLocalizedMessage()); 541 } 542 return; 543 } 544 try { 545 if (!tryCustomErrorPage(cms, req, res, errorCode)) { 546 cms.getRequestContext().setUri(handlerUri); 547 cms.getRequestContext().setSecureRequest(OpenCms.getSiteManager().usesSecureSite(req)); 548 // read the error handler file 549 file = cms.readFile(handlerUri, CmsResourceFilter.IGNORE_EXPIRATION); 550 OpenCms.getResourceManager().loadResource(cms, file, req, res); 551 } 552 } catch (CmsException e) { 553 // unable to load error page handler VFS resource 554 CmsMessageContainer container = Messages.get().container( 555 Messages.ERR_SHOW_ERR_HANDLER_RESOURCE_2, 556 Integer.valueOf(errorCode), 557 handlerUri); 558 throw new ServletException(org.opencms.jsp.Messages.getLocalizedMessage(container, req), e); 559 } 560 } 561 562 /** 563 * Tries to load the custom error page at the given rootPath. 564 * @param cms {@link CmsObject} used for reading the resource (site root and uri get adjusted!) 565 * @param req the current request 566 * @param res the current response 567 * @param rootPath the VFS root path to the error page resource 568 * @return a flag, indicating if the error page could be loaded 569 */ 570 private boolean loadCustomErrorPage( 571 CmsObject cms, 572 HttpServletRequest req, 573 HttpServletResponse res, 574 String rootPath) { 575 576 try { 577 578 // get the site of the error page resource 579 CmsSite errorSite = OpenCms.getSiteManager().getSiteForRootPath(rootPath); 580 cms.getRequestContext().setSiteRoot(errorSite.getSiteRoot()); 581 String relPath = cms.getRequestContext().removeSiteRoot(rootPath); 582 if (cms.existsResource(relPath)) { 583 cms.getRequestContext().setUri(relPath); 584 OpenCms.getResourceManager().loadResource(cms, cms.readResource(relPath), req, res); 585 return true; 586 } else { 587 return false; 588 } 589 } catch (Throwable e) { 590 // something went wrong log the exception and return false 591 LOG.error(e.getMessage(), e); 592 return false; 593 } 594 } 595 596 /** 597 * Tries to load a site specific error page. If 598 * @param cms {@link CmsObject} used for reading the resource (site root and uri get adjusted!) 599 * @param req the current request 600 * @param res the current response 601 * @param errorCode the error code to display 602 * @return a flag, indicating if the custom error page could be loaded. 603 */ 604 private boolean tryCustomErrorPage(CmsObject cms, HttpServletRequest req, HttpServletResponse res, int errorCode) { 605 606 String siteRoot = OpenCms.getSiteManager().matchRequest(req).getSiteRoot(); 607 CmsSite site = OpenCms.getSiteManager().getSiteForSiteRoot(siteRoot); 608 if (site != null) { 609 // store current site root and URI 610 String currentSiteRoot = cms.getRequestContext().getSiteRoot(); 611 String currentUri = cms.getRequestContext().getUri(); 612 try { 613 if (site.getErrorPage() != null) { 614 String rootPath = site.getErrorPage(); 615 if (loadCustomErrorPage(cms, req, res, rootPath)) { 616 return true; 617 } 618 } 619 String rootPath = CmsStringUtil.joinPaths(siteRoot, "/.errorpages/handle" + errorCode + ".html"); 620 if (loadCustomErrorPage(cms, req, res, rootPath)) { 621 return true; 622 } 623 } finally { 624 cms.getRequestContext().setSiteRoot(currentSiteRoot); 625 cms.getRequestContext().setUri(currentUri); 626 } 627 } 628 return false; 629 } 630}