Skip to content

Commit edb049c

Browse files
authored
Merge pull request #598 from jooby-project/585
Assets from file system location fix #585
2 parents fcf568b + 009a79d commit edb049c

File tree

9 files changed

+193
-54
lines changed

9 files changed

+193
-54
lines changed
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package org.jooby.issues;
2+
3+
import java.nio.file.Paths;
4+
5+
import org.jooby.test.ServerFeature;
6+
import org.junit.Test;
7+
8+
public class Issue585 extends ServerFeature {
9+
10+
{
11+
assets("/static/**", Paths.get("src/test/resources/static"));
12+
}
13+
14+
@Test
15+
public void shouldServeStaticFiles() throws Exception {
16+
request()
17+
.get("/static/images/fun.gif")
18+
.expect(200)
19+
.header("Content-Length", "79530");
20+
21+
request()
22+
.get("/static/images/prey.jpg")
23+
.expect(200)
24+
.header("Content-Length", "39003");
25+
}
26+
27+
@Test
28+
public void shouldNotAllowAccessToResourceOutsideScope() throws Exception {
29+
request()
30+
.get("/static/../forbidden.txt")
31+
.expect(404);
32+
}
33+
}

coverage-report/src/test/resources/forbidden.txt

Whitespace-only changes.
77.7 KB
Loading
38.1 KB
Loading

jooby/src/main/java/org/jooby/Jooby.java

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@
5858
import java.io.File;
5959
import java.lang.reflect.Type;
6060
import java.nio.charset.Charset;
61+
import java.nio.file.Path;
6162
import java.nio.file.Paths;
6263
import java.text.DecimalFormat;
6364
import java.text.NumberFormat;
@@ -1727,21 +1728,16 @@ private Route.Filter filter(final Class<? extends Route.Filter> filter) {
17271728
}
17281729

