From 98e0685d9485568479ba5c0a00f25626b99dc008 Mon Sep 17 00:00:00 2001 From: Michael Mattioli Date: Wed, 23 Jul 2014 22:59:53 -0400 Subject: [PATCH 01/59] str.format() on dhcp.py Improved string formatting by removing concatenation and replacing with str.format() usage --- pypxe/dhcp.py | 46 +++++++++++++++++++++++----------------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/pypxe/dhcp.py b/pypxe/dhcp.py index 29de407..f75fe53 100644 --- a/pypxe/dhcp.py +++ b/pypxe/dhcp.py @@ -38,24 +38,24 @@ def __init__(self, **serverSettings): if self.http and not self.ipxe: print '\nWARNING: HTTP selected but iPXE disabled. PXE ROM must support HTTP requests.\n' if self.ipxe and self.http: - self.filename = 'http://%s/%s' % (self.fileserver, self.filename) + self.filename = 'http://{fileserver}/{filename}'.format(fileserver = self.fileserver, filename = self.filename) if self.ipxe and not self.http: - self.filename = 'tftp://%s/%s' % (self.fileserver, self.filename) + self.filename = 'tftp://{fileserver}/{filename}'.format(fileserver = self.fileserver, filename = self.filename) if self.mode_debug: print 'NOTICE: DHCP server started in debug mode. DHCP server is using the following:' - print '\tDHCP Server IP: ' + self.ip - print '\tDHCP Server Port: ' + str (self.port) - print '\tDHCP Lease Range: ' + self.offerfrom + ' - ' + self.offerto - print '\tDHCP Subnet Mask: ' + self.subnetmask - print '\tDHCP Router: ' + self.router - print '\tDHCP DNS Server: ' + self.dnsserver - print '\tDHCP Broadcast Address: ' + self.broadcast - print '\tDHCP File Server IP: ' + self.fileserver - print '\tDHCP File Name: ' + self.filename - print '\tProxyDHCP Mode: ' + str(self.mode_proxy) - print '\tUsing iPXE: ' + str(self.ipxe) - print '\tUsing HTTP Server: ' + str(self.http) + print '\tDHCP Server IP: {}'.format(self.ip) + print '\tDHCP Server Port: {}'.format(self.port) + print '\tDHCP Lease Range: {} - {}'.format(self.offerfrom, self.offterto) + print '\tDHCP Subnet Mask: {}'.format(self.subnetmask) + print '\tDHCP Router: {}'.format(self.router) + print '\tDHCP DNS Server: {}'.format(self.dnsserver) + print '\tDHCP Broadcast Address: {}'.format(self.broadcast) + print '\tDHCP File Server IP: {}'.format(self.fileserver) + print '\tDHCP File Name: {}'.format(self.filename) + print '\tProxyDHCP Mode: {}'.format(self.mode_proxy) + print '\tUsing iPXE: {}'.format(self.ipxe) + print '\tUsing HTTP Server: {}'.format(self.http) self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) @@ -148,7 +148,7 @@ def craftHeader(self, message): self.leases[clientmac]['ip'] = offer self.leases[clientmac]['expire'] = time() + 86400 if self.mode_debug: - print '[DEBUG] New DHCP Assignment - MAC: ' + self.printMAC(clientmac) + ' -> IP: ' + self.leases[clientmac]['ip'] + print '[DEBUG] New DHCP Assignment - MAC: {MAC} -> IP: {IP}'.format(MAC = self.printMAC(clientmac), IP = self.leases[clientmac]['ip']) response += socket.inet_aton(offer) #yiaddr else: response += socket.inet_aton('0.0.0.0') @@ -203,9 +203,9 @@ def dhcpOffer(self, message): response = headerResponse + optionsResponse if self.mode_debug: print '[DEBUG] DHCPOFFER - Sending the following' - print '\t<--BEGIN HEADER-->\n\t' + repr(headerResponse) + '\n\t<--END HEADER-->' - print '\t<--BEGIN OPTIONS-->\n\t' + repr(optionsResponse) + '\n\t<--END OPTIONS-->' - print '\t<--BEGIN RESPONSE-->\n\t' + repr(response) + '\n\t<--END RESPONSE-->' + print '\t<--BEGIN HEADER-->\n\t{headerResponse}\n\t<--END HEADER-->'.format(headerResponse = repr(headerResponse)) + print '\t<--BEGIN OPTIONS-->\n\t{optionsResponse}\n\t<--END OPTIONS-->'.format(optionsResponse = repr(optionsResponse)) + print '\t<--BEGIN RESPONSE-->\n\t{response}\n\t<--END RESPONSE-->'.format(response = repr(response)) self.sock.sendto(response, (self.broadcast, 68)) def dhcpAck(self, message): @@ -215,9 +215,9 @@ def dhcpAck(self, message): response = headerResponse + optionsResponse if self.mode_debug: print '[DEBUG] DHCPACK - Sending the following' - print '\t<--BEGIN HEADER-->\n\t' + repr(headerResponse) + '\n\t<--END HEADER-->' - print '\t<--BEGIN OPTIONS-->\n\t' + repr(optionsResponse) + '\n\t<--END OPTIONS-->' - print '\t<--BEGIN RESPONSE-->\n\t' + repr(response) + '\n\t<--END RESPONSE-->' + print '\t<--BEGIN HEADER-->\n\t{headerResponse}\n\t<--END HEADER-->'.format(headerResponse = repr(headerResponse)) + print '\t<--BEGIN OPTIONS-->\n\t{optionsResponse}\n\t<--END OPTIONS-->'.format(optionsResponse = repr(optionsResponse)) + print '\t<--BEGIN RESPONSE-->\n\t{response}\n\t<--END RESPONSE-->'.format(response = repr(response)) self.sock.sendto(response, (self.broadcast, 68)) def listen(self): @@ -227,11 +227,11 @@ def listen(self): clientmac = struct.unpack('!28x6s', message[:34]) if self.mode_debug: print '[DEBUG] Received message' - print '\t<--BEGIN MESSAGE-->\n\t' + repr(message) + '\n\t<--END MESSAGE-->' + print '\t<--BEGIN MESSAGE-->\n\t{message}\n\t<--END MESSAGE-->'.format(message = repr(message)) options = self.tlvParse(message[240:]) if self.mode_debug: print '[DEBUG] Parsed received options' - print '\t<--BEGIN OPTIONS-->\n\t' + repr(options) + '\n\t<--END OPTIONS-->' + print '\t<--BEGIN OPTIONS-->\n\t{options}\n\t<--END OPTIONS-->'.format(options = repr(options)) if not (60 in options and 'PXEClient' in options[60][0]) : continue type = ord(options[53][0]) #see RFC2131 page 10 if type == 1: From c1a1c3b99d6ec17bcf849bbfecb16743b303ef3d Mon Sep 17 00:00:00 2001 From: Michael Mattioli Date: Wed, 23 Jul 2014 23:11:15 -0400 Subject: [PATCH 02/59] Correct Typo in dhcp.py Correct a typo in a variable name --- pypxe/dhcp.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pypxe/dhcp.py b/pypxe/dhcp.py index f75fe53..fdbde65 100644 --- a/pypxe/dhcp.py +++ b/pypxe/dhcp.py @@ -46,7 +46,7 @@ def __init__(self, **serverSettings): print 'NOTICE: DHCP server started in debug mode. DHCP server is using the following:' print '\tDHCP Server IP: {}'.format(self.ip) print '\tDHCP Server Port: {}'.format(self.port) - print '\tDHCP Lease Range: {} - {}'.format(self.offerfrom, self.offterto) + print '\tDHCP Lease Range: {} - {}'.format(self.offerfrom, self.offerto) print '\tDHCP Subnet Mask: {}'.format(self.subnetmask) print '\tDHCP Router: {}'.format(self.router) print '\tDHCP DNS Server: {}'.format(self.dnsserver) From f06814f73b76bd0db44e82ca983b284d3c45c789 Mon Sep 17 00:00:00 2001 From: Michael Mattioli Date: Wed, 23 Jul 2014 23:19:44 -0400 Subject: [PATCH 03/59] str.format() on http.py Improved string formatting by removing concatenation and replacing with str.format() usage --- pypxe/http.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/pypxe/http.py b/pypxe/http.py index f693fc2..8f5fa7f 100644 --- a/pypxe/http.py +++ b/pypxe/http.py @@ -31,16 +31,16 @@ def __init__(self, **serverSettings): if self.mode_debug: print 'NOTICE: HTTP server started in debug mode. HTTP server is using the following:' - print '\tHTTP Server IP: ' + self.ip - print '\tHTTP Server Port: ' + str(self.port) - print '\tHTTP Network Boot Directory: ' + self.netbootDirectory + print '\tHTTP Server IP: {}'.format(self.ip) + print '\tHTTP Server Port: {}'.format(self.port) + print '\tHTTP Network Boot Directory: {}'.format(self.netbootDirectory) def handleRequest(self, connection, addr): '''This method handles HTTP request''' request = connection.recv(1024) if self.mode_debug: - print '[DEBUG] HTTP Recieved message from ' + repr(addr) - print '\t<--BEGIN MESSAGE-->\n\t' + repr(request) + '\n\t<--END MESSAGE-->' + print '[DEBUG] HTTP Recieved message from {addr}'.format(addr = repr(addr)) + print '\t<--BEGIN MESSAGE-->\n\t{request}\n\t<--END MESSAGE-->'.format(request = repr(request)) startline = request.split('\r\n')[0].split(' ') method = startline[0] target = startline[1] @@ -55,8 +55,8 @@ def handleRequest(self, connection, addr): connection.send(response) connection.close() if self.mode_debug: - print '[DEBUG] HTTP Sending message to ' + repr(addr) - print '\t<--BEING MESSAGE-->\n\t' + repr(response) + '\n\t<--END MESSAGE-->' + print '[DEBUG] HTTP Sending message to {addr}'.format(addr = repr(addr)) + print '\t<--BEING MESSAGE-->\n\t{response}\n\t<--END MESSAGE-->'.format(response = repr(response)) return response += 'Content-Length: %d\r\n' % os.path.getsize(target) response += '\r\n' @@ -64,8 +64,8 @@ def handleRequest(self, connection, addr): connection.send(response) connection.close() if self.mode_debug: - print '[DEBUG] HTTP Sending message to ' + repr(addr) - print '\t<--BEING MESSAGE-->\n\t' + repr(response) + '\n\t<--END MESSAGE-->' + print '[DEBUG] HTTP Sending message to {addr}'.format(addr = repr(addr)) + print '\t<--BEING MESSAGE-->\n\t{response}\n\t<--END MESSAGE-->'.format(response = repr(response)) return handle = open(target) response += handle.read() @@ -73,9 +73,9 @@ def handleRequest(self, connection, addr): connection.send(response) connection.close() if self.mode_debug: - print '[DEBUG] HTTP Sending message to ' + repr(addr) - print '\t<--BEING MESSAGE-->\n\t' + repr(response) + '\n\t<--END MESSAGE-->' - print '\tHTTP File Sent - http://%s -> %s:%d' % (target, addr[0], addr[1]) + print '[DEBUG] HTTP Sending message to {addr}'.format(addr = repr(addr)) + print '\t<--BEING MESSAGE-->\n\t{response}\n\t<--END MESSAGE-->'.format(response = repr(response)) + print '\tHTTP File Sent - http://{target} -> {addr[0]}:{addr[1]}'.format(target = target, addr = addr) def listen(self): '''This method is the main loop that listens for requests''' From 2f820ece79fbab6483024a52827f020a327572a8 Mon Sep 17 00:00:00 2001 From: Michael Mattioli Date: Wed, 23 Jul 2014 23:23:47 -0400 Subject: [PATCH 04/59] str.format() on tftp.py Improved string formatting by removing concatenation and replacing with str.format() usage --- pypxe/tftp.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/pypxe/tftp.py b/pypxe/tftp.py index 56a48e3..a3d7b77 100644 --- a/pypxe/tftp.py +++ b/pypxe/tftp.py @@ -25,9 +25,9 @@ def __init__(self, **serverSettings): if self.mode_debug: print 'NOTICE: TFTP server started in debug mode. TFTP server is using the following:' - print '\tTFTP Server IP: ' + self.ip - print '\tTFTP Server Port: ' + str(self.port) - print '\tTFTP Network Boot Directory: ' + self.netbootDirectory + print '\tTFTP Server IP: {}'.format(self.ip) + print '\tTFTP Server Port: {}'.format(self.port) + print '\tTFTP Network Boot Directory: {}'.format(self.netbootDirectory) #key is (address, port) pair self.ongoing = defaultdict(lambda: {'filename': '', 'handle': None, 'block': 1, 'blksize': 512}) @@ -72,11 +72,11 @@ def sendBlock(self, address): if len(data) != descriptor['blksize']: descriptor['handle'].close() if self.mode_debug: - print '[DEBUG] TFTP File Sent - tftp://%s -> %s:%d' % (descriptor['filename'], address[0], address[1]) + print '[DEBUG] TFTP File Sent - tftp://{filename} -> {address[0]}:{address[1]}'.format(filename = descriptor['filename'], address = address) self.ongoing.pop(address) else: if self.mode_debug: - print '[DEBUG] TFTP Sending block ' + repr(descriptor['block']) + print '[DEBUG] TFTP Sending block {block}'.format(block = repr(descriptor['block']) descriptor['block'] += 1 def read(self, address, message): @@ -102,7 +102,7 @@ def read(self, address, message): filesize = os.path.getsize(self.ongoing[address]['filename']) if filesize > (2**16 * self.ongoing[address]['blksize']): print '\nWARNING: TFTP request too big, attempting transfer anyway.\n' - print '\tDetails: Filesize %s is too big for blksize %s.\n' % (filesize, self.ongoing[address]['blksize']) + print '\tDetails: Filesize {filesize} is too big for blksize {blksize}.\n'.format(filesize = filesize, blksize = self.ongoing[address]['blksize']) if 'tsize' in options: response += 'tsize' + chr(0) response += str(filesize) From 208d36eedf49f21261967f6ed101271d50674657 Mon Sep 17 00:00:00 2001 From: Michael Mattioli Date: Wed, 23 Jul 2014 23:26:41 -0400 Subject: [PATCH 05/59] Missing Paren in tftp.py Added missing paren in tftp.py causing failure to run --- pypxe/tftp.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pypxe/tftp.py b/pypxe/tftp.py index a3d7b77..3fc6d59 100644 --- a/pypxe/tftp.py +++ b/pypxe/tftp.py @@ -76,7 +76,7 @@ def sendBlock(self, address): self.ongoing.pop(address) else: if self.mode_debug: - print '[DEBUG] TFTP Sending block {block}'.format(block = repr(descriptor['block']) + print '[DEBUG] TFTP Sending block {block}'.format(block = repr(descriptor['block'])) descriptor['block'] += 1 def read(self, address, message): From 3978b591adf65c93fb4036091136989cf9bb74ed Mon Sep 17 00:00:00 2001 From: PsychoMario Date: Sat, 21 Feb 2015 13:04:10 +0000 Subject: [PATCH 06/59] Added json config support --- README.md | 4 ++++ example.json | 19 +++++++++++++++++++ pypxe-server.py | 26 ++++++++++++++++++++++++++ 3 files changed, 49 insertions(+) create mode 100644 example.json diff --git a/README.md b/README.md index 7464fa0..697bbac 100644 --- a/README.md +++ b/README.md @@ -58,6 +58,10 @@ The following are arguments that can be passed to `pypxe-server.py` when running * Description: Enable selected services in DEBUG mode * _This adds a level of verbosity so that you can see what's happening in the background. Debug statements are prefixed with `[DEBUG]` and indented to distinguish between normal output that the services give._ * Default: `False` + * __`--config`__ + * Description: Amend configuration from json file + * _Use the specified json file to amend the command line options. See example.json for more information._ + * Default: `None` * __DHCP Service Arguments__ _each of the following can be set one of two ways, you can use either/or_ * __`-s DHCP_SERVER_IP`__ or __`--dhcp-server-ip DHCP_SERVER_IP`__ * Description: Specify DHCP server IP address diff --git a/example.json b/example.json new file mode 100644 index 0000000..615c79f --- /dev/null +++ b/example.json @@ -0,0 +1,19 @@ +{ +"NETBOOT_DIR":"netboot", +"NETBOOT_FILE":"", +"DHCP_SERVER_IP":"192.168.2.2", +"DHCP_SERVER_PORT":67, +"DHCP_OFFER_BEGIN":"192.168.2.100", +"DHCP_OFFER_END":"192.168.2.150", +"DHCP_SUBNET":"255.255.255.0", +"DHCP_ROUTER":"192.168.2.1", +"DHCP_DNS":"8.8.8.8", +"DHCP_BROADCAST":"", +"DHCP_FILESERVER":"192.168.2.2", +"USE_IPXE":false, +"USE_HTTP":false, +"USE_TFTP":true, +"MODE_DEBUG":false, +"USE_DHCP":false, +"DHCP_MODE_PROXY":false +} diff --git a/pypxe-server.py b/pypxe-server.py index 70b6f53..675b631 100644 --- a/pypxe-server.py +++ b/pypxe-server.py @@ -1,6 +1,7 @@ import threading import os import sys +import json try: import argparse @@ -12,6 +13,9 @@ from pypxe import dhcp #PyPXE DHCP service from pypxe import http #PyPXE HTTP service +#json default +JSON_CONFIG = '' + #Default Network Boot File Directory NETBOOT_DIR = 'netboot' @@ -29,6 +33,14 @@ DHCP_BROADCAST = '' DHCP_FILESERVER = '192.168.2.2' +#Service bools +USE_IPXE = False +USE_HTTP = False +USE_TFTP = True +MODE_DEBUG = False +USE_DHCP = False +DHCP_MODE_PROXY = False + if __name__ == '__main__': try: #warn the user that they are starting PyPXE as non-root user @@ -45,6 +57,7 @@ parser.add_argument('--http', action = 'store_true', dest = 'USE_HTTP', help = 'Enable built-in HTTP server', default = False) parser.add_argument('--no-tftp', action = 'store_false', dest = 'USE_TFTP', help = 'Disable built-in TFTP server, by default it is enabled', default = True) parser.add_argument('--debug', action = 'store_true', dest = 'MODE_DEBUG', help = 'Adds verbosity to the selected services while they run', default = False) + parser.add_argument('--config', action = 'store', dest = 'JSON_CONFIG', help = 'Configure from a json file rather than the command line', default = JSON_CONFIG) #argument group for DHCP server exclusive = parser.add_mutually_exclusive_group(required = False) @@ -66,6 +79,19 @@ #parse the arguments given args = parser.parse_args() + if args.JSON_CONFIG: + try: + config = open(args.JSON_CONFIG) + except IOError: + sys.exit("Failed to open %s" % args.JSON_CONFIG) + try: + loadedcfg = json.load(config) + config.close() + except ValueError: + sys.exit("%s does not contain valid json" % args.JSON_CONFIG) + dargs = vars(args) + dargs.update(loadedcfg) + args = argparse.Namespace(**dargs) #pass warning to user regarding starting HTTP server without iPXE if args.USE_HTTP and not args.USE_IPXE and not args.USE_DHCP: From fadeab7a57d9b97cee0f511475a8c062bb7b6c67 Mon Sep 17 00:00:00 2001 From: PsychoMario Date: Sat, 21 Feb 2015 19:24:16 +0000 Subject: [PATCH 07/59] added granular debug operated. csv --- pypxe-server.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pypxe-server.py b/pypxe-server.py index 70b6f53..a12e02c 100644 --- a/pypxe-server.py +++ b/pypxe-server.py @@ -44,7 +44,7 @@ parser.add_argument('--ipxe', action = 'store_true', dest = 'USE_IPXE', help = 'Enable iPXE ROM', default = False) parser.add_argument('--http', action = 'store_true', dest = 'USE_HTTP', help = 'Enable built-in HTTP server', default = False) parser.add_argument('--no-tftp', action = 'store_false', dest = 'USE_TFTP', help = 'Disable built-in TFTP server, by default it is enabled', default = True) - parser.add_argument('--debug', action = 'store_true', dest = 'MODE_DEBUG', help = 'Adds verbosity to the selected services while they run', default = False) + parser.add_argument('--debug', action = 'store', dest = 'MODE_DEBUG', help = 'Comma Seperated (http,tftp,dhcp). Adds verbosity to the selected services while they run', default = '') #argument group for DHCP server exclusive = parser.add_mutually_exclusive_group(required = False) @@ -93,7 +93,7 @@ #configure/start TFTP server if args.USE_TFTP: print 'Starting TFTP server...' - tftpServer = tftp.TFTPD(mode_debug = args.MODE_DEBUG) + tftpServer = tftp.TFTPD(mode_debug = ("tftp" in args.MODE_DEBUG.lower() or "all" in args.MODE_DEBUG.lower())) tftpd = threading.Thread(target = tftpServer.listen) tftpd.daemon = True tftpd.start() @@ -119,7 +119,7 @@ useipxe = args.USE_IPXE, usehttp = args.USE_HTTP, mode_proxy = args.DHCP_MODE_PROXY, - mode_debug = args.MODE_DEBUG) + mode_debug = ("dhcp" in args.MODE_DEBUG.lower() or "all" in args.MODE_DEBUG.lower())) dhcpd = threading.Thread(target = dhcpServer.listen) dhcpd.daemon = True dhcpd.start() @@ -129,7 +129,7 @@ #configure/start HTTP server if args.USE_HTTP: print 'Starting HTTP server...' - httpServer = http.HTTPD(mode_debug = args.MODE_DEBUG) + httpServer = http.HTTPD(mode_debug = ("http" in args.MODE_DEBUG.lower() or "all" in args.MODE_DEBUG.lower())) httpd = threading.Thread(target = httpServer.listen) httpd.daemon = True httpd.start() From ef1f2e85f6cae4d27794e4264c3674f8794907ff Mon Sep 17 00:00:00 2001 From: PsychoMario Date: Sat, 21 Feb 2015 20:20:19 +0000 Subject: [PATCH 08/59] Server TID support, #40 --- pypxe/tftp.py | 33 ++++++++++++++++++++------------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/pypxe/tftp.py b/pypxe/tftp.py index a9398cb..049c7da 100644 --- a/pypxe/tftp.py +++ b/pypxe/tftp.py @@ -7,6 +7,7 @@ import socket import struct import os +import select from collections import defaultdict class TFTPD: @@ -30,7 +31,7 @@ def __init__(self, **serverSettings): print '\tTFTP Network Boot Directory: {}'.format(self.netbootDirectory) #key is (address, port) pair - self.ongoing = defaultdict(lambda: {'filename': '', 'handle': None, 'block': 1, 'blksize': 512}) + self.ongoing = defaultdict(lambda: {'filename': '', 'handle': None, 'block': 1, 'blksize': 512, 'sock': None}) # Start in network boot file directory and then chroot, # this simplifies target later as well as offers a slight security increase @@ -68,11 +69,12 @@ def sendBlock(self, address): response += struct.pack('!H', descriptor['block'] % 2 ** 16) data = descriptor['handle'].read(descriptor['blksize']) response += data - self.sock.sendto(response, address) + descriptor['sock'].sendto(response, address) if len(data) != descriptor['blksize']: descriptor['handle'].close() if self.mode_debug: print '[DEBUG] TFTP File Sent - tftp://{filename} -> {address[0]}:{address[1]}'.format(filename = descriptor['filename'], address = address) + descriptor['sock'].close() self.ongoing.pop(address) else: if self.mode_debug: @@ -109,18 +111,23 @@ def read(self, address, message): response += chr(0) if response: response = struct.pack('!H', 6) + response - self.sock.sendto(response, address) - self.sendBlock(address) + socknew = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + socknew.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + socknew.bind((self.ip, 0)) + socknew.sendto(response, address) + self.ongoing[address]['sock'] = socknew def listen(self): '''This method listens for incoming requests''' while True: - message, address = self.sock.recvfrom(1024) - opcode = struct.unpack('!H', message[:2])[0] - if opcode == 1: #read the request - if self.mode_debug: - print '[DEBUG] TFTP receiving request' - self.read(address, message) - if opcode == 4: - if self.ongoing.has_key(address): - self.sendBlock(address) + rlist, wlist, xlist = select.select([self.sock] + [self.ongoing[i]['sock'] for i in self.ongoing], [], []) + for sock in rlist: + message, address = sock.recvfrom(1024) + opcode = struct.unpack('!H', message[:2])[0] + if opcode == 1: #read the request + if self.mode_debug: + print '[DEBUG] TFTP receiving request' + self.read(address, message) + if opcode == 4: + if self.ongoing.has_key(address): + self.sendBlock(address) From cecb19bf68e58a8422142712b39d2f8ca7d56c34 Mon Sep 17 00:00:00 2001 From: PsychoMario Date: Sat, 21 Feb 2015 21:02:48 +0000 Subject: [PATCH 09/59] Proper blockcounting for #39 --- pypxe/tftp.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/pypxe/tftp.py b/pypxe/tftp.py index 049c7da..51f23ce 100644 --- a/pypxe/tftp.py +++ b/pypxe/tftp.py @@ -31,7 +31,7 @@ def __init__(self, **serverSettings): print '\tTFTP Network Boot Directory: {}'.format(self.netbootDirectory) #key is (address, port) pair - self.ongoing = defaultdict(lambda: {'filename': '', 'handle': None, 'block': 1, 'blksize': 512, 'sock': None}) + self.ongoing = defaultdict(lambda: {'filename': '', 'handle': None, 'block': 1, 'blksize': 512, 'sock':None}) # Start in network boot file directory and then chroot, # this simplifies target later as well as offers a slight security increase @@ -40,11 +40,11 @@ def __init__(self, **serverSettings): def filename(self, message): ''' - The first null-delimited field after the OPCODE + The first null-delimited field is the filename. This method returns the filename from the message. ''' - return message[2:].split(chr(0))[0] + return message.split(chr(0))[0] def notFound(self, address): ''' @@ -69,6 +69,8 @@ def sendBlock(self, address): response += struct.pack('!H', descriptor['block'] % 2 ** 16) data = descriptor['handle'].read(descriptor['blksize']) response += data + if self.mode_debug: + print '[DEBUG] TFTP Sending block {block}'.format(block = repr(descriptor['block'])) descriptor['sock'].sendto(response, address) if len(data) != descriptor['blksize']: descriptor['handle'].close() @@ -76,10 +78,6 @@ def sendBlock(self, address): print '[DEBUG] TFTP File Sent - tftp://{filename} -> {address[0]}:{address[1]}'.format(filename = descriptor['filename'], address = address) descriptor['sock'].close() self.ongoing.pop(address) - else: - if self.mode_debug: - print '[DEBUG] TFTP Sending block {block}'.format(block = repr(descriptor['block'])) - descriptor['block'] += 1 def read(self, address, message): ''' @@ -93,7 +91,7 @@ def read(self, address, message): return self.ongoing[address]['filename'] = filename self.ongoing[address]['handle'] = open(filename, 'r') - options = message.split(chr(0))[3: -1] + options = message.split(chr(0))[2: -1] options = dict(zip(options[0::2], options[1::2])) response = '' if 'blksize' in options: @@ -120,14 +118,17 @@ def read(self, address, message): def listen(self): '''This method listens for incoming requests''' while True: - rlist, wlist, xlist = select.select([self.sock] + [self.ongoing[i]['sock'] for i in self.ongoing], [], []) + rlist, wlist, xlist = select.select([self.sock] + [self.ongoing[i]['sock'] for i in self.ongoing if self.ongoing[i]['sock']], [], []) for sock in rlist: message, address = sock.recvfrom(1024) opcode = struct.unpack('!H', message[:2])[0] + message = message[2:] if opcode == 1: #read the request if self.mode_debug: print '[DEBUG] TFTP receiving request' self.read(address, message) if opcode == 4: if self.ongoing.has_key(address): + blockack = struct.unpack("!H", message[:2])[0] + self.ongoing[address]['block'] = blockack + 1 self.sendBlock(address) From 8cd1e7580e706a0ecf73c3c943306c8749cd6b48 Mon Sep 17 00:00:00 2001 From: PsychoMario Date: Sat, 21 Feb 2015 21:34:53 +0000 Subject: [PATCH 10/59] Retransmit, Timeout per #39 --- pypxe/tftp.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/pypxe/tftp.py b/pypxe/tftp.py index 51f23ce..8f53516 100644 --- a/pypxe/tftp.py +++ b/pypxe/tftp.py @@ -8,6 +8,7 @@ import struct import os import select +import time from collections import defaultdict class TFTPD: @@ -31,7 +32,7 @@ def __init__(self, **serverSettings): print '\tTFTP Network Boot Directory: {}'.format(self.netbootDirectory) #key is (address, port) pair - self.ongoing = defaultdict(lambda: {'filename': '', 'handle': None, 'block': 1, 'blksize': 512, 'sock':None}) + self.ongoing = defaultdict(lambda: {'filename': '', 'handle': None, 'block': 1, 'blksize': 512, 'sock':None, 'timeout':float("inf"), 'retries':3}) # Start in network boot file directory and then chroot, # this simplifies target later as well as offers a slight security increase @@ -65,13 +66,15 @@ def sendBlock(self, address): short int 3 -> Data Block ''' descriptor = self.ongoing[address] - response = struct.pack('!H', 3) #opcode 3 is DATA, also sent block number + response = struct.pack('!H', 3) #opcode 3 is DATA, also sent block number response += struct.pack('!H', descriptor['block'] % 2 ** 16) data = descriptor['handle'].read(descriptor['blksize']) response += data if self.mode_debug: print '[DEBUG] TFTP Sending block {block}'.format(block = repr(descriptor['block'])) descriptor['sock'].sendto(response, address) + self.ongoing[address]['retries'] -= 1 + self.ongoing[address]['timeout'] = time.time() if len(data) != descriptor['blksize']: descriptor['handle'].close() if self.mode_debug: @@ -131,4 +134,14 @@ def listen(self): if self.ongoing.has_key(address): blockack = struct.unpack("!H", message[:2])[0] self.ongoing[address]['block'] = blockack + 1 + self.ongoing[address]['retries'] = 3 self.sendBlock(address) + #Timeouts and Retries. Done after the above so timeout actually has a value + #Resent those that have timed out + for i in self.ongoing: + if self.ongoing[i]['timeout']+5 < time.time() and self.ongoing[i]['retries']: + print self.ongoing[i]['handle'].tell() + self.ongoing[i]['handle'].seek(-self.ongoing[i]['blksize'], 1) + self.sendBlock(i) + if not self.ongoing[i]['retries']: + self.ongoing.pop(i) From 1d797e90bb55e07f62f4d03dd6fa0fb521f8b8b5 Mon Sep 17 00:00:00 2001 From: PsychoMario Date: Sat, 21 Feb 2015 22:43:38 +0000 Subject: [PATCH 11/59] UEFI serving, per #46 --- netboot/ldlinux.e32 | Bin 0 -> 117376 bytes netboot/ldlinux.e64 | Bin 0 -> 134744 bytes netboot/syslinux.efi32 | Bin 0 -> 172494 bytes netboot/syslinux.efi64 | Bin 0 -> 176456 bytes pypxe/dhcp.py | 29 +++++++++++++++++++++++------ 5 files changed, 23 insertions(+), 6 deletions(-) create mode 100644 netboot/ldlinux.e32 create mode 100644 netboot/ldlinux.e64 create mode 100755 netboot/syslinux.efi32 create mode 100755 netboot/syslinux.efi64 diff --git a/netboot/ldlinux.e32 b/netboot/ldlinux.e32 new file mode 100644 index 0000000000000000000000000000000000000000..a65ef91c0bb5208f7bf791a948dc16fa871c0223 GIT binary patch literal 117376 zcmaHU3s_Xu`u-jqWz^9bjg*pV%qbzYpvW}QPywmb#tR6hj>tt&1cDj7{4j_1U~IR$ zEUaVYDJnb1j#ikLEH90sV4W`H(X?~AP+6^Myr7w)ne%_YwbuZ0&hww=*}VH*mv3FZ zZ{63{70DA*3B!mZW%Kb))VVFWKM|EnO$|<^G}Yghv_-lKG553MXQf zq%eSSzoG3}2To5na>p~}Csz*OzkU=S(WQfU7T`(5(-V(zLYD&`$($G_g^leY+4~}# zF)UoFMYj8dMMy?tqBUZJ6zz*NAulCOiX=G@OOm`&!z{@&u1TuBTN)>Mj8fk&6HG~Q zPZ$iw#9KGb3X^;W(+I0H&X9Pktxp~zgjJftx=&SR7`pF|O^lURAqTdmo`#(I4N~<~ z^H`h76L!d8M=lIfQul6SD+aE)Rf_k#X87jTSFK(|XGRMOU@(31`hgp44-K|TUYkj> zj;-vz#I!(qc0gThuS7|@-OwXhGH$%vTxXL8*4ax{I^OVfr>}Nvj)#Icxv#h#q%5<`aO^5FL+4L^m_r%i+EnbvmVc1@w|-Z6+E?g zUd6Kk&)@O<1J5QroAJDb=WRS4e_KGd;@O60JDweQLcjO%vKP;Xczk&1w_gY80FZ2{ z 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 literal 0 HcmV?d00001 diff --git a/netboot/ldlinux.e64 b/netboot/ldlinux.e64 new file mode 100644 index 0000000000000000000000000000000000000000..174cb94464d1b42814669a25ed658cccd8760d86 GIT binary patch literal 134744 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�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�Hy6G6RcvHQ?2od= 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 literal 0 HcmV?d00001 diff --git a/netboot/syslinux.efi64 b/netboot/syslinux.efi64 new file mode 100755 index 0000000000000000000000000000000000000000..cc5ffe2c963fd009224163d5b0232af8bc636298 GIT binary patch literal 176456 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 literal 0 HcmV?d00001 diff --git a/pypxe/dhcp.py b/pypxe/dhcp.py index fdbde65..7a09947 100644 --- a/pypxe/dhcp.py +++ b/pypxe/dhcp.py @@ -28,7 +28,12 @@ def __init__(self, **serverSettings): self.dnsserver = serverSettings.get('dnsserver', '8.8.8.8') self.broadcast = serverSettings.get('broadcast', '') self.fileserver = serverSettings.get('fileserver', '192.168.2.2') - self.filename = serverSettings.get('filename', 'pxelinux.0') + self.filename = serverSettings.get('filename', '') + if not self.filename: + self.forcefilename = True + self.filename = "pxelinux.0" + else: + self.forcefilename = False self.ipxe = serverSettings.get('useipxe', False) self.http = serverSettings.get('usehttp', False) self.mode_proxy = serverSettings.get('mode_proxy', False) #ProxyDHCP mode @@ -185,7 +190,19 @@ def craftOptions(self, opt53, clientmac): #filename null terminated if not self.ipxe or not self.leases[clientmac]['ipxe']: - response += self.tlvEncode(67, self.filename + chr(0)) + #http://www.syslinux.org/wiki/index.php/PXELINUX#UEFI + if 93 in self.options: + [arch] = struct.unpack("!H", self.options[93][0]) + if arch == 6: #EFI IA32 + response += self.tlvEncode(67, "syslinux.efi32" + chr(0)) + elif arch == 7: #EFI BC, x86-64 according to link above + response += self.tlvEncode(67, "syslinux.efi64" + chr(0)) + elif arch == 9: #EFI x86-64 + response += self.tlvEncode(67, "syslinux.efi64" + chr(0)) + if arch == 0: #BIOS/default + response += self.tlvEncode(67, "pxelinux.0" + chr(0)) + else: + response += self.tlvEncode(67, self.filename + chr(0)) else: response += self.tlvEncode(67, '/chainload.kpxe' + chr(0)) #chainload iPXE if opt53 == 5: #ACK @@ -228,12 +245,12 @@ def listen(self): if self.mode_debug: print '[DEBUG] Received message' print '\t<--BEGIN MESSAGE-->\n\t{message}\n\t<--END MESSAGE-->'.format(message = repr(message)) - options = self.tlvParse(message[240:]) + self.options = self.tlvParse(message[240:]) if self.mode_debug: print '[DEBUG] Parsed received options' - print '\t<--BEGIN OPTIONS-->\n\t{options}\n\t<--END OPTIONS-->'.format(options = repr(options)) - if not (60 in options and 'PXEClient' in options[60][0]) : continue - type = ord(options[53][0]) #see RFC2131 page 10 + print '\t<--BEGIN OPTIONS-->\n\t{options}\n\t<--END OPTIONS-->'.format(options = repr(self.options)) + if not (60 in self.options and 'PXEClient' in self.options[60][0]) : continue + type = ord(self.options[53][0]) #see RFC2131 page 10 if type == 1: if self.mode_debug: print '[DEBUG] Received DHCPOFFER' From b428de80be1abb20d49f441becc79b9826a39a29 Mon Sep 17 00:00:00 2001 From: PsychoMario Date: Sat, 21 Feb 2015 22:46:08 +0000 Subject: [PATCH 12/59] Fixed so setting the command line option overrides option 93 --- pypxe/dhcp.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pypxe/dhcp.py b/pypxe/dhcp.py index 7a09947..51d0215 100644 --- a/pypxe/dhcp.py +++ b/pypxe/dhcp.py @@ -30,10 +30,10 @@ def __init__(self, **serverSettings): self.fileserver = serverSettings.get('fileserver', '192.168.2.2') self.filename = serverSettings.get('filename', '') if not self.filename: - self.forcefilename = True + self.forcefilename = False self.filename = "pxelinux.0" else: - self.forcefilename = False + self.forcefilename = True self.ipxe = serverSettings.get('useipxe', False) self.http = serverSettings.get('usehttp', False) self.mode_proxy = serverSettings.get('mode_proxy', False) #ProxyDHCP mode @@ -191,7 +191,7 @@ def craftOptions(self, opt53, clientmac): #filename null terminated if not self.ipxe or not self.leases[clientmac]['ipxe']: #http://www.syslinux.org/wiki/index.php/PXELINUX#UEFI - if 93 in self.options: + if 93 in self.options and not self.forcefilename: [arch] = struct.unpack("!H", self.options[93][0]) if arch == 6: #EFI IA32 response += self.tlvEncode(67, "syslinux.efi32" + chr(0)) From db6a06465072e2b4965fb37f9ecda1ad83585c7c Mon Sep 17 00:00:00 2001 From: PsychoMario Date: Sun, 22 Feb 2015 14:57:47 +0000 Subject: [PATCH 13/59] removed relic --- pypxe-server.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/pypxe-server.py b/pypxe-server.py index 675b631..c7a7579 100644 --- a/pypxe-server.py +++ b/pypxe-server.py @@ -33,14 +33,6 @@ DHCP_BROADCAST = '' DHCP_FILESERVER = '192.168.2.2' -#Service bools -USE_IPXE = False -USE_HTTP = False -USE_TFTP = True -MODE_DEBUG = False -USE_DHCP = False -DHCP_MODE_PROXY = False - if __name__ == '__main__': try: #warn the user that they are starting PyPXE as non-root user From 9f177fa23f61cfbba38890d596ad1dfd0c1ae2d5 Mon Sep 17 00:00:00 2001 From: PsychoMario Date: Sun, 22 Feb 2015 15:02:35 +0000 Subject: [PATCH 14/59] added readme info --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 7464fa0..4a2bfaf 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,9 @@ The following are arguments that can be passed to `pypxe-server.py` when running * Description: Disable built-in TFTP server which is enabled by default * Default: `False` * __`--debug`__ - * Description: Enable selected services in DEBUG mode + * Description: Enable selected services in DEBUG mode. Services are + selected by passing the name in a comma seperated list. Options are http, + tftp and dhcp * _This adds a level of verbosity so that you can see what's happening in the background. Debug statements are prefixed with `[DEBUG]` and indented to distinguish between normal output that the services give._ * Default: `False` * __DHCP Service Arguments__ _each of the following can be set one of two ways, you can use either/or_ From ab27bc472e4218ed32e529a2cc536cddb7113ca9 Mon Sep 17 00:00:00 2001 From: Pietro Bertera Date: Wed, 15 Apr 2015 18:40:34 +0200 Subject: [PATCH 15/59] Logging using python loggin module, syslog support --- pypxe-server.py | 35 ++++++++++++++++------- pypxe/dhcp.py | 74 ++++++++++++++++++++++--------------------------- pypxe/http.py | 33 ++++++++++------------ pypxe/tftp.py | 32 ++++++++++----------- 4 files changed, 86 insertions(+), 88 deletions(-) diff --git a/pypxe-server.py b/pypxe-server.py index 70b6f53..9513cd4 100644 --- a/pypxe-server.py +++ b/pypxe-server.py @@ -1,6 +1,7 @@ import threading import os import sys +import logging try: import argparse @@ -45,7 +46,8 @@ parser.add_argument('--http', action = 'store_true', dest = 'USE_HTTP', help = 'Enable built-in HTTP server', default = False) parser.add_argument('--no-tftp', action = 'store_false', dest = 'USE_TFTP', help = 'Disable built-in TFTP server, by default it is enabled', default = True) parser.add_argument('--debug', action = 'store_true', dest = 'MODE_DEBUG', help = 'Adds verbosity to the selected services while they run', default = False) - + parser.add_argument('--syslog', action = 'store', dest = 'SYSLOG_SERVER', help = 'Syslog server', default = None) + #argument group for DHCP server exclusive = parser.add_mutually_exclusive_group(required = False) exclusive.add_argument('--dhcp', action = 'store_true', dest = 'USE_DHCP', help = 'Enable built-in DHCP server', default = False) @@ -67,9 +69,22 @@ #parse the arguments given args = parser.parse_args() + # setup logger + logger = logging.getLogger(sys.argv[0]) + if args.SYSLOG_SERVER: + handler = logging.handlers.SysLogHandler(address = SYSLOG_SERVER) + else: + handler = logging.StreamHandler() + formatter = logging.Formatter('%(asctime)s %(levelname)s %(message)s') + handler.setFormatter(formatter) + logger.addHandler(handler) + + if args.MODE_DEBUG: + logger.setLevel(logging.DEBUG) + #pass warning to user regarding starting HTTP server without iPXE if args.USE_HTTP and not args.USE_IPXE and not args.USE_DHCP: - print '\nWARNING: HTTP selected but iPXE disabled. PXE ROM must support HTTP requests.\n' + logger.warning('HTTP selected but iPXE disabled. PXE ROM must support HTTP requests.') #if the argument was pased to enabled ProxyDHCP then enable the DHCP server if args.DHCP_MODE_PROXY: @@ -92,8 +107,8 @@ #configure/start TFTP server if args.USE_TFTP: - print 'Starting TFTP server...' - tftpServer = tftp.TFTPD(mode_debug = args.MODE_DEBUG) + logger.info('Starting TFTP server...') + tftpServer = tftp.TFTPD(logger = logger) tftpd = threading.Thread(target = tftpServer.listen) tftpd.daemon = True tftpd.start() @@ -102,9 +117,9 @@ #configure/start DHCP server if args.USE_DHCP: if args.DHCP_MODE_PROXY: - print 'Starting DHCP server in ProxyDHCP mode...' + logger.info('Starting DHCP server in ProxyDHCP mode...') else: - print 'Starting DHCP server...' + logger.info('Starting DHCP server...') dhcpServer = dhcp.DHCPD( ip = args.DHCP_SERVER_IP, port = args.DHCP_SERVER_PORT, @@ -119,7 +134,7 @@ useipxe = args.USE_IPXE, usehttp = args.USE_HTTP, mode_proxy = args.DHCP_MODE_PROXY, - mode_debug = args.MODE_DEBUG) + logger = logger) dhcpd = threading.Thread(target = dhcpServer.listen) dhcpd.daemon = True dhcpd.start() @@ -128,14 +143,14 @@ #configure/start HTTP server if args.USE_HTTP: - print 'Starting HTTP server...' - httpServer = http.HTTPD(mode_debug = args.MODE_DEBUG) + logger.info('Starting HTTP server...') + httpServer = http.HTTPD(logger = logger) httpd = threading.Thread(target = httpServer.listen) httpd.daemon = True httpd.start() runningServices.append(httpd) - print 'PyPXE successfully initialized and running!' + logger.info('PyPXE successfully initialized and running!') while map(lambda x: x.isAlive(), runningServices): sleep(1) diff --git a/pypxe/dhcp.py b/pypxe/dhcp.py index 29de407..8dd5854 100644 --- a/pypxe/dhcp.py +++ b/pypxe/dhcp.py @@ -32,30 +32,29 @@ def __init__(self, **serverSettings): self.ipxe = serverSettings.get('useipxe', False) self.http = serverSettings.get('usehttp', False) self.mode_proxy = serverSettings.get('mode_proxy', False) #ProxyDHCP mode - self.mode_debug = serverSettings.get('mode_debug', False) #debug mode + self.logger = serverSettings.get('logger') self.magic = struct.pack('!I', 0x63825363) #magic cookie if self.http and not self.ipxe: - print '\nWARNING: HTTP selected but iPXE disabled. PXE ROM must support HTTP requests.\n' + self.logger.warning('HTTP selected but iPXE disabled. PXE ROM must support HTTP requests.') if self.ipxe and self.http: self.filename = 'http://%s/%s' % (self.fileserver, self.filename) if self.ipxe and not self.http: self.filename = 'tftp://%s/%s' % (self.fileserver, self.filename) - if self.mode_debug: - print 'NOTICE: DHCP server started in debug mode. DHCP server is using the following:' - print '\tDHCP Server IP: ' + self.ip - print '\tDHCP Server Port: ' + str (self.port) - print '\tDHCP Lease Range: ' + self.offerfrom + ' - ' + self.offerto - print '\tDHCP Subnet Mask: ' + self.subnetmask - print '\tDHCP Router: ' + self.router - print '\tDHCP DNS Server: ' + self.dnsserver - print '\tDHCP Broadcast Address: ' + self.broadcast - print '\tDHCP File Server IP: ' + self.fileserver - print '\tDHCP File Name: ' + self.filename - print '\tProxyDHCP Mode: ' + str(self.mode_proxy) - print '\tUsing iPXE: ' + str(self.ipxe) - print '\tUsing HTTP Server: ' + str(self.http) + self.logger.debug('NOTICE: DHCP server started in debug mode. DHCP server is using the following:') + self.logger.debug('\tDHCP Server IP: ' + self.ip) + self.logger.debug('\tDHCP Server Port: ' + str (self.port)) + self.logger.debug('\tDHCP Lease Range: ' + self.offerfrom + ' - ' + self.offerto) + self.logger.debug('\tDHCP Subnet Mask: ' + self.subnetmask) + self.logger.debug('\tDHCP Router: ' + self.router) + self.logger.debug('\tDHCP DNS Server: ' + self.dnsserver) + self.logger.debug('\tDHCP Broadcast Address: ' + self.broadcast) + self.logger.debug('\tDHCP File Server IP: ' + self.fileserver) + self.logger.debug('\tDHCP File Name: ' + self.filename) + self.logger.debug('\tProxyDHCP Mode: ' + str(self.mode_proxy)) + self.logger.debug('\tUsing iPXE: ' + str(self.ipxe)) + self.logger.debug('\tUsing HTTP Server: ' + str(self.http)) self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) @@ -147,11 +146,11 @@ def craftHeader(self, message): offer = self.nextIP() self.leases[clientmac]['ip'] = offer self.leases[clientmac]['expire'] = time() + 86400 - if self.mode_debug: - print '[DEBUG] New DHCP Assignment - MAC: ' + self.printMAC(clientmac) + ' -> IP: ' + self.leases[clientmac]['ip'] + self.logger.debug('New DHCP Assignment - MAC: ' + self.printMAC(clientmac) + ' -> IP: ' + self.leases[clientmac]['ip']) response += socket.inet_aton(offer) #yiaddr else: response += socket.inet_aton('0.0.0.0') + response += socket.inet_aton(self.fileserver) #siaddr response += socket.inet_aton('0.0.0.0') #giaddr response += chaddr #chaddr @@ -201,11 +200,10 @@ def dhcpOffer(self, message): clientmac, headerResponse = self.craftHeader(message) optionsResponse = self.craftOptions(2, clientmac) #DHCPOFFER response = headerResponse + optionsResponse - if self.mode_debug: - print '[DEBUG] DHCPOFFER - Sending the following' - print '\t<--BEGIN HEADER-->\n\t' + repr(headerResponse) + '\n\t<--END HEADER-->' - print '\t<--BEGIN OPTIONS-->\n\t' + repr(optionsResponse) + '\n\t<--END OPTIONS-->' - print '\t<--BEGIN RESPONSE-->\n\t' + repr(response) + '\n\t<--END RESPONSE-->' + self.logger.debug('DHCPOFFER - Sending the following') + self.logger.debug('\t<--BEGIN HEADER-->\n\t' + repr(headerResponse) + '\n\t<--END HEADER-->') + self.logger.debug('\t<--BEGIN OPTIONS-->\n\t' + repr(optionsResponse) + '\n\t<--END OPTIONS-->') + self.logger.debug('\t<--BEGIN RESPONSE-->\n\t' + repr(response) + '\n\t<--END RESPONSE-->') self.sock.sendto(response, (self.broadcast, 68)) def dhcpAck(self, message): @@ -213,11 +211,10 @@ def dhcpAck(self, message): clientmac, headerResponse = self.craftHeader(message) optionsResponse = self.craftOptions(5, clientmac) #DHCPACK response = headerResponse + optionsResponse - if self.mode_debug: - print '[DEBUG] DHCPACK - Sending the following' - print '\t<--BEGIN HEADER-->\n\t' + repr(headerResponse) + '\n\t<--END HEADER-->' - print '\t<--BEGIN OPTIONS-->\n\t' + repr(optionsResponse) + '\n\t<--END OPTIONS-->' - print '\t<--BEGIN RESPONSE-->\n\t' + repr(response) + '\n\t<--END RESPONSE-->' + self.logger.debug('DHCPACK - Sending the following') + self.logger.debug('\t<--BEGIN HEADER-->\n\t' + repr(headerResponse) + '\n\t<--END HEADER-->') + self.logger.debug('\t<--BEGIN OPTIONS-->\n\t' + repr(optionsResponse) + '\n\t<--END OPTIONS-->') + self.logger.debug('\t<--BEGIN RESPONSE-->\n\t' + repr(response) + '\n\t<--END RESPONSE-->') self.sock.sendto(response, (self.broadcast, 68)) def listen(self): @@ -225,24 +222,19 @@ def listen(self): while True: message, address = self.sock.recvfrom(1024) clientmac = struct.unpack('!28x6s', message[:34]) - if self.mode_debug: - print '[DEBUG] Received message' - print '\t<--BEGIN MESSAGE-->\n\t' + repr(message) + '\n\t<--END MESSAGE-->' + self.logger.debug('Received message') + self.logger.debug('\t<--BEGIN MESSAGE-->\n\t' + repr(message) + '\n\t<--END MESSAGE-->') options = self.tlvParse(message[240:]) - if self.mode_debug: - print '[DEBUG] Parsed received options' - print '\t<--BEGIN OPTIONS-->\n\t' + repr(options) + '\n\t<--END OPTIONS-->' - if not (60 in options and 'PXEClient' in options[60][0]) : continue + self.logger.debug('Parsed received options') + self.logger.debug('\t<--BEGIN OPTIONS-->\n\t' + repr(options) + '\n\t<--END OPTIONS-->') + #if not (60 in options and 'PXEClient' in options[60][0]) : continue type = ord(options[53][0]) #see RFC2131 page 10 if type == 1: - if self.mode_debug: - print '[DEBUG] Received DHCPOFFER' + self.logger.debug('Received DHCPOFFER') self.dhcpOffer(message) elif type == 3 and address[0] == '0.0.0.0' and not self.mode_proxy: - if self.mode_debug: - print '[DEBUG] Received DHCPACK' + self.logger.debug('Received DHCPACK') self.dhcpAck(message) elif type == 3 and address[0] != '0.0.0.0' and self.mode_proxy: - if self.mode_debug: - print '[DEBUG] Received DHCPACK' + self.logger.debug('Received DHCPACK') self.dhcpAck(message) diff --git a/pypxe/http.py b/pypxe/http.py index 5702832..8c6623f 100644 --- a/pypxe/http.py +++ b/pypxe/http.py @@ -18,7 +18,7 @@ def __init__(self, **serverSettings): self.ip = serverSettings.get('ip', '0.0.0.0') self.port = serverSettings.get('port', 80) self.netbootDirectory = serverSettings.get('netbootDirectory', '.') - self.mode_debug = serverSettings.get('mode_debug', False) #debug mode + self.logger = serverSettings.get('logger') self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) self.sock.bind((self.ip, self.port)) @@ -29,18 +29,16 @@ def __init__(self, **serverSettings): os.chdir (self.netbootDirectory) os.chroot ('.') - if self.mode_debug: - print 'NOTICE: HTTP server started in debug mode. HTTP server is using the following:' - print '\tHTTP Server IP: ' + self.ip - print '\tHTTP Server Port: ' + str(self.port) - print '\tHTTP Network Boot Directory: ' + self.netbootDirectory + self.logger.debug('NOTICE: HTTP server started in debug mode. HTTP server is using the following:') + self.logger.debug('\tHTTP Server IP: ' + self.ip) + self.logger.debug('\tHTTP Server Port: ' + str(self.port)) + self.logger.debug('\tHTTP Network Boot Directory: ' + self.netbootDirectory) def handleRequest(self, connection, addr): '''This method handles HTTP request''' request = connection.recv(1024) - if self.mode_debug: - print '[DEBUG] HTTP Recieved message from ' + repr(addr) - print '\t<--BEGIN MESSAGE-->\n\t' + repr(request) + '\n\t<--END MESSAGE-->' + self.logger.debug('HTTP Recieved message from ' + repr(addr)) + self.logger.debug('\n\t<--BEGIN MESSAGE-->\n\t' + repr(request) + '\n\t<--END MESSAGE-->') startline = request.split('\r\n')[0].split(' ') method = startline[0] target = startline[1] @@ -54,28 +52,25 @@ def handleRequest(self, connection, addr): if status[:3] in ('404', '501'): #fail out connection.send(response) connection.close() - if self.mode_debug: - print '[DEBUG] HTTP Sending message to ' + repr(addr) - print '\t<--BEING MESSAGE-->\n\t' + repr(response) + '\n\t<--END MESSAGE-->' + self.logger.debug('HTTP Sending message to ' + repr(addr)) + self.logger.debug('\n\t<--BEING MESSAGE-->\n\t' + repr(response) + '\n\t<--END MESSAGE-->') return response += 'Content-Length: %d\r\n' % os.path.getsize(target) response += '\r\n' if method == 'HEAD': connection.send(response) connection.close() - if self.mode_debug: - print '[DEBUG] HTTP Sending message to ' + repr(addr) - print '\t<--BEING MESSAGE-->\n\t' + repr(response) + '\n\t<--END MESSAGE-->' + self.logger.debug('HTTP Sending message to ' + repr(addr)) + self.logger.debug('\n\t<--BEING MESSAGE-->\n\t' + repr(response) + '\n\t<--END MESSAGE-->') return handle = open(target) response += handle.read() handle.close() connection.send(response) connection.close() - if self.mode_debug: - print '[DEBUG] HTTP Sending message to ' + repr(addr) - print '\t<--BEING MESSAGE-->\n\t' + repr(response) + '\n\t<--END MESSAGE-->' - print '\tHTTP File Sent - http://%s -> %s:%d' % (target, addr[0], addr[1]) + self.logger.debug('HTTP Sending message to ' + repr(addr)) + self.logger.debug('\n\t<--BEING MESSAGE-->\n\t' + repr(response) + '\n\t<--END MESSAGE-->') + self.logger.debug('\tHTTP File Sent - http://%s -> %s:%d' % (target, addr[0], addr[1])) def listen(self): '''This method is the main loop that listens for requests''' diff --git a/pypxe/tftp.py b/pypxe/tftp.py index 7f0bba5..5e8dd6d 100644 --- a/pypxe/tftp.py +++ b/pypxe/tftp.py @@ -16,18 +16,17 @@ class TFTPD: ''' def __init__(self, **serverSettings): self.ip = serverSettings.get('ip', '0.0.0.0') - self.port = serverSettings.get('port', 69) + self.port = serverSettings.get('port', 79) self.netbootDirectory = serverSettings.get('netbootDirectory', '.') - self.mode_debug = serverSettings.get('mode_debug', False) #debug mode + self.logger = serverSettings.get('logger') self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) self.sock.bind((self.ip, self.port)) - if self.mode_debug: - print 'NOTICE: TFTP server started in debug mode. TFTP server is using the following:' - print '\tTFTP Server IP: ' + self.ip - print '\tTFTP Server Port: ' + str(self.port) - print '\tTFTP Network Boot Directory: ' + self.netbootDirectory + self.logger.debug('NOTICE: TFTP server started in debug mode. TFTP server is using the following:') + self.logger.debug('\tTFTP Server IP: ' + self.ip) + self.logger.debug('\tTFTP Server Port: ' + str(self.port)) + self.logger.debug('\tTFTP Network Boot Directory: ' + self.netbootDirectory) #key is (address, port) pair self.ongoing = defaultdict(lambda: {'filename': '', 'handle': None, 'block': 1, 'blksize': 512}) @@ -55,8 +54,7 @@ def notFound(self, address): response = struct.pack('!H', 5) #error code response += struct.pack('!H', 1) #file not found response += 'File Not Found' - if self.mode_debug: - print "[DEBUG] TFTP Sending 'File Not Found'" + self.logger.debug("TFTP Sending 'File Not Found'") self.sock.sendto(response, address) def sendBlock(self, address): @@ -71,12 +69,10 @@ def sendBlock(self, address): self.sock.sendto(response, address) if len(data) != descriptor['blksize']: descriptor['handle'].close() - if self.mode_debug: - print '[DEBUG] TFTP File Sent - tftp://%s -> %s:%d' % (descriptor['filename'], address[0], address[1]) + self.logger.debug('TFTP File Sent - tftp://%s -> %s:%d' % (descriptor['filename'], address[0], address[1])) self.ongoing.pop(address) else: - if self.mode_debug: - print '[DEBUG] TFTP Sending block ' + repr(descriptor['block']) + self.logger.debug('TFTP Sending block ' + repr(descriptor['block'])) descriptor['block'] += 1 def read(self, address, message): @@ -86,7 +82,8 @@ def read(self, address, message): file does not exist -> reply with error ''' filename = self.filename(message) - if not os.path.lexists(filename): + self.logger.debug('Filename: %s' % filename) + if not os.path.isfile(filename): self.notFound(address) return self.ongoing[address]['filename'] = filename @@ -101,8 +98,8 @@ def read(self, address, message): self.ongoing[address]['blksize'] = int(options['blksize']) filesize = os.path.getsize(self.ongoing[address]['filename']) if filesize > (2**16 * self.ongoing[address]['blksize']): - print '\nWARNING: TFTP request too big, attempting transfer anyway.\n' - print '\tDetails: Filesize %s is too big for blksize %s.\n' % (filesize, self.ongoing[address]['blksize']) + self.logger.warning('TFTP request too big, attempting transfer anyway.\n') + self.logger.warning('Details: Filesize %s is too big for blksize %s.\n' % (filesize, self.ongoing[address]['blksize'])) if 'tsize' in options: response += 'tsize' + chr(0) response += str(filesize) @@ -118,8 +115,7 @@ def listen(self): message, address = self.sock.recvfrom(1024) opcode = struct.unpack('!H', message[:2])[0] if opcode == 1: #read the request - if self.mode_debug: - print '[DEBUG] TFTP receiving request' + self.logger.debug('TFTP receiving request') self.read(address, message) if opcode == 4: if self.ongoing.has_key(address): From 705f5435489b2408c8f01b6ea4e4b739b1faae58 Mon Sep 17 00:00:00 2001 From: Pietro Bertera Date: Wed, 15 Apr 2015 18:51:16 +0200 Subject: [PATCH 16/59] Syslog support --- pypxe-server.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pypxe-server.py b/pypxe-server.py index 9513cd4..815e677 100644 --- a/pypxe-server.py +++ b/pypxe-server.py @@ -2,6 +2,7 @@ import os import sys import logging +import logging.handlers try: import argparse @@ -47,6 +48,7 @@ parser.add_argument('--no-tftp', action = 'store_false', dest = 'USE_TFTP', help = 'Disable built-in TFTP server, by default it is enabled', default = True) parser.add_argument('--debug', action = 'store_true', dest = 'MODE_DEBUG', help = 'Adds verbosity to the selected services while they run', default = False) parser.add_argument('--syslog', action = 'store', dest = 'SYSLOG_SERVER', help = 'Syslog server', default = None) + parser.add_argument('--syslog-port', action = 'store', dest = 'SYSLOG_PORT', help = 'Syslog server port', default = 514) #argument group for DHCP server exclusive = parser.add_mutually_exclusive_group(required = False) @@ -72,7 +74,7 @@ # setup logger logger = logging.getLogger(sys.argv[0]) if args.SYSLOG_SERVER: - handler = logging.handlers.SysLogHandler(address = SYSLOG_SERVER) + handler = logging.handlers.SysLogHandler(address = (args.SYSLOG_SERVER, int(args.SYSLOG_PORT))) else: handler = logging.StreamHandler() formatter = logging.Formatter('%(asctime)s %(levelname)s %(message)s') From df048d9cd46f6280ccc207724a35f935a5c401ad Mon Sep 17 00:00:00 2001 From: Pietro Bertera Date: Wed, 15 Apr 2015 19:09:34 +0200 Subject: [PATCH 17/59] Made it backward compatibile with no logging code --- README.md | 6 ++++++ pypxe-server.py | 5 +++-- pypxe/dhcp.py | 16 +++++++++++++++- pypxe/http.py | 17 ++++++++++++++++- pypxe/tftp.py | 19 +++++++++++++++++-- 5 files changed, 57 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 7464fa0..d703542 100644 --- a/README.md +++ b/README.md @@ -58,6 +58,12 @@ The following are arguments that can be passed to `pypxe-server.py` when running * Description: Enable selected services in DEBUG mode * _This adds a level of verbosity so that you can see what's happening in the background. Debug statements are prefixed with `[DEBUG]` and indented to distinguish between normal output that the services give._ * Default: `False` + * __`--syslog`__ + * Description: Syslog server + * Default: `None` + * __`--syslog-port`__ + * Description: Syslog server port + * Default: `514` * __DHCP Service Arguments__ _each of the following can be set one of two ways, you can use either/or_ * __`-s DHCP_SERVER_IP`__ or __`--dhcp-server-ip DHCP_SERVER_IP`__ * Description: Specify DHCP server IP address diff --git a/pypxe-server.py b/pypxe-server.py index 815e677..9b90e58 100644 --- a/pypxe-server.py +++ b/pypxe-server.py @@ -110,7 +110,7 @@ #configure/start TFTP server if args.USE_TFTP: logger.info('Starting TFTP server...') - tftpServer = tftp.TFTPD(logger = logger) + tftpServer = tftp.TFTPD(mode_debug = args.MODE_DEBUG, logger = logger) tftpd = threading.Thread(target = tftpServer.listen) tftpd.daemon = True tftpd.start() @@ -136,6 +136,7 @@ useipxe = args.USE_IPXE, usehttp = args.USE_HTTP, mode_proxy = args.DHCP_MODE_PROXY, + mode_debug = args.MODE_DEBUG, logger = logger) dhcpd = threading.Thread(target = dhcpServer.listen) dhcpd.daemon = True @@ -146,7 +147,7 @@ #configure/start HTTP server if args.USE_HTTP: logger.info('Starting HTTP server...') - httpServer = http.HTTPD(logger = logger) + httpServer = http.HTTPD(mode_debug = args.MODE_DEBUG, logger = logger) httpd = threading.Thread(target = httpServer.listen) httpd.daemon = True httpd.start() diff --git a/pypxe/dhcp.py b/pypxe/dhcp.py index 8dd5854..6ae76e4 100644 --- a/pypxe/dhcp.py +++ b/pypxe/dhcp.py @@ -32,9 +32,23 @@ def __init__(self, **serverSettings): self.ipxe = serverSettings.get('useipxe', False) self.http = serverSettings.get('usehttp', False) self.mode_proxy = serverSettings.get('mode_proxy', False) #ProxyDHCP mode - self.logger = serverSettings.get('logger') + self.logger = serverSettings.get('logger', None) + self.mode_debug = serverSettings.get('mode_debug', False) self.magic = struct.pack('!I', 0x63825363) #magic cookie + if self.logger == None: + import logging + import logging.handlers + # setup logger + self.logger = logging.getLogger("dhcp") + handler = logging.StreamHandler() + formatter = logging.Formatter('%(asctime)s %(levelname)s %(message)s') + handler.setFormatter(formatter) + self.logger.addHandler(handler) + + if self.mode_debug: + self.logger.setLevel(logging.DEBUG) + if self.http and not self.ipxe: self.logger.warning('HTTP selected but iPXE disabled. PXE ROM must support HTTP requests.') if self.ipxe and self.http: diff --git a/pypxe/http.py b/pypxe/http.py index 8c6623f..296a035 100644 --- a/pypxe/http.py +++ b/pypxe/http.py @@ -18,7 +18,8 @@ def __init__(self, **serverSettings): self.ip = serverSettings.get('ip', '0.0.0.0') self.port = serverSettings.get('port', 80) self.netbootDirectory = serverSettings.get('netbootDirectory', '.') - self.logger = serverSettings.get('logger') + self.logger = serverSettings.get('logger', None) + self.mode_debug = serverSettings.get('mode_debug', False) self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) self.sock.bind((self.ip, self.port)) @@ -29,6 +30,20 @@ def __init__(self, **serverSettings): os.chdir (self.netbootDirectory) os.chroot ('.') + if self.logger == None: + import logging + import logging.handlers + # setup logger + self.logger = logging.getLogger("http") + handler = logging.StreamHandler() + formatter = logging.Formatter('%(asctime)s %(levelname)s %(message)s') + handler.setFormatter(formatter) + self.logger.addHandler(handler) + + if self.mode_debug: + self.logger.setLevel(logging.DEBUG) + + self.logger.debug('NOTICE: HTTP server started in debug mode. HTTP server is using the following:') self.logger.debug('\tHTTP Server IP: ' + self.ip) self.logger.debug('\tHTTP Server Port: ' + str(self.port)) diff --git a/pypxe/tftp.py b/pypxe/tftp.py index 5e8dd6d..41e3671 100644 --- a/pypxe/tftp.py +++ b/pypxe/tftp.py @@ -16,13 +16,27 @@ class TFTPD: ''' def __init__(self, **serverSettings): self.ip = serverSettings.get('ip', '0.0.0.0') - self.port = serverSettings.get('port', 79) + self.port = serverSettings.get('port', 69) self.netbootDirectory = serverSettings.get('netbootDirectory', '.') - self.logger = serverSettings.get('logger') + self.logger = serverSettings.get('logger', None) + self.mode_debug = serverSettings.get('mode_debug', False) self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) self.sock.bind((self.ip, self.port)) + if self.logger == None: + import logging + import logging.handlers + # setup logger + self.logger = logging.getLogger("tftp") + handler = logging.StreamHandler() + formatter = logging.Formatter('%(asctime)s %(levelname)s %(message)s') + handler.setFormatter(formatter) + self.logger.addHandler(handler) + + if self.mode_debug: + self.logger.setLevel(logging.DEBUG) + self.logger.debug('NOTICE: TFTP server started in debug mode. TFTP server is using the following:') self.logger.debug('\tTFTP Server IP: ' + self.ip) self.logger.debug('\tTFTP Server Port: ' + str(self.port)) @@ -36,6 +50,7 @@ def __init__(self, **serverSettings): os.chdir (self.netbootDirectory) os.chroot ('.') + def filename(self, message): ''' The first null-delimited field after the OPCODE From a55eba33d8c04f8436e2c17e4536a81795718e73 Mon Sep 17 00:00:00 2001 From: Pietro Bertera Date: Wed, 15 Apr 2015 19:21:09 +0200 Subject: [PATCH 18/59] added tftp documentation --- DOCUMENTATION.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/DOCUMENTATION.md b/DOCUMENTATION.md index 4d67579..0a632a8 100644 --- a/DOCUMENTATION.md +++ b/DOCUMENTATION.md @@ -78,7 +78,9 @@ The TFTP server class, __`TFTPD()`__, is constructed with the following __keywor * Description: This indicates whether or not the TFTP server should be started in debug mode or not. * Default: `False` * Type: bool - +* __`logger`__ + * Description: A [Logger](https://docs.python.org/2/library/logging.html#logger-objects) object used for logging messages, if `None` a local [StreamHandler](https://docs.python.org/2/library/logging.handlers.html#streamhandler) instance will be created + * Default: `None` ##DHCP Server `pypxe.dhcp` ###Importing From 76b63f2caf79110f0674448ccc516d76f0e676dc Mon Sep 17 00:00:00 2001 From: Pietro Bertera Date: Wed, 15 Apr 2015 19:23:45 +0200 Subject: [PATCH 19/59] typp fix --- DOCUMENTATION.md | 1 + 1 file changed, 1 insertion(+) diff --git a/DOCUMENTATION.md b/DOCUMENTATION.md index 0a632a8..7771ffb 100644 --- a/DOCUMENTATION.md +++ b/DOCUMENTATION.md @@ -81,6 +81,7 @@ The TFTP server class, __`TFTPD()`__, is constructed with the following __keywor * __`logger`__ * Description: A [Logger](https://docs.python.org/2/library/logging.html#logger-objects) object used for logging messages, if `None` a local [StreamHandler](https://docs.python.org/2/library/logging.handlers.html#streamhandler) instance will be created * Default: `None` + ##DHCP Server `pypxe.dhcp` ###Importing From 56aa43d70430e13a3e80565098968c1c64f1b119 Mon Sep 17 00:00:00 2001 From: Pietro Bertera Date: Wed, 15 Apr 2015 19:24:39 +0200 Subject: [PATCH 20/59] Added documentation for logger object --- DOCUMENTATION.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/DOCUMENTATION.md b/DOCUMENTATION.md index 7771ffb..c4df7d7 100644 --- a/DOCUMENTATION.md +++ b/DOCUMENTATION.md @@ -151,6 +151,9 @@ The DHCP server class, __`DHCPD()`__, is constructed with the following __keywor * Description: This indicates whether or not the DHCP server should be started in debug mode or not. * Default: `False` * Type: _bool_ +* __`logger`__ + * Description: A [Logger](https://docs.python.org/2/library/logging.html#logger-objects) object used for logging messages, if `None` a local [StreamHandler](https://docs.python.org/2/library/logging.handlers.html#streamhandler) instance will be created + * Default: `None` ##HTTP Server `pypxe.http` @@ -181,6 +184,9 @@ The HTTP server class, __`HTTPD()`__, is constructed with the following __keywor * Description: This indicates whether or not the HTTP server should be started in debug mode or not. * Default: `False` * Type: bool +* __`logger`__ + * Description: A [Logger](https://docs.python.org/2/library/logging.html#logger-objects) object used for logging messages, if `None` a local [StreamHandler](https://docs.python.org/2/library/logging.handlers.html#streamhandler) instance will be created + * Default: `None` ##Additional Information * The function `chr(0)` is used in multiple places throughout the servers. This denotes a `NULL` byte, or `\x00` From 82934f405f1e6e08fd1519a2242ae34cb559f895 Mon Sep 17 00:00:00 2001 From: Pietro Bertera Date: Thu, 16 Apr 2015 12:47:23 +0200 Subject: [PATCH 21/59] Revert "Merge branch 'master' into development" This reverts commit d05c034b989eb46fa77b2c0371335ab0d0bf206c, reversing changes made to fe3711aebe2a39eec6aef7685fd1c59702e7bb16. --- DOCUMENTATION.md | 9 ----- README.md | 6 ---- pypxe-server.py | 39 ++++++-------------- pypxe/dhcp.py | 92 +++++++++++++++++++++--------------------------- pypxe/http.py | 47 ++++++++++--------------- pypxe/tftp.py | 49 ++++++++++---------------- 6 files changed, 88 insertions(+), 154 deletions(-) diff --git a/DOCUMENTATION.md b/DOCUMENTATION.md index c4df7d7..4d67579 100644 --- a/DOCUMENTATION.md +++ b/DOCUMENTATION.md @@ -78,9 +78,6 @@ The TFTP server class, __`TFTPD()`__, is constructed with the following __keywor * Description: This indicates whether or not the TFTP server should be started in debug mode or not. * Default: `False` * Type: bool -* __`logger`__ - * Description: A [Logger](https://docs.python.org/2/library/logging.html#logger-objects) object used for logging messages, if `None` a local [StreamHandler](https://docs.python.org/2/library/logging.handlers.html#streamhandler) instance will be created - * Default: `None` ##DHCP Server `pypxe.dhcp` @@ -151,9 +148,6 @@ The DHCP server class, __`DHCPD()`__, is constructed with the following __keywor * Description: This indicates whether or not the DHCP server should be started in debug mode or not. * Default: `False` * Type: _bool_ -* __`logger`__ - * Description: A [Logger](https://docs.python.org/2/library/logging.html#logger-objects) object used for logging messages, if `None` a local [StreamHandler](https://docs.python.org/2/library/logging.handlers.html#streamhandler) instance will be created - * Default: `None` ##HTTP Server `pypxe.http` @@ -184,9 +178,6 @@ The HTTP server class, __`HTTPD()`__, is constructed with the following __keywor * Description: This indicates whether or not the HTTP server should be started in debug mode or not. * Default: `False` * Type: bool -* __`logger`__ - * Description: A [Logger](https://docs.python.org/2/library/logging.html#logger-objects) object used for logging messages, if `None` a local [StreamHandler](https://docs.python.org/2/library/logging.handlers.html#streamhandler) instance will be created - * Default: `None` ##Additional Information * The function `chr(0)` is used in multiple places throughout the servers. This denotes a `NULL` byte, or `\x00` diff --git a/README.md b/README.md index bdee69b..78cf81f 100644 --- a/README.md +++ b/README.md @@ -64,12 +64,6 @@ The following are arguments that can be passed to `pypxe-server.py` when running * Description: Amend configuration from json file * _Use the specified json file to amend the command line options. See example.json for more information._ * Default: `None` - * __`--syslog`__ - * Description: Syslog server - * Default: `None` - * __`--syslog-port`__ - * Description: Syslog server port - * Default: `514` * __DHCP Service Arguments__ _each of the following can be set one of two ways, you can use either/or_ * __`-s DHCP_SERVER_IP`__ or __`--dhcp-server-ip DHCP_SERVER_IP`__ * Description: Specify DHCP server IP address diff --git a/pypxe-server.py b/pypxe-server.py index 57ff230..990cd18 100644 --- a/pypxe-server.py +++ b/pypxe-server.py @@ -2,8 +2,6 @@ import os import sys import json -import logging -import logging.handlers try: import argparse @@ -52,9 +50,7 @@ parser.add_argument('--no-tftp', action = 'store_false', dest = 'USE_TFTP', help = 'Disable built-in TFTP server, by default it is enabled', default = True) parser.add_argument('--debug', action = 'store', dest = 'MODE_DEBUG', help = 'Comma Seperated (http,tftp,dhcp). Adds verbosity to the selected services while they run', default = '') parser.add_argument('--config', action = 'store', dest = 'JSON_CONFIG', help = 'Configure from a json file rather than the command line', default = JSON_CONFIG) - parser.add_argument('--syslog', action = 'store', dest = 'SYSLOG_SERVER', help = 'Syslog server', default = None) - parser.add_argument('--syslog-port', action = 'store', dest = 'SYSLOG_PORT', help = 'Syslog server port', default = 514) - + #argument group for DHCP server exclusive = parser.add_mutually_exclusive_group(required = False) exclusive.add_argument('--dhcp', action = 'store_true', dest = 'USE_DHCP', help = 'Enable built-in DHCP server', default = False) @@ -89,22 +85,9 @@ dargs.update(loadedcfg) args = argparse.Namespace(**dargs) - # setup logger - logger = logging.getLogger(sys.argv[0]) - if args.SYSLOG_SERVER: - handler = logging.handlers.SysLogHandler(address = (args.SYSLOG_SERVER, int(args.SYSLOG_PORT))) - else: - handler = logging.StreamHandler() - formatter = logging.Formatter('%(asctime)s %(levelname)s %(message)s') - handler.setFormatter(formatter) - logger.addHandler(handler) - - if args.MODE_DEBUG: - logger.setLevel(logging.DEBUG) - #pass warning to user regarding starting HTTP server without iPXE if args.USE_HTTP and not args.USE_IPXE and not args.USE_DHCP: - logger.warning('HTTP selected but iPXE disabled. PXE ROM must support HTTP requests.') + print '\nWARNING: HTTP selected but iPXE disabled. PXE ROM must support HTTP requests.\n' #if the argument was pased to enabled ProxyDHCP then enable the DHCP server if args.DHCP_MODE_PROXY: @@ -127,8 +110,8 @@ #configure/start TFTP server if args.USE_TFTP: - logger.info('Starting TFTP server...') - tftpServer = tftp.TFTPD(mode_debug = ("tftp" in args.MODE_DEBUG.lower() or "all" in args.MODE_DEBUG.lower()), logger = logger) + print 'Starting TFTP server...' + tftpServer = tftp.TFTPD(mode_debug = ("tftp" in args.MODE_DEBUG.lower() or "all" in args.MODE_DEBUG.lower())) tftpd = threading.Thread(target = tftpServer.listen) tftpd.daemon = True tftpd.start() @@ -137,9 +120,9 @@ #configure/start DHCP server if args.USE_DHCP: if args.DHCP_MODE_PROXY: - logger.info('Starting DHCP server in ProxyDHCP mode...') + print 'Starting DHCP server in ProxyDHCP mode...' else: - logger.info('Starting DHCP server...') + print 'Starting DHCP server...' dhcpServer = dhcp.DHCPD( ip = args.DHCP_SERVER_IP, port = args.DHCP_SERVER_PORT, @@ -154,9 +137,7 @@ useipxe = args.USE_IPXE, usehttp = args.USE_HTTP, mode_proxy = args.DHCP_MODE_PROXY, - mode_debug = ("dhcp" in args.MODE_DEBUG.lower() or "all" in args.MODE_DEBUG.lower()), - logger = logger) - + mode_debug = ("dhcp" in args.MODE_DEBUG.lower() or "all" in args.MODE_DEBUG.lower())) dhcpd = threading.Thread(target = dhcpServer.listen) dhcpd.daemon = True dhcpd.start() @@ -165,14 +146,14 @@ #configure/start HTTP server if args.USE_HTTP: - logger.info('Starting HTTP server...') - httpServer = http.HTTPD(mode_debug = ("http" in args.MODE_DEBUG.lower() or "all" in args.MODE_DEBUG.lower()), logger = logger) + print 'Starting HTTP server...' + httpServer = http.HTTPD(mode_debug = ("http" in args.MODE_DEBUG.lower() or "all" in args.MODE_DEBUG.lower())) httpd = threading.Thread(target = httpServer.listen) httpd.daemon = True httpd.start() runningServices.append(httpd) - logger.info('PyPXE successfully initialized and running!') + print 'PyPXE successfully initialized and running!' while map(lambda x: x.isAlive(), runningServices): sleep(1) diff --git a/pypxe/dhcp.py b/pypxe/dhcp.py index 4f3495c..51d0215 100644 --- a/pypxe/dhcp.py +++ b/pypxe/dhcp.py @@ -37,43 +37,30 @@ def __init__(self, **serverSettings): self.ipxe = serverSettings.get('useipxe', False) self.http = serverSettings.get('usehttp', False) self.mode_proxy = serverSettings.get('mode_proxy', False) #ProxyDHCP mode - self.logger = serverSettings.get('logger', None) - self.mode_debug = serverSettings.get('mode_debug', False) + self.mode_debug = serverSettings.get('mode_debug', False) #debug mode self.magic = struct.pack('!I', 0x63825363) #magic cookie - if self.logger == None: - import logging - import logging.handlers - # setup logger - self.logger = logging.getLogger("dhcp") - handler = logging.StreamHandler() - formatter = logging.Formatter('%(asctime)s %(levelname)s %(message)s') - handler.setFormatter(formatter) - self.logger.addHandler(handler) - - if self.mode_debug: - self.logger.setLevel(logging.DEBUG) - if self.http and not self.ipxe: - self.logger.warning('HTTP selected but iPXE disabled. PXE ROM must support HTTP requests.') + print '\nWARNING: HTTP selected but iPXE disabled. PXE ROM must support HTTP requests.\n' if self.ipxe and self.http: self.filename = 'http://{fileserver}/{filename}'.format(fileserver = self.fileserver, filename = self.filename) if self.ipxe and not self.http: self.filename = 'tftp://{fileserver}/{filename}'.format(fileserver = self.fileserver, filename = self.filename) - self.logger.debug('NOTICE: DHCP server started in debug mode. DHCP server is using the following:') - self.logger.debug('\tDHCP Server IP: ' + self.ip) - self.logger.debug('\tDHCP Server Port: ' + str (self.port)) - self.logger.debug('\tDHCP Lease Range: ' + self.offerfrom + ' - ' + self.offerto) - self.logger.debug('\tDHCP Subnet Mask: ' + self.subnetmask) - self.logger.debug('\tDHCP Router: ' + self.router) - self.logger.debug('\tDHCP DNS Server: ' + self.dnsserver) - self.logger.debug('\tDHCP Broadcast Address: ' + self.broadcast) - self.logger.debug('\tDHCP File Server IP: ' + self.fileserver) - self.logger.debug('\tDHCP File Name: ' + self.filename) - self.logger.debug('\tProxyDHCP Mode: ' + str(self.mode_proxy)) - self.logger.debug('\tUsing iPXE: ' + str(self.ipxe)) - self.logger.debug('\tUsing HTTP Server: ' + str(self.http)) + if self.mode_debug: + print 'NOTICE: DHCP server started in debug mode. DHCP server is using the following:' + print '\tDHCP Server IP: {}'.format(self.ip) + print '\tDHCP Server Port: {}'.format(self.port) + print '\tDHCP Lease Range: {} - {}'.format(self.offerfrom, self.offerto) + print '\tDHCP Subnet Mask: {}'.format(self.subnetmask) + print '\tDHCP Router: {}'.format(self.router) + print '\tDHCP DNS Server: {}'.format(self.dnsserver) + print '\tDHCP Broadcast Address: {}'.format(self.broadcast) + print '\tDHCP File Server IP: {}'.format(self.fileserver) + print '\tDHCP File Name: {}'.format(self.filename) + print '\tProxyDHCP Mode: {}'.format(self.mode_proxy) + print '\tUsing iPXE: {}'.format(self.ipxe) + print '\tUsing HTTP Server: {}'.format(self.http) self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) @@ -165,12 +152,11 @@ def craftHeader(self, message): offer = self.nextIP() self.leases[clientmac]['ip'] = offer self.leases[clientmac]['expire'] = time() + 86400 - self.logger.debug('New DHCP Assignment - MAC: {MAC} -> IP: {IP}'.format(MAC = self.printMAC(clientmac), IP = self.leases[clientmac]['ip'])) - + if self.mode_debug: + print '[DEBUG] New DHCP Assignment - MAC: {MAC} -> IP: {IP}'.format(MAC = self.printMAC(clientmac), IP = self.leases[clientmac]['ip']) response += socket.inet_aton(offer) #yiaddr else: response += socket.inet_aton('0.0.0.0') - response += socket.inet_aton(self.fileserver) #siaddr response += socket.inet_aton('0.0.0.0') #giaddr response += chaddr #chaddr @@ -232,11 +218,11 @@ def dhcpOffer(self, message): clientmac, headerResponse = self.craftHeader(message) optionsResponse = self.craftOptions(2, clientmac) #DHCPOFFER response = headerResponse + optionsResponse - - self.logger.debug('DHCPOFFER - Sending the following') - self.logger.debug('\t<--BEGIN HEADER-->\n\t{headerResponse}\n\t<--END HEADER-->'.format(headerResponse = repr(headerResponse))) - self.logger.debug('\t<--BEGIN OPTIONS-->\n\t{optionsResponse}\n\t<--END OPTIONS-->'.format(optionsResponse = repr(optionsResponse))) - self.logger.debug('\t<--BEGIN RESPONSE-->\n\t{response}\n\t<--END RESPONSE-->'.format(response = repr(response))) + if self.mode_debug: + print '[DEBUG] DHCPOFFER - Sending the following' + print '\t<--BEGIN HEADER-->\n\t{headerResponse}\n\t<--END HEADER-->'.format(headerResponse = repr(headerResponse)) + print '\t<--BEGIN OPTIONS-->\n\t{optionsResponse}\n\t<--END OPTIONS-->'.format(optionsResponse = repr(optionsResponse)) + print '\t<--BEGIN RESPONSE-->\n\t{response}\n\t<--END RESPONSE-->'.format(response = repr(response)) self.sock.sendto(response, (self.broadcast, 68)) def dhcpAck(self, message): @@ -244,11 +230,11 @@ def dhcpAck(self, message): clientmac, headerResponse = self.craftHeader(message) optionsResponse = self.craftOptions(5, clientmac) #DHCPACK response = headerResponse + optionsResponse - - self.logger.debug('DHCPACK - Sending the following') - self.logger.debug('\t<--BEGIN HEADER-->\n\t{headerResponse}\n\t<--END HEADER-->'.format(headerResponse = repr(headerResponse))) - self.logger.debug('\t<--BEGIN OPTIONS-->\n\t{optionsResponse}\n\t<--END OPTIONS-->'.format(optionsResponse = repr(optionsResponse))) - self.logger.debug('\t<--BEGIN RESPONSE-->\n\t{response}\n\t<--END RESPONSE-->'.format(response = repr(response))) + if self.mode_debug: + print '[DEBUG] DHCPACK - Sending the following' + print '\t<--BEGIN HEADER-->\n\t{headerResponse}\n\t<--END HEADER-->'.format(headerResponse = repr(headerResponse)) + print '\t<--BEGIN OPTIONS-->\n\t{optionsResponse}\n\t<--END OPTIONS-->'.format(optionsResponse = repr(optionsResponse)) + print '\t<--BEGIN RESPONSE-->\n\t{response}\n\t<--END RESPONSE-->'.format(response = repr(response)) self.sock.sendto(response, (self.broadcast, 68)) def listen(self): @@ -256,22 +242,24 @@ def listen(self): while True: message, address = self.sock.recvfrom(1024) clientmac = struct.unpack('!28x6s', message[:34]) - - self.logger.debug('Received message') - self.logger.debug('\t<--BEGIN MESSAGE-->\n\t{message}\n\t<--END MESSAGE-->'.format(message = repr(message))) + if self.mode_debug: + print '[DEBUG] Received message' + print '\t<--BEGIN MESSAGE-->\n\t{message}\n\t<--END MESSAGE-->'.format(message = repr(message)) self.options = self.tlvParse(message[240:]) - - self.logger.debug('Parsed received options') - self.logger.debug('\t<--BEGIN OPTIONS-->\n\t{options}\n\t<--END OPTIONS-->'.format(options = repr(self.options))) + if self.mode_debug: + print '[DEBUG] Parsed received options' + print '\t<--BEGIN OPTIONS-->\n\t{options}\n\t<--END OPTIONS-->'.format(options = repr(self.options)) if not (60 in self.options and 'PXEClient' in self.options[60][0]) : continue type = ord(self.options[53][0]) #see RFC2131 page 10 - if type == 1: - self.logger.debug('Received DHCPOFFER') + if self.mode_debug: + print '[DEBUG] Received DHCPOFFER' self.dhcpOffer(message) elif type == 3 and address[0] == '0.0.0.0' and not self.mode_proxy: - self.logger.debug('Received DHCPACK') + if self.mode_debug: + print '[DEBUG] Received DHCPACK' self.dhcpAck(message) elif type == 3 and address[0] != '0.0.0.0' and self.mode_proxy: - self.logger.debug('Received DHCPACK') + if self.mode_debug: + print '[DEBUG] Received DHCPACK' self.dhcpAck(message) diff --git a/pypxe/http.py b/pypxe/http.py index 7fbcd46..b27d459 100644 --- a/pypxe/http.py +++ b/pypxe/http.py @@ -18,8 +18,7 @@ def __init__(self, **serverSettings): self.ip = serverSettings.get('ip', '0.0.0.0') self.port = serverSettings.get('port', 80) self.netbootDirectory = serverSettings.get('netbootDirectory', '.') - self.logger = serverSettings.get('logger', None) - self.mode_debug = serverSettings.get('mode_debug', False) + self.mode_debug = serverSettings.get('mode_debug', False) #debug mode self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) self.sock.bind((self.ip, self.port)) @@ -30,29 +29,18 @@ def __init__(self, **serverSettings): os.chdir (self.netbootDirectory) os.chroot ('.') - if self.logger == None: - import logging - import logging.handlers - # setup logger - self.logger = logging.getLogger("http") - handler = logging.StreamHandler() - formatter = logging.Formatter('%(asctime)s %(levelname)s %(message)s') - handler.setFormatter(formatter) - self.logger.addHandler(handler) - - if self.mode_debug: - self.logger.setLevel(logging.DEBUG) - - self.logger.debug('NOTICE: HTTP server started in debug mode. HTTP server is using the following:') - self.logger.debug('\tHTTP Server IP: {}'.format(self.ip)) - self.logger.debug('\tHTTP Server Port: {}'.format(self.port)) - self.logger.debug('\tHTTP Network Boot Directory: {}'.format(self.netbootDirectory)) + if self.mode_debug: + print 'NOTICE: HTTP server started in debug mode. HTTP server is using the following:' + print '\tHTTP Server IP: {}'.format(self.ip) + print '\tHTTP Server Port: {}'.format(self.port) + print '\tHTTP Network Boot Directory: {}'.format(self.netbootDirectory) def handleRequest(self, connection, addr): '''This method handles HTTP request''' request = connection.recv(1024) - self.logger.debug('HTTP Recieved message from {addr}'.format(addr = repr(addr))) - self.logger.debug('\t<--BEGIN MESSAGE-->\n\t{request}\n\t<--END MESSAGE-->'.format(request = repr(request))) + if self.mode_debug: + print '[DEBUG] HTTP Recieved message from {addr}'.format(addr = repr(addr)) + print '\t<--BEGIN MESSAGE-->\n\t{request}\n\t<--END MESSAGE-->'.format(request = repr(request)) startline = request.split('\r\n')[0].split(' ') method = startline[0] target = startline[1] @@ -66,25 +54,28 @@ def handleRequest(self, connection, addr): if status[:3] in ('404', '501'): #fail out connection.send(response) connection.close() - self.logger.debug('HTTP Sending message to {addr}'.format(addr = repr(addr))) - self.logger.debug('\t<--BEING MESSAGE-->\n\t{response}\n\t<--END MESSAGE-->'.format(response = repr(response))) + if self.mode_debug: + print '[DEBUG] HTTP Sending message to {addr}'.format(addr = repr(addr)) + print '\t<--BEING MESSAGE-->\n\t{response}\n\t<--END MESSAGE-->'.format(response = repr(response)) return response += 'Content-Length: %d\r\n' % os.path.getsize(target) response += '\r\n' if method == 'HEAD': connection.send(response) connection.close() - self.logger.debug('HTTP Sending message to {addr}'.format(addr = repr(addr))) - self.logger.debug('\t<--BEING MESSAGE-->\n\t{response}\n\t<--END MESSAGE-->'.format(response = repr(response))) + if self.mode_debug: + print '[DEBUG] HTTP Sending message to {addr}'.format(addr = repr(addr)) + print '\t<--BEING MESSAGE-->\n\t{response}\n\t<--END MESSAGE-->'.format(response = repr(response)) return handle = open(target) response += handle.read() handle.close() connection.send(response) connection.close() - self.logger.debug('HTTP Sending message to {addr}'.format(addr = repr(addr))) - self.logger.debug('\t<--BEING MESSAGE-->\n\t{response}\n\t<--END MESSAGE-->'.format(response = repr(response))) - self.logger.debug('\tHTTP File Sent - http://{target} -> {addr[0]}:{addr[1]}'.format(target = target, addr = addr)) + if self.mode_debug: + print '[DEBUG] HTTP Sending message to {addr}'.format(addr = repr(addr)) + print '\t<--BEING MESSAGE-->\n\t{response}\n\t<--END MESSAGE-->'.format(response = repr(response)) + print '\tHTTP File Sent - http://{target} -> {addr[0]}:{addr[1]}'.format(target = target, addr = addr) def listen(self): '''This method is the main loop that listens for requests''' diff --git a/pypxe/tftp.py b/pypxe/tftp.py index 3f10b1e..8f53516 100644 --- a/pypxe/tftp.py +++ b/pypxe/tftp.py @@ -18,31 +18,18 @@ class TFTPD: ''' def __init__(self, **serverSettings): self.ip = serverSettings.get('ip', '0.0.0.0') - self.port = serverSettings.get('port', 79) + self.port = serverSettings.get('port', 69) self.netbootDirectory = serverSettings.get('netbootDirectory', '.') - self.logger = serverSettings.get('logger', None) - self.mode_debug = serverSettings.get('mode_debug', False) + self.mode_debug = serverSettings.get('mode_debug', False) #debug mode self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) self.sock.bind((self.ip, self.port)) - if self.logger == None: - import logging - import logging.handlers - # setup logger - self.logger = logging.getLogger("tftp") - handler = logging.StreamHandler() - formatter = logging.Formatter('%(asctime)s %(levelname)s %(message)s') - handler.setFormatter(formatter) - self.logger.addHandler(handler) - - if self.mode_debug: - self.logger.setLevel(logging.DEBUG) - - self.logger.debug('TFTP server started in debug mode. TFTP server is using the following:') - self.logger.debug('\tTFTP Server IP: {}'.format(self.ip)) - self.logger.debug('\tTFTP Server Port: {}'.format(self.port)) - self.logger.debug('\tTFTP Network Boot Directory: {}'.format(self.netbootDirectory)) + if self.mode_debug: + print 'NOTICE: TFTP server started in debug mode. TFTP server is using the following:' + print '\tTFTP Server IP: {}'.format(self.ip) + print '\tTFTP Server Port: {}'.format(self.port) + print '\tTFTP Network Boot Directory: {}'.format(self.netbootDirectory) #key is (address, port) pair self.ongoing = defaultdict(lambda: {'filename': '', 'handle': None, 'block': 1, 'blksize': 512, 'sock':None, 'timeout':float("inf"), 'retries':3}) @@ -52,7 +39,6 @@ def __init__(self, **serverSettings): os.chdir (self.netbootDirectory) os.chroot ('.') - def filename(self, message): ''' The first null-delimited field @@ -71,7 +57,8 @@ def notFound(self, address): response = struct.pack('!H', 5) #error code response += struct.pack('!H', 1) #file not found response += 'File Not Found' - self.logger.debug("TFTP Sending 'File Not Found'") + if self.mode_debug: + print "[DEBUG] TFTP Sending 'File Not Found'" self.sock.sendto(response, address) def sendBlock(self, address): @@ -83,13 +70,15 @@ def sendBlock(self, address): response += struct.pack('!H', descriptor['block'] % 2 ** 16) data = descriptor['handle'].read(descriptor['blksize']) response += data - self.logger.debug('TFTP Sending block {block}'.format(block = repr(descriptor['block']))) + if self.mode_debug: + print '[DEBUG] TFTP Sending block {block}'.format(block = repr(descriptor['block'])) descriptor['sock'].sendto(response, address) self.ongoing[address]['retries'] -= 1 self.ongoing[address]['timeout'] = time.time() if len(data) != descriptor['blksize']: descriptor['handle'].close() - self.logger.debug('TFTP File Sent - tftp://{filename} -> {address[0]}:{address[1]}'.format(filename = descriptor['filename'], address = address)) + if self.mode_debug: + print '[DEBUG] TFTP File Sent - tftp://{filename} -> {address[0]}:{address[1]}'.format(filename = descriptor['filename'], address = address) descriptor['sock'].close() self.ongoing.pop(address) @@ -100,13 +89,12 @@ def read(self, address, message): file does not exist -> reply with error ''' filename = self.filename(message) - self.logger.debug('Filename: %s' % filename) if not os.path.lexists(filename): self.notFound(address) return self.ongoing[address]['filename'] = filename self.ongoing[address]['handle'] = open(filename, 'r') - options = message.split(chr(0))[3: -1] + options = message.split(chr(0))[2: -1] options = dict(zip(options[0::2], options[1::2])) response = '' if 'blksize' in options: @@ -116,8 +104,8 @@ def read(self, address, message): self.ongoing[address]['blksize'] = int(options['blksize']) filesize = os.path.getsize(self.ongoing[address]['filename']) if filesize > (2**16 * self.ongoing[address]['blksize']): - self.logger.warning('TFTP request too big, attempting transfer anyway.\n') - self.logger.debug('\tDetails: Filesize {filesize} is too big for blksize {blksize}.\n'.format(filesize = filesize, blksize = self.ongoing[address]['blksize'])) + print '\nWARNING: TFTP request too big, attempting transfer anyway.\n' + print '\tDetails: Filesize {filesize} is too big for blksize {blksize}.\n'.format(filesize = filesize, blksize = self.ongoing[address]['blksize']) if 'tsize' in options: response += 'tsize' + chr(0) response += str(filesize) @@ -139,7 +127,8 @@ def listen(self): opcode = struct.unpack('!H', message[:2])[0] message = message[2:] if opcode == 1: #read the request - self.logger.debug('TFTP receiving request') + if self.mode_debug: + print '[DEBUG] TFTP receiving request' self.read(address, message) if opcode == 4: if self.ongoing.has_key(address): @@ -151,7 +140,7 @@ def listen(self): #Resent those that have timed out for i in self.ongoing: if self.ongoing[i]['timeout']+5 < time.time() and self.ongoing[i]['retries']: - self.logger.debug(self.ongoing[i]['handle'].tell()) + print self.ongoing[i]['handle'].tell() self.ongoing[i]['handle'].seek(-self.ongoing[i]['blksize'], 1) self.sendBlock(i) if not self.ongoing[i]['retries']: From 98ab2be0497a303b7b2850ec60ab59800e05d93b Mon Sep 17 00:00:00 2001 From: Pietro Bertera Date: Thu, 16 Apr 2015 16:16:34 +0200 Subject: [PATCH 22/59] Implemented logging support Implemented logging support trough logging Pyhton module, each service has their own logging handler to keep logging granularity --- DOCUMENTATION.md | 9 ++++++ README.md | 6 ++++ pypxe-server.py | 64 ++++++++++++++++++++++++++++++------- pypxe/dhcp.py | 82 +++++++++++++++++++++++++----------------------- pypxe/http.py | 45 +++++++++++++++----------- pypxe/tftp.py | 37 +++++++++++++--------- 6 files changed, 159 insertions(+), 84 deletions(-) diff --git a/DOCUMENTATION.md b/DOCUMENTATION.md index 4d67579..4a0e2c5 100644 --- a/DOCUMENTATION.md +++ b/DOCUMENTATION.md @@ -78,6 +78,9 @@ The TFTP server class, __`TFTPD()`__, is constructed with the following __keywor * Description: This indicates whether or not the TFTP server should be started in debug mode or not. * Default: `False` * Type: bool +* __`logger`__ + * Description: A [Logger](https://docs.python.org/2/library/logging.html#logger-objects) object used for logging messages, if `None` a local [StreamHandler](https://docs.python.org/2/library/logging.handlers.html#streamhandler) instance will be created + * Default: `None` ##DHCP Server `pypxe.dhcp` @@ -148,6 +151,9 @@ The DHCP server class, __`DHCPD()`__, is constructed with the following __keywor * Description: This indicates whether or not the DHCP server should be started in debug mode or not. * Default: `False` * Type: _bool_ +* __`logger`__ + * Description: A [Logger](https://docs.python.org/2/library/logging.html#logger-objects) object used for logging messages, if `None` a local [StreamHandler](https://docs.python.org/2/library/logging.handlers.html#streamhandler) instance will be created + * Default: `None` ##HTTP Server `pypxe.http` @@ -178,6 +184,9 @@ The HTTP server class, __`HTTPD()`__, is constructed with the following __keywor * Description: This indicates whether or not the HTTP server should be started in debug mode or not. * Default: `False` * Type: bool +* __`logger`__ + * Description: A [Logger](https://docs.python.org/2/library/logging.html#logger-objects) object used for logging messages, if `None` a local [StreamHandler](https://docs.python.org/2/library/logging.handlers.html#streamhandler) instance will be created + * Default: `None` ##Additional Information * The function `chr(0)` is used in multiple places throughout the servers. This denotes a `NULL` byte, or `\x00` diff --git a/README.md b/README.md index 78cf81f..bdee69b 100644 --- a/README.md +++ b/README.md @@ -64,6 +64,12 @@ The following are arguments that can be passed to `pypxe-server.py` when running * Description: Amend configuration from json file * _Use the specified json file to amend the command line options. See example.json for more information._ * Default: `None` + * __`--syslog`__ + * Description: Syslog server + * Default: `None` + * __`--syslog-port`__ + * Description: Syslog server port + * Default: `514` * __DHCP Service Arguments__ _each of the following can be set one of two ways, you can use either/or_ * __`-s DHCP_SERVER_IP`__ or __`--dhcp-server-ip DHCP_SERVER_IP`__ * Description: Specify DHCP server IP address diff --git a/pypxe-server.py b/pypxe-server.py index 990cd18..0855225 100644 --- a/pypxe-server.py +++ b/pypxe-server.py @@ -2,7 +2,8 @@ import os import sys import json - +import logging +import logging.handlers try: import argparse except ImportError: @@ -48,9 +49,11 @@ parser.add_argument('--ipxe', action = 'store_true', dest = 'USE_IPXE', help = 'Enable iPXE ROM', default = False) parser.add_argument('--http', action = 'store_true', dest = 'USE_HTTP', help = 'Enable built-in HTTP server', default = False) parser.add_argument('--no-tftp', action = 'store_false', dest = 'USE_TFTP', help = 'Disable built-in TFTP server, by default it is enabled', default = True) - parser.add_argument('--debug', action = 'store', dest = 'MODE_DEBUG', help = 'Comma Seperated (http,tftp,dhcp). Adds verbosity to the selected services while they run', default = '') + parser.add_argument('--debug', action = 'store', dest = 'MODE_DEBUG', help = 'Comma Seperated (sys,http,tftp,dhcp). Adds verbosity to the selected services while they run. Use \'all\' for enabling debug on all services', default = '') parser.add_argument('--config', action = 'store', dest = 'JSON_CONFIG', help = 'Configure from a json file rather than the command line', default = JSON_CONFIG) - + parser.add_argument('--syslog', action = 'store', dest = 'SYSLOG_SERVER', help = 'Syslog server', default = None) + parser.add_argument('--syslog-port', action = 'store', dest = 'SYSLOG_PORT', help = 'Syslog server port', default = 514) + #argument group for DHCP server exclusive = parser.add_mutually_exclusive_group(required = False) exclusive.add_argument('--dhcp', action = 'store_true', dest = 'USE_DHCP', help = 'Enable built-in DHCP server', default = False) @@ -85,9 +88,24 @@ dargs.update(loadedcfg) args = argparse.Namespace(**dargs) + # setup main logger + sys_logger = logging.getLogger("PyPXE") + if args.SYSLOG_SERVER: + handler = logging.handlers.SysLogHandler(address = (args.SYSLOG_SERVER, int(args.SYSLOG_PORT))) + else: + handler = logging.StreamHandler() + formatter = logging.Formatter('%(asctime)s %(name)s [%(levelname)s] %(message)s') + handler.setFormatter(formatter) + sys_logger.addHandler(handler) + + if "sys" in args.MODE_DEBUG.lower() or "all" in args.MODE_DEBUG.lower(): + sys_logger.setLevel(logging.DEBUG) + else: + sys_logger.setLevel(logging.INFO) + #pass warning to user regarding starting HTTP server without iPXE if args.USE_HTTP and not args.USE_IPXE and not args.USE_DHCP: - print '\nWARNING: HTTP selected but iPXE disabled. PXE ROM must support HTTP requests.\n' + sys_logger.warning('WARNING: HTTP selected but iPXE disabled. PXE ROM must support HTTP requests.') #if the argument was pased to enabled ProxyDHCP then enable the DHCP server if args.DHCP_MODE_PROXY: @@ -110,8 +128,15 @@ #configure/start TFTP server if args.USE_TFTP: - print 'Starting TFTP server...' - tftpServer = tftp.TFTPD(mode_debug = ("tftp" in args.MODE_DEBUG.lower() or "all" in args.MODE_DEBUG.lower())) + # setup tftp logger + tftp_logger = sys_logger.getChild("TFTP") + if "tftp" in args.MODE_DEBUG.lower() or "all" in args.MODE_DEBUG.lower(): + tftp_logger.setLevel(logging.DEBUG) + else: + tftp_logger.setLevel(logging.INFO) + + sys_logger.info('Starting TFTP server...') + tftpServer = tftp.TFTPD(mode_debug = ("tftp" in args.MODE_DEBUG.lower() or "all" in args.MODE_DEBUG.lower()), logger = tftp_logger) tftpd = threading.Thread(target = tftpServer.listen) tftpd.daemon = True tftpd.start() @@ -119,10 +144,17 @@ #configure/start DHCP server if args.USE_DHCP: + # setup dhcp logger + dhcp_logger = sys_logger.getChild("DHCP") + if "dhcp" in args.MODE_DEBUG.lower() or "all" in args.MODE_DEBUG.lower(): + dhcp_logger.setLevel(logging.DEBUG) + else: + dhcp_logger.setLevel(logging.INFO) + if args.DHCP_MODE_PROXY: - print 'Starting DHCP server in ProxyDHCP mode...' + sys_logger.info('Starting DHCP server in ProxyDHCP mode...') else: - print 'Starting DHCP server...' + sys_logger.info('Starting DHCP server...') dhcpServer = dhcp.DHCPD( ip = args.DHCP_SERVER_IP, port = args.DHCP_SERVER_PORT, @@ -137,7 +169,8 @@ useipxe = args.USE_IPXE, usehttp = args.USE_HTTP, mode_proxy = args.DHCP_MODE_PROXY, - mode_debug = ("dhcp" in args.MODE_DEBUG.lower() or "all" in args.MODE_DEBUG.lower())) + mode_debug = ("dhcp" in args.MODE_DEBUG.lower() or "all" in args.MODE_DEBUG.lower()), + logger = dhcp_logger) dhcpd = threading.Thread(target = dhcpServer.listen) dhcpd.daemon = True dhcpd.start() @@ -146,14 +179,21 @@ #configure/start HTTP server if args.USE_HTTP: - print 'Starting HTTP server...' - httpServer = http.HTTPD(mode_debug = ("http" in args.MODE_DEBUG.lower() or "all" in args.MODE_DEBUG.lower())) + # setup http logger + http_logger = sys_logger.getChild("HTTP") + if "http" in args.MODE_DEBUG.lower() or "all" in args.MODE_DEBUG.lower(): + http_logger.setLevel(logging.DEBUG) + else: + http_logger.setLevel(logging.INFO) + + sys_logger.info('Starting HTTP server...') + httpServer = http.HTTPD(mode_debug = ("http" in args.MODE_DEBUG.lower() or "all" in args.MODE_DEBUG.lower()), logger = http_logger) httpd = threading.Thread(target = httpServer.listen) httpd.daemon = True httpd.start() runningServices.append(httpd) - print 'PyPXE successfully initialized and running!' + sys_logger.info('PyPXE successfully initialized and running!') while map(lambda x: x.isAlive(), runningServices): sleep(1) diff --git a/pypxe/dhcp.py b/pypxe/dhcp.py index 51d0215..aa23d9a 100644 --- a/pypxe/dhcp.py +++ b/pypxe/dhcp.py @@ -7,6 +7,7 @@ import socket import struct import os +import logging from collections import defaultdict from time import time @@ -39,28 +40,39 @@ def __init__(self, **serverSettings): self.mode_proxy = serverSettings.get('mode_proxy', False) #ProxyDHCP mode self.mode_debug = serverSettings.get('mode_debug', False) #debug mode self.magic = struct.pack('!I', 0x63825363) #magic cookie + self.logger = serverSettings.get('logger', None) + + # setup logger + if self.logger == None: + self.logger = logging.getLogger("DHCP") + handler = logging.StreamHandler() + formatter = logging.Formatter('%(asctime)s %(name)s [%(levelname)s] %(message)s') + handler.setFormatter(formatter) + self.logger.addHandler(handler) + + if self.mode_debug: + self.logger.setLevel(logging.DEBUG) if self.http and not self.ipxe: - print '\nWARNING: HTTP selected but iPXE disabled. PXE ROM must support HTTP requests.\n' + self.logger.warning('WARNING: HTTP selected but iPXE disabled. PXE ROM must support HTTP requests.') if self.ipxe and self.http: self.filename = 'http://{fileserver}/{filename}'.format(fileserver = self.fileserver, filename = self.filename) if self.ipxe and not self.http: self.filename = 'tftp://{fileserver}/{filename}'.format(fileserver = self.fileserver, filename = self.filename) - if self.mode_debug: - print 'NOTICE: DHCP server started in debug mode. DHCP server is using the following:' - print '\tDHCP Server IP: {}'.format(self.ip) - print '\tDHCP Server Port: {}'.format(self.port) - print '\tDHCP Lease Range: {} - {}'.format(self.offerfrom, self.offerto) - print '\tDHCP Subnet Mask: {}'.format(self.subnetmask) - print '\tDHCP Router: {}'.format(self.router) - print '\tDHCP DNS Server: {}'.format(self.dnsserver) - print '\tDHCP Broadcast Address: {}'.format(self.broadcast) - print '\tDHCP File Server IP: {}'.format(self.fileserver) - print '\tDHCP File Name: {}'.format(self.filename) - print '\tProxyDHCP Mode: {}'.format(self.mode_proxy) - print '\tUsing iPXE: {}'.format(self.ipxe) - print '\tUsing HTTP Server: {}'.format(self.http) + self.logger.debug('NOTICE: DHCP server started in debug mode. DHCP server is using the following:') + self.logger.debug(' DHCP Server IP: {}'.format(self.ip)) + self.logger.debug(' DHCP Server Port: {}'.format(self.port)) + self.logger.debug(' DHCP Lease Range: {} - {}'.format(self.offerfrom, self.offerto)) + self.logger.debug(' DHCP Subnet Mask: {}'.format(self.subnetmask)) + self.logger.debug(' DHCP Router: {}'.format(self.router)) + self.logger.debug(' DHCP DNS Server: {}'.format(self.dnsserver)) + self.logger.debug(' DHCP Broadcast Address: {}'.format(self.broadcast)) + self.logger.debug(' DHCP File Server IP: {}'.format(self.fileserver)) + self.logger.debug(' DHCP File Name: {}'.format(self.filename)) + self.logger.debug(' ProxyDHCP Mode: {}'.format(self.mode_proxy)) + self.logger.debug(' tUsing iPXE: {}'.format(self.ipxe)) + self.logger.debug(' Using HTTP Server: {}'.format(self.http)) self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) @@ -152,8 +164,7 @@ def craftHeader(self, message): offer = self.nextIP() self.leases[clientmac]['ip'] = offer self.leases[clientmac]['expire'] = time() + 86400 - if self.mode_debug: - print '[DEBUG] New DHCP Assignment - MAC: {MAC} -> IP: {IP}'.format(MAC = self.printMAC(clientmac), IP = self.leases[clientmac]['ip']) + self.logger.debug('New DHCP Assignment - MAC: {MAC} -> IP: {IP}'.format(MAC = self.printMAC(clientmac), IP = self.leases[clientmac]['ip'])) response += socket.inet_aton(offer) #yiaddr else: response += socket.inet_aton('0.0.0.0') @@ -218,11 +229,10 @@ def dhcpOffer(self, message): clientmac, headerResponse = self.craftHeader(message) optionsResponse = self.craftOptions(2, clientmac) #DHCPOFFER response = headerResponse + optionsResponse - if self.mode_debug: - print '[DEBUG] DHCPOFFER - Sending the following' - print '\t<--BEGIN HEADER-->\n\t{headerResponse}\n\t<--END HEADER-->'.format(headerResponse = repr(headerResponse)) - print '\t<--BEGIN OPTIONS-->\n\t{optionsResponse}\n\t<--END OPTIONS-->'.format(optionsResponse = repr(optionsResponse)) - print '\t<--BEGIN RESPONSE-->\n\t{response}\n\t<--END RESPONSE-->'.format(response = repr(response)) + self.logger.debug('DHCPOFFER - Sending the following') + self.logger.debug(' <--BEGIN HEADER-->\n\t{headerResponse}\n\t<--END HEADER-->'.format(headerResponse = repr(headerResponse))) + self.logger.debug(' <--BEGIN OPTIONS-->\n\t{optionsResponse}\n\t<--END OPTIONS-->'.format(optionsResponse = repr(optionsResponse))) + self.logger.debug(' <--BEGIN RESPONSE-->\n\t{response}\n\t<--END RESPONSE-->'.format(response = repr(response))) self.sock.sendto(response, (self.broadcast, 68)) def dhcpAck(self, message): @@ -230,11 +240,10 @@ def dhcpAck(self, message): clientmac, headerResponse = self.craftHeader(message) optionsResponse = self.craftOptions(5, clientmac) #DHCPACK response = headerResponse + optionsResponse - if self.mode_debug: - print '[DEBUG] DHCPACK - Sending the following' - print '\t<--BEGIN HEADER-->\n\t{headerResponse}\n\t<--END HEADER-->'.format(headerResponse = repr(headerResponse)) - print '\t<--BEGIN OPTIONS-->\n\t{optionsResponse}\n\t<--END OPTIONS-->'.format(optionsResponse = repr(optionsResponse)) - print '\t<--BEGIN RESPONSE-->\n\t{response}\n\t<--END RESPONSE-->'.format(response = repr(response)) + self.logger.debug('DHCPACK - Sending the following') + self.logger.debug(' <--BEGIN HEADER-->\n\t{headerResponse}\n\t<--END HEADER-->'.format(headerResponse = repr(headerResponse))) + self.logger.debug(' <--BEGIN OPTIONS-->\n\t{optionsResponse}\n\t<--END OPTIONS-->'.format(optionsResponse = repr(optionsResponse))) + self.logger.debug(' <--BEGIN RESPONSE-->\n\t{response}\n\t<--END RESPONSE-->'.format(response = repr(response))) self.sock.sendto(response, (self.broadcast, 68)) def listen(self): @@ -242,24 +251,19 @@ def listen(self): while True: message, address = self.sock.recvfrom(1024) clientmac = struct.unpack('!28x6s', message[:34]) - if self.mode_debug: - print '[DEBUG] Received message' - print '\t<--BEGIN MESSAGE-->\n\t{message}\n\t<--END MESSAGE-->'.format(message = repr(message)) + self.logger.debug('Received message') + self.logger.debug(' <--BEGIN MESSAGE-->\n\t{message}\n\t<--END MESSAGE-->'.format(message = repr(message))) self.options = self.tlvParse(message[240:]) - if self.mode_debug: - print '[DEBUG] Parsed received options' - print '\t<--BEGIN OPTIONS-->\n\t{options}\n\t<--END OPTIONS-->'.format(options = repr(self.options)) + self.logger.debug('Parsed received options') + self.logger.debug(' <--BEGIN OPTIONS-->\n\t{options}\n\t<--END OPTIONS-->'.format(options = repr(self.options))) if not (60 in self.options and 'PXEClient' in self.options[60][0]) : continue type = ord(self.options[53][0]) #see RFC2131 page 10 if type == 1: - if self.mode_debug: - print '[DEBUG] Received DHCPOFFER' + self.logger.debug('Received DHCPOFFER') self.dhcpOffer(message) elif type == 3 and address[0] == '0.0.0.0' and not self.mode_proxy: - if self.mode_debug: - print '[DEBUG] Received DHCPACK' + self.logger.debug('Received DHCPACK') self.dhcpAck(message) elif type == 3 and address[0] != '0.0.0.0' and self.mode_proxy: - if self.mode_debug: - print '[DEBUG] Received DHCPACK' + self.logger.debug('Received DHCPACK') self.dhcpAck(message) diff --git a/pypxe/http.py b/pypxe/http.py index b27d459..972d89c 100644 --- a/pypxe/http.py +++ b/pypxe/http.py @@ -7,6 +7,7 @@ import socket import struct import os +import logging class HTTPD: ''' @@ -19,6 +20,19 @@ def __init__(self, **serverSettings): self.port = serverSettings.get('port', 80) self.netbootDirectory = serverSettings.get('netbootDirectory', '.') self.mode_debug = serverSettings.get('mode_debug', False) #debug mode + self.logger = serverSettings.get('logger', None) + + # setup logger + if self.logger == None: + self.logger = logging.getLogger("HTTP") + handler = logging.StreamHandler() + formatter = logging.Formatter('%(asctime)s %(name)s [%(levelname)s] %(message)s') + handler.setFormatter(formatter) + self.logger.addHandler(handler) + + if self.mode_debug: + self.logger.setLevel(logging.DEBUG) + self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) self.sock.bind((self.ip, self.port)) @@ -29,18 +43,16 @@ def __init__(self, **serverSettings): os.chdir (self.netbootDirectory) os.chroot ('.') - if self.mode_debug: - print 'NOTICE: HTTP server started in debug mode. HTTP server is using the following:' - print '\tHTTP Server IP: {}'.format(self.ip) - print '\tHTTP Server Port: {}'.format(self.port) - print '\tHTTP Network Boot Directory: {}'.format(self.netbootDirectory) + self.logger.debug('NOTICE: HTTP server started in debug mode. HTTP server is using the following:') + self.logger.debug(' HTTP Server IP: {}'.format(self.ip)) + self.logger.debug(' HTTP Server Port: {}'.format(self.port)) + self.logger.debug(' HTTP Network Boot Directory: {}'.format(self.netbootDirectory)) def handleRequest(self, connection, addr): '''This method handles HTTP request''' request = connection.recv(1024) - if self.mode_debug: - print '[DEBUG] HTTP Recieved message from {addr}'.format(addr = repr(addr)) - print '\t<--BEGIN MESSAGE-->\n\t{request}\n\t<--END MESSAGE-->'.format(request = repr(request)) + self.logger.debug('HTTP Recieved message from {addr}'.format(addr = repr(addr))) + self.logger.debug(' <--BEGIN MESSAGE-->\n\t{request}\n\t<--END MESSAGE-->'.format(request = repr(request))) startline = request.split('\r\n')[0].split(' ') method = startline[0] target = startline[1] @@ -54,28 +66,25 @@ def handleRequest(self, connection, addr): if status[:3] in ('404', '501'): #fail out connection.send(response) connection.close() - if self.mode_debug: - print '[DEBUG] HTTP Sending message to {addr}'.format(addr = repr(addr)) - print '\t<--BEING MESSAGE-->\n\t{response}\n\t<--END MESSAGE-->'.format(response = repr(response)) + self.logger.debug('HTTP Sending message to {addr}'.format(addr = repr(addr))) + self.logger.debug(' <--BEING MESSAGE-->\n\t{response}\n\t<--END MESSAGE-->'.format(response = repr(response))) return response += 'Content-Length: %d\r\n' % os.path.getsize(target) response += '\r\n' if method == 'HEAD': connection.send(response) connection.close() - if self.mode_debug: - print '[DEBUG] HTTP Sending message to {addr}'.format(addr = repr(addr)) - print '\t<--BEING MESSAGE-->\n\t{response}\n\t<--END MESSAGE-->'.format(response = repr(response)) + self.logger.debug('HTTP Sending message to {addr}'.format(addr = repr(addr))) + self.logger.debug(' <--BEING MESSAGE-->\n\t{response}\n\t<--END MESSAGE-->'.format(response = repr(response))) return handle = open(target) response += handle.read() handle.close() connection.send(response) connection.close() - if self.mode_debug: - print '[DEBUG] HTTP Sending message to {addr}'.format(addr = repr(addr)) - print '\t<--BEING MESSAGE-->\n\t{response}\n\t<--END MESSAGE-->'.format(response = repr(response)) - print '\tHTTP File Sent - http://{target} -> {addr[0]}:{addr[1]}'.format(target = target, addr = addr) + self.logger.debug('HTTP Sending message to {addr}'.format(addr = repr(addr))) + self.logger.debug(' <--BEING MESSAGE-->\n\t{response}\n\t<--END MESSAGE-->'.format(response = repr(response))) + self.logger.debug(' HTTP File Sent - http://{target} -> {addr[0]}:{addr[1]}'.format(target = target, addr = addr)) def listen(self): '''This method is the main loop that listens for requests''' diff --git a/pypxe/tftp.py b/pypxe/tftp.py index 8f53516..ed38c14 100644 --- a/pypxe/tftp.py +++ b/pypxe/tftp.py @@ -9,6 +9,7 @@ import os import select import time +import logging from collections import defaultdict class TFTPD: @@ -21,15 +22,26 @@ def __init__(self, **serverSettings): self.port = serverSettings.get('port', 69) self.netbootDirectory = serverSettings.get('netbootDirectory', '.') self.mode_debug = serverSettings.get('mode_debug', False) #debug mode + self.logger = serverSettings.get('logger', None) self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) self.sock.bind((self.ip, self.port)) + # setup logger + if self.logger == None: + self.logger = logging.getLogger("TFTP") + handler = logging.StreamHandler() + formatter = logging.Formatter('%(asctime)s %(name)s %(levelname)s %(message)s') + handler.setFormatter(formatter) + self.logger.addHandler(handler) + if self.mode_debug: - print 'NOTICE: TFTP server started in debug mode. TFTP server is using the following:' - print '\tTFTP Server IP: {}'.format(self.ip) - print '\tTFTP Server Port: {}'.format(self.port) - print '\tTFTP Network Boot Directory: {}'.format(self.netbootDirectory) + self.logger.setLevel(logging.DEBUG) + + self.logger.debug('NOTICE: TFTP server started in debug mode. TFTP server is using the following:') + self.logger.debug(' TFTP Server IP: {}'.format(self.ip)) + self.logger.debug('TFTP Server Port: {}'.format(self.port)) + self.logger.debug(' TFTP Network Boot Directory: {}'.format(self.netbootDirectory)) #key is (address, port) pair self.ongoing = defaultdict(lambda: {'filename': '', 'handle': None, 'block': 1, 'blksize': 512, 'sock':None, 'timeout':float("inf"), 'retries':3}) @@ -57,8 +69,7 @@ def notFound(self, address): response = struct.pack('!H', 5) #error code response += struct.pack('!H', 1) #file not found response += 'File Not Found' - if self.mode_debug: - print "[DEBUG] TFTP Sending 'File Not Found'" + self.logger.debug("TFTP Sending 'File Not Found'") self.sock.sendto(response, address) def sendBlock(self, address): @@ -70,15 +81,13 @@ def sendBlock(self, address): response += struct.pack('!H', descriptor['block'] % 2 ** 16) data = descriptor['handle'].read(descriptor['blksize']) response += data - if self.mode_debug: - print '[DEBUG] TFTP Sending block {block}'.format(block = repr(descriptor['block'])) + self.logger.debug('TFTP Sending block {block}'.format(block = repr(descriptor['block']))) descriptor['sock'].sendto(response, address) self.ongoing[address]['retries'] -= 1 self.ongoing[address]['timeout'] = time.time() if len(data) != descriptor['blksize']: descriptor['handle'].close() - if self.mode_debug: - print '[DEBUG] TFTP File Sent - tftp://{filename} -> {address[0]}:{address[1]}'.format(filename = descriptor['filename'], address = address) + self.logger.debug('TFTP File Sent - tftp://{filename} -> {address[0]}:{address[1]}'.format(filename = descriptor['filename'], address = address)) descriptor['sock'].close() self.ongoing.pop(address) @@ -104,8 +113,8 @@ def read(self, address, message): self.ongoing[address]['blksize'] = int(options['blksize']) filesize = os.path.getsize(self.ongoing[address]['filename']) if filesize > (2**16 * self.ongoing[address]['blksize']): - print '\nWARNING: TFTP request too big, attempting transfer anyway.\n' - print '\tDetails: Filesize {filesize} is too big for blksize {blksize}.\n'.format(filesize = filesize, blksize = self.ongoing[address]['blksize']) + self.logger.warning('TFTP request too big, attempting transfer anyway.') + self.logger.warning(' Details: Filesize {filesize} is too big for blksize {blksize}.\n'.format(filesize = filesize, blksize = self.ongoing[address]['blksize'])) if 'tsize' in options: response += 'tsize' + chr(0) response += str(filesize) @@ -127,8 +136,7 @@ def listen(self): opcode = struct.unpack('!H', message[:2])[0] message = message[2:] if opcode == 1: #read the request - if self.mode_debug: - print '[DEBUG] TFTP receiving request' + self.logger.debug('TFTP receiving request') self.read(address, message) if opcode == 4: if self.ongoing.has_key(address): @@ -140,7 +148,6 @@ def listen(self): #Resent those that have timed out for i in self.ongoing: if self.ongoing[i]['timeout']+5 < time.time() and self.ongoing[i]['retries']: - print self.ongoing[i]['handle'].tell() self.ongoing[i]['handle'].seek(-self.ongoing[i]['blksize'], 1) self.sendBlock(i) if not self.ongoing[i]['retries']: From 3830fd0a209e59c84e9f696c41c5c614cba88083 Mon Sep 17 00:00:00 2001 From: Pietro Bertera Date: Thu, 16 Apr 2015 17:14:14 +0200 Subject: [PATCH 23/59] Moved request validation in DHCP.validateReq() --- DOCUMENTATION.md | 2 ++ pypxe/dhcp.py | 13 ++++++++++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/DOCUMENTATION.md b/DOCUMENTATION.md index 4d67579..6189eb4 100644 --- a/DOCUMENTATION.md +++ b/DOCUMENTATION.md @@ -18,6 +18,8 @@ Also included in these options are our PXE options. The minimum required option Once the four way handshake is complete, the client will send a TFTP read request to the given fileserver IP address requesting the given filename. +By default only requests declaring the 'PXEClient' value dhcp option 60 are served, this is defined by [PXE specifications](http://www.pix.net/software/pxeboot/archive/pxespec.pdf) If you're using PyPXE as a library you can change this behavior extending the *DHCP* class and overwriting the *validateReq* method. + ###ProxyDHCP ProxyDHCP mode is useful for when you either cannot (or do not want to) change the main DHCP server on a network. The bulk of ProxyDHCP information can be found in the [Intel PXE spec](http://www.pix.net/software/pxeboot/archive/pxespec.pdf). The main idea behind ProxyDHCP is that the main network DHCP server can hand out the IP leases while the ProxyDHCP server hands out the PXE information to each client. Therefore, slightly different information is sent in the ProxyDHCP packets. diff --git a/pypxe/dhcp.py b/pypxe/dhcp.py index 51d0215..87fa9ba 100644 --- a/pypxe/dhcp.py +++ b/pypxe/dhcp.py @@ -237,6 +237,16 @@ def dhcpAck(self, message): print '\t<--BEGIN RESPONSE-->\n\t{response}\n\t<--END RESPONSE-->'.format(response = repr(response)) self.sock.sendto(response, (self.broadcast, 68)) + def validateReq(self): + # client request is valid only if contains Vendor-Class = PXEClient + if 60 in self.options and 'PXEClient' in self.options[60][0]: + if self.mode_debug: + print '[DEBUG] Valid client request received' + return True + if self.mode_debug: + print '[DEBUG] Invalid client request received' + return False + def listen(self): '''Main listen loop''' while True: @@ -249,7 +259,8 @@ def listen(self): if self.mode_debug: print '[DEBUG] Parsed received options' print '\t<--BEGIN OPTIONS-->\n\t{options}\n\t<--END OPTIONS-->'.format(options = repr(self.options)) - if not (60 in self.options and 'PXEClient' in self.options[60][0]) : continue + if not self.validateReq(): + continue type = ord(self.options[53][0]) #see RFC2131 page 10 if type == 1: if self.mode_debug: From ed85e75c0b71970f39ca700701a58d949beb99fe Mon Sep 17 00:00:00 2001 From: Pietro Bertera Date: Thu, 16 Apr 2015 18:10:50 +0200 Subject: [PATCH 24/59] Added validateReq() method --- DOCUMENTATION.md | 2 ++ pypxe/dhcp.py | 13 ++++++++++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/DOCUMENTATION.md b/DOCUMENTATION.md index 4a0e2c5..d298f1f 100644 --- a/DOCUMENTATION.md +++ b/DOCUMENTATION.md @@ -18,6 +18,8 @@ Also included in these options are our PXE options. The minimum required option Once the four way handshake is complete, the client will send a TFTP read request to the given fileserver IP address requesting the given filename. +By default only requests declaring the 'PXEClient' value dhcp option 60 are served, this is defined by [PXE specifications](http://www.pix.net/software/pxeboot/archive/pxespec.pdf) If you're using PyPXE as a library you can change this behavior extending the *DHCP* class and overwriting the *validateReq* method. + ###ProxyDHCP ProxyDHCP mode is useful for when you either cannot (or do not want to) change the main DHCP server on a network. The bulk of ProxyDHCP information can be found in the [Intel PXE spec](http://www.pix.net/software/pxeboot/archive/pxespec.pdf). The main idea behind ProxyDHCP is that the main network DHCP server can hand out the IP leases while the ProxyDHCP server hands out the PXE information to each client. Therefore, slightly different information is sent in the ProxyDHCP packets. diff --git a/pypxe/dhcp.py b/pypxe/dhcp.py index aa23d9a..7972c29 100644 --- a/pypxe/dhcp.py +++ b/pypxe/dhcp.py @@ -235,6 +235,16 @@ def dhcpOffer(self, message): self.logger.debug(' <--BEGIN RESPONSE-->\n\t{response}\n\t<--END RESPONSE-->'.format(response = repr(response))) self.sock.sendto(response, (self.broadcast, 68)) + def validateReq(self): + # client request is valid only if contains Vendor-Class = PXEClient + if 60 in self.options and 'PXEClient' in self.options[60][0]: + if self.mode_debug: + self.logger.debug('Valid client request received') + return True + if self.mode_debug: + self.logger.debug('Invalid client request received') + return False + def dhcpAck(self, message): '''This method responds to DHCP request with acknowledge''' clientmac, headerResponse = self.craftHeader(message) @@ -256,7 +266,8 @@ def listen(self): self.options = self.tlvParse(message[240:]) self.logger.debug('Parsed received options') self.logger.debug(' <--BEGIN OPTIONS-->\n\t{options}\n\t<--END OPTIONS-->'.format(options = repr(self.options))) - if not (60 in self.options and 'PXEClient' in self.options[60][0]) : continue + if not self.validateReq(): + continue type = ord(self.options[53][0]) #see RFC2131 page 10 if type == 1: self.logger.debug('Received DHCPOFFER') From d9548060cb08db3abb1aee3c5f4713f043dc1b8b Mon Sep 17 00:00:00 2001 From: Pietro Bertera Date: Thu, 16 Apr 2015 18:19:09 +0200 Subject: [PATCH 25/59] Revert "Added validateReq() method" This reverts commit ed85e75c0b71970f39ca700701a58d949beb99fe. --- DOCUMENTATION.md | 2 -- pypxe/dhcp.py | 13 +------------ 2 files changed, 1 insertion(+), 14 deletions(-) diff --git a/DOCUMENTATION.md b/DOCUMENTATION.md index d298f1f..4a0e2c5 100644 --- a/DOCUMENTATION.md +++ b/DOCUMENTATION.md @@ -18,8 +18,6 @@ Also included in these options are our PXE options. The minimum required option Once the four way handshake is complete, the client will send a TFTP read request to the given fileserver IP address requesting the given filename. -By default only requests declaring the 'PXEClient' value dhcp option 60 are served, this is defined by [PXE specifications](http://www.pix.net/software/pxeboot/archive/pxespec.pdf) If you're using PyPXE as a library you can change this behavior extending the *DHCP* class and overwriting the *validateReq* method. - ###ProxyDHCP ProxyDHCP mode is useful for when you either cannot (or do not want to) change the main DHCP server on a network. The bulk of ProxyDHCP information can be found in the [Intel PXE spec](http://www.pix.net/software/pxeboot/archive/pxespec.pdf). The main idea behind ProxyDHCP is that the main network DHCP server can hand out the IP leases while the ProxyDHCP server hands out the PXE information to each client. Therefore, slightly different information is sent in the ProxyDHCP packets. diff --git a/pypxe/dhcp.py b/pypxe/dhcp.py index 7972c29..aa23d9a 100644 --- a/pypxe/dhcp.py +++ b/pypxe/dhcp.py @@ -235,16 +235,6 @@ def dhcpOffer(self, message): self.logger.debug(' <--BEGIN RESPONSE-->\n\t{response}\n\t<--END RESPONSE-->'.format(response = repr(response))) self.sock.sendto(response, (self.broadcast, 68)) - def validateReq(self): - # client request is valid only if contains Vendor-Class = PXEClient - if 60 in self.options and 'PXEClient' in self.options[60][0]: - if self.mode_debug: - self.logger.debug('Valid client request received') - return True - if self.mode_debug: - self.logger.debug('Invalid client request received') - return False - def dhcpAck(self, message): '''This method responds to DHCP request with acknowledge''' clientmac, headerResponse = self.craftHeader(message) @@ -266,8 +256,7 @@ def listen(self): self.options = self.tlvParse(message[240:]) self.logger.debug('Parsed received options') self.logger.debug(' <--BEGIN OPTIONS-->\n\t{options}\n\t<--END OPTIONS-->'.format(options = repr(self.options))) - if not self.validateReq(): - continue + if not (60 in self.options and 'PXEClient' in self.options[60][0]) : continue type = ord(self.options[53][0]) #see RFC2131 page 10 if type == 1: self.logger.debug('Received DHCPOFFER') From 01421b15a6d0e89040f7876ce2c159daa2b544d7 Mon Sep 17 00:00:00 2001 From: Pietro Bertera Date: Thu, 16 Apr 2015 18:30:38 +0200 Subject: [PATCH 26/59] Python Logging support + DHCP.validateReq() --- DOCUMENTATION.md | 2 ++ pypxe/dhcp.py | 13 ++++++++++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/DOCUMENTATION.md b/DOCUMENTATION.md index 4a0e2c5..d298f1f 100644 --- a/DOCUMENTATION.md +++ b/DOCUMENTATION.md @@ -18,6 +18,8 @@ Also included in these options are our PXE options. The minimum required option Once the four way handshake is complete, the client will send a TFTP read request to the given fileserver IP address requesting the given filename. +By default only requests declaring the 'PXEClient' value dhcp option 60 are served, this is defined by [PXE specifications](http://www.pix.net/software/pxeboot/archive/pxespec.pdf) If you're using PyPXE as a library you can change this behavior extending the *DHCP* class and overwriting the *validateReq* method. + ###ProxyDHCP ProxyDHCP mode is useful for when you either cannot (or do not want to) change the main DHCP server on a network. The bulk of ProxyDHCP information can be found in the [Intel PXE spec](http://www.pix.net/software/pxeboot/archive/pxespec.pdf). The main idea behind ProxyDHCP is that the main network DHCP server can hand out the IP leases while the ProxyDHCP server hands out the PXE information to each client. Therefore, slightly different information is sent in the ProxyDHCP packets. diff --git a/pypxe/dhcp.py b/pypxe/dhcp.py index aa23d9a..d7d7416 100644 --- a/pypxe/dhcp.py +++ b/pypxe/dhcp.py @@ -246,6 +246,16 @@ def dhcpAck(self, message): self.logger.debug(' <--BEGIN RESPONSE-->\n\t{response}\n\t<--END RESPONSE-->'.format(response = repr(response))) self.sock.sendto(response, (self.broadcast, 68)) + def validateReq(self): + # client request is valid only if contains Vendor-Class = PXEClient + if 60 in self.options and 'PXEClient' in self.options[60][0]: + if self.mode_debug: + print '[DEBUG] Valid client request received' + return True + if self.mode_debug: + print '[DEBUG] Invalid client request received' + return False + def listen(self): '''Main listen loop''' while True: @@ -256,7 +266,8 @@ def listen(self): self.options = self.tlvParse(message[240:]) self.logger.debug('Parsed received options') self.logger.debug(' <--BEGIN OPTIONS-->\n\t{options}\n\t<--END OPTIONS-->'.format(options = repr(self.options))) - if not (60 in self.options and 'PXEClient' in self.options[60][0]) : continue + if not self.validateReq(): + continue type = ord(self.options[53][0]) #see RFC2131 page 10 if type == 1: self.logger.debug('Received DHCPOFFER') From 733970b928b9cd689bfb711ba4dfbfd24504b08e Mon Sep 17 00:00:00 2001 From: Pietro Bertera Date: Wed, 15 Apr 2015 19:21:09 +0200 Subject: [PATCH 27/59] added tftp documentation --- DOCUMENTATION.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/DOCUMENTATION.md b/DOCUMENTATION.md index 6189eb4..b4cb0e2 100644 --- a/DOCUMENTATION.md +++ b/DOCUMENTATION.md @@ -80,7 +80,9 @@ The TFTP server class, __`TFTPD()`__, is constructed with the following __keywor * Description: This indicates whether or not the TFTP server should be started in debug mode or not. * Default: `False` * Type: bool - +* __`logger`__ + * Description: A [Logger](https://docs.python.org/2/library/logging.html#logger-objects) object used for logging messages, if `None` a local [StreamHandler](https://docs.python.org/2/library/logging.handlers.html#streamhandler) instance will be created + * Default: `None` ##DHCP Server `pypxe.dhcp` ###Importing From 31879dd3a3165e5ca936d09a43079d8e4f782497 Mon Sep 17 00:00:00 2001 From: Pietro Bertera Date: Wed, 15 Apr 2015 19:23:45 +0200 Subject: [PATCH 28/59] typp fix --- DOCUMENTATION.md | 1 + 1 file changed, 1 insertion(+) diff --git a/DOCUMENTATION.md b/DOCUMENTATION.md index b4cb0e2..9ff9475 100644 --- a/DOCUMENTATION.md +++ b/DOCUMENTATION.md @@ -83,6 +83,7 @@ The TFTP server class, __`TFTPD()`__, is constructed with the following __keywor * __`logger`__ * Description: A [Logger](https://docs.python.org/2/library/logging.html#logger-objects) object used for logging messages, if `None` a local [StreamHandler](https://docs.python.org/2/library/logging.handlers.html#streamhandler) instance will be created * Default: `None` + ##DHCP Server `pypxe.dhcp` ###Importing From f8c53efd62f320beda6dea02a81e58cb6dbef2bd Mon Sep 17 00:00:00 2001 From: Pietro Bertera Date: Wed, 15 Apr 2015 19:24:39 +0200 Subject: [PATCH 29/59] Added documentation for logger object --- DOCUMENTATION.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/DOCUMENTATION.md b/DOCUMENTATION.md index 9ff9475..a929f56 100644 --- a/DOCUMENTATION.md +++ b/DOCUMENTATION.md @@ -153,6 +153,9 @@ The DHCP server class, __`DHCPD()`__, is constructed with the following __keywor * Description: This indicates whether or not the DHCP server should be started in debug mode or not. * Default: `False` * Type: _bool_ +* __`logger`__ + * Description: A [Logger](https://docs.python.org/2/library/logging.html#logger-objects) object used for logging messages, if `None` a local [StreamHandler](https://docs.python.org/2/library/logging.handlers.html#streamhandler) instance will be created + * Default: `None` ##HTTP Server `pypxe.http` @@ -183,6 +186,9 @@ The HTTP server class, __`HTTPD()`__, is constructed with the following __keywor * Description: This indicates whether or not the HTTP server should be started in debug mode or not. * Default: `False` * Type: bool +* __`logger`__ + * Description: A [Logger](https://docs.python.org/2/library/logging.html#logger-objects) object used for logging messages, if `None` a local [StreamHandler](https://docs.python.org/2/library/logging.handlers.html#streamhandler) instance will be created + * Default: `None` ##Additional Information * The function `chr(0)` is used in multiple places throughout the servers. This denotes a `NULL` byte, or `\x00` From 14b8e88e70896a4e9f8c1a981b9a6883bf50a112 Mon Sep 17 00:00:00 2001 From: Pietro Bertera Date: Thu, 16 Apr 2015 22:21:26 +0200 Subject: [PATCH 30/59] Removed sys logging selector and typo --- pypxe-server.py | 24 ++---------------------- pypxe/dhcp.py | 2 +- 2 files changed, 3 insertions(+), 23 deletions(-) diff --git a/pypxe-server.py b/pypxe-server.py index 0855225..db40551 100644 --- a/pypxe-server.py +++ b/pypxe-server.py @@ -49,7 +49,7 @@ parser.add_argument('--ipxe', action = 'store_true', dest = 'USE_IPXE', help = 'Enable iPXE ROM', default = False) parser.add_argument('--http', action = 'store_true', dest = 'USE_HTTP', help = 'Enable built-in HTTP server', default = False) parser.add_argument('--no-tftp', action = 'store_false', dest = 'USE_TFTP', help = 'Disable built-in TFTP server, by default it is enabled', default = True) - parser.add_argument('--debug', action = 'store', dest = 'MODE_DEBUG', help = 'Comma Seperated (sys,http,tftp,dhcp). Adds verbosity to the selected services while they run. Use \'all\' for enabling debug on all services', default = '') + parser.add_argument('--debug', action = 'store', dest = 'MODE_DEBUG', help = 'Comma Seperated (http,tftp,dhcp). Adds verbosity to the selected services while they run. Use \'all\' for enabling debug on all services', default = '') parser.add_argument('--config', action = 'store', dest = 'JSON_CONFIG', help = 'Configure from a json file rather than the command line', default = JSON_CONFIG) parser.add_argument('--syslog', action = 'store', dest = 'SYSLOG_SERVER', help = 'Syslog server', default = None) parser.add_argument('--syslog-port', action = 'store', dest = 'SYSLOG_PORT', help = 'Syslog server port', default = 514) @@ -97,11 +97,7 @@ formatter = logging.Formatter('%(asctime)s %(name)s [%(levelname)s] %(message)s') handler.setFormatter(formatter) sys_logger.addHandler(handler) - - if "sys" in args.MODE_DEBUG.lower() or "all" in args.MODE_DEBUG.lower(): - sys_logger.setLevel(logging.DEBUG) - else: - sys_logger.setLevel(logging.INFO) + sys_logger.setLevel(logging.INFO) #pass warning to user regarding starting HTTP server without iPXE if args.USE_HTTP and not args.USE_IPXE and not args.USE_DHCP: @@ -130,11 +126,6 @@ if args.USE_TFTP: # setup tftp logger tftp_logger = sys_logger.getChild("TFTP") - if "tftp" in args.MODE_DEBUG.lower() or "all" in args.MODE_DEBUG.lower(): - tftp_logger.setLevel(logging.DEBUG) - else: - tftp_logger.setLevel(logging.INFO) - sys_logger.info('Starting TFTP server...') tftpServer = tftp.TFTPD(mode_debug = ("tftp" in args.MODE_DEBUG.lower() or "all" in args.MODE_DEBUG.lower()), logger = tftp_logger) tftpd = threading.Thread(target = tftpServer.listen) @@ -146,11 +137,6 @@ if args.USE_DHCP: # setup dhcp logger dhcp_logger = sys_logger.getChild("DHCP") - if "dhcp" in args.MODE_DEBUG.lower() or "all" in args.MODE_DEBUG.lower(): - dhcp_logger.setLevel(logging.DEBUG) - else: - dhcp_logger.setLevel(logging.INFO) - if args.DHCP_MODE_PROXY: sys_logger.info('Starting DHCP server in ProxyDHCP mode...') else: @@ -176,16 +162,10 @@ dhcpd.start() runningServices.append(dhcpd) - #configure/start HTTP server if args.USE_HTTP: # setup http logger http_logger = sys_logger.getChild("HTTP") - if "http" in args.MODE_DEBUG.lower() or "all" in args.MODE_DEBUG.lower(): - http_logger.setLevel(logging.DEBUG) - else: - http_logger.setLevel(logging.INFO) - sys_logger.info('Starting HTTP server...') httpServer = http.HTTPD(mode_debug = ("http" in args.MODE_DEBUG.lower() or "all" in args.MODE_DEBUG.lower()), logger = http_logger) httpd = threading.Thread(target = httpServer.listen) diff --git a/pypxe/dhcp.py b/pypxe/dhcp.py index 13f7c8e..deb87b7 100644 --- a/pypxe/dhcp.py +++ b/pypxe/dhcp.py @@ -62,7 +62,7 @@ def __init__(self, **serverSettings): self.logger.debug('NOTICE: DHCP server started in debug mode. DHCP server is using the following:') self.logger.debug(' DHCP Server IP: {}'.format(self.ip)) - self.logger.debug(' DHCP Server Port: {}'.format(self.port)) + self.logger.debug(' DHCP Server Port: {}'.format(self.port)) self.logger.debug(' DHCP Lease Range: {} - {}'.format(self.offerfrom, self.offerto)) self.logger.debug(' DHCP Subnet Mask: {}'.format(self.subnetmask)) self.logger.debug(' DHCP Router: {}'.format(self.router)) From 6a95bf8e31b06ec461f2b871598efe5c4d1c3ca4 Mon Sep 17 00:00:00 2001 From: Pietro Bertera Date: Thu, 16 Apr 2015 23:14:51 +0200 Subject: [PATCH 31/59] some TFTP fixes - added null byte after "File not found" ErrMsg. - missing socket in ongoing dict --- pypxe/tftp.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/pypxe/tftp.py b/pypxe/tftp.py index ed38c14..ca7f231 100644 --- a/pypxe/tftp.py +++ b/pypxe/tftp.py @@ -69,6 +69,7 @@ def notFound(self, address): response = struct.pack('!H', 5) #error code response += struct.pack('!H', 1) #file not found response += 'File Not Found' + response += chr(0) self.logger.debug("TFTP Sending 'File Not Found'") self.sock.sendto(response, address) @@ -119,13 +120,17 @@ def read(self, address, message): response += 'tsize' + chr(0) response += str(filesize) response += chr(0) + + socknew = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + socknew.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + socknew.bind((self.ip, 0)) + if response: response = struct.pack('!H', 6) + response - socknew = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - socknew.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - socknew.bind((self.ip, 0)) socknew.sendto(response, address) - self.ongoing[address]['sock'] = socknew + + self.ongoing[address]['sock'] = socknew + self.sendBlock(address) def listen(self): '''This method listens for incoming requests''' @@ -139,7 +144,7 @@ def listen(self): self.logger.debug('TFTP receiving request') self.read(address, message) if opcode == 4: - if self.ongoing.has_key(address): + if self.ongoing.has_key(address): blockack = struct.unpack("!H", message[:2])[0] self.ongoing[address]['block'] = blockack + 1 self.ongoing[address]['retries'] = 3 From 891dc76a436a8a5ee302c814d088c8c50591b7e8 Mon Sep 17 00:00:00 2001 From: Pietro Bertera Date: Fri, 17 Apr 2015 15:51:33 +0200 Subject: [PATCH 32/59] Fixed potential issues in TFTP --- pypxe/tftp.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pypxe/tftp.py b/pypxe/tftp.py index ca7f231..d331205 100644 --- a/pypxe/tftp.py +++ b/pypxe/tftp.py @@ -124,13 +124,13 @@ def read(self, address, message): socknew = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) socknew.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) socknew.bind((self.ip, 0)) + self.ongoing[address]['sock'] = socknew if response: response = struct.pack('!H', 6) + response socknew.sendto(response, address) - - self.ongoing[address]['sock'] = socknew - self.sendBlock(address) + else: + self.sendBlock(address) def listen(self): '''This method listens for incoming requests''' From 3d66fff80ae7772cf789c97f51477567737bda0f Mon Sep 17 00:00:00 2001 From: Pietro Bertera Date: Sun, 19 Apr 2015 15:03:38 +0200 Subject: [PATCH 33/59] TFTP improvement * added transfer mode check * added configurable TFTP timeout and retransmissions (library only) * fixed a bug in connection dropping if timeout reached --- DOCUMENTATION.md | 8 +++- pypxe/tftp.py | 100 +++++++++++++++++++++++++++++++++++------------ 2 files changed, 81 insertions(+), 27 deletions(-) diff --git a/DOCUMENTATION.md b/DOCUMENTATION.md index d298f1f..7ed42da 100644 --- a/DOCUMENTATION.md +++ b/DOCUMENTATION.md @@ -32,7 +32,7 @@ There are a few vendor-specific options under the DHCP option 43: The client should receive two DHCP OFFER packets in ProxyDHCP mode: the first from the main DHCP server and the second from the ProxyDHCP server. Once both are received, the client will continue on with the DHCP handshake and, after it is complete, the client will boot using the settings in the DHCP OFFER from the ProxyDHCP server. ##TFTP -We have only implemented the read OPCODE for the TFTP server, as PXE does not use write. The main TFTP protocol is defined in [RFC1350](http://www.ietf.org/rfc/rfc1350.txt) +We have only implemented the read OPCODE for the TFTP server, as PXE does not use write. Only *octet* transfer mode is supported. The main TFTP protocol is defined in [RFC1350](http://www.ietf.org/rfc/rfc1350.txt) ###blksize The blksize option, as defined in [RFC2348](http://www.ietf.org/rfc/rfc2348.txt) allows the client to specify the block size for each transfer packet. The blksize option is passed along with the read opcode, following the filename and mode. The format is blksize, followed by a null byte, followed by the ASCII base-10 representation of the blksize (i.e 512 rather than 0x200), followed by another null byte. @@ -83,6 +83,12 @@ The TFTP server class, __`TFTPD()`__, is constructed with the following __keywor * __`logger`__ * Description: A [Logger](https://docs.python.org/2/library/logging.html#logger-objects) object used for logging messages, if `None` a local [StreamHandler](https://docs.python.org/2/library/logging.handlers.html#streamhandler) instance will be created * Default: `None` +* __`default_retries`__ + * Description: The number of data retransmissions before dropping a connection + * Default: `3` +* __`timeout`__ + * Description: The time in seconds before re-sending an un-acked data block + * Default: `5` ##DHCP Server `pypxe.dhcp` diff --git a/pypxe/tftp.py b/pypxe/tftp.py index d331205..d4ebd3a 100644 --- a/pypxe/tftp.py +++ b/pypxe/tftp.py @@ -23,6 +23,8 @@ def __init__(self, **serverSettings): self.netbootDirectory = serverSettings.get('netbootDirectory', '.') self.mode_debug = serverSettings.get('mode_debug', False) #debug mode self.logger = serverSettings.get('logger', None) + self.default_retries = serverSettings.get('default_retries', 3) + self.timeout = serverSettings.get('timeout', 5) self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) self.sock.bind((self.ip, self.port)) @@ -44,7 +46,7 @@ def __init__(self, **serverSettings): self.logger.debug(' TFTP Network Boot Directory: {}'.format(self.netbootDirectory)) #key is (address, port) pair - self.ongoing = defaultdict(lambda: {'filename': '', 'handle': None, 'block': 1, 'blksize': 512, 'sock':None, 'timeout':float("inf"), 'retries':3}) + self.ongoing = defaultdict(lambda: {'filename': '', 'handle': None, 'block': 1, 'blksize': 512, 'sock': None, 'sent_time': float("inf"), 'retries': self.default_retries}) # Start in network boot file directory and then chroot, # this simplifies target later as well as offers a slight security increase @@ -59,20 +61,38 @@ def filename(self, message): ''' return message.split(chr(0))[0] - def notFound(self, address): + def tftpError(self, address, code=1, message="File Not Found"): ''' - short int 5 -> Error - short int 1 -> File Not Found - - This method sends the message to the client + short int 5 -> Error Opcode + This method sends the error message to the client + + Error codes: + Value Meaning + 0 Not defined, see error message (if any). + 1 File not found. + 2 Access violation. + 3 Disk full or allocation exceeded. + 4 Illegal TFTP operation. + 5 Unknown transfer ID. + 6 File already exists. + 7 No such user. ''' - response = struct.pack('!H', 5) #error code - response += struct.pack('!H', 1) #file not found - response += 'File Not Found' + response = struct.pack('!H', 5) # error opcode + response += struct.pack('!H', code) # error code + response += message response += chr(0) - self.logger.debug("TFTP Sending 'File Not Found'") + self.logger.debug("TFTP Sending '{code}: {message}'".format(code = code, message = message)) self.sock.sendto(response, address) + def mode(self, message): + ''' + The second null-delimited field + is the transfer mode. This method returns the mode + from the message. + ''' + return message.split(chr(0))[1] + + def sendBlock(self, address): ''' short int 3 -> Data Block @@ -82,10 +102,10 @@ def sendBlock(self, address): response += struct.pack('!H', descriptor['block'] % 2 ** 16) data = descriptor['handle'].read(descriptor['blksize']) response += data - self.logger.debug('TFTP Sending block {block}'.format(block = repr(descriptor['block']))) + self.logger.debug('TFTP Sending block {block} to client {ip}:{port}'.format(block = repr(descriptor['block']), ip = address[0], port = address[1])) descriptor['sock'].sendto(response, address) self.ongoing[address]['retries'] -= 1 - self.ongoing[address]['timeout'] = time.time() + self.ongoing[address]['sent_time'] = time.time() if len(data) != descriptor['blksize']: descriptor['handle'].close() self.logger.debug('TFTP File Sent - tftp://{filename} -> {address[0]}:{address[1]}'.format(filename = descriptor['filename'], address = address)) @@ -98,10 +118,16 @@ def read(self, address, message): file exists -> reply with file file does not exist -> reply with error ''' + mode = self.mode(message) + if mode != 'octet': + self.logger.error("Mode '{mode}' not supported".format(mode = mode)) + self.tftpError(address, 5, 'Mode {mode} not supported'.format(mode = mode)) + return filename = self.filename(message) if not os.path.lexists(filename): - self.notFound(address) + self.tftpError(address, 1, 'File Not Found') return + self.ongoing[address]['filename'] = filename self.ongoing[address]['handle'] = open(filename, 'r') options = message.split(chr(0))[2: -1] @@ -121,39 +147,61 @@ def read(self, address, message): response += str(filesize) response += chr(0) - socknew = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - socknew.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - socknew.bind((self.ip, 0)) - self.ongoing[address]['sock'] = socknew + # Create the data socket only if needed + if self.ongoing[address]['sock'] == None: + socknew = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + socknew.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + socknew.bind((self.ip, 0)) + self.ongoing[address]['sock'] = socknew if response: response = struct.pack('!H', 6) + response socknew.sendto(response, address) - else: + # if no options in request and no block sent yet start sending data + elif self.ongoing[address]['block'] == 1: self.sendBlock(address) + else: + self.logger.warning('Ignored TFTP request: no options in a middle of a transmission') def listen(self): '''This method listens for incoming requests''' while True: - rlist, wlist, xlist = select.select([self.sock] + [self.ongoing[i]['sock'] for i in self.ongoing if self.ongoing[i]['sock']], [], []) + rlist, wlist, xlist = select.select([self.sock] + [self.ongoing[i]['sock'] for i in self.ongoing if self.ongoing[i]['sock']], [], [], 0) for sock in rlist: message, address = sock.recvfrom(1024) opcode = struct.unpack('!H', message[:2])[0] message = message[2:] - if opcode == 1: #read the request + if opcode == 1: # read the request self.logger.debug('TFTP receiving request') self.read(address, message) - if opcode == 4: + if opcode == 4: # ack if self.ongoing.has_key(address): blockack = struct.unpack("!H", message[:2])[0] + if blockack < self.ongoing[address]['block']: + self.logger.warning('Ignoring duplicated ACK received for block {blockack}'.format(blockack = blockack)) + continue + if blockack > self.ongoing[address]['block']: + self.logger.warning('Ignoring out of sequence ACK received for block {blockack}'.format(blockack = blockack)) + continue self.ongoing[address]['block'] = blockack + 1 - self.ongoing[address]['retries'] = 3 + self.ongoing[address]['retries'] = self.default_retries self.sendBlock(address) - #Timeouts and Retries. Done after the above so timeout actually has a value - #Resent those that have timed out + + # Timeouts and Retries. Done after the above so timeout actually has a value + # Resent those that have timed out + dead_conn = [] for i in self.ongoing: - if self.ongoing[i]['timeout']+5 < time.time() and self.ongoing[i]['retries']: + if self.ongoing[i]['sent_time'] + self.timeout < time.time() and self.ongoing[i]['retries']: + self.logger.debug('No ACK received for block: {block}, retrying'.format(block = self.ongoing[address]['block'])) self.ongoing[i]['handle'].seek(-self.ongoing[i]['blksize'], 1) self.sendBlock(i) if not self.ongoing[i]['retries']: - self.ongoing.pop(i) + self.logger.debug('Max retries reached, aborting connection with {client}:{port}'.format(client = i[0], port = i[1])) + self.tftpError(i, 0, 'Timeout reached') + self.ongoing[i]['handle'].close() + self.ongoing[i]['sock'].close() + dead_conn.append(i) + + # Clean up dead connections + for i in dead_conn: + self.ongoing.pop(i) From ef6d7dfd7f085e69413d81ed598ab8bbf7339101 Mon Sep 17 00:00:00 2001 From: Pietro Bertera Date: Sun, 19 Apr 2015 17:18:24 +0200 Subject: [PATCH 34/59] TFTP improvements #2 - added filename in file not found error - fixed .pop() error --- pypxe/tftp.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pypxe/tftp.py b/pypxe/tftp.py index d4ebd3a..e7a30c1 100644 --- a/pypxe/tftp.py +++ b/pypxe/tftp.py @@ -66,7 +66,7 @@ def tftpError(self, address, code=1, message="File Not Found"): short int 5 -> Error Opcode This method sends the error message to the client - Error codes: + Error codes from RFC1350 page 10: Value Meaning 0 Not defined, see error message (if any). 1 File not found. @@ -125,6 +125,7 @@ def read(self, address, message): return filename = self.filename(message) if not os.path.lexists(filename): + self.logger.debug("File '{filename}' not found, sending error message to the client".format(filename = filename) ) self.tftpError(address, 1, 'File Not Found') return @@ -204,4 +205,4 @@ def listen(self): # Clean up dead connections for i in dead_conn: - self.ongoing.pop(i) + self.ongoing.remove(i) From 7e3f064ff14a0ab5c860d7cf02c94ecf02f9c3d4 Mon Sep 17 00:00:00 2001 From: PsychoMario Date: Tue, 21 Apr 2015 19:06:10 +0100 Subject: [PATCH 35/59] TFTP class based rewrite --- pypxe/tftp.py | 336 +++++++++++++++++++++++++++----------------------- 1 file changed, 185 insertions(+), 151 deletions(-) diff --git a/pypxe/tftp.py b/pypxe/tftp.py index e7a30c1..41c4738 100644 --- a/pypxe/tftp.py +++ b/pypxe/tftp.py @@ -10,7 +10,177 @@ import select import time import logging -from collections import defaultdict +import math +#from collections import defaultdict + +class Psocket(socket.socket): + '''Subclassed socket.socket to enable a link back to the client object''' + parent = None + +class Client: + '''Client instance for TFTPD.''' + def __init__(self, mainsock, parent): + # main socket recieve `message` + # self.ongoing.append(Client(message, retries, timeout)) + # select from main socket + [x.sock for x in self.ongoing] + self.default_retries = parent.default_retries + self.timeout = parent.timeout + self.ip = parent.ip + self.message, self.address = mainsock.recvfrom(1024) + self.logger = parent.logger.getChild('Client.{}'.format(self.address)) + self.logger.debug('TFTP recieving request') + self.retries = self.default_retries + self.block = 1 + self.blksize = 512 + self.sent_time = float("inf") + self.dead = False + self.fh = None + self.filename = '' + + # message from the main socket + self.handle() + + def ready(self): + '''Called when there is something to be read on our socket''' + self.message = self.sock.recv(1024) + self.handle() + + def sendBlock(self): + '''Send the next block of data, setting the timeout and retry variables + accordingly.''' + data = self.fh.read(self.blksize) + # opcode 3 == DATA, wraparound block number + response = struct.pack("!HH", 3, self.block % 65536) + response += data + self.sock.sendto(response, self.address) + self.logger.debug("Sending block %d", self.block) + self.retries -= 1 + self.sent_time = time.time() + + def noACK(self): + '''Have we timed out waiting for an ACK from the client?''' + if self.sent_time + self.timeout < time.time(): + return True + return False + + def noRetries(self): + '''Has the client ran out of retry attempts''' + if not self.retries: + return True + return False + + def validMode(self): + '''Is the file read mode octet? If not, send an error''' + mode = self.message.split(chr(0))[1] + if mode == "octet": return True + self.sendError(5, 'Mode {} not supported'.format(mode)) + return False + + def checkFile(self): + '''Does the file exist and is it a file. If not, send an error''' + filename = self.message.split(chr(0))[0] + if os.path.lexists(filename) and os.path.isfile(filename): + self.filename = filename + return True + self.sendError(1, 'File Not Found') + return False + + def parseOptions(self): + '''Extract the options sent from a client, if any, calculate the last + block based on the filesize and blocksize''' + options = self.message.split(chr(0))[2: -1] + options = dict(zip(options[0::2], map(int, options[1::2]))) + self.blksize = options.get('blksize', self.blksize) + self.lastblock = math.ceil(self.filesize / float(self.blksize)) + self.tsize = True if 'tsize' in options else False + if self.filesize > (2**16)*self.blksize: + self.logger.warning('TFTP request too big, attempting transfer anyway.') + self.logger.debug(' Details: Filesize %s is too big for blksize %s.\n', self.filesize, self.blksize) + + if len(options): + # we need to know later if we actually had any options + self.block = 0 + return True + else: + return False + + def replyOptions(self): + '''If we got sent options, we need to ack them''' + # only called if options, so send them all + response = struct.pack("!H", 6) + + response += 'blksize' + chr(0) + response += str(self.blksize) + chr(0) + response += 'tsize' + chr(0) + response += str(self.filesize) + chr(0) + + self.sock.sendto(response, self.address) + + def newRequest(self): + '''Called when we get a read request from the parent socket. Open our + own socket and check the read request. If we don't have any options, + send the first block''' + self.sock = Psocket(socket.AF_INET, socket.SOCK_DGRAM) + self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + self.sock.bind((self.ip, 0)) + # used by select() to find ready clients + self.sock.parent = self + + if not self.validMode() or not self.checkFile(): + # some clients just ACK the error (wrong code?) + # so forcefully shutdown + self.complete() + return + + self.fh = open(self.filename, 'rb') + self.filesize = os.path.getsize(self.filename) + + if not self.parseOptions(): + # no options recieved so start transfer + if self.block == 1: + self.sendBlock() + return + + # we got some options, so ack those first + self.replyOptions() + + def sendError(self, code = 1, message = "File Not Found"): + '''Send an error code and string to a client''' + response = struct.pack('!H', 5) # error opcode + response += struct.pack('!H', code) # error code + response += message + response += chr(0) + self.sock.sendto(response, self.address) + self.logger.debug("TFTP Sending '%d: %s'", code, message) + + def complete(self): + '''When we've finished sending a file, we need to close it, the + socket, and mark ourselves as dead to be cleaned up''' + self.fh.close() + self.sock.close() + self.dead = True + + def handle(self): + '''Take the message from the parent socket and act accordingly''' + #if addr not in ongoing, call this, else ready() + [opcode] = struct.unpack("!H", self.message[:2]) + if opcode == 1: + self.message = self.message[2:] + self.newRequest() + elif opcode == 4: + [block] = struct.unpack("!H", self.message[2:4]) + if block < self.block: + self.logger.warning('Ignoring duplicated ACK received for block %d', self.block) + elif block > self.block: + self.logger.warning('Ignoring out of sequence ACK received for block %d', self.block) + elif block == self.lastblock: + self.logger.debug("Completed sending %s", self.filename) + self.complete() + else: + self.block = block + 1 + self.retries = self.default_retries + self.sendBlock() + class TFTPD: ''' @@ -45,164 +215,28 @@ def __init__(self, **serverSettings): self.logger.debug('TFTP Server Port: {}'.format(self.port)) self.logger.debug(' TFTP Network Boot Directory: {}'.format(self.netbootDirectory)) - #key is (address, port) pair - self.ongoing = defaultdict(lambda: {'filename': '', 'handle': None, 'block': 1, 'blksize': 512, 'sock': None, 'sent_time': float("inf"), 'retries': self.default_retries}) + self.ongoing = [] # Start in network boot file directory and then chroot, # this simplifies target later as well as offers a slight security increase os.chdir (self.netbootDirectory) os.chroot ('.') - def filename(self, message): - ''' - The first null-delimited field - is the filename. This method returns the filename - from the message. - ''' - return message.split(chr(0))[0] - - def tftpError(self, address, code=1, message="File Not Found"): - ''' - short int 5 -> Error Opcode - This method sends the error message to the client - - Error codes from RFC1350 page 10: - Value Meaning - 0 Not defined, see error message (if any). - 1 File not found. - 2 Access violation. - 3 Disk full or allocation exceeded. - 4 Illegal TFTP operation. - 5 Unknown transfer ID. - 6 File already exists. - 7 No such user. - ''' - response = struct.pack('!H', 5) # error opcode - response += struct.pack('!H', code) # error code - response += message - response += chr(0) - self.logger.debug("TFTP Sending '{code}: {message}'".format(code = code, message = message)) - self.sock.sendto(response, address) - - def mode(self, message): - ''' - The second null-delimited field - is the transfer mode. This method returns the mode - from the message. - ''' - return message.split(chr(0))[1] - - - def sendBlock(self, address): - ''' - short int 3 -> Data Block - ''' - descriptor = self.ongoing[address] - response = struct.pack('!H', 3) #opcode 3 is DATA, also sent block number - response += struct.pack('!H', descriptor['block'] % 2 ** 16) - data = descriptor['handle'].read(descriptor['blksize']) - response += data - self.logger.debug('TFTP Sending block {block} to client {ip}:{port}'.format(block = repr(descriptor['block']), ip = address[0], port = address[1])) - descriptor['sock'].sendto(response, address) - self.ongoing[address]['retries'] -= 1 - self.ongoing[address]['sent_time'] = time.time() - if len(data) != descriptor['blksize']: - descriptor['handle'].close() - self.logger.debug('TFTP File Sent - tftp://{filename} -> {address[0]}:{address[1]}'.format(filename = descriptor['filename'], address = address)) - descriptor['sock'].close() - self.ongoing.pop(address) - - def read(self, address, message): - ''' - On RRQ OPCODE: - file exists -> reply with file - file does not exist -> reply with error - ''' - mode = self.mode(message) - if mode != 'octet': - self.logger.error("Mode '{mode}' not supported".format(mode = mode)) - self.tftpError(address, 5, 'Mode {mode} not supported'.format(mode = mode)) - return - filename = self.filename(message) - if not os.path.lexists(filename): - self.logger.debug("File '{filename}' not found, sending error message to the client".format(filename = filename) ) - self.tftpError(address, 1, 'File Not Found') - return - - self.ongoing[address]['filename'] = filename - self.ongoing[address]['handle'] = open(filename, 'r') - options = message.split(chr(0))[2: -1] - options = dict(zip(options[0::2], options[1::2])) - response = '' - if 'blksize' in options: - response += 'blksize' + chr(0) - response += options['blksize'] - response += chr(0) - self.ongoing[address]['blksize'] = int(options['blksize']) - filesize = os.path.getsize(self.ongoing[address]['filename']) - if filesize > (2**16 * self.ongoing[address]['blksize']): - self.logger.warning('TFTP request too big, attempting transfer anyway.') - self.logger.warning(' Details: Filesize {filesize} is too big for blksize {blksize}.\n'.format(filesize = filesize, blksize = self.ongoing[address]['blksize'])) - if 'tsize' in options: - response += 'tsize' + chr(0) - response += str(filesize) - response += chr(0) - - # Create the data socket only if needed - if self.ongoing[address]['sock'] == None: - socknew = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - socknew.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - socknew.bind((self.ip, 0)) - self.ongoing[address]['sock'] = socknew - - if response: - response = struct.pack('!H', 6) + response - socknew.sendto(response, address) - # if no options in request and no block sent yet start sending data - elif self.ongoing[address]['block'] == 1: - self.sendBlock(address) - else: - self.logger.warning('Ignored TFTP request: no options in a middle of a transmission') def listen(self): '''This method listens for incoming requests''' while True: - rlist, wlist, xlist = select.select([self.sock] + [self.ongoing[i]['sock'] for i in self.ongoing if self.ongoing[i]['sock']], [], [], 0) + # Remove complete clients to select doesn't fail + map(self.ongoing.remove, [client for client in self.ongoing if client.dead]) + rlist, _, _ = select.select([self.sock] + [client.sock for client in self.ongoing if not client.dead], [], [], 0) for sock in rlist: - message, address = sock.recvfrom(1024) - opcode = struct.unpack('!H', message[:2])[0] - message = message[2:] - if opcode == 1: # read the request - self.logger.debug('TFTP receiving request') - self.read(address, message) - if opcode == 4: # ack - if self.ongoing.has_key(address): - blockack = struct.unpack("!H", message[:2])[0] - if blockack < self.ongoing[address]['block']: - self.logger.warning('Ignoring duplicated ACK received for block {blockack}'.format(blockack = blockack)) - continue - if blockack > self.ongoing[address]['block']: - self.logger.warning('Ignoring out of sequence ACK received for block {blockack}'.format(blockack = blockack)) - continue - self.ongoing[address]['block'] = blockack + 1 - self.ongoing[address]['retries'] = self.default_retries - self.sendBlock(address) - - # Timeouts and Retries. Done after the above so timeout actually has a value - # Resent those that have timed out - dead_conn = [] - for i in self.ongoing: - if self.ongoing[i]['sent_time'] + self.timeout < time.time() and self.ongoing[i]['retries']: - self.logger.debug('No ACK received for block: {block}, retrying'.format(block = self.ongoing[address]['block'])) - self.ongoing[i]['handle'].seek(-self.ongoing[i]['blksize'], 1) - self.sendBlock(i) - if not self.ongoing[i]['retries']: - self.logger.debug('Max retries reached, aborting connection with {client}:{port}'.format(client = i[0], port = i[1])) - self.tftpError(i, 0, 'Timeout reached') - self.ongoing[i]['handle'].close() - self.ongoing[i]['sock'].close() - dead_conn.append(i) - - # Clean up dead connections - for i in dead_conn: - self.ongoing.remove(i) + if sock == self.sock: + # main socket, so new client + self.ongoing.append(Client(sock, self)) + else: + # client socket, so tell the client object it's ready + sock.parent.ready() + # If we haven't recieved an ACK in timeout time, retry + [client.sendBlock() for client in self.ongoing if client.noACK()] + # If we have run out of retries, kill the client. + [client.complete() for client in self.ongoing if client.noRetries()] From 0afff7bba649d1c2d332a3ce48cea625b7647cd2 Mon Sep 17 00:00:00 2001 From: PsychoMario Date: Tue, 21 Apr 2015 19:15:53 +0100 Subject: [PATCH 36/59] re-add error codes from RFC --- pypxe/tftp.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/pypxe/tftp.py b/pypxe/tftp.py index 41c4738..b51a1b8 100644 --- a/pypxe/tftp.py +++ b/pypxe/tftp.py @@ -145,7 +145,17 @@ def newRequest(self): self.replyOptions() def sendError(self, code = 1, message = "File Not Found"): - '''Send an error code and string to a client''' + '''Send an error code and string to a client + Error codes from RFC1350 page 10: + Value Meaning + 0 Not defined, see error message (if any). + 1 File not found. + 2 Access violation. + 3 Disk full or allocation exceeded. + 4 Illegal TFTP operation. + 5 Unknown transfer ID. + 6 File already exists. + 7 No such user.''' response = struct.pack('!H', 5) # error opcode response += struct.pack('!H', code) # error code response += message From b1d4a0643d4dfb3966a681d3d9bb1f20079f3d16 Mon Sep 17 00:00:00 2001 From: Michael Mattioli Date: Fri, 17 Apr 2015 02:39:38 -0400 Subject: [PATCH 37/59] update copyright to 2015 update the copyright year in the license to 2015 --- LICENSE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE.md b/LICENSE.md index 36045fb..b8fd531 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2014 PsychoMario +Copyright (c) 2015 PsychoMario Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal From e717fccf27bb84016c9b47340c3862503db5e21e Mon Sep 17 00:00:00 2001 From: Michael Mattioli Date: Tue, 21 Apr 2015 23:26:16 -0400 Subject: [PATCH 38/59] renamed JSON config file Renamed the JSON config file so there is a better understanding of what it is for right off the bat. --- example.json | 19 ------------------- example_cfg.json | 17 +++++++++++++++++ 2 files changed, 17 insertions(+), 19 deletions(-) delete mode 100644 example.json create mode 100644 example_cfg.json diff --git a/example.json b/example.json deleted file mode 100644 index 615c79f..0000000 --- a/example.json +++ /dev/null @@ -1,19 +0,0 @@ -{ -"NETBOOT_DIR":"netboot", -"NETBOOT_FILE":"", -"DHCP_SERVER_IP":"192.168.2.2", -"DHCP_SERVER_PORT":67, -"DHCP_OFFER_BEGIN":"192.168.2.100", -"DHCP_OFFER_END":"192.168.2.150", -"DHCP_SUBNET":"255.255.255.0", -"DHCP_ROUTER":"192.168.2.1", -"DHCP_DNS":"8.8.8.8", -"DHCP_BROADCAST":"", -"DHCP_FILESERVER":"192.168.2.2", -"USE_IPXE":false, -"USE_HTTP":false, -"USE_TFTP":true, -"MODE_DEBUG":false, -"USE_DHCP":false, -"DHCP_MODE_PROXY":false -} diff --git a/example_cfg.json b/example_cfg.json new file mode 100644 index 0000000..46b95f5 --- /dev/null +++ b/example_cfg.json @@ -0,0 +1,17 @@ +{ "NETBOOT_DIR" : "netboot", + "NETBOOT_FILE" : "", + "DHCP_SERVER_IP" : "192.168.2.2", + "DHCP_SERVER_PORT" : 67, + "DHCP_OFFER_BEGIN" : "192.168.2.100", + "DHCP_OFFER_END" : "192.168.2.150", + "DHCP_SUBNET" : "255.255.255.0", + "DHCP_ROUTER" : "192.168.2.1", + "DHCP_DNS" : "8.8.8.8", + "DHCP_BROADCAST" : "", + "DHCP_FILESERVER" : "192.168.2.2", + "USE_IPXE" : false, + "USE_HTTP" : false, + "USE_TFTP" : true, + "MODE_DEBUG" : false, + "USE_DHCP" : false, + "DHCP_MODE_PROXY" : false } From 614198df2c8627dfe9d256f0e2cd9c0c3e352423 Mon Sep 17 00:00:00 2001 From: Michael Mattioli Date: Tue, 21 Apr 2015 23:38:40 -0400 Subject: [PATCH 39/59] update documentation and pypxe-server Updated whitespace throughout. Updated comments throughout. Added comments throughout. Updated/fixed some double-quoted strings to single quoted strings. Updated to str.format() where applicable. --- DOCUMENTATION.md | 48 +++++++++++++-------------- README.md | 14 ++++---- pypxe-server.py | 84 +++++++++++++++++++++++++++--------------------- 3 files changed, 78 insertions(+), 68 deletions(-) diff --git a/DOCUMENTATION.md b/DOCUMENTATION.md index 7ed42da..ba3fe7c 100644 --- a/DOCUMENTATION.md +++ b/DOCUMENTATION.md @@ -1,9 +1,9 @@ -#Background ->The Preboot eXecution Environment (PXE, also known as Pre-Execution Environment; sometimes pronounced "pixie") is an environment to boot computers using a network interface independently of data storage devices (like hard disks) or installed operating systems. -[Wikipedia](https://en.wikipedia.org/wiki/Preboot_Execution_Environment) +# Background +>The Preboot eXecution Environment (PXE, also known as Pre-Execution Environment; sometimes pronounced "pixie") is an environment to boot computers using a network interface independently of data storage devices (like hard disks) or installed operating systems. -[Wikipedia](https://en.wikipedia.org/wiki/Preboot_Execution_Environment) -PXE allows computers to boot from a binary image stored on a server, rather than the local hardware. Broadly speaking, a DHCP server informs a client of the TFTP server and filename from which to boot. +PXE allows computers to boot from a binary image stored on a server, rather than the local hardware. Broadly speaking, a DHCP server informs a client of the TFTP server and filename from which to boot. -##DHCP +## DHCP In the standard DHCP mode, the server has been implemented from [RFC2131](http://www.ietf.org/rfc/rfc2131.txt), [RFC2132](http://www.ietf.org/rfc/rfc2132.txt), and the [DHCP Wikipedia Entry](https://en.wikipedia.org/wiki/Dynamic_Host_Configuration_Protocol). The DHCP server is in charge of assigning new clients with IP addresses, and informing them of the location of the TFTP server and filename they should look for. The top half of the DHCP request, as seen on the Wikipedia entry, consists of some network information, followed by 192 legacy null bytes, and then the magic cookie. @@ -20,7 +20,7 @@ Once the four way handshake is complete, the client will send a TFTP read reques By default only requests declaring the 'PXEClient' value dhcp option 60 are served, this is defined by [PXE specifications](http://www.pix.net/software/pxeboot/archive/pxespec.pdf) If you're using PyPXE as a library you can change this behavior extending the *DHCP* class and overwriting the *validateReq* method. -###ProxyDHCP +### ProxyDHCP ProxyDHCP mode is useful for when you either cannot (or do not want to) change the main DHCP server on a network. The bulk of ProxyDHCP information can be found in the [Intel PXE spec](http://www.pix.net/software/pxeboot/archive/pxespec.pdf). The main idea behind ProxyDHCP is that the main network DHCP server can hand out the IP leases while the ProxyDHCP server hands out the PXE information to each client. Therefore, slightly different information is sent in the ProxyDHCP packets. There are multiple ways to implement ProxyDHCP: broadcast, multicast, unicast or lookback. Lookback is the simplest implementation and this is what we have chosen to use. When we receive a DHCP DISCOVER from a client, we respond with a DHCP OFFER but the OFFER packet is sent without a few fields we would normally send in standard DHCP mode (this includes an offered IP address, along with any other network information such as router, DNS server(s), etc.). What we include in this OFFER packet (which isn't in a normal DHCP packet), is a vendor-class identifier of 'PXEClient' - this string identifies the packet as being relevant to PXE booting. @@ -31,18 +31,18 @@ There are a few vendor-specific options under the DHCP option 43: The client should receive two DHCP OFFER packets in ProxyDHCP mode: the first from the main DHCP server and the second from the ProxyDHCP server. Once both are received, the client will continue on with the DHCP handshake and, after it is complete, the client will boot using the settings in the DHCP OFFER from the ProxyDHCP server. -##TFTP +## TFTP We have only implemented the read OPCODE for the TFTP server, as PXE does not use write. Only *octet* transfer mode is supported. The main TFTP protocol is defined in [RFC1350](http://www.ietf.org/rfc/rfc1350.txt) -###blksize +### blksize The blksize option, as defined in [RFC2348](http://www.ietf.org/rfc/rfc2348.txt) allows the client to specify the block size for each transfer packet. The blksize option is passed along with the read opcode, following the filename and mode. The format is blksize, followed by a null byte, followed by the ASCII base-10 representation of the blksize (i.e 512 rather than 0x200), followed by another null byte. -##HTTP +## HTTP We have implemented GET and HEAD, as there is no requirement for any other methods. The referenced RFCs are [RFC2616](http://www.ietf.org/rfc/rfc2616.txt) and [RFC7230](http://www.ietf.org/rfc/rfc7230.txt). The HEAD method is used by some PXE ROMs to find the Content-Length before the GET is sent. -#PyPXE Services +# PyPXE Services The PyPXE library provies the following services for the purpose of creating a Python-based PXE environment: TFTP, HTTP, and DHCP. Each service must be imorted independently as such: * `from pypxe import tftp` or `import pypxe.tftp` imports the TFTP service @@ -51,9 +51,9 @@ The PyPXE library provies the following services for the purpose of creating a P **See [`pypxe-server.py`](pypxe-server.py) in the root of the repo for example usage on how to call, define, and setup the services.** When running any Python script that uses these classes, it should be run as a user with root privileges as they bind to interfaces and without root privileges the services will most likely fail to bind properly. -##TFTP Server `pypxe.tftp` +## TFTP Server `pypxe.tftp` -###Importing +### Importing The TFTP service can be imported _one_ of the following two ways: ```python from pypxe import tftp @@ -62,7 +62,7 @@ from pypxe import tftp import pypxe.tftp ``` -###Usage +### Usage The TFTP server class, __`TFTPD()`__, is constructed with the following __keyword arguments__: * __`ip`__ * Description: This is the IP address that the TFTP server will bind to. @@ -81,18 +81,18 @@ The TFTP server class, __`TFTPD()`__, is constructed with the following __keywor * Default: `False` * Type: bool * __`logger`__ - * Description: A [Logger](https://docs.python.org/2/library/logging.html#logger-objects) object used for logging messages, if `None` a local [StreamHandler](https://docs.python.org/2/library/logging.handlers.html#streamhandler) instance will be created + * Description: A [Logger](https://docs.python.org/2/library/logging.html#logger-objects) object used for logging messages, if `None` a local [StreamHandler](https://docs.python.org/2/library/logging.handlers.html#streamhandler) instance will be created. * Default: `None` * __`default_retries`__ - * Description: The number of data retransmissions before dropping a connection + * Description: The number of data retransmissions before dropping a connection. * Default: `3` * __`timeout`__ - * Description: The time in seconds before re-sending an un-acked data block + * Description: The time in seconds before re-sending an un-acked data block. * Default: `5` -##DHCP Server `pypxe.dhcp` +## DHCP Server `pypxe.dhcp` -###Importing +### Importing The DHCP service can be imported _one_ of the following two ways: ```python from pypxe import dhcp @@ -111,7 +111,7 @@ The DHCP server class, __`DHCPD()`__, is constructed with the following __keywor * Description: This it the port that the TFTP server will run on. * Default: `67` (default port to listen for DHCP requests) * Type: _int_ -* __`offerfrom`__ +* __`offerfrom`__ * Description: This specifies the beginning of the range of IP addreses that the DHCP server will hand out to clients. * Default: `'192.168.2.100'` * Type: _string_ @@ -160,12 +160,12 @@ The DHCP server class, __`DHCPD()`__, is constructed with the following __keywor * Default: `False` * Type: _bool_ * __`logger`__ - * Description: A [Logger](https://docs.python.org/2/library/logging.html#logger-objects) object used for logging messages, if `None` a local [StreamHandler](https://docs.python.org/2/library/logging.handlers.html#streamhandler) instance will be created + * Description: A [Logger](https://docs.python.org/2/library/logging.html#logger-objects) object used for logging messages, if `None` a local [StreamHandler](https://docs.python.org/2/library/logging.handlers.html#streamhandler) instance will be created. * Default: `None` -##HTTP Server `pypxe.http` +## HTTP Server `pypxe.http` -###Importing +### Importing The HTTP service can be imported _one_ of the following two ways: ```python from pypxe import http @@ -174,7 +174,7 @@ from pypxe import http import pypxe.http ``` -###Usage +### Usage The HTTP server class, __`HTTPD()`__, is constructed with the following __keyword arguments__: * __`ip`__ * Description: This is the IP address that the HTTP server will bind to. @@ -193,10 +193,10 @@ The HTTP server class, __`HTTPD()`__, is constructed with the following __keywor * Default: `False` * Type: bool * __`logger`__ - * Description: A [Logger](https://docs.python.org/2/library/logging.html#logger-objects) object used for logging messages, if `None` a local [StreamHandler](https://docs.python.org/2/library/logging.handlers.html#streamhandler) instance will be created + * Description: A [Logger](https://docs.python.org/2/library/logging.html#logger-objects) object used for logging messages, if `None` a local [StreamHandler](https://docs.python.org/2/library/logging.handlers.html#streamhandler) instance will be created. * Default: `None` -##Additional Information +## Additional Information * The function `chr(0)` is used in multiple places throughout the servers. This denotes a `NULL` byte, or `\x00` * Python 2.6 does not include the `argparse` module, it is included in the standard library as of 2.7 and newer. The `argparse` module is required to take in command line arguments and `pypxe-server.py` will not run without it. * The TFTP server currently does not support transfer of large files, this is a known issue (see #35). Instead of using TFTP to transfer large files (roughly 33MB or greater) it is recommended that you use the HTTP server to do so. iPXE supports direct boot from HTTP and certain kernels (once you've booted into `pxelinux.0` via TFTP) support fetching files via HTTP as well. diff --git a/README.md b/README.md index bdee69b..39b90a2 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,11 @@ -#About +# About This repository contains code that provides a working PXE server (via HTTP, TFTP, DHCP, and/or iPXE) implemented purely in Python. Currently, only Python 2.6 and newer is supported. Please read [`DOCUMENTATION.md`](DOCUMENTATION.md) for further explanation of the PyPXE project as well as recommended use. See the [issues page](https://github.com/psychomario/PyPXE/issues) for open issues, bugs, and enhancements/improvements. **DISCLAIMER:** None of thes implemented services are fully compliant with any standards or specifications. However, the true specifications and standards were followed when building PyPXE and while they work for PXE any other uses are purely coincidental. Use at your own risk. -##Usage +## Usage -###Using PyPXE as a Library +### Using PyPXE as a Library PyPXE implements the following services for the purpose of creating a Python-based PXE environment: TFTP, HTTP, and DHCP. Each PyPXE service must be imported individually. For example, to import the TFTP service simply use: ```python from pypxe import tftp @@ -16,7 +16,7 @@ import pypxe.tftp ``` For more information on how each service works and how to manipulate them, see [`DOCUMENTATION.md`](DOCUMENTATION.md). -###QuickStart +### QuickStart `pypxe-server.py` uses all three services in combination with the option of enabling/disabling them individually while also setting some options. Edit the `pypxe-server.py` settings to your preferred settings or run with `--help` or `-h` to see what command line arguments you can pass. Treat the provided `netboot` directory as `tftpboot` that you would typically see on a TFTP server, put all of your network-bootable files in there and setup your menu(s) in `netboot/pxelinux.cfg/default`. **Note:** Python 2.6 does not include the `argparse` module, it is included in the standard library as of 2.7 and newer. The `argparse` module is required to take in command line arguments and `pypxe-server.py` will not run without it. @@ -90,7 +90,7 @@ The following are arguments that can be passed to `pypxe-server.py` when running * Description: Specify DHCP lease router * Default: `192.168.2.1` * __`-d DHCP_DNS`__ or __`--dhcp-dns DHCP_DNS`__ - * Description: Specify DHCP lease DNS server + * Description: Specify DHCP lease DNS server * Default: `8.8.8.8` * __`-c DHCP_BROADCAST`__ or __`--dhcp-broadcast DHCP_BROADCAST`__ * Description: Specify DHCP broadcast address @@ -100,13 +100,13 @@ The following are arguments that can be passed to `pypxe-server.py` when running * Default: `192.168.2.2` * __File Name/Directory Arguments__ * __`-a NETBOOT_DIR`__ or __`--netboot-dir NETBOOT_DIR`__ - * Description: Specify the local directory where network boot files will be served + * Description: Specify the local directory where network boot files will be served * Default: `'netboot'` * __`-i NETBOOT_FILE`__ or __`--netboot-file NETBOOT_FILE`__ * Description: Specify the PXE boot file name * Default: _automatically set based on what services are enabled or disabled, see [`DOCUMENTATION.md`](DOCUMENTATION.md) for further explanation_ -##Notes +## Notes * `Core.iso` located in `netboot` is from the [TinyCore Project](http://distro.ibiblio.org/tinycorelinux/) and is provided as an example to network boot from using PyPXE * `chainload.kpxe` located in `netboot` is the `undionly.kpxe` from the [iPXE Project](http://ipxe.org/) * `ldlinux.c32`, `libutil.c32`, `pxelinux.0`, `menu.c32`, and `memdisk` located in `netboot` are from the [SYSLINUX Project](http://www.syslinux.org/) version [6.02](http://www.syslinux.org/wiki/index.php/Syslinux_6_Changelog#Changes_in_6.02) diff --git a/pypxe-server.py b/pypxe-server.py index db40551..e00107a 100644 --- a/pypxe-server.py +++ b/pypxe-server.py @@ -4,26 +4,27 @@ import json import logging import logging.handlers + try: import argparse except ImportError: sys.exit("ImportError: You do not have the Python 'argparse' module installed. Please install the 'argparse' module and try again.") from time import sleep -from pypxe import tftp #PyPXE TFTP service -from pypxe import dhcp #PyPXE DHCP service -from pypxe import http #PyPXE HTTP service +from pypxe import tftp # PyPXE TFTP service +from pypxe import dhcp # PyPXE DHCP service +from pypxe import http # PyPXE HTTP service -#json default +# JSON default JSON_CONFIG = '' -#Default Network Boot File Directory +# default network boot file directory NETBOOT_DIR = 'netboot' -#Default PXE Boot File +# default PXE boot file NETBOOT_FILE = '' -#DHCP Default Server Settings +# DHCP sefault server settings DHCP_SERVER_IP = '192.168.2.2' DHCP_SERVER_PORT = 67 DHCP_OFFER_BEGIN = '192.168.2.100' @@ -36,25 +37,25 @@ if __name__ == '__main__': try: - #warn the user that they are starting PyPXE as non-root user + # warn the user that they are starting PyPXE as non-root user if os.getuid() != 0: print '\nWARNING: Not root. Servers will probably fail to bind.\n' - + # - # Define Command Line Arguments + # define command line arguments # - #main service arguments + # main service arguments parser = argparse.ArgumentParser(description = 'Set options at runtime. Defaults are in %(prog)s', formatter_class = argparse.ArgumentDefaultsHelpFormatter) parser.add_argument('--ipxe', action = 'store_true', dest = 'USE_IPXE', help = 'Enable iPXE ROM', default = False) parser.add_argument('--http', action = 'store_true', dest = 'USE_HTTP', help = 'Enable built-in HTTP server', default = False) parser.add_argument('--no-tftp', action = 'store_false', dest = 'USE_TFTP', help = 'Disable built-in TFTP server, by default it is enabled', default = True) parser.add_argument('--debug', action = 'store', dest = 'MODE_DEBUG', help = 'Comma Seperated (http,tftp,dhcp). Adds verbosity to the selected services while they run. Use \'all\' for enabling debug on all services', default = '') - parser.add_argument('--config', action = 'store', dest = 'JSON_CONFIG', help = 'Configure from a json file rather than the command line', default = JSON_CONFIG) + parser.add_argument('--config', action = 'store', dest = 'JSON_CONFIG', help = 'Configure from a JSON file rather than the command line', default = JSON_CONFIG) parser.add_argument('--syslog', action = 'store', dest = 'SYSLOG_SERVER', help = 'Syslog server', default = None) parser.add_argument('--syslog-port', action = 'store', dest = 'SYSLOG_PORT', help = 'Syslog server port', default = 514) - #argument group for DHCP server + # argument group for DHCP server exclusive = parser.add_mutually_exclusive_group(required = False) exclusive.add_argument('--dhcp', action = 'store_true', dest = 'USE_DHCP', help = 'Enable built-in DHCP server', default = False) exclusive.add_argument('--dhcp-proxy', action = 'store_true', dest = 'DHCP_MODE_PROXY', help = 'Enable built-in DHCP server in proxy mode (implies --dhcp)', default = False) @@ -68,28 +69,28 @@ parser.add_argument('-c', '--dhcp-broadcast', action = 'store', dest = 'DHCP_BROADCAST', help = 'DHCP broadcast address', default = DHCP_BROADCAST) parser.add_argument('-f', '--dhcp-fileserver', action = 'store', dest = 'DHCP_FILESERVER', help = 'DHCP fileserver IP', default = DHCP_FILESERVER) - #network boot directory and file name arguments + # network boot directory and file name arguments parser.add_argument('-a', '--netboot-dir', action = 'store', dest = 'NETBOOT_DIR', help = 'Local file serve directory', default = NETBOOT_DIR) parser.add_argument('-i', '--netboot-file', action = 'store', dest = 'NETBOOT_FILE', help = 'PXE boot file name (after iPXE if --ipxe)', default = NETBOOT_FILE) - #parse the arguments given + # parse the arguments given args = parser.parse_args() if args.JSON_CONFIG: try: config = open(args.JSON_CONFIG) except IOError: - sys.exit("Failed to open %s" % args.JSON_CONFIG) + sys.exit('Failed to open {0}'.format(args.JSON_CONFIG)) try: loadedcfg = json.load(config) config.close() except ValueError: - sys.exit("%s does not contain valid json" % args.JSON_CONFIG) + sys.exit('{0} does not contain valid JSON'.format(args.JSON_CONFIG)) dargs = vars(args) dargs.update(loadedcfg) args = argparse.Namespace(**dargs) # setup main logger - sys_logger = logging.getLogger("PyPXE") + sys_logger = logging.getLogger('PyPXE') if args.SYSLOG_SERVER: handler = logging.handlers.SysLogHandler(address = (args.SYSLOG_SERVER, int(args.SYSLOG_PORT))) else: @@ -99,15 +100,15 @@ sys_logger.addHandler(handler) sys_logger.setLevel(logging.INFO) - #pass warning to user regarding starting HTTP server without iPXE + # pass warning to user regarding starting HTTP server without iPXE if args.USE_HTTP and not args.USE_IPXE and not args.USE_DHCP: sys_logger.warning('WARNING: HTTP selected but iPXE disabled. PXE ROM must support HTTP requests.') - - #if the argument was pased to enabled ProxyDHCP then enable the DHCP server + + # if the argument was pased to enabled ProxyDHCP then enable the DHCP server if args.DHCP_MODE_PROXY: args.USE_DHCP = True - #if the network boot file name was not specified in the argument, set it based on what services were enabled/disabled + # if the network boot file name was not specified in the argument, set it based on what services were enabled/disabled if args.NETBOOT_FILE == '': if not args.USE_IPXE: args.NETBOOT_FILE = 'pxelinux.0' @@ -116,31 +117,37 @@ else: args.NETBOOT_FILE = 'boot.http.ipxe' - #serve all files from one directory + # serve all files from one directory os.chdir (args.NETBOOT_DIR) - - #make a list of running threads for each service + + # make a list of running threads for each service runningServices = [] - #configure/start TFTP server + # configure/start TFTP server if args.USE_TFTP: - # setup tftp logger - tftp_logger = sys_logger.getChild("TFTP") + + # setup TFTP logger + tftp_logger = sys_logger.getChild('TFTP') sys_logger.info('Starting TFTP server...') - tftpServer = tftp.TFTPD(mode_debug = ("tftp" in args.MODE_DEBUG.lower() or "all" in args.MODE_DEBUG.lower()), logger = tftp_logger) + + # setup the thread + tftpServer = tftp.TFTPD(mode_debug = ('tftp' in args.MODE_DEBUG.lower() or 'all' in args.MODE_DEBUG.lower()), logger = tftp_logger) tftpd = threading.Thread(target = tftpServer.listen) tftpd.daemon = True tftpd.start() runningServices.append(tftpd) - #configure/start DHCP server + # configure/start DHCP server if args.USE_DHCP: - # setup dhcp logger - dhcp_logger = sys_logger.getChild("DHCP") + + # setup DHCP logger + dhcp_logger = sys_logger.getChild('DHCP') if args.DHCP_MODE_PROXY: sys_logger.info('Starting DHCP server in ProxyDHCP mode...') else: sys_logger.info('Starting DHCP server...') + + # setup the thread dhcpServer = dhcp.DHCPD( ip = args.DHCP_SERVER_IP, port = args.DHCP_SERVER_PORT, @@ -155,19 +162,22 @@ useipxe = args.USE_IPXE, usehttp = args.USE_HTTP, mode_proxy = args.DHCP_MODE_PROXY, - mode_debug = ("dhcp" in args.MODE_DEBUG.lower() or "all" in args.MODE_DEBUG.lower()), + mode_debug = ('dhcp' in args.MODE_DEBUG.lower() or 'all' in args.MODE_DEBUG.lower()), logger = dhcp_logger) dhcpd = threading.Thread(target = dhcpServer.listen) dhcpd.daemon = True dhcpd.start() runningServices.append(dhcpd) - #configure/start HTTP server + # configure/start HTTP server if args.USE_HTTP: - # setup http logger - http_logger = sys_logger.getChild("HTTP") + + # setup HTTP logger + http_logger = sys_logger.getChild('HTTP') sys_logger.info('Starting HTTP server...') - httpServer = http.HTTPD(mode_debug = ("http" in args.MODE_DEBUG.lower() or "all" in args.MODE_DEBUG.lower()), logger = http_logger) + + # setup the thread + httpServer = http.HTTPD(mode_debug = ("http" in args.MODE_DEBUG.lower() or 'all' in args.MODE_DEBUG.lower()), logger = http_logger) httpd = threading.Thread(target = httpServer.listen) httpd.daemon = True httpd.start() From a69b7bae0303f8c137c4efd647c4a4c65ec91616 Mon Sep 17 00:00:00 2001 From: PsychoMario Date: Thu, 23 Apr 2015 11:19:08 +0100 Subject: [PATCH 40/59] out of leases fix, tftp file not found fix, associated error regression --- pypxe/dhcp.py | 9 ++++++++- pypxe/tftp.py | 12 ++++++++---- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/pypxe/dhcp.py b/pypxe/dhcp.py index deb87b7..e7c916e 100644 --- a/pypxe/dhcp.py +++ b/pypxe/dhcp.py @@ -11,6 +11,9 @@ from collections import defaultdict from time import time +class OutOfLeasesError(Exception): + pass + class DHCPD: ''' This class implements a DHCP Server, limited to pxe options, @@ -111,6 +114,7 @@ def nextIP(self): for offset in xrange(tohost - fromhost): if (fromhost + offset) % 256 and fromhost + offset not in leased: return decode(fromhost + offset) + raise OutOfLeasesError("Ran out of IP addresses to lease") def tlvEncode(self, tag, value): ''' @@ -270,7 +274,10 @@ def listen(self): type = ord(self.options[53][0]) #see RFC2131 page 10 if type == 1: self.logger.debug('Received DHCPOFFER') - self.dhcpOffer(message) + try: + self.dhcpOffer(message) + except OutOfLeasesError: + self.logger.critical("Ran out of DHCP leases") elif type == 3 and address[0] == '0.0.0.0' and not self.mode_proxy: self.logger.debug('Received DHCPACK') self.dhcpAck(message) diff --git a/pypxe/tftp.py b/pypxe/tftp.py index b51a1b8..12e9371 100644 --- a/pypxe/tftp.py +++ b/pypxe/tftp.py @@ -82,7 +82,7 @@ def checkFile(self): if os.path.lexists(filename) and os.path.isfile(filename): self.filename = filename return True - self.sendError(1, 'File Not Found') + self.sendError(1, 'File Not Found', filename = filename) return False def parseOptions(self): @@ -144,7 +144,7 @@ def newRequest(self): # we got some options, so ack those first self.replyOptions() - def sendError(self, code = 1, message = "File Not Found"): + def sendError(self, code = 1, message = "File Not Found", filename = ""): '''Send an error code and string to a client Error codes from RFC1350 page 10: Value Meaning @@ -161,12 +161,16 @@ def sendError(self, code = 1, message = "File Not Found"): response += message response += chr(0) self.sock.sendto(response, self.address) - self.logger.debug("TFTP Sending '%d: %s'", code, message) + self.logger.debug("TFTP Sending '%d: %s' %s", code, message, filename) def complete(self): '''When we've finished sending a file, we need to close it, the socket, and mark ourselves as dead to be cleaned up''' - self.fh.close() + try: + self.fh.close() + except AttributeError: + #We've not opened yet, or file-not-found + pass self.sock.close() self.dead = True From 4de2c6e95b3eb6fa507b6c49d0628d80f9b604d5 Mon Sep 17 00:00:00 2001 From: Michael Mattioli Date: Thu, 7 May 2015 13:20:36 -0400 Subject: [PATCH 41/59] major styling and documentation improvements changed all variables, class names, function names, etc. in accordance with PEP8 changed all strings to single-quoted improved use of str.format() to make it compatible with Python 2.6 updated docstrings throughout for improved explanations --- pypxe-server.py | 38 +++--- pypxe/dhcp.py | 322 +++++++++++++++++++++++++----------------------- pypxe/http.py | 66 +++++----- pypxe/tftp.py | 176 ++++++++++++++------------ 4 files changed, 318 insertions(+), 284 deletions(-) diff --git a/pypxe-server.py b/pypxe-server.py index e00107a..5051629 100644 --- a/pypxe-server.py +++ b/pypxe-server.py @@ -121,7 +121,7 @@ os.chdir (args.NETBOOT_DIR) # make a list of running threads for each service - runningServices = [] + running_services = [] # configure/start TFTP server if args.USE_TFTP: @@ -131,11 +131,11 @@ sys_logger.info('Starting TFTP server...') # setup the thread - tftpServer = tftp.TFTPD(mode_debug = ('tftp' in args.MODE_DEBUG.lower() or 'all' in args.MODE_DEBUG.lower()), logger = tftp_logger) - tftpd = threading.Thread(target = tftpServer.listen) + tftp_server = tftp.TFTPD(mode_debug = ('tftp' in args.MODE_DEBUG.lower() or 'all' in args.MODE_DEBUG.lower()), logger = tftp_logger) + tftpd = threading.Thread(target = tftp_server.listen) tftpd.daemon = True tftpd.start() - runningServices.append(tftpd) + running_services.append(tftpd) # configure/start DHCP server if args.USE_DHCP: @@ -148,26 +148,26 @@ sys_logger.info('Starting DHCP server...') # setup the thread - dhcpServer = dhcp.DHCPD( + dhcp_server = dhcp.DHCPD( ip = args.DHCP_SERVER_IP, port = args.DHCP_SERVER_PORT, - offerfrom = args.DHCP_OFFER_BEGIN, - offerto = args.DHCP_OFFER_END, - subnet = args.DHCP_SUBNET, + offer_from = args.DHCP_OFFER_BEGIN, + offer_to = args.DHCP_OFFER_END, + subnet_mask = args.DHCP_SUBNET, router = args.DHCP_ROUTER, - dnsserver = args.DHCP_DNS, + dns_server = args.DHCP_DNS, broadcast = args.DHCP_BROADCAST, - fileserver = args.DHCP_FILESERVER, - filename = args.NETBOOT_FILE, - useipxe = args.USE_IPXE, - usehttp = args.USE_HTTP, + file_server = args.DHCP_FILESERVER, + file_name = args.NETBOOT_FILE, + use_ipxe = args.USE_IPXE, + use_http = args.USE_HTTP, mode_proxy = args.DHCP_MODE_PROXY, mode_debug = ('dhcp' in args.MODE_DEBUG.lower() or 'all' in args.MODE_DEBUG.lower()), logger = dhcp_logger) - dhcpd = threading.Thread(target = dhcpServer.listen) + dhcpd = threading.Thread(target = dhcp_server.listen) dhcpd.daemon = True dhcpd.start() - runningServices.append(dhcpd) + running_services.append(dhcpd) # configure/start HTTP server if args.USE_HTTP: @@ -177,15 +177,15 @@ sys_logger.info('Starting HTTP server...') # setup the thread - httpServer = http.HTTPD(mode_debug = ("http" in args.MODE_DEBUG.lower() or 'all' in args.MODE_DEBUG.lower()), logger = http_logger) - httpd = threading.Thread(target = httpServer.listen) + http_server = http.HTTPD(mode_debug = ('http' in args.MODE_DEBUG.lower() or 'all' in args.MODE_DEBUG.lower()), logger = http_logger) + httpd = threading.Thread(target = http_server.listen) httpd.daemon = True httpd.start() - runningServices.append(httpd) + running_services.append(httpd) sys_logger.info('PyPXE successfully initialized and running!') - while map(lambda x: x.isAlive(), runningServices): + while map(lambda x: x.isAlive(), running_services): sleep(1) except KeyboardInterrupt: diff --git a/pypxe/dhcp.py b/pypxe/dhcp.py index e7c916e..38d9f30 100644 --- a/pypxe/dhcp.py +++ b/pypxe/dhcp.py @@ -17,37 +17,38 @@ class OutOfLeasesError(Exception): class DHCPD: ''' This class implements a DHCP Server, limited to pxe options, - where the subnet /24 is hard coded. Implemented from RFC2131, - RFC2132, https://en.wikipedia.org/wiki/Dynamic_Host_Configuration_Protocol - and http://www.pix.net/software/pxeboot/archive/pxespec.pdf + where the subnet /24 is hard coded. + Implemented from RFC2131, RFC2132, + https://en.wikipedia.org/wiki/Dynamic_Host_Configuration_Protocol, + and http://www.pix.net/software/pxeboot/archive/pxespec.pdf. ''' - def __init__(self, **serverSettings): - - self.ip = serverSettings.get('ip', '192.168.2.2') - self.port = serverSettings.get('port', 67) - self.offerfrom = serverSettings.get('offerfrom', '192.168.2.100') - self.offerto = serverSettings.get('offerto', '192.168.2.150') - self.subnetmask = serverSettings.get('subnetmask', '255.255.255.0') - self.router = serverSettings.get('router', '192.168.2.1') - self.dnsserver = serverSettings.get('dnsserver', '8.8.8.8') - self.broadcast = serverSettings.get('broadcast', '') - self.fileserver = serverSettings.get('fileserver', '192.168.2.2') - self.filename = serverSettings.get('filename', '') - if not self.filename: - self.forcefilename = False - self.filename = "pxelinux.0" + def __init__(self, **server_settings): + + self.ip = server_settings.get('ip', '192.168.2.2') + self.port = server_settings.get('port', 67) + self.offer_from = server_settings.get('offer_from', '192.168.2.100') + self.offer_to = server_settings.get('offer_to', '192.168.2.150') + self.subnet_mask = server_settings.get('subnet_mask', '255.255.255.0') + self.router = server_settings.get('router', '192.168.2.1') + self.dns_server = server_settings.get('dns_server', '8.8.8.8') + self.broadcast = server_settings.get('broadcast', '') + self.file_server = server_settings.get('file_server', '192.168.2.2') + self.file_name = server_settings.get('file_name', '') + if not self.file_name: + self.force_file_name = False + self.file_name = "pxelinux.0" else: - self.forcefilename = True - self.ipxe = serverSettings.get('useipxe', False) - self.http = serverSettings.get('usehttp', False) - self.mode_proxy = serverSettings.get('mode_proxy', False) #ProxyDHCP mode - self.mode_debug = serverSettings.get('mode_debug', False) #debug mode - self.magic = struct.pack('!I', 0x63825363) #magic cookie - self.logger = serverSettings.get('logger', None) + self.force_file_name = True + self.ipxe = server_settings.get('use_ipxe', False) + self.http = server_settings.get('use_http', False) + self.mode_proxy = server_settings.get('mode_proxy', False) # ProxyDHCP mode + self.mode_debug = server_settings.get('mode_debug', False) # debug mode + self.magic = struct.pack('!I', 0x63825363) # magic cookie + self.logger = server_settings.get('logger', None) # setup logger if self.logger == None: - self.logger = logging.getLogger("DHCP") + self.logger = logging.getLogger('DHCP') handler = logging.StreamHandler() formatter = logging.Formatter('%(asctime)s %(name)s [%(levelname)s] %(message)s') handler.setFormatter(formatter) @@ -59,80 +60,76 @@ def __init__(self, **serverSettings): if self.http and not self.ipxe: self.logger.warning('WARNING: HTTP selected but iPXE disabled. PXE ROM must support HTTP requests.') if self.ipxe and self.http: - self.filename = 'http://{fileserver}/{filename}'.format(fileserver = self.fileserver, filename = self.filename) + self.file_name = 'http://{0}/{1}'.format(self.file_server, self.file_name) if self.ipxe and not self.http: - self.filename = 'tftp://{fileserver}/{filename}'.format(fileserver = self.fileserver, filename = self.filename) + self.file_name = 'tftp://{0}/{1}'.format(self.file_server, self.file_name) self.logger.debug('NOTICE: DHCP server started in debug mode. DHCP server is using the following:') - self.logger.debug(' DHCP Server IP: {}'.format(self.ip)) - self.logger.debug(' DHCP Server Port: {}'.format(self.port)) - self.logger.debug(' DHCP Lease Range: {} - {}'.format(self.offerfrom, self.offerto)) - self.logger.debug(' DHCP Subnet Mask: {}'.format(self.subnetmask)) - self.logger.debug(' DHCP Router: {}'.format(self.router)) - self.logger.debug(' DHCP DNS Server: {}'.format(self.dnsserver)) - self.logger.debug(' DHCP Broadcast Address: {}'.format(self.broadcast)) - self.logger.debug(' DHCP File Server IP: {}'.format(self.fileserver)) - self.logger.debug(' DHCP File Name: {}'.format(self.filename)) - self.logger.debug(' ProxyDHCP Mode: {}'.format(self.mode_proxy)) - self.logger.debug(' tUsing iPXE: {}'.format(self.ipxe)) - self.logger.debug(' Using HTTP Server: {}'.format(self.http)) + self.logger.debug('DHCP Server IP: {0}'.format(self.ip)) + self.logger.debug('DHCP Server Port: {0}'.format(self.port)) + self.logger.debug('DHCP Lease Range: {0} - {1}'.format(self.offer_from, self.offer_to)) + self.logger.debug('DHCP Subnet Mask: {0}'.format(self.subnet_mask)) + self.logger.debug('DHCP Router: {0}'.format(self.router)) + self.logger.debug('DHCP DNS Server: {0}'.format(self.dns_server)) + self.logger.debug('DHCP Broadcast Address: {0}'.format(self.broadcast)) + self.logger.debug('DHCP File Server IP: {0}'.format(self.file_server)) + self.logger.debug('DHCP File Name: {0}'.format(self.file_name)) + self.logger.debug('ProxyDHCP Mode: {0}'.format(self.mode_proxy)) + self.logger.debug('Using iPXE: {0}'.format(self.ipxe)) + self.logger.debug('Using HTTP Server: {0}'.format(self.http)) self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) self.sock.bind(('', self.port )) - - #key is mac + + # key is MAC self.leases = defaultdict(lambda: {'ip': '', 'expire': 0, 'ipxe': self.ipxe}) - def nextIP(self): + def next_ip(self): ''' This method returns the next unleased IP from range; also does lease expiry by overwrite. ''' - #if we use ints, we don't have to deal with octet overflow - #or nested loops (up to 3 with 10/8); convert both to 32bit integers - - #e.g '192.168.1.1' to 3232235777 + # if we use ints, we don't have to deal with octet overflow + # or nested loops (up to 3 with 10/8); convert both to 32-bit integers + + # e.g '192.168.1.1' to 3232235777 encode = lambda x: struct.unpack('!I', socket.inet_aton(x))[0] - - #e.g 3232235777 to '192.168.1.1' + + # e.g 3232235777 to '192.168.1.1' decode = lambda x: socket.inet_ntoa(struct.pack('!I', x)) - - fromhost = encode(self.offerfrom) - tohost = encode(self.offerto) - - #pull out already leased ips + + from_host = encode(self.offer_from) + to_host = encode(self.offer_to) + + # pull out already leased IPs leased = [self.leases[i]['ip'] for i in self.leases if self.leases[i]['expire'] > time()] - - #convert to 32bit int + + # convert to 32-bit int leased = map(encode, leased) - - #loop through, make sure not already leased and not in form X.Y.Z.0 - for offset in xrange(tohost - fromhost): - if (fromhost + offset) % 256 and fromhost + offset not in leased: - return decode(fromhost + offset) - raise OutOfLeasesError("Ran out of IP addresses to lease") - - def tlvEncode(self, tag, value): - ''' - Encode a TLV option - ''' - return struct.pack("BB", tag, len(value)) + value - def tlvParse(self, raw): - ''' - Parse a string of TLV encoded options. - ''' + # loop through, make sure not already leased and not in form X.Y.Z.0 + for offset in xrange(to_host - from_host): + if (from_host + offset) % 256 and from_host + offset not in leased: + return decode(from_host + offset) + raise OutOfLeasesError('Ran out of IP addresses to lease!') + + def tlv_encode(self, tag, value): + '''Encode a TLV option.''' + return struct.pack('BB', tag, len(value)) + value + + def tlv_parse(self, raw): + '''Parse a string of TLV-encoded options.''' ret = {} while(raw): tag = struct.unpack('B', raw[0])[0] - if tag == 0: #padding + if tag == 0: # padding raw = raw[1:] continue - if tag == 255: #end marker + if tag == 255: # end marker break length = struct.unpack('B', raw[1])[0] value = raw[2:2 + length] @@ -143,114 +140,127 @@ def tlvParse(self, raw): ret[tag] = [value] return ret - def printMAC(self, mac): + def print_mac(self, mac): ''' This method converts the MAC Address from binary to human-readable format for logging. ''' return ':'.join(map(lambda x: hex(x)[2:].zfill(2), struct.unpack('BBBBBB', mac))).upper() - def craftHeader(self, message): - '''This method crafts the DHCP header using parts of the message''' + def craft_header(self, message): + '''This method crafts the DHCP header using parts of the message.''' xid, flags, yiaddr, giaddr, chaddr = struct.unpack('!4x4s2x2s4x4s4x4s16s', message[:44]) - clientmac = chaddr[:6] - - #op, htype, hlen, hops, xid + client_mac = chaddr[:6] + + # op, htype, hlen, hops, xid response = struct.pack('!BBBB4s', 2, 1, 6, 0, xid) if not self.mode_proxy: - response += struct.pack('!HHI', 0, 0, 0) #secs, flags, ciaddr + response += struct.pack('!HHI', 0, 0, 0) # secs, flags, ciaddr else: response += struct.pack('!HHI', 0, 0x8000, 0) if not self.mode_proxy: - if self.leases[clientmac]['ip']: #OFFER - offer = self.leases[clientmac]['ip'] - else: #ACK - offer = self.nextIP() - self.leases[clientmac]['ip'] = offer - self.leases[clientmac]['expire'] = time() + 86400 - self.logger.debug('New DHCP Assignment - MAC: {MAC} -> IP: {IP}'.format(MAC = self.printMAC(clientmac), IP = self.leases[clientmac]['ip'])) - response += socket.inet_aton(offer) #yiaddr + if self.leases[client_mac]['ip']: # OFFER + offer = self.leases[client_mac]['ip'] + else: # ACK + offer = self.next_ip() + self.leases[client_mac]['ip'] = offer + self.leases[client_mac]['expire'] = time() + 86400 + self.logger.debug('New DHCP Assignment - MAC: {0} -> IP: {1}'.format(self.print_mac(client_mac), self.leases[client_mac]['ip'])) + response += socket.inet_aton(offer) # yiaddr else: response += socket.inet_aton('0.0.0.0') - response += socket.inet_aton(self.fileserver) #siaddr - response += socket.inet_aton('0.0.0.0') #giaddr - response += chaddr #chaddr - - #bootp legacy pad - response += chr(0) * 64 #server name + response += socket.inet_aton(self.file_server) # siaddr + response += socket.inet_aton('0.0.0.0') # giaddr + response += chaddr # chaddr + + # BOOTP legacy pad + response += chr(0) * 64 # server name if self.mode_proxy: - response += self.filename - response += chr(0) * (128 - len(self.filename)) + response += self.file_name + response += chr(0) * (128 - len(self.file_name)) else: response += chr(0) * 128 - response += self.magic #magic section - return (clientmac, response) + response += self.magic # magic section + return (client_mac, response) - def craftOptions(self, opt53, clientmac): - '''This method crafts the DHCP option fields + def craft_options(self, opt53, client_mac): + ''' + This method crafts the DHCP option fields opt53: 2 - DHCPOFFER 5 - DHCPACK (See RFC2132 9.6) ''' - response = self.tlvEncode(53, chr(opt53)) #message type, offer - response += self.tlvEncode(54, socket.inet_aton(self.ip)) #DHCP Server + response = self.tlv_encode(53, chr(opt53)) # message type, OFFER + response += self.tlv_encode(54, socket.inet_aton(self.ip)) # DHCP Server if not self.mode_proxy: - response += self.tlvEncode(1, socket.inet_aton(self.subnetmask)) #SubnetMask - response += self.tlvEncode(3, socket.inet_aton(self.router)) #Router - response += self.tlvEncode(51, struct.pack('!I', 86400)) #lease time - - #TFTP Server OR HTTP Server; if iPXE, need both - response += self.tlvEncode(66, self.fileserver) - - #filename null terminated - if not self.ipxe or not self.leases[clientmac]['ipxe']: - #http://www.syslinux.org/wiki/index.php/PXELINUX#UEFI - if 93 in self.options and not self.forcefilename: + response += self.tlv_encode(1, socket.inet_aton(self.subnet_mask)) # SubnetMask + response += self.tlv_encode(3, socket.inet_aton(self.router)) # Router + response += self.tlv_encode(51, struct.pack('!I', 86400)) # lease time + + # TFTP Server OR HTTP Server; if iPXE, need both + response += self.tlv_encode(66, self.file_server) + + # file_name null terminated + if not self.ipxe or not self.leases[client_mac]['ipxe']: + # http://www.syslinux.org/wiki/index.php/PXELINUX#UEFI + if 93 in self.options and not self.force_file_name: [arch] = struct.unpack("!H", self.options[93][0]) - if arch == 6: #EFI IA32 - response += self.tlvEncode(67, "syslinux.efi32" + chr(0)) - elif arch == 7: #EFI BC, x86-64 according to link above - response += self.tlvEncode(67, "syslinux.efi64" + chr(0)) - elif arch == 9: #EFI x86-64 - response += self.tlvEncode(67, "syslinux.efi64" + chr(0)) - if arch == 0: #BIOS/default - response += self.tlvEncode(67, "pxelinux.0" + chr(0)) + if arch == 6: # EFI IA32 + response += self.tlv_encode(67, "syslinux.efi32" + chr(0)) + elif arch == 7: # EFI BC, x86-64 according to link above + response += self.tlv_encode(67, "syslinux.efi64" + chr(0)) + elif arch == 9: # EFI x86-64 + response += self.tlv_encode(67, "syslinux.efi64" + chr(0)) + if arch == 0: # BIOS/default + response += self.tlv_encode(67, "pxelinux.0" + chr(0)) else: - response += self.tlvEncode(67, self.filename + chr(0)) + response += self.tlv_encode(67, self.file_name + chr(0)) else: - response += self.tlvEncode(67, '/chainload.kpxe' + chr(0)) #chainload iPXE - if opt53 == 5: #ACK - self.leases[clientmac]['ipxe'] = False + response += self.tlv_encode(67, '/chainload.kpxe' + chr(0)) # chainload iPXE + if opt53 == 5: # ACK + self.leases[client_mac]['ipxe'] = False if self.mode_proxy: - response += self.tlvEncode(60, 'PXEClient') + response += self.tlv_encode(60, 'PXEClient') response += struct.pack('!BBBBBBB4sB', 43, 10, 6, 1, 0b1000, 10, 4, chr(0) + 'PXE', 0xff) response += '\xff' return response - def dhcpOffer(self, message): - '''This method responds to DHCP discovery with offer''' - clientmac, headerResponse = self.craftHeader(message) - optionsResponse = self.craftOptions(2, clientmac) #DHCPOFFER - response = headerResponse + optionsResponse + def dhcp_offer(self, message): + '''This method responds to DHCP discovery with offer.''' + client_mac, header_response = self.craft_header(message) + options_response = self.craft_options(2, client_mac) # DHCPOFFER + response = header_response + options_response self.logger.debug('DHCPOFFER - Sending the following') - self.logger.debug(' <--BEGIN HEADER-->\n\t{headerResponse}\n\t<--END HEADER-->'.format(headerResponse = repr(headerResponse))) - self.logger.debug(' <--BEGIN OPTIONS-->\n\t{optionsResponse}\n\t<--END OPTIONS-->'.format(optionsResponse = repr(optionsResponse))) - self.logger.debug(' <--BEGIN RESPONSE-->\n\t{response}\n\t<--END RESPONSE-->'.format(response = repr(response))) + self.logger.debug('<--BEGIN HEADER-->') + self.logger.debug('{0}'.format(repr(header_response))) + self.logger.debug('<--END HEADER-->') + self.logger.debug('<--BEGIN OPTIONS-->') + self.logger.debug('{0}'.format(repr(options_response))) + self.logger.debug('<--END OPTIONS-->') + self.logger.debug('<--BEGIN RESPONSE-->') + self.logger.debug('{0}'.format(repr(response))) + self.logger.debug('<--END RESPONSE-->') self.sock.sendto(response, (self.broadcast, 68)) - def dhcpAck(self, message): - '''This method responds to DHCP request with acknowledge''' - clientmac, headerResponse = self.craftHeader(message) - optionsResponse = self.craftOptions(5, clientmac) #DHCPACK - response = headerResponse + optionsResponse + def dhcp_ack(self, message): + '''This method responds to DHCP request with acknowledge.''' + client_mac, header_response = self.craft_header(message) + options_response = self.craft_options(5, client_mac) # DHCPACK + response = header_response + options_response self.logger.debug('DHCPACK - Sending the following') - self.logger.debug(' <--BEGIN HEADER-->\n\t{headerResponse}\n\t<--END HEADER-->'.format(headerResponse = repr(headerResponse))) - self.logger.debug(' <--BEGIN OPTIONS-->\n\t{optionsResponse}\n\t<--END OPTIONS-->'.format(optionsResponse = repr(optionsResponse))) - self.logger.debug(' <--BEGIN RESPONSE-->\n\t{response}\n\t<--END RESPONSE-->'.format(response = repr(response))) + self.logger.debug('<--BEGIN HEADER-->') + self.logger.debug('{0}'.format(repr(header_response))) + self.logger.debug('<--END HEADER-->') + self.logger.debug('<--BEGIN OPTIONS-->') + self.logger.debug('{0}'.format(repr(options_response))) + self.logger.debug('<--END OPTIONS-->') + self.logger.debug('<--BEGIN RESPONSE-->') + self.logger.debug('{0}'.format(repr(response))) + self.logger.debug('<--END RESPONSE-->') self.sock.sendto(response, (self.broadcast, 68)) - def validateReq(self): + def validate_req(self): # client request is valid only if contains Vendor-Class = PXEClient if 60 in self.options and 'PXEClient' in self.options[60][0]: self.logger.debug('Valid client request received') @@ -260,27 +270,31 @@ def validateReq(self): return False def listen(self): - '''Main listen loop''' + '''Main listen loop.''' while True: message, address = self.sock.recvfrom(1024) - clientmac = struct.unpack('!28x6s', message[:34]) + client_mac = struct.unpack('!28x6s', message[:34]) self.logger.debug('Received message') - self.logger.debug(' <--BEGIN MESSAGE-->\n\t{message}\n\t<--END MESSAGE-->'.format(message = repr(message))) - self.options = self.tlvParse(message[240:]) + self.logger.debug('<--BEGIN MESSAGE-->') + self.logger.debug('{0}'.format(repr(message))) + self.logger.debug('<--END MESSAGE-->') + self.options = self.tlv_parse(message[240:]) self.logger.debug('Parsed received options') - self.logger.debug(' <--BEGIN OPTIONS-->\n\t{options}\n\t<--END OPTIONS-->'.format(options = repr(self.options))) - if not self.validateReq(): + self.logger.debug('<--BEGIN OPTIONS-->') + self.logger.debug('{0}'.format(repr(self.options))) + self.logger.debug('<--END OPTIONS-->') + if not self.validate_req(): continue - type = ord(self.options[53][0]) #see RFC2131 page 10 + type = ord(self.options[53][0]) # see RFC2131, page 10 if type == 1: self.logger.debug('Received DHCPOFFER') try: - self.dhcpOffer(message) + self.dhcp_offer(message) except OutOfLeasesError: self.logger.critical("Ran out of DHCP leases") elif type == 3 and address[0] == '0.0.0.0' and not self.mode_proxy: self.logger.debug('Received DHCPACK') - self.dhcpAck(message) + self.dhcp_ack(message) elif type == 3 and address[0] != '0.0.0.0' and self.mode_proxy: self.logger.debug('Received DHCPACK') - self.dhcpAck(message) + self.dhcp_ack(message) diff --git a/pypxe/http.py b/pypxe/http.py index 972d89c..39289e7 100644 --- a/pypxe/http.py +++ b/pypxe/http.py @@ -12,19 +12,19 @@ class HTTPD: ''' This class implements a HTTP Server, limited to GET and HEAD, - from RFC2616, RFC7230 + from RFC2616, RFC7230. ''' - def __init__(self, **serverSettings): - - self.ip = serverSettings.get('ip', '0.0.0.0') - self.port = serverSettings.get('port', 80) - self.netbootDirectory = serverSettings.get('netbootDirectory', '.') - self.mode_debug = serverSettings.get('mode_debug', False) #debug mode - self.logger = serverSettings.get('logger', None) + def __init__(self, **server_settings): + + self.ip = server_settings.get('ip', '0.0.0.0') + self.port = server_settings.get('port', 80) + self.netboot_directory = server_settings.get('netboot_directory', '.') + self.mode_debug = server_settings.get('mode_debug', False) # debug mode + self.logger = server_settings.get('logger', None) # setup logger if self.logger == None: - self.logger = logging.getLogger("HTTP") + self.logger = logging.getLogger('HTTP') handler = logging.StreamHandler() formatter = logging.Formatter('%(asctime)s %(name)s [%(levelname)s] %(message)s') handler.setFormatter(formatter) @@ -38,21 +38,23 @@ def __init__(self, **serverSettings): self.sock.bind((self.ip, self.port)) self.sock.listen(1) - # Start in network boot file directory and then chroot, + # start in network boot file directory and then chroot, # this simplifies target later as well as offers a slight security increase - os.chdir (self.netbootDirectory) + os.chdir (self.netboot_directory) os.chroot ('.') self.logger.debug('NOTICE: HTTP server started in debug mode. HTTP server is using the following:') - self.logger.debug(' HTTP Server IP: {}'.format(self.ip)) - self.logger.debug(' HTTP Server Port: {}'.format(self.port)) - self.logger.debug(' HTTP Network Boot Directory: {}'.format(self.netbootDirectory)) + self.logger.debug('HTTP Server IP: {0}'.format(self.ip)) + self.logger.debug('HTTP Server Port: {0}'.format(self.port)) + self.logger.debug('HTTP Network Boot Directory: {0}'.format(self.netboot_directory)) - def handleRequest(self, connection, addr): - '''This method handles HTTP request''' + def handle_request(self, connection, addr): + '''This method handles HTTP request.''' request = connection.recv(1024) self.logger.debug('HTTP Recieved message from {addr}'.format(addr = repr(addr))) - self.logger.debug(' <--BEGIN MESSAGE-->\n\t{request}\n\t<--END MESSAGE-->'.format(request = repr(request))) + self.logger.debug('<--BEGIN MESSAGE-->') + self.logger.debug('{0}'.format(repr(request))) + self.logger.debug('<--END MESSAGE-->') startline = request.split('\r\n')[0].split(' ') method = startline[0] target = startline[1] @@ -62,32 +64,38 @@ def handleRequest(self, connection, addr): status = '501 Not Implemented' else: status = '200 OK' - response = 'HTTP/1.1 %s\r\n' % status - if status[:3] in ('404', '501'): #fail out + response = 'HTTP/1.1 {0}\r\n'.format(status) + if status[:3] in ('404', '501'): # fail out connection.send(response) connection.close() - self.logger.debug('HTTP Sending message to {addr}'.format(addr = repr(addr))) - self.logger.debug(' <--BEING MESSAGE-->\n\t{response}\n\t<--END MESSAGE-->'.format(response = repr(response))) + self.logger.debug('HTTP Sending message to {0}'.format(repr(addr))) + self.logger.debug('<--BEING MESSAGE-->' + self.logger.debug('{0}'.format(repr(response))) + self.logger.debug('<--END MESSAGE-->') return - response += 'Content-Length: %d\r\n' % os.path.getsize(target) + response += 'Content-Length: {0}\r\n'.format(os.path.getsize(target)) response += '\r\n' if method == 'HEAD': connection.send(response) connection.close() - self.logger.debug('HTTP Sending message to {addr}'.format(addr = repr(addr))) - self.logger.debug(' <--BEING MESSAGE-->\n\t{response}\n\t<--END MESSAGE-->'.format(response = repr(response))) + self.logger.debug('HTTP Sending message to {0}'.format(repr(addr))) + self.logger.debug('<--BEING MESSAGE-->') + self.logger.debug('{0}'.format(repr(response))) + self.logger.debug('<--END MESSAGE-->') return handle = open(target) response += handle.read() handle.close() connection.send(response) connection.close() - self.logger.debug('HTTP Sending message to {addr}'.format(addr = repr(addr))) - self.logger.debug(' <--BEING MESSAGE-->\n\t{response}\n\t<--END MESSAGE-->'.format(response = repr(response))) - self.logger.debug(' HTTP File Sent - http://{target} -> {addr[0]}:{addr[1]}'.format(target = target, addr = addr)) + self.logger.debug('HTTP Sending message to {0}'.format(repr(addr))) + self.logger.debug('<--BEING MESSAGE-->' + self.logger.debug('{0}'.format(repr(response))) + self.logger.debug('<--END MESSAGE-->') + self.logger.debug('HTTP File Sent - http://{target} -> {addr[0]}:{addr[1]}'.format(target = target, addr = addr)) def listen(self): - '''This method is the main loop that listens for requests''' + '''This method is the main loop that listens for requests.''' while True: conn, addr = self.sock.accept() - self.handleRequest(conn, addr) + self.handle_request(conn, addr) diff --git a/pypxe/tftp.py b/pypxe/tftp.py index 12e9371..5337724 100644 --- a/pypxe/tftp.py +++ b/pypxe/tftp.py @@ -11,28 +11,26 @@ import time import logging import math -#from collections import defaultdict -class Psocket(socket.socket): - '''Subclassed socket.socket to enable a link back to the client object''' +class ParentSocket(socket.socket): + '''Subclassed socket.socket to enable a link-back to the client object.''' parent = None + class Client: '''Client instance for TFTPD.''' def __init__(self, mainsock, parent): - # main socket recieve `message` - # self.ongoing.append(Client(message, retries, timeout)) - # select from main socket + [x.sock for x in self.ongoing] + self.default_retries = parent.default_retries self.timeout = parent.timeout self.ip = parent.ip self.message, self.address = mainsock.recvfrom(1024) - self.logger = parent.logger.getChild('Client.{}'.format(self.address)) - self.logger.debug('TFTP recieving request') + self.logger = parent.logger.getChild('Client.{0}'.format(self.address)) + self.logger.debug('TFTP recieving request...') self.retries = self.default_retries self.block = 1 self.blksize = 512 - self.sent_time = float("inf") + self.sent_time = float('inf') self.dead = False self.fh = None self.filename = '' @@ -41,43 +39,48 @@ def __init__(self, mainsock, parent): self.handle() def ready(self): - '''Called when there is something to be read on our socket''' + '''Called when there is something to be read on our socket.''' self.message = self.sock.recv(1024) self.handle() - def sendBlock(self): - '''Send the next block of data, setting the timeout and retry variables - accordingly.''' + def send_block(self): + ''' + Sends the next block of data, setting the timeout and retry + variables accordingly. + ''' data = self.fh.read(self.blksize) # opcode 3 == DATA, wraparound block number - response = struct.pack("!HH", 3, self.block % 65536) + response = struct.pack('!HH', 3, self.block % 65536) response += data self.sock.sendto(response, self.address) - self.logger.debug("Sending block %d", self.block) + self.logger.debug('Sending block {0}'.format(self.block)) self.retries -= 1 self.sent_time = time.time() - def noACK(self): - '''Have we timed out waiting for an ACK from the client?''' + def no_ack(self): + '''Determines if we timed out waiting for an ACK from the client.''' if self.sent_time + self.timeout < time.time(): return True return False - def noRetries(self): - '''Has the client ran out of retry attempts''' + def no_retries(self): + '''Determines if the client ran out of retry attempts.''' if not self.retries: return True return False - def validMode(self): - '''Is the file read mode octet? If not, send an error''' + def valid_mode(self): + '''Determines if the file read mode octet; if not, send an error.''' mode = self.message.split(chr(0))[1] - if mode == "octet": return True - self.sendError(5, 'Mode {} not supported'.format(mode)) + if mode == 'octet': return True + self.sendError(5, 'Mode {0} not supported'.format(mode)) return False - def checkFile(self): - '''Does the file exist and is it a file. If not, send an error''' + def check_file(self): + ''' + Determines if the file exist and if it is a file; if not, + send an error. + ''' filename = self.message.split(chr(0))[0] if os.path.lexists(filename) and os.path.isfile(filename): self.filename = filename @@ -85,9 +88,11 @@ def checkFile(self): self.sendError(1, 'File Not Found', filename = filename) return False - def parseOptions(self): - '''Extract the options sent from a client, if any, calculate the last - block based on the filesize and blocksize''' + def parse_options(self): + ''' + Extracts the options sent from a client; if any, calculates the last + block based on the filesize and blocksize. + ''' options = self.message.split(chr(0))[2: -1] options = dict(zip(options[0::2], map(int, options[1::2]))) self.blksize = options.get('blksize', self.blksize) @@ -95,7 +100,7 @@ def parseOptions(self): self.tsize = True if 'tsize' in options else False if self.filesize > (2**16)*self.blksize: self.logger.warning('TFTP request too big, attempting transfer anyway.') - self.logger.debug(' Details: Filesize %s is too big for blksize %s.\n', self.filesize, self.blksize) + self.logger.debug('Details: Filesize {0} is too big for blksize {1}.'.format(self.filesize, self.blksize)) if len(options): # we need to know later if we actually had any options @@ -104,8 +109,8 @@ def parseOptions(self): else: return False - def replyOptions(self): - '''If we got sent options, we need to ack them''' + def reply_options(self): + '''Acknowledges any options received.''' # only called if options, so send them all response = struct.pack("!H", 6) @@ -117,16 +122,18 @@ def replyOptions(self): self.sock.sendto(response, self.address) def newRequest(self): - '''Called when we get a read request from the parent socket. Open our - own socket and check the read request. If we don't have any options, - send the first block''' - self.sock = Psocket(socket.AF_INET, socket.SOCK_DGRAM) + ''' + When receiving a read request from the parent socket, open our + own socket and check the read request; if we don't have any options, + send the first block. + ''' + self.sock = ParentSocket(socket.AF_INET, socket.SOCK_DGRAM) self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) self.sock.bind((self.ip, 0)) # used by select() to find ready clients self.sock.parent = self - if not self.validMode() or not self.checkFile(): + if not self.valid_mode() or not self.check_file(): # some clients just ACK the error (wrong code?) # so forcefully shutdown self.complete() @@ -135,48 +142,53 @@ def newRequest(self): self.fh = open(self.filename, 'rb') self.filesize = os.path.getsize(self.filename) - if not self.parseOptions(): + if not self.parse_options(): # no options recieved so start transfer if self.block == 1: - self.sendBlock() + self.send_block() return # we got some options, so ack those first - self.replyOptions() - - def sendError(self, code = 1, message = "File Not Found", filename = ""): - '''Send an error code and string to a client - Error codes from RFC1350 page 10: - Value Meaning - 0 Not defined, see error message (if any). - 1 File not found. - 2 Access violation. - 3 Disk full or allocation exceeded. - 4 Illegal TFTP operation. - 5 Unknown transfer ID. - 6 File already exists. - 7 No such user.''' + self.reply_options() + + def sendError(self, code = 1, message = 'File Not Found', filename = ''): + ''' + Sends an error code and string to a client. + + Error codes from RFC1350 page 10: + Value Meaning + 0 Not defined, see error message (if any). + 1 File not found. + 2 Access violation. + 3 Disk full or allocation exceeded. + 4 Illegal TFTP operation. + 5 Unknown transfer ID. + 6 File already exists. + 7 No such user. + ''' response = struct.pack('!H', 5) # error opcode response += struct.pack('!H', code) # error code response += message response += chr(0) self.sock.sendto(response, self.address) - self.logger.debug("TFTP Sending '%d: %s' %s", code, message, filename) + self.logger.debug('TFTP Sending {0}: {1} {2}'.format(code, message, filename)) def complete(self): - '''When we've finished sending a file, we need to close it, the - socket, and mark ourselves as dead to be cleaned up''' + ''' + Closes a file and socket after sending it + and marks ourselves as dead to be cleaned up. + ''' try: self.fh.close() except AttributeError: - #We've not opened yet, or file-not-found + # we've not opened yet, or file-not-found pass self.sock.close() self.dead = True def handle(self): - '''Take the message from the parent socket and act accordingly''' - #if addr not in ongoing, call this, else ready() + '''Takes the message from the parent socket and act accordingly.''' + # if addr not in ongoing, call this, else ready() [opcode] = struct.unpack("!H", self.message[:2]) if opcode == 1: self.message = self.message[2:] @@ -184,16 +196,16 @@ def handle(self): elif opcode == 4: [block] = struct.unpack("!H", self.message[2:4]) if block < self.block: - self.logger.warning('Ignoring duplicated ACK received for block %d', self.block) + self.logger.warning('Ignoring duplicated ACK received for block {0}'.format(self.block)) elif block > self.block: - self.logger.warning('Ignoring out of sequence ACK received for block %d', self.block) + self.logger.warning('Ignoring out of sequence ACK received for block {0}'.format(self.block)) elif block == self.lastblock: - self.logger.debug("Completed sending %s", self.filename) + self.logger.debug('Completed sending {0}'.format(self.filename)) self.complete() else: self.block = block + 1 self.retries = self.default_retries - self.sendBlock() + self.send_block() class TFTPD: @@ -201,21 +213,21 @@ class TFTPD: This class implements a read-only TFTP server implemented from RFC1350 and RFC2348 ''' - def __init__(self, **serverSettings): - self.ip = serverSettings.get('ip', '0.0.0.0') - self.port = serverSettings.get('port', 69) - self.netbootDirectory = serverSettings.get('netbootDirectory', '.') - self.mode_debug = serverSettings.get('mode_debug', False) #debug mode - self.logger = serverSettings.get('logger', None) - self.default_retries = serverSettings.get('default_retries', 3) - self.timeout = serverSettings.get('timeout', 5) + def __init__(self, **server_settings): + self.ip = server_settings.get('ip', '0.0.0.0') + self.port = server_settings.get('port', 69) + self.netbook_directory = server_settings.get('netbook_directory', '.') + self.mode_debug = server_settings.get('mode_debug', False) # debug mode + self.logger = server_settings.get('logger', None) + self.default_retries = server_settings.get('default_retries', 3) + self.timeout = server_settings.get('timeout', 5) self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) self.sock.bind((self.ip, self.port)) # setup logger if self.logger == None: - self.logger = logging.getLogger("TFTP") + self.logger = logging.getLogger('TFTP') handler = logging.StreamHandler() formatter = logging.Formatter('%(asctime)s %(name)s %(levelname)s %(message)s') handler.setFormatter(formatter) @@ -225,22 +237,22 @@ def __init__(self, **serverSettings): self.logger.setLevel(logging.DEBUG) self.logger.debug('NOTICE: TFTP server started in debug mode. TFTP server is using the following:') - self.logger.debug(' TFTP Server IP: {}'.format(self.ip)) - self.logger.debug('TFTP Server Port: {}'.format(self.port)) - self.logger.debug(' TFTP Network Boot Directory: {}'.format(self.netbootDirectory)) + self.logger.debug('TFTP Server IP: {0}'.format(self.ip)) + self.logger.debug('TFTP Server Port: {0}'.format(self.port)) + self.logger.debug('TFTP Network Boot Directory: {0}'.format(self.netbook_directory)) self.ongoing = [] - # Start in network boot file directory and then chroot, + # start in network boot file directory and then chroot, # this simplifies target later as well as offers a slight security increase - os.chdir (self.netbootDirectory) + os.chdir (self.netbook_directory) os.chroot ('.') def listen(self): - '''This method listens for incoming requests''' + '''This method listens for incoming requests.''' while True: - # Remove complete clients to select doesn't fail + # remove complete clients to select doesn't fail map(self.ongoing.remove, [client for client in self.ongoing if client.dead]) rlist, _, _ = select.select([self.sock] + [client.sock for client in self.ongoing if not client.dead], [], [], 0) for sock in rlist: @@ -250,7 +262,7 @@ def listen(self): else: # client socket, so tell the client object it's ready sock.parent.ready() - # If we haven't recieved an ACK in timeout time, retry - [client.sendBlock() for client in self.ongoing if client.noACK()] - # If we have run out of retries, kill the client. - [client.complete() for client in self.ongoing if client.noRetries()] + # if we haven't recieved an ACK in timeout time, retry + [client.send_block() for client in self.ongoing if client.no_ack()] + # if we have run out of retries, kill the client. + [client.complete() for client in self.ongoing if client.no_retries()] From 3c5d2cb3db4be1218e5c0d5b2195e1b6f2c09725 Mon Sep 17 00:00:00 2001 From: Michael Mattioli Date: Thu, 7 May 2015 21:06:55 -0400 Subject: [PATCH 42/59] minor bug fixes This commit contains bug fixes and logging improvements as per #76 --- pypxe-server.py | 6 +++--- pypxe/dhcp.py | 40 +++++++++++++++++++++------------------- pypxe/http.py | 18 ++++++++---------- pypxe/tftp.py | 6 +++--- 4 files changed, 35 insertions(+), 35 deletions(-) diff --git a/pypxe-server.py b/pypxe-server.py index 5051629..7d37931 100644 --- a/pypxe-server.py +++ b/pypxe-server.py @@ -77,7 +77,7 @@ args = parser.parse_args() if args.JSON_CONFIG: try: - config = open(args.JSON_CONFIG) + config = open(args.JSON_CONFIG, 'rb') except IOError: sys.exit('Failed to open {0}'.format(args.JSON_CONFIG)) try: @@ -95,14 +95,14 @@ handler = logging.handlers.SysLogHandler(address = (args.SYSLOG_SERVER, int(args.SYSLOG_PORT))) else: handler = logging.StreamHandler() - formatter = logging.Formatter('%(asctime)s %(name)s [%(levelname)s] %(message)s') + formatter = logging.Formatter('%(asctime)s [%(levelname)s] %(name)s %(message)s') handler.setFormatter(formatter) sys_logger.addHandler(handler) sys_logger.setLevel(logging.INFO) # pass warning to user regarding starting HTTP server without iPXE if args.USE_HTTP and not args.USE_IPXE and not args.USE_DHCP: - sys_logger.warning('WARNING: HTTP selected but iPXE disabled. PXE ROM must support HTTP requests.') + sys_logger.warning('HTTP selected but iPXE disabled. PXE ROM must support HTTP requests.') # if the argument was pased to enabled ProxyDHCP then enable the DHCP server if args.DHCP_MODE_PROXY: diff --git a/pypxe/dhcp.py b/pypxe/dhcp.py index 38d9f30..427c6b9 100644 --- a/pypxe/dhcp.py +++ b/pypxe/dhcp.py @@ -16,8 +16,7 @@ class OutOfLeasesError(Exception): class DHCPD: ''' - This class implements a DHCP Server, limited to pxe options, - where the subnet /24 is hard coded. + This class implements a DHCP Server, limited to PXE options. Implemented from RFC2131, RFC2132, https://en.wikipedia.org/wiki/Dynamic_Host_Configuration_Protocol, and http://www.pix.net/software/pxeboot/archive/pxespec.pdf. @@ -50,7 +49,7 @@ def __init__(self, **server_settings): if self.logger == None: self.logger = logging.getLogger('DHCP') handler = logging.StreamHandler() - formatter = logging.Formatter('%(asctime)s %(name)s [%(levelname)s] %(message)s') + formatter = logging.Formatter('%(asctime)s [%(levelname)s] %(name)s %(message)s') handler.setFormatter(formatter) self.logger.addHandler(handler) @@ -58,7 +57,7 @@ def __init__(self, **server_settings): self.logger.setLevel(logging.DEBUG) if self.http and not self.ipxe: - self.logger.warning('WARNING: HTTP selected but iPXE disabled. PXE ROM must support HTTP requests.') + self.logger.warning('HTTP selected but iPXE disabled. PXE ROM must support HTTP requests.') if self.ipxe and self.http: self.file_name = 'http://{0}/{1}'.format(self.file_server, self.file_name) if self.ipxe and not self.http: @@ -67,11 +66,14 @@ def __init__(self, **server_settings): self.logger.debug('NOTICE: DHCP server started in debug mode. DHCP server is using the following:') self.logger.debug('DHCP Server IP: {0}'.format(self.ip)) self.logger.debug('DHCP Server Port: {0}'.format(self.port)) - self.logger.debug('DHCP Lease Range: {0} - {1}'.format(self.offer_from, self.offer_to)) - self.logger.debug('DHCP Subnet Mask: {0}'.format(self.subnet_mask)) - self.logger.debug('DHCP Router: {0}'.format(self.router)) - self.logger.debug('DHCP DNS Server: {0}'.format(self.dns_server)) - self.logger.debug('DHCP Broadcast Address: {0}'.format(self.broadcast)) + + if not self.mod_proxy: + self.logger.debug('DHCP Lease Range: {0} - {1}'.format(self.offer_from, self.offer_to)) + self.logger.debug('DHCP Subnet Mask: {0}'.format(self.subnet_mask)) + self.logger.debug('DHCP Router: {0}'.format(self.router)) + self.logger.debug('DHCP DNS Server: {0}'.format(self.dns_server)) + self.logger.debug('DHCP Broadcast Address: {0}'.format(self.broadcast)) + self.logger.debug('DHCP File Server IP: {0}'.format(self.file_server)) self.logger.debug('DHCP File Name: {0}'.format(self.file_name)) self.logger.debug('ProxyDHCP Mode: {0}'.format(self.mode_proxy)) @@ -125,13 +127,13 @@ def tlv_parse(self, raw): '''Parse a string of TLV-encoded options.''' ret = {} while(raw): - tag = struct.unpack('B', raw[0])[0] + [tag] = struct.unpack('B', raw[0]) if tag == 0: # padding raw = raw[1:] continue if tag == 255: # end marker break - length = struct.unpack('B', raw[1])[0] + [length] = struct.unpack('B', raw[1]) value = raw[2:2 + length] raw = raw[2 + length:] if tag in ret: @@ -165,7 +167,7 @@ def craft_header(self, message): offer = self.next_ip() self.leases[client_mac]['ip'] = offer self.leases[client_mac]['expire'] = time() + 86400 - self.logger.debug('New DHCP Assignment - MAC: {0} -> IP: {1}'.format(self.print_mac(client_mac), self.leases[client_mac]['ip'])) + self.logger.debug('New Assignment - MAC: {0} -> IP: {1}'.format(self.print_mac(client_mac), self.leases[client_mac]['ip'])) response += socket.inet_aton(offer) # yiaddr else: response += socket.inet_aton('0.0.0.0') @@ -206,18 +208,18 @@ def craft_options(self, opt53, client_mac): # http://www.syslinux.org/wiki/index.php/PXELINUX#UEFI if 93 in self.options and not self.force_file_name: [arch] = struct.unpack("!H", self.options[93][0]) - if arch == 6: # EFI IA32 + if arch == 0: # BIOS/default + response += self.tlv_encode(67, "pxelinux.0" + chr(0)) + elif arch == 6: # EFI IA32 response += self.tlv_encode(67, "syslinux.efi32" + chr(0)) elif arch == 7: # EFI BC, x86-64 according to link above response += self.tlv_encode(67, "syslinux.efi64" + chr(0)) elif arch == 9: # EFI x86-64 response += self.tlv_encode(67, "syslinux.efi64" + chr(0)) - if arch == 0: # BIOS/default - response += self.tlv_encode(67, "pxelinux.0" + chr(0)) else: response += self.tlv_encode(67, self.file_name + chr(0)) else: - response += self.tlv_encode(67, '/chainload.kpxe' + chr(0)) # chainload iPXE + response += self.tlv_encode(67, 'chainload.kpxe' + chr(0)) # chainload iPXE if opt53 == 5: # ACK self.leases[client_mac]['ipxe'] = False if self.mode_proxy: @@ -263,10 +265,10 @@ def dhcp_ack(self, message): def validate_req(self): # client request is valid only if contains Vendor-Class = PXEClient if 60 in self.options and 'PXEClient' in self.options[60][0]: - self.logger.debug('Valid client request received') + self.logger.debug('PXE client request received') return True if self.mode_debug: - self.logger.debug('Invalid client request received') + self.logger.debug('Non-PXE client request received') return False def listen(self): @@ -291,7 +293,7 @@ def listen(self): try: self.dhcp_offer(message) except OutOfLeasesError: - self.logger.critical("Ran out of DHCP leases") + self.logger.critical("Ran out of leases") elif type == 3 and address[0] == '0.0.0.0' and not self.mode_proxy: self.logger.debug('Received DHCPACK') self.dhcp_ack(message) diff --git a/pypxe/http.py b/pypxe/http.py index 39289e7..fcd049c 100644 --- a/pypxe/http.py +++ b/pypxe/http.py @@ -26,7 +26,7 @@ def __init__(self, **server_settings): if self.logger == None: self.logger = logging.getLogger('HTTP') handler = logging.StreamHandler() - formatter = logging.Formatter('%(asctime)s %(name)s [%(levelname)s] %(message)s') + formatter = logging.Formatter('%(asctime)s [%(levelname)s] %(name)s %(message)s') handler.setFormatter(formatter) self.logger.addHandler(handler) @@ -51,13 +51,11 @@ def __init__(self, **server_settings): def handle_request(self, connection, addr): '''This method handles HTTP request.''' request = connection.recv(1024) - self.logger.debug('HTTP Recieved message from {addr}'.format(addr = repr(addr))) + self.logger.debug('Recieved message from {addr}'.format(addr = repr(addr))) self.logger.debug('<--BEGIN MESSAGE-->') self.logger.debug('{0}'.format(repr(request))) self.logger.debug('<--END MESSAGE-->') - startline = request.split('\r\n')[0].split(' ') - method = startline[0] - target = startline[1] + method, target, version = request.split('\r\n')[0].split(' ') if not os.path.lexists(target) or not os.path.isfile(target): status = '404 Not Found' elif method not in ('GET', 'HEAD'): @@ -68,7 +66,7 @@ def handle_request(self, connection, addr): if status[:3] in ('404', '501'): # fail out connection.send(response) connection.close() - self.logger.debug('HTTP Sending message to {0}'.format(repr(addr))) + self.logger.debug('Sending message to {0}'.format(repr(addr))) self.logger.debug('<--BEING MESSAGE-->' self.logger.debug('{0}'.format(repr(response))) self.logger.debug('<--END MESSAGE-->') @@ -78,21 +76,21 @@ def handle_request(self, connection, addr): if method == 'HEAD': connection.send(response) connection.close() - self.logger.debug('HTTP Sending message to {0}'.format(repr(addr))) + self.logger.debug('Sending message to {0}'.format(repr(addr))) self.logger.debug('<--BEING MESSAGE-->') self.logger.debug('{0}'.format(repr(response))) self.logger.debug('<--END MESSAGE-->') return - handle = open(target) + handle = open(target, 'rb') response += handle.read() handle.close() connection.send(response) connection.close() - self.logger.debug('HTTP Sending message to {0}'.format(repr(addr))) + self.logger.debug('Sending message to {0}'.format(repr(addr))) self.logger.debug('<--BEING MESSAGE-->' self.logger.debug('{0}'.format(repr(response))) self.logger.debug('<--END MESSAGE-->') - self.logger.debug('HTTP File Sent - http://{target} -> {addr[0]}:{addr[1]}'.format(target = target, addr = addr)) + self.logger.debug('File Sent - http://{target} -> {addr[0]}:{addr[1]}'.format(target = target, addr = addr)) def listen(self): '''This method is the main loop that listens for requests.''' diff --git a/pypxe/tftp.py b/pypxe/tftp.py index 5337724..d2cf460 100644 --- a/pypxe/tftp.py +++ b/pypxe/tftp.py @@ -26,7 +26,7 @@ def __init__(self, mainsock, parent): self.ip = parent.ip self.message, self.address = mainsock.recvfrom(1024) self.logger = parent.logger.getChild('Client.{0}'.format(self.address)) - self.logger.debug('TFTP recieving request...') + self.logger.debug('Recieving request...') self.retries = self.default_retries self.block = 1 self.blksize = 512 @@ -99,7 +99,7 @@ def parse_options(self): self.lastblock = math.ceil(self.filesize / float(self.blksize)) self.tsize = True if 'tsize' in options else False if self.filesize > (2**16)*self.blksize: - self.logger.warning('TFTP request too big, attempting transfer anyway.') + self.logger.warning('Request too big, attempting transfer anyway.') self.logger.debug('Details: Filesize {0} is too big for blksize {1}.'.format(self.filesize, self.blksize)) if len(options): @@ -229,7 +229,7 @@ def __init__(self, **server_settings): if self.logger == None: self.logger = logging.getLogger('TFTP') handler = logging.StreamHandler() - formatter = logging.Formatter('%(asctime)s %(name)s %(levelname)s %(message)s') + formatter = logging.Formatter('%(asctime)s %(levelname)s %(name)s %(message)s') handler.setFormatter(formatter) self.logger.addHandler(handler) From 2f19887cf389083331c5f29d054f911945420be6 Mon Sep 17 00:00:00 2001 From: Michael Mattioli Date: Thu, 7 May 2015 21:22:56 -0400 Subject: [PATCH 43/59] minor typo fixes, string improvements, and comment improvements MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixed some minor typos Re-worded some comments for clearer meaning Changed a few more double-quoted strings to single-quoted strings that weren’t caught earlier --- pypxe-server.py | 11 ++++------- pypxe/dhcp.py | 8 ++++---- pypxe/tftp.py | 33 +++++++++++++++++---------------- 3 files changed, 25 insertions(+), 27 deletions(-) diff --git a/pypxe-server.py b/pypxe-server.py index 7d37931..7833ee3 100644 --- a/pypxe-server.py +++ b/pypxe-server.py @@ -24,7 +24,7 @@ # default PXE boot file NETBOOT_FILE = '' -# DHCP sefault server settings +# DHCP default server settings DHCP_SERVER_IP = '192.168.2.2' DHCP_SERVER_PORT = 67 DHCP_OFFER_BEGIN = '192.168.2.100' @@ -41,10 +41,6 @@ if os.getuid() != 0: print '\nWARNING: Not root. Servers will probably fail to bind.\n' - # - # define command line arguments - # - # main service arguments parser = argparse.ArgumentParser(description = 'Set options at runtime. Defaults are in %(prog)s', formatter_class = argparse.ArgumentDefaultsHelpFormatter) parser.add_argument('--ipxe', action = 'store_true', dest = 'USE_IPXE', help = 'Enable iPXE ROM', default = False) @@ -55,7 +51,7 @@ parser.add_argument('--syslog', action = 'store', dest = 'SYSLOG_SERVER', help = 'Syslog server', default = None) parser.add_argument('--syslog-port', action = 'store', dest = 'SYSLOG_PORT', help = 'Syslog server port', default = 514) - # argument group for DHCP server + # DHCP server arguments exclusive = parser.add_mutually_exclusive_group(required = False) exclusive.add_argument('--dhcp', action = 'store_true', dest = 'USE_DHCP', help = 'Enable built-in DHCP server', default = False) exclusive.add_argument('--dhcp-proxy', action = 'store_true', dest = 'DHCP_MODE_PROXY', help = 'Enable built-in DHCP server in proxy mode (implies --dhcp)', default = False) @@ -108,7 +104,8 @@ if args.DHCP_MODE_PROXY: args.USE_DHCP = True - # if the network boot file name was not specified in the argument, set it based on what services were enabled/disabled + # if the network boot file name was not specified in the argument, + # set it based on what services were enabled/disabled if args.NETBOOT_FILE == '': if not args.USE_IPXE: args.NETBOOT_FILE = 'pxelinux.0' diff --git a/pypxe/dhcp.py b/pypxe/dhcp.py index 427c6b9..fbf954a 100644 --- a/pypxe/dhcp.py +++ b/pypxe/dhcp.py @@ -35,7 +35,7 @@ def __init__(self, **server_settings): self.file_name = server_settings.get('file_name', '') if not self.file_name: self.force_file_name = False - self.file_name = "pxelinux.0" + self.file_name = 'pxelinux.0' else: self.force_file_name = True self.ipxe = server_settings.get('use_ipxe', False) @@ -52,7 +52,6 @@ def __init__(self, **server_settings): formatter = logging.Formatter('%(asctime)s [%(levelname)s] %(name)s %(message)s') handler.setFormatter(formatter) self.logger.addHandler(handler) - if self.mode_debug: self.logger.setLevel(logging.DEBUG) @@ -67,7 +66,8 @@ def __init__(self, **server_settings): self.logger.debug('DHCP Server IP: {0}'.format(self.ip)) self.logger.debug('DHCP Server Port: {0}'.format(self.port)) - if not self.mod_proxy: + # debug info for ProxyDHCP mode + if not self.mode_proxy: self.logger.debug('DHCP Lease Range: {0} - {1}'.format(self.offer_from, self.offer_to)) self.logger.debug('DHCP Subnet Mask: {0}'.format(self.subnet_mask)) self.logger.debug('DHCP Router: {0}'.format(self.router)) @@ -196,7 +196,7 @@ def craft_options(self, opt53, client_mac): response = self.tlv_encode(53, chr(opt53)) # message type, OFFER response += self.tlv_encode(54, socket.inet_aton(self.ip)) # DHCP Server if not self.mode_proxy: - response += self.tlv_encode(1, socket.inet_aton(self.subnet_mask)) # SubnetMask + response += self.tlv_encode(1, socket.inet_aton(self.subnet_mask)) # Subnet Mask response += self.tlv_encode(3, socket.inet_aton(self.router)) # Router response += self.tlv_encode(51, struct.pack('!I', 86400)) # lease time diff --git a/pypxe/tftp.py b/pypxe/tftp.py index d2cf460..c590e7e 100644 --- a/pypxe/tftp.py +++ b/pypxe/tftp.py @@ -153,18 +153,19 @@ def newRequest(self): def sendError(self, code = 1, message = 'File Not Found', filename = ''): ''' - Sends an error code and string to a client. - - Error codes from RFC1350 page 10: - Value Meaning - 0 Not defined, see error message (if any). - 1 File not found. - 2 Access violation. - 3 Disk full or allocation exceeded. - 4 Illegal TFTP operation. - 5 Unknown transfer ID. - 6 File already exists. - 7 No such user. + Sends an error code and string to a client. See RFC1350, page 10 for + details. + + Value Meaning + ===== ======= + 0 Not defined, see error message (if any). + 1 File not found. + 2 Access violation. + 3 Disk full or allocation exceeded. + 4 Illegal TFTP operation. + 5 Unknown transfer ID. + 6 File already exists. + 7 No such user. ''' response = struct.pack('!H', 5) # error opcode response += struct.pack('!H', code) # error code @@ -181,7 +182,7 @@ def complete(self): try: self.fh.close() except AttributeError: - # we've not opened yet, or file-not-found + # we have not opened yet, or file-not-found pass self.sock.close() self.dead = True @@ -189,12 +190,12 @@ def complete(self): def handle(self): '''Takes the message from the parent socket and act accordingly.''' # if addr not in ongoing, call this, else ready() - [opcode] = struct.unpack("!H", self.message[:2]) + [opcode] = struct.unpack('!H', self.message[:2]) if opcode == 1: self.message = self.message[2:] self.newRequest() elif opcode == 4: - [block] = struct.unpack("!H", self.message[2:4]) + [block] = struct.unpack('!H', self.message[2:4]) if block < self.block: self.logger.warning('Ignoring duplicated ACK received for block {0}'.format(self.block)) elif block > self.block: @@ -264,5 +265,5 @@ def listen(self): sock.parent.ready() # if we haven't recieved an ACK in timeout time, retry [client.send_block() for client in self.ongoing if client.no_ack()] - # if we have run out of retries, kill the client. + # if we have run out of retries, kill the client [client.complete() for client in self.ongoing if client.no_retries()] From aaffb34517baf1b27d99a3364350000c74e81fe6 Mon Sep 17 00:00:00 2001 From: Michael Mattioli Date: Thu, 7 May 2015 21:37:29 -0400 Subject: [PATCH 44/59] fixed missing parens and changed variable name changed a variable name in accordance with PEP8 Added two missing parens which caused syntax errors --- pypxe-server.py | 4 ++-- pypxe/http.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pypxe-server.py b/pypxe-server.py index 7833ee3..2a2727f 100644 --- a/pypxe-server.py +++ b/pypxe-server.py @@ -77,12 +77,12 @@ except IOError: sys.exit('Failed to open {0}'.format(args.JSON_CONFIG)) try: - loadedcfg = json.load(config) + loaded_config = json.load(config) config.close() except ValueError: sys.exit('{0} does not contain valid JSON'.format(args.JSON_CONFIG)) dargs = vars(args) - dargs.update(loadedcfg) + dargs.update(loaded_config) args = argparse.Namespace(**dargs) # setup main logger diff --git a/pypxe/http.py b/pypxe/http.py index fcd049c..252265e 100644 --- a/pypxe/http.py +++ b/pypxe/http.py @@ -67,7 +67,7 @@ def handle_request(self, connection, addr): connection.send(response) connection.close() self.logger.debug('Sending message to {0}'.format(repr(addr))) - self.logger.debug('<--BEING MESSAGE-->' + self.logger.debug('<--BEING MESSAGE-->') self.logger.debug('{0}'.format(repr(response))) self.logger.debug('<--END MESSAGE-->') return @@ -87,7 +87,7 @@ def handle_request(self, connection, addr): connection.send(response) connection.close() self.logger.debug('Sending message to {0}'.format(repr(addr))) - self.logger.debug('<--BEING MESSAGE-->' + self.logger.debug('<--BEING MESSAGE-->') self.logger.debug('{0}'.format(repr(response))) self.logger.debug('<--END MESSAGE-->') self.logger.debug('File Sent - http://{target} -> {addr[0]}:{addr[1]}'.format(target = target, addr = addr)) From 67171dca2e1561697431af42998c2ab45e90cee6 Mon Sep 17 00:00:00 2001 From: Michael Mattioli Date: Thu, 7 May 2015 22:01:01 -0400 Subject: [PATCH 45/59] fix JSON config file bug, variable rename, changed logging format for tftp changed the logging format for TFTP to include square brackets around the level name renamed variable in accordance with PEP8 fixed example JSON file which had a boolean instead of an empty string for MODE_DEBUG --- example_cfg.json | 4 ++-- pypxe-server.py | 8 ++++---- pypxe/tftp.py | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/example_cfg.json b/example_cfg.json index 46b95f5..d5481dc 100644 --- a/example_cfg.json +++ b/example_cfg.json @@ -12,6 +12,6 @@ "USE_IPXE" : false, "USE_HTTP" : false, "USE_TFTP" : true, - "MODE_DEBUG" : false, - "USE_DHCP" : false, + "MODE_DEBUG" : "", + "USE_DHCP" : true, "DHCP_MODE_PROXY" : false } diff --git a/pypxe-server.py b/pypxe-server.py index 2a2727f..ed8ec0a 100644 --- a/pypxe-server.py +++ b/pypxe-server.py @@ -71,14 +71,14 @@ # parse the arguments given args = parser.parse_args() - if args.JSON_CONFIG: + if args.JSON_CONFIG: # load from configuration file try: - config = open(args.JSON_CONFIG, 'rb') + config_file = open(args.JSON_CONFIG, 'rb') except IOError: sys.exit('Failed to open {0}'.format(args.JSON_CONFIG)) try: - loaded_config = json.load(config) - config.close() + loaded_config = json.load(config_file) + config_file.close() except ValueError: sys.exit('{0} does not contain valid JSON'.format(args.JSON_CONFIG)) dargs = vars(args) diff --git a/pypxe/tftp.py b/pypxe/tftp.py index c590e7e..d1dd6e2 100644 --- a/pypxe/tftp.py +++ b/pypxe/tftp.py @@ -230,7 +230,7 @@ def __init__(self, **server_settings): if self.logger == None: self.logger = logging.getLogger('TFTP') handler = logging.StreamHandler() - formatter = logging.Formatter('%(asctime)s %(levelname)s %(name)s %(message)s') + formatter = logging.Formatter('%(asctime)s [%(levelname)s] %(name)s %(message)s') handler.setFormatter(formatter) self.logger.addHandler(handler) From f3e7f873ec2b2705e3cd74a4e7176eff8db657cf Mon Sep 17 00:00:00 2001 From: Michael Mattioli Date: Thu, 7 May 2015 23:18:54 -0400 Subject: [PATCH 46/59] make pypxe server script executable changed PyPXE server script to be executable --- pypxe-server.py | 1 + 1 file changed, 1 insertion(+) mode change 100644 => 100755 pypxe-server.py diff --git a/pypxe-server.py b/pypxe-server.py old mode 100644 new mode 100755 index ed8ec0a..00d58cf --- a/pypxe-server.py +++ b/pypxe-server.py @@ -1,3 +1,4 @@ +#! /usr/bin/env python import threading import os import sys From a283c928b0d1d232a3600ed9eaf5b7a39f9f0678 Mon Sep 17 00:00:00 2001 From: Michael Mattioli Date: Thu, 7 May 2015 23:19:30 -0400 Subject: [PATCH 47/59] removed space in execution declaration --- pypxe-server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pypxe-server.py b/pypxe-server.py index 00d58cf..db5e488 100755 --- a/pypxe-server.py +++ b/pypxe-server.py @@ -1,4 +1,4 @@ -#! /usr/bin/env python +#!/usr/bin/env python import threading import os import sys From 37ccc6225dc23b026035cee49c925ee5978ee7a0 Mon Sep 17 00:00:00 2001 From: Michael Mattioli Date: Fri, 8 May 2015 12:15:45 -0400 Subject: [PATCH 48/59] adding DNS option to DHCP response adding the DNS server to the DHCP response, this fixes #77 --- pypxe/dhcp.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pypxe/dhcp.py b/pypxe/dhcp.py index fbf954a..89156bc 100644 --- a/pypxe/dhcp.py +++ b/pypxe/dhcp.py @@ -198,6 +198,7 @@ def craft_options(self, opt53, client_mac): if not self.mode_proxy: response += self.tlv_encode(1, socket.inet_aton(self.subnet_mask)) # Subnet Mask response += self.tlv_encode(3, socket.inet_aton(self.router)) # Router + response += self.tlv_encode(6, socket.inet_aton(self.dns_server)) # DNS response += self.tlv_encode(51, struct.pack('!I', 86400)) # lease time # TFTP Server OR HTTP Server; if iPXE, need both From fd1d2f3b37ec115b61c767b69ce5b4553e58e21c Mon Sep 17 00:00:00 2001 From: Michael Mattioli Date: Fri, 8 May 2015 22:09:46 -0400 Subject: [PATCH 49/59] CLI options now take precedence over JSON configuration options if loading settings from a JSON configuration file, any subsequently set CLI options will take precedence despite whatever is in the JSON configuration file updated default settings and example JSON configuration file to include syslog server and syslog port --- example_cfg.json | 6 ++- pypxe-server.py | 109 ++++++++++++++++++++++++----------------------- 2 files changed, 60 insertions(+), 55 deletions(-) diff --git a/example_cfg.json b/example_cfg.json index d5481dc..98cb347 100644 --- a/example_cfg.json +++ b/example_cfg.json @@ -9,9 +9,11 @@ "DHCP_DNS" : "8.8.8.8", "DHCP_BROADCAST" : "", "DHCP_FILESERVER" : "192.168.2.2", + "SYSLOG_SERVER" : null, + "SYSLOG_PORT" : 514, "USE_IPXE" : false, "USE_HTTP" : false, "USE_TFTP" : true, - "MODE_DEBUG" : "", "USE_DHCP" : true, - "DHCP_MODE_PROXY" : false } + "DHCP_MODE_PROXY" : false, + "MODE_DEBUG" : "" } diff --git a/pypxe-server.py b/pypxe-server.py index db5e488..312048f 100755 --- a/pypxe-server.py +++ b/pypxe-server.py @@ -16,25 +16,57 @@ from pypxe import dhcp # PyPXE DHCP service from pypxe import http # PyPXE HTTP service -# JSON default -JSON_CONFIG = '' - -# default network boot file directory -NETBOOT_DIR = 'netboot' - -# default PXE boot file -NETBOOT_FILE = '' - -# DHCP default server settings -DHCP_SERVER_IP = '192.168.2.2' -DHCP_SERVER_PORT = 67 -DHCP_OFFER_BEGIN = '192.168.2.100' -DHCP_OFFER_END = '192.168.2.150' -DHCP_SUBNET = '255.255.255.0' -DHCP_ROUTER = '192.168.2.1' -DHCP_DNS = '8.8.8.8' -DHCP_BROADCAST = '' -DHCP_FILESERVER = '192.168.2.2' +# default settings +SETTINGS = {'NETBOOT_DIR':'netboot', + 'NETBOOT_FILE':'', + 'DHCP_SERVER_IP':'192.168.2.2', + 'DHCP_SERVER_PORT':67, + 'DHCP_OFFER_BEGIN':'192.168.2.100', + 'DHCP_OFFER_END':'192.168.2.150', + 'DHCP_SUBNET':'255.255.255.0', + 'DHCP_DNS':'8.8.8.8', + 'DHCP_ROUTER':'192.168.2.1', + 'DHCP_BROADCAST':'', + 'DHCP_FILESERVER':'192.168.2.2', + 'SYSLOG_SERVER':None, + 'SYSLOG_PORT':514, + 'USE_IPXE':False, + 'USE_HTTP':False, + 'USE_TFTP':True, + 'USE_DHCP':True, + 'DHCP_MODE_PROXY':False, + 'MODE_DEBUG':''} + +def parse_cli_arguments(default = SETTINGS): + # main service arguments + parser = argparse.ArgumentParser(description = 'Set options at runtime. Defaults are in %(prog)s', formatter_class = argparse.ArgumentDefaultsHelpFormatter) + parser.add_argument('--ipxe', action = 'store_true', dest = 'USE_IPXE', help = 'Enable iPXE ROM', default = default['USE_IPXE']) + parser.add_argument('--http', action = 'store_true', dest = 'USE_HTTP', help = 'Enable built-in HTTP server', default = default['USE_HTTP']) + parser.add_argument('--no-tftp', action = 'store_false', dest = 'USE_TFTP', help = 'Disable built-in TFTP server, by default it is enabled', default = default['USE_TFTP']) + parser.add_argument('--debug', action = 'store', dest = 'MODE_DEBUG', help = 'Comma Seperated (http,tftp,dhcp). Adds verbosity to the selected services while they run. Use \'all\' for enabling debug on all services', default = default['MODE_DEBUG']) + parser.add_argument('--config', action = 'store', dest = 'JSON_CONFIG', help = 'Configure from a JSON file rather than the command line', default = '') + parser.add_argument('--syslog', action = 'store', dest = 'SYSLOG_SERVER', help = 'Syslog server', default = default['SYSLOG_SERVER']) + parser.add_argument('--syslog-port', action = 'store', dest = 'SYSLOG_PORT', help = 'Syslog server port', default = default['SYSLOG_PORT']) + + # DHCP server arguments + exclusive = parser.add_mutually_exclusive_group(required = False) + exclusive.add_argument('--dhcp', action = 'store_true', dest = 'USE_DHCP', help = 'Enable built-in DHCP server', default = default['USE_DHCP']) + exclusive.add_argument('--dhcp-proxy', action = 'store_true', dest = 'DHCP_MODE_PROXY', help = 'Enable built-in DHCP server in proxy mode (implies --dhcp)', default = default['DHCP_MODE_PROXY']) + parser.add_argument('-s', '--dhcp-server-ip', action = 'store', dest = 'DHCP_SERVER_IP', help = 'DHCP Server IP', default = default['DHCP_SERVER_IP']) + parser.add_argument('-p', '--dhcp-server-port', action = 'store', dest = 'DHCP_SERVER_PORT', help = 'DHCP Server Port', default = default['DHCP_SERVER_PORT']) + parser.add_argument('-b', '--dhcp-begin', action = 'store', dest = 'DHCP_OFFER_BEGIN', help = 'DHCP lease range start', default = default['DHCP_OFFER_BEGIN']) + parser.add_argument('-e', '--dhcp-end', action = 'store', dest = 'DHCP_OFFER_END', help = 'DHCP lease range end', default = default['DHCP_OFFER_END']) + parser.add_argument('-n', '--dhcp-subnet', action = 'store', dest = 'DHCP_SUBNET', help = 'DHCP lease subnet', default = default['DHCP_SUBNET']) + parser.add_argument('-r', '--dhcp-router', action = 'store', dest = 'DHCP_ROUTER', help = 'DHCP lease router', default = default['DHCP_ROUTER']) + parser.add_argument('-d', '--dhcp-dns', action = 'store', dest = 'DHCP_DNS', help = 'DHCP lease DNS server', default = default['DHCP_DNS']) + parser.add_argument('-c', '--dhcp-broadcast', action = 'store', dest = 'DHCP_BROADCAST', help = 'DHCP broadcast address', default = default['DHCP_BROADCAST']) + parser.add_argument('-f', '--dhcp-fileserver', action = 'store', dest = 'DHCP_FILESERVER', help = 'DHCP fileserver IP', default = default['DHCP_FILESERVER']) + + # network boot directory and file name arguments + parser.add_argument('-a', '--netboot-dir', action = 'store', dest = 'NETBOOT_DIR', help = 'Local file serve directory', default = default['NETBOOT_DIR']) + parser.add_argument('-i', '--netboot-file', action = 'store', dest = 'NETBOOT_FILE', help = 'PXE boot file name (after iPXE if --ipxe)', default = default['NETBOOT_FILE']) + + return parser.parse_args() if __name__ == '__main__': try: @@ -42,37 +74,9 @@ if os.getuid() != 0: print '\nWARNING: Not root. Servers will probably fail to bind.\n' - # main service arguments - parser = argparse.ArgumentParser(description = 'Set options at runtime. Defaults are in %(prog)s', formatter_class = argparse.ArgumentDefaultsHelpFormatter) - parser.add_argument('--ipxe', action = 'store_true', dest = 'USE_IPXE', help = 'Enable iPXE ROM', default = False) - parser.add_argument('--http', action = 'store_true', dest = 'USE_HTTP', help = 'Enable built-in HTTP server', default = False) - parser.add_argument('--no-tftp', action = 'store_false', dest = 'USE_TFTP', help = 'Disable built-in TFTP server, by default it is enabled', default = True) - parser.add_argument('--debug', action = 'store', dest = 'MODE_DEBUG', help = 'Comma Seperated (http,tftp,dhcp). Adds verbosity to the selected services while they run. Use \'all\' for enabling debug on all services', default = '') - parser.add_argument('--config', action = 'store', dest = 'JSON_CONFIG', help = 'Configure from a JSON file rather than the command line', default = JSON_CONFIG) - parser.add_argument('--syslog', action = 'store', dest = 'SYSLOG_SERVER', help = 'Syslog server', default = None) - parser.add_argument('--syslog-port', action = 'store', dest = 'SYSLOG_PORT', help = 'Syslog server port', default = 514) - - # DHCP server arguments - exclusive = parser.add_mutually_exclusive_group(required = False) - exclusive.add_argument('--dhcp', action = 'store_true', dest = 'USE_DHCP', help = 'Enable built-in DHCP server', default = False) - exclusive.add_argument('--dhcp-proxy', action = 'store_true', dest = 'DHCP_MODE_PROXY', help = 'Enable built-in DHCP server in proxy mode (implies --dhcp)', default = False) - parser.add_argument('-s', '--dhcp-server-ip', action = 'store', dest = 'DHCP_SERVER_IP', help = 'DHCP Server IP', default = DHCP_SERVER_IP) - parser.add_argument('-p', '--dhcp-server-port', action = 'store', dest = 'DHCP_SERVER_PORT', help = 'DHCP Server Port', default = DHCP_SERVER_PORT) - parser.add_argument('-b', '--dhcp-begin', action = 'store', dest = 'DHCP_OFFER_BEGIN', help = 'DHCP lease range start', default = DHCP_OFFER_BEGIN) - parser.add_argument('-e', '--dhcp-end', action = 'store', dest = 'DHCP_OFFER_END', help = 'DHCP lease range end', default = DHCP_OFFER_END) - parser.add_argument('-n', '--dhcp-subnet', action = 'store', dest = 'DHCP_SUBNET', help = 'DHCP lease subnet', default = DHCP_SUBNET) - parser.add_argument('-r', '--dhcp-router', action = 'store', dest = 'DHCP_ROUTER', help = 'DHCP lease router', default = DHCP_ROUTER) - parser.add_argument('-d', '--dhcp-dns', action = 'store', dest = 'DHCP_DNS', help = 'DHCP lease DNS server', default = DHCP_DNS) - parser.add_argument('-c', '--dhcp-broadcast', action = 'store', dest = 'DHCP_BROADCAST', help = 'DHCP broadcast address', default = DHCP_BROADCAST) - parser.add_argument('-f', '--dhcp-fileserver', action = 'store', dest = 'DHCP_FILESERVER', help = 'DHCP fileserver IP', default = DHCP_FILESERVER) - - # network boot directory and file name arguments - parser.add_argument('-a', '--netboot-dir', action = 'store', dest = 'NETBOOT_DIR', help = 'Local file serve directory', default = NETBOOT_DIR) - parser.add_argument('-i', '--netboot-file', action = 'store', dest = 'NETBOOT_FILE', help = 'PXE boot file name (after iPXE if --ipxe)', default = NETBOOT_FILE) - - # parse the arguments given - args = parser.parse_args() - if args.JSON_CONFIG: # load from configuration file + # configure + args = parse_cli_arguments() + if args.JSON_CONFIG: # load from configuration file if specified try: config_file = open(args.JSON_CONFIG, 'rb') except IOError: @@ -82,9 +86,8 @@ config_file.close() except ValueError: sys.exit('{0} does not contain valid JSON'.format(args.JSON_CONFIG)) - dargs = vars(args) - dargs.update(loaded_config) - args = argparse.Namespace(**dargs) + SETTINGS.update(loaded_config) # update settings with JSON config + args = parse_cli_arguments(SETTINGS) # CLI options take precedence # setup main logger sys_logger = logging.getLogger('PyPXE') From e46121500a302f962b75b93d744adc754bfa3a24 Mon Sep 17 00:00:00 2001 From: Michael Mattioli Date: Fri, 8 May 2015 22:23:25 -0400 Subject: [PATCH 50/59] removed use of kwarg in parsing of CLI options removed use of keywork argument in the parsing of CLI options as it is redundant --- pypxe-server.py | 42 +++++++++++++++++++++--------------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/pypxe-server.py b/pypxe-server.py index 312048f..77a1f17 100755 --- a/pypxe-server.py +++ b/pypxe-server.py @@ -37,34 +37,34 @@ 'DHCP_MODE_PROXY':False, 'MODE_DEBUG':''} -def parse_cli_arguments(default = SETTINGS): +def parse_cli_arguments(): # main service arguments parser = argparse.ArgumentParser(description = 'Set options at runtime. Defaults are in %(prog)s', formatter_class = argparse.ArgumentDefaultsHelpFormatter) - parser.add_argument('--ipxe', action = 'store_true', dest = 'USE_IPXE', help = 'Enable iPXE ROM', default = default['USE_IPXE']) - parser.add_argument('--http', action = 'store_true', dest = 'USE_HTTP', help = 'Enable built-in HTTP server', default = default['USE_HTTP']) - parser.add_argument('--no-tftp', action = 'store_false', dest = 'USE_TFTP', help = 'Disable built-in TFTP server, by default it is enabled', default = default['USE_TFTP']) - parser.add_argument('--debug', action = 'store', dest = 'MODE_DEBUG', help = 'Comma Seperated (http,tftp,dhcp). Adds verbosity to the selected services while they run. Use \'all\' for enabling debug on all services', default = default['MODE_DEBUG']) + parser.add_argument('--ipxe', action = 'store_true', dest = 'USE_IPXE', help = 'Enable iPXE ROM', default = SETTINGS['USE_IPXE']) + parser.add_argument('--http', action = 'store_true', dest = 'USE_HTTP', help = 'Enable built-in HTTP server', default = SETTINGS['USE_HTTP']) + parser.add_argument('--no-tftp', action = 'store_false', dest = 'USE_TFTP', help = 'Disable built-in TFTP server, by default it is enabled', default = SETTINGS['USE_TFTP']) + parser.add_argument('--debug', action = 'store', dest = 'MODE_DEBUG', help = 'Comma Seperated (http,tftp,dhcp). Adds verbosity to the selected services while they run. Use \'all\' for enabling debug on all services', default = SETTINGS['MODE_DEBUG']) parser.add_argument('--config', action = 'store', dest = 'JSON_CONFIG', help = 'Configure from a JSON file rather than the command line', default = '') - parser.add_argument('--syslog', action = 'store', dest = 'SYSLOG_SERVER', help = 'Syslog server', default = default['SYSLOG_SERVER']) - parser.add_argument('--syslog-port', action = 'store', dest = 'SYSLOG_PORT', help = 'Syslog server port', default = default['SYSLOG_PORT']) + parser.add_argument('--syslog', action = 'store', dest = 'SYSLOG_SERVER', help = 'Syslog server', default = SETTINGS['SYSLOG_SERVER']) + parser.add_argument('--syslog-port', action = 'store', dest = 'SYSLOG_PORT', help = 'Syslog server port', default = SETTINGS['SYSLOG_PORT']) # DHCP server arguments exclusive = parser.add_mutually_exclusive_group(required = False) - exclusive.add_argument('--dhcp', action = 'store_true', dest = 'USE_DHCP', help = 'Enable built-in DHCP server', default = default['USE_DHCP']) - exclusive.add_argument('--dhcp-proxy', action = 'store_true', dest = 'DHCP_MODE_PROXY', help = 'Enable built-in DHCP server in proxy mode (implies --dhcp)', default = default['DHCP_MODE_PROXY']) - parser.add_argument('-s', '--dhcp-server-ip', action = 'store', dest = 'DHCP_SERVER_IP', help = 'DHCP Server IP', default = default['DHCP_SERVER_IP']) - parser.add_argument('-p', '--dhcp-server-port', action = 'store', dest = 'DHCP_SERVER_PORT', help = 'DHCP Server Port', default = default['DHCP_SERVER_PORT']) - parser.add_argument('-b', '--dhcp-begin', action = 'store', dest = 'DHCP_OFFER_BEGIN', help = 'DHCP lease range start', default = default['DHCP_OFFER_BEGIN']) - parser.add_argument('-e', '--dhcp-end', action = 'store', dest = 'DHCP_OFFER_END', help = 'DHCP lease range end', default = default['DHCP_OFFER_END']) - parser.add_argument('-n', '--dhcp-subnet', action = 'store', dest = 'DHCP_SUBNET', help = 'DHCP lease subnet', default = default['DHCP_SUBNET']) - parser.add_argument('-r', '--dhcp-router', action = 'store', dest = 'DHCP_ROUTER', help = 'DHCP lease router', default = default['DHCP_ROUTER']) - parser.add_argument('-d', '--dhcp-dns', action = 'store', dest = 'DHCP_DNS', help = 'DHCP lease DNS server', default = default['DHCP_DNS']) - parser.add_argument('-c', '--dhcp-broadcast', action = 'store', dest = 'DHCP_BROADCAST', help = 'DHCP broadcast address', default = default['DHCP_BROADCAST']) - parser.add_argument('-f', '--dhcp-fileserver', action = 'store', dest = 'DHCP_FILESERVER', help = 'DHCP fileserver IP', default = default['DHCP_FILESERVER']) + exclusive.add_argument('--dhcp', action = 'store_true', dest = 'USE_DHCP', help = 'Enable built-in DHCP server', default = SETTINGS['USE_DHCP']) + exclusive.add_argument('--dhcp-proxy', action = 'store_true', dest = 'DHCP_MODE_PROXY', help = 'Enable built-in DHCP server in proxy mode (implies --dhcp)', default = SETTINGS['DHCP_MODE_PROXY']) + parser.add_argument('-s', '--dhcp-server-ip', action = 'store', dest = 'DHCP_SERVER_IP', help = 'DHCP Server IP', default = SETTINGS['DHCP_SERVER_IP']) + parser.add_argument('-p', '--dhcp-server-port', action = 'store', dest = 'DHCP_SERVER_PORT', help = 'DHCP Server Port', default = SETTINGS['DHCP_SERVER_PORT']) + parser.add_argument('-b', '--dhcp-begin', action = 'store', dest = 'DHCP_OFFER_BEGIN', help = 'DHCP lease range start', default = SETTINGS['DHCP_OFFER_BEGIN']) + parser.add_argument('-e', '--dhcp-end', action = 'store', dest = 'DHCP_OFFER_END', help = 'DHCP lease range end', default = SETTINGS['DHCP_OFFER_END']) + parser.add_argument('-n', '--dhcp-subnet', action = 'store', dest = 'DHCP_SUBNET', help = 'DHCP lease subnet', default = SETTINGS['DHCP_SUBNET']) + parser.add_argument('-r', '--dhcp-router', action = 'store', dest = 'DHCP_ROUTER', help = 'DHCP lease router', default = SETTINGS['DHCP_ROUTER']) + parser.add_argument('-d', '--dhcp-dns', action = 'store', dest = 'DHCP_DNS', help = 'DHCP lease DNS server', default = SETTINGS['DHCP_DNS']) + parser.add_argument('-c', '--dhcp-broadcast', action = 'store', dest = 'DHCP_BROADCAST', help = 'DHCP broadcast address', default = SETTINGS['DHCP_BROADCAST']) + parser.add_argument('-f', '--dhcp-fileserver', action = 'store', dest = 'DHCP_FILESERVER', help = 'DHCP fileserver IP', default = SETTINGS['DHCP_FILESERVER']) # network boot directory and file name arguments - parser.add_argument('-a', '--netboot-dir', action = 'store', dest = 'NETBOOT_DIR', help = 'Local file serve directory', default = default['NETBOOT_DIR']) - parser.add_argument('-i', '--netboot-file', action = 'store', dest = 'NETBOOT_FILE', help = 'PXE boot file name (after iPXE if --ipxe)', default = default['NETBOOT_FILE']) + parser.add_argument('-a', '--netboot-dir', action = 'store', dest = 'NETBOOT_DIR', help = 'Local file serve directory', default = SETTINGS['NETBOOT_DIR']) + parser.add_argument('-i', '--netboot-file', action = 'store', dest = 'NETBOOT_FILE', help = 'PXE boot file name (after iPXE if --ipxe)', default = SETTINGS['NETBOOT_FILE']) return parser.parse_args() @@ -87,7 +87,7 @@ def parse_cli_arguments(default = SETTINGS): except ValueError: sys.exit('{0} does not contain valid JSON'.format(args.JSON_CONFIG)) SETTINGS.update(loaded_config) # update settings with JSON config - args = parse_cli_arguments(SETTINGS) # CLI options take precedence + args = parse_cli_arguments() # re-parse, CLI options take precedence # setup main logger sys_logger = logging.getLogger('PyPXE') From b76a311dfb91bfcca9a8cffe0de01d5738f0ed9b Mon Sep 17 00:00:00 2001 From: Michael Mattioli Date: Fri, 8 May 2015 23:19:19 -0400 Subject: [PATCH 51/59] documentation improvements updated variable names used tables instead of unordered lists updated explanations and defaults --- DOCUMENTATION.md | 139 ++++++++++++----------------------------------- README.md | 113 ++++++++++++++------------------------ 2 files changed, 78 insertions(+), 174 deletions(-) diff --git a/DOCUMENTATION.md b/DOCUMENTATION.md index ba3fe7c..6b1b41c 100644 --- a/DOCUMENTATION.md +++ b/DOCUMENTATION.md @@ -64,31 +64,16 @@ import pypxe.tftp ### Usage The TFTP server class, __`TFTPD()`__, is constructed with the following __keyword arguments__: -* __`ip`__ - * Description: This is the IP address that the TFTP server will bind to. - * Default: `'0.0.0.0'` (so that it binds to all available interfaces) - * Type: _string_ -* __`port`__ - * Description: This it the port that the TFTP server will run on. - * Default: `69` (default port for TFTP) - * Type: _int_ -* __`netbootDirectory`__ - * Description: This is the directory that the TFTP server will serve files from similarly to that of `tftpboot`. - * Default: `'.'` (current directory) - * Type: _string_ -* __`mode_debug`__ - * Description: This indicates whether or not the TFTP server should be started in debug mode or not. - * Default: `False` - * Type: bool -* __`logger`__ - * Description: A [Logger](https://docs.python.org/2/library/logging.html#logger-objects) object used for logging messages, if `None` a local [StreamHandler](https://docs.python.org/2/library/logging.handlers.html#streamhandler) instance will be created. - * Default: `None` -* __`default_retries`__ - * Description: The number of data retransmissions before dropping a connection. - * Default: `3` -* __`timeout`__ - * Description: The time in seconds before re-sending an un-acked data block. - * Default: `5` + +|Keyword Argument|Description|Default|Type| +|---|---|---|---| +|__`ip`__|This is the IP address that the TFTP server will bind to.|`'0.0.0.0'` (so that it binds to all available interfaces)| _string_| +|__`port`__|This it the port that the TFTP server will run on.|`69` (default port for TFTP)|_int_| +|__`netboot_directory`__|This is the directory that the TFTP server will serve files from similarly to that of `tftpboot`.|`'.'` (current directory)|_string_| +|__`mode_debug`__|This indicates whether or not the TFTP server should be started in debug mode or not.|`False`|`bool`| +|__`logger`__|A [Logger](https://docs.python.org/2/library/logging.html#logger-objects) object used for logging messages, if `None` a local [StreamHandler](https://docs.python.org/2/library/logging.handlers.html#streamhandler) instance will be created.|`None`|[_Logger_](https://docs.python.org/2/library/logging.html#logger-objects)| +|__`default_retries`__|The number of data retransmissions before dropping a connection.|`3`|_int_| +|__`timeout`__|The time in seconds before re-sending an un-acknowledged data block.|`5`|_int_| ## DHCP Server `pypxe.dhcp` @@ -103,65 +88,24 @@ import pypxe.dhcp ###Usage The DHCP server class, __`DHCPD()`__, is constructed with the following __keyword arguments__: -* __`ip`__ - * Description: This is the IP address that the DHCP server itself binds to. - * Default: `'192.168.2.2'` - * Type: _string_ -* __`port`__ - * Description: This it the port that the TFTP server will run on. - * Default: `67` (default port to listen for DHCP requests) - * Type: _int_ -* __`offerfrom`__ - * Description: This specifies the beginning of the range of IP addreses that the DHCP server will hand out to clients. - * Default: `'192.168.2.100'` - * Type: _string_ -* __`offerto`__ - * Description: This specifies the end of the range of IP addresses that the DHCP server will hand out to clients. - * Default: `'192.168.2.150'` - * Type: _string_ -* __`subnetmask`__ - * Description: This specifies the subnet mask that the DHCP server will specify to clients. - * Default: `'255.255.255.0'` - * Type: _string_ -* __`router`__ - * Description: This specifies the IP address of the router that the DHCP server will specify to clients. - * Default: `'192.168.2.1'` - * Type: _string_ -* __`dnsserver`__ - * Description: This specifies the DNS server that the DHCP server will specify to clients. Only one DNS server can be passed. - * Default: `'8.8.8.8'` ([Google Public DNS](https://developers.google.com/speed/public-dns/)) - * Type: _string_ -* __`broadcast`__ - * Description: This specifies the broadcast address the DHCP will broadcast packets to. - * Default: `''` - * Type: _string_ -* __`fileserver`__ - * Description: This is the IP address of the file server containing network boot files that the DHCP server will specify to clients. - * Default: `'192.168.2.2'` - * Type: _string_ -* __`filename`__ - * Description: This specifies the file name that the client should look for on the remote server. - * Default: `'pxelinux.0'` - * Type: _string_ -* __`useipxe`__ - * Description: This indicates whether or not iPXE is being used and adjusts itself accordingly. - * Default: `False` - * Type: _bool_ -* __`usehttp`__ - * Description: This indicates whether or not the built-in HTTP server is being used and adjusts itself accordingly. - * Default: `False` - * Type: _bool_ -* __`mode_proxy`__ - * Description: This indicates whether or not the DHCP server should be started in ProxyDHCP mode or not. - * Default: `False` - * Type: _bool_ -* __`mode_debug`__ - * Description: This indicates whether or not the DHCP server should be started in debug mode or not. - * Default: `False` - * Type: _bool_ -* __`logger`__ - * Description: A [Logger](https://docs.python.org/2/library/logging.html#logger-objects) object used for logging messages, if `None` a local [StreamHandler](https://docs.python.org/2/library/logging.handlers.html#streamhandler) instance will be created. - * Default: `None` + +|Keyword Argument|Description|Default|Type| +|---|---|---|---| +|__`ip`__|This is the IP address that the DHCP server itself binds to.|``'192.168.2.2'`|_string_| +|__`port`__|This it the port that the TFTP server will run on.|`67` (default port to listen for DHCP requests)|_int_| +|__`offer_from`__|This specifies the beginning of the range of IP addresses that the DHCP server will hand out to clients.|`'192.168.2.100'`|_string_| +|__`offer_to`__|This specifies the end of the range of IP addresses that the DHCP server will hand out to clients.|`'192.168.2.150'`|_string_| +|__`subnet_mask`__|This specifies the subnet mask that the DHCP server will specify to clients.|`'255.255.255.0'`|_string_| +|__`router`__|This specifies the IP address of the router that the DHCP server will specify to clients.|`'192.168.2.1'`|_string_| +|__`dns_server`__|This specifies the DNS server that the DHCP server will specify to clients. Only one DNS server can be passed.|`'8.8.8.8'` ([Google Public DNS](https://developers.google.com/speed/public-dns/))|_string_| +|__`broadcast`__|This specifies the broadcast address the DHCP will broadcast packets to.|`''`|_string_| +|__`file_server`__|This is the IP address of the file server containing network boot files that the DHCP server will specify to clients.|`'192.168.2.2'`|_string_| +|__`file_name`__|This specifies the file name that the client should look for on the remote server.|`'pxelinux.0'`|_string_| +|__`use_ipxe`__|This indicates whether or not iPXE is being used and adjusts itself accordingly.|`False`|_bool_| +|__`use_http`__|This indicates whether or not the built-in HTTP server is being used and adjusts itself accordingly.|`False`|_bool_| +|__`mode_proxy`__|This indicates whether or not the DHCP server should be started in ProxyDHCP mode or not.|`False`|_bool_| +|__`mode_debug`__|This indicates whether or not the DHCP server should be started in debug mode or not.|`False`|_bool_| +|__`logger`__|A [Logger](https://docs.python.org/2/library/logging.html#logger-objects) object used for logging messages, if `None` a local [StreamHandler](https://docs.python.org/2/library/logging.handlers.html#streamhandler) instance will be created.|`None`|[_Logger_](https://docs.python.org/2/library/logging.html#logger-objects)| ## HTTP Server `pypxe.http` @@ -176,25 +120,14 @@ import pypxe.http ### Usage The HTTP server class, __`HTTPD()`__, is constructed with the following __keyword arguments__: -* __`ip`__ - * Description: This is the IP address that the HTTP server will bind to. - * Default: `'0.0.0.0'` (so that it binds to all available interfaces) - * Type: _string_ -* __`port`__ - * Description: This it the port that the HTTP server will run on. - * Default: `80` (default port for HTTP) - * Type: _int_ -* __`netbootDirectory`__ - * Description: This is the directory that the HTTP server will serve files from similarly to that of `tftpboot`. - * Default: `'.'` (current directory) - * Type: _string_ -* __`mode_debug`__ - * Description: This indicates whether or not the HTTP server should be started in debug mode or not. - * Default: `False` - * Type: bool -* __`logger`__ - * Description: A [Logger](https://docs.python.org/2/library/logging.html#logger-objects) object used for logging messages, if `None` a local [StreamHandler](https://docs.python.org/2/library/logging.handlers.html#streamhandler) instance will be created. - * Default: `None` + +|Keyword Argument|Description|Default|Type| +|---|---|---|---| +|__`ip`__|This is the IP address that the HTTP server will bind to.|`'0.0.0.0'` (so that it binds to all available interfaces)|_string_| +|__`port`__|This it the port that the HTTP server will run on.|`80` (default port for HTTP)|_int_| +|__`netboot_directory`__|This is the directory that the HTTP server will serve files from similarly to that of `tftpboot`.|`'.'` (current directory)|_string_| +|__`mode_debug`__|This indicates whether or not the HTTP server should be started in debug mode or not.|`False`|bool| +|__`logger`__|A [Logger](https://docs.python.org/2/library/logging.html#logger-objects) object used for logging messages, if `None` a local [StreamHandler](https://docs.python.org/2/library/logging.handlers.html#streamhandler) instance will be created.|`None`|[_Logger_](https://docs.python.org/2/library/logging.html#logger-objects)| ## Additional Information * The function `chr(0)` is used in multiple places throughout the servers. This denotes a `NULL` byte, or `\x00` diff --git a/README.md b/README.md index 39b90a2..f83510c 100644 --- a/README.md +++ b/README.md @@ -34,77 +34,48 @@ $ sudo python pypxe-server.py --dhcp $ sudo python pypxe-server.py --dhcp-proxy ``` -**PyPXE Server Arguments** - -The following are arguments that can be passed to `pypxe-server.py` when running from the command line - -* __Main Arguments__ - * __`--ipxe`__ - * Description: Enable iPXE ROM - * Default: `False` - * __`--http`__ - * Description: Enable built-in HTTP server - * Default: `False` - * __`--dhcp`__ - * Description: Enable built-in DHCP server - * Default: `False` - * __`--dhcp-proxy`__ - * Description: Enable built-in DHCP server in proxy mode (implies `--dhcp`) - * Default: `False` - * __`--no-tftp`__ - * Description: Disable built-in TFTP server which is enabled by default - * Default: `False` - * __`--debug`__ - * Description: Enable selected services in DEBUG mode. Services are - selected by passing the name in a comma seperated list. Options are http, - tftp and dhcp - * _This adds a level of verbosity so that you can see what's happening in the background. Debug statements are prefixed with `[DEBUG]` and indented to distinguish between normal output that the services give._ - * Default: `False` - * __`--config`__ - * Description: Amend configuration from json file - * _Use the specified json file to amend the command line options. See example.json for more information._ - * Default: `None` - * __`--syslog`__ - * Description: Syslog server - * Default: `None` - * __`--syslog-port`__ - * Description: Syslog server port - * Default: `514` -* __DHCP Service Arguments__ _each of the following can be set one of two ways, you can use either/or_ - * __`-s DHCP_SERVER_IP`__ or __`--dhcp-server-ip DHCP_SERVER_IP`__ - * Description: Specify DHCP server IP address - * Default: `192.168.2.2` - * __`-p DHCP_SERVER_PORT`__ or __`--dhcp-server-port DHCP_SERVER_PORT`__ - * Description: Specify DHCP server port - * Default: `67` - * __`-b DHCP_OFFER_BEGIN`__ or __`--dhcp-begin DHCP_OFFER_BEGIN`__ - * Description: Specify DHCP lease range start - * Default: `192.168.2.100` - * __`-e DHCP_OFFER_END`__ or __`--dhcp-end DHCP_OFFER_END`__ - * Description: Specify DHCP lease range end - * Default: `192.168.2.150` - * __`-n DHCP_SUBNET`__ or __`--dhcp-subnet DHCP_SUBNET`__ - * Description: Specify DHCP subnet - * Default: `255.255.255.0` - * __`-r DHCP_ROUTER`__ or __`--dhcp-router DHCP_ROUTER`__ - * Description: Specify DHCP lease router - * Default: `192.168.2.1` - * __`-d DHCP_DNS`__ or __`--dhcp-dns DHCP_DNS`__ - * Description: Specify DHCP lease DNS server - * Default: `8.8.8.8` - * __`-c DHCP_BROADCAST`__ or __`--dhcp-broadcast DHCP_BROADCAST`__ - * Description: Specify DHCP broadcast address - * Default: `''` - * __`-f DHCP_FILESERVER_IP`__ or __`--dhcp-fileserver-ip DHCP_FILESERVER_IP`__ - * Description: Specify DHCP file server IP address - * Default: `192.168.2.2` -* __File Name/Directory Arguments__ - * __`-a NETBOOT_DIR`__ or __`--netboot-dir NETBOOT_DIR`__ - * Description: Specify the local directory where network boot files will be served - * Default: `'netboot'` - * __`-i NETBOOT_FILE`__ or __`--netboot-file NETBOOT_FILE`__ - * Description: Specify the PXE boot file name - * Default: _automatically set based on what services are enabled or disabled, see [`DOCUMENTATION.md`](DOCUMENTATION.md) for further explanation_ +#### PyPXE Server Arguments + +The following are arguments that can be passed to `pypxe-server.py` when running from the command line: + +##### Main Arguments + +|Argument|Description|Default| +|---|---|---| +|__`--ipxe`__|Enable iPXE ROM|`False`| +|__`--http`__|Enable built-in HTTP server|`False`| +|__`--dhcp`__|Enable built-in DHCP server|`False`| +|__`--dhcp-proxy`__|Enable built-in DHCP server in proxy mode (implies `--dhcp`)|`False`| +|__`--no-tftp`__|Disable built-in TFTP server which is enabled by default|`False`| +|__`--debug`__|Enable selected services in DEBUG mode; services are selected by passing the name in a comma separated list. **Options are: http, tftp and dhcp** _This adds a level of verbosity so that you can see what's happening in the background._|`''`| +|__`--config`__|Load configuration from JSON file. (see `example_cfg.json`)|`None`| +|__`--syslog`__|Specify a syslog server|`None`| +|__`--syslog-port`__|Specify a syslog server port|`514`| + + +##### DHCP Service Arguments + +**_each of the following can be set one of two ways, you can use either/or_** + +|Argument|Description|Default| +|---|---|---| +|__`-s DHCP_SERVER_IP`__ or __`--dhcp-server-ip DHCP_SERVER_IP`__|Specify DHCP server IP address|`192.168.2.2`| +|__`-p DHCP_SERVER_PORT`__ or __`--dhcp-server-port DHCP_SERVER_PORT`__|Specify DHCP server port|`67`| +|__`-b DHCP_OFFER_BEGIN`__ or __`--dhcp-begin DHCP_OFFER_BEGIN`__|Specify DHCP lease range start|`192.168.2.100`| +|__`-e DHCP_OFFER_END`__ or __`--dhcp-end DHCP_OFFER_END`__|Specify DHCP lease range end|`192.168.2.150`| +|__`-n DHCP_SUBNET`__ or __`--dhcp-subnet DHCP_SUBNET`__|Specify DHCP subnet mask|`255.255.255.0`| +|__`-r DHCP_ROUTER`__ or __`--dhcp-router DHCP_ROUTER`__|Specify DHCP lease router|`192.168.2.1`| +|__`-d DHCP_DNS`__ or __`--dhcp-dns DHCP_DNS`__|Specify DHCP lease DNS server|`8.8.8.8`| +|__`-c DHCP_BROADCAST`__ or __`--dhcp-broadcast DHCP_BROADCAST`__|Specify DHCP broadcast address|`''`| +|__`-f DHCP_FILESERVER_IP`__ or __`--dhcp-fileserver-ip DHCP_FILESERVER_IP`__|Specify DHCP file server IP address|`192.168.2.2`| + + +##### File Name/Directory Arguments + +|Argument|Description|Default| +|---|---|---| +|__`-a NETBOOT_DIR`__ or __`--netboot-dir NETBOOT_DIR`__|Specify the local directory where network boot files will be served|`'netboot'`| +|__`-i NETBOOT_FILE`__ or __`--netboot-file NETBOOT_FILE`__|Specify the PXE boot file name|_automatically set based on what services are enabled or disabled, see [`DOCUMENTATION.md`](DOCUMENTATION.md) for further explanation_| ## Notes * `Core.iso` located in `netboot` is from the [TinyCore Project](http://distro.ibiblio.org/tinycorelinux/) and is provided as an example to network boot from using PyPXE From f2521a4f706af101ebd5106a9c06a686680d2630 Mon Sep 17 00:00:00 2001 From: Michael Mattioli Date: Fri, 8 May 2015 23:21:36 -0400 Subject: [PATCH 52/59] typo fix fixed a markdown typo and removed a line which was redundant --- DOCUMENTATION.md | 2 +- README.md | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/DOCUMENTATION.md b/DOCUMENTATION.md index 6b1b41c..35564d6 100644 --- a/DOCUMENTATION.md +++ b/DOCUMENTATION.md @@ -91,7 +91,7 @@ The DHCP server class, __`DHCPD()`__, is constructed with the following __keywor |Keyword Argument|Description|Default|Type| |---|---|---|---| -|__`ip`__|This is the IP address that the DHCP server itself binds to.|``'192.168.2.2'`|_string_| +|__`ip`__|This is the IP address that the DHCP server itself binds to.|`'192.168.2.2'`|_string_| |__`port`__|This it the port that the TFTP server will run on.|`67` (default port to listen for DHCP requests)|_int_| |__`offer_from`__|This specifies the beginning of the range of IP addresses that the DHCP server will hand out to clients.|`'192.168.2.100'`|_string_| |__`offer_to`__|This specifies the end of the range of IP addresses that the DHCP server will hand out to clients.|`'192.168.2.150'`|_string_| diff --git a/README.md b/README.md index f83510c..818141b 100644 --- a/README.md +++ b/README.md @@ -55,8 +55,6 @@ The following are arguments that can be passed to `pypxe-server.py` when running ##### DHCP Service Arguments -**_each of the following can be set one of two ways, you can use either/or_** - |Argument|Description|Default| |---|---|---| |__`-s DHCP_SERVER_IP`__ or __`--dhcp-server-ip DHCP_SERVER_IP`__|Specify DHCP server IP address|`192.168.2.2`| From 5c5f8489227948fdae3d405c01b77a5458ca5ec7 Mon Sep 17 00:00:00 2001 From: Michael Mattioli Date: Fri, 8 May 2015 23:23:47 -0400 Subject: [PATCH 53/59] linked to file in documentation added a markdown link to the example JSON file in the documentation --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 818141b..ff1fd23 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,7 @@ The following are arguments that can be passed to `pypxe-server.py` when running |__`--dhcp-proxy`__|Enable built-in DHCP server in proxy mode (implies `--dhcp`)|`False`| |__`--no-tftp`__|Disable built-in TFTP server which is enabled by default|`False`| |__`--debug`__|Enable selected services in DEBUG mode; services are selected by passing the name in a comma separated list. **Options are: http, tftp and dhcp** _This adds a level of verbosity so that you can see what's happening in the background._|`''`| -|__`--config`__|Load configuration from JSON file. (see `example_cfg.json`)|`None`| +|__`--config`__|Load configuration from JSON file. (see [`example_cfg.json`](example_cfg.json))|`None`| |__`--syslog`__|Specify a syslog server|`None`| |__`--syslog-port`__|Specify a syslog server port|`514`| From a7235803eddf977da97829190d1a435a29f96b42 Mon Sep 17 00:00:00 2001 From: Michael Mattioli Date: Sat, 9 May 2015 12:25:17 -0400 Subject: [PATCH 54/59] implemented client threading for HTTPD each client on the HTTP server is now pushed onto its own thread --- pypxe/http.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pypxe/http.py b/pypxe/http.py index 252265e..781e7ac 100644 --- a/pypxe/http.py +++ b/pypxe/http.py @@ -7,6 +7,7 @@ import socket import struct import os +import threading import logging class HTTPD: @@ -96,4 +97,6 @@ def listen(self): '''This method is the main loop that listens for requests.''' while True: conn, addr = self.sock.accept() - self.handle_request(conn, addr) + client = threading.Thread(target = self.handle_request, args = (conn, addr)) + client.daemon = True; + client.start() From f9238d1b6e53a1b6e4285b7f0f428ccdfc6a67a2 Mon Sep 17 00:00:00 2001 From: Michael Mattioli Date: Sat, 9 May 2015 12:44:18 -0400 Subject: [PATCH 55/59] removed redundant debug flag removed redundant flag in debug message for TFTP server --- pypxe/tftp.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pypxe/tftp.py b/pypxe/tftp.py index d1dd6e2..ac4c1f4 100644 --- a/pypxe/tftp.py +++ b/pypxe/tftp.py @@ -172,7 +172,7 @@ def sendError(self, code = 1, message = 'File Not Found', filename = ''): response += message response += chr(0) self.sock.sendto(response, self.address) - self.logger.debug('TFTP Sending {0}: {1} {2}'.format(code, message, filename)) + self.logger.debug('Sending {0}: {1} {2}'.format(code, message, filename)) def complete(self): ''' From 630b211bf54fb360e94b8742fb28918e97d85d72 Mon Sep 17 00:00:00 2001 From: Michael Mattioli Date: Sat, 9 May 2015 13:07:37 -0400 Subject: [PATCH 56/59] removed overly verbose debug output from HTTP server removed overly verbose debug output to simplify logging and debugging --- pypxe/http.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/pypxe/http.py b/pypxe/http.py index 781e7ac..cc29b61 100644 --- a/pypxe/http.py +++ b/pypxe/http.py @@ -87,10 +87,7 @@ def handle_request(self, connection, addr): handle.close() connection.send(response) connection.close() - self.logger.debug('Sending message to {0}'.format(repr(addr))) - self.logger.debug('<--BEING MESSAGE-->') - self.logger.debug('{0}'.format(repr(response))) - self.logger.debug('<--END MESSAGE-->') + self.logger.debug('Sending file to {0}'.format(repr(addr))) self.logger.debug('File Sent - http://{target} -> {addr[0]}:{addr[1]}'.format(target = target, addr = addr)) def listen(self): From da07d70671b1ea5abf0bcc8c18dae4028bbe09d3 Mon Sep 17 00:00:00 2001 From: Michael Mattioli Date: Sat, 9 May 2015 13:53:31 -0400 Subject: [PATCH 57/59] removed redundant debug message and fixed unicode error removed a redundant debug message being sent to the logger fixed unicode encoding issue when loading configuration settings from a JSON file --- pypxe-server.py | 5 +++++ pypxe/http.py | 1 - 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/pypxe-server.py b/pypxe-server.py index 77a1f17..f8cdbbc 100755 --- a/pypxe-server.py +++ b/pypxe-server.py @@ -86,6 +86,11 @@ def parse_cli_arguments(): config_file.close() except ValueError: sys.exit('{0} does not contain valid JSON'.format(args.JSON_CONFIG)) + for setting in loaded_config: + if type(loaded_config[setting]) is unicode: + loaded_config[setting] = loaded_config[setting].encode('ascii') + for option in loaded_config.itervalues(): + print type(option) SETTINGS.update(loaded_config) # update settings with JSON config args = parse_cli_arguments() # re-parse, CLI options take precedence diff --git a/pypxe/http.py b/pypxe/http.py index cc29b61..d1f97fc 100644 --- a/pypxe/http.py +++ b/pypxe/http.py @@ -87,7 +87,6 @@ def handle_request(self, connection, addr): handle.close() connection.send(response) connection.close() - self.logger.debug('Sending file to {0}'.format(repr(addr))) self.logger.debug('File Sent - http://{target} -> {addr[0]}:{addr[1]}'.format(target = target, addr = addr)) def listen(self): From 0f97bc4940ca4c78d131a9bbf90bfa01e4b9f1cb Mon Sep 17 00:00:00 2001 From: Michael Mattioli Date: Sat, 9 May 2015 14:02:55 -0400 Subject: [PATCH 58/59] removed print lines used to debug removed print lines that were left when used for debugging --- pypxe-server.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/pypxe-server.py b/pypxe-server.py index f8cdbbc..066feba 100755 --- a/pypxe-server.py +++ b/pypxe-server.py @@ -89,8 +89,6 @@ def parse_cli_arguments(): for setting in loaded_config: if type(loaded_config[setting]) is unicode: loaded_config[setting] = loaded_config[setting].encode('ascii') - for option in loaded_config.itervalues(): - print type(option) SETTINGS.update(loaded_config) # update settings with JSON config args = parse_cli_arguments() # re-parse, CLI options take precedence From 052ee5f27ec03db554774c12bdda2ee95566864f Mon Sep 17 00:00:00 2001 From: Michael Mattioli Date: Mon, 11 May 2015 13:15:23 -0400 Subject: [PATCH 59/59] version bump to v1.5 changed package version to v1.5 --- pypxe/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pypxe/__init__.py b/pypxe/__init__.py index e43be03..fcb6b5d 100644 --- a/pypxe/__init__.py +++ b/pypxe/__init__.py @@ -1 +1 @@ -__version__ = '1.0' \ No newline at end of file +__version__ = '1.5'