Repository: tipsy/javalin (kotlin)

Issue:
Aliased Files and Symbolic links
Hi! I need to serve static files which can be symbolic links, but Javalin can't see them if I simply add
`config.addStaticFiles("path/to/files/", Location.EXTERNAL);`
As I know static files are processes directly by Jetty, right? How can I update Jetty handlers via Javalin API to allow symbolic linked files? Thank you!

Pull request:
Resolves #1086
Added possibility to set alias checks to serve static files. Because `ResourceHandler` was called manually it was impossible to attach context to enable jetty's alias checks. So I re-implemented `ResourceHandler.getResource()` to check aliases set by user in `StaticFileConfig` during configuring Javalin config.

**Example:**
```
Javalin.create(config -> {
    // will serve successfully all aliased/linked files
    config.addStaticFiles("src/test/external/", Location.EXTERNAL, Collections.singletonList(new ContextHandler.ApproveAliases()));
    
   // will server only checked by custom rules aliased/linked files
   config.addStaticFiles("src/test/external/", Location.EXTERNAL, Collections.singletonList(new MyAliasCheck()));

   // won't server aliased/linked files
   config.addStaticFiles("src/test/external/", Location.EXTERNAL);
}).start(7070);
```

Patch:
diff --git a/javalin/src/main/java/io/javalin/core/JavalinConfig.java b/javalin/src/main/java/io/javalin/core/JavalinConfig.java
index f4bdd05c3..38aaf9950 100644
--- a/javalin/src/main/java/io/javalin/core/JavalinConfig.java
+++ b/javalin/src/main/java/io/javalin/core/JavalinConfig.java
@@ -27,13 +27,17 @@
 import io.javalin.http.staticfiles.ResourceHandler;
 import io.javalin.http.staticfiles.StaticFileConfig;
 import io.javalin.websocket.WsHandler;
+
+import java.util.Collections;
 import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
 import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.function.Consumer;
 import java.util.function.Supplier;
 import java.util.stream.Stream;
 import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.server.handler.ContextHandler;
 import org.eclipse.jetty.server.session.SessionHandler;
 import org.eclipse.jetty.servlet.ServletContextHandler;
 import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory;
