Compare commits

..

16 Commits

4 changed files with 274 additions and 36 deletions

View File

@ -0,0 +1,121 @@
"""
Networking client to connect to a server and find server hosts
"""
import ipaddress
import random
import psutil
import socket
SERVER_PORT = 29987
SERVER_BUFFER = 1024
SCAN_TIMEOUT = 3
class InterfaceInfo:
"""
Class used for storing interface information to make it easier to use
"""
def __init__(self, interface_name, network):
self.interface_name = interface_name
self.network = network
def __str__(self):
return 'Interface:{0}, Network:{1}'.format(self.interface_name, self.network)
def __repr__(self):
return '(Interface:{0}, Network:{1})'.format(self.interface_name, self.network)
def request_server_info(server_ip, server_port):
"""
Request server info from a PyPong server given an IP and a port
:param server_ip: Server IP to request info from
:param server_port: Port to connect to server on
:return: response given by server (or lack thereof)
"""
request = 'PYPONGREQ;SVRINFO'
udp_client_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# Timeout is set to SCAN_TIMEOUT plus a random number between 0 and SCAN_TIMEOUT so that threads can
# start asynchronously to avoid a lot of requests being sent to machines at once
# Average time is SCAN_TIMEOUT*1.5
udp_client_socket.settimeout(SCAN_TIMEOUT + (SCAN_TIMEOUT * random.random()))
try:
udp_client_socket.sendto(str.encode(request, 'UTF-8'), (server_ip, server_port))
except OSError:
pass
try:
server_response = udp_client_socket.recvfrom(SERVER_BUFFER)[0].decode('UTF-8')
except TimeoutError:
server_response = '{0};TIMEOUT'.format(server_ip)
except UnicodeDecodeError:
server_response = '{0};MANGLED_RESPONSE'.format(server_ip)
udp_client_socket.close()
return server_response
def get_interface_info():
"""
Gets information using psutil about the current machine's interfaces and their networks. This only works for IPv4
networks and will not return any data in regard to IPv6 links.
:return: List of InterfaceInfo objects containing information about all interfaces on the current machine excluding
the loopback interface (lo or 127.0.0.1)
"""
raw_interface_data = psutil.net_if_addrs()
interface_data = []
for interface in raw_interface_data:
# Extract needed information from psutil and put it into a InterfaceInfo object
address = (raw_interface_data.get(interface)[0]).address
cidr_suffix = ipaddress.IPv4Network('{0}/{1}'.format(address, raw_interface_data.get(interface)[0].netmask),
strict=False).prefixlen
# Do not add the loopback interface or IPv6 interfaces
if ':' not in address and address != '127.0.0.1':
interface_data.append(InterfaceInfo(interface, '{0}/{1}'.format(address, cidr_suffix)))
return interface_data
def get_ips_from_network(network):
"""
Generate a list of IP addresses given an address in CIDR notation. This works from smallest to largest, meaning that
it builds a list starting at <ADDRESS>/32 (aka just the current machine) decrementing the CIDR by 1 until the
correct subnet is reached.
:param network:
:return:
"""
network_split = network.split('/')
address = network_split[0]
cidr = int(network_split[1])
all_addresses = []
test_cidr = 32 # Start with the machine itself
while test_cidr >= cidr:
# Generate all addresses inside the current subnet given by 'address/test_cidr'
current_addresses = [str(ip) for ip in ipaddress.IPv4Network('{0}/{1}'.format(address, test_cidr),
strict=False)]
# Do not add the address again if it is already in the list of all addresses
for test_address in current_addresses:
if test_address not in all_addresses:
all_addresses.append(test_address)
test_cidr -= 1
return all_addresses
def check_ip(ip_address):
"""
Check a single IP address for a running game server
:param ip_address: IP address to check
:return: response of the server (or timeout if none)
"""
response = request_server_info(ip_address, SERVER_PORT)
return response

View File

