From f04dd453469b16242120c09f835708461bfb4f67 Mon Sep 17 00:00:00 2001 From: Cort Buffington Date: Tue, 10 Sep 2013 15:28:18 -0500 Subject: [PATCH] MAJOR UPDATE! Added a derived class for the rare event of an un-authenticated IPSC, this necessitated moving two global functions into the classese. Changes to prepare this as a module -- all "user space" packets that are received now have expliciet callback functions (handily located in one place) Moved the supporting characters into the ipsc sub-directory. The config is the only likely one to be touched. Improved some of the logging, but also commented it out to make things run faster when it is not in use. --- ipsc.py | 274 +++++++++++------- ipsc/.gitignore | 5 + ipsc/__init__.py | 0 ipsc_logger.py => ipsc/ipsc_logger.py | 0 ipsc_mask.py => ipsc/ipsc_mask.py | 0 .../ipsc_message_types.py | 0 .../my_ipsc_config_SAMPLE.py | 1 + 7 files changed, 179 insertions(+), 101 deletions(-) create mode 100644 ipsc/.gitignore create mode 100644 ipsc/__init__.py rename ipsc_logger.py => ipsc/ipsc_logger.py (100%) rename ipsc_mask.py => ipsc/ipsc_mask.py (100%) rename ipsc_message_types.py => ipsc/ipsc_message_types.py (100%) rename my_ipsc_config_SAMPLE.py => ipsc/my_ipsc_config_SAMPLE.py (98%) diff --git a/ipsc.py b/ipsc.py index ba57a04..bfd18ba 100644 --- a/ipsc.py +++ b/ipsc.py @@ -25,91 +25,50 @@ import socket # Import system logger configuration # try: - from ipsc_logger import logger + from ipsc.ipsc_logger import logger except ImportError: sys.exit('System logger configuraiton not found or invalid') # Import configuration and informational data structures # try: - from my_ipsc_config import NETWORK + from ipsc.my_ipsc_config import NETWORK except ImportError: sys.exit('Configuration file not found, or not valid formatting') # Import IPSC message types and version information # try: - from ipsc_message_types import * + from ipsc.ipsc_message_types import * except ImportError: sys.exit('IPSC message types file not found or invalid') # Import IPSC flag mask values # try: - from ipsc_mask import * + from ipsc.ipsc_mask import * except ImportError: sys.exit('IPSC mask values file not found or invalid') #************************************************ -# GLOBALLY SCOPED FUNCTIONS +# CALLBACK FUNCTIONS FOR USER PACKET TYPES #************************************************ - -# Remove the hash from a paket and return the payload -# -def strip_hash(_data): -# _log = logger.debug - return _data[:-10] - - -# Determine if the provided peer ID is valid for the provided network -# -def valid_peer(_peer_list, _peerid): -# _log = logger.debug - if _peerid in _peer_list: - return True - return False - - -# Determine if the provided master ID is valid for the provided network -# -def valid_master(_network, _peerid): -# _log = logger.debug - if NETWORK[_network]['MASTER']['RADIO_ID'] == _peerid: - return True - else: - return False - - -# Take a packet to be SENT, calcualte auth hash and return the whole thing -# -def hashed_packet(_key, _data): -# _log = logger.debug - _hash = binascii.a2b_hex((hmac.new(_key,_data,hashlib.sha1)).hexdigest()[:20]) - return (_data + _hash) +def call_ctl_1(): + pass +def call_ctl_2(): + pass -# Take a RECEIVED packet, calculate the auth hash and verify authenticity -# -def validate_auth(_key, _data): -# _log = logger.debug - _payload = strip_hash(_data) - _hash = _data[-10:] - _chk_hash = binascii.a2b_hex((hmac.new(_key,_payload,hashlib.sha1)).hexdigest()[:20]) - - if _chk_hash == _hash: -# _log(' AUTH: Valid - Payload: %s, Hash: %s', binascii.b2a_hex(_payload), binascii.b2a_hex(_hash)) - return True - else: -# _log(' AUTH: Invalid - Payload: %s, Hash: %s', binascii.b2a_hex(_payload), binascii.b2a_hex(_hash)) - return False - - -# Forward Group Voice Packet -# -def fwd_group_voice(_network, _data): +def call_ctl_3(): + pass + +def xcmp_xnl(): + pass + +def group_voice(): # _log = logger.debug _src_group = _data[9:12] _src_ipsc = _data[1:5] @@ -128,18 +87,69 @@ def fwd_group_voice(_network, _data): _data = hashed_packet(NETWORK[_target]['LOCAL']['AUTH_KEY'], _data) # Send the packet to all peers in the target IPSC send_to_ipsc(_target, _data) - + +def private_voice(): + pass + +def group_data(): + pass + +def private_data(): + pass + +def unknown_message(): + pass + + + +#************************************************ +# UTILITY FUNCTIONS FOR INTERNAL USE +#************************************************ + +# Remove the hash from a paket and return the payload +# +def strip_hash(_data): +# _log = logger.debug +# _log('Stripped Packet: %s', binascii.b2a_hex(_data[:-10])) + return _data[:-10] + + +# Determine if the provided peer ID is valid for the provided network +# +def valid_peer(_peer_list, _peerid): +# _log = logger.debug + if _peerid in _peer_list: +# _log('Peer List Has An Entry For: %s', binascii.b2a_hex(_peerid)) + return True +# _log('Peer List Does NOT Have An Entry For: %s', binascii.b2a_hex(_peerid)) + return False + + +# Determine if the provided master ID is valid for the provided network +# +def valid_master(_network, _peerid): +# _log = logger.debug + if NETWORK[_network]['MASTER']['RADIO_ID'] == _peerid: +# _log('Master ID is Valid: %s', binascii.b2a_hex(_peerid)) + return True + else: +# _log('Master ID is NOT Valid: %s', binascii.b2a_hex(_peerid)) + return False + # Accept a complete packet, ready to be sent, and send it to all active peers + master in an IPSC # def send_to_ipsc(_target, _packet): # _log = logger.debug # Send to the Master +# _log('Sending %s to:', binascii.b2a_hex(_packet) networks[_target].transport.write(_packet, (NETWORK[_target]['MASTER']['IP'], NETWORK[_target]['MASTER']['PORT'])) +# _log(' Master: %s', binascii.b2a_hex(NETWORK[_target]['MASTER']['RADIO_ID'])) # Send to each connected Peer for peer in NETWORK[_target]['PEERS']: if peer['STATUS']['CONNECTED'] == True: networks[_target].transport.write(_packet, (peer['IP'], peer['PORT'])) +# _log(' Peer: %s', binascii.b2a_hex(peer['RADIO_ID'])) # De-register a peer from an IPSC by removing it's infomation @@ -147,10 +157,16 @@ def send_to_ipsc(_target, _packet): def de_register_peer(_network, _peerid): # _log = logger.debug # Iterate for the peer in our data +# _log('Peer De-Registration Requested for: %s', binascii.b2a_hex(_peerid)) for peer in NETWORK[_network]['PEERS']: # If we find the peer, remove it (we should find it) if _peerid == peer['RADIO_ID']: NETWORK[_network]['PEERS'].remove(peer) +# _log(' Peer Found And De-Registered') + return + else: +# _log(' Peer NOT Found') + pass # Take a recieved peer list and the network it belongs to, process and populate the @@ -211,6 +227,9 @@ def process_peer_list(_data, _network, _peer_list): _ts2 = False # If this entry was NOT already in our list, add it. + # Note: We keep a "simple" peer list in addition to the large data + # structure because soemtimes, we just need to identify a + # peer quickly. if _hex_radio_id not in _peer_list: _peer_list.append(_hex_radio_id) NETWORK[_network]['PEERS'].append({ @@ -231,8 +250,11 @@ def process_peer_list(_data, _network, _peer_list): # def print_peer_list(_network): # _log = logger.info - if not NETWORK[_network]['PEERS']: - print('No peer list for: {}' .format(_network)) + _status = NETWORK[_network]['MASTER']['STATUS']['PEER-LIST'] + print('Peer List Status for {}: {}' .format(_network, _status)) + + if _status and not NETWORK[_network]['PEERS']: + print('We are the only peer for: %s' % _network) return print('Peer List for: %s' % _network) @@ -256,9 +278,10 @@ def print_peer_list(_network): #************************************************ #************************************************ -# INITIAL SETUP of IPSC INSTANCE +# Base Class (used nearly all of the time) #************************************************ + class IPSC(DatagramProtocol): # Modify the initializer to set up our environment and build the packets @@ -304,27 +327,49 @@ class IPSC(DatagramProtocol): self.PEER_REG_REPLY_PKT = (PEER_REG_REPLY + self._local_id + IPSC_VER) self.PEER_ALIVE_REQ_PKT = (PEER_ALIVE_REQ + self._local_id + self.TS_FLAGS) self.PEER_ALIVE_REPLY_PKT = (PEER_ALIVE_REPLY + self._local_id + self.TS_FLAGS) - + else: # If we didn't get called correctly, log it! # logger.error('(%s) Unexpected arguments found.', self._network) - + + # This is called by REACTOR when it starts, We use it to set up the timed # loop for each instance of the IPSC engine # def startProtocol(self): # Timed loop for IPSC connection establishment and maintenance - # Others could be added later for things like updating a Web - # page, etc.... + # Others could be added later for things like updating a status + # Web page, etc.... # self._call = task.LoopingCall(self.timed_loop) self._loop = self._call.start(self._local['ALIVE_TIMER']) -#************************************************ -# FUNCTIONS FOR IPSC Network Engine -#************************************************ + # Take a packet to be SENT, calcualte auth hash and return the whole thing + # + def hashed_packet(self, _key, _data): + # _log = logger.debug + _hash = binascii.a2b_hex((hmac.new(_key,_data,hashlib.sha1)).hexdigest()[:20]) + # _log('Hash for: %s is %s', binascii.b2a_hex(_data), binascii.b2a_hex(_hash) + return (_data + _hash) + + + # Take a RECEIVED packet, calculate the auth hash and verify authenticity + # + def validate_auth(self, _key, _data): + # _log = logger.debug + _payload = strip_hash(_data) + _hash = _data[-10:] + _chk_hash = binascii.a2b_hex((hmac.new(_key,_payload,hashlib.sha1)).hexdigest()[:20]) + + if _chk_hash == _hash: + # _log(' AUTH: Valid - Payload: %s, Hash: %s', binascii.b2a_hex(_payload), binascii.b2a_hex(_hash)) + return True + else: + # _log(' AUTH: Invalid - Payload: %s, Hash: %s', binascii.b2a_hex(_payload), binascii.b2a_hex(_hash)) + return False + #************************************************ # TIMED LOOP - MY CONNECTION MAINTENANCE @@ -333,16 +378,17 @@ class IPSC(DatagramProtocol): def timed_loop(self): # Right now, without this, we really dont' know anything is happening. print_peer_list(self._network) + # If the master isn't connected, we have to do that before we can do anything else! - if (self._master_stat['CONNECTED'] == False): - reg_packet = hashed_packet(self._local['AUTH_KEY'], self.MASTER_REG_REQ_PKT) + if self._master_stat['CONNECTED'] == False: + reg_packet = self.hashed_packet(self._local['AUTH_KEY'], self.MASTER_REG_REQ_PKT) self.transport.write(reg_packet, (self._master_sock)) # Once the master is connected, we have to send keep-alives.. and make sure we get them back elif (self._master_stat['CONNECTED'] == True): # Send keep-alive to the master - master_alive_packet = hashed_packet(self._local['AUTH_KEY'], self.MASTER_ALIVE_PKT) + master_alive_packet = self.hashed_packet(self._local['AUTH_KEY'], self.MASTER_ALIVE_PKT) self.transport.write(master_alive_packet, (self._master_sock)) # If we had a keep-alive outstanding by the time we send another, mark it missed. @@ -365,7 +411,7 @@ class IPSC(DatagramProtocol): # If the master is connected and we don't have a peer-list yet.... if ((self._master_stat['CONNECTED'] == True) and (self._master_stat['PEER-LIST'] == False)): # Ask the master for a peer-list - peer_list_req_packet = hashed_packet(self._local['AUTH_KEY'], self.PEER_LIST_REQ_PKT) + peer_list_req_packet = self.hashed_packet(self._local['AUTH_KEY'], self.PEER_LIST_REQ_PKT) self.transport.write(peer_list_req_packet, (self._master_sock)) # If we do ahve a peer-list, we need to register with the peers and send keep-alives... @@ -377,11 +423,11 @@ class IPSC(DatagramProtocol): continue # If we haven't registered to a peer, send a registration if peer['STATUS']['CONNECTED'] == False: - peer_reg_packet = hashed_packet(self._local['AUTH_KEY'], self.PEER_REG_REQ_PKT) + peer_reg_packet = self.hashed_packet(self._local['AUTH_KEY'], self.PEER_REG_REQ_PKT) self.transport.write(peer_reg_packet, (peer['IP'], peer['PORT'])) # If we have registered with the peer, then send a keep-alive elif peer['STATUS']['CONNECTED'] == True: - peer_alive_req_packet = hashed_packet(self._local['AUTH_KEY'], self.PEER_ALIVE_REQ_PKT) + peer_alive_req_packet = self.hashed_packet(self._local['AUTH_KEY'], self.PEER_ALIVE_REQ_PKT) self.transport.write(peer_alive_req_packet, (peer['IP'], peer['PORT'])) # If we have a keep-alive outstanding by the time we send another, mark it missed. @@ -421,7 +467,7 @@ class IPSC(DatagramProtocol): # if bool(self._local['AUTH_KEY']) == True: # Validate - if validate_auth(self._local['AUTH_KEY'], data) == False: + if self.validate_auth(self._local['AUTH_KEY'], data) == False: logger.warning('(%s) AuthError: IPSC packet failed authentication. Type %s: Peer ID: %s', self._network, binascii.b2a_hex(_packettype), _dec_peerid) return # Strip the hash, we won't need it anymore @@ -434,8 +480,8 @@ class IPSC(DatagramProtocol): if not(valid_master(self._network, _peerid) == False or valid_peer(self._peer_list, _peerid) == False): logger.warning('(%s) PeerError: Peer not in peer-list: %s', self._network, _dec_peerid) return - # Group voice callback function - fwd_group_voice(self._network, data) + group_voice(self._network, data) + # IPSC keep alives, master and peer, come next in processing priority # @@ -446,7 +492,7 @@ class IPSC(DatagramProtocol): return # Generate a hashed paket from our template and send it. - peer_alive_reply_packet = hashed_packet(self._local['AUTH_KEY'], self.PEER_ALIVE_REPLY_PKT) + peer_alive_reply_packet = self.hashed_packet(self._local['AUTH_KEY'], self.PEER_ALIVE_REPLY_PKT) self.transport.write(peer_alive_reply_packet, (host, port)) elif (_packettype == MASTER_ALIVE_REPLY): @@ -483,8 +529,7 @@ class IPSC(DatagramProtocol): # Answer a peer registration request -- simple, no callback runction needed elif (_packettype == PEER_REG_REQ): # TO DO TO DO TO DO TO DO ***ADD CODE TO VALIDATE THE PEER IS IN OUR PEER-LIST HERE*** -# ***MAKE SURE WE CHECK TO SEE IF WE NEED TO MAKE AN AUTHENITCATED PACKET FIRST*** - peer_reg_reply_packet = hashed_packet(self._local['AUTH_KEY'], self.PEER_REG_REPLY_PKT) + peer_reg_reply_packet = self.hashed_packet(self._local['AUTH_KEY'], self.PEER_REG_REPLY_PKT) self.transport.write(peer_reg_reply_packet, (host, port)) elif (_packettype == PEER_REG_REPLY): @@ -493,19 +538,11 @@ class IPSC(DatagramProtocol): peer['STATUS']['CONNECTED'] = True elif (_packettype == PEER_LIST_REPLY): - self._peer_list = process_peer_list(data, self._network, self._peer_list) - - # Other "user" related packet types that we don't do much or anything with yet - # - elif (_packettype == PVT_VOICE): - logger.warning('<<- (%s) Voice Packet From:%s:%s', self._network, host, port) - - elif (_packettype == GROUP_DATA): - logger.warning('<<- (%s) Group Data Packet From:%s:%s', self._network, host, port) - - elif (_packettype == PVT_DATA): - logger.warning('<<- (%s) Private Data Packet From From:%s:%s', self._network, host, port) - + if len(data) > 18: + self._peer_list = process_peer_list(data, self._network, self._peer_list) + else: + NETWORK[self._network]['MASTER']['STATUS']['PEER-LIST'] = True + elif (_packettype == DE_REG_REQ): de_register_peer(self._network, _peerid) logger.warning('<<- (%s) Peer De-Registration Request From:%s:%s', self._network, host, port) @@ -516,20 +553,51 @@ class IPSC(DatagramProtocol): elif (_packettype == RPT_WAKE_UP): logger.warning('<<- (%s) Repeater Wake-Up Packet From:%s:%s', self._network, host, port) - # Technically, we're not paying any attention to these types because we're not part of the XCMP call control structure + # Other "user" related packet types that we don't do much or anything with yet # - elif (_packettype == XCMP_XNL): - logger.warning('<<- (%s) XCMP_XNL From:%s:%s, but we did not indicate XCMP capable!', self._network, host, port) + elif (_packettype == PVT_VOICE): + private_voice() + + elif (_packettype == GROUP_DATA): + group_data() + + elif (_packettype == PVT_DATA): + private_data() + + elif (_packettype == XCMP_XNL): # NOTE: We currently indicate we are not XCMP/XNL capable! + xcmp_xnl() - elif (_packettype in (CALL_CTL_1, CALL_CTL_2, CALL_CTL_3)): - logger.warning('<<- (%s) Call Control Packet From:%s:%s', self._network, host, port) + elif (_packettype == CALL_CTL_1): + call_control_1() + + elif (_packettype == CALL_CTL_2): + call_control_2() + + elif (_packettype == CALL_CTL_3): + call_control_3() # If there's a packet type we don't know aobut, it should be logged so we can figure it out and take an appropriate action! else: - packet_type = binascii.b2a_hex(_packettype) - logger.error('<<- (%s) Received Unprocessed Type %s From:%s:%s', self._network, packet_type, host, port) + unknown_message(_packettype, data) +#************************************************ +# Derived Class +# used in the rare event of an +# unauthenticated IPSC network. +#************************************************ +class UnauthIPSC(IPSC): + + # There isn't a hash to build, so just return the data + # + def hashed_packet(self, _key, _data): + return (_data) + + # Everything is validated, so just return True + # + def validate_auth(self, _key, _data): + return True + #************************************************ # MAIN PROGRAM LOOP STARTS HERE @@ -538,7 +606,11 @@ class IPSC(DatagramProtocol): if __name__ == '__main__': networks = {} for ipsc_network in NETWORK: - networks[ipsc_network] = IPSC(ipsc_network) + if NETWORK[ipsc_network]['LOCAL']['AUTH_ENABLED'] == True: + networks[ipsc_network] = IPSC(ipsc_network) + else: + networks[ipsc_network] = UnauthIPSC(ipsc_network) + if (NETWORK[ipsc_network]['LOCAL']['ENABLED']): reactor.listenUDP(NETWORK[ipsc_network]['LOCAL']['PORT'], networks[ipsc_network]) reactor.run() \ No newline at end of file diff --git a/ipsc/.gitignore b/ipsc/.gitignore new file mode 100644 index 0000000..8ae94f2 --- /dev/null +++ b/ipsc/.gitignore @@ -0,0 +1,5 @@ +.DS_Store +.dropbox +Icon +my_ipsc_config.py +*.pyc diff --git a/ipsc/__init__.py b/ipsc/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ipsc_logger.py b/ipsc/ipsc_logger.py similarity index 100% rename from ipsc_logger.py rename to ipsc/ipsc_logger.py diff --git a/ipsc_mask.py b/ipsc/ipsc_mask.py similarity index 100% rename from ipsc_mask.py rename to ipsc/ipsc_mask.py diff --git a/ipsc_message_types.py b/ipsc/ipsc_message_types.py similarity index 100% rename from ipsc_message_types.py rename to ipsc/ipsc_message_types.py diff --git a/my_ipsc_config_SAMPLE.py b/ipsc/my_ipsc_config_SAMPLE.py similarity index 98% rename from my_ipsc_config_SAMPLE.py rename to ipsc/my_ipsc_config_SAMPLE.py index 20130d1..9acd224 100644 --- a/my_ipsc_config_SAMPLE.py +++ b/ipsc/my_ipsc_config_SAMPLE.py @@ -24,6 +24,7 @@ NETWORK = { 'ALIVE_TIMER': 5, # Seconds between keep-alives and registration attempts 'MAX_MISSED': 5, # Maximum number of keep-alives missed before de-registration 'RADIO_ID': b'\x00\x00\x00\x0A', + 'AUTH_ENABLED': True, 'AUTH_KEY': b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01', 'ENABLED': True, 'STATUS': {