588 lines
20 KiB
Python
588 lines
20 KiB
Python
#!/usr/bin/env python3
|
||
# -*- coding: UTF-8 -*-
|
||
|
||
import argparse
|
||
import socket
|
||
import os
|
||
import sys
|
||
import struct
|
||
import time
|
||
import random
|
||
import traceback # useful for exception handling
|
||
import threading
|
||
|
||
from pprint import pprint
|
||
|
||
|
||
# config
|
||
INCOMING_BUFFER = 1024
|
||
OUTGOING_BUFFER = INCOMING_BUFFER * 10
|
||
ICMP_TYPE = 8
|
||
|
||
|
||
def setupArgumentParser() -> argparse.Namespace:
|
||
parser = argparse.ArgumentParser(
|
||
description='A collection of Network Applications developed for SCC.203.')
|
||
parser.set_defaults(func=ICMPPing, hostname='lancaster.ac.uk')
|
||
subparsers = parser.add_subparsers(help='sub-command help')
|
||
|
||
parser_p = subparsers.add_parser('ping', aliases=['p'], help='run ping')
|
||
parser_p.set_defaults(timeout=4)
|
||
parser_p.add_argument('hostname', type=str, help='host to ping towards')
|
||
parser_p.add_argument(
|
||
'--count',
|
||
'-c',
|
||
nargs='?',
|
||
type=int,
|
||
help='number of times to ping the host before stopping')
|
||
parser_p.add_argument(
|
||
'--timeout',
|
||
'-t',
|
||
nargs='?',
|
||
type=int,
|
||
help='maximum timeout before considering request lost')
|
||
parser_p.set_defaults(func=ICMPPing)
|
||
|
||
parser_t = subparsers.add_parser('traceroute', aliases=['t'],
|
||
help='run traceroute')
|
||
parser_t.set_defaults(timeout=4, protocol='icmp')
|
||
parser_t.add_argument(
|
||
'hostname',
|
||
type=str,
|
||
help='host to traceroute towards')
|
||
parser_t.add_argument(
|
||
'--timeout',
|
||
'-t',
|
||
nargs='?',
|
||
type=int,
|
||
help='maximum timeout before considering request lost')
|
||
parser_t.add_argument('--protocol', '-p', nargs='?', type=str,
|
||
help='protocol to send request with (UDP/ICMP)')
|
||
parser_t.set_defaults(func=Traceroute)
|
||
|
||
parser_pt = subparsers.add_parser('paris-traceroute', aliases=['pt'],
|
||
help='run paris-traceroute')
|
||
parser_pt.set_defaults(timeout=4, protocol='icmp')
|
||
parser_pt.add_argument(
|
||
'hostname',
|
||
type=str,
|
||
help='host to traceroute towards')
|
||
parser_pt.add_argument(
|
||
'--timeout',
|
||
'-t',
|
||
nargs='?',
|
||
type=int,
|
||
help='maximum timeout before considering request lost')
|
||
parser_pt.add_argument('--protocol', '-p', nargs='?', type=str,
|
||
help='protocol to send request with (UDP/ICMP)')
|
||
parser_pt.set_defaults(func=ParisTraceroute)
|
||
|
||
parser_w = subparsers.add_parser(
|
||
'web', aliases=['w'], help='run web server')
|
||
parser_w.set_defaults(port=8080)
|
||
parser_w.add_argument('--port', '-p', type=int, nargs='?',
|
||
help='port number to start web server listening on')
|
||
parser_w.set_defaults(func=WebServer)
|
||
|
||
parser_x = subparsers.add_parser('proxy', aliases=['x'], help='run proxy')
|
||
parser_x.set_defaults(port=8000)
|
||
parser_x.add_argument('--port', '-p', type=int, nargs='?',
|
||
help='port number to start web server listening on')
|
||
parser_x.set_defaults(func=Proxy)
|
||
|
||
args = parser.parse_args()
|
||
return args
|
||
|
||
|
||
class NetworkApplication:
|
||
|
||
def checksum(self, dataToChecksum: str) -> str:
|
||
csum = 0
|
||
countTo = (len(dataToChecksum) // 2) * 2
|
||
count = 0
|
||
|
||
while count < countTo:
|
||
thisVal = dataToChecksum[count + 1] * 256 + dataToChecksum[count]
|
||
csum = csum + thisVal
|
||
csum = csum & 0xffffffff
|
||
count = count + 2
|
||
|
||
if countTo < len(dataToChecksum):
|
||
csum = csum + dataToChecksum[len(dataToChecksum) - 1]
|
||
csum = csum & 0xffffffff
|
||
|
||
csum = (csum >> 16) + (csum & 0xffff)
|
||
csum = csum + (csum >> 16)
|
||
answer = ~csum
|
||
answer = answer & 0xffff
|
||
answer = answer >> 8 | (answer << 8 & 0xff00)
|
||
|
||
answer = socket.htons(answer)
|
||
|
||
return answer
|
||
|
||
def printOneResult(
|
||
self,
|
||
destinationAddress: str,
|
||
packetLength: int,
|
||
time: float,
|
||
ttl: int,
|
||
destinationHostname=''):
|
||
if destinationHostname:
|
||
print(
|
||
"%d bytes from %s (%s): ttl=%d time=%.2f ms" %
|
||
(packetLength, destinationHostname, destinationAddress, ttl, time))
|
||
else:
|
||
print("%d bytes from %s: ttl=%d time=%.2f ms" %
|
||
(packetLength, destinationAddress, ttl, time))
|
||
|
||
def printAdditionalDetails(
|
||
self,
|
||
packetLoss=0.0,
|
||
minimumDelay=0.0,
|
||
averageDelay=0.0,
|
||
maximumDelay=0.0):
|
||
print("%.2f%% packet loss" % (packetLoss))
|
||
if minimumDelay > 0 and averageDelay > 0 and maximumDelay > 0:
|
||
print("rtt min/avg/max = %.2f/%.2f/%.2f ms" %
|
||
(minimumDelay, averageDelay, maximumDelay))
|
||
|
||
def printMultipleResults(
|
||
self,
|
||
ttl: int,
|
||
destinationAddress: str,
|
||
measurements: list,
|
||
destinationHostname=''):
|
||
latencies = ''
|
||
noResponse = True
|
||
for rtt in measurements:
|
||
if rtt is not None:
|
||
latencies += str(round(rtt, 3))
|
||
latencies += ' ms '
|
||
noResponse = False
|
||
else:
|
||
latencies += '* '
|
||
|
||
if noResponse is False:
|
||
print(
|
||
"%d %s (%s) %s" %
|
||
(ttl,
|
||
destinationHostname,
|
||
destinationAddress,
|
||
latencies))
|
||
else:
|
||
print("%d %s" % (ttl, latencies))
|
||
|
||
|
||
class ICMPPing(NetworkApplication):
|
||
# Task 1.1: ICMP Ping
|
||
|
||
def receiveOnePing(self, icmpSocket, destinationAddress, ID, timeout):
|
||
# 1. Wait for the socket to receive a reply
|
||
icmpSocket.settimeout(timeout)
|
||
|
||
try:
|
||
reply, addr = icmpSocket.recvfrom(2048)
|
||
except socket.timeout as msg:
|
||
print("No data received from socket within timeout period. Message: " + str(msg))
|
||
sys.exit(1)
|
||
|
||
# 2. Once received, record time of receipt, otherwise, handle a timeout
|
||
recv_time = time.time()
|
||
|
||
# 4. Unpack the packet header for useful information, including the ID
|
||
# icmp header of the received packet,
|
||
# bottom of packet because of network byte-order
|
||
# align offset to include the layer 2 encap = 14
|
||
reply_size = struct.unpack(">H", reply[16 - 14:18 - 14])[0]
|
||
reply_ttl = struct.unpack(">B", reply[22 - 14:23 - 14])[0]
|
||
reply_id = struct.unpack(">B", reply[38 - 14:39 - 14])[0]
|
||
|
||
# 5. Check that the ID matches between the request and reply
|
||
if reply_id != ID:
|
||
print("Received packet ID not match")
|
||
sys.exit(1)
|
||
|
||
# 6. Return recv time + packet size
|
||
return (recv_time, reply_size, reply_ttl)
|
||
|
||
def sendOnePing(self, icmpSocket, destinationAddress, ID):
|
||
# 1. Build ICMP header
|
||
header = struct.pack('BBHHH', ICMP_TYPE, 0, 0, ID, 1)
|
||
data = bytes("Task 1.1: ICMP Ping", 'utf-8')
|
||
|
||
# 2. Checksum ICMP packet using given function
|
||
new_checksum = self.checksum(header + data)
|
||
|
||
# 3. Insert checksum into packet
|
||
header = struct.pack('BBHHH', ICMP_TYPE, 0, new_checksum, ID, 1)
|
||
packet = header + data
|
||
|
||
# 4. Send packet using socket
|
||
while packet:
|
||
sent = icmpSocket.sendto(packet, (destinationAddress, 1500)) # 1500 = Port number
|
||
packet = packet[sent:]
|
||
|
||
# 5. Record time of sending
|
||
sendTime = time.time()
|
||
return sendTime
|
||
|
||
def doOnePing(self, destinationAddress, timeout):
|
||
# 1. Create ICMP socket
|
||
# Sends raw packets to ipv4 addresses
|
||
new_socket = socket.socket(socket.AF_INET, socket.SOCK_RAW, socket.getprotobyname('icmp'))
|
||
ID = 1
|
||
|
||
# 2. Call sendOnePing function
|
||
sendTime = self.sendOnePing(new_socket, destinationAddress, ID)
|
||
|
||
# 3. Call receiveOnePing function
|
||
(recv_time, reply_len, reply_size) = self.receiveOnePing(new_socket, destinationAddress, ID, 1)
|
||
|
||
# 4. Close ICMP socket
|
||
new_socket.close()
|
||
|
||
# 5. Return total network delay
|
||
send_time_ms = sendTime * 1000
|
||
recv_time_ms = recv_time * 1000
|
||
|
||
total_delay = recv_time_ms - send_time_ms
|
||
|
||
return (total_delay, reply_len, reply_size)
|
||
|
||
def __init__(self, args):
|
||
print('Ping to: %s...' % (args.hostname))
|
||
# 1. Look up hostname, resolving it to an IP address
|
||
|
||
# # 2. Call doOnePing function, approximately every second
|
||
try:
|
||
destinationAddress = socket.gethostbyname(args.hostname)
|
||
|
||
while True:
|
||
(total_delay, reply_len, reply_size) = self.doOnePing(destinationAddress, 1)
|
||
# 3. Print out the returned delay (and other relevant details) using the printOneResult method
|
||
self.printOneResult(destinationAddress, reply_len, total_delay, reply_size, args.hostname)
|
||
time.sleep(1)
|
||
|
||
# 4. Continue this process until stopped
|
||
|
||
except BaseException:
|
||
print("Host name not recognised")
|
||
|
||
|
||
class Traceroute(NetworkApplication):
|
||
# Task 1.2: Traceroute
|
||
|
||
def __init__(self, args):
|
||
# Please ensure you print each result using the printOneResult method!
|
||
print('Traceroute to: %s...' % (args.hostname))
|
||
|
||
# Get IP of destination
|
||
dest_address = socket.gethostbyname(args.hostname)
|
||
|
||
# init ttl_count_up
|
||
ttl_count_up = 1
|
||
while True:
|
||
# Creates sockets
|
||
recv_socket = socket.socket(socket.AF_INET, socket.SOCK_RAW, socket.IPPROTO_ICMP)
|
||
send_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_ICMP)
|
||
send_socket.setsockopt(socket.SOL_IP, socket.IP_TTL, ttl_count_up)
|
||
|
||
# COnstruct and send packet
|
||
header = struct.pack('BBHHH', ICMP_TYPE, 0, 0, 5, 1)
|
||
data = "Task 1.2: Traceroute".encode()
|
||
|
||
# NOTE: the direction constrainted by checksum generating function, so the above BBHHH will not be modified
|
||
new_checksum = self.checksum(header + data)
|
||
header = struct.pack('BBHHH', ICMP_TYPE, 0, new_checksum, 5, 1)
|
||
packet = header + data
|
||
send_socket.sendto(packet, (dest_address, 1024 * 10))
|
||
|
||
send_time = time.time() # Record beginning time
|
||
|
||
# Loop until packet received
|
||
run = True
|
||
while run:
|
||
recv_packet, address = recv_socket.recvfrom(1024 * 4)
|
||
address = address[0]
|
||
|
||
run = False
|
||
|
||
send_socket.close()
|
||
recv_socket.close()
|
||
|
||
recv_time = time.time()
|
||
|
||
# try best to resolv hostname
|
||
try:
|
||
hostname = socket.gethostbyaddr(address)[0]
|
||
except BaseException:
|
||
hostname = address
|
||
|
||
self.printOneResult(address, sys.getsizeof(packet), (recv_time - send_time) * 1000, ttl_count_up, hostname)
|
||
ttl_count_up += 1
|
||
|
||
# dest reach, exit loop
|
||
if address == dest_address:
|
||
break
|
||
|
||
|
||
|
||
|
||
class ParisTraceroute(NetworkApplication):
|
||
# Task 1.3: Paris-Traceroute
|
||
# A well-known limitation of trace route is that it may indicate a path that does not actually
|
||
# exist in the presence of “load-balancers” in the network. Consider the example below where
|
||
# a source host Src sends traceroute traffic to a destination host Dst.
|
||
|
||
def getIdentifier(self, checkSumWanted):
|
||
return 0xf7ff - checkSumWanted
|
||
|
||
def __init__(self, args):
|
||
|
||
try:
|
||
print('Paris-Traceroute to: %s...' % (args.hostname))
|
||
|
||
# Get IP of destination
|
||
dest_ip = socket.gethostbyname(args.hostname)
|
||
|
||
ttl_count_up = 1
|
||
# in paris-traceroute, use checksum as identifier
|
||
check_sum_count_up = 1
|
||
|
||
while True:
|
||
# Creates sockets
|
||
recv_socket = socket.socket(socket.AF_INET, socket.SOCK_RAW, socket.IPPROTO_ICMP)
|
||
send_socket = socket.socket(socket.AF_INET, socket.SOCK_RAW, socket.IPPROTO_ICMP)
|
||
|
||
# Limit ttl of socket
|
||
send_socket.setsockopt(socket.SOL_IP, socket.IP_TTL, ttl_count_up)
|
||
|
||
# COnstruct and send packet
|
||
# Header is type (8), code (8), checksum (16), id (16), sequence (16)
|
||
header = struct.pack('!BBHHH', ICMP_TYPE, 0, 0, 0, 0)
|
||
|
||
# NOTE: the checksum acts as a identifier, get the apporiate identifier to get the wanted checksum
|
||
# BE(big endian) used here
|
||
# checked with opensource from https://paris-traceroute.net
|
||
new_identifier = self.getIdentifier(check_sum_count_up)
|
||
header = struct.pack('!BBHHH', ICMP_TYPE, 0, check_sum_count_up, new_identifier, 0)
|
||
|
||
packet = header
|
||
send_socket.sendto(packet, (dest_ip, OUTGOING_BUFFER))
|
||
|
||
# jot down start time for diff
|
||
start_time = time.time()
|
||
|
||
# Loop until packet received
|
||
run = True
|
||
while run:
|
||
recv_packet, address = recv_socket.recvfrom(INCOMING_BUFFER)
|
||
address = address[0]
|
||
|
||
run = False
|
||
|
||
# close socket after done
|
||
send_socket.close()
|
||
recv_socket.close()
|
||
|
||
recv_time = time.time()
|
||
|
||
# try best to resolv hostname
|
||
try:
|
||
try_res_hostname = socket.gethostbyaddr(address)[0]
|
||
except BaseException:
|
||
# bypass if cannot resolv hostname
|
||
try_res_hostname = address
|
||
|
||
self.printOneResult(address, sys.getsizeof(packet), (recv_time - start_time) * 1000, ttl_count_up, try_res_hostname)
|
||
ttl_count_up += 1
|
||
check_sum_count_up += 1
|
||
|
||
# dest reach, exit loop
|
||
if address == dest_ip:
|
||
break
|
||
|
||
except BaseException as err:
|
||
print('error occured', err)
|
||
sys.exit(1)
|
||
|
||
|
||
class WebServer(NetworkApplication):
|
||
|
||
def handleRequest(self, tcpSocket):
|
||
# 1. Receive request message from the client on connection socket
|
||
getrequest = tcpSocket.recv(INCOMING_BUFFER).decode()
|
||
print(getrequest)
|
||
|
||
# 2. Extract the path of the requested object from the message (second
|
||
# part of the HTTP header)
|
||
headers = getrequest.split('\n')
|
||
filename = headers[0].split()[1]
|
||
|
||
try:
|
||
# 3. Read the corresponding file from disk
|
||
filetosend = open(filename.replace('/', ''))
|
||
content = filetosend.read()
|
||
filetosend.close()
|
||
|
||
# 4. Store in temporary buffer
|
||
response = 'HTTP/1.0 200 OK\n\n' + content
|
||
|
||
# 5. Send the correct HTTP response error
|
||
except FileNotFoundError:
|
||
response = 'HTTP/1.0 404 NOT FOUND\n\nFile Not Found'
|
||
|
||
# 6. Send the content of the file to the socket
|
||
tcpSocket.sendall(response.encode())
|
||
|
||
# 7. Close the connection socket
|
||
tcpSocket.close()
|
||
pass
|
||
|
||
def __init__(self, args):
|
||
print('Web Server starting on port: %i...' % (args.port))
|
||
# 1. Create server socket
|
||
server_socket = socket.socket()
|
||
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||
|
||
# 2. Bind the server socket to server address and server port
|
||
server_socket.bind(("127.0.0.1", args.port))
|
||
|
||
# 3. Continuously listen for connections to server socket
|
||
server_socket.listen()
|
||
|
||
# 4. When a connection is accepted, call handleRequest function,
|
||
# passing new connection socket
|
||
# (see https://docs.python.org/3/library/socket.html#socket.socket.accept)
|
||
run = True
|
||
while run:
|
||
client_socket, client_address = server_socket.accept()
|
||
self.handleRequest(client_socket)
|
||
|
||
# 5. Close server socket
|
||
server_socket.close()
|
||
run = False
|
||
|
||
|
||
|
||
|
||
class Proxy(NetworkApplication):
|
||
# Task 2.2: Web Proxy
|
||
# As with Task 2.1, there are a number of ways to test your Web Proxy. For example, to
|
||
# generate requests using curl, we can use the following:
|
||
# curl neverssl.com - -proxy 127.0.0.1: 8000
|
||
# This assumes that the Web Proxy is running on the local machine and bound to port 8000.
|
||
# In this case, the URL requested from the proxy is neverssl.com.
|
||
def handleRequest(self, tcp_socket):
|
||
dst_host = ''
|
||
|
||
# receive request from client
|
||
full_req = tcp_socket.recv(INCOMING_BUFFER).decode('utf-8')
|
||
# print("Full req =", full_req)
|
||
|
||
first_line = full_req.split('\r\n')[0]
|
||
[http_action, full_url, http_ver] = first_line.split(' ')
|
||
sainted_url = full_url.split('://')[1].replace('/', '')
|
||
try_split_port = sainted_url.split(':')
|
||
if (len(try_split_port) > 1):
|
||
dst_host, dst_port = try_split_port
|
||
else:
|
||
dst_host = sainted_url
|
||
dst_port = 80
|
||
|
||
try:
|
||
# try convert to ip, if not emit gaierror
|
||
dst_ip = socket.gethostbyname(dst_host)
|
||
|
||
# create new socket for sending request
|
||
outgoing_req_socket = socket.socket(
|
||
socket.AF_INET, socket.SOCK_STREAM)
|
||
outgoing_req_socket.settimeout(2)
|
||
|
||
# connect to dst server
|
||
outgoing_req_socket.connect((dst_ip, dst_port))
|
||
|
||
# forward request getting from proxy
|
||
outgoing_req_socket.send(full_req.encode('utf-8'))
|
||
# print("forwarded the request")
|
||
|
||
# receive data from the server
|
||
while True:
|
||
reply = outgoing_req_socket.recv(INCOMING_BUFFER)
|
||
|
||
if len(reply) > 0:
|
||
# forward reply to originator
|
||
tcp_socket.send(reply)
|
||
|
||
else:
|
||
# buffer empty, forward reply done
|
||
break
|
||
|
||
# close port
|
||
outgoing_req_socket.close()
|
||
|
||
# handle cannot convert hostname to ip
|
||
except socket.gaierror as msg:
|
||
print("Couldn't convert domain to ip", dst_host, msg)
|
||
|
||
if tcp_socket:
|
||
tcp_socket.close()
|
||
sys.exit(1)
|
||
|
||
# handle socket timeout
|
||
except socket.timeout:
|
||
print("Connection timeout")
|
||
if outgoing_req_socket:
|
||
outgoing_req_socket.close()
|
||
return
|
||
|
||
# final overflow for any error
|
||
except socket.error as msg:
|
||
print("Socket error:", msg)
|
||
if outgoing_req_socket:
|
||
outgoing_req_socket.close()
|
||
if tcp_socket:
|
||
tcp_socket.close()
|
||
sys.exit(1)
|
||
|
||
def __init__(self, args):
|
||
server_ip = '127.0.0.1'
|
||
server_port = args.port
|
||
print('Task 2.2: Web Proxy, starting on port: %i...' % (server_port))
|
||
|
||
# 1. Create server socket
|
||
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||
|
||
# 2. Bind the server socket to server address and server port
|
||
server_socket.bind((server_ip, server_port))
|
||
|
||
# 3. Continuously listen for connections to server socket
|
||
server_socket.listen(1)
|
||
serving = True
|
||
try:
|
||
while serving:
|
||
# 4. When a connection is accepted,
|
||
# -> call handleIncomingRequest function,
|
||
# -> passing new connection socket
|
||
# (see https://docs.python.org/3/library/socket.html#socket.socket.accept)
|
||
connection, address = server_socket.accept()
|
||
self.handleRequest(connection)
|
||
|
||
except socket.error as msg:
|
||
if server_socket:
|
||
server_socket.close()
|
||
print("Socket error:", msg)
|
||
sys.exit(1)
|
||
|
||
finally:
|
||
# 5. Close server socket
|
||
if server_socket:
|
||
server_socket.close()
|
||
sys.exit(0)
|
||
|
||
|
||
if __name__ == "__main__":
|
||
args = setupArgumentParser()
|
||
args.func(args)
|