001/* 002 * Copyright 2012 Anyware Services 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016package org.ametys.runtime.servlet; 017 018import java.io.File; 019import java.io.FileInputStream; 020import java.io.FileOutputStream; 021import java.io.IOException; 022import java.io.InputStream; 023import java.io.OutputStream; 024import java.net.URL; 025import java.nio.file.Files; 026import java.nio.file.Path; 027import java.time.ZonedDateTime; 028import java.util.Arrays; 029import java.util.Collection; 030import java.util.Enumeration; 031import java.util.Map; 032import java.util.Properties; 033import java.util.UUID; 034import java.util.regex.Pattern; 035 036import javax.servlet.ServletConfig; 037import javax.servlet.ServletContext; 038import javax.servlet.ServletException; 039import javax.servlet.ServletOutputStream; 040import javax.servlet.http.HttpServlet; 041import javax.servlet.http.HttpServletRequest; 042import javax.servlet.http.HttpServletResponse; 043import javax.servlet.http.HttpSession; 044import javax.xml.XMLConstants; 045import javax.xml.parsers.SAXParserFactory; 046import javax.xml.transform.OutputKeys; 047import javax.xml.transform.TransformerFactory; 048import javax.xml.transform.sax.SAXTransformerFactory; 049import javax.xml.transform.sax.TransformerHandler; 050import javax.xml.transform.stream.StreamResult; 051import javax.xml.transform.stream.StreamSource; 052import javax.xml.validation.Schema; 053import javax.xml.validation.SchemaFactory; 054 055import org.apache.avalon.excalibur.logger.LoggerManager; 056import org.apache.avalon.framework.configuration.Configuration; 057import org.apache.avalon.framework.configuration.ConfigurationException; 058import org.apache.avalon.framework.configuration.DefaultConfigurationBuilder; 059import org.apache.avalon.framework.container.ContainerUtil; 060import org.apache.avalon.framework.context.DefaultContext; 061import org.apache.cocoon.Cocoon; 062import org.apache.cocoon.ConnectionResetException; 063import org.apache.cocoon.Constants; 064import org.apache.cocoon.ResourceNotFoundException; 065import org.apache.cocoon.environment.http.HttpContext; 066import org.apache.cocoon.environment.http.HttpEnvironment; 067import org.apache.cocoon.servlet.multipart.MultipartHttpServletRequest; 068import org.apache.cocoon.util.ClassUtils; 069import org.apache.cocoon.util.log.SLF4JLoggerAdapter; 070import org.apache.cocoon.util.log.SLF4JLoggerManager; 071import org.apache.cocoon.xml.XMLUtils; 072import org.apache.commons.collections.EnumerationUtils; 073import org.apache.commons.io.FileUtils; 074import org.apache.commons.io.IOUtils; 075import org.apache.commons.lang3.StringUtils; 076import org.apache.commons.lang3.exception.ExceptionUtils; 077import org.apache.commons.lang3.time.StopWatch; 078import org.apache.log4j.Appender; 079import org.apache.log4j.LogManager; 080import org.apache.log4j.xml.DOMConfigurator; 081import org.apache.xml.serializer.OutputPropertiesFactory; 082import org.slf4j.Logger; 083import org.slf4j.LoggerFactory; 084import org.slf4j.MDC; 085import org.xml.sax.SAXException; 086import org.xml.sax.XMLReader; 087 088import org.ametys.core.ObservationConstants; 089import org.ametys.core.authentication.AuthenticateAction; 090import org.ametys.core.migration.MigrationEngine; 091import org.ametys.core.observation.Event; 092import org.ametys.core.observation.ObservationManager; 093import org.ametys.core.user.CurrentUserProvider; 094import org.ametys.core.user.UserIdentity; 095import org.ametys.core.util.DateUtils; 096import org.ametys.runtime.config.Config; 097import org.ametys.runtime.config.ConfigManager; 098import org.ametys.runtime.data.AmetysHomeLock; 099import org.ametys.runtime.data.AmetysHomeLockException; 100import org.ametys.runtime.log.MemoryAppender; 101import org.ametys.runtime.plugin.Init; 102import org.ametys.runtime.plugin.InitExtensionPoint; 103import org.ametys.runtime.plugin.PluginsManager; 104import org.ametys.runtime.plugin.PluginsManager.Status; 105import org.ametys.runtime.plugin.component.PluginsComponentManager; 106import org.ametys.runtime.plugins.admin.jvmstatus.ActiveSessionListener; 107import org.ametys.runtime.request.RequestFactory; 108import org.ametys.runtime.request.RequestListener; 109import org.ametys.runtime.request.RequestListenerManager; 110import org.ametys.runtime.util.AmetysHomeHelper; 111 112/** 113 * Main entry point for applications.<br> 114 * Overrides the CocoonServlet to add some initialization.<br> 115 */ 116public class RuntimeServlet extends HttpServlet 117{ 118 /** Constant for storing the {@link ServletConfig} in the Avalon context */ 119 public static final String CONTEXT_SERVLET_CONFIG = "servlet-config"; 120 121 /** Constant for storing the servlet context URL in the Avalon context */ 122 public static final String CONTEXT_CONTEXT_ROOT = "context-root"; 123 124 /** The cocoon.xconf URL */ 125 public static final String COCOON_CONF_URL = "/org/ametys/runtime/cocoon/cocoon.xconf"; 126 127 /** Default max upload size (10 Mb) */ 128 public static final int DEFAULT_MAX_UPLOAD_SIZE = 10 * 1024 * 1024; 129 130 /** The maintenance file name */ 131 public static final String MAINTENANCE_FILENAME = "maintenance.xml"; 132 133 /** The config file name */ 134 public static final String CONFIG_FILE_NAME = "config.xml"; 135 136 /** Name of the servlet initialization parameter for the ametys home property */ 137 public static final String AMETYS_HOME_PROPERTY = "ametys.home.property"; 138 139 /** The default ametys home path (relative to the servlet context) */ 140 public static final String AMETYS_HOME_DEFAULT = "/WEB-INF/data"; 141 142 /** The optional location (relative to application context) of the file that configures the list of plugins/workspaces/kernel to get somewhere else */ 143 public static final String EXTERNAL_LOCATIONS = "WEB-INF/param/external-locations.xml"; 144 145 /** File name in the administrator folder of the ametys home */ 146 public static final String INSTANCE_FILENAME = "instance-info.xml"; 147 148 /** The run modes */ 149 public enum RunMode 150 { 151 /** Application is currently starting */ 152 STARTING, 153 /** Application is currently migrating */ 154 MIGRATING, 155 /** Running init methods */ 156 INITIALIZING, 157 /** Normal execution mode */ 158 NORMAL, 159 /** Maintenance mode (See #getMaintenanceStatus) */ 160 MAINTENANCE, 161 /** Safe mode (See PluginsManager#getStatus) */ 162 SAFEMODE, 163 /** Fatal error during startup */ 164 FATALERROR, 165 /** Application is currently stopping */ 166 STOPPING 167 } 168 169 /** The maintenance status of Ametys */ 170 public enum MaintenanceStatus 171 { 172 /** No maintenance activated */ 173 NONE, 174 /** The maintenance was automatically switched on at startup due to a failed migration */ 175 AUTOMATIC, 176 /** The maintenance was manually switched on */ 177 FORCED 178 } 179 /** Informations about the maintenance status when forced 180 * @param comment The comment to explain why the maintenance was started 181 * @param initiator The user that started the maintenance 182 * @param since The date the maintenance was stated */ 183 public record ForcedMainteanceInformations(String comment, UserIdentity initiator, ZonedDateTime since) { /* empty */ } 184 185 private static RunMode _runMode = RunMode.STARTING; 186 private static MaintenanceStatus _maintenanceStatus; 187 private static ForcedMainteanceInformations _forcedMainteanceInformations; 188 private static Logger _logger; 189 private static ServletContext _servletContext; 190 191 private String _servletContextPath; 192 private URL _servletContextURL; 193 private File _ametysHome; 194 195 private DefaultContext _avalonContext; 196 private HttpContext _context; 197 private Cocoon _cocoon; 198 private RequestFactory _requestFactory; 199 private File _workDir; 200 201 private int _maxUploadSize = DEFAULT_MAX_UPLOAD_SIZE; 202 private File _uploadDir; 203 private File _cacheDir; 204 205 private AmetysHomeLock _ametysHomeLock; 206 207 private LoggerManager _loggerManager; 208 209 private Throwable _exception; 210 211 private Collection<Pattern> _allowedURLPattern = Arrays.asList(Pattern.compile("_admin($|/.*)"), Pattern.compile("plugins/[^/]+/resources/.*")); 212 213 @Override 214 public void init() throws ServletException 215 { 216 try 217 { 218 // Set this property in order to avoid a System.err.println (CatalogManager.java) 219 if (System.getProperty("xml.catalog.ignoreMissing") == null) 220 { 221 System.setProperty("xml.catalog.ignoreMissing", "true"); 222 } 223 224 _servletContext = getServletContext(); 225 _servletContext.addListener(ActiveSessionListener.class); 226 _servletContextPath = _servletContext.getRealPath("/"); 227 228 _avalonContext = new DefaultContext(); 229 _context = new HttpContext(_servletContext); 230 _avalonContext.put(Constants.CONTEXT_ENVIRONMENT_CONTEXT, _context); 231 _avalonContext.put(Constants.CONTEXT_DEFAULT_ENCODING, "UTF-8"); 232 _avalonContext.put(CONTEXT_SERVLET_CONFIG, getServletConfig()); 233 234 _servletContextURL = new File(_servletContextPath).toURI().toURL(); 235 _avalonContext.put(CONTEXT_CONTEXT_ROOT, _servletContextURL); 236 237 URL configFile = getClass().getResource(COCOON_CONF_URL); 238 _avalonContext.put(Constants.CONTEXT_CONFIG_URL, configFile); 239 240 _workDir = new File((File) _servletContext.getAttribute("javax.servlet.context.tempdir"), "cocoon-files"); 241 _workDir.mkdirs(); 242 _avalonContext.put(Constants.CONTEXT_WORK_DIR, _workDir); 243 244 _cacheDir = new File(_workDir, "cache-dir"); 245 _cacheDir.mkdirs(); 246 _avalonContext.put(Constants.CONTEXT_CACHE_DIR, _cacheDir); 247 248 // Create temp dir if it does not exist 249 File tmpDir = new File(System.getProperty("java.io.tmpdir")); 250 if (!tmpDir.exists()) 251 { 252 FileUtils.forceMkdir(tmpDir); 253 } 254 255 // Init the Ametys home directory and lock before setting the logger. 256 // The log folder is located in the Ametys home directory. 257 _initAmetysHome(); 258 259 // Configuration file 260 File config = FileUtils.getFile(_ametysHome, AmetysHomeHelper.AMETYS_HOME_CONFIG_DIR, CONFIG_FILE_NAME); 261 final String fileName = config.getCanonicalPath(); 262 Config.setFilename(fileName); 263 264 // Init logger 265 _initLogger(); 266 267 _initAmetys(); 268 } 269 catch (Throwable t) 270 { 271 setRunMode(RunMode.FATALERROR); 272 273 if (_logger != null) 274 { 275 _logger.error("Error while loading Ametys. Entering in error mode.", t); 276 } 277 else 278 { 279 System.out.println("Error while loading Ametys. Entering in error mode."); 280 t.printStackTrace(); 281 } 282 283 _exception = t; 284 285 _disposeCocoon(); 286 } 287 } 288 289 private void _initAmetysHome() throws AmetysHomeLockException 290 { 291 String ametysHomePath = null; 292 293 boolean hasAmetysHomeProp = EnumerationUtils.toList(getInitParameterNames()).contains(AMETYS_HOME_PROPERTY); 294 if (!hasAmetysHomeProp) 295 { 296 System.out.println(String.format("[INFO] The '%s' servlet initialization parameter was not found. The Ametys home directory default value will be used '%s'", 297 AMETYS_HOME_PROPERTY, AMETYS_HOME_DEFAULT)); 298 } 299 else 300 { 301 String ametysHomeEnv = getInitParameter(AMETYS_HOME_PROPERTY); 302 if (StringUtils.isEmpty(ametysHomeEnv)) 303 { 304 System.out.println(String.format("[WARN] The '%s' servlet initialization parameter appears to be empty. Ametys home directory default value will be used '%s'", 305 AMETYS_HOME_PROPERTY, AMETYS_HOME_DEFAULT)); 306 } 307 else 308 { 309 ametysHomePath = System.getenv(ametysHomeEnv); 310 if (StringUtils.isEmpty(ametysHomePath)) 311 { 312 System.out.println(String.format( 313 "[WARN] The '%s' environment variable was not found or was empty. Ametys home directory default value will be used '%s'", 314 ametysHomeEnv, AMETYS_HOME_DEFAULT)); 315 } 316 } 317 } 318 319 if (StringUtils.isEmpty(ametysHomePath)) 320 { 321 ametysHomePath = _servletContext.getRealPath(AMETYS_HOME_DEFAULT); 322 } 323 324 System.out.println("Acquiring lock on " + ametysHomePath); 325 326 // Acquire the lock on Ametys home 327 _ametysHome = new File(ametysHomePath); 328 _ametysHome.mkdirs(); 329 330 _ametysHomeLock = new AmetysHomeLock(_ametysHome); 331 _ametysHomeLock.acquire(); 332 } 333 334 /** 335 * Get the instance identifier 336 * @return The UUID of the Ametys instance or null if the id could not be generated at startup 337 * @throws IllegalStateException If the file cannot be read 338 */ 339 public static String getInstanceId() 340 { 341 Path statisticsFile = AmetysHomeHelper.getAmetysHome().toPath().resolve(INSTANCE_FILENAME); 342 343 if (Files.exists(statisticsFile)) 344 { 345 try (InputStream is = Files.newInputStream(statisticsFile)) 346 { 347 Configuration configuration = new DefaultConfigurationBuilder().build(is); 348 return configuration.getChild("unique-id").getValue(); 349 } 350 catch (Exception e) 351 { 352 throw new IllegalStateException("Cannot read the ametys-home://" + INSTANCE_FILENAME + " file. Consider remove it and restart Ametys to recreate it", e); 353 } 354 } 355 356 throw new IllegalStateException("The file ametys-home://" + INSTANCE_FILENAME + " does not exist while it should have created at startup"); 357 } 358 359 private static void _createInstanceId() 360 { 361 try 362 { 363 Path statisticsFile = AmetysHomeHelper.getAmetysHome().toPath().resolve(INSTANCE_FILENAME); 364 365 if (Files.exists(statisticsFile)) 366 { 367 return; 368 } 369 370 String newId = UUID.randomUUID().toString(); 371 372 try (OutputStream os = Files.newOutputStream(statisticsFile)) 373 { 374 // create a transformer for saving sax into a file 375 TransformerHandler th = ((SAXTransformerFactory) TransformerFactory.newInstance()).newTransformerHandler(); 376 377 StreamResult result = new StreamResult(os); 378 th.setResult(result); 379 380 // create the format of result 381 Properties format = new Properties(); 382 format.put(OutputKeys.METHOD, "xml"); 383 format.put(OutputKeys.INDENT, "yes"); 384 format.put(OutputKeys.ENCODING, "UTF-8"); 385 format.put(OutputPropertiesFactory.S_KEY_INDENT_AMOUNT, "2"); 386 th.getTransformer().setOutputProperties(format); 387 388 th.startDocument(); 389 XMLUtils.startElement(th, "instance"); 390 XMLUtils.createElement(th, "unique-id", newId); 391 XMLUtils.endElement(th, "instance"); 392 th.endDocument(); 393 } 394 } 395 catch (Exception e) 396 { 397 _logger.error("Cannot create the instance id", e); 398 } 399 } 400 401 private void _initAmetys() throws Exception 402 { 403 setRunMode(RunMode.STARTING); 404 _maintenanceStatus = null; 405 _exception = null; 406 407 // WEB-INF/param/runtime.xml loading 408 _loadRuntimeConfig(); 409 410 _createCocoon(); 411 412 // Upload initialization 413 _maxUploadSize = DEFAULT_MAX_UPLOAD_SIZE; 414 _uploadDir = new File(RuntimeConfig.getInstance().getAmetysHome(), "uploads"); 415 416 if (ConfigManager.getInstance().isComplete()) 417 { 418 Long maxUploadSizeParam = Config.getInstance().getValue("runtime.upload.max-size"); 419 if (maxUploadSizeParam != null) 420 { 421 // if the feature core/runtime.upload is deactivated, use the default value (10 Mb) 422 _maxUploadSize = maxUploadSizeParam.intValue(); 423 } 424 } 425 426 _uploadDir.mkdirs(); 427 _avalonContext.put(Constants.CONTEXT_UPLOAD_DIR, _uploadDir); 428 429 _requestFactory = new RequestFactory(true, _uploadDir, false, true, _maxUploadSize, "UTF-8"); 430 431 // Generate the instance-info file 432 _createInstanceId(); 433 434 PluginsComponentManager pluginCM = (PluginsComponentManager) _servletContext.getAttribute("PluginsComponentManager"); 435 if (doMigrationAndInit(pluginCM)) 436 { 437 restartCocoon(null); 438 } 439 else 440 { 441 if (PluginsManager.getInstance().isSafeMode()) 442 { 443 setRunMode(RunMode.SAFEMODE); 444 } 445 else if (getMaintenanceStatus() != MaintenanceStatus.NONE) 446 { 447 setRunMode(RunMode.MAINTENANCE); 448 } 449 else 450 { 451 setRunMode(RunMode.NORMAL); 452 } 453 } 454 } 455 456 private void _initLogger() 457 { 458 // Configure Log4j 459 String logj4fFile = _servletContext.getRealPath("/WEB-INF/log4j.xml"); 460 461 // Hack to have context-relative log files, because of lack in configuration capabilities in log4j. 462 // If there are more than one Ametys in the same JVM, the property will be successively set for each instance, 463 // so we heavily rely on DOMConfigurator beeing synchronous. 464 System.setProperty("ametys.home.dir", _ametysHome.getAbsolutePath()); 465 DOMConfigurator.configure(logj4fFile); 466 System.clearProperty("ametys.home.dir"); 467 468 Appender appender = new MemoryAppender(); 469 appender.setName(org.ametys.plugins.core.ui.log.LogManager.MEMORY_APPENDER_NAME); 470 LogManager.getRootLogger().addAppender(appender); 471 Enumeration<org.apache.log4j.Logger> categories = LogManager.getCurrentLoggers(); 472 while (categories.hasMoreElements()) 473 { 474 org.apache.log4j.Logger logger = categories.nextElement(); 475 logger.addAppender(appender); 476 } 477 478 _loggerManager = new SLF4JLoggerManager(); 479 _logger = LoggerFactory.getLogger(getClass()); 480 } 481 482 private void _createCocoon() throws Exception 483 { 484 _avalonContext.put(Constants.CONTEXT_CLASS_LOADER, getClass().getClassLoader()); 485 _avalonContext.put(Constants.CONTEXT_CLASSPATH, ""); 486 487 URL configFile = (URL) _avalonContext.get(Constants.CONTEXT_CONFIG_URL); 488 489 _logger.info("Reloading from: {}", configFile.toExternalForm()); 490 491 Cocoon c = (Cocoon) ClassUtils.newInstance("org.apache.cocoon.Cocoon"); 492 ContainerUtil.enableLogging(c, new SLF4JLoggerAdapter(_logger)); 493 c.setLoggerManager(_loggerManager); 494 ContainerUtil.contextualize(c, _avalonContext); 495 ContainerUtil.initialize(c); 496 497 _cocoon = c; 498 } 499 500 /** 501 * Init migration and plugins 502 * @param pluginCM Plugins Component Manager 503 * @return true if a server restart is required 504 * @throws Exception Something went wrong 505 */ 506 public static boolean doMigrationAndInit(PluginsComponentManager pluginCM) throws Exception 507 { 508 if (_initMigration(pluginCM)) 509 { 510 return true; 511 } 512 513 _initPlugins(pluginCM); 514 515 return false; 516 } 517 518 private static void _initPlugins(PluginsComponentManager pluginCM) throws Exception 519 { 520 setRunMode(RunMode.INITIALIZING); 521 522 // If we're in safe mode 523 if (!PluginsManager.getInstance().isSafeMode()) 524 { 525 // Plugins Init class execution 526 InitExtensionPoint initExtensionPoint = (InitExtensionPoint) pluginCM.lookup(InitExtensionPoint.ROLE); 527 for (String id : initExtensionPoint.getExtensionsIds()) 528 { 529 Init init = initExtensionPoint.getExtension(id); 530 init.init(); 531 } 532 533 // Application Init class execution if available 534 if (pluginCM.hasComponent(Init.ROLE)) 535 { 536 Init init = (Init) pluginCM.lookup(Init.ROLE); 537 init.init(); 538 } 539 } 540 } 541 542 private static boolean _initMigration(PluginsComponentManager pluginCM) throws Exception 543 { 544 setRunMode(RunMode.MIGRATING); 545 546 MigrationEngine migrationEngine = (MigrationEngine) pluginCM.lookup(MigrationEngine.ROLE); 547 return migrationEngine.migrate(); 548 } 549 550 private void _loadRuntimeConfig() throws ServletException 551 { 552 Configuration runtimeConf = null; 553 try 554 { 555 // XML Schema validation 556 SAXParserFactory factory = SAXParserFactory.newInstance(); 557 factory.setNamespaceAware(true); 558 559 URL schemaURL = getClass().getResource("runtime-4.1.xsd"); 560 Schema schema = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI).newSchema(schemaURL); 561 factory.setSchema(schema); 562 563 XMLReader reader = factory.newSAXParser().getXMLReader(); 564 DefaultConfigurationBuilder runtimeConfBuilder = new DefaultConfigurationBuilder(reader); 565 566 File runtimeConfigFile = new File(_servletContextPath, "WEB-INF/param/runtime.xml"); 567 try (InputStream runtime = new FileInputStream(runtimeConfigFile)) 568 { 569 runtimeConf = runtimeConfBuilder.build(runtime, runtimeConfigFile.getAbsolutePath()); 570 } 571 } 572 catch (Exception e) 573 { 574 _logger.error("Unable to load runtime file at 'WEB-INF/param/runtime.xml'. PluginsManager will enter in safe mode.", e); 575 } 576 577 Configuration externalConf = null; 578 try 579 { 580 DefaultConfigurationBuilder externalConfBuilder = new DefaultConfigurationBuilder(); 581 582 File externalConfigFile = new File(_servletContextPath, EXTERNAL_LOCATIONS); 583 if (externalConfigFile.exists()) 584 { 585 try (InputStream external = new FileInputStream(externalConfigFile)) 586 { 587 externalConf = externalConfBuilder.build(external, externalConfigFile.getAbsolutePath()); 588 } 589 } 590 } 591 catch (Exception e) 592 { 593 _logger.error("Unable to load external locations values at " + EXTERNAL_LOCATIONS, e); 594 throw new ServletException("Unable to load external locations values at " + EXTERNAL_LOCATIONS, e); 595 } 596 597 RuntimeConfig.configure(runtimeConf, externalConf, _ametysHome, _servletContextPath); 598 } 599 600 @Override 601 public void destroy() 602 { 603 setRunMode(RunMode.STOPPING); 604 605 if (_cocoon != null) 606 { 607 _logger.debug("Servlet destroyed - disposing Cocoon"); 608 _disposeCocoon(); 609 } 610 611 if (_ametysHomeLock != null) 612 { 613 _ametysHomeLock.release(); 614 _ametysHomeLock = null; 615 } 616 617 _avalonContext = null; 618 _logger = null; 619 _loggerManager = null; 620 } 621 622 623 private final void _disposeCocoon() 624 { 625 if (_cocoon != null) 626 { 627 ContainerUtil.dispose(_cocoon); 628 _cocoon = null; 629 } 630 } 631 632 @Override 633 public final void service(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException 634 { 635 req.setCharacterEncoding("UTF-8"); 636 637 // Error mode 638 if (_exception != null) 639 { 640 _renderError(req, res, _exception, "An error occured during Ametys initialization."); 641 return; 642 } 643 644 // Restarting 645 if (_cocoon == null) 646 { 647 _renderError(req, res, null, "Ametys is restarting. Please try again later."); 648 return; 649 } 650 651 String uri = _retrieveUri(req); 652 653 if (PluginsManager.getInstance().isSafeMode()) 654 { 655 // safe mode 656 String finalUri = uri; 657 boolean allowed = _allowedURLPattern.stream().anyMatch(p -> p.matcher(finalUri).matches()); 658 if (!allowed) 659 { 660 res.addHeader("X-Ametys-SafeMode", "true"); 661 662 Status status = PluginsManager.getInstance().getStatus(); 663 664 if (status == Status.NO_CONFIG) 665 { 666 res.sendRedirect(req.getContextPath() + "/_admin/public/first-start.html"); 667 return; 668 } 669 else if (status == Status.CONFIG_INCOMPLETE) 670 { 671 res.sendRedirect(req.getContextPath() + "/_admin/public/load-config.html"); 672 return; 673 } 674 else 675 { 676 res.sendRedirect(req.getContextPath() + "/_admin/public/safe-mode.html"); 677 return; 678 } 679 } 680 } 681 682 UserIdentity authenticatedUser = null; 683 HttpSession session = req.getSession(false); 684 if (session != null) 685 { 686 authenticatedUser = (UserIdentity) session.getAttribute(AuthenticateAction.SESSION_USERIDENTITY); 687 } 688 689 MDC.remove("user"); 690 691 if (authenticatedUser != null) 692 { 693 MDC.put("user", UserIdentity.userIdentityToString(authenticatedUser)); 694 } 695 696 MDC.put("requestURI", req.getRequestURI()); 697 698 StopWatch stopWatch = new StopWatch(); 699 HttpServletRequest request = null; 700 try 701 { 702 // used for timing the processing 703 stopWatch.start(); 704 705 _fireRequestStarted(req); 706 707 // get the request (wrapped if contains multipart-form data) 708 request = _requestFactory.getServletRequest(req); 709 710 // Process the request 711 HttpEnvironment env = new HttpEnvironment(uri, _servletContextURL.toExternalForm(), request, res, _servletContext, _context, "UTF-8", "UTF-8"); 712 env.enableLogging(new SLF4JLoggerAdapter(_logger)); 713 714 if (!_cocoon.process(env)) 715 { 716 // We reach this when there is nothing in the processing change that matches 717 // the request. For example, no matcher matches. 718 _logger.error("The Cocoon engine failed to process the request."); 719 _renderError(request, res, null, "Cocoon engine failed to process the request"); 720 } 721 } 722 catch (ResourceNotFoundException | ConnectionResetException | IOException e) 723 { 724 _logger.warn(e.toString()); 725 _renderError(request, res, e, e.getMessage()); 726 } 727 catch (Throwable e) 728 { 729 _logger.error("Internal Cocoon Problem", e); 730 _renderError(request, res, e, "Internal Cocoon Problem"); 731 } 732 finally 733 { 734 stopWatch.stop(); 735 _logger.info("'{}' processed in {} ms.", uri, stopWatch.getDuration()); 736 737 try 738 { 739 if (request instanceof MultipartHttpServletRequest) 740 { 741 _logger.debug("Deleting uploaded file(s)."); 742 ((MultipartHttpServletRequest) request).cleanup(); 743 } 744 } 745 catch (IOException e) 746 { 747 _logger.error("Cocoon got an Exception while trying to cleanup the uploaded files.", e); 748 } 749 } 750 751 _fireRequestEnded(req); 752 753 if (Boolean.TRUE.equals(req.getAttribute("org.ametys.runtime.reload"))) 754 { 755 restartCocoon(req); 756 } 757 } 758 759 /** 760 * Restart cocoon 761 * @param req The http servlet request, used to read safeMode and normalMode 762 * request attribute if possible. If null, safe mode will be 763 * forced only if it was already forced. 764 */ 765 public void restartCocoon(HttpServletRequest req) 766 { 767 try 768 { 769 ConfigManager.getInstance().dispose(); 770 _disposeCocoon(); 771 _servletContext.removeAttribute("PluginsComponentManager"); 772 773 // By default force safe mode if it was already forced 774 boolean wasForcedSafeMode = PluginsManager.getInstance().getStatus() == PluginsManager.Status.SAFE_MODE_FORCED; 775 boolean forceSafeMode = wasForcedSafeMode; 776 if (req != null) 777 { 778 // Also, checks if there is some specific request attributes 779 // Force safe mode if is explicitly requested 780 // Or force safe mode it was already forced unless normal mode is explicitly requested 781 forceSafeMode = Boolean.TRUE.equals(req.getAttribute("org.ametys.runtime.reload.safeMode")); 782 if (!forceSafeMode) 783 { 784 forceSafeMode = wasForcedSafeMode && !Boolean.TRUE.equals(req.getAttribute("org.ametys.runtime.reload.normalMode")); 785 } 786 } 787 788 _servletContext.setAttribute("org.ametys.runtime.forceSafeMode", forceSafeMode); 789 790 _initAmetys(); 791 } 792 catch (Exception e) 793 { 794 setRunMode(RunMode.FATALERROR); 795 796 _logger.error("Error while reloading Ametys. Entering in error mode.", e); 797 _exception = e; 798 } 799 } 800 801 private String _retrieveUri(HttpServletRequest req) 802 { 803 String uri = req.getServletPath(); 804 805 String pathInfo = req.getPathInfo(); 806 if (pathInfo != null) 807 { 808 uri += pathInfo; 809 } 810 811 if (uri.length() > 0 && uri.charAt(0) == '/') 812 { 813 uri = uri.substring(1); 814 } 815 return uri; 816 } 817 818 @SuppressWarnings("unchecked") 819 private void _fireRequestStarted(HttpServletRequest req) 820 { 821 Collection< ? extends RequestListener> listeners = (Collection< ? extends RequestListener>) _servletContext.getAttribute(RequestListenerManager.CONTEXT_ATTRIBUTE_REQUEST_LISTENERS); 822 823 if (listeners == null) 824 { 825 return; 826 } 827 828 for (RequestListener listener : listeners) 829 { 830 listener.requestStarted(req); 831 } 832 } 833 834 @SuppressWarnings("unchecked") 835 private void _fireRequestEnded(HttpServletRequest req) 836 { 837 Collection<? extends RequestListener> listeners = (Collection< ? extends RequestListener>) _servletContext.getAttribute(RequestListenerManager.CONTEXT_ATTRIBUTE_REQUEST_LISTENERS); 838 839 if (listeners == null) 840 { 841 return; 842 } 843 844 for (RequestListener listener : listeners) 845 { 846 listener.requestEnded(req); 847 } 848 } 849 850 /** 851 * Set the maintenance status 852 * @param maintenanceStatus The new maintenance status. Cannot be null. 853 * @param forcedMainteanceInformations The informations if the mode is FORCED. Can be null. 854 */ 855 public static void setMaintenanceStatus(MaintenanceStatus maintenanceStatus, ForcedMainteanceInformations forcedMainteanceInformations) 856 { 857 if (_maintenanceStatus != maintenanceStatus) 858 { 859 LoggerFactory.getLogger(Runtime.class).debug("Switching maintenance status from " + _maintenanceStatus + " to " + maintenanceStatus); 860 } 861 862 ForcedMainteanceInformations fixedForcedMainteanceInformations = null; 863 if (forcedMainteanceInformations != null) 864 { 865 fixedForcedMainteanceInformations = new ForcedMainteanceInformations(StringUtils.defaultString(forcedMainteanceInformations.comment).replaceAll("\r?\n", "<br/>"), forcedMainteanceInformations.initiator, forcedMainteanceInformations.since); 866 } 867 868 try 869 { 870 File maintenanceFile = FileUtils.getFile(AmetysHomeHelper.getAmetysHome(), AmetysHomeHelper.AMETYS_HOME_ADMINISTRATOR_DIR, MAINTENANCE_FILENAME); 871 if (maintenanceStatus != MaintenanceStatus.FORCED) 872 { 873 if (maintenanceFile.exists()) 874 { 875 FileUtils.delete(maintenanceFile); 876 } 877 } 878 else 879 { 880 // create the result where to write 881 maintenanceFile.getParentFile().mkdirs(); 882 883 try (OutputStream os = new FileOutputStream(maintenanceFile)) 884 { 885 // create a transformer for saving sax into a file 886 TransformerHandler th = ((SAXTransformerFactory) TransformerFactory.newInstance()).newTransformerHandler(); 887 888 StreamResult result = new StreamResult(os); 889 th.setResult(result); 890 891 // create the format of result 892 Properties format = new Properties(); 893 format.put(OutputKeys.METHOD, "xml"); 894 format.put(OutputKeys.INDENT, "yes"); 895 format.put(OutputKeys.ENCODING, "UTF-8"); 896 format.put(OutputPropertiesFactory.S_KEY_INDENT_AMOUNT, "2"); 897 th.getTransformer().setOutputProperties(format); 898 899 th.startDocument(); 900 XMLUtils.startElement(th, "maintenance"); 901 if (fixedForcedMainteanceInformations != null) 902 { 903 XMLUtils.createElement(th, "comment", StringUtils.defaultString(fixedForcedMainteanceInformations.comment)); 904 XMLUtils.createElement(th, "initiator", StringUtils.defaultString(UserIdentity.userIdentityToString(fixedForcedMainteanceInformations.initiator))); 905 if (fixedForcedMainteanceInformations.since != null) 906 { 907 XMLUtils.createElement(th, "since", DateUtils.zonedDateTimeToString(fixedForcedMainteanceInformations.since)); 908 } 909 } 910 XMLUtils.endElement(th, "maintenance"); 911 th.endDocument(); 912 } 913 } 914 } 915 catch (Exception e) 916 { 917 throw new RuntimeException("Cannot change maintenance status", e); 918 } 919 920 _maintenanceStatus = maintenanceStatus; 921 _forcedMainteanceInformations = fixedForcedMainteanceInformations; 922 923 try 924 { 925 PluginsComponentManager pluginCM = (PluginsComponentManager) _servletContext.getAttribute("PluginsComponentManager"); 926 ObservationManager observationManager = (ObservationManager) pluginCM.lookup(ObservationManager.ROLE); 927 CurrentUserProvider currentUserProvider = (CurrentUserProvider) pluginCM.lookup(CurrentUserProvider.ROLE); 928 if (observationManager != null) 929 { 930 observationManager.notify(new Event(ObservationConstants.EVENT_RUNTIME_MAINTENANCE, currentUserProvider.getUser(), Map.of())); 931 } 932 } 933 catch (Exception e) 934 { 935 // Safe mode 936 } 937 } 938 939 /** 940 * Get the current maintenance status 941 * @return The maintenance status 942 */ 943 public static MaintenanceStatus getMaintenanceStatus() 944 { 945 if (_maintenanceStatus == null) 946 { 947 File maintenanceFile = FileUtils.getFile(AmetysHomeHelper.getAmetysHome(), AmetysHomeHelper.AMETYS_HOME_ADMINISTRATOR_DIR, MAINTENANCE_FILENAME); 948 949 if (!maintenanceFile.exists()) 950 { 951 _maintenanceStatus = MaintenanceStatus.NONE; 952 } 953 else 954 { 955 _maintenanceStatus = MaintenanceStatus.FORCED; 956 try 957 { 958 Configuration configuration = new DefaultConfigurationBuilder().buildFromFile(maintenanceFile); 959 _forcedMainteanceInformations = new ForcedMainteanceInformations(configuration.getChild("comment").getValue(""), 960 UserIdentity.stringToUserIdentity(configuration.getChild("initiator").getValue(null)), 961 DateUtils.parseZonedDateTime(configuration.getChild("since").getValue(null))); 962 } 963 catch (ConfigurationException | SAXException | IOException e) 964 { 965 _logger.error("Cannot read the maintenance status file", e); 966 _forcedMainteanceInformations = null; 967 } 968 } 969 } 970 return _maintenanceStatus; 971 } 972 973 /** 974 * When maintenance status is forced, this comment may explains why 975 * @return The comment. Can be null even in forced mode. 976 */ 977 public static ForcedMainteanceInformations getMaintenanceStatusForcedInformations() 978 { 979 return _forcedMainteanceInformations; 980 } 981 982 /** 983 * Set the run mode 984 * @param mode the running mode 985 */ 986 public static void setRunMode(RunMode mode) 987 { 988 if (_runMode != mode) 989 { 990 LoggerFactory.getLogger(RuntimeServlet.class).debug("Switching run mode from " + _runMode + " to " + mode); 991 } 992 993 _runMode = mode; 994 } 995 996 /** 997 * Get the run mode 998 * @return the current run mode 999 */ 1000 public static RunMode getRunMode() 1001 { 1002 return _runMode; 1003 } 1004 1005 private void _renderError(HttpServletRequest req, HttpServletResponse res, Throwable throwable, String message) throws ServletException 1006 { 1007 ServletConfig config = getServletConfig(); 1008 1009 if (config == null) 1010 { 1011 throw new ServletException("Cannot access to ServletConfig"); 1012 } 1013 1014 try 1015 { 1016 ServletOutputStream os = res.getOutputStream(); 1017 String path = req.getRequestURI().substring(req.getContextPath().length()); 1018 1019 // Favicon associated with the error page. 1020 if (path.equals("/favicon.ico")) 1021 { 1022 try (InputStream is = getClass().getResourceAsStream("favicon.ico")) 1023 { 1024 res.setStatus(200); 1025 res.setContentType(config.getServletContext().getMimeType("favicon.ico")); 1026 1027 IOUtils.copy(is, os); 1028 1029 return; 1030 } 1031 } 1032 1033 res.setStatus(500); 1034 res.setContentType("text/html; charset=UTF-8"); 1035 1036 SAXTransformerFactory saxFactory = (SAXTransformerFactory) TransformerFactory.newInstance(); 1037 TransformerHandler th; 1038 1039 try (InputStream is = getClass().getResourceAsStream("fatal.xsl")) 1040 { 1041 StreamSource errorSource = new StreamSource(is); 1042 th = saxFactory.newTransformerHandler(errorSource); 1043 } 1044 1045 Properties format = new Properties(); 1046 format.put(OutputKeys.METHOD, "xml"); 1047 format.put(OutputKeys.ENCODING, "UTF-8"); 1048 format.put(OutputKeys.DOCTYPE_SYSTEM, "about:legacy-compat"); 1049 1050 th.getTransformer().setOutputProperties(format); 1051 1052 th.getTransformer().setParameter("code", 500); 1053 th.getTransformer().setParameter("realPath", config.getServletContext().getRealPath("/")); 1054 th.getTransformer().setParameter("contextPath", req.getContextPath()); 1055 1056 StreamResult result = new StreamResult(os); 1057 th.setResult(result); 1058 1059 th.startDocument(); 1060 1061 XMLUtils.startElement(th, "http://apache.org/cocoon/exception/1.0", "exception-report"); 1062 XMLUtils.startElement(th, "http://apache.org/cocoon/exception/1.0", "message"); 1063 XMLUtils.data(th, message); 1064 XMLUtils.endElement(th, "http://apache.org/cocoon/exception/1.0", "message"); 1065 1066 XMLUtils.startElement(th, "http://apache.org/cocoon/exception/1.0", "stacktrace"); 1067 if (throwable != null) 1068 { 1069 XMLUtils.data(th, ExceptionUtils.getStackTrace(throwable)); 1070 } 1071 XMLUtils.endElement(th, "http://apache.org/cocoon/exception/1.0", "stacktrace"); 1072 XMLUtils.endElement(th, "http://apache.org/cocoon/exception/1.0", "ex:exception-report"); 1073 1074 th.endDocument(); 1075 } 1076 catch (Exception e) 1077 { 1078 // Nothing we can do anymore ... 1079 throw new ServletException(e); 1080 } 1081 } 1082 1083 /* 1084 * private void _runMaintenanceMode(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException { ServletConfig config = getServletConfig(); if (config == null) { throw new ServletException("Cannot access to ServletConfig"); } ServletOutputStream os = res.getOutputStream(); if (req.getRequestURI().startsWith("/WEB-INF/error/")) { // Service des fichiers statiques de la page d'erreur File f = new File(config.getServletContext().getRealPath(req.getRequestURI())); if (f.exists()) { res.setStatus(200); InputStream is = new FileInputStream(f); SourceUtil.copy(is, os); is.close(); } else { res.setStatus(404); } return; } res.setStatus(500); SAXTransformerFactory saxFactory = (SAXTransformerFactory) TransformerFactory.newInstance(); InputStream is; File errorXSL = new File(config.getServletContext().getRealPath("/WEB-INF/error/error.xsl")); if (errorXSL.exists()) { is = new FileInputStream(errorXSL); } else { is = getClass().getResourceAsStream("/org/ametys/runtime/kernel/pages/error/error.xsl"); } try { StreamSource errorSource = new StreamSource(is); Templates templates = saxFactory.newTemplates(errorSource); TransformerHandler th = saxFactory.newTransformerHandler(templates); is.close(); StreamResult result = new StreamResult(os); th.setResult(result); th.startDocument(); th.startElement("", "error", "error", new AttributesImpl()); saxMaintenanceMessage(th); th.endElement("", "error", "error"); th.endDocument(); } catch (Exception ex) { throw new ServletException("Unable to send maintenance page", ex); } } 1085 */ 1086 1087 /** 1088 * In maintenance mode, send error information as SAX events.<br> 1089 * 1090 * @param ch the contentHandler receiving the message 1091 * @throws SAXException if an error occured while send SAX events 1092 */ 1093 /* 1094 * protected void saxMaintenanceMessage(ContentHandler ch) throws SAXException { String maintenanceMessage = "The application is under maintenance. Please retry later."; ch.characters(maintenanceMessage.toCharArray(), 0, maintenanceMessage.length()); } private boolean _accept(HttpServletRequest req) { // FIX ME à ne pas mettre en dur return req.getRequestURI().startsWith("_admin"); } 1095 */ 1096}