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.servlet.multipart.RequestFactory; 069import org.apache.cocoon.util.ClassUtils; 070import org.apache.cocoon.util.log.SLF4JLoggerAdapter; 071import org.apache.cocoon.util.log.SLF4JLoggerManager; 072import org.apache.cocoon.xml.XMLUtils; 073import org.apache.commons.collections.EnumerationUtils; 074import org.apache.commons.io.FileUtils; 075import org.apache.commons.io.IOUtils; 076import org.apache.commons.lang3.StringUtils; 077import org.apache.commons.lang3.exception.ExceptionUtils; 078import org.apache.commons.lang3.time.StopWatch; 079import org.apache.log4j.Appender; 080import org.apache.log4j.LogManager; 081import org.apache.log4j.xml.DOMConfigurator; 082import org.apache.xml.serializer.OutputPropertiesFactory; 083import org.slf4j.Logger; 084import org.slf4j.LoggerFactory; 085import org.slf4j.MDC; 086import org.xml.sax.SAXException; 087import org.xml.sax.XMLReader; 088 089import org.ametys.core.ObservationConstants; 090import org.ametys.core.authentication.AuthenticateAction; 091import org.ametys.core.migration.MigrationExtensionPoint; 092import org.ametys.core.observation.Event; 093import org.ametys.core.observation.ObservationManager; 094import org.ametys.core.user.CurrentUserProvider; 095import org.ametys.core.user.UserIdentity; 096import org.ametys.core.util.DateUtils; 097import org.ametys.runtime.config.Config; 098import org.ametys.runtime.config.ConfigManager; 099import org.ametys.runtime.data.AmetysHomeLock; 100import org.ametys.runtime.data.AmetysHomeLockException; 101import org.ametys.runtime.log.MemoryAppender; 102import org.ametys.runtime.plugin.Init; 103import org.ametys.runtime.plugin.InitExtensionPoint; 104import org.ametys.runtime.plugin.PluginsManager; 105import org.ametys.runtime.plugin.PluginsManager.Status; 106import org.ametys.runtime.plugin.component.PluginsComponentManager; 107import org.ametys.runtime.plugins.admin.jvmstatus.ActiveSessionListener; 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 Files.createDirectories(statisticsFile.getParent()); 373 try (OutputStream os = Files.newOutputStream(statisticsFile)) 374 { 375 // create a transformer for saving sax into a file 376 TransformerHandler th = ((SAXTransformerFactory) TransformerFactory.newInstance()).newTransformerHandler(); 377 378 StreamResult result = new StreamResult(os); 379 th.setResult(result); 380 381 // create the format of result 382 Properties format = new Properties(); 383 format.put(OutputKeys.METHOD, "xml"); 384 format.put(OutputKeys.INDENT, "yes"); 385 format.put(OutputKeys.ENCODING, "UTF-8"); 386 format.put(OutputPropertiesFactory.S_KEY_INDENT_AMOUNT, "2"); 387 th.getTransformer().setOutputProperties(format); 388 389 th.startDocument(); 390 XMLUtils.startElement(th, "instance"); 391 XMLUtils.createElement(th, "unique-id", newId); 392 XMLUtils.endElement(th, "instance"); 393 th.endDocument(); 394 } 395 } 396 catch (Exception e) 397 { 398 _logger.error("Cannot create the instance id", e); 399 } 400 } 401 402 private void _initAmetys() throws Exception 403 { 404 setRunMode(RunMode.STARTING); 405 _maintenanceStatus = null; 406 _exception = null; 407 408 // WEB-INF/param/runtime.xml loading 409 _loadRuntimeConfig(); 410 411 _createCocoon(); 412 413 // Upload initialization 414 _maxUploadSize = DEFAULT_MAX_UPLOAD_SIZE; 415 _uploadDir = new File(RuntimeConfig.getInstance().getAmetysHome(), "uploads"); 416 417 if (ConfigManager.getInstance().isComplete()) 418 { 419 Long maxUploadSizeParam = Config.getInstance().getValue("runtime.upload.max-size"); 420 if (maxUploadSizeParam != null) 421 { 422 // if the feature core/runtime.upload is deactivated, use the default value (10 Mb) 423 _maxUploadSize = maxUploadSizeParam.intValue(); 424 } 425 } 426 427 _uploadDir.mkdirs(); 428 _avalonContext.put(Constants.CONTEXT_UPLOAD_DIR, _uploadDir); 429 430 _requestFactory = new RequestFactory(true, _uploadDir, false, true, _maxUploadSize, "UTF-8"); 431 432 // Generate the instance-info file 433 _createInstanceId(); 434 435 PluginsComponentManager pluginCM = (PluginsComponentManager) _servletContext.getAttribute("PluginsComponentManager"); 436 if (doMigrationAndInit(pluginCM)) 437 { 438 restartCocoon(null); 439 } 440 else 441 { 442 if (PluginsManager.getInstance().isSafeMode()) 443 { 444 setRunMode(RunMode.SAFEMODE); 445 } 446 else if (getMaintenanceStatus() != MaintenanceStatus.NONE) 447 { 448 setRunMode(RunMode.MAINTENANCE); 449 } 450 else 451 { 452 setRunMode(RunMode.NORMAL); 453 } 454 } 455 } 456 457 private void _initLogger() 458 { 459 // Configure Log4j 460 String logj4fFile = _servletContext.getRealPath("/WEB-INF/log4j.xml"); 461 462 // Hack to have context-relative log files, because of lack in configuration capabilities in log4j. 463 // If there are more than one Ametys in the same JVM, the property will be successively set for each instance, 464 // so we heavily rely on DOMConfigurator beeing synchronous. 465 System.setProperty("ametys.home.dir", _ametysHome.getAbsolutePath()); 466 DOMConfigurator.configure(logj4fFile); 467 System.clearProperty("ametys.home.dir"); 468 469 Appender appender = new MemoryAppender(); 470 appender.setName(org.ametys.plugins.core.ui.log.LogManager.MEMORY_APPENDER_NAME); 471 LogManager.getRootLogger().addAppender(appender); 472 Enumeration<org.apache.log4j.Logger> categories = LogManager.getCurrentLoggers(); 473 while (categories.hasMoreElements()) 474 { 475 org.apache.log4j.Logger logger = categories.nextElement(); 476 logger.addAppender(appender); 477 } 478 479 _loggerManager = new SLF4JLoggerManager(); 480 _logger = LoggerFactory.getLogger(getClass()); 481 } 482 483 private void _createCocoon() throws Exception 484 { 485 _avalonContext.put(Constants.CONTEXT_CLASS_LOADER, getClass().getClassLoader()); 486 _avalonContext.put(Constants.CONTEXT_CLASSPATH, ""); 487 488 URL configFile = (URL) _avalonContext.get(Constants.CONTEXT_CONFIG_URL); 489 490 _logger.info("Reloading from: {}", configFile.toExternalForm()); 491 492 Cocoon c = (Cocoon) ClassUtils.newInstance("org.apache.cocoon.Cocoon"); 493 ContainerUtil.enableLogging(c, new SLF4JLoggerAdapter(_logger)); 494 c.setLoggerManager(_loggerManager); 495 ContainerUtil.contextualize(c, _avalonContext); 496 ContainerUtil.initialize(c); 497 498 _cocoon = c; 499 } 500 501 /** 502 * Init migration and plugins 503 * @param pluginCM Plugins Component Manager 504 * @return true if a server restart is required 505 * @throws Exception Something went wrong 506 */ 507 public static boolean doMigrationAndInit(PluginsComponentManager pluginCM) throws Exception 508 { 509 if (_initMigration(pluginCM)) 510 { 511 return true; 512 } 513 514 _initPlugins(pluginCM); 515 516 return false; 517 } 518 519 private static void _initPlugins(PluginsComponentManager pluginCM) throws Exception 520 { 521 setRunMode(RunMode.INITIALIZING); 522 523 // If we're in safe mode 524 if (!PluginsManager.getInstance().isSafeMode()) 525 { 526 // Plugins Init class execution 527 InitExtensionPoint initExtensionPoint = (InitExtensionPoint) pluginCM.lookup(InitExtensionPoint.ROLE); 528 for (String id : initExtensionPoint.getExtensionsIds()) 529 { 530 Init init = initExtensionPoint.getExtension(id); 531 init.init(); 532 } 533 534 // Application Init class execution if available 535 if (pluginCM.hasComponent(Init.ROLE)) 536 { 537 Init init = (Init) pluginCM.lookup(Init.ROLE); 538 init.init(); 539 } 540 } 541 } 542 543 private static boolean _initMigration(PluginsComponentManager pluginCM) throws Exception 544 { 545 setRunMode(RunMode.MIGRATING); 546 547 MigrationExtensionPoint migrationEP = (MigrationExtensionPoint) pluginCM.lookup(MigrationExtensionPoint.ROLE); 548 return migrationEP.doMigration(); 549 } 550 551 private void _loadRuntimeConfig() throws ServletException 552 { 553 Configuration runtimeConf = null; 554 try 555 { 556 // XML Schema validation 557 SAXParserFactory factory = SAXParserFactory.newInstance(); 558 factory.setNamespaceAware(true); 559 560 URL schemaURL = getClass().getResource("runtime-4.1.xsd"); 561 Schema schema = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI).newSchema(schemaURL); 562 factory.setSchema(schema); 563 564 XMLReader reader = factory.newSAXParser().getXMLReader(); 565 DefaultConfigurationBuilder runtimeConfBuilder = new DefaultConfigurationBuilder(reader); 566 567 File runtimeConfigFile = new File(_servletContextPath, "WEB-INF/param/runtime.xml"); 568 try (InputStream runtime = new FileInputStream(runtimeConfigFile)) 569 { 570 runtimeConf = runtimeConfBuilder.build(runtime, runtimeConfigFile.getAbsolutePath()); 571 } 572 } 573 catch (Exception e) 574 { 575 _logger.error("Unable to load runtime file at 'WEB-INF/param/runtime.xml'. PluginsManager will enter in safe mode.", e); 576 } 577 578 Configuration externalConf = null; 579 try 580 { 581 DefaultConfigurationBuilder externalConfBuilder = new DefaultConfigurationBuilder(); 582 583 File externalConfigFile = new File(_servletContextPath, EXTERNAL_LOCATIONS); 584 if (externalConfigFile.exists()) 585 { 586 try (InputStream external = new FileInputStream(externalConfigFile)) 587 { 588 externalConf = externalConfBuilder.build(external, externalConfigFile.getAbsolutePath()); 589 } 590 } 591 } 592 catch (Exception e) 593 { 594 _logger.error("Unable to load external locations values at " + EXTERNAL_LOCATIONS, e); 595 throw new ServletException("Unable to load external locations values at " + EXTERNAL_LOCATIONS, e); 596 } 597 598 RuntimeConfig.configure(runtimeConf, externalConf, _ametysHome, _servletContextPath); 599 } 600 601 @Override 602 public void destroy() 603 { 604 setRunMode(RunMode.STOPPING); 605 606 if (_cocoon != null) 607 { 608 _logger.debug("Servlet destroyed - disposing Cocoon"); 609 _disposeCocoon(); 610 } 611 612 if (_ametysHomeLock != null) 613 { 614 _ametysHomeLock.release(); 615 _ametysHomeLock = null; 616 } 617 618 _avalonContext = null; 619 _logger = null; 620 _loggerManager = null; 621 } 622 623 624 private final void _disposeCocoon() 625 { 626 if (_cocoon != null) 627 { 628 ContainerUtil.dispose(_cocoon); 629 _cocoon = null; 630 } 631 } 632 633 @Override 634 public final void service(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException 635 { 636 req.setCharacterEncoding("UTF-8"); 637 638 // add the cocoon header timestamp 639 res.addHeader("X-Cocoon-Version", Constants.VERSION); 640 641 // Error mode 642 if (_exception != null) 643 { 644 _renderError(req, res, _exception, "An error occured during Ametys initialization."); 645 return; 646 } 647 648 // Restarting 649 if (_cocoon == null) 650 { 651 _renderError(req, res, null, "Ametys is restarting. Please try again later."); 652 return; 653 } 654 655 String uri = _retrieveUri(req); 656 657 if (PluginsManager.getInstance().isSafeMode()) 658 { 659 // safe mode 660 String finalUri = uri; 661 boolean allowed = _allowedURLPattern.stream().anyMatch(p -> p.matcher(finalUri).matches()); 662 if (!allowed) 663 { 664 res.addHeader("X-Ametys-SafeMode", "true"); 665 666 Status status = PluginsManager.getInstance().getStatus(); 667 668 if (status == Status.NO_CONFIG) 669 { 670 res.sendRedirect(req.getContextPath() + "/_admin/public/first-start.html"); 671 return; 672 } 673 else if (status == Status.CONFIG_INCOMPLETE) 674 { 675 res.sendRedirect(req.getContextPath() + "/_admin/public/load-config.html"); 676 return; 677 } 678 else 679 { 680 res.sendRedirect(req.getContextPath() + "/_admin/public/safe-mode.html"); 681 return; 682 } 683 } 684 } 685 686 UserIdentity authenticatedUser = null; 687 HttpSession session = req.getSession(false); 688 if (session != null) 689 { 690 authenticatedUser = (UserIdentity) session.getAttribute(AuthenticateAction.SESSION_USERIDENTITY); 691 } 692 693 MDC.remove("user"); 694 695 if (authenticatedUser != null) 696 { 697 MDC.put("user", UserIdentity.userIdentityToString(authenticatedUser)); 698 } 699 700 MDC.put("requestURI", req.getRequestURI()); 701 702 StopWatch stopWatch = new StopWatch(); 703 HttpServletRequest request = null; 704 try 705 { 706 // used for timing the processing 707 stopWatch.start(); 708 709 _fireRequestStarted(req); 710 711 // get the request (wrapped if contains multipart-form data) 712 request = _requestFactory.getServletRequest(req); 713 714 // Process the request 715 HttpEnvironment env = new HttpEnvironment(uri, _servletContextURL.toExternalForm(), request, res, _servletContext, _context, "UTF-8", "UTF-8"); 716 env.enableLogging(new SLF4JLoggerAdapter(_logger)); 717 718 if (!_cocoon.process(env)) 719 { 720 // We reach this when there is nothing in the processing change that matches 721 // the request. For example, no matcher matches. 722 _logger.error("The Cocoon engine failed to process the request."); 723 _renderError(request, res, null, "Cocoon engine failed to process the request"); 724 } 725 } 726 catch (ResourceNotFoundException | ConnectionResetException | IOException e) 727 { 728 _logger.warn(e.toString()); 729 _renderError(request, res, e, e.getMessage()); 730 } 731 catch (Throwable e) 732 { 733 _logger.error("Internal Cocoon Problem", e); 734 _renderError(request, res, e, "Internal Cocoon Problem"); 735 } 736 finally 737 { 738 stopWatch.stop(); 739 _logger.info("'{}' processed in {} ms.", uri, stopWatch.getTime()); 740 741 try 742 { 743 if (request instanceof MultipartHttpServletRequest) 744 { 745 _logger.debug("Deleting uploaded file(s)."); 746 ((MultipartHttpServletRequest) request).cleanup(); 747 } 748 } 749 catch (IOException e) 750 { 751 _logger.error("Cocoon got an Exception while trying to cleanup the uploaded files.", e); 752 } 753 } 754 755 _fireRequestEnded(req); 756 757 if (Boolean.TRUE.equals(req.getAttribute("org.ametys.runtime.reload"))) 758 { 759 restartCocoon(req); 760 } 761 } 762 763 /** 764 * Restart cocoon 765 * @param req The http servlet request, used to read safeMode and normalMode 766 * request attribute if possible. If null, safe mode will be 767 * forced only if it was already forced. 768 */ 769 public void restartCocoon(HttpServletRequest req) 770 { 771 try 772 { 773 ConfigManager.getInstance().dispose(); 774 _disposeCocoon(); 775 _servletContext.removeAttribute("PluginsComponentManager"); 776 777 // By default force safe mode if it was already forced 778 boolean wasForcedSafeMode = PluginsManager.getInstance().getStatus() == PluginsManager.Status.SAFE_MODE_FORCED; 779 boolean forceSafeMode = wasForcedSafeMode; 780 if (req != null) 781 { 782 // Also, checks if there is some specific request attributes 783 // Force safe mode if is explicitly requested 784 // Or force safe mode it was already forced unless normal mode is explicitly requested 785 forceSafeMode = Boolean.TRUE.equals(req.getAttribute("org.ametys.runtime.reload.safeMode")); 786 if (!forceSafeMode) 787 { 788 forceSafeMode = wasForcedSafeMode && !Boolean.TRUE.equals(req.getAttribute("org.ametys.runtime.reload.normalMode")); 789 } 790 } 791 792 _servletContext.setAttribute("org.ametys.runtime.forceSafeMode", forceSafeMode); 793 794 _initAmetys(); 795 } 796 catch (Exception e) 797 { 798 setRunMode(RunMode.FATALERROR); 799 800 _logger.error("Error while reloading Ametys. Entering in error mode.", e); 801 _exception = e; 802 } 803 } 804 805 private String _retrieveUri(HttpServletRequest req) 806 { 807 String uri = req.getServletPath(); 808 809 String pathInfo = req.getPathInfo(); 810 if (pathInfo != null) 811 { 812 uri += pathInfo; 813 } 814 815 if (uri.length() > 0 && uri.charAt(0) == '/') 816 { 817 uri = uri.substring(1); 818 } 819 return uri; 820 } 821 822 @SuppressWarnings("unchecked") 823 private void _fireRequestStarted(HttpServletRequest req) 824 { 825 Collection< ? extends RequestListener> listeners = (Collection< ? extends RequestListener>) _servletContext.getAttribute(RequestListenerManager.CONTEXT_ATTRIBUTE_REQUEST_LISTENERS); 826 827 if (listeners == null) 828 { 829 return; 830 } 831 832 for (RequestListener listener : listeners) 833 { 834 listener.requestStarted(req); 835 } 836 } 837 838 @SuppressWarnings("unchecked") 839 private void _fireRequestEnded(HttpServletRequest req) 840 { 841 Collection<? extends RequestListener> listeners = (Collection< ? extends RequestListener>) _servletContext.getAttribute(RequestListenerManager.CONTEXT_ATTRIBUTE_REQUEST_LISTENERS); 842 843 if (listeners == null) 844 { 845 return; 846 } 847 848 for (RequestListener listener : listeners) 849 { 850 listener.requestEnded(req); 851 } 852 } 853 854 /** 855 * Set the maintenance status 856 * @param maintenanceStatus The new maintenance status. Cannot be null. 857 * @param forcedMainteanceInformations The informations if the mode is FORCED. Can be null. 858 */ 859 public static void setMaintenanceStatus(MaintenanceStatus maintenanceStatus, ForcedMainteanceInformations forcedMainteanceInformations) 860 { 861 if (_maintenanceStatus != maintenanceStatus) 862 { 863 LoggerFactory.getLogger(Runtime.class).debug("Switching maintenance status from " + _maintenanceStatus + " to " + maintenanceStatus); 864 } 865 866 ForcedMainteanceInformations fixedForcedMainteanceInformations = null; 867 if (forcedMainteanceInformations != null) 868 { 869 fixedForcedMainteanceInformations = new ForcedMainteanceInformations(StringUtils.defaultString(forcedMainteanceInformations.comment).replaceAll("\r?\n", "<br/>"), forcedMainteanceInformations.initiator, forcedMainteanceInformations.since); 870 } 871 872 try 873 { 874 File maintenanceFile = FileUtils.getFile(AmetysHomeHelper.getAmetysHome(), AmetysHomeHelper.AMETYS_HOME_ADMINISTRATOR_DIR, MAINTENANCE_FILENAME); 875 if (maintenanceStatus != MaintenanceStatus.FORCED) 876 { 877 if (maintenanceFile.exists()) 878 { 879 FileUtils.delete(maintenanceFile); 880 } 881 } 882 else 883 { 884 // create the result where to write 885 maintenanceFile.getParentFile().mkdirs(); 886 887 try (OutputStream os = new FileOutputStream(maintenanceFile)) 888 { 889 // create a transformer for saving sax into a file 890 TransformerHandler th = ((SAXTransformerFactory) TransformerFactory.newInstance()).newTransformerHandler(); 891 892 StreamResult result = new StreamResult(os); 893 th.setResult(result); 894 895 // create the format of result 896 Properties format = new Properties(); 897 format.put(OutputKeys.METHOD, "xml"); 898 format.put(OutputKeys.INDENT, "yes"); 899 format.put(OutputKeys.ENCODING, "UTF-8"); 900 format.put(OutputPropertiesFactory.S_KEY_INDENT_AMOUNT, "2"); 901 th.getTransformer().setOutputProperties(format); 902 903 th.startDocument(); 904 XMLUtils.startElement(th, "maintenance"); 905 if (fixedForcedMainteanceInformations != null) 906 { 907 XMLUtils.createElement(th, "comment", StringUtils.defaultString(fixedForcedMainteanceInformations.comment)); 908 XMLUtils.createElement(th, "initiator", StringUtils.defaultString(UserIdentity.userIdentityToString(fixedForcedMainteanceInformations.initiator))); 909 if (fixedForcedMainteanceInformations.since != null) 910 { 911 XMLUtils.createElement(th, "since", DateUtils.zonedDateTimeToString(fixedForcedMainteanceInformations.since)); 912 } 913 } 914 XMLUtils.endElement(th, "maintenance"); 915 th.endDocument(); 916 } 917 } 918 } 919 catch (Exception e) 920 { 921 throw new RuntimeException("Cannot change maintenance status", e); 922 } 923 924 _maintenanceStatus = maintenanceStatus; 925 _forcedMainteanceInformations = fixedForcedMainteanceInformations; 926 927 try 928 { 929 PluginsComponentManager pluginCM = (PluginsComponentManager) _servletContext.getAttribute("PluginsComponentManager"); 930 ObservationManager observationManager = (ObservationManager) pluginCM.lookup(ObservationManager.ROLE); 931 CurrentUserProvider currentUserProvider = (CurrentUserProvider) pluginCM.lookup(CurrentUserProvider.ROLE); 932 if (observationManager != null) 933 { 934 observationManager.notify(new Event(ObservationConstants.EVENT_RUNTIME_MAINTENANCE, currentUserProvider.getUser(), Map.of())); 935 } 936 } 937 catch (Exception e) 938 { 939 // Safe mode 940 } 941 } 942 943 /** 944 * Get the current maintenance status 945 * @return The maintenance status 946 */ 947 public static MaintenanceStatus getMaintenanceStatus() 948 { 949 if (_maintenanceStatus == null) 950 { 951 File maintenanceFile = FileUtils.getFile(AmetysHomeHelper.getAmetysHome(), AmetysHomeHelper.AMETYS_HOME_ADMINISTRATOR_DIR, MAINTENANCE_FILENAME); 952 953 if (!maintenanceFile.exists()) 954 { 955 _maintenanceStatus = MaintenanceStatus.NONE; 956 } 957 else 958 { 959 _maintenanceStatus = MaintenanceStatus.FORCED; 960 try 961 { 962 Configuration configuration = new DefaultConfigurationBuilder().buildFromFile(maintenanceFile); 963 _forcedMainteanceInformations = new ForcedMainteanceInformations(configuration.getChild("comment").getValue(""), 964 UserIdentity.stringToUserIdentity(configuration.getChild("initiator").getValue(null)), 965 DateUtils.parseZonedDateTime(configuration.getChild("since").getValue(null))); 966 } 967 catch (ConfigurationException | SAXException | IOException e) 968 { 969 _logger.error("Cannot read the maintenance status file", e); 970 _forcedMainteanceInformations = null; 971 } 972 } 973 } 974 return _maintenanceStatus; 975 } 976 977 /** 978 * When maintenance status is forced, this comment may explains why 979 * @return The comment. Can be null even in forced mode. 980 */ 981 public static ForcedMainteanceInformations getMaintenanceStatusForcedInformations() 982 { 983 return _forcedMainteanceInformations; 984 } 985 986 /** 987 * Set the run mode 988 * @param mode the running mode 989 */ 990 public static void setRunMode(RunMode mode) 991 { 992 if (_runMode != mode) 993 { 994 LoggerFactory.getLogger(RuntimeServlet.class).debug("Switching run mode from " + _runMode + " to " + mode); 995 } 996 997 _runMode = mode; 998 } 999 1000 /** 1001 * Get the run mode 1002 * @return the current run mode 1003 */ 1004 public static RunMode getRunMode() 1005 { 1006 return _runMode; 1007 } 1008 1009 private void _renderError(HttpServletRequest req, HttpServletResponse res, Throwable throwable, String message) throws ServletException 1010 { 1011 ServletConfig config = getServletConfig(); 1012 1013 if (config == null) 1014 { 1015 throw new ServletException("Cannot access to ServletConfig"); 1016 } 1017 1018 try 1019 { 1020 ServletOutputStream os = res.getOutputStream(); 1021 String path = req.getRequestURI().substring(req.getContextPath().length()); 1022 1023 // Favicon associated with the error page. 1024 if (path.equals("/favicon.ico")) 1025 { 1026 try (InputStream is = getClass().getResourceAsStream("favicon.ico")) 1027 { 1028 res.setStatus(200); 1029 res.setContentType(config.getServletContext().getMimeType("favicon.ico")); 1030 1031 IOUtils.copy(is, os); 1032 1033 return; 1034 } 1035 } 1036 1037 res.setStatus(500); 1038 res.setContentType("text/html; charset=UTF-8"); 1039 1040 SAXTransformerFactory saxFactory = (SAXTransformerFactory) TransformerFactory.newInstance(); 1041 TransformerHandler th; 1042 1043 try (InputStream is = getClass().getResourceAsStream("fatal.xsl")) 1044 { 1045 StreamSource errorSource = new StreamSource(is); 1046 th = saxFactory.newTransformerHandler(errorSource); 1047 } 1048 1049 Properties format = new Properties(); 1050 format.put(OutputKeys.METHOD, "xml"); 1051 format.put(OutputKeys.ENCODING, "UTF-8"); 1052 format.put(OutputKeys.DOCTYPE_SYSTEM, "about:legacy-compat"); 1053 1054 th.getTransformer().setOutputProperties(format); 1055 1056 th.getTransformer().setParameter("code", 500); 1057 th.getTransformer().setParameter("realPath", config.getServletContext().getRealPath("/")); 1058 th.getTransformer().setParameter("contextPath", req.getContextPath()); 1059 1060 StreamResult result = new StreamResult(os); 1061 th.setResult(result); 1062 1063 th.startDocument(); 1064 1065 XMLUtils.startElement(th, "http://apache.org/cocoon/exception/1.0", "exception-report"); 1066 XMLUtils.startElement(th, "http://apache.org/cocoon/exception/1.0", "message"); 1067 XMLUtils.data(th, message); 1068 XMLUtils.endElement(th, "http://apache.org/cocoon/exception/1.0", "message"); 1069 1070 XMLUtils.startElement(th, "http://apache.org/cocoon/exception/1.0", "stacktrace"); 1071 if (throwable != null) 1072 { 1073 XMLUtils.data(th, ExceptionUtils.getStackTrace(throwable)); 1074 } 1075 XMLUtils.endElement(th, "http://apache.org/cocoon/exception/1.0", "stacktrace"); 1076 XMLUtils.endElement(th, "http://apache.org/cocoon/exception/1.0", "ex:exception-report"); 1077 1078 th.endDocument(); 1079 } 1080 catch (Exception e) 1081 { 1082 // Nothing we can do anymore ... 1083 throw new ServletException(e); 1084 } 1085 } 1086 1087 /* 1088 * 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); } } 1089 */ 1090 1091 /** 1092 * In maintenance mode, send error information as SAX events.<br> 1093 * 1094 * @param ch the contentHandler receiving the message 1095 * @throws SAXException if an error occured while send SAX events 1096 */ 1097 /* 1098 * 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"); } 1099 */ 1100}