├── requirements-websocket.txt ├── requirements.txt ├── README.md ├── .gitignore ├── LICENSE ├── serverTCP.py ├── serverWebSocket.py └── protocolBase.py /requirements-websocket.txt: -------------------------------------------------------------------------------- 1 | autobahn -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | twisted 2 | txzmq 3 | requests -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Pushjet-Connectors 2 | These connectors are built to allow users to listen for messages using TCP and websockets. 3 | 4 | *todo:* publish init.d and systemd scripts 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | *.egg-info/ 23 | .installed.cfg 24 | *.egg 25 | 26 | # PyInstaller 27 | # Usually these files are written by a python script from a template 28 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 29 | *.manifest 30 | *.spec 31 | 32 | # Installer logs 33 | pip-log.txt 34 | pip-delete-this-directory.txt 35 | 36 | # Unit test / coverage reports 37 | htmlcov/ 38 | .tox/ 39 | .coverage 40 | .coverage.* 41 | .cache 42 | nosetests.xml 43 | coverage.xml 44 | *,cover 45 | 46 | # Translations 47 | *.mo 48 | *.pot 49 | 50 | # Django stuff: 51 | *.log 52 | 53 | # Sphinx documentation 54 | docs/_build/ 55 | 56 | # PyBuilder 57 | target/ 58 | 59 | # PyCharm 60 | .idea 61 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015, Pushjet 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 15 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 16 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 17 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 18 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 19 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 20 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 21 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 22 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 23 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | 25 | -------------------------------------------------------------------------------- /serverTCP.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python2 2 | 3 | from argparse import ArgumentParser 4 | from sys import stdout 5 | 6 | from protocolBase import PushjetProtocolBase 7 | from twisted.internet import reactor, protocol 8 | from twisted.python import log 9 | 10 | 11 | class PushjetTCPBase(protocol.Protocol, PushjetProtocolBase): 12 | magic_start = '\002' 13 | magic_end = '\003' 14 | uuid_len = 36 15 | _buffer = b'' 16 | 17 | def __init__(self): 18 | PushjetProtocolBase.__init__(self, args.api, args.pub) 19 | 20 | def dataReceived(self, data): 21 | self._buffer += data 22 | if len(self._buffer) >= self.uuid_len: 23 | frame = self._buffer[:self.uuid_len] 24 | self._buffer = self._buffer[self.uuid_len:] 25 | print frame 26 | self.onClientMessage(frame) 27 | 28 | def sendMessage(self, message): 29 | self.transport.writeSequence((self.magic_start, message, self.magic_end)) 30 | 31 | 32 | class PushjetTCPBaseFactory(protocol.Factory): 33 | protocol = PushjetTCPBase 34 | 35 | def __init__(self, reactor): 36 | self.reactor = reactor 37 | 38 | if __name__ == '__main__': 39 | parser = ArgumentParser(description='Pushjet websocket server') 40 | 41 | parser.add_argument('--port', '-p', default='7171', type=int, 42 | help='the port the server should bind to (default: 7171)') 43 | parser.add_argument('--api', '-a', default='https://api.pushjet.io', type=str, metavar='SRV', 44 | help='the api server url (default: https://api.pushjet.io)') 45 | parser.add_argument('--pub', '-z', default='ipc:///tmp/pushjet-publisher.ipc', type=str, 46 | help='the publisher uri for receiving messages (default: ipc:///tmp/pushjet-publisher.ipc)') 47 | parser.add_argument('--log', '-l', default=None, type=str, metavar='PATH', dest='logfile', 48 | help='log file path') 49 | parser.add_argument('--quiet', '-q', action='store_true') 50 | 51 | args = parser.parse_args() 52 | 53 | if not args.quiet: 54 | log.startLogging(stdout) 55 | if args.logfile: 56 | log.startLogging(open(args.logfile, 'a')) 57 | print("Started logging to file %s" % args.logfile) 58 | 59 | reactor.listenTCP(args.port, PushjetTCPBaseFactory(reactor)) 60 | reactor.run() 61 | -------------------------------------------------------------------------------- /serverWebSocket.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python2.7 2 | 3 | from argparse import ArgumentParser 4 | from sys import stdout 5 | 6 | from autobahn.twisted.websocket import WebSocketServerProtocol, WebSocketServerFactory 7 | from protocolBase import PushjetProtocolBase 8 | from twisted.python import log 9 | from twisted.internet import reactor 10 | 11 | 12 | class PushjetWebSocketBase(WebSocketServerProtocol, PushjetProtocolBase): 13 | def __init__(self): 14 | PushjetProtocolBase.__init__(self, args.api, args.pub) 15 | WebSocketServerProtocol.__init__(self) 16 | self.onMessage = self.onClientMessage 17 | 18 | def onConnect(self, request): 19 | print "New connection:", request.peer 20 | 21 | def sendMessage(self, payload, **kwargs): 22 | return super(WebSocketServerProtocol, self).sendMessage(self.toAscii(payload), **kwargs) 23 | 24 | 25 | if __name__ == '__main__': 26 | parser = ArgumentParser(description='Pushjet websocket server') 27 | 28 | parser.add_argument('--host', '-u', default='127.0.0.1', type=str, 29 | help='the host the server should bind to (default: 127.0.0.1, this is sane)') 30 | parser.add_argument('--port', '-p', default='8181', type=int, 31 | help='the port the server should bind to (default: 8181)') 32 | parser.add_argument('--api', '-a', default='https://api.pushjet.io', type=str, metavar='SRV', 33 | help='the api server url (default: https://api.pushjet.io)') 34 | parser.add_argument('--pub', '-z', default='ipc:///tmp/pushjet-publisher.ipc', type=str, 35 | help='the publisher uri for receiving messages (default: ipc:///tmp/pushjet-publisher.ipc)') 36 | parser.add_argument('--log', '-l', default=None, type=str, metavar='PATH', dest='logfile', 37 | help='log file path') 38 | parser.add_argument('--quiet', '-q', action='store_true') 39 | 40 | args = parser.parse_args() 41 | 42 | if not args.quiet: 43 | log.startLogging(stdout) 44 | if args.logfile: 45 | log.startLogging(open(args.logfile, 'a')) 46 | print("Started logging to file %s" % args.logfile) 47 | 48 | wsUri = 'ws://%s:%i' % (args.host, args.port) 49 | 50 | factory = WebSocketServerFactory(wsUri, debug=False) 51 | factory.protocol = PushjetWebSocketBase 52 | 53 | reactor.listenTCP(args.port, factory) 54 | reactor.run() 55 | -------------------------------------------------------------------------------- /protocolBase.py: -------------------------------------------------------------------------------- 1 | from re import compile 2 | from txzmq import ZmqEndpoint, ZmqFactory, ZmqSubConnection, ZmqEndpointType 3 | import json 4 | import requests 5 | 6 | 7 | _zmqFactory = ZmqFactory() 8 | 9 | 10 | class PushjetProtocolBase(object): 11 | _uuidRe = compile(r'^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$') 12 | _errorTemplate = '{"error":{"id":%i,"message":"%s"}}' 13 | 14 | def __init__(self, apiUri, pubUri): 15 | self.api = apiUri.rstrip('/') 16 | self.zmqEndpoint = ZmqEndpoint(ZmqEndpointType.connect, pubUri) 17 | self.zmq = None 18 | self.uuid = None 19 | self.subscriptions = [] 20 | 21 | @staticmethod 22 | def isUuid(s): 23 | return bool(PushjetProtocolBase._uuidRe.match(s)) 24 | 25 | def onZmqMessage(self, data): 26 | tag, message = data.split(' ', 1) 27 | self.sendMessage(message) 28 | 29 | decoded = json.loads(message) 30 | if 'message' in decoded: 31 | self.markReadAsync() 32 | if 'subscription' in decoded: 33 | token = decoded['subscription']['service']['public'] 34 | if token in self.subscriptions: 35 | self.zmq.unsubscribe(token) 36 | else: 37 | self.zmq.subscribe(token) 38 | 39 | def markReadAsync(self): 40 | self.factory.reactor.callFromThread(self.markRead) 41 | 42 | def markRead(self): 43 | url = "%s/message?uuid=%s" % (self.api, self.uuid) 44 | data = requests.delete(url).json() 45 | 46 | if 'error' in data: 47 | print "Could mark messages read for %s got error %i: %s" % ( 48 | self.uuid, data['error']['id'], data['error']['message'] 49 | ) 50 | 51 | def getMessages(self): 52 | url = "%s/message?uuid=%s" % (self.api, self.uuid) 53 | data = requests.get(url).json() 54 | 55 | if 'error' in data: 56 | print "Could fetch messages for %s got error %i: %s" % ( 57 | self.uuid, data['error']['id'], data['error']['message'] 58 | ) 59 | return [] 60 | return data['messages'] 61 | 62 | def updateSubscriptionsAsync(self): 63 | self.factory.reactor.callFromThread(self.updateSubscriptions) 64 | 65 | def updateSubscriptions(self): 66 | url = "%s/subscription?uuid=%s" % (self.api, self.uuid) 67 | subscriptions = requests.get(url).json() 68 | 69 | if 'error' in subscriptions: 70 | print "Could not fetch subscriptions for %s got error %i: %s" % ( 71 | self.uuid, subscriptions['error']['id'], subscriptions['error']['message'] 72 | ) 73 | else: 74 | tokens = [x['service']['public'] for x in subscriptions['subscriptions']] 75 | 76 | # Make sure we are always subscriptioning to messages that are meant 77 | # for our client 78 | tokens.append(self.uuid) 79 | 80 | unsubscribe = [self.toAscii(x) for x in self.subscriptions if x not in tokens] 81 | subscribe = [self.toAscii(x) for x in tokens if x not in self.subscriptions] 82 | self.subscriptions = tokens 83 | 84 | map(self.zmq.unsubscribe, unsubscribe) 85 | map(self.zmq.subscribe, subscribe) 86 | print "Successfully updated subscriptions for %s" % self.uuid 87 | 88 | def onClientMessage(self, payload, binary=False): 89 | if binary: 90 | message = self._errorTemplate % (-1, 'Expected text got binary data') 91 | self.sendMessage(message) 92 | elif self.uuid: # Already initialized 93 | return 94 | elif not self.isUuid(payload): 95 | message = self._errorTemplate % (1, 'Invalid client uuid') 96 | self.sendMessage(message) 97 | else: # Initialize ZMQ 98 | self.uuid = payload 99 | 100 | self.zmq = ZmqSubConnection(_zmqFactory, self.zmqEndpoint) 101 | self.zmq.gotMessage = self.onZmqMessage 102 | self.updateSubscriptionsAsync() 103 | 104 | self.sendMessage('{"status": "ok"}') 105 | 106 | msg = self.getMessages() 107 | for m in msg: 108 | self.sendMessage(json.dumps({'message': m})) 109 | 110 | @staticmethod 111 | def toAscii(s): 112 | return s.encode('ascii', 'ignore') 113 | 114 | def sendMessage(self, message): 115 | raise NotImplementedError() 116 | --------------------------------------------------------------------------------