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 // add the cocoon header timestamp 638 res.addHeader("X-Cocoon-Version", Constants.VERSION); 639 640 // Error mode 641 if (_exception != null) 642 { 643 _renderError(req, res, _exception, "An error occured during Ametys initialization."); 644 return; 645 } 646 647 // Restarting 648 if (_cocoon == null) 649 { 650 _renderError(req, res, null, "Ametys is restarting. Please try again later."); 651 return; 652 } 653 654 String uri = _retrieveUri(req); 655 656 if (PluginsManager.getInstance().isSafeMode()) 657 { 658 // safe mode 659 String finalUri = uri; 660 boolean allowed = _allowedURLPattern.stream().anyMatch(p -> p.matcher(finalUri).matches()); 661 if (!allowed) 662 { 663 res.addHeader("X-Ametys-SafeMode", "true"); 664 665 Status status = PluginsManager.getInstance().getStatus(); 666 667 if (status == Status.NO_CONFIG) 668 { 669 res.sendRedirect(req.getContextPath() + "/_admin/public/first-start.html"); 670 return; 671 } 672 else if (status == Status.CONFIG_INCOMPLETE) 673 { 674 res.sendRedirect(req.getContextPath() + "/_admin/public/load-config.html"); 675 return; 676 } 677 else 678 { 679 res.sendRedirect(req.getContextPath() + "/_admin/public/safe-mode.html"); 680 return; 681 } 682 } 683 } 684 685 UserIdentity authenticatedUser = null; 686 HttpSession session = req.getSession(false); 687 if (session != null) 688 { 689 authenticatedUser = (UserIdentity) session.getAttribute(AuthenticateAction.SESSION_USERIDENTITY); 690 } 691 692 MDC.remove("user"); 693 694 if (authenticatedUser != null) 695 { 696 MDC.put("user", UserIdentity.userIdentityToString(authenticatedUser)); 697 } 698 699 MDC.put("requestURI", req.getRequestURI()); 700 701 StopWatch stopWatch = new StopWatch(); 702 HttpServletRequest request = null; 703 try 704 { 705 // used for timing the processing 706 stopWatch.start(); 707 708 _fireRequestStarted(req); 709 710 // get the request (wrapped if contains multipart-form data) 711 request = _requestFactory.getServletRequest(req); 712 713 // Process the request 714 HttpEnvironment env = new HttpEnvironment(uri, _servletContextURL.toExternalForm(), request, res, _servletContext, _context, "UTF-8", "UTF-8"); 715 env.enableLogging(new SLF4JLoggerAdapter(_logger)); 716 717 if (!_cocoon.process(env)) 718 { 719 // We reach this when there is nothing in the processing change that matches 720 // the request. For example, no matcher matches. 721 _logger.error("The Cocoon engine failed to process the request."); 722 _renderError(request, res, null, "Cocoon engine failed to process the request"); 723 } 724 } 725 catch (ResourceNotFoundException | ConnectionResetException | IOException e) 726 { 727 _logger.warn(e.toString()); 728 _renderError(request, res, e, e.getMessage()); 729 } 730 catch (Throwable e) 731 { 732 _logger.error("Internal Cocoon Problem", e); 733 _renderError(request, res, e, "Internal Cocoon Problem"); 734 } 735 finally 736 { 737 stopWatch.stop(); 738 _logger.info("'{}' processed in {} ms.", uri, stopWatch.getTime()); 739 740 try 741 { 742 if (request instanceof MultipartHttpServletRequest) 743 { 744 _logger.debug("Deleting uploaded file(s)."); 745 ((MultipartHttpServletRequest) request).cleanup(); 746 } 747 } 748 catch (IOException e) 749 { 750 _logger.error("Cocoon got an Exception while trying to cleanup the uploaded files.", e); 751 } 752 } 753 754 _fireRequestEnded(req); 755 756 if (Boolean.TRUE.equals(req.getAttribute("org.ametys.runtime.reload"))) 757 { 758 restartCocoon(req); 759 } 760 } 761 762 /** 763 * Restart cocoon 764 * @param req The http servlet request, used to read safeMode and normalMode 765 * request attribute if possible. If null, safe mode will be 766 * forced only if it was already forced. 767 */ 768 public void restartCocoon(HttpServletRequest req) 769 { 770 try 771 { 772 ConfigManager.getInstance().dispose(); 773 _disposeCocoon(); 774 _servletContext.removeAttribute("PluginsComponentManager"); 775 776 // By default force safe mode if it was already forced 777 boolean wasForcedSafeMode = PluginsManager.getInstance().getStatus() == PluginsManager.Status.SAFE_MODE_FORCED; 778 boolean forceSafeMode = wasForcedSafeMode; 779 if (req != null) 780 { 781 // Also, checks if there is some specific request attributes 782 // Force safe mode if is explicitly requested 783 // Or force safe mode it was already forced unless normal mode is explicitly requested 784 forceSafeMode = Boolean.TRUE.equals(req.getAttribute("org.ametys.runtime.reload.safeMode")); 785 if (!forceSafeMode) 786 { 787 forceSafeMode = wasForcedSafeMode && !Boolean.TRUE.equals(req.getAttribute("org.ametys.runtime.reload.normalMode")); 788 } 789 } 790 791 _servletContext.setAttribute("org.ametys.runtime.forceSafeMode", forceSafeMode); 792 793 _initAmetys(); 794 } 795 catch (Exception e) 796 { 797 setRunMode(RunMode.FATALERROR); 798 799 _logger.error("Error while reloading Ametys. Entering in error mode.", e); 800 _exception = e; 801 } 802 } 803 804 private String _retrieveUri(HttpServletRequest req) 805 { 806 String uri = req.getServletPath(); 807 808 String pathInfo = req.getPathInfo(); 809 if (pathInfo != null) 810 { 811 uri += pathInfo; 812 } 813 814 if (uri.length() > 0 && uri.charAt(0) == '/') 815 { 816 uri = uri.substring(1); 817 } 818 return uri; 819 } 820 821 @SuppressWarnings("unchecked") 822 private void _fireRequestStarted(HttpServletRequest req) 823 { 824 Collection< ? extends RequestListener> listeners = (Collection< ? extends RequestListener>) _servletContext.getAttribute(RequestListenerManager.CONTEXT_ATTRIBUTE_REQUEST_LISTENERS); 825 826 if (listeners == null) 827 { 828 return; 829 } 830 831 for (RequestListener listener : listeners) 832 { 833 listener.requestStarted(req); 834 } 835 } 836 837 @SuppressWarnings("unchecked") 838 private void _fireRequestEnded(HttpServletRequest req) 839 { 840 Collection<? extends RequestListener> listeners = (Collection< ? extends RequestListener>) _servletContext.getAttribute(RequestListenerManager.CONTEXT_ATTRIBUTE_REQUEST_LISTENERS); 841 842 if (listeners == null) 843 { 844 return; 845 } 846 847 for (RequestListener listener : listeners) 848 { 849 listener.requestEnded(req); 850 } 851 } 852 853 /** 854 * Set the maintenance status 855 * @param maintenanceStatus The new maintenance status. Cannot be null. 856 * @param forcedMainteanceInformations The informations if the mode is FORCED. Can be null. 857 */ 858 public static void setMaintenanceStatus(MaintenanceStatus maintenanceStatus, ForcedMainteanceInformations forcedMainteanceInformations) 859 { 860 if (_maintenanceStatus != maintenanceStatus) 861 { 862 LoggerFactory.getLogger(Runtime.class).debug("Switching maintenance status from " + _maintenanceStatus + " to " + maintenanceStatus); 863 } 864 865 ForcedMainteanceInformations fixedForcedMainteanceInformations = null; 866 if (forcedMainteanceInformations != null) 867 { 868 fixedForcedMainteanceInformations = new ForcedMainteanceInformations(StringUtils.defaultString(forcedMainteanceInformations.comment).replaceAll("\r?\n", "<br/>"), forcedMainteanceInformations.initiator, forcedMainteanceInformations.since); 869 } 870 871 try 872 { 873 File maintenanceFile = FileUtils.getFile(AmetysHomeHelper.getAmetysHome(), AmetysHomeHelper.AMETYS_HOME_ADMINISTRATOR_DIR, MAINTENANCE_FILENAME); 874 if (maintenanceStatus != MaintenanceStatus.FORCED) 875 { 876 if (maintenanceFile.exists()) 877 { 878 FileUtils.delete(maintenanceFile); 879 } 880 } 881 else 882 { 883 // create the result where to write 884 maintenanceFile.getParentFile().mkdirs(); 885 886 try (OutputStream os = new FileOutputStream(maintenanceFile)) 887 { 888 // create a transformer for saving sax into a file 889 TransformerHandler th = ((SAXTransformerFactory) TransformerFactory.newInstance()).newTransformerHandler(); 890 891 StreamResult result = new StreamResult(os); 892 th.setResult(result); 893 894 // create the format of result 895 Properties format = new Properties(); 896 format.put(OutputKeys.METHOD, "xml"); 897 format.put(OutputKeys.INDENT, "yes"); 898 format.put(OutputKeys.ENCODING, "UTF-8"); 899 format.put(OutputPropertiesFactory.S_KEY_INDENT_AMOUNT, "2"); 900 th.getTransformer().setOutputProperties(format); 901 902 th.startDocument(); 903 XMLUtils.startElement(th, "maintenance"); 904 if (fixedForcedMainteanceInformations != null) 905 { 906 XMLUtils.createElement(th, "comment", StringUtils.defaultString(fixedForcedMainteanceInformations.comment)); 907 XMLUtils.createElement(th, "initiator", StringUtils.defaultString(UserIdentity.userIdentityToString(fixedForcedMainteanceInformations.initiator))); 908 if (fixedForcedMainteanceInformations.since != null) 909 { 910 XMLUtils.createElement(th, "since", DateUtils.zonedDateTimeToString(fixedForcedMainteanceInformations.since)); 911 } 912 } 913 XMLUtils.endElement(th, "maintenance"); 914 th.endDocument(); 915 } 916 } 917 } 918 catch (Exception e) 919 { 920 throw new RuntimeException("Cannot change maintenance status", e); 921 } 922 923 _maintenanceStatus = maintenanceStatus; 924 _forcedMainteanceInformations = fixedForcedMainteanceInformations; 925 926 try 927 { 928 PluginsComponentManager pluginCM = (PluginsComponentManager) _servletContext.getAttribute("PluginsComponentManager"); 929 ObservationManager observationManager = (ObservationManager) pluginCM.lookup(ObservationManager.ROLE); 930 CurrentUserProvider currentUserProvider = (CurrentUserProvider) pluginCM.lookup(CurrentUserProvider.ROLE); 931 if (observationManager != null) 932 { 933 observationManager.notify(new Event(ObservationConstants.EVENT_RUNTIME_MAINTENANCE, currentUserProvider.getUser(), Map.of())); 934 } 935 } 936 catch (Exception e) 937 { 938 // Safe mode 939 } 940 } 941 942 /** 943 * Get the current maintenance status 944 * @return The maintenance status 945 */ 946 public static MaintenanceStatus getMaintenanceStatus() 947 { 948 if (_maintenanceStatus == null) 949 { 950 File maintenanceFile = FileUtils.getFile(AmetysHomeHelper.getAmetysHome(), AmetysHomeHelper.AMETYS_HOME_ADMINISTRATOR_DIR, MAINTENANCE_FILENAME); 951 952 if (!maintenanceFile.exists()) 953 { 954 _maintenanceStatus = MaintenanceStatus.NONE; 955 } 956 else 957 { 958 _maintenanceStatus = MaintenanceStatus.FORCED; 959 try 960 { 961 Configuration configuration = new DefaultConfigurationBuilder().buildFromFile(maintenanceFile); 962 _forcedMainteanceInformations = new ForcedMainteanceInformations(configuration.getChild("comment").getValue(""), 963 UserIdentity.stringToUserIdentity(configuration.getChild("initiator").getValue(null)), 964 DateUtils.parseZonedDateTime(configuration.getChild("since").getValue(null))); 965 } 966 catch (ConfigurationException | SAXException | IOException e) 967 { 968 _logger.error("Cannot read the maintenance status file", e); 969 _forcedMainteanceInformations = null; 970 } 971 } 972 } 973 return _maintenanceStatus; 974 } 975 976 /** 977 * When maintenance status is forced, this comment may explains why 978 * @return The comment. Can be null even in forced mode. 979 */ 980 public static ForcedMainteanceInformations getMaintenanceStatusForcedInformations() 981 { 982 return _forcedMainteanceInformations; 983 } 984 985 /** 986 * Set the run mode 987 * @param mode the running mode 988 */ 989 public static void setRunMode(RunMode mode) 990 { 991 if (_runMode != mode) 992 { 993 LoggerFactory.getLogger(RuntimeServlet.class).debug("Switching run mode from " + _runMode + " to " + mode); 994 } 995 996 _runMode = mode; 997 } 998 999 /** 1000 * Get the run mode 1001 * @return the current run mode 1002 */ 1003 public static RunMode getRunMode() 1004 { 1005 return _runMode; 1006 } 1007 1008 private void _renderError(HttpServletRequest req, HttpServletResponse res, Throwable throwable, String message) throws ServletException 1009 { 1010 ServletConfig config = getServletConfig(); 1011 1012 if (config == null) 1013 { 1014 throw new ServletException("Cannot access to ServletConfig"); 1015 } 1016 1017 try 1018 { 1019 ServletOutputStream os = res.getOutputStream(); 1020 String path = req.getRequestURI().substring(req.getContextPath().length()); 1021 1022 // Favicon associated with the error page. 1023 if (path.equals("/favicon.ico")) 1024 { 1025 try (InputStream is = getClass().getResourceAsStream("favicon.ico")) 1026 { 1027 res.setStatus(200); 1028 res.setContentType(config.getServletContext().getMimeType("favicon.ico")); 1029 1030 IOUtils.copy(is, os); 1031 1032 return; 1033 } 1034 } 1035 1036 res.setStatus(500); 1037 res.setContentType("text/html; charset=UTF-8"); 1038 1039 SAXTransformerFactory saxFactory = (SAXTransformerFactory) TransformerFactory.newInstance(); 1040 TransformerHandler th; 1041 1042 try (InputStream is = getClass().getResourceAsStream("fatal.xsl")) 1043 { 1044 StreamSource errorSource = new StreamSource(is); 1045 th = saxFactory.newTransformerHandler(errorSource); 1046 } 1047 1048 Properties format = new Properties(); 1049 format.put(OutputKeys.METHOD, "xml"); 1050 format.put(OutputKeys.ENCODING, "UTF-8"); 1051 format.put(OutputKeys.DOCTYPE_SYSTEM, "about:legacy-compat"); 1052 1053 th.getTransformer().setOutputProperties(format); 1054 1055 th.getTransformer().setParameter("code", 500); 1056 th.getTransformer().setParameter("realPath", config.getServletContext().getRealPath("/")); 1057 th.getTransformer().setParameter("contextPath", req.getContextPath()); 1058 1059 StreamResult result = new StreamResult(os); 1060 th.setResult(result); 1061 1062 th.startDocument(); 1063 1064 XMLUtils.startElement(th, "http://apache.org/cocoon/exception/1.0", "exception-report"); 1065 XMLUtils.startElement(th, "http://apache.org/cocoon/exception/1.0", "message"); 1066 XMLUtils.data(th, message); 1067 XMLUtils.endElement(th, "http://apache.org/cocoon/exception/1.0", "message"); 1068 1069 XMLUtils.startElement(th, "http://apache.org/cocoon/exception/1.0", "stacktrace"); 1070 if (throwable != null) 1071 { 1072 XMLUtils.data(th, ExceptionUtils.getStackTrace(throwable)); 1073 } 1074 XMLUtils.endElement(th, "http://apache.org/cocoon/exception/1.0", "stacktrace"); 1075 XMLUtils.endElement(th, "http://apache.org/cocoon/exception/1.0", "ex:exception-report"); 1076 1077 th.endDocument(); 1078 } 1079 catch (Exception e) 1080 { 1081 // Nothing we can do anymore ... 1082 throw new ServletException(e); 1083 } 1084 } 1085 1086 /* 1087 * 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); } } 1088 */ 1089 1090 /** 1091 * In maintenance mode, send error information as SAX events.<br> 1092 * 1093 * @param ch the contentHandler receiving the message 1094 * @throws SAXException if an error occured while send SAX events 1095 */ 1096 /* 1097 * 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"); } 1098 */ 1099}