Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
it.anyplace.sync.bep.BlockExchangeConnectionHandler Maven / Gradle / Ivy
/*
* Copyright (C) 2016 Davide Imbriaco
*
* This Java file 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 http://mozilla.org/MPL/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 it.anyplace.sync.bep;
import it.anyplace.sync.bep.protos.BlockExchageProtos;
import com.google.common.base.Function;
import static com.google.common.base.MoreObjects.firstNonNull;
import it.anyplace.sync.core.security.KeystoreHandler;
import static com.google.common.base.Objects.equal;
import it.anyplace.sync.core.configuration.ConfigurationService;
import java.io.DataInputStream;
import java.io.IOException;
import java.net.Socket;
import java.nio.ByteBuffer;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.collect.BiMap;
import com.google.common.collect.ImmutableBiMap;
import com.google.common.eventbus.EventBus;
import com.google.protobuf.ByteString;
import com.google.protobuf.GeneratedMessage;
import it.anyplace.sync.bep.protos.BlockExchageProtos.ClusterConfig;
import it.anyplace.sync.bep.protos.BlockExchageProtos.Device;
import it.anyplace.sync.bep.protos.BlockExchageProtos.Folder;
import it.anyplace.sync.bep.protos.BlockExchageProtos.Index;
import it.anyplace.sync.bep.protos.BlockExchageProtos.IndexUpdate;
import java.io.DataOutputStream;
import java.lang.reflect.InvocationTargetException;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import net.jpountz.lz4.LZ4Factory;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.tuple.Pair;
import static it.anyplace.sync.core.security.KeystoreHandler.deviceIdStringToHashData;
import it.anyplace.sync.bep.protos.BlockExchageProtos.Response;
import it.anyplace.sync.core.beans.DeviceAddress;
import it.anyplace.sync.client.protocol.rp.RelayClient;
import java.io.Closeable;
import com.google.common.base.Predicate;
import com.google.common.collect.Iterables;
import it.anyplace.sync.bep.protos.BlockExchageProtos.Request;
import static it.anyplace.sync.core.security.KeystoreHandler.hashDataToDeviceIdString;
import java.util.Collections;
import java.util.concurrent.Callable;
import static it.anyplace.sync.core.security.KeystoreHandler.BEP;
import it.anyplace.sync.bep.protos.BlockExchageProtos.Ping;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ScheduledExecutorService;
import javax.net.ssl.SSLSocket;
import com.google.common.collect.Maps;
import com.google.common.eventbus.Subscribe;
import java.util.Map;
import com.google.common.collect.Sets;
import java.util.Set;
import it.anyplace.sync.core.beans.IndexInfo;
import it.anyplace.sync.core.beans.FolderInfo;
import it.anyplace.sync.httprelay.client.HttpRelayClient;
import it.anyplace.sync.bep.beans.ClusterConfigFolderInfo;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import it.anyplace.sync.core.beans.DeviceInfo;
import it.anyplace.sync.core.events.DeviceAddressActiveEvent;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
/*
* To change this template, choose Tools | Templates
* and open the template in the editor.
*/
/**
*
* @author aleph
*/
public class BlockExchangeConnectionHandler implements Closeable {
private final static int MAGIC = 0x2EA7D90B;
private final Logger logger = LoggerFactory.getLogger(getClass());
private final ConfigurationService configuration;
private final ExecutorService outExecutorService = Executors.newSingleThreadExecutor(),
inExecutorService = Executors.newSingleThreadExecutor(),
messageProcessingService = Executors.newCachedThreadPool();
private final ScheduledExecutorService periodicExecutorService = Executors.newSingleThreadScheduledExecutor();
private final EventBus eventBus = new EventBus();
private Socket socket;
private DataInputStream in;
private DataOutputStream out;
private ConnectionInfo connectionInfo;
private final DeviceAddress address;
private long lastActive = Long.MIN_VALUE;
private ClusterConfigInfo clusterConfigInfo;
private IndexHandler indexHandler;
private boolean isClosed = false, isConnected = false;
public BlockExchangeConnectionHandler(ConfigurationService configuration, DeviceAddress deviceAddress) {
checkNotNull(configuration);
this.configuration = configuration;
this.address = deviceAddress;
}
public DeviceAddress getAddress() {
return address;
}
public ClusterConfigInfo getClusterConfigInfo() {
return clusterConfigInfo;
}
public IndexHandler getIndexHandler() {
return indexHandler;
}
public void setIndexHandler(IndexHandler indexHandler) {
checkNotClosed();
this.indexHandler = indexHandler;
}
public void checkNotClosed() {
checkArgument(!isClosed(), "connection %s closed", this);
}
public boolean isConnected() {
return isConnected;
}
public BlockExchangeConnectionHandler connect() throws Exception {
checkNotClosed();
checkArgument(socket == null && !isConnected, "already connected!");
logger.info("connecting to {}", address.getAddress());
KeystoreHandler keystoreHandler = KeystoreHandler.newLoader().loadAndStore(configuration);
try {
switch (address.getType()) {
case TCP:
logger.debug("opening tcp ssl connection");
socket = keystoreHandler.createSocket(address.getSocketAddress(), BEP);
break;
case RELAY: {
logger.debug("opening relay connection");
socket = keystoreHandler.wrapSocket(new RelayClient(configuration).openRelayConnection(address), BEP);
break;
}
case HTTP_RELAY:
case HTTPS_RELAY: {
logger.debug("opening http relay connection");
socket = keystoreHandler.wrapSocket(new HttpRelayClient(configuration).openRelayConnection(address), BEP);
break;
}
default:
throw new UnsupportedOperationException("unsupported address type = " + address.getType());
}
in = new DataInputStream(socket.getInputStream());
out = new DataOutputStream(socket.getOutputStream());
sendHelloMessage(BlockExchageProtos.Hello.newBuilder()
.setClientName(configuration.getClientName())
.setClientVersion(configuration.getClientVersion())
.setDeviceName(configuration.getDeviceName())
.build().toByteArray());
markActivityOnSocket();
BlockExchageProtos.Hello hello = receiveHelloMessage();
logger.trace("received hello message = {}", hello);
connectionInfo = new ConnectionInfo();
connectionInfo.setClientName(hello.getClientName());
connectionInfo.setClientVersion(hello.getClientVersion());
connectionInfo.setDeviceName(hello.getDeviceName());
logger.info("connected to device = {}", connectionInfo);
keystoreHandler.checkSocketCerificate((SSLSocket) socket, address.getDeviceId());
{
ClusterConfig.Builder clusterConfigBuilder = ClusterConfig.newBuilder();
for (String folder : configuration.getFolderNames()) {
Folder.Builder folderBuilder = clusterConfigBuilder.addFoldersBuilder().setId(folder);
{
//our device
Device.Builder deviceBuilder = folderBuilder.addDevicesBuilder()
.setId(ByteString.copyFrom(deviceIdStringToHashData(configuration.getDeviceId())));
if (indexHandler != null) {
deviceBuilder.setIndexId(indexHandler.getSequencer().indexId())
.setMaxSequence(indexHandler.getSequencer().currentSequence());
}
}
{
//other device
Device.Builder deviceBuilder = folderBuilder.addDevicesBuilder()
.setId(ByteString.copyFrom(deviceIdStringToHashData(address.getDeviceId())));
if (indexHandler != null) {
IndexInfo indexSequenceInfo = indexHandler.getIndexRepository().findIndexInfoByDeviceAndFolder(address.getDeviceId(), folder);
if (indexSequenceInfo != null) {
deviceBuilder
.setIndexId(indexSequenceInfo.getIndexId())
.setMaxSequence(indexSequenceInfo.getLocalSequence());
logger.info("send delta index info device = {} index = {} max (local) sequence = {}",
indexSequenceInfo.getDeviceId(),
indexSequenceInfo.getIndexId(),
indexSequenceInfo.getLocalSequence());
}
}
}
//TODO other devices??
}
sendMessage(clusterConfigBuilder.build());
}
final Object clusterConfigWaitingLock = new Object();
synchronized (clusterConfigWaitingLock) {
Object listener = new Object() {
@Subscribe
public void handleClusterConfigMessageProcessedEvent(ClusterConfigMessageProcessedEvent event) {
synchronized (clusterConfigWaitingLock) {
clusterConfigWaitingLock.notifyAll();
}
}
@Subscribe
public void handleConnectionClosedEvent(ConnectionClosedEvent event) {
synchronized (clusterConfigWaitingLock) {
clusterConfigWaitingLock.notifyAll();
}
}
};
eventBus.register(listener);
startMessageListenerService();
while (clusterConfigInfo == null && !isClosed()) {
logger.debug("wait for cluster config");
clusterConfigWaitingLock.wait();
}
checkNotNull(clusterConfigInfo, "unable to retrieve cluster config from peer!");
eventBus.unregister(listener);
}
for (String folder : configuration.getFolderNames()) {
if (hasFolder(folder)) {
sendIndexMessage(folder);
}
}
periodicExecutorService.scheduleWithFixedDelay(new Runnable() {
@Override
public void run() {
sendPing();
}
}, 90, 90, TimeUnit.SECONDS);
} catch (Exception ex) {
close();
throw ex;
}
isConnected = true;
return this;
}
private void sendIndexMessage(String folder) {
sendMessage(Index.newBuilder()
.setFolder(folder)
.build());
}
private void closeBg() {
new Thread(new Runnable() {
@Override
public void run() {
close();
}
}).start();
}
private BlockExchageProtos.Hello receiveHelloMessage() throws IOException {
logger.trace("receiving hello message");
int magic = in.readInt();
checkArgument(magic == MAGIC, "magic mismatch, expected %s, got %s", MAGIC, magic);
int length = in.readShort();
checkArgument(length > 0, "invalid lenght, must be >0, got %s", length);
byte[] buffer = new byte[length];
in.readFully(buffer);
logger.trace("received hello message");
return BlockExchageProtos.Hello.parseFrom(buffer);
}
private Future sendHelloMessage(final byte[] payload) {
return outExecutorService.submit(new Runnable() {
@Override
public void run() {
try {
logger.trace("sending message");
ByteBuffer header = ByteBuffer.allocate(6);
header.putInt(MAGIC);
header.putShort((short) payload.length);
out.write(header.array());
out.write(payload);
out.flush();
logger.trace("sent message");
} catch (IOException ex) {
if (outExecutorService.isShutdown()) {
return;
}
logger.error("error writing to output stream", ex);
closeBg();
}
}
});
}
private Future sendPing() {
return sendMessage(Ping.newBuilder().build());
}
/**
* test connection, throw exception if failed
*
* @throws InterruptedException
* @throws ExecutionException
*/
public void testConnection() throws InterruptedException, ExecutionException {
checkNotClosed();
sendPing().get();
}
private final static BiMap> messageTypes = ImmutableBiMap.>builder()
.put(BlockExchageProtos.MessageType.CLOSE, BlockExchageProtos.Close.class)
.put(BlockExchageProtos.MessageType.CLUSTER_CONFIG, BlockExchageProtos.ClusterConfig.class)
.put(BlockExchageProtos.MessageType.DOWNLOAD_PROGRESS, BlockExchageProtos.DownloadProgress.class)
.put(BlockExchageProtos.MessageType.INDEX, BlockExchageProtos.Index.class)
.put(BlockExchageProtos.MessageType.INDEX_UPDATE, BlockExchageProtos.IndexUpdate.class)
.put(BlockExchageProtos.MessageType.PING, BlockExchageProtos.Ping.class)
.put(BlockExchageProtos.MessageType.REQUEST, BlockExchageProtos.Request.class)
.put(BlockExchageProtos.MessageType.RESPONSE, BlockExchageProtos.Response.class)
.build();
private void markActivityOnSocket() {
lastActive = System.currentTimeMillis();
}
private Pair receiveMessage() throws IOException {
logger.trace("receiving message");
int headerLength = in.readShort();
while (headerLength == 0) {
logger.warn("got headerLength == 0, skipping short");
headerLength = in.readShort();
}
markActivityOnSocket();
checkArgument(headerLength > 0, "invalid lenght, must be >0, got %s", headerLength);
byte[] headerBuffer = new byte[headerLength];
in.readFully(headerBuffer);
BlockExchageProtos.Header header = BlockExchageProtos.Header.parseFrom(headerBuffer);
logger.trace("message type = {} compression = {}", header.getType(), header.getCompression());
int messageLength;
while ((messageLength = in.readInt()) == 0) {
logger.warn("received readInt() == 0, expecting 'bep message header length' (int >0), ignoring (keepalive?)");
}
checkArgument(messageLength >= 0, "invalid lenght, must be >=0, got %s", messageLength);
byte[] messageBuffer = new byte[messageLength];
in.readFully(messageBuffer);
markActivityOnSocket();
if (equal(header.getCompression(), BlockExchageProtos.MessageCompression.LZ4)) {
int uncompressedLength = ByteBuffer.wrap(messageBuffer).getInt();
messageBuffer = LZ4Factory.fastestInstance().fastDecompressor().decompress(messageBuffer, 4, uncompressedLength);
}
checkArgument(messageTypes.containsKey(header.getType()), "unsupported message type = %s", header.getType());
try {
GeneratedMessage message = (GeneratedMessage) messageTypes.get(header.getType()).getMethod("parseFrom", byte[].class).invoke(null, (Object) messageBuffer);
return Pair.of(header.getType(), message);
} catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException | NoSuchMethodException | SecurityException ex) {
throw new RuntimeException(ex);
}
}
public Future sendMessage(final GeneratedMessage message) {
checkNotClosed();
checkArgument(messageTypes.containsValue(message.getClass()));
final BlockExchageProtos.Header header = BlockExchageProtos.Header.newBuilder()
.setCompression(BlockExchageProtos.MessageCompression.NONE)
.setType(messageTypes.inverse().get(message.getClass()))
.build();
final byte[] headerData = header.toByteArray(), messageData = message.toByteArray(); //TODO compression
return outExecutorService.submit(new Callable() {
@Override
public Object call() throws Exception {
try {
logger.debug("sending message type = {} {}", header.getType(), getIdForMessage(message));
logger.trace("sending message = {}", message);
markActivityOnSocket();
out.writeShort(headerData.length);
out.write(headerData);
out.writeInt(messageData.length);//with compression, check this
out.write(messageData);
out.flush();
markActivityOnSocket();
logger.debug("sent message {}", getIdForMessage(message));
} catch (IOException ex) {
if (!outExecutorService.isShutdown()) {
logger.error("error writing to output stream", ex);
closeBg();
}
throw ex;
}
return null;
}
});
}
@Override
public void close() {
if (!isClosed()) {
isClosed = true;
periodicExecutorService.shutdown();
outExecutorService.shutdown();
inExecutorService.shutdown();
messageProcessingService.shutdown();
if (out != null) {
IOUtils.closeQuietly(out);
out = null;
}
if (in != null) {
IOUtils.closeQuietly(in);
in = null;
}
if (socket != null) {
IOUtils.closeQuietly(socket);
socket = null;
}
logger.info("closed connection {}", address);
eventBus.post(ConnectionClosedEvent.INSTANCE);
try {
periodicExecutorService.awaitTermination(2, TimeUnit.SECONDS);
outExecutorService.awaitTermination(2, TimeUnit.SECONDS);
inExecutorService.awaitTermination(2, TimeUnit.SECONDS);
messageProcessingService.awaitTermination(2, TimeUnit.SECONDS);
} catch (InterruptedException ex) {
}
}
}
/**
* return time elapsed since last activity on socket, in millis
*
* @return
*/
public long getLastActive() {
return System.currentTimeMillis() - lastActive;
}
public EventBus getEventBus() {
return eventBus;
}
/**
* get id for message bean/instance, for log tracking
*
* @param message
* @return id for message bean
*/
private static String getIdForMessage(GeneratedMessage message) {
if (message instanceof Request) {
return Integer.toString(((Request) message).getId());
} else if (message instanceof Response) {
return Integer.toString(((Response) message).getId());
} else {
return Integer.toString(Math.abs(message.hashCode()));
}
}
public boolean isClosed() {
return isClosed;
}
private void startMessageListenerService() {
inExecutorService.submit(new Runnable() {
@Override
public void run() {
try {
while (!Thread.interrupted()) {
final Pair message = receiveMessage();
logger.debug("received message type = {} {}", message.getLeft(), getIdForMessage(message.getRight()));
logger.trace("received message = {}", message.getRight());
messageProcessingService.submit(new Runnable() {
@Override
public void run() {
logger.debug("processing message type = {} {}", message.getLeft(), getIdForMessage(message.getRight()));
try {
switch (message.getLeft()) {
case INDEX:
eventBus.post(new IndexMessageReceivedEvent((Index) message.getValue()));
break;
case INDEX_UPDATE:
eventBus.post(new IndexUpdateMessageReceivedEvent((IndexUpdate) message.getValue()));
break;
case REQUEST:
eventBus.post(new RequestMessageReceivedEvent((Request) message.getValue()));
break;
case RESPONSE:
eventBus.post(new ResponseMessageReceivedEvent((Response) message.getValue()));
break;
case PING:
logger.debug("ping message received");
break;
case CLOSE:
logger.info("received close message = {}", message.getValue());
closeBg();
break;
case CLUSTER_CONFIG: {
checkArgument(clusterConfigInfo == null, "received cluster config message twice!");
clusterConfigInfo = new ClusterConfigInfo();
ClusterConfig clusterConfig = (ClusterConfig) message.getValue();
for (Folder folder : firstNonNull(clusterConfig.getFoldersList(), Collections.emptyList())) {
ClusterConfigFolderInfo.Builder builder = ClusterConfigFolderInfo.newBuilder()
.setFolder(folder.getId())
.setLabel(folder.getLabel());
Map devicesById = Maps.uniqueIndex(firstNonNull(folder.getDevicesList(), Collections.emptyList()),
new Function() {
@Override
public String apply(Device input) {
return hashDataToDeviceIdString(input.getId().toByteArray());
}
});
Device otherDevice = devicesById.get(address.getDeviceId()),
ourDevice = devicesById.get(configuration.getDeviceId());
if (otherDevice != null) {
builder.setAnnounced(true);
}
final ClusterConfigFolderInfo folderInfo;
if (ourDevice != null) {
folderInfo = builder.setShared(true).build();
logger.info("folder shared from device = {} folder = {}", address.getDeviceId(), folderInfo);
if (!configuration.getFolderNames().contains(folderInfo.getFolder())) {
configuration.edit().addFolders(new FolderInfo(folderInfo.getFolder(), folderInfo.getLabel()));
logger.info("new folder shared = {}", folderInfo);
eventBus.post(new NewFolderSharedEvent() {
@Override
public String getFolder() {
return folderInfo.getFolder();
}
});
}
} else {
folderInfo = builder.build();
logger.info("folder not shared from device = {} folder = {}", address.getDeviceId(), folderInfo);
}
clusterConfigInfo.putFolderInfo(folderInfo);
configuration.edit().addPeers(Iterables.filter(Iterables.transform(firstNonNull(folder.getDevicesList(), Collections.emptyList()), new Function() {
@Override
public DeviceInfo apply(Device device) {
String deviceId = hashDataToDeviceIdString(device.getId().toByteArray()),
name = device.hasName() ? device.getName() : null;
return new DeviceInfo(deviceId, name);
}
}), new Predicate() {
@Override
public boolean apply(DeviceInfo s) {
return !equal(s.getDeviceId(), configuration.getDeviceId());
}
}));
}
configuration.edit().persistLater();
eventBus.post(new ClusterConfigMessageProcessedEvent(clusterConfig));
}
break;
}
} catch (Exception ex) {
if (messageProcessingService.isShutdown()) {
return;
}
logger.error("error processing message", ex);
closeBg();
throw ex;
}
}
});
}
} catch (Exception ex) {
if (inExecutorService.isShutdown()) {
return;
}
logger.error("error receiving message", ex);
closeBg();
}
}
});
}
public String getDeviceId() {
return getAddress().getDeviceId();
}
public abstract class MessageReceivedEvent implements DeviceAddressActiveEvent {
private final E message;
private MessageReceivedEvent(E message) {
checkNotNull(message);
this.message = message;
}
public E getMessage() {
return message;
}
public BlockExchangeConnectionHandler getConnectionHandler() {
return BlockExchangeConnectionHandler.this;
}
@Override
public DeviceAddress getDeviceAddress() {
return getConnectionHandler().getAddress();
}
}
public abstract class AnyIndexMessageReceivedEvent extends MessageReceivedEvent {
private AnyIndexMessageReceivedEvent(E message) {
super(message);
}
public abstract List getFilesList();
public abstract String getFolder();
}
public class IndexMessageReceivedEvent extends AnyIndexMessageReceivedEvent {
private IndexMessageReceivedEvent(Index message) {
super(message);
}
@Override
public List getFilesList() {
return getMessage().getFilesList();
}
@Override
public String getFolder() {
return getMessage().getFolder();
}
}
public class IndexUpdateMessageReceivedEvent extends AnyIndexMessageReceivedEvent {
private IndexUpdateMessageReceivedEvent(IndexUpdate message) {
super(message);
}
@Override
public List getFilesList() {
return getMessage().getFilesList();
}
@Override
public String getFolder() {
return getMessage().getFolder();
}
}
public class RequestMessageReceivedEvent extends MessageReceivedEvent {
private RequestMessageReceivedEvent(Request message) {
super(message);
}
}
public class ResponseMessageReceivedEvent extends MessageReceivedEvent {
private ResponseMessageReceivedEvent(Response message) {
super(message);
}
}
public class ClusterConfigMessageProcessedEvent extends MessageReceivedEvent {
private ClusterConfigMessageProcessedEvent(ClusterConfig message) {
super(message);
}
}
public enum ConnectionClosedEvent {
INSTANCE
}
@Override
public String toString() {
return "BlockExchangeConnectionHandler{" + "address=" + address + ", lastActive=" + (getLastActive() / 1000d) + "secs ago}";
}
private static class ConnectionInfo {
private String deviceName, clientName, clientVersion;
public String getDeviceName() {
return deviceName;
}
public void setDeviceName(String deviceName) {
this.deviceName = deviceName;
}
public String getClientName() {
return clientName;
}
public void setClientName(String clientName) {
this.clientName = clientName;
}
public String getClientVersion() {
return clientVersion;
}
public void setClientVersion(String clientVersion) {
this.clientVersion = clientVersion;
}
@Override
public String toString() {
return "ConnectionInfo{" + "deviceName=" + deviceName + ", clientName=" + clientName + ", clientVersion=" + clientVersion + '}';
}
}
public class ClusterConfigInfo {
private final Map folderInfoById = Maps.newConcurrentMap();
public ClusterConfigFolderInfo getFolderInfo(String folderId) {
ClusterConfigFolderInfo folderInfo = folderInfoById.get(folderId);
if (folderInfo == null) {
folderInfo = ClusterConfigFolderInfo.newBuilder().setFolder(folderId).build();
folderInfoById.put(folderId, folderInfo);
}
return folderInfo;
}
private void putFolderInfo(ClusterConfigFolderInfo folderInfo) {
folderInfoById.put(folderInfo.getFolder(), folderInfo);
}
public Set getSharedFolders() {
return Sets.newTreeSet(Iterables.transform(Iterables.filter(folderInfoById.values(), new Predicate() {
@Override
public boolean apply(ClusterConfigFolderInfo input) {
return input.isShared();
}
}), new Function() {
@Override
public String apply(ClusterConfigFolderInfo input) {
return input.getFolder();
}
}));
}
}
public boolean hasFolder(String folder) {
return getClusterConfigInfo().getSharedFolders().contains(folder);
}
public abstract class NewFolderSharedEvent {
public abstract String getFolder();
}
}