/* * Copyright 2015 Christian Basler * * 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 ch.dissem.bitmessage.factory; import ch.dissem.bitmessage.entity.*; import ch.dissem.bitmessage.entity.payload.GenericPayload; import ch.dissem.bitmessage.entity.payload.ObjectPayload; import ch.dissem.bitmessage.entity.valueobject.InventoryVector; import ch.dissem.bitmessage.entity.valueobject.NetworkAddress; import ch.dissem.bitmessage.exception.NodeException; import ch.dissem.bitmessage.utils.AccessCounter; import ch.dissem.bitmessage.utils.Decode; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import static ch.dissem.bitmessage.entity.NetworkMessage.MAGIC_BYTES; import static ch.dissem.bitmessage.utils.Singleton.security; /** * Creates protocol v3 network messages from {@link InputStream InputStreams} */ class V3MessageFactory { private static Logger LOG = LoggerFactory.getLogger(V3MessageFactory.class); public static NetworkMessage read(InputStream in) throws IOException { findMagic(in); String command = getCommand(in); int length = (int) Decode.uint32(in); byte[] checksum = Decode.bytes(in, 4); byte[] payloadBytes = Decode.bytes(in, length); if (testChecksum(checksum, payloadBytes)) { MessagePayload payload = getPayload(command, new ByteArrayInputStream(payloadBytes), length); if (payload != null) return new NetworkMessage(payload); else return null; } else { throw new IOException("Checksum failed for message '" + command + "'"); } } private static MessagePayload getPayload(String command, InputStream stream, int length) throws IOException { switch (command) { case "version": return parseVersion(stream); case "verack": return new VerAck(); case "addr": return parseAddr(stream); case "inv": return parseInv(stream); case "getdata": return parseGetData(stream); case "object": return readObject(stream, length); case "custom": return readCustom(stream, length); default: LOG.debug("Unknown command: " + command); return null; } } private static MessagePayload readCustom(InputStream in, int length) throws IOException { return CustomMessage.read(in, length); } public static ObjectMessage readObject(InputStream in, int length) throws IOException { AccessCounter counter = new AccessCounter(); byte nonce[] = Decode.bytes(in, 8, counter); long expiresTime = Decode.int64(in, counter); long objectType = Decode.uint32(in, counter); long version = Decode.varInt(in, counter); long stream = Decode.varInt(in, counter); byte[] data = Decode.bytes(in, length - counter.length()); ObjectPayload payload; try { ByteArrayInputStream dataStream = new ByteArrayInputStream(data); payload = Factory.getObjectPayload(objectType, version, stream, dataStream, data.length); } catch (Exception e) { LOG.trace("Could not parse object payload - using generic payload instead", e); payload = new GenericPayload(version, stream, data); } return new ObjectMessage.Builder() .nonce(nonce) .expiresTime(expiresTime) .objectType(objectType) .stream(stream) .payload(payload) .build(); } private static GetData parseGetData(InputStream stream) throws IOException { long count = Decode.varInt(stream); GetData.Builder builder = new GetData.Builder(); for (int i = 0; i < count; i++) { builder.addInventoryVector(parseInventoryVector(stream)); } return builder.build(); } private static Inv parseInv(InputStream stream) throws IOException { long count = Decode.varInt(stream); Inv.Builder builder = new Inv.Builder(); for (int i = 0; i < count; i++) { builder.addInventoryVector(parseInventoryVector(stream)); } return builder.build(); } private static Addr parseAddr(InputStream stream) throws IOException { long count = Decode.varInt(stream); Addr.Builder builder = new Addr.Builder(); for (int i = 0; i < count; i++) { builder.addAddress(parseAddress(stream, false)); } return builder.build(); } private static Version parseVersion(InputStream stream) throws IOException { int version = Decode.int32(stream); long services = Decode.int64(stream); long timestamp = Decode.int64(stream); NetworkAddress addrRecv = parseAddress(stream, true); NetworkAddress addrFrom = parseAddress(stream, true); long nonce = Decode.int64(stream); String userAgent = Decode.varString(stream); long[] streamNumbers = Decode.varIntList(stream); return new Version.Builder() .version(version) .services(services) .timestamp(timestamp) .addrRecv(addrRecv).addrFrom(addrFrom) .nonce(nonce) .userAgent(userAgent) .streams(streamNumbers).build(); } private static InventoryVector parseInventoryVector(InputStream stream) throws IOException { return new InventoryVector(Decode.bytes(stream, 32)); } private static NetworkAddress parseAddress(InputStream stream, boolean light) throws IOException { long time; long streamNumber; if (!light) { time = Decode.int64(stream); streamNumber = Decode.uint32(stream); // This isn't consistent, not sure if this is correct } else { time = 0; streamNumber = 0; } long services = Decode.int64(stream); byte[] ipv6 = Decode.bytes(stream, 16); int port = Decode.uint16(stream); return new NetworkAddress.Builder().time(time).stream(streamNumber).services(services).ipv6(ipv6).port(port).build(); } private static boolean testChecksum(byte[] checksum, byte[] payload) { byte[] payloadChecksum = security().sha512(payload); for (int i = 0; i < checksum.length; i++) { if (checksum[i] != payloadChecksum[i]) { return false; } } return true; } private static String getCommand(InputStream stream) throws IOException { byte[] bytes = new byte[12]; int end = -1; for (int i = 0; i < bytes.length; i++) { bytes[i] = (byte) stream.read(); if (end == -1) { if (bytes[i] == 0) end = i; } else { if (bytes[i] != 0) throw new IOException("'\\0' padding expected for command"); } } return new String(bytes, 0, end, "ASCII"); } private static void findMagic(InputStream in) throws IOException { int pos = 0; for (int i = 0; i < 1620000; i++) { byte b = (byte) in.read(); if (b == MAGIC_BYTES[pos]) { if (pos + 1 == MAGIC_BYTES.length) { return; } } else if (pos > 0 && b == MAGIC_BYTES[0]) { pos = 1; } else { pos = 0; } pos++; } throw new NodeException("Failed to find MAGIC bytes in stream"); } }