def handle_set_led(server, body, source): """ Sends SET_LED message to the destination that corresponds with the source address in the cache. """ logging.debug('SET_LED') if 'state' not in body: msg = 'missing field: "state" required' logging.debug(msg) server.send(Error.json(Error.BAD_REQ, msg), source) return state = body['state'] if not isinstance(state, int) or state < 0 or state > 2: msg = 'state must be an int in range [0,2]' logging.debug(msg) server.send(Error.json(Error.BAD_REQ, msg), source) return car_addr = server.get_destination(source) if car_addr == None: msg = 'invalid destination' logging.debug(msg) server.send(Error.json(Error.BAD_REQ, msg), source) return server.send(json.dumps(body).encode('utf-8'), car_addr)
def receive_forever(self): """ Start an infinite loop that will indefinitely block on a receive until a new message comes in. If the message is JSON-formatted and has a "type" key, then the corresponding handler will be run. """ while True: body, addr = self.receive() if body == None and addr == None: # There was an error with the received message continue if body['type'] not in self.handlers: # Provided type is not a registered handler logging.debug('Invalid message type', body) self.send(Error.json(Error.BAD_REQ, 'invalid message type'), addr) continue try: self.handlers[body['type']](self, body, addr) except CloseServer: self.close() return except Exception as e: self.send(Error.json(Error.SERVER_ERR, str(e)), addr)
def handle_connect_car(server, body, source): logging.debug('CONNECT CAR') if 'car_id' not in body: logging.debug('missing field: car_id') server.send(Error.json(Error.BAD_REQ, 'missing field: car_id'), source) return car_id = body['car_id'] if not isinstance(car_id, int): msg = '"car_id" must be an integer' logging.debug(msg) server.send(Error.json(Error.BAD_REQ, msg), source) return with server.get_db() as (dbconnect, cursor): cursor.execute('select * from cars where (id=?)', (car_id, )) entry = cursor.fetchone() request_ip = source[0] if entry is None: msg = 'car does not exist' logging.debug(msg) server.send(Error.json(Error.BAD_REQ, msg), source) elif entry[2] != request_ip: msg = 'IP address does not match car ID' logging.debug(msg) server.send(Error.json(Error.BAD_REQ, msg), source) else: cursor.execute('update cars set isOn=1 where (id=?)', (car_id, )) dbconnect.commit() data = '{"type": %d}' % MsgType.ACK server.send(data.encode('utf-8'), source)
def handle_link(server, body, source): logging.debug('LINK') if 'car_id' not in body or 'user_id' not in body: msg = 'missing field: "car_id", "user_id" required' logging.debug(msg) server.send(Error.json(Error.BAD_REQ, msg), source) return car_id = body['car_id'] user_id = body['user_id'] if not isinstance(car_id, int) or not isinstance(user_id, int): msg = '"car_id" and "user_id" must be integers' logging.debug(msg) server.send(Error.json(Error.BAD_REQ, msg), source) return with server.get_db() as (_, cursor): cursor.execute('select * from cars where id=? and userID=?', (car_id, user_id)) entry = cursor.fetchone() if entry == None: msg = 'car does not exist' logging.debug(msg) server.send(Error.json(Error.BAD_REQ, msg), source) elif not entry[3]: # Check the isOn column msg = 'car is not available' logging.debug(msg) server.send(Error.json(Error.BAD_REQ, msg), source) else: server.add_route(source, (entry[2], CAR_PORT)) data = '{"type": %d}' % MsgType.ACK server.send(data.encode('utf-8'), source)
def handle_set_led(server, body, addr): """ Passes the new LED state to the Arduino via the serial port. Arguments: server -- instance of Server class body -- JSON body of UDP packet received addr -- source destination of received UDP packet """ logging.debug('SET LED') if 'state' not in body: msg = 'missing "state" field' logging.debug(msg) server.send(Error.json(Error.BAD_REQ, msg), addr) return state = body['state'] if not isinstance(state, int) or state < 0 or state > 2: msg = '"state" must integer in range [0, 2]' logging.debug(msg) server.send(Error.json(Error.BAD_REQ, msg), addr) return SerialMsg.write_8_bit(server.serial, SerialMsg.LED, state) server.send(json.dumps({'type': MsgType.ACK}).encode('utf-8'), addr)
def handle_move(server, body, addr): """ Passes the joystick coordinates to the Arduino via the serial port. Arguments: server -- instance of Server class body -- JSON body of UDP packet received addr -- source destination of received UDP packet """ logging.debug('MOVE') if 'x' not in body or 'y' not in body: msg = 'body must include "x" and "y" fields' logging.debug(msg) server.send(Error.json(Error.BAD_REQ, msg), addr) return x = body['x'] y = body['y'] if not isinstance(x, int) or not isinstance(y, int) or \ x < 0 or x > 1023 or y < 0 or y > 1023: msg = '"x" and "y" values must be within the range [0, 1023]' logging.debug(msg) server.send(Error.json(Error.BAD_REQ, msg), addr) return SerialMsg.write_16_bit(server.serial, SerialMsg.MOVE, body['x'], body['y'])
def handle_login(server, body, source): logging.debug('LOGIN') ''' 1. Compare salted-and-hashed passwords 2. If success: get car list from database and send to the app If failure: send failed login message ''' # Check data is valid. if not, send an error packet if 'name' not in body or 'password' not in body: message = 'missing field: "name", "password" required' logging.debug(message) server.send(Error.json(Error.BAD_REQ, message), source) return # Get JSON data name = body['name'] password = body['password'] if not isinstance(name, str) or not isinstance(password, str): msg = '"name" and "password" must be strings' logging.debug(msg) server.send(Error.json(Error.BAD_REQ, msg), source) return # Get user from db. Send an error if user doesn't exist. with server.get_db() as (dbconnect, cursor): cursor.execute("select * from users where name=(?)", [name]) entry = cursor.fetchone() if entry is None: message = "User does not exist" logging.debug(message) server.send(Error.json(Error.BAD_REQ, message), source) return # Get salt as bytes salt = entry[2] b_salt = b64decode(salt.encode('utf-8')) # Get salted password string from database salted_password = entry[3] # Salt the login password new_password = hashlib.pbkdf2_hmac('sha256', password.encode('utf-8'), b_salt, 100000) str_new_password = b64encode(new_password).decode('utf-8') # Compare two passwords as strings if str_new_password == salted_password: # Send Confirmation to App logging.debug("User login successful") user_id = entry[0] ackJSON = {"type": MsgType.ACK, "user_id": user_id} _send_JSON(server, source, ackJSON) else: message = "Password is incorrect" logging.debug(message) server.send(Error.json(Error.BAD_REQ, message), source)
def handle_register_user(server, body, source): logging.debug('REGISTER USER') ''' 1. Store username, salt, and salted-and-hashed-password in 'users' table 2. Send a confirmation (ACK) back to the app ''' # Check data is valid. if not, send an error packet if 'name' not in body or 'password' not in body: message = 'missing field: "name", "password" required' logging.debug(message) server.send(Error.json(Error.BAD_REQ, message), source) return # Get JSON data name = body['name'] password = body['password'] if not isinstance(name, str) or not isinstance(password, str): msg = '"name" and "password" must be strings' logging.debug(msg) server.send(Error.json(Error.BAD_REQ, msg), source) return # Salt password salt = os.urandom(32) password = hashlib.pbkdf2_hmac('sha256', password.encode('utf-8'), salt, 100000) # Create user in db. Send an error if user already exists. with server.get_db() as (dbconnect, cursor): cursor.execute("select * from users where name=(?)", [name]) entry = cursor.fetchone() if entry is None: cursor.execute("insert into users (name,salt,password) values (?,?,?)",\ (name,b64encode(salt).decode('utf-8'), b64encode(password).decode('utf-8'))) dbconnect.commit() else: message = "User already exists" logging.debug(message) server.send(Error.json(Error.BAD_REQ, message), source) return user_id = cursor.lastrowid # Send Confirmation to App logging.debug("User registration successful") ackJSON = { "type": MsgType.ACK, "user_id": user_id, } _send_JSON(server, source, ackJSON)
def handle_get_cars(server, body, source): logging.debug('GET CARS') ''' 1. Get list of cars from databse 2. If successful: send list of cars to app ''' # Check data is valid if 'user_id' not in body: message = 'missing field: "user_id" required' logging.debug(message) server.send(Error.json(Error.BAD_REQ, message), source) return # Get JSON data user_id = body["user_id"] if not isinstance(user_id, int): msg = '"user_id" must be an integer' logging.debug(msg) server.send(Error.json(Error.BAD_REQ, msg), source) return # Create cars list cars = [] with server.get_db() as (_, cursor): cursor.execute("select * from cars where userID=(?)", [user_id]) entry = cursor.fetchall() # Return error if no cars are under userID if entry is None: message = "User has no registered cars" logging.debug(message) server.send(Error.json(Error.BAD_REQ, message), source) return else: # Add car_name and car_id to list for each car returned for row in entry: cars.append({ "id": row[0], "name": row[1], "is_on": bool(row[3]) }) carsJSON = {"type": MsgType.ACK, "cars": cars} _send_JSON(server, source, carsJSON)
def handle_ack(server, body, source): """ Sends ACK message to the destination that corresponds with the source address in the cache. """ logging.debug('ACK') dest = server.get_destination(source) if dest == None: msg = 'invalid destination' logging.debug(msg) server.send(Error.json(Error.BAD_REQ, msg), source) return server.send(json.dumps(body).encode('utf-8'), dest)
def handle_movement(server, body, source): logging.debug('MOVEMENT') ''' 1. Get desination IP from cache (if not in cache, get from 'cars' table) 2. Forward the message ''' # Check data is valid if 'x' not in body or 'y' not in body: message = 'missing field: "x", "y" required' logging.debug(message) server.send(Error.json(Error.BAD_REQ, message), source) return # Get JSON data x = body['x'] y = body['y'] if not isinstance(x, int) or not isinstance(y, int) or \ x < 0 or x > 1023 or y < 0 or y > 1023: msg = '"x" and "y" values must be within the range [0, 1023]' logging.debug(msg) server.send(Error.json(Error.BAD_REQ, msg), addr) return # Check cache for car ip address car_addr = server.get_destination(source) if car_addr is None: # Return bad request. message = 'car not connected' logging.debug(message) server.send(Error.json(Error.BAD_REQ, message), source) return # Send movement data to car _send_JSON(server, car_addr, body)
def _receive_forever(self): """ Start an infinite loop that will indefinitely block on a receive until a new message comes in. If the message is JSON-formatted and has a "type" key, then the corresponding handler will be run in a new thread. """ while True: data, addr = self.socket.recvfrom(self.BUFFER_SIZE) try: body = json.loads(data) except json.JSONDecodeError: logging.debug('Received invalid JSON') self.send(Error.json(Error.BAD_REQ, 'invalid JSON'), addr) continue if body['type'] in self.handlers: handler_thread = threading.Thread( target=self.handlers[body['type']], args=(self, body, addr)) handler_thread.start() else: logging.debug('Invalid message type', body) self.send(Error.json(Error.BAD_REQ, 'invalid message type'), addr)
def receive(self): """ Receive a new message and return the data and source address. If the message is JSON-formatted, then it will return the parsed body and the source address. If not, it will send an error to the sender. """ data, addr = self.socket.recvfrom(self.BUFFER_SIZE) try: body = json.loads(data) except json.JSONDecodeError: logging.debug('Received invalid JSON') self.send(Error.json(Error.BAD_REQ, 'invalid JSON'), addr) return None, None return body, addr
def handle_register_car(server, body, source): logging.debug('REGISTER CAR') ''' 1. Add a row in the 'cars' table 2. Send a confirmation (ACK) back to the car ''' ip = source[0] # Check data is valid. if not, send an error packet if 'name' not in body or 'user_id' not in body: message = 'missing field: "name", "user_id" required' logging.debug(message) server.send(Error.json(Error.BAD_REQ, message), source) return # Get JSON data name = body['name'] user_id = body['user_id'] if not isinstance(name, str) or not isinstance(user_id, int): msg = '"name" must be a string and "user_id" must be an integer' logging.debug(msg) server.send(Error.json(Error.BAD_REQ, msg), source) return with server.get_db() as (dbconnect, cursor): # Check that the user exists in the database cursor.execute("select * from users where id=(?)", [user_id]) entry = cursor.fetchone() # Send error packet if entry is None: message = "User is not registered" logging.debug(message) server.send(Error.json(Error.BAD_REQ, message), source) return # Check that the user does not already have car with that name cursor.execute("select * from cars where name=(?) and userID=(?)", (name, user_id)) entry = cursor.fetchone() # Send error if car already exists if entry is None: cursor.execute( "insert into cars (name,ip,userID,isOn) values(?,?,?,?)", (name, ip, user_id, 0)) dbconnect.commit() else: message = "Car name already registered" logging.debug(message) server.send(Error.json(Error.BAD_REQ, message), source) return car_id = cursor.lastrowid with open(HAPROXY_CFG, 'r+') as cfg: content = cfg.read() cfg.seek(0) cfg.truncate() # Insert ACL based on car ID acl_regex = r'\1 acl url_car{0} path_beg /{0}\n'.format(car_id) content = re.sub(ACL_START_REGEX, acl_regex, content) # Insert rule based on ACL rule_regex = r'\1 use_backend car{0} if url_car{0}\n'.format(car_id) content = re.sub(RULE_START_REGEX, rule_regex, content) # Insert backend based on car IP backend_regex = \ r'''\1backend car{0} mode http reqrep ^([^\ ]*\ /){0}[/]?(.*) \\1\\2 server car{0}_app {1}:8000 check\n\n'''.format(car_id, ip) content = re.sub(BACKEND_START_REGEX, backend_regex, content) cfg.write(content) subprocess.run(['systemctl', 'restart', 'haproxy']) # Send Confirmation to App logging.debug("Car registration successful") ackJSON = {"type": MsgType.ACK, "car_id": car_id} _send_JSON(server, source, ackJSON)
def handle_connect_wifi(server, body, addr): """ Connects to the specified network in the body. Raises an exception if connection fails. Arguments: server -- instance of Server class body -- JSON body of UDP packet received addr -- source destination of received UDP packet """ logging.debug('WIFI CONN') if 'ssid' not in body or 'user_id' not in body or 'car_name' not in body: msg = 'body must include "ssid" and "user_id" and "car_name" fields' logging.debug(msg) server.send(Error.json(Error.BAD_REQ, msg), addr) return ssid = body['ssid'] if not isinstance(ssid, str): msg = '"ssid" must be a string' logging.debug(msg) server.send(Error.json(Error.BAD_REQ, msg), addr) return if 'password' not in body: argList = ['./shell-scripts/connect-wifi.sh', ssid] else: password = body['password'] if not isinstance(password, str): msg = '"password" must be a string' logging.debug(msg) server.send(Error.json(Error.BAD_REQ, msg), addr) return argList = ['./shell-scripts/connect-wifi.sh', ssid, password] try: process = subprocess.run(argList, check=True, capture_output=True, text=True) except subprocess.CalledProcessError as err: logging.debug("connect-wifi.sh exited with code {1:d}: {2}"\ .format(ssid, err.returncode, err.output)) raise Exception("Failed to connect to {0} network".format(ssid)) except Exception as err: logging.debug("error running connect-wifi.sh: {0}".format(str(err))) raise Exception("Failed to connect to {0} network".format(ssid)) logging.debug('wifi connection succeeded, sending ACK to app') # Respond to client with an ACK to confirm the connection was successful. # Retry if client sends the WIFI_CONN message within 10 seconds to indicate # that the ACK message was dropped. server.socket.settimeout(10) while True: server.send(json.dumps({'type': MsgType.ACK}).encode('utf-8'), addr) try: body, addr = server.receive() except socket.timeout: # WIFI_CONN message was not retried therefore the ACK was received # successfully logging.debug('ACK received by app') break logging.debug('retry ACK send') server.socket.settimeout(None) logging.debug('stopping access point') subprocess.run('./shell-scripts/stop-ap.sh') # Update metadata with user ID server.metadata.set_user_id(body['user_id']) server.metadata.set_car_name(body['car_name']) raise CloseServer