/** * */ package vnet.sms.gateway.nettysupport.login.incoming; import static org.apache.commons.lang.Validate.notNull; import java.io.Serializable; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; import org.jboss.netty.channel.ChannelHandlerContext; import org.jboss.netty.channel.MessageEvent; import org.jboss.netty.channel.SimpleChannelUpstreamHandler; import org.jboss.netty.util.Timeout; import org.jboss.netty.util.Timer; import org.jboss.netty.util.TimerTask; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import vnet.sms.common.messages.GsmPdu; import vnet.sms.common.wme.WindowedMessageEvent; import vnet.sms.common.wme.acknowledge.SendLoginRequestAckEvent; import vnet.sms.common.wme.acknowledge.SendLoginRequestNackEvent; import vnet.sms.common.wme.receive.ReceivedLoginRequestEvent; import vnet.sms.gateway.nettysupport.MessageProcessingContext; /** * @author obergner * */ public class IncomingLoginRequestsChannelHandler<ID extends Serializable> extends SimpleChannelUpstreamHandler { public static final String NAME = "vnet.sms.gateway:incoming-login-handler"; private final Logger log = LoggerFactory .getLogger(getClass()); private final AuthenticationManager authenticationManager; private final AtomicReference<Authentication> authenticatedClient = new AtomicReference<Authentication>(); private final long failedLoginResponseDelayMillis; private final Timer failedLoginResponseTimer; public IncomingLoginRequestsChannelHandler( final AuthenticationManager authenticationManager, final long failedLoginResponseDelayMillis, final Timer failedLoginResponseTimer) { notNull(authenticationManager, "Argument 'authenticationManager' must not be null"); notNull(failedLoginResponseTimer, "Argument 'failedLoginResponseTimer' must not be null"); this.authenticationManager = authenticationManager; this.failedLoginResponseDelayMillis = failedLoginResponseDelayMillis; this.failedLoginResponseTimer = failedLoginResponseTimer; } /** * @see org.jboss.netty.channel.SimpleChannelUpstreamHandler#messageReceived(org.jboss.netty.channel.ChannelHandlerContext, * org.jboss.netty.channel.MessageEvent) */ @Override public void messageReceived(final ChannelHandlerContext ctx, final MessageEvent e) throws Exception { if (e instanceof ReceivedLoginRequestEvent) { loginRequestReceived(ctx, (ReceivedLoginRequestEvent<ID>) e); } else if (e instanceof WindowedMessageEvent) { nonLoginRequestReceived(ctx, (WindowedMessageEvent<ID, ? extends GsmPdu>) e); } else { throw new IllegalArgumentException("Unsupported MessageEvent: " + e); } } private void loginRequestReceived(final ChannelHandlerContext ctx, final ReceivedLoginRequestEvent<ID> e) { this.log.info( "Attempting to authenticate current channel {} using credentials from {} ...", ctx.getChannel(), e); if (isCurrentChannelAuthenticated()) { this.log.warn( "Ignoring attempt to re-authenticate an already authenticated channel {}", ctx.getChannel()); return; } try { final Authentication authentication = this.authenticationManager .authenticate(new UsernamePasswordAuthenticationToken(e .getMessage().getUsername(), e.getMessage() .getPassword())); processSuccessfulAuthentication(ctx, e, authentication); } catch (final AuthenticationException ae) { processFailedAuthentication(ctx, e, ae); } // Send LoginRequest further upstream - it might be needed for auditing, // logging, metrics ... ctx.sendUpstream(e); } private void processSuccessfulAuthentication( final ChannelHandlerContext ctx, final ReceivedLoginRequestEvent<ID> e, final Authentication authentication) { if (!this.authenticatedClient.compareAndSet(null, authentication)) { this.log.warn( "Ignoring attempt to re-authenticate an already authenticated channel {}", ctx.getChannel()); return; } try { MessageProcessingContext.INSTANCE.onUserEnter(authentication); this.log.info( "Successfully authenticated channel {} - authenticated user is {}", ctx.getChannel(), authentication.getPrincipal()); ctx.sendDownstream(SendLoginRequestAckEvent.accept(e)); // Inform the wider community ... ctx.sendUpstream(new ChannelSuccessfullyAuthenticatedEvent(ctx .getChannel(), e.getMessage())); } finally { MessageProcessingContext.INSTANCE.onUserExit(authentication); } } private void processFailedAuthentication(final ChannelHandlerContext ctx, final ReceivedLoginRequestEvent<ID> e, final AuthenticationException ae) { this.log.warn("Authentication using credentials from " + e + " failed - will delay negative response for [" + this.failedLoginResponseDelayMillis + "] milliseconds to prevent DoS attacks", ae); this.failedLoginResponseTimer.newTimeout( this.new DelayFailedLoginResponse(ctx, e), this.failedLoginResponseDelayMillis, TimeUnit.MILLISECONDS); // Inform the wider community ... ctx.sendUpstream(new ChannelAuthenticationFailedEvent(ctx.getChannel(), e.getMessage(), ae)); } private void nonLoginRequestReceived(final ChannelHandlerContext ctx, final WindowedMessageEvent<ID, ? extends GsmPdu> e) { if (!isCurrentChannelAuthenticated()) { nonLoginRequestReceivedOnUnauthenticatedChannel(ctx, e); return; } nonLoginRequestReceivedOnAuthenticatedChannel(ctx, e); } private void nonLoginRequestReceivedOnUnauthenticatedChannel( final ChannelHandlerContext ctx, final WindowedMessageEvent<ID, ? extends GsmPdu> e) { this.log.warn( "Received non-login request {} on UNAUTHENTICATED channel {} - DISCARD", e, ctx.getChannel()); ctx.sendDownstream(NonLoginMessageReceivedOnUnauthenticatedChannelEvent .discardNonLoginMessage(e)); return; } private void nonLoginRequestReceivedOnAuthenticatedChannel( final ChannelHandlerContext ctx, final WindowedMessageEvent<ID, ? extends GsmPdu> e) throws IllegalArgumentException { try { MessageProcessingContext.INSTANCE .onUserEnter(this.authenticatedClient.get()); this.log.trace( "Received non-login request {} on authenticated channel {} - will propagate event further upstream", e, ctx.getChannel()); ctx.sendUpstream(e); } finally { MessageProcessingContext.INSTANCE .onUserExit(this.authenticatedClient.get()); } } private boolean isCurrentChannelAuthenticated() { return this.authenticatedClient.get() != null; } private final class DelayFailedLoginResponse implements TimerTask { private final ChannelHandlerContext ctx; private final ReceivedLoginRequestEvent<ID> rejectedLogin; DelayFailedLoginResponse(final ChannelHandlerContext ctx, final ReceivedLoginRequestEvent<ID> rejectedLogin) { this.ctx = ctx; this.rejectedLogin = rejectedLogin; } @Override public void run(final Timeout timeout) throws Exception { if (timeout.isCancelled() || !this.ctx.getChannel().isOpen()) { return; } IncomingLoginRequestsChannelHandler.this.log .warn("Sending response to failed login request {} after delay of {} milliseconds", this.rejectedLogin.getMessage(), IncomingLoginRequestsChannelHandler.this.failedLoginResponseDelayMillis); this.ctx.sendDownstream(SendLoginRequestNackEvent .reject(this.rejectedLogin)); } } }