blob: b7083e69850e160a6ab15d1b6a88949750caff17 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more contributor license
* agreements. See the NOTICE file distributed with this work for additional information regarding
* copyright ownership. The ASF licenses this file to You under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License. You may obtain a
* copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package org.apache.geode.internal.cache.tier.sockets;
import static org.apache.geode.distributed.ConfigurationProperties.SECURITY_CLIENT_AUTHENTICATOR;
import static org.apache.geode.distributed.ConfigurationProperties.SECURITY_CLIENT_AUTH_INIT;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.lang.reflect.Method;
import java.util.Properties;
import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.Logger;
import org.apache.geode.DataSerializer;
import org.apache.geode.LogWriter;
import org.apache.geode.annotations.VisibleForTesting;
import org.apache.geode.annotations.internal.MutableForTesting;
import org.apache.geode.cache.CacheClosedException;
import org.apache.geode.cache.client.PoolFactory;
import org.apache.geode.cache.client.ServerRefusedConnectionException;
import org.apache.geode.cache.client.internal.ClientSideHandshakeImpl;
import org.apache.geode.distributed.DistributedMember;
import org.apache.geode.distributed.DistributedSystem;
import org.apache.geode.internal.ClassLoadUtils;
import org.apache.geode.internal.HeapDataOutputStream;
import org.apache.geode.internal.cache.tier.ConnectionProxy;
import org.apache.geode.internal.logging.InternalLogWriter;
import org.apache.geode.internal.security.CallbackInstantiator;
import org.apache.geode.internal.security.Credentials;
import org.apache.geode.internal.security.SecurityService;
import org.apache.geode.internal.serialization.KnownVersion;
import org.apache.geode.logging.internal.log4j.api.LogService;
import org.apache.geode.security.AuthInitialize;
import org.apache.geode.security.AuthenticationExpiredException;
import org.apache.geode.security.AuthenticationFailedException;
import org.apache.geode.security.AuthenticationRequiredException;
import org.apache.geode.security.Authenticator;
import org.apache.geode.security.GemFireSecurityException;
public abstract class Handshake {
private static final Logger logger = LogService.getLogger();
protected static final byte REPLY_OK = (byte) 59;
protected static final byte REPLY_REFUSED = (byte) 60;
protected static final byte REPLY_INVALID = (byte) 61;
protected static final byte REPLY_EXCEPTION_AUTHENTICATION_REQUIRED = (byte) 62;
protected static final byte REPLY_EXCEPTION_AUTHENTICATION_FAILED = (byte) 63;
protected static final byte REPLY_EXCEPTION_DUPLICATE_DURABLE_CLIENT = (byte) 64;
protected static final byte REPLY_WAN_CREDENTIALS = (byte) 65;
protected static final byte REPLY_AUTH_NOT_REQUIRED = (byte) 66;
public static final byte REPLY_SERVER_IS_LOCATOR = (byte) 67;
/**
* Test hook for client version support
*
* @since GemFire 5.7
*/
@MutableForTesting
protected static KnownVersion currentClientVersion = ConnectionProxy.VERSION;
protected SecurityService securityService;
/** This is used as part of our hash, so it must be a stable value */
protected abstract byte getReplyCode();
protected int clientReadTimeout = PoolFactory.DEFAULT_READ_TIMEOUT;
protected DistributedSystem system;
protected ClientProxyMembershipID id;
protected Properties credentials;
protected EncryptorImpl encryptor;
// Security mode flags
/** No credentials being sent */
public static final byte CREDENTIALS_NONE = (byte) 0;
/** Credentials being sent without encryption on the wire */
public static final byte CREDENTIALS_NORMAL = (byte) 1;
/** Credentials being sent with Diffie-Hellman key encryption */
public static final byte CREDENTIALS_DHENCRYPT = (byte) 2;
public static final byte SECURITY_MULTIUSER_NOTIFICATIONCHANNEL = (byte) 3;
public static final String PUBLIC_KEY_FILE_PROP = "security-client-kspath";
public static final String PUBLIC_KEY_PASSWD_PROP = "security-client-kspasswd";
public static final String PRIVATE_KEY_FILE_PROP = "security-server-kspath";
public static final String PRIVATE_KEY_ALIAS_PROP = "security-server-ksalias";
public static final String PRIVATE_KEY_PASSWD_PROP = "security-server-kspasswd";
/** @since GemFire 5.7 */
public static final byte CONFLATION_DEFAULT = 0;
/** @since GemFire 5.7 */
public static final byte CONFLATION_ON = 1;
/** @since GemFire 5.7 */
public static final byte CONFLATION_OFF = 2;
/** @since GemFire 5.7 */
protected byte clientConflation = CONFLATION_DEFAULT;
/**
* @since GemFire 6.0.3 List of per client property override bits.
*/
protected byte[] overrides;
/**
* Test hooks for per client conflation
*
* @since GemFire 5.7
*/
public static final byte clientConflationForTesting = 0;
public static final boolean setClientConflationForTesting = false;
/** Constructor used for subclasses */
protected Handshake() {}
/**
* Clone a HandShake to be used in creating other connections
*/
protected Handshake(Handshake handshake) {
clientConflation = handshake.clientConflation;
clientReadTimeout = handshake.clientReadTimeout;
credentials = handshake.credentials;
overrides = handshake.overrides;
system = handshake.system;
id = handshake.id;
securityService = handshake.securityService;
encryptor = new EncryptorImpl(handshake.encryptor);
}
protected void setClientConflation(byte value) {
clientConflation = value;
switch (clientConflation) {
case CONFLATION_DEFAULT:
case CONFLATION_OFF:
case CONFLATION_ON:
break;
default:
throw new IllegalArgumentException("Illegal clientConflation");
}
}
protected byte[] getOverrides() {
return overrides;
}
protected void setOverrides(byte[] values) {
byte override = values[0];
setClientConflation(((byte) (override & 0x03)));
}
// used by CacheClientNotifier's handshake reading code
public static byte[] extractOverrides(byte[] values) {
byte override = values[0];
byte[] overrides = new byte[1];
for (int item = 0; item < overrides.length; item++) {
overrides[item] = (byte) (override & 0x03);
override = (byte) (override >>> 2);
}
return overrides;
}
/**
* This method writes what readCredential() method expects to read. (Note the use of singular
* credential). It is similar to writeCredentials(), except that it doesn't write
* credential-properties.
*
* This is only used by the {@link ClientSideHandshakeImpl}.
*/
protected byte writeCredential(DataOutputStream dos, DataInputStream dis, String authInit,
boolean isNotification, DistributedMember member, HeapDataOutputStream heapdos)
throws IOException, GemFireSecurityException {
if (!encryptor.isEnabled()) {
heapdos.writeByte(CREDENTIALS_NORMAL);
encryptor.setAppSecureMode(CREDENTIALS_NORMAL);
heapdos.flush();
dos.write(heapdos.toByteArray());
dos.flush();
return -1;
}
byte acceptanceCode = -1;
acceptanceCode = encryptor.writeEncryptedCredential(dos, dis, heapdos);
if (acceptanceCode != REPLY_OK && acceptanceCode != REPLY_AUTH_NOT_REQUIRED) {
// Ignore the useless data
dis.readByte();
dis.readInt();
if (!isNotification) {
DataSerializer.readByteArray(dis);
}
readMessage(dis, dos, acceptanceCode, member);
}
dos.flush();
return acceptanceCode;
}
public void writeCredentials(DataOutputStream dos, DataInputStream dis, Properties p_credentials,
boolean isNotification, DistributedMember member)
throws IOException, GemFireSecurityException {
HeapDataOutputStream hdos = new HeapDataOutputStream(32, KnownVersion.CURRENT);
try {
writeCredentials(dos, dis, p_credentials, isNotification, member, hdos);
} finally {
hdos.close();
}
}
/**
* This assumes that authentication is the last piece of info in handshake
*/
public void writeCredentials(DataOutputStream dos, DataInputStream dis, Properties p_credentials,
boolean isNotification, DistributedMember member, HeapDataOutputStream heapdos)
throws IOException, GemFireSecurityException {
if (p_credentials == null) {
// No credentials indicator
heapdos.writeByte(CREDENTIALS_NONE);
heapdos.flush();
dos.write(heapdos.toByteArray());
dos.flush();
return;
}
if (!encryptor.isEnabled()) {
// Normal credentials without encryption indicator
heapdos.writeByte(CREDENTIALS_NORMAL);
DataSerializer.writeProperties(p_credentials, heapdos);
heapdos.flush();
dos.write(heapdos.toByteArray());
dos.flush();
return;
}
byte acceptanceCode = encryptor.writeEncryptedCredentials(dos, dis, p_credentials, heapdos);
if (acceptanceCode != REPLY_OK && acceptanceCode != REPLY_AUTH_NOT_REQUIRED) {
// Ignore the useless data
dis.readByte();
dis.readInt();
if (!isNotification) {
DataSerializer.readByteArray(dis);
}
readMessage(dis, dos, acceptanceCode, member);
}
dos.flush();
}
/**
* Throws AuthenticationRequiredException if authentication is required but there are no
* credentials.
*/
static void throwIfMissingRequiredCredentials(boolean requireAuthentication,
boolean hasCredentials) {
if (requireAuthentication && !hasCredentials) {
throw new AuthenticationRequiredException(
"No security credentials are provided");
}
}
// This assumes that authentication is the last piece of info in handshake
Properties readCredential(DataInputStream dis, DataOutputStream dos, DistributedSystem system)
throws GemFireSecurityException, IOException {
Properties credentials = null;
boolean requireAuthentication = securityService.isClientSecurityRequired();
try {
byte secureMode = dis.readByte();
throwIfMissingRequiredCredentials(requireAuthentication, secureMode != CREDENTIALS_NONE);
if (secureMode == CREDENTIALS_NORMAL) {
encryptor.setAppSecureMode(CREDENTIALS_NORMAL);
} else if (secureMode == CREDENTIALS_DHENCRYPT) {
encryptor.readEncryptedCredentials(dis, dos, system, requireAuthentication);
}
} catch (IOException ex) {
throw ex;
} catch (GemFireSecurityException ex) {
throw ex;
} catch (Exception ex) {
throw new AuthenticationFailedException(
"Failure in reading credentials", ex);
}
return credentials;
}
protected void readMessage(DataInputStream dis, DataOutputStream dos, byte acceptanceCode,
DistributedMember member) throws IOException, AuthenticationRequiredException,
AuthenticationFailedException, ServerRefusedConnectionException {
String message = dis.readUTF();
if (message.length() == 0 && acceptanceCode != REPLY_WAN_CREDENTIALS) {
return; // success
}
switch (acceptanceCode) {
case REPLY_EXCEPTION_AUTHENTICATION_REQUIRED:
throw new AuthenticationRequiredException(message);
case REPLY_EXCEPTION_AUTHENTICATION_FAILED:
throw new AuthenticationFailedException(message);
case REPLY_EXCEPTION_DUPLICATE_DURABLE_CLIENT:
throw new ServerRefusedConnectionException(member, message);
case REPLY_WAN_CREDENTIALS:
checkIfAuthenticWanSite(dis, dos, member);
break;
default:
throw new ServerRefusedConnectionException(member, message);
}
}
public boolean isOK() {
return getReplyCode() == REPLY_OK;
}
public void setClientReadTimeout(int clientReadTimeout) {
this.clientReadTimeout = clientReadTimeout;
}
public int getClientReadTimeout() {
return clientReadTimeout;
}
/**
* Indicates whether some other object is "equal to" this one.
*
* @param other the reference object with which to compare.
* @return true if this object is the same as the obj argument; false otherwise.
*/
@Override
public boolean equals(Object other) {
if (other == this) {
return true;
}
if (!(other instanceof Handshake)) {
return false;
}
final Handshake that = (Handshake) other;
return id.isSameDSMember(that.id) && getReplyCode() == that.getReplyCode();
}
@Override
public int hashCode() {
final int mult = 37;
int result = id.hashCode();
result = mult * result + getReplyCode();
return result;
}
@Override
public String toString() {
StringBuilder buf =
new StringBuilder().append("HandShake@").append(System.identityHashCode(this))
.append(" code: ").append(getReplyCode());
if (id != null) {
buf.append(" identity: ");
buf.append(id);
}
return buf.toString();
}
public ClientProxyMembershipID getMembershipId() {
return id;
}
public static Properties getCredentials(String authInitMethod, Properties securityProperties,
DistributedMember server, boolean isPeer, LogWriter logWriter,
LogWriter securityLogWriter) throws AuthenticationRequiredException {
Properties credentials = null;
// if no authInit, Try to extract the credentials directly from securityProps
if (StringUtils.isBlank(authInitMethod)) {
return Credentials.getCredentials(securityProperties);
}
// if authInit exists
try {
AuthInitialize auth =
CallbackInstantiator.getObjectOfType(authInitMethod, AuthInitialize.class);
auth.init(logWriter, securityLogWriter);
try {
credentials = auth.getCredentials(securityProperties, server, isPeer);
} finally {
auth.close();
}
} catch (GemFireSecurityException ex) {
throw ex;
} catch (Exception ex) {
throw new AuthenticationRequiredException(
String.format("Failed to acquire AuthInitialize method %s",
authInitMethod),
ex);
}
return credentials;
}
protected Properties getCredentials(DistributedMember member) {
String authInitMethod = system.getProperties().getProperty(SECURITY_CLIENT_AUTH_INIT);
return getCredentials(authInitMethod, system.getSecurityProperties(), member, false,
system.getLogWriter(),
system.getSecurityLogWriter());
}
/**
* this static method is used by CacheClientNotifier in registerClient when creating a queue for a
* client. This assumes that authentication is the last piece of info in handshake
*/
public static Properties readCredentials(DataInputStream dis, DataOutputStream dos,
DistributedSystem system, SecurityService securityService)
throws GemFireSecurityException, IOException {
boolean requireAuthentication = securityService.isClientSecurityRequired();
Properties credentials = null;
try {
byte secureMode = dis.readByte();
throwIfMissingRequiredCredentials(requireAuthentication, secureMode != CREDENTIALS_NONE);
if (secureMode == CREDENTIALS_NORMAL) {
if (requireAuthentication) {
credentials = DataSerializer.readProperties(dis);
} else {
DataSerializer.readProperties(dis); // ignore the credentials
}
} else if (secureMode == CREDENTIALS_DHENCRYPT) {
credentials = EncryptorImpl.getDecryptedCredentials(dis, dos, system, requireAuthentication,
credentials);
} else if (secureMode == SECURITY_MULTIUSER_NOTIFICATIONCHANNEL) {
// hitesh there will be no credential CCP will get credential(Principal) using
// ServerConnection..
logger.debug("readCredential where multiuser mode creating callback connection");
}
} catch (IOException ex) {
throw ex;
} catch (GemFireSecurityException ex) {
throw ex;
} catch (Exception ex) {
throw new AuthenticationFailedException(
"Failure in reading credentials", ex);
}
return credentials;
}
/**
* this could return either a Subject or a Principal depending on if it's integrated security or
* not
*/
public static Object verifyCredentials(String authenticatorMethod, Properties credentials,
Properties securityProperties, InternalLogWriter logWriter,
InternalLogWriter securityLogWriter, DistributedMember member,
SecurityService securityService)
throws AuthenticationRequiredException, AuthenticationFailedException, CacheClosedException {
if (!AcceptorImpl.isAuthenticationRequired()) {
return null;
}
return authenticate(authenticatorMethod, credentials, securityProperties, logWriter,
securityLogWriter,
member, securityService);
}
@VisibleForTesting
static Object authenticate(String authenticatorMethod, Properties credentials,
Properties securityProperties, InternalLogWriter logWriter,
InternalLogWriter securityLogWriter, DistributedMember member,
SecurityService securityService) {
Authenticator auth = null;
try {
if (securityService.isIntegratedSecurity()) {
return securityService.login(credentials);
} else {
Method instanceGetter = ClassLoadUtils.methodFromName(authenticatorMethod);
auth = (Authenticator) instanceGetter.invoke(null, (Object[]) null);
auth.init(securityProperties, logWriter, securityLogWriter);
return auth.authenticate(credentials, member);
}
} catch (AuthenticationFailedException | AuthenticationExpiredException
| CacheClosedException ex) {
throw ex;
} catch (Exception ex) {
throw new AuthenticationFailedException(ex.getMessage(), ex);
} finally {
if (auth != null) {
auth.close();
}
}
}
public Object verifyCredentials()
throws AuthenticationRequiredException, AuthenticationFailedException {
String methodName = system.getProperties().getProperty(SECURITY_CLIENT_AUTHENTICATOR);
return verifyCredentials(methodName, credentials, system.getSecurityProperties(),
(InternalLogWriter) system.getLogWriter(),
(InternalLogWriter) system.getSecurityLogWriter(), id.getDistributedMember(),
securityService);
}
private void checkIfAuthenticWanSite(DataInputStream dis, DataOutputStream dos,
DistributedMember member) throws GemFireSecurityException, IOException {
if (credentials == null) {
return;
}
String authenticator = system.getProperties().getProperty(SECURITY_CLIENT_AUTHENTICATOR);
Properties peerWanProps = readCredentials(dis, dos, system, securityService);
verifyCredentials(authenticator, peerWanProps, system.getSecurityProperties(),
(InternalLogWriter) system.getLogWriter(),
(InternalLogWriter) system.getSecurityLogWriter(), member, securityService);
}
}