diff --git a/doc/protocol.md b/doc/protocol.md index 02c28bf..b21a615 100644 --- a/doc/protocol.md +++ b/doc/protocol.md @@ -16,10 +16,12 @@ x bytes: [(bytes 8-?): PAYLOAD] - REQUESTS (msb is 0): - `LIST` (0x00) - `LOAD` (0x01) + - `HASH` (0x02) - RESPONSES (msb is 1): - `LIST` (0x80) - `LOAD` (0x81) + - `HASH` (0x82) - `VERSION ERROR` (0xC0) - `PROTOCOL ERROR` (0xC1) - `INTERNAL ERROR` (0xC2) @@ -63,6 +65,39 @@ Payload contains y bytes: [] ``` +### Hash +#### Hash request +Get hash of a file. Payload contains + +``` +4 bytes: [(bytes 8-11): FILENAME SIZE] +y bytes: [] +z bytes: [ALGO_NAMES requested separated by \n] (ex.: SHA-256, MD5) +``` + +If file does not exists, a NotFound can be responded. + +#### Hash response +Payload contains: + + +``` +4 bytes: [(bytes 8-11): FILENAME SIZE] +y bytes: [] +[[ multiple algo hashes bloc]] +``` + +A algo hash bloc contains: + +``` +4 bytes [ALGO_NAME size] +? [ALGO_NAME] +4 bytes: [HASH SIZE (bytes)] / or 0 if this hash algorithm is unsupported. +?? [HASH] + +``` + + ### Other response code (errors) #### Version error Response when datagram received use wrong version code. diff --git a/src/protocolP2P/HashAlgorithm.java b/src/protocolP2P/HashAlgorithm.java new file mode 100644 index 0000000..283190a --- /dev/null +++ b/src/protocolP2P/HashAlgorithm.java @@ -0,0 +1,49 @@ +package protocolP2P; +import java.util.HashMap; +import java.util.Map; + +/** HashAlgorithm enum. + * @author Louis Royer + * @author Flavien Haas + * @author JS Auge + * @version 1.0 + */ +public enum HashAlgorithm { + SHA512("SHA-512"), + MD5("MD5"); + + private String name; + /* To be able to convert name to enum */ + private static final Map BY_CODE = new HashMap<>(); + /* Initialization of HashMap */ + static { + for (HashAlgorithm h: values()) { + assert !BY_CODE.containsKey(Byte.valueOf(h.name)) : "Duplicate in " + HashAlgorithm.class.getCanonicalName(); + BY_CODE.put(Byte.valueOf(h.name), h); + } + } + + HashAlgorithm(String name) { + this.name = name; + } + + public String getName() { + return name; + } + + /** Gives enum from name. + * @param name name of the hash + * @return enum element + * @throws InterlanError + */ + protected static HashAlgorithm fromName(String name) throws InternalError { + HashAlgorithm h = BY_CODE.get(Byte.valueOf(name)); + if (h == null) { + throw new InternalError(); + } + return h; + } + +} + + diff --git a/src/protocolP2P/HashRequest.java b/src/protocolP2P/HashRequest.java new file mode 100644 index 0000000..f711ec8 --- /dev/null +++ b/src/protocolP2P/HashRequest.java @@ -0,0 +1,158 @@ +package protocolP2P; +import protocolP2P.Payload; +import protocolP2P.HashAlgorithm; +import java.io.UnsupportedEncodingException; +import exception.TransmissionError; +import exception.SizeError; +import exception.ProtocolError; +import exception.InternalError; +import tools.BytesArrayTools; + + +/** Representation of payload for hash request. + * @author Louis Royer + * @author Flavien Haas + * @author JS Auge + * @version 1.0 + */ + +public class HashRequest extends Payload { + private String filename; + private HashAlgorithm[] algoList; + private static final int FILENAME_SIZE_POSITION = PAYLOAD_START_POSITION; + private static final int FILENAME_POSITION = FILENAME_SIZE_POSITION + 4; + + /** Constructor (typically used by the server) with a filename parameter. + * @param filename name of the file to download. Must not be empty. + * @param algoList List of hash algorithms used + * @throws InternalError + * + */ + public HashRequest(String filename, HashAlgorithm[] algoList) throws InternalError { + super(RequestResponseCode.HASH_REQUEST); + /* assert to help debugging */ + assert filename.length() != 0 : "Size of filename in HashRequest must not be empty"; + if (filename.length() == 0) { + throw new InternalError(); + } + this.filename = filename; + assert algoList.length != 0 : "Hash algorithms list must not be empty"; + if (algoList.length == 0) { + throw new InternalError(); + } + this.algoList = algoList; + } + + /** Constructor (typically used by client) with a byte[] parameter containing the Packet received. + * @param packet the full Packet received + * @throws SizeError + * @throws InternalError + * @throws ProtocolError + * @throws TransmissionError + */ + protected HashRequest(byte[] packet) throws TransmissionError, SizeError, ProtocolError, InternalError { + super(packet); + /* assert to help debugging */ + assert requestResponseCode == RequestResponseCode.HASH_REQUEST : "HashRequest subclass is incompatible with this Packet, request/response code must be checked before using this constructor"; + /* InternalErrorException */ + if (requestResponseCode!= RequestResponseCode.HASH_REQUEST) { + throw new InternalError(); + } + /* Read filename */ + int filenameSize = BytesArrayTools.readInt(packet, FILENAME_SIZE_POSITION); + try { + filename = new String(packet, FILENAME_POSITION, filenameSize, "UTF-8"); + } catch (UnsupportedEncodingException e) { + throw new InternalError(); + } + + int size = getPayloadSize(packet); + try { + String[] l = (new String(packet, FILENAME_POSITION + filenameSize, size, "UTF-8")).split("\n"); + int i = 0; + for(String algo : l) { + algoList[i] = HashAlgorithm.fromName(algo); + i++; + } + } catch (UnsupportedEncodingException e) { + throw new InternalError(); + } + } + + /** Returns a byte[] containing Packet with padding. + * This Packet is still incomplete and should not be send directly. + * ProtocolP2PPacket will use this method to generate the complete Packet. + * @return Packet with padding + * @throws InternalError + */ + protected byte[] toPacket() throws InternalError { + // compute size + int filenameSize = filename.length(); + int size = FILENAME_POSITION + filenameSize; + for (HashAlgorithm h : algoList) { + size += h.getName().length(); + size += 1; // size for '\n' + } + size -=1; // remove trailing '\n' + byte[] packet = new byte[size + 1]; // java initialize all to zero + + // set request/response code + packet[RequestResponseCode.RRCODE_POSITION] = requestResponseCode.codeValue; + + // set Payload size + setPayloadSize(size - FILENAME_SIZE_POSITION, packet); + + BytesArrayTools.write(packet, FILENAME_SIZE_POSITION, filenameSize); + + // Write filename + int bCount = FILENAME_POSITION; + try { + byte[] sb = filename.getBytes("UTF-8"); + for(byte b : sb) { + packet[bCount] = b; + bCount += 1; + } + } catch (UnsupportedEncodingException e) { + throw new InternalError(); + } + boolean firstIter = true; + for(HashAlgorithm h : algoList) { + String s = h.getName(); + if (!firstIter) { + firstIter = false; + try { + packet[bCount] = "\n".getBytes("UTF-8")[0]; // separator + } catch (UnsupportedEncodingException e) { + throw new InternalError(); + } + bCount += 1; + } + // Copy algoname + try { + byte[] sb = s.getBytes("UTF-8"); + for(byte b : sb) { + packet[bCount] = b; + bCount += 1; + } + } catch (UnsupportedEncodingException e) { + throw new InternalError(); + } + + } + return packet; + } + + /** AlgoList getter. + * @return List of HashAlgorithms + */ + public HashAlgorithm[] getAlgoList() { + return algoList; + } + + /** Filename getter. + * @return filename + */ + public String getFilename() { + return filename; + } +} diff --git a/src/protocolP2P/HashResponse.java b/src/protocolP2P/HashResponse.java new file mode 100644 index 0000000..9cb6d2f --- /dev/null +++ b/src/protocolP2P/HashResponse.java @@ -0,0 +1,184 @@ +package protocolP2P; +import protocolP2P.Payload; +import java.util.HashMap; +import java.util.Map; +import java.io.UnsupportedEncodingException; +import exception.TransmissionError; +import exception.SizeError; +import exception.ProtocolError; +import exception.InternalError; +import tools.BytesArrayTools; +import java.security.MessageDigest; +import java.security.DigestInputStream; +import java.nio.file.Files; +import java.nio.file.Paths; + +/** Representation of payload for hash response. + * @author Louis Royer + * @author Flavien Haas + * @author JS Auge + * @version 1.0 + */ + +public class HashResponse extends Payload { + private String filename; + private Map hashes; + private static final int FILENAME_SIZE_POSITION = PAYLOAD_START_POSITION; + private static final int FILENAME_POSITION = FILENAME_SIZE_POSITION + 4; + + /** Constructor (typically used by the server) with a filename parameter. + * @param filename name of the file to download. Must not be empty. + * @param hashes HashMap containing hashes for file. + * @throws InternalError + * + */ + public HashResponse(String filename, Map hashes) throws InternalError { + super(RequestResponseCode.HASH_RESPONSE); + /* assert to help debugging */ + assert filename.length() != 0 : "Size of filename in HashResponse must not be empty"; + if (filename.length() == 0) { + throw new InternalError(); + } + this.filename = filename; + + // initialize hashes + assert !hashes.isEmpty() : "At least 1 hash must be provided"; + if (hashes.isEmpty()) { + throw new InternalError(); + } + this.hashes = hashes; + } + + /** Constructor (typically used by client) with a byte[] parameter containing the Packet received. + * @param packet the full Packet received + * @throws SizeError + * @throws InternalError + * @throws ProtocolError + * @throws TransmissionError + */ + protected HashResponse(byte[] packet) throws TransmissionError, SizeError, ProtocolError, InternalError { + super(packet); + /* assert to help debugging */ + assert requestResponseCode == RequestResponseCode.HASH_RESPONSE : "HashResponse subclass is incompatible with this Packet, request/response code must be checked before using this constructor"; + /* InternalErrorException */ + if (requestResponseCode!= RequestResponseCode.HASH_RESPONSE) { + throw new InternalError(); + } + /* Read filename */ + int filenameSize = BytesArrayTools.readInt(packet, FILENAME_SIZE_POSITION); + try { + filename = new String(packet, FILENAME_POSITION, filenameSize, "UTF-8"); + } catch (UnsupportedEncodingException e) { + throw new InternalError(); + } + + int size = getPayloadSize(packet); + int start = FILENAME_POSITION + filenameSize; + do { + int algoNameSize = BytesArrayTools.readInt(packet, start); + start +=4; + try { + String algoName = new String(packet, start, algoNameSize, "UTF-8"); + start += algoNameSize; + int hashSize = BytesArrayTools.readInt(packet, start); + start += 8; + if (hashSize != 0) { + hashes.put(HashAlgorithm.fromName(algoName), new String(packet, start, hashSize, "UTF-8")); + } + start += hashSize; + } catch (UnsupportedEncodingException e) { + throw new InternalError(); + } + } while (start < size); + + } + + /** Returns a byte[] containing Packet with padding. + * This Packet is still incomplete and should not be send directly. + * ProtocolP2PPacket will use this method to generate the complete Packet. + * @return Packet with padding + * @throws InternalError + */ + protected byte[] toPacket() throws InternalError { + // compute size + int filenameSize = filename.length(); + int size = FILENAME_POSITION + filenameSize; + for (HashAlgorithm h : hashes.keySet()) { + size += 4 + h.getName().length(); + } + for (String s : hashes.values()) { + size += 4 + s.length(); + } + byte[] packet = new byte[size + 1]; // java initialize all to zero + + // set request/response code + packet[RequestResponseCode.RRCODE_POSITION] = requestResponseCode.codeValue; + + // set Payload size + setPayloadSize(size - FILENAME_SIZE_POSITION, packet); + + BytesArrayTools.write(packet, FILENAME_SIZE_POSITION, filenameSize); + + // Write filename + int bCount = FILENAME_POSITION; + try { + byte[] sb = filename.getBytes("UTF-8"); + for(byte b : sb) { + packet[bCount] = b; + bCount += 1; + } + } catch (UnsupportedEncodingException e) { + throw new InternalError(); + } + for(HashAlgorithm h : hashes.keySet()) { + String s = h.getName(); + BytesArrayTools.write(packet, bCount, (int)s.length()); + bCount += 4; + // Copy algoname + try { + byte[] sb = s.getBytes("UTF-8"); + for(byte b : sb) { + packet[bCount] = b; + bCount += 1; + } + } catch (UnsupportedEncodingException e) { + throw new InternalError(); + } + s = hashes.get(s); + if (s == null) { + BytesArrayTools.write(packet, bCount, (int)0); + bCount += 4; + } else { + BytesArrayTools.write(packet, bCount, (int)s.length()); + bCount +=4; + // copy hash + try { + byte[] sb = s.getBytes("UTF-8"); + for(byte b : sb) { + packet[bCount] = b; + bCount += 1; + } + } catch (UnsupportedEncodingException e) { + throw new InternalError(); + } + } + } + return packet; + } + + /** hash getter + * @param hashAlgo HashAlgorithm to return hash from + * @return hash + */ + public String getHash(HashAlgorithm hashAlgo) { + return hashes.get(hashAlgo); + } + + /** filename getter. + * @return filename + */ + public String getFilename() { + return filename; + } + +} diff --git a/src/protocolP2P/Payload.java b/src/protocolP2P/Payload.java index 6868b56..cbe9767 100644 --- a/src/protocolP2P/Payload.java +++ b/src/protocolP2P/Payload.java @@ -3,6 +3,8 @@ import protocolP2P.RequestResponseCode; import protocolP2P.FilePart; import protocolP2P.FileList; import protocolP2P.LoadRequest; +import protocolP2P.HashRequest; +import protocolP2P.HashResponse; import exception.ProtocolError; import exception.InternalError; import exception.TransmissionError; @@ -28,6 +30,8 @@ public class Payload { assert requestResponseCode != RequestResponseCode.LIST_RESPONSE || (this instanceof FileList) : "LIST_RESPONSE must use FilePart class"; assert requestResponseCode != RequestResponseCode.LOAD_RESPONSE || (this instanceof FilePart) : "LOAD_RESPONSE must use FileList class"; assert requestResponseCode != RequestResponseCode.LOAD_REQUEST || (this instanceof LoadRequest) : "LOAD_REQUEST must use LoadRequest class"; + assert requestResponseCode != RequestResponseCode.HASH_REQUEST || (this instanceof HashRequest) : "HASH_REQUEST must use HashRequest class"; + assert requestResponseCode != RequestResponseCode.HASH_RESPONSE || (this instanceof HashResponse) : "HASH_RESPONSE must use HashResponse class"; this.requestResponseCode = requestResponseCode; checkRequestResponseCode(); // this can throw InternalError } diff --git a/src/protocolP2P/RequestResponseCode.java b/src/protocolP2P/RequestResponseCode.java index 2549c95..b69ebb3 100644 --- a/src/protocolP2P/RequestResponseCode.java +++ b/src/protocolP2P/RequestResponseCode.java @@ -14,8 +14,10 @@ import java.lang.Byte; public enum RequestResponseCode { LIST_REQUEST(CodeType.REQUEST, (byte)0x00), LOAD_REQUEST(CodeType.REQUEST, (byte)0x01), + HASH_REQUEST(CodeType.REQUEST, (byte)0x02), LIST_RESPONSE(CodeType.RESPONSE, (byte)0x80), LOAD_RESPONSE(CodeType.RESPONSE, (byte)0x81), + HASH_RESPONSE(CodeType.RESPONSE, (byte)0x82), VERSION_ERROR(CodeType.ERROR, (byte)0xC0), PROTOCOL_ERROR(CodeType.ERROR, (byte)0xC1), INTERNAL_ERROR(CodeType.ERROR, (byte)0xC2), @@ -49,10 +51,11 @@ public enum RequestResponseCode { /** Gives enum from Packet code. * @param code value of the element in packet - * @return enum element + * @return enum element + * @throws ProtocolError */ protected static RequestResponseCode fromCode(byte code) throws ProtocolError { - RequestResponseCode r= BY_CODE.get(Byte.valueOf(code)); + RequestResponseCode r = BY_CODE.get(Byte.valueOf(code)); if (r == null) { throw new ProtocolError(); } diff --git a/src/tools/LogLevel.java b/src/tools/LogLevel.java index 5ff9900..4bc7169 100644 --- a/src/tools/LogLevel.java +++ b/src/tools/LogLevel.java @@ -1,4 +1,10 @@ package tools; +/** LogLevel Enum. + * @author Louis Royer + * @author Flavien Haas + * @author JS Auge + * @version 1.0 + */ public enum LogLevel { Error, Info,