@ -2,19 +2,29 @@
Starting launcher of the game. This is where you host new games or join other ones. Starting launcher of the game. This is where you host new games or join other ones.
""" """
import tkinter as tk import tkinter as tk
import concurrent.futures
import pypong.networking.client as client
global running global running
# Don't allow any more than this number of threads when looking for games
# Can cripple a network at high numbers, 128 is recommended (can scan a /24 network in ~9 seconds on average)
# Higher range subnets will exponentially take more time to finish (/16 would take ~38.4 minutes at 128 req/s)
MAX_SCAN_REQUESTS = 128
def main(): def main():
""" """
Runs the game launcher. Runs the game launcher.
""" """
# Executor used for scanning local network for servers
executor = concurrent.futures.ThreadPoolExecutor(max_workers=MAX_SCAN_REQUESTS)
main_window = tk.Tk() main_window = tk.Tk()
main_window.title('PynPong Launcher') main_window.title('PynPong Launcher')
main_window.resizable(width=False, height=False) main_window.resizable(width=False, height=False)
main_window.geometry('500x250') main_window.geometry('500x300')
def quit_launcher(): def quit_launcher():
""" """
@ -22,20 +32,96 @@ def main():
""" """
global running global running
running = False running = False
# Stop executor from creating new scanning tasks
executor.shutdown(wait=False, cancel_futures=True)
def get_games(): def check_game(game_ip):
""" """
Refresh the games list. Checks to see if there is a game bein ghosted at a given IP
:param game_ip: IP address to check for a host
:return: Response from the server (or timeout if nothing found)
"""
result = client.check_ip(game_ip)
return result
def get_games(network):
"""
Refreshes the game list.
:param network: The network to scan for games on
""" """
listbox.delete(0, tk.END) listbox.delete(0, tk.END)
listbox.insert(0, 'Searching for games...') listbox.insert(0, ' Building IP range...')
# Networking code will go here eventually main_window.update()
possible_ips = client.get_ips_from_network(network.get().split(' ')[0])
listbox.delete(0, tk.END)
listbox.insert(0, 'this might take some time...')
listbox.insert(0, 'Refreshing server listing,')
main_window.update()
futures = []
# Use the executor to create threads to search for games
for ip in possible_ips:
future = executor.submit(check_game, ip)
futures.append(future)
game_found = False
for future in futures:
result = future.result()
if 'PYPONGRST;SVRINFO' in result: # Check to make sure response has the right header and type
raw_response = result.split(';') # Split response by block (denoted by semicolons)
response = []
# Split up each block by chunks (<COMMAND>:<RESPONSE>)
# e.g. the block 'NAME:Nicholas Dyer' -> ['NAME', 'Nicholas Dyer']
for block in raw_response:
chunks = block.split(':')
for chunk in chunks:
response.append(chunk)
# Make sure the response has the NAME command
if 'NAME' in response:
# Session name is the next index after the command
session_name = response[response.index('NAME') + 1]
if not game_found: # If this is the first game found, clear out the list
listbox.delete(0, tk.END)
game_found = True
listbox.insert(0, session_name)
main_window.update() # Update screen while finding games (laggy, but works)
if not game_found:
listbox.delete(0, tk.END)
listbox.insert(0, 'No games found!')
def get_addresses():
"""
Get the current machine's local interfaces and their corresponding addresses in CIDR notation
:return: Address in CIDR notation along with the interface
Example:
['172.20.126.4/24 [eth0]', 192.168.2.43/16 [wlan0]']
"""
interface_info = client.get_interface_info()
shortened_info = []
for interface in interface_info:
shortened_info.append('{0} [{1}]'.format(interface.network, interface.interface_name))
return shortened_info
# Create the top title label # Create the top title label
title_label = tk.Label(text='PynPong Launcher', fg='white', bg='#000040', padx=1, pady=20) title_label = tk.Label(text='PynPong Launcher', fg='white', bg='#000040', padx=1, pady=20)
title_label.pack(side=tk.TOP, fill=tk.BOTH) title_label.pack(side=tk.TOP, fill=tk.BOTH)
# Create the list of games # Create the list of games
global listbox
listbox = tk.Listbox(main_window) listbox = tk.Listbox(main_window)
listbox.pack(side=tk.LEFT, expand=True, fill=tk.BOTH) listbox.pack(side=tk.LEFT, expand=True, fill=tk.BOTH)
# Add scrollbar # Add scrollbar
@ -49,7 +135,13 @@ def main():
button_frame = tk.Frame(main_window) button_frame = tk.Frame(main_window)
tk.Button(button_frame, text='Host a Game', height=3, width=30).pack() tk.Button(button_frame, text='Host a Game', height=3, width=30).pack()
tk.Button(button_frame, text='Join Selected Game', height=3, width=30).pack() tk.Button(button_frame, text='Join Selected Game', height=3, width=30).pack()
tk.Button(button_frame, text='Refresh Game List', height=2, width=20, command=get_games).pack() tk.Button(button_frame, text='Refresh Game List', height=2, width=20,
command=lambda: get_games(selected_network)).pack()
networks = get_addresses()
selected_network = tk.StringVar()
selected_network.set(networks[0])
interface_menu = tk.OptionMenu(button_frame, selected_network, *networks)
interface_menu.pack()
button_frame.pack(side=tk.RIGHT) button_frame.pack(side=tk.RIGHT)
# Set it so that if the X is pressed the application quits # Set it so that if the X is pressed the application quits

