diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/html/HtmlDocSerializer.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/html/HtmlDocSerializer.java index d5aefcfa64..aa092bdc3a 100644 --- a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/html/HtmlDocSerializer.java +++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/html/HtmlDocSerializer.java @@ -158,6 +158,84 @@ public class HtmlDocSerializer extends HtmlStrippedDocSerializer { */ public static final String HTMLDOC_asideFloat = PREFIX + ".asideFloat.s"; + /** + * Configuration property: Content Security Policy (CSP) Hash algorithm name. + * + *
Property:
+ * + * + *
Description:
+ *

+ * Allows you to set a CSP Hash algorithm name. + * + *

+ * By default, this feature is disabled. + * + *

Example:
+ *

+ * @HtmlDocConfig( + * cspHash="sha256" + * ) + *

+ * @since 9.0.0 + */ + public static final String HTMLDOC_cspHash = PREFIX + ".cspHash.s"; + + /** + * Configuration property: Content Security Policy (CSP) nonce algorithm name. + * + *
Property:
+ * + * + *
Description:
+ *

+ * Allows you to set a CSP nonce algorithm name. + * + *

+ * By default, this feature is disabled. + * + *

Example:
+ *

+ * @HtmlDocConfig( + * cspNonce="SecureRandom" + * ) + *

+ * @since 9.0.0 + */ + public static final String HTMLDOC_cspNonce = PREFIX + ".cspNonce.s"; + /** * Configuration property: Footer section contents. * @@ -717,6 +795,8 @@ public class HtmlDocSerializer extends HtmlStrippedDocSerializer { //------------------------------------------------------------------------------------------------------------------- private final String[] style, stylesheet, script, navlinks, head, header, nav, aside, footer; + private final CspHash cspHash; + private final CspNonce cspNonce; private final AsideFloat asideFloat; private final String noResultsMessage; private final boolean nowrap; @@ -776,6 +856,9 @@ public HtmlDocSerializer(ContextProperties cp, String produces, String accept) { navlinks = cp.getArray(HTMLDOC_navlinks, String.class).orElse(new String[0]); noResultsMessage = cp.getString(HTMLDOC_noResultsMessage).orElse("

no results

"); template = cp.getInstance(HTMLDOC_template, HtmlDocTemplate.class).orElseGet(BasicHtmlDocTemplate::new); + + cspHash = cp.get(HTMLDOC_cspHash, CspHash.class).orElse(CspHash.DEFAULT); + cspNonce = cp.get(HTMLDOC_cspNonce, CspNonce.class).orElse(CspNonce.DEFAULT); widgets = new HtmlWidgetMap(); widgets.append(cp.getInstanceArray(HTMLDOC_widgets, HtmlWidget.class).orElse(new HtmlWidget[0])); @@ -845,6 +928,26 @@ protected final AsideFloat getAsideFloat() { return asideFloat; } + /** + * CSP hash algorithm name. + * @return + * CSP hash algorithm name. + * @since 9.0.0 + */ + protected final CspHash getCspHash() { + return cspHash; + } + + /** + * CSP nonce algorithm name. + * @return + * CSP nonce algorithm name. + * @since 9.0.0 + */ + protected final CspNonce getCspNonce() { + return cspNonce; + } + /** * Footer section contents. * @@ -996,6 +1099,8 @@ public OMap toMap() { .a("asideFloat", asideFloat) .a("footer", footer) .a("style", style) + .a("cspHash", cspHash) + .a("cspNonce", cspNonce) .a("head", head) .a("stylesheet", stylesheet) .a("nowrap", nowrap) @@ -1004,4 +1109,5 @@ public OMap toMap() { .a("widgets", widgets.keySet()) ); } + } diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/html/HtmlDocSerializerBuilder.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/html/HtmlDocSerializerBuilder.java index bb6f618894..be39e5f3ce 100644 --- a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/html/HtmlDocSerializerBuilder.java +++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/html/HtmlDocSerializerBuilder.java @@ -12,19 +12,48 @@ // *************************************************************************************************************************** package org.apache.juneau.html; -import static org.apache.juneau.html.HtmlDocSerializer.*; - -import java.lang.annotation.*; -import java.lang.reflect.*; -import java.nio.charset.*; -import java.util.*; - -import org.apache.juneau.*; -import org.apache.juneau.http.header.*; -import org.apache.juneau.internal.*; -import org.apache.juneau.reflect.*; -import org.apache.juneau.svl.*; -import org.apache.juneau.xml.*; +import static org.apache.juneau.html.HtmlDocSerializer.HTMLDOC_aside; +import static org.apache.juneau.html.HtmlDocSerializer.HTMLDOC_asideFloat; +import static org.apache.juneau.html.HtmlDocSerializer.HTMLDOC_cspHash; +import static org.apache.juneau.html.HtmlDocSerializer.HTMLDOC_cspNonce; +import static org.apache.juneau.html.HtmlDocSerializer.HTMLDOC_footer; +import static org.apache.juneau.html.HtmlDocSerializer.HTMLDOC_head; +import static org.apache.juneau.html.HtmlDocSerializer.HTMLDOC_header; +import static org.apache.juneau.html.HtmlDocSerializer.HTMLDOC_nav; +import static org.apache.juneau.html.HtmlDocSerializer.HTMLDOC_navlinks; +import static org.apache.juneau.html.HtmlDocSerializer.HTMLDOC_navlinks_add; +import static org.apache.juneau.html.HtmlDocSerializer.HTMLDOC_noResultsMessage; +import static org.apache.juneau.html.HtmlDocSerializer.HTMLDOC_nowrap; +import static org.apache.juneau.html.HtmlDocSerializer.HTMLDOC_script; +import static org.apache.juneau.html.HtmlDocSerializer.HTMLDOC_script_add; +import static org.apache.juneau.html.HtmlDocSerializer.HTMLDOC_style; +import static org.apache.juneau.html.HtmlDocSerializer.HTMLDOC_style_add; +import static org.apache.juneau.html.HtmlDocSerializer.HTMLDOC_stylesheet; +import static org.apache.juneau.html.HtmlDocSerializer.HTMLDOC_stylesheet_add; +import static org.apache.juneau.html.HtmlDocSerializer.HTMLDOC_template; +import static org.apache.juneau.html.HtmlDocSerializer.HTMLDOC_widgets; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; +import java.nio.charset.Charset; +import java.util.Locale; +import java.util.Map; +import java.util.TimeZone; + +import org.apache.juneau.ContextProperties; +import org.apache.juneau.UriContext; +import org.apache.juneau.UriRelativity; +import org.apache.juneau.UriResolution; +import org.apache.juneau.UriResolver; +import org.apache.juneau.Visibility; +import org.apache.juneau.html.annotation.CspHash; +import org.apache.juneau.html.annotation.CspNonce; +import org.apache.juneau.http.header.MediaType; +import org.apache.juneau.internal.FluentSetter; +import org.apache.juneau.internal.FluentSetters; +import org.apache.juneau.reflect.AnnotationList; +import org.apache.juneau.svl.VarResolverSession; +import org.apache.juneau.xml.Namespace; /** * Builder class for building instances of HTML Doc serializers. @@ -624,6 +653,98 @@ public HtmlDocSerializerBuilder applyAnnotations(AnnotationList al, VarResolverS return this; } + /** + * Configuration property: Content Security Policy (CSP) Hash algorithm name. + * + *
Property:
+ * + * + *
Description:
+ *

+ * Allows you to set a CSP Hash algorithm name. + * + *

+ * By default, this feature is disabled. + * + *

Example:
+ *

+ * @HtmlDocConfig( + * cspHash="sha256" + * ) + *

+ * @param value + * The new value for this property. + * @return This object (for method chaining). + * @since 9.0.0 + */ + @FluentSetter + public HtmlDocSerializerBuilder cspHash(CspHash value) { + set(HTMLDOC_cspHash, value); + return this; + } + + /** + * Configuration property: Content Security Policy (CSP) nonce algorithm name. + * + *
Property:
+ * + * + *
Description:
+ *

+ * Allows you to set a CSP nonce algorithm name. + * + *

+ * By default, this feature is disabled. + * + *

Example:
+ *

+ * @HtmlDocConfig( + * cspNonce="SecureRandom" + * ) + *

+ * @param value + * The new value for this property. + * @return This object (for method chaining). + * @since 9.0.0 + */ + @FluentSetter + public HtmlDocSerializerBuilder cspNonce(CspNonce value) { + set(HTMLDOC_cspNonce, value); + return this; + } + @Override /* GENERATED - ContextBuilder */ public HtmlDocSerializerBuilder debug() { super.debug(); diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/html/HtmlDocSerializerSession.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/html/HtmlDocSerializerSession.java index 719202f3ed..6c18145a57 100644 --- a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/html/HtmlDocSerializerSession.java +++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/html/HtmlDocSerializerSession.java @@ -19,6 +19,8 @@ import org.apache.juneau.*; import org.apache.juneau.collections.*; +import org.apache.juneau.html.annotation.CspHash; +import org.apache.juneau.html.annotation.CspNonce; import org.apache.juneau.serializer.*; import org.apache.juneau.svl.*; @@ -40,6 +42,8 @@ public class HtmlDocSerializerSession extends HtmlStrippedDocSerializerSession { private final AsideFloat asideFloat; private final Set style, stylesheet, script; private final boolean nowrap; + private final CspHash cspHash; + private final CspNonce cspNonce; /** * Create a new session using properties specified in the context. @@ -67,6 +71,9 @@ protected HtmlDocSerializerSession(HtmlDocSerializer ctx, SerializerSessionArgs style = ASet.of(sp.get(HTMLDOC_style, String[].class).orElse(ctx.getStyle())); stylesheet = ASet.of(sp.get(HTMLDOC_stylesheet, String[].class).orElse(ctx.getStylesheet())); script = ASet.of(sp.get(HTMLDOC_script, String[].class).orElse(ctx.getScript())); + + cspHash = sp.get(HTMLDOC_cspHash, CspHash.class).orElse(ctx.getCspHash()); + cspNonce = sp.get(HTMLDOC_cspNonce, CspNonce.class).orElse(ctx.getCspNonce()); head = sp.get(HTMLDOC_head, String[].class).orElse(ctx.getHead()); nowrap = sp.get(HTMLDOC_nowrap, boolean.class).orElse(ctx.isNowrap()); @@ -142,6 +149,30 @@ protected final AsideFloat getAsideFloat() { return asideFloat; } + /** + * Configuration property: CSP hash algorithm name. + * + * @see HtmlDocSerializer#HTMLDOC_cspHash + * @return + * CSP hash algorithm name. + * @since 9.0.0 + */ + protected final CspHash getCspHash() { + return cspHash; + } + + /** + * Configuration property: CSP nonce algorithm name. + * + * @see HtmlDocSerializer#HTMLDOC_cspNonce + * @return + * CSP nonce algorithm name. + * @since 9.0.0 + */ + protected final CspNonce getCspNonce() { + return cspNonce; + } + /** * Configuration property: Footer section contents. * @@ -294,8 +325,11 @@ public OMap toMap() { .a("navlinks", navlinks) .a("script", script) .a("style", style) + .a("cspHash", cspHash) + .a("cspNonce", cspNonce) .a("stylesheet", stylesheet) .a("varResolver", getVarResolver()) ); } + } diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/html/annotation/CspHash.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/html/annotation/CspHash.java new file mode 100644 index 0000000000..bb704c2529 --- /dev/null +++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/html/annotation/CspHash.java @@ -0,0 +1,76 @@ +// *************************************************************************************************************************** +// * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file * +// * distributed with this work for additional information regarding copyright ownership. The ASF licenses this file * +// * to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance * +// * with the License. You may obtain a copy of the License at * +// * * +// * http://www.apache.org/licenses/LICENSE-2.0 * +// * * +// * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an * +// * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the * +// * specific language governing permissions and limitations under the License. * +// *************************************************************************************************************************** + +package org.apache.juneau.html.annotation; + +/** + * Hash algorithms defined for + * Content Security Policy + * elements {@code script} and {@code style}. + * + * @see Content Security Policy + * Level 2 (W3C Recommendation, 15 December 2016) + * @see CSP 2 + * valid hashes + * @see CSP 3 + * valid hashes + * @since 9.0.0 + */ +public enum CspHash { + + /** + * Don't use a hash algorithm. + */ + DEFAULT(""), + + /** + * SHA-256 hash algorithm. + * + * @see Valid + * hashes. + */ + SHA256("sha256"), + + /** + * SHA-384 hash algorithm. + * + * @see Valid + * hashes. + */ + + SHA384("sha384"), + /** + * SHA-512 hash algorithm. + * + * @see Valid + * hashes. + */ + SHA512("sha512"); + + /** Value in HTML. */ + private final String value; + + /** Constructs a new instance. */ + CspHash(String value) { + this.value = value; + } + + /** + * Gets the value for HTML. + * + * @return the value for HTML. + */ + public String value() { + return value; + } +} diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/html/annotation/CspNonce.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/html/annotation/CspNonce.java new file mode 100644 index 0000000000..aeed0a15eb --- /dev/null +++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/html/annotation/CspNonce.java @@ -0,0 +1,62 @@ +// *************************************************************************************************************************** +// * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file * +// * distributed with this work for additional information regarding copyright ownership. The ASF licenses this file * +// * to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance * +// * with the License. You may obtain a copy of the License at * +// * * +// * http://www.apache.org/licenses/LICENSE-2.0 * +// * * +// * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an * +// * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the * +// * specific language governing permissions and limitations under the License. * +// *************************************************************************************************************************** + +package org.apache.juneau.html.annotation; + +/** + * Nonce algorithms defined for use with + * Content Security Policy + * elements {@code script} and {@code style}. + * + * @see Content Security Policy + * Level 2 (W3C Recommendation, 15 December 2016) + * @see Content Security Policy + * Level 3 (W3C Working Draft, 15 October 2018) + * @see Nonce reuse + * @since 9.0.0 + */ +public enum CspNonce { + + /** + * Don't use a nonce. + */ + DEFAULT(""), + + /** + * The secure random algorithm. + * + * @see Valid + * hashes. + * @see Nonce reuse + */ + // Assumes we are using java.security.SecureRandom as opposed to java.util.Random + // But leave room for other sources of randomness. + SECURE_RANDOM("SecureRandom"); + + /** Value in HTML. */ + private final String value; + + /** Constructs a new instance. */ + CspNonce(String value) { + this.value = value; + } + + /** + * Gets the value for HTML. + * + * @return the value for HTML. + */ + public String value() { + return value; + } +} diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/html/annotation/HtmlDocConfig.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/html/annotation/HtmlDocConfig.java index 9b8051910c..e3139f0931 100644 --- a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/html/annotation/HtmlDocConfig.java +++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/html/annotation/HtmlDocConfig.java @@ -142,6 +142,92 @@ */ String asideFloat() default "DEFAULT"; + /** + * Configuration property: A hash algorithm defined in + * Content Security Policy for + * the elements {@code script} and {@code style}. + *

+ * The possible algorithm names from {@link CspHash} are: + *

+ *

+ *

+ * Adds the HTTP header in the form: + *

+ * Content-Security-Policy: script-src 'ALGORITHM-base64 encoded hash' + *

+ * Note that the header can carry the directives: + * + *

+ * + *
Example:
+ *

+ * The annotation: + *

+ * @HtmlDocConfig( + * cspHash="sha256" + * ) + *

+ * For the SHA-256 hash for the script {@code alert('Hello, world.');} is + * {@code qznLcsROx4GACP2dm0UCKCzCG+HiZ1guq6ZZDob/Tng=}. + *

+ * Generates the HTTP header: + *

+ * Content-Security-Policy: script-src 'sha256-qznLcsROx4GACP2dm0UCKCzCG+HiZ1guq6ZZDob/Tng=' + *

+ *

+ * + * @since 9.0.0 + */ + String cspHash() default "DEFAULT"; + + /** + * Configuration property: Nonce algorithms defined for use with + * Content Security Policy + * for the elements {@code script} and {@code style}. + *

+ * The possible algorithm names from {@link CspNonce} are: + *

+ *

+ *

+ * Adds the HTTP header in the form: + *

+ * Content-Security-Policy: script-src 'nonce-$RANDOM' + *

+ * Note that the header can carry the directives: + * + *

+ * + *
Example:
+ *

+ * The annotation: + *

+ * @HtmlDocConfig( + * cspNonce="SecureRandom" + * ) + *

+ *

+ * Generates the HTTP header like: + *

+ * Content-Security-Policy: script-src 'nonce-Nc3n83cnSAd3wc3Sasdfn939hc3' + *

+ *

+ * + * @since 9.0.0 + */ + String cspNonce() default "DEFAULT"; + /** * Configuration property: Footer section contents. * diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/html/annotation/HtmlDocConfigAnnotation.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/html/annotation/HtmlDocConfigAnnotation.java index 7b680f68af..8670c8f7eb 100644 --- a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/html/annotation/HtmlDocConfigAnnotation.java +++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/html/annotation/HtmlDocConfigAnnotation.java @@ -50,6 +50,8 @@ public void apply(AnnotationInfo ai, ContextPropertiesBuilder cpb cpb.setIf(a.aside().length > 0, HTMLDOC_aside, resolveList(a.aside(), cpb.peek(String[].class, HTMLDOC_aside))); cpb.setIf(! "DEFAULT".equalsIgnoreCase(a.asideFloat()), HTMLDOC_asideFloat, a.asideFloat().toUpperCase()); + cpb.setIf(! "DEFAULT".equalsIgnoreCase(a.cspHash()), HTMLDOC_cspHash, a.cspHash().toUpperCase()); + cpb.setIf(! "DEFAULT".equalsIgnoreCase(a.cspNonce()), HTMLDOC_cspNonce, a.cspNonce().toUpperCase()); cpb.setIf(a.footer().length > 0, HTMLDOC_footer, resolveList(a.footer(), cpb.peek(String[].class, HTMLDOC_footer))); cpb.setIf(a.head().length > 0, HTMLDOC_head, resolveList(a.head(), cpb.peek(String[].class, HTMLDOC_head))); cpb.setIf(a.header().length > 0, HTMLDOC_header, resolveList(a.header(), cpb.peek(String[].class, HTMLDOC_header))); diff --git a/juneau-utest/src/test/java/org/apache/juneau/html/HtmlDocConfigAnnotation_Test.java b/juneau-utest/src/test/java/org/apache/juneau/html/HtmlDocConfigAnnotation_Test.java index 53bf83a15e..52ef5a02e9 100644 --- a/juneau-utest/src/test/java/org/apache/juneau/html/HtmlDocConfigAnnotation_Test.java +++ b/juneau-utest/src/test/java/org/apache/juneau/html/HtmlDocConfigAnnotation_Test.java @@ -60,6 +60,9 @@ public String apply(Object t) { @HtmlDocConfig( aside="$X{foo}", + asideFloat="DEFAULT", + cspHash="sha256", + cspNonce="SECURE_RANDOM", footer="$X{foo}", head="$X{foo}", header="$X{foo}", @@ -80,6 +83,9 @@ public void basic() throws Exception { AnnotationList al = a.getAnnotationList(); HtmlDocSerializerSession x = HtmlDocSerializer.create().applyAnnotations(al, sr).build().createSession(); check("foo", x.getAside()); + check("RIGHT", x.getAsideFloat()); + check("SHA256", x.getCspHash()); + check("SECURE_RANDOM", x.getCspNonce()); check("foo", x.getFooter()); check("foo", x.getHead()); check("foo", x.getHeader()); @@ -106,6 +112,7 @@ public void defaults() throws Exception { AnnotationList al = b.getAnnotationList(); HtmlDocSerializerSession x = HtmlDocSerializer.create().applyAnnotations(al, sr).build().createSession(); check("", x.getAside()); + check("RIGHT", x.getAsideFloat()); check("", x.getFooter()); check("", x.getHead()); check("", x.getHeader()); @@ -131,6 +138,9 @@ public void noAnnotation() throws Exception { AnnotationList al = c.getAnnotationList(); HtmlDocSerializerSession x = HtmlDocSerializer.create().applyAnnotations(al, sr).build().createSession(); check("", x.getAside()); + check("RIGHT", x.getAsideFloat()); + check("DEFAULT", x.getCspHash()); + check("DEFAULT", x.getCspNonce()); check("", x.getFooter()); check("", x.getHead()); check("", x.getHeader());