/*
* Copyright 2010 NCHOVY
*
* Licensed 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.krakenapps.rpc.impl;
import java.net.InetSocketAddress;
import java.security.cert.X509Certificate;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import org.jboss.netty.channel.Channel;
import org.krakenapps.rpc.RpcAsyncCallback;
import org.krakenapps.rpc.RpcAsyncResult;
import org.krakenapps.rpc.RpcAsyncTable;
import org.krakenapps.rpc.RpcBlockingTable;
import org.krakenapps.rpc.RpcConnection;
import org.krakenapps.rpc.RpcConnectionEventListener;
import org.krakenapps.rpc.RpcConnectionState;
import org.krakenapps.rpc.RpcContext;
import org.krakenapps.rpc.RpcException;
import org.krakenapps.rpc.RpcExceptionEvent;
import org.krakenapps.rpc.RpcMessage;
import org.krakenapps.rpc.RpcPeeringCallback;
import org.krakenapps.rpc.RpcService;
import org.krakenapps.rpc.RpcServiceBinding;
import org.krakenapps.rpc.RpcSession;
import org.krakenapps.rpc.RpcSessionEvent;
import org.krakenapps.rpc.RpcSessionEventCallback;
import org.krakenapps.rpc.RpcSessionState;
import org.krakenapps.rpc.RpcTrustLevel;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class RpcConnectionImpl implements RpcConnection, RpcSessionEventCallback {
private final Logger logger = LoggerFactory.getLogger(RpcConnectionImpl.class.getName());
private RpcConnectionState state;
private Channel channel;
private RpcTrustLevel trustedLevel;
private Map<String, RpcServiceBinding> bindingMap;
private Map<Integer, RpcSession> sessionMap;
private Map<String, Object> props;
private AtomicInteger idCounter;
private AtomicInteger sessionCounter;
private RpcBlockingTableImpl blockingTable;
private RpcAsyncTableImpl asyncTable;
private String guid;
private Set<RpcConnectionEventListener> callbacks;
private final Lock controlLock = new ReentrantLock();
private Condition controlReady = controlLock.newCondition();
private volatile boolean isControlReady = false;
private final Lock peeringLock = new ReentrantLock();
private Condition peeringReady = peeringLock.newCondition();
// peer information
private String peerGuid;
private String nonce;
private RpcTrustLevel trustLevel;
private X509Certificate peerCert;
public RpcConnectionImpl(Channel channel, String guid) {
this.state = RpcConnectionState.Opened;
this.channel = channel;
this.bindingMap = new ConcurrentHashMap<String, RpcServiceBinding>();
this.sessionMap = new ConcurrentHashMap<Integer, RpcSession>();
this.props = new ConcurrentHashMap<String, Object>();
this.idCounter = new AtomicInteger();
this.asyncTable = new RpcAsyncTableImpl();
this.blockingTable = new RpcBlockingTableImpl();
this.guid = guid;
this.callbacks = Collections.newSetFromMap(new ConcurrentHashMap<RpcConnectionEventListener, Boolean>());
if (isClient())
sessionCounter = new AtomicInteger(1); // session id will be odd
// number
else
sessionCounter = new AtomicInteger(0); // session id will be even
// number
}
@Override
public int getId() {
return channel.getId();
}
@Override
public boolean isOpen() {
return state == RpcConnectionState.Opened;
}
@Override
public boolean isClient() {
return channel.getParent() == null;
}
@Override
public RpcConnectionState getState() {
return state;
}
@Override
public RpcTrustLevel getTrustedLevel() {
return trustedLevel;
}
@Override
public void setTrustedLevel(RpcTrustLevel trustedLevel) {
this.trustedLevel = trustedLevel;
// wake all
try {
peeringLock.lock();
peeringReady.signalAll();
} finally {
peeringLock.unlock();
}
}
@Override
public String getPeerGuid() {
return peerGuid;
}
@Override
public void setPeerGuid(String peerGuid) {
this.peerGuid = peerGuid;
}
@Override
public X509Certificate getPeerCertificate() {
return peerCert;
}
@Override
public String getNonce() {
return nonce;
}
@Override
public void setNonce(String nonce) {
this.nonce = nonce;
}
@Override
public RpcTrustLevel getTrustLevel() {
return trustLevel;
}
@Override
public void setTrustLevel(RpcTrustLevel trustLevel) {
this.trustLevel = trustLevel;
}
@Override
public InetSocketAddress getRemoteAddress() {
return (InetSocketAddress) channel.getRemoteAddress();
}
@Override
public Collection<RpcServiceBinding> getServiceBindings() {
return Collections.unmodifiableCollection(bindingMap.values());
}
@Override
public void bind(String name, RpcService service) {
logger.trace("kraken rpc: binding {}:{} to {}", new Object[] { channel.getId(), name, service.getClass().getName() });
bindingMap.put(name, new RpcServiceBindingImpl(name, service));
}
@Override
public void unbind(String name) {
bindingMap.remove(name);
}
@Override
public RpcServiceBinding findServiceBinding(String serviceName) {
return bindingMap.get(serviceName);
}
@Override
public RpcSession createSession(String serviceName) throws RpcException, InterruptedException {
return createSession(serviceName, new Properties());
}
@Override
public RpcSession createSession(String serviceName, Properties props) throws RpcException, InterruptedException {
RpcSession session = findSession(0);
Integer sessionId = (Integer) session.call("session-request", new Object[] { serviceName, toMap(props) }, 5000);
if (sessionId != null) {
RpcSessionImpl newSession = new RpcSessionImpl(sessionId, serviceName, session.getConnection());
sessionMap.put(sessionId, newSession);
return newSession;
}
return null;
}
private Map<String, Object> toMap(Properties props) {
Map<String, Object> m = new HashMap<String, Object>();
for (Object key : props.keySet()) {
m.put(key.toString(), props.get(key));
}
return m;
}
public RpcSession openSession(int newSessionId, String serviceName) {
RpcServiceBinding binding = bindingMap.get(serviceName);
if (binding == null)
return null;
RpcService service = binding.getService();
RpcSession session = new RpcSessionImpl(newSessionId, serviceName, this);
RpcSessionEvent event = new RpcSessionEventImpl(RpcSessionEvent.Opened, session);
try {
// create pseudo rpc message and invoke session request callback
RpcMessage pseudoMessage = new RpcMessage(
new Object[] { new HashMap<String, Object>(), new HashMap<String, Object>() });
pseudoMessage.setSession(session);
RpcContext.setMessage(pseudoMessage);
service.sessionRequested(event);
if (session.getState() == RpcSessionState.Opened) {
if (!session.getServiceName().equals("rpc-control"))
logger.trace("kraken rpc: session created [{}]", session);
sessionMap.put(newSessionId, session);
// invoke session opened callback and try peering
service.sessionOpened(event);
return session;
}
} catch (Exception e) {
logger.error("kraken rpc: cannot create session", e);
}
return null;
}
@Override
public RpcSession findSession(int sessionId) {
return sessionMap.get(sessionId);
}
@Override
public Collection<RpcSession> getSessions() {
return Collections.unmodifiableCollection(sessionMap.values());
}
@Override
public RpcSession getSession(int id) {
return sessionMap.get(id);
}
@Override
public void send(RpcMessage msg) {
if (!msg.containsHeader("id"))
throw new IllegalStateException("id header required");
channel.write(msg);
}
@Override
public void close() {
if (state == RpcConnectionState.Closed)
return;
state = RpcConnectionState.Closed;
// close all sessions
for (RpcSession s : getSessions()) {
s.close();
}
// unbind all services
for (RpcServiceBinding binding : bindingMap.values()) {
RpcService s = binding.getService();
try {
s.connectionClosed(this);
} catch (Exception e) {
RpcExceptionEvent event = new RpcExceptionEventImpl(e);
s.exceptionCaught(event);
}
}
// notify handler
for (RpcConnectionEventListener callback : callbacks) {
try {
callback.connectionClosed(this);
} catch (Exception e) {
logger.warn("kraken rpc: event listener should not throw exception", e);
}
}
// close socket
channel.close().awaitUninterruptibly();
logger.trace("kraken rpc: connection closed id={}, peer={}", channel.getId(), getRemoteAddress());
}
@Override
public Object getProperty(String key) {
return props.get(key);
}
@Override
public void setProperty(String key, Object value) {
props.put(key, value);
}
@Override
public Collection<String> getPropertyKeys() {
return Collections.unmodifiableCollection(props.keySet());
}
@Override
public void sessionClosed(RpcSession session) {
RpcServiceBinding binding = bindingMap.get(session.getServiceName());
RpcService service = binding.getService();
try {
RpcSessionEvent event = new RpcSessionEventImpl(RpcSessionEvent.Closed, session);
service.sessionClosed(event);
} catch (Exception e) {
RpcExceptionEvent event = new RpcExceptionEventImpl(e);
service.exceptionCaught(event);
}
}
@Override
public void requestPeering(RpcPeeringCallback callback) {
requestPeering(callback, null);
}
@Override
public void requestPeering(RpcPeeringCallback callback, String password) {
if (password != null)
setProperty("password", password);
RpcSession session = findSession(0);
// call peer request
session.call("peering-request", new Object[] { guid }, new NonPasswordPeering(session, callback, password));
}
@Override
public void addListener(RpcConnectionEventListener callback) {
callbacks.add(callback);
}
@Override
public void removeListener(RpcConnectionEventListener callback) {
callbacks.remove(callback);
}
@Override
public int nextMessageId() {
return idCounter.incrementAndGet();
}
@Override
public int nextSessionId() {
return sessionCounter.addAndGet(2);
}
@Override
public RpcBlockingTable getBlockingTable() {
return blockingTable;
}
@Override
public RpcAsyncTable getAsyncTable() {
return asyncTable;
}
@Override
public void waitControlReady() {
try {
if (!isControlReady)
logger.trace("kraken rpc: waiting control ready for [{}]", getId());
controlLock.lock();
while (!isControlReady) {
controlReady.await();
}
} catch (InterruptedException e) {
logger.trace("kraken rpc: interrupted waiting for peer [{}] control ready", peerGuid);
} finally {
controlLock.unlock();
}
}
@Override
public void waitPeering() {
waitPeering(0);
}
@Override
public void waitPeering(long timeout) {
try {
long begin = new Date().getTime();
peeringLock.lock();
while (trustedLevel == null) {
if (timeout > 0 && new Date().getTime() - begin > timeout) {
logger.error("kraken rpc: give up waiting peering [{}] response", peerGuid);
throw new RpcException("peering timeout: " + timeout);
}
peeringReady.await(200, TimeUnit.MILLISECONDS);
}
} catch (InterruptedException e) {
logger.trace("kraken rpc: interrupted waiting for peering [{}]", peerGuid);
} finally {
peeringLock.unlock();
}
}
@Override
public void setControlReady() {
try {
controlLock.lock();
isControlReady = true;
controlReady.signalAll();
} finally {
controlLock.unlock();
}
}
public void setPeerCert(X509Certificate peerCert) {
this.peerCert = peerCert;
}
@Override
public String toString() {
return String.format("id=%d, peer=(%s, %s), trusted level=%s, ssl=%s", channel.getId(), getPeerGuid(),
getRemoteAddress(), trustedLevel, getPeerCertificate() != null);
}
private class NonPasswordPeering implements RpcAsyncCallback {
private RpcSession session;
private RpcPeeringCallback callback;
private String password;
public NonPasswordPeering(RpcSession session, RpcPeeringCallback callback, String password) {
this.session = session;
this.callback = callback;
this.password = password;
}
@Override
public void onComplete(RpcAsyncResult r) {
if (r.isError()) {
logger.error("kraken rpc: peering error", r.getException());
session.getConnection().close();
return;
}
Object[] tuple = (Object[]) r.getReturn();
String type = (String) tuple[0];
peerGuid = (String) tuple[1];
logger.debug("kraken rpc: non password peering result [type={}, peer={}]", type, peerGuid);
if (type.equals("success"))
onSuccess(tuple);
else if (type.equals("challenge"))
onFaliure(tuple);
}
private void onFaliure(Object[] tuple) {
// set peer information
String nonce = (String) tuple[2];
// hash = sha1(password + nonce)
String hash = PasswordUtil.calculatePasswordHash(password, nonce);
// try authenticate
session.call("authenticate", new Object[] { guid, hash }, new PasswordPeering(session, callback));
}
private void onSuccess(Object[] tuple) {
// no challenge
int trustedLevel = (Integer) tuple[2];
setTrustedLevel(RpcTrustLevel.parse(trustedLevel));
logger.trace("kraken rpc: {} login success, trusted level = {}", new Object[] { peerGuid, trustedLevel });
RpcConnection conn = session.getConnection();
callback.onCompleted(conn);
}
}
private class PasswordPeering implements RpcAsyncCallback {
private RpcSession session;
private RpcPeeringCallback callback;
public PasswordPeering(RpcSession session, RpcPeeringCallback callback) {
this.session = session;
this.callback = callback;
}
@Override
public void onComplete(RpcAsyncResult r) {
Object[] ret = (Object[]) r.getReturn();
boolean peered = (Boolean) ret[0];
int trustedLevel = (Integer) ret[1];
// challenge result
logger.trace("kraken rpc: {} login result = {}, trusted level = {}", new Object[] { peerGuid, peered, trustedLevel });
if (peered)
setTrustedLevel(RpcTrustLevel.parse(trustedLevel));
callback.onCompleted(session.getConnection());
}
}
}