View File

@ -1,36 +1,25 @@
import psutil import socket
LOCAL_IP = '127.0.0.1'
SERVER_PORT = 29987
SERVER_BUFFER = 1024
class InterfaceInfo: udp_server_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
""" udp_server_socket.bind((LOCAL_IP, SERVER_PORT))
Class used for storing interface information to make it easier to use
"""
def __init__(self, interface_name, address, subnet_mask):
self.interface_name = interface_name
self.address = address
self.subnet_mask = subnet_mask
def __str__(self): waiting_for_connection = True
return '(Interface:{0}, Address:{1}, Subnet Mask:{2})'.format(self.interface_name,
self.address, self.subnet_mask)
def __repr__(self): while waiting_for_connection:
return '(Interface:{0}, Address:{1}, Subnet Mask:{2})'.format(self.interface_name, received = udp_server_socket.recvfrom(SERVER_BUFFER)
self.address, self.subnet_mask) message = received[0]
address = received[1]
decoded_message = message.decode('UTF-8')
if decoded_message.split(';')[0] != 'PYPONGREQ':
message = 'PYPONGRST;ERROR:INVALID HEADER'
udp_server_socket.sendto(message.encode('UTF-8'), address)
else:
message = 'PYPONGRST;ECHO:{0}'.format(decoded_message)
udp_server_socket.sendto(message.encode('UTF-8'), address)
def main(): udp_server_socket.close()
raw_interface_data = psutil.net_if_addrs()
interface_data = []
# Extract needed information from psutil and put it into a InterfaceInfo object
for interface in raw_interface_data:
address = (raw_interface_data.get(interface)[0]).address
subnet_mask = (raw_interface_data.get(interface)[0]).netmask
interface_data.append(InterfaceInfo(interface, address, subnet_mask))
print(interface_data)
if __name__ == '__main__':
main()

View File

@ -0,0 +1,36 @@
"""
Contains the Packet class to help with the transmission, reading, and verification of infermation over the network
"""
class Packet:
"""
Class that contains information to be sent over the network. Can self-verify and has methods (will have methods) to
easily encode and decode packet information
"""
VALID_HEADERS = [
'PYPONGREQ',
'PYPONGRST'
]
VALID_TYPES = [
'SVRINFO'
]
def __init__(self, header, msg_type, *message):
self.header = header
self.msg_type = msg_type
self.message = []
for x in message:
self.message.append(x)
def integrity_check(self):
"""
Verify that the information in the packet is correct and makes sense
:return: True if packet verifies correctly, False if not
"""
if self.header not in Packet.VALID_HEADERS:
return False
if self.msg_type not in Packet.VALID_TYPES:
return False
return True