-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* Merge pull request #66 from SentryMan/internals Refactor Error Handling registration * static resources * use interface * rename * work with jlink * doc * add another assert * Update StaticResourceHandlerBuilder.java * Update README.md * get built in mime types * get exact size * simplify builder * Update JdkServerStart.java * Update StaticResourceHandlerBuilder.java * handle jar uris * Update StaticResourceHandlerBuilder.java * fix jars
- Loading branch information
Showing
33 changed files
with
939 additions
and
28 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -23,6 +23,3 @@ var app = Jex.create() | |
.port(8080) | ||
.start(); | ||
``` | ||
|
||
### TODO | ||
- static file configuration |
69 changes: 69 additions & 0 deletions
69
avaje-jex/src/main/java/io/avaje/jex/AbstractStaticHandler.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
package io.avaje.jex; | ||
|
||
import java.net.FileNameMap; | ||
import java.net.URLConnection; | ||
import java.util.Map; | ||
import java.util.Objects; | ||
import java.util.function.Predicate; | ||
|
||
import com.sun.net.httpserver.HttpExchange; | ||
|
||
import io.avaje.jex.http.BadRequestException; | ||
import io.avaje.jex.http.NotFoundException; | ||
|
||
abstract sealed class AbstractStaticHandler implements ExchangeHandler | ||
permits StaticFileHandler, PathResourceHandler, JarResourceHandler { | ||
|
||
protected final Map<String, String> mimeTypes; | ||
protected final String filesystemRoot; | ||
protected final String urlPrefix; | ||
protected final Predicate<Context> skipFilePredicate; | ||
protected final Map<String, String> headers; | ||
private static final FileNameMap MIME_MAP = URLConnection.getFileNameMap(); | ||
|
||
protected AbstractStaticHandler( | ||
String urlPrefix, | ||
String filesystemRoot, | ||
Map<String, String> mimeTypes, | ||
Map<String, String> headers, | ||
Predicate<Context> skipFilePredicate) { | ||
this.filesystemRoot = filesystemRoot; | ||
this.urlPrefix = urlPrefix; | ||
this.skipFilePredicate = skipFilePredicate; | ||
this.headers = headers; | ||
this.mimeTypes = mimeTypes; | ||
} | ||
|
||
protected void throw404(HttpExchange jdkExchange) { | ||
throw new NotFoundException("File Not Found for request: " + jdkExchange.getRequestURI()); | ||
} | ||
|
||
// This is one function to avoid giving away where we failed | ||
protected void reportPathTraversal() { | ||
throw new BadRequestException("Path traversal attempt detected"); | ||
} | ||
|
||
protected String getExt(String path) { | ||
int slashIndex = path.lastIndexOf('/'); | ||
String basename = (slashIndex < 0) ? path : path.substring(slashIndex + 1); | ||
|
||
int dotIndex = basename.lastIndexOf('.'); | ||
if (dotIndex >= 0) { | ||
return basename.substring(dotIndex + 1); | ||
} else { | ||
return ""; | ||
} | ||
} | ||
|
||
protected String lookupMime(String path) { | ||
var lower = path.toLowerCase(); | ||
|
||
return Objects.requireNonNullElseGet( | ||
MIME_MAP.getContentTypeFor(path), | ||
() -> { | ||
String ext = getExt(lower); | ||
|
||
return mimeTypes.getOrDefault(ext, "application/octet-stream"); | ||
}); | ||
} | ||
} |
25 changes: 25 additions & 0 deletions
25
avaje-jex/src/main/java/io/avaje/jex/ClassResourceLoader.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
package io.avaje.jex; | ||
|
||
import java.io.InputStream; | ||
import java.net.URL; | ||
|
||
/** | ||
* Loading resources from the classpath or module path. | ||
* | ||
* <p>When not specified Avaje Jex provides a default implementation that looks to find resources | ||
* using the class loader associated with the ClassResourceLoader. | ||
* | ||
* <p>As a fallback, {@link ClassLoader#getSystemResourceAsStream(String)} is used if the loader returns null. | ||
*/ | ||
public interface ClassResourceLoader { | ||
|
||
static ClassResourceLoader fromClass(Class<?> clazz) { | ||
|
||
return new DefaultResourceLoader(clazz); | ||
} | ||
|
||
/** Return the URL for the given resource or return null if it cannot be found. */ | ||
URL getResource(String resourcePath); | ||
|
||
InputStream getResourceAsStream(String resourcePath); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
47 changes: 47 additions & 0 deletions
47
avaje-jex/src/main/java/io/avaje/jex/DefaultResourceLoader.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
package io.avaje.jex; | ||
|
||
import java.io.InputStream; | ||
import java.net.URL; | ||
import java.util.Objects; | ||
import java.util.Optional; | ||
|
||
final class DefaultResourceLoader implements ClassResourceLoader { | ||
|
||
private final Class<?> clazz; | ||
|
||
DefaultResourceLoader(Class<?> clazz) { | ||
|
||
this.clazz = clazz; | ||
} | ||
|
||
@Override | ||
public URL getResource(String resourcePath) { | ||
|
||
var url = clazz.getResource(resourcePath); | ||
if (url == null) { | ||
// search the module path for top level resource | ||
url = | ||
Optional.ofNullable(ClassLoader.getSystemResource(resourcePath)) | ||
.orElseGet( | ||
() -> Thread.currentThread().getContextClassLoader().getResource(resourcePath)); | ||
} | ||
return Objects.requireNonNull(url, "Unable to locate resource: " + resourcePath); | ||
} | ||
|
||
@Override | ||
public InputStream getResourceAsStream(String resourcePath) { | ||
|
||
var url = clazz.getResourceAsStream(resourcePath); | ||
if (url == null) { | ||
// search the module path for top level resource | ||
url = | ||
Optional.ofNullable(ClassLoader.getSystemResourceAsStream(resourcePath)) | ||
.orElseGet( | ||
() -> | ||
Thread.currentThread() | ||
.getContextClassLoader() | ||
.getResourceAsStream(resourcePath)); | ||
} | ||
return Objects.requireNonNull(url, "Unable to locate resource: " + resourcePath); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
80 changes: 80 additions & 0 deletions
80
avaje-jex/src/main/java/io/avaje/jex/JarResourceHandler.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,80 @@ | ||
package io.avaje.jex; | ||
|
||
import java.io.IOException; | ||
import java.net.URL; | ||
import java.nio.file.Paths; | ||
import java.util.Map; | ||
import java.util.function.Predicate; | ||
|
||
final class JarResourceHandler extends AbstractStaticHandler implements ExchangeHandler { | ||
|
||
private final URL indexFile; | ||
private final URL singleFile; | ||
private final ClassResourceLoader resourceLoader; | ||
|
||
JarResourceHandler( | ||
String urlPrefix, | ||
String filesystemRoot, | ||
Map<String, String> mimeTypes, | ||
Map<String, String> headers, | ||
Predicate<Context> skipFilePredicate, | ||
ClassResourceLoader resourceLoader, | ||
URL indexFile, | ||
URL singleFile) { | ||
super(urlPrefix, filesystemRoot, mimeTypes, headers, skipFilePredicate); | ||
|
||
this.resourceLoader = resourceLoader; | ||
this.indexFile = indexFile; | ||
this.singleFile = singleFile; | ||
} | ||
|
||
@Override | ||
public void handle(Context ctx) throws IOException { | ||
|
||
final var jdkExchange = ctx.jdkExchange(); | ||
|
||
if (singleFile != null) { | ||
sendURL(ctx, singleFile.getPath(), singleFile); | ||
return; | ||
} | ||
|
||
if (skipFilePredicate.test(ctx)) { | ||
throw404(jdkExchange); | ||
} | ||
|
||
final String wholeUrlPath = jdkExchange.getRequestURI().getPath(); | ||
|
||
if (wholeUrlPath.endsWith("/") || wholeUrlPath.equals(urlPrefix)) { | ||
sendURL(ctx, indexFile.getPath(), indexFile); | ||
return; | ||
} | ||
|
||
final String urlPath = wholeUrlPath.substring(urlPrefix.length()); | ||
|
||
final String normalizedPath = | ||
Paths.get(filesystemRoot, urlPath).normalize().toString().replace("\\", "/"); | ||
|
||
if (!normalizedPath.startsWith(filesystemRoot)) { | ||
reportPathTraversal(); | ||
} | ||
|
||
try (var fis = resourceLoader.getResourceAsStream(normalizedPath)) { | ||
ctx.header("Content-Type", lookupMime(normalizedPath)); | ||
ctx.headers(headers); | ||
ctx.write(fis); | ||
} catch (final Exception e) { | ||
throw404(ctx.jdkExchange()); | ||
} | ||
} | ||
|
||
private void sendURL(Context ctx, String urlPath, URL path) throws IOException { | ||
|
||
try (var fis = path.openStream()) { | ||
ctx.header("Content-Type", lookupMime(urlPath)); | ||
ctx.headers(headers); | ||
ctx.write(fis); | ||
} catch (final Exception e) { | ||
throw404(ctx.jdkExchange()); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
84 changes: 84 additions & 0 deletions
84
avaje-jex/src/main/java/io/avaje/jex/PathResourceHandler.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,84 @@ | ||
package io.avaje.jex; | ||
|
||
import java.io.IOException; | ||
import java.nio.file.Files; | ||
import java.nio.file.Path; | ||
import java.util.Map; | ||
import java.util.function.Predicate; | ||
|
||
final class PathResourceHandler extends AbstractStaticHandler implements ExchangeHandler { | ||
|
||
private final Path indexFile; | ||
private final Path singleFile; | ||
|
||
PathResourceHandler( | ||
String urlPrefix, | ||
String filesystemRoot, | ||
Map<String, String> mimeTypes, | ||
Map<String, String> headers, | ||
Predicate<Context> skipFilePredicate, | ||
Path indexFile, | ||
Path singleFile) { | ||
super(urlPrefix, filesystemRoot, mimeTypes, headers, skipFilePredicate); | ||
|
||
this.indexFile = indexFile; | ||
this.singleFile = singleFile; | ||
} | ||
|
||
@Override | ||
public void handle(Context ctx) throws IOException { | ||
|
||
if (singleFile != null) { | ||
sendPathIS(ctx, singleFile.toString(), singleFile); | ||
return; | ||
} | ||
|
||
final var jdkExchange = ctx.jdkExchange(); | ||
if (skipFilePredicate.test(ctx)) { | ||
throw404(jdkExchange); | ||
} | ||
|
||
final String wholeUrlPath = jdkExchange.getRequestURI().getPath(); | ||
|
||
if (wholeUrlPath.endsWith("/") || wholeUrlPath.equals(urlPrefix)) { | ||
sendPathIS(ctx, indexFile.toString(), indexFile); | ||
|
||
return; | ||
} | ||
|
||
final String urlPath = wholeUrlPath.substring(urlPrefix.length()); | ||
|
||
Path path; | ||
try { | ||
path = Path.of(filesystemRoot, urlPath).toRealPath(); | ||
|
||
} catch (final IOException e) { | ||
// This may be more benign (i.e. not an attack, just a 403), | ||
// but we don't want an attacker to be able to discern the difference. | ||
reportPathTraversal(); | ||
return; | ||
} | ||
|
||
final String canonicalPath = path.toString(); | ||
if (!canonicalPath.startsWith(filesystemRoot)) { | ||
reportPathTraversal(); | ||
} | ||
|
||
sendPathIS(ctx, urlPath, path); | ||
} | ||
|
||
private void sendPathIS(Context ctx, String urlPath, Path path) throws IOException { | ||
final var exchange = ctx.jdkExchange(); | ||
final String mimeType = lookupMime(urlPath); | ||
ctx.header("Content-Type", mimeType); | ||
ctx.headers(headers); | ||
exchange.sendResponseHeaders(200, Files.size(path)); | ||
try (var fis = Files.newInputStream(path); | ||
var os = exchange.getResponseBody()) { | ||
|
||
fis.transferTo(os); | ||
} catch (final Exception e) { | ||
throw404(ctx.jdkExchange()); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
package io.avaje.jex; | ||
|
||
public enum ResourceLocation { | ||
CLASS_PATH, | ||
FILE | ||
} |
Oops, something went wrong.