Merge branch 'master' of https://github.uci.edu/rtrimana/smart_home_traffic
authorrtrimana <rtrimana@uci.edu>
Tue, 24 Jul 2018 18:01:12 +0000 (11:01 -0700)
committerrtrimana <rtrimana@uci.edu>
Tue, 24 Jul 2018 18:01:12 +0000 (11:01 -0700)
28 files changed:
Code/Projects/DateWriter/DateWriter.java [new file with mode: 0644]
Code/Projects/SmartPlugDetector/src/main/java/edu/uci/iotproject/Conversation.java
Code/Projects/SmartPlugDetector/src/main/java/edu/uci/iotproject/DnsMap.java
Code/Projects/SmartPlugDetector/src/main/java/edu/uci/iotproject/FinAckPair.java
Code/Projects/SmartPlugDetector/src/main/java/edu/uci/iotproject/Main.java
Code/Projects/SmartPlugDetector/src/main/java/edu/uci/iotproject/TcpReassembler.java [new file with mode: 0644]
Code/Projects/SmartPlugDetector/src/main/java/edu/uci/iotproject/analysis/PcapPacketFilter.java [new file with mode: 0644]
Code/Projects/SmartPlugDetector/src/main/java/edu/uci/iotproject/analysis/PcapPacketPair.java [new file with mode: 0644]
Code/Projects/SmartPlugDetector/src/main/java/edu/uci/iotproject/analysis/TcpConversationUtils.java [new file with mode: 0644]
Code/Projects/SmartPlugDetector/src/main/java/edu/uci/iotproject/analysis/TriggerTrafficExtractor.java [new file with mode: 0644]
Code/Projects/SmartPlugDetector/src/main/java/edu/uci/iotproject/io/PcapHandleReader.java [new file with mode: 0644]
Code/Projects/SmartPlugDetector/src/main/java/edu/uci/iotproject/io/TriggerTimesFileReader.java [new file with mode: 0644]
Code/Projects/TplinkPlugClient/.gitignore [new file with mode: 0644]
Code/Projects/TplinkPlugClient/.idea/compiler.xml [new file with mode: 0644]
Code/Projects/TplinkPlugClient/.idea/copyright/profiles_settings.xml [new file with mode: 0644]
Code/Projects/TplinkPlugClient/.idea/modules.xml [new file with mode: 0644]
Code/Projects/TplinkPlugClient/.idea/modules/TplinkPlugClient.iml [new file with mode: 0644]
Code/Projects/TplinkPlugClient/.idea/modules/TplinkPlugClient_main.iml [new file with mode: 0644]
Code/Projects/TplinkPlugClient/.idea/modules/TplinkPlugClient_test.iml [new file with mode: 0644]
Code/Projects/TplinkPlugClient/build.gradle [new file with mode: 0644]
Code/Projects/TplinkPlugClient/gradle/wrapper/gradle-wrapper.jar [new file with mode: 0644]
Code/Projects/TplinkPlugClient/gradle/wrapper/gradle-wrapper.properties [new file with mode: 0644]
Code/Projects/TplinkPlugClient/gradlew [new file with mode: 0755]
Code/Projects/TplinkPlugClient/gradlew.bat [new file with mode: 0644]
Code/Projects/TplinkPlugClient/settings.gradle [new file with mode: 0644]
Code/Projects/TplinkPlugClient/src/main/java/edu/uci/iotproject/tplinkplug/Configuration.java [new file with mode: 0644]
Code/Projects/TplinkPlugClient/src/main/java/edu/uci/iotproject/tplinkplug/Main.java [new file with mode: 0644]
Code/Projects/TplinkPlugClient/src/main/java/edu/uci/iotproject/tplinkplug/TplinkPlugWanClient.java [new file with mode: 0644]

diff --git a/Code/Projects/DateWriter/DateWriter.java b/Code/Projects/DateWriter/DateWriter.java
new file mode 100644 (file)
index 0000000..adde143
--- /dev/null
@@ -0,0 +1,54 @@
+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
index 66e7804dd1355f3f5bde6760ba4a7dbef7cbf086..05d97e3924b45afa55e198361300e26bbfa287a1 100644 (file)
@@ -43,15 +43,28 @@ public class Conversation {
     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.
@@ -59,6 +72,29 @@ public class 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)
@@ -73,7 +109,9 @@ public class 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<>();
     }
 
@@ -89,17 +127,23 @@ public class Conversation {
     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; }
