--- /dev/null
+import java.io.File;
+import java.io.BufferedReader;
+import java.io.FileReader;
+import java.io.PrintWriter;
+import java.time.LocalDate;
+import java.time.LocalTime;
+import java.time.format.DateTimeFormatter;
+import java.io.IOException;
+
+public class DateWriter {
+
+ public static void main(String[] args) throws IOException {
+ if (args.length < 3) {
+ System.out.println("Usage: java /path/to/file/with/timestamps /path/to/new/timestamp/file/with/dates initial_date_in_uuuu-MM-dd_format");
+ System.exit(1);
+ }
+ String pathOriginal = args[0];
+ String pathModified = args[1];
+ String initialDateStr = args[2];
+ LocalDate date = LocalDate.parse(initialDateStr, DateTimeFormatter.ofPattern("uuuu-MM-dd"));
+ File originalFile = new File(pathOriginal);
+ // Create output file
+ File modifiedFile = new File(pathModified);
+ modifiedFile.createNewFile();
+ BufferedReader reader = new BufferedReader(new FileReader(originalFile));
+ PrintWriter writer = new PrintWriter(modifiedFile);
+ String line = null;
+ String prevLine = null;
+ while ((line = reader.readLine()) != null) {
+ if (isNewDay(line, prevLine)) {
+ // Advance date
+ date = date.plusDays(1);
+ }
+ writer.println(String.format("%s %s", date.toString(), line));
+ prevLine = line;
+ }
+ writer.flush();
+ writer.close();
+ reader.close();
+ }
+
+ private static boolean isNewDay(String line, String prevLine) {
+ if (prevLine == null) {
+ return false;
+ }
+ // First part handles case where we pass midnight and the following timestamp is an AM timestamp
+ // Second case handles case where we pass midnight, but the following timestamp is a PM timestamp
+ return line.endsWith("AM") && prevLine.endsWith("PM") || toLocalTime(line).isBefore(toLocalTime(prevLine));
+ }
+
+ private static LocalTime toLocalTime(String timeString) {
+ return LocalTime.parse(timeString, DateTimeFormatter.ofPattern("h:mm:ss a"));
+ }
+}
\ No newline at end of file
private final int mServerPort;
/**
- * The list of packets pertaining to this conversation.
+ * The list of packets (with payload) pertaining to this conversation.
*/
private final List<PcapPacket> mPackets;
/**
- * Contains the sequence numbers seen so far for this {@code Conversation}.
+ * Contains the sequence numbers used thus far by the host that is considered the <em>client</em> in this
+ * {@code Conversation}.
* Used for filtering out retransmissions.
*/
- private final Set<Integer> mSeqNumbers;
+ private final Set<Integer> mSeqNumbersClient;
+
+ /**
+ * Contains the sequence numbers used thus far by the host that is considered the <em>server</em> in this
+ * {@code Conversation}.
+ * Used for filtering out retransmissions.
+ */
+ private final Set<Integer> mSeqNumbersSrv;
+
+ /**
+ * List of SYN packets pertaining to this conversation.
+ */
+ private List<PcapPacket> mSynPackets;
/**
* List of pairs FINs and their corresponding ACKs associated with this conversation.
private List<FinAckPair> mFinPackets;
/* End instance properties */
+ /**
+ * Factory method for creating a {@code Conversation} from a {@link PcapPacket}.
+ * @param pcapPacket The {@code PcapPacket} that wraps a TCP segment for which a {@code Conversation} is to be initiated.
+ * @param clientIsSrc If {@code true}, the source address and source port found in the IP datagram and TCP segment
+ * wrapped in the {@code PcapPacket} are regarded as pertaining to the client, and the destination
+ * address and destination port are regarded as pertaining to the server---and vice versa if set
+ * to {@code false}.
+ * @return A {@code Conversation} initiated with ip:port for client and server according to the direction of the packet.
+ */
+ public static Conversation fromPcapPacket(PcapPacket pcapPacket, boolean clientIsSrc) {
+ IpV4Packet ipPacket = pcapPacket.get(IpV4Packet.class);
+ TcpPacket tcpPacket = pcapPacket.get(TcpPacket.class);
+ String clientIp = clientIsSrc ? ipPacket.getHeader().getSrcAddr().getHostAddress() :
+ ipPacket.getHeader().getDstAddr().getHostAddress();
+ String srvIp = clientIsSrc ? ipPacket.getHeader().getDstAddr().getHostAddress() :
+ ipPacket.getHeader().getSrcAddr().getHostAddress();
+ int clientPort = clientIsSrc ? tcpPacket.getHeader().getSrcPort().valueAsInt() :
+ tcpPacket.getHeader().getDstPort().valueAsInt();
+ int srvPort = clientIsSrc ? tcpPacket.getHeader().getDstPort().valueAsInt() :
+ tcpPacket.getHeader().getSrcPort().valueAsInt();
+ return new Conversation(clientIp, clientPort, srvIp, srvPort);
+ }
+
/**
* Constructs a new {@code Conversation}.
* @param clientIp The IP of the host that is considered the client (i.e. the host that initiates the conversation)
this.mServerIp = serverIp;
this.mServerPort = serverPort;
this.mPackets = new ArrayList<>();
- this.mSeqNumbers = new HashSet<>();
+ this.mSeqNumbersClient = new HashSet<>();
+ this.mSeqNumbersSrv = new HashSet<>();
+ this.mSynPackets = new ArrayList<>();
this.mFinPackets = new ArrayList<>();
}
public void addPacket(PcapPacket packet, boolean ignoreRetransmissions) {
// Precondition: verify that packet does indeed pertain to conversation.
onAddPrecondition(packet);
- // For now we only support TCP flows.
- TcpPacket tcpPacket = Objects.requireNonNull(packet.get(TcpPacket.class));
- int seqNo = tcpPacket.getHeader().getSequenceNumber();
- if (ignoreRetransmissions && mSeqNumbers.contains(seqNo)) {
+ if (ignoreRetransmissions && isRetransmission(packet)) {
// Packet is a retransmission. Ignore it.
return;
}
- // Update set of sequence numbers seen so far with sequence number of new packet.
- mSeqNumbers.add(seqNo);
+ // Select direction-dependent set of sequence numbers seen so far and update it with sequence number of new packet.
+ addSeqNumber(packet);
// Finally add packet to list of packets pertaining to this conversation.
mPackets.add(packet);
+ // Preserve order of packets in list: sort according to timestamp.
+ if (mPackets.size() > 1 &&
+ mPackets.get(mPackets.size()-1).getTimestamp().isBefore(mPackets.get(mPackets.size()-2).getTimestamp())) {
+ Collections.sort(mPackets, (o1, o2) -> {
+ if (o1.getTimestamp().isBefore(o2.getTimestamp())) { return -1; }
+ else if (o2.getTimestamp().isBefore(o1.getTimestamp())) { return 1; }
+ else { return 0; }
+ });
+ }
}
/**
return Collections.unmodifiableList(mPackets);
}
+ /**
+ * Records a TCP SYN packet as pertaining to this conversation (adds it to the the internal list).
+ * Attempts to add duplicate SYN packets will be ignored, and the caller is made aware of the attempt to add a
+ * duplicate by the return value being {@code false}.
+ *
+ * @param synPacket A {@link PcapPacket} wrapping a TCP SYN packet.
+ * @return {@code true} if the packet was successfully added to this {@code Conversation}, {@code false} otherwise.
+ */
+ public boolean addSynPacket(PcapPacket synPacket) {
+ onAddPrecondition(synPacket);
+ final IpV4Packet synPacketIpSection = synPacket.get(IpV4Packet.class);
+ final TcpPacket synPacketTcpSection = synPacket.get(TcpPacket.class);
+ if (synPacketTcpSection == null || !synPacketTcpSection.getHeader().getSyn()) {
+ throw new IllegalArgumentException("Not a SYN packet.");
+ }
+ // We are only interested in recording one copy of the two SYN packets (one SYN packet in each direction), i.e.,
+ // we want to discard retransmitted SYN packets.
+ if (mSynPackets.size() >= 2) {
+ return false;
+ }
+ // Check the set of recorded SYN packets to see if we have already recorded a SYN packet going in the same
+ // direction as the packet given in the argument.
+ boolean matchingPrevSyn = mSynPackets.stream().anyMatch(p -> {
+ IpV4Packet pIp = p.get(IpV4Packet.class);
+ TcpPacket pTcp = p.get(TcpPacket.class);
+ boolean srcAddrMatch = synPacketIpSection.getHeader().getSrcAddr().getHostAddress().
+ equals(pIp.getHeader().getSrcAddr().getHostAddress());
+ boolean dstAddrMatch = synPacketIpSection.getHeader().getDstAddr().getHostAddress().
+ equals(pIp.getHeader().getDstAddr().getHostAddress());
+ boolean srcPortMatch = synPacketTcpSection.getHeader().getSrcPort().valueAsInt() ==
+ pTcp.getHeader().getSrcPort().valueAsInt();
+ boolean dstPortMatch = synPacketTcpSection.getHeader().getDstPort().valueAsInt() ==
+ pTcp.getHeader().getDstPort().valueAsInt();
+ return srcAddrMatch && dstAddrMatch && srcPortMatch && dstPortMatch;
+ });
+ if (matchingPrevSyn) {
+ return false;
+ }
+ // Update direction-dependent set of sequence numbers and record/log packet.
+ addSeqNumber(synPacket);
+ return mSynPackets.add(synPacket);
+
+ /*
+ mSynPackets.stream().anyMatch(p -> {
+ IpV4Packet pIp = p.get(IpV4Packet.class);
+ TcpPacket pTcp = p.get(TcpPacket.class);
+ boolean srcAddrMatch = synPacketIpSection.getHeader().getSrcAddr().getHostAddress().
+ equals(pIp.getHeader().getSrcAddr().getHostAddress());
+ boolean dstAddrMatch = synPacketIpSection.getHeader().getDstAddr().getHostAddress().
+ equals(pIp.getHeader().getDstAddr().getHostAddress());
+ boolean srcPortMatch = synPacketTcpSection.getHeader().getSrcPort().valueAsInt() ==
+ pTcp.getHeader().getSrcPort().valueAsInt();
+ boolean dstPortMatch = synPacketTcpSection.getHeader().getDstPort().value() ==
+ pTcp.getHeader().getDstPort().value();
+
+ boolean fourTupleMatch = srcAddrMatch && dstAddrMatch && srcPortMatch && dstPortMatch;
+
+ boolean seqNoMatch = synPacketTcpSection.getHeader().getSequenceNumber() ==
+ pTcp.getHeader().getSequenceNumber();
+
+ if (fourTupleMatch && !seqNoMatch) {
+ // If the four tuple that identifies the conversation matches, but the sequence number is different,
+ // it means that this SYN packet is, in fact, an attempt to establish a **new** connection, and hence
+ // the given packet is NOT part of this conversation, even though the ip:port combinations are (by
+ // chance) selected such that they match this conversation.
+ throw new IllegalArgumentException("Attempt to add SYN packet that belongs to a different conversation " +
+ "(which is identified by the same four tuple as this conversation)");
+ }
+ return fourTupleMatch && seqNoMatch;
+ });
+ */
+ }
+
+ /**
+ * Get a list of SYN packets pertaining to this {@code Conversation}.
+ * The returned list is a read-only list.
+ * @return the list of SYN packets pertaining to this {@code Conversation}.
+ */
+ public List<PcapPacket> getSynPackets() {
+ return Collections.unmodifiableList(mSynPackets);
+ }
+
/**
* Adds a TCP FIN packet to the list of TCP FIN packets associated with this conversation.
* @param finPacket The TCP FIN packet that is to be added to (associated with) this conversation.
public void addFinPacket(PcapPacket finPacket) {
// Precondition: verify that packet does indeed pertain to conversation.
onAddPrecondition(finPacket);
+ // TODO: should call addSeqNumber here?
+ addSeqNumber(finPacket);
mFinPackets.add(new FinAckPair(finPacket));
}
}
}
+ /**
+ * <p>
+ * Determines if the TCP packet contained in {@code packet} is a retransmission of a previously seen (logged)
+ * packet.
+ * </p>
+ *
+ * <b>
+ * TODO:
+ * the current implementation, which uses a set of previously seen sequence numbers, will consider a segment
+ * with a reused sequence number---occurring as a result of sequence number wrap around for a very long-lived
+ * connection---as a retransmission (and may therefore end up discarding it even though it is in fact NOT a
+ * retransmission). Ideas?
+ * </b>
+ *
+ * @param packet The packet.
+ * @return {@code true} if {@code packet} was determined to be a retransmission, {@code false} otherwise.
+ */
+ public boolean isRetransmission(PcapPacket packet) {
+ // Extract sequence number.
+ int seqNo = packet.get(TcpPacket.class).getHeader().getSequenceNumber();
+ switch (getDirection(packet)) {
+ case CLIENT_TO_SERVER:
+ return mSeqNumbersClient.contains(seqNo);
+ case SERVER_TO_CLIENT:
+ return mSeqNumbersSrv.contains(seqNo);
+ default:
+ throw new RuntimeException(String.format("Unexpected value of enum '%s'",
+ Direction.class.getSimpleName()));
+ }
+ }
+
+ /**
+ * Extracts the TCP sequence number from {@code packet} and adds it to the proper set of sequence numbers by
+ * analyzing the direction of the packet.
+ * @param packet A TCP packet (wrapped in a {@code PcapPacket}) that was added to this conversation and whose
+ * sequence number is to be recorded as seen.
+ */
+ private void addSeqNumber(PcapPacket packet) {
+ // Note: below check is redundant if client code is correct as the call to check the precondition should already
+ // have been made by the addXPacket method that invokes this method. As such, the call below may be removed in
+ // favor of speed, but the improvement will be minor, hence the added safety may be worth it.
+ onAddPrecondition(packet);
+ // Extract sequence number.
+ int seqNo = packet.get(TcpPacket.class).getHeader().getSequenceNumber();
+ // Determine direction of packet and add packet's sequence number to corresponding set of sequence numbers.
+ switch (getDirection(packet)) {
+ case CLIENT_TO_SERVER:
+ // Client to server packet.
+ mSeqNumbersClient.add(seqNo);
+ break;
+ case SERVER_TO_CLIENT:
+ // Server to client packet.
+ mSeqNumbersSrv.add(seqNo);
+ break;
+ default:
+ throw new RuntimeException(String.format("Unexpected value of enum '%s'",
+ Direction.class.getSimpleName()));
+ }
+ }
+
+ /**
+ * Determine the direction of {@code packet}.
+ * @param packet The packet whose direction is to be determined.
+ * @return A {@link Direction} indicating the direction of the packet.
+ */
+ private Direction getDirection(PcapPacket packet) {
+ IpV4Packet ipPacket = packet.get(IpV4Packet.class);
+ String ipSrc = ipPacket.getHeader().getSrcAddr().getHostAddress();
+ String ipDst = ipPacket.getHeader().getDstAddr().getHostAddress();
+ // Determine direction of packet.
+ if (ipSrc.equals(mClientIp) && ipDst.equals(mServerIp)) {
+ // Client to server packet.
+ return Direction.CLIENT_TO_SERVER;
+ } else if (ipSrc.equals(mServerIp) && ipDst.equals(mClientIp)) {
+ // Server to client packet.
+ return Direction.SERVER_TO_CLIENT;
+ } else {
+ throw new IllegalArgumentException("getDirection: packet not related to " + getClass().getSimpleName());
+ }
+ }
+
+ /**
+ * Utility enum for expressing the direction of a packet pertaining to this {@code Conversation}.
+ */
+ private enum Direction {
+ CLIENT_TO_SERVER, SERVER_TO_CLIENT
+ }
+
}
\ No newline at end of file
package edu.uci.iotproject;
+import org.pcap4j.core.PacketListener;
+import org.pcap4j.core.PcapPacket;
import org.pcap4j.packet.Packet;
import org.pcap4j.packet.DnsPacket;
import org.pcap4j.packet.DnsResourceRecord;
import org.pcap4j.packet.namednumber.DnsResourceRecordType;
-import java.io.EOFException;
+
import java.net.Inet4Address;
import java.net.UnknownHostException;
-import java.time.Instant;
import java.util.*;
-import java.util.concurrent.TimeoutException;
+
/**
* This is a class that does DNS mapping.
* @author Rahmadi Trimananda (rtrimana@uci.edu)
* @version 0.1
*/
-public class DnsMap {
+public class DnsMap implements PacketListener {
/* Class properties */
private Map<String, Set<String>> ipToHostnameMap;
/* Constructor */
public DnsMap() {
+ ipToHostnameMap = new HashMap<>();
+ }
- ipToHostnameMap = new HashMap<String, Set<String>>();
+ @Override
+ public void gotPacket(PcapPacket packet) {
+ try {
+ validateAndAddNewEntry(packet);
+ } catch (UnknownHostException e) {
+ e.printStackTrace();
+ }
}
-
/**
* Gets a packet and determine if this is a DNS packet
*
* @return DnsPacket object or null
*/
private DnsPacket getDnsPacket(Packet packet) {
-
DnsPacket dnsPacket = packet.get(DnsPacket.class);
return dnsPacket;
}
-
/**
* Checks DNS packet and build the map data structure that
* maps IP addresses to DNS hostnames
*
- * @param packet Packet object
+ * @param packet PcapPacket object
*/
- public void validateAndAddNewEntry(Packet packet) throws UnknownHostException {
-
+ public void validateAndAddNewEntry(PcapPacket packet) throws UnknownHostException {
// Make sure that this is a DNS packet
DnsPacket dnsPacket = getDnsPacket(packet);
if (dnsPacket != null) {
-
// We only care about DNS answers
if (dnsPacket.getHeader().getAnswers().size() != 0) {
-
String hostname = dnsPacket.getHeader().getQuestions().get(0).getQName().getName();
for(DnsResourceRecord answer : dnsPacket.getHeader().getAnswers()) {
// We only care about type A records
byte[] ipBytes = answer.getRData().getRawData();
// Convert to string representation.
String ip = Inet4Address.getByAddress(ipBytes).getHostAddress();
- Set<String> hostnameSet = new HashSet<String>();
+ Set<String> hostnameSet = new HashSet<>();
hostnameSet.add(hostname);
// Update or insert depending on presence of key:
// Concat the existing set and the new set if ip already present as key,
* @param hostname Hostname to check
*/
public boolean isRelatedToCloudServer(String address, String hostname) {
-
return ipToHostnameMap.getOrDefault(address, EMPTY_SET).contains(hostname);
}
}
* <pre>
* public FinAckPair(PcapPacket finPacket, PcapPacket correspondingAckPacket) {
* mFinPacket = finPacket;
- * // Below line is considered back practice as the object has not been fully initialized at this stage.
+ * // Below line is considered bad practice as the object has not been fully initialized at this stage.
* if (!this.isCorrespondingAckPacket(correspondingAckPacket)) {
* // ... throw exception
* }
package edu.uci.iotproject;
-import edu.uci.iotproject.maclayer.MacLayerFlowPattern;
-import edu.uci.iotproject.maclayer.MacLayerFlowPatternFinder;
+import edu.uci.iotproject.analysis.PcapPacketPair;
+import edu.uci.iotproject.analysis.TcpConversationUtils;
+import edu.uci.iotproject.analysis.TriggerTrafficExtractor;
+import edu.uci.iotproject.io.TriggerTimesFileReader;
import org.pcap4j.core.*;
+import org.pcap4j.packet.IpV4Packet;
+import org.pcap4j.packet.namednumber.DataLinkType;
import java.io.EOFException;
import java.net.UnknownHostException;
-import java.util.*;
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
import java.util.concurrent.TimeoutException;
/**
// MacLayerFlowPatternFinder finder = new MacLayerFlowPatternFinder(handle, pattern);
// finder.findFlowPattern();
// -------------------------------------------------------------------------------------------------------------
-
- //final String fileName = args.length > 0 ? args[0] : "/home/rtrimana/pcap_processing/smart_home_traffic/Code/Projects/SmartPlugDetector/pcap/wlan1.local.dns.pcap";
- final String fileName = args.length > 0 ? args[0] : "/scratch/June-2018/TPLink/wlan1/tplink.wlan1.local.pcap";
- //final String fileName = args.length > 0 ? args[0] : "/scratch/June-2018/DLink/wlan1/dlink.wlan1.local.pcap";
- final String trainingFileName = "./pcap/TP_LINK_LOCAL_ON_SUBSET.pcap";
-// final String trainingFileName = "./pcap/TP_LINK_LOCAL_ON.pcap";
//
-// // ====== Debug code ======
- PcapHandle handle;
- PcapHandle trainingPcap;
- try {
- handle = Pcaps.openOffline(fileName, PcapHandle.TimestampPrecision.NANO);
- trainingPcap = Pcaps.openOffline(trainingFileName, PcapHandle.TimestampPrecision.NANO);
- } catch (PcapNativeException pne) {
- handle = Pcaps.openOffline(fileName);
- trainingPcap = Pcaps.openOffline(trainingFileName);
+// //final String fileName = args.length > 0 ? args[0] : "/home/rtrimana/pcap_processing/smart_home_traffic/Code/Projects/SmartPlugDetector/pcap/wlan1.local.dns.pcap";
+// final String fileName = args.length > 0 ? args[0] : "/scratch/June-2018/TPLink/wlan1/tplink.wlan1.local.pcap";
+// //final String fileName = args.length > 0 ? args[0] : "/scratch/June-2018/DLink/wlan1/dlink.wlan1.local.pcap";
+// final String trainingFileName = "./pcap/TP_LINK_LOCAL_ON_SUBSET.pcap";
+//// final String trainingFileName = "./pcap/TP_LINK_LOCAL_ON.pcap";
+////
+//// // ====== Debug code ======
+// PcapHandle handle;
+// PcapHandle trainingPcap;
+// try {
+// handle = Pcaps.openOffline(fileName, PcapHandle.TimestampPrecision.NANO);
+// trainingPcap = Pcaps.openOffline(trainingFileName, PcapHandle.TimestampPrecision.NANO);
+// } catch (PcapNativeException pne) {
+// handle = Pcaps.openOffline(fileName);
+// trainingPcap = Pcaps.openOffline(trainingFileName);
+// }
+////
+//// // TODO: The followings are the way to extract multiple hostnames and their associated packet lengths lists
+//// //List<String> list = new ArrayList<>();
+//// //list.add("events.tplinkra.com");
+//// //FlowPattern fp = new FlowPattern("TP_LINK_LOCAL_ON", list, trainingPcap);
+//// //List<String> list2 = new ArrayList<>();
+//// //list2.add("devs.tplinkcloud.com");
+//// //list2.add("events.tplinkra.com");
+//// //FlowPattern fp3 = new FlowPattern("TP_LINK_REMOTE_ON", list2, trainingPcap);
+////
+// FlowPattern fp = new FlowPattern("TP_LINK_LOCAL_ON", "events.tplinkra.com", trainingPcap);
+// //FlowPattern fp = new FlowPattern("DLINK_LOCAL_ON", "rfe-us-west-1.dch.dlink.com", trainingPcap);
+// FlowPatternFinder fpf = new FlowPatternFinder(handle, fp);
+// fpf.start();
+////
+//// // ========================
+
+ /*
+ PcapReader pcapReader = new PcapReader(args[0]);
+ PcapProcessingPipeline pipeline = new PcapProcessingPipeline(pcapReader);
+ TcpReassembler tcpReassembler = new TcpReassembler();
+ pipeline.addPcapPacketConsumer(tcpReassembler);
+ pipeline.executePipeline();
+ System.out.println("Pipeline terminated");
+
+ List<List<PcapPacketPair>> pairs = new ArrayList<>();
+ for (Conversation c : tcpReassembler.getTcpConversations()) {
+ pairs.add(TcpConversationUtils.extractPacketPairs(c));
}
-//
-// // TODO: The followings are the way to extract multiple hostnames and their associated packet lengths lists
-// //List<String> list = new ArrayList<>();
-// //list.add("events.tplinkra.com");
-// //FlowPattern fp = new FlowPattern("TP_LINK_LOCAL_ON", list, trainingPcap);
-// //List<String> list2 = new ArrayList<>();
-// //list2.add("devs.tplinkcloud.com");
-// //list2.add("events.tplinkra.com");
-// //FlowPattern fp3 = new FlowPattern("TP_LINK_REMOTE_ON", list2, trainingPcap);
-//
- FlowPattern fp = new FlowPattern("TP_LINK_LOCAL_ON", "events.tplinkra.com", trainingPcap);
- //FlowPattern fp = new FlowPattern("DLINK_LOCAL_ON", "rfe-us-west-1.dch.dlink.com", trainingPcap);
- FlowPatternFinder fpf = new FlowPatternFinder(handle, fp);
- fpf.start();
-//
-// // ========================
+ */
+
+ /*
+ // -------- 07-17-2018 --------
+ // Only consider packets to/from the TP-Link plug.
+ PcapReader pcapReader = new PcapReader(args[0], "ip host 192.168.1.159");
+ TcpReassembler tcpReassembler = new TcpReassembler();
+ PcapPacket packet;
+ while((packet = pcapReader.readNextPacket()) != null) {
+ tcpReassembler.consumePacket(packet);
+ }
+ // Now we have a set of reassembled TCP conversations.
+ List<Conversation> conversations = tcpReassembler.getTcpConversations();
+ for(Conversation c : conversations) {
+ List<PcapPacketPair> pairs = TcpConversationUtils.extractPacketPairs(c);
+ for (PcapPacketPair pair : pairs) {
+ // TODO ...
+ // 1. discard packets that are not within X seconds after trigger time
+ // 2. conversations may be (are) with different servers - so need to plot in different plots, one per hostname?
+ }
+ }
+
+ // ----------------------------
+ */
+
+ // -------- 07-19-2018 --------
+ TriggerTimesFileReader ttfr = new TriggerTimesFileReader();
+ List<Instant> triggerTimes = ttfr.readTriggerTimes("/Users/varmarken/Downloads/tplink-feb-13-2018.timestamps", false);
+// triggerTimes.stream().forEach(i -> System.out.println(i.atZone(TriggerTimesFileReader.ZONE_ID_LOS_ANGELES).toString()));
+ String pcapFile = "/Users/varmarken/Development/Repositories/UCI/NetworkingGroup/smart_home_traffic/Code/Projects/SmartPlugDetector/pcap/wlan1.local.dns.pcap";
+ String tpLinkPlugIp = "192.168.1.159";
+ TriggerTrafficExtractor tte = new TriggerTrafficExtractor(pcapFile, triggerTimes, tpLinkPlugIp);
+ final PcapDumper outputter = Pcaps.openDead(DataLinkType.EN10MB, 65536).dumpOpen("/Users/varmarken/temp/traces/output/tplink-filtered.pcap");
+ DnsMap dnsMap = new DnsMap();
+ TcpReassembler tcpReassembler = new TcpReassembler();
+ tte.performExtraction(pkt -> {
+ try {
+ outputter.dump(pkt);
+ } catch (NotOpenException e) {
+ e.printStackTrace();
+ }
+ }, dnsMap, tcpReassembler);
+ outputter.flush();
+ outputter.close();
+
+ int packets = 0;
+ for (Conversation c : tcpReassembler.getTcpConversations()) {
+ packets += c.getPackets().size();
+ packets += c.getSynPackets().size();
+ // only count the FIN packets, not the ACKs; every FinAckPair holds a FIN packet
+ packets += c.getFinAckPairs().size();
+ }
+ // Produces 271 packets for the Feb 13 experiment
+ // Applying filter: "(tcp and not tcp.len == 0 and not tcp.analysis.retransmission and not tcp.analysis.fast_retransmission) or (tcp.flags.syn == 1) or (tcp.flags.fin == 1)"
+ // to the file gives 295 packets, but there are 24 TCP-Out-Of-Order SYN/SYNACKs which are filtered as retransmissions in Conversation, so the numbers seem to match.
+ System.out.println("number of packets: " + packets);
+
+ List<List<PcapPacketPair>> pairs = new ArrayList<>();
+ for (Conversation c : tcpReassembler.getTcpConversations()) {
+ pairs.add(TcpConversationUtils.extractPacketPairs(c));
+ }
+ // Sort pairs according to timestamp of first packet of conversation for (debugging) convenience.
+ Collections.sort(pairs, (l1, l2) -> {
+ if (l1.get(0).getFirst().getTimestamp().isBefore(l2.get(0).getFirst().getTimestamp())) return -1;
+ else if (l2.get(0).getFirst().getTimestamp().isBefore(l1.get(0).getFirst().getTimestamp())) return 1;
+ else return 0;
+ });
+ System.out.println("list of pairs produced");
+ List<PcapPacketPair> eventstplinkraPairs = new ArrayList<>();
+ List<List<PcapPacketPair>> otherPairs = new ArrayList<>();
+ String hostname = "events.tplinkra.com";
+ for (List<PcapPacketPair> lppp : pairs) {
+ IpV4Packet ipPacket = lppp.get(0).getFirst().get(IpV4Packet.class);
+ // If packets are associated with the hostname
+ if (dnsMap.isRelatedToCloudServer(ipPacket.getHeader().getSrcAddr().getHostAddress(), hostname) ||
+ dnsMap.isRelatedToCloudServer(ipPacket.getHeader().getDstAddr().getHostAddress(), hostname)) {
+ eventstplinkraPairs.addAll(lppp);
+ } else {
+ // Pairs associated with different server
+ otherPairs.add(lppp);
+ }
+ }
+ HashMap<String, Integer> pairCount = new HashMap<>();
+ for (PcapPacketPair ppp : eventstplinkraPairs) {
+ if (pairCount.containsKey(ppp.toString())) {
+ pairCount.put(ppp.toString(), pairCount.get(ppp.toString()) + 1);
+ } else {
+ pairCount.put(ppp.toString(), 1);
+ }
+ }
+ System.out.println("pairCount map built");
+ // ----------------------------
}
+
}
+
+
+// TP-Link MAC 50:c7:bf:33:1f:09 and usually IP 192.168.1.159 (remember to verify per file)
\ No newline at end of file
--- /dev/null
+package edu.uci.iotproject;
+
+import org.pcap4j.core.PacketListener;
+import org.pcap4j.core.PcapPacket;
+import org.pcap4j.packet.TcpPacket;
+
+import java.util.*;
+
+/**
+ * Reassembles TCP conversations (streams).
+ *
+ * @author Janus Varmarken {@literal <jvarmark@uci.edu>}
+ * @author Rahmadi Trimananda {@literal <rtrimana@uci.edu>}
+ */
+public class TcpReassembler implements PacketListener {
+
+ /**
+ * Holds <em>open</em> {@link Conversation}s, i.e., {@code Conversation}s that have <em>not</em> been detected as
+ * (gracefully) terminated based on the set of packets observed thus far.
+ * A {@link Conversation} is moved to {@link #mTerminatedConversations} if it can be determined that it is has
+ * terminated. Termination can be detected by a) observing two {@link FinAckPair}s, one in each direction, (graceful
+ * termination, see {@link Conversation#isGracefullyShutdown()}) or b) by observing a SYN packet that matches the
+ * four tuple of an existing {@code Conversation}, but which holds a <em>different</em> sequence number than the
+ * same-direction SYN packet recorded for the {@code Conversation}.
+ * <p>
+ * Note that due to limitations of the {@link Set} interface (specifically, there is no {@code get(T t)} method),
+ * we have to resort to a {@link Map} (in which keys map to themselves) to "mimic" a set with {@code get(T t)}
+ * functionality.
+ *
+ * @see <a href="https://stackoverflow.com/questions/7283338/getting-an-element-from-a-set">this question on StackOverflow.com</a>
+ */
+ private final Map<Conversation, Conversation> mOpenConversations = new HashMap<>();
+
+ /**
+ * Holds <em>terminated</em> {@link Conversation}s.
+ * TODO: Should turn this into a list to avoid unintentional overwrite of a Conversation in case ephemeral port number is reused at a later stage...?
+ */
+ private final Map<Conversation, Conversation> mTerminatedConversations = new HashMap<>();
+
+ @Override
+ public void gotPacket(PcapPacket pcapPacket) {
+ TcpPacket tcpPacket = pcapPacket.get(TcpPacket.class);
+ if (tcpPacket == null) {
+ return;
+ }
+ // ... TODO?
+ processPacket(pcapPacket);
+ }
+
+ /**
+ * Get the reassembled TCP connections. Note that if this is called while packets are still being processed (by
+ * calls to {@link #gotPacket(PcapPacket)}), the behavior is undefined and the returned list may be inconsistent.
+ * @return The reassembled TCP connections.
+ */
+ public List<Conversation> getTcpConversations() {
+ ArrayList<Conversation> combined = new ArrayList<>();
+ combined.addAll(mTerminatedConversations.values());
+ combined.addAll(mOpenConversations.values());
+ return combined;
+ }
+
+ private void processPacket(PcapPacket pcapPacket) {
+ TcpPacket tcpPacket = pcapPacket.get(TcpPacket.class);
+ // Handle client connection initiation attempts.
+ if (tcpPacket.getHeader().getSyn() && !tcpPacket.getHeader().getAck()) {
+ // A segment with the SYN flag set, but no ACK flag indicates that a client is attempting to initiate a new
+ // connection.
+ processNewConnectionRequest(pcapPacket);
+ return;
+ }
+ // Handle server connection initiation acknowledgement
+ if (tcpPacket.getHeader().getSyn() && tcpPacket.getHeader().getAck()) {
+ // A segment with both the SYN and ACK flags set indicates that the server has accepted the client's request
+ // to initiate a new connection.
+ processNewConnectionAck(pcapPacket);
+ return;
+ }
+ // Handle resets
+ if (tcpPacket.getHeader().getRst()) {
+ processRstPacket(pcapPacket);
+ return;
+ }
+ // Handle FINs
+ if (tcpPacket.getHeader().getFin()) {
+ // Handle FIN packet.
+ processFinPacket(pcapPacket);
+ }
+ // Handle ACKs (currently only ACKs of FINS)
+ if (tcpPacket.getHeader().getAck()) {
+ processAck(pcapPacket);
+ }
+ // Handle packets that carry payload (application data).
+ if (tcpPacket.getPayload() != null) {
+ processPayloadPacket(pcapPacket);
+ }
+ }
+
+ private void processNewConnectionRequest(PcapPacket clientSynPacket) {
+ // A SYN w/o ACK always originates from the client.
+ Conversation conv = Conversation.fromPcapPacket(clientSynPacket, true);
+ conv.addSynPacket(clientSynPacket);
+ // Is there an ongoing conversation for the same four tuple (clientIp, clientPort, serverIp, serverPort) as
+ // found in the new SYN packet?
+ Conversation ongoingConv = mOpenConversations.get(conv);
+ if (ongoingConv != null) {
+ if (ongoingConv.isRetransmission(clientSynPacket)) {
+ // SYN retransmission detected, do nothing.
+ return;
+ // TODO: the way retransmission detection is implemented may cause a bug for connections where we have
+ // not recorded the initial SYN, but only the SYN ACK, as retransmission is determined by comparing the
+ // sequence numbers of initial SYNs -- and if no initial SYN is present for the Conversation, the new
+ // SYN will be interpreted as a retransmission. Possible fix: let isRentransmission ALWAYS return false
+ // when presented with a SYN packet when the Conversation already holds a SYN ACK packet?
+ } else {
+ // New SYN has different sequence number than SYN recorded for ongoingConv, so this must be an attempt
+ // to establish a new conversation with the same four tuple as ongoingConv.
+ // Mark existing connection as terminated.
+ // TODO: is this 100% theoretically correct, e.g., if many connection attempts are made back to back? And RST packets?
+ mTerminatedConversations.put(ongoingConv, ongoingConv);
+ mOpenConversations.remove(ongoingConv);
+ }
+ }
+ // Finally, update the map of open connections with the new connection.
+ mOpenConversations.put(conv, conv);
+ }
+
+
+ /*
+ * TODO a problem across the board for all processXPacket methods below:
+ * if we start the capture in the middle of a TCP connection, we will not have an entry for the conversation in the
+ * map as we have not seen the initial SYN packet.
+ * Two ways we can address this:
+ * a) Perform null-checks and ignore packets for which we have not seen SYN
+ * + easy to get correct
+ * - we discard data (issue for long-lived connections!)
+ * b) Add a corresponding conversation entry whenever we encounter a packet that does not map to a conversation
+ * + we consider all data
+ * - not immediately clear if this will introduce bugs (incorrectly mapping packets to wrong conversations?)
+ *
+ * [[[ I went with option b) for now; see getOngoingConversationOrCreateNew(PcapPacket pcapPacket). ]]]
+ */
+
+ private void processNewConnectionAck(PcapPacket srvSynPacket) {
+ // Find the corresponding ongoing connection, if any (if we start the capture just *after* the initial SYN, no
+ // ongoing conversation entry will exist, so it must be created in that case).
+// Conversation conv = mOpenConversations.get(Conversation.fromPcapPacket(srvSynPacket, false));
+ Conversation conv = getOngoingConversationOrCreateNew(srvSynPacket);
+ // Note: exploits &&'s short-circuit operation: only attempts to add non-retransmissions.
+ if (!conv.isRetransmission(srvSynPacket) && !conv.addSynPacket(srvSynPacket)) {
+ // For safety/debugging: if NOT a retransmission and add fails,
+ // something has gone terribly wrong/invariant is broken.
+ throw new IllegalStateException("Attempt to add SYN ACK packet that was NOT a retransmission failed." +
+ Conversation.class.getSimpleName() + " invariant broken.");
+ }
+ }
+
+ private void processRstPacket(PcapPacket rstPacket) {
+ Conversation conv = getOngoingConversationOrCreateNew(rstPacket);
+ // Move conversation to set of terminated conversations.
+ mTerminatedConversations.put(conv, conv);
+ mOpenConversations.remove(conv, conv);
+ }
+
+ private void processFinPacket(PcapPacket finPacket) {
+// getOngoingConversationForPacket(finPacket).addFinPacket(finPacket);
+ getOngoingConversationOrCreateNew(finPacket).addFinPacket(finPacket);
+ }
+
+ private void processAck(PcapPacket ackPacket) {
+// getOngoingConversationForPacket(ackPacket).attemptAcknowledgementOfFin(ackPacket);
+ // Note that unlike the style for SYN, FIN, and payload packets, for "ACK only" packets, we want to avoid
+ // creating a new conversation.
+ Conversation conv = getOngoingConversationForPacket(ackPacket);
+ if (conv != null) {
+ // The ACK may be an ACK of a FIN, so attempt to mark the FIN as ack'ed.
+ conv.attemptAcknowledgementOfFin(ackPacket);
+ if (conv.isGracefullyShutdown()) {
+ // Move conversation to set of terminated conversations.
+ mTerminatedConversations.put(conv, conv);
+ mOpenConversations.remove(conv);
+ }
+ }
+ // Note: add (additional) processing of ACKs (that are not ACKs of FINs) as necessary here...
+ }
+
+ private void processPayloadPacket(PcapPacket pcapPacket) {
+// getOngoingConversationForPacket(pcapPacket).addPacket(pcapPacket, true);
+ getOngoingConversationOrCreateNew(pcapPacket).addPacket(pcapPacket, true);
+ }
+
+ /**
+ * Locates an ongoing conversation (if any) that {@code pcapPacket} pertains to.
+ * @param pcapPacket The packet that is to be mapped to an ongoing {@code Conversation}.
+ * @return The {@code Conversation} matching {@code pcapPacket} or {@code null} if there is no match.
+ */
+ private Conversation getOngoingConversationForPacket(PcapPacket pcapPacket) {
+ // We cannot know if this is a client-to-server or server-to-client packet without trying both options...
+ Conversation conv = mOpenConversations.get(Conversation.fromPcapPacket(pcapPacket, true));
+ if (conv == null) {
+ conv = mOpenConversations.get(Conversation.fromPcapPacket(pcapPacket, false));
+ }
+ return conv;
+ }
+
+ /**
+ * Like {@link #getOngoingConversationForPacket(PcapPacket)}, but creates and inserts a new {@code Conversation}
+ * into {@link #mOpenConversations} if no open conversation is found (i.e., in the case that
+ * {@link #getOngoingConversationForPacket(PcapPacket)} returns {@code null}).
+ *
+ * @param pcapPacket The packet that is to be mapped to an ongoing {@code Conversation}.
+ * @return The existing, ongoing {@code Conversation} matching {@code pcapPacket} or the newly created one in case
+ * no match was found.
+ */
+ private Conversation getOngoingConversationOrCreateNew(PcapPacket pcapPacket) {
+ Conversation conv = getOngoingConversationForPacket(pcapPacket);
+ if (conv == null) {
+ TcpPacket tcpPacket = pcapPacket.get(TcpPacket.class);
+ if (tcpPacket.getHeader().getSyn() && tcpPacket.getHeader().getAck()) {
+ // A SYN ACK packet always originates from the server (it is a reply to the initial SYN packet from the client)
+ conv = Conversation.fromPcapPacket(pcapPacket, false);
+ } else {
+ // TODO: can we do anything else but arbitrarily select who is designated as the server in this case?
+ conv = Conversation.fromPcapPacket(pcapPacket, false);
+ }
+ mOpenConversations.put(conv, conv);
+ }
+ return conv;
+ }
+}
--- /dev/null
+package edu.uci.iotproject.analysis;
+
+import org.pcap4j.core.PcapPacket;
+
+/**
+ * TODO add class documentation.
+ *
+ * @author Janus Varmarken
+ */
+public interface PcapPacketFilter {
+
+ boolean shouldIncludePacket(PcapPacket packet);
+
+}
--- /dev/null
+package edu.uci.iotproject.analysis;
+
+import org.pcap4j.core.PcapPacket;
+
+/**
+ * TODO add class documentation.
+ *
+ * @author Janus Varmarken
+ */
+public class PcapPacketPair {
+
+ private final PcapPacket mFirst;
+
+ private final PcapPacket mSecond;
+
+ public PcapPacketPair(PcapPacket first, PcapPacket second) {
+ mFirst = first;
+ mSecond = second;
+ }
+
+ public PcapPacket getFirst() { return mFirst; }
+
+ public PcapPacket getSecond() { return mSecond; }
+
+ @Override
+ public String toString() {
+ return getFirst().length() + ", " + (getSecond() == null ? "null" : getSecond().length());
+ }
+}
--- /dev/null
+package edu.uci.iotproject.analysis;
+
+import edu.uci.iotproject.Conversation;
+import edu.uci.iotproject.util.PcapPacketUtils;
+import org.pcap4j.core.PcapPacket;
+import org.pcap4j.packet.IpV4Packet;
+import org.pcap4j.packet.TcpPacket;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * TODO add class documentation.
+ *
+ * @author Janus Varmarken
+ */
+public class TcpConversationUtils {
+
+ public static List<PcapPacketPair> extractPacketPairs(Conversation conv) {
+ List<PcapPacket> packets = conv.getPackets();
+ List<PcapPacketPair> pairs = new ArrayList<>();
+ int i = 0;
+ while (i < packets.size()) {
+ PcapPacket p1 = packets.get(i);
+ String p1SrcIp = p1.get(IpV4Packet.class).getHeader().getSrcAddr().getHostAddress();
+ int p1SrcPort = p1.get(TcpPacket.class).getHeader().getSrcPort().valueAsInt();
+ if (i+1 < packets.size()) {
+ PcapPacket p2 = packets.get(i+1);
+ if (PcapPacketUtils.isSource(p2, p1SrcIp, p1SrcPort)) {
+ // Two packets in a row going in the same direction -> create one item pair for p1
+ pairs.add(new PcapPacketPair(p1, null));
+ // Advance one packet as the following two packets may form a valid two-item pair.
+ i++;
+ } else {
+ // The two packets form a response-reply pair, create two-item pair.
+ pairs.add(new PcapPacketPair(p1, p2));
+ // Advance two packets as we have already processed the packet at index i+1 in order to create the pair.
+ i += 2;
+ }
+ } else {
+ // Last packet of conversation => one item pair
+ pairs.add(new PcapPacketPair(p1, null));
+ // Advance i to ensure termination.
+ i++;
+ }
+ }
+ return pairs;
+ // TODO: what if there is long time between response and reply packet? Should we add a threshold and exclude those cases?
+ }
+
+}
--- /dev/null
+package edu.uci.iotproject.analysis;
+
+import edu.uci.iotproject.io.PcapHandleReader;
+import org.pcap4j.core.*;
+
+import java.time.Instant;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.TimeoutException;
+
+/**
+ * TODO add class documentation.
+ *
+ * @author Janus Varmarken
+ */
+public class TriggerTrafficExtractor implements PcapPacketFilter {
+
+ private final String mPcapFilePath;
+ private final List<Instant> mTriggerTimes;
+ private final String mDeviceIp;
+
+ private int mTriggerIndex = 0;
+
+
+ private static final int INCLUSION_WINDOW_MILLIS = 3_000;
+
+ public TriggerTrafficExtractor(String pcapFilePath, List<Instant> triggerTimes, String deviceIp) throws PcapNativeException, NotOpenException {
+ mPcapFilePath = pcapFilePath;
+ // Ensure that trigger times are sorted in ascending as we rely on this fact in the logic that works out if a
+ // packet is related to a trigger.
+ Collections.sort(triggerTimes, (i1, i2) -> {
+ if (i1.isBefore(i2)) return -1;
+ else if (i2.isBefore(i1)) return 1;
+ else return 0;
+ });
+ mTriggerTimes = Collections.unmodifiableList(triggerTimes);
+ mDeviceIp = deviceIp;
+ }
+
+
+ public void performExtraction(PacketListener... extractedPacketsConsumers) throws PcapNativeException, NotOpenException, TimeoutException {
+ PcapHandle handle;
+ try {
+ handle = Pcaps.openOffline(mPcapFilePath, PcapHandle.TimestampPrecision.NANO);
+ } catch (PcapNativeException pne) {
+ handle = Pcaps.openOffline(mPcapFilePath);
+ }
+ // Use the native support for BPF to immediately filter irrelevant traffic.
+ handle.setFilter("ip host " + mDeviceIp, BpfProgram.BpfCompileMode.OPTIMIZE);
+ PcapHandleReader pcapReader = new PcapHandleReader(handle, this, extractedPacketsConsumers);
+ pcapReader.readFromHandle();
+ // Reset trigger index (in case client code chooses to rerun the extraction)
+ mTriggerIndex = 0;
+ }
+
+ @Override
+ public boolean shouldIncludePacket(PcapPacket packet) {
+ if (mTriggerIndex >= mTriggerTimes.size()) {
+ // Don't include packet if we've exhausted the list of trigger times.
+ return false;
+ }
+
+ // TODO hmm, is this correct?
+ Instant trigger = mTriggerTimes.get(mTriggerIndex);
+ if (trigger.isBefore(packet.getTimestamp()) &&
+ packet.getTimestamp().isBefore(trigger.plusMillis(INCLUSION_WINDOW_MILLIS))) {
+ // Packet lies within INCLUSION_WINDOW_MILLIS after currently considered trigger, include it.
+ return true;
+ } else {
+ if (!trigger.isBefore(packet.getTimestamp())) {
+ // Packet is before currently considered trigger, so it shouldn't be included
+ return false;
+ } else {
+ // Packet is >= INCLUSION_WINDOW_MILLIS after currently considered trigger.
+ // Proceed to next trigger to see if it lies in range of that.
+ // Note that there's an assumption here that no two trigger intervals don't overlap!
+ mTriggerIndex++;
+ return shouldIncludePacket(packet);
+ }
+ }
+ }
+
+}
--- /dev/null
+package edu.uci.iotproject.io;
+
+import edu.uci.iotproject.analysis.PcapPacketFilter;
+import org.pcap4j.core.*;
+
+import java.io.EOFException;
+import java.util.concurrent.TimeoutException;
+
+/**
+ * Reads packets from a {@link PcapHandle} (online or offline) and delivers those packets that pass the test exercised
+ * by the provided {@link PcapPacketFilter} onto the provided {@link PacketListener}s.
+ *
+ * @author Janus Varmarken {@literal <jvarmark@uci.edu>}
+ * @author Rahmadi Trimananda {@literal <rtrimana@uci.edu>}
+ */
+public class PcapHandleReader {
+
+ private final PcapPacketFilter mPacketFilter;
+ private final PcapHandle mHandle;
+ private final PacketListener[] mPacketListeners;
+
+ /**
+ * Create a {@code PcapHandleReader}.
+ * @param handle An <em>open</em> {@link PcapHandle} that packets will be read from.
+ * @param packetFilter A {@link PcapPacketFilter} that dictates which of the packets read from {@code handle} should
+ * be delivered to {@code packetListeners}. Note that while a value of {@code null} is not
+ * permitted here, the caller can instead simply provide an implementation that always returns
+ * {@code true} if they want to include all packets read from {@code handle}.
+ * @param packetListeners One or more {@link PacketListener}s to which those packets read from {@code handle} that
+ * pass through {@code packetFilter} are delivered.
+ */
+ public PcapHandleReader(PcapHandle handle, PcapPacketFilter packetFilter, PacketListener... packetListeners) {
+ mHandle = handle;
+ mPacketFilter = packetFilter;
+ mPacketListeners = packetListeners;
+ }
+
+
+ /**
+ * Start reading (and filtering) packets from the provided {@link PcapHandle}.
+ * @throws PcapNativeException if an error occurs in the pcap native library.
+ * @throws NotOpenException if the provided {@code PcapHandle} is not open.
+ * @throws TimeoutException if packets are being read from a live capture and the timeout expired.
+ */
+ public void readFromHandle() throws PcapNativeException, NotOpenException, TimeoutException {
+ try {
+ PcapPacket prevPacket = null;
+ PcapPacket packet;
+ while ((packet = mHandle.getNextPacketEx()) != null) {
+ if (prevPacket != null && packet.getTimestamp().isBefore(prevPacket.getTimestamp())) {
+ System.out.println("Out-of-order (in terms of timestamp) packet detected");
+ /*
+ // Fail early if assumption doesn't hold.
+ mHandle.close();
+ throw new AssertionError("Packets not in ascending temporal order");
+ */
+ }
+ if (mPacketFilter.shouldIncludePacket(packet)) {
+ // Packet accepted for inclusion; deliver it to observing client code.
+ for (PacketListener consumer : mPacketListeners) {
+ consumer.gotPacket(packet);
+ }
+ }
+ prevPacket = packet;
+ }
+ } catch (EOFException eof) {
+ // Reached end of file. All good.
+ System.out.println(String.format("%s: finished reading pcap file", getClass().getSimpleName()));
+ }
+ mHandle.close();
+ }
+
+}
--- /dev/null
+package edu.uci.iotproject.io;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileReader;
+import java.io.IOException;
+import java.time.Instant;
+import java.time.LocalDateTime;
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.*;
+
+/**
+ * Parses a file to obtain the timestamps at which the smart plug was toggled on/off.
+ *
+ * @author Rahmadi Trimananda {@literal <rtrimana@uci.edu>}
+ * @author Janus Varmarken {@literal <jvarmark@uci.edu>}
+ */
+public class TriggerTimesFileReader {
+
+ public static final ZoneId ZONE_ID_LOS_ANGELES = ZoneId.of("America/Los_Angeles");
+ public static final ZoneId ZONE_ID_BUDAPEST = ZoneId.of("Europe/Budapest");
+
+ /**
+ * Reads a file with trigger timestamps and parses the timestamps into {@link Instant}s using the rules specified
+ * by {@link #parseTriggerTimestamp(String, boolean)}.
+ * @param fileName The absolute path to the file with trigger timestamps.
+ * @param _24hFormat {@code true} if the timestamps in the file are in 24 hour format, {@code false} if they are in
+ * AM/PM format.
+ * @return A containing the trigger timestamps represented as {@code Instant}s.
+ */
+ public List<Instant> readTriggerTimes(String fileName, boolean _24hFormat) {
+ List<Instant> listTriggerTimes = new ArrayList<>();
+ try {
+ File file = new File(fileName);
+ BufferedReader br = new BufferedReader(new FileReader(file));
+ String s;
+ while ((s = br.readLine()) != null) {
+ listTriggerTimes.add(parseTriggerTimestamp(s, _24hFormat));
+ }
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ System.out.println("List has: " + listTriggerTimes.size());
+ return listTriggerTimes;
+ }
+
+ /**
+ * Parses a timestamp string to an {@link Instant} (UTC). Assumes timestamps are LA time.
+ * Format is expected to be either "uuuu-MM-dd HH:mm:ss" or "uuuu-MM-dd h:mm:ss a".
+ *
+ * @param timestampStr The string containing a date-time timestamp for LA's timezone.
+ * @param _24hFormat {@code true} if the time in {@code timestampStr} is given in 24 hour format, {@code false} if
+ * it is given in AM/PM format.
+ * @return An {@code Instant} representation of the parsed timestamp. Note that the {@code Instant} marks a point on
+ * the timeline in UTC. Use {@link Instant#atZone(ZoneId)} to convert to the corresponding time in a given
+ * timezone.
+ */
+ public Instant parseTriggerTimestamp(String timestampStr, boolean _24hFormat) {
+ // Note: only one 'h' when not prefixed with leading 0 for 1-9; and only one 'a' for AM/PM marker in Java 8 time
+ String format = _24hFormat ? "uuuu-MM-dd HH:mm:ss" : "uuuu-MM-dd h:mm:ss a";
+ LocalDateTime localDateTime = LocalDateTime.parse(timestampStr, DateTimeFormatter.ofPattern(format, Locale.US));
+ ZonedDateTime laZonedDateTime = localDateTime.atZone(ZONE_ID_LOS_ANGELES);
+ return laZonedDateTime.toInstant();
+ }
+
+}
--- /dev/null
+# Borrowed from https://github.com/github/gitignore/blob/master/Global/JetBrains.gitignore
+
+# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm
+# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
+
+# User-specific stuff:
+.idea/**/workspace.xml
+.idea/**/tasks.xml
+.idea/dictionaries
+
+ # JetBrains says not to ignore this one, but it is often inhibitted by a lot of machine specific automatic changes
+.idea/misc.xml
+
+# Sensitive or high-churn files:
+.idea/**/dataSources/
+.idea/**/dataSources.ids
+.idea/**/dataSources.xml
+.idea/**/dataSources.local.xml
+.idea/**/sqlDataSources.xml
+.idea/**/dynamic.xml
+.idea/**/uiDesigner.xml
+
+# Janus: Exclude idea artifacts
+.idea/artifacts
+
+# Gradle: (combination of the JetBrains gitiginre and Gradle gitignore at )
+.idea/**/gradle.xml
+.idea/**/libraries
+.gradle
+/build/
+# Ignore Gradle GUI config
+gradle-app.setting
+# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored)
+!gradle-wrapper.jar
+# Cache of project
+.gradletasknamecache
+# # Work around https://youtrack.jetbrains.com/issue/IDEA-116898
+# gradle/wrapper/gradle-wrapper.properties
+
+# CMake
+cmake-build-debug/
+
+# Mongo Explorer plugin:
+.idea/**/mongoSettings.xml
+
+## File-based project format:
+*.iws
+
+## Plugin-specific files:
+
+# IntelliJ
+out/
+
+# mpeltonen/sbt-idea plugin
+.idea_modules/
+
+# JIRA plugin
+atlassian-ide-plugin.xml
+
+# Cursive Clojure plugin
+.idea/replstate.xml
+
+# Crashlytics plugin (for Android Studio and IntelliJ)
+com_crashlytics_export_strings.xml
+crashlytics.properties
+crashlytics-build.properties
+fabric.properties
+
+#ignore config files as they are user specific and hold login credentials
+src/main/resources/cfg/config.properties
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+ <component name="CompilerConfiguration">
+ <option name="DEFAULT_COMPILER" value="Javac" />
+ <resourceExtensions />
+ <wildcardResourcePatterns>
+ <entry name="!?*.java" />
+ <entry name="!?*.form" />
+ <entry name="!?*.class" />
+ <entry name="!?*.groovy" />
+ <entry name="!?*.scala" />
+ <entry name="!?*.flex" />
+ <entry name="!?*.kt" />
+ <entry name="!?*.clj" />
+ <entry name="!?*.aj" />
+ </wildcardResourcePatterns>
+ <annotationProcessing>
+ <profile default="true" name="Default" enabled="false">
+ <processorPath useClasspath="true" />
+ </profile>
+ </annotationProcessing>
+ <bytecodeTargetLevel>
+ <module name="TplinkPlugClient_main" target="1.8" />
+ <module name="TplinkPlugClient_test" target="1.8" />
+ </bytecodeTargetLevel>
+ </component>
+</project>
\ No newline at end of file
--- /dev/null
+<component name="CopyrightManager">
+ <settings default="" />
+</component>
\ No newline at end of file
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+ <component name="ProjectModuleManager">
+ <modules>
+ <module fileurl="file://$PROJECT_DIR$/.idea/modules/TplinkPlugClient.iml" filepath="$PROJECT_DIR$/.idea/modules/TplinkPlugClient.iml" />
+ <module fileurl="file://$PROJECT_DIR$/.idea/modules/TplinkPlugClient_main.iml" filepath="$PROJECT_DIR$/.idea/modules/TplinkPlugClient_main.iml" group="TplinkPlugClient" />
+ <module fileurl="file://$PROJECT_DIR$/.idea/modules/TplinkPlugClient_test.iml" filepath="$PROJECT_DIR$/.idea/modules/TplinkPlugClient_test.iml" group="TplinkPlugClient" />
+ </modules>
+ </component>
+</project>
\ No newline at end of file
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<module external.linked.project.id="TplinkPlugClient" external.linked.project.path="$MODULE_DIR$/../.." external.root.project.path="$MODULE_DIR$/../.." external.system.id="GRADLE" external.system.module.group="edu.uci.iotproject.tplinkplug" external.system.module.version="1.0-SNAPSHOT" type="JAVA_MODULE" version="4">
+ <component name="NewModuleRootManager" inherit-compiler-output="true">
+ <exclude-output />
+ <content url="file://$MODULE_DIR$/../..">
+ <excludeFolder url="file://$MODULE_DIR$/../../.gradle" />
+ <excludeFolder url="file://$MODULE_DIR$/../../build" />
+ </content>
+ <orderEntry type="inheritedJdk" />
+ <orderEntry type="sourceFolder" forTests="false" />
+ </component>
+</module>
\ No newline at end of file
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<module external.linked.project.id="TplinkPlugClient:main" external.linked.project.path="$MODULE_DIR$/../.." external.root.project.path="$MODULE_DIR$/../.." external.system.id="GRADLE" external.system.module.group="edu.uci.iotproject.tplinkplug" external.system.module.type="sourceSet" external.system.module.version="1.0-SNAPSHOT" type="JAVA_MODULE" version="4">
+ <component name="NewModuleRootManager" LANGUAGE_LEVEL="JDK_1_8" inherit-compiler-output="false">
+ <output url="file://$MODULE_DIR$/../../build/classes/main" />
+ <exclude-output />
+ <content url="file://$MODULE_DIR$/../../src/main">
+ <sourceFolder url="file://$MODULE_DIR$/../../src/main/java" isTestSource="false" />
+ <sourceFolder url="file://$MODULE_DIR$/../../src/main/resources" type="java-resource" />
+ </content>
+ <orderEntry type="inheritedJdk" />
+ <orderEntry type="sourceFolder" forTests="false" />
+ <orderEntry type="library" name="Gradle: com.mashape.unirest:unirest-java:1.4.9" level="project" />
+ <orderEntry type="library" name="Gradle: javax.ws.rs:javax.ws.rs-api:2.1" level="project" />
+ <orderEntry type="library" name="Gradle: org.apache.httpcomponents:httpclient:4.5.2" level="project" />
+ <orderEntry type="library" name="Gradle: org.apache.httpcomponents:httpasyncclient:4.1.1" level="project" />
+ <orderEntry type="library" scope="RUNTIME" name="Gradle: org.glassfish.jersey.core:jersey-client:2.27" level="project" />
+ <orderEntry type="library" name="Gradle: org.apache.httpcomponents:httpmime:4.5.2" level="project" />
+ <orderEntry type="library" scope="RUNTIME" name="Gradle: org.glassfish.jersey.inject:jersey-hk2:2.27" level="project" />
+ <orderEntry type="library" name="Gradle: org.json:json:20160212" level="project" />
+ <orderEntry type="library" name="Gradle: org.apache.httpcomponents:httpcore:4.4.4" level="project" />
+ <orderEntry type="library" name="Gradle: commons-logging:commons-logging:1.2" level="project" />
+ <orderEntry type="library" name="Gradle: commons-codec:commons-codec:1.9" level="project" />
+ <orderEntry type="library" name="Gradle: org.apache.httpcomponents:httpcore-nio:4.4.4" level="project" />
+ <orderEntry type="library" scope="RUNTIME" name="Gradle: org.glassfish.jersey.core:jersey-common:2.27" level="project" />
+ <orderEntry type="library" scope="RUNTIME" name="Gradle: org.glassfish.hk2.external:javax.inject:2.5.0-b42" level="project" />
+ <orderEntry type="library" scope="RUNTIME" name="Gradle: org.glassfish.hk2:hk2-locator:2.5.0-b42" level="project" />
+ <orderEntry type="library" scope="RUNTIME" name="Gradle: javax.annotation:javax.annotation-api:1.2" level="project" />
+ <orderEntry type="library" scope="RUNTIME" name="Gradle: org.glassfish.hk2:osgi-resource-locator:1.0.1" level="project" />
+ <orderEntry type="library" scope="RUNTIME" name="Gradle: org.glassfish.hk2.external:aopalliance-repackaged:2.5.0-b42" level="project" />
+ <orderEntry type="library" scope="RUNTIME" name="Gradle: org.glassfish.hk2:hk2-api:2.5.0-b42" level="project" />
+ <orderEntry type="library" scope="RUNTIME" name="Gradle: org.glassfish.hk2:hk2-utils:2.5.0-b42" level="project" />
+ <orderEntry type="library" scope="RUNTIME" name="Gradle: org.javassist:javassist:3.22.0-CR2" level="project" />
+ <orderEntry type="library" scope="RUNTIME" name="Gradle: javax.inject:javax.inject:1" level="project" />
+ </component>
+</module>
\ No newline at end of file
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<module external.linked.project.id="TplinkPlugClient:test" external.linked.project.path="$MODULE_DIR$/../.." external.root.project.path="$MODULE_DIR$/../.." external.system.id="GRADLE" external.system.module.group="edu.uci.iotproject.tplinkplug" external.system.module.type="sourceSet" external.system.module.version="1.0-SNAPSHOT" type="JAVA_MODULE" version="4">
+ <component name="NewModuleRootManager" LANGUAGE_LEVEL="JDK_1_8" inherit-compiler-output="false">
+ <output-test url="file://$MODULE_DIR$/../../build/classes/test" />
+ <exclude-output />
+ <content url="file://$MODULE_DIR$/../../src/test">
+ <sourceFolder url="file://$MODULE_DIR$/../../src/test/java" isTestSource="true" />
+ <sourceFolder url="file://$MODULE_DIR$/../../src/test/resources" type="java-test-resource" />
+ </content>
+ <orderEntry type="inheritedJdk" />
+ <orderEntry type="sourceFolder" forTests="false" />
+ <orderEntry type="module" module-name="TplinkPlugClient_main" />
+ <orderEntry type="library" name="Gradle: com.mashape.unirest:unirest-java:1.4.9" level="project" />
+ <orderEntry type="library" name="Gradle: javax.ws.rs:javax.ws.rs-api:2.1" level="project" />
+ <orderEntry type="library" name="Gradle: junit:junit:4.11" level="project" />
+ <orderEntry type="library" name="Gradle: org.apache.httpcomponents:httpclient:4.5.2" level="project" />
+ <orderEntry type="library" name="Gradle: org.apache.httpcomponents:httpasyncclient:4.1.1" level="project" />
+ <orderEntry type="library" scope="RUNTIME" name="Gradle: org.glassfish.jersey.core:jersey-client:2.27" level="project" />
+ <orderEntry type="library" name="Gradle: org.apache.httpcomponents:httpmime:4.5.2" level="project" />
+ <orderEntry type="library" scope="RUNTIME" name="Gradle: org.glassfish.jersey.inject:jersey-hk2:2.27" level="project" />
+ <orderEntry type="library" name="Gradle: org.json:json:20160212" level="project" />
+ <orderEntry type="library" name="Gradle: org.hamcrest:hamcrest-core:1.3" level="project" />
+ <orderEntry type="library" name="Gradle: org.apache.httpcomponents:httpcore:4.4.4" level="project" />
+ <orderEntry type="library" name="Gradle: commons-logging:commons-logging:1.2" level="project" />
+ <orderEntry type="library" name="Gradle: commons-codec:commons-codec:1.9" level="project" />
+ <orderEntry type="library" scope="RUNTIME" name="Gradle: org.glassfish.jersey.core:jersey-common:2.27" level="project" />
+ <orderEntry type="library" name="Gradle: org.apache.httpcomponents:httpcore-nio:4.4.4" level="project" />
+ <orderEntry type="library" scope="RUNTIME" name="Gradle: org.glassfish.hk2.external:javax.inject:2.5.0-b42" level="project" />
+ <orderEntry type="library" scope="RUNTIME" name="Gradle: org.glassfish.hk2:hk2-locator:2.5.0-b42" level="project" />
+ <orderEntry type="library" scope="RUNTIME" name="Gradle: javax.annotation:javax.annotation-api:1.2" level="project" />
+ <orderEntry type="library" scope="RUNTIME" name="Gradle: org.glassfish.hk2:osgi-resource-locator:1.0.1" level="project" />
+ <orderEntry type="library" scope="RUNTIME" name="Gradle: org.glassfish.hk2.external:aopalliance-repackaged:2.5.0-b42" level="project" />
+ <orderEntry type="library" scope="RUNTIME" name="Gradle: org.glassfish.hk2:hk2-api:2.5.0-b42" level="project" />
+ <orderEntry type="library" scope="RUNTIME" name="Gradle: org.glassfish.hk2:hk2-utils:2.5.0-b42" level="project" />
+ <orderEntry type="library" scope="RUNTIME" name="Gradle: org.javassist:javassist:3.22.0-CR2" level="project" />
+ <orderEntry type="library" scope="RUNTIME" name="Gradle: javax.inject:javax.inject:1" level="project" />
+ </component>
+ <component name="TestModuleProperties" production-module="TplinkPlugClient_main" />
+</module>
\ No newline at end of file
--- /dev/null
+group 'edu.uci.iotproject.tplinkplug'
+version '1.0-SNAPSHOT'
+
+apply plugin: 'java'
+apply plugin: 'application'
+
+mainClassName = 'edu.uci.iotproject.tplinkplug.Main'
+
+sourceCompatibility = 1.8
+
+repositories {
+ mavenCentral()
+}
+
+dependencies {
+
+ compile group: 'com.mashape.unirest', name: 'unirest-java', version: '1.4.9'
+
+ compile group: 'javax.ws.rs', name: 'javax.ws.rs-api', version: '2.1'
+ runtime group: 'org.glassfish.jersey.core', name: 'jersey-client', version: '2.27'
+ runtime group: 'org.glassfish.jersey.inject', name: 'jersey-hk2', version: '2.27'
+
+ testCompile group: 'junit', name: 'junit', version: '4.11'
+}
--- /dev/null
+#Thu May 31 18:44:49 PDT 2018
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-2.9-all.zip
--- /dev/null
+#!/usr/bin/env bash
+
+##############################################################################
+##
+## Gradle start up script for UN*X
+##
+##############################################################################
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS=""
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn ( ) {
+ echo "$*"
+}
+
+die ( ) {
+ echo
+ echo "$*"
+ echo
+ exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+case "`uname`" in
+ CYGWIN* )
+ cygwin=true
+ ;;
+ Darwin* )
+ darwin=true
+ ;;
+ MINGW* )
+ msys=true
+ ;;
+esac
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG=`dirname "$PRG"`"/$link"
+ fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD="java"
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
+ MAX_FD_LIMIT=`ulimit -H -n`
+ if [ $? -eq 0 ] ; then
+ if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+ MAX_FD="$MAX_FD_LIMIT"
+ fi
+ ulimit -n $MAX_FD
+ if [ $? -ne 0 ] ; then
+ warn "Could not set maximum file descriptor limit: $MAX_FD"
+ fi
+ else
+ warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+ fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+ GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin ; then
+ APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+ CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+ JAVACMD=`cygpath --unix "$JAVACMD"`
+
+ # We build the pattern for arguments to be converted via cygpath
+ ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+ SEP=""
+ for dir in $ROOTDIRSRAW ; do
+ ROOTDIRS="$ROOTDIRS$SEP$dir"
+ SEP="|"
+ done
+ OURCYGPATTERN="(^($ROOTDIRS))"
+ # Add a user-defined pattern to the cygpath arguments
+ if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+ OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+ fi
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ i=0
+ for arg in "$@" ; do
+ CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+ CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
+
+ if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
+ eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+ else
+ eval `echo args$i`="\"$arg\""
+ fi
+ i=$((i+1))
+ done
+ case $i in
+ (0) set -- ;;
+ (1) set -- "$args0" ;;
+ (2) set -- "$args0" "$args1" ;;
+ (3) set -- "$args0" "$args1" "$args2" ;;
+ (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+ (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+ (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+ (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+ (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+ (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+ esac
+fi
+
+# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
+function splitJvmOpts() {
+ JVM_OPTS=("$@")
+}
+eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
+JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
+
+exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
--- /dev/null
+@if "%DEBUG%" == "" @echo off\r
+@rem ##########################################################################\r
+@rem\r
+@rem Gradle startup script for Windows\r
+@rem\r
+@rem ##########################################################################\r
+\r
+@rem Set local scope for the variables with windows NT shell\r
+if "%OS%"=="Windows_NT" setlocal\r
+\r
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.\r
+set DEFAULT_JVM_OPTS=\r
+\r
+set DIRNAME=%~dp0\r
+if "%DIRNAME%" == "" set DIRNAME=.\r
+set APP_BASE_NAME=%~n0\r
+set APP_HOME=%DIRNAME%\r
+\r
+@rem Find java.exe\r
+if defined JAVA_HOME goto findJavaFromJavaHome\r
+\r
+set JAVA_EXE=java.exe\r
+%JAVA_EXE% -version >NUL 2>&1\r
+if "%ERRORLEVEL%" == "0" goto init\r
+\r
+echo.\r
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.\r
+echo.\r
+echo Please set the JAVA_HOME variable in your environment to match the\r
+echo location of your Java installation.\r
+\r
+goto fail\r
+\r
+:findJavaFromJavaHome\r
+set JAVA_HOME=%JAVA_HOME:"=%\r
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe\r
+\r
+if exist "%JAVA_EXE%" goto init\r
+\r
+echo.\r
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%\r
+echo.\r
+echo Please set the JAVA_HOME variable in your environment to match the\r
+echo location of your Java installation.\r
+\r
+goto fail\r
+\r
+:init\r
+@rem Get command-line arguments, handling Windowz variants\r
+\r
+if not "%OS%" == "Windows_NT" goto win9xME_args\r
+if "%@eval[2+2]" == "4" goto 4NT_args\r
+\r
+:win9xME_args\r
+@rem Slurp the command line arguments.\r
+set CMD_LINE_ARGS=\r
+set _SKIP=2\r
+\r
+:win9xME_args_slurp\r
+if "x%~1" == "x" goto execute\r
+\r
+set CMD_LINE_ARGS=%*\r
+goto execute\r
+\r
+:4NT_args\r
+@rem Get arguments from the 4NT Shell from JP Software\r
+set CMD_LINE_ARGS=%$\r
+\r
+:execute\r
+@rem Setup the command line\r
+\r
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar\r
+\r
+@rem Execute Gradle\r
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%\r
+\r
+:end\r
+@rem End local scope for the variables with windows NT shell\r
+if "%ERRORLEVEL%"=="0" goto mainEnd\r
+\r
+:fail\r
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of\r
+rem the _cmd.exe /c_ return code!\r
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1\r
+exit /b 1\r
+\r
+:mainEnd\r
+if "%OS%"=="Windows_NT" endlocal\r
+\r
+:omega\r
--- /dev/null
+rootProject.name = 'TplinkPlugClient'
+
--- /dev/null
+package edu.uci.iotproject.tplinkplug;
+
+import java.io.IOException;
+import java.util.MissingResourceException;
+import java.util.Objects;
+import java.util.Properties;
+
+/**
+ * TODO add class documentation.
+ *
+ * @author Janus Varmarken
+ */
+public class Configuration {
+
+ /**
+ * Name of the file in the resources folder that contains the configuration.
+ */
+ private static final String RESOURCE_FILENAME = "/cfg/config.properties";
+
+ private static final Properties PROPERTIES;
+
+ // ==== Begin keys used in properties file ====
+ private static final String APP_SERVER_URL_KEY = "appServerUrl";
+ private static final String LOGIN_TOKEN_KEY = "token";
+ private static final String DEVICE_ID_KEY = "deviceId";
+ // ===== End keys used in properties file =====
+
+ // ==== Begin cached values of PROPERTIES contents ====
+ private static final String APP_SERVER_URL;
+ private static final String LOGIN_TOKEN;
+ private static final String DEVICE_ID;
+ // ===== End cached values of PROPERTIES contents =====
+
+ static {
+ PROPERTIES = new Properties();
+ try {
+ PROPERTIES.load(Configuration.class.getResourceAsStream(RESOURCE_FILENAME));
+ APP_SERVER_URL = Objects.requireNonNull(PROPERTIES.getProperty(APP_SERVER_URL_KEY, null),
+ String.format("No value for key '%s' in properties file '%s'", APP_SERVER_URL_KEY, RESOURCE_FILENAME));
+ LOGIN_TOKEN = Objects.requireNonNull(PROPERTIES.getProperty(LOGIN_TOKEN_KEY, null),
+ String.format("No value for key '%s' in properties file '%s'", LOGIN_TOKEN_KEY, RESOURCE_FILENAME));
+ DEVICE_ID = Objects.requireNonNull(PROPERTIES.getProperty(DEVICE_ID_KEY, null),
+ String.format("No value for key '%s' in properties file '%s'", DEVICE_ID_KEY, RESOURCE_FILENAME));
+ } catch (IOException e) {
+ e.printStackTrace();
+ throw new MissingResourceException(
+ String.format("Configuration file not found in resources. Missing file: '%s'", RESOURCE_FILENAME),
+ Configuration.class.getName(),
+ RESOURCE_FILENAME
+ );
+ }
+ }
+
+ private Configuration() {
+
+ }
+
+ public static String getAppServerUrl() {
+ return APP_SERVER_URL;
+ }
+
+ public static String getLoginToken() {
+ return LOGIN_TOKEN;
+ }
+
+ public static final String getDeviceId() {
+ return DEVICE_ID;
+ }
+}
--- /dev/null
+package edu.uci.iotproject.tplinkplug;
+
+/**
+ * TODO add class documentation.
+ *
+ * @author Janus Varmarken
+ */
+public class Main {
+
+ public static void main(String[] args) throws InterruptedException {
+ TplinkPlugWanClient client = new TplinkPlugWanClient();
+ int c = 0;
+ while (c < 15) {
+ if (c % 2 == 0) {
+ client.powerOn();
+ }
+ else {
+ client.powerOff();
+ }
+ Thread.sleep(5_000);
+ c++;
+ }
+ }
+
+}
+
+
+// To login, POST following JSON to https://wap.tplinkcloud.com
+// The UUID is generated by the client - possibly used for tracking future logins from the same device?
+// {
+// "method": "login",
+// "params": {
+// "appType": "Kasa_Android",
+// "cloudUserName": "iotuser22@gmail.com",
+// "cloudPassword": "Hqeas2tplink",
+// "terminalUUID": "7e8691de-cf4b-4727-ab31-863b4d4919b4"
+// }
+// }
+// Login output
+// {"error_code":0,"result":{"accountId":"1619813","regTime":"2017-08-06 06:28:38","email":"iotuser22@gmail.com","token":"a749210e-A9F3yu9IMYGWAepK0KCVNp0"}}
+
+// To get list of devices, POST following JSON to https://wap.tplinkcloud.com?token=TOKEN_FROM_LOGIN_RESPONSE_HERE
+// {"method":"getDeviceList"}
+// getDeviceList output (note that the appServerUrl points to the URL to send device control actions (on/off) to (in this case https://use1-wap.tplinkcloud.com)
+// {"error_code":0,"result":{"deviceList":[{"fwVer":"1.4.3 Build 170504 Rel.144921","deviceName":"Smart Wi-Fi LED Bulb with Color Changing","status":0,"alias":"My_TPLink_LightBulb","deviceType":"IOT.SMARTBULB","appServerUrl":"https://use1-wap.tplinkcloud.com","deviceModel":"LB130(US)","deviceMac":"50C7BF59D584","role":0,"isSameRegion":true,"hwId":"111E35908497A05512E259BB76801E10","fwId":"00000000000000000000000000000000","oemId":"05BF7B3BE1675C5A6867B7A7E4C9F6F7","deviceId":"8012CE834562C3304F4FD28FBFBA86E4185B6843","deviceHwVer":"1.0"},{"fwVer":"1.2.5 Build 171206 Rel.085954","deviceName":"Wi-Fi Smart Plug With Energy Monitoring","status":1,"alias":"My Smart Plug","deviceType":"IOT.SMARTPLUGSWITCH","appServerUrl":"https://use1-wap.tplinkcloud.com","deviceModel":"HS110(US)","deviceMac":"50C7BF331F09","role":0,"isSameRegion":true,"hwId":"60FF6B258734EA6880E186F8C96DDC61","fwId":"00000000000000000000000000000000","oemId":"FFF22CFF774A0B89F7624BFC6F50D5DE","deviceId":"800617CC047187F5251E5B88567ACC6D1819FDCF","deviceHwVer":"1.0"}]}}
+
+
+
+
+// async set_relay_state(state){
+// return await super.tplink_request( {"system":{"set_relay_state":{"state": state }}} )
+// }
+
+// deviceId 800617CC047187F5251E5B88567ACC6D1819FDCF or alias "My Smart Plug" ?
+// {
+// "method":"passthrough",
+// "params": {
+// "deviceId": "My Smart Plug",
+// "requestData": {"system":{"set_relay_state":{"state": 0 }}} // 0 for off, 1 for on
+// }
+// }
+
+// curl --request POST "https://use1-wap.tplinkcloud.com/?token=a749210e-A9F3yu9IMYGWAepK0KCVNp0 HTTP/1.1" --data '{"method":"passthrough", "params": {"deviceId": "800617CC047187F5251E5B88567ACC6D1819FDCF", "requestData": "{\"system\":{\"set_relay_state\":{\"state\":1}}}" }}' --header "Content-Type: application/json"
\ No newline at end of file
--- /dev/null
+package edu.uci.iotproject.tplinkplug;
+
+import com.mashape.unirest.http.HttpResponse;
+import com.mashape.unirest.http.JsonNode;
+import com.mashape.unirest.http.Unirest;
+import com.mashape.unirest.http.exceptions.UnirestException;
+
+import javax.ws.rs.client.Client;
+import javax.ws.rs.client.ClientBuilder;
+import javax.ws.rs.client.Entity;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+
+/**
+ * TODO add class documentation.
+ *
+ * @author Janus Varmarken
+ */
+public class TplinkPlugWanClient {
+
+// private Client mRestClient = ClientBuilder.newClient();
+
+ public TplinkPlugWanClient() {
+
+ }
+
+ public void powerOn() {
+ System.out.println(String.format("%s.powerOn() invoked", getClass().getSimpleName()));
+ sendRequest(PlugCommand.ON);
+ }
+
+ public void powerOff() {
+ System.out.println(String.format("%s.powerOff() invoked", getClass().getSimpleName()));
+ sendRequest(PlugCommand.OFF);
+ }
+
+ private void sendRequest(PlugCommand plugCommand) {
+
+ String url = String.format("%s/?token=%s", Configuration.getAppServerUrl(), Configuration.getLoginToken());
+ String payload = buildSetRelayStatePayload(plugCommand);
+
+ try {
+ HttpResponse<JsonNode> response = Unirest.post(url).
+ header("cache-control", "no-cache").
+ header("Content-Type", MediaType.APPLICATION_JSON).
+ body(payload).asJson();
+ String debug = null;
+ } catch (UnirestException e) {
+ e.printStackTrace();
+ }
+
+// Response response = mRestClient.target(url).request(MediaType.APPLICATION_JSON).
+// header("cache-control", "no-cache").
+// header("Content-Type", MediaType.APPLICATION_JSON).
+// post(Entity.text(payload));
+
+ // TODO actually parse the response.
+ String debugPoint = null;
+ }
+
+ private String buildSetRelayStatePayload(PlugCommand command) {
+ return String.format("{ \"method\":\"passthrough\", \"params\": { \"deviceId\": \"%s\", \"requestData\": \"{\\\"system\\\":{\\\"set_relay_state\\\":{\\\"state\\\":%d}}}\"}}",
+ Configuration.getDeviceId(), command.equals(PlugCommand.ON) ? 1 : 0);
+ }
+
+ private static enum PlugCommand {
+ ON, OFF
+ }
+}
\ No newline at end of file