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