Repository: tipsy/javalin (kotlin)

Issue:
"AccessManager" in Javalin 6 is buggy, beforeMatched and before
**Actual behavior (the bug)**
As in the Migrationguide stated, the old AccessManager behaviour can be constructed via the `beforeMatched` method which states to be called even for static files. I have multiple `staticFiles` files enabled, also webjars. `beforeMatched` only gets called for webjars (which seem to be handled by an internal handler) and my manually defined handlers, but not for `/` and `/other`. If i change `beforeMatched` to `before` the behaviour is as expected, called for every request, but `ctx.routeRoles()` is empty, breaking the access manager.

Sample output from the "AccessManager" using `beforeMatched`:
```
2024-05-02 14:28:56,429 INFO  [JettyServerThreadPool-55] App: beforeMatched: [0:0:0:0:0:0:0:1] GET /admin/channels > [ADMIN]
2024-05-02 14:28:56,430 WARN  [JettyServerThreadPool-55] App: access forbidden: GET /admin/channels for [0:0:0:0:0:0:0:1] Chrome
(no /other!)
```
and with `before`:
```
2024-05-02 14:42:44,847 INFO  [JettyServerThreadPool-55] App: before: [0:0:0:0:0:0:0:1] GET /admin/channels > []
2024-05-02 14:42:47,716 INFO  [JettyServerThreadPool-31] App: before: [0:0:0:0:0:0:0:1] GET /other/test.html > []
(other but no roles)
```

**Expected behavior**
beforeMatched to be called for pretty much **EVERY** request.

**To Reproduce**
```java
config.staticFiles.enableWebjars();

config.staticFiles.add(staticFiles -> {
    staticFiles.hostedPath = "/";
    staticFiles.directory = "/public";
    staticFiles.location = Location.CLASSPATH;
});

config.staticFiles.add(staticFiles -> {
    staticFiles.hostedPath = "/other";
    staticFiles.directory = "./path/in/filesystem";
    staticFiles.location = Location.EXTERNAL;
});

// simplified
javalin.beforeMatched(ctx -> {
  LOG.info("beforeMatched: {} {} {} > {}", ctx.ip(), ctx.method(), ctx.path(), ctx.routeRoles());

  if (ctx.routeRoles().isEmpty()) {
      if (ctx.path().startsWith("/other/")) {
          // allow localhost only
          if ("127.0.0.1".equals(ctx.ip())) {
              return;
          }
          throw new ForbiddenResponse();
      }

      LOG.warn("handler {} {} has no access role defined!", ctx.method(), ctx.path());
      return;
  }

  if (ctx.routeRoles().contains(AccessRole.PUBLIC)) {
      return;
  }

  if (!ctx.routeRoles().contains(getLoggedOnRoles(...)) {
      LOG.warn("access forbidden: {} {} for {} {}", ctx.method(), ctx.path(), ctx.ip(), ctx.userAgent());
      ctx.skipRemainingHandlers().redirect("/admin/login");
  }
});
```

**Additional context**
Java 11 with Javalin version 6.1.3.

Pull request:
Closes https://github.com/javalin/javalin/issues/2235

