Max | 2450bc2 | 2016-08-02 16:54:55 +0200 | [diff] [blame] | 1 | #!/usr/bin/python2 |
| 2 | |
| 3 | mod_license = ''' |
| 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 | |
| 25 | import sys, argparse, random, logging, tornado.ioloop, tornado.web, tornado.tcpclient, tornado.httpclient, eventsource, bsc_control |
| 26 | from eventsource import listener, request |
| 27 | |
| 28 | ''' |
| 29 | N. B: this is not an example of building proper REST API or building secure web application. |
| 30 | It's only purpose is to illustrate conversion of Osmocom's Control Interface to web-friendly API. |
| 31 | Exposing this to Internet while connected to production network might lead to all sorts of mischief and mayhem |
| 32 | from NSA' TAO breaking into your network to zombie apocalypse. Do NOT do that. |
| 33 | ''' |
| 34 | |
| 35 | token = None |
| 36 | stream = None |
| 37 | url = None |
| 38 | |
| 39 | ''' |
| 40 | Returns json according to following schema - see http://json-schema.org/documentation.html for details: |
| 41 | { |
| 42 | "title": "Ctrl Schema", |
| 43 | "type": "object", |
| 44 | "properties": { |
| 45 | "variable": { |
| 46 | "type": "string" |
| 47 | }, |
| 48 | "varlue": { |
| 49 | "type": "string" |
| 50 | } |
| 51 | }, |
| 52 | "required": ["interface", "variable", "value"] |
| 53 | } |
| 54 | Example validation from command-line: |
| 55 | json validate --schema-file=schema.json --document-file=data.json |
| 56 | The interface is represented as string because it might look different for IPv4 vs v6. |
| 57 | ''' |
| 58 | |
| 59 | def read_header(data): |
| 60 | t_length = bsc_control.ipa_ctrl_header(data) |
| 61 | if (t_length): |
| 62 | stream.read_bytes(t_length - 1, callback = read_trap) |
| 63 | else: |
| 64 | print >> sys.stderr, "protocol error: length missing in %s!" % data |
| 65 | |
| 66 | @tornado.gen.coroutine |
| 67 | def read_trap(data): |
| 68 | (t, z, v, p) = data.split() |
| 69 | if (t != 'TRAP' or int(z) != 0): |
| 70 | print >> sys.stderr, "protocol error: TRAP != %s or 0! = %d" % (t, int(z)) |
| 71 | else: |
| 72 | yield tornado.httpclient.AsyncHTTPClient().fetch(tornado.httpclient.HTTPRequest(url = "%s/%s/%s" % (url, "ping", token), |
| 73 | method = 'POST', |
| 74 | headers = {'Content-Type': 'application/json'}, |
| 75 | body = tornado.escape.json_encode({ 'variable' : v, 'value' : p }))) |
| 76 | stream.read_bytes(4, callback = read_header) |
| 77 | |
| 78 | @tornado.gen.coroutine |
| 79 | def trap_setup(host, port, target_host, target_port, tk): |
| 80 | global stream |
| 81 | global url |
| 82 | global token |
| 83 | token = tk |
| 84 | url = "http://%s:%s/sse" % (host, port) |
| 85 | stream = yield tornado.tcpclient.TCPClient().connect(target_host, target_port) |
| 86 | stream.read_bytes(4, callback = read_header) |
| 87 | |
| 88 | def get_v(s, v): |
| 89 | return { 'variable' : v, 'value' : bsc_control.get_var(s, tornado.escape.native_str(v)) } |
| 90 | |
| 91 | class CtrlHandler(tornado.web.RequestHandler): |
| 92 | def initialize(self): |
| 93 | self.skt = bsc_control.connect(self.settings['ctrl_host'], self.settings['ctrl_port']) |
| 94 | |
| 95 | def get(self, v): |
| 96 | self.write(get_v(self.skt, v)) |
| 97 | |
| 98 | def post(self): |
| 99 | self.write(get_v(self.skt, self.get_argument("variable"))) |
| 100 | |
| 101 | class SetCtrl(CtrlHandler): |
| 102 | def get(self, var, val): |
| 103 | bsc_control.set_var(self.skt, tornado.escape.native_str(var), tornado.escape.native_str(val)) |
| 104 | super(SetCtrl, self).get(tornado.escape.native_str(var)) |
| 105 | |
| 106 | def post(self): |
| 107 | bsc_control.set_var(self.skt, tornado.escape.native_str(self.get_argument("variable")), tornado.escape.native_str(self.get_argument("value"))) |
| 108 | super(SetCtrl, self).post() |
| 109 | |
| 110 | class Slash(tornado.web.RequestHandler): |
| 111 | def get(self): |
| 112 | self.write('<html><head><title>%s</title></head><body>Using Tornado framework v%s' |
| 113 | '<form action="/get" method="POST">' |
| 114 | '<input type="text" name="variable">' |
| 115 | '<input type="submit" value="GET">' |
| 116 | '</form>' |
| 117 | '<form action="/set" method="POST">' |
| 118 | '<input type="text" name="variable">' |
| 119 | '<input type="text" name="value">' |
| 120 | '<input type="submit" value="SET">' |
| 121 | '</form>' |
| 122 | '</body></html>' % ("Osmocom Control Interface Proxy", tornado.version)) |
| 123 | |
| 124 | if __name__ == '__main__': |
| 125 | p = argparse.ArgumentParser(description='Osmocom Control Interface proxy.') |
| 126 | p.add_argument('-c', '--control-port', type = int, default = 4252, help = "Target Control Interface port") |
| 127 | p.add_argument('-a', '--control-host', default = 'localhost', help = "Target Control Interface adress") |
| 128 | p.add_argument('-b', '--host', default = 'localhost', help = "Adress to bind proxy's web interface") |
| 129 | p.add_argument('-p', '--port', type = int, default = 6969, help = "Port to bind proxy's web interface") |
| 130 | p.add_argument('-d', '--debug', action='store_true', help = "Activate debugging (default off)") |
| 131 | p.add_argument('-t', '--token', default = 'osmocom', help = "Token to be used by SSE client in URL e. g. http://127.0.0.1:8888/poll/osmocom where 'osmocom' is default token value") |
| 132 | p.add_argument('-k', '--keepalive', type = int, default = 5000, help = "Timeout betwwen keepalive messages, in milliseconds, defaults to 5000") |
| 133 | args = p.parse_args() |
| 134 | random.seed() |
| 135 | tornado.netutil.Resolver.configure('tornado.netutil.ThreadedResolver') # Use non-blocking resolver |
| 136 | logging.basicConfig() |
| 137 | application = tornado.web.Application([ |
| 138 | (r"/", Slash), |
| 139 | (r"/get", CtrlHandler), |
| 140 | (r"/get/(.*)", CtrlHandler), |
| 141 | (r"/set", SetCtrl), |
| 142 | (r"/set/(.*)/(.*)", SetCtrl), |
| 143 | (r"/sse/(.*)/(.*)", listener.EventSourceHandler, dict(event_class = listener.JSONIdEvent, keepalive = args.keepalive)), |
| 144 | ], debug = args.debug, ctrl_host = args.control_host, ctrl_port = args.control_port) |
| 145 | application.listen(address = args.host, port = args.port) |
| 146 | trap_setup(args.host, args.port, application.settings['ctrl_host'], application.settings['ctrl_port'], args.token) |
| 147 | tornado.ioloop.IOLoop.instance().start() |