diff --git a/build.gradle b/build.gradle index f55cec7c1f..0e4ec62d90 100644 --- a/build.gradle +++ b/build.gradle @@ -42,6 +42,7 @@ apply from: rootProject.file('gradle/tasks/portlet.gradle') apply from: rootProject.file('gradle/tasks/docker.gradle') apply from: rootProject.file('gradle/tasks/perf.gradle') apply from: rootProject.file('gradle/tasks/playwright.gradle') +apply from: rootProject.file('gradle/tasks/aop.gradle') /* * If gradle/tasks/custom.gradle exists, tasks from that diff --git a/custom/aop/build.gradle b/custom/aop/build.gradle new file mode 100644 index 0000000000..70319e400c --- /dev/null +++ b/custom/aop/build.gradle @@ -0,0 +1,47 @@ + + +buildscript { + repositories { + mavenLocal() + mavenCentral() + } + dependencies { + } +} + +plugins { + id "io.freefair.aspectj.post-compile-weaving" version "5.3.3.3" +} + +apply plugin: 'java' + +repositories { + mavenLocal() + mavenCentral() +} + +// Needed by aspectjweaver to prevent lint dependency failure +configurations.testImplementation { + exclude(group: "org.aspectj", module: "aspectjrt") +} + +// Needed by aspectjweaver to prevent lint dependency failure +configurations.runtimeOnly { + exclude(group: "org.aspectj", module: "aspectjrt") +} + +dependencies { +// needs javax.mail.Authenticator for tomcat-embed-core + compile("javax.mail:mail:1.4.7") + + // needed for the post-compile weaving of HttpSessionAspect + // we assume that tomcat version for uPortal and uPortal-start projects + // are the same. inpath() will create an exploded version of tomcat-embed-core + // within the WEB-INFO/classes directory. The only class that is modified + // is org.apache.catalina.session.StandardSessionFacade, which implements + // HttpSession and is the primary class that calls HttpSession.setAttributes + // uPortal-start will copy that class into the appropriate location for + // uPortal-start's tomcat location + compileOnly "org.apache.tomcat.embed:tomcat-embed-core:${tomcatVersion}" + inpath "org.apache.tomcat.embed:tomcat-embed-core:${tomcatVersion}" +} \ No newline at end of file diff --git a/custom/aop/src/main/java/org/apereo/portal/context/HttpSessionAspect.java b/custom/aop/src/main/java/org/apereo/portal/context/HttpSessionAspect.java new file mode 100644 index 0000000000..140fc768db --- /dev/null +++ b/custom/aop/src/main/java/org/apereo/portal/context/HttpSessionAspect.java @@ -0,0 +1,60 @@ +/** + * Licensed to Apereo under one or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information regarding copyright ownership. Apereo + * licenses this file to you under the Apache License, Version 2.0 (the "License"); you may not use + * this file except in compliance with the License. You may obtain a copy of the License at the + * following location: + * + *
http://www.apache.org/licenses/LICENSE-2.0 + * + *
Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apereo.portal.context; + +import java.io.Serializable; +import java.util.logging.Level; +import java.util.logging.Logger; +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Before; + +/** + * HttpSessionAspect is responsible for instrumenting classes that implement HttpSession with + * logging data, to determine if the instance of HttpSession is trying to add a class that is not + * serialzable to the session. HttpSessionAspect is not needed at this point for any production + * functionality; it is only used to identify classes that need to be modified to be serializable. + * + *
This is class is being included in the uPortal-webapp package so that as part of the uPortal + * gradle build, the needed classes are available as part of the uPortal packaging. uPortal-start + * will then pull needed instrumented classes and include them in the appropriate place within the + * tomcat instance. This assumes that uPortal-start is being used to manage the installation into + * the appropriate tomcat server. + * + * @author mgillian + */ +@Aspect +public class HttpSessionAspect { + private static final Logger log = Logger.getLogger(HttpSessionAspect.class.getName()); + + @Before("execution(* javax.servlet.http.HttpSession+.setAttribute(..))") + public void loggingAdvice(JoinPoint joinPoint) { + Object[] args = joinPoint.getArgs(); + if (args.length < 2) { + log.log(Level.SEVERE, "HttpSession.setAttribute does not have at least 2 parameters"); + return; + } + + // HttpSession.setAttribute takes two parameters: + // key: a string name for the parameter, not important for this use case + // value: the object to be stored, which must be serializable + Object value = args[1]; + if (!(value instanceof Serializable)) { + log.log(Level.INFO, "value [" + value.getClass().getName() + "] is NOT serializable"); + } else { + log.log(Level.FINE, "value [" + value.getClass().getName() + "] is serializable"); + } + } +} diff --git a/etc/tomcat/conf/logging.properties b/etc/tomcat/conf/logging.properties new file mode 100644 index 0000000000..514d2c585b --- /dev/null +++ b/etc/tomcat/conf/logging.properties @@ -0,0 +1,81 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +handlers = 1catalina.org.apache.juli.AsyncFileHandler, 2localhost.org.apache.juli.AsyncFileHandler, 3manager.org.apache.juli.AsyncFileHandler, 4host-manager.org.apache.juli.AsyncFileHandler, 5aop-serializer.org.apache.juli.AsyncFileHandler, java.util.logging.ConsoleHandler + +.handlers = 1catalina.org.apache.juli.AsyncFileHandler, 5aop-serializer.org.apache.juli.AsyncFileHandler, java.util.logging.ConsoleHandler + +############################################################ +# Handler specific properties. +# Describes specific configuration info for Handlers. +############################################################ + +1catalina.org.apache.juli.AsyncFileHandler.level = FINE +1catalina.org.apache.juli.AsyncFileHandler.directory = ${catalina.base}/logs +1catalina.org.apache.juli.AsyncFileHandler.prefix = catalina. +1catalina.org.apache.juli.AsyncFileHandler.encoding = UTF-8 + +2localhost.org.apache.juli.AsyncFileHandler.level = FINE +2localhost.org.apache.juli.AsyncFileHandler.directory = ${catalina.base}/logs +2localhost.org.apache.juli.AsyncFileHandler.prefix = localhost. +2localhost.org.apache.juli.AsyncFileHandler.encoding = UTF-8 + +3manager.org.apache.juli.AsyncFileHandler.level = FINE +3manager.org.apache.juli.AsyncFileHandler.directory = ${catalina.base}/logs +3manager.org.apache.juli.AsyncFileHandler.prefix = manager. +3manager.org.apache.juli.AsyncFileHandler.encoding = UTF-8 + +4host-manager.org.apache.juli.AsyncFileHandler.level = FINE +4host-manager.org.apache.juli.AsyncFileHandler.directory = ${catalina.base}/logs +4host-manager.org.apache.juli.AsyncFileHandler.prefix = host-manager. +4host-manager.org.apache.juli.AsyncFileHandler.encoding = UTF-8 + +java.util.logging.ConsoleHandler.level = FINE +java.util.logging.ConsoleHandler.formatter = org.apache.juli.OneLineFormatter +java.util.logging.ConsoleHandler.encoding = UTF-8 + +5aop-serializer.org.apache.juli.AsyncFileHandler.level = INFO +5aop-serializer.org.apache.juli.AsyncFileHandler.directory = ${catalina.base}/logs +5aop-serializer.org.apache.juli.AsyncFileHandler.prefix = aop-serializer. +5aop-serializer.org.apache.juli.AsyncFileHandler.encoding = UTF-8 +############################################################ +# Facility specific properties. +# Provides extra control for each logger. +############################################################ + +org.apache.catalina.core.ContainerBase.[Catalina].[localhost].level = INFO +org.apache.catalina.core.ContainerBase.[Catalina].[localhost].handlers = 2localhost.org.apache.juli.AsyncFileHandler + +org.apache.catalina.core.ContainerBase.[Catalina].[localhost].[/manager].level = INFO +org.apache.catalina.core.ContainerBase.[Catalina].[localhost].[/manager].handlers = 3manager.org.apache.juli.AsyncFileHandler + +org.apache.catalina.core.ContainerBase.[Catalina].[localhost].[/host-manager].level = INFO +org.apache.catalina.core.ContainerBase.[Catalina].[localhost].[/host-manager].handlers = 4host-manager.org.apache.juli.AsyncFileHandler + +org.apereo.portal.[Catalina].[localhost].[/aop-serializer].level = INFO +org.apereo.portal.[Catalina].[localhost].[/aop-serializer].handlers = 5aop-serializer.org.apache.juli.AsyncFileHandler + +# For example, set the org.apache.catalina.util.LifecycleBase logger to log +# each component that extends LifecycleBase changing state: +#org.apache.catalina.util.LifecycleBase.level = FINE + +# To see debug messages in TldLocationsCache, uncomment the following line: +#org.apache.jasper.compiler.TldLocationsCache.level = FINE + +# To see debug messages for HTTP/2 handling, uncomment the following line: +#org.apache.coyote.http2.level = FINE + +# To see debug messages for WebSocket handling, uncomment the following line: +#org.apache.tomcat.websocket.level = FINE diff --git a/gradle.properties b/gradle.properties index 7fe2581675..b1397280b7 100644 --- a/gradle.properties +++ b/gradle.properties @@ -19,7 +19,7 @@ resourceServerVersion=1.0.48 # Includes newer front-end dependencies and web components resourceServerWebjarVersion=1.5.0 simpleContentPortletVersion=3.4.0 -uPortalVersion=5.14.0 +uPortalVersion=5.14.1-SNAPSHOT webProxyPortletVersion=2.4.0 # Versions of WebJars included with resource-server @@ -32,6 +32,7 @@ tomcatVersion=8.5.85 servletApiDependency=javax.servlet:javax.servlet-api:3.0.1 ccppVersion=1.0 personDirectoryVersion=1.8.14 +aspectjVersion=1.9.9.1 # Dependencies driven by Pluto Portlet Engine version plutoVersion=2.1.0-M3 @@ -59,6 +60,10 @@ casXercesImplVersion=2.12.1 org.gradle.parallel=true +# if true, run AOP against HttpSession implementation and +# add code to capture objects written to session that are not serializable +deploySessionReplicationAop=false + # These properties control image & version settings of Docker images built by the CLI. # Customize the first in this file, and the second with a -P argument # (e.g. $ ./gradlew dockerBuildImages -PdockerImageVersion=latest) diff --git a/gradle/tasks/aop.gradle b/gradle/tasks/aop.gradle new file mode 100644 index 0000000000..42a82f3558 --- /dev/null +++ b/gradle/tasks/aop.gradle @@ -0,0 +1,91 @@ +repositories { + mavenLocal() + mavenCentral() +} + +apply plugin: 'java' + +sourceSets { + main { + java { + srcDirs = ["custom/aop/src/main/java", "custom/aop/src", "aop/src/main/java", "aop/src", "src/main/java", "src"] + } + } +} + +dependencies { + compileOnly "org.aspectj:aspectjrt:${aspectjVersion}" + runtimeOnly 'org.aspectj:aspectjweaver:1.9.9.1' +} + +/* + * deployAopArchives is a custom task that moves HttpSession instrumented classes and needed + * dependencies to the correct place within ./gradle/tomcat. There are three primary parts: + * 1. Build a jar that contains two cusomt classes, HttpSessionAspect and StandardSessionFacade + * and puts them into the .gradle/tomcat/lib folder in their own jar. HttpSession's implementing class + * must be located in tomcat's lib folder or it won't be found, and the aspect must be with it. + * 2. Put the aspectjweaver.1.9.9.1 jar in the same lib folder + * 3. Update the catalina.jar that also lives in this lib folder by deleting the now duplicated + * StandardSessionFacade. The current implementation creates a duplicate jar with the missing file + * and deletes the original file to prevent conflict. Once this is done, it's hard to recover without + * doing a tomcatInstall, so this may need additinoal work, if this function is run. + * If this function is not run, uPortal on tomcat will run as normal. + */ +task deployAopArchives() { + dependsOn ':portalProperties' + dependsOn ':downloadDependencies' + dependsOn ':buildAopJar' + dependsOn ':buildCatalinaAopJar' + doLast { + String serverHome = rootProject.ext['buildProperties'].getProperty('server.home') + def path = "${project.projectDir}/custom/aop/build/runtime" + copy { + from path + include "*.jar" + into "${serverHome}/lib" + } + delete "${serverHome}/lib/catalina.jar" + } +} + +task downloadDependencies(type :Copy) { + def path = "${project.projectDir}/custom/aop/build/runtime" + from sourceSets.main.runtimeClasspath + into path + + doFirst { + ant.delete(dir: path) + ant.mkdir(dir: path) + } +} + +task buildAopJar(type: Zip) { + dependsOn ":custom:aop:compileJava" + + description 'Build AOP Archive for HttpSession testing' + +// def path = "${project.projectDir}/custom/aop/build/runtime" + def path = "${project.projectDir}/custom/aop/build/classes/java/main" + def destinationPath = "${project.projectDir}/custom/aop/build/runtime" + from (path) { + include("org/apereo/portal/context/HttpSessionAspect.class") + include("org/apache/catalina/session/StandardSessionFacade.class") + } + archiveName 'uPortalAopArchive.jar' + destinationDir(file(destinationPath)) +} + +task buildCatalinaAopJar(type: Zip) { + dependsOn ':tomcatInstall' + // I'm not sure why, but :portalProperties does not expose server.home + // like it does in other functions. This is directly related to type: Zip + // in the function designation. If the tomcat server is located in another location, + // this property will need to be updated + String serverHome = ".gradle/tomcat" + from zipTree("${serverHome}/lib/catalina.jar").matching { + exclude 'org/apache/catalina/session/StandardSessionFacade.class' + } + archiveName 'catalina-aop.jar' + destinationDir(file("${serverHome}/lib")) +} + diff --git a/gradle/tasks/tomcat.gradle b/gradle/tasks/tomcat.gradle index 19c1994437..b98f1fbf4e 100644 --- a/gradle/tasks/tomcat.gradle +++ b/gradle/tasks/tomcat.gradle @@ -78,6 +78,13 @@ task tomcatConfig() { description 'Configures Tomcat servlet container, used to update an existing Tomcat install' dependsOn ':portalProperties' + // when running the uPortal tomcatDeploy, you may also deploy session replication code + // if deploySessionReplicationAop is true in gradle.properties + def isDeploySessionReplicationEnabled = project.rootProject.properties.deploySessionReplicationAop.toBoolean() + if (isDeploySessionReplicationEnabled) { + dependsOn ":deployAopArchives" + } + doLast { String serverHome = rootProject.ext['buildProperties'].getProperty('server.home') diff --git a/overlays/uPortal/build.gradle b/overlays/uPortal/build.gradle index f2780973e0..61a6a042a9 100644 --- a/overlays/uPortal/build.gradle +++ b/overlays/uPortal/build.gradle @@ -162,6 +162,13 @@ ext { shellDir = rootProject.file("${buildDir}/shell") } +// This is only needed because during uPortal's build and AOP weaving, +// it modifies the class org.apache.catalina.session.StandardSessionFacade and +// embeds AOP functionality. +dependencies { + implementation "org.aspectj:aspectjweaver:1.9.9.1" +} + dataInit { description 'Drop and recreate uPortal tables and reimport data' diff --git a/settings.gradle b/settings.gradle index 45a3b7142a..cf216522c4 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,5 +1,7 @@ rootProject.name = 'uPortal-start' +include 'custom:aop' + include 'overlays:Announcements' include 'overlays:basiclti-portlet' include 'overlays:BookmarksPortlet'