diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/AllowSymLinkAliasChecker.java b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/AllowSymLinkAliasChecker.java new file mode 100644 index 00000000000..2157688ca49 --- /dev/null +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/AllowSymLinkAliasChecker.java @@ -0,0 +1,87 @@ +// +// ======================================================================== +// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.server.handler; + +import java.io.File; +import java.net.URI; +import java.nio.file.Files; +import java.nio.file.Path; + +import org.eclipse.jetty.server.handler.ContextHandler.AliasCheck; +import org.eclipse.jetty.util.log.Log; +import org.eclipse.jetty.util.log.Logger; +import org.eclipse.jetty.util.resource.Resource; + +public class AllowSymLinkAliasChecker implements AliasCheck +{ + private static final Logger LOG = Log.getLogger(AllowSymLinkAliasChecker.class); + + @Override + public boolean check(String path, Resource resource) + { + try + { + File file =resource.getFile(); + if (file==null) + return false; + + // If the file exists + if (file.exists()) + { + // we can use the real path method to check the symlinks resolve to the alias + URI real = file.toPath().toRealPath().toUri(); + if (real.equals(resource.getAlias().toURI())) + { + LOG.debug("Allow symlink {} --> {}",resource,real); + return true; + } + } + else + { + // file does not exists, so we have to walk the path and links ourselves. + Path p = file.toPath().toAbsolutePath(); + File d = p.getRoot().toFile(); + for (Path e:p) + { + d=new File(d,e.toString()); + + while (d.exists() && Files.isSymbolicLink(d.toPath())) + { + Path link=Files.readSymbolicLink(d.toPath()); + if (!link.isAbsolute()) + link=link.resolve(d.toPath()); + d=link.toFile().getAbsoluteFile().getCanonicalFile(); + } + } + if (resource.getAlias().toURI().equals(d.toURI())) + { + LOG.debug("Allow symlink {} --> {}",resource,d); + return true; + } + } + } + catch(Exception e) + { + e.printStackTrace(); + LOG.ignore(e); + } + return false; + } + +} \ No newline at end of file diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ContextHandler.java b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ContextHandler.java index b3039445607..91d66baa23a 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ContextHandler.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/ContextHandler.java @@ -40,6 +40,7 @@ import java.util.Map; import java.util.Set; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.Future; + import javax.servlet.DispatcherType; import javax.servlet.Filter; import javax.servlet.FilterRegistration; @@ -1297,7 +1298,7 @@ public class ContextHandler extends ScopedHandler implements Attributes, Gracefu LOG.warn(this+" contextPath ends with /*"); contextPath=contextPath.substring(0,contextPath.length()-2); } - else if (contextPath.endsWith("/")) + else if (contextPath.length()>1 && contextPath.endsWith("/")) { LOG.warn(this+" contextPath ends with /"); contextPath=contextPath.substring(0,contextPath.length()-1); @@ -2651,5 +2652,5 @@ public class ContextHandler extends ScopedHandler implements Attributes, Gracefu return r.startsWith(a) && r.length()==a.length()+1 && r.endsWith("/"); } } - + } diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/handler/ContextHandlerGetResourceTest.java b/jetty-server/src/test/java/org/eclipse/jetty/server/handler/ContextHandlerGetResourceTest.java index 9612d8b1add..2a7ac387100 100644 --- a/jetty-server/src/test/java/org/eclipse/jetty/server/handler/ContextHandlerGetResourceTest.java +++ b/jetty-server/src/test/java/org/eclipse/jetty/server/handler/ContextHandlerGetResourceTest.java @@ -44,7 +44,9 @@ public class ContextHandlerGetResourceTest private static Server server; private static ContextHandler context; private static File docroot; + private static File otherroot; private final static AtomicBoolean allowAliases= new AtomicBoolean(false); + private final static AtomicBoolean allowSymlinks= new AtomicBoolean(false); @BeforeClass public static void beforeClass() throws Exception @@ -60,12 +62,25 @@ public class ContextHandlerGetResourceTest data.createNewFile(); File verylong = new File(sub,"TextFile.Long.txt"); verylong.createNewFile(); + + otherroot = new File("target/tests/otherroot").getCanonicalFile().getAbsoluteFile(); + FS.ensureDirExists(otherroot); + FS.ensureEmpty(otherroot); + File other = new File(otherroot,"other.txt"); + other.createNewFile(); - if (!OS.IS_WINDOWS) + File transit = new File(docroot.getParentFile(),"transit"); + System.err.println("transit "+transit); + transit.delete(); + + if (OS.IS_UNIX) { // Create alias as 8.3 name so same test will produce an alias on both windows an normal systems File eightDotThree=new File(sub,"TEXTFI~1.TXT"); Files.createSymbolicLink(eightDotThree.toPath(),verylong.toPath()); + + Files.createSymbolicLink(new File(docroot,"other").toPath(),new File("../transit").toPath()); + Files.createSymbolicLink(transit.toPath(),otherroot.toPath()); } @@ -74,13 +89,18 @@ public class ContextHandlerGetResourceTest context.setBaseResource(Resource.newResource(docroot)); context.addAliasCheck(new ContextHandler.AliasCheck() { + final AllowSymLinkAliasChecker symlinkcheck = new AllowSymLinkAliasChecker(); @Override public boolean check(String path, Resource resource) { + if (allowAliases.get()) + return true; + if (allowSymlinks.get()) + return symlinkcheck.check(path,resource); return allowAliases.get(); } }); - + server.setHandler(context); server.start(); } @@ -308,4 +328,57 @@ public class ContextHandlerGetResourceTest } } + + @Test + public void testSymlinkKnown() throws Exception + { + if (!OS.IS_UNIX) + return; + try + { + allowSymlinks.set(true); + + final String path="/other/other.txt"; + + Resource resource=context.getResource(path); + assertNotNull(resource); + assertEquals("other.txt",resource.getFile().getName()); + assertEquals(docroot,resource.getFile().getParentFile().getParentFile()); + assertTrue(resource.exists()); + + URL url=context.getServletContext().getResource(path); + assertEquals(docroot,new File(url.toURI()).getParentFile().getParentFile()); + } + finally + { + allowSymlinks.set(false); + } + + } + + @Test + public void testSymlinkUnknown() throws Exception + { + if (!OS.IS_UNIX) + return; + try + { + allowSymlinks.set(true); + + final String path="/other/unknown.txt"; + + Resource resource=context.getResource(path); + assertNotNull(resource); + assertEquals("unknown.txt",resource.getFile().getName()); + assertEquals(docroot,resource.getFile().getParentFile().getParentFile()); + assertFalse(resource.exists()); + + URL url=context.getServletContext().getResource(path); + assertNull(url); + } + finally + { + allowSymlinks.set(false); + } + } }