+            });
+        }
     }
 
     /**
@@ -112,6 +156,88 @@ public class Conversation {
         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.
@@ -119,6 +245,8 @@ public class 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));
     }
 
@@ -216,4 +344,92 @@ public class Conversation {
         }
     }
 
+    /**
+     * <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
index d2f91f318aedf44c6de0cbd94a6af84c187280f8..d88f0cfbb553392636b5ae2b501b4333203555bf 100644 (file)
@@ -1,16 +1,17 @@
 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.
@@ -20,7 +21,7 @@ import java.util.concurrent.TimeoutException;
  * @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;
@@ -31,11 +32,18 @@ public class DnsMap {
     
     /* 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
      *
@@ -43,27 +51,22 @@ public class DnsMap {
      * @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
@@ -78,7 +81,7 @@ public class DnsMap {
                     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,
@@ -98,7 +101,6 @@ public class DnsMap {
      * @param   hostname    Hostname to check
      */
     public boolean isRelatedToCloudServer(String address, String hostname) {
-        
         return ipToHostnameMap.getOrDefault(address, EMPTY_SET).contains(hostname);
     }
 }
index a4b10bc2a1def01fb79b92e32318767b64cf10de..d4451f31849b6da30376dea2799e7ca40f79a26e 100644 (file)
@@ -116,7 +116,7 @@ public class FinAckPair {
      * <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
      *         }
