Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions http-tests/proxy/GET-proxied-external-502.sh
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ add-agent-to-group.sh \

curl -k -w "%{http_code}\n" -o /dev/null -s \
-G \
-H "Accept: application/n-triples" \
-E "$AGENT_CERT_FILE":"$AGENT_CERT_PWD" \
--data-urlencode "uri=http://f1d2d4cf-90bb-4f5b-ae4b-921e584b6edd.org" \
"$END_USER_BASE_URL" \
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
*/
package com.atomgraph.linkeddatahub.server.filter.request;

import com.atomgraph.core.MediaTypes;
import com.atomgraph.client.MediaTypes;
import com.atomgraph.client.util.HTMLMediaTypePredicate;
import com.atomgraph.client.vocabulary.AC;
import com.atomgraph.core.exception.BadGatewayException;
Expand Down Expand Up @@ -84,15 +84,14 @@
* ACL is not checked for proxy requests: the proxy is a global transport function, not a document
* operation. Access control is enforced by the target endpoint.
* <p>
* This filter intentionally does <em>not</em> proxy requests from clients that explicitly accept
* (X)HTML. Rendering arbitrary external URIs as (X)HTML through the full server-side pipeline
* (SPARQL DESCRIBE + XSLT) for every browser-originated proxy request would cause unbounded resource
* exhaustion — a connection-pool and CPU amplification attack vector. Instead, requests whose
* {@code Accept} header contains a non-wildcard {@code text/html} or {@code application/xhtml+xml}
* type fall through to the downstream handler, which serves the LDH application shell; the
* client-side Saxon-JS layer then issues a second, RDF-typed request that <em>does</em> hit this
* filter and is handled cheaply. Pure API clients that send only {@code *}{@code /*} (e.g. curl)
* reach the proxy because they do not list an explicit HTML type.
* This filter does <em>not</em> proxy requests from clients that explicitly accept (X)HTML.
* Rendering arbitrary external URIs as (X)HTML through the full server-side pipeline
* (SPARQL DESCRIBE + XSLT) is expensive and creates a resource-exhaustion attack vector.
* When the {@code Accept} header contains a non-wildcard {@code text/html} or
* {@code application/xhtml+xml} type, the filter returns immediately so the downstream handler
* serves the LDH application shell; the client-side Saxon-JS layer then issues a second, RDF-typed
* request that hits this filter and is proxied cheaply. Pure API clients that send only
* {@code *}{@code /*} (e.g. curl) reach the proxy because they do not list an explicit HTML type.
*
* @author Martynas Jusevičius {@literal <martynas@atomgraph.com>}
*/
Expand All @@ -102,11 +101,11 @@ public class ProxyRequestFilter implements ContainerRequestFilter
{

private static final Logger log = LoggerFactory.getLogger(ProxyRequestFilter.class);
private static final MediaTypes MEDIA_TYPES = new MediaTypes();
private static final Pattern LINK_SPLITTER = Pattern.compile(",(?=\\s*<)");

@Inject com.atomgraph.linkeddatahub.Application system;
@Inject jakarta.inject.Provider<Optional<Ontology>> ontology;
@Inject MediaTypes mediaTypes;
@Context Request request;

@Override
Expand All @@ -117,26 +116,34 @@ public void filter(ContainerRequestContext requestContext) throws IOException

URI targetURI = targetOpt.get();

// do not proxy requests from clients that explicitly accept (X)HTML — they expect the app shell,
// which the downstream handler serves. Browsers list text/html as a non-wildcard type; pure API
// clients (curl etc.) send only */* and must reach the proxy.
// Defending against resource exhaustion: proxying + full server-side XSLT rendering for arbitrary
// external URIs on every browser request would amplify CPU and connection-pool load unboundedly.
// do not proxy requests from clients that explicitly accept (X)HTML — they expect the app
// shell, which the downstream handler serves. Browsers list text/html as a non-wildcard type;
// pure API clients (curl etc.) send only */* and must reach the proxy.
// (X)HTML is not offered for proxied documents — rendering external RDF as HTML server-side
// (SPARQL DESCRIBE + XSLT) is expensive and creates a resource-exhaustion attack vector
boolean clientAcceptsHtml = requestContext.getAcceptableMediaTypes().stream()
.anyMatch(mt -> !mt.isWildcardType() && !mt.isWildcardSubtype() &&
(mt.isCompatible(MediaType.TEXT_HTML_TYPE) ||
mt.isCompatible(MediaType.APPLICATION_XHTML_XML_TYPE)));
if (clientAcceptsHtml) return;

// negotiate the response format from RDF/SPARQL writable types
// negotiate the response format from RDF/SPARQL writable types only
// (client.MediaTypes prepends HTML/XHTML; strip them so selectVariant cannot pick them)
List<MediaType> writableTypes = new ArrayList<>(getMediaTypes().getWritable(Model.class));
writableTypes.addAll(getMediaTypes().getWritable(ResultSet.class));
writableTypes.removeIf(mt -> mt.isCompatible(MediaType.TEXT_HTML_TYPE) ||
mt.isCompatible(MediaType.APPLICATION_XHTML_XML_TYPE));
List<Variant> variants = com.atomgraph.core.model.impl.Response.getVariants(
writableTypes,
getSystem().getSupportedLanguages(),
new ArrayList<>());
Variant selectedVariant = getRequest().selectVariant(variants);
if (selectedVariant == null) return; // client accepts no RDF/SPARQL type

Variant variant = getRequest().selectVariant(variants);
if (variant == null)
{
if (log.isTraceEnabled()) log.trace("Requested Variant {} is not on the list of acceptable Response Variants: {}", variant, variants);
throw new NotAcceptableException();
}

// strip #fragment (servers do not receive fragment identifiers)
if (targetURI.getFragment() != null)
Expand All @@ -156,7 +163,7 @@ public void filter(ContainerRequestContext requestContext) throws IOException
{
if (log.isDebugEnabled()) log.debug("Serving mapped URI from DataManager cache: {}", targetURI);
Model model = getSystem().getDataManager().loadModel(targetURI.toString());
requestContext.abortWith(getResponse(model, Response.Status.OK, selectedVariant));
requestContext.abortWith(getResponse(model, Response.Status.OK, variant));
return;
}

Expand All @@ -174,7 +181,7 @@ public void filter(ContainerRequestContext requestContext) throws IOException
if (!description.isEmpty())
{
if (log.isDebugEnabled()) log.debug("Serving URI from namespace ontology: {}", targetURI);
requestContext.abortWith(getResponse(description, Response.Status.OK, selectedVariant));
requestContext.abortWith(getResponse(description, Response.Status.OK, variant));
return;
}
}
Expand Down Expand Up @@ -221,7 +228,7 @@ else if (agentContext instanceof IDTokenSecurityContext idTokenSecurityContext)
{
// provide the target URI as a base URI hint so ModelProvider / HtmlJsonLDReader can resolve relative references
clientResponse.getHeaders().putSingle(com.atomgraph.core.io.ModelProvider.REQUEST_URI_HEADER, targetURI.toString());
requestContext.abortWith(getResponse(clientResponse, selectedVariant));
requestContext.abortWith(getResponse(clientResponse, variant));
}
}
catch (MessageBodyProviderNotFoundException ex)
Expand Down Expand Up @@ -381,14 +388,13 @@ public Optional<Ontology> getOntology()
}

