Skip to content

Commit

Permalink
Merge pull request #638 from danfickle/external_resource_access_control
Browse files Browse the repository at this point in the history
Implement external access control based on previous PR
  • Loading branch information
danfickle committed Jan 20, 2021
2 parents 78cdb00 + 79f7416 commit d20825b
Show file tree
Hide file tree
Showing 18 changed files with 240 additions and 24 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
*/
package com.openhtmltopdf.extend;

import com.openhtmltopdf.outputdevice.helper.ExternalResourceType;
import com.openhtmltopdf.resource.CSSResource;
import com.openhtmltopdf.resource.ImageResource;
import com.openhtmltopdf.resource.XMLResource;
Expand Down Expand Up @@ -51,29 +52,57 @@ public interface UserAgentCallback {
* @param uri Location of the CSS
* @return A CSSResource for the content at the URI.
*/
CSSResource getCSSResource(String uri);
default CSSResource getCSSResource(String uri) {
return getCSSResource(uri, ExternalResourceType.CSS);
}

CSSResource getCSSResource(String uri, ExternalResourceType type);

/**
* Retrieves the Image at the given URI. This is a synchronous call.
*
* @param uri Location of the image
* @return An ImageResource for the content at the URI.
*/
ImageResource getImageResource(String uri);
default ImageResource getImageResource(String uri) {
return getImageResource(uri, ExternalResourceType.IMAGE_RASTER);
}

ImageResource getImageResource(String uri, ExternalResourceType type);


/**
* @deprecated
* Use {@link #getXMLResource(String, ExternalResourceType)} instead.
*/
@Deprecated
default XMLResource getXMLResource(String uri) {
return getXMLResource(uri, ExternalResourceType.XML_XHTML);
}

/**
* Retrieves the XML at the given URI. This is a synchronous call.
*
* @param uri Location of the XML
* @param type Either xhtml or svg.
* @return A XMLResource for the content at the URI.
*/
XMLResource getXMLResource(String uri);

XMLResource getXMLResource(String uri, ExternalResourceType type);

/**
* @deprecated
* Use {@link #getBinaryResource(String, ExternalResourceType)} instead.
*/
@Deprecated
default byte[] getBinaryResource(String uri) {
return getBinaryResource(uri, ExternalResourceType.BINARY);
}

/**
* Retrieves a binary resource located at a given URI and returns its contents
* as a byte array or <code>null</code> if the resource could not be loaded.
*/
byte[] getBinaryResource(String uri);
byte[] getBinaryResource(String uri, ExternalResourceType type);

/**
* Normally, returns true if the user agent has visited this URI. UserAgent should consider
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.BiPredicate;
import java.util.function.Consumer;

/**
Expand All @@ -37,6 +38,10 @@ public abstract class BaseRendererBuilder<TFinalClass extends BaseRendererBuilde
public abstract static class BaseRendererBuilderState {
public final List<AddedFont> _fonts = new ArrayList<>();
public final List<FSDOMMutator> _domMutators = new ArrayList<>();

public BiPredicate<String, ExternalResourceType> _beforeAccessController = new NaiveUserAgent.DefaultAccessController();
public BiPredicate<String, ExternalResourceType> _afterAccessController = new NaiveUserAgent.DefaultAccessController();

public final Map<String, FSStreamFactory> _streamFactoryMap = new HashMap<>();
public FSUriResolver _resolver;
public String _html;
Expand Down Expand Up @@ -560,6 +565,38 @@ protected Closeable applyDiagnosticConsumer() {
return ThreadCtx.applyDiagnosticConsumer(state._diagnosticConsumer);
}

/**
* Allows to set <strong>one</strong> external access controller to run
* before the uri resolver and one to run after the uri resolver.
*
* The predicate will receive the uri and the resource type. If it returns
* false, the resource will not be loaded but the rendering process will
* attempt to continue without the resource.
*
* A default controller is registered that allows everything except file
* embed resources. To override the default controller, register a controller
* with after priority.
*/
public TFinalClass useExternalResourceAccessControl(
BiPredicate<String, ExternalResourceType> allowExternalResource,
ExternalResourceControlPriority priority) {
if (priority == null) {
// Default is after as safest.
priority = ExternalResourceControlPriority.RUN_AFTER_RESOLVING_URI;
}

switch (priority) {
case RUN_AFTER_RESOLVING_URI:
state._afterAccessController = allowExternalResource;
break;
case RUN_BEFORE_RESOLVING_URI:
state._beforeAccessController = allowExternalResource;
break;
}

return (TFinalClass) this;
}

public enum TextDirection {
RTL, LTR
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.openhtmltopdf.outputdevice.helper;

public enum ExternalResourceControlPriority {
RUN_BEFORE_RESOLVING_URI,
RUN_AFTER_RESOLVING_URI;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.openhtmltopdf.outputdevice.helper;

public enum ExternalResourceType {
FONT,
FILE_EMBED,
CSS,
IMAGE_RASTER,
XML_XHTML,
XML_SVG,
BINARY,
PDF,
SVG_BINARY;
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ public FontFaceFontSupplier(SharedContext ctx, String src) {

@Override
public InputStream supply() {
byte[] font1 = ctx.getUserAgentCallback().getBinaryResource(src);
byte[] font1 = ctx.getUserAgentCallback().getBinaryResource(src, ExternalResourceType.FONT);

if (font1 == null) {
XRLog.log(Level.WARNING, LogMessageId.LogMessageId1Param.EXCEPTION_COULD_NOT_LOAD_FONT_FACE, src);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,11 @@
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.function.BiPredicate;
import java.util.logging.Level;

import javax.imageio.ImageIO;
Expand All @@ -38,6 +40,8 @@
import com.openhtmltopdf.extend.FSStreamFactory;
import com.openhtmltopdf.extend.FSStream;
import com.openhtmltopdf.extend.UserAgentCallback;
import com.openhtmltopdf.outputdevice.helper.ExternalResourceControlPriority;
import com.openhtmltopdf.outputdevice.helper.ExternalResourceType;
import com.openhtmltopdf.resource.CSSResource;
import com.openhtmltopdf.resource.ImageResource;
import com.openhtmltopdf.resource.XMLResource;
Expand All @@ -63,6 +67,8 @@ public class NaiveUserAgent implements UserAgentCallback, DocumentListener {
protected final LinkedHashMap<String, ImageResource> _imageCache = new LinkedHashMap<>();
protected final FSUriResolver DEFAULT_URI_RESOLVER = new DefaultUriResolver();

protected final Map<ExternalResourceControlPriority, BiPredicate<String, ExternalResourceType>> _accessControllers = new EnumMap<>(ExternalResourceControlPriority.class);

protected FSUriResolver _resolver = DEFAULT_URI_RESOLVER;
protected String _baseUri;
protected Map<String, FSStreamFactory> _protocolsStreamFactory = new HashMap<>(2);
Expand Down Expand Up @@ -266,9 +272,15 @@ protected String readAll(Reader reader) throws IOException {
* @return A CSSResource containing the CSS reader or null if not available.
*/
@Override
public CSSResource getCSSResource(String uri) {
public CSSResource getCSSResource(String uri, ExternalResourceType type) {
if (!checkAccessAllowed(uri, type, ExternalResourceControlPriority.RUN_BEFORE_RESOLVING_URI)) {
return null;
}
String resolved = _resolver.resolveURI(this._baseUri, uri);

if (!checkAccessAllowed(resolved, type, ExternalResourceControlPriority.RUN_AFTER_RESOLVING_URI)) {
return null;
}

if (resolved == null) {
XRLog.log(Level.INFO, LogMessageId.LogMessageId2Param.LOAD_URI_RESOLVER_REJECTED_LOADING_AT_URI, "CSS resource", uri);
return null;
Expand All @@ -286,11 +298,16 @@ public CSSResource getCSSResource(String uri) {
* @return An ImageResource containing the image.
*/
@Override
public ImageResource getImageResource(String uri) {
public ImageResource getImageResource(String uri, ExternalResourceType type) {
ImageResource ir;


if (!checkAccessAllowed(uri, type, ExternalResourceControlPriority.RUN_BEFORE_RESOLVING_URI)) {
return null;
}
String resolved = _resolver.resolveURI(this._baseUri, uri);
if (!checkAccessAllowed(resolved, type, ExternalResourceControlPriority.RUN_AFTER_RESOLVING_URI)) {
return null;
}

if (resolved == null) {
XRLog.log(Level.INFO, LogMessageId.LogMessageId2Param.LOAD_URI_RESOLVER_REJECTED_LOADING_AT_URI, "image resource", uri);
Expand Down Expand Up @@ -345,9 +362,15 @@ public ImageResource getImageResource(String uri) {
* @return An XMLResource containing the image.
*/
@Override
public XMLResource getXMLResource(String uri) {
public XMLResource getXMLResource(String uri, ExternalResourceType type) {
if (!checkAccessAllowed(uri, type, ExternalResourceControlPriority.RUN_BEFORE_RESOLVING_URI)) {
return null;
}
String resolved = _resolver.resolveURI(this._baseUri, uri);

if (!checkAccessAllowed(resolved, type, ExternalResourceControlPriority.RUN_AFTER_RESOLVING_URI)) {
return null;
}

if (resolved == null) {
XRLog.log(Level.INFO, LogMessageId.LogMessageId2Param.LOAD_URI_RESOLVER_REJECTED_LOADING_AT_URI, "XML resource", uri);
return null;
Expand All @@ -363,9 +386,15 @@ public XMLResource getXMLResource(String uri) {
}

@Override
public byte[] getBinaryResource(String uri) {
public byte[] getBinaryResource(String uri, ExternalResourceType type) {
if (!checkAccessAllowed(uri, type, ExternalResourceControlPriority.RUN_BEFORE_RESOLVING_URI)) {
return null;
}
String resolved = _resolver.resolveURI(this._baseUri, uri);

if (!checkAccessAllowed(resolved, type, ExternalResourceControlPriority.RUN_AFTER_RESOLVING_URI)) {
return null;
}

if (resolved == null) {
XRLog.log(Level.INFO, LogMessageId.LogMessageId2Param.LOAD_URI_RESOLVER_REJECTED_LOADING_AT_URI, "binary resource", uri);
return null;
Expand Down Expand Up @@ -422,6 +451,61 @@ public void setBaseURL(String uri) {
_baseUri = uri;
}

public static class DefaultAccessController
implements BiPredicate<String, ExternalResourceType> {

public boolean test(String uri, ExternalResourceType resourceType) {
if (resourceType == null) {
return false;
}

switch (resourceType) {
case BINARY:
case CSS:
case FONT:
case IMAGE_RASTER:
case XML_XHTML:
case XML_SVG:
case PDF:
case SVG_BINARY:
return true;
case FILE_EMBED:
return false;
}

return false;
}
}

public void setAccessController(
ExternalResourceControlPriority prio,
BiPredicate<String, ExternalResourceType> controller) {
this._accessControllers.put(prio, controller);
}

public boolean checkAccessAllowed(
String uriOrResolved,
ExternalResourceType type,
ExternalResourceControlPriority priority) {
BiPredicate<String, ExternalResourceType> controller = this._accessControllers.get(priority);

if (uriOrResolved == null) {
return false;
}

if (controller == null) {
return true;
}

boolean passed = controller.test(uriOrResolved, type);

if (!passed) {
XRLog.log(Level.WARNING, LogMessageId.LogMessageId2Param.LOAD_RESOURCE_ACCESS_REJECTED, uriOrResolved, type);
}

return passed;
}

public static class DefaultUriResolver implements FSUriResolver {
/**
* Resolves the URI; if absolute, leaves as is, if relative, returns an
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,7 @@ enum LogMessageId2Param implements LogMessageId {
LOAD_URI_RESOLVER_REJECTED_LOADING_AT_URI(XRLog.LOAD, "URI resolver rejected loading {} at ({})"),
LOAD_COULD_NOT_READ_URI_AT_URL_MAY_BE_RELATIVE(XRLog.LOAD, "Could not read {} as a URL; may be relative. Testing using parent URL {}"),
LOAD_WAS_ABLE_TO_READ_FROM_URI_USING_PARENT_URL(XRLog.LOAD, "Was able to read from {} using parent URL {}"),
LOAD_RESOURCE_ACCESS_REJECTED(XRLog.LOAD, "URI {} with type {} was rejected by resource access controller"),

GENERAL_FATAL_INFINITE_LOOP_BUG_IN_LINE_BREAKING_ALGO(XRLog.GENERAL, "A fatal infinite loop bug was detected in the line breaking " +
"algorithm for break-word! Start-substring=[{}], end={}"),
Expand Down
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<html>
<head>
<style>
@page {
size: 200px 200px;
}
</style>
<link rel="stylesheet" href="stylesheets/basic.css" />
</head>
<body>
<div id="one">RED</div>
<div id="two">BLUE</div>
</body>
</html>
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
div#one { color: red; }
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
import com.openhtmltopdf.objects.zxing.ZXingObjectDrawer;
import com.openhtmltopdf.outputdevice.helper.BaseRendererBuilder.FSFontUseCase;
import com.openhtmltopdf.outputdevice.helper.BaseRendererBuilder.FontStyle;
import com.openhtmltopdf.outputdevice.helper.ExternalResourceControlPriority;
import com.openhtmltopdf.outputdevice.helper.ExternalResourceType;

import org.junit.Before;
import org.junit.BeforeClass;
Expand Down Expand Up @@ -1348,6 +1350,20 @@ public void testCSSImportLoop() throws IOException {
assertTrue(vt.runTest("css-import-loop"));
}

/**
* Tests banning loading of external CSS.
*/
@Test
public void testExternalAccessControl() throws IOException {
assertTrue(
vt.runTest(
"external-access-control",
builder -> builder.useExternalResourceAccessControl(
(uri, type) -> type != ExternalResourceType.CSS,
ExternalResourceControlPriority.RUN_AFTER_RESOLVING_URI)
));
}

// TODO:
// + Elements that appear just on generated overflow pages.
// + content property (page counters, etc)
Expand Down
Loading

0 comments on commit d20825b

Please sign in to comment.