diff --git a/pom.xml b/pom.xml index a64fd1a3b..68dc37cab 100644 --- a/pom.xml +++ b/pom.xml @@ -14,7 +14,7 @@ Provides a modern and scalable web server as SIRIUS module - 12.0-rc24 + 12.0-rc25 diff --git a/src/main/java/sirius/web/controller/ControllerDispatcher.java b/src/main/java/sirius/web/controller/ControllerDispatcher.java index f6860827d..66f8ae9a8 100644 --- a/src/main/java/sirius/web/controller/ControllerDispatcher.java +++ b/src/main/java/sirius/web/controller/ControllerDispatcher.java @@ -13,6 +13,7 @@ import sirius.kernel.async.Promise; import sirius.kernel.async.TaskContext; import sirius.kernel.async.Tasks; +import sirius.kernel.commons.CachingSupplier; import sirius.kernel.commons.Callback; import sirius.kernel.commons.Explain; import sirius.kernel.commons.PriorityCollector; @@ -22,7 +23,6 @@ import sirius.kernel.di.std.Register; import sirius.kernel.health.Exceptions; import sirius.kernel.health.Log; -import sirius.kernel.nls.NLS; import sirius.web.ErrorCodeException; import sirius.web.http.Firewall; import sirius.web.http.InputStreamHandler; @@ -31,7 +31,6 @@ import sirius.web.http.WebContext; import sirius.web.http.WebDispatcher; import sirius.web.security.UserContext; -import sirius.web.security.UserInfo; import sirius.web.services.JSONStructuredOutput; import java.lang.reflect.InvocationTargetException; @@ -163,7 +162,10 @@ public boolean dispatch(WebContext ctx) throws Exception { private void performRoute(WebContext ctx, Route route, List params) { try { - setupContext(ctx, route); + TaskContext.get() + .setSystem(SYSTEM_MVC) + .setSubSystem(route.getController().getClass().getSimpleName()) + .setJob(ctx.getRequestedURI()); // Intercept call... for (Interceptor interceptor : interceptors) { @@ -172,17 +174,8 @@ private void performRoute(WebContext ctx, Route route, List params) { } } - // Install user. This is forcefully called here to ensure that the ScopeDetetor - // and the user manager are guaranteed to be invoked one we enter the controller code... - UserInfo user = UserContext.getCurrentUser(); + String missingPermission = route.checkAuth(new CachingSupplier<>(UserContext::getCurrentUser)); - // If the underlying ScopeDetector made a redirect (for whatever reasons) - // the response will be committed and we can (must) safely return... - if (ctx.isResponseCommitted()) { - return; - } - - String missingPermission = route.checkAuth(user); if (missingPermission != null) { handlePermissionError(ctx, route, missingPermission); } else { @@ -201,12 +194,6 @@ private void performRoute(WebContext ctx, Route route, List params) { } private void executeRoute(WebContext ctx, Route route, List params) throws Exception { - // If a user authenticated during this call...bind to session! - UserContext userCtx = UserContext.get(); - if (userCtx.getUser().isLoggedIn()) { - userCtx.attachUserToSession(); - } - if (route.isJSONCall()) { executeJSONCall(ctx, route, params); } else { @@ -251,14 +238,6 @@ private void handlePermissionError(WebContext ctx, Route route, String missingPe ctx.respondWith().error(HttpResponseStatus.UNAUTHORIZED); } - private void setupContext(WebContext ctx, Route route) { - CallContext.getCurrent().setLang(NLS.makeLang(ctx.getLang())); - TaskContext.get() - .setSystem(SYSTEM_MVC) - .setSubSystem(route.getController().getClass().getSimpleName()) - .setJob(ctx.getRequestedURI()); - } - private void handleFailure(WebContext ctx, Route route, Throwable ex) { try { CallContext.getCurrent() diff --git a/src/main/java/sirius/web/controller/Route.java b/src/main/java/sirius/web/controller/Route.java index ea911d1c3..e6606fb99 100644 --- a/src/main/java/sirius/web/controller/Route.java +++ b/src/main/java/sirius/web/controller/Route.java @@ -31,6 +31,7 @@ import java.util.Collections; import java.util.List; import java.util.Set; +import java.util.function.Supplier; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; @@ -251,12 +252,12 @@ private void setAtPosition(List list, int position, Object value) { * @return null if the user is authorized or otherwise the name of the permission which the user is * missing. */ - protected String checkAuth(UserInfo user) { + protected String checkAuth(Supplier user) { if (permissions == null) { return null; } for (String p : permissions) { - if (!user.hasPermission(p)) { + if (!user.get().hasPermission(p)) { return p; } } diff --git a/src/main/java/sirius/web/http/Response.java b/src/main/java/sirius/web/http/Response.java index eb7bab379..e85abd551 100644 --- a/src/main/java/sirius/web/http/Response.java +++ b/src/main/java/sirius/web/http/Response.java @@ -35,6 +35,7 @@ import io.netty.handler.stream.ChunkedWriteHandler; import sirius.kernel.Sirius; import sirius.kernel.async.CallContext; +import sirius.kernel.async.ExecutionPoint; import sirius.kernel.commons.MultiMap; import sirius.kernel.commons.Strings; import sirius.kernel.di.std.Part; @@ -293,6 +294,25 @@ private void setupHeaders(DefaultHttpResponse response) { response.headers() .set("Strict-Transport-Security", "max-age=" + WebContext.hstsMaxAge + "; includeSubDomains"); } + + // NEVER allow a Set-Cookie header within a cached request... + if (response.headers().contains(HttpHeaderNames.SET_COOKIE)) { + if (response.headers().contains(HttpHeaderNames.EXPIRES)) { + WebServer.LOG.WARN("A response with 'set-cookie' and 'expires' was created for URI: %s%n%s%n%s", + wc.getRequestedURI(), + wc, + ExecutionPoint.snapshot()); + response.headers().remove(HttpHeaderNames.EXPIRES); + } + String cacheControl = response.headers().get(HttpHeaderNames.CACHE_CONTROL); + if (cacheControl != null && !cacheControl.startsWith(HttpHeaderValues.NO_CACHE.toString())) { + WebServer.LOG.WARN("A response with 'set-cookie' and 'cache-control' was created for URI: %s%n%s%n%s", + wc.getRequestedURI(), + wc, + ExecutionPoint.snapshot()); + response.headers().set(HttpHeaderNames.CACHE_CONTROL, HttpHeaderValues.NO_CACHE + ", max-age=0"); + } + } } private void updateStatistics(HttpResponseStatus status) { diff --git a/src/main/java/sirius/web/http/WebContext.java b/src/main/java/sirius/web/http/WebContext.java index f9fcd7028..61f3d85ad 100644 --- a/src/main/java/sirius/web/http/WebContext.java +++ b/src/main/java/sirius/web/http/WebContext.java @@ -78,6 +78,8 @@ import java.util.Set; import java.util.UUID; import java.util.concurrent.atomic.AtomicLong; +import java.util.regex.Matcher; +import java.util.regex.Pattern; /** * Provides access to a request received by the WebServer. @@ -92,6 +94,9 @@ public class WebContext implements SubContext { private static final String PROTOCOL_HTTPS = "https"; private static final String PROTOCOL_HTTP = "http"; + private static final Pattern ACCEPT_LANGUAGE_PATTERN = + Pattern.compile(" *([a-z]{2})(-[a-z]{2})? *(;q=([0-9.]+) *)?"); + /* * Underlying channel to send and receive data */ @@ -193,11 +198,6 @@ public class WebContext implements SubContext { */ private volatile boolean sessionModified; - /* - * Contains the decoded language as two-letter code - */ - private String lang; - /* * Specifies the microtiming key used for this request. If null, no microtiming will be recorded. */ @@ -1171,46 +1171,28 @@ private long determineSessionCookieTTL() { /** * Returns the accepted language of the client as two-letter language code. * - * @return the two-letter code of the accepted language of the user agent. Returns the current language, if no - * supported language was submitted. + * @return the two-letter code of the accepted language of the user agent or null if no valid accept + * language was found */ + @Nullable public String getLang() { - if (lang == null) { - lang = parseAcceptLanguage(); - } - return lang; - } - - /* - * Parses the accept language header - */ - private String parseAcceptLanguage() { double bestQ = 0; - String currentLang = CallContext.getCurrent().getLang(); + String currentLang = null; String header = getHeader(HttpHeaderNames.ACCEPT_LANGUAGE); if (Strings.isEmpty(header)) { return currentLang; } header = header.toLowerCase(); - for (String str : header.split(",")) { - String[] arr = str.trim().replace("-", "_").split(";"); - - //Parse the q-value - double q = 1.0D; - for (String s : arr) { - s = s.trim(); - if (s.startsWith("q=")) { - q = Double.parseDouble(s.substring(2).trim()); - break; + for (String languageBlock : header.split(",")) { + Matcher m = ACCEPT_LANGUAGE_PATTERN.matcher(languageBlock); + if (m.matches()) { + double q = Value.of(m.group(4)).asDouble(1.0d); + String language = m.group(1); + if (q > bestQ && NLS.isSupportedLanguage(language)) { + bestQ = q; + currentLang = language; } } - - //Parse the locale - String[] l = arr[0].split("_"); - if (l.length > 0 && q > bestQ && NLS.isSupportedLanguage(l[0])) { - currentLang = l[0]; - bestQ = q; - } } return currentLang; diff --git a/src/main/java/sirius/web/http/WebServerHandler.java b/src/main/java/sirius/web/http/WebServerHandler.java index 7b1863efd..334618342 100644 --- a/src/main/java/sirius/web/http/WebServerHandler.java +++ b/src/main/java/sirius/web/http/WebServerHandler.java @@ -35,6 +35,7 @@ import sirius.kernel.health.Average; import sirius.kernel.health.Exceptions; import sirius.kernel.nls.NLS; +import sirius.web.security.UserContext; import javax.net.ssl.SSLHandshakeException; import java.io.File; @@ -158,12 +159,21 @@ private WebContext setupContext(ChannelHandlerContext ctx, HttpRequest req) { currentCall = CallContext.initialize(); currentCall.addToMDC("uri", req.uri()); WebContext wc = currentCall.get(WebContext.class); - if (ssl) { - wc.ssl = true; - } + wc.ssl = this.ssl; wc.setCtx(ctx); wc.setRequest(req); currentCall.get(TaskContext.class).setSystem("HTTP").setJob(wc.getRequestedURI()); + + // Adds a deferred handler to determine the language to i18n stuff. + // If a user is present, the system will sooner or later detect it and set the appropriate + // language. If not, this handler will be evaluated, check for a user in the session or + // if everything else fails, parse the lang header. + currentCall.deferredSetLang(callContext -> { + if (!callContext.get(UserContext.class).bindUserIfPresent(wc).isPresent()) { + callContext.setLang(NLS.makeLang(wc.getLang())); + } + }); + return wc; } diff --git a/src/main/java/sirius/web/security/GenericUserManager.java b/src/main/java/sirius/web/security/GenericUserManager.java index f008cd66a..c1280de28 100644 --- a/src/main/java/sirius/web/security/GenericUserManager.java +++ b/src/main/java/sirius/web/security/GenericUserManager.java @@ -38,16 +38,6 @@ */ public abstract class GenericUserManager implements UserManager { - /** - * Defines the name used to store the user detail for cookie login storage - */ - private static final String USER_COOKIE_SUFFIX = "-sirius-user"; - - /** - * Defines the name used to store the token detail for cookie login storage - */ - private static final String TOKEN_COOKIE_SUFFIX = "-sirius-token"; - /** * /** * Defines the default grace period (max age of an sso timestamp) which is accepted by the system @@ -55,6 +45,7 @@ public abstract class GenericUserManager implements UserManager { private static final long DEFAULT_SSO_GRACE_INTERVAL = TimeUnit.HOURS.toSeconds(24); private static final String SUFFIX_USER_ID = "-user-id"; private static final String SUFFIX_TENANT_ID = "-tenant-id"; + private static final String SUFFIX_TTL = "-ttl"; protected final ScopeInfo scope; protected final Extension config; @@ -65,8 +56,7 @@ public abstract class GenericUserManager implements UserManager { protected String ssoSecret; protected List publicRoles; protected List defaultRoles; - protected List trustedRoles; - protected Duration loginCookieTTL; + protected Duration loginTTL; protected UserInfo defaultUser; @SuppressWarnings("unchecked") @@ -80,8 +70,7 @@ protected GenericUserManager(ScopeInfo scope, Extension config) { this.keepLoginEnabled = config.get("keepLoginEnabled").asBoolean(true); this.publicRoles = config.get("publicRoles").get(List.class, Collections.emptyList()); this.defaultRoles = config.get("defaultRoles").get(List.class, Collections.emptyList()); - this.trustedRoles = config.get("trustedRoles").get(List.class, Collections.emptyList()); - this.loginCookieTTL = config.get("loginCookieTTL").get(Duration.class, Duration.ofDays(90)); + this.loginTTL = config.get("loginTTL").get(Duration.class, Duration.ofDays(90)); this.defaultUser = buildDefaultUser(); } @@ -183,7 +172,12 @@ protected void recordUserLogin(WebContext ctx, UserInfo user) { * @param user the user that logged in */ protected void updateLoginCookie(WebContext ctx, UserInfo user) { - ctx.setCustomSessionCookieTTL(isKeepLogin(ctx) ? loginCookieTTL : Duration.ZERO); + ctx.setCustomSessionCookieTTL(isKeepLogin(ctx) ? null : Duration.ZERO); + ctx.setSessionValue(scope.getScopeId() + SUFFIX_USER_ID, user.getUserId()); + ctx.setSessionValue(scope.getScopeId() + SUFFIX_TENANT_ID, user.getTenantId()); + ctx.setSessionValue(scope.getScopeId() + SUFFIX_TTL, + TimeUnit.SECONDS.convert(System.currentTimeMillis(), TimeUnit.MILLISECONDS) + + loginTTL.getSeconds()); } private boolean isKeepLogin(WebContext ctx) { @@ -243,40 +237,6 @@ protected Tuple extractChallengeAndResponse(WebContext ctx) { return null; } - private UserInfo loginViaCookie(WebContext ctx) { - if (!keepLoginEnabled) { - return null; - } - String user = ctx.getCookieValue(scope.getScopeId() + USER_COOKIE_SUFFIX); - String token = ctx.getCookieValue(scope.getScopeId() + TOKEN_COOKIE_SUFFIX); - - if (Strings.isEmpty(user) || Strings.isEmpty(token)) { - return null; - } - - UserInfo result = findUserByName(ctx, user); - if (result == null) { - return null; - } - - // The cookie token is TIMESTAMP:MD5 - Tuple challengeResponse = Strings.split(token, ":"); - // Verify age... - if (checkTokenTTL(Value.of(challengeResponse.getFirst()).asLong(0), loginCookieTTL.getSeconds())) { - // Verify hash... - if (checkTokenValidity(user, challengeResponse)) { - log("Cookie-Login of %s succeeded with token: %s", user, token); - return result; - } else { - log("Cookie-Login of %s failed due to invalid hash in token: %s", user, token); - } - } else { - log("Cookie-Login of %s failed due to outdated timestamp in token: %s", user, token); - } - - return null; - } - private boolean checkTokenTTL(long timestamp, long maxTtl) { return Math.abs(timestamp - (System.currentTimeMillis() / 1000)) < maxTtl; } @@ -336,18 +296,13 @@ protected String computeSSOHashInput(String user, String timestamp) { /** * Applies profile transformations and adds default roles to the set of given roles. * - * @param roles the roles granted to a user - * @param trusted determines if the user is considered a trusted user - * (Usually determined via {@link sirius.web.http.WebContext#isTrusted()}). + * @param roles the roles granted to a user * @return a set of permissions which contain the given roles as well as the default roles and profile * transformations */ - protected Set transformRoles(Collection roles, boolean trusted) { + protected Set transformRoles(Collection roles) { Set allRoles = Sets.newTreeSet(roles); allRoles.addAll(defaultRoles); - if (trusted) { - allRoles.addAll(trustedRoles); - } return Permissions.applyProfilesAndPublicRoles(allRoles); } @@ -399,6 +354,11 @@ private UserInfo loginViaUsernameAndPassword(WebContext ctx) { protected UserInfo findUserInSession(WebContext ctx) { Value userId = ctx.getSessionValue(scope.getScopeId() + SUFFIX_USER_ID); String tenantId = ctx.getSessionValue(scope.getScopeId() + SUFFIX_TENANT_ID).asString(); + Long ttl = ctx.getSessionValue(scope.getScopeId() + SUFFIX_TTL).getLong(); + + if (ttl != null && ttl < System.currentTimeMillis()) { + return null; + } if (!userId.isFilled() || !isUserStillValid(userId.asString())) { return null; @@ -483,33 +443,16 @@ protected UserSettings getScopeSettings() { @Nonnull protected abstract String computeLang(WebContext ctx, String userId); - /** - * Attaches the given user to the current session. - *

- * This will make the login persistent across requests (if session management is enabled). - * - * @param user the user to attach to the session - * @param ctx the current request to attach the user to - */ - @Override - public void attachToSession(@Nonnull UserInfo user, @Nonnull WebContext ctx) { - ctx.setSessionValue(scope.getScopeId() + SUFFIX_TENANT_ID, user.getTenantId()); - ctx.setSessionValue(scope.getScopeId() + SUFFIX_USER_ID, user.getUserId()); - } - /** * Removes all stored user information from the current session. * - * @param user the current user - passed in, in case a cache etc. has to be cleared - * @param ctx the request to remove all data from + * @param ctx the request to remove all data from */ @Override - public void detachFromSession(@Nonnull UserInfo user, @Nonnull WebContext ctx) { + public void logout(@Nonnull WebContext ctx) { ctx.setSessionValue(scope.getScopeId() + SUFFIX_TENANT_ID, null); ctx.setSessionValue(scope.getScopeId() + SUFFIX_USER_ID, null); - - ctx.deleteCookie(scope.getScopeId() + USER_COOKIE_SUFFIX); - ctx.deleteCookie(scope.getScopeId() + TOKEN_COOKIE_SUFFIX); + ctx.setSessionValue(scope.getScopeId() + SUFFIX_TTL, null); } @Override diff --git a/src/main/java/sirius/web/security/PublicUserManager.java b/src/main/java/sirius/web/security/PublicUserManager.java index 41a78ed88..18a99529f 100644 --- a/src/main/java/sirius/web/security/PublicUserManager.java +++ b/src/main/java/sirius/web/security/PublicUserManager.java @@ -27,9 +27,6 @@ * This roles granted can be controlled by two config entries. One is security.publicRoles which * also affects all other user managers. The other is defaultRoles which has to be defined within * the scope. - *

- * Note that also trustedRoles can be defined to control roles which are only added to a trusted user - * (i.e. from the local network). */ public class PublicUserManager extends GenericUserManager { @@ -53,7 +50,7 @@ protected PublicUserManager(ScopeInfo scope, Extension config) { super(scope, config); this.user = UserInfo.Builder.createUser(PUBLIC_PLACEHOLDER) .withUsername(PUBLIC_PLACEHOLDER) - .withPermissions(transformRoles(Collections.emptyList(), false)) + .withPermissions(transformRoles(Collections.emptyList())) .build(); } @@ -103,12 +100,7 @@ protected String computeLang(WebContext ctx, String userId) { } @Override - public void attachToSession(@Nonnull UserInfo user, @Nonnull WebContext ctx) { - // Not required - there is actually no user... - } - - @Override - public void detachFromSession(@Nonnull UserInfo user, @Nonnull WebContext ctx) { + public void logout(@Nonnull WebContext ctx) { // Not required - there is actually no user... } diff --git a/src/main/java/sirius/web/security/UserContext.java b/src/main/java/sirius/web/security/UserContext.java index b8b054717..26d4424bb 100644 --- a/src/main/java/sirius/web/security/UserContext.java +++ b/src/main/java/sirius/web/security/UserContext.java @@ -28,6 +28,7 @@ import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Optional; /** * Used to access the current user and scope. @@ -210,12 +211,39 @@ private void bindUserToRequest(WebContext ctx) { UserManager manager = getUserManager(); UserInfo user = manager.bindToRequest(ctx); setCurrentUser(user); - CallContext.getCurrent().setLang(user.getLang()); } else { setCurrentUser(UserInfo.NOBODY); } } + /** + * Bind a user from the session if available. + *

+ * If no user is available (currently logged in) nothing will happen. User {@link #getUser()} + * to fully bind a user and attempt a login. + * + * @param ctx the current web context to bind against + * @return the user which was found in the session or an empty optional if none is present + */ + public Optional bindUserIfPresent(WebContext ctx) { + if (ctx == null || !ctx.isValid()) { + return Optional.empty(); + } + + if (currentUser != null) { + return Optional.of(currentUser); + } + + UserManager manager = getUserManager(); + UserInfo user = manager.findUserForRequest(ctx); + if (user.isLoggedIn()) { + setCurrentUser(user); + return Optional.of(user); + } + + return Optional.empty(); + } + /** * Installs the given scope as current scope. *

@@ -243,8 +271,10 @@ public void setCurrentScope(ScopeInfo scope) { */ public void setCurrentUser(@Nullable UserInfo user) { this.currentUser = user == null ? UserInfo.NOBODY : user; - CallContext.getCurrent().addToMDC(MDC_USER_ID, () -> currentUser.getUserId()); - CallContext.getCurrent().addToMDC(MDC_USER_NAME, () -> currentUser.getUserName()); + CallContext call = CallContext.getCurrent(); + call.addToMDC(MDC_USER_ID, () -> currentUser.getUserId()); + call.addToMDC(MDC_USER_NAME, () -> currentUser.getUserName()); + call.setLang(user.getLang()); } /** @@ -411,7 +441,7 @@ public String getFieldErrorMessage(String field) { /** * Returns the current user. *

- * If no user is present yet, it tries to parse the current {@link WebContext} and retireve the user from the + * If no user is present yet, it tries to parse the current {@link WebContext} and retrieve the user from the * session. * * @return the currently active user @@ -465,23 +495,6 @@ public boolean isUserPresent() { return currentUser != null; } - /** - * Binds the currently active user to the session. - *

- * This will make the authentication of a user persistent als long as the session remains - */ - public void attachUserToSession() { - WebContext ctx = CallContext.getCurrent().get(WebContext.class); - if (!ctx.isValid()) { - return; - } - if (!getUser().isLoggedIn()) { - return; - } - UserManager manager = getUserManager(); - manager.attachToSession(getUser(), ctx); - } - /** * Determines and returns the current user manager. *

@@ -507,7 +520,7 @@ public void detachUserFromSession() { return; } UserManager manager = getUserManager(); - manager.detachFromSession(getCurrentUser(), ctx); + manager.logout(ctx); } /** diff --git a/src/main/java/sirius/web/security/UserManager.java b/src/main/java/sirius/web/security/UserManager.java index ccb8946fb..75d63f0ef 100644 --- a/src/main/java/sirius/web/security/UserManager.java +++ b/src/main/java/sirius/web/security/UserManager.java @@ -65,23 +65,14 @@ public interface UserManager { @Nullable UserInfo findUserByCredentials(@Nullable WebContext ctx, String user, String password); - /** - * Makes the currently authenticated user persistent by storing the required information in the session. - * - * @param user the user to store - * @param ctx the request containing the session - */ - void attachToSession(@Nonnull UserInfo user, @Nonnull WebContext ctx); - /** * Removes all stored data from the session *

* This can be considered a logout operation. * - * @param user the user to logout * @param ctx the request containing the session */ - void detachFromSession(@Nonnull UserInfo user, @Nonnull WebContext ctx); + void logout(@Nonnull WebContext ctx); /** * Determines if a login via username and password is possible. diff --git a/src/main/java/sirius/web/services/ServiceDispatcher.java b/src/main/java/sirius/web/services/ServiceDispatcher.java index 7cc565ae1..4f70f5fd8 100644 --- a/src/main/java/sirius/web/services/ServiceDispatcher.java +++ b/src/main/java/sirius/web/services/ServiceDispatcher.java @@ -11,6 +11,7 @@ import io.netty.handler.codec.http.HttpResponseStatus; import sirius.kernel.async.CallContext; import sirius.kernel.async.TaskContext; +import sirius.kernel.commons.CachingSupplier; import sirius.kernel.commons.PriorityCollector; import sirius.kernel.commons.Strings; import sirius.kernel.commons.Tuple; @@ -113,22 +114,10 @@ private void invokeService(WebContext ctx, ServiceCall call, StructuredService s } } - // Install language - CallContext.getCurrent().setLang(NLS.makeLang(ctx.getLang())); - - // Install user. This is forcefully called here to ensure that the ScopeDetetor - // and the user manager are guaranteed to be invoked one we enter the service code... - UserInfo user = UserContext.getCurrentUser(); - - // If the underlying ScopeDetector made a redirect (for whatever reasons) - // the response will be committed and we can (must) safely return... - if (ctx.isResponseCommitted()) { - return; - } - // ... and check permissions + CachingSupplier userSupplier = new CachingSupplier<>(UserContext::getCurrentUser); for (String p : Permissions.computePermissionsFromAnnotations(serv.getClass())) { - if (!user.hasPermission(p)) { + if (!userSupplier.get().hasPermission(p)) { ctx.respondWith().error(HttpResponseStatus.UNAUTHORIZED, "Missing permission: " + p); return; } diff --git a/src/main/resources/component-web.conf b/src/main/resources/component-web.conf index 7c813edf1..6498a5f11 100644 --- a/src/main/resources/component-web.conf +++ b/src/main/resources/component-web.conf @@ -435,9 +435,6 @@ security { # Defines roles granted to all "users". defaultRoles = [ "permission-system-state", "permission-system-tags", "permission-system-tags-state" ] - - # Defines roles granted to trusted users (Those whose IP match http.firewall.trustedIPs). - trustedRoles = [ ] } } diff --git a/src/test/java/sirius/web/controller/TestController.java b/src/test/java/sirius/web/controller/TestController.java index fb4fcda77..3865965e6 100644 --- a/src/test/java/sirius/web/controller/TestController.java +++ b/src/test/java/sirius/web/controller/TestController.java @@ -53,6 +53,12 @@ public void testJSON(WebContext ctx, JSONStructuredOutput out) { out.property("test", ctx.getParameter("test")); } + @Routed("/test/cookieCacheTest") + public void testCookieCacheTest(WebContext ctx) { + ctx.setCookie("Test", "1", 3600); + ctx.respondWith().cached().direct(HttpResponseStatus.OK, "OK"); + } + @Routed(value = "/test/json-param/:1", jsonCall = true) public void testJSONParam(WebContext ctx, JSONStructuredOutput out, String param) { out.property("test", param); diff --git a/src/test/java/sirius/web/http/WebContextSpec.groovy b/src/test/java/sirius/web/http/WebContextSpec.groovy index cafa71d67..57e8719b8 100644 --- a/src/test/java/sirius/web/http/WebContextSpec.groovy +++ b/src/test/java/sirius/web/http/WebContextSpec.groovy @@ -8,6 +8,8 @@ package sirius.web.http +import io.netty.handler.codec.http.HttpHeaderNames +import io.netty.handler.codec.http.HttpHeaderValues import sirius.kernel.BaseSpecification class WebContextSpec extends BaseSpecification { @@ -64,4 +66,17 @@ class WebContextSpec extends BaseSpecification { and: r.get("a").isFilled() } + + + def "parseAcceptLanguage works as expected"(header, lang) { + expect: + TestRequest.GET("/test?a=a").addHeader(HttpHeaderNames.ACCEPT_LANGUAGE, header).getLang() == lang + where: + header | lang + "de, en;q=0.8" | "de" + "en, de;q=0.8" | "en" + "xx, de;q=0.8, en-gb;q=0.7" | "de" + "xx, de;q=0.5, en-gb;q=0.7" | "en" + } + } diff --git a/src/test/java/sirius/web/http/WebServerSpec.groovy b/src/test/java/sirius/web/http/WebServerSpec.groovy index 6ac514c03..2b9017cc4 100644 --- a/src/test/java/sirius/web/http/WebServerSpec.groovy +++ b/src/test/java/sirius/web/http/WebServerSpec.groovy @@ -28,9 +28,6 @@ import sirius.kernel.health.LogHelper *

* This ensures a basic performance profile and also makes sure no trivial race conditions or memory leaks * are added. - * - * @author Andreas Haufler (aha@scireum.de) - * @since 2015/05 */ class WebServerSpec extends BaseSpecification { @@ -40,7 +37,11 @@ class WebServerSpec extends BaseSpecification { c.connect() def result = new String(ByteStreams.toByteArray(c.getInputStream()), Charsets.UTF_8) expectedHeaders.each { k, v -> - if (!Strings.areEqual(c.getHeaderField(k), v)) { + if ("*" == v) { + if (Strings.isEmpty(c.getHeaderField(k))) { + throw new IllegalStateException("Header: " + k + " was expected, but not set") + } + } else if (!Strings.areEqual(c.getHeaderField(k), v)) { throw new IllegalStateException("Header: " + k + " was " + c.getHeaderField(k) + " instead of " + v) } } @@ -48,6 +49,21 @@ class WebServerSpec extends BaseSpecification { return result } + /** + * Ensures that set-cookie and caching headers aren't mixed. + */ + def "Invoke /test/cookieCacheTest"() { + given: + def uri = "/test/cookieCacheTest" + def headers = ['accept-encoding': 'gzip'] + // File is too small to be compressed! + def expectedHeaders = ['set-cookie': '*', 'expires': null, 'cache-control': 'no-cache, max-age=0'] + when: + def data = callAndRead(uri, headers, expectedHeaders) + then: + notThrown(IllegalStateException) + } + def "Invoke /assets/test.css to test"() { given: def uri = "/assets/test.css" @@ -314,7 +330,7 @@ class WebServerSpec extends BaseSpecification { when: u.setRequestMethod("POST") u.setRequestProperty("Content-Type", - "application/x-www-form-urlencoded") + "application/x-www-form-urlencoded") u.setRequestProperty("Content-Length", Integer.toString(testString.getBytes().length)) u.setDoInput(true) @@ -351,7 +367,7 @@ class WebServerSpec extends BaseSpecification { when: u.setRequestMethod("POST") u.setRequestProperty("Content-Type", - "application/x-www-form-urlencoded") + "application/x-www-form-urlencoded") u.setRequestProperty("Content-Length", Integer.toString(testString.getBytes().length)) u.setDoInput(true) @@ -400,11 +416,11 @@ class WebServerSpec extends BaseSpecification { def counter = 0 def count = 0 def buffer = new byte[8192] - while((count = input.read(buffer)) > 0) { + while ((count = input.read(buffer)) > 0) { counter += count } - return counter; + return counter } def "HTTP pipelining is supported correctly"() { diff --git a/src/test/resources/test.conf b/src/test/resources/test.conf index a65d55cfb..2351c42e2 100644 --- a/src/test/resources/test.conf +++ b/src/test/resources/test.conf @@ -2,4 +2,6 @@ sirius.frameworks { web.test-firewall: true } +nls.languages = [de, en] + sirius.customizations = ["customized"]