/*
* The MIT License
*
* Copyright 2014 sorrge.
*
* 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 DEALINGS IN
* THE SOFTWARE.
*/
package org.nyan.dch.communication;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.Set;
import org.nyan.dch.crypto.SHA256Hash;
import org.nyan.dch.node.IRemoteNode;
import org.nyan.dch.node.IRemoteResponseListener;
import org.nyan.dch.posts.Board;
import org.nyan.dch.posts.PostData;
/**
*
* @author sorrge
*/
public class RemoteNodeMessages implements IRemoteNode, IRemoteHostListener
{
public static final int MaxMessageSize = 100000, GiveAddressesCount = 10;
public static enum MessageTypes
{
PushPost(0), PullPosts(1), UpdateThreadsAndGetOthers(2), GetOtherPostsInThread(3), GiveAddresses(4), GiveMyAddress(5);
public final int id;
private static final HashMap<Integer, MessageTypes> idMap = new HashMap<>();
private MessageTypes(int id)
{
this.id = id;
}
static
{
for (MessageTypes type : MessageTypes.values())
idMap.put(type.id, type);
}
static MessageTypes FromID(int id)
{
return idMap.get(id);
}
};
private final IRemoteHost host;
private IRemoteResponseListener node;
public final IConnections connections;
public RemoteNodeMessages(IRemoteHost host, IConnections connections)
{
this.host = host;
this.connections = connections;
}
public void SetResponseListener(IRemoteResponseListener node, boolean iAmInitiator)
{
this.node = node;
node.OnlineStatusChanged(this, true, iAmInitiator);
}
@Override
public void ReceiveData(byte[] data) throws ProtocolException
{
if (data.length > MaxMessageSize)
throw new ProtocolException("Received message is too long");
ByteArrayInputStream bais = new ByteArrayInputStream(data);
DataInputStream str = new DataInputStream(bais);
try
{
MessageTypes t = MessageTypes.FromID(str.readInt());
if (t == null)
throw new ProtocolException("Received an unknown message");
switch (t)
{
case PushPost:
node.PostReceived(this, new PostData(str));
break;
case PullPosts:
node.SendPosts(this, DecodeHashes(bais, str));
break;
case UpdateThreadsAndGetOthers:
String board = str.readUTF();
if (board.length() > Board.MaxBoardNameLength)
throw new ProtocolException(String.format("Board name is longer than %d characters", Board.MaxBoardNameLength));
node.UpdateThreadsAndSendOthers(this, board, DecodeHashes(bais, str));
break;
case GetOtherPostsInThread:
String board1 = str.readUTF();
if (board1.length() > Board.MaxBoardNameLength)
throw new ProtocolException(String.format("Board name is longer than %d characters", Board.MaxBoardNameLength));
SHA256Hash threadId = new SHA256Hash(str);
node.SendOtherPostsInThread(this, board1, threadId, DecodeHashes(bais, str));
break;
case GiveAddresses:
int addressesRead = 0;
while(bais.available() > 0)
{
if(++addressesRead > GiveAddressesCount)
throw new ProtocolException("Too many addresses received");
connections.AddAddress(connections.GetTransport().ReadAddress(str));
}
break;
case GiveMyAddress:
IAddress addr = connections.GetTransport().ReadAddress(str);
long cookie = str.readLong();
boolean wantConfirmation = str.readBoolean();
connections.AddressSupplied(host, addr, cookie, wantConfirmation);
break;
default:
throw new ProtocolException(String.format("Can't process message: %s", t.toString()));
}
if(bais.available() != 0)
throw new ProtocolException("Garbage after the end of a received message");
}
catch (IOException ex)
{
throw new ProtocolException("Unknown error while parsing the message");
}
}
private static ArrayList<SHA256Hash> DecodeHashes(ByteArrayInputStream bais, DataInputStream str) throws IOException, ProtocolException
{
int numHashes = bais.available() / SHA256Hash.BytesLength;
if(numHashes * SHA256Hash.BytesLength != bais.available())
throw new ProtocolException("Unexpected end of message");
ArrayList<SHA256Hash> hashes = new ArrayList<>(numHashes);
for(int i = 0; i < numHashes; ++i)
hashes.add(new SHA256Hash(str));
return hashes;
}
@Override
public void Disconnected(boolean iAmInitiator)
{
node.OnlineStatusChanged(this, false, iAmInitiator);
connections.Disconnected(host);
}
@Override
public void PushPost(PostData post) throws ProtocolException
{
ByteArrayOutputStream res = new ByteArrayOutputStream(MaxMessageSize / 10);
try
{
DataOutputStream str = new DataOutputStream(res);
str.writeInt(MessageTypes.PushPost.id);
post.Write(str);
}
catch(IOException ex)
{
throw new ProtocolException("Unknown error while preparing the message");
}
if(res.size() > MaxMessageSize)
throw new ProtocolException("The message is too large");
host.SendData(res.toByteArray());
}
@Override
public void PullPosts(Collection<SHA256Hash> postsNeeded) throws ProtocolException
{
final int hashesPerMessage = (MaxMessageSize - Integer.SIZE / 8) / SHA256Hash.BytesLength;
final int totalMessages = (postsNeeded.size() - 1) / hashesPerMessage + 1;
ByteArrayOutputStream baos = new ByteArrayOutputStream(totalMessages > 1 ? MaxMessageSize : postsNeeded.size() * SHA256Hash.BytesLength + Integer.SIZE / 8);
try
{
DataOutputStream str = new DataOutputStream(baos);
str.writeInt(MessageTypes.PullPosts.id);
int inCurrentStream = 0;
for(SHA256Hash s : postsNeeded)
{
s.Write(str);
if(++inCurrentStream >= hashesPerMessage)
{
host.SendData(baos.toByteArray());
baos.reset();
str.writeInt(MessageTypes.PullPosts.id);
inCurrentStream = 0;
}
}
if(inCurrentStream > 0)
host.SendData(baos.toByteArray());
}
catch(IOException ex)
{
throw new ProtocolException("Unknown error while preparing the message");
}
}
@Override
public void UpdateThreadsAndGetOthers(String board, Collection<SHA256Hash> myThreads) throws ProtocolException
{
if(board.length() > Board.MaxBoardNameLength)
throw new ProtocolException(String.format("Board name is longer than %d characters", board.length()));
if(myThreads.size() > Board.MaxThreads)
throw new ProtocolException("Too many threads to update");
ByteArrayOutputStream boam = new ByteArrayOutputStream(MaxMessageSize / 10);
try
{
DataOutputStream str = new DataOutputStream(boam);
str.writeInt(MessageTypes.UpdateThreadsAndGetOthers.id);
str.writeUTF(board);
if(boam.size() + myThreads.size() * SHA256Hash.BytesLength > MaxMessageSize)
throw new ProtocolException(String.format("Can't update %d threads on board %s", myThreads.size(), board));
for(SHA256Hash t : myThreads)
t.Write(str);
host.SendData(boam.toByteArray());
}
catch(IOException ex)
{
throw new ProtocolException("Unknown error while preparing the message");
}
}
@Override
public void GetOtherPostsInThread(String board, SHA256Hash threadId, Collection<SHA256Hash> myPosts) throws ProtocolException
{
if(board.length() > Board.MaxBoardNameLength)
throw new ProtocolException(String.format("Board name is longer than %d characters", Board.MaxBoardNameLength));
ByteArrayOutputStream boam = new ByteArrayOutputStream(MaxMessageSize / 10);
try
{
DataOutputStream str = new DataOutputStream(boam);
str.writeInt(MessageTypes.GetOtherPostsInThread.id);
str.writeUTF(board);
threadId.Write(str);
if(boam.size() + myPosts.size() * SHA256Hash.BytesLength > MaxMessageSize)
throw new ProtocolException(String.format("Can't update %d posts on board %s in thread %s", myPosts.size(), board, threadId.toString()));
for(SHA256Hash t : myPosts)
t.Write(str);
host.SendData(boam.toByteArray());
}
catch(IOException ex)
{
throw new ProtocolException("Unknown error while preparing the message");
}
}
public void GiveAddresses(Collection<IAddress> addresses) throws ProtocolException
{
if(addresses.isEmpty() || addresses.size() > GiveAddressesCount)
throw new ProtocolException("Wrong number of addresses given");
ByteArrayOutputStream boam = new ByteArrayOutputStream(MaxMessageSize / 10);
try
{
DataOutputStream str = new DataOutputStream(boam);
str.writeInt(MessageTypes.GiveAddresses.id);
for(IAddress a : addresses)
a.Write(str);
host.SendData(boam.toByteArray());
}
catch(IOException ex)
{
throw new ProtocolException("Unknown error while preparing the message");
}
}
void GiveMyAddress(IAddress myAddress, boolean wantConfirmation) throws ProtocolException
{
if(myAddress == null)
throw new ProtocolException("Wrong address given");
// System.out.printf("Giving my address: %s, want confirmation: %s\n", myAddress, wantConfirmation);
ByteArrayOutputStream boam = new ByteArrayOutputStream(MaxMessageSize / 10);
try
{
DataOutputStream str = new DataOutputStream(boam);
str.writeInt(MessageTypes.GiveMyAddress.id);
myAddress.Write(str);
str.writeLong(host.GetCookie());
str.writeBoolean(wantConfirmation);
host.SendData(boam.toByteArray());
}
catch(IOException ex)
{
throw new ProtocolException("Unknown error while preparing the message");
}
}
}