com.couchbase.client.core.endpoint.kv.KeyValueAuthHandler Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of core-io Show documentation
Show all versions of core-io Show documentation
The official Couchbase JVM Core IO Library
/**
* Copyright (C) 2014 Couchbase, Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALING
* IN THE SOFTWARE.
*/
package com.couchbase.client.core.endpoint.kv;
import com.couchbase.client.core.logging.CouchbaseLogger;
import com.couchbase.client.core.logging.CouchbaseLoggerFactory;
import com.couchbase.client.deps.io.netty.handler.codec.memcache.binary.BinaryMemcacheResponseStatus;
import com.couchbase.client.deps.io.netty.handler.codec.memcache.binary.DefaultBinaryMemcacheRequest;
import com.couchbase.client.deps.io.netty.handler.codec.memcache.binary.DefaultFullBinaryMemcacheRequest;
import com.couchbase.client.deps.io.netty.handler.codec.memcache.binary.FullBinaryMemcacheRequest;
import com.couchbase.client.deps.io.netty.handler.codec.memcache.binary.FullBinaryMemcacheResponse;
import com.couchbase.client.deps.io.netty.buffer.ByteBuf;
import com.couchbase.client.deps.io.netty.buffer.Unpooled;
import com.couchbase.client.deps.io.netty.channel.ChannelFuture;
import com.couchbase.client.deps.io.netty.channel.ChannelHandlerContext;
import com.couchbase.client.deps.io.netty.channel.ChannelOutboundHandler;
import com.couchbase.client.deps.io.netty.channel.ChannelPromise;
import com.couchbase.client.deps.io.netty.channel.SimpleChannelInboundHandler;
import com.couchbase.client.deps.io.netty.util.CharsetUtil;
import com.couchbase.client.deps.io.netty.util.concurrent.Future;
import com.couchbase.client.deps.io.netty.util.concurrent.GenericFutureListener;
import javax.security.auth.callback.Callback;
import javax.security.auth.callback.CallbackHandler;
import javax.security.auth.callback.NameCallback;
import javax.security.auth.callback.PasswordCallback;
import javax.security.auth.callback.UnsupportedCallbackException;
import javax.security.sasl.Sasl;
import javax.security.sasl.SaslClient;
import java.io.IOException;
import java.net.SocketAddress;
/**
* A SASL Client which communicates through the memcache binary protocol.
*
* @author Michael Nitschinger
* @since 1.0
*/
public class KeyValueAuthHandler
extends SimpleChannelInboundHandler
implements CallbackHandler, ChannelOutboundHandler {
/**
* The logger used.
*/
private static final CouchbaseLogger LOGGER = CouchbaseLoggerFactory.getInstance(KeyValueAuthHandler.class);
/**
* The memcache opcode for the SASL mechs list.
*/
public static final byte SASL_LIST_MECHS_OPCODE = 0x20;
/**
* The memcache opcode for the SASL initial auth.
*/
private static final byte SASL_AUTH_OPCODE = 0x21;
/**
* The memcache opcode for the SASL consecutive steps.
*/
private static final byte SASL_STEP_OPCODE = 0x22;
/**
* The server response indicating SASL auth success.
*/
private static final byte SASL_AUTH_SUCCESS = BinaryMemcacheResponseStatus.SUCCESS;
/**
* The server response indicating SASL auth failure.
*/
private static final byte SASL_AUTH_FAILURE = BinaryMemcacheResponseStatus.AUTH_ERROR;
/**
* The username to auth against.
*/
private final String username;
/**
* The password of the user.
*/
private final String password;
/**
* The handler context.
*/
private ChannelHandlerContext ctx;
/**
* The JVM {@link SaslClient} that handles the actual authentication process.
*/
private SaslClient saslClient;
/**
* Contains the selected SASL auth mechanism once decided.
*/
private String selectedMechanism;
/**
* The connect promise issued by the connect process.
*/
private ChannelPromise originalPromise;
/**
* Creates a new {@link KeyValueAuthHandler}.
*
* @param username the name of the user/bucket.
* @param password the password associated with the user/bucket.
*/
public KeyValueAuthHandler(String username, String password) {
this.username = username;
this.password = password == null ? "" : password;
}
/**
* Once the channel is marked as active, the SASL negotiation is started.
*
* @param ctx the handler context.
* @throws Exception if something goes wrong during negotiation.
*/
@Override
public void channelActive(final ChannelHandlerContext ctx) throws Exception {
this.ctx = ctx;
ctx.writeAndFlush(new DefaultBinaryMemcacheRequest().setOpcode(SASL_LIST_MECHS_OPCODE));
}
/**
* Callback handler needed for the {@link SaslClient} which supplies username and password.
*
* @param callbacks the possible callbacks.
* @throws IOException if something goes wrong during negotiation.
* @throws UnsupportedCallbackException if something goes wrong during negotiation.
*/
@Override
public void handle(final Callback[] callbacks) throws IOException, UnsupportedCallbackException {
for (Callback callback : callbacks) {
if (callback instanceof NameCallback) {
((NameCallback) callback).setName(username);
} else if (callback instanceof PasswordCallback) {
((PasswordCallback) callback).setPassword(password.toCharArray());
} else {
throw new AuthenticationException("SASLClient requested unsupported callback: " + callback);
}
}
}
/**
* Dispatches incoming SASL responses to the appropriate handler methods.
*
* @param ctx the handler context.
* @param msg the incoming message to investigate.
* @throws Exception if something goes wrong during negotiation.
*/
@Override
protected void channelRead0(ChannelHandlerContext ctx, FullBinaryMemcacheResponse msg) throws Exception {
if (msg.getOpcode() == SASL_LIST_MECHS_OPCODE) {
handleListMechsResponse(ctx, msg);
} else if (msg.getOpcode() == SASL_AUTH_OPCODE) {
handleAuthResponse(ctx, msg);
} else if (msg.getOpcode() == SASL_STEP_OPCODE) {
checkIsAuthed(msg);
}
}
/**
* Handles an incoming SASL list mechanisms response and dispatches the next SASL AUTH step.
*
* @param ctx the handler context.
* @param msg the incoming message to investigate.
* @throws Exception if something goes wrong during negotiation.
*/
private void handleListMechsResponse(ChannelHandlerContext ctx, FullBinaryMemcacheResponse msg) throws Exception {
String remote = ctx.channel().remoteAddress().toString();
String[] supportedMechanisms = msg.content().toString(CharsetUtil.UTF_8).split(" ");
if (supportedMechanisms.length == 0) {
throw new AuthenticationException("Received empty SASL mechanisms list from server: " + remote);
}
saslClient = Sasl.createSaslClient(supportedMechanisms, null, "couchbase", remote, null, this);
selectedMechanism = saslClient.getMechanismName();
int mechanismLength = selectedMechanism.length();
byte[] bytePayload = saslClient.hasInitialResponse() ? saslClient.evaluateChallenge(new byte[]{}) : null;
ByteBuf payload = bytePayload != null ? ctx.alloc().buffer().writeBytes(bytePayload) : Unpooled.EMPTY_BUFFER;
FullBinaryMemcacheRequest initialRequest = new DefaultFullBinaryMemcacheRequest(
selectedMechanism,
Unpooled.EMPTY_BUFFER,
payload
);
initialRequest
.setOpcode(SASL_AUTH_OPCODE)
.setKeyLength((short) mechanismLength)
.setTotalBodyLength(mechanismLength + payload.readableBytes());
ChannelFuture future = ctx.writeAndFlush(initialRequest);
future.addListener(new GenericFutureListener>() {
@Override
public void operationComplete(Future future) throws Exception {
if (!future.isSuccess()) {
LOGGER.warn("Error during SASL Auth negotiation phase.", future);
}
}
});
}
/**
* Handles an incoming SASL AUTH response and - if needed - dispatches the SASL STEPs.
*
* @param ctx the handler context.
* @param msg the incoming message to investigate.
* @throws Exception if something goes wrong during negotiation.
*/
private void handleAuthResponse(ChannelHandlerContext ctx, FullBinaryMemcacheResponse msg) throws Exception {
if (saslClient.isComplete()) {
checkIsAuthed(msg);
return;
}
byte[] response = new byte[msg.content().readableBytes()];
msg.content().readBytes(response);
byte[] evaluatedBytes = saslClient.evaluateChallenge(response);
if (evaluatedBytes != null) {
String[] evaluated = new String(evaluatedBytes).split(" ");
ByteBuf content = Unpooled.copiedBuffer(username + "\0" + evaluated[1], CharsetUtil.UTF_8);
FullBinaryMemcacheRequest stepRequest = new DefaultFullBinaryMemcacheRequest(
selectedMechanism,
Unpooled.EMPTY_BUFFER,
content
);
stepRequest
.setOpcode(SASL_STEP_OPCODE)
.setKeyLength((short) selectedMechanism.length())
.setTotalBodyLength(content.readableBytes() + selectedMechanism.length());
ChannelFuture future = ctx.writeAndFlush(stepRequest);
future.addListener(new GenericFutureListener>() {
@Override
public void operationComplete(Future future) throws Exception {
if (!future.isSuccess()) {
LOGGER.warn("Error during SASL Auth negotiation phase.", future);
}
}
});
} else {
throw new AuthenticationException("SASL Challenge evaluation returned null.");
}
}
/**
* Once authentication is completed, check the response and react appropriately to the upper layers.
*
* @param msg the incoming message to investigate.
*/
private void checkIsAuthed(final FullBinaryMemcacheResponse msg) {
switch (msg.getStatus()) {
case SASL_AUTH_SUCCESS:
originalPromise.setSuccess();
ctx.pipeline().remove(this);
break;
case SASL_AUTH_FAILURE:
originalPromise.setFailure(new AuthenticationException("Authentication Failure"));
break;
default:
originalPromise.setFailure(new AuthenticationException("Unhandled SASL auth status: "
+ msg.getStatus()));
}
}
@Override
public void bind(ChannelHandlerContext ctx, SocketAddress localAddress, ChannelPromise promise) throws Exception {
ctx.bind(localAddress, promise);
}
@Override
public void connect(ChannelHandlerContext ctx, SocketAddress remoteAddress, SocketAddress localAddress,
ChannelPromise promise) throws Exception {
originalPromise = promise;
ChannelPromise downPromise = ctx.newPromise();
downPromise.addListener(new GenericFutureListener>() {
@Override
public void operationComplete(Future future) throws Exception {
if (!future.isSuccess() && !originalPromise.isDone()) {
originalPromise.setFailure(future.cause());
}
}
});
ctx.connect(remoteAddress, localAddress, downPromise);
}
@Override
public void disconnect(ChannelHandlerContext ctx, ChannelPromise promise) throws Exception {
ctx.disconnect(promise);
}
@Override
public void close(ChannelHandlerContext ctx, ChannelPromise promise) throws Exception {
ctx.close(promise);
}
@Override
public void deregister(ChannelHandlerContext ctx, ChannelPromise promise) throws Exception {
ctx.deregister(promise);
}
@Override
public void read(ChannelHandlerContext ctx) throws Exception {
ctx.read();
}
@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
ctx.write(msg, promise);
}
@Override
public void flush(ChannelHandlerContext ctx) throws Exception {
ctx.flush();
}
}