package act.security; /*- * #%L * ACT Framework * %% * Copyright (C) 2014 - 2017 ActFramework * %% * 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. * #L% */ import act.Act; import act.app.ActionContext; import org.osgl.$; import org.osgl.Osgl; import org.osgl.http.H; import org.osgl.inject.BeanSpec; import org.osgl.util.C; import org.osgl.util.E; import org.osgl.util.S; import java.lang.annotation.*; import java.lang.reflect.Method; import java.lang.reflect.Type; import java.util.Collection; import static org.osgl.http.H.Header.Names.*; /** * Provice CORS header manipulation methods */ public class CORS { /** * Mark a controller class or action handler method that * must not add any CORS headers irregarding to the * global CORS setting */ @Documented @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.METHOD, ElementType.TYPE}) public @interface Disable { } /** * Mark a controller class or action handler method that * needs to add `Access-Control-Allow-Origin` header */ @Documented @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.METHOD, ElementType.TYPE}) public @interface AllowOrigin { /** * The value set to the `Access-Control-Allow-Origin` header * @return the value */ String value() default "*"; } /** * Mark a controller class or action handler method that * needs to add `Access-Control-Allow-Headers` header */ @Documented @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.METHOD, ElementType.TYPE}) public @interface AllowHeaders { /** * The value set to the `Access-Control-Allow-Headers` header * @return the value */ String value() default "*"; } /** * Mark a controller class or action handler method that * needs to add `Access-Control-Expose-Headers` header */ @Documented @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.METHOD, ElementType.TYPE}) public @interface ExposeHeaders { /** * The value set to the `Access-Control-Expose-Headers` header * @return the value */ String value() default "*"; } /** * Mark a controller class or action handler method that * needs to add `Access-Control-Allow-Credentials` header * and set the value to `false` */ @Documented @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.METHOD, ElementType.TYPE}) public @interface DisallowCredentials { } /** * Mark a controller class or action handler method that * needs to add `Access-Control-Allow-Credentials` header * and set the value to `true` */ @Documented @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.METHOD, ElementType.TYPE}) public @interface AllowCredentials { } /** * Mark a controller class or action handler method that * needs to add `Access-Control-Max-Age` header */ @Documented @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.METHOD, ElementType.TYPE}) public @interface MaxAge { /** * The value set to the `Access-Control-Max-Age` header * @return the value */ int value() default 30 * 60; } public static Spec spec(Collection<H.Method> methods) { return new Spec(methods); } public static Spec spec(Class controller) { return spec(BeanSpec.of(controller, Act.injector())); } public static Spec spec(Method action) { Type type = Method.class; Annotation[] annotations = action.getDeclaredAnnotations(); return spec(BeanSpec.of(type, annotations, Act.injector())); } private static Spec spec(BeanSpec beanSpec) { return new Spec() .with(beanSpec.getAnnotation(Disable.class)) .with(beanSpec.getAnnotation(AllowOrigin.class)) .with(beanSpec.getAnnotation(ExposeHeaders.class)) .with(beanSpec.getAnnotation(AllowCredentials.class)) .with(beanSpec.getAnnotation(DisallowCredentials.class)) .with(beanSpec.getAnnotation(AllowHeaders.class)) .with(beanSpec.getAnnotation(MaxAge.class)); } public static class Spec extends $.Visitor<ActionContext> { public static final Spec DUMB = new Spec() { @Override public void visit(ActionContext context) throws Osgl.Break { // do nothing implementation } @Override public void applyTo(ActionContext context) throws Osgl.Break { // do nothing implementation } }; private boolean disableCORS; private String origin; private String methods; private String exposeHeaders; private String allowHeaders; private Boolean allowCredentials; private int maxAge = -1; private boolean effective = false; private Spec(Collection<H.Method> methodSet) { E.illegalArgumentIf(methodSet.isEmpty()); methods = S.join(", ", C.list(methodSet).map($.F.<H.Method>asString())); effective = true; } private Spec() {} public boolean effective() { return effective; } public boolean disabled() { return disableCORS; } public Spec with(Disable disableCORS) { if (null != disableCORS) { this.effective = true; this.disableCORS = true; } return this; } public Spec with(AllowOrigin allowOrigin) { if (null != allowOrigin) { this.effective = true; origin = allowOrigin.value(); } return this; } public Spec with(AllowHeaders allowHeaders) { if (null != allowHeaders) { this.effective = true; this.allowHeaders = allowHeaders.value(); } return this; } public Spec with(ExposeHeaders exposeHeaders) { if (null != exposeHeaders) { this.effective = true; this.exposeHeaders = exposeHeaders.value(); } return this; } public Spec with(AllowCredentials allowCredentials) { if (null != allowCredentials) { this.effective = true; this.allowCredentials = true; } return this; } public Spec with(DisallowCredentials disallowCredentials) { if (null != disallowCredentials) { this.effective = true; this.allowCredentials = false; } return this; } public Spec with(MaxAge maxAge) { if (null != maxAge) { this.effective = true; this.maxAge = maxAge.value(); } return this; } @Override public void visit(ActionContext context) throws Osgl.Break { applyTo(context); } public void applyTo(ActionContext context) throws Osgl.Break { if (!effective) { return; } if (disableCORS) { context.disableCORS(); return; } H.Response r = context.resp(); if (null != origin) { r.addHeaderIfNotAdded(ACCESS_CONTROL_ALLOW_ORIGIN, origin); } if (context.isOptionsMethod()) { if (null != methods) { r.addHeaderIfNotAdded(ACCESS_CONTROL_ALLOW_METHODS, methods); } if (null != exposeHeaders) { r.addHeaderIfNotAdded(ACCESS_CONTROL_EXPOSE_HEADERS, exposeHeaders); } if (null != allowCredentials) { r.addHeaderIfNotAdded(ACCESS_CONTROL_ALLOW_CREDENTIALS, S.string(allowCredentials)); } if (null != allowHeaders) { r.addHeaderIfNotAdded(ACCESS_CONTROL_ALLOW_HEADERS, allowHeaders); } if (-1 < maxAge) { r.addHeaderIfNotAdded(ACCESS_CONTROL_MAX_AGE, S.string(maxAge)); } } } public Spec chain(final Spec next) { if (!next.effective()) { return this; } if (!effective()) { return next; } if (next.disabled()) { return next; } if (disabled()) { return this; } final Spec me = this; return new Spec() { @Override public boolean effective() { return true; } @Override public void applyTo(ActionContext context) throws Osgl.Break { me.visit(context); next.visit(context); } }; } } }