Patch:
diff --git a/javalin/src/main/java/io/javalin/jetty/JettyResourceHandler.kt b/javalin/src/main/java/io/javalin/jetty/JettyResourceHandler.kt
index f4bfd7f69..714e42988 100644
--- a/javalin/src/main/java/io/javalin/jetty/JettyResourceHandler.kt
+++ b/javalin/src/main/java/io/javalin/jetty/JettyResourceHandler.kt
@@ -42,7 +42,7 @@ class JettyResourceHandler(val pvt: PrivateConfig) : JavalinResourceHandler {
         if (pvt.jetty.server?.isStarted == true) handlers.add(ConfigurableHandler(config, pvt.jetty.server!!)) else lateInitConfigs.add(config)
 
     override fun canHandle(ctx: Context) = nonSkippedHandlers(ctx.jettyReq()).any { handler ->
-        return try {
+        try {
             fileOrWelcomeFile(handler, ctx.target) != null
         } catch (e: Exception) {
             e.message?.contains("Rejected alias reference") == true ||  // we want to say these are un-handleable (404)


Test patch:
diff --git a/javalin/src/test/java/io/javalin/TestBeforeAfterMatched.kt b/javalin/src/test/java/io/javalin/TestBeforeAfterMatched.kt
index 45eb897c9..fe6f93561 100644
--- a/javalin/src/test/java/io/javalin/TestBeforeAfterMatched.kt
+++ b/javalin/src/test/java/io/javalin/TestBeforeAfterMatched.kt
@@ -9,6 +9,7 @@ import io.javalin.http.servlet.DefaultTasks.ERROR
 import io.javalin.http.servlet.DefaultTasks.HTTP
 import io.javalin.http.staticfiles.Location
 import io.javalin.security.RouteRole
+import io.javalin.testing.TestDependency
 import io.javalin.testing.TestUtil
 import kong.unirest.HttpResponse
 import org.assertj.core.api.Assertions.assertThat
@@ -222,6 +223,35 @@ class TestBeforeAfterMatched {
         assertThat(res.body).describedAs("body").contains("<h1>HTML works</h1>")
     }
 
+    @Test
+    fun `beforeMatched runs for webjars`() = TestUtil.test(Javalin.create { config ->
+        config.staticFiles.enableWebjars()
+    }) { app, http ->
+        app.beforeMatched { it.header("X-Matched-Before", "123") }
+        app.afterMatched { it.header("X-Matched-After", "456") }
+        val res = http.get("/webjars/swagger-ui/${TestDependency.swaggerVersion}/swagger-ui.css")
+        assertThat(res.headers.getFirst("X-Matched-Before")).describedAs("before-header").isEqualTo("123")
+        assertThat(res.headers.getFirst("X-Matched-After")).describedAs("after-header").isEqualTo("456")
+    }
+
+    @Test
+    fun `beforeMatched runs for every ResourceHandler`() = TestUtil.test(Javalin.create { config ->
+        config.staticFiles.add("/public/subdir", Location.CLASSPATH)
+        config.staticFiles.add("/public/assets", Location.CLASSPATH)
+        config.staticFiles.add("src/test/external/", Location.EXTERNAL)
+    }) { app, http ->
+        app.beforeMatched { it.header("X-Matched-Before", "abc") }
+        app.afterMatched { it.header("X-Matched-After", "xyz") }
+        fun assertHeaders(path: String) {
+            val res = http.get(path)
+            assertThat(res.headers.getFirst("X-Matched-Before")).describedAs("before-header").isEqualTo("abc")
+            assertThat(res.headers.getFirst("X-Matched-After")).describedAs("after-header").isEqualTo("xyz")
+        }
+        assertHeaders("/index.html") // from /public/subdir
+        assertHeaders("/filtered-styles.css") // from /public/assets
+        assertHeaders("/txt.txt") // from external
+    }
+
     @Test
     fun `beforeMatched runs for ResourceHandler - precompress`() = TestUtil.test(Javalin.create { config ->
         config.staticFiles.add {
diff --git a/javalin/src/test/java/io/javalin/testing/WebDriverUtil.kt b/javalin/src/test/java/io/javalin/testing/WebDriverUtil.kt
index 4b399d8cc..363931185 100644
--- a/javalin/src/test/java/io/javalin/testing/WebDriverUtil.kt
+++ b/javalin/src/test/java/io/javalin/testing/WebDriverUtil.kt
@@ -15,6 +15,9 @@ object WebDriverUtil {
         if (TestEnvironment.isNotCiServer && Math.random() < 0.8) {
             assumeTrue(false, "Skipping random WebDriver tests because we're not on a CI server and the tests are sooo slow")
         }
+        if (TestEnvironment.isCiServer && TestEnvironment.isWindows) { // TODO: remove this when flakiness is fixed
+            assumeTrue(false, "Skipping WebDriver tests on Windows CI server because they are flaky")
+        }
         return ChromeDriver(ChromeOptions().apply {
             addArguments("--no-sandbox")
             addArguments("--headless=new")