/**
* Copyright 2013-2015 Seagate Technology LLC.
*
* This Source Code Form is subject to the terms of the Mozilla
* Public License, v. 2.0. If a copy of the MPL was not
* distributed with this file, You can obtain one at
* https://mozilla.org/MP:/2.0/.
*
* This program is distributed in the hope that it will be useful,
* but is provided AS-IS, WITHOUT ANY WARRANTY; including without
* the implied warranty of MERCHANTABILITY, NON-INFRINGEMENT or
* FITNESS FOR A PARTICULAR PURPOSE. See the Mozilla Public
* License for more details.
*
* See www.openkinetic.org for more project information
*/
package com.seagate.kinetic.client.internal;
import java.io.IOException;
import java.security.Key;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.crypto.spec.SecretKeySpec;
import kinetic.client.CallbackHandler;
import kinetic.client.ClientConfiguration;
import kinetic.client.KineticClient;
import kinetic.client.KineticException;
import com.google.protobuf.ByteString;
import com.seagate.kinetic.client.io.IoHandler;
import com.seagate.kinetic.common.lib.Hmac;
import com.seagate.kinetic.common.lib.Hmac.HmacException;
import com.seagate.kinetic.common.lib.KineticMessage;
import com.seagate.kinetic.proto.Kinetic.Command;
import com.seagate.kinetic.proto.Kinetic.Command.Header;
import com.seagate.kinetic.proto.Kinetic.Command.MessageType;
import com.seagate.kinetic.proto.Kinetic.Command.Range;
import com.seagate.kinetic.proto.Kinetic.Message;
import com.seagate.kinetic.proto.Kinetic.Message.AuthType;
import com.seagate.kinetic.proto.Kinetic.Message.Builder;
/**
* Perform request response operation synchronously or asynchronously on behalf
* of a Kinetic client.
*
* @see KineticClient
* @see DefaultKineticClient
*
* @author James Hughes
* @author Chiaming Yang
*/
public class ClientProxy {
private final static Logger logger = Logger.getLogger(ClientProxy.class
.getName());
// client configuration
private ClientConfiguration config = null;
// client io handler
private IoHandler iohandler = null;
// user id
private long user = 1;
// connection id
private long connectionID = 1234;
// sequence
private long sequence = 1;
// cluster version
private long clusterVersion = 43;
// key associated with this client instance
private Key myKey = null;
// hmac key map
private final Map<Long, Key> hmacKeyMap = new HashMap<Long, Key>();
private volatile boolean isConnectionIdSetByServer = false;
private CountDownLatch cidLatch = new CountDownLatch (1);
private boolean isClosed = false;
/**
* Construct a new instance of client proxy
*
* @param config
* client configuration for the current instance
* @throws KineticException
* if any internal error occurred.
*/
public ClientProxy(ClientConfiguration config)
throws KineticException {
// client config
this.config = config;
// get user principal from client config
user = config.getUserId();
// build aclmap
this.buildHmacKeyMap();
// get cluster version from client config
this.clusterVersion = config.getClusterVersion();
// connection id
this.connectionID = config.getConnectionId();
// io handler
this.iohandler = new IoHandler(this);
if (this.iohandler.shouldWaitForStatusMessage()) {
// wait for status message
this.waitForStatusMessage();
}
}
private void waitForStatusMessage() throws KineticException {
try {
this.cidLatch.await(30, TimeUnit.SECONDS);
} catch (InterruptedException e) {
logger.log(Level.WARNING, e.getMessage(), e);
}
if (this.isConnectionIdSetByServer == false) {
throw new KineticException("Hand shake failed with the service.");
}
}
/**
* Set client connection ID by server. his is set by from the server. The
* client library uses this connectionID after this is set by the server.
*
* @param cid the connection id to be set for this connection.
*/
public void setConnectionId(KineticMessage kresponse) {
if (this.isConnectionIdSetByServer) {
/**
* if already set by server, simply return.
*/
return;
}
if (kresponse.getMessage().getAuthType() != AuthType.UNSOLICITEDSTATUS) {
return;
}
/**
* check if set connection ID is needed.
*/
if (kresponse.getCommand().getHeader().hasConnectionID()) {
this.connectionID = kresponse.getCommand().getHeader()
.getConnectionID();
if (this.config.getExpectedWwn() != null) {
String recvd = kresponse.getCommand().getBody().getGetLog()
.getConfiguration().getWorldWideName().toStringUtf8();
if (config.getExpectedWwn().equals(recvd) == false) {
this.close();
logger.log(Level.SEVERE, "wwn does not match., expected="
+ config.getExpectedWwn() + ", but received: "
+ recvd);
} else {
// set flag to true
this.isConnectionIdSetByServer = true;
}
} else {
// set flag to true
this.isConnectionIdSetByServer = true;
}
// count down
this.cidLatch.countDown();
}
}
/**
* Get client configuration instance for this client instance.
*
* @return client configuration instance for this client instance.
*/
public ClientConfiguration getConfiguration() {
return this.config;
}
/**
* build client acl instance.
*
* @return acl instance.
*/
private void buildHmacKeyMap() {
Key key = new SecretKeySpec(ByteString.copyFromUtf8(config.getKey())
.toByteArray(), "HmacSHA1");
hmacKeyMap.put(config.getUserId(), key);
this.myKey = key;
}
/*
* KeyRange is a class the defines a range of keys.
*/
class KeyRange {
// start key and end key
private ByteString startKey, endKey;
// start key, end key, reverse boolean flags
private boolean startKeyInclusive, endKeyInclusive, reverse;
// max returned keys
private int maxReturn;
/**
* Constructor for a new instance of key range.
*
* @param startKey
* the start key of key range
* @param startKeyInclusive
* is start key inclusive
* @param endKey
* end key of key range
* @param endKeyInclusive
* is end key inclusive
* @param maxReturned
* max allowed return keys.
* @param reverse
* true if op is performed in reverse order
*/
public KeyRange(ByteString startKey, boolean startKeyInclusive,
ByteString endKey, boolean endKeyInclusive, int maxReturned,
boolean reverse) {
setStartKey(startKey);
setStartKeyInclusive(startKeyInclusive);
setEndKey(endKey);
setEndKeyInclusive(endKeyInclusive);
setMaxReturned(maxReturned);
setReverse(reverse);
}
public ByteString getStartKey() {
return startKey;
}
public void setStartKey(ByteString k1) {
this.startKey = k1;
}
public ByteString getEndKey() {
return endKey;
}
public void setEndKey(ByteString k2) {
this.endKey = k2;
}
public boolean isStartKeyInclusive() {
return startKeyInclusive;
}
public void setStartKeyInclusive(boolean i1) {
this.startKeyInclusive = i1;
}
public boolean isEndKeyInclusive() {
return endKeyInclusive;
}
public void setEndKeyInclusive(boolean i2) {
this.endKeyInclusive = i2;
}
public int getMaxReturned() {
return maxReturn;
}
public void setMaxReturned(int n) {
this.maxReturn = n;
}
public boolean isReverse() {
return reverse;
}
public void setReverse(boolean reverse) {
this.reverse = reverse;
}
}
/**
* Returns a list of keys based on the key range specification.
*
* @param range
* specify the range of keys to be returned. This does not return
* the values.
*
* @return an array of keys in db that matched the specified range.
*
* @throws KineticException
* if any internal error occurred.
*
* @see KineticClient#getKeyRange(byte[], boolean, byte[], boolean, int)
* @see KeyRange
*/
List<ByteString> getKeyRange(KeyRange range) throws KineticException {
// perform key range op
KineticMessage resp = doRange(range);
// return list of matched keys.
return resp.getCommand().getBody().getRange()
.getKeysList();
}
/**
* Perform range operation based on the specified key range specification.
*
* @param keyRange
* key range specification to be performed.
* @return the response message from the range operation.
*
* @throws LCException
* if any internal error occurred.
*/
KineticMessage doRange(KeyRange keyRange) throws KineticException {
//request message
KineticMessage request = null;
// response message
KineticMessage respond = null;
try {
// request message
request = MessageFactory
.createKineticMessageWithBuilder();
Command.Builder commandBuilder = (Command.Builder) request.getCommand();
// set message type
commandBuilder.getHeaderBuilder()
.setMessageType(MessageType.GETKEYRANGE);
// get range builder
Range.Builder op = commandBuilder.getBodyBuilder()
.getRangeBuilder();
// set parameters for the op
op.setStartKey(keyRange.getStartKey());
op.setEndKey(keyRange.getEndKey());
op.setStartKeyInclusive(keyRange.isStartKeyInclusive());
op.setEndKeyInclusive(keyRange.isEndKeyInclusive());
op.setMaxReturned(keyRange.getMaxReturned());
op.setReverse(keyRange.isReverse());
// send request
respond = request(request);
MessageFactory.checkReply(request, respond);
// return response
return respond;
} catch (KineticException ke) {
//re-throw ke
throw ke;
} catch (Exception e) {
//make a new kinetic exception
KineticException ke = new KineticException (e);
ke.setRequestMessage(request);
ke.setResponseMessage(respond);
//throw ke
throw ke;
}
}
public class LCException extends Exception {
private static final long serialVersionUID = -6118533510243882800L;
LCException(String s) {
super(s);
}
}
/**
* Utility to throw internal LCException.
*
* @param exceptionMessage
* the message for the exception.
*
* @throws LCException
* the exception type to be thrown.
*/
private void throwLcException(String exceptionMessage) throws LCException {
throw new LCException(exceptionMessage);
}
/**
* Send a kinetic request message to drive/simulator.
*
* @param krequest the request message
* @return response the response message
* @throws KineticException if the command operation failed.
*
* @see kinetic.client.VersionMismatchException
* @see kinetic.client.ClusterVersionFailureException
*/
KineticMessage request(KineticMessage krequest) throws KineticException {
KineticMessage kresponse = null;
try {
kresponse = this.doRequest(krequest);
//check status code
MessageFactory.checkReply(krequest, kresponse);
} catch (KineticException ke) {
ke.setRequestMessage(krequest);
ke.setResponseMessage(kresponse);
throw ke;
} catch (Exception e) {
throwKineticException (e, krequest, kresponse);
}
return kresponse;
}
private void throwKineticException(Exception e, KineticMessage request,
KineticMessage response) throws KineticException {
//new instance
KineticException ke = new KineticException (e);
//set request message
ke.setRequestMessage(request);
//set response message
ke.setResponseMessage(response);
throw ke;
}
/**
* Send the specified request message synchronously to the Kinetic service.
*
* @param message
* the request message from the client.
*
* @return the response message from the service.
*
* @throws LCException
* if any errors occur.
*
* @see #requestAsync(com.seagate.kinetic.proto.Kinetic.Message.Builder,
* CallbackHandler)
*/
KineticMessage doRequest(KineticMessage kmreq) throws LCException {
// response kinetic message
KineticMessage kmresp = null;
try {
// require to obtain lock to prevent possible dead-lock
// such as if connection close is triggered from remote.
synchronized (this) {
kmresp = this.iohandler.getMessageHandler().write(kmreq);
}
// check if we do received a response
if (kmresp == null) {
throwLcException("Timeout - unable to receive response message within " + config.getRequestTimeoutMillis() + " ms");
}
// check hmac if this is a hmac auth type
if (kmreq.getMessage().getAuthType() == AuthType.HMACAUTH) {
if (!Hmac.check(kmresp, myKey)) {
throwLcException("Hmac failed compare");
}
}
} catch (LCException lce) {
// re-throw
throw lce;
} catch (HmacException e) {
throwLcException("Hmac failed compute");
} catch (java.net.SocketTimeoutException e) {
throwLcException("Socket Timeout");
} catch (IOException e1) {
throwLcException("IO error");
} catch (InterruptedException ite) {
throwLcException(ite.getMessage());
}
return kmresp;
}
/**
*
* Send the specified request message asynchronously to the Kinetic service.
*
* @param message
* the request message to be sent asynchronously to the Kinetic
* service.
*
* @param handler
* the callback handler for the asynchronous request.
*
* @throws KineticException
* if any internal error occur.
*
* @see CallbackHandler
* @see #request(com.seagate.kinetic.proto.Kinetic.Message.Builder)
*/
<T> void requestAsync(KineticMessage kineticMessage, CallbackHandler<T> handler)
throws KineticException {
try {
// create context message for the async operation
CallbackContext<T> context = new CallbackContext<T>(handler);
// set request message to the context so we can get it when response
// is received
context.setRequestMessage(kineticMessage);
// send the async request message
this.iohandler.getMessageHandler().writeAsync(kineticMessage, context);
} catch (Exception e) {
logger.log(Level.WARNING, e.getMessage(), e);
throw new KineticException(e.getMessage());
}
}
void requestNoAck(KineticMessage kmreq) throws KineticException {
try {
// finalizeHeader(kmreq);
this.iohandler.getMessageHandler().writeNoAck(kmreq);
} catch (Exception e) {
KineticException ke = new KineticException(e.getMessage());
ke.setRequestMessage(kmreq);
throw ke;
}
}
/**
* Check hmac based on the specified message.
*
* @param message
* the protocol buffer message from which hmac value is
* validated.
*
* @return true if hmac is validate. Otherwise, return false.
*/
public boolean checkHmac(KineticMessage message) {
boolean flag = false;
try {
//Hmac.check(message, this.myKey);
byte[] bytes = message.getMessage().getCommandBytes().toByteArray();
Hmac.check(bytes, this.myKey, message.getMessage().getHmacAuth().getHmac());
flag = true;
} catch (Exception e) {
logger.log(Level.WARNING, e.getMessage(), e);
}
return flag;
}
/**
* Filled in required header fields for the request message.
*
* @param message
* the request protocol buffer message.
*/
public void finalizeHeader(KineticMessage kineticMessage) {
Message.Builder messageBuilder = (Builder) kineticMessage.getMessage();
Command.Builder commandBuilder = (Command.Builder) kineticMessage.getCommand();
// get header builder
Header.Builder header = commandBuilder.getHeaderBuilder();
// set cluster version
header.setClusterVersion(clusterVersion);
// set connection id.
header.setConnectionID(connectionID);
// set sequence number.
header.setSequence(getNextSequence());
/**
* calculate and set tag value for the message
*/
if (header.getMessageType() == MessageType.PUT) {
if (commandBuilder.getBodyBuilder().getKeyValueBuilder().hasTag() == false) {
// set tag to empty for backward compatibility with drive.
// this can be removed when drive does not require the tag
// to be set.
commandBuilder.getBodyBuilder().getKeyValueBuilder()
.setTag(ByteString.EMPTY);
// commandBuilder.getBodyBuilder().getKeyValueBuilder()
// .setAlgorithm(Algorithm.INVALID_ALGORITHM);
}
}
/**
* calculate and set hmac value for this message
*/
// get command byte string
ByteString commandByteString = commandBuilder.build().toByteString();
// get command bytes for hmac calculation
byte[] commandBytes = commandByteString.toByteArray();
// calculate HMAC
try {
if (messageBuilder.getAuthType() == AuthType.HMACAUTH) {
// calculate hmac
ByteString hmac = Hmac.calc(commandBytes, myKey);
// set identity
messageBuilder.getHmacAuthBuilder().setIdentity(user);
// set hmac
messageBuilder.getHmacAuthBuilder().setHmac(hmac);
}
// set command bytes to message
messageBuilder.setCommandBytes(ByteString.copyFrom(commandBytes));
} catch (HmacException e) {
logger.log(Level.WARNING, e.getMessage(), e);
}
}
/**
* Get next sequence number for this connection (client instance).
*
* @return next unique number for this client instance.
*/
private synchronized long getNextSequence() {
return sequence++;
}
/**
* close io handler and release associated resources.
*/
public synchronized void close() {
if (this.isClosed) {
return;
}
try {
if (this.iohandler != null) {
iohandler.close();
}
} finally {
this.isClosed = true;
}
}
}