diff --git a/.classpath b/.classpath deleted file mode 100644 index 540156aa..00000000 --- a/.classpath +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/.github/FUNDING.YML b/.github/FUNDING.YML new file mode 100644 index 00000000..7e59d133 --- /dev/null +++ b/.github/FUNDING.YML @@ -0,0 +1 @@ +patreon: ortussolutions diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..9ccc4993 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,62 @@ +# This workflow uses actions that are not certified by GitHub. +# They are provided by a third-party and are governed by separate terms of service, privacy policy, and support documentation. +# This workflow will build a package using Gradle and then publish it to GitHub packages when a release is created +# For more information see: https://github.com/actions/setup-java/blob/main/docs/advanced-usage.md#Publishing-using-gradle + +name: Runwar CI + +# If your target is master or develop branch +on: + push: + branches: [ master, develop ] + pull_request: + branches: [ master, develop ] + + # We get the required keys + workflow_call: + secrets: + AWS_ACCESS_KEY: + required: true + AWS_ACCESS_SECRET: + required: true + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Checkout Repository + uses: actions/checkout@v2 + with: + fetch-depth: 0 + + - name: Set up JDK 11 + uses: actions/setup-java@v2 + with: + java-version: '11' + distribution: 'adopt' + + # Do the build + - name: Build with Gradle + run: ./gradlew --refresh-dependencies + + # Creating S3 destination folder name based on the version and copying just the jar into temp directory + - name: Change folder source + run: | + echo "folderName=`basename -s .jar dist/libs/runwar-*.jar | sed -e 's/runwar-//'`" >> $GITHUB_ENV + mkdir dist/libs/temp && cp dist/libs/runwar-*.jar dist/libs/temp + + - name: Upload JAR to S3 + + # Run the upload on master branch + if: ${{ github.ref == 'refs/heads/master' || github.ref == 'refs/heads/develop' }} + + uses: jakejarvis/s3-sync-action@master + with: + args: --acl public-read + env: + AWS_S3_BUCKET: "downloads.ortussolutions.com" + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_ACCESS_SECRET }} + SOURCE_DIR: "dist/libs/temp/" + DEST_DIR: "cfmlprojects/runwar/${{env.folderName}}" diff --git a/.gitignore b/.gitignore index 74616b7e..92f5ea39 100644 --- a/.gitignore +++ b/.gitignore @@ -1,29 +1,31 @@ -/bin -/dist -/lib -/build -/undertow -/testlibs -src/test/resources/war/simple.war/WEB-INF/urlrewrite.xml -src/test/resources/war/simple.war/WEB-INF/logs/ -src/test/resources/work/ -.gradle -gradle-app.setting -gradle/jvm -!gradle-wrapper.jar -.gradletasknamecache -.nb-gradle - -#ignore the various too-lazy-to-fix-and-lame-anyway bash tests -gradle/*.sh -!gradle/jvm-setup.sh -testwars - -#IDE files -*.project -.settings -.idea -*.iml -*.ipr -*.iws -/settings.xml \ No newline at end of file +/bin +/dist +/lib +/libs +/build +/undertow +/testlibs +src/test/resources/war/simple.war/WEB-INF/urlrewrite.xml +src/test/resources/war/simple.war/WEB-INF/logs/ +src/test/resources/work/ +.gradle +gradle-app.setting +gradle/jvm +!gradle-wrapper.jar +.gradletasknamecache +.nb-gradle + +#ignore the various too-lazy-to-fix-and-lame-anyway bash tests +gradle/*.sh +!gradle/jvm-setup.sh +testwars + +#IDE files +*.project +.settings +.idea +*.iml +*.ipr +*.iws +/settings.xml +.classpath diff --git a/build.gradle b/build.gradle index 14bc6082..fcebb29c 100644 --- a/build.gradle +++ b/build.gradle @@ -6,8 +6,6 @@ plugins { id 'com.github.johnrengelman.shadow' version '4.0.2' id "com.diffplug.gradle.oomph.ide" version "3.17.1" id "se.bjurr.gitchangelog.git-changelog-gradle-plugin" version "1.55" - // id "com.fizzed.rocker" version "0.24.0" -// id 'com.zyxist.chainsaw' version '0.3.1' } defaultTasks 'clean', 'shadow' @@ -24,7 +22,6 @@ apply from: 'gradle/config.gradle' sourceCompatibility = 1.8 targetCompatibility = 1.8 tasks.withType(JavaCompile) { - options.bootstrapClasspath = fileTree(include: ['**/jce.jar','**/rt.jar'], dir: "${System.properties['java.home']}/") options.encoding = 'UTF-8' options.compilerArgs += '-Xlint:unchecked' options.compilerArgs += '-Xlint:deprecation' @@ -45,7 +42,6 @@ wrapper{ javadoc { options.addStringOption('Xdoclint:none', '-quiet') -// options.addBooleanOption('html5', true) } tasks.withType(Test) { @@ -54,23 +50,9 @@ tasks.withType(Test) { sourceSets { main { output.dir(generatedResources, builtBy: 'generateVersionFile') - // rocker { - // srcDir('src/main/rocker') - // } } } -/* - processResources { - filesMatching('properties/*.properties') { - filter ReplaceTokens, tokens: [ - 'build.version': project.property("version"), - 'build.timestamp': project.buildTimestamp - ] - } - } - */ - test { // Enable JUnit 5 (Gradle 4.6+). useJUnitPlatform { /*jvmArgs "-verbose:class"*/ } @@ -85,21 +67,6 @@ test { testLogging.showStandardStreams = true } -/*rocker { - failOnError true - skipTouch true - // must not be empty when skipTouch is equal to false - touchFile "" - javaVersion '1.8' - extendsClass null - extendsModelClass null - optimize null - discardLogicWhitespace true - targetCharset null - suffixRegex null - postProcessing null -}*/ - // create an OSGI manifest apply plugin: 'com.diffplug.gradle.osgi.bndmanifest' osgiBndManifest { copyTo 'META-INF/MANIFEST.MF' } @@ -133,21 +100,14 @@ task sourceJar(type: Jar) { from sourceSets.main.allSource } -/* -tasks.withType(JavaCompile) { - options.compilerArgs.addAll([ - "--add-exports", - "java.base/jdk.internal.misc=ALL-UNNAMED", - "--add-exports", - "java.base/jdk.internal.misc=runwar" - ]) -} -*/ - shadowJar { archiveClassifier = null mergeServiceFiles() exclude('**/*.java') + manifest { + attributes( + "Multi-Release": true ) + } } apply from: 'gradle/proguard.gradle' apply from: 'gradle/maven.gradle' @@ -158,27 +118,11 @@ task copyToLib(type: Copy) { from configurations.runtime } -/* - task generateGitChangelogTemplateWithGitHubIssues(type: se.bjurr.gitchangelog.plugin.gradle.GitChangelogTask) { - gitHubApi = "https://api.github.com/repos/Ortus-Solutions/runwar" - gitHubToken = System.properties['GITHUB_OAUTH2TOKEN'] - gitHubIssuePattern = "#([0-9]*)" - file = new File("${buildDir}/CHANGELOG.md") - templateContent = file('gradle/changelog/changelog.mustache').getText('UTF-8') - } - */ - task gitChangelogTask() { doLast { GitChangelogApi builder; builder = gitChangelogApiBuilder() - /* - println builder.withGitHubApi("https://api.github.com/repos/Ortus-Solutions/runwar") - .withGitHubToken(System.properties['GITHUB_OAUTH2TOKEN']) - .withGitHubIssuePattern("#([0-9]*)") - .withTemplatePath('gradle/changelog/changelog.mustache') - .render(); - */ + println builder.withFromRef("master") .withToRef("HEAD") .withGitHubApi("https://api.github.com/repos/Ortus-Solutions/runwar") @@ -187,7 +131,7 @@ task gitChangelogTask() { .withNoIssueName("These commits have no issue in their commit comment") .withTemplateContent(""" # Changelog - + {{#tags}} ## {{name}} {{^hasIssue}} @@ -197,32 +141,24 @@ task gitChangelogTask() { {{#hasLink}} [{{issue}}]({{link}}) {{title}} {{#commits}} - [{{hash}}] {{{messageTitle}}} (https://github.com/{{ownerName}}/{{repoName}}/commit/{{hash}}) + [{{hash}}] {{{messageTitle}}} (https://github.com/{{ownerName}}/{{repoName}}/commit/{{hash}}) {{/commits}} {{/hasLink}} {{/issues}} - + {{#issues}} {{^hasLink}} Commits: {{#commits}} - [{{hash}}] {{{messageTitle}}} (https://github.com/{{ownerName}}/{{repoName}}/commit/{{hash}}) + [{{hash}}] {{{messageTitle}}} (https://github.com/{{ownerName}}/{{repoName}}/commit/{{hash}}) {{/commits}} {{/hasLink}} {{/issues}} - + {{/tags}} """.stripIndent()) .render() - /* - println builder.withFromRef("refs/tags/3.7.0") - .withToRef("refs/tags/3.8.0") - .withTemplatePath('gradle/changelog/changelog.mustache') - .render() - */ - // file = new File("${buildDir}/CHANGELOG.md"); - // templateContent = file('gradle/changelog/changelog.mustache').getText('UTF-8'); } } diff --git a/dependencies.gradle b/dependencies.gradle index ead1e6f5..ef69eb49 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -1,6 +1,6 @@ buildscript { repositories { maven { url "https://plugins.gradle.org/m2/" } } - dependencies { classpath "com.github.jengelman.gradle.plugins:shadow:4.0.2" } + dependencies { classpath "com.github.jengelman.gradle.plugins:shadow:5.2.0" } } import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar @@ -69,24 +69,18 @@ dependencies { compile(files(dorkboxUberJar.archivePath){ builtBy dorkboxUberJar }) // java 9+ compat - //compile 'com.sun.activation:javax.activation:1.2.0' compile group: 'org.graylog.repackaged', name: 'os-platform-finder', version: '1.2.3' // configuration -// compile group: 'net.minidev', name: 'json-smart', version: '2.3' compile group: 'net.minidev', name: 'json-smart-mini', version: '1.0.8' - //compile group: 'net.sf.jopt-simple', name: 'jopt-simple', version: '5.0-beta-1' compile group: 'commons-cli', name: 'commons-cli', version: '1.2' compile group: 'org.jooq', name: 'joox', version: '1.2.0' // logging -// compile("log4j:log4j:1.2.17") - compile("org.slf4j:slf4j-log4j12:1.7.30") - // compile("org.jboss.logmanager:jboss-logmanager:2.1.0.Final") + compile("org.apache.logging.log4j:log4j-slf4j-impl:2.17.1") + compile("org.apache.logging.log4j:log4j-core:2.17.1") compile("org.jboss.logging:jboss-logging:3.4.1.Final") compile("org.jboss.logging:jboss-logging-annotations:2.2.1.Final") - // compile("org.apache.logging.log4j:log4j-core:2.10.0") - // compile("org.apache.logging.log4j:log4j-slf4j-impl:2.10.0") // forked version of tuckey for now //compile("org.cfmlprojects:urlrewritefilter:${project.urlRewriteVersion}") ///missing version of forked project @@ -102,25 +96,15 @@ dependencies { // ssl compile("org.bouncycastle:bcpkix-jdk15on:1.57") - // version compare - //compile("com.vdurmont:semver4j:2.2.0") { transitive = false } // ansi colors for logging system output compile("com.jcabi:jcabi-log:0.18.1") { transitive = false } if( org.gradle.util.VersionNumber.parse( System.getProperty( 'java.version' ) ) > org.gradle.util.VersionNumber.parse( '1.8' ) ) { - // needed for hibernate annotations since not included in later JDKs - // the javax packages that are removed in Java 11+ -// implementation( 'org.jboss.spec.javax.xml.ws:jboss-jaxws-api_2.3_spec:1.0.0.Final') -// implementation( 'org.jboss.spec.javax.xml.soap:jboss-saaj-api_1.3_spec:1.0.6.Final') implementation 'org.jboss.spec.javax.xml.bind:jboss-jaxb-api_2.3_spec:1.0.1.Final' -// implementation 'javax.jws:jsr181-api:1.0-MR1' } //runtimeOnly 'org.cfmlprojects:javashim:1.0.1' // for Railo (sun.misc.VM) implementation 'org.jboss.spec.javax.xml.bind:jboss-jaxb-api_2.3_spec:1.0.1.Final' - // -// compile 'net.bytebuddy:byte-buddy:1.9.2' -// compile 'net.bytebuddy:byte-buddy-agent:1.9.2' // tests testCompile("org.apache.httpcomponents:httpclient:4.5.12") testCompile("org.apache.httpcomponents:httpmime:4.5.12") @@ -130,12 +114,6 @@ dependencies { testCompile('org.junit.jupiter:junit-jupiter-params:5.2.0') } -//configurations.compile.withDependencies {deps -> -// println "Resolving dependencies" -// dependencies { -// compile "log4j:log4j:1.2.17" -// } -//} diff --git a/gradle/version b/gradle/version index 7f071f30..fb5f09b3 100644 --- a/gradle/version +++ b/gradle/version @@ -1 +1 @@ -4.5.2-8c518409301275042e75fd5e9ea11bc1910ae186-21dc893be75d9b4bea888e499dd534d30df8c100 \ No newline at end of file +4.7.3-8cded729fa5fd97ad7d29c14702230eade9da17c-70d36e81a8b364649708b309d1741b798c6a36b1 \ No newline at end of file diff --git a/src/main/java/runwar/BrowserOpener.java b/src/main/java/runwar/BrowserOpener.java index 3d360d1f..14799ddc 100644 --- a/src/main/java/runwar/BrowserOpener.java +++ b/src/main/java/runwar/BrowserOpener.java @@ -29,7 +29,6 @@ public static void openURL(String url, String preferred_browser) { return; } try { - RunwarLogger.LOG.info(url); if (osName.startsWith("Mac OS")) { if (!preferred_browser.equalsIgnoreCase("default")) { try { diff --git a/src/main/java/runwar/LifecyleHandler.java b/src/main/java/runwar/LifecyleHandler.java index c58f2779..4cf320e0 100644 --- a/src/main/java/runwar/LifecyleHandler.java +++ b/src/main/java/runwar/LifecyleHandler.java @@ -21,7 +21,7 @@ public class LifecyleHandler implements HttpHandler { } @Override - public void handleRequest(final HttpServerExchange inExchange) { + public void handleRequest(final HttpServerExchange inExchange) throws Exception { inExchange.addExchangeCompleteListener((httpServerExchange, nextListener) -> { if ( serverOptions.debug() && httpServerExchange.getStatusCode() > 399) { @@ -43,7 +43,7 @@ public void handleRequest(final HttpServerExchange inExchange) { // This is to ensure some sort of error page always makes it back to the browser. if (exchange.getStatusCode() != 200) { - final String errorPage = "Error" + StatusCodes.getReason( exchange.getStatusCode() ) + ""; + final String errorPage = "Error" + StatusCodes.getReason( exchange.getStatusCode() ) + "
(Check logs for more info)"; exchange.getResponseHeaders().put(Headers.CONTENT_LENGTH, "" + errorPage.length()); exchange.getResponseHeaders().put(Headers.CONTENT_TYPE, "text/html"); Sender sender = exchange.getResponseSender(); @@ -53,13 +53,16 @@ public void handleRequest(final HttpServerExchange inExchange) { return false; }); - try { - - CONTEXT_LOG.debug("requested: '" + Server.fullExchangePath(inExchange) + "'"); - - next.handleRequest(inExchange); - } catch (Exception e) { - RunwarLogger.CONTEXT_LOG.error("LifecyleHandler handleRequest triggered", e); - } + + CONTEXT_LOG.debug("requested: '" + Server.fullExchangePath(inExchange) + "'"); + + // This allows the exchange to be available to the IO thread. + Server.setCurrentExchange(inExchange); + + next.handleRequest(inExchange); + + // Clean up after + Server.setCurrentExchange(null); + } } \ No newline at end of file diff --git a/src/main/java/runwar/RunwarConfigurer.java b/src/main/java/runwar/RunwarConfigurer.java index 1be77f13..06281cb3 100644 --- a/src/main/java/runwar/RunwarConfigurer.java +++ b/src/main/java/runwar/RunwarConfigurer.java @@ -57,24 +57,29 @@ void configureServerResourceHandler(DeploymentInfo servletBuilder) { File webInfDir = serverOptions.webInfDir(); String cfengine = serverOptions.cfEngineName(); String cfusionDir = new File(webInfDir,"cfusion").getAbsolutePath().replace('\\', '/'); - String cfformDir = new File(webInfDir,"cfform").getAbsolutePath().replace('\\', '/'); - - final String cfClasspath = "%s/lib/updates,%s/lib/,%s/lib/axis2,%s/gateway/lib/,%s/../cfform/jars,%s/../flex/jars,%s/lib/oosdk/lib,%s/lib/oosdk/classes".replaceAll("%s", cfusionDir); - final HashMap cfprops = new HashMap<>(); - cfprops.put("coldfusion.home", cfusionDir); - cfprops.put("coldfusion.rootDir", cfusionDir); - cfprops.put("coldfusion.libPath", cfusionDir + "/lib"); - cfprops.put("flex.dir", cfformDir); - cfprops.put("coldfusion.jsafe.defaultalgo", "FIPS186Random"); - cfprops.put("coldfusion.classPath", cfClasspath); - // Hide error messages about MediaLib stuff - cfprops.put("com.sun.media.jai.disableMediaLib", "true"); - // Make the embedded version of Jetty inside Adobe CF shut up since it dumps everything to the error stream - cfprops.put("java.security.policy", cfusionDir + "/lib/coldfusion.policy"); - cfprops.put("java.security.auth.policy", cfusionDir + "/lib/neo_jaas.policy"); - cfprops.put("java.nixlibrary.path", cfusionDir + "/lib"); - cfprops.put("java.library.path", cfusionDir + "/lib"); + if (cfengine.equals("adobe") || cfengine.equals("") && new File(cfusionDir).exists()) { + String cfformDir = new File(webInfDir,"cfform").getAbsolutePath().replace('\\', '/'); + + final String cfClasspath = "%s/lib/updates,%s/lib/,%s/lib/axis2,%s/gateway/lib/,%s/../cfform/jars,%s/../flex/jars,%s/lib/oosdk/lib,%s/lib/oosdk/classes".replaceAll("%s", cfusionDir); + final HashMap cfprops = new HashMap<>(); + + // TODO: See if the next 6 lines are actuallly needed or not in a CF WAR. + cfprops.put("coldfusion.home", cfusionDir); + cfprops.put("coldfusion.rootDir", cfusionDir); + cfprops.put("coldfusion.libPath", cfusionDir + "/lib"); + cfprops.put("flex.dir", cfformDir); + cfprops.put("coldfusion.jsafe.defaultalgo", "FIPS186Random"); + cfprops.put("coldfusion.classPath", cfClasspath); + + // Hide error messages about MediaLib stuff + cfprops.put("com.sun.media.jai.disableMediaLib", "true"); + // Make the embedded version of Jetty inside Adobe CF shut up since it dumps everything to the error stream + cfprops.put("java.security.policy", cfusionDir + "/lib/coldfusion.policy"); + cfprops.put("java.security.auth.policy", cfusionDir + "/lib/neo_jaas.policy"); + cfprops.put("java.nixlibrary.path", cfusionDir + "/lib"); + cfprops.put("java.library.path", cfusionDir + "/lib"); + LOG.debug("Setting coldfusion.home: '" + cfusionDir + "'"); LOG.debug("Setting coldfusion.classpath: '" + cfClasspath + "'"); LOG.debug("Setting flex.dir (cfform): '" + cfformDir + "'"); @@ -87,19 +92,7 @@ void configureServerResourceHandler(DeploymentInfo servletBuilder) { cfengine = "adobe"; } - if(serverMode.equals(Server.Mode.SERVLET)) { - configureServerServlet(servletBuilder); - } - else if(webInfDir.exists() || serverMode.equals(Server.Mode.SERVLET)) { - configureServerWar(servletBuilder); - } - else { - if (getClassLoader() == null) { - throw new RuntimeException("FATAL: Could not load any libs for war: " + warFile.getAbsolutePath()); - } - servletBuilder.setClassLoader(getClassLoader()); - LOG.debug("Running default web server '" + warFile.getAbsolutePath()+ "'"); - } + configureServerWar(servletBuilder); if(cfengine.equals("adobe")) { String cfCompilerOutput = (String) servletBuilder.getServletContextAttributes().get("coldfusion.compiler.outputDir"); if(cfCompilerOutput == null || cfCompilerOutput.matches("^.?WEB-INF.*?")){ @@ -125,17 +118,12 @@ else if(webInfDir.exists() || serverMode.equals(Server.Mode.SERVLET)) { private void configureServerWar(DeploymentInfo servletBuilder) { File warFile = serverOptions.warFile(); File webInfDir = serverOptions.webInfDir(); - Long transferMinSize= serverOptions.transferMinSize(); LOG.debug("found WEB-INF: '" + webInfDir.getAbsolutePath() + "'"); if (getClassLoader() == null) { throw new RuntimeException("FATAL: Could not load any libs for war: " + warFile.getAbsolutePath()); } servletBuilder.setClassLoader(getClassLoader()); - Set contentDirs = new HashSet<>(); - Map aliases = new HashMap<>(); - serverOptions.contentDirectories().forEach(s -> contentDirs.add(Paths.get(s))); - serverOptions.aliases().forEach((s, s2) -> aliases.put(s,Paths.get(s2))); - servletBuilder.setResourceManager(server.getResourceManager(warFile, transferMinSize, contentDirs, aliases, webInfDir)); + WebXMLParser.parseWebXml(serverOptions.webXmlFile(), servletBuilder, serverOptions.ignoreWebXmlWelcomePages(), serverOptions.ignoreWebXmlRestMappings(), false); File webXMLOverrideFile = serverOptions.webXmlOverrideFile(); if(webXMLOverrideFile!=null){ @@ -145,84 +133,6 @@ private void configureServerWar(DeploymentInfo servletBuilder) { } } - private void configureServerServlet(DeploymentInfo servletBuilder) { - File warFile = serverOptions.warFile(); - String cfengine = serverOptions.cfEngineName(); - String cfmlServletConfigWebDir = serverOptions.cfmlServletConfigWebDir(); - String cfmlServletConfigServerDir = serverOptions.cfmlServletConfigServerDir(); - Long transferMinSize = serverOptions.transferMinSize(); - File webXmlFile = serverOptions.webXmlFile(); - - if (getClassLoader() == null) { - throw new RuntimeException("FATAL: Could not load any libs for war: " + warFile.getAbsolutePath()); - } - if (cfmlServletConfigWebDir == null) { - File webConfigDirFile = new File(Server.getThisJarLocation().getParentFile(), "engine/cfml/server/cfml-web/"); - cfmlServletConfigWebDir = webConfigDirFile.getPath() + "/" + serverOptions.serverName(); - } - LOG.debug("cfml.web.config.dir: " + cfmlServletConfigWebDir); - if (cfmlServletConfigServerDir == null || cfmlServletConfigServerDir.length() == 0) { - File serverConfigDirFile = new File(Server.getThisJarLocation().getParentFile(), "engine/cfml/server/"); - cfmlServletConfigServerDir = serverConfigDirFile.getAbsolutePath(); - } - LOG.debug("cfml.server.config.dir: " + cfmlServletConfigServerDir); - File webInfDir; - if (System.getProperty("cfml.webinf") == null) { - webInfDir = new File(cfmlServletConfigWebDir, "WEB-INF/"); - } else { - webInfDir = new File(System.getProperty("cfml.webinf")); - LOG.debug("Found cfml.webinf system property: " + webInfDir.getPath()); - } - LOG.debug("cfml.webinf: " + webInfDir.getPath()); - serverOptions.webInfDir(webInfDir); - - // servletBuilder.setResourceManager(new CFMLResourceManager(new - // File(homeDir,"server/"), transferMinSize, contentDirs)); - File internalCFMLServerRoot = webInfDir; - if(!internalCFMLServerRoot.mkdirs()){ - LOG.errorf("Unable to create cfml resource server root: %s", internalCFMLServerRoot.getAbsolutePath()); - } - Set contentDirs = new HashSet<>(); - Map aliases = new HashMap<>(); - serverOptions.contentDirectories().forEach(s -> contentDirs.add(Paths.get(s))); - serverOptions.aliases().forEach((s, s2) -> aliases.put(s,Paths.get(s2))); - servletBuilder.setResourceManager(server.getResourceManager(warFile, transferMinSize, contentDirs, aliases, internalCFMLServerRoot)); - - servletBuilder.setClassLoader(getClassLoader()); - if (webXmlFile != null) { - LOG.debug("using specified web.xml : " + webXmlFile.getAbsolutePath()); - WebXMLParser.parseWebXml(webXmlFile, servletBuilder, serverOptions.ignoreWebXmlWelcomePages(), serverOptions.ignoreWebXmlRestMappings(), false); - } else { - Class cfmlServlet = getCFMLServletClass(cfengine); - Class restServletClass = getRestServletClass(cfengine); - LOG.debug("loaded servlet classes"); - servletBuilder.addServlet( - servlet("CFMLServlet", cfmlServlet) - .setRequireWelcomeFileMapping(true) - .addInitParam("configuration",cfmlServletConfigWebDir) - .addInitParam(cfengine+"-server-root",cfmlServletConfigServerDir) - .addMapping("*.cfm") - .addMapping("*.cfc") - .addMapping("/index.cfc/*") - .addMapping("/index.cfm/*") - .addMapping("/index.cfml/*") - .setLoadOnStartup(1) - ); - if(serverOptions.servletRestEnable()) { - LOG.debug("Adding REST servlet"); - ServletInfo restServlet = servlet("RESTServlet", restServletClass) - .setRequireWelcomeFileMapping(true) - .addInitParam(cfengine+"-web-directory",cfmlServletConfigWebDir) - .setLoadOnStartup(2); - for(String path : serverOptions.servletRestMappings()) { - restServlet.addMapping(path); - } - servletBuilder.addServlet(restServlet); - } - } - } - - @SuppressWarnings("unchecked") private void configureURLRewrite(DeploymentInfo servletBuilder, File webInfDir) throws ClassNotFoundException { if(serverOptions.urlRewriteEnable()) { @@ -429,13 +339,12 @@ public void handleChanges(Collection changes) { */ // Default list of what the default servlet will serve - String allowedExt = "3gp,3gpp,7z,ai,aif,aiff,asf,asx,atom,au,avi,bin,bmp,btm,cco,crt,css,csv,deb,der,dmg,doc,docx,eot,eps,flv,font,gif,hqx,htc,htm,html,ico,img,ini,iso,jad,jng,jnlp,jpeg,jpg,js,json,kar,kml,kmz,m3u8,m4a,m4v,map,mid,midi,mml,mng,mov,mp3,mp4,mpeg,mpeg4,mpg,msi,msm,msp,ogg,otf,pdb,pdf,pem,pl,pm,png,ppt,pptx,prc,ps,psd,ra,rar,rpm,rss,rtf,run,sea,shtml,sit,svg,svgz,swf,tar,tcl,tif,tiff,tk,ts,ttf,txt,wav,wbmp,webm,webp,wmf,wml,wmlc,wmv,woff,woff2,xhtml,xls,xlsx,xml,xpi,xspf,zip,aifc,aac,apk,bak,bk,bz2,cdr,cmx,dat,dtd,eml,fla,gz,gzip,ipa,ia,indd,hey,lz,maf,markdown,md,mkv,mp1,mp2,mpe,odt,ott,odg,odf,ots,pps,pot,pmd,pub,raw,sdd,tsv,xcf,yml,yaml"; - // Add any custom additions by our users + String allowedExt = "3gp,3gpp,7z,ai,aif,aiff,asf,asx,atom,au,avi,bin,bmp,btm,cco,crt,css,csv,deb,der,dmg,doc,docx,eot,eps,flv,font,gif,hqx,htc,htm,html,ico,img,ini,iso,jad,jng,jnlp,jpeg,jpg,js,json,kar,kml,kmz,m3u8,m4a,m4v,map,mid,midi,mml,mng,mov,mp3,mp4,mpeg,mpeg4,mpg,msi,msm,msp,ogg,otf,pdb,pdf,pem,pl,pm,png,ppt,pptx,prc,ps,psd,ra,rar,rpm,rss,rtf,run,sea,shtml,sit,svg,svgz,swf,tar,tcl,tif,tiff,tk,ts,ttf,txt,wav,wbmp,webm,webp,wmf,wml,wmlc,wmv,woff,woff2,xhtml,xls,xlsx,xml,xpi,xspf,zip,aifc,aac,apk,bak,bk,bz2,cdr,cmx,dat,dtd,eml,fla,gz,gzip,ipa,ia,indd,hey,lz,maf,markdown,md,mkv,mp1,mp2,mpe,odt,ott,odg,odf,ots,pps,pot,pmd,pub,raw,sdd,tsv,xcf,yml,yaml,handlebars,hbs"; // Add any custom additions by our users if( serverOptions.defaultServletAllowedExt().length() > 0 ) { allowedExt += "," + serverOptions.defaultServletAllowedExt(); } - LOG.info("Extensions allowed by the default servlet for static files: " + allowedExt); + LOG.debug("Extensions allowed by the default servlet for static files: " + allowedExt); allowedExt = allowedExt.toLowerCase(); StringBuilder allowedExtBuilder = new StringBuilder(); @@ -462,17 +371,17 @@ public void handleChanges(Collection changes) { LOG.debug("No welcome pages set yet, so adding defaults to deployment manager."); servletBuilder.addWelcomePages(defaultWelcomeFiles); } - LOG.info("welcome pages in deployment manager: " + servletBuilder.getWelcomePages()); + LOG.debug("welcome pages in deployment manager: " + servletBuilder.getWelcomePages()); if(serverOptions.ignoreWebXmlRestMappings() && serverOptions.servletRestEnable()) { - LOG.info("Overriding web.xml rest mappings with " + Arrays.toString( serverOptions.servletRestMappings() ) ); + LOG.debug("Overriding web.xml rest mappings with " + Arrays.toString( serverOptions.servletRestMappings() ) ); for (Map.Entry stringServletInfoEntry : servletBuilder.getServlets().entrySet()) { ServletInfo restServlet = stringServletInfoEntry.getValue(); // LOG.trace("Checking servlet named: " + restServlet.getName() + " to see if it's a REST servlet."); if (restServlet.getName().toLowerCase().equals("restservlet") || restServlet.getName().toLowerCase().equals("cfrestservlet")) { for (String path : serverOptions.servletRestMappings()) { restServlet.addMapping(path); - LOG.info("Added rest mapping: " + path + " to " + restServlet.getName()); + LOG.debug("Added rest mapping: " + path + " to " + restServlet.getName()); } } } diff --git a/src/main/java/runwar/Server.java b/src/main/java/runwar/Server.java index 71305bf2..fd11ae5d 100644 --- a/src/main/java/runwar/Server.java +++ b/src/main/java/runwar/Server.java @@ -3,6 +3,7 @@ import io.undertow.Handlers; import io.undertow.Undertow; import io.undertow.Undertow.Builder; +import io.undertow.client.ClientConnection; import io.undertow.UndertowOptions; import io.undertow.predicate.Predicates; import io.undertow.server.DefaultByteBufferPool; @@ -24,6 +25,10 @@ import io.undertow.servlet.api.DeploymentInfo; import io.undertow.servlet.api.DeploymentManager; import io.undertow.servlet.api.ServletSessionConfig; +import io.undertow.servlet.api.ThreadSetupAction; +import io.undertow.servlet.api.ThreadSetupAction.Handle; +import io.undertow.util.AttachmentKey; +import io.undertow.util.HeaderValues; import io.undertow.util.Headers; import io.undertow.util.HttpString; import io.undertow.websockets.jsr.WebSocketDeploymentInfo; @@ -41,6 +46,7 @@ import runwar.security.SecurityManager; import runwar.tray.Tray; import runwar.undertow.MappedResourceManager; +import runwar.undertow.HostResourceManager; import runwar.undertow.RequestDebugHandler; import runwar.util.ClassLoaderUtils; import runwar.util.PortRequisitioner; @@ -53,7 +59,9 @@ import java.lang.reflect.Method; import java.net.*; import java.nio.file.Path; +import java.nio.file.Paths; import java.util.List; +import java.util.concurrent.ConcurrentHashMap; import java.util.*; import javax.servlet.Servlet; import javax.servlet.http.HttpServletResponse; @@ -64,14 +72,20 @@ import static runwar.logging.RunwarLogger.CONTEXT_LOG; import static runwar.logging.RunwarLogger.LOG; +import static runwar.logging.RunwarLogger.MAPPER_LOG; + import runwar.util.Utils; +@SuppressWarnings( "deprecation" ) public class Server { public static String processName = "Starting Server..."; + public static final AttachmentKey DEPLOYMENT_KEY = AttachmentKey.create(String.class); + private static final ThreadLocal currentExchange= new ThreadLocal(); private volatile static ServerOptionsImpl serverOptions; private static MariaDB4jManager mariadb4jManager; - private DeploymentManager manager; + private ConcurrentHashMap deployments = new ConcurrentHashMap(); + private HashSet deploymentKeyWarnings = new HashSet(); private Undertow undertow; private MonitorThread monitor; @@ -90,8 +104,7 @@ public class Server { private String serverMode; private PrintStream originalSystemOut; private PrintStream originalSystemErr; - - private static final int METADATA_MAX_AGE = 2000; + private static final Thread mainThread = Thread.currentThread(); private static XnioWorker worker, logWorker; @@ -111,22 +124,10 @@ public Server(int seconds) { private void initClassLoader(List _classpath) { if (_classLoader == null) { int paths = _classpath.size(); - LOG.debug("Initializing classloader with " + _classpath.size() + " libraries"); - // Thread.currentThread().setContextClassLoader(new JavaShimClassLoader(Thread.currentThread().getContextClassLoader())); -// LOG.debug("Booted:" + VM.isBooted()); + LOG.debug("Initializing classloader with " + _classpath.size() + " jar(s)"); if (paths > 0) { LOG.tracef("classpath: %s", _classpath); _classLoader = new URLClassLoader(_classpath.toArray(new URL[paths])); - // _classLoader = new URLClassLoader(_classpath.toArray(new URL[_classpath.size()]),Thread.currentThread().getContextClassLoader()); - // _classLoader = new URLClassLoader(_classpath.toArray(new URL[_classpath.size()]),ClassLoader.getSystemClassLoader()); - // _classLoader = new XercesFriendlyURLClassLoader(_classpath.toArray(new URL[_classpath.size()]),ClassLoader.getSystemClassLoader()); - //Thread.currentThread().setContextClassLoader(_classLoader); -// try { -// Class yourMainClass = Class.forName("sun.misc.VM", true, _classLoader); -// } catch (ClassNotFoundException e) { -// e.printStackTrace(); -// } - } else { _classLoader = Thread.currentThread().getContextClassLoader(); } @@ -196,9 +197,10 @@ private synchronized void requisitionPorts() { public synchronized void startServer(final ServerOptions options) throws Exception { serverOptions = (ServerOptionsImpl) options; - LoggerFactory.configure(serverOptions); + //LoggerFactory.configure(serverOptions); // redirect out and err to context logger hookSystemStreams(); + serverState = ServerState.STARTING; if (serverOptions.action().equals("stop")) { Stop.stopServer(serverOptions, true); @@ -226,8 +228,8 @@ public synchronized void startServer(final ServerOptions options) throws Excepti configurer.generateSelfSignedCertificate(); } + LOG.info(bar); LOG.info("Starting RunWAR " + getVersion()); - LOG.debug("Starting Server: " + options.host()); requisitionPorts(); Builder serverBuilder = Undertow.builder(); @@ -235,21 +237,23 @@ public synchronized void startServer(final ServerOptions options) throws Excepti LOG.debug("SERVER BUILDER:" + serverOptions.httpEnable()); if (serverOptions.httpEnable()) { - LOG.debug("Server Builder - PORT:" + ports.get("http").socket + " HOST:" + host); + LOG.info("Binding HTTP on " + host + ":" + ports.get("http").socket ); serverBuilder.addHttpListener(ports.get("http").socket, realHost); } else { - LOG.info("HTTP Enabled:" + serverOptions.httpEnable()); + LOG.debug("HTTP Enabled:" + serverOptions.httpEnable()); } - LOG.info("HTTP2 Enabled:" + serverOptions.http2Enable()); if (serverOptions.http2Enable()) { + LOG.info("Enabling HTTP/2"); serverBuilder.setServerOption(UndertowOptions.ENABLE_HTTP2, true); + } else { + LOG.debug("HTTP2 Enabled:" + serverOptions.http2Enable()); } if (serverOptions.sslEnable()) { int sslPort = ports.get("https").socket; serverOptions.directBuffers(true); - LOG.info("Enabling SSL protocol on port " + sslPort); + LOG.info("Binding SSL on " + host + ":" + sslPort ); if (serverOptions.sslEccDisable() && cfengine.toLowerCase().equals("adobe")) { LOG.debug("disabling com.sun.net.ssl.enableECC"); @@ -277,46 +281,23 @@ public synchronized void startServer(final ServerOptions options) throws Excepti System.exit(1); } } else { - LOG.info("HTTP sslEnable:" + serverOptions.sslEnable()); + LOG.debug("sslEnable:" + serverOptions.sslEnable()); } if (serverOptions.ajpEnable()) { - LOG.info("Enabling AJP protocol on port " + serverOptions.ajpPort()); + LOG.info("Binding AJP on " + host + ":" + serverOptions.ajpPort() ); serverBuilder.addAjpListener(serverOptions.ajpPort(), realHost); if (serverOptions.undertowOptions().getMap().size() == 0) { // if no options is set, default to the large packet size serverBuilder.setServerOption(UndertowOptions.MAX_AJP_PACKET_SIZE, 65536); } } else { - LOG.info("HTTP ajpEnable:" + serverOptions.ajpEnable()); + LOG.debug("ajpEnable:" + serverOptions.ajpEnable()); } securityManager = new SecurityManager(); - - // if the war is archived, unpack it to system temp - if (warFile.exists() && !warFile.isDirectory()) { - URL zipResource = warFile.toURI().toURL(); - String warDir = warFile.getName().toLowerCase().replace(".war", ""); - warFile = new File(warFile.getParentFile(), warDir); - if (!warFile.exists()) { - if (!warFile.mkdir()) { - LOG.error("Unable to explode WAR to " + warFile.getAbsolutePath()); - } else { - LOG.debug("Exploding compressed WAR to " + warFile.getAbsolutePath()); - LaunchUtil.unzipResource(zipResource, warFile, false); - } - } else { - LOG.debug("Using already exploded WAR in " + warFile.getAbsolutePath()); - } - warPath = warFile.getAbsolutePath(); - if (serverOptions.warFile().getAbsolutePath().equals(serverOptions.contentDirs())) { - serverOptions.contentDirs(warFile.getAbsolutePath()); - } - serverOptions.warFile(warFile); - } else { - LOG.info("HTTP warFile exists:" + warFile.exists()); - LOG.info("HTTP warFile isDirectory:" + warFile.isDirectory()); - } + + LOG.debug("WAR root:" + warFile.getAbsolutePath()); if (!warFile.exists()) { throw new RuntimeException("war does not exist: " + warFile.getAbsolutePath()); } @@ -330,7 +311,7 @@ public synchronized void startServer(final ServerOptions options) throws Excepti Thread.sleep(200); System.exit(0); } else { - LOG.info("HTTP background:" + serverOptions.background()); + LOG.debug("background:" + serverOptions.background()); } File webinf = serverOptions.webInfDir(); @@ -346,7 +327,7 @@ public synchronized void startServer(final ServerOptions options) throws Excepti libDirs = libDirs + ","; } libDirs = libDirs + webinf.getAbsolutePath() + "/lib"; - LOG.info("Adding additional lib dir of: " + webinf.getAbsolutePath() + "/lib"); + LOG.debug("Adding additional lib dir of: " + webinf.getAbsolutePath() + "/lib"); serverOptions.libDirs(libDirs); } @@ -360,7 +341,7 @@ public synchronized void startServer(final ServerOptions options) throws Excepti } if (serverOptions.mariaDB4jImportSQLFile() != null) { - LOG.info("Importing sql file: " + serverOptions.mariaDB4jImportSQLFile().toURI().toURL()); + LOG.debug("Importing sql file: " + serverOptions.mariaDB4jImportSQLFile().toURI().toURL()); cp.add(serverOptions.mariaDB4jImportSQLFile().toURI().toURL()); } cp.addAll(getClassesList(new File(webinf, "/classes"))); @@ -407,21 +388,7 @@ public synchronized void startServer(final ServerOptions options) throws Excepti System.setProperty("apple.awt.UIElement", "true"); } } - LOG.info(bar); - LOG.info("Starting - port:" + ports.get("http") + " stop-port:" + ports.get("stop") + " warpath:" + warPath); - LOG.info("context: " + contextPath + " - version: " + getVersion()); - if (serverOptions.contentDirectories().size() > 0) { - JSONArray jsonArray = new JSONArray(); - jsonArray.addAll(serverOptions.contentDirectories()); - LOG.info("web-dirs: " + jsonArray.toJSONString()); - } else { - LOG.debug("no content directories configured"); - } - if (serverOptions.aliases().size() > 0) { - LOG.info("aliases: " + new JSONObject(serverOptions.aliases()).toJSONString()); - } else { - LOG.debug("no aliases configured"); - } + LOG.info("Servlet Context: " + contextPath ); LOG.info("Log Directory: " + serverOptions.logDir().getAbsolutePath()); LOG.info(bar); addShutDownHook(); @@ -435,12 +402,12 @@ public synchronized void startServer(final ServerOptions options) throws Excepti logXnioOptions(serverXnioOptions); if (serverOptions.ioThreads() != 0) { - LOG.info("IO Threads: " + serverOptions.ioThreads()); + LOG.debug("IO Threads: " + serverOptions.ioThreads()); serverBuilder.setIoThreads(serverOptions.ioThreads()); // posterity: ignored when managing worker serverXnioOptions.set(Options.WORKER_IO_THREADS, serverOptions.ioThreads()); } if (serverOptions.workerThreads() != 0) { - LOG.info("Worker threads: " + serverOptions.workerThreads()); + LOG.debug("Worker threads: " + serverOptions.workerThreads()); serverBuilder.setWorkerThreads(serverOptions.workerThreads()); // posterity: ignored when managing worker serverXnioOptions.set(Options.WORKER_TASK_CORE_THREADS, serverOptions.workerThreads()) .set(Options.WORKER_TASK_MAX_THREADS, serverOptions.workerThreads()); @@ -462,14 +429,6 @@ public synchronized void startServer(final ServerOptions options) throws Excepti servletSessionConfig.setHttpOnly(serverOptions.cookieHttpOnly()); servletSessionConfig.setSecure(serverOptions.cookieSecure()); - final DeploymentInfo servletBuilder = deployment() - .setContextPath(contextPath.equals("/") ? "" : contextPath) - .setTempDir(new File(System.getProperty("java.io.tmpdir"))) - .setDeploymentName(warPath) - .setServletSessionConfig(servletSessionConfig) - .setDisplayName(serverName) - .setServerName("WildFly / Undertow"); - // hack to prevent . being picked up as the system path (jacob.x.dll) final String jarPath = getThisJarLocation().getPath(); String javaLibraryPath = System.getProperty("java.library.path"); @@ -484,49 +443,155 @@ public synchronized void startServer(final ServerOptions options) throws Excepti } System.setProperty("java.library.path", javaLibraryPath); LOG.trace("java.library.path:" + System.getProperty("java.library.path")); + + final DeploymentInfo servletBuilder = deployment() + .setContextPath(contextPath.equals("/") ? "" : contextPath) + .setTempDir(new File(System.getProperty("java.io.tmpdir"))) + .setDeploymentName("site1") + .setServletSessionConfig(servletSessionConfig) + .setDisplayName(serverName) + .setServerName("WildFly / Undertow") + .addThreadSetupAction( new ThreadSetupAction() { + + public Handle setup(final HttpServerExchange exchange) { + + // This allows the exchange to be available to the task thread. + currentExchange.set(exchange); + return new Handle() { + + @Override + public void tearDown() { + currentExchange.remove(); + } + }; + } + }); + configurer.configureServlet(servletBuilder); + configurer.configureServerResourceHandler(servletBuilder); + if (serverOptions.basicAuthEnable()) { - securityManager.configureAuth(servletBuilder, serverOptions);//SECURITY_MANAGER + securityManager.configureAuth(servletBuilder, serverOptions); } - configurer.configureServlet(servletBuilder); - // TODO: probably best to create a new worker for websockets, if we want fastness, but for now we share // TODO: add buffer pool size (maybe-- direct is best at 16k), enable/disable be good I reckon tho servletBuilder.addServletContextAttribute(WebSocketDeploymentInfo.ATTRIBUTE_NAME, new WebSocketDeploymentInfo().setBuffers(new DefaultByteBufferPool(true, 1024 * 16)).setWorker(worker)); LOG.debug("Added websocket context"); + + // Create default context + createServletDeployment( servletBuilder, serverOptions.warFile(), configurer, ServletDeployment.DEFAULT, null ); - // Remove this if/when this ticket is complete: - // https://issues.redhat.com/browse/UNDERTOW-1747 - servletBuilder.addOuterHandlerChainWrapper(next -> new HttpHandler() { + + HttpHandler hostHandler = new HttpHandler() { + @Override - public void handleRequest(HttpServerExchange exchange) throws Exception { - if (exchange.getStatusCode() > 399 && exchange.getResponseContentLength() == -1) { - ServletRequestContext src = exchange.getAttachment(ServletRequestContext.ATTACHMENT_KEY); - ((HttpServletResponse) src.getServletResponse()).sendError(exchange.getStatusCode()); - } else { - next.handleRequest(exchange); - } + public void handleRequest(final HttpServerExchange exchange) throws Exception { + ServletDeployment deployment; + + // If we're not auto-creating contexts, then just pass to our default servlet deployment + if( !serverOptions.autoCreateContexts() ) { + deployment = deployments.get( ServletDeployment.DEFAULT ); + + // Otherwise, see if a deployment already exists + } else { + + if( !isHeaderSafe( exchange, "", "X-Webserver-Context" ) ) return; + + String deploymentKey = exchange.getRequestHeaders().getFirst( "X-Webserver-Context" ); + if( deploymentKey == null ){ + deploymentKey = exchange.getHostName().toLowerCase(); + } + // Save into the exchange for later in the thread + exchange.putAttachment(DEPLOYMENT_KEY, deploymentKey); + + deployment = deployments.get( deploymentKey ); + if( deployment == null ) { + + if( !isHeaderSafe( exchange, deploymentKey, "X-Tomcat-DocRoot" ) ) return; + String docRoot = exchange.getRequestHeaders().getFirst( "X-Tomcat-DocRoot" ); + + if( docRoot != null && !docRoot.isEmpty() ) { + File docRootFile = new File( docRoot ); + if( docRootFile.exists() && docRootFile.isDirectory() ) { + + // Enforce X-ModCFML-SharedKey + if( !isHeaderSafe( exchange, deploymentKey, "X-ModCFML-SharedKey" ) ) return; + String modCFMLSharedKey = exchange.getRequestHeaders().getFirst( "X-ModCFML-SharedKey" ); + if( modCFMLSharedKey == null ) { + modCFMLSharedKey = ""; + } + + // If a secret was provided, enforce it + if( !serverOptions.autoCreateContextsSecret().equals( "" ) && !serverOptions.autoCreateContextsSecret().equals( modCFMLSharedKey ) ) { + exchange.getResponseHeaders().put(Headers.CONTENT_TYPE, "text/plain"); + exchange.setStatusCode(403); + exchange.getResponseSender().send( "The web server's X-ModCFML-SharedKey was not supplied or doesn't match the configured secret." ); + logOnce( deploymentKey, "SharedKeyNotMatch", "debug", "The web server's X-ModCFML-SharedKey [" + modCFMLSharedKey + "] was not supplied or doesn't match the auto-create-contexts-secret setting [" + ( serverOptions.autoCreateContextsSecret() == null ? "" : serverOptions.autoCreateContextsSecret() ) + "] for deploymentKey [" + deploymentKey + "]." ); + return; + } + String vDirs = null; + if( serverOptions.autoCreateContextsVDirs() ) { + if( !isHeaderSafe( exchange, deploymentKey, "x-vdirs" ) ) return; + vDirs = exchange.getRequestHeaders().getFirst( "x-vdirs" ); + if( vDirs != null && !vDirs.isEmpty() ) { + // Ensure we can trust the x-vdirs header. Only use it if the x-vdirs-sharedkey header is also supplied with the shared key + if( !isHeaderSafe( exchange, deploymentKey, "x-vdirs-sharedkey" ) ) return; + String vDirsSharedKey = exchange.getRequestHeaders().getFirst( "x-vdirs-sharedkey" ); + if( vDirsSharedKey == null || vDirsSharedKey.isEmpty() ) { + vDirs = null; + logOnce( deploymentKey, "NovDirsSharedKey", "warn", "The x-vdirs header was provided, but it is being igonred because no x-vdirs-sharedkey header is present." ); + } else { + // If a secret was provided, enforce it + if( !serverOptions.autoCreateContextsSecret().equals( "" ) && !serverOptions.autoCreateContextsSecret().equals( vDirsSharedKey ) ) { + vDirs = null; + logOnce( deploymentKey, "VDirsSharedKeyNotMatch", "warn", "The x-vdirs header was provided, but it is being igonred because the x-vdirs-sharedkey header [" + vDirsSharedKey + "] doesn't match the auto-create-contexts-secret setting [" + ( serverOptions.autoCreateContextsSecret() == null ? "" : serverOptions.autoCreateContextsSecret() ) + "] for deploymentKey [" + deploymentKey + "]." ); + } + } + } + } + try { + deployment = createServletDeployment( servletBuilder, docRootFile, configurer, deploymentKey, vDirs ); + } catch ( MaxContextsException e ) { + + exchange.getResponseHeaders().put(Headers.CONTENT_TYPE, "text/plain"); + exchange.setStatusCode(500); + exchange.getResponseSender().send( e.getMessage() ); + + logOnce( deploymentKey, "MaxContextsException", "error", e.getMessage() + " The requested deploymentKey was [" + deploymentKey + "]" ); + return; + } + } else { + LOG.warn( "X-Tomcat-DocRoot of [ + docRoot + ] does not exist or is not directory. Using default context." ); + deployment = deployments.get( ServletDeployment.DEFAULT ); + } + } else { + logOnce( deploymentKey, "NoDocRootHeader", "warn", "X-Tomcat-DocRoot is null or empty. Using default context for deploymentKey [" + deploymentKey + "]." ); + deployment = deployments.get( ServletDeployment.DEFAULT ); + } + + } + } + + deployment.getServletInitialHandler().handleRequest(exchange); + } @Override public String toString() { - return "Runwar OuterHandlerChainWrapper"; + return "Runwar HostHandler"; } - }); - - manager = defaultContainer().addDeployment(servletBuilder); - manager.deploy(); - HttpHandler servletHandler = manager.start(); + }; + LOG.debug("started servlet deployment manager"); if (serverOptions.bufferSize() != 0) { - LOG.info("Buffer Size: " + serverOptions.bufferSize()); + LOG.debug("Buffer Size: " + serverOptions.bufferSize()); serverBuilder.setBufferSize(serverOptions.bufferSize()); } - LOG.info("Direct Buffers: " + serverOptions.directBuffers()); + LOG.debug("Direct Buffers: " + serverOptions.directBuffers()); serverBuilder.setDirectBuffers(serverOptions.directBuffers()); final PathHandler pathHandler = new PathHandler(Handlers.redirect(contextPath)) { @@ -550,7 +615,7 @@ public void handleRequest(final HttpServerExchange exchange) throws Exception { // clear any welcome-file info cached after initial request *NOT THREAD SAFE* if (serverOptions.directoryListingRefreshEnable() && exchange.getRequestPath().endsWith("/")) { CONTEXT_LOG.trace("*** Resetting servlet path info"); - manager.getDeployment().getServletPaths().invalidate(); + //manager.getDeployment().getServletPaths().invalidate(); } if (serverOptions.debug() && serverOptions.testing() && exchange.getRequestPath().endsWith("/dumprunwarrequest")) { @@ -567,6 +632,7 @@ public void handleRequest(final HttpServerExchange exchange) throws Exception { // Not ending the exchange here so the servlet can still send any custom error page. exchange.setStatusCode( 404 ); } + super.handleRequest(exchange); } } @@ -576,8 +642,8 @@ public String toString() { return "Runwar PathHandler"; } }; - - pathHandler.addPrefixPath(contextPath, servletHandler); + + pathHandler.addPrefixPath(contextPath, hostHandler); HttpHandler httpHandler = pathHandler; if (serverOptions.predicateFile() != null) { @@ -625,7 +691,7 @@ public String toString() { .setLogBaseName(serverOptions.logAccessBaseFileName()) .setLogNameSuffix(serverOptions.logSuffix()) .build(); - LOG.info("Logging combined access to " + serverOptions.logAccessDir() + " base name of '" + serverOptions.logAccessBaseFileName() + "." + serverOptions.logSuffix() + ", rotated daily'"); + LOG.debug("Logging combined access to " + serverOptions.logAccessDir() + " base name of '" + serverOptions.logAccessBaseFileName() + "." + serverOptions.logSuffix() + ", rotated daily'"); httpHandler = new AccessLogHandler(httpHandler, accessLogReceiver, "combined", Server.class.getClassLoader()); } @@ -646,7 +712,7 @@ public String toString() { } if (serverOptions.basicAuthEnable()) { - securityManager.configureAuth(httpHandler, serverBuilder, options); //SECURITY_MANAGER + securityManager.configureAuth(httpHandler, serverBuilder, options); } else { serverBuilder.setHandler(httpHandler); } @@ -704,7 +770,7 @@ public String toString() { } setServerState(ServerState.STARTED); if (serverOptions.mariaDB4jEnable()) { - LOG.info("MariaDB support enable"); + LOG.debug("MariaDB support enable"); mariadb4jManager = new MariaDB4jManager(_classLoader); try { mariadb4jManager.start(serverOptions.mariaDB4jPort(), serverOptions.mariaDB4jBaseDir(), @@ -734,7 +800,7 @@ public String toString() { private void setUndertowOptions(Builder serverBuilder) { OptionMap undertowOptionsMap = serverOptions.undertowOptions().getMap(); for (Option option : undertowOptionsMap) { - LOG.info("UndertowOption " + option.getName() + ':' + undertowOptionsMap.get(option)); + LOG.debug("UndertowOption " + option.getName() + ':' + undertowOptionsMap.get(option)); serverBuilder.setServerOption(option, undertowOptionsMap.get(option)); } } @@ -743,7 +809,7 @@ private void setUndertowOptions(Builder serverBuilder) { private void logXnioOptions(OptionMap.Builder xnioOptions) { OptionMap serverXnioOptionsMap = xnioOptions.getMap(); for (Option option : serverXnioOptionsMap) { - LOG.info("XNIO-Option " + option.getName() + ':' + serverXnioOptionsMap.get(option)); + LOG.debug("XNIO-Option " + option.getName() + ':' + serverXnioOptionsMap.get(option)); } } @@ -752,7 +818,7 @@ PortRequisitioner getPorts() { } static String fullExchangePath(HttpServerExchange exchange) { - return exchange.getRequestPath() + (exchange.getQueryString().length() > 0 ? "?" + exchange.getQueryString() : ""); + return exchange.getRequestURL() + (exchange.getQueryString().length() > 0 ? "?" + exchange.getQueryString() : ""); } private synchronized void hookSystemStreams() { @@ -830,15 +896,20 @@ public void stopServer() { if (serverOptions.mariaDB4jEnable()) { mariadb4jManager.stop(); } - if (manager != null) { + if (deployments != null) { try { - switch (manager.getState()) { - case UNDEPLOYED: - break; - default: - manager.stop(); - manager.undeploy(); - } + for ( Map.Entry deployment : deployments.entrySet() ) { + DeploymentManager manager = deployment.getValue().getDeploymentManager(); + switch (manager.getState()) { + case UNDEPLOYED: + break; + default: + manager.stop(); + manager.undeploy(); + } + + } + if (undertow != null) { undertow.stop(); } @@ -863,14 +934,14 @@ public void stopServer() { } tray.unhookTray(); + if (System.getProperty("runwar.listloggers") != null && Boolean.parseBoolean(System.getProperty("runwar.listloggers"))) { + LoggerFactory.listLoggers(); + } unhookSystemStreams(); if (System.getProperty("runwar.classlist") != null && Boolean.parseBoolean(System.getProperty("runwar.classlist"))) { ClassLoaderUtils.listAllClasses(serverOptions.logDir() + "/classlist.txt"); } - if (System.getProperty("runwar.listloggers") != null && Boolean.parseBoolean(System.getProperty("runwar.listloggers"))) { - LoggerFactory.listLoggers(); - } if (monitor != null) { LOG.debug("Stopping server monitor"); @@ -890,15 +961,50 @@ public void stopServer() { } - ResourceManager getResourceManager(File warFile, Long transferMinSize, Set contentDirs, Map aliases, File internalCFMLServerRoot) { - MappedResourceManager mappedResourceManager = new MappedResourceManager(warFile, transferMinSize, contentDirs, aliases, internalCFMLServerRoot,serverOptions); - if (serverOptions.directoryListingRefreshEnable() || !serverOptions.bufferEnable()) { + public ResourceManager getResourceManager(File warFile, Long transferMinSize, Map aliases, File internalCFMLServerRoot) { + Boolean cached = !serverOptions.directoryListingRefreshEnable() && serverOptions.cacheServletPaths(); + + LOG.debugf("Initialized " + ( cached ? "CACHED " : "" ) + "MappedResourceManager - base: %s, web-inf: %s, aliases: %s", warFile.getAbsolutePath(), internalCFMLServerRoot.getAbsolutePath(), aliases); + + MappedResourceManager mappedResourceManager = new MappedResourceManager(warFile, transferMinSize, aliases, internalCFMLServerRoot, serverOptions); + if ( !cached ) { return mappedResourceManager; } - final DirectBufferCache dataCache = new DirectBufferCache(1000, 10, 1000 * 10 * 1000, BufferAllocator.DIRECT_BYTE_BUFFER_ALLOCATOR, METADATA_MAX_AGE); - final int metadataCacheSize = 100; - final long maxFileSize = 10000; - return new CachingResourceManager(metadataCacheSize, maxFileSize, dataCache, mappedResourceManager, METADATA_MAX_AGE); + + LOG.debugf("ResourceManager Cache total size: %s MB", serverOptions.fileCacheTotalSizeMB() ); + LOG.debugf("ResourceManager Cache max file size: %s KB", serverOptions.fileCacheMaxFileSizeKB() ); + + // 8 hours in in milliseconds-- used for both the path metadata cache AND the file contents cache + // Setting to -1 will never expire items from the cache, which is tempting-- but having some sort of expiration will keep errant entries from clogging the cache forever + int METADATA_MAX_AGE = 8 * 60 * 60 * 1000; + /* DirectBufferCache.sliceSize: internally DirectBufferCache has a buffer pool. This pool is responsible for allocating byte buffers to store the data that is in the cache, + * the size of those buffers will be sliceSize * slicesPerPage. Each byte buffer region that is allocated in the memory is split into slicesPerPage, and then each buffer + * will have sliceSize. To give you an example, if you have 50 slicesPerPage and each one is 10,000 bytes long (this would be sliceSize), each time a buffer is needed, + * it allocates a region whose size is 500,000 bytes. That region is split into 50 byte buffers of length 10,000 (in bytes). + * If those buffers are all used at some point and not reclaimed, when the pool needs more buffers, it will allocate another 500,000 bytes long chunk, and so on, but there is a limit to it, + * which is maxMemory + */ + // Max file size to cache directly in memory-- measured in bytes. + final long maxFileSize = serverOptions.fileCacheMaxFileSizeKB() * 1024; // Convert KB to B + /* DirectBufferCache.maxMemory: this is the maximum number of bytes that can be allocated by the pool. So, in the example above, supposed that slicesPerPage is 50, and sliceSize is 10,000 bytes, + * if you have a maxMemory of 1,000,000 bytes, it means that the buffer pool can only allocate two chunks of 500,000 bytes each, because that's the number of 500,000 bytes long + * regions that fit into 1,000,000. If none of those buffers are reclaimed at some point, and more buffers are needed, the buffer pool will refuse to do more allocations. + * The cache will remove the oldest entry from usage pov because it is LRU. This cache is used by CachingResourceManager to store contents of files in direct memory. */ + int maxMemory = serverOptions.fileCacheTotalSizeMB() * 1024 * 1024; // Convert MB to B + // Number of paths to cache. i.e. /foo.txt maps to C:/webroot/foo.txt + // I assume the memory overhead of the meta is nearly zero since it's just a single POJO instance per path + final int metadataCacheSize = 10000; + if( maxMemory > 0 && maxFileSize > 0 ) { + int sliceSize = 1024 * 1024; // 1 KB per slice + // DirectBufferCache slicesPerPage: the explanation is right above. + int slicesPerPage = 10; // 10 slices per page means 10 KB per buffer + final DirectBufferCache dataCache = new DirectBufferCache(sliceSize, slicesPerPage, maxMemory, BufferAllocator.DIRECT_BYTE_BUFFER_ALLOCATOR, METADATA_MAX_AGE); + + return new CachingResourceManager(metadataCacheSize, maxFileSize, dataCache, mappedResourceManager, METADATA_MAX_AGE); + } else { + LOG.debug("ResourceManager file cache disabled since size is zero. Path lookups will still be cached." ); + return new CachingResourceManager(metadataCacheSize, maxFileSize, null, mappedResourceManager, METADATA_MAX_AGE); + } } public static File getThisJarLocation() { @@ -1158,11 +1264,134 @@ public static String getProcessName() { public String getServerState() { return serverState; } + + public synchronized ServletDeployment createServletDeployment( DeploymentInfo servletBuilder, File webroot, RunwarConfigurer configurer, String deploymentKey, String vDirs ) throws Exception { + ServletDeployment deployment; + + // If another thread already created this deployment + if( ( deployment = deployments.get( deploymentKey ) ) != null ) { + return deployment; + } + + if( deployments.size() > serverOptions.autoCreateContextsMax() ) { + throw new MaxContextsException( "Cannot create new servlet deployment. The configured max is [" + serverOptions.autoCreateContextsMax() + "]." ); + } + + LOG.info("Creating deployment [" + deploymentKey + "] in " + webroot.toString() ); + + File webInfDir = serverOptions.webInfDir(); + Long transferMinSize= serverOptions.transferMinSize(); + Map aliases = new HashMap<>(); + serverOptions.aliases().forEach((s, s2) -> aliases.put(s,Paths.get(s2))); + + // Add any web server VDirs to Undertow. They come in this format: + // /foo,C:\path\to\foo;/bar,C:\path\to\bar + if( vDirs != null && !vDirs.isEmpty() ) { + // Parsing logic borrowed from mod_cfml source: + // https://github.com/paulklinkenberg/mod_cfml/blob/32e1fd868d7698f91ad12cffcaeb17258b4071d8/java/mod_cfml-valve/src/mod_cfml/core.java#L409-L420 + String[] aVDirs = vDirs.split(";"); + for (int i=0; i 1 && dirParts[1].length() > 1) { + // windows paths to forward slash + dirParts[1] = dirParts[1].replace("\\", "/"); + if( !aliases.containsKey( dirParts[0] ) ) { + aliases.put( dirParts[0], Paths.get( dirParts[1] ) ); + } + } + } + } + + ResourceManager resourceManager = getResourceManager(webroot, transferMinSize, aliases, webInfDir); + + + // For non=Adobe (Lucee), create actual servlet context + if( serverOptions.cfEngineName().toLowerCase().indexOf("adobe") == -1 ) { + servletBuilder.setResourceManager(resourceManager); + DeploymentManager manager = defaultContainer().addDeployment(servletBuilder); + manager.deploy(); + + deployment = new ServletDeployment( manager.start(), manager ); + LOG.debug("New servlet context created for [" + deploymentKey + "]" ); + // For Adobe + } else { + // For default deployment, create initial resource manager and deploy + if( deploymentKey.equals(ServletDeployment.DEFAULT) ) { + + servletBuilder.setResourceManager( new HostResourceManager( resourceManager ) ); + DeploymentManager manager = defaultContainer().addDeployment(servletBuilder); + manager.deploy(); + deployment = new ServletDeployment( manager.start(), manager ); + LOG.debug("Initial servlet context created for [" + deploymentKey + "]" ); + + // For all subsequent deploys, reuse default deployment and simply add new resource manager + } else { + + ((HostResourceManager)servletBuilder.getResourceManager()).addResourceManager( deploymentKey, resourceManager ); + deployment = deployments.get(ServletDeployment.DEFAULT); + LOG.debug("New resource manager added to deployment [" + deploymentKey + "]" ); + + } + + } + + deployments.put(deploymentKey, deployment); + + return deployment; + } + + private void logOnce( String deploymentKey, String type, String severity, String message ) { + String logKey = deploymentKey + type; + severity = severity.toLowerCase(); + if( !deploymentKeyWarnings.contains( logKey ) ) { + deploymentKeyWarnings.add( logKey ); + switch (severity) { + case "trace": + LOG.trace( message ); + break; + case "debug": + LOG.debug( message ); + break; + case "info": + LOG.info( message ); + break; + case "warn": + LOG.warn( message ); + break; + case "error": + LOG.error( message ); + break; + case "fatal": + LOG.fatal( message ); + break; + default: + LOG.info( message ); + } + + } + } + + private Boolean isHeaderSafe( HttpServerExchange exchange, String deploymentKey, String headerName ) { + HeaderValues headerValues = exchange.getRequestHeaders().get( headerName ); + if( headerValues != null && headerValues.size() > 1 ) { + exchange.getResponseHeaders().put(Headers.CONTENT_TYPE, "text/plain"); + exchange.setStatusCode(403); + exchange.getResponseSender().send( "The request header [" + headerName + "] was supplied " + headerValues.size() + " times which is likely a configuration error. CommandBox won't serve requests with fishy ModCFML headers for security." ); + logOnce( deploymentKey, "SharedKeyNotMatch", "debug", "The request header [" + headerName + "] was supplied " + headerValues.size() + " times which is likely a configuration error. The values are " + headerValues.toString() + "" + + ". CommandBox won't serve requests with fishy ModCFML headers for security." ); + return false; + } + return true; + } - public DeploymentManager getManager() { - return manager; + public static HttpServerExchange getCurrentExchange() { + return currentExchange.get(); } + public static void setCurrentExchange( HttpServerExchange exchange ) { + currentExchange.set( exchange ); + } + public static class ServerState { public static final String STARTING = "STARTING"; @@ -1296,4 +1525,35 @@ public void stopListening(boolean systemExitOnStop) { } + public class ServletDeployment { + + private final HttpHandler servletInitialHandler; + private final DeploymentManager deploymentManager; + public final static String DEFAULT = "default"; + + public ServletDeployment(HttpHandler servletInitialHandler, DeploymentManager deploymentManager) { + this.servletInitialHandler = servletInitialHandler; + this.deploymentManager = deploymentManager; + } + + public HttpHandler getServletInitialHandler() { + return servletInitialHandler; + } + + public DeploymentManager getDeploymentManager() { + return deploymentManager; + } + } + + private class MaxContextsException extends Exception { + + public MaxContextsException( String message ) { + super( message ); + } + + } + + } + + diff --git a/src/main/java/runwar/Start.java b/src/main/java/runwar/Start.java index 8dec4c0e..8cfaa7cc 100644 --- a/src/main/java/runwar/Start.java +++ b/src/main/java/runwar/Start.java @@ -31,20 +31,6 @@ public Start(int seconds) { new Server(seconds); } - private static void launchServer(String balanceHost, ServerOptions serverOptions){ - String[] schemeHostAndPort = balanceHost.split(":"); - if(schemeHostAndPort.length != 3) { - throw new RuntimeException("hosts for balancehost should have scheme, host and port, e.g.: http://127.0.0.1:55555"); - } - String host = schemeHostAndPort[1].replaceAll("^//", ""); - int port = Integer.parseInt(schemeHostAndPort[2]); - int stopPort = port+1; - RunwarLogger.LOG.info("Starting instance: " + host + " on port "+ schemeHostAndPort[2]); - LaunchUtil.relaunchAsBackgroundProcess(serverOptions.host(host).startedFromCommandLine(true) - .httpPort(port).stopPort(stopPort).loadBalance(""), false); - - } - public static void main(String[] args) throws Exception { ServerOptions serverOptions = CommandLineHandler.parseLogArguments(args); LoggerFactory.configure(serverOptions); @@ -58,75 +44,15 @@ public static void main(String[] args) throws Exception { serverOptions = CommandLineHandler.parseArguments(args); } serverOptions.startedFromCommandLine(true); - if(serverOptions.loadBalance() != null && serverOptions.loadBalance().length > 0) { - final List balanceHosts = new ArrayList(); - RunwarLogger.LOG.info("Initializing..."); - final LoadBalancingProxyClient loadBalancer = new LoadBalancingProxyClient(); - for(String balanceHost : serverOptions.loadBalance()) { - if(serverOptions.warFile() != null) { - RunwarLogger.LOG.info("Starting instance of " + serverOptions.warFile().getParent() +"..."); - launchServer(balanceHost,serverOptions); - } - loadBalancer.addHost(new URI(balanceHost)); - balanceHosts.add(balanceHost); - RunwarLogger.LOG.info("Added balanced host: " + balanceHost); - Thread.sleep(3000); - } - int port = serverOptions.httpPort(); - loadBalancer.setConnectionsPerThread(20); - RunwarLogger.LOG.info("Hosts loaded"); - - RunwarLogger.LOG.info("Starting load balancer on 127.0.0.1 port " + port + "..."); - ProxyHandler proxyHandler = ProxyHandler.builder().setProxyClient(loadBalancer) - .setMaxRequestTime(30000).setNext(ResponseCodeHandler.HANDLE_404).build(); - Undertow reverseProxy = Undertow.builder().addHttpListener(port, "localhost").setIoThreads(4) - .setHandler(proxyHandler).build(); - reverseProxy.start(); - RunwarLogger.LOG.info("View balancer admin on http://127.0.0.1:9080"); - Undertow adminServer = Undertow.builder() - .addHttpListener(9080, "localhost") - .setHandler(new HttpHandler() { - @Override - public void handleRequest(final HttpServerExchange exchange) throws Exception { - exchange.getResponseHeaders().put(Headers.CONTENT_TYPE, "text/html"); - if(exchange.getQueryParameters().get("addHost") != null) { - String balanceHost = URLDecoder.decode(exchange.getQueryParameters().get("addHost").toString(),"UTF-8"); - balanceHost = balanceHost.replaceAll("]|\\[", ""); - loadBalancer.addHost(new URI(balanceHost)); - balanceHosts.add(balanceHost); - } - if(exchange.getQueryParameters().get("removeHost") != null) { - String balanceHost = URLDecoder.decode(exchange.getQueryParameters().get("removeHost").toString(),"UTF-8"); - balanceHost = balanceHost.replaceAll("]|\\[", ""); - loadBalancer.removeHost(new URI(balanceHost)); - balanceHosts.remove(balanceHost); - } - String response = ""; - for(String balanceHost : balanceHosts) { - String[] schemeHostAndPort = balanceHost.split(":"); - String host = schemeHostAndPort[1].replaceAll("^//", ""); - int port = schemeHostAndPort.length >2 ? Integer.parseInt(schemeHostAndPort[2]) : 80 ; - response += balanceHost + " remove Listening: "+serverListening(host, port)+"
"; - } - exchange.getResponseSender().send("

