blob: 70c1ef0ece54e2e0f88dc75ed60d58bbff988a4c [file] [log] [blame]
Maxe732c2c2017-11-23 16:42:59 +01001#!/usr/bin/python3
2# -*- mode: python-mode; py-indent-tabs-mode: nil -*-
3"""
4/*
5 * Copyright (C) 2016 sysmocom s.f.m.c. GmbH
6 *
7 * All Rights Reserved
8 *
9 * This program is free software; you can redistribute it and/or modify
10 * it under the terms of the GNU General Public License as published by
11 * the Free Software Foundation; either version 3 of the License, or
12 * (at your option) any later version.
13 *
14 * This program is distributed in the hope that it will be useful,
15 * but WITHOUT ANY WARRANTY; without even the implied warranty of
16 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 * GNU General Public License for more details.
18 *
19 * You should have received a copy of the GNU General Public License along
20 * with this program; if not, write to the Free Software Foundation, Inc.,
21 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
22 */
23"""
24
Max566f2a72017-12-21 14:38:39 +010025__version__ = "0.7.1" # bump this on every non-trivial change
Maxe732c2c2017-11-23 16:42:59 +010026
27from osmopy.osmo_ipa import Ctrl, IPA
28from twisted.internet.protocol import ReconnectingClientFactory
29from twisted.internet import reactor
30from twisted.protocols import basic
31import argparse, logging, sys
32
33class IPACommon(basic.Int16StringReceiver):
34 """
35 Generic IPA protocol handler: include some routines for simpler subprotocols.
36 It's not intended as full implementation of all subprotocols, rather common ground and example code.
37 """
38 def dbg(self, line):
39 """
40 Debug print helper
41 """
42 self.factory.log.debug(line)
43
44 def osmo_CTRL(self, data):
45 """
46 OSMO CTRL protocol
47 Placeholder, see corresponding derived class
48 """
49 pass
50
51 def osmo_MGCP(self, data):
52 """
53 OSMO MGCP extension
54 """
55 self.dbg('OSMO MGCP received %s' % data)
56
57 def osmo_LAC(self, data):
58 """
59 OSMO LAC extension
60 """
61 self.dbg('OSMO LAC received %s' % data)
62
63 def osmo_SMSC(self, data):
64 """
65 OSMO SMSC extension
66 """
67 self.dbg('OSMO SMSC received %s' % data)
68
69 def osmo_ORC(self, data):
70 """
71 OSMO ORC extension
72 """
73 self.dbg('OSMO ORC received %s' % data)
74
75 def osmo_GSUP(self, data):
76 """
77 OSMO GSUP extension
78 """
79 self.dbg('OSMO GSUP received %s' % data)
80
81 def osmo_OAP(self, data):
82 """
83 OSMO OAP extension
84 """
85 self.dbg('OSMO OAP received %s' % data)
86
Harald Weltedbb57eb2018-10-13 13:15:51 +020087 def osmo_RSPRO(self, data):
88 """
89 OSMO RSPRO (Remote Sim Protocol) extension
90 """
91 self.dbg('OSMO RSPRO received %s' % data)
92
Maxe732c2c2017-11-23 16:42:59 +010093 def osmo_UNKNOWN(self, data):
94 """
95 OSMO defaul extension handler
96 """
97 self.dbg('OSMO unknown extension received %s' % data)
98
99 def handle_RSL(self, data, proto, extension):
100 """
101 RSL protocol handler
102 """
103 self.dbg('IPA RSL received message with extension %s' % extension)
104
105 def handle_CCM(self, data, proto, msgt):
106 """
107 CCM (IPA Connection Management)
108 Placeholder, see corresponding derived class
109 """
110 pass
111
112 def handle_SCCP(self, data, proto, extension):
113 """
114 SCCP protocol handler
115 """
116 self.dbg('IPA SCCP received message with extension %s' % extension)
117
118 def handle_OML(self, data, proto, extension):
119 """
120 OML protocol handler
121 """
122 self.dbg('IPA OML received message with extension %s' % extension)
123
124 def handle_OSMO(self, data, proto, extension):
125 """
126 Dispatcher point for OSMO subprotocols based on extension name, lambda default should never happen
127 """
128 method = getattr(self, 'osmo_' + IPA().ext(extension), lambda: "extension dispatch failure")
129 method(data)
130
131 def handle_MGCP(self, data, proto, extension):
132 """
133 MGCP protocol handler
134 """
135 self.dbg('IPA MGCP received message with attribute %s' % extension)
136
137 def handle_UNKNOWN(self, data, proto, extension):
138 """
139 Default protocol handler
140 """
141 self.dbg('IPA received message for %s (%s) protocol with attribute %s' % (IPA().proto(proto), proto, extension))
142
143 def process_chunk(self, data):
144 """
145 Generic message dispatcher for IPA (sub)protocols based on protocol name, lambda default should never happen
146 """
147 (_, proto, extension, content) = IPA().del_header(data)
148 if content is not None:
149 self.dbg('IPA received %s::%s [%d/%d] %s' % (IPA().proto(proto), IPA().ext_name(proto, extension), len(data), len(content), content))
150 method = getattr(self, 'handle_' + IPA().proto(proto), lambda: "protocol dispatch failure")
151 method(content, proto, extension)
152
153 def dataReceived(self, data):
154 """
155 Override for dataReceived from Int16StringReceiver because of inherently incompatible interpretation of length
156 If default handler is used than we would always get off-by-1 error (Int16StringReceiver use equivalent of l + 2)
157 """
158 if len(data):
159 (head, tail) = IPA().split_combined(data)
160 self.process_chunk(head)
161 self.dataReceived(tail)
162
163 def connectionMade(self):
164 """
165 We have to resetDelay() here to drop internal state to default values to make reconnection logic work
166 Make sure to call this via super() if overriding to keep reconnection logic intact
167 """
168 addr = self.transport.getPeer()
169 self.dbg('IPA connected to %s:%d peer' % (addr.host, addr.port))
170 self.factory.resetDelay()
171
172
173class CCM(IPACommon):
174 """
175 Implementation of CCM protocol for IPA multiplex
176 """
177 def ack(self):
178 self.transport.write(IPA().id_ack())
179
180 def ping(self):
181 self.transport.write(IPA().ping())
182
183 def pong(self):
184 self.transport.write(IPA().pong())
185
186 def handle_CCM(self, data, proto, msgt):
187 """
188 CCM (IPA Connection Management)
189 Only basic logic necessary for tests is implemented (ping-pong, id ack etc)
190 """
191 if msgt == IPA.MSGT['ID_GET']:
192 self.transport.getHandle().sendall(IPA().id_resp(self.factory.ccm_id))
193 # if we call
194 # self.transport.write(IPA().id_resp(self.factory.test_id))
195 # instead, than we would have to also call
196 # reactor.callLater(1, self.ack)
197 # instead of self.ack()
198 # otherwise the writes will be glued together - hence the necessity for ugly hack with 1s timeout
199 # Note: this still might work depending on the IPA implementation details on the other side
200 self.ack()
201 # schedule PING in 4s
202 reactor.callLater(4, self.ping)
203 if msgt == IPA.MSGT['PING']:
204 self.pong()
205
206
207class CTRL(IPACommon):
208 """
209 Implementation of Osmocom control protocol for IPA multiplex
210 """
211 def ctrl_SET(self, data, op_id, v):
212 """
213 Handle CTRL SET command
214 """
215 self.dbg('CTRL SET [%s] %s' % (op_id, v))
216
217 def ctrl_SET_REPLY(self, data, op_id, v):
218 """
219 Handle CTRL SET reply
220 """
221 self.dbg('CTRL SET REPLY [%s] %s' % (op_id, v))
222
223 def ctrl_GET(self, data, op_id, v):
224 """
225 Handle CTRL GET command
226 """
227 self.dbg('CTRL GET [%s] %s' % (op_id, v))
228
229 def ctrl_GET_REPLY(self, data, op_id, v):
230 """
231 Handle CTRL GET reply
232 """
233 self.dbg('CTRL GET REPLY [%s] %s' % (op_id, v))
234
235 def ctrl_TRAP(self, data, op_id, v):
236 """
237 Handle CTRL TRAP command
238 """
239 self.dbg('CTRL TRAP [%s] %s' % (op_id, v))
240
241 def ctrl_ERROR(self, data, op_id, v):
242 """
243 Handle CTRL ERROR reply
244 """
245 self.dbg('CTRL ERROR [%s] %s' % (op_id, v))
246
247 def osmo_CTRL(self, data):
248 """
249 OSMO CTRL message dispatcher, lambda default should never happen
250 For basic tests only, appropriate handling routines should be replaced: see CtrlServer for example
251 """
Max566f2a72017-12-21 14:38:39 +0100252 self.dbg('OSMO CTRL received %s::%s' % Ctrl().parse_kv(data))
Maxe732c2c2017-11-23 16:42:59 +0100253 (cmd, op_id, v) = data.decode('utf-8').split(' ', 2)
254 method = getattr(self, 'ctrl_' + cmd, lambda: "CTRL unknown command")
255 method(data, op_id, v)
256
257
258class IPAServer(CCM):
259 """
260 Test implementation of IPA server
261 Demonstrate CCM opearation by overriding necessary bits from CCM
262 """
263 def connectionMade(self):
264 """
265 Keep reconnection logic working by calling routine from CCM
266 Initiate CCM upon connection
267 """
268 addr = self.transport.getPeer()
269 self.factory.log.info('IPA server: connection from %s:%d client' % (addr.host, addr.port))
270 super(IPAServer, self).connectionMade()
271 self.transport.write(IPA().id_get())
272
273
274class CtrlServer(CTRL):
275 """
276 Test implementation of CTRL server
277 Demonstarte CTRL handling by overriding simpler routines from CTRL
278 """
279 def connectionMade(self):
280 """
281 Keep reconnection logic working by calling routine from CTRL
282 Send TRAP upon connection
283 Note: we can't use sendString() because of it's incompatibility with IPA interpretation of length prefix
284 """
285 addr = self.transport.getPeer()
286 self.factory.log.info('CTRL server: connection from %s:%d client' % (addr.host, addr.port))
287 super(CtrlServer, self).connectionMade()
288 self.transport.write(Ctrl().trap('LOL', 'what'))
289 self.transport.write(Ctrl().trap('rulez', 'XXX'))
290
291 def reply(self, r):
292 self.transport.write(Ctrl().add_header(r))
293
294 def ctrl_SET(self, data, op_id, v):
295 """
296 CTRL SET command: always succeed
297 """
298 self.dbg('SET [%s] %s' % (op_id, v))
299 self.reply('SET_REPLY %s %s' % (op_id, v))
300
301 def ctrl_GET(self, data, op_id, v):
302 """
303 CTRL GET command: always fail
304 """
305 self.dbg('GET [%s] %s' % (op_id, v))
306 self.reply('ERROR %s No variable found' % op_id)
307
308
309class IPAFactory(ReconnectingClientFactory):
310 """
311 Generic IPA Client Factory which can be used to store state for various subprotocols and manage connections
312 Note: so far we do not really need separate Factory for acting as a server due to protocol simplicity
313 """
314 protocol = IPACommon
315 log = None
316 ccm_id = IPA().identity(unit=b'1515/0/1', mac=b'b0:0b:fa:ce:de:ad:be:ef', utype=b'sysmoBTS', name=b'StingRay', location=b'hell', sw=IPA.version.encode('utf-8'))
317
318 def __init__(self, proto=None, log=None, ccm_id=None):
319 if proto:
320 self.protocol = proto
321 if ccm_id:
322 self.ccm_id = ccm_id
323 if log:
324 self.log = log
325 else:
326 self.log = logging.getLogger('IPAFactory')
327 self.log.setLevel(logging.CRITICAL)
328 self.log.addHandler(logging.NullHandler)
329
330 def clientConnectionFailed(self, connector, reason):
331 """
332 Only necessary for as debugging aid - if we can somehow set parent's class noisy attribute then we can omit this method
333 """
334 self.log.warning('IPAFactory connection failed: %s' % reason.getErrorMessage())
335 ReconnectingClientFactory.clientConnectionFailed(self, connector, reason)
336
337 def clientConnectionLost(self, connector, reason):
338 """
339 Only necessary for as debugging aid - if we can somehow set parent's class noisy attribute then we can omit this method
340 """
341 self.log.warning('IPAFactory connection lost: %s' % reason.getErrorMessage())
342 ReconnectingClientFactory.clientConnectionLost(self, connector, reason)
343
344
345if __name__ == '__main__':
346 p = argparse.ArgumentParser("Twisted IPA (module v%s) app" % IPA.version)
347 p.add_argument('-v', '--version', action='version', version="%(prog)s v" + __version__)
348 p.add_argument('-p', '--port', type=int, default=4250, help="Port to use for CTRL interface")
349 p.add_argument('-d', '--host', default='localhost', help="Adress to use for CTRL interface")
350 cs = p.add_mutually_exclusive_group()
351 cs.add_argument("-c", "--client", action='store_true', help="asume client role")
352 cs.add_argument("-s", "--server", action='store_true', help="asume server role")
353 ic = p.add_mutually_exclusive_group()
354 ic.add_argument("--ipa", action='store_true', help="use IPA protocol")
355 ic.add_argument("--ctrl", action='store_true', help="use CTRL protocol")
356 args = p.parse_args()
357 test = False
358
359 log = logging.getLogger('TwistedIPA')
360 log.setLevel(logging.DEBUG)
361 log.addHandler(logging.StreamHandler(sys.stdout))
362
363 if args.ctrl:
364 if args.client:
365 # Start osmo-bsc to receive TRAP messages when osmo-bts-* connects to it
366 print('CTRL client, connecting to %s:%d' % (args.host, args.port))
367 reactor.connectTCP(args.host, args.port, IPAFactory(CTRL, log))
368 test = True
369 if args.server:
370 # Use bsc_control.py to issue set/get commands
371 print('CTRL server, listening on port %d' % args.port)
372 reactor.listenTCP(args.port, IPAFactory(CtrlServer, log))
373 test = True
374 if args.ipa:
375 if args.client:
376 # Start osmo-nitb which would initiate A-bis/IP session
377 print('IPA client, connecting to %s ports %d and %d' % (args.host, IPA.TCP_PORT_OML, IPA.TCP_PORT_RSL))
378 reactor.connectTCP(args.host, IPA.TCP_PORT_OML, IPAFactory(CCM, log))
379 reactor.connectTCP(args.host, IPA.TCP_PORT_RSL, IPAFactory(CCM, log))
380 test = True
381 if args.server:
382 # Start osmo-bts-* which would attempt to connect to us
383 print('IPA server, listening on ports %d and %d' % (IPA.TCP_PORT_OML, IPA.TCP_PORT_RSL))
384 reactor.listenTCP(IPA.TCP_PORT_RSL, IPAFactory(IPAServer, log))
385 reactor.listenTCP(IPA.TCP_PORT_OML, IPAFactory(IPAServer, log))
386 test = True
387 if test:
388 reactor.run()
389 else:
390 print("Please specify which protocol in which role you'd like to test.")