install_prereq: Download latest Jansson.
[asterisk/asterisk.git] / contrib / scripts / spandspflow2pcap.py
1 #!/usr/bin/env python
2 # vim: set ts=8 sw=4 sts=4 et ai tw=79:
3 '''
4 Usage: ./spandspflow2pcap.py SPANDSP_LOG SENDFAX_PCAP
5
6 Takes a log from Asterisk with SpanDSP, extracts the "received" data
7 and puts it in a pcap file. Use 'fax set debug on' and configure
8 logger.conf to get fax logs.
9
10 Input data should look something like this::
11
12     [2013-08-07 15:17:34] FAX[23479] res_fax.c: FLOW T.38 Rx     5: IFP c0 01 ...
13
14 Output data will look like a valid pcap file ;-)
15
16 This allows you to reconstruct received faxes into replayable pcaps.
17
18 Replaying is expected to be done by SIPp with sipp-sendfax.xml. The
19 SIPp binary used for replaying must have image (fax) support. This means
20 you'll need a version higher than 3.5.0 (unreleased when writing this),
21 or the git master branch: https://github.com/SIPp/sipp
22
23
24 Author: Walter Doekes, OSSO B.V. (2013,2015,2016)
25 License: Public Domain
26 '''
27 from base64 import b16decode
28 from datetime import datetime, timedelta
29 from re import search
30 from time import mktime
31 from struct import pack
32 import sys
33
34
35 LOSSY = False
36 EMPTY_RECOVERY = False
37
38
39 def n2b(text):
40     return b16decode(text.replace(' ', '').replace('\n', '').upper())
41
42
43 class FaxPcap(object):
44     PCAP_PREAMBLE = n2b('d4 c3 b2 a1 02 00 04 00'
45                         '00 00 00 00 00 00 00 00'
46                         'ff ff 00 00 71 00 00 00')
47
48     def __init__(self, outfile):
49         self.outfile = outfile
50         self.date = None
51         self.dateoff = timedelta(seconds=0)
52         self.seqno = None
53         self.udpseqno = 128
54         self.prev_data = None
55
56         # Only do this if at pos 0?
57         self.outfile.write(self.PCAP_PREAMBLE)
58
59     def data2packet(self, date, udpseqno, seqno, data, prev_data):
60         sum16 = '\x43\x21'  # checksum is irrelevant for sipp sending
61
62         new_prev = data  # without seqno..
63         data = '%s%s' % (pack('>H', seqno), data)
64         if prev_data:
65             if LOSSY and (seqno % 3) == 2:
66                 return '', new_prev
67             if EMPTY_RECOVERY:
68                 # struct ast_frame f[16], we have room for a few
69                 # packets.
70                 packets = 14
71                 data += '\x00%c%s%s' % (
72                     chr(packets + 1), '\x00' * packets, prev_data)
73             else:
74                 # Add 1 previous packet, without the seqno.
75                 data += '\x00\x01' + prev_data
76
77         kwargs = {'udpseqno': pack('>H', udpseqno), 'sum16': sum16}
78
79         kwargs['data'] = data
80         kwargs['lenb16'] = pack('>H', len(kwargs['data']) + 8)
81         udp = '\x00\x01\x00\x02%(lenb16)s%(sum16)s%(data)s' % kwargs
82
83         kwargs['data'] = udp
84         kwargs['lenb16'] = pack('>H', len(kwargs['data']) + 20)
85         ip = ('\x45\xb8%(lenb16)s%(udpseqno)s\x00\x00\xf9\x11%(sum16)s\x01'
86               '\x01\x01\x01\x02\x02\x02\x02%(data)s') % kwargs
87
88         kwargs['data'] = ip
89         frame = ('\x00\x00\x00\x01\x00\x06\x00\x30\x48\xb1\x1c\x34\x00\x00'
90                  '\x08\x00%(data)s') % kwargs
91
92         kwargs['data'] = frame
93         sec = mktime(date.timetuple())
94         msec = date.microsecond
95         datalen = len(kwargs['data'])
96         kwargs['pre'] = pack('<IIII', sec, msec, datalen, datalen)
97         packet = '%(pre)s%(data)s' % kwargs
98
99         return (packet, new_prev)
100
101     def add(self, date, seqno, data):
102         if self.seqno is None:
103             self.seqno = 0
104             for i in range(seqno):
105                 # In case the first zeroes were dropped, add them.
106                 self.add(date, i, '\x00')
107         assert seqno == self.seqno, '%s != %s' % (seqno, self.seqno)
108
109         # Data is prepended by len(data).
110         data = chr(len(data)) + data
111
112         # Auto-increasing dates
113         if self.date is None or date > self.date:
114             # print 'date is larger', date, self.date
115             self.date = date
116         elif (date < self.date.replace(microsecond=0)):
117             assert False, ('We increased too fast.. decrease delta: %r/%r' %
118                            (date, self.date))
119         else:
120             self.date += timedelta(microseconds=9000)
121
122         print seqno, '\t', self.date + self.dateoff
123
124         # Make packet.
125         packet, prev_data = self.data2packet(self.date + self.dateoff,
126                                              self.udpseqno, self.seqno,
127                                              data, self.prev_data)
128         self.outfile.write(packet)
129
130         # Increase values.
131         self.udpseqno += 1
132         self.seqno += 1
133         self.prev_data = prev_data
134
135     def add_garbage(self, date):
136         if self.date is None or date > self.date:
137             self.date = date
138
139         packet, ignored = self.data2packet(self.date, self.udpseqno,
140                                            0xffff, 'GARBAGE', '')
141         self.udpseqno += 1
142
143         self.outfile.write(packet)
144
145
146 with open(sys.argv[1], 'r') as infile:
147     with open(sys.argv[2], 'wb') as outfile:
148         first = True
149         p = FaxPcap(outfile)
150         # p.add(datetime.now(), 0, n2b('06'))
151         # p.add(datetime.now(), 1, n2b('c0 01 80 00 00 ff'))
152
153         for lineno, line in enumerate(infile):
154             # Look for lines like:
155             # [2013-08-07 15:17:34] FAX[23479] res_fax.c: \
156             #   FLOW T.38 Rx     5: IFP c0 01 80 00 00 ff
157             if 'FLOW T.38 Rx' not in line:
158                 continue
159             if 'IFP' not in line:
160                 continue
161
162             match = search(r'(\d{4})-(\d\d)-(\d\d) (\d\d):(\d\d):(\d\d)', line)
163             assert match
164             date = datetime(*[int(i) for i in match.groups()])
165
166             match = search(r'Rx\s*(\d+):', line)
167             assert match
168             seqno = int(match.groups()[0])
169
170             match = search(r': IFP ([0-9a-f ]+)', line)
171             assert match
172             data = n2b(match.groups()[0])
173
174             # Have the file start a second early.
175             if first:
176                 p.add_garbage(date)
177                 first = False
178
179             # Add the packets.
180             #
181             # T.38 basic format of UDPTL payload section with redundancy:
182             #
183             # UDPTL_SEQNO
184             # - 2 sequence number (big endian)
185             # UDPTL_PRIMARY_PAYLOAD (T30?)
186             # - 1 subpacket length (excluding this byte)
187             # - 1 type of message (e.g. 0xd0 for data(?))
188             # - 1 items in data field (e.g. 0x01)
189             # - 2 length of data (big endian)
190             # - N data
191             # RECOVERY (optional)
192             # - 2 count of previous seqno packets (big endian)
193             # - N UDPTL_PRIMARY_PAYLOAD of (seqno-1)
194             # - N UDPTL_PRIMARY_PAYLOAD of (seqno-2)
195             # - ...
196             #
197             p.add(date, seqno, data)