From 0069a6ce9838109e18344f96b4a1d97f322a260d Mon Sep 17 00:00:00 2001 From: Brad Wood Date: Wed, 14 Feb 2024 14:43:42 -0600 Subject: [PATCH 01/30] COMMANDBOX-1616 --- gradle/version | 2 +- src/main/java/runwar/Server.java | 105 ++-- .../java/runwar/options/ConfigParser.java | 72 +-- src/main/java/runwar/options/SiteOptions.java | 31 -- src/main/java/runwar/tray/Tray.java | 150 +++--- .../undertow/BindingMatcherHandler.java | 479 +++++++++--------- .../java/runwar/undertow/ListenerManager.java | 327 ++++++------ .../rock/json/ServerOptionsJSON.rocker.raw | 58 --- 8 files changed, 507 insertions(+), 717 deletions(-) delete mode 100644 src/main/rocker/runwar/rock/json/ServerOptionsJSON.rocker.raw diff --git a/gradle/version b/gradle/version index ab1603e0..49b44f7d 100644 --- a/gradle/version +++ b/gradle/version @@ -1 +1 @@ -5.0.0-25600eadb08f3c204344404470d41494340f2702-060f7b26320ac8ccf3354119f4f0eff75f986553 \ No newline at end of file +5.0.1-ccc46e035cf6589314e267a6a162d4f9b026590e-96ea216b7cfb6883a6af15c191a94953a0596b9c \ No newline at end of file diff --git a/src/main/java/runwar/Server.java b/src/main/java/runwar/Server.java index 7c19605d..4466b6cd 100644 --- a/src/main/java/runwar/Server.java +++ b/src/main/java/runwar/Server.java @@ -1,92 +1,61 @@ package runwar; -import io.undertow.Handlers; +import static io.undertow.servlet.Servlets.deployment; +import static runwar.logging.RunwarLogger.CONTEXT_LOG; +import static runwar.logging.RunwarLogger.LOG; + +import java.awt.Image; +import java.io.File; +import java.io.IOException; +import java.io.PrintStream; +import java.io.PrintWriter; +import java.lang.management.ManagementFactory; +import java.lang.reflect.Method; +import java.net.ConnectException; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.ServerSocket; +import java.net.Socket; +import java.net.SocketException; +import java.net.SocketTimeoutException; +import java.net.URL; +import java.net.URLClassLoader; +import java.net.UnknownHostException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Timer; +import java.util.TimerTask; +import java.util.regex.Pattern; + +import javax.servlet.Servlet; + +import org.xnio.Option; +import org.xnio.OptionMap; + 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.predicate.Predicate; import io.undertow.server.DefaultByteBufferPool; +import io.undertow.server.HandlerWrapper; import io.undertow.server.HttpHandler; import io.undertow.server.HttpServerExchange; -import io.undertow.server.handlers.PathHandler; -import io.undertow.server.handlers.ProxyPeerAddressHandler; -import io.undertow.server.handlers.SSLHeaderHandler; -import io.undertow.server.handlers.accesslog.AccessLogHandler; -import io.undertow.server.handlers.accesslog.DefaultAccessLogReceiver; -import io.undertow.server.handlers.builder.PredicatedHandler; -import io.undertow.server.handlers.builder.PredicatedHandlersParser; -import io.undertow.server.handlers.cache.DirectBufferCache; -import io.undertow.server.handlers.encoding.ContentEncodingRepository; -import io.undertow.server.handlers.encoding.EncodingHandler; -import io.undertow.server.handlers.encoding.GzipEncodingProvider; -import io.undertow.server.handlers.resource.CachingResourceManager; -import io.undertow.server.handlers.resource.ResourceManager; -import io.undertow.server.handlers.resource.ResourceHandler; import io.undertow.servlet.api.DeploymentInfo; -import io.undertow.servlet.api.DeploymentManager; import io.undertow.servlet.api.ServletSessionConfig; -import io.undertow.util.CanonicalPathUtils; -import io.undertow.util.AttachmentKey; -import io.undertow.util.HeaderValues; -import io.undertow.server.handlers.resource.Resource; -import io.undertow.util.Headers; -import io.undertow.util.HttpString; -import io.undertow.util.MimeMappings; -import io.undertow.io.Sender; import io.undertow.websockets.jsr.WebSocketDeploymentInfo; import net.minidev.json.JSONArray; import net.minidev.json.JSONObject; -import runwar.options.ConfigParser.JSONOption; -import org.xnio.Option; -import org.xnio.*; import runwar.logging.LoggerFactory; import runwar.logging.LoggerPrintStream; -import runwar.logging.RunwarAccessLogReceiver; import runwar.mariadb4j.MariaDB4jManager; import runwar.options.ServerOptions; import runwar.options.SiteOptions; -import runwar.security.SSLUtil; import runwar.tray.Tray; -import runwar.undertow.MappedResourceManager; -import runwar.undertow.HostResourceManager; -import runwar.undertow.RequestDebugHandler; +import runwar.undertow.BindingMatcherHandler; +import runwar.undertow.ListenerManager; import runwar.undertow.SSLCertHeaderHandler; -import runwar.undertow.LifecyleHandler; -import runwar.undertow.WelcomeFileHandler; import runwar.undertow.SiteDeployment; import runwar.undertow.SiteDeploymentManager; import runwar.util.ClassLoaderUtils; -import runwar.util.RequestDumper; - -import javax.net.ssl.SSLContext; -import java.awt.*; -import java.io.*; -import java.util.*; -import java.util.List; -import java.lang.management.ManagementFactory; -import java.lang.reflect.Method; -import java.net.*; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.concurrent.ConcurrentHashMap; -import javax.servlet.Servlet; -import javax.servlet.http.HttpServletResponse; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.ServletRequest; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import static io.undertow.servlet.Servlets.defaultContainer; -import static io.undertow.servlet.Servlets.deployment; -import io.undertow.servlet.handlers.ServletRequestContext; -import io.undertow.server.HandlerWrapper; - -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") diff --git a/src/main/java/runwar/options/ConfigParser.java b/src/main/java/runwar/options/ConfigParser.java index a00bc8fe..c80e9684 100644 --- a/src/main/java/runwar/options/ConfigParser.java +++ b/src/main/java/runwar/options/ConfigParser.java @@ -1,22 +1,22 @@ package runwar.options; +import static runwar.logging.RunwarLogger.CONF_LOG; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + import net.minidev.json.JSONArray; import net.minidev.json.JSONObject; import net.minidev.json.JSONValue; import net.minidev.json.parser.ParseException; import runwar.LaunchUtil; -import runwar.Server; -import runwar.logging.RunwarLogger; import runwar.logging.LoggerFactory; -import java.io.File; -import java.io.IOException; -import java.net.MalformedURLException; -import java.util.*; -import java.util.stream.Collectors; - -import static runwar.logging.RunwarLogger.CONF_LOG; - public class ConfigParser { private ServerOptions serverOptions; @@ -311,46 +311,6 @@ private void parseOptions() { if (siteConfig.hasOption("resourceManagerLogging")) { site.resourceManagerLogging(siteConfig.getOptionBoolean("resourceManagerLogging")); } - if (siteConfig.hasOption("host")) { - site.host(siteConfig.getOptionValue("host")); - } - - if (siteConfig.hasOption("HTTPEnable")) { - site.httpEnable(siteConfig.getOptionBoolean("HTTPEnable")); - } - if (siteConfig.hasOption("port")) { - site.httpPort(((Number) siteConfig.getParsedOptionValue("port")).intValue()); - } - if (siteConfig.hasOption("HTTP2Enable")) { - site.http2Enable(siteConfig.getOptionBoolean("HTTP2Enable")); - } - if (siteConfig.hasOption("AJPEnable")) { - site.ajpEnable(siteConfig.getOptionBoolean("AJPEnable")); - } - if (siteConfig.hasOption("AJPPort")) { - site.ajpPort(((Number) siteConfig.getParsedOptionValue("AJPPort")).intValue()); - } - if (siteConfig.hasOption("SSLEnable")) { - site.sslEnable(siteConfig.getOptionBoolean("SSLEnable")); - if (!siteConfig.hasOption("sessionCookieSecure")) { - serverOptions.secureCookies(true); - } - } - if (siteConfig.hasOption("SSLPort")) { - site.sslPort(((Number) siteConfig.getParsedOptionValue("SSLPort")).intValue()); - } - - if (siteConfig.hasOption("SSLCertFile")) { - File certFile = getFile(siteConfig.getOptionValue("SSLCertFile")); - site.sslCertificate(certFile); - } - if (siteConfig.hasOption("SSLKeyFile")) { - File keyFile = getFile(siteConfig.getOptionValue("SSLKeyFile")); - site.sslKey(keyFile); - } - if (siteConfig.hasOption("SSLKeyPass")) { - site.sslKeyPass(siteConfig.getOptionValue("SSLKeyPass").toCharArray()); - } if (siteConfig.hasOption("clientCertMode")) { site.clientCertNegotiation(siteConfig.getOptionValue("clientCertMode")); @@ -364,13 +324,11 @@ private void parseOptions() { if (siteConfig.hasOption("clientCertEnable")) { site.clientCertEnable(siteConfig.getOptionBoolean("clientCertEnable")); - if (site.clientCertEnable()) { - if (siteConfig.hasOption("clientCertSubjectDNs")) { - site.clientCertSubjectDNs(siteConfig.getOptionArray("clientCertSubjectDNs")); - } - if (siteConfig.hasOption("clientCertIssuerDNs")) { - site.clientCertIssuerDNs(siteConfig.getOptionArray("clientCertIssuerDNs")); - } + if (siteConfig.hasOption("clientCertSubjectDNs")) { + site.clientCertSubjectDNs(siteConfig.getOptionArray("clientCertSubjectDNs")); + } + if (siteConfig.hasOption("clientCertIssuerDNs")) { + site.clientCertIssuerDNs(siteConfig.getOptionArray("clientCertIssuerDNs")); } } diff --git a/src/main/java/runwar/options/SiteOptions.java b/src/main/java/runwar/options/SiteOptions.java index 4156a00f..df79759e 100644 --- a/src/main/java/runwar/options/SiteOptions.java +++ b/src/main/java/runwar/options/SiteOptions.java @@ -7,7 +7,6 @@ import net.minidev.json.JSONArray; import net.minidev.json.JSONObject; -import runwar.options.ServerOptions; public class SiteOptions { @@ -287,18 +286,6 @@ public SiteOptions welcomeFiles(String[] welcomeFiles) { return this; } - public SiteOptions sslCertificate(File file) { - this.sslCertificate = file; - return this; - } - - public File sslCertificate() { - if (sslCertificate != null && !sslCertificate.exists()) { - throw new IllegalArgumentException("Certificate file does not exist: " + sslCertificate.getAbsolutePath()); - } - return this.sslCertificate; - } - public SiteOptions clientCertNegotiation(String clientCertNegotiation) { this.clientCertNegotiation = clientCertNegotiation.toUpperCase(); return this; @@ -353,24 +340,6 @@ public JSONArray clientCertIssuerDNs() { return this.clientCertIssuerDNs; } - public SiteOptions sslKey(File file) { - this.sslKey = file; - return this; - } - - public File sslKey() { - return this.sslKey; - } - - public SiteOptions sslKeyPass(char[] pass) { - this.sslKeyPass = pass; - return this; - } - - public char[] sslKeyPass() { - return this.sslKeyPass; - } - public SiteOptions transferMinSize(Long minSize) { if (minSize == -1L) { // Effectivley turns it off diff --git a/src/main/java/runwar/tray/Tray.java b/src/main/java/runwar/tray/Tray.java index 937ac895..3a9dff1c 100644 --- a/src/main/java/runwar/tray/Tray.java +++ b/src/main/java/runwar/tray/Tray.java @@ -4,53 +4,58 @@ import static runwar.LaunchUtil.getResourceAsString; import static runwar.LaunchUtil.openURL; import static runwar.LaunchUtil.readFile; +import static runwar.util.Reflection.invoke; +import static runwar.util.Reflection.method; -import java.awt.*; +import java.awt.BorderLayout; +import java.awt.Dimension; +import java.awt.GraphicsEnvironment; +import java.awt.Image; +import java.awt.Toolkit; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; -import java.io.*; +import java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.lang.reflect.Method; import java.net.URL; import java.util.HashMap; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; import javax.imageio.ImageIO; +import javax.swing.JButton; +import javax.swing.JFrame; +import javax.swing.JOptionPane; +import javax.swing.JScrollPane; +import javax.swing.JTextArea; + +import dorkbox.notify.Notify; +import dorkbox.notify.Pos; import dorkbox.systemTray.Checkbox; import dorkbox.systemTray.Menu; import dorkbox.systemTray.MenuItem; import dorkbox.systemTray.Separator; import dorkbox.systemTray.SystemTray; import dorkbox.util.OS; -import javax.swing.JOptionPane; -import javax.swing.JScrollPane; -import javax.swing.JTextArea; -import dorkbox.notify.Notify; -import dorkbox.notify.Pos; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.Future; -import java.util.concurrent.TimeUnit; -import java.util.function.Consumer; -import javax.swing.JButton; -import javax.swing.JPanel; import net.minidev.json.JSONArray; import net.minidev.json.JSONObject; import net.minidev.json.JSONValue; import runwar.LaunchUtil; import runwar.Server; import runwar.Start; -import runwar.gui.JsonForm; import runwar.logging.RunwarLogger; import runwar.options.ServerOptions; -import runwar.options.ServerOptions; import runwar.util.Utils; -import javax.swing.JFileChooser; -import javax.swing.JFrame; -import runwar.gui.SubmitActionlistioner; - -import java.lang.reflect.Method; -import static runwar.util.Reflection.invoke; -import static runwar.util.Reflection.method; public class Tray { @@ -74,17 +79,17 @@ public void hookTray(final Server server) { if (trayIsHooked) { return; } -// available 3.13+ + // available 3.13+ SystemTray.AUTO_SIZE = true; SystemTray.FORCE_GTK2 = true; SystemTray.AUTO_FIX_INCONSISTENCIES = true; - RunwarLogger.LOG.trace("SystemTray Version: " + SystemTray.getVersion() ); - if( server.getServerOptions().logLevel().toUpperCase().equals( "TRACE" ) ) { + RunwarLogger.LOG.trace("SystemTray Version: " + SystemTray.getVersion()); + if (server.getServerOptions().logLevel().toUpperCase().equals("TRACE")) { RunwarLogger.LOG.trace("Settting SystemTray.DEBUG = true"); SystemTray.DEBUG = true; } System.setProperty("SWT_GTK3", "0"); -// SystemTray.FORCE_TRAY_TYPE = TrayType.; + // SystemTray.FORCE_TRAY_TYPE = TrayType.; if (GraphicsEnvironment.isHeadless()) { RunwarLogger.LOG.debug("Server is in headless mode, System Tray is not supported"); return; @@ -103,8 +108,7 @@ public void hookTray(final Server server) { ServerOptions serverOptions = server.getServerOptions(); String iconImage = serverOptions.iconImage(); - // String host = serverOptions.host(); - // int portNumber = serverOptions.getSites().get(0).httpPort(); + final int stopSocket = serverOptions.stopPort(); String processName = serverOptions.processName(); String PID = server.getPID(); @@ -118,14 +122,14 @@ public void hookTray(final Server server) { variableMap.put("logDir", warpath); variableMap.put("app.logDir", warpath); variableMap.put("web.webroot", warpath); - // variableMap.put("runwar.port", Integer.toString(portNumber)); - // variableMap.put("web.http.port", Integer.toString(portNumber)); - // variableMap.put("web.ajp.port", Integer.toString(portNumber)); + // variableMap.put("runwar.port", Integer.toString(portNumber)); + // variableMap.put("web.http.port", Integer.toString(portNumber)); + // variableMap.put("web.ajp.port", Integer.toString(portNumber)); variableMap.put("runwar.processName", processName); variableMap.put("processName", processName); variableMap.put("runwar.PID", PID); - // variableMap.put("runwar.host", host); - // variableMap.put("web.host", host); + // variableMap.put("runwar.host", host); + // variableMap.put("web.host", host); variableMap.put("runwar.stopsocket", Integer.toString(stopSocket)); String trayConfigJSON; @@ -140,7 +144,8 @@ public void hookTray(final Server server) { trayIsHooked = true; } - private void instantiateMenu(String trayConfigJSON, String statusText, String iconImage, HashMap variableMap, Server server) { + private void instantiateMenu(String trayConfigJSON, String statusText, String iconImage, + HashMap variableMap, Server server) { JSONObject menu; setVariableMap(variableMap); menu = getTrayConfig(trayConfigJSON, statusText, variableMap); @@ -167,11 +172,12 @@ public void addMenuItems(JSONArray items, Menu menu, Server server) { if (Utils.getIgnoreCase(itemInfo, "image") != null) { is = getImageInputStream(Utils.getIgnoreCase(itemInfo, "image").toString()); } else { - //check if property action is used + // check if property action is used if (Utils.getIgnoreCase(itemInfo, "action") != null) { - //set Defaults + // set Defaults try { - if (Utils.getIgnoreCase(itemInfo, "command") != null && Utils.getIgnoreCase(itemInfo, "command").toString().toLowerCase().startsWith("box ")) { + if (Utils.getIgnoreCase(itemInfo, "command") != null && Utils.getIgnoreCase(itemInfo, "command") + .toString().toLowerCase().startsWith("box ")) { is = getClass().getResourceAsStream("/box.png"); } else { switch ((String) Utils.getIgnoreCase(itemInfo, "action")) { @@ -210,11 +216,12 @@ public void addMenuItems(JSONArray items, Menu menu, Server server) { menuItem = new MenuItem(label, is, new ExitAction(server)); menuItem.setShortcut('s'); } else if (action.equalsIgnoreCase("restartserver")) { - //menuItem = new MenuItem(label, is, new RestartAction(server)); + // menuItem = new MenuItem(label, is, new RestartAction(server)); String command = "box server restart"; String workingDirectory = server.getServerOptions().warUriString(); String shell = Utils.availableShellPick(); - menuItem = new MenuItem(label, is, new RunShellCommandAction(command, workingDirectory, false, shell)); + menuItem = new MenuItem(label, is, + new RunShellCommandAction(command, workingDirectory, false, shell)); menuItem.setShortcut('r'); } else if (action.equalsIgnoreCase("getversion")) { menuItem = new MenuItem("Version: " + Server.getVersion(), is, new GetVersionAction()); @@ -229,7 +236,8 @@ public void addMenuItems(JSONArray items, Menu menu, Server server) { menuItem.setShortcut('b'); } else if (action.equalsIgnoreCase("run")) { String command = getString(itemInfo, "command", ""); - String workingDirectory = getString(itemInfo, "workingDirectory", server.getServerOptions().warUriString()); + String workingDirectory = getString(itemInfo, "workingDirectory", + server.getServerOptions().warUriString()); String shell = getString(itemInfo, "shell", Utils.availableShellPick()); Boolean waitResponse = true; try { @@ -238,12 +246,15 @@ public void addMenuItems(JSONArray items, Menu menu, Server server) { RunwarLogger.LOG.error("Invalid waitResponse value"); waitResponse = true; } - menuItem = new MenuItem(label, is, new RunShellCommandAction(command, workingDirectory, waitResponse, shell)); + menuItem = new MenuItem(label, is, + new RunShellCommandAction(command, workingDirectory, waitResponse, shell)); } else if (action.equalsIgnoreCase("runAsync")) { String command = getString(itemInfo, "command", ""); - String workingDirectory = getString(itemInfo, "workingDirectory", server.getServerOptions().warUriString()); + String workingDirectory = getString(itemInfo, "workingDirectory", + server.getServerOptions().warUriString()); String shell = getString(itemInfo, "shell", Utils.availableShellPick()); - menuItem = new MenuItem(label, is, new RunShellCommandAction(command, workingDirectory, false, shell)); + menuItem = new MenuItem(label, is, + new RunShellCommandAction(command, workingDirectory, false, shell)); } else { RunwarLogger.LOG.error("Unknown menu item action \"" + action + "\" for \"" + label + "\""); } @@ -287,7 +298,8 @@ public static JSONObject getTrayConfig(String jsonText, String defaultTitle, Has String title = getString(config, "title", defaultTitle); config.put("title", title); String tooltip = getString(config, "tooltip", defaultTitle); - // SystemTray limits tooltip to 64, so enforce that and maybe clean up a cut-off word + // SystemTray limits tooltip to 64, so enforce that and maybe clean up a cut-off + // word if (tooltip.length() > 64) { tooltip = tooltip.substring(0, 61); tooltip = tooltip.substring(0, Math.min(tooltip.length(), tooltip.lastIndexOf(" "))) + "..."; @@ -324,7 +336,8 @@ public static JSONObject getTrayConfig(String jsonText, String defaultTitle, Has } private static String getString(JSONObject menu, String key, String defaultValue) { - String value = Utils.getIgnoreCase(menu, key) != null ? Utils.getIgnoreCase(menu, key).toString() : defaultValue; + String value = Utils.getIgnoreCase(menu, key) != null ? Utils.getIgnoreCase(menu, key).toString() + : defaultValue; return replaceMenuTokens(value); } @@ -472,7 +485,7 @@ public static InputStream getImageInputStream(String iconImage) { } return null; } -///// + ///// private static void showDialog(String content) { final JTextArea jta = new JTextArea(content); @@ -511,22 +524,22 @@ private static class RunShellCommandAction implements ActionListener { private String workingDirectory; private boolean waitResponse; - String[] shells = new String[]{"/bin/bash", "/usr/bin/bash", - "/bin/pfbash", "/usr/bin/pfbash", - "/bin/csh", "/usr/bin/csh", - "/bin/pfcsh", "/usr/bin/pfcsh", - "/bin/jsh", "/usr/bin/jsh", - "/bin/ksh", "/usr/bin/ksh", - "/bin/pfksh", "/usr/bin/pfksh", - "/bin/ksh93", "/usr/bin/ksh93", - "/bin/pfksh93", "/usr/bin/pfksh93", - "/bin/pfsh", "/usr/bin/pfsh", - "/bin/tcsh", "/usr/bin/tcsh", - "/bin/pftcsh", "/usr/bin/pftcsh", - "/usr/xpg4/bin/sh", "/usr/xp4/bin/pfsh", - "/bin/zsh", "/usr/bin/zsh", - "/bin/pfzsh", "/usr/bin/pfzsh", - "/bin/sh", "/usr/bin/sh",}; + String[] shells = new String[] { "/bin/bash", "/usr/bin/bash", + "/bin/pfbash", "/usr/bin/pfbash", + "/bin/csh", "/usr/bin/csh", + "/bin/pfcsh", "/usr/bin/pfcsh", + "/bin/jsh", "/usr/bin/jsh", + "/bin/ksh", "/usr/bin/ksh", + "/bin/pfksh", "/usr/bin/pfksh", + "/bin/ksh93", "/usr/bin/ksh93", + "/bin/pfksh93", "/usr/bin/pfksh93", + "/bin/pfsh", "/usr/bin/pfsh", + "/bin/tcsh", "/usr/bin/tcsh", + "/bin/pftcsh", "/usr/bin/pftcsh", + "/usr/xpg4/bin/sh", "/usr/xp4/bin/pfsh", + "/bin/zsh", "/usr/bin/zsh", + "/bin/pfzsh", "/usr/bin/pfzsh", + "/bin/sh", "/usr/bin/sh", }; RunShellCommandAction(String command, String workingDirectory, Boolean waitResponse, String shell) { this.command = command; @@ -614,8 +627,7 @@ public void run() { try { this.jta = new JTextArea(""); - StreamGobbler streamGobbler - = new StreamGobbler(process.getInputStream(), this::printString); + StreamGobbler streamGobbler = new StreamGobbler(process.getInputStream(), this::printString); this.service = Executors.newSingleThreadExecutor(); this.jsp = new JScrollPane(jta) { @Override @@ -705,7 +717,8 @@ private void stopCommandAttempt(java.awt.event.ActionEvent evt) { } if (tsk != null) { tsk.cancel(true); - }; + } + ; } catch (InterruptedException ex) { RunwarLogger.LOG.error("An Error Occurred trying to stop command :" + this.title, ex); } @@ -744,7 +757,8 @@ public void actionPerformed(ActionEvent e) { try { RunwarLogger.LOG.info("Exiting..."); server.stopServer(); - String message = "Server shut down " + (server.serverWentDown() ? "" : "un") + "successfully, shutting down tray"; + String message = "Server shut down " + (server.serverWentDown() ? "" : "un") + + "successfully, shutting down tray"; RunwarLogger.LOG.debug(message); if (systemTray != null) { try { @@ -826,7 +840,7 @@ public void actionPerformed(ActionEvent e) { try { LaunchUtil.browseDirectory(path); } catch (Exception ex) { - if (!LaunchUtil.isLinux() || !LaunchUtil.execute(new String[]{"xdg-open", path})) { + if (!LaunchUtil.isLinux() || !LaunchUtil.execute(new String[] { "xdg-open", path })) { displayMessage("Error", "Sorry, unable to open the file browser: " + ex.getLocalizedMessage()); } } diff --git a/src/main/java/runwar/undertow/BindingMatcherHandler.java b/src/main/java/runwar/undertow/BindingMatcherHandler.java index 344a2e59..086db481 100644 --- a/src/main/java/runwar/undertow/BindingMatcherHandler.java +++ b/src/main/java/runwar/undertow/BindingMatcherHandler.java @@ -1,137 +1,70 @@ -package runwar; +package runwar.undertow; +import static runwar.logging.RunwarLogger.LOG; -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.predicate.Predicate; -import io.undertow.server.DefaultByteBufferPool; +import java.io.File; +import java.net.InetSocketAddress; +import java.util.HashSet; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; +import java.util.regex.Pattern; + +import io.undertow.io.Sender; import io.undertow.server.HttpHandler; import io.undertow.server.HttpServerExchange; -import io.undertow.server.handlers.PathHandler; -import io.undertow.server.handlers.ProxyPeerAddressHandler; -import io.undertow.server.handlers.SSLHeaderHandler; -import io.undertow.server.handlers.accesslog.AccessLogHandler; -import io.undertow.server.handlers.accesslog.DefaultAccessLogReceiver; -import io.undertow.server.handlers.builder.PredicatedHandler; -import io.undertow.server.handlers.builder.PredicatedHandlersParser; -import io.undertow.server.handlers.cache.DirectBufferCache; -import io.undertow.server.handlers.encoding.ContentEncodingRepository; -import io.undertow.server.handlers.encoding.EncodingHandler; -import io.undertow.server.handlers.encoding.GzipEncodingProvider; -import io.undertow.server.handlers.resource.CachingResourceManager; -import io.undertow.server.handlers.resource.ResourceManager; -import io.undertow.server.handlers.resource.ResourceHandler; import io.undertow.servlet.api.DeploymentInfo; -import io.undertow.servlet.api.DeploymentManager; -import io.undertow.servlet.api.ServletSessionConfig; -import io.undertow.util.CanonicalPathUtils; -import io.undertow.util.AttachmentKey; import io.undertow.util.HeaderValues; -import io.undertow.server.handlers.resource.Resource; import io.undertow.util.Headers; -import io.undertow.util.HttpString; -import io.undertow.util.MimeMappings; -import io.undertow.io.Sender; -import io.undertow.websockets.jsr.WebSocketDeploymentInfo; import net.minidev.json.JSONArray; import net.minidev.json.JSONObject; -import org.xnio.*; -import runwar.logging.LoggerFactory; -import runwar.logging.LoggerPrintStream; -import runwar.logging.RunwarAccessLogReceiver; -import runwar.mariadb4j.MariaDB4jManager; +import runwar.LaunchUtil; +import runwar.RunwarConfigurer; import runwar.options.ServerOptions; -import runwar.options.SiteOptions; -import runwar.options.ConfigParser.JSONOption; -import runwar.security.SSLUtil; -import runwar.security.SecurityManager; -import runwar.tray.Tray; -import runwar.undertow.MappedResourceManager; -import runwar.undertow.HostResourceManager; -import runwar.undertow.RequestDebugHandler; -import runwar.undertow.SSLCertHeaderHandler; -import runwar.undertow.LifecyleHandler; -import runwar.undertow.WelcomeFileHandler; -import runwar.undertow.SiteDeployment; -import runwar.util.ClassLoaderUtils; -import runwar.undertow.SiteDeploymentManager; -import runwar.util.RequestDumper; import runwar.util.MaxContextsException; -import javax.net.ssl.SSLContext; -import java.awt.*; -import java.io.*; -import java.lang.management.ManagementFactory; -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; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.ServletRequest; -import java.util.regex.Matcher; -import java.util.regex.Pattern; -import java.util.Optional; - -import static io.undertow.servlet.Servlets.defaultContainer; -import static io.undertow.servlet.Servlets.deployment; -import io.undertow.servlet.handlers.ServletRequestContext; -import io.undertow.server.HandlerWrapper; -import runwar.undertow.SiteDeploymentManager; -import runwar.LaunchUtil; - -import static runwar.logging.RunwarLogger.LOG; -import static runwar.logging.RunwarLogger.MAPPER_LOG; - -import static runwar.logging.RunwarLogger.LOG; - -@SuppressWarnings( "deprecation" ) +@SuppressWarnings("deprecation") public class BindingMatcherHandler implements HttpHandler { private ServerOptions serverOptions; private JSONObject bindings; - private HashSet deploymentKeyWarnings = new HashSet(); + private HashSet deploymentKeyWarnings = new HashSet(); private SiteDeploymentManager siteDeploymentManager; private RunwarConfigurer configurer; private DeploymentInfo servletBuilder; - private Map > bindingSiteCache = new ConcurrentHashMap>(); + private Map> bindingSiteCache = new ConcurrentHashMap>(); private final String error404Site; - BindingMatcherHandler( ServerOptions serverOptions, SiteDeploymentManager siteDeploymentManager, RunwarConfigurer configurer, DeploymentInfo servletBuilder) { + public BindingMatcherHandler(ServerOptions serverOptions, SiteDeploymentManager siteDeploymentManager, + RunwarConfigurer configurer, DeploymentInfo servletBuilder) { this.serverOptions = serverOptions; this.bindings = serverOptions.bindings(); this.siteDeploymentManager = siteDeploymentManager; this.configurer = configurer; this.servletBuilder = servletBuilder; - this.error404Site = LaunchUtil.getResourceAsString( "runwar/error-404-site.html" ); + this.error404Site = LaunchUtil.getResourceAsString("runwar/error-404-site.html"); } @Override public void handleRequest(final HttpServerExchange exchange) throws Exception { SiteDeployment deployment; String deploymentKey; - ConcurrentHashMap deployments = siteDeploymentManager.getDeployments(); + ConcurrentHashMap deployments = siteDeploymentManager.getDeployments(); - if( serverOptions.getSites().size() > 1 ) { - String IP = exchange.getConnection().getLocalAddress( InetSocketAddress.class ).getAddress().getHostAddress().toLowerCase(); - String port = String.valueOf( exchange.getConnection().getLocalAddress( InetSocketAddress.class ).getPort() ); + if (serverOptions.getSites().size() > 1) { + String IP = exchange.getConnection().getLocalAddress(InetSocketAddress.class).getAddress().getHostAddress() + .toLowerCase(); + String port = String.valueOf(exchange.getConnection().getLocalAddress(InetSocketAddress.class).getPort()); String hostName = exchange.getHostName().toLowerCase(); JSONObject match; - match = findBindingCached( IP, port, hostName ); + match = findBindingCached(IP, port, hostName); - if( match == null ) { - String message = "Can't find a matching binding for IP [" + IP + "], port [" + port + "], and hostname [" + hostName + "]"; - LOG.debug( message ); - final String errorPage = this.error404Site.replace( "@@message@@", escapeHTML( message ) ); + if (match == null) { + String message = "Can't find a matching binding for IP [" + IP + "], port [" + port + + "], and hostname [" + hostName + "]"; + LOG.debug(message); + final String errorPage = this.error404Site.replace("@@message@@", escapeHTML(message)); exchange.setStatusCode(404); exchange.getResponseHeaders().put(Headers.CONTENT_LENGTH, "" + errorPage.length()); exchange.getResponseHeaders().put(Headers.CONTENT_TYPE, "text/html"); @@ -140,91 +73,118 @@ public void handleRequest(final HttpServerExchange exchange) throws Exception { return; } - deploymentKey = (String)match.get( "site" ); - LOG.trace( "Binding is for site: " + deploymentKey ); + deploymentKey = (String) match.get("site"); + LOG.trace("Binding is for site: " + deploymentKey); exchange.putAttachment(SiteDeploymentManager.DEPLOYMENT_KEY, deploymentKey); - deployment = deployments.get( deploymentKey ); + deployment = deployments.get(deploymentKey); } - // If we're not auto-creating contexts, then just pass to our default servlet deployment - else if( !serverOptions.autoCreateContexts() ) { - deployment = deployments.get( SiteDeployment.DEFAULT ); + // If we're not auto-creating contexts, then just pass to our default servlet + // deployment + else if (!serverOptions.autoCreateContexts()) { + deployment = deployments.get(SiteDeployment.DEFAULT); - // Otherwise, see if a deployment already exists + // Otherwise, see if a deployment already exists } else { - if( !isHeaderSafe( exchange, "", "X-Webserver-Context" ) ) return; + if (!isHeaderSafe(exchange, "", "X-Webserver-Context")) + return; - deploymentKey = exchange.getRequestHeaders().getFirst( "X-Webserver-Context" ); - if( deploymentKey == null ){ + 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(SiteDeploymentManager.DEPLOYMENT_KEY, deploymentKey); - deployment = deployments.get( deploymentKey ); - if( deployment == null ) { + deployment = deployments.get(deploymentKey); + if (deployment == null) { - if( !isHeaderSafe( exchange, deploymentKey, "X-Tomcat-DocRoot" ) ) return; - String docRoot = exchange.getRequestHeaders().getFirst( "X-Tomcat-DocRoot" ); + 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() ) { + 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 ) { + 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 ) ) { + 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 + "]." ); + 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() ) { + 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 ignored because no x-vdirs-sharedkey header is present." ); + logOnce(deploymentKey, "NovDirsSharedKey", "warn", + "The x-vdirs header was provided, but it is being ignored because no x-vdirs-sharedkey header is present."); } else { // If a secret was provided, enforce it - if( !serverOptions.autoCreateContextsSecret().equals( "" ) && !serverOptions.autoCreateContextsSecret().equals( vDirsSharedKey ) ) { + if (!serverOptions.autoCreateContextsSecret().equals("") + && !serverOptions.autoCreateContextsSecret().equals(vDirsSharedKey)) { vDirs = null; - logOnce( deploymentKey, "VDirsSharedKeyNotMatch", "warn", "The x-vdirs header was provided, but it is being ignored because the x-vdirs-sharedkey header [" + vDirsSharedKey + "] doesn't match the auto-create-contexts-secret setting [" + ( serverOptions.autoCreateContextsSecret() == null ? "" : serverOptions.autoCreateContextsSecret() ) + "] for deploymentKey [" + deploymentKey + "]." ); + logOnce(deploymentKey, "VDirsSharedKeyNotMatch", "warn", + "The x-vdirs header was provided, but it is being ignored 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 = siteDeploymentManager.createSiteDeployment( servletBuilder, docRootFile, configurer, deploymentKey, vDirs, serverOptions.getSites().get(0) ); - } catch ( MaxContextsException e ) { + deployment = siteDeploymentManager.createSiteDeployment(servletBuilder, docRootFile, + configurer, deploymentKey, vDirs, serverOptions.getSites().get(0)); + } catch (MaxContextsException e) { exchange.getResponseHeaders().put(Headers.CONTENT_TYPE, "text/plain"); exchange.setStatusCode(500); - exchange.getResponseSender().send( e.getMessage() ); + exchange.getResponseSender().send(e.getMessage()); - logOnce( deploymentKey, "MaxContextsException", "error", e.getMessage() + " The requested deploymentKey was [" + deploymentKey + "]" ); + 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( SiteDeployment.DEFAULT ); + LOG.warn("X-Tomcat-DocRoot of [" + docRoot + + "] does not exist or is not directory. Using default context."); + deployment = deployments.get(SiteDeployment.DEFAULT); } } else { - logOnce( deploymentKey, "NoDocRootHeader", "warn", "X-Tomcat-DocRoot is null or empty. Using default context for deploymentKey [" + deploymentKey + "]." ); - deployment = deployments.get( SiteDeployment.DEFAULT ); + logOnce(deploymentKey, "NoDocRootHeader", "warn", + "X-Tomcat-DocRoot is null or empty. Using default context for deploymentKey [" + + deploymentKey + "]."); + deployment = deployments.get(SiteDeployment.DEFAULT); } } @@ -232,32 +192,35 @@ else if( !serverOptions.autoCreateContexts() ) { // Save into the exchange for later in the thread exchange.putAttachment(SiteDeploymentManager.SITE_DEPLOYMENT_KEY, deployment); - deployment.processRequest( exchange ); + deployment.processRequest(exchange); } /** - * Caches found bindings in a HashMap. This serves as a simple cache, but it is possible to send a huge amount of - * requests with random hostnames to the server and "fill up" the Map as there is reaping mechanism. As such, + * Caches found bindings in a HashMap. This serves as a simple cache, but it is + * possible to send a huge amount of + * requests with random hostnames to the server and "fill up" the Map as there + * is reaping mechanism. As such, * we'll only cache the first 10,000 IP/port/hostname combinations we see. */ - private JSONObject findBindingCached( String IP, String port, String hostName ) { + private JSONObject findBindingCached(String IP, String port, String hostName) { String cacheBindingKey = IP + ":" + port + ":" + hostName; - if( bindingSiteCache.containsKey( cacheBindingKey ) ) { - Optional match = bindingSiteCache.get( cacheBindingKey ); - if( match.isPresent() ) { + if (bindingSiteCache.containsKey(cacheBindingKey)) { + Optional match = bindingSiteCache.get(cacheBindingKey); + if (match.isPresent()) { return match.get(); } else { return null; } } // May be null, but we still want to cache even that - Optional match = Optional.ofNullable( findBinding( IP, port, hostName ) ); - // A little protection to prevent an unlimited number of incoming hostname variations from eating up crazy memory - if( bindingSiteCache.size() < 10000 ) { - bindingSiteCache.put( cacheBindingKey, match ); + Optional match = Optional.ofNullable(findBinding(IP, port, hostName)); + // A little protection to prevent an unlimited number of incoming hostname + // variations from eating up crazy memory + if (bindingSiteCache.size() < 10000) { + bindingSiteCache.put(cacheBindingKey, match); } - if( match.isPresent() ) { + if (match.isPresent()) { return match.get(); } else { return null; @@ -278,159 +241,178 @@ private JSONObject findBindingCached( String IP, String port, String hostName ) * - Any IP and any hostname * - Default site * - * Note, the port always must match, unless there is a default site, then we don't care. + * Note, the port always must match, unless there is a default site, then we + * don't care. */ - private JSONObject findBinding( String IP, String port, String hostName ) { + private JSONObject findBinding(String IP, String port, String hostName) { JSONObject match; // 1. Try exact IP and hostname match String bindingKey = IP + ":" + port + ":" + hostName; - LOG.trace( "Trying binding key: " + bindingKey ); - match = (JSONObject)bindings.get( bindingKey ); - if( match != null ) return match; + LOG.trace("Trying binding key: " + bindingKey); + match = (JSONObject) bindings.get(bindingKey); + if (match != null) + return match; // 2. Try exact IP and hostname ends with match - match = findHostWildcardEndsWith( hostName, bindings, IP + ":" + port + "::endswith:" ); - if( match != null ) return match; + match = findHostWildcardEndsWith(hostName, bindings, IP + ":" + port + "::endswith:"); + if (match != null) + return match; // 3. Try exact IP and hostname starts with match - match = findHostWildcardStartsWith( hostName, bindings, IP + ":" + port + "::startswith:" ); - if( match != null ) return match; + match = findHostWildcardStartsWith(hostName, bindings, IP + ":" + port + "::startswith:"); + if (match != null) + return match; // 4. Try exact IP and hostname regex match - match = findHostWildcardRegex( hostName, bindings, IP + ":" + port + "::regex:" ); - if( match != null ) return match; + match = findHostWildcardRegex(hostName, bindings, IP + ":" + port + "::regex:"); + if (match != null) + return match; // 5. Try Any IP and hostname exact match bindingKey = "0.0.0.0:" + port + ":" + hostName; - LOG.trace( "Trying binding key: " + bindingKey ); - match = (JSONObject)bindings.get( bindingKey ); - if( match != null ) return match; + LOG.trace("Trying binding key: " + bindingKey); + match = (JSONObject) bindings.get(bindingKey); + if (match != null) + return match; // 6. Try any IP and hostname ends with match - match = findHostWildcardEndsWith( hostName, bindings, "0.0.0.0:" + port + "::endswith:" ); - if( match != null ) return match; + match = findHostWildcardEndsWith(hostName, bindings, "0.0.0.0:" + port + "::endswith:"); + if (match != null) + return match; // 7. Try any IP and hostname starts with match - match = findHostWildcardStartsWith( hostName, bindings, "0.0.0.0:" + port + "::startswith:" ); - if( match != null ) return match; + match = findHostWildcardStartsWith(hostName, bindings, "0.0.0.0:" + port + "::startswith:"); + if (match != null) + return match; // 8. Try Any IP and hostname regex match - match = findHostWildcardRegex( hostName, bindings, "0.0.0.0:" + port + "::regex:" ); - if( match != null ) return match; + match = findHostWildcardRegex(hostName, bindings, "0.0.0.0:" + port + "::regex:"); + if (match != null) + return match; // 9. Try Exact IP and any hostname - bindingKey = IP + ":" + port + ":*" ; - LOG.trace( "Trying binding key: " + bindingKey ); - match = (JSONObject)bindings.get( bindingKey ); - if( match != null ) return match; + bindingKey = IP + ":" + port + ":*"; + LOG.trace("Trying binding key: " + bindingKey); + match = (JSONObject) bindings.get(bindingKey); + if (match != null) + return match; // 10. Try Any IP and any hostname - bindingKey = "0.0.0.0:" + port + ":*" ; - LOG.trace( "Trying binding key: " + bindingKey ); - match = (JSONObject)bindings.get( bindingKey ); - if( match != null ) return match; + bindingKey = "0.0.0.0:" + port + ":*"; + LOG.trace("Trying binding key: " + bindingKey); + match = (JSONObject) bindings.get(bindingKey); + if (match != null) + return match; // 11. Look for a default site - bindingKey = "default" ; - LOG.trace( "Trying binding key: " + bindingKey ); - match = (JSONObject)bindings.get( bindingKey ); + bindingKey = "default"; + LOG.trace("Trying binding key: " + bindingKey); + match = (JSONObject) bindings.get(bindingKey); // Match can still be null if there was no default site return match; } - private JSONObject findHostWildcardEndsWith( String hostName, JSONObject bindings, String bindingKey ) { - JSONArray options = (JSONArray)bindings.get( bindingKey ); - if( options == null ) { + private JSONObject findHostWildcardEndsWith(String hostName, JSONObject bindings, String bindingKey) { + JSONArray options = (JSONArray) bindings.get(bindingKey); + if (options == null) { return null; } - for( Object option : options ) { - JSONObject binding = (JSONObject)option; - String thisOptionMatch = (String)(binding.get( "endsWithMatch" )); - LOG.trace( "Checking if [" + hostName + "] ends with [" + thisOptionMatch + "] for binding [" + bindingKey + "]" ); - if( hostName.endsWith( thisOptionMatch ) ) { + for (Object option : options) { + JSONObject binding = (JSONObject) option; + String thisOptionMatch = (String) (binding.get("endsWithMatch")); + LOG.trace("Checking if [" + hostName + "] ends with [" + thisOptionMatch + "] for binding [" + bindingKey + + "]"); + if (hostName.endsWith(thisOptionMatch)) { return binding; } } return null; } - private JSONObject findHostWildcardStartsWith( String hostName, JSONObject bindings, String bindingKey ) { - JSONArray options = (JSONArray)bindings.get( bindingKey ); - if( options == null ) { + private JSONObject findHostWildcardStartsWith(String hostName, JSONObject bindings, String bindingKey) { + JSONArray options = (JSONArray) bindings.get(bindingKey); + if (options == null) { return null; } - for( Object option : options ) { - JSONObject binding = (JSONObject)option; - String thisOptionMatch = (String)(binding.get( "startsWithMatch" )); - LOG.trace( "Checking if [" + hostName + "] starts with [" + thisOptionMatch + "] for binding [" + bindingKey + "]" ); - if( hostName.startsWith( thisOptionMatch ) ) { + for (Object option : options) { + JSONObject binding = (JSONObject) option; + String thisOptionMatch = (String) (binding.get("startsWithMatch")); + LOG.trace("Checking if [" + hostName + "] starts with [" + thisOptionMatch + "] for binding [" + bindingKey + + "]"); + if (hostName.startsWith(thisOptionMatch)) { return binding; } } return null; } - private JSONObject findHostWildcardRegex( String hostName, JSONObject bindings, String bindingKey ) { - JSONArray options = (JSONArray)bindings.get( bindingKey ); - if( options == null ) { + private JSONObject findHostWildcardRegex(String hostName, JSONObject bindings, String bindingKey) { + JSONArray options = (JSONArray) bindings.get(bindingKey); + if (options == null) { return null; } - for( Object option : options ) { - JSONObject binding = (JSONObject)option; - Pattern thisOptionMatch = (Pattern)binding.get( "pattern" ); - String thisOptionStr = (String)(binding.get( "regexMatch" )); - LOG.trace( "Checking if [" + hostName + "] matches the regex [" + thisOptionStr + "] for binding [" + bindingKey + "]" ); - if( thisOptionMatch.matcher(hostName).matches() ) { + for (Object option : options) { + JSONObject binding = (JSONObject) option; + Pattern thisOptionMatch = (Pattern) binding.get("pattern"); + String thisOptionStr = (String) (binding.get("regexMatch")); + LOG.trace("Checking if [" + hostName + "] matches the regex [" + thisOptionStr + "] for binding [" + + bindingKey + "]"); + if (thisOptionMatch.matcher(hostName).matches()) { return binding; } } return null; } - 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 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; + 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; } @Override @@ -438,11 +420,10 @@ public String toString() { return "Runwar HostHandler"; } - private String escapeHTML( String text ) { + private String escapeHTML(String text) { return text .replace("<", "<") .replace(">", ">") .replace("&", "&"); } } - diff --git a/src/main/java/runwar/undertow/ListenerManager.java b/src/main/java/runwar/undertow/ListenerManager.java index ef8b4637..0ab49be5 100644 --- a/src/main/java/runwar/undertow/ListenerManager.java +++ b/src/main/java/runwar/undertow/ListenerManager.java @@ -1,137 +1,83 @@ -package runwar; +package runwar.undertow; + +import static runwar.logging.RunwarLogger.LOG; + +import java.io.File; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +import javax.net.ssl.SSLContext; + +import org.xnio.OptionMap; +import org.xnio.Options; +import org.xnio.SslClientAuthMode; -import io.undertow.Handlers; -import io.undertow.Undertow; import io.undertow.Undertow.Builder; import io.undertow.Undertow.ListenerBuilder; import io.undertow.Undertow.ListenerType; -import io.undertow.client.ClientConnection; import io.undertow.UndertowOptions; -import io.undertow.predicate.Predicates; -import io.undertow.predicate.Predicate; -import io.undertow.server.DefaultByteBufferPool; -import io.undertow.server.HttpHandler; -import io.undertow.server.HttpServerExchange; -import io.undertow.server.handlers.PathHandler; -import io.undertow.server.handlers.ProxyPeerAddressHandler; -import io.undertow.server.handlers.SSLHeaderHandler; -import io.undertow.server.handlers.accesslog.AccessLogHandler; -import io.undertow.server.handlers.accesslog.DefaultAccessLogReceiver; -import io.undertow.server.handlers.builder.PredicatedHandler; -import io.undertow.server.handlers.builder.PredicatedHandlersParser; -import io.undertow.server.handlers.cache.DirectBufferCache; -import io.undertow.server.handlers.encoding.ContentEncodingRepository; -import io.undertow.server.handlers.encoding.EncodingHandler; -import io.undertow.server.handlers.encoding.GzipEncodingProvider; -import io.undertow.server.handlers.resource.CachingResourceManager; -import io.undertow.server.handlers.resource.ResourceManager; -import io.undertow.server.handlers.resource.ResourceHandler; -import io.undertow.servlet.api.DeploymentInfo; -import io.undertow.servlet.api.DeploymentManager; -import io.undertow.servlet.api.ServletSessionConfig; -import io.undertow.util.CanonicalPathUtils; -import io.undertow.util.AttachmentKey; -import io.undertow.util.HeaderValues; -import io.undertow.server.handlers.resource.Resource; -import io.undertow.util.Headers; -import io.undertow.util.HttpString; -import io.undertow.util.MimeMappings; -import io.undertow.io.Sender; -import io.undertow.websockets.jsr.WebSocketDeploymentInfo; import io.undertow.protocols.ssl.SNIContextMatcher; import io.undertow.protocols.ssl.SNISSLContext; +import io.undertow.server.HttpHandler; +import io.undertow.server.HttpServerExchange; import net.minidev.json.JSONArray; import net.minidev.json.JSONObject; +import runwar.Server; import runwar.options.ConfigParser.JSONOption; -import org.xnio.*; -import org.xnio.OptionMap; -import org.xnio.Options; -import runwar.logging.LoggerFactory; -import runwar.logging.LoggerPrintStream; -import runwar.logging.RunwarAccessLogReceiver; -import runwar.mariadb4j.MariaDB4jManager; import runwar.options.ServerOptions; import runwar.security.SSLUtil; -import runwar.security.SecurityManager; -import runwar.tray.Tray; -import runwar.undertow.MappedResourceManager; -import runwar.undertow.HostResourceManager; -import runwar.undertow.RequestDebugHandler; -import runwar.undertow.SSLCertHeaderHandler; -import runwar.undertow.LifecyleHandler; -import runwar.undertow.WelcomeFileHandler; -import runwar.undertow.SiteDeployment; -import runwar.undertow.SiteDeploymentManager; -import runwar.util.ClassLoaderUtils; -import runwar.util.RequestDumper; - -import javax.net.ssl.SSLContext; -import java.awt.*; -import java.io.*; -import java.util.*; -import java.lang.management.ManagementFactory; -import java.lang.reflect.Method; -import java.net.*; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.concurrent.ConcurrentHashMap; -import javax.servlet.Servlet; -import javax.servlet.http.HttpServletResponse; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.ServletRequest; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import io.undertow.servlet.handlers.ServletRequestContext; -import io.undertow.server.HandlerWrapper; -import static runwar.logging.RunwarLogger.LOG; public class ListenerManager { - ListenerManager(){ + ListenerManager() { } - public static void configureListeners(Builder serverBuilder, ServerOptions serverOptions ) { + public static void configureListeners(Builder serverBuilder, ServerOptions serverOptions) { JSONOption listeners = serverOptions.listeners(); String cfengine = serverOptions.cfEngineName(); - LOG.info("Listeners:" ); + LOG.info("Listeners:"); - if( listeners.hasOption( "http" ) ) { - JSONOption HTTPListeners = listeners.g( "http" ); - for( String key : HTTPListeners.getKeys() ) { - JSONOption listener = HTTPListeners.g( key ); - LOG.info(" - Binding HTTP on " + listener.getOptionValue("IP") + ":" + listener.getOptionValue("port") ); + if (listeners.hasOption("http")) { + JSONOption HTTPListeners = listeners.g("http"); + for (String key : HTTPListeners.getKeys()) { + JSONOption listener = HTTPListeners.g(key); + LOG.info( + " - Binding HTTP on " + listener.getOptionValue("IP") + ":" + listener.getOptionValue("port")); OptionMap.Builder socketOptions = OptionMap.builder(); - if (listener.hasOption("HTTP2Enable" ) ) { - LOG.debug(" Setting HTTP/2 enabled: " + listener.getOptionBoolean("HTTP2Enable" ) ); + if (listener.hasOption("HTTP2Enable")) { + LOG.debug(" Setting HTTP/2 enabled: " + listener.getOptionBoolean("HTTP2Enable")); // Undertow ignores this :/ - socketOptions.set(UndertowOptions.ENABLE_HTTP2, listener.getOptionBoolean("HTTP2Enable" ) ); - // Only this server-wide setting appears to do anything. If it's not set, set it. - if( serverOptions.undertowOptions().getMap().get( UndertowOptions.ENABLE_HTTP2 )==null ) { - serverOptions.undertowOptions().set(UndertowOptions.ENABLE_HTTP2, listener.getOptionBoolean("HTTP2Enable" ) ); - // Otherwise, set it, favoring true + socketOptions.set(UndertowOptions.ENABLE_HTTP2, listener.getOptionBoolean("HTTP2Enable")); + // Only this server-wide setting appears to do anything. If it's not set, set + // it. + if (serverOptions.undertowOptions().getMap().get(UndertowOptions.ENABLE_HTTP2) == null) { + serverOptions.undertowOptions().set(UndertowOptions.ENABLE_HTTP2, + listener.getOptionBoolean("HTTP2Enable")); + // Otherwise, set it, favoring true } else { - serverOptions.undertowOptions().set(UndertowOptions.ENABLE_HTTP2, listener.getOptionBoolean("HTTP2Enable" ) || serverOptions.undertowOptions().getMap().get( UndertowOptions.ENABLE_HTTP2 ) ); + serverOptions.undertowOptions().set(UndertowOptions.ENABLE_HTTP2, + listener.getOptionBoolean("HTTP2Enable") + || serverOptions.undertowOptions().getMap().get(UndertowOptions.ENABLE_HTTP2)); } } serverBuilder.addListener( - new ListenerBuilder() - .setType( ListenerType.HTTP ) - .setPort( listener.getOptionInt("port") ) - .setHost( listener.getOptionValue("IP") ) - .setOverrideSocketOptions( socketOptions.getMap() ) - ); + new ListenerBuilder() + .setType(ListenerType.HTTP) + .setPort(listener.getOptionInt("port")) + .setHost(listener.getOptionValue("IP")) + .setOverrideSocketOptions(socketOptions.getMap())); } } - if( listeners.hasOption( "ssl" ) ) { - JSONOption HTTPSListeners = listeners.g( "ssl" ); - for( String key : HTTPSListeners.getKeys() ) { - JSONOption listener = HTTPSListeners.g( key ); - LOG.info(" - Binding SSL on " + listener.getOptionValue("IP") + ":" + listener.getOptionValue("port") ); + if (listeners.hasOption("ssl")) { + JSONOption HTTPSListeners = listeners.g("ssl"); + for (String key : HTTPSListeners.getKeys()) { + JSONOption listener = HTTPSListeners.g(key); + LOG.info(" - Binding SSL on " + listener.getOptionValue("IP") + ":" + listener.getOptionValue("port")); if (serverOptions.sslEccDisable() && cfengine.toLowerCase().equals("adobe")) { LOG.debug(" Disabling com.sun.net.ssl.enableECC"); @@ -139,47 +85,51 @@ public static void configureListeners(Builder serverBuilder, ServerOptions serve } try { - JSONOption clientCert = listener.g( "clientCert" ); - String[] sslAddCACerts=null; - String sslTruststore=null; - String sslTruststorePass=null; - - if ( clientCert.hasOption( "CATrustStoreFile" ) ) { - sslTruststore = clientCert.getOptionValue( "CATrustStoreFile" ); - if( clientCert.hasOption( "CATrustStorePass" ) && clientCert.getOptionValue( "CATrustStorePass" ) != null ) { - sslTruststorePass = clientCert.getOptionValue( "CATrustStorePass" ); + JSONOption clientCert = listener.g("clientCert"); + String[] sslAddCACerts = null; + String sslTruststore = null; + String sslTruststorePass = null; + + if (clientCert.hasOption("CATrustStoreFile")) { + sslTruststore = clientCert.getOptionValue("CATrustStoreFile"); + if (clientCert.hasOption("CATrustStorePass") + && clientCert.getOptionValue("CATrustStorePass") != null) { + sslTruststorePass = clientCert.getOptionValue("CATrustStorePass"); } else { sslTruststorePass = ""; } } - // Even if there is a trust store provided above, any certs below will be added in along with the original contents. - if ( clientCert.hasOption( "CACertFiles" ) ) { - sslAddCACerts = clientCert.getOptionArray( "CACertFiles" ).stream().toArray(String[]::new); + // Even if there is a trust store provided above, any certs below will be added + // in along with the original contents. + if (clientCert.hasOption("CACertFiles")) { + sslAddCACerts = clientCert.getOptionArray("CACertFiles").stream().toArray(String[]::new); } - JSONArray certs = listener.getOptionArray( "certs" ); + JSONArray certs = listener.getOptionArray("certs"); SNIContextMatcher.Builder sniMatchBuilder = new SNIContextMatcher.Builder(); boolean first = true; - SSLContext sslContext=null; - if( certs.size() > 0 ) { + SSLContext sslContext = null; + if (certs.size() > 0) { - for( Object certObject : certs ) { - JSONOption cert = new JSONOption( (JSONObject)certObject ); + for (Object certObject : certs) { + JSONOption cert = new JSONOption((JSONObject) certObject); - File certFile = cert.getOptionFile( "certFile" ); - File keyFile = cert.getOptionFile( "keyFile" ); + File certFile = cert.getOptionFile("certFile"); + File keyFile = cert.getOptionFile("keyFile"); char[] keypass; - if( cert.hasOption( "keyPass" ) && cert.getOptionValue( "keyPass" ) != null ) { - keypass = cert.getOptionValue( "keyPass" ).toCharArray(); + if (cert.hasOption("keyPass") && cert.getOptionValue("keyPass") != null) { + keypass = cert.getOptionValue("keyPass").toCharArray(); } else { keypass = "".toCharArray(); } - sslContext = SSLUtil.createSSLContext(certFile, keyFile, keypass, null, sslTruststore, sslTruststorePass, sslAddCACerts, sniMatchBuilder); - if( first ) { + sslContext = SSLUtil.createSSLContext(certFile, keyFile, keypass, null, sslTruststore, + sslTruststorePass, sslAddCACerts, sniMatchBuilder); + if (first) { // The first SSL Context we come across becomes the default - // If the site allows in a hostname not matched by any of the certs, this context will get used. + // If the site allows in a hostname not matched by any of the certs, this + // context will get used. sniMatchBuilder.setDefaultContext(sslContext); first = false; } @@ -190,63 +140,68 @@ public static void configureListeners(Builder serverBuilder, ServerOptions serve } } else { - sslContext = SSLUtil.createSSLContext( null, sslTruststore, sslTruststorePass, sslAddCACerts, sniMatchBuilder ); + sslContext = SSLUtil.createSSLContext(null, sslTruststore, sslTruststorePass, sslAddCACerts, + sniMatchBuilder); sniMatchBuilder.setDefaultContext(sslContext); } // Only enable SNI if there was more than 1 cert - if( certs.size() > 1 ) { + if (certs.size() > 1) { LOG.debug(" Enabling SNI on SSLContext. (" + certs.size() + " certs)"); - sslContext = new SNISSLContext( sniMatchBuilder.build() ); + sslContext = new SNISSLContext(sniMatchBuilder.build()); } OptionMap.Builder socketOptions = OptionMap.builder(); - if (listener.hasOption("HTTP2Enable" ) ) { - LOG.debug(" Setting HTTP/2 enabled: " + listener.getOptionBoolean("HTTP2Enable" ) ); + if (listener.hasOption("HTTP2Enable")) { + LOG.debug(" Setting HTTP/2 enabled: " + listener.getOptionBoolean("HTTP2Enable")); // Undertow ignores this :/ - socketOptions.set(UndertowOptions.ENABLE_HTTP2, listener.getOptionBoolean("HTTP2Enable" ) ); - // Only this server-wide setting appears to do anything. If it's not set, set it. - if( serverOptions.undertowOptions().getMap().get( UndertowOptions.ENABLE_HTTP2 )==null ) { - serverOptions.undertowOptions().set(UndertowOptions.ENABLE_HTTP2, listener.getOptionBoolean("HTTP2Enable" ) ); - // Otherwise, set it, favoring true + socketOptions.set(UndertowOptions.ENABLE_HTTP2, listener.getOptionBoolean("HTTP2Enable")); + // Only this server-wide setting appears to do anything. If it's not set, set + // it. + if (serverOptions.undertowOptions().getMap().get(UndertowOptions.ENABLE_HTTP2) == null) { + serverOptions.undertowOptions().set(UndertowOptions.ENABLE_HTTP2, + listener.getOptionBoolean("HTTP2Enable")); + // Otherwise, set it, favoring true } else { - serverOptions.undertowOptions().set(UndertowOptions.ENABLE_HTTP2, listener.getOptionBoolean("HTTP2Enable" ) || serverOptions.undertowOptions().getMap().get( UndertowOptions.ENABLE_HTTP2 ) ); + serverOptions.undertowOptions().set(UndertowOptions.ENABLE_HTTP2, + listener.getOptionBoolean("HTTP2Enable") || serverOptions.undertowOptions().getMap() + .get(UndertowOptions.ENABLE_HTTP2)); } } - if ( clientCert.hasOption( "mode" ) ) { - LOG.debug(" Client Cert Negotiation: " + clientCert.getOptionValue( "mode" ) ); - socketOptions.set(Options.SSL_CLIENT_AUTH_MODE, SslClientAuthMode.valueOf( clientCert.getOptionValue( "mode" ).toUpperCase() ) ); + if (clientCert.hasOption("mode")) { + LOG.debug(" Client Cert Negotiation: " + clientCert.getOptionValue("mode")); + socketOptions.set(Options.SSL_CLIENT_AUTH_MODE, + SslClientAuthMode.valueOf(clientCert.getOptionValue("mode").toUpperCase())); } - if( clientCert.hasOption( "SSLRenegotiationEnable" ) && clientCert.getOptionBoolean( "SSLRenegotiationEnable" ) ) { + if (clientCert.hasOption("SSLRenegotiationEnable") + && clientCert.getOptionBoolean("SSLRenegotiationEnable")) { LOG.warn(" SSL Client cert renegotiation is enabled. Disabling HTTP/2 and TLS1.3"); - socketOptions.set(UndertowOptions.ENABLE_HTTP2, false ); - if( !socketOptions.getMap().contains( Options.SSL_ENABLED_PROTOCOLS ) ) { - socketOptions.setSequence( Options.SSL_ENABLED_PROTOCOLS, "TLSv1.1", "TLSv1.2" ); + socketOptions.set(UndertowOptions.ENABLE_HTTP2, false); + if (!socketOptions.getMap().contains(Options.SSL_ENABLED_PROTOCOLS)) { + socketOptions.setSequence(Options.SSL_ENABLED_PROTOCOLS, "TLSv1.1", "TLSv1.2"); } } serverBuilder.addListener( - new ListenerBuilder() - .setType( ListenerType.HTTPS ) - .setPort( listener.getOptionInt("port") ) - .setHost( listener.getOptionValue("IP") ) - .setSslContext( sslContext ) - .setOverrideSocketOptions( socketOptions.getMap() ) - ); + new ListenerBuilder() + .setType(ListenerType.HTTPS) + .setPort(listener.getOptionInt("port")) + .setHost(listener.getOptionValue("IP")) + .setSslContext(sslContext) + .setOverrideSocketOptions(socketOptions.getMap())); } catch (Exception e) { - throw new RuntimeException( "Unable to start SSL", e ); + throw new RuntimeException("Unable to start SSL", e); } } } - - if( listeners.hasOption( "ajp" ) ) { - JSONOption AJPListeners = listeners.g( "ajp" ); - for( String key : AJPListeners.getKeys() ) { - JSONOption listener = AJPListeners.g( key ); - LOG.info(" - Binding AJP on " + listener.getOptionValue("IP") + ":" + listener.getOptionValue("port") ); + if (listeners.hasOption("ajp")) { + JSONOption AJPListeners = listeners.g("ajp"); + for (String key : AJPListeners.getKeys()) { + JSONOption listener = AJPListeners.g(key); + LOG.info(" - Binding AJP on " + listener.getOptionValue("IP") + ":" + listener.getOptionValue("port")); OptionMap.Builder socketOptions = OptionMap.builder(); if (serverOptions.undertowOptions().getMap().size() == 0) { // if no options is set, default to the large packet size @@ -255,33 +210,35 @@ public static void configureListeners(Builder serverBuilder, ServerOptions serve final String AJPPort = listener.getOptionValue("port"); serverBuilder.addListener( - new ListenerBuilder() - .setType( ListenerType.AJP ) - .setPort( listener.getOptionInt("port") ) - .setHost( listener.getOptionValue("IP") ) - .setOverrideSocketOptions( socketOptions.getMap() ) - .setRootHandler( new HttpHandler(){ - - @Override - public void handleRequest(final HttpServerExchange exchange) throws Exception { - - Map attrs = exchange.getAttachment(HttpServerExchange.REQUEST_ATTRIBUTES); - if(attrs == null) { - exchange.putAttachment(HttpServerExchange.REQUEST_ATTRIBUTES, attrs = new HashMap<>()); - } - - // Mark this request as coming from this AJP port as Undertow doesn't seem to provide any way to get this info from the exchange later. - attrs.put( "__ajp_port", AJPPort ); - Server.getRootHandler().handleRequest( exchange ); - } - - @Override - public String toString() { - return "AJP Identifying Handler"; - } - - } ) - ); + new ListenerBuilder() + .setType(ListenerType.AJP) + .setPort(listener.getOptionInt("port")) + .setHost(listener.getOptionValue("IP")) + .setOverrideSocketOptions(socketOptions.getMap()) + .setRootHandler(new HttpHandler() { + + @Override + public void handleRequest(final HttpServerExchange exchange) throws Exception { + + Map attrs = exchange + .getAttachment(HttpServerExchange.REQUEST_ATTRIBUTES); + if (attrs == null) { + exchange.putAttachment(HttpServerExchange.REQUEST_ATTRIBUTES, + attrs = new HashMap<>()); + } + + // Mark this request as coming from this AJP port as Undertow doesn't seem to + // provide any way to get this info from the exchange later. + attrs.put("__ajp_port", AJPPort); + Server.getRootHandler().handleRequest(exchange); + } + + @Override + public String toString() { + return "AJP Identifying Handler"; + } + + })); } } diff --git a/src/main/rocker/runwar/rock/json/ServerOptionsJSON.rocker.raw b/src/main/rocker/runwar/rock/json/ServerOptionsJSON.rocker.raw deleted file mode 100644 index dbbba716..00000000 --- a/src/main/rocker/runwar/rock/json/ServerOptionsJSON.rocker.raw +++ /dev/null @@ -1,58 +0,0 @@ -@args(runwar.options.ServerOptionsImpl options) -{ - "name":"@options.serverName()", - "startTimeout":@options.launchTimeout(), - "stopsocket":@options.stopPort(), - "debug":@options.debug(), - "console":false, - "pidFile": "@options.pidFile()?:""", - "openBrowser":@options.openbrowser(), - "openBrowserURL":"@options.openbrowserURL()?:""", - "app":{ - "logDir":"@options.logDir()", - "cfengine":"@options.cfEngineName()", - "serverHomeDirectory":"@options.warFile()?:""", - "libDirs": "@options.libDirs()?:""", - "webConfigDir": "@options.cfmlServletConfigWebDir()?:""", - "serverConfigDir": "@options.cfmlServletConfigServerDir()?:""", - "webXML": "@options.webXmlFile()?:""", - "WARPath": "@options.warFile()?:""", - "restMappings": [@String.join(",", options.servletRestMappings())?:""] - }, - "trayOptions":@options.trayConfigJSON()?:"[]", - "jvm": [@(options.jvmArgs() != null ? String.join(",", options.jvmArgs()) : "")], - "web":{ - "webroot":"@options.warFile()?:""", - "host":"@options.host()?:""", - "directoryBrowsing":"@options.directoryListingEnable()?:"false"", - "welcomeFiles": [@(options.welcomeFiles() != null ? String.join(",", options.welcomeFiles()) : "")], - "contentDirs":@options.toJson(options.contentDirectories()), - "aliases":@options.toJson(options.aliases()), - "errorPages": @(options.errorPages() != null ? options.toJson(options.errorPages()) : "{}"), - "http":{ - "port":@options.httpPort(), - "enable":@options.httpEnable() - }, - "ajp":{ - "port":@options.ajpPort(), - "enable":@options.ajpEnable() - }, - "ssl":{ - "enable":@options.sslEnable(), - "port":@options.sslPort(), - "certFile":"@options.sslCertificate()?:""", - "keyFile":"@options.sslKey()?:""", - "keyPass":"@options.sslKeyPass()?:""" - }, - "rewrites": { - "enable": @options.urlRewriteEnable(), - "config": "@options.urlRewriteFile()?:""", - "statusPath": "@options.urlRewriteStatusPath()?:""", - "configReloadSeconds": @options.urlRewriteCheckInterval()?:0 - }, - "basicAuth": { - "enable": @options.basicAuthEnable(), - "users": @(options.basicAuth() != null ? options.toJson(options.basicAuth()) : "{}") - } - } -} \ No newline at end of file From a3512ac222572e870311a9bdca9d45d468384778 Mon Sep 17 00:00:00 2001 From: Brad Wood Date: Mon, 26 Feb 2024 13:45:54 -0600 Subject: [PATCH 02/30] Fix string comparison regression --- gradle/version | 2 +- src/main/java/runwar/options/SiteOptions.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/version b/gradle/version index 49b44f7d..5035f4fd 100644 --- a/gradle/version +++ b/gradle/version @@ -1 +1 @@ -5.0.1-ccc46e035cf6589314e267a6a162d4f9b026590e-96ea216b7cfb6883a6af15c191a94953a0596b9c \ No newline at end of file +5.0.2-0069a6ce9838109e18344f96b4a1d97f322a260d-96ea216b7cfb6883a6af15c191a94953a0596b9c \ No newline at end of file diff --git a/src/main/java/runwar/options/SiteOptions.java b/src/main/java/runwar/options/SiteOptions.java index df79759e..6c4581b1 100644 --- a/src/main/java/runwar/options/SiteOptions.java +++ b/src/main/java/runwar/options/SiteOptions.java @@ -385,7 +385,7 @@ public SiteOptions errorPages(JSONObject errorpages) { for (String key : errorpages.keySet()) { String strCode = key.toString().trim(); - Integer code = strCode.toLowerCase() == "default" ? 1 : Integer.parseInt(strCode); + Integer code = strCode.toLowerCase().equals("default") ? 1 : Integer.parseInt(strCode); String location = errorpages.get(key).toString(); location = location.startsWith("/") ? location : "/" + location; this.errorPages.put(code, location); From 5341ae82cb44255ff7d3b0b9ba4d95f60494b9a2 Mon Sep 17 00:00:00 2001 From: Brad Wood Date: Wed, 6 Mar 2024 17:29:07 -0600 Subject: [PATCH 03/30] Fix for transfer min size and Java warnings --- src/main/java/runwar/Server.java | 9 +++------ src/main/java/runwar/options/ConfigParser.java | 2 +- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/src/main/java/runwar/Server.java b/src/main/java/runwar/Server.java index 4466b6cd..b6664ff1 100644 --- a/src/main/java/runwar/Server.java +++ b/src/main/java/runwar/Server.java @@ -10,7 +10,6 @@ import java.io.PrintStream; import java.io.PrintWriter; import java.lang.management.ManagementFactory; -import java.lang.reflect.Method; import java.net.ConnectException; import java.net.InetAddress; import java.net.InetSocketAddress; @@ -34,6 +33,8 @@ import org.xnio.Option; import org.xnio.OptionMap; +import com.apple.eawt.Application; + import io.undertow.Undertow; import io.undertow.server.DefaultByteBufferPool; import io.undertow.server.HandlerWrapper; @@ -258,11 +259,7 @@ public synchronized void startServer(final ServerOptions options) throws Excepti System.setProperty("apple.laf.useScreenMenuBar", "true"); System.setProperty("-Xdock:name", processName); try { - Class appClass = Class.forName("com.apple.eawt.Application"); - Method getAppMethod = appClass.getMethod("getApplication"); - Object appInstance = getAppMethod.invoke(null); - Method dockMethod = appInstance.getClass().getMethod("setDockIconImage", java.awt.Image.class); - dockMethod.invoke(appInstance, dockIcon); + Application.getApplication().setDockIconImage(dockIcon); } catch (Exception e) { LOG.warn(" Error setting dock icon image", e); } diff --git a/src/main/java/runwar/options/ConfigParser.java b/src/main/java/runwar/options/ConfigParser.java index c80e9684..0d6b7e96 100644 --- a/src/main/java/runwar/options/ConfigParser.java +++ b/src/main/java/runwar/options/ConfigParser.java @@ -420,7 +420,7 @@ private void parseOptions() { // issues as detailed here: // https://issues.redhat.com/browse/UNDERTOW-584 if (siteConfig.hasOption("sendFileMinSizeKB")) { - site.transferMinSize(Long.valueOf(siteConfig.getOptionValue("transferMinSize")) * 1024); + site.transferMinSize(Long.valueOf(siteConfig.getOptionValue("sendFileMinSizeKB")) * 1024); } if (siteConfig.hasOption("GZipEnable")) { From 34ef669222d1c38da6ad8791b8a79d66ba05a79c Mon Sep 17 00:00:00 2001 From: Brad Wood Date: Thu, 14 Mar 2024 21:22:59 -0500 Subject: [PATCH 04/30] Extra null check --- gradle/version | 2 +- src/main/java/runwar/undertow/SSLCertHeaderHandler.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/version b/gradle/version index 5035f4fd..49f6c427 100644 --- a/gradle/version +++ b/gradle/version @@ -1 +1 @@ -5.0.2-0069a6ce9838109e18344f96b4a1d97f322a260d-96ea216b7cfb6883a6af15c191a94953a0596b9c \ No newline at end of file +5.0.3-5341ae82cb44255ff7d3b0b9ba4d95f60494b9a2-96ea216b7cfb6883a6af15c191a94953a0596b9c \ No newline at end of file diff --git a/src/main/java/runwar/undertow/SSLCertHeaderHandler.java b/src/main/java/runwar/undertow/SSLCertHeaderHandler.java index 7dcefe60..b1db0487 100644 --- a/src/main/java/runwar/undertow/SSLCertHeaderHandler.java +++ b/src/main/java/runwar/undertow/SSLCertHeaderHandler.java @@ -94,7 +94,7 @@ public void handleRequest(HttpServerExchange exchange) throws Exception { setCGIElement( exchange, CERT_KEYSIZE, String.valueOf( ssl.getKeySize() ) ); // Set details of the server cert so it's in our CGI scope - if( ssl.getSSLSession().getLocalCertificates() != null ) { + if( ssl.getSSLSession() != null && ssl.getSSLSession().getLocalCertificates() != null ) { Certificate[] serverCerts = ssl.getSSLSession().getLocalCertificates(); if(serverCerts.length > 0 && serverCerts[0] instanceof X509Certificate ) { X509Certificate serverCert = (X509Certificate)serverCerts[0]; From b1e977f110e9aa75ded847cf03b4b836c79f410c Mon Sep 17 00:00:00 2001 From: Brad Wood Date: Fri, 15 Mar 2024 13:45:44 -0500 Subject: [PATCH 05/30] Roll back this "fix" as it doesn't always work --- gradle/version | 2 +- src/main/java/runwar/Server.java | 9 ++++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/gradle/version b/gradle/version index 49f6c427..8f0f5123 100644 --- a/gradle/version +++ b/gradle/version @@ -1 +1 @@ -5.0.3-5341ae82cb44255ff7d3b0b9ba4d95f60494b9a2-96ea216b7cfb6883a6af15c191a94953a0596b9c \ No newline at end of file +5.0.3-34ef669222d1c38da6ad8791b8a79d66ba05a79c-96ea216b7cfb6883a6af15c191a94953a0596b9c \ No newline at end of file diff --git a/src/main/java/runwar/Server.java b/src/main/java/runwar/Server.java index b6664ff1..d8dadd6b 100644 --- a/src/main/java/runwar/Server.java +++ b/src/main/java/runwar/Server.java @@ -13,6 +13,7 @@ import java.net.ConnectException; import java.net.InetAddress; import java.net.InetSocketAddress; +import java.lang.reflect.Method; import java.net.ServerSocket; import java.net.Socket; import java.net.SocketException; @@ -33,8 +34,6 @@ import org.xnio.Option; import org.xnio.OptionMap; -import com.apple.eawt.Application; - import io.undertow.Undertow; import io.undertow.server.DefaultByteBufferPool; import io.undertow.server.HandlerWrapper; @@ -259,7 +258,11 @@ public synchronized void startServer(final ServerOptions options) throws Excepti System.setProperty("apple.laf.useScreenMenuBar", "true"); System.setProperty("-Xdock:name", processName); try { - Application.getApplication().setDockIconImage(dockIcon); + Class appClass = Class.forName("com.apple.eawt.Application"); + Method getAppMethod = appClass.getMethod("getApplication"); + Object appInstance = getAppMethod.invoke(null); + Method dockMethod = appInstance.getClass().getMethod("setDockIconImage", java.awt.Image.class); + dockMethod.invoke(appInstance, dockIcon); } catch (Exception e) { LOG.warn(" Error setting dock icon image", e); } From 5c05cddf9fca7296707c473ea834c834d760067e Mon Sep 17 00:00:00 2001 From: Brad Wood Date: Thu, 23 May 2024 01:12:58 -0500 Subject: [PATCH 06/30] COMMANDBOX-1623 --- dependencies.gradle | 2 +- .../DecodedRequestPathAttribute.java | 95 +++++++++++++++++++ .../handler/FrameworkRewritesBuilder.java | 19 ++-- ...ndertow.attribute.ExchangeAttributeBuilder | 1 + 4 files changed, 107 insertions(+), 10 deletions(-) create mode 100644 src/main/java/runwar/undertow/attribute/DecodedRequestPathAttribute.java diff --git a/dependencies.gradle b/dependencies.gradle index f1146bb2..cc17b157 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -10,7 +10,7 @@ configurations { } ext { - undertowVersion = '2.2.28.Final' + undertowVersion = '2.2.32.Final' urlRewriteVersion = '5.0.2-SNAPSHOT' proguardVersion = '6.0.2' systemTrayVersion = '3.17' diff --git a/src/main/java/runwar/undertow/attribute/DecodedRequestPathAttribute.java b/src/main/java/runwar/undertow/attribute/DecodedRequestPathAttribute.java new file mode 100644 index 00000000..7a9f5d68 --- /dev/null +++ b/src/main/java/runwar/undertow/attribute/DecodedRequestPathAttribute.java @@ -0,0 +1,95 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package runwar.undertow.attribute; + +import runwar.undertow.RewriteMap; +import runwar.undertow.SiteDeployment; +import runwar.undertow.SiteDeploymentManager; + +import io.undertow.server.HttpServerExchange; +import io.undertow.attribute.ExchangeAttribute; +import io.undertow.attribute.ExchangeAttributeBuilder; +import io.undertow.attribute.ReadOnlyAttributeException; +import io.undertow.attribute.RequestPathAttribute; +import io.undertow.attribute.ExchangeAttributes; +import io.undertow.server.HandlerWrapper; +import io.undertow.server.HttpHandler; +import io.undertow.UndertowLogger; +import io.undertow.Handlers; +import io.undertow.server.handlers.builder.PredicatedHandler; +import io.undertow.server.handlers.builder.PredicatedHandlersParser; +import io.undertow.util.QueryParameterUtils; +import io.undertow.server.handlers.builder.HandlerBuilder; + +import java.util.Collections; +import java.util.Map; +import java.util.Set; +import java.util.List; + +import runwar.Server; + +/** + * A cookie + * + * @author Stuart Douglas + */ +public class DecodedRequestPathAttribute implements ExchangeAttribute { + + public static final String DECODED_REQUEST_PATH = "%{DECODED_REQUEST_PATH}"; + + public static final ExchangeAttribute INSTANCE = new DecodedRequestPathAttribute(); + + private DecodedRequestPathAttribute() { + + } + + @Override + public String readAttribute(final HttpServerExchange exchange) { + return exchange.getRequestPath(); + } + + @Override + public void writeAttribute(final HttpServerExchange exchange, final String newValue) + throws ReadOnlyAttributeException { + throw new ReadOnlyAttributeException(); + } + + @Override + public String toString() { + return DECODED_REQUEST_PATH; + } + + public static final class Builder implements ExchangeAttributeBuilder { + + @Override + public String name() { + return "Decoded Request Path"; + } + + @Override + public ExchangeAttribute build(final String token) { + return token.equals(DECODED_REQUEST_PATH) ? INSTANCE : null; + } + + @Override + public int priority() { + return 0; + } + } +} diff --git a/src/main/java/runwar/undertow/handler/FrameworkRewritesBuilder.java b/src/main/java/runwar/undertow/handler/FrameworkRewritesBuilder.java index 134a3e13..9ed8a4ea 100644 --- a/src/main/java/runwar/undertow/handler/FrameworkRewritesBuilder.java +++ b/src/main/java/runwar/undertow/handler/FrameworkRewritesBuilder.java @@ -28,15 +28,15 @@ public String name() { } public Map> parameters() { - return Collections.emptyMap(); + return Collections.emptyMap(); } public Set requiredParameters() { - return Collections.emptySet(); + return Collections.emptySet(); } public String defaultParameter() { - return null; + return null; } public HandlerWrapper build(final Map config) { @@ -45,12 +45,13 @@ public HandlerWrapper build(final Map config) { @Override public HttpHandler wrap(HttpHandler toWrap) { List ph = PredicatedHandlersParser.parse( - "not regex-nocase('^/(flex2gateway|flashservices/gateway|messagebroker|lucee|rest|cfide|CFIDE|cfformgateway|jrunscripts|cf_scripts|mapping-tag|CFFileServlet)/.*')" - + " and not path-prefix-nocase(/tuckey-status)" - + " and not path-nocase(/pms)" - + " and not path-nocase(/favicon.ico)" - + " and not is-file" - + " and not is-directory -> rewrite( '/index.cfm%{REQUEST_URL}' )", Server.getClassLoader()); + "not regex-nocase('^/(flex2gateway|flashservices/gateway|messagebroker|lucee|rest|cfide|CFIDE|cfformgateway|jrunscripts|cf_scripts|mapping-tag|CFFileServlet)/.*')" + + " and not path-prefix-nocase(/tuckey-status)" + + " and not path-nocase(/pms)" + + " and not path-nocase(/favicon.ico)" + + " and not is-file" + + " and not is-directory -> rewrite( '/index.cfm%{DECODED_REQUEST_PATH}' )", + Server.getClassLoader()); return Handlers.predicates(ph, toWrap); } }; diff --git a/src/main/resources/META-INF/services/io.undertow.attribute.ExchangeAttributeBuilder b/src/main/resources/META-INF/services/io.undertow.attribute.ExchangeAttributeBuilder index 5dd63d36..d4d84614 100644 --- a/src/main/resources/META-INF/services/io.undertow.attribute.ExchangeAttributeBuilder +++ b/src/main/resources/META-INF/services/io.undertow.attribute.ExchangeAttributeBuilder @@ -1 +1,2 @@ runwar.undertow.attribute.MapAttribute$Builder +runwar.undertow.attribute.DecodedRequestPathAttribute$Builder \ No newline at end of file From 7f511aa03c868d166b477f91772af24951f02dde Mon Sep 17 00:00:00 2001 From: Brad Wood Date: Fri, 28 Jun 2024 12:11:05 -0500 Subject: [PATCH 07/30] COMMANDBOX-1625 --- src/main/java/runwar/undertow/WelcomeFileHandler.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/java/runwar/undertow/WelcomeFileHandler.java b/src/main/java/runwar/undertow/WelcomeFileHandler.java index 161a9a67..9531f337 100644 --- a/src/main/java/runwar/undertow/WelcomeFileHandler.java +++ b/src/main/java/runwar/undertow/WelcomeFileHandler.java @@ -8,6 +8,8 @@ import io.undertow.server.handlers.resource.Resource; import io.undertow.server.handlers.resource.ResourceManager; import io.undertow.util.CanonicalPathUtils; +import io.undertow.util.RedirectBuilder; +import io.undertow.util.StatusCodes; public class WelcomeFileHandler implements HttpHandler { @@ -24,7 +26,7 @@ public class WelcomeFileHandler implements HttpHandler { @Override public void handleRequest(final HttpServerExchange exchange) throws Exception { Resource resource = resourceManager.getResource(canonicalize(exchange.getRelativePath())); - if (resource != null && resource.isDirectory()) { + if (resource != null && resource.isDirectory() && exchange.getRelativePath().endsWith("/")) { Resource indexResource = getIndexFiles(exchange, resourceManager, resource.getPath(), welcomeFiles); if (indexResource != null) { String newPath = indexResource.getPath(); From d3416fac056638692c8e25a15b861fc2e9c1c255 Mon Sep 17 00:00:00 2001 From: Brad Wood Date: Fri, 28 Jun 2024 12:12:37 -0500 Subject: [PATCH 08/30] COMMANDBOX-1626 --- dependencies.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dependencies.gradle b/dependencies.gradle index cc17b157..8ab8c8ac 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -10,7 +10,7 @@ configurations { } ext { - undertowVersion = '2.2.32.Final' + undertowVersion = '2.2.33.Final' urlRewriteVersion = '5.0.2-SNAPSHOT' proguardVersion = '6.0.2' systemTrayVersion = '3.17' From 90fc10f1ae0aaf4d8e0240ccc92d13578a1874bb Mon Sep 17 00:00:00 2001 From: Brad Wood Date: Fri, 28 Jun 2024 12:15:52 -0500 Subject: [PATCH 09/30] COMMANDBOX-1627 --- src/main/java/runwar/options/SiteOptions.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/runwar/options/SiteOptions.java b/src/main/java/runwar/options/SiteOptions.java index 6c4581b1..7f313b1f 100644 --- a/src/main/java/runwar/options/SiteOptions.java +++ b/src/main/java/runwar/options/SiteOptions.java @@ -86,7 +86,7 @@ public class SiteOptions { private final Map mimeTypes = new HashMap(); - private String servletPassPredicate = "regex( '^/(.+?\\.cf[cm])(/.*)?$' )"; + private String servletPassPredicate = "regex( '^/(.+?\\.cf[cms])(/.*)?$' ) or regex( '^/(.+?\\.bx[sm]{0,1})(/.*)?$' )"; public ServerOptions getServerOptions() { return serverOptions; From 06436d92c6c3f75695cb1dbba7d189b8cb227d47 Mon Sep 17 00:00:00 2001 From: Brad Wood Date: Fri, 28 Jun 2024 17:04:07 -0500 Subject: [PATCH 10/30] Update ci.yml --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9ccc4993..26aab2c3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -34,7 +34,7 @@ jobs: uses: actions/setup-java@v2 with: java-version: '11' - distribution: 'adopt' + distribution: 'temurin' # Do the build - name: Build with Gradle From 0cd61a54e6b59871869c74c64bb27ccf5ee126a9 Mon Sep 17 00:00:00 2001 From: Brad Wood Date: Fri, 28 Jun 2024 17:16:48 -0500 Subject: [PATCH 11/30] Update ci.yml --- .github/workflows/ci.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 26aab2c3..2e1b56a6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -36,6 +36,12 @@ jobs: java-version: '11' distribution: 'temurin' + - name: Print JAVA_HOME + run: echo $JAVA_HOME + + - name: Check Java version + run: java -version + # Do the build - name: Build with Gradle run: ./gradlew --refresh-dependencies From 5e6ac2af5ba081a262508c80c4850c9b4ea8bde2 Mon Sep 17 00:00:00 2001 From: Brad Wood Date: Fri, 28 Jun 2024 17:19:42 -0500 Subject: [PATCH 12/30] Update ci.yml --- .github/workflows/ci.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2e1b56a6..9d1da181 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -41,7 +41,10 @@ jobs: - name: Check Java version run: java -version - + + - name: Configure Gradle to use the setup JDK + run: echo "org.gradle.java.home=$JAVA_HOME" >> $GITHUB_WORKSPACE/gradle.properties + # Do the build - name: Build with Gradle run: ./gradlew --refresh-dependencies From b491fc8ee0b6073c178477d33fc77d62f82db1a6 Mon Sep 17 00:00:00 2001 From: Brad Wood Date: Fri, 28 Jun 2024 17:22:10 -0500 Subject: [PATCH 13/30] Update ci.yml --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9d1da181..2669cc9d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -43,7 +43,7 @@ jobs: run: java -version - name: Configure Gradle to use the setup JDK - run: echo "org.gradle.java.home=$JAVA_HOME" >> $GITHUB_WORKSPACE/gradle.properties + run: echo "org.gradle.java.home=$JAVA_HOME" >> ~/gradle.properties # Do the build - name: Build with Gradle From c8b1fdc03693212c50d92702223564caf5cea4a7 Mon Sep 17 00:00:00 2001 From: Brad Wood Date: Fri, 28 Jun 2024 17:27:20 -0500 Subject: [PATCH 14/30] Update ci.yml --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2669cc9d..90f5dc4e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -43,7 +43,7 @@ jobs: run: java -version - name: Configure Gradle to use the setup JDK - run: echo "org.gradle.java.home=$JAVA_HOME" >> ~/gradle.properties + run: echo "org.gradle.java.home=$JAVA_HOME" >> ~/.gradle/gradle.properties # Do the build - name: Build with Gradle From 67c84a6486cf909f48c3f146f79cb21e7b0a9478 Mon Sep 17 00:00:00 2001 From: Brad Wood Date: Fri, 28 Jun 2024 17:29:23 -0500 Subject: [PATCH 15/30] Update ci.yml --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 90f5dc4e..b9ee1de9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -43,7 +43,7 @@ jobs: run: java -version - name: Configure Gradle to use the setup JDK - run: echo "org.gradle.java.home=$JAVA_HOME" >> ~/.gradle/gradle.properties + run: touch ~/.gradle/gradle.properties && echo "org.gradle.java.home=$JAVA_HOME" >> ~/.gradle/gradle.properties # Do the build - name: Build with Gradle From 46505b89235294d5ac1ff2b95697c36ef39090a1 Mon Sep 17 00:00:00 2001 From: Brad Wood Date: Fri, 28 Jun 2024 17:32:20 -0500 Subject: [PATCH 16/30] Update ci.yml --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b9ee1de9..85ce1207 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -43,7 +43,7 @@ jobs: run: java -version - name: Configure Gradle to use the setup JDK - run: touch ~/.gradle/gradle.properties && echo "org.gradle.java.home=$JAVA_HOME" >> ~/.gradle/gradle.properties + run: mkdir ~/.gradle/ && echo "org.gradle.java.home=$JAVA_HOME" >> ~/.gradle/gradle.properties # Do the build - name: Build with Gradle From 1b3a7edb1bd6006ce94047900ecb39a10a633997 Mon Sep 17 00:00:00 2001 From: Brad Wood Date: Fri, 28 Jun 2024 17:35:10 -0500 Subject: [PATCH 17/30] Update ci.yml --- .github/workflows/ci.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 85ce1207..36db5f4b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -44,7 +44,10 @@ jobs: - name: Configure Gradle to use the setup JDK run: mkdir ~/.gradle/ && echo "org.gradle.java.home=$JAVA_HOME" >> ~/.gradle/gradle.properties - + + - name: Print Gradle properties + run: cat ~/.gradle/gradle.properties + # Do the build - name: Build with Gradle run: ./gradlew --refresh-dependencies From 99edf2c3263057dfc54558a2d54a78b3eceb3c84 Mon Sep 17 00:00:00 2001 From: Brad Wood Date: Fri, 28 Jun 2024 17:36:50 -0500 Subject: [PATCH 18/30] Update ci.yml --- .github/workflows/ci.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 36db5f4b..d8db5bee 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -47,7 +47,12 @@ jobs: - name: Print Gradle properties run: cat ~/.gradle/gradle.properties - + + - name: Create symlink to Java home + run: | + mkdir -p gradle/jvm/11 + ln -s $JAVA_HOME gradle/jvm/11/latest + # Do the build - name: Build with Gradle run: ./gradlew --refresh-dependencies From addb745c75852606ee7fa31062cf424b8582b429 Mon Sep 17 00:00:00 2001 From: Brad Wood Date: Fri, 28 Jun 2024 17:38:18 -0500 Subject: [PATCH 19/30] Update ci.yml --- .github/workflows/ci.yml | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d8db5bee..cd7b8c96 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -35,18 +35,6 @@ jobs: with: java-version: '11' distribution: 'temurin' - - - name: Print JAVA_HOME - run: echo $JAVA_HOME - - - name: Check Java version - run: java -version - - - name: Configure Gradle to use the setup JDK - run: mkdir ~/.gradle/ && echo "org.gradle.java.home=$JAVA_HOME" >> ~/.gradle/gradle.properties - - - name: Print Gradle properties - run: cat ~/.gradle/gradle.properties - name: Create symlink to Java home run: | From 06bab10df910f6e01ed290108762a532f44f3f7e Mon Sep 17 00:00:00 2001 From: Brad Wood Date: Mon, 1 Jul 2024 16:15:45 -0500 Subject: [PATCH 20/30] COMMANDBOX-1625 --- src/main/java/runwar/undertow/WelcomeFileHandler.java | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/main/java/runwar/undertow/WelcomeFileHandler.java b/src/main/java/runwar/undertow/WelcomeFileHandler.java index 9531f337..4e12e638 100644 --- a/src/main/java/runwar/undertow/WelcomeFileHandler.java +++ b/src/main/java/runwar/undertow/WelcomeFileHandler.java @@ -10,6 +10,7 @@ import io.undertow.util.CanonicalPathUtils; import io.undertow.util.RedirectBuilder; import io.undertow.util.StatusCodes; +import io.undertow.util.Headers; public class WelcomeFileHandler implements HttpHandler { @@ -26,7 +27,14 @@ public class WelcomeFileHandler implements HttpHandler { @Override public void handleRequest(final HttpServerExchange exchange) throws Exception { Resource resource = resourceManager.getResource(canonicalize(exchange.getRelativePath())); - if (resource != null && resource.isDirectory() && exchange.getRelativePath().endsWith("/")) { + if (resource != null && resource.isDirectory()) { + if (!exchange.getRequestPath().endsWith("/")) { + exchange.setStatusCode(StatusCodes.FOUND); + exchange.getResponseHeaders().put(Headers.LOCATION, + RedirectBuilder.redirect(exchange, exchange.getRelativePath() + "/", true)); + exchange.endExchange(); + return; + } Resource indexResource = getIndexFiles(exchange, resourceManager, resource.getPath(), welcomeFiles); if (indexResource != null) { String newPath = indexResource.getPath(); From ba383f44e79f5b395c948296c2f499bdce392cb6 Mon Sep 17 00:00:00 2001 From: Brad Wood Date: Sat, 13 Jul 2024 23:26:48 -0500 Subject: [PATCH 21/30] COMMANDBOX-1629 --- gradle/version | 2 +- .../predicate/IsDirectoryPredicate.java | 27 ++++-- .../undertow/predicate/IsFilePredicate.java | 83 +++++++++++++++---- 3 files changed, 89 insertions(+), 23 deletions(-) diff --git a/gradle/version b/gradle/version index 8f0f5123..ff05b919 100644 --- a/gradle/version +++ b/gradle/version @@ -1 +1 @@ -5.0.3-34ef669222d1c38da6ad8791b8a79d66ba05a79c-96ea216b7cfb6883a6af15c191a94953a0596b9c \ No newline at end of file +5.0.4-06bab10df910f6e01ed290108762a532f44f3f7e-96ea216b7cfb6883a6af15c191a94953a0596b9c \ No newline at end of file diff --git a/src/main/java/runwar/undertow/predicate/IsDirectoryPredicate.java b/src/main/java/runwar/undertow/predicate/IsDirectoryPredicate.java index c94534d0..e9ac7e66 100644 --- a/src/main/java/runwar/undertow/predicate/IsDirectoryPredicate.java +++ b/src/main/java/runwar/undertow/predicate/IsDirectoryPredicate.java @@ -18,6 +18,7 @@ package runwar.undertow.predicate; +import io.undertow.UndertowLogger; import io.undertow.attribute.ExchangeAttribute; import io.undertow.attribute.ExchangeAttributes; import io.undertow.predicate.Predicate; @@ -25,6 +26,7 @@ import io.undertow.server.HttpServerExchange; import io.undertow.server.handlers.resource.Resource; import io.undertow.server.handlers.resource.ResourceManager; +import io.undertow.UndertowLogger; import java.io.IOException; import java.util.Collections; @@ -37,12 +39,19 @@ /** * Predicate that returns true if the given location corresponds to a directory. - * This is a copy of the servlet version, but uses a resource manager attached to the exchange + * This is a copy of the servlet version, but uses a resource manager attached + * to the exchange * * @author Stuart Douglas, Brad Wood */ public class IsDirectoryPredicate implements Predicate { + private static final boolean traceEnabled; + + static { + traceEnabled = UndertowLogger.PREDICATE_LOGGER.isTraceEnabled(); + } + private final ExchangeAttribute location; public IsDirectoryPredicate(final ExchangeAttribute location) { @@ -54,15 +63,23 @@ public boolean resolve(final HttpServerExchange exchange) { String location = this.location.readAttribute(exchange); SiteDeployment deployment = exchange.getAttachment(SiteDeploymentManager.SITE_DEPLOYMENT_KEY); - if(deployment == null) { - throw new RuntimeException( "is-directory predicate could not access the site deployment on this exchange" ); + if (deployment == null) { + throw new RuntimeException("is-directory predicate could not access the site deployment on this exchange"); } ResourceManager manager = deployment.getResourceManager(); try { Resource resource = manager.getResource(location); - if(resource == null) { + if (resource == null) { + if (traceEnabled) { + UndertowLogger.PREDICATE_LOGGER.tracef( + "is-directory check of [%s] returned [null], so directory does not exist.", location); + } return false; } + if (traceEnabled) { + UndertowLogger.PREDICATE_LOGGER.tracef("is-directory check of [%s] returned %s.", location, + resource.isDirectory()); + } return resource.isDirectory(); } catch (IOException e) { throw new RuntimeException(e); @@ -101,7 +118,7 @@ public String defaultParameter() { @Override public Predicate build(final Map config) { ExchangeAttribute value = (ExchangeAttribute) config.get("value"); - if(value == null) { + if (value == null) { value = ExchangeAttributes.relativePath(); } return new IsDirectoryPredicate(value); diff --git a/src/main/java/runwar/undertow/predicate/IsFilePredicate.java b/src/main/java/runwar/undertow/predicate/IsFilePredicate.java index f25c5dd3..04b2cc90 100644 --- a/src/main/java/runwar/undertow/predicate/IsFilePredicate.java +++ b/src/main/java/runwar/undertow/predicate/IsFilePredicate.java @@ -18,6 +18,7 @@ package runwar.undertow.predicate; +import io.undertow.UndertowLogger; import io.undertow.attribute.ExchangeAttribute; import io.undertow.attribute.ExchangeAttributes; import io.undertow.predicate.Predicate; @@ -37,12 +38,19 @@ import runwar.undertow.SiteDeploymentManager; /** - * Predicate that returns true if the given location corresponds to a regular file. + * Predicate that returns true if the given location corresponds to a regular + * file. * * @author Stuart Douglas */ public class IsFilePredicate implements Predicate { + private static final boolean traceEnabled; + + static { + traceEnabled = UndertowLogger.PREDICATE_LOGGER.isTraceEnabled(); + } + private final ExchangeAttribute location; private final boolean requireContent; @@ -60,28 +68,69 @@ public boolean resolve(final HttpServerExchange exchange) { String location = this.location.readAttribute(exchange); SiteDeployment deployment = exchange.getAttachment(SiteDeploymentManager.SITE_DEPLOYMENT_KEY); - if(deployment == null) { - throw new RuntimeException( "is-file predicate could not access the site deployment on this exchange" ); + if (deployment == null) { + throw new RuntimeException("is-file predicate could not access the site deployment on this exchange"); } ResourceManager manager = deployment.getResourceManager(); try { - Resource resource = manager.getResource(location); - if(resource == null) { - return false; - } - if(resource.isDirectory()) { - return false; - } - if(requireContent){ - return resource.getContentLength() != null && resource.getContentLength() > 0; - } else { - return true; - } + return resolveInternal(exchange, manager, location); } catch (IOException e) { throw new RuntimeException(e); } } + private void logTrace(String format, Object... args) { + if (traceEnabled) { + UndertowLogger.PREDICATE_LOGGER.tracef(format, args); + } + } + + /** + * internal recursive method to resolve the file. If not found, we strip path + * segments until we find a file or run out of path + * This allows us to ignore path info which the servlet will strip off such as + * foo/index.cfm/bar/baz + * + * @param exchange The exchange + * @param manager The resource manager + * @param location The location + * @return true if the location is a file + * @throws IOException + */ + private boolean resolveInternal(HttpServerExchange exchange, ResourceManager manager, String location) + throws IOException { + + logTrace("is-file checking path [%s] ...", location); + Resource resource = manager.getResource(location); + if (resource == null) { + // /a/b is min for 2 segments + if (location.length() > 3) { + // remove any trailing slash + if (location.endsWith("/")) { + location = location.substring(0, location.length() - 1); + } + int lastSlashIndex = location.lastIndexOf('/'); + if (lastSlashIndex > 0) { // More than one character in the path, not counting a leading slash + return resolveInternal(exchange, manager, location.substring(0, lastSlashIndex)); + } + } + logTrace("is-file check of [%s] returned [null], so file does not exist.", location); + return false; + } + if (resource.isDirectory()) { + logTrace("is-file check of [%s] returned false because path is a directory.", location); + return false; + } + if (requireContent) { + boolean result = resource.getContentLength() != null && resource.getContentLength() > 0; + logTrace("is-file check of [%s] and content length > 0 check returned %s.", location, result); + return result; + } else { + logTrace("is-file check of [%s] returned true.", location); + return true; + } + } + @Override public String toString() { return "is-file( " + location.toString() + " )"; @@ -115,8 +164,8 @@ public String defaultParameter() { @Override public Predicate build(final Map config) { ExchangeAttribute value = (ExchangeAttribute) config.get("value"); - Boolean requireContent = (Boolean)config.get("require-content"); - if(value == null) { + Boolean requireContent = (Boolean) config.get("require-content"); + if (value == null) { value = ExchangeAttributes.relativePath(); } return new IsFilePredicate(value, requireContent == null ? false : requireContent); From 0de55e3b7a2f4c8dd5b33ef11edc7a90a39506e7 Mon Sep 17 00:00:00 2001 From: Brad Wood Date: Sat, 13 Jul 2024 23:36:23 -0500 Subject: [PATCH 22/30] COMMANDBOX-1630 --- gradle/version | 2 +- src/main/java/runwar/RunwarConfigurer.java | 37 ++-- .../java/runwar/options/ServerOptions.java | 2 + .../undertow/SiteDeploymentManager.java | 197 +++++++++++------- 4 files changed, 139 insertions(+), 99 deletions(-) diff --git a/gradle/version b/gradle/version index ff05b919..36e508cb 100644 --- a/gradle/version +++ b/gradle/version @@ -1 +1 @@ -5.0.4-06bab10df910f6e01ed290108762a532f44f3f7e-96ea216b7cfb6883a6af15c191a94953a0596b9c \ No newline at end of file +5.0.5-06bab10df910f6e01ed290108762a532f44f3f7e-96ea216b7cfb6883a6af15c191a94953a0596b9c \ No newline at end of file diff --git a/src/main/java/runwar/RunwarConfigurer.java b/src/main/java/runwar/RunwarConfigurer.java index 431ac227..2e6fabb1 100644 --- a/src/main/java/runwar/RunwarConfigurer.java +++ b/src/main/java/runwar/RunwarConfigurer.java @@ -1,5 +1,21 @@ package runwar; +import static io.undertow.Handlers.predicate; +import static runwar.logging.RunwarLogger.LOG; + +import java.io.File; +import java.io.IOException; +import java.net.URL; +import java.util.ArrayList; +import java.util.BitSet; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Arrays; + +import javax.servlet.DispatcherType; +import javax.servlet.Filter; + import io.undertow.predicate.Predicates; import io.undertow.server.handlers.cache.CacheHandler; import io.undertow.server.handlers.cache.DirectBufferCache; @@ -8,25 +24,8 @@ import io.undertow.servlet.handlers.DefaultServlet; import io.undertow.util.MimeMappings; import runwar.options.ServerOptions; -import runwar.security.SelfSignedCertificate; import runwar.undertow.WebXMLParser; -import javax.servlet.DispatcherType; -import javax.servlet.Filter; -import javax.servlet.Servlet; -import java.io.File; -import java.io.IOException; -import java.net.URL; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.security.GeneralSecurityException; -import java.util.*; -import java.util.BitSet; - -import static io.undertow.Handlers.predicate; -import static io.undertow.servlet.Servlets.servlet; -import static runwar.logging.RunwarLogger.LOG; - public class RunwarConfigurer { private static ServerOptions serverOptions; @@ -189,7 +188,9 @@ private void configurePathInfoFilter(DeploymentInfo servletBuilder) throws Class regexPathInfoFilter = (Class) Server.class.getClassLoader() .loadClass("org.cfmlprojects.regexpathinfofilter.RegexPathInfoFilter"); } - servletBuilder.addFilter(new FilterInfo("RegexPathInfoFilter", regexPathInfoFilter)); + FilterInfo filterInfo = new FilterInfo("RegexPathInfoFilter", regexPathInfoFilter); + filterInfo.addInitParam("regex", "^(/.+?\\.cf[cm]|/.+?\\.bx[sm])(/.*)"); + servletBuilder.addFilter(filterInfo); servletBuilder.addFilterUrlMapping("RegexPathInfoFilter", "/*", DispatcherType.REQUEST); servletBuilder.addFilterUrlMapping("RegexPathInfoFilter", "/*", DispatcherType.FORWARD); } diff --git a/src/main/java/runwar/options/ServerOptions.java b/src/main/java/runwar/options/ServerOptions.java index 46684252..f7836b4d 100644 --- a/src/main/java/runwar/options/ServerOptions.java +++ b/src/main/java/runwar/options/ServerOptions.java @@ -834,6 +834,8 @@ public ServerOptions cfEngineName(String cfengineName) { this.cfengineName = "adobe"; } else if (cfengineName.toLowerCase().contains("railo")) { this.cfengineName = "railo"; + } else if (cfengineName.toLowerCase().contains("boxlang")) { + this.cfengineName = "boxlang"; } else if (cfengineName.equals("")) { this.cfengineName = ""; } else { diff --git a/src/main/java/runwar/undertow/SiteDeploymentManager.java b/src/main/java/runwar/undertow/SiteDeploymentManager.java index 065849e3..1d73ca38 100644 --- a/src/main/java/runwar/undertow/SiteDeploymentManager.java +++ b/src/main/java/runwar/undertow/SiteDeploymentManager.java @@ -1,6 +1,5 @@ package runwar.undertow; - import io.undertow.Handlers; import io.undertow.Undertow; import io.undertow.Undertow.Builder; @@ -90,151 +89,189 @@ import static runwar.logging.RunwarLogger.LOG; import static runwar.logging.RunwarLogger.MAPPER_LOG; -@SuppressWarnings( "deprecation" ) +@SuppressWarnings("deprecation") public class SiteDeploymentManager { public static final AttachmentKey DEPLOYMENT_KEY = AttachmentKey.create(String.class); public static final AttachmentKey SITE_DEPLOYMENT_KEY = AttachmentKey.create(SiteDeployment.class); - private ConcurrentHashMap deployments = new ConcurrentHashMap(); - private SiteDeployment adobeDefaultDeployment=null; + private ConcurrentHashMap deployments = new ConcurrentHashMap(); + private SiteDeployment adobeDefaultDeployment = null; private ServerOptions serverOptions; - public SiteDeploymentManager( ServerOptions serverOptions ) throws Exception { + public SiteDeploymentManager(ServerOptions serverOptions) throws Exception { this.serverOptions = serverOptions; } - public ConcurrentHashMap getDeployments() { + public ConcurrentHashMap getDeployments() { return deployments; } - public synchronized SiteDeployment createSiteDeployment( DeploymentInfo servletBuilder, File webroot, RunwarConfigurer configurer, String deploymentKey, String vDirs, SiteOptions siteOptions ) throws Exception { - SiteDeployment deployment; + public synchronized SiteDeployment createSiteDeployment(DeploymentInfo servletBuilder, File webroot, + RunwarConfigurer configurer, String deploymentKey, String vDirs, SiteOptions siteOptions) throws Exception { + SiteDeployment deployment; - // If another thread already created this deployment - if( ( deployment = deployments.get( deploymentKey ) ) != null ) { - return 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() + "]." ); - } + if (deployments.size() > serverOptions.autoCreateContextsMax()) { + throw new MaxContextsException("Cannot create new servlet deployment. The configured max is [" + + serverOptions.autoCreateContextsMax() + "]."); + } - LOG.info("Creating deployment [" + deploymentKey + "]" ); + LOG.info("Creating deployment [" + deploymentKey + "]"); - File webInfDir = serverOptions.webInfDir(); - Long transferMinSize= siteOptions.transferMinSize(); - Map aliases = new HashMap<>(); - siteOptions.aliases().forEach((s, s2) -> aliases.put(s,Paths.get(s2))); + File webInfDir = serverOptions.webInfDir(); + Long transferMinSize = siteOptions.transferMinSize(); + Map aliases = new HashMap<>(); + siteOptions.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] ) ); - } - } - } + 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 < aVDirs.length; i++) { + String[] dirParts = aVDirs[i].split(","); + if (dirParts.length == 2 && dirParts[0].length() > 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, siteOptions); try { - Server.setCurrentDeploymentKey( deploymentKey ); + Server.setCurrentDeploymentKey(deploymentKey); // For non=Adobe (Lucee), create actual servlet context - if( serverOptions.cfEngineName().toLowerCase().indexOf("adobe") == -1 ) { + if (serverOptions.cfEngineName().toLowerCase().indexOf("adobe") == -1 + && serverOptions.cfEngineName().toLowerCase().indexOf("boxlang") == -1) { servletBuilder.setResourceManager(resourceManager); DeploymentManager manager = defaultContainer().addDeployment(servletBuilder); manager.deploy(); - deployment = new SiteDeployment( manager.start(), manager, siteOptions, serverOptions, resourceManager ); - LOG.debug(" New servlet context created for [" + deploymentKey + "]" ); - // For Adobe + deployment = new SiteDeployment(manager.start(), manager, siteOptions, serverOptions, resourceManager); + LOG.debug(" New servlet context created for [" + deploymentKey + "]"); + // For Adobe/boxlang } else { // For first deployment, create initial resource manager and deploy - if( deployments.size() == 0 ) { - ResourceManager hostResourceManager = new HostResourceManager( resourceManager ); - servletBuilder.setResourceManager( hostResourceManager ); + if (deployments.size() == 0) { + ResourceManager hostResourceManager = new HostResourceManager(resourceManager); + servletBuilder.setResourceManager(hostResourceManager); DeploymentManager manager = defaultContainer().addDeployment(servletBuilder); manager.deploy(); - deployment = new SiteDeployment( manager.start(), manager, siteOptions, serverOptions, hostResourceManager ); + deployment = new SiteDeployment(manager.start(), manager, siteOptions, serverOptions, + hostResourceManager); this.adobeDefaultDeployment = deployment; - LOG.debug(" Initial servlet context created for [" + deploymentKey + "]" ); + LOG.debug(" Initial servlet context created for [" + deploymentKey + "]"); - // For all subsequent deploys, reuse default deployment and simply add new resource manager + // For all subsequent deploys, reuse default deployment and simply add new + // resource manager } else { - ((HostResourceManager)servletBuilder.getResourceManager()).addResourceManager( deploymentKey, resourceManager ); - // Create a new deployment and site handler chain that calls the same servlet initial handler - deployment = new SiteDeployment( this.adobeDefaultDeployment.getServletInitialHandler(), this.adobeDefaultDeployment.getDeploymentManager(), siteOptions, serverOptions, this.adobeDefaultDeployment.getResourceManager() ); - LOG.debug(" Cloned servlet context added for deployment [" + deploymentKey + "]" ); + ((HostResourceManager) servletBuilder.getResourceManager()).addResourceManager(deploymentKey, + resourceManager); + // Create a new deployment and site handler chain that calls the same servlet + // initial handler + deployment = new SiteDeployment(this.adobeDefaultDeployment.getServletInitialHandler(), + this.adobeDefaultDeployment.getDeploymentManager(), siteOptions, serverOptions, + this.adobeDefaultDeployment.getResourceManager()); + LOG.debug(" Cloned servlet context added for deployment [" + deploymentKey + "]"); } } } finally { - Server.setCurrentDeploymentKey( null ); + Server.setCurrentDeploymentKey(null); } - deployments.put(deploymentKey, deployment); + deployments.put(deploymentKey, deployment); - return deployment; + return deployment; } - public ResourceManager getResourceManager(File warFile, Long transferMinSize, Map aliases, File internalCFMLServerRoot, SiteOptions siteOptions) { - Boolean cached = siteOptions.cacheServletPaths(); + public ResourceManager getResourceManager(File warFile, Long transferMinSize, Map aliases, + File internalCFMLServerRoot, SiteOptions siteOptions) { + Boolean cached = siteOptions.cacheServletPaths(); - LOG.debugf(" Initialized " + ( cached ? "CACHED " : "" ) + "MappedResourceManager" ); - LOG.infof(" Web Root: %s", warFile.getAbsolutePath() ); - if( aliases.size() > 0 ) { - LOG.debugf(" Aliases: %s", aliases ); + LOG.debugf(" Initialized " + (cached ? "CACHED " : "") + "MappedResourceManager"); + LOG.infof(" Web Root: %s", warFile.getAbsolutePath()); + if (aliases.size() > 0) { + LOG.debugf(" Aliases: %s", aliases); } - MappedResourceManager mappedResourceManager = new MappedResourceManager(warFile, transferMinSize, aliases, internalCFMLServerRoot, siteOptions); - if ( !cached ) { + MappedResourceManager mappedResourceManager = new MappedResourceManager(warFile, transferMinSize, aliases, + internalCFMLServerRoot, siteOptions); + if (!cached) { return mappedResourceManager; } - LOG.debugf(" ResourceManager Cache total size: %s MB", siteOptions.fileCacheTotalSizeMB() ); - LOG.debugf(" ResourceManager Cache max file size: %s KB", siteOptions.fileCacheMaxFileSizeKB() ); + LOG.debugf(" ResourceManager Cache total size: %s MB", siteOptions.fileCacheTotalSizeMB()); + LOG.debugf(" ResourceManager Cache max file size: %s KB", siteOptions.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 + // 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 + /* + * 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 = siteOptions.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. */ + /* + * 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 = siteOptions.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 + // 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 ) { + 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); + 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); + 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); + 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); } } From f80572aee93481d1fab90950411ee51e97a9255c Mon Sep 17 00:00:00 2001 From: Brad Wood Date: Thu, 26 Sep 2024 23:14:42 -0500 Subject: [PATCH 23/30] COMMANDBOX-1642 --- gradle/version | 2 +- .../java/runwar/options/ConfigParser.java | 20 + src/main/java/runwar/options/SiteOptions.java | 32 +- .../java/runwar/undertow/SiteDeployment.java | 15 +- .../runwar/undertow/WebsocketHandler.java | 101 +++ .../undertow/WebsocketReceiveListener.java | 588 ++++++++++++++++++ 6 files changed, 754 insertions(+), 4 deletions(-) create mode 100644 src/main/java/runwar/undertow/WebsocketHandler.java create mode 100644 src/main/java/runwar/undertow/WebsocketReceiveListener.java diff --git a/gradle/version b/gradle/version index 36e508cb..79d716e6 100644 --- a/gradle/version +++ b/gradle/version @@ -1 +1 @@ -5.0.5-06bab10df910f6e01ed290108762a532f44f3f7e-96ea216b7cfb6883a6af15c191a94953a0596b9c \ No newline at end of file +5.0.6-0de55e3b7a2f4c8dd5b33ef11edc7a90a39506e7-96ea216b7cfb6883a6af15c191a94953a0596b9c \ No newline at end of file diff --git a/src/main/java/runwar/options/ConfigParser.java b/src/main/java/runwar/options/ConfigParser.java index 0d6b7e96..80a04161 100644 --- a/src/main/java/runwar/options/ConfigParser.java +++ b/src/main/java/runwar/options/ConfigParser.java @@ -296,6 +296,26 @@ private void parseOptions() { site.webroot(getFile(siteConfig.getOptionValue("webroot"))); + if (siteConfig.hasOption("webSocketEnable")) { + site.webSocketEnable(siteConfig.getOptionBoolean("webSocketEnable")); + } + + if (siteConfig.hasOption("webSocketURI")) { + String webSocketURI = siteConfig.getOptionValue("webSocketURI"); + if (!webSocketURI.startsWith("/")) { + webSocketURI = "/" + webSocketURI; + } + site.webSocketURI(webSocketURI); + } + + if (siteConfig.hasOption("webSocketListener")) { + String webSocketListener = siteConfig.getOptionValue("webSocketListener"); + if (!webSocketListener.startsWith("/")) { + webSocketListener = "/" + webSocketListener; + } + site.webSocketListener(webSocketListener); + } + if (siteConfig.hasOption("servletPassPredicate")) { site.servletPassPredicate(siteConfig.getOptionValue("servletPassPredicate")); } diff --git a/src/main/java/runwar/options/SiteOptions.java b/src/main/java/runwar/options/SiteOptions.java index 7f313b1f..95963d6b 100644 --- a/src/main/java/runwar/options/SiteOptions.java +++ b/src/main/java/runwar/options/SiteOptions.java @@ -12,11 +12,12 @@ public class SiteOptions { private ServerOptions serverOptions; - private String host = "127.0.0.1", logAccessBaseFileName = "access", cfmlDirs, siteName = "default"; + private String host = "127.0.0.1", logAccessBaseFileName = "access", cfmlDirs, siteName = "default", + webSocketURI = "/ws", webSocketListener = "/WebSocket.cfc"; private int portNumber = 8088, ajpPort = 8009, sslPort = 1443; - private boolean enableAJP = false, enableSSL = false, enableHTTP = true; + private boolean enableAJP = false, enableSSL = false, enableHTTP = true, webSocketEnable = false; private boolean directoryListingEnable = true, logAccessEnable = false; @@ -97,6 +98,33 @@ public SiteOptions setServerOptions(ServerOptions serverOptions) { return this; } + public String webSocketURI() { + return webSocketURI; + } + + public SiteOptions webSocketURI(String webSocketURI) { + this.webSocketURI = webSocketURI; + return this; + } + + public String webSocketListener() { + return webSocketListener; + } + + public SiteOptions webSocketListener(String webSocketListener) { + this.webSocketListener = webSocketListener; + return this; + } + + public boolean webSocketEnable() { + return webSocketEnable; + } + + public SiteOptions webSocketEnable(boolean webSocketEnable) { + this.webSocketEnable = webSocketEnable; + return this; + } + public String siteName() { return siteName; } diff --git a/src/main/java/runwar/undertow/SiteDeployment.java b/src/main/java/runwar/undertow/SiteDeployment.java index 681bac6c..00025b5f 100644 --- a/src/main/java/runwar/undertow/SiteDeployment.java +++ b/src/main/java/runwar/undertow/SiteDeployment.java @@ -32,6 +32,7 @@ import io.undertow.util.HeaderValues; import io.undertow.server.handlers.resource.Resource; import io.undertow.util.Headers; +import io.undertow.server.HttpHandler; import io.undertow.util.HttpString; import io.undertow.util.MimeMappings; import io.undertow.io.Sender; @@ -91,6 +92,7 @@ public class SiteDeployment { private final HttpHandler siteInitialHandler; private final HttpHandler servletInitialHandler; + private WebsocketHandler websocketHandler; private MetricsHandler siteMetricsHandler; private final DeploymentManager deploymentManager; private SecurityManager securityManager; @@ -331,8 +333,15 @@ public String toString() { httpHandler = this.siteMetricsHandler; } - return new LifecyleHandler(httpHandler, serverOptions, siteOptions); + httpHandler = new LifecyleHandler(httpHandler, serverOptions, siteOptions); + + if (siteOptions.webSocketEnable()) { + LOG.info(" WebSocket Server started"); + httpHandler = new WebsocketHandler(httpHandler, serverOptions, siteOptions); + this.websocketHandler = (WebsocketHandler) httpHandler; + } + return httpHandler; } public void processRequest(HttpServerExchange exchange) throws Exception { @@ -393,6 +402,10 @@ public MetricsHandler getSiteMetricsHandler() { return siteMetricsHandler; } + public WebsocketHandler getWebsocketHandler() { + return websocketHandler; + } + public void stop() { try { switch (deploymentManager.getState()) { diff --git a/src/main/java/runwar/undertow/WebsocketHandler.java b/src/main/java/runwar/undertow/WebsocketHandler.java new file mode 100644 index 00000000..213f6a84 --- /dev/null +++ b/src/main/java/runwar/undertow/WebsocketHandler.java @@ -0,0 +1,101 @@ +package runwar.undertow; + +import static runwar.logging.RunwarLogger.LOG; + +import io.undertow.websockets.core.WebSockets; +import io.undertow.io.Sender; +import io.undertow.server.HttpHandler; +import io.undertow.server.HttpServerExchange; +import io.undertow.util.Headers; +import io.undertow.util.StatusCodes; +import io.undertow.websockets.WebSocketConnectionCallback; +import io.undertow.websockets.core.AbstractReceiveListener; +import io.undertow.websockets.core.BufferedTextMessage; +import io.undertow.websockets.core.WebSocketChannel; +import io.undertow.websockets.spi.WebSocketHttpExchange; +import io.undertow.attribute.ExchangeAttributes; +import io.undertow.server.DefaultResponseListener; +import io.undertow.websockets.core.WebSocketChannel; +import io.undertow.websockets.core.WebSocketVersion; +import io.undertow.websockets.spi.WebSocketHttpExchange; +import io.undertow.server.HttpHandler; +import io.undertow.server.HttpServerExchange; +import io.undertow.websockets.WebSocketProtocolHandshakeHandler; +import io.undertow.websockets.core.StreamSinkFrameChannel; +import io.undertow.websockets.core.WebSocketChannel; +import io.undertow.websockets.core.WebSocketFrameType; +import io.undertow.server.handlers.PathHandler; +import runwar.logging.RunwarLogger; +import runwar.options.ServerOptions; +import runwar.options.SiteOptions; +import runwar.Server; +import runwar.LaunchUtil; +import java.util.Map; +import java.util.List; +import java.util.HashMap; +import java.util.Set; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.HashSet; + +public class WebsocketHandler extends PathHandler { + + private final HttpHandler next; + private final ServerOptions serverOptions; + private final SiteOptions siteOptions; + private final Set connections = Collections.synchronizedSet(new HashSet<>()); + private final WebSocketProtocolHandshakeHandler webSocketProtocolHandshakeHandler; + + public WebsocketHandler(final HttpHandler next, ServerOptions serverOptions, SiteOptions siteOptions) { + super(next); + this.next = next; + this.serverOptions = serverOptions; + this.siteOptions = siteOptions; + this.webSocketProtocolHandshakeHandler = new WebSocketProtocolHandshakeHandler( + new WebSocketConnectionCallback() { + @Override + public void onConnect(WebSocketHttpExchange WSexchange, WebSocketChannel channel) { + // Add the new channel to the set of connections + connections.add(channel); + WebsocketReceiveListener listener = new WebsocketReceiveListener(Server.getCurrentExchange(), + next, + serverOptions, + siteOptions, channel); + channel.getReceiveSetter().set(listener); + channel.getCloseSetter().set((c) -> { + connections.remove(channel); + listener.onClose(c); + }); + channel.resumeReceives(); + } + }, next); + + // In reality, this can just be `/` and apply to all URLs, but a specific suffix + // makes it easier to proxy at the web server level + addPrefixPath(siteOptions.webSocketURI(), webSocketProtocolHandshakeHandler); + } + + /** + * Get all connections + */ + public Set getConnections() { + return connections; + } + + public void sendMessage(WebSocketChannel channel, String message) { + if (channel == null || !channel.isOpen()) { + return; + } + WebSockets.sendText(message, channel, null); + } + + public void broadcastMessage(String message) { + // Iterate over all open connections and send the message + for (WebSocketChannel channel : connections) { + sendMessage(channel, message); + } + } + +} \ No newline at end of file diff --git a/src/main/java/runwar/undertow/WebsocketReceiveListener.java b/src/main/java/runwar/undertow/WebsocketReceiveListener.java new file mode 100644 index 00000000..4ef56968 --- /dev/null +++ b/src/main/java/runwar/undertow/WebsocketReceiveListener.java @@ -0,0 +1,588 @@ +package runwar.undertow; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.net.SocketAddress; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.nio.channels.FileChannel; +import java.util.List; + +import org.xnio.conduits.ConduitStreamSinkChannel; +import org.xnio.conduits.StreamSinkConduit; +import org.xnio.channels.Channels; +import org.xnio.channels.ConnectedStreamChannel; +import org.xnio.channels.Configurable; +import org.xnio.channels.StreamSourceChannel; +import org.xnio.conduits.WriteReadyHandler; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.Channel; +import java.nio.channels.ClosedChannelException; +import java.nio.channels.WritableByteChannel; + +import org.xnio.ChannelListener; +import org.xnio.Option; +import org.xnio.OptionMap; +import org.xnio.Pool; +import org.xnio.StreamConnection; +import org.xnio.XnioIoThread; +import org.xnio.XnioWorker; +import org.xnio.channels.Configurable; +import org.xnio.channels.ConnectedChannel; +import org.xnio.channels.StreamSinkChannel; +import org.xnio.channels.StreamSourceChannel; +import org.xnio.conduits.ConduitStreamSinkChannel; +import org.xnio.conduits.ConduitStreamSourceChannel; +import org.xnio.conduits.ReadReadyHandler; +import org.xnio.conduits.StreamSinkConduit; +import org.xnio.conduits.StreamSourceConduit; +import org.xnio.conduits.WriteReadyHandler; + +import io.undertow.UndertowMessages; +import io.undertow.connector.ByteBufferPool; +import io.undertow.server.Connectors; +import io.undertow.server.DefaultByteBufferPool; +import io.undertow.server.HttpHandler; +import io.undertow.server.HttpServerExchange; +import io.undertow.server.HttpUpgradeListener; +import io.undertow.server.SSLSessionInfo; +import io.undertow.server.ServerConnection; +import io.undertow.server.XnioBufferPoolAdaptor; +import io.undertow.server.protocol.framed.AbstractFramedChannel; +import io.undertow.util.AttachmentKey; +import io.undertow.util.BadRequestException; +import io.undertow.util.ParameterLimitException; +import io.undertow.websockets.core.AbstractReceiveListener; +import io.undertow.websockets.core.BufferedTextMessage; +import io.undertow.websockets.core.WebSocketChannel; +import runwar.Server; +import runwar.Server.ServerState; +import runwar.options.ServerOptions; +import runwar.options.SiteOptions; +import io.undertow.websockets.core.WebSocketChannel; +import io.undertow.websockets.core.WebSocketFrameType; +import io.undertow.websockets.core.StreamSinkFrameChannel; + +@SuppressWarnings("deprecation") +public class WebsocketReceiveListener extends AbstractReceiveListener { + public static final AttachmentKey> WEBSOCKET_REQUEST_DETAILS = AttachmentKey.create(List.class); + + private HttpServerExchange exchange; + private HttpHandler next; + private ServerOptions serverOptions; + private SiteOptions siteOptions; + private WebSocketChannel channel; + + /** + * constructor + * + * @param exchange the HttpServerExchange + */ + public WebsocketReceiveListener(HttpServerExchange exchange, HttpHandler next, ServerOptions serverOptions, + SiteOptions siteOptions, WebSocketChannel channel) { + this.exchange = exchange; + this.next = next; + this.serverOptions = serverOptions; + this.siteOptions = siteOptions; + this.channel = channel; + + dispatchRequest("onConnect", List.of(channel)); + } + + /** + * onClose method + * + * @param channel the WebSocketChannel + */ + public void onClose(AbstractFramedChannel channel) { + // System.out.println("dispatching onClose"); + dispatchRequest("onClose", List.of(channel)); + } + + /** + * onFullTextMessage method + * + * @param channel the WebSocketChannel + * @param message the BufferedTextMessage + * + * @throws IOException + */ + @Override + protected void onFullTextMessage(WebSocketChannel channel, BufferedTextMessage message) + throws IOException { + // System.out.println("dispatching onFullTextMessage"); + dispatchRequest("onFullTextMessage", List.of(message, channel)); + } + + public void dispatchRequest(String method, List requestDetails) { + // If the server is shutting down, there's nothign to do. + if (Server.getServerState().equals(Server.ServerState.STOPPING) + || Server.getServerState().equals(Server.ServerState.STOPPED)) { + return; + } + + String newUri = siteOptions.webSocketListener() + "?method=onProcess&WSMethod=" + method; + // System.out.println( "dispatching request: " + newUri ); + + final DefaultByteBufferPool bufferPool = new DefaultByteBufferPool(false, 1024, 0, 0); + MockServerConnection connection = new MockServerConnection(bufferPool); + + // Create a new HttpServerExchange for the new request + HttpServerExchange newExchange = new HttpServerExchange(connection); + newExchange.putAttachment(SiteDeploymentManager.SITE_DEPLOYMENT_KEY, + exchange.getAttachment(SiteDeploymentManager.SITE_DEPLOYMENT_KEY)); + + // Put the details on the new exchange so we can access them in our CF code + newExchange.putAttachment(WEBSOCKET_REQUEST_DETAILS, requestDetails); + + // copy headers (like cookies) any from the original request (except Upgrade) + this.exchange.getRequestHeaders().forEach(header -> { + if (!header.getHeaderName().toString().equalsIgnoreCase("Upgrade")) { + newExchange.getRequestHeaders().add(header.getHeaderName(), header.getFirst()); + } + }); + newExchange.setRequestMethod(this.exchange.getRequestMethod()); + newExchange.setProtocol(this.exchange.getProtocol()); + newExchange.setRequestScheme(this.exchange.getRequestScheme()); + newExchange.setSourceAddress(this.exchange.getSourceAddress()); + newExchange.setDestinationAddress(this.exchange.getDestinationAddress()); + + final StringBuilder sb = new StringBuilder(); + try { + Connectors.setExchangeRequestPath(newExchange, newUri, sb); + } catch (ParameterLimitException e) { + e.printStackTrace(); + } + + // This sets the requestpath, relativepath, querystring, and parses the query + // parameters + newExchange.setRequestURI(this.exchange.getRequestScheme() + "://" + this.exchange.getHostAndPort() + newUri, + true); + + // Call the handler for the new URI + try { + HttpHandler exchangeSetter = new HttpHandler() { + + @Override + public void handleRequest(final HttpServerExchange exchange) throws Exception { + HttpServerExchange currentExchange = Server.getCurrentExchange(); + String currentDeployKey = Server.getCurrentDeploymentKey(); + try { + // This allows the exchange to be available to the thread. + Server.setCurrentExchange(newExchange); + Server.setCurrentDeploymentKey(currentDeployKey); + next.handleRequest(newExchange); + } finally { + // Clean up after + Server.setCurrentExchange(currentExchange); + Server.setCurrentDeploymentKey(currentDeployKey); + } + } + + @Override + public String toString() { + return "Websocket Exchange Setter Handler"; + } + }; + if (exchange.isInIoThread()) { + exchange.dispatch(exchangeSetter); + } else { + exchangeSetter.handleRequest(exchange); + } + } catch (Exception e) { + System.out.println("Error dispatching request: " + e.getMessage()); + e.printStackTrace(); + } + + } + + @SuppressWarnings("deprecation") + private static class MockServerConnection extends ServerConnection { + + private final ByteBufferPool bufferPool; + private SSLSessionInfo sslSessionInfo; + private XnioBufferPoolAdaptor poolAdaptor; + + private MockServerConnection(ByteBufferPool bufferPool) { + this.bufferPool = bufferPool; + } + + @Override + @SuppressWarnings("deprecation") + public Pool getBufferPool() { + if (poolAdaptor == null) { + poolAdaptor = new XnioBufferPoolAdaptor(getByteBufferPool()); + } + return poolAdaptor; + } + + @Override + public ByteBufferPool getByteBufferPool() { + return bufferPool; + } + + @Override + public XnioWorker getWorker() { + return null; + } + + @Override + public XnioIoThread getIoThread() { + return null; + } + + @Override + public HttpServerExchange sendOutOfBandResponse(HttpServerExchange exchange) { + throw UndertowMessages.MESSAGES.outOfBandResponseNotSupported(); + } + + @Override + public boolean isContinueResponseSupported() { + return false; + } + + @Override + public void terminateRequestChannel(HttpServerExchange exchange) { + + } + + @Override + public boolean isOpen() { + return true; + } + + @Override + public boolean supportsOption(Option option) { + return false; + } + + @Override + public T getOption(Option option) throws IOException { + return null; + } + + @Override + public T setOption(Option option, T value) throws IllegalArgumentException, IOException { + return null; + } + + @Override + public void close() throws IOException { + } + + @Override + public SocketAddress getPeerAddress() { + return null; + } + + @Override + public A getPeerAddress(Class type) { + return null; + } + + @Override + public ChannelListener.Setter getCloseSetter() { + return null; + } + + @Override + public SocketAddress getLocalAddress() { + return null; + } + + @Override + public A getLocalAddress(Class type) { + return null; + } + + @Override + public OptionMap getUndertowOptions() { + return OptionMap.EMPTY; + } + + @Override + public int getBufferSize() { + return 1024; + } + + @Override + public SSLSessionInfo getSslSessionInfo() { + return sslSessionInfo; + } + + @Override + public void setSslSessionInfo(SSLSessionInfo sessionInfo) { + sslSessionInfo = sessionInfo; + } + + @Override + public void addCloseListener(CloseListener listener) { + } + + @Override + public StreamConnection upgradeChannel() { + return null; + } + + @Override + public ConduitStreamSinkChannel getSinkChannel() { + return new ConduitStreamSinkChannel(Configurable.EMPTY, new DummyStreamSinkConduit()); + } + + @Override + public ConduitStreamSourceChannel getSourceChannel() { + return new ConduitStreamSourceChannel(Configurable.EMPTY, new DummyStreamSourceConduit()); + } + + @Override + protected StreamSinkConduit getSinkConduit(HttpServerExchange exchange, StreamSinkConduit conduit) { + return conduit; + } + + @Override + protected boolean isUpgradeSupported() { + return false; + } + + @Override + protected boolean isConnectSupported() { + return false; + } + + @Override + protected void exchangeComplete(HttpServerExchange exchange) { + } + + @Override + protected void setUpgradeListener(HttpUpgradeListener upgradeListener) { + // ignore + } + + @Override + protected void setConnectListener(HttpUpgradeListener connectListener) { + // ignore + } + + @Override + protected void maxEntitySizeUpdated(HttpServerExchange exchange) { + } + + @Override + public String getTransportProtocol() { + return "mock"; + } + + @Override + public boolean isRequestTrailerFieldsSupported() { + return false; + } + } + + private static class DummyStreamSinkConduit implements StreamSinkConduit { + + @Override + public int write(ByteBuffer src) throws IOException { + // Ignore all input + int len = src.remaining(); + src.position(src.limit()); + return len; + } + + @Override + public long write(ByteBuffer[] srcs, int offset, int length) throws IOException { + // Ignore all input + long total = 0; + for (int i = offset; i < offset + length; i++) { + total += srcs[i].remaining(); + srcs[i].position(srcs[i].limit()); + } + return total; + } + + @Override + public long writeFinal(ByteBuffer[] srcs, int offset, int length) throws IOException { + // Ignore all input + long total = 0; + for (int i = offset; i < offset + length; i++) { + total += srcs[i].remaining(); + srcs[i].position(srcs[i].limit()); + } + return total; + } + + @Override + public long transferFrom(StreamSourceChannel source, long count, ByteBuffer throughBuffer) throws IOException { + // Ignore all input + return count; + } + + @Override + public int writeFinal(ByteBuffer src) throws IOException { + // Ignore all input + int len = src.remaining(); + src.position(src.limit()); + return len; + } + + @Override + public XnioIoThread getWriteThread() { + // Return a dummy value or null + return null; + } + + @Override + public XnioWorker getWorker() { + // Return a dummy value or null + return null; + } + + @Override + public long transferFrom(java.nio.channels.FileChannel src, long position, long count) throws IOException { + // Ignore all input + return count; + } + + @Override + public void setWriteReadyHandler(WriteReadyHandler handler) { + // Do nothing + } + + @Override + public boolean flush() throws IOException { + // Do nothing + return true; + } + + @Override + public void terminateWrites() throws IOException { + // Do nothing + } + + @Override + public void truncateWrites() throws IOException { + // Do nothing + } + + @Override + public boolean isWriteShutdown() { + return false; + } + + @Override + public void resumeWrites() { + // Do nothing + } + + @Override + public void suspendWrites() { + // Do nothing + } + + @Override + public void wakeupWrites() { + // Do nothing + } + + @Override + public boolean isWriteResumed() { + return false; + } + + @Override + public void awaitWritable() throws IOException { + // Do nothing + } + + @Override + public void awaitWritable(long time, java.util.concurrent.TimeUnit timeUnit) throws IOException { + // Do nothing + } + + } + + private static class DummyStreamSourceConduit implements StreamSourceConduit { + + @Override + public long transferTo(long position, long count, FileChannel target) throws IOException { + // Mock implementation: return 0 to indicate no bytes transferred + return 0; + } + + @Override + public long transferTo(long count, ByteBuffer throughBuffer, StreamSinkChannel target) throws IOException { + // Mock implementation: return 0 to indicate no bytes transferred + return -1; + } + + @Override + public int read(ByteBuffer dst) throws IOException { + // Mock implementation: return -1 to indicate end of input + return -1; + } + + @Override + public long read(ByteBuffer[] dsts, int offs, int len) throws IOException { + // Mock implementation: return -1 to indicate end of input + return -1; + } + + @Override + public void terminateReads() throws IOException { + // Mock implementation: do nothing + } + + @Override + public boolean isReadShutdown() { + // Mock implementation: return true to indicate reads are shutdown + return true; + } + + @Override + public void resumeReads() { + // Mock implementation: do nothing + } + + @Override + public void suspendReads() { + // Mock implementation: do nothing + } + + @Override + public void wakeupReads() { + // Mock implementation: do nothing + } + + @Override + public boolean isReadResumed() { + // Mock implementation: return false to indicate reads are not resumed + return false; + } + + @Override + public void awaitReadable() throws IOException { + // Mock implementation: do nothing + } + + @Override + public void awaitReadable(long time, java.util.concurrent.TimeUnit timeUnit) throws IOException { + // Mock implementation: do nothing + } + + @Override + public XnioIoThread getReadThread() { + return null; + } + + @Override + public void setReadReadyHandler(ReadReadyHandler handler) { + + } + + @Override + public XnioWorker getWorker() { + return null; + } + } +} \ No newline at end of file From edf25002165e806e9d7e82b0ed990b65d25a299e Mon Sep 17 00:00:00 2001 From: Brad Wood Date: Fri, 11 Oct 2024 00:42:12 -0500 Subject: [PATCH 24/30] Update for STOMP --- gradle/version | 2 +- .../runwar/undertow/WebsocketHandler.java | 12 +- .../undertow/WebsocketReceiveListener.java | 138 ++++++++++-------- 3 files changed, 93 insertions(+), 59 deletions(-) diff --git a/gradle/version b/gradle/version index 79d716e6..f6ad5aba 100644 --- a/gradle/version +++ b/gradle/version @@ -1 +1 @@ -5.0.6-0de55e3b7a2f4c8dd5b33ef11edc7a90a39506e7-96ea216b7cfb6883a6af15c191a94953a0596b9c \ No newline at end of file +5.0.7-0de55e3b7a2f4c8dd5b33ef11edc7a90a39506e7-96ea216b7cfb6883a6af15c191a94953a0596b9c \ No newline at end of file diff --git a/src/main/java/runwar/undertow/WebsocketHandler.java b/src/main/java/runwar/undertow/WebsocketHandler.java index 213f6a84..7ded1edc 100644 --- a/src/main/java/runwar/undertow/WebsocketHandler.java +++ b/src/main/java/runwar/undertow/WebsocketHandler.java @@ -53,7 +53,13 @@ public WebsocketHandler(final HttpHandler next, ServerOptions serverOptions, Sit this.next = next; this.serverOptions = serverOptions; this.siteOptions = siteOptions; + + Set handshakes = new HashSet<>(); + handshakes.add(new Hybi13Handshake(Set.of("v12.stomp", "v11.stomp", "v10.stomp"), false)); + handshakes.add(new Hybi08Handshake(Set.of("v12.stomp", "v11.stomp", "v10.stomp"), false)); + handshakes.add(new Hybi07Handshake(Set.of("v12.stomp", "v11.stomp", "v10.stomp"), false)); this.webSocketProtocolHandshakeHandler = new WebSocketProtocolHandshakeHandler( + handshakes, new WebSocketConnectionCallback() { @Override public void onConnect(WebSocketHttpExchange WSexchange, WebSocketChannel channel) { @@ -88,7 +94,11 @@ public void sendMessage(WebSocketChannel channel, String message) { if (channel == null || !channel.isOpen()) { return; } - WebSockets.sendText(message, channel, null); + // I'm not clear if Undertow handles this, but just to be safe only send one + // message to a channel at once to avoid threading issues + synchronized (channel) { + WebSockets.sendText(message, channel, null); + } } public void broadcastMessage(String message) { diff --git a/src/main/java/runwar/undertow/WebsocketReceiveListener.java b/src/main/java/runwar/undertow/WebsocketReceiveListener.java index 4ef56968..bb5ce93d 100644 --- a/src/main/java/runwar/undertow/WebsocketReceiveListener.java +++ b/src/main/java/runwar/undertow/WebsocketReceiveListener.java @@ -82,7 +82,7 @@ public class WebsocketReceiveListener extends AbstractReceiveListener { */ public WebsocketReceiveListener(HttpServerExchange exchange, HttpHandler next, ServerOptions serverOptions, SiteOptions siteOptions, WebSocketChannel channel) { - this.exchange = exchange; + this.initialExchange = exchange; this.next = next; this.serverOptions = serverOptions; this.siteOptions = siteOptions; @@ -97,7 +97,6 @@ public WebsocketReceiveListener(HttpServerExchange exchange, HttpHandler next, S * @param channel the WebSocketChannel */ public void onClose(AbstractFramedChannel channel) { - // System.out.println("dispatching onClose"); dispatchRequest("onClose", List.of(channel)); } @@ -112,8 +111,21 @@ public void onClose(AbstractFramedChannel channel) { @Override protected void onFullTextMessage(WebSocketChannel channel, BufferedTextMessage message) throws IOException { - // System.out.println("dispatching onFullTextMessage"); - dispatchRequest("onFullTextMessage", List.of(message, channel)); + dispatchRequest("onFullTextMessage", List.of(message.getData(), channel)); + } + + protected void onFullBinaryMessage(final WebSocketChannel channel, BufferedBinaryMessage message) + throws IOException { + // turn the message into a string + Pooled pbb = message.getData(); + ByteBuffer[] bb = pbb.getResource(); + StringBuilder sb = new StringBuilder(); + for (ByteBuffer b : bb) { + sb.append(new String(b.array(), "UTF-8")); + } + dispatchRequest("onFullBinaryMessage", List.of(sb.toString(), channel)); + + message.getData().free(); } public void dispatchRequest(String method, List requestDetails) { @@ -122,62 +134,62 @@ public void dispatchRequest(String method, List requestDetails) { || Server.getServerState().equals(Server.ServerState.STOPPED)) { return; } + try { + String newUri = siteOptions.webSocketListener() + "?method=onProcess&WSMethod=" + method; - String newUri = siteOptions.webSocketListener() + "?method=onProcess&WSMethod=" + method; - // System.out.println( "dispatching request: " + newUri ); - - final DefaultByteBufferPool bufferPool = new DefaultByteBufferPool(false, 1024, 0, 0); - MockServerConnection connection = new MockServerConnection(bufferPool); + final DefaultByteBufferPool bufferPool = new DefaultByteBufferPool(false, 1024, 0, 0); + MockServerConnection connection = new MockServerConnection(bufferPool, initialExchange); - // Create a new HttpServerExchange for the new request - HttpServerExchange newExchange = new HttpServerExchange(connection); - newExchange.putAttachment(SiteDeploymentManager.SITE_DEPLOYMENT_KEY, - exchange.getAttachment(SiteDeploymentManager.SITE_DEPLOYMENT_KEY)); + // Create a new HttpServerExchange for the new request + HttpServerExchange newExchange = new HttpServerExchange(connection); + newExchange.putAttachment(SiteDeploymentManager.SITE_DEPLOYMENT_KEY, + initialExchange.getAttachment(SiteDeploymentManager.SITE_DEPLOYMENT_KEY)); - // Put the details on the new exchange so we can access them in our CF code - newExchange.putAttachment(WEBSOCKET_REQUEST_DETAILS, requestDetails); + // Put the details on the new exchange so we can access them in our CF code + newExchange.putAttachment(WEBSOCKET_REQUEST_DETAILS, requestDetails); - // copy headers (like cookies) any from the original request (except Upgrade) - this.exchange.getRequestHeaders().forEach(header -> { - if (!header.getHeaderName().toString().equalsIgnoreCase("Upgrade")) { - newExchange.getRequestHeaders().add(header.getHeaderName(), header.getFirst()); + // copy headers (like cookies) any from the original request (except Upgrade) + this.initialExchange.getRequestHeaders().forEach(header -> { + if (!header.getHeaderName().toString().equalsIgnoreCase("Upgrade")) { + newExchange.getRequestHeaders().add(header.getHeaderName(), header.getFirst()); + } + }); + newExchange.setRequestMethod(this.initialExchange.getRequestMethod()); + newExchange.setProtocol(this.initialExchange.getProtocol()); + newExchange.setRequestScheme(this.initialExchange.getRequestScheme()); + newExchange.setSourceAddress(this.initialExchange.getSourceAddress()); + newExchange.setDestinationAddress(this.initialExchange.getDestinationAddress()); + + final StringBuilder sb = new StringBuilder(); + try { + Connectors.setExchangeRequestPath(newExchange, newUri, sb); + } catch (ParameterLimitException | BadRequestException e) { + e.printStackTrace(); } - }); - newExchange.setRequestMethod(this.exchange.getRequestMethod()); - newExchange.setProtocol(this.exchange.getProtocol()); - newExchange.setRequestScheme(this.exchange.getRequestScheme()); - newExchange.setSourceAddress(this.exchange.getSourceAddress()); - newExchange.setDestinationAddress(this.exchange.getDestinationAddress()); - - final StringBuilder sb = new StringBuilder(); - try { - Connectors.setExchangeRequestPath(newExchange, newUri, sb); - } catch (ParameterLimitException e) { - e.printStackTrace(); - } - // This sets the requestpath, relativepath, querystring, and parses the query - // parameters - newExchange.setRequestURI(this.exchange.getRequestScheme() + "://" + this.exchange.getHostAndPort() + newUri, - true); + // This sets the requestpath, relativepath, querystring, and parses the query + // parameters + newExchange.setRequestURI( + this.initialExchange.getRequestScheme() + "://" + this.initialExchange.getHostAndPort() + newUri, + true); - // Call the handler for the new URI - try { + // Call the handler for the new URI HttpHandler exchangeSetter = new HttpHandler() { @Override public void handleRequest(final HttpServerExchange exchange) throws Exception { - HttpServerExchange currentExchange = Server.getCurrentExchange(); - String currentDeployKey = Server.getCurrentDeploymentKey(); + try { // This allows the exchange to be available to the thread. - Server.setCurrentExchange(newExchange); + Server.setCurrentExchange(exchange); Server.setCurrentDeploymentKey(currentDeployKey); - next.handleRequest(newExchange); + next.handleRequest(exchange); + } catch (Exception e) { + e.printStackTrace(); } finally { // Clean up after - Server.setCurrentExchange(currentExchange); - Server.setCurrentDeploymentKey(currentDeployKey); + Server.setCurrentExchange(null); + Server.setCurrentDeploymentKey(null); } } @@ -186,13 +198,12 @@ public String toString() { return "Websocket Exchange Setter Handler"; } }; - if (exchange.isInIoThread()) { - exchange.dispatch(exchangeSetter); - } else { - exchangeSetter.handleRequest(exchange); - } + + // We are in an IO thread, so we can dispatch the request directly to the worker + // pool + newExchange.dispatch(exchangeSetter); } catch (Exception e) { - System.out.println("Error dispatching request: " + e.getMessage()); + System.out.println("Error dispatching websocket request: " + e.getMessage()); e.printStackTrace(); } @@ -204,9 +215,11 @@ private static class MockServerConnection extends ServerConnection { private final ByteBufferPool bufferPool; private SSLSessionInfo sslSessionInfo; private XnioBufferPoolAdaptor poolAdaptor; + private HttpServerExchange initialExchange; - private MockServerConnection(ByteBufferPool bufferPool) { + private MockServerConnection(ByteBufferPool bufferPool, HttpServerExchange initialExchange) { this.bufferPool = bufferPool; + this.initialExchange = initialExchange; } @Override @@ -225,7 +238,7 @@ public ByteBufferPool getByteBufferPool() { @Override public XnioWorker getWorker() { - return null; + return initialExchange.getConnection().getWorker(); } @Override @@ -328,12 +341,12 @@ public StreamConnection upgradeChannel() { @Override public ConduitStreamSinkChannel getSinkChannel() { - return new ConduitStreamSinkChannel(Configurable.EMPTY, new DummyStreamSinkConduit()); + return new ConduitStreamSinkChannel(Configurable.EMPTY, new DummyStreamSinkConduit(initialExchange)); } @Override public ConduitStreamSourceChannel getSourceChannel() { - return new ConduitStreamSourceChannel(Configurable.EMPTY, new DummyStreamSourceConduit()); + return new ConduitStreamSourceChannel(Configurable.EMPTY, new DummyStreamSourceConduit(initialExchange)); } @Override @@ -382,6 +395,12 @@ public boolean isRequestTrailerFieldsSupported() { private static class DummyStreamSinkConduit implements StreamSinkConduit { + private HttpServerExchange initialExchange; + + public DummyStreamSinkConduit(HttpServerExchange initialExchange) { + this.initialExchange = initialExchange; + } + @Override public int write(ByteBuffer src) throws IOException { // Ignore all input @@ -434,8 +453,7 @@ public XnioIoThread getWriteThread() { @Override public XnioWorker getWorker() { - // Return a dummy value or null - return null; + return initialExchange.getConnection().getWorker(); } @Override @@ -504,6 +522,12 @@ public void awaitWritable(long time, java.util.concurrent.TimeUnit timeUnit) thr private static class DummyStreamSourceConduit implements StreamSourceConduit { + private HttpServerExchange initialExchange; + + public DummyStreamSourceConduit(HttpServerExchange initialExchange) { + this.initialExchange = initialExchange; + } + @Override public long transferTo(long position, long count, FileChannel target) throws IOException { // Mock implementation: return 0 to indicate no bytes transferred @@ -582,7 +606,7 @@ public void setReadReadyHandler(ReadReadyHandler handler) { @Override public XnioWorker getWorker() { - return null; + return initialExchange.getConnection().getWorker(); } } } \ No newline at end of file From c3022c037de80be8ed8403dcfb81f9e0d5407846 Mon Sep 17 00:00:00 2001 From: Brad Wood Date: Fri, 11 Oct 2024 00:45:48 -0500 Subject: [PATCH 25/30] Missed var name change --- src/main/java/runwar/undertow/WebsocketReceiveListener.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/runwar/undertow/WebsocketReceiveListener.java b/src/main/java/runwar/undertow/WebsocketReceiveListener.java index bb5ce93d..bded81ff 100644 --- a/src/main/java/runwar/undertow/WebsocketReceiveListener.java +++ b/src/main/java/runwar/undertow/WebsocketReceiveListener.java @@ -69,7 +69,7 @@ public class WebsocketReceiveListener extends AbstractReceiveListener { public static final AttachmentKey> WEBSOCKET_REQUEST_DETAILS = AttachmentKey.create(List.class); - private HttpServerExchange exchange; + private HttpServerExchange initialExchange; private HttpHandler next; private ServerOptions serverOptions; private SiteOptions siteOptions; From 814e4af6a5b0f2523296bd2eb4e00a46c70493c6 Mon Sep 17 00:00:00 2001 From: Brad Wood Date: Fri, 11 Oct 2024 00:50:46 -0500 Subject: [PATCH 26/30] Add some imports --- .../java/runwar/undertow/WebsocketHandler.java | 16 ++++++++++------ .../undertow/WebsocketReceiveListener.java | 2 ++ 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/src/main/java/runwar/undertow/WebsocketHandler.java b/src/main/java/runwar/undertow/WebsocketHandler.java index 7ded1edc..4ab09dbe 100644 --- a/src/main/java/runwar/undertow/WebsocketHandler.java +++ b/src/main/java/runwar/undertow/WebsocketHandler.java @@ -1,17 +1,21 @@ package runwar.undertow; -import static runwar.logging.RunwarLogger.LOG; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; import io.undertow.websockets.core.WebSockets; import io.undertow.io.Sender; import io.undertow.server.HttpHandler; -import io.undertow.server.HttpServerExchange; -import io.undertow.util.Headers; -import io.undertow.util.StatusCodes; +import io.undertow.server.handlers.PathHandler; import io.undertow.websockets.WebSocketConnectionCallback; -import io.undertow.websockets.core.AbstractReceiveListener; -import io.undertow.websockets.core.BufferedTextMessage; +import io.undertow.websockets.WebSocketProtocolHandshakeHandler; import io.undertow.websockets.core.WebSocketChannel; +import io.undertow.websockets.core.WebSockets; +import io.undertow.websockets.core.protocol.Handshake; +import io.undertow.websockets.core.protocol.version07.Hybi07Handshake; +import io.undertow.websockets.core.protocol.version08.Hybi08Handshake; +import io.undertow.websockets.core.protocol.version13.Hybi13Handshake; import io.undertow.websockets.spi.WebSocketHttpExchange; import io.undertow.attribute.ExchangeAttributes; import io.undertow.server.DefaultResponseListener; diff --git a/src/main/java/runwar/undertow/WebsocketReceiveListener.java b/src/main/java/runwar/undertow/WebsocketReceiveListener.java index bded81ff..f8c49b80 100644 --- a/src/main/java/runwar/undertow/WebsocketReceiveListener.java +++ b/src/main/java/runwar/undertow/WebsocketReceiveListener.java @@ -26,6 +26,7 @@ import org.xnio.Option; import org.xnio.OptionMap; import org.xnio.Pool; +import org.xnio.Pooled; import org.xnio.StreamConnection; import org.xnio.XnioIoThread; import org.xnio.XnioWorker; @@ -55,6 +56,7 @@ import io.undertow.util.BadRequestException; import io.undertow.util.ParameterLimitException; import io.undertow.websockets.core.AbstractReceiveListener; +import io.undertow.websockets.core.BufferedBinaryMessage; import io.undertow.websockets.core.BufferedTextMessage; import io.undertow.websockets.core.WebSocketChannel; import runwar.Server; From 9f2f81451fd80fb47119506b7033a455feba0c2c Mon Sep 17 00:00:00 2001 From: Brad Wood Date: Fri, 11 Oct 2024 00:56:15 -0500 Subject: [PATCH 27/30] Need deploy key --- src/main/java/runwar/undertow/WebsocketReceiveListener.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/java/runwar/undertow/WebsocketReceiveListener.java b/src/main/java/runwar/undertow/WebsocketReceiveListener.java index f8c49b80..ba7a281d 100644 --- a/src/main/java/runwar/undertow/WebsocketReceiveListener.java +++ b/src/main/java/runwar/undertow/WebsocketReceiveListener.java @@ -175,6 +175,8 @@ public void dispatchRequest(String method, List requestDetails) { this.initialExchange.getRequestScheme() + "://" + this.initialExchange.getHostAndPort() + newUri, true); + final String currentDeployKey = Server.getCurrentDeploymentKey(); + // Call the handler for the new URI HttpHandler exchangeSetter = new HttpHandler() { From f79370ac55fcea4abf2b2d8d57497d63eced8a1c Mon Sep 17 00:00:00 2001 From: Brad Wood Date: Fri, 11 Oct 2024 01:07:48 -0500 Subject: [PATCH 28/30] Exception type not needed --- src/main/java/runwar/undertow/WebsocketReceiveListener.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/runwar/undertow/WebsocketReceiveListener.java b/src/main/java/runwar/undertow/WebsocketReceiveListener.java index ba7a281d..5ff780c1 100644 --- a/src/main/java/runwar/undertow/WebsocketReceiveListener.java +++ b/src/main/java/runwar/undertow/WebsocketReceiveListener.java @@ -165,7 +165,7 @@ public void dispatchRequest(String method, List requestDetails) { final StringBuilder sb = new StringBuilder(); try { Connectors.setExchangeRequestPath(newExchange, newUri, sb); - } catch (ParameterLimitException | BadRequestException e) { + } catch (ParameterLimitException e) { e.printStackTrace(); } From 50973207dc8418fdeb5fce1727f682b31ad5c105 Mon Sep 17 00:00:00 2001 From: Brad Wood Date: Fri, 11 Oct 2024 01:44:24 -0500 Subject: [PATCH 29/30] Try this method of setting URI --- .../runwar/undertow/WebsocketReceiveListener.java | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/main/java/runwar/undertow/WebsocketReceiveListener.java b/src/main/java/runwar/undertow/WebsocketReceiveListener.java index 5ff780c1..872a871a 100644 --- a/src/main/java/runwar/undertow/WebsocketReceiveListener.java +++ b/src/main/java/runwar/undertow/WebsocketReceiveListener.java @@ -137,7 +137,8 @@ public void dispatchRequest(String method, List requestDetails) { return; } try { - String newUri = siteOptions.webSocketListener() + "?method=onProcess&WSMethod=" + method; + String newUri = siteOptions.webSocketListener(); + String qs = "?method=onProcess&WSMethod=" + method; final DefaultByteBufferPool bufferPool = new DefaultByteBufferPool(false, 1024, 0, 0); MockServerConnection connection = new MockServerConnection(bufferPool, initialExchange); @@ -162,12 +163,9 @@ public void dispatchRequest(String method, List requestDetails) { newExchange.setSourceAddress(this.initialExchange.getSourceAddress()); newExchange.setDestinationAddress(this.initialExchange.getDestinationAddress()); - final StringBuilder sb = new StringBuilder(); - try { - Connectors.setExchangeRequestPath(newExchange, newUri, sb); - } catch (ParameterLimitException e) { - e.printStackTrace(); - } + exchange.setRequestPath(newUri); + exchange.setRelativePath(newUri); + exchange.setQueryString(qs); // This sets the requestpath, relativepath, querystring, and parses the query // parameters From b9312092a6d2ae0b21c78351801312e57e3265f2 Mon Sep 17 00:00:00 2001 From: Brad Wood Date: Fri, 11 Oct 2024 01:45:54 -0500 Subject: [PATCH 30/30] wrong var name --- src/main/java/runwar/undertow/WebsocketReceiveListener.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/runwar/undertow/WebsocketReceiveListener.java b/src/main/java/runwar/undertow/WebsocketReceiveListener.java index 872a871a..9a67d30f 100644 --- a/src/main/java/runwar/undertow/WebsocketReceiveListener.java +++ b/src/main/java/runwar/undertow/WebsocketReceiveListener.java @@ -163,9 +163,9 @@ public void dispatchRequest(String method, List requestDetails) { newExchange.setSourceAddress(this.initialExchange.getSourceAddress()); newExchange.setDestinationAddress(this.initialExchange.getDestinationAddress()); - exchange.setRequestPath(newUri); - exchange.setRelativePath(newUri); - exchange.setQueryString(qs); + newExchange.setRequestPath(newUri); + newExchange.setRelativePath(newUri); + newExchange.setQueryString(qs); // This sets the requestpath, relativepath, querystring, and parses the query // parameters