[ 
https://issues.apache.org/jira/browse/WW-5791?focusedWorklogId=1049686&page=com.atlassian.jira.plugin.system.issuetabpanels:worklog-tabpanel#worklog-1049686
 ]

ASF GitHub Bot logged work on WW-5791:
--------------------------------------

                Author: ASF GitHub Bot
            Created on: 23/Oct/26 13:27
            Start Date: 23/Oct/26 13:27
    Worklog Time Spent: 10m 
      Work Description: github-advanced-security[bot] commented on code in PR 
#2053:
URL: https://github.com/apache/struts/pull/2053#discussion_r4360029496


##########
boot/src/main/java/org/apache/struts2/boot/StrutsBootServer.java:
##########
@@ -0,0 +1,244 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.struts2.boot;
+
+import jakarta.servlet.FilterConfig;
+import jakarta.servlet.ServletException;
+import org.apache.catalina.Context;
+import org.apache.catalina.LifecycleState;
+import org.apache.catalina.connector.Connector;
+import org.apache.catalina.servlets.DefaultServlet;
+import org.apache.catalina.startup.Tomcat;
+import org.apache.catalina.valves.ErrorReportValve;
+import org.apache.struts2.config.ConfigurationException;
+import org.apache.struts2.dispatcher.filter.StrutsPrepareAndExecuteFilter;
+import org.apache.tomcat.util.descriptor.web.FilterDef;
+import org.apache.tomcat.util.descriptor.web.FilterMap;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Comparator;
+import java.util.UUID;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.stream.Stream;
+
+public final class StrutsBootServer implements AutoCloseable {
+
+    private static final String FILTER_NAME = "struts";
+
+    private final Tomcat tomcat;
+    private final Path baseDir;
+    private final BootSettings settings;
+    private final AtomicBoolean closed = new AtomicBoolean();
+    private final Thread shutdownHook = new Thread(this::close, 
"struts-boot-shutdown");
+    private Connector connector;
+
+    private StrutsBootServer(Tomcat tomcat, Path baseDir, BootSettings 
settings) {
+        this.tomcat = tomcat;
+        this.baseDir = baseDir;
+        this.settings = settings;
+    }
+
+    static StrutsBootServer start(ActionRegistry registry, BootSettings 
settings) {
+        Path baseDir = createBaseDir();
+        Tomcat tomcat = new Tomcat();
+        StrutsBootServer server = new StrutsBootServer(tomcat, baseDir, 
settings);
+        CapturingStrutsFilter filter = new CapturingStrutsFilter();
+        try {
+            tomcat.setBaseDir(baseDir.toString());
+            hideErrorReportDetails(tomcat);
+            Path docBase = Files.createDirectory(baseDir.resolve("webapp"));
+            Context context = tomcat.addContext(settings.contextPath(), 
docBase.toString());
+            context.addServletContainerInitializer(
+                    (classes, servletContext) -> 
servletContext.setAttribute(ActionRegistry.SERVLET_CONTEXT_ATTRIBUTE, registry),
+                    null);
+            Tomcat.addServlet(context, "default", new DefaultServlet());
+            context.addServletMappingDecoded("/", "default");
+            addStrutsFilter(context, filter);
+
+            tomcat.start();
+            assertContextStarted(context, filter);
+            server.openConnector();
+        } catch (Exception e) {
+            ConfigurationException failure = e instanceof 
ConfigurationException configurationException
+                    ? configurationException
+                    : new ConfigurationException("Struts Boot failed to start: 
" + e.getMessage(), e);
+            server.closeAfterFailure(failure);
+            throw failure;
+        } catch (Error e) {
+            server.closeAfterFailure(e);
+            throw e;
+        }
+        Runtime.getRuntime().addShutdownHook(server.shutdownHook);
+        return server;
+    }
+
+    public int port() {
+        return connector.getLocalPort();
+    }
+
+    public String baseUrl() {
+        return "http://"; + urlHost() + ":" + port() + settings.contextPath();
+    }
+
+    public Path baseDir() {
+        return baseDir;
+    }
+
+    public void await() {
+        tomcat.getServer().await();
+    }
+
+    @Override
+    public void close() {
+        if (!closed.compareAndSet(false, true)) {
+            return;
+        }
+        try {
+            tomcat.stop();
+            tomcat.destroy();
+        } catch (Exception e) {
+            throw new ConfigurationException("Struts Boot failed to stop 
Tomcat", e);
+        } finally {
+            deleteRecursively(baseDir);
+            removeShutdownHook();
+        }
+    }
+
+    private void closeAfterFailure(Throwable failure) {
+        try {
+            close();
+        } catch (RuntimeException closeFailure) {
+            failure.addSuppressed(closeFailure);
+        }
+    }
+
+    private void openConnector() {
+        Connector created = new Connector();
+        created.setPort(settings.port());
+        settings.address().ifPresent(address -> {
+            if (!created.setProperty("address", address)) {
+                throw new ConfigurationException("Invalid struts.boot.address: 
'" + address + "'");
+            }
+        });
+        connector = created;
+        tomcat.setConnector(created);
+        if (created.getState() != LifecycleState.STARTED || 
created.getLocalPort() < 0) {
+            throw new ConfigurationException("Struts Boot failed to start: 
connector state "
+                    + created.getState() + " on port " + settings.port());
+        }
+    }
+
+    private String urlHost() {
+        return settings.address()
+                .filter(address -> !address.equals("0.0.0.0") && 
!address.equals("::"))
+                .map(address -> address.contains(":") ? "[" + address + "]" : 
address)
+                .orElse("127.0.0.1");
+    }
+
+    private void removeShutdownHook() {
+        if (Thread.currentThread() == shutdownHook) {
+            return;
+        }
+        try {
+            Runtime.getRuntime().removeShutdownHook(shutdownHook);
+        } catch (IllegalStateException ignored) {
+            // the JVM is already shutting down
+        }
+    }
+
+    private static void assertContextStarted(Context context, 
CapturingStrutsFilter filter) {
+        Throwable initFailure = filter.failure.get();
+        if (initFailure != null || context.getState() != 
LifecycleState.STARTED) {
+            String reason = initFailure == null ? "context state " + 
context.getState() : rootMessage(initFailure);
+            throw new ConfigurationException("Struts Boot failed to start: " + 
reason, initFailure);
+        }
+    }
+
+    private static void hideErrorReportDetails(Tomcat tomcat) {
+        ErrorReportValve valve = new ErrorReportValve();
+        valve.setShowReport(false);
+        valve.setShowServerInfo(false);
+        tomcat.getHost().getPipeline().addValve(valve);
+    }
+
+    private static void addStrutsFilter(Context context, CapturingStrutsFilter 
filter) {
+        FilterDef filterDef = new FilterDef();
+        filterDef.setFilterName(FILTER_NAME);
+        filterDef.setFilter(filter);
+        filterDef.setFilterClass(filter.getClass().getName());
+        filterDef.addInitParameter("configProviders", 
RegistryConfigurationProvider.class.getName());
+        context.addFilterDef(filterDef);
+        FilterMap filterMap = new FilterMap();
+        filterMap.setFilterName(FILTER_NAME);
+        filterMap.addURLPattern("/*");
+        context.addFilterMap(filterMap);
+    }
+
+    private static Path createBaseDir() {
+        try {
+            return Files.createTempDirectory("struts-boot-" + 
UUID.randomUUID());

Review Comment:
   ## SonarCloud / Temporary files should not be created in publicly writable 
directories
   
   <!--SONAR_ISSUE_KEY:AaFo8azi8N41xjDym7t_-->Make sure publicly writable 
directories are used safely here. <p>See more on <a 
href="https://sonarcloud.io/project/issues?id=apache_struts&issues=AaFo8azi8N41xjDym7t_&open=AaFo8azi8N41xjDym7t_&pullRequest=2053";>SonarQube
 Cloud</a></p>
   
   [Show more 
details](https://github.com/apache/struts/security/code-scanning/1508)





Issue Time Tracking
-------------------

    Worklog Id:     (was: 1049686)
    Time Spent: 0.5h  (was: 20m)

> Struts Boot: run a Struts application from main() with an embedded container 
> and programmatic action registration
> -----------------------------------------------------------------------------------------------------------------
>
>                 Key: WW-5791
>                 URL: https://issues.apache.org/jira/browse/WW-5791
>             Project: Struts 2
>          Issue Type: New Feature
>          Components: Core
>            Reporter: Lukasz Lenart
>            Assignee: Lukasz Lenart
>            Priority: Major
>             Fix For: 7.5.0
>
>          Time Spent: 0.5h
>  Remaining Estimate: 0h
>
> h2. Goal
> Give newcomers a working Struts application with almost no setup: add one 
> dependency, write an action class, run {{main()}}, see the page. No 
> {{web.xml}}, no {{struts.xml}}, no external servlet container. Standalone — 
> no Spring dependency, no changes to core.
> h2. Scope
> * New {{struts2-boot-starter}} artifact (module {{boot/}}, listed in the BOM) 
> pulling in core and embedded Tomcat, registering 
> {{StrutsPrepareAndExecuteFilter}} in code. Tomcat follows core's Jakarta 
> line: 10.1 by default, 11 under {{-Pjakartaee11}}.
> * {{StrutsBoot.run(App.class, args)}} entry point (blocks); 
> {{StrutsBoot.start(...)}} returns a closeable server handle for tests. The 
> application class implements {{StrutsBootApplication}}.
> * Fluent action registration, fed into the configuration through a 
> {{ConfigurationProvider}} hooked in via the existing {{configProviders}} 
> filter init parameter. No Convention plugin, no classpath scanning.
> {code:java}public class HelloApp implements StrutsBootApplication {
>     public static void main(String[] args) {
>         StrutsBoot.run(HelloApp.class, args);
>     }
>     @Override
>     public void actions(ActionRegistry actions) {
>         actions.action("hello", HelloAction.class)
>                .success("hello.ftl");
>         actions.namespace("/admin", admin -> {
>             admin.action("users", UserAction.class)
>                  .method("list")
>                  .success("admin/users.ftl");
>             admin.action("saveUser", UserAction.class)
>                  .method("save")
>                  .httpMethods(POST)
>                  .result("input", "admin/userForm.ftl")
>                  .redirectAction("success", "users");
>         });
>     }
> }
> {code}
> * Defaults: each namespace is a package extending {{struts-default}}; top 
> level is namespace {{""}}; {{method}} defaults to {{execute}}; 
> {{success(path)}} is shorthand for {{result("success", path)}}; interceptors 
> default to {{defaultStack}}, overridable per action or namespace by stack 
> name; {{httpMethods(...)}} maps to the http-method restriction from WW-3717.
> * Registry config is never broader than the equivalent {{struts.xml}}: Strict 
> Method Invocation stays on, action classes go into the OGNL allowlist like 
> XML-declared ones, and each action allows only its registered method.
> * Views: FreeMarker only, chosen by {{.ftl}} extension, resolved from 
> {{classpath:/templates/}}.
> * Startup fails with a {{ConfigurationException}} for: slashes in action 
> names (use namespaces), non-{{.ftl}} views, missing templates, unknown 
> interceptor stacks, non-public action classes or ones without a public no-arg 
> constructor, and duplicate actions in the same namespace (within the registry 
> or against {{struts.xml}}).
> * Configuration: the existing {{struts.properties}}; Boot settings 
> {{struts.boot.port}} (default 8080), {{struts.boot.address}} (default: all 
> interfaces) and {{struts.boot.context-path}} (default empty), overridable by 
> {{--key=value}} arguments. Not {{application.properties}}, to avoid clashing 
> with Spring Boot.
> * Container defaults: error pages carry no stack trace or server version; 
> {{DefaultServlet}} serves only an empty per-run docBase; Tomcat's base 
> directory is a fresh temp directory deleted on shutdown; {{devMode}} is never 
> switched on.
> * A {{struts.xml}} on the classpath is still loaded alongside the registry.
> * Example app {{apps/boot-hello}}, shaded into a runnable jar with 
> {{maven-shade-plugin}}; not deployed to Maven Central.
> * {{THREAT_MODEL.md}} gains a Boot section: the starter's container defaults 
> are in model.
> h2. Parked for later
> * Running an existing Struts WAR as a self-contained jar (migration path).
> * JSP views in the embedded container.
> * Gradle plugin.
> * Alternative containers (Jetty, Undertow).
> * Fluent definition of custom interceptor stacks.
> * Global results, global exception mappings, wildcard actions in the registry.
> * Optional bridge to Convention plugin annotations.
> * Dev-mode reload of templates and registry.
> * Project generator / quick-start archetype.
> * Health and info endpoints.
> * A dedicated Maven plugin or nested-jar launcher, needed before plugins can 
> be used in Boot apps (shading overwrites duplicate {{struts-plugin.xml}} 
> files).



--
This message was sent by Atlassian Jira
(v10.3.26#10030026)

Reply via email to