Balanced Hosts

Add Host:
" + response); - } - }).build(); - adminServer.start(); - - - RunwarLogger.LOG.info("Started load balancer."); - - } else { - - Server server = new Server(); - try { - server.startServer(serverOptions); - } catch (Exception e) { - e.printStackTrace(); - System.exit(1); - } - + + Server server = new Server(); + try { + server.startServer(serverOptions); + } catch (Exception e) { + e.printStackTrace(); + System.exit(1); } + } public static boolean serverListening(String host, int port) { diff --git a/src/main/java/runwar/logging/LoggerFactory.java b/src/main/java/runwar/logging/LoggerFactory.java index dc47aca6..388692ea 100644 --- a/src/main/java/runwar/logging/LoggerFactory.java +++ b/src/main/java/runwar/logging/LoggerFactory.java @@ -6,30 +6,41 @@ import java.util.ArrayList; import java.util.Enumeration; import java.util.List; -import org.apache.log4j.Appender; -import org.apache.log4j.ConsoleAppender; -import org.apache.log4j.Level; -import org.apache.log4j.LogManager; -import org.apache.log4j.Logger; -import org.apache.log4j.PatternLayout; -import org.apache.log4j.RollingFileAppender; - -public class LoggerFactory { +import org.apache.logging.log4j.core.Appender; +import org.apache.logging.log4j.core.Filter; +import org.apache.logging.log4j.core.appender.ConsoleAppender; +import org.apache.logging.log4j.Level; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.core.LoggerContext; +import org.apache.logging.log4j.core.config.Configuration; +import org.apache.logging.log4j.Logger; +import org.apache.logging.log4j.core.layout.PatternLayout; +import org.apache.logging.log4j.core.appender.RollingFileAppender; +import org.apache.logging.log4j.core.config.NullConfiguration; +import org.apache.logging.log4j.core.config.LoggerConfig; +import org.apache.logging.log4j.core.filter.ThresholdFilter; +import org.apache.logging.log4j.core.appender.rolling.SizeBasedTriggeringPolicy; +import org.apache.logging.log4j.core.appender.rolling.DefaultRolloverStrategy; + +public class LoggerFactory { private static volatile boolean initialized = false; private static volatile String logFile; private static volatile String logLevel; - private static volatile String logPattern; private static volatile List appenders; - private static volatile List loggers; - private static volatile List urlrewriteLoggers; + private static volatile List loggers; + private static volatile List urlrewriteLoggers; private static volatile RollingFileAppender rewriteLogAppender; private static volatile ConsoleAppender consoleAppender; private static ServerOptions serverOptions; public static synchronized void configure(ServerOptions options) { - - Logger.getRootLogger().getLoggerRepository().resetConfiguration(); + + LoggerContext loggerContext = ((LoggerContext)LogManager.getContext(false)); + loggerContext.setConfiguration(new NullConfiguration()); + loggerContext.updateLoggers(); + Configuration log4jConfig = loggerContext.getConfiguration(); + serverOptions = options; logLevel = serverOptions.logLevel().toUpperCase(); appenders = new ArrayList<>(); @@ -37,151 +48,154 @@ public static synchronized void configure(ServerOptions options) { Level level = Level.toLevel(logLevel); consoleAppender = consoleAppender(serverOptions.getLogPattern()); appenders.add(consoleAppender); - Logger.getRootLogger().setLevel(Level.WARN); - Logger.getRootLogger().addAppender(consoleAppender); + LoggerConfig rootLoggerConfig = log4jConfig.getLoggerConfig(LogManager.ROOT_LOGGER_NAME); + // For some reason the null configuration above doesn't remove the default console appender in the root logger. + rootLoggerConfig.getAppenders().forEach( (appenderName,appender) -> rootLoggerConfig.removeAppender(appenderName) ); + rootLoggerConfig.setLevel(Level.WARN); + rootLoggerConfig.addAppender(consoleAppender,level,null); + - Logger DORKBOX_LOG = Logger.getLogger("dorkbox.systemTray.SystemTray"); + LoggerConfig DORKBOX_LOG = new LoggerConfig( "dorkbox.systemTray.SystemTray", Level.ERROR, false ); loggers.add(DORKBOX_LOG); - Logger OSCACHE_LOG = Logger.getLogger("com.opensymphony.oscache.base.Config"); + LoggerConfig OSCACHE_LOG = new LoggerConfig( "com.opensymphony.oscache.base.Config", Level.WARN, false ); loggers.add(OSCACHE_LOG); - Logger JBOSS_LOG = Logger.getLogger("org.jboss.logging"); + LoggerConfig JBOSS_LOG = new LoggerConfig( "org.jboss.logging", Level.WARN, false ); loggers.add(JBOSS_LOG); - Logger UNDERTOW_LOG = Logger.getLogger("io.undertow.servlet"); + LoggerConfig UNDERTOW_LOG = new LoggerConfig( "io.undertow.servlet", Level.WARN, false ); loggers.add(UNDERTOW_LOG); - Logger UNDERTOW_PREDICATE_LOG = Logger.getLogger("io.undertow.predicate"); + LoggerConfig UNDERTOW_PREDICATE_LOG = new LoggerConfig( "io.undertow.predicate", Level.WARN, false ); loggers.add(UNDERTOW_PREDICATE_LOG); - Logger UNDERTOW_REQUEST_DUMPER_LOG = Logger.getLogger("io.undertow.request.dump"); + LoggerConfig UNDERTOW_PROXY_LOG = new LoggerConfig( "io.undertow.proxy", Level.WARN, false ); + loggers.add(UNDERTOW_PROXY_LOG); + + LoggerConfig UNDERTOW_REQUEST_DUMPER_LOG = new LoggerConfig( "io.undertow.request.dump", Level.INFO, false ); loggers.add(UNDERTOW_REQUEST_DUMPER_LOG); - - Logger UNDERTOW_IO_LOG = Logger.getLogger("io.undertow"); + + LoggerConfig UNDERTOW_REQUEST_SECURITY = new LoggerConfig( "io.undertow.request.security", Level.WARN, false ); + loggers.add(UNDERTOW_REQUEST_SECURITY); + + LoggerConfig UNDERTOW_IO_LOG = new LoggerConfig( "io.undertow", Level.WARN, false ); loggers.add(UNDERTOW_IO_LOG); - Logger XNIO_LOG = Logger.getLogger("org.xnio"); + LoggerConfig XNIO_LOG = new LoggerConfig( "org.xnio", Level.WARN, false ); loggers.add(XNIO_LOG); - Logger HTTP_CLIENT_LOG = Logger.getLogger("org.apache.http.client.protocol"); + LoggerConfig HTTP_CLIENT_LOG = new LoggerConfig( "org.apache.http.client.protocol", Level.WARN, false ); loggers.add(HTTP_CLIENT_LOG); - Logger RUNWAR_SERVER = Logger.getLogger("runwar.server"); + LoggerConfig RUNWAR_SERVER = new LoggerConfig( "runwar.server", level, false ); loggers.add(RUNWAR_SERVER); - Logger RUNWAR_CONTEXT = Logger.getLogger("runwar.context"); + LoggerConfig RUNWAR_CONTEXT = new LoggerConfig( "runwar.context", level, false ); loggers.add(RUNWAR_CONTEXT); - Logger RUNWAR_CONFIG = Logger.getLogger("runwar.config"); + LoggerConfig RUNWAR_CONFIG = new LoggerConfig( "runwar.config", Level.INFO, false ); loggers.add(RUNWAR_CONFIG); - Logger RUNWAR_SECURITY = Logger.getLogger("runwar.security"); + LoggerConfig RUNWAR_SECURITY = new LoggerConfig( "runwar.security", Level.WARN, false ); loggers.add(RUNWAR_SECURITY); - Logger RUNWAR_REQUEST = Logger.getLogger("runwar.request"); + LoggerConfig RUNWAR_REQUEST = new LoggerConfig( "runwar.request", Level.WARN, false ); loggers.add(RUNWAR_REQUEST); - - Logger RUNWAR_BACKGROUND = Logger.getLogger("runwar.background"); - RUNWAR_BACKGROUND.addAppender(consoleAppender("%m%n")); + LoggerConfig RUNWAR_BACKGROUND = new LoggerConfig( "runwar.background", Level.WARN, false ); RUNWAR_BACKGROUND.setLevel(Level.TRACE); - RUNWAR_BACKGROUND.setAdditivity(false); - - if (serverOptions.urlRewriteLog() != null) { - rewriteLogAppender = new RollingFileAppender(); - rewriteLogAppender.setName("URLRewriteFileLogger"); - rewriteLogAppender.setFile(serverOptions.urlRewriteLog().getAbsolutePath()); - rewriteLogAppender.setLayout(new PatternLayout(serverOptions.getLogPattern())); - rewriteLogAppender.setThreshold(Level.toLevel(logLevel)); - rewriteLogAppender.setAppend(true); - rewriteLogAppender.setMaxFileSize("10MB"); - rewriteLogAppender.setMaxBackupIndex(3); - rewriteLogAppender.activateOptions(); - } + RUNWAR_BACKGROUND.addAppender(consoleAppender("%m%n"),RUNWAR_BACKGROUND.getLevel(), null); + RUNWAR_BACKGROUND.setAdditive(false); - RUNWAR_SERVER.setLevel(level); - RUNWAR_CONTEXT.setLevel(level); - RUNWAR_CONFIG.setLevel(Level.INFO); - RUNWAR_SECURITY.setLevel(Level.WARN); - RUNWAR_REQUEST.setLevel(Level.WARN); - DORKBOX_LOG.setLevel(Level.ERROR); - UNDERTOW_LOG.setLevel(Level.WARN); - UNDERTOW_IO_LOG.setLevel(Level.WARN); - XNIO_LOG.setLevel(Level.WARN); - HTTP_CLIENT_LOG.setLevel(Level.WARN); - UNDERTOW_REQUEST_DUMPER_LOG.setLevel(Level.INFO); System.setProperty("org.eclipse.jetty.LEVEL", "WARN"); - if (serverOptions.debug() || !logLevel.equalsIgnoreCase("info")) { + if( serverOptions.resourceManagerLogging() ) { + RUNWAR_REQUEST.setLevel(level); + } + if (logLevel.equalsIgnoreCase("trace")) { DORKBOX_LOG.setLevel(level); - appenders.forEach(DORKBOX_LOG::addAppender); + appenders.forEach(a -> DORKBOX_LOG.addAppender(a, DORKBOX_LOG.getLevel(), null)); UNDERTOW_LOG.setLevel(level); UNDERTOW_PREDICATE_LOG.setLevel(level); + UNDERTOW_PROXY_LOG.setLevel(level); + UNDERTOW_IO_LOG.setLevel(level); HTTP_CLIENT_LOG.setLevel(level); RUNWAR_CONFIG.setLevel(level); RUNWAR_SERVER.setLevel(level); RUNWAR_CONTEXT.setLevel(level); RUNWAR_SECURITY.setLevel(level); - // This logger is only used in the resource mapper and is really chatty - // Consider a setting to enable it only when troubleshooting file system mapping issues - if( serverOptions.resourceManagerLogging() ) { - RUNWAR_REQUEST.setLevel(level); - } - - Logger.getRootLogger().setLevel(level); - configureUrlRewriteLoggers(true); - } else { - RUNWAR_REQUEST.setLevel(Level.INFO); + rootLoggerConfig.setLevel(level); + configureUrlRewriteLoggers(true,log4jConfig); + } else { RUNWAR_SECURITY.setLevel(Level.DEBUG); UNDERTOW_PREDICATE_LOG.setLevel(Level.DEBUG); - configureUrlRewriteLoggers(false); + UNDERTOW_PROXY_LOG.setLevel(Level.DEBUG); + configureUrlRewriteLoggers(false,log4jConfig); } } if (serverOptions.hasLogDir()) { logFile = serverOptions.logDir().getPath() + '/' + serverOptions.logFileName() + ".out.txt"; - RollingFileAppender fa = new RollingFileAppender(); - fa.setName("FileLogger"); - fa.setFile(logFile); - fa.setLayout(new PatternLayout(serverOptions.logPattern())); - fa.setThreshold(Level.toLevel(logLevel)); - fa.setAppend(true); - fa.setMaxFileSize("10MB"); - fa.setMaxBackupIndex(10); - fa.activateOptions(); + + RollingFileAppender fa = RollingFileAppender.newBuilder() + .setName("FileLogger") + .withFileName(logFile) + .withFilePattern( logFile + "%i") + .setLayout( + PatternLayout.newBuilder() + .withPattern(serverOptions.getLogPattern()) + .build() ) + .setFilter(ThresholdFilter.createFilter(Level.toLevel(logLevel), Filter.Result.ACCEPT, Filter.Result.DENY)) + .withAppend(true) + .withPolicy(SizeBasedTriggeringPolicy.createPolicy("10MB")) + .withStrategy( + DefaultRolloverStrategy.newBuilder() + .withMax("10") + .build()) + .build(); + + fa.start(); appenders.add(fa); - Logger.getRootLogger().addAppender(fa); + rootLoggerConfig.addAppender(fa,Level.toLevel(logLevel),null); } - Logger.getRootLogger().addAppender(consoleAppender); - loggers.forEach(logger -> appenders.forEach(appender -> { - logger.addAppender(appender); - logger.setAdditivity(false); - })); + loggers.forEach(logger -> { + appenders.forEach(appender -> logger.addAppender(appender,logger.getLevel(),null) ); + log4jConfig.addLogger(logger.getName(), logger); + }); + loggerContext.updateLoggers(); initialized = true; - if (System.getProperty("runwar.dumploggerstyles") != null) { + if (System.getProperty("runwar.dumploggerstyles") != null && Boolean.parseBoolean(System.getProperty("runwar.dumploggerstyles"))) { RunwarLogger.LOG.trace("This is a TRACE message"); RunwarLogger.LOG.debug("This is a DEBUG message"); RunwarLogger.LOG.warn("This is a WARN message"); RunwarLogger.LOG.error("This is an ERROR message"); } + if (System.getProperty("runwar.listloggers") != null && Boolean.parseBoolean(System.getProperty("runwar.listloggers"))) { + listLoggers(); + } } private static ConsoleAppender consoleAppender(String pattern) { - ConsoleAppender appender = new ConsoleAppender(); - PatternLayout layout = new PatternLayout(); - layout.setConversionPattern(pattern); - appender.setLayout(layout); - appender.setName("rw.console"); - appender.setThreshold(Level.toLevel(logLevel)); - appender.activateOptions(); + + ConsoleAppender appender = ConsoleAppender.newBuilder() + .setName("rw.console") + .setLayout( + PatternLayout.newBuilder() + .withPattern(pattern) + .build() ) + .setFilter(ThresholdFilter.createFilter(Level.toLevel(logLevel), Filter.Result.ACCEPT, Filter.Result.DENY)) + .build(); + + appender.start(); + return appender; } @@ -190,26 +204,26 @@ public static boolean isInitialized() { } public static synchronized boolean initialize() { - return initialize(false); + return true; + //return initialize(false); } public static synchronized boolean initialize(boolean force) { - if (!initialized || force) - configure(new ServerOptionsImpl().logDir("")); - return initialized; + return true; + // if (!initialized || force) +// configure(new ServerOptionsImpl().logDir("")); + // return initialized; } - public static void configureUrlRewriteLoggers(boolean isTrace) { - boolean hadLoggers = urlrewriteLoggers == null; - Logger REWRITE_CONDITION_LOG = Logger.getLogger("org.tuckey.web.filters.urlrewrite.Condition"); - Logger REWRITE_RULE_LOG = Logger.getLogger("org.tuckey.web.filters.urlrewrite.RuleBase"); - Logger REWRITE_SUBSTITUTION_LOG = Logger - .getLogger("org.tuckey.web.filters.urlrewrite.substitution.VariableReplacer"); - Logger REWRITE_EXECUTION_LOG = Logger.getLogger("org.tuckey.web.filters.urlrewrite.RuleExecutionOutput"); - Logger REWRITE_WRITER_LOG = Logger.getLogger("org.tuckey.web.filters.urlrewrite.UrlRewriter"); - Logger REWRITE_URL_LOG = Logger.getLogger("org.tuckey.web.filters.urlrewrite"); - Logger REWRITE_FILTER = Logger.getLogger("runwar.util.UrlRewriteFilter"); - Logger REWRITE_LOG = Logger.getLogger("org.tuckey.web.filters.urlrewrite.utils.Log"); + public static void configureUrlRewriteLoggers(boolean isTrace, Configuration log4jConfig) { + LoggerConfig REWRITE_CONDITION_LOG = new LoggerConfig( "org.tuckey.web.filters.urlrewrite.Condition", Level.WARN, false ); + LoggerConfig REWRITE_RULE_LOG = new LoggerConfig( "org.tuckey.web.filters.urlrewrite.RuleBase", Level.WARN, false ); + LoggerConfig REWRITE_SUBSTITUTION_LOG = new LoggerConfig( "org.tuckey.web.filters.urlrewrite.substitution.VariableReplacer", Level.WARN, false ); + LoggerConfig REWRITE_EXECUTION_LOG = new LoggerConfig( "org.tuckey.web.filters.urlrewrite.RuleExecutionOutput", Level.WARN, false ); + LoggerConfig REWRITE_WRITER_LOG = new LoggerConfig( "org.tuckey.web.filters.urlrewrite.UrlRewriter", Level.WARN, false ); + LoggerConfig REWRITE_URL_LOG = new LoggerConfig( "org.tuckey.web.filters.urlrewrite", Level.WARN, false ); + LoggerConfig REWRITE_FILTER = new LoggerConfig( "runwar.util.UrlRewriteFilter", Level.WARN, false ); + LoggerConfig REWRITE_LOG = new LoggerConfig( "org.tuckey.web.filters.urlrewrite.utils.Log", Level.WARN, false ); urlrewriteLoggers = new ArrayList<>(); urlrewriteLoggers.add(REWRITE_CONDITION_LOG); urlrewriteLoggers.add(REWRITE_RULE_LOG); @@ -220,44 +234,64 @@ public static void configureUrlRewriteLoggers(boolean isTrace) { urlrewriteLoggers.add(REWRITE_FILTER); urlrewriteLoggers.add(REWRITE_LOG); - if (rewriteLogAppender != null) { - RunwarLogger.CONF_LOG.infof("Enabling URL rewrite log: %s", rewriteLogAppender.getFile()); - urlrewriteLoggers.forEach(logger -> { - logger.addAppender(rewriteLogAppender); - logger.setAdditivity(false); - }); - } + urlrewriteLoggers.forEach(logger -> { + log4jConfig.addLogger(logger.getName(), logger); + }); if (isTrace) { RunwarLogger.CONF_LOG.infof("Enabling URL rewrite log level: %s", "TRACE"); urlrewriteLoggers.forEach(logger -> { logger.setLevel(Level.TRACE); - logger.addAppender(consoleAppender(serverOptions.getLogPattern())); - logger.setAdditivity(false); + logger.addAppender(consoleAppender(serverOptions.getLogPattern()),logger.getLevel(),null); }); } else { - if(!hadLoggers){ - RunwarLogger.CONF_LOG.infof("Enabling URL rewrite log level: %s", "DEBUG"); - } - urlrewriteLoggers.forEach(logger -> { - logger.setLevel(Level.WARN); - logger.addAppender(consoleAppender(serverOptions.getLogPattern())); - logger.setAdditivity(false); - }); + RunwarLogger.CONF_LOG.infof("Enabling URL rewrite log level: %s", "DEBUG"); REWRITE_EXECUTION_LOG.setLevel(Level.DEBUG); REWRITE_WRITER_LOG.setLevel(Level.DEBUG); + urlrewriteLoggers.forEach(logger -> { + logger.addAppender(consoleAppender(serverOptions.getLogPattern()),logger.getLevel(),null); + }); + } + + if (serverOptions.urlRewriteLog() != null) { + rewriteLogAppender = RollingFileAppender.newBuilder() + .setName("URLRewriteFileLogger") + .withFileName(serverOptions.urlRewriteLog().getAbsolutePath()) + .withFilePattern( serverOptions.urlRewriteLog().getAbsolutePath() + "%i") + .setLayout( + PatternLayout.newBuilder() + .withPattern(serverOptions.getLogPattern()) + .build() ) + .setFilter(ThresholdFilter.createFilter(Level.toLevel(logLevel), Filter.Result.ACCEPT, Filter.Result.DENY)) + .withAppend(true) + .withPolicy(SizeBasedTriggeringPolicy.createPolicy("10MB")) + .withStrategy( + DefaultRolloverStrategy.newBuilder() + .withMax("3") + .build()) + .build(); + + rewriteLogAppender.start(); + + RunwarLogger.CONF_LOG.infof("Enabling URL rewrite log: %s", rewriteLogAppender.getFileName()); + urlrewriteLoggers.forEach(logger -> { + logger.addAppender(rewriteLogAppender,logger.getLevel(),null); + }); } + } public static void listLoggers() { - for (Enumeration loggers = LogManager.getCurrentLoggers(); loggers.hasMoreElements();) { - Logger logger = (Logger) loggers.nextElement(); - System.out.println("Logger: " + logger.getName()); - for (Enumeration appenders = logger.getAllAppenders(); appenders.hasMoreElements();) { - Appender appender = (Appender) appenders.nextElement(); - System.out.println(" appender: " + appender.getName()); - } - } + + LoggerContext loggerContext = ((LoggerContext)LogManager.getContext(false)); + Configuration log4jConfig = loggerContext.getConfiguration(); + System.out.println("Printing out " + log4jConfig.getLoggers().size() + " loggers."); + + log4jConfig.getLoggers().forEach( (loggerName,loggerConfig) -> { + System.out.println("Logger: " + loggerConfig.getName() + " (" + loggerConfig.getLevel().name() + ")"); + loggerConfig.getAppenders().forEach( (appenderName,appender) -> System.out.println(" appender: " + appenderName) ); + } ); + } } diff --git a/src/main/java/runwar/logging/RunwarLogger.java b/src/main/java/runwar/logging/RunwarLogger.java index 8dfddae6..09a7c1a4 100644 --- a/src/main/java/runwar/logging/RunwarLogger.java +++ b/src/main/java/runwar/logging/RunwarLogger.java @@ -38,14 +38,6 @@ public interface RunwarLogger extends BasicLogger { boolean initialized = LoggerFactory.initialize(); - /* - * public default void doityo() { - * LoggerFactory.init(Server.getServerOptions()); - * System.out.println("default method"); - * System.setProperty("runwar.logfile", "/tmp/fart"); - * System.setProperty("runwar.logLevel", "DEBUG" ); - * System.setProperty("runwar.logpattern", "%m%n" ); } - */ Logger LOG = Logger.getLogger("runwar.server"); Logger CONTEXT_LOG = Logger.getLogger("runwar.context"); Logger CONF_LOG = Logger.getLogger("runwar.config"); @@ -54,25 +46,6 @@ public interface RunwarLogger extends BasicLogger { Logger BACKGROUNDED_LOG = Logger.getLogger("runwar.background"); Logger MONITOR_LOG = Logger.getLogger("runwar.monitor"); - // RunwarLogger ROOT_LOGGER = Logger.getMessageLogger(RunwarLogger.class, RunwarLogger.class.getPackage().getName()); -// RunwarLogger CLIENT_LOGGER = Logger.getMessageLogger(RunwarLogger.class, ClientConnection.class.getPackage().getName()); -// -// RunwarLogger REQUEST_LOGGER = Logger.getMessageLogger(RunwarLogger.class, RunwarLogger.class.getPackage().getName() + ".request"); -// RunwarLogger SESSION_LOGGER = Logger.getMessageLogger(RunwarLogger.class, RunwarLogger.class.getPackage().getName() + ".session"); -// RunwarLogger SECURITY_LOGGER = Logger.getMessageLogger(RunwarLogger.class, RunwarLogger.class.getPackage().getName() + ".request.security"); -// RunwarLogger PROXY_REQUEST_LOGGER = Logger.getMessageLogger(RunwarLogger.class, RunwarLogger.class.getPackage().getName() + ".proxy"); -// RunwarLogger REQUEST_DUMPER_LOGGER = Logger.getMessageLogger(RunwarLogger.class, RunwarLogger.class.getPackage().getName() + ".request.dump"); - - /** - * Logger used for IO exceptions. Generally these should be suppressed, - * because they are of little interest, and it is easy for an attacker to - * fill up the logs by intentionally causing IO exceptions. - * @param cause the cause - */ - // RunwarLogger REQUEST_IO_LOGGER = - // Logger.getMessageLogger(RunwarLogger.class, - // RunwarLogger.class.getPackage().getName() + ".request.io"); - @LogMessage(level = ERROR) @Message(id = 5001, value = "An exception occurred processing the request") void exceptionProcessingRequest(@Cause Throwable cause); diff --git a/src/main/java/runwar/options/CommandLineHandler.java b/src/main/java/runwar/options/CommandLineHandler.java index d859c241..60925312 100644 --- a/src/main/java/runwar/options/CommandLineHandler.java +++ b/src/main/java/runwar/options/CommandLineHandler.java @@ -390,7 +390,7 @@ private static Options getOptions() { options.addOption(OptionBuilder .withLongOpt("transfer-min-size") - .withDescription("Minimun transfer file size to offload to OS. (100)\n") + .withDescription("Minimun transfer file size to offload to OS.") .hasArg().withArgName(Keys.TRANSFERMINSIZE).withType(Long.class) .create(Keys.TRANSFERMINSIZE)); @@ -620,6 +620,48 @@ private static Options getOptions() { .hasArg().withArgName("true|false") .create(Keys.CACHESERVLETPATHS)); + options.addOption(OptionBuilder + .withLongOpt("file-cache-total-size-mb") + .withDescription("Total size of the resource cache in megabytes. Only used if cache-servlet-paths is enabled") + .hasArg().withArgName("true|false") + .create(Keys.FILECACHETOTALSIZEMB)); + + options.addOption(OptionBuilder + .withLongOpt("file-cache-max-file-size-kb") + .withDescription("Max size of idividual static files to enable caching for them. Only used if cache-servlet-paths is enabled") + .hasArg().withArgName("true|false") + .create(Keys.FILECACHEMAXFILESIZEKB)); + + options.addOption(OptionBuilder + .withLongOpt("auto-create-contexts") + .withDescription("Automatically create new servlet contexts based on host name (for use behind web server using virtual hosts)") + .hasArg().withArgName("true|false") + .create(Keys.AUTOCREATECONTEXTS)); + + options.addOption(OptionBuilder + .withLongOpt("auto-create-contexts-secret") + .withDescription("Secret for automatically creating new servlet contexts") + .hasArg().withArgName("secret") + .create(Keys.AUTOCREATECONTEXTSSECRET)); + + options.addOption(OptionBuilder + .withLongOpt("auto-create-contexts-max") + .withDescription("Max number of servlet contexts to create") + .hasArg().withArgName("max") + .create(Keys.AUTOCREATECONTEXTSMAX)); + + options.addOption(OptionBuilder + .withLongOpt("auto-create-contexts-vdirs") + .withDescription("Automatically copy virtual directories from the front end web server to Undertow)") + .hasArg().withArgName("true|false") + .create(Keys.AUTOCREATECONTEXTSVDIRS)); + + options.addOption(OptionBuilder + .withLongOpt("log-pattern") + .withDescription("Log4j formatter pattern for log messages") + .hasArg().withArgName("[%-5p] %c: %m%n") + .create(Keys.LOGPATTERN)); + options.addOption(new Option("h", Keys.HELP, false, "print this message")); options.addOption(new Option("v", "version", false, "print runwar version and undertow version")); @@ -656,6 +698,10 @@ public static ServerOptions parseLogArguments(String[] args, ServerOptions serve serverOptions.logLevel(line.getOptionValue("level")); } + if (hasOptionValue(line, Keys.LOGPATTERN)) { + serverOptions.logPattern(line.getOptionValue(Keys.LOGPATTERN)); + } + if (line.hasOption(Keys.WAR)) { String warPath = line.getOptionValue(Keys.WAR); serverOptions.warFile(getFile(warPath)); @@ -669,6 +715,19 @@ public static ServerOptions parseLogArguments(String[] args, ServerOptions serve } else { serverOptions.logDir(); } + + + if (hasOptionValue(line, Keys.RESOURCEMANAGERLOGGING)) { + serverOptions.resourceManagerLogging(Boolean.valueOf(line.getOptionValue(Keys.RESOURCEMANAGERLOGGING))); + } + + if (hasOptionValue(line, Keys.URLREWRITELOG)) { + serverOptions.urlRewriteLog(new File(line.getOptionValue(Keys.URLREWRITELOG))); + if (!line.hasOption(Keys.URLREWRITEENABLE)) { + serverOptions.urlRewriteEnable(true); + } + } + return serverOptions; } catch (Exception exp) { exp.printStackTrace(); @@ -869,12 +928,6 @@ public static ServerOptions parseArguments(String[] args, ServerOptions serverOp serverOptions.urlRewriteEnable(true); } } - if (hasOptionValue(line, Keys.URLREWRITELOG)) { - serverOptions.urlRewriteLog(new File(line.getOptionValue(Keys.URLREWRITELOG))); - if (!line.hasOption(Keys.URLREWRITEENABLE)) { - serverOptions.urlRewriteEnable(true); - } - } if (line.hasOption(Keys.URLREWRITEENABLE)) { serverOptions.urlRewriteEnable(Boolean.valueOf(line.getOptionValue(Keys.URLREWRITEENABLE))); } @@ -928,14 +981,34 @@ public static ServerOptions parseArguments(String[] args, ServerOptions serverOp serverOptions.caseSensitiveWebServer(Boolean.valueOf(line.getOptionValue(Keys.CASESENSITIVEWEBSERVER))); } - if (hasOptionValue(line, Keys.RESOURCEMANAGERLOGGING)) { - serverOptions.resourceManagerLogging(Boolean.valueOf(line.getOptionValue(Keys.RESOURCEMANAGERLOGGING))); - } - if (hasOptionValue(line, Keys.CACHESERVLETPATHS)) { serverOptions.cacheServletPaths(Boolean.valueOf(line.getOptionValue(Keys.CACHESERVLETPATHS))); } + if (hasOptionValue(line, Keys.FILECACHETOTALSIZEMB)) { + serverOptions.fileCacheTotalSizeMB(Integer.valueOf(line.getOptionValue(Keys.FILECACHETOTALSIZEMB))); + } + + if (hasOptionValue(line, Keys.FILECACHEMAXFILESIZEKB)) { + serverOptions.fileCacheMaxFileSizeKB(Integer.valueOf(line.getOptionValue(Keys.FILECACHEMAXFILESIZEKB))); + } + + if (hasOptionValue(line, Keys.AUTOCREATECONTEXTS)) { + serverOptions.autoCreateContexts(Boolean.valueOf(line.getOptionValue(Keys.AUTOCREATECONTEXTS))); + } + + if (hasOptionValue(line, Keys.AUTOCREATECONTEXTSSECRET)) { + serverOptions.autoCreateContextsSecret(line.getOptionValue(Keys.AUTOCREATECONTEXTSSECRET)); + } + + if (hasOptionValue(line, Keys.AUTOCREATECONTEXTSMAX)) { + serverOptions.autoCreateContextsMax(Integer.valueOf(line.getOptionValue(Keys.AUTOCREATECONTEXTSMAX))); + } + + if (hasOptionValue(line, Keys.AUTOCREATECONTEXTSVDIRS)) { + serverOptions.autoCreateContextsVDirs(Boolean.valueOf(line.getOptionValue(Keys.AUTOCREATECONTEXTSVDIRS))); + } + if (line.hasOption(Keys.OPENURL)) { serverOptions.openbrowserURL(line.getOptionValue(Keys.OPENURL)); if (!line.hasOption(Keys.OPENBROWSER)) { diff --git a/src/main/java/runwar/options/ServerOptions.java b/src/main/java/runwar/options/ServerOptions.java index 2173e0b2..d92bb5e1 100644 --- a/src/main/java/runwar/options/ServerOptions.java +++ b/src/main/java/runwar/options/ServerOptions.java @@ -107,7 +107,6 @@ public static final class Keys { final static String COOKIEHTTPONLY = "cookiehttponly"; final static String COOKIESECURE = "cookiesecure"; final static String SERVERMODE = "mode"; - final static String BUFFEREnable = "bufferenable"; final static String SSLECCDISABLE = "SSLECCDISABLE"; final static String SSLSELFSIGN = "sslselfsign"; final static String SERVICE = "service"; @@ -118,6 +117,13 @@ public static final class Keys { final static String CASESENSITIVEWEBSERVER="caseSensitiveWebServer"; final static String RESOURCEMANAGERLOGGING="resourceManagerLogging"; final static String CACHESERVLETPATHS="cacheServletPaths"; + final static String FILECACHETOTALSIZEMB="fileCacheTotalSizeMB"; + final static String FILECACHEMAXFILESIZEKB="fileCacheMaxFileSizeKB"; + final static String AUTOCREATECONTEXTS="autoCreateContexts"; + final static String AUTOCREATECONTEXTSSECRET="autoCreateContextsSecret"; + final static String AUTOCREATECONTEXTSMAX="autoCreateContextsMax"; + final static String AUTOCREATECONTEXTSVDIRS="autoCreateContextsVDirs"; + final static String LOGPATTERN="logPattern"; } @@ -386,11 +392,35 @@ public static final class Keys { Boolean resourceManagerLogging(); Boolean cacheServletPaths(); + + Integer fileCacheTotalSizeMB(); + + Integer fileCacheMaxFileSizeKB(); - ServerOptions resourceManagerLogging(Boolean resourceManagerLogging); + Boolean autoCreateContexts(); + + String autoCreateContextsSecret(); + + Integer autoCreateContextsMax(); + Boolean autoCreateContextsVDirs(); + + ServerOptions resourceManagerLogging(Boolean resourceManagerLogging); + ServerOptions cacheServletPaths(Boolean cacheServletPaths); - + + ServerOptions fileCacheTotalSizeMB( Integer fileCacheTotalSizeMB ); + + ServerOptions fileCacheMaxFileSizeKB( Integer fileCacheMaxFileSizeKB ); + + ServerOptions autoCreateContexts(Boolean autoCreateContexts); + + ServerOptions autoCreateContextsSecret(String autoCreateContextsSecret); + + ServerOptions autoCreateContextsMax(Integer autoCreateContextsMax); + + ServerOptions autoCreateContextsVDirs(Boolean autoCreateContextsVDirs); + ServerOptions sslCertificate(File file); File sslCertificate(); @@ -545,10 +575,6 @@ public static final class Keys { String serverMode(); - boolean bufferEnable(); - - ServerOptions bufferEnable(boolean enable); - boolean startedFromCommandLine(); ServerOptions startedFromCommandLine(boolean enable); @@ -576,5 +602,6 @@ public static final class Keys { File predicateFile(); ServerOptions predicateFile(File predicateFile); + String getLogPattern(); } \ No newline at end of file diff --git a/src/main/java/runwar/options/ServerOptionsImpl.java b/src/main/java/runwar/options/ServerOptionsImpl.java index 8fe7d56c..d9327765 100644 --- a/src/main/java/runwar/options/ServerOptionsImpl.java +++ b/src/main/java/runwar/options/ServerOptionsImpl.java @@ -78,7 +78,7 @@ public class ServerOptionsImpl implements ServerOptions { private String gzipPredicate = "request-larger-than(1500)"; - private Long transferMinSize = (long) 100; + private Long transferMinSize = (long) 1024 * 1024 * 10; // 10 MB private boolean mariadb4jEnable = false; @@ -118,8 +118,6 @@ public class ServerOptionsImpl implements ServerOptions { private JSONArray trayConfigJSON; - private boolean bufferEnable = false; - private boolean sslEccDisable = true; private boolean sslSelfSign = false; @@ -135,7 +133,19 @@ public class ServerOptionsImpl implements ServerOptions { private Boolean resourceManagerLogging= false; private Boolean cacheServletPaths= false; - + + private Integer fileCacheTotalSizeMB = 50; // 50MB cache (up to 10 buffers) + + private Integer fileCacheMaxFileSizeKB = 50; // cache files up to 50KB in size; + + private Boolean autoCreateContexts= false; + + private String autoCreateContextsSecret=""; + + private Integer autoCreateContextsMax=200; + + private Boolean autoCreateContextsVDirs=false; + private final Map aliases = new HashMap<>(); private Set contentDirectories = new HashSet<>(); @@ -2058,24 +2068,6 @@ public String serverMode() { return Mode.DEFAULT; } - - /* - * @see runwar.options.ServerOptions#bufferEnable(boolean) - */ - @Override - public ServerOptions bufferEnable(boolean enable) { - this.bufferEnable = enable; - return this; - } - - /* - * @see runwar.options.ServerOptions#bufferEnable() - */ - @Override - public boolean bufferEnable() { - return this.bufferEnable ; - } - /* * @see runwar.options.ServerOptions#startedFromCommandLine(boolean) */ @@ -2191,7 +2183,77 @@ public ServerOptions resourceManagerLogging(Boolean resourceManagerLogging) { this.resourceManagerLogging = resourceManagerLogging; return this; } + + /* + * @see runwar.options.ServerOptions#autoCreateContexts() + */ + @Override + public Boolean autoCreateContexts() { + return autoCreateContexts; + } + + /* + * @see runwar.options.ServerOptions#autoCreateContexts(boolean) + */ + @Override + public ServerOptions autoCreateContexts(Boolean autoCreateContexts) { + this.autoCreateContexts = autoCreateContexts; + return this; + } + + /* + * @see runwar.options.ServerOptions#autoCreateContextsSecret() + */ + @Override + public String autoCreateContextsSecret() { + return autoCreateContextsSecret; + } + /* + * @see runwar.options.ServerOptions#autoCreateContextsSecret(String) + */ + @Override + public ServerOptions autoCreateContextsSecret(String autoCreateContextsSecret) { + this.autoCreateContextsSecret = autoCreateContextsSecret; + return this; + } + + + + /* + * @see runwar.options.ServerOptions#autoCreateContextsVDirs() + */ + @Override + public Boolean autoCreateContextsVDirs() { + return autoCreateContextsVDirs; + } + + /* + * @see runwar.options.ServerOptions#autoCreateContextsVDirs(boolean) + */ + @Override + public ServerOptions autoCreateContextsVDirs(Boolean autoCreateContextsVDirs) { + this.autoCreateContextsVDirs = autoCreateContextsVDirs; + return this; + } + + /* + * @see runwar.options.ServerOptions#autoCreateContextsMax() + */ + @Override + public Integer autoCreateContextsMax() { + return autoCreateContextsMax; + } + + /* + * @see runwar.options.ServerOptions#autoCreateContextsMax(Integer) + */ + @Override + public ServerOptions autoCreateContextsMax(Integer autoCreateContextsMax) { + this.autoCreateContextsMax = autoCreateContextsMax; + return this; + } + /* * @see runwar.options.ServerOptions#cacheServletPaths() */ @@ -2208,7 +2270,41 @@ public ServerOptions cacheServletPaths(Boolean cacheServletPaths) { this.cacheServletPaths = cacheServletPaths; return this; } + + /* + * @see runwar.options.ServerOptions#fileCacheTotalSizeMB() + */ + @Override + public Integer fileCacheTotalSizeMB() { + return fileCacheTotalSizeMB; + } + /* + * @see runwar.options.ServerOptions#fileCacheTotalSizeMB(Integer) + */ + @Override + public ServerOptions fileCacheTotalSizeMB(Integer fileCacheTotalSizeMB) { + this.fileCacheTotalSizeMB = fileCacheTotalSizeMB; + return this; + } + + /* + * @see runwar.options.ServerOptions#fileCacheMaxFileSizeKB() + */ + @Override + public Integer fileCacheMaxFileSizeKB() { + return fileCacheMaxFileSizeKB; + } + + /* + * @see runwar.options.ServerOptions#fileCacheMaxFileSizeKB(Integer) + */ + @Override + public ServerOptions fileCacheMaxFileSizeKB(Integer fileCacheMaxFileSizeKB) { + this.fileCacheMaxFileSizeKB = fileCacheMaxFileSizeKB; + return this; + } + /* * @see runwar.options.ServerOptions#caseSensitiveWebServer() */ diff --git a/src/main/java/runwar/undertow/HostResourceManager.java b/src/main/java/runwar/undertow/HostResourceManager.java new file mode 100644 index 00000000..5c780eeb --- /dev/null +++ b/src/main/java/runwar/undertow/HostResourceManager.java @@ -0,0 +1,147 @@ +package runwar.undertow; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.InvalidPathException; +import java.nio.file.LinkOption; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.concurrent.ConcurrentHashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Stream; + +import java.util.Optional; + +import io.undertow.server.HttpServerExchange; +import io.undertow.server.handlers.resource.FileResource; +import io.undertow.server.handlers.resource.FileResourceManager; +import io.undertow.server.handlers.resource.Resource; +import io.undertow.server.handlers.resource.ResourceChangeEvent; +import io.undertow.server.handlers.resource.ResourceChangeListener; +import io.undertow.server.handlers.resource.ResourceManager; +import runwar.Server; +import runwar.Server.ServletDeployment; +import runwar.options.ServerOptions; +import io.undertow.servlet.handlers.ServletRequestContext; + +import static runwar.logging.RunwarLogger.LOG; +import static runwar.logging.RunwarLogger.MAPPER_LOG; + +/** + * The host resource manager contains 1 or more actual resoruce managers inside, mapped to deploy keys. When undertow needs to map a real path, + * this resource manager will find and return the actual resoruce manager who's base path maps to the host/deployment key in the thread's current exchange. + * + * @author Brad + * + */ +public class HostResourceManager implements ResourceManager { + + private HashMap resourceManagers; + + /** + * Create an instance. A default resource manager is required + * @param defaultResourceManager The default resource manager to use when the deploy key isn't found or doesn't match + */ + public HostResourceManager( ResourceManager defaultResourceManager ) { + resourceManagers = new HashMap(); + + addResourceManager( Server.ServletDeployment.DEFAULT, defaultResourceManager ); + } + + /** + * Add a new resource manager that maps to a host/deploy key + * @param deploymentKey The key that should map to this resource manager + * @param resourceManager The resource manager. + */ + public void addResourceManager( String deploymentKey, ResourceManager resourceManager ) { + resourceManagers.put(deploymentKey, resourceManager); + } + + /** + * Get the resource from the underlying resource manager matching the current deployment key in the exchange + */ + @Override + public Resource getResource(String path) throws IOException { + return getResourceManager().getResource( path ); + } + + /** + * Find the resource manager that matches the deploy key in the exchange. + * If there is no exchange, no deploy key, or an unrecognized deploy key, the default resource manager is returned. + * @return A resource manager. + */ + public ResourceManager getResourceManager() { + HttpServerExchange exchange = null; + ResourceManager resourceManager = null; + String deploymentKey = null; + exchange = Server.getCurrentExchange(); + + if( exchange != null ) { + deploymentKey = exchange.getAttachment(Server.DEPLOYMENT_KEY); + if( deploymentKey != null ) { + MAPPER_LOG.debug("Current exchange's deploymentKey is: " + deploymentKey ); + } + } + + if( deploymentKey == null ) { + deploymentKey = Server.ServletDeployment.DEFAULT; + } + + resourceManager = resourceManagers.get( deploymentKey ); + if( resourceManager != null ) { + return resourceManager; + } else { + return resourceManagers.get( Server.ServletDeployment.DEFAULT ); + } + + } + + /** + * Closes all the resource managers stored internall + */ + @Override + public void close() throws IOException { + resourceManagers.forEach((s, s2) -> { + try { + s2.close(); + } catch (Exception e) { + e.printStackTrace(); + } + } ); + } + + /** + * Delegate to the current matching resource manager + */ + @Override + public boolean isResourceChangeListenerSupported() { + return getResourceManager().isResourceChangeListenerSupported(); + } + + /** + * Delegate to the current matching resource manager + */ + @Override + public synchronized void registerResourceChangeListener(ResourceChangeListener listener) { + getResourceManager().registerResourceChangeListener( listener ); + } + + /** + * Delegate to the current matching resource manager + */ + @Override + public synchronized void removeResourceChangeListener(ResourceChangeListener listener) { + getResourceManager().removeResourceChangeListener( listener ); + } + + +} diff --git a/src/main/java/runwar/undertow/MappedResourceManager.java b/src/main/java/runwar/undertow/MappedResourceManager.java index 08327bdd..58a5e0b7 100644 --- a/src/main/java/runwar/undertow/MappedResourceManager.java +++ b/src/main/java/runwar/undertow/MappedResourceManager.java @@ -2,14 +2,18 @@ import java.io.File; import java.io.IOException; +import java.nio.file.FileSystems; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.InvalidPathException; import java.nio.file.LinkOption; +import java.util.ArrayList; +import java.util.Collection; import java.util.HashMap; import java.util.concurrent.ConcurrentHashMap; import java.util.HashSet; +import java.util.List; import java.util.Map; import java.util.Set; import java.util.regex.Matcher; @@ -19,9 +23,20 @@ import io.undertow.server.handlers.resource.FileResource; import io.undertow.server.handlers.resource.FileResourceManager; +import io.undertow.server.handlers.resource.PathResourceManager; import io.undertow.server.handlers.resource.Resource; +import io.undertow.server.handlers.resource.ResourceChangeEvent; +import io.undertow.server.handlers.resource.ResourceChangeListener; import runwar.options.ServerOptions; + + +import org.xnio.FileChangeCallback; +import org.xnio.FileChangeEvent; +import org.xnio.FileSystemWatcher; +import org.xnio.OptionMap; +import org.xnio.Xnio; + import static runwar.logging.RunwarLogger.MAPPER_LOG; public class MappedResourceManager extends FileResourceManager { @@ -29,16 +44,16 @@ public class MappedResourceManager extends FileResourceManager { private ServerOptions serverOptions; private Boolean forceCaseSensitiveWebServer; private Boolean forceCaseInsensitiveWebServer; - private Boolean cacheServletPaths; private HashMap aliases; - private ConcurrentHashMap caseInsensitiveCache = new ConcurrentHashMap(); - private ConcurrentHashMap> servletPathCache = new ConcurrentHashMap>(); - private HashSet contentDirs; private File WEBINF = null, CFIDE = null; private static boolean isCaseSensitiveFS = caseSensitivityCheck(); private static final Pattern CFIDE_REGEX_PATTERN; private static final Pattern WEBINF_REGEX_PATTERN; private final FileResource baseResource; + + // testing + private final List listeners = new ArrayList<>(); + private List fileSystemWatchers = new ArrayList<>(); static { CFIDE_REGEX_PATTERN = Pattern.compile("(?i)^[\\\\/]?CFIDE([\\\\/].*)?"); @@ -47,26 +62,21 @@ public class MappedResourceManager extends FileResourceManager { private final boolean allowResourceChangeListeners; - public MappedResourceManager(File base, long transferMinSize, Set contentDirs, Map aliases, File webinfDir, ServerOptions serverOptions) { + public MappedResourceManager(File base, long transferMinSize, Map aliases, File webinfDir, ServerOptions serverOptions) { super(base, transferMinSize); - this.allowResourceChangeListeners = false; - this.contentDirs = (HashSet) contentDirs; + this.allowResourceChangeListeners = true; this.aliases = (HashMap) aliases; this.serverOptions = serverOptions; this.forceCaseSensitiveWebServer = serverOptions.caseSensitiveWebServer() != null && serverOptions.caseSensitiveWebServer(); this.forceCaseInsensitiveWebServer = serverOptions.caseSensitiveWebServer() != null && !serverOptions.caseSensitiveWebServer(); - this.cacheServletPaths = serverOptions.cacheServletPaths(); this.baseResource = new FileResource( getBase(), this, "/"); if(webinfDir != null){ WEBINF = webinfDir; CFIDE = new File(WEBINF.getParentFile(),"CFIDE"); - MAPPER_LOG.debugf("Initialized MappedResourceManager - base: %s, web-inf: %s, contentDirs: %s, aliases: %s",base.getAbsolutePath(), WEBINF.getAbsolutePath(), contentDirs, aliases); if (!WEBINF.exists()) { throw new RuntimeException("The specified WEB-INF does not exist: " + WEBINF.getAbsolutePath()); } - } else { - MAPPER_LOG.debugf("Initialized MappedResourceManager - base: %s, web-inf: %s, contentDirs: %s, aliases: %s",base.getAbsolutePath(), contentDirs, aliases); } } @@ -77,21 +87,8 @@ public Resource getResource(String path) { } MAPPER_LOG.debug("* requested: '" + path + "'"); - // If cache is enabled and this path is found, return it - if( cacheServletPaths ) { - Optional cacheCheck = servletPathCache.get( path ); - if( cacheCheck != null ) { - if( cacheCheck.isPresent() ) { - MAPPER_LOG.debugf("** path mapped to (cached): '%s'", cacheCheck.get().getFile().toString() ); - } else { - MAPPER_LOG.debugf("** path mapped to (cached): '%s'", "null (not found)" ); - } - return cacheCheck.orElse( null ); - } - } - if( path.equals( "/" ) ) { - MAPPER_LOG.debugf("** path mapped to (cached): '%s'", getBase()); + MAPPER_LOG.debugf("** path mapped to: '%s'", getBase()); return this.baseResource; } @@ -120,16 +117,6 @@ public Resource getResource(String path) { if (!Files.exists(reqFile)) { reqFile = getAliasedFile(aliases, path); } - /* if (reqFile == null) { - for (Path cfmlDirsFile : contentDirs) { - reqFile = Paths.get(cfmlDirsFile.toString(), path.replace(cfmlDirsFile.toAbsolutePath().toString(), "")); - MAPPER_LOG.tracef("checking: '%s' = '%s'", cfmlDirsFile.toAbsolutePath().toString(), reqFile.toAbsolutePath()); - if (Files.exists(reqFile)) { - MAPPER_LOG.tracef("Exists: '%s'", reqFile.toAbsolutePath().toString()); - break; - } - } - }*/ } if (reqFile != null ) { @@ -137,12 +124,9 @@ public Resource getResource(String path) { } if (reqFile == null ) { - MAPPER_LOG.debugf("** No mapped resource for: '%s' (reqFile was: '%s')",path,reqFile != null ? reqFile.toString() : "null"); - Resource superResult = super.getResource(path); - if( cacheServletPaths ) { - servletPathCache.put( path, Optional.ofNullable( superResult ) ); - } - return superResult; + MAPPER_LOG.debugf( "** No real resource found on disk for: '%s'", path ); + //Resource superResult = super.getResource(path); + return null; } if(reqFile.toString().indexOf('\\') > 0) { @@ -176,28 +160,16 @@ public Resource getResource(String path) { throw new InvalidPathException( "Real file path [" + realPath + "] doesn't match [" + originalPath + "]", "" ); } - MAPPER_LOG.debugf("** path mapped to: '%s'", reqFile); - - if( cacheServletPaths ) { - servletPathCache.put( path, Optional.of( new FileResource(reqFile.toFile(), this, path) ) ); - } - return new FileResource(reqFile.toFile(), this, path); + MAPPER_LOG.debugf("** path mapped to real file: '%s'", reqFile); + return new FileResource(reqFile.toFile(), this, path); } catch( InvalidPathException e ){ MAPPER_LOG.debugf("** InvalidPathException for: '%s'",path != null ? path : "null"); MAPPER_LOG.debug("** " + e.getMessage()); - - if( cacheServletPaths ) { - servletPathCache.put( path, Optional.empty() ); - } return null; } catch( IOException e ){ MAPPER_LOG.debugf("** IOException for: '%s'",path != null ? path : "null"); MAPPER_LOG.debug("** " + e.getMessage()); - - if( cacheServletPaths ) { - servletPathCache.put( path, Optional.empty() ); - } return null; } } @@ -208,6 +180,7 @@ static Path getAliasedFile(HashMap aliasMap, String path) { path = path.replace("/file:", ""); for( Path file : aliasMap.values()) { if(path.startsWith(file.toAbsolutePath().toString())) { + MAPPER_LOG.trace("** Path is exact match for alias: '" + file.toAbsolutePath().toString() + "'"); return Paths.get(path); } } @@ -225,6 +198,7 @@ static Path getAliasedFile(HashMap aliasMap, String path) { if(file.toString().indexOf('\\') > 0){ file = Paths.get(file.toString().replace('/', '\\')); } + MAPPER_LOG.trace("** Path is matched inside alias: '" + pathDir.toLowerCase() + "'"); return file; } } @@ -238,16 +212,7 @@ Path pathExists(Path path) { } if( isCaseSensitiveFS && forceCaseInsensitiveWebServer ) { MAPPER_LOG.debugf("*** Case insensitive check for %s",path); - - Path cacheLookup = caseInsensitiveCache.get(path.toString()); - if( cacheLookup != null && Files.exists( cacheLookup ) ) { - MAPPER_LOG.tracef("*** Case insensitive lookup found in cache %s -> %s",path, cacheLookup); - return cacheLookup; - } else if( cacheLookup != null ) { - MAPPER_LOG.tracef("*** Case insensitive lookup removed from cache %s",path); - caseInsensitiveCache.remove(path.toString()); - } - + String realPath = ""; String[] pathSegments = path.toString().replace('\\', '/').split( "/" ); if( pathSegments.length > 0 && pathSegments[0].contains(":") ){ @@ -281,49 +246,11 @@ Path pathExists(Path path) { } // If we made it through the outer loop, we've found a match Path realPathFinal = Paths.get( realPath ); - - MAPPER_LOG.tracef("*** Case insensitive lookup put in cache %s -> %s",path,realPathFinal); - caseInsensitiveCache.put(path.toString(), realPathFinal ); return realPathFinal; } return null; } - private void processMappings(String cfmlDirList) { - HashSet dirs = new HashSet<>(); - // Stream.of(cfmlDirList.split(",")) - // .map (elem -> new String(elem)) - // .collect(Collectors.toList()); - Stream.of(cfmlDirList.split(",")).forEach( aDirList -> { - Path path; - String dir; - String virtual = ""; - String[] directoryAndAliasList = aDirList.trim().split("="); - if(directoryAndAliasList.length == 1){ - dir = directoryAndAliasList[0].trim(); - } else { - dir = directoryAndAliasList[1].trim(); - virtual = directoryAndAliasList[0].trim(); - } - dir = dir.endsWith("/") ? dir : dir + '/'; - path = Paths.get(dir).normalize().toAbsolutePath(); - if(virtual.length() == 0){ - dirs.add(path); - } else { - virtual = virtual.startsWith("/") ? virtual : "/" + virtual; - virtual = virtual.endsWith("/") ? virtual.substring(0, virtual.length() - 1) : virtual; - aliases.put(virtual.toLowerCase(), path); - } - String aliasInfo = virtual.isEmpty() ? "" : " as " + virtual; - if (!path.toFile().exists()) { - MAPPER_LOG.errorf("Does not exist, cannot serve content from: " + path + aliasInfo); - } else { - MAPPER_LOG.info("Serving content from " + path + aliasInfo); - } - }); - contentDirs = dirs; - } - HashMap getAliases() { return aliases; } @@ -332,24 +259,6 @@ HashMap getAliases() { public boolean isResourceChangeListenerSupported() { return allowResourceChangeListeners; } - - /** - * Super nasty thing to display caller chain, just for debugging/experiments - * @return list of steps - */ - private static String getRecentSteps() { - StringBuilder recentSteps = new StringBuilder(); - StackTraceElement[] traceElements = Thread.currentThread().getStackTrace(); - final int maxStepCount = 3; - final int skipCount = 2; - - for (int i = Math.min(maxStepCount + skipCount, traceElements.length) - 1; i >= skipCount; i--) { - String className = traceElements[i].getClassName().substring(traceElements[i].getClassName().lastIndexOf(".") + 1); - recentSteps.append(" >> ").append(className).append(".").append(traceElements[i].getMethodName()).append("()"); - } - - return recentSteps.toString(); - } private static boolean caseSensitivityCheck() { try { @@ -373,10 +282,99 @@ private static boolean caseSensitivityCheck() { } return true; } + + public synchronized void registerResourceChangeListener(ResourceChangeListener listener) { + MAPPER_LOG.trace("Adding change listener for mapped resource manager"); + if(!allowResourceChangeListeners) { + //by rights we should throw an exception here, but this works around a bug in Wildfly where it just assumes + //PathResourceManager supports this. This will be fixed in a later version + return; + } + if (!fileSystem.equals(FileSystems.getDefault())) { + throw new IllegalStateException("Resource change listeners not supported when using a non-default file system"); + } + listeners.add(listener); + if (fileSystemWatchers.isEmpty()) { + fileSystemWatchers.add( createFileSystemWatcher( base, "" ) ); + if( WEBINF != null ){ + fileSystemWatchers.add( createFileSystemWatcher( WEBINF.getAbsolutePath(), "/WEB-INF" ) ); + } + if( CFIDE != null && CFIDE.exists() ){ + fileSystemWatchers.add( createFileSystemWatcher( CFIDE.getAbsolutePath(), "/CFIDE" ) ); + } + aliases.forEach( (alias,path) -> { + // In case there is a broken alias pointing nowhere + if( Files.exists( path ) ) { + createFileSystemWatcher( path.toString(), alias ); + } + } ); + + } + } - public void clearCaches() { - caseInsensitiveCache.clear(); - servletPathCache.clear(); + FileSystemWatcher createFileSystemWatcher( String basePath, String prefix ) { + + MAPPER_LOG.trace("Creating file system watcher in [ " + basePath + " ] with alias [ " + prefix + " ]"); + + final String thePrefix; + if( prefix.startsWith("/") ) { + thePrefix = prefix.substring(1); + } else { + thePrefix = prefix; + } + + FileSystemWatcher fileSystemWatcher = Xnio.getInstance().createFileSystemWatcher("Watcher for " + basePath, OptionMap.EMPTY); + fileSystemWatcher.watchPath(new File(basePath), new FileChangeCallback() { + @Override + public void handleChanges(Collection changes) { + synchronized (MappedResourceManager.this) { + final List events = new ArrayList<>(); + for (FileChangeEvent change : changes) { + if (change.getFile().getAbsolutePath().startsWith(basePath)) { + String path = change.getFile().getAbsolutePath().substring(basePath.length()); + if (File.separatorChar == '\\' && path.contains(File.separator)) { + path = path.replace(File.separatorChar, '/'); + } + if( !thePrefix.isEmpty() ) { + if( path.startsWith("/") ) { + path = path.substring(1); + } + if( thePrefix.endsWith("/") ) { + path = thePrefix + path; + } else { + path = thePrefix + "/" + path; + } + } + events.add(new ResourceChangeEvent( path, ResourceChangeEvent.Type.valueOf(change.getType().name()))); + } + } + for (ResourceChangeListener listener : listeners) { + listener.handleChanges(events); + } + } + } + }); + return fileSystemWatcher; } + @Override + public synchronized void removeResourceChangeListener(ResourceChangeListener listener) { + if(!allowResourceChangeListeners) { + return; + } + listeners.remove(listener); + super.removeResourceChangeListener(listener); + } + + public synchronized void close() throws IOException { + fileSystemWatchers.forEach(w -> { + try { + w.close(); + } catch (Exception e) { + e.printStackTrace(); + } + } ); + super.close(); + } + } diff --git a/src/main/java/runwar/undertow/WebXMLParser.java b/src/main/java/runwar/undertow/WebXMLParser.java index ad7d8bb0..4bd0d3f3 100644 --- a/src/main/java/runwar/undertow/WebXMLParser.java +++ b/src/main/java/runwar/undertow/WebXMLParser.java @@ -58,15 +58,17 @@ public static void parseWebXml(File webxml, DeploymentInfo info, if (displayName != null) { info.setDisplayName(displayName); } - - $(doc).find("context-param").each(ctx -> { + + Match contextParams = $(doc).find("context-param"); + trace("Total No. of context-params: %s", contextParams.size()); + + contextParams.each(ctx -> { String pName = getRequired(ctx,"param-name"); String pValue = getRequired(ctx,"param-value"); info.addServletContextAttribute(pName, pValue); info.addInitParameter(pName, pValue); CONF_LOG.tracef("context param: '%s' = '%s'", pName, pValue); }); - trace("Total No. of context-params: %s", info.getServletContextAttributes().size()); //do listeners Match listeners = $(doc).find("listener"); @@ -130,7 +132,7 @@ public static void parseWebXml(File webxml, DeploymentInfo info, }); Match servletMappings = $(doc).find("servlet-mapping"); - trace("Total No. of servlet-mappings: %s", servletMappings.size()); + trace("Total No. of servlet-mappings: %s", servletMappings.size()); servletMappings.each(mappingElement -> { String servletName = getRequired(mappingElement, "servlet-name"); ServletInfo servlet = info.getServlets().get(servletName); @@ -146,7 +148,10 @@ public static void parseWebXml(File webxml, DeploymentInfo info, urlPattern); } else { CONF_LOG.tracef("mapping servlet-name: %s, url-pattern: %s", servletName, urlPattern); - servlet.addMapping(urlPattern); + trace( "does mapping exist already %s", servlet.getMappings().contains( urlPattern ) ); + if( !servlet.getMappings().contains( urlPattern ) ){ + servlet.addMapping(urlPattern); + } } }); }