/**
* Returns the media types registry.
* Core MediaTypes do not include (X)HTML types, which is what we want here.
* Returns the media types registry used for content negotiation and outbound {@code Accept} headers.
*
* @return media types
*/
public MediaTypes getMediaTypes()
{
return MEDIA_TYPES;
return mediaTypes;
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import com.atomgraph.linkeddatahub.server.model.impl.Dispatcher;
import com.atomgraph.linkeddatahub.server.security.AuthorizationContext;
import com.atomgraph.linkeddatahub.vocabulary.ACL;
import com.atomgraph.linkeddatahub.vocabulary.LAPP;
import java.io.IOException;
import java.net.URI;
import java.util.Optional;
Expand Down Expand Up @@ -80,6 +81,8 @@ public void filter(ContainerRequestContext request, ContainerResponseContext res
if (!isProxyRequest && getApplication().isPresent())
{
Application application = getApplication().get();
// add Link rel=lapp:application
response.getHeaders().add(HttpHeaders.LINK, new Link(URI.create(application.getURI()), LAPP.application.getURI(), null));
// add Link rel=ldt:ontology, if the ontology URI is specified
if (application.getOntology() != null)
response.getHeaders().add(HttpHeaders.LINK, new Link(URI.create(application.getOntology().getURI()), LDT.ontology.getURI(), null));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,4 +91,7 @@ public static String getURI()
/** Origin property for subdomain-based application matching */
public static final ObjectProperty origin = m_model.createObjectProperty(NS + "origin");

/** Application property (for Link header rel) */
public static final ObjectProperty application = m_model.createObjectProperty( NS + "application" );

}
3 changes: 0 additions & 3 deletions src/main/java/com/atomgraph/linkeddatahub/vocabulary/LDH.java
Original file line number Diff line number Diff line change
Expand Up @@ -92,9 +92,6 @@ public static String getURI()
/** Violation value property */
public static final DatatypeProperty violationValue = m_model.createDatatypeProperty( NS + "violationValue" );

/** Access to property */
public static final ObjectProperty access_to = m_model.createObjectProperty(NS + "access-to"); // TO-DO: move to client-side?

/** Request URI property */
public static final ObjectProperty requestUri = m_model.createObjectProperty(NS + "requestUri");

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,6 @@ public abstract class XSLTWriterBase extends com.atomgraph.client.writer.XSLTWri
@Inject jakarta.inject.Provider<Optional<com.atomgraph.linkeddatahub.apps.model.Application>> application;
@Inject jakarta.inject.Provider<DataManager> dataManager;
@Inject jakarta.inject.Provider<XsltExecutableSupplier> xsltExecSupplier;
@Inject jakarta.inject.Provider<List<Mode>> modes;
@Inject jakarta.inject.Provider<ContainerRequestContext> crc;
@Inject jakarta.inject.Provider<Optional<AuthorizationContext>> authorizationContext;

Expand Down Expand Up @@ -134,9 +133,6 @@ public <T extends XdmValue> Map<QName, XdmValue> getParameters(MultivaluedMap<St
URI endpointURI = getLinkURI(headerMap, SD.endpoint);
if (endpointURI != null) params.put(new QName("sd", SD.endpoint.getNameSpace(), SD.endpoint.getLocalName()), new XdmAtomicValue(endpointURI));

URI proxyTargetURI = (URI) getContainerRequestContext().getProperty(AC.uri.getURI());
if (proxyTargetURI != null) params.put(new QName("ac", AC.uri.getNameSpace(), AC.uri.getLocalName()), new XdmAtomicValue(proxyTargetURI));

String forShapeURI = getUriInfo().getQueryParameters().getFirst(LDH.forShape.getLocalName());
if (forShapeURI != null) params.put(new QName("ldh", LDH.forShape.getNameSpace(), LDH.forShape.getLocalName()), new XdmAtomicValue(URI.create(forShapeURI)));

Expand All @@ -159,11 +155,6 @@ public <T extends XdmValue> Map<QName, XdmValue> getParameters(MultivaluedMap<St
params.put(new QName("acl", ACL.mode.getNameSpace(), ACL.mode.getLocalName()),
XdmValue.makeSequence(getAuthorizationContext().get().get().getModeURIs()));

// TO-DO: move to client-side?
if (getUriInfo().getQueryParameters().containsKey(LDH.access_to.getLocalName()))
params.put(new QName("ldh", LDH.access_to.getNameSpace(), LDH.access_to.getLocalName()),
new XdmAtomicValue(URI.create(getUriInfo().getQueryParameters().getFirst(LDH.access_to.getLocalName()))));

if (getHttpHeaders().getRequestHeader(HttpHeaders.REFERER) != null)
{
URI referer = URI.create(getHttpHeaders().getRequestHeader(HttpHeaders.REFERER).get(0));
Expand Down Expand Up @@ -250,11 +241,7 @@ public StreamSource getSource(Model model) throws IOException
@Override
public String getSystemId()
{
// for proxy requests, use the external URI as the XSLT document base URI
URI proxyTarget = (URI) getContainerRequestContext().getProperty(AC.uri.getURI());
if (proxyTarget != null) return proxyTarget.toString();

return getContainerRequestContext().getUriInfo().getRequestUri().toString();
return getUriInfo().getRequestUri().toString();
}

/**
Expand Down Expand Up @@ -314,22 +301,6 @@ public XsltExecutable getXsltExecutable()
return xsltExecSupplier.get().get();
}

@Override
public List<URI> getModes(Set<String> namespaces)
{
return getModes().stream().map(Mode::get).collect(Collectors.toList());
}

/**
* Returns a list of enabled layout modes.
*
* @return list of modes
*/
public List<Mode> getModes()
{
return modes.get();
}

@Override
public Set<String> getSupportedNamespaces()
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
html { height: 100%; }
body { height: calc(100% - 120px); padding-top: 120px; padding-bottom: 0; }
/* only the main navbar is fixed-top now; tab bar + action bar are sticky inside #tab-body */
body { height: calc(100% - 55px); padding-top: 55px; padding-bottom: 0; }
#tab-bar { position: sticky; top: 55px; z-index: 1000; margin-bottom: 0; }
/* Override Bootstrap's overflow:auto which would make action-bar sticky relative to #tab-content instead of the viewport */
#tab-content { overflow: visible; }
#tab-bar.navbar-inner { min-height: 36px; padding: 4px 0; background: #ccc; box-shadow: none; }
#tab-bar-list.nav-tabs { margin-bottom: 0; border-bottom: none; }
#tab-bar-list.nav-tabs > li > a { color: #007fff; padding: 8px 12px; text-shadow: none; border-color: #aaa #aaa transparent; }
#tab-bar-list.nav-tabs > li.active > a, #tab-bar .nav-tabs > li.active > a:hover { color: #555555; background: rgb(223, 223, 223); border-color: #aaa #aaa transparent; }
#tab-bar-list .tab-close { margin-left: 4px; font-size: 10px; color: #888; cursor: pointer; vertical-align: middle; }
#tab-bar-list .tab-close:hover { color: #333; }
body.embed { padding-top: 0; }
ul.dropdown-menu { max-height: 26em; overflow-x: hidden; overflow-y: auto; }
ul.dropdown-menu ul { margin: 0; }
Expand All @@ -17,16 +27,17 @@ ul.dropdown-menu ul { margin: 0; }
.navbar-form .input-append { margin-top: 10px; }
.navbar-form .input-append select { margin-top: 0; height: 34px; }
.navbar-form .btn-search { background-image: url('../icons/ic_search_white_24px.svg'); background-position: center center; background-repeat: no-repeat; width: 34px; height: 34px; }
.action-bar { background-color: #dfdfdf; }
.action-bar { position: sticky; top: var(--action-bar-top, 55px); z-index: 999; background: #dfdfdf; padding: 0; box-shadow: none; }
.action-bar form { margin-bottom: 0; }
.action-bar .span7 .row-fluid > * { margin-top: 10px; }
.action-bar .row-fluid > .span2, .action-bar .row-fluid > .span3 { margin-top: 10px; }
.action-bar .add-constructor, .dropdown-menu .add-constructor { background-color: inherit; display: block; text-align: left; width: 100%; }
.action-bar .add-constructor:hover { color: #ffffff; background-color: #007af5; }
.action-bar .breadcrumb { background-color: inherit; margin-bottom: 0; padding-left: 0; padding-top: 5px; }
.action-bar .breadcrumb .container-logo { background-image: url('../icons/folder.svg'); background-position: left center; background-repeat: no-repeat; padding-left: 28px; }
.action-bar .breadcrumb .item-logo { background-image: url('../icons/file.svg'); background-position: left center; background-repeat: no-repeat; padding-left: 28px; }
.action-bar .breadcrumb .btn-group { margin-top: -4px; }
.action-bar #breadcrumb-nav > .label-info { padding: 8px 15px; margin-right: 8px; font-size: inherit; background-color: #9954bb; }
.action-bar .breadcrumb-nav > .label-info { padding: 8px 15px; margin-right: 8px; font-size: inherit; background-color: #9954bb; }
.action-bar #doc-controls { text-align: right; padding-top: 5px; }
.action-bar #doc-controls .btn-edit { margin-top: -5px; margin-left: 10px; }
.action-bar p.alert { margin-bottom: 0; }
Expand Down Expand Up @@ -107,10 +118,10 @@ li button.btn-edit-constructors, li button.btn-add-data, li button.btn-add-ontol
.caret.caret-reversed { border-bottom: 4px solid #000000; border-top-width: 0; }
.faceted-nav input[type=checkbox]:checked + span { font-weight: bold; }
.parallax-nav a { cursor: pointer; }
#content-body { min-height: calc(100% - 14em); }
#content-body > [about].row-fluid { overflow-x: auto; margin-bottom: 20px; }
#content-body > [about].row-fluid, .constructor-triple.row-fluid { border-bottom: 2px solid rgb(223, 223, 223); }
#content-body > [about].row-fluid.drag-over { border-bottom: 4px dotted #0f82f5; }
.content-body { min-height: calc(100% - 14em); margin-top: 14px; }
.content-body > [about].row-fluid.block { overflow-x: auto; margin-bottom: 20px; }
.content-body > [about].row-fluid.block, .constructor-triple.row-fluid { border-bottom: 2px solid rgb(223, 223, 223); }
.content-body > [about].row-fluid.block.drag-over { border-bottom: 4px dotted #0f82f5; }
.row-fluid.block { max-height: 80em; }
.row-fluid.block .drag-handle { display: none; width: 30px; background-color: #149bdf; background-image: radial-gradient(circle at 3px 3px, #0480be 1px, transparent 1.5px); background-size: 6px 6px; border-radius: 2px; cursor: move; }
.list-mode.active { background-image: url('../icons/ic_navigate_before_black_24px.svg'); }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,18 +32,6 @@ exclude-result-prefixes="#all">
<xsl:template match="rdf:RDF" mode="bs2:NavBarNavList">
<xsl:if test="$foaf:Agent//@rdf:about">
<ul class="nav pull-right">
<!-- <li>
<xsl:if test="$ac:mode = '&ac;QueryEditorMode'">
<xsl:attribute name="class" select="'active'"/>
</xsl:if>

<a href="{ac:build-uri((), map{ 'mode': '&ac;QueryEditorMode' })}" class="query-editor">
<xsl:value-of>
<xsl:apply-templates select="key('resources', 'sparql-editor', document(resolve-uri('static/com/atomgraph/linkeddatahub/xsl/bootstrap/2.3.2/translations.rdf', $ac:contextUri)))" mode="ac:label"/>
</xsl:value-of>
</a>
</li>-->

<xsl:variable name="notification-query" as="xs:string">
<![CDATA[
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ xmlns:bs2="http://graphity.org/xsl/bootstrap/2.3.2"
exclude-result-prefixes="#all">

<xsl:template match="rdf:RDF[ac:absolute-path(ldh:request-uri()) = resolve-uri(encode-for-uri('sign up'), $ldt:base)]" mode="bs2:ContentBody" priority="2" use-when="system-property('xsl:product-name') = 'SAXON'">
<div about="{ac:absolute-path(base-uri($main-doc))}" id="content-body" class="container-fluid">
<div about="{ac:absolute-path(base-uri($main-doc))}" class="container-fluid content-body">
<xsl:apply-templates select="key('resources', ac:absolute-path(base-uri($main-doc)))" mode="ldh:ContentList"/>

<xsl:apply-templates select="." mode="bs2:Row"/>
Expand Down
Loading
Loading