index f3b8a005bdb37009c0d81b4d50565b4750a567f2..e0a51a317a962a453881f320b71054a16abd42cd 100644 (file)
@@ -1,12 +1,20 @@
 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;
 
 /**
@@ -37,38 +45,147 @@ public class Main {
 //        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
diff --git a/Code/Projects/SmartPlugDetector/src/main/java/edu/uci/iotproject/TcpReassembler.java b/Code/Projects/SmartPlugDetector/src/main/java/edu/uci/iotproject/TcpReassembler.java
new file mode 100644 (file)
index 0000000..6cce343
--- /dev/null
@@ -0,0 +1,229 @@
+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;
+    }
+}
diff --git a/Code/Projects/SmartPlugDetector/src/main/java/edu/uci/iotproject/analysis/PcapPacketFilter.java b/Code/Projects/SmartPlugDetector/src/main/java/edu/uci/iotproject/analysis/PcapPacketFilter.java
new file mode 100644 (file)
index 0000000..529faf4
--- /dev/null
@@ -0,0 +1,14 @@
+package edu.uci.iotproject.analysis;
+
+import org.pcap4j.core.PcapPacket;
+
+/**
+ * TODO add class documentation.
+ *
+ * @author Janus Varmarken
+ */
+public interface PcapPacketFilter {
+
+    boolean shouldIncludePacket(PcapPacket packet);
+
+}
diff --git a/Code/Projects/SmartPlugDetector/src/main/java/edu/uci/iotproject/analysis/PcapPacketPair.java b/Code/Projects/SmartPlugDetector/src/main/java/edu/uci/iotproject/analysis/PcapPacketPair.java
new file mode 100644 (file)
index 0000000..1d28895
--- /dev/null
@@ -0,0 +1,29 @@
+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());
+    }
+}
diff --git a/Code/Projects/SmartPlugDetector/src/main/java/edu/uci/iotproject/analysis/TcpConversationUtils.java b/Code/Projects/SmartPlugDetector/src/main/java/edu/uci/iotproject/analysis/TcpConversationUtils.java
new file mode 100644 (file)
index 0000000..a598f81
--- /dev/null
@@ -0,0 +1,51 @@
+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?
+    }
+
+}
diff --git a/Code/Projects/SmartPlugDetector/src/main/java/edu/uci/iotproject/analysis/TriggerTrafficExtractor.java b/Code/Projects/SmartPlugDetector/src/main/java/edu/uci/iotproject/analysis/TriggerTrafficExtractor.java
new file mode 100644 (file)
index 0000000..c0b553c
--- /dev/null
@@ -0,0 +1,83 @@
+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);
+            }
+        }
+    }
+
+}
diff --git a/Code/Projects/SmartPlugDetector/src/main/java/edu/uci/iotproject/io/PcapHandleReader.java b/Code/Projects/SmartPlugDetector/src/main/java/edu/uci/iotproject/io/PcapHandleReader.java
new file mode 100644 (file)
index 0000000..2c387f3
--- /dev/null
@@ -0,0 +1,73 @@
+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();
+    }
+
+}
diff --git a/Code/Projects/SmartPlugDetector/src/main/java/edu/uci/iotproject/io/TriggerTimesFileReader.java b/Code/Projects/SmartPlugDetector/src/main/java/edu/uci/iotproject/io/TriggerTimesFileReader.java
new file mode 100644 (file)
index 0000000..592bcd7
--- /dev/null
@@ -0,0 +1,68 @@
+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();
+    }
+
+}
diff --git a/Code/Projects/TplinkPlugClient/.gitignore b/Code/Projects/TplinkPlugClient/.gitignore
new file mode 100644 (file)
index 0000000..4336e49
--- /dev/null
@@ -0,0 +1,70 @@
+# 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
diff --git a/Code/Projects/TplinkPlugClient/.idea/compiler.xml b/Code/Projects/TplinkPlugClient/.idea/compiler.xml
new file mode 100644 (file)
index 0000000..05e1e3d
--- /dev/null
@@ -0,0 +1,27 @@
+<?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
diff --git a/Code/Projects/TplinkPlugClient/.idea/copyright/profiles_settings.xml b/Code/Projects/TplinkPlugClient/.idea/copyright/profiles_settings.xml
new file mode 100644 (file)
index 0000000..e7bedf3
--- /dev/null
@@ -0,0 +1,3 @@
+<component name="CopyrightManager">
+  <settings default="" />
+</component>
\ No newline at end of file
diff --git a/Code/Projects/TplinkPlugClient/.idea/modules.xml b/Code/Projects/TplinkPlugClient/.idea/modules.xml
new file mode 100644 (file)
index 0000000..b57a14e
--- /dev/null
@@ -0,0 +1,10 @@
+<?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
diff --git a/Code/Projects/TplinkPlugClient/.idea/modules/TplinkPlugClient.iml b/Code/Projects/TplinkPlugClient/.idea/modules/TplinkPlugClient.iml
new file mode 100644 (file)
index 0000000..39928b5
--- /dev/null
@@ -0,0 +1,12 @@
+<?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
diff --git a/Code/Projects/TplinkPlugClient/.idea/modules/TplinkPlugClient_main.iml b/Code/Projects/TplinkPlugClient/.idea/modules/TplinkPlugClient_main.iml
new file mode 100644 (file)
index 0000000..cf8382a
--- /dev/null
@@ -0,0 +1,35 @@
+<?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
diff --git a/Code/Projects/TplinkPlugClient/.idea/modules/TplinkPlugClient_test.iml b/Code/Projects/TplinkPlugClient/.idea/modules/TplinkPlugClient_test.iml
new file mode 100644 (file)
index 0000000..9a19c02
--- /dev/null
@@ -0,0 +1,39 @@
+<?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
diff --git a/Code/Projects/TplinkPlugClient/build.gradle b/Code/Projects/TplinkPlugClient/build.gradle
new file mode 100644 (file)
index 0000000..f92f811
--- /dev/null
@@ -0,0 +1,24 @@
+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'
+}
diff --git a/Code/Projects/TplinkPlugClient/gradle/wrapper/gradle-wrapper.jar b/Code/Projects/TplinkPlugClient/gradle/wrapper/gradle-wrapper.jar
new file mode 100644 (file)
index 0000000..9411448
Binary files /dev/null and b/Code/Projects/TplinkPlugClient/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/Code/Projects/TplinkPlugClient/gradle/wrapper/gradle-wrapper.properties b/Code/Projects/TplinkPlugClient/gradle/wrapper/gradle-wrapper.properties
new file mode 100644 (file)
index 0000000..7a51471
--- /dev/null
@@ -0,0 +1,6 @@
+#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
diff --git a/Code/Projects/TplinkPlugClient/gradlew b/Code/Projects/TplinkPlugClient/gradlew
new file mode 100755 (executable)
index 0000000..9d82f78
--- /dev/null
@@ -0,0 +1,160 @@
+#!/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 "$@"
diff --git a/Code/Projects/TplinkPlugClient/gradlew.bat b/Code/Projects/TplinkPlugClient/gradlew.bat
new file mode 100644 (file)
index 0000000..aec9973
--- /dev/null
@@ -0,0 +1,90 @@
+@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
diff --git a/Code/Projects/TplinkPlugClient/settings.gradle b/Code/Projects/TplinkPlugClient/settings.gradle
new file mode 100644 (file)
index 0000000..8a4dc83
--- /dev/null
@@ -0,0 +1,2 @@
+rootProject.name = 'TplinkPlugClient'
+
diff --git a/Code/Projects/TplinkPlugClient/src/main/java/edu/uci/iotproject/tplinkplug/Configuration.java b/Code/Projects/TplinkPlugClient/src/main/java/edu/uci/iotproject/tplinkplug/Configuration.java
new file mode 100644 (file)
index 0000000..ba9baa2
--- /dev/null
@@ -0,0 +1,69 @@
+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;
+    }
+}
diff --git a/Code/Projects/TplinkPlugClient/src/main/java/edu/uci/iotproject/tplinkplug/Main.java b/Code/Projects/TplinkPlugClient/src/main/java/edu/uci/iotproject/tplinkplug/Main.java
new file mode 100644 (file)
index 0000000..7109a41
--- /dev/null
@@ -0,0 +1,63 @@
+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
diff --git a/Code/Projects/TplinkPlugClient/src/main/java/edu/uci/iotproject/tplinkplug/TplinkPlugWanClient.java b/Code/Projects/TplinkPlugClient/src/main/java/edu/uci/iotproject/tplinkplug/TplinkPlugWanClient.java
new file mode 100644 (file)
index 0000000..2bf1c6c
--- /dev/null
@@ -0,0 +1,69 @@
+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