/*
* Copyright (c) 2014-2015 the original author or authors
*
* Licensed 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 io.werval.filters;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Repeatable;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.Locale;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import io.werval.api.Config;
import io.werval.api.context.Context;
import io.werval.api.filters.FilterChain;
import io.werval.api.filters.FilterWith;
import io.werval.api.outcomes.Outcome;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import static io.werval.util.Strings.EMPTY;
import static io.werval.util.Strings.hasTextOrNull;
import static io.werval.api.context.CurrentContext.application;
import static io.werval.api.context.CurrentContext.outcomes;
import static io.werval.api.context.CurrentContext.request;
import static io.werval.api.http.Method.POST;
import static io.werval.api.mime.MimeTypes.APPLICATION_JSON;
/**
* Content-Security-Policy Annotation.
* <p>
* Adds Content Security Policy (CSP) header to HTTP response.
* <p>
* See the <a href="http://www.w3.org/TR/CSP/">W3C Candidate Recommendation</a> and the
* <a href="https://developer.mozilla.org/en-US/docs/Web/Security/CSP">MDN CSP documentation</a>.
* <p>
* Sets {@literal Content-Security-Policy}, {@literal X-Content-Security-Policy} and {@literal X-WebKit-CSP} headers.
* Using their {@literal -Report-Only} flavour when needed.
* <p>
* In the wild, CSP reporting is pretty useless as user's bookmarklets, extensions may create a lot of false positives.
* However this can prove really useful at development or QA stage.
* To support this, the {@link ViolationLogger} controller comes as a simple violation logging facility.
* Add the following route to your Application to get all violations loggued, assuming your {@literal report-uri} CSP
* directive is set to {@literal /csp-violations}.
* <blockquote>
* <pre>POST /csp-violations io.werval.filters.ContentSecurityPolicy$ViolationLogger.logViolation</pre>
* </blockquote>
* Logging level is {@literal WARN} by default, this can be changed by setting the
* {@literal werval.filters.csp.report_log_level} configuration property to {@literal error}, {@literal warn},
* {@literal info}, {@literal debug} or {@literal trace} value.
*
* @navassoc 1 apply 1 Filter
*/
@FilterWith( ContentSecurityPolicy.Filter.class )
@Target( { ElementType.METHOD, ElementType.TYPE } )
@Retention( RetentionPolicy.RUNTIME )
@Inherited
@Documented
@Repeatable( ContentSecurityPolicy.Repeat.class )
public @interface ContentSecurityPolicy
{
String policy() default EMPTY;
boolean reportOnly() default false;
/**
* Content-Security-Policy Filter.
*/
public static class Filter
implements io.werval.api.filters.Filter<ContentSecurityPolicy>
{
@Override
public CompletableFuture<Outcome> filter(
FilterChain chain,
Context context,
Optional<ContentSecurityPolicy> annotation
)
{
return chain.next( context ).thenApply(
(outcome) ->
{
Config config = context.application().config();
String value = annotation.map(
annot -> hasTextOrNull( annot.policy() )
).orElse(
config.string( "werval.filters.csp.policy" )
);
boolean reportOnly = annotation.map(
annot -> annot.reportOnly() ? true : null
).orElse(
config.bool( "werval.filters.csp.report_only" )
);
outcome.responseHeader().headers().withSingle(
reportOnly ? "Content-Security-Policy-Report-Only" : "Content-Security-Policy",
value
);
outcome.responseHeader().headers().withSingle(
reportOnly ? "X-Content-Security-Policy-Report-Only" : "X-Content-Security-Policy",
value
);
outcome.responseHeader().headers().withSingle(
reportOnly ? "X-WebKit-CSP-Report-Only" : "X-WebKit-CSP",
value
);
return outcome;
}
);
}
}
/**
* CSP Violation Logger Controller.
*
* @navassoc 1 works-with 1 Filter
*/
public static class ViolationLogger
{
private static final Logger LOG = LoggerFactory.getLogger( ViolationLogger.class );
/**
* HTTP Interaction that logs CSP Violations.
*
* @return {@literal 400 Bad Request} if the request is not a {@literal POST} of {@literal application/json},
* {@literal 204 No Content} uppon success.
*/
public Outcome logViolation()
{
if( !POST.equals( request().method() )
|| ( request().contentType().isPresent()
&& !APPLICATION_JSON.equals( request().contentType().get() ) ) )
{
return outcomes().badRequest().build();
}
String violationReport = request().body().asString();
switch( application().config().string( "werval.filters.csp.report_log_level" ).toLowerCase( Locale.US ) )
{
case "error":
LOG.error( violationReport );
break;
case "info":
LOG.info( violationReport );
break;
case "debug":
LOG.debug( violationReport );
break;
case "trace":
LOG.trace( violationReport );
break;
case "warn":
default:
LOG.warn( violationReport );
}
return outcomes().noContent().build();
}
}
/**
* ContentSecurityPolicy repeatable annotation support.
*
* @navassoc 1 repeat * ContentSecurityPolicy
*/
@Retention( RetentionPolicy.RUNTIME )
@Target( { ElementType.METHOD, ElementType.TYPE } )
@Inherited
@Documented
public static @interface Repeat
{
ContentSecurityPolicy[] value();
}
}