66e7804dd1355f3f5bde6760ba4a7dbef7cbf086
[pingpong.git] / Code / Projects / SmartPlugDetector / src / main / java / edu / uci / iotproject / Conversation.java
1 package edu.uci.iotproject;
2
3 import edu.uci.iotproject.util.PcapPacketUtils;
4 import org.pcap4j.core.PcapPacket;
5 import org.pcap4j.packet.IpV4Packet;
6 import org.pcap4j.packet.TcpPacket;
7
8 import java.util.*;
9
10 /**
11  * Models a (TCP) conversation/connection/session/flow (packet's belonging to the same session between a client and a
12  * server).
13  * Holds a list of {@link PcapPacket}s identified as pertaining to the flow. Note that this list is <em>not</em>
14  * considered when determining equality of two {@code Conversation} instances in order to allow for a
15  * {@code Conversation} to function as a key in data structures such as {@link java.util.Map} and {@link java.util.Set}.
16  * See {@link #equals(Object)} for the definition of equality.
17  *
18  * @author Janus Varmarken {@literal <jvarmark@uci.edu>}
19  * @author Rahmadi Trimananda {@literal <rtrimana@uci.edu>}
20  */
21 public class Conversation {
22
23     /* Begin instance properties */
24     /**
25      * The IP of the host that is considered the client (i.e. the host that initiates the conversation)
26      * in this conversation.
27      */
28     private final String mClientIp;
29
30     /**
31      * The port number used by the host that is considered the client in this conversation.
32      */
33     private final int mClientPort;
34
35     /**
36      * The IP of the host that is considered the server (i.e. is the responder) in this conversation.
37      */
38     private final String mServerIp;
39
40     /**
41      * The port number used by the server in this conversation.
42      */
43     private final int mServerPort;
44
45     /**
46      * The list of packets pertaining to this conversation.
47      */
48     private final List<PcapPacket> mPackets;
49
50     /**
51      * Contains the sequence numbers seen so far for this {@code Conversation}.
52      * Used for filtering out retransmissions.
53      */
54     private final Set<Integer> mSeqNumbers;
55
56     /**
57      * List of pairs FINs and their corresponding ACKs associated with this conversation.
58      */
59     private List<FinAckPair> mFinPackets;
60     /* End instance properties */
61
62     /**
63      * Constructs a new {@code Conversation}.
64      * @param clientIp The IP of the host that is considered the client (i.e. the host that initiates the conversation)
65      *                 in the conversation.
66      * @param clientPort The port number used by the client for the conversation.
67      * @param serverIp The IP of the host that is considered the server (i.e. is the responder) in the conversation.
68      * @param serverPort The port number used by the server for the conversation.
69      */
70     public Conversation(String clientIp, int clientPort, String serverIp, int serverPort) {
71         this.mClientIp = clientIp;
72         this.mClientPort = clientPort;
73         this.mServerIp = serverIp;
74         this.mServerPort = serverPort;
75         this.mPackets = new ArrayList<>();
76         this.mSeqNumbers = new HashSet<>();
77         this.mFinPackets = new ArrayList<>();
78     }
79
80     /**
81      * Add a packet to the list of packets associated with this conversation.
82      * @param packet The packet that is to be added to (associated with) this conversation.
83      * @param ignoreRetransmissions Boolean value indicating if retransmissions should be ignored.
84      *                              If set to {@code true}, {@code packet} will <em>not</em> be added to the
85      *                              internal list of packets pertaining to this {@code Conversation}
86      *                              <em>iff</em> the sequence number of {@code packet} was already
87      *                              seen in a previous packet.
88      */
89     public void addPacket(PcapPacket packet, boolean ignoreRetransmissions) {
90         // Precondition: verify that packet does indeed pertain to conversation.
91         onAddPrecondition(packet);
92         // For now we only support TCP flows.
93         TcpPacket tcpPacket = Objects.requireNonNull(packet.get(TcpPacket.class));
94         int seqNo = tcpPacket.getHeader().getSequenceNumber();
95         if (ignoreRetransmissions && mSeqNumbers.contains(seqNo)) {
96             // Packet is a retransmission. Ignore it.
97             return;
98         }
99         // Update set of sequence numbers seen so far with sequence number of new packet.
100         mSeqNumbers.add(seqNo);
101         // Finally add packet to list of packets pertaining to this conversation.
102         mPackets.add(packet);
103     }
104
105     /**
106      * Get a list of packets pertaining to this {@code Conversation}.
107      * The returned list is a read-only list.
108      * @return the list of packets pertaining to this {@code Conversation}.
109      */
110     public List<PcapPacket> getPackets() {
111         // Return read-only view to prevent external code from manipulating internal state (preserve invariant).
112         return Collections.unmodifiableList(mPackets);
113     }
114
115     /**
116      * Adds a TCP FIN packet to the list of TCP FIN packets associated with this conversation.
117      * @param finPacket The TCP FIN packet that is to be added to (associated with) this conversation.
118      */
119     public void addFinPacket(PcapPacket finPacket) {
120         // Precondition: verify that packet does indeed pertain to conversation.
121         onAddPrecondition(finPacket);
122         mFinPackets.add(new FinAckPair(finPacket));
123     }
124
125     /**
126      * Attempt to ACK any FIN packets held by this conversation.
127      * @param ackPacket The ACK for a FIN previously added to this conversation.
128      */
129     public void attemptAcknowledgementOfFin(PcapPacket ackPacket) {
130         // Precondition: verify that the packet pertains to this conversation.
131         onAddPrecondition(ackPacket);
132         // Mark unack'ed FIN(s) that this ACK matches as ACK'ed (there might be more than one in case of retransmissions..?)
133         mFinPackets.replaceAll(finAckPair -> !finAckPair.isAcknowledged() && finAckPair.isCorrespondingAckPacket(ackPacket) ? new FinAckPair(finAckPair.getFinPacket(), ackPacket) : finAckPair);
134     }
135
136     /**
137      * Retrieves an unmodifiable view of the list of {@link FinAckPair}s associated with this {@code Conversation}.
138      * @return an unmodifiable view of the list of {@link FinAckPair}s associated with this {@code Conversation}.
139      */
140     public List<FinAckPair> getFinAckPairs() {
141         return Collections.unmodifiableList(mFinPackets);
142     }
143
144     /**
145      * Get if this {@code Conversation} is considered to have been gracefully shut down.
146      * A {@code Conversation} has been gracefully shut down if it contains a FIN+ACK pair for both directions
147      * (client to server, and server to client).
148      * @return {@code true} if the connection has been gracefully shut down, false otherwise.
149      */
150     public boolean isGracefullyShutdown() {
151         //  The conversation has been gracefully shut down if we have recorded a FIN from both the client and the server which have both been ack'ed.
152         return mFinPackets.stream().anyMatch(finAckPair -> finAckPair.isAcknowledged() && PcapPacketUtils.isSource(finAckPair.getFinPacket(), mClientIp, mClientPort)) &&
153                 mFinPackets.stream().anyMatch(finAckPair -> finAckPair.isAcknowledged() && PcapPacketUtils.isSource(finAckPair.getFinPacket(), mServerIp, mServerPort));
154     }
155
156     // =========================================================================================================
157     // We simply reuse equals and hashCode methods of String.class to be able to use this class as a key
158     // in a Map.
159
160     /**
161      * <em>Note:</em> currently, equality is determined based on pairwise equality of the elements of the four tuple
162      * ({@link #mClientIp}, {@link #mClientPort}, {@link #mServerIp}, {@link #mServerPort}) for {@code this} and
163      * {@code obj}.
164      * @param obj The object to test for equality with {@code this}.
165      * @return {@code true} if {@code obj} is considered equal to {@code this} based on the definition of equality given above.
166      */
167     @Override
168     public boolean equals(Object obj) {
169         return obj instanceof Conversation && this.toString().equals(obj.toString());
170     }
171
172     @Override
173     public int hashCode() {
174         return toString().hashCode();
175     }
176     // =========================================================================================================
177
178     @Override
179     public String toString() {
180         return String.format("%s:%d %s:%d", mClientIp, mClientPort, mServerIp, mServerPort);
181     }
182
183     /**
184      * Invoke to verify that the precondition holds when a caller attempts to add a packet to this {@code Conversation}.
185      * An {@link IllegalArgumentException} is thrown if the precondition is violated.
186      * @param packet the packet to be added to this {@code Conversation}
187      */
188     private void onAddPrecondition(PcapPacket packet) {
189         // Apply precondition to preserve class invariant: all packets in mPackets must match the 4 tuple that
190         // defines the conversation.
191         IpV4Packet ipPacket = Objects.requireNonNull(packet.get(IpV4Packet.class));
192         // For now we only support TCP flows.
193         TcpPacket tcpPacket = Objects.requireNonNull(packet.get(TcpPacket.class));
194         String ipSrc = ipPacket.getHeader().getSrcAddr().getHostAddress();
195         String ipDst = ipPacket.getHeader().getDstAddr().getHostAddress();
196         int srcPort = tcpPacket.getHeader().getSrcPort().valueAsInt();
197         int dstPort = tcpPacket.getHeader().getDstPort().valueAsInt();
198         String clientIp, serverIp;
199         int clientPort, serverPort;
200         if (ipSrc.equals(mClientIp)) {
201             clientIp = ipSrc;
202             clientPort = srcPort;
203             serverIp = ipDst;
204             serverPort = dstPort;
205         } else {
206             clientIp = ipDst;
207             clientPort = dstPort;
208             serverIp = ipSrc;
209             serverPort = srcPort;
210         }
211         if (!(clientIp.equals(mClientIp) && clientPort == mClientPort &&
212                 serverIp.equals(mServerIp) && serverPort == mServerPort)) {
213             throw new IllegalArgumentException(
214                     String.format("Attempt to add packet that does not pertain to %s",
215                             Conversation.class.getSimpleName()));
216         }
217     }
218
219 }