001/* 002 * Copyright 2002-2018 the original author or authors. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * https://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 017package org.springframework.messaging.simp.user; 018 019import java.security.Principal; 020import java.util.Collections; 021import java.util.HashSet; 022import java.util.Set; 023 024import org.apache.commons.logging.Log; 025import org.apache.commons.logging.LogFactory; 026 027import org.springframework.messaging.Message; 028import org.springframework.messaging.MessageHeaders; 029import org.springframework.messaging.simp.SimpMessageHeaderAccessor; 030import org.springframework.messaging.simp.SimpMessageType; 031import org.springframework.util.Assert; 032import org.springframework.util.PathMatcher; 033import org.springframework.util.StringUtils; 034 035/** 036 * A default implementation of {@code UserDestinationResolver} that relies 037 * on a {@link SimpUserRegistry} to find active sessions for a user. 038 * 039 * <p>When a user attempts to subscribe, e.g. to "/user/queue/position-updates", 040 * the "/user" prefix is removed and a unique suffix added based on the session 041 * id, e.g. "/queue/position-updates-useri9oqdfzo" to ensure different users can 042 * subscribe to the same logical destination without colliding. 043 * 044 * <p>When sending to a user, e.g. "/user/{username}/queue/position-updates", the 045 * "/user/{username}" prefix is removed and a suffix based on active session id's 046 * is added, e.g. "/queue/position-updates-useri9oqdfzo". 047 * 048 * @author Rossen Stoyanchev 049 * @author Brian Clozel 050 * @since 4.0 051 */ 052public class DefaultUserDestinationResolver implements UserDestinationResolver { 053 054 private static final Log logger = LogFactory.getLog(DefaultUserDestinationResolver.class); 055 056 057 private final SimpUserRegistry userRegistry; 058 059 private String prefix = "/user/"; 060 061 private boolean removeLeadingSlash = false; 062 063 064 /** 065 * Create an instance that will access user session id information through 066 * the provided registry. 067 * @param userRegistry the registry, never {@code null} 068 */ 069 public DefaultUserDestinationResolver(SimpUserRegistry userRegistry) { 070 Assert.notNull(userRegistry, "SimpUserRegistry must not be null"); 071 this.userRegistry = userRegistry; 072 } 073 074 075 /** 076 * Return the configured {@link SimpUserRegistry}. 077 */ 078 public SimpUserRegistry getSimpUserRegistry() { 079 return this.userRegistry; 080 } 081 082 /** 083 * The prefix used to identify user destinations. Any destinations that do not 084 * start with the given prefix are not be resolved. 085 * <p>The default prefix is "/user/". 086 * @param prefix the prefix to use 087 */ 088 public void setUserDestinationPrefix(String prefix) { 089 Assert.hasText(prefix, "Prefix must not be empty"); 090 this.prefix = (prefix.endsWith("/") ? prefix : prefix + "/"); 091 } 092 093 /** 094 * Return the configured prefix for user destinations. 095 */ 096 public String getDestinationPrefix() { 097 return this.prefix; 098 } 099 100 /** 101 * Use this property to indicate whether the leading slash from translated 102 * user destinations should be removed or not. This depends on the 103 * destination prefixes the message broker is configured with. 104 * <p>By default this is set to {@code false}, i.e. 105 * "do not change the target destination", although 106 * {@link org.springframework.messaging.simp.config.AbstractMessageBrokerConfiguration 107 * AbstractMessageBrokerConfiguration} may change that to {@code true} 108 * if the configured destinations do not have a leading slash. 109 * @param remove whether to remove the leading slash 110 * @since 4.3.14 111 */ 112 public void setRemoveLeadingSlash(boolean remove) { 113 this.removeLeadingSlash = remove; 114 } 115 116 /** 117 * Whether to remove the leading slash from target destinations. 118 * @since 4.3.14 119 */ 120 public boolean isRemoveLeadingSlash() { 121 return this.removeLeadingSlash; 122 } 123 124 /** 125 * Provide the {@code PathMatcher} in use for working with destinations 126 * which in turn helps to determine whether the leading slash should be 127 * kept in actual destinations after removing the 128 * {@link #setUserDestinationPrefix userDestinationPrefix}. 129 * <p>By default actual destinations have a leading slash, e.g. 130 * {@code /queue/position-updates} which makes sense with brokers that 131 * support destinations with slash as separator. When a {@code PathMatcher} 132 * is provided that supports an alternative separator, then resulting 133 * destinations won't have a leading slash, e.g. {@code 134 * jms.queue.position-updates}. 135 * @param pathMatcher the PathMatcher used to work with destinations 136 * @since 4.3 137 * @deprecated as of 4.3.14 this property is no longer used and is replaced 138 * by {@link #setRemoveLeadingSlash(boolean)} that indicates more explicitly 139 * whether to keep the leading slash which may or may not be the case 140 * regardless of how the {@code PathMatcher} is configured. 141 */ 142 @Deprecated 143 public void setPathMatcher(PathMatcher pathMatcher) { 144 // Do nothing 145 } 146 147 148 @Override 149 public UserDestinationResult resolveDestination(Message<?> message) { 150 ParseResult parseResult = parse(message); 151 if (parseResult == null) { 152 return null; 153 } 154 String user = parseResult.getUser(); 155 String sourceDestination = parseResult.getSourceDestination(); 156 Set<String> targetSet = new HashSet<String>(); 157 for (String sessionId : parseResult.getSessionIds()) { 158 String actualDestination = parseResult.getActualDestination(); 159 String targetDestination = getTargetDestination( 160 sourceDestination, actualDestination, sessionId, user); 161 if (targetDestination != null) { 162 targetSet.add(targetDestination); 163 } 164 } 165 String subscribeDestination = parseResult.getSubscribeDestination(); 166 return new UserDestinationResult(sourceDestination, targetSet, subscribeDestination, user); 167 } 168 169 private ParseResult parse(Message<?> message) { 170 MessageHeaders headers = message.getHeaders(); 171 String sourceDestination = SimpMessageHeaderAccessor.getDestination(headers); 172 if (sourceDestination == null || !checkDestination(sourceDestination, this.prefix)) { 173 return null; 174 } 175 SimpMessageType messageType = SimpMessageHeaderAccessor.getMessageType(headers); 176 switch (messageType) { 177 case SUBSCRIBE: 178 case UNSUBSCRIBE: 179 return parseSubscriptionMessage(message, sourceDestination); 180 case MESSAGE: 181 return parseMessage(headers, sourceDestination); 182 default: 183 return null; 184 } 185 } 186 187 private ParseResult parseSubscriptionMessage(Message<?> message, String sourceDestination) { 188 MessageHeaders headers = message.getHeaders(); 189 String sessionId = SimpMessageHeaderAccessor.getSessionId(headers); 190 if (sessionId == null) { 191 logger.error("No session id. Ignoring " + message); 192 return null; 193 } 194 int prefixEnd = this.prefix.length() - 1; 195 String actualDestination = sourceDestination.substring(prefixEnd); 196 if (isRemoveLeadingSlash()) { 197 actualDestination = actualDestination.substring(1); 198 } 199 Principal principal = SimpMessageHeaderAccessor.getUser(headers); 200 String user = (principal != null ? principal.getName() : null); 201 Set<String> sessionIds = Collections.singleton(sessionId); 202 return new ParseResult(sourceDestination, actualDestination, sourceDestination, sessionIds, user); 203 } 204 205 private ParseResult parseMessage(MessageHeaders headers, String sourceDest) { 206 int prefixEnd = this.prefix.length(); 207 int userEnd = sourceDest.indexOf('/', prefixEnd); 208 Assert.isTrue(userEnd > 0, "Expected destination pattern \"/user/{userId}/**\""); 209 String actualDest = sourceDest.substring(userEnd); 210 String subscribeDest = this.prefix.substring(0, prefixEnd - 1) + actualDest; 211 String userName = sourceDest.substring(prefixEnd, userEnd); 212 userName = StringUtils.replace(userName, "%2F", "/"); 213 214 String sessionId = SimpMessageHeaderAccessor.getSessionId(headers); 215 Set<String> sessionIds; 216 if (userName.equals(sessionId)) { 217 userName = null; 218 sessionIds = Collections.singleton(sessionId); 219 } 220 else { 221 sessionIds = getSessionIdsByUser(userName, sessionId); 222 } 223 224 if (isRemoveLeadingSlash()) { 225 actualDest = actualDest.substring(1); 226 } 227 return new ParseResult(sourceDest, actualDest, subscribeDest, sessionIds, userName); 228 } 229 230 private Set<String> getSessionIdsByUser(String userName, String sessionId) { 231 Set<String> sessionIds; 232 SimpUser user = this.userRegistry.getUser(userName); 233 if (user != null) { 234 if (user.getSession(sessionId) != null) { 235 sessionIds = Collections.singleton(sessionId); 236 } 237 else { 238 Set<SimpSession> sessions = user.getSessions(); 239 sessionIds = new HashSet<String>(sessions.size()); 240 for (SimpSession session : sessions) { 241 sessionIds.add(session.getId()); 242 } 243 } 244 } 245 else { 246 sessionIds = Collections.emptySet(); 247 } 248 return sessionIds; 249 } 250 251 protected boolean checkDestination(String destination, String requiredPrefix) { 252 return destination.startsWith(requiredPrefix); 253 } 254 255 /** 256 * This method determines how to translate the source "user" destination to an 257 * actual target destination for the given active user session. 258 * @param sourceDestination the source destination from the input message. 259 * @param actualDestination a subset of the destination without any user prefix. 260 * @param sessionId the id of an active user session, never {@code null}. 261 * @param user the target user, possibly {@code null}, e.g if not authenticated. 262 * @return a target destination, or {@code null} if none 263 */ 264 @SuppressWarnings("unused") 265 protected String getTargetDestination(String sourceDestination, String actualDestination, 266 String sessionId, String user) { 267 268 return actualDestination + "-user" + sessionId; 269 } 270 271 @Override 272 public String toString() { 273 return "DefaultUserDestinationResolver[prefix=" + this.prefix + "]"; 274 } 275 276 277 /** 278 * A temporary placeholder for a parsed source "user" destination. 279 */ 280 private static class ParseResult { 281 282 private final String sourceDestination; 283 284 private final String actualDestination; 285 286 private final String subscribeDestination; 287 288 private final Set<String> sessionIds; 289 290 private final String user; 291 292 public ParseResult(String sourceDest, String actualDest, String subscribeDest, 293 Set<String> sessionIds, String user) { 294 295 this.sourceDestination = sourceDest; 296 this.actualDestination = actualDest; 297 this.subscribeDestination = subscribeDest; 298 this.sessionIds = sessionIds; 299 this.user = user; 300 } 301 302 public String getSourceDestination() { 303 return this.sourceDestination; 304 } 305 306 public String getActualDestination() { 307 return this.actualDestination; 308 } 309 310 public String getSubscribeDestination() { 311 return this.subscribeDestination; 312 } 313 314 public Set<String> getSessionIds() { 315 return this.sessionIds; 316 } 317 318 public String getUser() { 319 return this.user; 320 } 321 } 322 323}