diff --git a/DOCUMENTATION.md b/DOCUMENTATION.md index 4d67579..35564d6 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. @@ -18,7 +18,9 @@ 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. -###ProxyDHCP +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. 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. @@ -29,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 -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) +## 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 @@ -49,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 @@ -60,28 +62,22 @@ 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. - * 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 - -##DHCP Server `pypxe.dhcp` - -###Importing + +|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` + +### Importing The DHCP service can be imported _one_ of the following two ways: ```python from pypxe import dhcp @@ -92,66 +88,28 @@ 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_ - -##HTTP Server `pypxe.http` - -###Importing + +|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` + +### Importing The HTTP service can be imported _one_ of the following two ways: ```python from pypxe import http @@ -160,26 +118,18 @@ 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. - * 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 - -##Additional Information + +|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` * 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/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 diff --git a/README.md b/README.md index 7464fa0..ff1fd23 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. @@ -34,67 +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 - * _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_ - * __`-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_ - -##Notes +#### 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`](example_cfg.json))|`None`| +|__`--syslog`__|Specify a syslog server|`None`| +|__`--syslog-port`__|Specify a syslog server port|`514`| + + +##### DHCP Service Arguments + +|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 * `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/example_cfg.json b/example_cfg.json new file mode 100644 index 0000000..98cb347 --- /dev/null +++ b/example_cfg.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", + "SYSLOG_SERVER" : null, + "SYSLOG_PORT" : 514, + "USE_IPXE" : false, + "USE_HTTP" : false, + "USE_TFTP" : true, + "USE_DHCP" : true, + "DHCP_MODE_PROXY" : false, + "MODE_DEBUG" : "" } diff --git a/netboot/ldlinux.e32 b/netboot/ldlinux.e32 new file mode 100644 index 0000000..a65ef91 Binary files /dev/null and b/netboot/ldlinux.e32 differ diff --git a/netboot/ldlinux.e64 b/netboot/ldlinux.e64 new file mode 100644 index 0000000..174cb94 Binary files /dev/null and b/netboot/ldlinux.e64 differ diff --git a/netboot/syslinux.efi32 b/netboot/syslinux.efi32 new file mode 100755 index 0000000..385b51d Binary files /dev/null and b/netboot/syslinux.efi32 differ diff --git a/netboot/syslinux.efi64 b/netboot/syslinux.efi64 new file mode 100755 index 0000000..cc5ffe2 Binary files /dev/null and b/netboot/syslinux.efi64 differ diff --git a/pypxe-server.py b/pypxe-server.py old mode 100644 new mode 100755 index 70b6f53..066feba --- a/pypxe-server.py +++ b/pypxe-server.py @@ -1,6 +1,10 @@ +#!/usr/bin/env python import threading import os import sys +import json +import logging +import logging.handlers try: import argparse @@ -8,74 +12,107 @@ 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 - -#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' +from pypxe import tftp # PyPXE TFTP service +from pypxe import dhcp # PyPXE DHCP service +from pypxe import http # PyPXE HTTP service + +# 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(): + # 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 = 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 = 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 = 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 = 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() 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 - # - - #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_true', dest = 'MODE_DEBUG', help = 'Adds verbosity to the selected services while they run', default = False) - - #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) - 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() - - #pass warning to user regarding starting HTTP server without iPXE + + # 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: + sys.exit('Failed to open {0}'.format(args.JSON_CONFIG)) + try: + loaded_config = json.load(config_file) + 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') + SETTINGS.update(loaded_config) # update settings with JSON config + args = parse_cli_arguments() # re-parse, CLI options take precedence + + # 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 [%(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: - 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 + 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: 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' @@ -84,60 +121,75 @@ 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 - runningServices = [] - #configure/start TFTP server + # make a list of running threads for each service + running_services = [] + + # configure/start TFTP server if args.USE_TFTP: - print 'Starting TFTP server...' - tftpServer = tftp.TFTPD(mode_debug = args.MODE_DEBUG) - tftpd = threading.Thread(target = tftpServer.listen) + + # setup TFTP logger + tftp_logger = sys_logger.getChild('TFTP') + sys_logger.info('Starting TFTP server...') + + # setup the thread + 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 + # configure/start DHCP server if args.USE_DHCP: + + # setup DHCP logger + dhcp_logger = sys_logger.getChild('DHCP') 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...' - dhcpServer = dhcp.DHCPD( + sys_logger.info('Starting DHCP server...') + + # setup the thread + 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 = args.MODE_DEBUG) - dhcpd = threading.Thread(target = dhcpServer.listen) + mode_debug = ('dhcp' in args.MODE_DEBUG.lower() or 'all' in args.MODE_DEBUG.lower()), + logger = dhcp_logger) + dhcpd = threading.Thread(target = dhcp_server.listen) dhcpd.daemon = True dhcpd.start() - runningServices.append(dhcpd) - + running_services.append(dhcpd) - #configure/start HTTP server + # configure/start HTTP server if args.USE_HTTP: - print 'Starting HTTP server...' - httpServer = http.HTTPD(mode_debug = args.MODE_DEBUG) - httpd = threading.Thread(target = httpServer.listen) + + # setup HTTP logger + http_logger = sys_logger.getChild('HTTP') + sys_logger.info('Starting HTTP server...') + + # setup the thread + 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) - print 'PyPXE successfully initialized and running!' + 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/__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' diff --git a/pypxe/dhcp.py b/pypxe/dhcp.py index 29de407..89156bc 100644 --- a/pypxe/dhcp.py +++ b/pypxe/dhcp.py @@ -7,113 +7,133 @@ import socket import struct import os +import logging from collections import defaultdict from time import time +class OutOfLeasesError(Exception): + pass + 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 + 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. ''' - 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', 'pxelinux.0') - 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 + 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.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') + handler = logging.StreamHandler() + 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) 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) + self.file_name = 'http://{0}/{1}'.format(self.file_server, self.file_name) if self.ipxe and not self.http: - self.filename = 'tftp://%s/%s' % (self.fileserver, self.filename) + self.file_name = 'tftp://{0}/{1}'.format(self.file_server, self.file_name) - 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('DHCP Server IP: {0}'.format(self.ip)) + self.logger.debug('DHCP Server Port: {0}'.format(self.port)) + + # 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)) + 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) - def tlvEncode(self, tag, value): - ''' - Encode a TLV option - ''' - return struct.pack("BB", tag, len(value)) + value + # 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 tlvParse(self, raw): - ''' - Parse a string of TLV encoded options. - ''' + 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 + [tag] = struct.unpack('B', raw[0]) + 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] + [length] = struct.unpack('B', raw[1]) value = raw[2:2 + length] raw = raw[2 + length:] if tag in ret: @@ -122,127 +142,162 @@ 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 - if self.mode_debug: - print '[DEBUG] New DHCP Assignment - 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 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']: - response += self.tlvEncode(67, self.filename + chr(0)) + 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 + 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 == 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)) + else: + 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 - 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-->' + 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-->') + 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 - 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-->' + 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-->') + 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 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('PXE client request received') + return True + if self.mode_debug: + self.logger.debug('Non-PXE client request received') + 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]) - if self.mode_debug: - print '[DEBUG] Received message' - print '\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 - type = ord(options[53][0]) #see RFC2131 page 10 + client_mac = struct.unpack('!28x6s', message[:34]) + self.logger.debug('Received message') + 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-->') + 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 if type == 1: - if self.mode_debug: - print '[DEBUG] Received DHCPOFFER' - self.dhcpOffer(message) + self.logger.debug('Received DHCPOFFER') + try: + self.dhcp_offer(message) + except OutOfLeasesError: + self.logger.critical("Ran out of leases") elif type == 3 and address[0] == '0.0.0.0' and not self.mode_proxy: - if self.mode_debug: - print '[DEBUG] Received DHCPACK' - self.dhcpAck(message) + self.logger.debug('Received DHCPACK') + self.dhcp_ack(message) elif type == 3 and address[0] != '0.0.0.0' and self.mode_proxy: - if self.mode_debug: - print '[DEBUG] Received DHCPACK' - self.dhcpAck(message) + self.logger.debug('Received DHCPACK') + self.dhcp_ack(message) diff --git a/pypxe/http.py b/pypxe/http.py index 5702832..d1f97fc 100644 --- a/pypxe/http.py +++ b/pypxe/http.py @@ -7,78 +7,92 @@ import socket import struct import os +import threading +import logging 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 + 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') + handler = logging.StreamHandler() + 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) + 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)) 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 ('.') - 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('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) - if self.mode_debug: - print '[DEBUG] HTTP Recieved message from ' + repr(addr) - print '\t<--BEGIN MESSAGE-->\n\t' + repr(request) + '\n\t<--END MESSAGE-->' - startline = request.split('\r\n')[0].split(' ') - method = startline[0] - target = startline[1] + 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-->') + 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'): 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() - 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('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() - 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('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() - 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('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) + client = threading.Thread(target = self.handle_request, args = (conn, addr)) + client.daemon = True; + client.start() diff --git a/pypxe/tftp.py b/pypxe/tftp.py index 7f0bba5..ac4c1f4 100644 --- a/pypxe/tftp.py +++ b/pypxe/tftp.py @@ -7,120 +7,263 @@ import socket import struct import os -from collections import defaultdict +import select +import time +import logging +import math -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.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)) +class ParentSocket(socket.socket): + '''Subclassed socket.socket to enable a link-back to the client object.''' + parent = None - 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 - #key is (address, port) pair - self.ongoing = defaultdict(lambda: {'filename': '', 'handle': None, 'block': 1, 'blksize': 512}) +class Client: + '''Client instance for TFTPD.''' + def __init__(self, mainsock, parent): - # 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 ('.') + 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.{0}'.format(self.address)) + self.logger.debug('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 filename(self, message): + def ready(self): + '''Called when there is something to be read on our socket.''' + self.message = self.sock.recv(1024) + self.handle() + + def send_block(self): ''' - The first null-delimited field after the OPCODE - is the filename. This method returns the filename - from the message. + Sends the next block of data, setting the timeout and retry + variables accordingly. ''' - return message[2:].split(chr(0))[0] + 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 {0}'.format(self.block)) + self.retries -= 1 + self.sent_time = time.time() - def notFound(self, address): - ''' - short int 5 -> Error - short int 1 -> File Not Found + 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 - This method sends the message to the client + def no_retries(self): + '''Determines if the client ran out of retry attempts.''' + if not self.retries: + return True + return False + + 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 {0} not supported'.format(mode)) + return False + + def check_file(self): ''' - 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.sock.sendto(response, address) + 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 + return True + self.sendError(1, 'File Not Found', filename = filename) + return False - def sendBlock(self, address): + def parse_options(self): ''' - short int 3 -> Data Block + Extracts the options sent from a client; if any, calculates the last + block based on the filesize and blocksize. ''' - 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.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.ongoing.pop(address) + 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('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): + # we need to know later if we actually had any options + self.block = 0 + return True else: - if self.mode_debug: - print '[DEBUG] TFTP Sending block ' + repr(descriptor['block']) - descriptor['block'] += 1 + return False + + def reply_options(self): + '''Acknowledges any options received.''' + # only called if options, so send them all + response = struct.pack("!H", 6) - def read(self, address, message): + 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): ''' - On RRQ OPCODE: - file exists -> reply with file - file does not exist -> reply with error + 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. ''' - filename = self.filename(message) - if not os.path.lexists(filename): - self.notFound(address) + 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.valid_mode() or not self.check_file(): + # 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.parse_options(): + # no options recieved so start transfer + if self.block == 1: + self.send_block() return - self.ongoing[address]['filename'] = filename - self.ongoing[address]['handle'] = open(filename, 'r') - options = message.split(chr(0))[3: -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']): - 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']) - if 'tsize' in options: - response += 'tsize' + chr(0) - response += str(filesize) - response += chr(0) - if response: - response = struct.pack('!H', 6) + response - self.sock.sendto(response, address) - self.sendBlock(address) + + # we got some options, so ack those first + self.reply_options() + + def sendError(self, code = 1, message = 'File Not Found', filename = ''): + ''' + 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 + response += message + response += chr(0) + self.sock.sendto(response, self.address) + self.logger.debug('Sending {0}: {1} {2}'.format(code, message, filename)) + + def complete(self): + ''' + Closes a file and socket after sending it + and marks ourselves as dead to be cleaned up. + ''' + try: + self.fh.close() + except AttributeError: + # we have not opened yet, or file-not-found + pass + self.sock.close() + self.dead = True + + 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]) + 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 {0}'.format(self.block)) + elif block > 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 {0}'.format(self.filename)) + self.complete() + else: + self.block = block + 1 + self.retries = self.default_retries + self.send_block() + + +class TFTPD: + ''' + This class implements a read-only TFTP server + implemented from RFC1350 and RFC2348 + ''' + 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') + handler = logging.StreamHandler() + 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) + + self.logger.debug('NOTICE: TFTP server started in debug mode. TFTP server is using the following:') + 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, + # this simplifies target later as well as offers a slight security increase + os.chdir (self.netbook_directory) + os.chroot ('.') + def listen(self): - '''This method listens for incoming requests''' + '''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) + # 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: + 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.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()]