def send_heartbeat(url, key): json_msg = {} json_msg['method'] = 'heartbeat' json_msg['id'] = 4 params = {} params['kiosk-id'] = DIEID if UPTIME < 60: # under a minute uptimestr = "%.2f seconds" % UPTIME elif UPTIME < 60*60: # under a hour uptimestr = "%.2f minutes" % (UPTIME/(60.0)) elif UPTIME < 60*60*24*3: # under 3 days uptimestr = "%.2f hours" % (UPTIME/(60.0*60.0)) else: # over 3 days uptimestr = "%.2f days" % (UPTIME/(60.0*60.0*24.0)) params['uptime'] = uptimestr params['sw-version'] = '1.0.0' params['time'] = datetime.utcnow() json_msg['params'] = params jsstr = json.dumps(json_msg) hash = MD5.new(jsstr).digest() baseconverter = BaseConverter('0123456789abcdef') sign_16encode = baseconverter.from_decimal(key.sign(hash, "")[0]) #print "encoded: %s" % sign_16encode #print "signature: %d" % key.sign(hash, "")[0] #print "hash: %s" % "".join(["%02x " % ord(x) for x in hash]) headers = {'X-eko-signature': sign_16encode} print jsstr #test decoding x = key.publickey().verify(hash, (baseconverter.to_decimal(sign_16encode),)) req = urllib2.Request(url, jsstr, headers) response = urllib2.urlopen(req) the_page = response.read() print the_page
def get_messages(url, key): json_msg = {} json_msg['method'] = 'get_messages' json_msg['id'] = 4 json_msg['params'] = {'kiosk-id': DIEID} jsstr = json.dumps(json_msg) hash = MD5.new(jsstr).digest() baseconverter = BaseConverter('0123456789abcdef') sign_16encode = baseconverter.from_decimal(key.sign(hash, "")[0]) #print "encoded: %s" % sign_16encode #print "signature: %d" % key.sign(hash, "")[0] #print "hash: %s" % "".join(["%02x " % ord(x) for x in hash]) headers = {'X-eko-signature': sign_16encode} print jsstr #test decoding x = key.publickey().verify(hash, (baseconverter.to_decimal(sign_16encode),)) req = urllib2.Request(url, jsstr, headers) response = urllib2.urlopen(req) the_page = response.read() print the_page
class JSONRPCMethods(object): """ .. py:class:: JSONRPCMethods(request) Contains methods accessible by the JSON RPC. :param request: The WebOb request object. """ def __init__(self, request): self.request = request self.bconv = BaseConverter('0123456789abcdef') def _extract_signature_hdr(self): """ .. py:func:: _extract_signature_hdr() Extracts the signature from the request header and calculates the request hash. :rtype (hash, (signature,)): A tuple containing the hash and signature. """ hash = MD5.new(self.request.body).digest() try: signature = self.bconv.to_decimal(self.request.headers['X-eko-signature']) except KeyError: logging.critical("Client message did not provide verification signature") # useful for debugging RSA issues logging.debug("signature long : %d ::: " % signature) logging.debug("hdr: %s" % self.request.headers['X-eko-signature']) logging.debug("hash: %s" % "".join(["%02x " % ord(x) for x in hash])) return (hash, (signature,)) def _get_public_key(self, kiosk): """ .. py:func:: _get_public_key(kiosk) Returns the public key for this kiosk, either from memcache or the datastore. :param kiosk: The kiosk object. :rtype RSAobj: A public key object. """ # check memcache pubkey = memcache.get(kiosk.dieid, namespace='rsa-publickeys') if pubkey is not None: return pubkey else: # get encoded public key from database pubkey_n = self.bconv.to_decimal(kiosk.pubkey_n) pubkey_e = self.bconv.to_decimal(kiosk.pubkey_e) #create the public key try: pubkey = RSA.construct((pubkey_n, pubkey_e)) except: logging.critical("Could not create public key for kiosk") return None if not memcache.add(kiosk.dieid, pubkey, namespace='rsa-publickeys'): logging.error("Unable to save rsa key to memcache.") return pubkey def get_messages(self, method, id, **kwargs): """ .. py:func:: get_messages(method, id, **kwargs) Returns all server messages that are pending for the kiosk. :param method: The name of the requested json method (should be equal to 'get_messages') :param id: Unused :param **kwargs: Keyword arguments to the function. Should have non-unicode keys. :rtype dict: The response object. Expects the following in kwargs: kiosk-id : The unique kiosk identifier Expects the following headers: X-eko-signature: RSA signed hash of request body. """ dieid = kwargs['kiosk-id'] logging.debug('device with id : %s is requesting incoming messages.' % dieid) # grab kiosk, internally utilises memcache kiosk = Kiosk.kiosk_from_dieid(dieid) if not kiosk: logging.warn('Unrecognised kiosk attempted to fetch server messages (IP: %s, dieid %s).' % (self.request.remote_addr, dieid)) return self._standard_error_response(method, id, 'Not recognised', 20, 'Kiosk not registered with system.') logging.info('Receiving message fetch request from kiosk %s at IP %s' % (kiosk.name, self.request.remote_addr)) hash, signature = self._extract_signature_hdr() if not signature[0]: # prepare error response return self._standard_error_response(method, id, 'Signature Absent', 10, 'Verification signature not present.') pubkey = self._get_public_key(kiosk) if not pubkey: return self._standard_error_response(method, id, 'Signature Fail', 11, 'Unable to create public key from config.') if pubkey and signature[0]: if pubkey.verify(hash, signature): #message is authentic messages = self._get_server_messages(kiosk) data = {} data['result'] = messages data['error'] = None data['id'] = id return data else: return self._standard_error_response(method, id, 'Signature Incorrect', 12, 'Verification signature failed check.') def _get_server_messages(self, kiosk): """ .. py:func:: _get_server_messages(kiosk) Returns server messages that haven't yet been downloaded by a kiosk. :param kiosk: The kiosk object which owns the messages. """ messages = kiosk.servermessage_set.filter('retrieved =', False).fetch(10) response = [] for message in messages: dict = {} dict['msg'] = message.message dict['msg_type'] = message.msg_type dict['date'] = message.date message.retrieved = True message.retrieved_date = datetime.utcnow() message.put() response.append(dict) return response def _standard_error_response(self, func, id, ename, ecode, edesc): """Creates a json object in the format of an error""" resp = {} error_dict = {} error_dict['name'] = ename error_dict['code'] = ecode error_dict['message'] = edesc resp['method'] = func resp['result'] = None resp['id'] = id resp['error'] = error_dict return resp def heartbeat(self, method, id, **kwargs): """ .. py:func:: heartbeat(method, id, **kwargs) Registers a kiosks IP address, software version and uptime in the datastore. :param method: The name of the requested json method (should be equal to 'heartbeat') :param id: Unused :param **kwargs: Keyword arguments to the function. Should have non-unicode keys. :rtype dict: The response object. Expects the following in kwargs: kiosk-id : The unique kiosk identifier uptime: A string representing the time elapsed since the script executed. sw_version: Version of the datalogger software running on the device. time: The UTC time on the datalogger. Expects the following headers: X-eko-signature: RSA signed hash of request body. """ # extract arguments dieid = kwargs['kiosk-id'] logging.debug('Device with id : %s has a pulse from %s.' % (dieid, self.request.remote_addr)) uptime = kwargs['uptime'] sw_version = kwargs['sw-version'] local_inetadr = kwargs['rwanda-ip'] time = kwargs['time'] # find the kiosk from the kiosk id kiosk = Kiosk.kiosk_from_dieid(dieid) if not kiosk: logging.warn('Unrecognised kiosk attempted %s (IP: %s, sw_version: %s, dieid: %s).' % (method, self.request.remote_addr, sw_version, dieid)) return self._standard_error_response(method, id, 'Not recognised', 20, 'Kiosk not registered with system.') # log this event logging.info('Receiving kiosk heartbeat from kiosk %s at IP %s' % (kiosk.name, self.request.remote_addr)) hash, signature = self._extract_signature_hdr() if not signature[0]: # prepare error response return self._standard_error_response(method, id, 'Signature Absent', 10, 'Verification signature not present.') pubkey = self._get_public_key(kiosk) if not pubkey: return self._standard_error_response(method, id, 'Signature Fail', 11, 'Unable to create public key from config.') if pubkey and signature: if pubkey.verify(hash, signature): #message is authentic self._create_heartbeat(kiosk, uptime, sw_version, time, self.request.remote_addr, local_inetadr) data = {} data['result'] = 'Success' data['error'] = None data['id'] = id return data else: return self._standard_error_response(method, id, 'Signature Incorrect', 12, 'Verification signature failed check.') # registers the heartbeat in the data store def _create_heartbeat(self, kiosk, uptime, sw_version, time, ip, local_inetadr): """ .. py:func:: _create_heartbeat(kiosk, uptime, sw_version, time, ip) Creates a heartbeat record in the datastore. """ h = Heartbeat() h.client_ip = ip h.client_intip = local_inetadr h.client_uptime = uptime h.kiosk = kiosk h.software_version = sw_version if isinstance(time, datetime): h.client_time = time else: h.client_time = datetime.utcnow() logging.warn("Client sent me a non datetime object in the request!") h.server_time = datetime.utcnow() oldh = memcache.get(kiosk.dieid, namespace='latest-beats') # if we have cached a kiosk which has the same IP as this request, just update the cache if oldh: if oldh.client_ip != h.client_ip: # if the IP has changed, add to the db if not oldh.is_saved(): # old h is the last hb at that address oldh.put() h.put() elif h.bad_skew(): # if the skew is worth noting, add to the db logging.debug("Commiting to db for bad skew!") h.put() else: # we dont have anything cached, so add anyway h.put() memcache.set(kiosk.dieid, h, namespace='latest-beats') logging.debug("Heartbeat registered from %s@%s" % (kiosk.name, ip)) return def post_messages(self, method, id, **kwargs): """ .. py:func:: post_messages(method, id, **kwargs) Registers a message from a kiosk in the datastore. :param method: The name of the requested json method (should be equal to 'post_messages') :param id: Unused :param **kwargs: Keyword arguments to the function. Should have non-unicode keys. :rtype dict: The response object. Expects the following in kwargs: kiosk-id : The unique kiosk identifier message: A text message. origin: The origin of the message (subsystem). date: The UTC time on the datalogger that the message originated. [session-ref] : An optional reference to a upload session. Expects the following headers: X-eko-signature: RSA signed hash of request body. """ # extract arguments dieid = kwargs['kiosk-id'] logging.debug('Device with id : %s has a pulse from %s.' % (dieid, self.request.remote_addr)) messages = kwargs['messages'] # find the kiosk from the kiosk id logging.debug('device with id : %s has a pulse.' % dieid) kiosk = Kiosk.kiosk_from_dieid(dieid) if not kiosk: logging.warn('Unrecognised kiosk attempted %s (IP: %s, dieid: %s).' % (method, self.request.remote_addr, dieid)) return self._standard_error_response(method, id, 'Not recognised', 20, 'Kiosk not registered with system.') # log this event logging.info('Receiving %d kiosk messages from kiosk %s at IP %s' % (len(messages), kiosk.name, self.request.remote_addr)) hash, signature = self._extract_signature_hdr() if not signature[0]: # prepare error response return self._standard_error_response(method, id, 'Signature Absent', 10, 'Verification signature not present.') pubkey = self._get_public_key(kiosk) if not pubkey: return self._standard_error_response(method, id, 'Signature Fail', 11, 'Unable to create public key from config.') if pubkey and signature: if pubkey.verify(hash, signature): #message is authentic for message in messages: self._create_clientmsg(kiosk, message, self.request.remote_addr) data = {} data['result'] = 'Success' data['error'] = None data['id'] = id return data else: return self._standard_error_response(method, id, 'Signature Incorrect', 12, 'Verification signature failed check.') # registers the heartbeat in the data store def _create_clientmsg(self, kiosk, message, ip): """ .. py:func:: _create_clientmsg(kiosk, message) Creates a kisok message record in the datastore. """ h = KioskMessage() h.ip = ip h.date = datetime.utcnow() h.kiosk = kiosk if 'session-ref' in message.keys(): session_uuid = message['session-ref'] else: session_uuid = None if 'message' in message.keys(): if message['message'] is not None: h.message = message['message'] else: logging.error("No message in kiosk msg from %s" % kiosk.dieid) if 'origin' in message.keys(): if message['origin'] is not None: h.origin = message['origin'] else: logging.error("No origin for kiosk msg from %s" % kiosk.dieid) if 'origin-date' in message.keys(): if message['origin-date'] is not None: if isinstance(message['origin-date'], datetime): h.origin_date = message['origin-date'] else: try: h.origin_date = datetime.strptime(message['origin-date'], '%Y-%m-%dT%H:%M:%S') except ValueError: logging.exception("Cannot treat %s as datetime." % message['origin-date']) else: logging.error("No origin date for kiosk msg from %s" % kiosk.dieid) if session_uuid is not None: sess = db.GqlQuery("SELECT __key__ FROM SyncSession WHERE client_ref = :1", session_uuid).get() if sess: h.session_ref = sess h.put() logging.info("Kiosk Message registered from %s@%s" % (kiosk.name, ip)) return
def post(self): logging.debug('Running File Upload Handler') bconv = BaseConverter('0123456789abcdef') dieid = self.request.get('kiosk-id') if not dieid: self.error(403) self.response.write('No device id provided.\n') logger.warn('Device attempted contact without die id from ip %s.' % self.request.remote_addr) return logging.info("File upload incoming from kiosk : %s" % dieid) kiosk = Kiosk.kiosk_from_dieid(dieid) if not kiosk: self.error(400) self.response.write('Kiosk is unregistered on system.\n') logging.warn('Unregistered kiosk on ip %s with dieid %s.' % (self.request.remote_addr, dieid)) return logging.debug("Encoded sig: %s." % self.request.headers['X-eko-signature']) # look for the signature try: signature = bconv.to_decimal(self.request.headers['X-eko-signature']) logging.debug("Decoded sig: %s." % str(signature)) challenge = self.request.headers['X-eko-challenge'] logging.debug("Challenge: %s." % challenge) # signature should be uuid we sent kiosk signed with the public key verify = self._verify_client(kiosk, signature, challenge) except: logging.exception("Authentication signature not found.\n") verify = False # auth failed if not verify: logging.error("Kiosk id %s did not pass id check." % dieid) self.response.write('Unable to verify identity of kiosk.\n') return # a uuid that identifies this data packet on the remote device client_ref = self.request.headers['X-eko-challenge'] if not client_ref: logging.error("No client reference provided.") self.response.write('Client Error: No reference provided.') return # the type of data being sent (logs, readings, etc...) type = self.request.get('type') # version of the client software_version = self.request.get('software_version') # manifest try: manifest = self.get_uploads(field_name='manifest')[0] logging.debug("Manifest uploaded, size: %s" % str(manifest.size)) except: logging.exception("Manifest missing from upload data.") self.response.write('Manifest Not Found\n') # payload try: payload = self.get_uploads(field_name='payload')[0] except: logging.exception("Payload missing from upload data.") self.response.write('Payload Not Found\n') sync = SyncSession.get_by_clientref(client_ref) if not sync: logging.error("Upload attempt without requesting sync session.") self.response.write('Upload request improperly handled.') return sync.kiosk = kiosk sync.data_type = type sync.payload_size = 0 # payload size + manifest size if payload: sync.payload_size += payload.size if manifest: sync.payload_size += manifest.size sync.payload = payload sync.manifest = manifest sync.software_version = software_version #sync.client_ip = self.request.remote_addr sync.end_date = datetime.utcnow() try: sync.put() logging.debug("Sync packet succesfully added to datastore") self.response.write('Success\n') except: logging.exception("Adding sync packet failed.") self.response.write('Failure\n') return