17291730
@Override
1730-
public Route.Definition assets(final String path) {
1731-
return assets(path, "/");
1731+
public Definition assets(final String path, final Path basedir) {
1732+
AssetHandler handler = new AssetHandler(basedir);
1733+
configureAssetHandler(handler);
1734+
return assets(path, handler);
17321735
}
17331736

17341737
@Override
17351738
public Route.Definition assets(final String path, final String location) {
17361739
AssetHandler handler = new AssetHandler(location);
1737-
onStart(r -> {
1738-
Config conf = r.require(Config.class);
1739-
handler
1740-
.cdn(conf.getString("assets.cdn"))
1741-
.lastModified(conf.getBoolean("assets.lastModified"))
1742-
.etag(conf.getBoolean("assets.etag"))
1743-
.maxAge(conf.getString("assets.cache.maxAge"));
1744-
});
1740+
configureAssetHandler(handler);
17451741
return assets(path, handler);
17461742
}
17471743

@@ -2028,7 +2024,8 @@ public Jooby map(final Mapper<?> mapper) {
20282024
* @param injectorFactory the injection provider
20292025
* @return this instance.
20302026
*/
2031-
public Jooby injector(final BiFunction<Stage, com.google.inject.Module, Injector> injectorFactory) {
2027+
public Jooby injector(
2028+
final BiFunction<Stage, com.google.inject.Module, Injector> injectorFactory) {
20322029
this.injectorFactory = injectorFactory;
20332030
return this;
20342031
}
@@ -3200,4 +3197,15 @@ private static Logger logger(final Jooby app) {
32003197
return LoggerFactory.getLogger(app.getClass());
32013198
}
32023199

3200+
public void configureAssetHandler(final AssetHandler handler) {
3201+
onStart(r -> {
3202+
Config conf = r.require(Config.class);
3203+
handler
3204+
.cdn(conf.getString("assets.cdn"))
3205+
.lastModified(conf.getBoolean("assets.lastModified"))
3206+
.etag(conf.getBoolean("assets.etag"))
3207+
.maxAge(conf.getString("assets.cache.maxAge"));
3208+
});
3209+
}
3210+
32033211
}

jooby/src/main/java/org/jooby/Router.java

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
*/
1919
package org.jooby;
2020

21+
import java.nio.file.Path;
2122
import java.util.Optional;
2223
import java.util.concurrent.Executor;
2324
import java.util.function.Predicate;
@@ -1435,7 +1436,40 @@ Route.Collection delete(String path1,
14351436
* @param path The path to publish.
14361437
* @return A new route definition.
14371438
*/
1438-
Route.Definition assets(String path);
1439+
default Route.Definition assets(final String path) {
1440+
return assets(path, "/");
1441+
}
1442+
1443+
/**
1444+
* Static files handler on external location.
1445+
*
1446+
* <pre>
1447+
* assets("/assets/**", Paths.get("/www"));
1448+
* </pre>
1449+
*
1450+
* For example <code>GET /assets/file.js</code> will be resolve as <code>/www/file.js</code> on
1451+
* server file system.
1452+
*
1453+
* <p>
1454+
* The {@link AssetHandler} one step forward and add support for serving files from a CDN out of
1455+
* the box. All you have to do is to define a <code>assets.cdn</code> property:
1456+
* </p>
1457+
* <pre>
1458+
* assets.cdn = "http://d7471vfo50fqt.cloudfront.net"
1459+
* </pre>
1460+
*
1461+
* A GET to <code>/assets/js/index.js</code> will be redirected to:
1462+
* <code>http://d7471vfo50fqt.cloudfront.net/assets/js/index.js</code>.
1463+
*
1464+
* You can turn on/off <code>ETag</code> and <code>Last-Modified</code> headers too using
1465+
* <code>assets.etag</code> and <code>assets.lastModified</code>. These two properties are enabled
1466+
* by default.
1467+
*
1468+
* @param path The path to publish.
1469+
* @param basedir Base directory.
1470+
* @return A new route definition.
1471+
*/
1472+
Route.Definition assets(final String path, Path basedir);
14391473

14401474
/**
14411475
* Static files handler. Like {@link #assets(String)} but let you specify a different classpath

jooby/src/main/java/org/jooby/handlers/AssetHandler.java

Lines changed: 56 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,11 @@
2020

2121
import static java.util.Objects.requireNonNull;
2222

23-
import java.io.File;
2423
import java.net.MalformedURLException;
2524
import java.net.URL;
26-
import java.net.URLClassLoader;
25+
import java.nio.file.Files;
26+
import java.nio.file.Path;
27+
import java.nio.file.Paths;
2728
import java.text.MessageFormat;
2829
import java.time.Duration;
2930
import java.util.Date;
@@ -84,13 +85,15 @@
8485
*/
8586
public class AssetHandler implements Route.Handler {
8687

87-
private static final Function1<ClassLoader, ClassLoader> cloader = loader().memoized();
88+
private interface Loader {
89+
URL getResource(String name);
90+
}
8891

8992
private static final Function1<String, String> prefix = prefix().memoized();
9093

9194
private Function2<Request, String, String> fn;
9295

93-
private ClassLoader loader;
96+
private Loader loader;
9497

9598
private String cdn;
9699

@@ -128,7 +131,38 @@ public class AssetHandler implements Route.Handler {
128131
* @param loader The one who load the static resources.
129132
*/
130133
public AssetHandler(final String pattern, final ClassLoader loader) {
131-
init(Route.normalize(pattern), loader);
134+
init(Route.normalize(pattern), Paths.get("public"), loader);
135+
}
136+
137+
/**
138+
* <p>
139+
* Creates a new {@link AssetHandler}. The handler accepts a location pattern, that serve for
140+
* locating the static resource.
141+
* </p>
142+
*
143+
* Given <code>assets("/assets/**", "/")</code> with:
144+
*
145+
* <pre>
146+
* GET /assets/js/index.js it translates the path to: /assets/js/index.js
147+
* </pre>
148+
*
149+
* Given <code>assets("/js/**", "/assets")</code> with:
150+
*
151+
* <pre>
152+
* GET /js/index.js it translate the path to: /assets/js/index.js
153+
* </pre>
154+
*
155+
* Given <code>assets("/webjars/**", "/META-INF/resources/webjars/{0}")</code> with:
156+
*
157+
* <pre>
158+
* GET /webjars/jquery/2.1.3/jquery.js it translate the path to: /META-INF/resources/webjars/jquery/2.1.3/jquery.js
159+
* </pre>
160+
*
161+
* @param pattern Pattern to locate static resources.
162+
* @param basedir Base directory.
163+
*/
164+
public AssetHandler(final Path basedir) {
165+
init("/{0}", basedir, getClass().getClassLoader());
132166
}
133167

134168
/**
@@ -158,7 +192,7 @@ public AssetHandler(final String pattern, final ClassLoader loader) {
158192
* @param pattern Pattern to locate static resources.
159193
*/
160194
public AssetHandler(final String pattern) {
161-
init(Route.normalize(pattern), getClass().getClassLoader());
195+
init(Route.normalize(pattern), Paths.get("public"), getClass().getClassLoader());
162196
}
163197

164198
/**
@@ -317,40 +351,34 @@ protected URL resolve(final String path) throws Exception {
317351
return loader.getResource(path);
318352
}
319353

320-
private void init(final String pattern, final ClassLoader loader) {
354+
private void init(final String pattern, final Path basedir, final ClassLoader loader) {
321355
requireNonNull(loader, "Resource loader is required.");
322356
this.fn = pattern.equals("/")
323357
? (req, p) -> prefix.apply(p)
324358
: (req, p) -> MessageFormat.format(prefix.apply(pattern), vars(req));
325-
this.loader = cloader.apply(loader);
359+
this.loader = loader(basedir, loader);
326360
}
327361

328362
private static Object[] vars(final Request req) {
329363
Map<Object, String> vars = req.route().vars();
330364
return vars.values().toArray(new Object[vars.size()]);
331365
}
332366

333-
private static Function1<ClassLoader, ClassLoader> loader() {
334-
return parent -> {
335-
File publicDir = new File("public");
336-
if (publicDir.exists()) {
337-
try {
338-
return new URLClassLoader(new URL[]{publicDir.toURI().toURL() }, null) {
339-
@Override
340-
public URL getResource(final String name) {
341-
URL url = findResource(name);
342-
if (url == null) {
343-
url = parent.getResource(name);
344-
}
345-
return url;
346-
};
347-
};
348-
} catch (MalformedURLException ex) {
349-
// shh
367+
private static Loader loader(final Path basedir, final ClassLoader classloader) {
368+
if (Files.exists(basedir)) {
369+
return name -> {
370+
Path path = basedir.resolve(name).normalize();
371+
if (Files.exists(path) && path.startsWith(basedir)) {
372+
try {
373+
return path.toUri().toURL();
374+
} catch (MalformedURLException x) {
375+
// shh
376+
}
350377
}
351-
}
352-
return parent;
353-
};
378+
return classloader.getResource(name);
379+
};
380+
}
381+
return classloader::getResource;
354382
}
355383

356384
private static Function1<String, String> prefix() {

jooby/src/test/java/org/jooby/handlers/AssetHandlerTest.java

Lines changed: 36 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
import java.net.MalformedURLException;
88
import java.net.URI;
99
import java.net.URL;
10+
import java.nio.file.Files;
11+
import java.nio.file.Path;
1012
import java.nio.file.Paths;
1113

1214
import org.jooby.test.MockUnit;
@@ -17,14 +19,14 @@
1719
import org.powermock.modules.junit4.PowerMockRunner;
1820

1921
@RunWith(PowerMockRunner.class)
20-
@PrepareForTest({AssetHandler.class, File.class })
22+
@PrepareForTest({AssetHandler.class, File.class, Paths.class, Files.class })
2123
public class AssetHandlerTest {
2224

2325
@Test
2426
public void customClassloader() throws Exception {
2527
URI uri = Paths.get("src", "test", "resources", "org", "jooby").toUri();
2628
new MockUnit(ClassLoader.class)
27-
.expect(publicDir(uri))
29+
.expect(publicDir(uri, "JoobyTest.js"))
2830
.run(unit -> {
2931
URL value = new AssetHandler("/", unit.get(ClassLoader.class))
3032
.resolve("JoobyTest.js");
@@ -36,7 +38,7 @@ public void customClassloader() throws Exception {
3638
public void shouldCallParentOnMissing() throws Exception {
3739
URI uri = Paths.get("src", "test", "resources", "org", "jooby").toUri();
3840
new MockUnit(ClassLoader.class)
39-
.expect(publicDir(uri))
41+
.expect(publicDir(uri, "index.js", false))
4042
.expect(unit -> {
4143
ClassLoader loader = unit.get(ClassLoader.class);
4244
expect(loader.getResource("index.js")).andReturn(uri.toURL());
@@ -50,15 +52,16 @@ public void shouldCallParentOnMissing() throws Exception {
5052

5153
@Test
5254
public void ignoreMalformedURL() throws Exception {
55+
Path path = Paths.get("src", "test", "resources", "org", "jooby");
5356
new MockUnit(ClassLoader.class, URI.class)
54-
.expect(publicDir(null))
57+
.expect(publicDir(null, "index.js"))
5558
.expect(unit -> {
5659
URI uri = unit.get(URI.class);
5760
expect(uri.toURL()).andThrow(new MalformedURLException());
5861
})
5962
.expect(unit -> {
6063
ClassLoader loader = unit.get(ClassLoader.class);
61-
expect(loader.getResource("index.js")).andReturn(Paths.get("src", "test", "resources", "org", "jooby").toUri().toURL());
64+
expect(loader.getResource("index.js")).andReturn(path.toUri().toURL());
6265
})
6366
.run(unit -> {
6467
URL value = new AssetHandler("/", unit.get(ClassLoader.class))
@@ -67,18 +70,37 @@ public void ignoreMalformedURL() throws Exception {
6770
});
6871
}
6972

70-
private Block publicDir(final URI uri) {
73+
private Block publicDir(final URI uri, final String name) {
74+
return publicDir(uri, name, true);
75+
}
76+
77+
private Block publicDir(final URI uri, final String name, final boolean exists) {
7178
return unit -> {
72-
File publicDir = unit.constructor(File.class)
73-
.build("public");
74-
expect(publicDir.exists()).andReturn(true);
75-
if (uri != null) {
76-
expect(publicDir.toURI()).andReturn(uri);
77-
} else {
78-
expect(publicDir.toURI()).andReturn(unit.get(URI.class));
79+
unit.mockStatic(Paths.class);
80+
81+
Path basedir = unit.mock(Path.class);
82+
83+
expect(Paths.get("public")).andReturn(basedir);
84+
85+
Path path = unit.mock(Path.class);
86+
expect(basedir.resolve(name)).andReturn(path);
87+
expect(path.normalize()).andReturn(path);
88+
89+
if (exists) {
90+
expect(path.startsWith(basedir)).andReturn(true);
7991
}
8092

81-
unit.registerMock(File.class, publicDir);
93+
unit.mockStatic(Files.class);
94+
expect(Files.exists(basedir)).andReturn(true);
95+
expect(Files.exists(path)).andReturn(exists);
96+
97+
if (exists) {
98+
if (uri != null) {
99+
expect(path.toUri()).andReturn(uri);
100+
} else {
101+
expect(path.toUri()).andReturn(unit.get(URI.class));
102+
}
103+
}
82104
};
83105
}
84106

0 commit comments

Comments
 (0)