diff --git a/http-tests/proxy/GET-proxied-external-502.sh b/http-tests/proxy/GET-proxied-external-502.sh index 3e297c65b..c0c6bfc45 100755 --- a/http-tests/proxy/GET-proxied-external-502.sh +++ b/http-tests/proxy/GET-proxied-external-502.sh @@ -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" \ diff --git a/src/main/java/com/atomgraph/linkeddatahub/server/filter/request/ProxyRequestFilter.java b/src/main/java/com/atomgraph/linkeddatahub/server/filter/request/ProxyRequestFilter.java index 23f635a00..964fefbb9 100644 --- a/src/main/java/com/atomgraph/linkeddatahub/server/filter/request/ProxyRequestFilter.java +++ b/src/main/java/com/atomgraph/linkeddatahub/server/filter/request/ProxyRequestFilter.java @@ -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; @@ -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. *

- * This filter intentionally does not 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 does 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 not 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 } */ @@ -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> ontology; + @Inject MediaTypes mediaTypes; @Context Request request; @Override @@ -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 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 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) @@ -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; } @@ -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; } } @@ -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) @@ -381,14 +388,13 @@ public Optional 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; } /** diff --git a/src/main/java/com/atomgraph/linkeddatahub/server/filter/response/ResponseHeadersFilter.java b/src/main/java/com/atomgraph/linkeddatahub/server/filter/response/ResponseHeadersFilter.java index 5c9bc4785..770a3dbd2 100644 --- a/src/main/java/com/atomgraph/linkeddatahub/server/filter/response/ResponseHeadersFilter.java +++ b/src/main/java/com/atomgraph/linkeddatahub/server/filter/response/ResponseHeadersFilter.java @@ -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; @@ -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)); diff --git a/src/main/java/com/atomgraph/linkeddatahub/vocabulary/LAPP.java b/src/main/java/com/atomgraph/linkeddatahub/vocabulary/LAPP.java index 479bf44d3..7ef0a28f1 100644 --- a/src/main/java/com/atomgraph/linkeddatahub/vocabulary/LAPP.java +++ b/src/main/java/com/atomgraph/linkeddatahub/vocabulary/LAPP.java @@ -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" ); + } diff --git a/src/main/java/com/atomgraph/linkeddatahub/vocabulary/LDH.java b/src/main/java/com/atomgraph/linkeddatahub/vocabulary/LDH.java index 17414f681..e8dcaebfe 100644 --- a/src/main/java/com/atomgraph/linkeddatahub/vocabulary/LDH.java +++ b/src/main/java/com/atomgraph/linkeddatahub/vocabulary/LDH.java @@ -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"); diff --git a/src/main/java/com/atomgraph/linkeddatahub/writer/XSLTWriterBase.java b/src/main/java/com/atomgraph/linkeddatahub/writer/XSLTWriterBase.java index 00818c9de..e51e8aae9 100644 --- a/src/main/java/com/atomgraph/linkeddatahub/writer/XSLTWriterBase.java +++ b/src/main/java/com/atomgraph/linkeddatahub/writer/XSLTWriterBase.java @@ -90,7 +90,6 @@ public abstract class XSLTWriterBase extends com.atomgraph.client.writer.XSLTWri @Inject jakarta.inject.Provider> application; @Inject jakarta.inject.Provider dataManager; @Inject jakarta.inject.Provider xsltExecSupplier; - @Inject jakarta.inject.Provider> modes; @Inject jakarta.inject.Provider crc; @Inject jakarta.inject.Provider> authorizationContext; @@ -134,9 +133,6 @@ public Map getParameters(MultivaluedMap Map getParameters(MultivaluedMap getModes(Set namespaces) - { - return getModes().stream().map(Mode::get).collect(Collectors.toList()); - } - - /** - * Returns a list of enabled layout modes. - * - * @return list of modes - */ - public List getModes() - { - return modes.get(); - } - @Override public Set getSupportedNamespaces() { diff --git a/src/main/webapp/static/com/atomgraph/linkeddatahub/css/bootstrap.css b/src/main/webapp/static/com/atomgraph/linkeddatahub/css/bootstrap.css index c4e40f8f1..323c7b577 100644 --- a/src/main/webapp/static/com/atomgraph/linkeddatahub/css/bootstrap.css +++ b/src/main/webapp/static/com/atomgraph/linkeddatahub/css/bootstrap.css @@ -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; } @@ -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; } @@ -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'); } diff --git a/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/bootstrap/2.3.2/admin/acl/layout.xsl b/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/bootstrap/2.3.2/admin/acl/layout.xsl index 0c4a69c4f..08664947c 100644 --- a/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/bootstrap/2.3.2/admin/acl/layout.xsl +++ b/src/main/webapp/static/com/atomgraph/linkeddatahub/xsl/bootstrap/2.3.2/admin/acl/layout.xsl @@ -32,18 +32,6 @@ exclude-result-prefixes="#all">