1#!/usr/bin/env python
2"""
3Process packet capture files and produce a nice HTML
4report of MSN Chat sessions.
5
6Copyright (c) 2003 by Gilbert Ramirez <gram@alumni.rice.edu>
7
8SPDX-License-Identifier: GPL-2.0-or-later
9"""
10
11import os
12import re
13import sys
14import array
15import string
16import WiresharkXML
17import getopt
18
19# By default we output the HTML to stdout
20out_fh = sys.stdout
21
22class MSNMessage:
23    pass
24
25class MSN_MSG(MSNMessage):
26    def __init__(self, timestamp, user, message):
27        self.timestamp = timestamp
28        self.user = user
29        self.message = message
30
31
32class Conversation:
33    """Keeps track of a single MSN chat session"""
34
35    re_MSG_out = re.compile("MSG (?P<TrID>\d+) (?P<ACKTYPE>[UNA]) (?P<len>\d+)")
36    re_MSG_in  = re.compile("MSG (?P<user>\S+)@(?P<domain>\S+) (?P<alias>\S+) (?P<len>\d+)")
37
38    USER_NOT_FOUND = -1
39    DEFAULT_USER = None
40
41
42    DEFAULT_USER_COLOR = "#0000ff"
43    USER_COLORS = [ "#ff0000", "#00ff00",
44            "#800000", "#008000", "#000080" ]
45
46    DEFAULT_USER_TEXT_COLOR = "#000000"
47    USER_TEXT_COLOR = "#000080"
48
49    def __init__(self):
50        self.packets = []
51        self.messages = []
52
53    def AddPacket(self, packet):
54        self.packets.append(packet)
55
56    def Summarize(self):
57        for packet in self.packets:
58            msg = self.CreateMSNMessage(packet)
59            if msg:
60                self.messages.append(msg)
61            else:
62                 #XXX
63                 pass
64
65
66    def CreateMSNMessage(self, packet):
67        msnms = packet.get_items("msnms")[0]
68
69        # Check the first line in the msnms transmission for the user
70        child = msnms.children[0]
71        user = self.USER_NOT_FOUND
72
73        m = self.re_MSG_out.search(child.show)
74        if m:
75            user = self.DEFAULT_USER
76
77        else:
78            m = self.re_MSG_in.search(child.show)
79            if m:
80                user = m.group("alias")
81
82        if user == self.USER_NOT_FOUND:
83            print >> sys.stderr, "No match for", child.show
84            sys.exit(1)
85            return None
86
87        msg = ""
88
89        i = 5
90        check_trailing = 0
91        if len(msnms.children) > 5:
92            check_trailing = 1
93
94        while i < len(msnms.children):
95            msg += msnms.children[i].show
96            if check_trailing:
97                j = msg.find("MSG ")
98                if j >= 0:
99                    msg = msg[:j]
100                    i += 5
101                else:
102                    i += 6
103            else:
104                i += 6
105
106        timestamp = packet.get_items("frame.time")[0].get_show()
107        i = timestamp.rfind(".")
108        timestamp = timestamp[:i]
109
110        return MSN_MSG(timestamp, user, msg)
111
112    def MsgToHTML(self, text):
113        bytes = array.array("B")
114
115        new_string = text
116        i = new_string.find("\\")
117
118        while i > -1:
119            # At the end?
120            if i == len(new_string) - 1:
121                # Just let the default action
122                # copy everything to 'bytes'
123                break
124
125            if new_string[i+1] in string.digits:
126                left = new_string[:i]
127                bytes.fromstring(left)
128
129                right = new_string[i+4:]
130
131                oct_string = new_string[i+1:i+4]
132                char = int(oct_string, 8)
133                bytes.append(char)
134
135                new_string = right
136
137            # ignore \r and \n
138            elif new_string[i+1] in "rn":
139                copy_these = new_string[:i]
140                bytes.fromstring(copy_these)
141                new_string = new_string[i+2:]
142
143            else:
144                copy_these = new_string[:i+2]
145                bytes.fromstring(copy_these)
146                new_string = new_string[i+2:]
147
148            i = new_string.find("\\")
149
150
151        bytes.fromstring(new_string)
152
153        return bytes
154
155    def CreateHTML(self, default_user):
156        if not self.messages:
157            return
158
159        print >> out_fh, """
160<HR><BR><H3 Align=Center> ---- New Conversation @ %s ----</H3><BR>""" \
161            % (self.messages[0].timestamp)
162
163        user_color_assignments = {}
164
165        for msg in self.messages:
166            # Calculate 'user' and 'user_color' and 'user_text_color'
167            if msg.user == self.DEFAULT_USER:
168                user = default_user
169                user_color = self.DEFAULT_USER_COLOR
170                user_text_color = self.DEFAULT_USER_TEXT_COLOR
171            else:
172                user = msg.user
173                user_text_color = self.USER_TEXT_COLOR
174                if user_color_assignments.has_key(user):
175                    user_color = user_color_assignments[user]
176                else:
177                    num_assigned = len(user_color_assignments.keys())
178                    user_color = self.USER_COLORS[num_assigned]
179                    user_color_assignments[user] = user_color
180
181            # "Oct  6, 2003 21:45:25"  --> "21:45:25"
182            timestamp = msg.timestamp.split()[-1]
183
184            htmlmsg = self.MsgToHTML(msg.message)
185
186            print >> out_fh, """
187<FONT COLOR="%s"><FONT SIZE="2">(%s) </FONT><B>%s:</B></FONT> <FONT COLOR="%s">""" \
188                % (user_color, timestamp, user, user_text_color)
189
190            htmlmsg.tofile(out_fh)
191
192            print >> out_fh, "</FONT><BR>"
193
194
195class CaptureFile:
196    """Parses a single a capture file and keeps track of
197    all chat sessions in the file."""
198
199    def __init__(self, capture_filename, tshark):
200        """Run tshark on the capture file and parse
201        the data."""
202        self.conversations = []
203        self.conversations_map = {}
204
205        pipe = os.popen(tshark + " -Tpdml -n -R "
206            "'msnms contains \"X-MMS-IM-Format\"' "
207            "-r " + capture_filename, "r")
208
209        WiresharkXML.parse_fh(pipe, self.collect_packets)
210
211        for conv in self.conversations:
212            conv.Summarize()
213
214    def collect_packets(self, packet):
215        """Collect the packets passed back from WiresharkXML.
216        Sort them by TCP/IP conversation, as there could be multiple
217        clients per machine."""
218        # Just in case we're looking at tunnelling protocols where
219        # more than one IP or TCP header exists, look at the last one,
220        # which would be the one inside the tunnel.
221        src_ip = packet.get_items("ip.src")[-1].get_show()
222        dst_ip = packet.get_items("ip.dst")[-1].get_show()
223        src_tcp = packet.get_items("tcp.srcport")[-1].get_show()
224        dst_tcp = packet.get_items("tcp.dstport")[-1].get_show()
225
226        key_params = [src_ip, dst_ip, src_tcp, dst_tcp]
227        key_params.sort()
228        key = '|'.join(key_params)
229
230        if not self.conversations_map.has_key(key):
231            conv = self.conversations_map[key] = Conversation()
232            self.conversations.append(conv)
233        else:
234            conv = self.conversations_map[key]
235
236        conv.AddPacket(packet)
237
238
239    def CreateHTML(self, default_user):
240        if not self.conversations:
241            return
242
243        for conv in self.conversations:
244            conv.CreateHTML(default_user)
245
246
247def run_filename(filename, default_user, tshark):
248    """Process one capture file."""
249
250    capture = CaptureFile(filename, tshark)
251    capture.CreateHTML(default_user)
252
253
254def run(filenames, default_user, tshark):
255    # HTML Header
256    print >> out_fh, """
257<HTML><TITLE>MSN Conversation</TITLE>
258<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
259<BODY>
260"""
261    for filename in filenames:
262        run_filename(filename, default_user, tshark)
263
264    # HTML Footer
265    print >> out_fh, """
266<HR>
267</BODY>
268</HTML>
269"""
270
271
272def usage():
273    print >> sys.stderr, "msnchat [OPTIONS] CAPTURE_FILE [...]"
274    print >> sys.stderr, "  -o FILE       name of output file"
275    print >> sys.stderr, "  -t TSHARK  location of tshark binary"
276    print >> sys.stderr, "  -u USER       name for unknown user"
277    sys.exit(1)
278
279def main():
280    default_user = "Unknown"
281    tshark = "tshark"
282
283    optstring = "ho:t:u:"
284    longopts = ["help"]
285
286    try:
287        opts, args = getopt.getopt(sys.argv[1:], optstring, longopts)
288    except getopt.GetoptError:
289        usage()
290
291    for opt, arg in opts:
292        if opt == "-h" or opt == "--help":
293            usage()
294
295        elif opt == "-o":
296            filename = arg
297            global out_fh
298            try:
299                out_fh = open(filename, "w")
300            except IOError:
301                sys.exit("Could not open %s for writing." % (filename,))
302
303        elif opt == "-u":
304            default_user = arg
305
306        elif opt == "-t":
307            tshark = arg
308
309        else:
310            sys.exit("Unhandled command-line option: " + opt)
311
312    run(args, default_user, tshark)
313
314if __name__ == '__main__':
315    main()
316