@@ -49,6 +53,7 @@ public class JavalinConfig {
     public boolean prefer405over404 = false;
     public boolean enforceSsl = false;
     public boolean precompressStaticFiles = false;
+    public ContextHandler.AliasCheck aliasCheckForStaticFiles = null;
     public boolean showJavalinBanner = true;
     public boolean logIfServerNotStarted = true;
     public boolean ignoreTrailingSlashes = true;
@@ -119,7 +124,7 @@ public JavalinConfig addStaticFiles(@NotNull String path, @NotNull Location loca
     public JavalinConfig addStaticFiles(@NotNull String urlPathPrefix, @NotNull String path, @NotNull Location location) {
         JettyUtil.disableJettyLogger();
         if (inner.resourceHandler == null)
-            inner.resourceHandler = new JettyResourceHandler(precompressStaticFiles);
+            inner.resourceHandler = new JettyResourceHandler(precompressStaticFiles, aliasCheckForStaticFiles);
         inner.resourceHandler.addStaticFileConfig(new StaticFileConfig(urlPathPrefix, path, location));
         return this;
     }
diff --git a/javalin/src/main/java/io/javalin/http/staticfiles/JettyResourceHandler.kt b/javalin/src/main/java/io/javalin/http/staticfiles/JettyResourceHandler.kt
index 722d206a1..e5bd07211 100644
--- a/javalin/src/main/java/io/javalin/http/staticfiles/JettyResourceHandler.kt
+++ b/javalin/src/main/java/io/javalin/http/staticfiles/JettyResourceHandler.kt
@@ -11,26 +11,33 @@ import io.javalin.core.util.Header
 import io.javalin.core.util.Util
 import io.javalin.http.JavalinResponseWrapper
 import org.eclipse.jetty.server.Request
+import org.eclipse.jetty.server.handler.ContextHandler.AliasCheck
 import org.eclipse.jetty.server.handler.ResourceHandler
+import org.eclipse.jetty.util.URIUtil
 import org.eclipse.jetty.util.resource.EmptyResource
 import org.eclipse.jetty.util.resource.Resource
 import java.io.File
 import javax.servlet.http.HttpServletRequest
 import javax.servlet.http.HttpServletResponse
 
-class JettyResourceHandler(val precompressStaticFiles: Boolean = false) : io.javalin.http.staticfiles.ResourceHandler {
+class JettyResourceHandler(val precompressStaticFiles: Boolean = false, val aliasCheck: AliasCheck? = null) : io.javalin.http.staticfiles.ResourceHandler {
 
     val handlers = mutableListOf<ResourceHandler>()
 
     override fun addStaticFileConfig(config: StaticFileConfig) {
-        val handler = if (config.path == "/webjars") WebjarHandler() else PrefixableHandler(config.urlPathPrefix).apply {
-            resourceBase = getResourcePath(config)
-            isDirAllowed = false
-            isEtags = true
-            Javalin.log?.info("""Static file handler added:
+        val handler = if (config.path == "/webjars") {
+            WebjarHandler()
+        } else {
+            val handler = if (aliasCheck != null) AliasResourceHandler(config.urlPathPrefix, aliasCheck) else PrefixableHandler(config.urlPathPrefix)
+            handler.apply {
+                resourceBase = getResourcePath(config)
+                isDirAllowed = false
+                isEtags = true
+                Javalin.log?.info("""Static file handler added:
                 |    {urlPathPrefix: "${config.urlPathPrefix}", path: "${config.path}", location: Location.${config.location}}
                 |    Resolved path: '${getResourcePath(config)}'
                 """.trimMargin())
+            }
         }
         handlers.add(handler.apply { start() })
     }
@@ -39,16 +46,43 @@ class JettyResourceHandler(val precompressStaticFiles: Boolean = false) : io.jav
         override fun getResource(path: String) = Resource.newClassPathResource("META-INF/resources$path") ?: super.getResource(path)
     }
 
-    inner class PrefixableHandler(private var urlPathPrefix: String) : ResourceHandler() {
+    open inner class PrefixableHandler(private var urlPathPrefix: String) : ResourceHandler() {
         override fun getResource(path: String): Resource {
             val targetResource by lazy { path.removePrefix(urlPathPrefix) }
             return when {
-                urlPathPrefix == "/" -> super.getResource(path) // same as regular ResourceHandler
-                targetResource == "" -> super.getResource("/") // directory without trailing '/'
+                urlPathPrefix == "/" -> getResourceFromPath(path)!!
+                targetResource == "" -> getResourceFromPath("/")!! // directory without trailing '/'
                 !path.startsWith(urlPathPrefix) -> EmptyResource.INSTANCE
                 !targetResource.startsWith("/") -> EmptyResource.INSTANCE
-                else -> super.getResource(targetResource)
+                else -> getResourceFromPath(targetResource)!!
+            }
+        }
+
+        // same as regular ResourceHandler
+        open fun getResourceFromPath(path: String): Resource? {
+            return super.getResource(path)
+        }
+    }
+
+    inner class AliasResourceHandler(urlPathPrefix: String, private val aliasCheck: AliasCheck) : PrefixableHandler(urlPathPrefix) {
+
+        // same as regular ResourceHandler but with alias checks
+        override fun getResourceFromPath(path: String) : Resource? {
+            try {
+                var resource: Resource? = null
+                if (baseResource != null) {
+                    val caconicalPath = URIUtil.canonicalPath(path)
+                    resource = baseResource.addPath(caconicalPath)
+                    if (resource != null && resource.isAlias && !aliasCheck.check(path, resource)) {
+                        return null
+                    }
+                }
+                if ((resource == null || !resource.exists()) && path.endsWith("/jetty-dir.css")) resource = stylesheet
+                return resource!!
+            } catch (ignored: java.lang.Exception) {
+
             }
+            return null
         }
     }
 


Test patch:
diff --git a/javalin/src/test/external/txt.txt b/javalin/src/test/external/txt.txt
new file mode 100644
index 000000000..2e85aeda3
--- /dev/null
+++ b/javalin/src/test/external/txt.txt
@@ -0,0 +1,1 @@
+Sample text
diff --git a/javalin/src/test/java/io/javalin/TestStaticFiles.kt b/javalin/src/test/java/io/javalin/TestStaticFiles.kt
index 180e39ae8..356c55934 100644
--- a/javalin/src/test/java/io/javalin/TestStaticFiles.kt
+++ b/javalin/src/test/java/io/javalin/TestStaticFiles.kt
@@ -14,15 +14,15 @@ import io.javalin.http.staticfiles.Location
 import io.javalin.testing.TestUtil
 import org.assertj.core.api.Assertions.assertThat
 import org.eclipse.jetty.server.ServletResponseHttpWrapper
+import org.eclipse.jetty.server.handler.ContextHandler
 import org.eclipse.jetty.servlet.FilterHolder
 import org.junit.Test
+import java.io.File
+import java.io.IOException
+import java.nio.file.Files
+import java.nio.file.Paths
 import java.util.*
-import javax.servlet.DispatcherType
-import javax.servlet.Filter
-import javax.servlet.FilterChain
-import javax.servlet.FilterConfig
-import javax.servlet.ServletRequest
-import javax.servlet.ServletResponse
+import javax.servlet.*
 
 class TestStaticFiles {
 
@@ -63,6 +63,61 @@ class TestStaticFiles {
         }
     }
 
+    private val staticWithAliasResourceApp: Javalin by lazy {
+        Javalin.create { servlet ->
+            // block aliases for txt files
+            servlet.aliasCheckForStaticFiles = ContextHandler.AliasCheck { path, resource -> !path.endsWith(".txt") }
+            servlet.addStaticFiles("src/test/external/", Location.EXTERNAL)
+        }
+    }
+
+    private val staticNoAliasCheckResourceApp: Javalin by lazy {
+        Javalin.create { servlet ->
+            servlet.addStaticFiles("src/test/external/", Location.EXTERNAL)
+        }
+    }
+
+    private fun createSymLink(targetPath: String, linkPath: String): File? {
+        val target = Paths.get(targetPath).toAbsolutePath()
+        val link = Paths.get(linkPath).toAbsolutePath()
+        // delete before creating new link
+        link.toFile().delete()
+        try {
+            Files.createSymbolicLink(link, target)
+        } catch (e: IOException) {
+            e.printStackTrace()
+            return null
+        }
+        return link.toFile()
+    }
+
+    @Test
+    fun `alias checks for static files should work`() {
+        val createdHtml = createSymLink("src/test/external/html.html", "src/test/external/linked_html.html")
+        if (createdHtml != null) {
+            val createdTxt = createSymLink("src/test/external/txt.txt", "src/test/external/linked_txt.txt")
+            if (createdTxt != null) {
+                TestUtil.test(staticWithAliasResourceApp) { _, http ->
+                    assertThat(http.get("/linked_html.html").status).isEqualTo(200)
+                    assertThat(http.get("/linked_txt.txt").status).isEqualTo(404)
+                }
+                createdTxt.delete()
+            }
+            createdHtml.delete()
+        }
+    }
+
+    @Test
+    fun `if aliases are not specified returns 404 for linked static file`() {
+        val created = createSymLink("src/test/external/html.html", "src/test/external/linked_html.html")
+        if (created != null) {
+            TestUtil.test(staticNoAliasCheckResourceApp) { _, http ->
+                assertThat(http.get("/linked_html.html").status).isEqualTo(404)
+            }
+            created.delete()
+        }
+    }
+
     @Test
     fun `serving HTML from classpath works`() = TestUtil.test(defaultStaticResourceApp) { _, http ->
         assertThat(http.get("/html.html").status).isEqualTo(200)
diff --git a/javalin/src/test/java/io/javalin/examples/HelloWorldStaticFiles_linked.java b/javalin/src/test/java/io/javalin/examples/HelloWorldStaticFiles_linked.java
new file mode 100644
index 000000000..ec2089384
--- /dev/null
+++ b/javalin/src/test/java/io/javalin/examples/HelloWorldStaticFiles_linked.java
@@ -0,0 +1,39 @@
+/*
+ * Javalin - https://javalin.io
+ * Copyright 2017 David Åse
+ * Licensed under Apache 2.0: https://github.com/tipsy/javalin/blob/master/LICENSE
+ */
+
+package io.javalin.examples;
+
+import io.javalin.Javalin;
+import io.javalin.http.staticfiles.Location;
+import org.eclipse.jetty.server.handler.ContextHandler;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+
+public class HelloWorldStaticFiles_linked {
+
+    public static void main(String[] args) {
+        createSymLink("src/test/external/html.html", "src/test/external/linked_html.html");
+
+        Javalin.create(config -> {
+            config.aliasCheckForStaticFiles = new ContextHandler.ApproveAliases();
+            config.addStaticFiles("src/test/external/", Location.EXTERNAL);
+        }).start(7070);
+    }
+
+    private static void createSymLink(String targetPath, String linkPath) {
+        Path target = Paths.get(targetPath).toAbsolutePath();
+        Path link = Paths.get(linkPath).toAbsolutePath();
+        try {
+            Files.createSymbolicLink(link, target);
+        } catch (IOException e) {
+            e.printStackTrace();
+        }
+    }
+
+}