1
1
mirror of https://github.com/eblot/pybootd.git synced 2024-09-11 14:06:57 +03:00

Merge branch 'python3'

This commit is contained in:
Emmanuel Blot 2019-09-06 18:56:37 +02:00
commit bbf0b15edb
12 changed files with 712 additions and 457 deletions

2
.gitignore vendored
View File

@ -1,5 +1,7 @@
*.egg-info *.egg-info
*.pyc *.pyc
*.pyo *.pyo
MANIFEST
**/*-local.ini
dist/ dist/
build/ build/

View File

@ -5,50 +5,56 @@ PyBootd
Overview Overview
~~~~~~~~ ~~~~~~~~
PyBootd is a daemon supporting a subset of the BOOTP, DHCP, PXE and TFTP PyBootd is a daemon supporting a subset of the BOOTP, DHCP, PXE, TFTP and HTTP
protocols, with some handy extensions. protocols, with some handy extensions.
One of its main goals is to provide a simple solution to boot up any One of its main goals is to provide a simple solution to boot up any
PXE-enabled personal computer, with no other tool required but a standard PXE-enabled personal computer, with no other tool required but a standard
Python installation. Python installation.
It is not designed to be feature-complete, but to be used as an easy modifiable
code to develop custom boot solutions
Pybootd can be used for any network boot up, or to install an OS without any Pybootd can be used for any network boot up, or to install an OS without any
physical support such as a USB key or a CD/DVD. physical support such as a USB key or a CD/DVD.
Requirements Requirements
~~~~~~~~~~~~ ~~~~~~~~~~~~
Python Python
------ ------
- Python_ 2.7 or above is required. Python_ 3.x is not yet supported. - Python_ 3.5+ or above is required. Python_ 2.x is not longer supported.
- Six_ compatibility module
- Netifaces_ Python module is required on OS X; on Linux only, iproute2_ can be - Netifaces_ Python module is required on OS X; on Linux only, iproute2_ can be
used as an alternative used as an alternative
- Optional: python-pkg-resources_ Python module - Optional: python_pkg_resources_ Python module
.. _Python: http://python.org/ .. _Python: http://python.org/
.. _Netifaces: http://alastairs-place.net/netifaces/ .. _Netifaces: http://alastairs-place.net/netifaces/
.. _iproute2: http://www.linuxfoundation.org/collaborate/workgroups/networking/iproute2 .. _iproute2: http://www.linuxfoundation.org/collaborate/workgroups/networking/iproute2
.. _python-pkg-resources: http://pythonhosted.org/distribute/pkg_resources.html .. _python_pkg_resources: http://pythonhosted.org/distribute/pkg_resources.html
.. _Six: http://pythonhosted.org/six
Permissions Permissions
----------- -----------
- DHCP protocol requires the daemon to listen on port 67. - DHCP protocol requires the daemon to listen on port 67.
- TFTP protocol requires the daemon to listen on port 69. - TFTP protocol requires the daemon to listen on port 69.
- HTTP optional daemon may be run on any port.
As these ports are within the server's range (<1024), the superuser privileges As these ports are within the server's range (<1024), the superuser privileges
are required on Unix hosts (Linux, Mac OS X, ...) to start up these daemons. are required on Unix hosts (Linux, Mac OS X, ...) to start up these daemons.
Status Status
~~~~~~ ~~~~~~
This project is in beta development stage. This project is in beta development stage.
Supported features Supported features
~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~
- Access control: - Access control:
1. None (any remote host can be served) 1. None (any remote host can be served)
@ -72,6 +78,20 @@ Supported features
- It is possible to use pybootd with only one of the services, either TFTP or - It is possible to use pybootd with only one of the services, either TFTP or
DHCP DHCP
- A very basic HTTP server can be optionally enabled to serve files over HTTP
for complex hosts that require additional files (such as a root file system)
after the initial boot sequence.
Warning
~~~~~~~
There is no strong checking of permissions nor robust file path management, so
it is recommended NOT to run this daemon on a host with sensitive content.
Although only read requests are implemented, there is no enforcement or
strong validation of received data and strings from adversary remote clients.
FAQ FAQ
~~~ ~~~
@ -81,7 +101,7 @@ Common errors
``pybootd.pxed.BootpError: Unable to detect network configuration`` ``pybootd.pxed.BootpError: Unable to detect network configuration``
This error is often triggered when the ``pool_start`` address is not This error is often triggered when the ``pool_start`` address is not
part of a valid network. Double check the network configuration and part of a valid network. Double check the network configuration and
fix up the ``[bootp]`` section so that it matches the actual fix up the ``[bootpd]`` section so that it matches the actual
network. If you don't want to allocate addresses dynamically from network. If you don't want to allocate addresses dynamically from
the pool (with ``pool_count = 0``), you still need to specify the pool (with ``pool_count = 0``), you still need to specify
``pool_start`` to some address in the local network you want to ``pool_start`` to some address in the local network you want to
@ -95,19 +115,21 @@ Common errors
Configuration Configuration
------------- -------------
``pybootd`` has a few option switches. The server offers two services: bootp ``pybootd`` has a few option switches. The server offers two services: *bootpd*
(which supports Dhcp and PXE extensions) and tftp. It is possible to disable (which supports DHCP and PXE extensions) and *tftpd*. It is possible to disable
either services. either services.
Usage: pybootd.py [options] Usage: pybootd.py [-h] [-c CONFIG] [-p] [-t] [-d]
PXE boot up server, a tiny BOOTP/DHCP/TFTP server Tiny BOOTP/DHCP/TFTP/PXE server
Options: Options:
-h, --help show this help message and exit -h, --help show this help message and exit
-c CONFIG, --config=CONFIG -c CONFIG, --config CONFIG
configuration file configuration file
-p, --pxe enable BOOTP/DHCP/PXE server only -p, --pxe only enable BOOTP/DHCP/PXE server
-t, --tftp enable TFTP server only -t, --tftp only enable TFTP server
-H, --http enable HTTP server (default: disabled)
-d, --debug enable debug mode
``pybootd`` daemon uses a configuration file, in ``.ini`` format, for all other ``pybootd`` daemon uses a configuration file, in ``.ini`` format, for all other
options. options.
@ -145,8 +167,8 @@ client requests at least an IP address twice:
``file`` ``file``
The path to the output log file, if ``type`` is set to ``file``. The path to the output log file, if ``type`` is set to ``file``.
``[bootp]`` section ``[bootpd]`` section
................... ....................
``access`` ``access``
Type of access control list. If this option is not defined, all BOOTP Type of access control list. If this option is not defined, all BOOTP
@ -228,36 +250,40 @@ client requests at least an IP address twice:
``servername`` ``servername``
Name of the BOOTP server. Name of the BOOTP server.
``[mac]`` section ``[mac]`` section
................. .................
The ``[mac]`` section contains one entry for each MAC address to allow or The ``[mac]`` section contains one entry for each MAC address to allow or
block. The value for each entry is a boolean, *i.e.*:: block. The value for each entry is a boolean, *i.e.*::
AA-BB-CC-DD-EE-FF = enable AA-BB-CC-DD-EE-FF = enable
Note that due to a limitation of the configuration parser, ':' byte separator
in MAC addresses is not allowed, please use '-' separator.
Note that due to a limitation of the configuration parser, ':' byte separator
in MAC addresses is not allowed, please use '-' separator.
``[static_dhcp]`` section ``[static_dhcp]`` section
......................... .........................
The ``[static_dhcp]`` section contains one entry for each MAC The ``[static_dhcp]`` section contains one entry for each MAC
address to associate with a specific IP address. The IP address can be address to associate with a specific IP address. The IP address can be
any IPv4 address in dotted notation, *i.e.*: any IPv4 address in dotted notation, *i.e.*:
AA-BB-CC-DD-EE-FF = 192.168.1.2 AA-BB-CC-DD-EE-FF = 192.168.1.2
The MAC addresses specified here will automatically be allowed,
unless ``[mac]`` section specifies otherwise.
The MAC addresses specified here will automatically be allowed,
unless ``[mac]`` section specifies otherwise.
``[uuid]`` section ``[uuid]`` section
.................. ..................
The ``[uuid]`` section contains one entry for each UUID to allow or block. The ``[uuid]`` section contains one entry for each UUID to allow or block.
The value for each entry is a boolean, *i.e.*:: The value for each entry is a boolean, *i.e.*::
xxxxxxxx-aaaa-bbbb-cccc-yyyyyyyyyyyy = enable
xxxxxxxx-aaaa-bbbb-cccc-yyyyyyyyyyyy = enable
``[http]`` section ``[http]`` section
.................. ..................
@ -281,8 +307,9 @@ The ``pxe``/``dhcp`` option pair enables the remote HTTP server to identify
the boot phase: either a BIOS initialization or an OS boot sequence. When such the boot phase: either a BIOS initialization or an OS boot sequence. When such
differentiation is useless, both options may refer to the same path. differentiation is useless, both options may refer to the same path.
``[tftp]`` section
.................. ``[tftpd]`` section
...................
``address`` ``address``
Address to listen to incoming TFTP requests. When the BOOTP daemon is Address to listen to incoming TFTP requests. When the BOOTP daemon is
@ -310,11 +337,34 @@ differentiation is useless, both options may refer to the same path.
- an absolute path, when the ``root`` option starts with ``/``, - an absolute path, when the ``root`` option starts with ``/``,
- a URL prefix, to access remote files. - a URL prefix, to access remote files.
``[httpd]`` section
...................
``address``
Address to listen to incoming HTTP requests. When the BOOTP daemon is
enabled this option is better omitted, as the address is automatically
received from the BOOTP daemon.
``port``
Alternative port for incoming HTTP request, default to 80
``root``
Base directory for the HTTP service. This path is automatically prepended
to the pathname issued from the TFTP client. It can either point to a local
directory for now.
``check_ip``
Whether to enforce HTTP client IP or not. When enabled, requests from
clients that have not obtained an IP address from the BOOTP daemon are
rejected.
``[filters]`` section ``[filters]`` section
..................... .....................
The ``filters`` section allows on-the-fly pathnames transformation. When a TFTP The ``filters`` section allows on-the-fly pathnames transformation. When a TFTP
client requests some specific filenames, the *tftp* server can translate them client requests some specific filenames, the *tftpd* server can translate them
to other ones. to other ones.
This option is useful to serve the very same configuration file (''e.g.'' This option is useful to serve the very same configuration file (''e.g.''
@ -332,8 +382,8 @@ braces, such as ``{varname}``.
For now, the only supported variable is ``filename``, which is replaced with For now, the only supported variable is ``filename``, which is replaced with
the actual requested filename. the actual requested filename.
The *value* part can also contain a special marker, that tells the *tftp* The *value* part can also contain a special marker, that tells the *tftpd*
daemon to read the replacement pattern from a file. This special marker should server to read the replacement pattern from a file. This special marker should
be written with enclosing brackets, such as ``[file]``. be written with enclosing brackets, such as ``[file]``.
Examples Examples
@ -343,7 +393,7 @@ The following filter::
pxelinux.cfg/* = pybootd/etc/pxe.cfg pxelinux.cfg/* = pybootd/etc/pxe.cfg
tells the *tftp* server that all client requests matching the tells the *tftpd* server that all client requests matching the
``pxelinux.cfg/*`` pattern should be served the ``pybootd/etc/pxe.cfg`` file ``pxelinux.cfg/*`` pattern should be served the ``pybootd/etc/pxe.cfg`` file
instead. This prevents the client to perform the usual time-costing fallback instead. This prevents the client to perform the usual time-costing fallback
requests using UUID, MAC, and suffix addresses before eventually falling requests using UUID, MAC, and suffix addresses before eventually falling
@ -353,7 +403,7 @@ The following filter::
startup = [dir/{filename}.cfg] startup = [dir/{filename}.cfg]
tells the *tftp* server that when the ``startup`` file is requested, it should tells the *tftpd* server that when the ``startup`` file is requested, it should
read out the actual filename from the ``dir/startup.cfg`` file. read out the actual filename from the ``dir/startup.cfg`` file.
HTTP-based authentication HTTP-based authentication
@ -382,15 +432,16 @@ this feature. It can be found within the ``tests/`` subdirectory. See the
``config.ini`` file for this test daemon. The test daemon expects the ``pxe`` ``config.ini`` file for this test daemon. The test daemon expects the ``pxe``
path to be set to ``/boot`` and the ``dhcp`` path to ``/linux``. path to be set to ``/boot`` and the ``dhcp`` path to ``/linux``.
Sample configurations Sample configurations
~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~
Installing a Debian 6.0 machine from the official archive Installing a Debian 6.0 machine from the official archive
--------------------------------------------------------- ---------------------------------------------------------
As the *tftp* daemon is able to retrieve remote files using the HTTP protocol, As pybootd's *tftpd* server is able to retrieve remote files using the HTTP
there is no need to manually download any file from a Debian mirror. The daemon protocol, there is no need to manually download any file from a Debian mirror.
will forward all file requests to the mirror on behalf of the client being The daemon will forward all file requests to the mirror on behalf of the client
installed. being installed.
The ``pybootd.ini`` would contain:: The ``pybootd.ini`` would contain::
@ -400,7 +451,7 @@ The ``pybootd.ini`` would contain::
; show informative and error messages only (disable verbose mode) ; show informative and error messages only (disable verbose mode)
level = info level = info
[bootp] [bootpd]
; do not force a full PXE boot-up cycle to accept the client ; do not force a full PXE boot-up cycle to accept the client
allow_simple_dhcp = enable allow_simple_dhcp = enable
; First BOOTP/DHCP address to generate ; First BOOTP/DHCP address to generate
@ -410,7 +461,7 @@ The ``pybootd.ini`` would contain::
; boot-up executable the client should request through TFTP ; boot-up executable the client should request through TFTP
boot_file = pxelinux.0 boot_file = pxelinux.0
[tftp] [tftpd]
; URL to install a Debian 6.0 Intel/AMD 64-bit network installation ; URL to install a Debian 6.0 Intel/AMD 64-bit network installation
root = http://http.us.debian.org/debian/dists/squeeze/main/installer-amd64/current/images/netboot root = http://http.us.debian.org/debian/dists/squeeze/main/installer-amd64/current/images/netboot

View File

@ -1,7 +1,7 @@
#!/usr/bin/env python #!/usr/bin/env python3
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
# Copyright (c) 2010-2016 Emmanuel Blot <emmanuel.blot@free.fr> # Copyright (c) 2010-2019 Emmanuel Blot <emmanuel.blot@free.fr>
# Copyright (c) 2010-2011 Neotion # Copyright (c) 2010-2011 Neotion
# #
# This library is free software; you can redistribute it and/or # This library is free software; you can redistribute it and/or

View File

@ -1,6 +1,4 @@
# -*- coding: utf-8 -*- # Copyright (c) 2010-2019 Emmanuel Blot <emmanuel.blot@free.fr>
#
# Copyright (c) 2010-2016 Emmanuel Blot <emmanuel.blot@free.fr>
# Copyright (c) 2010-2011 Neotion # Copyright (c) 2010-2011 Neotion
# #
# This library is free software; you can redistribute it and/or # This library is free software; you can redistribute it and/or
@ -18,33 +16,8 @@
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
import os import os
import sys
__version__ = '1.7.0'
def _get_package_name(default='', version='1.5.0'):
try:
from pkg_resources import WorkingSet
except ImportError:
ws = []
else:
ws = WorkingSet()
_path, _ = os.path.split(os.path.dirname(
sys.modules['pybootd'].__file__))
_path = os.path.normpath(_path)
if 'nt' not in os.name:
for dist in ws:
if os.path.samefile(os.path.normpath(dist.location), _path):
return dist.project_name, dist.version
else: # tweak for windows
_path = os.path.abspath(_path).lower()
for dist in ws:
if 'pybootd' in dist.location:
if _path == os.path.abspath(dist.location).lower():
return dist.project_name, dist.version
return default, version
PRODUCT_NAME, __version__ = _get_package_name('pybootd')
def pybootd_path(path): def pybootd_path(path):
@ -61,9 +34,9 @@ def pybootd_path(path):
except ImportError: except ImportError:
raise IOError('pkg_resources module not available') raise IOError('pkg_resources module not available')
try: try:
newpath = resource_filename(Requirement.parse(PRODUCT_NAME), path) newpath = resource_filename(Requirement.parse('pybootd'), path)
if not newpath: if not newpath:
localpath = get_distribution(PRODUCT_NAME).location localpath = get_distribution('pybootd').location
newpath = os.path.join(localpath, path) newpath = os.path.join(localpath, path)
except DistributionNotFound: except DistributionNotFound:
newpath = path newpath = path

View File

@ -1,7 +1,6 @@
#!/usr/bin/env python #!/usr/bin/env python3
# -*- coding: utf-8 -*-
# #
# Copyright (c) 2010-2016 Emmanuel Blot <emmanuel.blot@free.fr> # Copyright (c) 2010-2019 Emmanuel Blot <emmanuel.blot@free.fr>
# Copyright (c) 2010-2011 Neotion # Copyright (c) 2010-2011 Neotion
# #
# This library is free software; you can redistribute it and/or # This library is free software; you can redistribute it and/or
@ -18,69 +17,98 @@
# License along with this library; if not, write to the Free Software # License along with this library; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
"""Boot up server, a tiny BOOTP/DHCP/TFTP/PXE server""" """Tiny BOOTP/DHCP/TFTP/PXE server"""
from argparse import ArgumentParser
import os from collections import OrderedDict
import sys from os.path import isfile
from pxed import BootpServer
from pybootd import pybootd_path, PRODUCT_NAME, __version__ as VERSION
from six import print_
from tftpd import TftpServer
from util import logger_factory, EasyConfigParser
from threading import Thread from threading import Thread
from sys import exit as sysexit, modules, stderr
from traceback import format_exc
from . import pybootd_path, __version__
from .httpd import HttpServer
from .pxed import BootpServer
from .tftpd import TftpServer
from .util import logger_factory, EasyConfigParser
#pylint: disable-msg=broad-except
#pylint: disable-msg=missing-docstring
#pylint: disable-msg=invalid-name
class BootpDaemon(Thread): class Daemon(Thread):
def __init__(self, logger, config): def __init__(self, debug):
super(BootpDaemon, self).__init__(name="BootpDeamon") super(Daemon, self).__init__(name=self.__class__.__name__, daemon=True)
self.daemon = True self._server = None
self._debug = debug
def run(self):
try:
self._server.start()
except KeyboardInterrupt:
raise
except Exception as exc:
print('\nError: %s' % exc, stderr)
if self._debug:
print(format_exc(chain=False), file=stderr)
raise
def stop(self):
self._server.stop()
class BootpDaemon(Daemon):
def __init__(self, logger, config, debug):
super(BootpDaemon, self).__init__(debug)
self._server = BootpServer(logger=logger, config=config) self._server = BootpServer(logger=logger, config=config)
def get_netconfig(self): def get_netconfig(self):
return self._server.get_netconfig() return self._server.get_netconfig()
def is_managed_ip(self, ip):
return self._server.is_managed_ip(ip)
def get_filename(self, ip): def get_filename(self, ip):
return self._server.get_filename(ip) return self._server.get_filename(ip)
def run(self):
self._server.bind()
self._server.forever()
class TftpDaemon(Daemon):
class TftpDaemon(Thread): def __init__(self, logger, config, debug, bootpd=None):
super(TftpDaemon, self).__init__(debug)
def __init__(self, logger, config, bootpd=None):
super(TftpDaemon, self).__init__(name="TftpDeamon")
self.daemon = True
self._server = TftpServer(logger=logger, config=config, bootpd=bootpd) self._server = TftpServer(logger=logger, config=config, bootpd=bootpd)
def run(self):
self._server.bind() class HttpDaemon(Daemon):
self._server.forever()
def __init__(self, logger, config, debug, bootpd=None):
super(HttpDaemon, self).__init__(debug)
self.daemon = True
self._server = HttpServer(logger=logger, config=config, bootpd=bootpd)
def main(): def main():
debug = False debug = False
try: try:
from argparse import ArgumentParser argparser = ArgumentParser(description=modules[__name__].__doc__)
argparser = ArgumentParser(description=sys.modules[__name__].__doc__) argparser.add_argument('-c', '--config',
argparser.add_argument('-c', '--config', dest='config',
default='pybootd/etc/pybootd.ini', default='pybootd/etc/pybootd.ini',
help='configuration file') help='configuration file')
argparser.add_argument('-p', '--pxe', dest='pxe', argparser.add_argument('-p', '--pxe', action='store_true',
action='store_true', help='only enable BOOTP/DHCP/PXE server')
help='enable BOOTP/DHCP/PXE server only') argparser.add_argument('-t', '--tftp', action='store_true',
argparser.add_argument('-t', '--tftp', dest='tftp', help='only enable TFTP server')
action='store_true', argparser.add_argument('-H', '--http', action='store_true',
help='enable TFTP server only') help='enable HTTP server (default: disabled)')
argparser.add_argument('-d', '--debug', action='store_true', argparser.add_argument('-d', '--debug', action='store_true',
help='enable debug mode') help='enable debug mode')
args = argparser.parse_args() args = argparser.parse_args()
debug = args.debug debug = args.debug
if not os.path.isfile(args.config): if not isfile(args.config):
argparser.error('Invalid configuration file') argparser.error('Invalid configuration file')
if args.pxe and args.tftp: if args.pxe and args.tftp:
@ -88,32 +116,47 @@ def main():
cfgparser = EasyConfigParser() cfgparser = EasyConfigParser()
with open(pybootd_path(args.config), 'rt') as config: with open(pybootd_path(args.config), 'rt') as config:
cfgparser.readfp(config) cfgparser.read_file(config)
logger = logger_factory(logtype=cfgparser.get('logger', 'type', logger = logger_factory(logtype=cfgparser.get('logger', 'type',
'stderr'), 'stderr'),
logfile=cfgparser.get('logger', 'file'), logfile=cfgparser.get('logger', 'file'),
level=cfgparser.get('logger', 'level', level=cfgparser.get('logger', 'level',
'info')) 'info'))
logger.info('-'.join((PRODUCT_NAME, VERSION))) logger.info('-'.join(('pybootd', __version__)))
daemon = None daemons = OrderedDict()
if not args.tftp: if not args.tftp:
daemon = BootpDaemon(logger, cfgparser) daemon = BootpDaemon(logger, cfgparser, debug)
daemon.start() daemon.start()
daemons['bootp'] = daemon
if not args.pxe: if not args.pxe:
daemon = TftpDaemon(logger, cfgparser, daemon) daemon = TftpDaemon(logger, cfgparser, debug,
daemons.get('bootp', None))
daemon.start() daemon.start()
if daemon: daemons['tftp'] = daemon
while True: if args.http:
daemon.join(0.5) daemon = HttpDaemon(logger, cfgparser, debug, daemons.get('bootp'))
daemon.start()
daemons['http'] = daemon
resume = True
while daemons:
zombies = set()
for name, daemon in daemons.items():
if not resume:
daemon.stop()
daemon.join(0.1)
if not daemon.is_alive(): if not daemon.is_alive():
break logger.warn('%s daemon terminated', name)
except Exception as e: zombies.add(name)
print_('\nError: %s' % e, file=sys.stderr) resume = False
for name in zombies:
del daemons[name]
except Exception as exc:
print('\nError: %s' % exc, file=stderr)
if debug: if debug:
import traceback print(format_exc(chain=False), file=stderr)
print_(traceback.format_exc(), file=sys.stderr) sysexit(1)
sys.exit(1)
except KeyboardInterrupt: except KeyboardInterrupt:
print_("Aborting...") print("\nAborting...", file=stderr)
sysexit(2)

View File

@ -2,7 +2,7 @@
type = stderr type = stderr
level = info level = info
[bootp] [bootpd]
address = 0.0.0.0 address = 0.0.0.0
; pool_start should be in a valid subnet ; pool_start should be in a valid subnet
pool_start = 192.168.25.100 pool_start = 192.168.25.100
@ -13,14 +13,14 @@ boot_file = pxelinux.0
lease_time = 86400 lease_time = 86400
access = mac access = mac
allow_simple_dhcp = enable allow_simple_dhcp = enable
dns = 10.130.0.2 dns = 8.8.8.8
boot_file = pxelinux.0
set_gateway = true set_gateway = true
; use "nc -l -u 127.0.0.1 -p 12345" to debug ; use "nc -l -u 127.0.0.1 -p 12345" to debug
; notify = 192.168.26.201:12345;192.168.26.200:12345 ; notify = 192.168.26.201:12345;192.168.26.200:12345
[mac] [mac]
00-1E-4F-C4-95-EE = enable ; see doc: byte separator should be defined with dash, not column
00-AA-55-12-34-56 = enable
[uuid] [uuid]
12345678-abcd-ef00-1111-abcdefabcdef = enable 12345678-abcd-ef00-1111-abcdefabcdef = enable
@ -32,10 +32,16 @@ pxe = boot
dhcp = linux dhcp = linux
always_check = disable always_check = disable
[tftp] [tftpd]
;address = (use address from bootpd) ;address = (use address from bootpd)
;root = ./images root = ./images
root = http://http.us.debian.org/debian/dists/squeeze/main/installer-amd64/current/images/netboot ;root = http://http.us.debian.org/debian/dists/squeeze/main/installer-amd64/current/images/netboot
[httpd]
; address = (use address from bootpd)
port = 8000
check_ip = yes
root = /tmp
[filters] [filters]
;pxelinux.cfg/* = pybootd/etc/pxe.cfg ;pxelinux.cfg/* = pybootd/etc/pxe.cfg

99
pybootd/httpd.py Executable file
View File

@ -0,0 +1,99 @@
# Copyright (c) 2010-2019 Emmanuel Blot <emmanuel.blot@free.fr>
# Copyright (c) 2010-2011 Neotion
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
"""HTTPd tiny server to exercise the pybootd daemon"""
from argparse import ArgumentParser, FileType
from functools import partial
from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer
from os import getcwd
from os.path import isfile, join as joinpath, realpath
from sys import exit as sysexit, modules, stderr
from traceback import format_exc
from urllib.parse import parse_qs, urlsplit
from .util import logger_factory, to_bool, to_int, EasyConfigParser
#pylint: disable-msg=broad-except
#pylint: disable-msg=missing-docstring
#pylint: disable-msg=invalid-name
class HttpRequestHandler(SimpleHTTPRequestHandler):
def __init__(self, *args, **kwargs):
super(HttpRequestHandler, self).__init__(*args, **kwargs)
def do_HEAD(self):
if self._validate():
super(HttpRequestHandler, self).do_HEAD()
def do_GET(self):
if self._validate():
super(HttpRequestHandler, self).do_GET()
def log_request(self, code='-', size='-'):
self.server.log.info('Request: %s %s', code, size)
def log_error(self, fmt, *args):
self.server.log.error(fmt, *args)
def _validate(self):
log = self.server.log
bootpd = self.server.bootpd
if bootpd and self.server.check_ip:
ipaddr = self.client_address[0]
if not bootpd.is_managed_ip(ipaddr):
log.info('Unknown IP: %s', ipaddr)
self.send_error(401, 'Not authorized')
return False
log.debug("Request: %s" % self.path)
path = realpath(joinpath(self.directory, self.path.lstrip('/')))
if not path.startswith(self.directory):
log.info('Malformed path: %s', path)
self.send_error(403, 'Forbidden')
return False
if not isfile(path):
log.info('Invalid path: %s', path)
self.send_error(404, 'Not found')
return False
return True
class HttpServer(ThreadingHTTPServer):
HTTP_SECTION = 'httpd'
def __init__(self, logger, config, bootpd=None):
self.log = logger
self.config = config
self.bootpd = bootpd
netconfig = bootpd and bootpd.get_netconfig()
address = (self.config.get(self.HTTP_SECTION, 'address',
netconfig and netconfig['server']),
int(self.config.get(self.HTTP_SECTION, 'port', '80')))
root = realpath(self.config.get(self.HTTP_SECTION, 'root', None))
self.check_ip = to_bool(self.config.get(self.HTTP_SECTION, 'check_ip',
'yes'))
self.log.info('Listening to %s:%s' % address)
handler = partial(HttpRequestHandler, directory=root)
super(HttpServer, self).__init__(address, handler)
def start(self):
self.serve_forever()
def stop(self):
self.shutdown()

View File

@ -1,6 +1,4 @@
# -*- coding: utf-8 -*- # Copyright (c) 2010-2019 Emmanuel Blot <emmanuel.blot@free.fr>
#
# Copyright (c) 2010-2016 Emmanuel Blot <emmanuel.blot@free.fr>
# Copyright (c) 2010-2011 Neotion # Copyright (c) 2010-2011 Neotion
# #
# This library is free software; you can redistribute it and/or # This library is free software; you can redistribute it and/or
@ -17,27 +15,40 @@
# License along with this library; if not, write to the Free Software # License along with this library; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
import re
import select
import socket
import string
import struct
import sys
import time
from binascii import hexlify from binascii import hexlify
from pybootd import PRODUCT_NAME from re import compile as recompile
from select import select
from socket import (inet_aton, inet_ntoa, socket,
AF_INET, SOCK_DGRAM, IPPROTO_UDP, SOL_SOCKET,
SO_BROADCAST, SO_REUSEADDR)
from struct import calcsize as scalc, pack as spack, unpack as sunpack
from time import sleep
from traceback import format_exc
from urllib.error import HTTPError, URLError
from urllib.parse import urlencode, urlunsplit
from urllib.request import urlopen
from .util import hexline, to_bool, iptoint, inttoip, get_iface_config from .util import hexline, to_bool, iptoint, inttoip, get_iface_config
#pylint: disable-msg=broad-except
#pylint: disable-msg=invalid-name
#pylint: disable-msg=missing-docstring
#pylint: disable-msg=too-many-return-statements
#pylint: disable-msg=too-many-branches
#pylint: disable-msg=too-many-locals
#pylint: disable-msg=too-many-statements
#pylint: disable-msg=too-many-nested-blocks
BOOTP_PORT_REQUEST = 67 BOOTP_PORT_REQUEST = 67
BOOTP_PORT_REPLY = 68 BOOTP_PORT_REPLY = 68
BOOTREQUEST = 1 BOOTREQUEST = 1
BOOTREPLY = 2 BOOTREPLY = 2
BOOTPFormat = '!4bIHH4s4s4s4s16s64s128s64s' BOOTPFORMAT = '!4bIHH4s4s4s4s16s64s128s64s'
BOOTPFormatSize = struct.calcsize(BOOTPFormat) BOOTPFORMATSIZE = scalc(BOOTPFORMAT)
DHCPFormat = '!4bIHH4s4s4s4s16s64s128s4s' DHCPFORMAT = '!4bIHH4s4s4s4s16s64s128s4s'
DHCPFormatSize = struct.calcsize(DHCPFormat) DHCPFORMATSIZE = scalc(DHCPFORMAT)
(BOOTP_OP, BOOTP_HTYPE, BOOTP_HLEN, BOOTP_HOPS, BOOTP_XID, BOOTP_SECS, (BOOTP_OP, BOOTP_HTYPE, BOOTP_HLEN, BOOTP_HOPS, BOOTP_XID, BOOTP_SECS,
BOOTP_FLAGS, BOOTP_CIADDR, BOOTP_YIADDR, BOOTP_SIADDR, BOOTP_GIADDR, BOOTP_FLAGS, BOOTP_CIADDR, BOOTP_YIADDR, BOOTP_SIADDR, BOOTP_GIADDR,
@ -46,7 +57,7 @@ DHCPFormatSize = struct.calcsize(DHCPFormat)
BOOTP_FLAGS_NONE = 0 BOOTP_FLAGS_NONE = 0
BOOTP_FLAGS_BROADCAST = 1<<15 BOOTP_FLAGS_BROADCAST = 1<<15
COOKIE='\0x63\0x82\0x53\0x63' COOKIE = r'\0x63\0x82\0x53\0x63'
DHCP_OPTIONS = {0: 'Byte padding', DHCP_OPTIONS = {0: 'Byte padding',
1: 'Subnet mask', 1: 'Subnet mask',
@ -169,6 +180,8 @@ class BootpServer:
ACCESS_REMOTE = ['http'] # Access modes, remotely retrieved ACCESS_REMOTE = ['http'] # Access modes, remotely retrieved
(ST_IDLE, ST_PXE, ST_DHCP) = range(3) # Current state (ST_IDLE, ST_PXE, ST_DHCP) = range(3) # Current state
BOOTP_SECTION = 'bootpd'
def __init__(self, logger, config): def __init__(self, logger, config):
self.sock = [] self.sock = []
self.log = logger self.log = logger
@ -177,26 +190,24 @@ class BootpServer:
self.ippool = {} # key MAC address string, value assigned IP string self.ippool = {} # key MAC address string, value assigned IP string
self.filepool = {} # key IP string, value pathname self.filepool = {} # key IP string, value pathname
self.states = {} # key MAC address string, value client state self.states = {} # key MAC address string, value client state
name_ = PRODUCT_NAME.split('-') self.pool_start = self.config.get(self.BOOTP_SECTION, 'pool_start')
name_[0] = 'bootp'
self.bootp_section = '_'.join(name_)
self.pool_start = self.config.get(self.bootp_section, 'pool_start')
if not self.pool_start: if not self.pool_start:
raise BootpError('Missing pool_start definition') raise BootpError('Missing pool_start definition')
self.pool_count = int(self.config.get(self.bootp_section, self.pool_count = int(self.config.get(self.BOOTP_SECTION,
'pool_count', '10')) 'pool_count', '10'))
self.netconfig = get_iface_config(self.pool_start) self.netconfig = get_iface_config(self.pool_start)
if not self.netconfig: if not self.netconfig:
host = self.config.get(self.bootp_section, 'address', '0.0.0.0') host = self.config.get(self.BOOTP_SECTION, 'address', '0.0.0.0')
self.netconfig = get_iface_config(host) self.netconfig = get_iface_config(host)
if not self.netconfig: if not self.netconfig:
raise BootpError('Unable to detect network configuration') # the available networks on the host may not match the config...
raise BootpError('Unable to detect a matching network config')
keys = sorted(self.netconfig.keys()) keys = sorted(self.netconfig.keys())
self.log.info('Using %s' % ', '.join(map( self.log.info('Using %s' % ', '.join(map(
':'.join, zip(keys, [self.netconfig[k] for k in keys])))) ':'.join, zip(keys, [self.netconfig[k] for k in keys]))))
nlist = self.config.get(self.bootp_section, 'notify') nlist = self.config.get(self.BOOTP_SECTION, 'notify')
self.notify = [] self.notify = []
if nlist: if nlist:
try: try:
@ -204,9 +215,9 @@ class BootpServer:
for n in nlist: for n in nlist:
n = n.strip().split(':') n = n.strip().split(':')
self.notify.append((n[0], int(n[1]))) self.notify.append((n[0], int(n[1])))
except Exception, e: except Exception as exc:
raise BootpError('Invalid notification URL: %s' % str(e)) raise BootpError('Invalid notification URL: %s' % exc)
access = self.config.get(self.bootp_section, 'access') access = self.config.get(self.BOOTP_SECTION, 'access')
if not access: if not access:
self.acl = None self.acl = None
else: else:
@ -235,7 +246,7 @@ class BootpServer:
msg = ','.join([notice, uuid_str, mac_str, ip]) msg = ','.join([notice, uuid_str, mac_str, ip])
else: else:
msg = ','.join([notice, mac_str, ip]) msg = ','.join([notice, mac_str, ip])
notify_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) notify_sock = socket(AF_INET, SOCK_DGRAM)
for n in self.notify: for n in self.notify:
self.log.info('Notifying %s with %s' % (n, msg)) self.log.info('Notifying %s with %s' % (n, msg))
notify_sock.sendto(msg, n) notify_sock.sendto(msg, n)
@ -244,42 +255,52 @@ class BootpServer:
def get_netconfig(self): def get_netconfig(self):
return self.netconfig return self.netconfig
def bind(self): def is_managed_ip(self, address):
host = self.config.get(self.bootp_section, 'address', '0.0.0.0') return address in self.ippool.values()
port = self.config.get(self.bootp_section, 'port',
def start(self):
host = self.config.get(self.BOOTP_SECTION, 'address', '0.0.0.0')
port = self.config.get(self.BOOTP_SECTION, 'port',
str(BOOTP_PORT_REQUEST)) str(BOOTP_PORT_REQUEST))
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, sock = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP)
socket.IPPROTO_UDP) sock.setsockopt(SOL_SOCKET, SO_BROADCAST, 1)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) sock.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
self.sock.append(sock) self.sock.append(sock)
self.log.info('Listening to %s:%s' % (host, port)) self.log.info('Listening to %s:%s' % (host, port))
sock.bind((host, int(port))) sock.bind((host, int(port)))
self._resume = True
def forever(self): while self._resume:
while True:
try: try:
r, w, e = select.select(self.sock, [], self.sock) r = select(self.sock, [], self.sock, 0.25)[0]
if not r:
continue
for sock in r: for sock in r:
data, addr = sock.recvfrom(556) data, addr = sock.recvfrom(556)
self.handle(sock, addr, data) self.handle(sock, addr, data)
except Exception, e: except Exception as exc:
import traceback self.log.critical('%s\n%s' % (exc, format_exc()))
self.log.critical('%s\n%s' % (str(e), traceback.format_exc())) sleep(1)
time.sleep(1)
def stop(self):
self._resume = False
def parse_options(self, tail): def parse_options(self, tail):
self.log.debug('Parsing DHCP options') self.log.debug('Parsing DHCP options')
dhcp_tags = {} dhcp_tags = {}
padding_count = 0
while tail: while tail:
tag = ord(tail[0]) tag = tail[0]
# padding # padding
if tag == 0: if tag == 0:
padding_count += 1
if padding_count > 255:
raise ValueError('Padding overflow')
continue continue
padding_count = 0
if tag == 0xff: if tag == 0xff:
return dhcp_tags return dhcp_tags
length = ord(tail[1]) length = tail[1]
(value, ) = struct.unpack('!%ss' % length, tail[2:2+length]) (value, ) = sunpack('!%ss' % length, tail[2:2+length])
tail = tail[2+length:] tail = tail[2+length:]
try: try:
option = DHCP_OPTIONS[tag] option = DHCP_OPTIONS[tag]
@ -292,47 +313,45 @@ class BootpServer:
dhcp_tags[tag] = value dhcp_tags[tag] = value
def build_pxe_options(self, options, server): def build_pxe_options(self, options, server):
buf = b''
try: try:
buf = ''
uuid = options[97] uuid = options[97]
buf += struct.pack('!BB%ds' % len(uuid), buf += spack('!BB%ds' % len(uuid),
97, len(uuid), uuid) 97, len(uuid), uuid)
clientclass = options[60] clientclass = options[60]
clientclass = clientclass[:clientclass.find(':')] clientclass = clientclass[:clientclass.find(':')]
buf += struct.pack('!BB%ds' % len(clientclass), buf += spack('!BB%ds' % len(clientclass),
60, len(clientclass), clientclass) 60, len(clientclass), clientclass)
vendor = '' vendor = ''
vendor += struct.pack('!BBB', PXE_DISCOVERY_CONTROL, 1, 0x0A) vendor += spack('!BBB', PXE_DISCOVERY_CONTROL, 1, 0x0A)
vendor += struct.pack('!BBHB4s', PXE_BOOT_SERVERS, 2+1+4, vendor += spack('!BBHB4s', PXE_BOOT_SERVERS, 2+1+4,
0, 1, server) 0, 1, server)
srvstr = 'Python' srvstr = 'Python'
vendor += struct.pack('!BBHB%ds' % len(srvstr), PXE_BOOT_MENU, vendor += spack('!BBHB%ds' % len(srvstr), PXE_BOOT_MENU,
2+1+len(srvstr), 0, len(srvstr), srvstr) 2+1+len(srvstr), 0, len(srvstr), srvstr)
prompt = 'Stupid PXE' prompt = 'Stupid PXE'
vendor += struct.pack('!BBB%ds' % len(prompt), PXE_MENU_PROMPT, vendor += spack('!BBB%ds' % len(prompt), PXE_MENU_PROMPT,
1+len(prompt), len(prompt), prompt) 1+len(prompt), len(prompt), prompt)
buf += struct.pack('!BB%ds' % len(vendor), 43, buf += spack('!BB%ds' % len(vendor), 43,
len(vendor), vendor) len(vendor), vendor)
buf += struct.pack('!BBB', 255, 0, 0) buf += spack('!BBB', 255, 0, 0)
return buf return buf
except KeyError, e: except KeyError as exc:
self.log.error('Missing options, cancelling: ' + str(e)) self.log.error('Missing options, cancelling: %s' % exc)
return None return b''
def build_dhcp_options(self, clientname): def build_dhcp_options(self, clientname):
buf = ''
if not clientname: if not clientname:
return buf return b''
buf += struct.pack('!BB%ds' % len(clientname), return spack('!BB%ds' % len(clientname),
12, len(clientname), clientname) 12, len(clientname), clientname)
return buf
def handle(self, sock, addr, data): def handle(self, sock, addr, data):
self.log.info('Sender: %s on socket %s' % (addr, sock.getsockname())) self.log.info('Sender: %s on socket %s' % (addr, sock.getsockname()))
if len(data) < DHCPFormatSize: if len(data) < DHCPFORMATSIZE:
self.log.error('Cannot be a DHCP or BOOTP request - too small!') self.log.error('Cannot be a DHCP or BOOTP request - too small!')
tail = data[DHCPFormatSize:] tail = data[DHCPFORMATSIZE:]
buf = list(struct.unpack(DHCPFormat, data[:DHCPFormatSize])) buf = list(sunpack(DHCPFORMAT, data[:DHCPFORMATSIZE]))
if buf[BOOTP_OP] != BOOTREQUEST: if buf[BOOTP_OP] != BOOTREQUEST:
self.log.warn('Not a BOOTREQUEST') self.log.warn('Not a BOOTREQUEST')
return return
@ -343,13 +362,13 @@ class BootpServer:
# Extras (DHCP options) # Extras (DHCP options)
try: try:
dhcp_msg_type = ord(options[53][0]) dhcp_msg_type = options[53][0]
except KeyError: except KeyError:
dhcp_msg_type = None dhcp_msg_type = None
server_addr = self.netconfig['server'] server_addr = self.netconfig['server']
mac_addr = buf[BOOTP_CHADDR][:6] mac_addr = buf[BOOTP_CHADDR][:6]
mac_str = ':'.join(['%02X' % ord(x) for x in mac_addr]) mac_str = ':'.join(['%02X' % x for x in mac_addr])
# is the UUID received (PXE mode) # is the UUID received (PXE mode)
if 97 in options and len(options[97]) == 17: if 97 in options and len(options[97]) == 17:
uuid = options[97][1:] uuid = options[97][1:]
@ -360,9 +379,9 @@ class BootpServer:
uuid = self.uuidpool.get(mac_addr, None) uuid = self.uuidpool.get(mac_addr, None)
pxe = False pxe = False
self.log.info('PXE UUID not present in request') self.log.info('PXE UUID not present in request')
uuid_str = uuid and ('%s-%s-%s-%s-%s' % tuple( uuid_str = uuid and ('%s-%s-%s-%s-%s' % tuple([hexlify(x)
[hexlify(x) for x in uuid[0:4], uuid[4:6], uuid[6:8], for x in (uuid[0:4], uuid[4:6], uuid[6:8], uuid[8:10], uuid[10:16])
uuid[8:10], uuid[10:16]])).upper() ])).upper()
if uuid_str: if uuid_str:
self.log.info('UUID is %s for MAC %s' % (uuid_str, mac_str)) self.log.info('UUID is %s for MAC %s' % (uuid_str, mac_str))
@ -388,12 +407,12 @@ class BootpServer:
# if the state has not evolved from idle, there is nothing to do # if the state has not evolved from idle, there is nothing to do
if newstate == self.ST_IDLE: if newstate == self.ST_IDLE:
self.log.info('Request from %s ignored (idle state)' % mac_str)
sdhcp = 'allow_simple_dhcp' sdhcp = 'allow_simple_dhcp'
simple_dhcp = \ simple_dhcp = \
self.config.has_option(self.bootp_section, sdhcp) and \ self.config.has_option(self.BOOTP_SECTION, sdhcp) and \
to_bool(self.config.get(self.bootp_section, sdhcp)) to_bool(self.config.get(self.BOOTP_SECTION, sdhcp))
if not simple_dhcp: if not simple_dhcp:
self.log.info('Request from %s ignored (idle state)' % mac_str)
return return
if not dhcp_msg_type: if not dhcp_msg_type:
# Legacy DHCP: assuming discover by default # Legacy DHCP: assuming discover by default
@ -404,8 +423,6 @@ class BootpServer:
# remote access is always validated on each request # remote access is always validated on each request
if self.access in self.ACCESS_REMOTE: if self.access in self.ACCESS_REMOTE:
# need to query a host to grant or reject access # need to query a host to grant or reject access
import urlparse
import urllib
netloc = self.config.get(self.access, 'location') netloc = self.config.get(self.access, 'location')
path = self.config.get(self.access, pxe and 'pxe' or 'dhcp') path = self.config.get(self.access, pxe and 'pxe' or 'dhcp')
timeout = int(self.config.get(self.access, 'timeout', '5')) timeout = int(self.config.get(self.access, 'timeout', '5'))
@ -422,14 +439,12 @@ class BootpServer:
if to_bool(always_check): if to_bool(always_check):
checkhost = True checkhost = True
if checkhost: if checkhost:
query = urllib.urlencode(parameters) query = urlencode(parameters)
urlparts = (self.access, netloc, path, query, '') urlparts = (self.access, netloc, path, query, '')
url = urlparse.urlunsplit(urlparts) url = urlunsplit(urlparts)
self.log.info('Requesting URL: %s' % url) self.log.info('Requesting URL: %s' % url)
import urllib2
import httplib
try: try:
up = urllib2.urlopen(url, timeout=timeout) up = urlopen(url, timeout=timeout)
for l in up: for l in up:
try: try:
# Look for extra definition within the reply # Look for extra definition within the reply
@ -441,16 +456,12 @@ class BootpServer:
filename = v filename = v
except ValueError: except ValueError:
pass pass
except urllib2.HTTPError, e: except HTTPError as exc:
self.log.error('HTTP Error: %s' % str(e)) self.log.error('HTTP Error: %s' % exc)
self.states[mac_str] = self.ST_IDLE self.states[mac_str] = self.ST_IDLE
return return
except urllib2.URLError, e: except URLError as exc:
self.log.critical('Internal error: %s' % str(e)) self.log.critical('Internal error: %s' % exc)
self.states[mac_str] = self.ST_IDLE
return
except httplib.HTTPException, e:
self.log.error('Server error: %s' % type(e))
self.states[mac_str] = self.ST_IDLE self.states[mac_str] = self.ST_IDLE
return return
# local access is only validated if mac address is not yet known # local access is only validated if mac address is not yet known
@ -476,9 +487,9 @@ class BootpServer:
# construct reply # construct reply
buf[BOOTP_HOPS] = 0 buf[BOOTP_HOPS] = 0
buf[BOOTP_OP] = BOOTREPLY buf[BOOTP_OP] = BOOTREPLY
self.log.info('Client IP: %s' % socket.inet_ntoa(buf[7])) ciaddr = buf[BOOTP_CIADDR]
if buf[BOOTP_CIADDR] == '\x00\x00\x00\x00': if not sunpack('!I', ciaddr)[0]:
self.log.debug('Client needs its address') self.log.info('Client needs its address')
ipaddr = iptoint(self.pool_start) ipaddr = iptoint(self.pool_start)
ip = None ip = None
if mac_str in self.ippool: if mac_str in self.ippool:
@ -486,7 +497,7 @@ class BootpServer:
self.log.info('Lease for MAC %s already defined as IP %s' % self.log.info('Lease for MAC %s already defined as IP %s' %
(mac_str, ip)) (mac_str, ip))
else: else:
for idx in xrange(self.pool_count): for idx in range(self.pool_count):
ipkey = inttoip(ipaddr+idx) ipkey = inttoip(ipaddr+idx)
self.log.debug('Check for IP %s' % ipkey) self.log.debug('Check for IP %s' % ipkey)
if ipkey not in self.ippool.values(): if ipkey not in self.ippool.values():
@ -497,32 +508,33 @@ class BootpServer:
raise BootpError('No more IP available in definined pool') raise BootpError('No more IP available in definined pool')
mask = iptoint(self.config.get( mask = iptoint(self.config.get(
self.bootp_section, 'netmask', self.netconfig['mask'])) self.BOOTP_SECTION, 'netmask', self.netconfig['mask']))
reply_broadcast = iptoint(ip) & mask reply_broadcast = iptoint(ip) & mask
reply_broadcast |= (~mask) & ((1 << 32)-1) reply_broadcast |= (~mask) & ((1 << 32)-1)
buf[BOOTP_YIADDR] = socket.inet_aton(ip) buf[BOOTP_YIADDR] = inet_aton(ip)
buf[BOOTP_SECS] = 0 buf[BOOTP_SECS] = 0
buf[BOOTP_FLAGS] = BOOTP_FLAGS_BROADCAST buf[BOOTP_FLAGS] = BOOTP_FLAGS_BROADCAST
relay = buf[BOOTP_GIADDR] relay = buf[BOOTP_GIADDR]
if relay != b'\x00\x00\x00\x00': if sunpack('!I', relay)[0]:
addr = (socket.inet_ntoa(relay), addr[1]) addr = (inet_ntoa(relay), addr[1])
else: else:
addr = (inttoip(reply_broadcast), addr[1]) addr = (inttoip(reply_broadcast), addr[1])
self.log.info('Reply to: %s:%s' % addr) self.log.info('Reply to: %s:%s' % addr)
else: else:
buf[BOOTP_YIADDR] = buf[BOOTP_CIADDR] self.log.info('Client IP: %s' % inet_ntoa(ciaddr))
ip = socket.inet_ntoa(buf[BOOTP_YIADDR]) buf[BOOTP_YIADDR] = ciaddr
buf[BOOTP_SIADDR] = socket.inet_aton(server_addr) ip = inet_ntoa(buf[BOOTP_YIADDR])
buf[BOOTP_SIADDR] = inet_aton(server_addr)
# sname # sname
buf[BOOTP_SNAME] = \ buf[BOOTP_SNAME] = \
'.'.join([self.config.get(self.bootp_section, '.'.join([self.config.get(self.BOOTP_SECTION,
'servername', 'unknown'), 'servername', 'unknown'),
self.config.get(self.bootp_section, self.config.get(self.BOOTP_SECTION,
'domain', 'localdomain')]) 'domain', 'localdomain')]).encode()
# file # file
buf[BOOTP_FILE] = self.config.get(self.bootp_section, buf[BOOTP_FILE] = self.config.get(self.BOOTP_SECTION,
'boot_file', '\x00') 'boot_file', '\x00').encode()
if not dhcp_msg_type: if not dhcp_msg_type:
self.log.warn('No DHCP message type found, discarding request') self.log.warn('No DHCP message type found, discarding request')
@ -566,38 +578,37 @@ class BootpServer:
else: else:
self.log.debug('No filename defined for IP %s' % ip) self.log.debug('No filename defined for IP %s' % ip)
pkt = struct.pack(DHCPFormat, *buf) pkt = spack(DHCPFORMAT, *buf)
pkt += struct.pack('!BBB', DHCP_MSG, 1, dhcp_reply) pkt += spack('!BBB', DHCP_MSG, 1, dhcp_reply)
server = socket.inet_aton(server_addr) server = inet_aton(server_addr)
pkt += struct.pack('!BB4s', DHCP_SERVER, 4, server) pkt += spack('!BB4s', DHCP_SERVER, 4, server)
mask = socket.inet_aton(self.config.get( mask = inet_aton(self.config.get(
self.bootp_section, 'netmask', self.netconfig['mask'])) self.BOOTP_SECTION, 'netmask', self.netconfig['mask']))
pkt += struct.pack('!BB4s', DHCP_IP_MASK, 4, mask) pkt += spack('!BB4s', DHCP_IP_MASK, 4, mask)
gateway_addr = self.config.get(self.bootp_section, 'gateway', '') gateway_addr = self.config.get(self.BOOTP_SECTION, 'gateway', '')
if gateway_addr: if gateway_addr:
gateway = socket.inet_aton(gateway_addr) gateway = inet_aton(gateway_addr)
else: else:
gateway = server gateway = server
pkt += struct.pack('!BB4s', DHCP_IP_GATEWAY, 4, gateway) pkt += spack('!BB4s', DHCP_IP_GATEWAY, 4, gateway)
dns = self.config.get(self.bootp_section, dns = self.config.get(self.BOOTP_SECTION,
'dns', None) 'dns', None)
if dns: if dns:
if dns.lower() == 'auto': if dns.lower() == 'auto':
dns_list = self.get_dns_servers() or [socket.inet_ntoa(server)] dns_list = self.get_dns_servers() or [inet_ntoa(server)]
else: else:
dns_list = dns.split(';') dns_list = dns.split(';')
for dns_str in dns_list: for dns_str in dns_list:
dns_ip = socket.inet_aton(dns_str) dns_ip = inet_aton(dns_str)
pkt += struct.pack('!BB4s', DHCP_IP_DNS, 4, dns_ip) pkt += spack('!BB4s', DHCP_IP_DNS, 4, dns_ip)
pkt += struct.pack('!BBI', DHCP_LEASE_TIME, 4, pkt += spack('!BBI', DHCP_LEASE_TIME, 4,
int(self.config.get(self.bootp_section, int(self.config.get(self.BOOTP_SECTION,
'lease_time', 'lease_time',
str(24*3600)))) str(24*3600))))
pkt += struct.pack('!BB', DHCP_END, 0)
# do not attempt to produce a PXE-augmented response for # do not attempt to produce a PXE-augmented response for
# regular DHCP requests # regular DHCP requests
@ -608,12 +619,15 @@ class BootpServer:
else: else:
extra_buf = self.build_dhcp_options(hostname) extra_buf = self.build_dhcp_options(hostname)
pkt += extra_buf
pkt += spack('!BB', DHCP_END, 0)
# update the UUID cache # update the UUID cache
if pxe: if pxe:
self.uuidpool[mac_addr] = uuid self.uuidpool[mac_addr] = uuid
# send the response # send the response
sock.sendto(pkt + extra_buf, addr) sock.sendto(pkt, addr)
# update the current state # update the current state
if currentstate != newstate: if currentstate != newstate:
@ -622,7 +636,7 @@ class BootpServer:
self.states[mac_str] = newstate self.states[mac_str] = newstate
def get_dns_servers(self): def get_dns_servers(self):
nscre = re.compile('nameserver\s+(\d{1,3}.\d{1,3}.\d{1,3}.\d{1,3})\s') nscre = recompile(r'nameserver\s+(\d{1,3}.\d{1,3}.\d{1,3}.\d{1,3})\s')
result = [] result = []
try: try:
with open('/etc/resolv.conf', 'r') as resolv: with open('/etc/resolv.conf', 'r') as resolv:
@ -632,7 +646,7 @@ class BootpServer:
dns = mo.group(1) dns = mo.group(1)
self.log.info('Found nameserver: %s' % dns) self.log.info('Found nameserver: %s' % dns)
result.append(dns) result.append(dns)
except Exception, e: except Exception:
pass pass
if not result: if not result:
self.log.info('No nameserver found') self.log.info('No nameserver found')

View File

@ -1,7 +1,6 @@
#!/usr/bin/env python #!/usr/bin/env python3
# -*- coding: utf-8 -*-
# #
# Copyright (c) 2010-2016 Emmanuel Blot <emmanuel.blot@free.fr> # Copyright (c) 2010-2019 Emmanuel Blot <emmanuel.blot@free.fr>
# Copyright (c) 2010-2011 Neotion # Copyright (c) 2010-2011 Neotion
# #
# This library is free software; you can redistribute it and/or # This library is free software; you can redistribute it and/or
@ -18,14 +17,22 @@
# License along with this library; if not, write to the Free Software # License along with this library; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
import sys """HTTPd tiny server to exercise the pybootd daemon"""
import urlparse
from BaseHTTPServer import HTTPServer, BaseHTTPRequestHandler from argparse import ArgumentParser, FileType
from optparse import OptionParser from http.server import BaseHTTPRequestHandler, HTTPServer
from util import logger_factory, to_bool, to_int, EasyConfigParser from sys import exit as sysexit, modules, stderr
from traceback import format_exc
from urllib.parse import parse_qs, urlsplit
from pybootd.util import logger_factory, to_bool, to_int, EasyConfigParser
class HttpdDaemon(HTTPServer): #pylint: disable-msg=broad-except
#pylint: disable-msg=missing-docstring
#pylint: disable-msg=invalid-name
class HttpdTestDaemon(HTTPServer):
class ReqHandler(BaseHTTPRequestHandler): class ReqHandler(BaseHTTPRequestHandler):
@ -33,8 +40,9 @@ class HttpdDaemon(HTTPServer):
log = self.server.log log = self.server.log
log.debug("GET from %s:%d" % self.client_address) log.debug("GET from %s:%d" % self.client_address)
log.debug("Request: %s" % self.path) log.debug("Request: %s" % self.path)
urlparts = urlparse.urlsplit(self.path) urlparts = urlsplit(self.path)
query = urlparse.parse_qs(urlparts.query) query = parse_qs(urlparts.query)
uuid = ''
if urlparts.path in ('/boot', '/linux'): if urlparts.path in ('/boot', '/linux'):
if 'uuid' in query: if 'uuid' in query:
uuids = query['uuid'] uuids = query['uuid']
@ -79,30 +87,39 @@ class HttpdDaemon(HTTPServer):
self.serve_forever() self.serve_forever()
if __name__ == "__main__": def main():
usage = 'Usage: %prog [options]\n' \ debug = False
' HTTPd tiny server to exercise the pybootd daemon'
optparser = OptionParser(usage=usage)
optparser.add_option('-c', '--config', dest='config',
help='configuration file')
(options, args) = optparser.parse_args(sys.argv[1:])
if not options.config:
raise AssertionError('Missing configuration file')
cfgparser = EasyConfigParser()
with open(options.config, 'rt') as config:
cfgparser.readfp(config)
logger = logger_factory(logtype=cfgparser.get('logger', 'type', 'stderr'),
logfile=cfgparser.get('logger', 'file'),
level=cfgparser.get('logger', 'level', 'info'))
try: try:
bt = HttpdDaemon(logger, cfgparser) argparser = ArgumentParser(description=modules[__name__].__doc__)
argparser.add_argument('-c', '--config', dest='config', required=True,
type=FileType('rt'),
help='configuration file')
argparser.add_argument('-d', '--debug', action='store_true',
help='enable debug mode')
args = argparser.parse_args()
cfgparser = EasyConfigParser()
cfgparser.read_file(args.config)
logger = logger_factory(logtype=cfgparser.get('logger', 'type',
'stderr'),
logfile=cfgparser.get('logger', 'file'),
level=cfgparser.get('logger', 'level', 'info'))
bt = HttpdTestDaemon(logger, cfgparser)
bt.start() bt.start()
while True: while True:
import time import time
time.sleep(5) time.sleep(5)
except Exception as exc:
print('\nError: %s' % exc, file=stderr)
if debug:
print(format_exc(chain=False), file=stderr)
sysexit(1)
except KeyboardInterrupt: except KeyboardInterrupt:
print "Aborting..." print("\nAborting...", file=stderr)
sysexit(2)
if __name__ == '__main__':
main()

View File

@ -1,6 +1,4 @@
# -*- coding: utf-8 -*- # Copyright (c) 2010-2019 Emmanuel Blot <emmanuel.blot@free.fr>
#
# Copyright (c) 2010-2016 Emmanuel Blot <emmanuel.blot@free.fr>
# Copyright (c) 2010-2011 Neotion # Copyright (c) 2010-2011 Neotion
# #
# This library is free software; you can redistribute it and/or # This library is free software; you can redistribute it and/or
@ -18,29 +16,43 @@
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
import os import os
import re from configparser import NoSectionError
import select from io import StringIO
import socket from re import compile as recompile, sub as resub
import string from select import select
import struct from socket import socket, AF_INET, SOCK_DGRAM
import sys from struct import pack as spack, unpack as sunpack
import time from sys import argv, exc_info
import thread from threading import Thread
import urllib2 from time import time as now
import urlparse from traceback import format_exc
from ConfigParser import NoSectionError from urllib.parse import urlparse, urlsplit
from cStringIO import StringIO from urllib.request import urlopen
from pybootd import pybootd_path from . import pybootd_path
from util import hexline from .util import hexline
#pylint: disable-msg=broad-except
#pylint: disable-msg=invalid-name
#pylint: disable-msg=missing-docstring
__all__ = ['TftpServer']
TFTP_PORT = 69 TFTP_PORT = 69
class TftpError(AssertionError): class TftpError(RuntimeError):
(NOT_DEF,
FILE_NOT_FOUND,
ACCESS_ERROR,
ALLOC_EXCEED,
ILLEGAL,
UNKNOWN_ID,
ALREADY_EXIST,
NO_SUCH_USER) = range(8)
"""Any TFTP error""" """Any TFTP error"""
pass def __init__(self, code, msg):
super(TftpError, self).__init__(msg)
self.code = code
class TftpConnection(object): class TftpConnection(object):
@ -50,6 +62,7 @@ class TftpConnection(object):
ACK = 4 ACK = 4
ERR = 5 ERR = 5
OACK = 6 OACK = 6
HDRSIZE = 4 # number of bytes for OPCODE and BLOCK in header HDRSIZE = 4 # number of bytes for OPCODE and BLOCK in header
def __init__(self, server, port=0): def __init__(self, server, port=0):
@ -70,7 +83,7 @@ class TftpConnection(object):
def _bind(self, host='', port=TFTP_PORT): def _bind(self, host='', port=TFTP_PORT):
self.log.debug('bind %s:%d' % (host, port)) self.log.debug('bind %s:%d' % (host, port))
self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) self.sock = socket(AF_INET, SOCK_DGRAM)
if host or port: if host or port:
self.sock.bind((host, port)) self.sock.bind((host, port))
@ -86,7 +99,7 @@ class TftpConnection(object):
timeout = self.timeout timeout = self.timeout
retry = self.server.retry retry = self.server.retry
while retry: while retry:
r, w, e = select.select([fno], [], [fno], timeout) r = select([fno], [], [fno], timeout)[0]
if not r: if not r:
# We timed out -- retransmit # We timed out -- retransmit
retry = retry - 1 retry = retry - 1
@ -98,7 +111,7 @@ class TftpConnection(object):
if addr == client_addr: if addr == client_addr:
break break
else: else:
raise TftpError(4, 'Transfer timed out') raise TftpError(TftpError.ALLOC_EXCEED, 'Transfer timed out')
# end while # end while
return self.parse(data) return self.parse(data)
@ -121,14 +134,15 @@ class TftpConnection(object):
path = self.server.bootpd.get_filename(client_ip) path = self.server.bootpd.get_filename(client_ip)
return path return path
def parse(self, data, unpack=struct.unpack): def parse(self, data, unpack=sunpack):
self.log.debug('parse') self.log.debug('parse')
buf = buffer(data) buf = data
pkt = {} pkt = {}
opcode = pkt['opcode'] = unpack('!h', buf[:2])[0] opcode = pkt['opcode'] = unpack('!h', buf[:2])[0]
if (opcode == self.RRQ) or (opcode == self.WRQ): if (opcode == self.RRQ) or (opcode == self.WRQ):
resource, mode, options = string.split(data[2:], '\000', 2) resource, mode, options = data[2:].split(b'\x00', 2)
resource = self.server.fcre.sub(self._filter_file, resource) resource = self.server.fcre.sub(self._filter_file,
resource.decode())
if self.server.root and self.is_url(self.server.root): if self.server.root and self.is_url(self.server.root):
resource = '%s/%s' % (self.server.root, resource) resource = '%s/%s' % (self.server.root, resource)
else: else:
@ -138,7 +152,7 @@ class TftpConnection(object):
if not self.server.genfilecre.match(resource): if not self.server.genfilecre.match(resource):
if resource.startswith('^%s' % os.sep): if resource.startswith('^%s' % os.sep):
resource = os.path.join( resource = os.path.join(
os.path.dirname(sys.argv[0]), os.path.dirname(argv[0]),
resource.lstrip('^%s' % os.sep)) resource.lstrip('^%s' % os.sep))
elif self.server.root: elif self.server.root:
if self.server.root.startswith(os.sep): if self.server.root.startswith(os.sep):
@ -147,7 +161,7 @@ class TftpConnection(object):
resource) resource)
else: else:
# Relative root directory, from the daemon path # Relative root directory, from the daemon path
daemonpath = os.path.dirname(sys.argv[0]) daemonpath = os.path.dirname(argv[0])
if not daemonpath.startswith(os.sep): if not daemonpath.startswith(os.sep):
daemonpath = os.path.normpath( daemonpath = os.path.normpath(
os.path.join(os.getcwd(), daemonpath)) os.path.join(os.getcwd(), daemonpath))
@ -158,7 +172,7 @@ class TftpConnection(object):
pkt['filename'] = resource pkt['filename'] = resource
pkt['mode'] = mode pkt['mode'] = mode
while options: while options:
key, value, options = options.split('\000', 2) key, value, options = options.split(b'\x00', 2)
if key == 'blksize': if key == 'blksize':
self.blocksize = int(value) self.blocksize = int(value)
elif key == 'timeout': elif key == 'timeout':
@ -173,7 +187,7 @@ class TftpConnection(object):
errnum = pkt['errnum'] = unpack('!h', buf[2:4])[0] errnum = pkt['errnum'] = unpack('!h', buf[2:4])[0]
errtxt = pkt['errtxt'] = buf[4:-1] errtxt = pkt['errtxt'] = buf[4:-1]
else: else:
raise TftpError(4, 'Unknown packet type') raise TftpError(TftpError.ILLEGAL, 'Unknown packet type')
return pkt return pkt
def retransmit(self): def retransmit(self):
@ -194,7 +208,7 @@ class TftpConnection(object):
pkt = self.parse(data) pkt = self.parse(data)
opcode = pkt['opcode'] opcode = pkt['opcode']
if opcode not in (RRQ, WRQ): if opcode not in (RRQ, WRQ):
raise TftpError(4, 'Bad request') raise TftpError(TftpError.ILLEGAL, 'Bad request')
# Start lock-step transfer # Start lock-step transfer
self.active = 1 self.active = 1
@ -215,13 +229,12 @@ class TftpConnection(object):
elif opcode == ERR: elif opcode == ERR:
self.recv_err(pkt) self.recv_err(pkt)
else: else:
raise TftpError(5, 'Invalid opcode') raise TftpError(TftpError.ILLEGAL, 'Invalid opcode')
self.log.debug('End of active: %s:%s' % addr) self.log.debug('End of active: %s:%s' % addr)
except TftpError, detail: except TftpError as exc:
self.send_error(detail[0], detail[1]) self.send_error(exc.code, str(exc))
except: except Exception:
import traceback self.log.error(format_exc(chain=False))
self.log.error(traceback.format_exc())
self.log.debug('Ending connection %s:%s' % addr) self.log.debug('Ending connection %s:%s' % addr)
def recv_ack(self, pkt): def recv_ack(self, pkt):
@ -245,19 +258,19 @@ class TftpConnection(object):
self.handle_err(pkt) self.handle_err(pkt)
self.retransmit() self.retransmit()
def send_data(self, data, pack=struct.pack): def send_data(self, data, pack=spack):
self.log.debug('send_data') self.log.debug('send_data')
if not self.time: if not self.time:
self.time = time.time() self.time = now()
blocksize = self.blocksize blocksize = self.blocksize
block = self.blockNumber = self.blockNumber + 1 block = self.blockNumber = self.blockNumber + 1
lendata = len(data) lendata = len(data)
format = '!hh%ds' % lendata fmt = '!hh%ds' % lendata
pkt = pack(format, self.DATA, block, data) pkt = pack(fmt, self.DATA, block, data)
self.send(pkt) self.send(pkt)
self.active = (len(data) == blocksize) self.active = (len(data) == blocksize)
if not self.active and self.time: if not self.active and self.time:
total = time.time()-self.time total = now()-self.time
self.time = 0 self.time = 0
try: try:
name = self.file.name name = self.file.name
@ -270,31 +283,30 @@ class TftpConnection(object):
except AttributeError: except AttributeError:
# StringIO does not have a 'name' attribute # StringIO does not have a 'name' attribute
pass pass
except Exception: except Exception as exc:
import traceback self.log.error('Error: %s' % exc)
traceback.print_exc() self.log.warn('%s', format_exc(chain=False))
pass
def send_ack(self, pack=struct.pack): def send_ack(self, pack=spack):
self.log.debug('send_ack') self.log.debug('send_ack')
block = self.blockNumber block = self.blockNumber
self.blockNumber = self.blockNumber + 1 self.blockNumber = self.blockNumber + 1
format = '!hh' fmt = '!hh'
pkt = pack(format, self.ACK, block) pkt = pack(fmt, self.ACK, block)
self.send(pkt) self.send(pkt)
def send_error(self, errnum, errtext, pack=struct.pack): def send_error(self, errnum, errtext, pack=spack):
self.log.debug('send_error') self.log.debug('send_error')
errtext = errtext + '\000' errtext = errtext.encode() + b'\x00'
format = '!hh%ds' % len(errtext) fmt = '!hh%ds' % len(errtext)
outdata = pack(format, self.ERR, errnum, errtext) outdata = pack(fmt, self.ERR, errnum, errtext)
self.sock.sendto(outdata, self.client_addr) self.sock.sendto(outdata, self.client_addr)
def send_oack(self, options, pack=struct.pack): def send_oack(self, options, pack=spack):
self.log.debug('send_oack') self.log.debug('send_oack')
pkt = pack('!h', self.OACK) pkt = pack('!h', self.OACK)
for k, v in options: for k, v in options:
pkt += k + '\x00' + v + '\x00' pkt += k.encode() + b'\x00' + v.encode() + b'\x00'
self.send(pkt) self.send(pkt)
# clear out the last packet buffer to prevent from retransmitting it # clear out the last packet buffer to prevent from retransmitting it
self.lastpkt = '' self.lastpkt = ''
@ -310,14 +322,15 @@ class TftpConnection(object):
else: else:
try: try:
if self.is_url(resource): if self.is_url(resource):
rp = urllib2.urlopen(resource) rp = urlopen(resource)
meta = rp.info() meta = rp.info()
filesize = int(meta.getheaders('Content-Length')[0]) filesize = int(meta.getheaders('Content-Length')[0])
else: else:
filesize = os.stat(resource)[6] filesize = os.stat(resource)[6]
except Exception: except Exception:
self.active = False self.active = False
self.send_error(1, 'Cannot access resource') self.send_error(TftpError.FILE_NOT_FOUND,
'Cannot access resource')
self.log.warn('Cannot stat resource %s' % resource) self.log.warn('Cannot stat resource %s' % resource)
return return
self.log.info('Send size request file %s size: %d' % self.log.info('Send size request file %s size: %d' %
@ -332,16 +345,17 @@ class TftpConnection(object):
else: else:
try: try:
if self.is_url(resource): if self.is_url(resource):
self.file = urlopen(resource)
self.log.info("Sending resource '%s'" % resource) self.log.info("Sending resource '%s'" % resource)
self.file = urllib2.urlopen(resource)
else: else:
resource = os.path.realpath(resource) resource = os.path.realpath(resource)
self.log.info("Sending file '%s'" % resource)
self.file = open(resource, 'rb') self.file = open(resource, 'rb')
except Exception: self.log.info("Sending file '%s'" % resource)
self.send_error(1, 'Cannot open resource') except Exception as exc:
self.send_error(TftpError.FILE_NOT_FOUND,
'Cannot open resource')
self.log.warn('Cannot open file for reading %s: %s' % self.log.warn('Cannot open file for reading %s: %s' %
sys.exc_info()[:2]) (resource, exc))
return return
if 'tsize' not in pkt: if 'tsize' not in pkt:
self.send_data(self.file.read(self.blocksize)) self.send_data(self.file.read(self.blocksize))
@ -357,9 +371,9 @@ class TftpConnection(object):
self.log.info('Receiving file: %s' % resource) self.log.info('Receiving file: %s' % resource)
self.file = open(resource, 'wb') self.file = open(resource, 'wb')
except: except:
self.send_error(1, 'Cannot open file') self.send_error(TftpError.FILE_NOT_FOUND, 'Cannot open file')
self.log.error('Cannot open file for writing %s: %s' % self.log.error('Cannot open file for writing %s: %s' %
sys.exc_info()[:2]) exc_info()[:2])
return return
self.send_ack() self.send_ack()
@ -379,7 +393,7 @@ class TftpConnection(object):
@staticmethod @staticmethod
def is_url(path): def is_url(path):
return bool(urlparse.urlsplit(path).scheme) return bool(urlsplit(path).scheme)
class TftpServer: class TftpServer:
@ -388,40 +402,48 @@ class TftpServer:
Each request is handled in its own thread Each request is handled in its own thread
""" """
TFTP_SECTION = 'tftpd'
def __init__(self, logger, config, bootpd=None): def __init__(self, logger, config, bootpd=None):
self.log = logger self.log = logger
self.config = config self.config = config
self.sock = [] self.sock = []
self.bootpd = bootpd self.bootpd = bootpd
self.blocksize = int(self.config.get('tftp', 'blocksize', '512')) self.blocksize = int(self.config.get(self.TFTP_SECTION, 'blocksize',
self.timeout = float(self.config.get('tftp', 'timeout', '2.0')) '512'))
self.retry = int(self.config.get('tftp', 'blocksize', '5')) self.timeout = float(self.config.get(self.TFTP_SECTION, 'timeout', '2.0'))
self.root = self.config.get('tftp', 'root', os.getcwd()) self.retry = int(self.config.get(self.TFTP_SECTION, 'blocksize', '5'))
self.root = self.config.get(self.TFTP_SECTION, 'root', os.getcwd())
self.fcre, self.filepatterns = self.get_file_filters() self.fcre, self.filepatterns = self.get_file_filters()
self.genfilecre = re.compile(r'\[(?P<name>[\w\.\-]+)\]') self.genfilecre = recompile(r'\[(?P<name>[\w\.\-]+)\]')
self._resume = False
def bind(self): def start(self):
netconfig = self.bootpd and self.bootpd.get_netconfig() netconfig = self.bootpd and self.bootpd.get_netconfig()
host = self.config.get('tftp', 'address', host = self.config.get(self.TFTP_SECTION, 'address',
netconfig and netconfig['server']) netconfig and netconfig['server'])
if not host: if not host:
raise TftpError('TFTP address no defined') raise TftpError(TftpError.NO_SUCH_USER, 'TFTP address no defined')
port = int(self.config.get('tftp', 'port', str(TFTP_PORT))) port = int(self.config.get(self.TFTP_SECTION, 'port', str(TFTP_PORT)))
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) sock = socket(AF_INET, SOCK_DGRAM)
self.sock.append(sock) self.sock.append(sock)
sock.bind((host, port)) sock.bind((host, port))
self.log.info('Listening to %s:%s' % (host, port))
def forever(self): self._resume = True
while True: while self._resume:
if self.bootpd: r = select(self.sock, [], self.sock, 0.25)[0]
if not self.bootpd.is_alive(): if not r:
self.log.info('Bootp daemon is dead, exiting') continue
break
r, w, e = select.select(self.sock, [], self.sock)
for sock in r: for sock in r:
data, addr = sock.recvfrom(516) data, addr = sock.recvfrom(516)
t = TftpConnection(self) tc = TftpConnection(self)
thread.start_new_thread(t.connect, (addr, data)) thread = Thread(target=tc.connect, args=(addr, data),
daemon=True)
thread.start()
def stop(self):
self._resume = False
def filter_file(self, connexion, mo): def filter_file(self, connexion, mo):
# extract the position of the matching pattern, then extract the # extract the position of the matching pattern, then extract the
@ -432,8 +454,9 @@ class TftpServer:
if not filename: if not filename:
continue continue
filepattern = self.filepatterns[group] filepattern = self.filepatterns[group]
return re.sub(r'\{(\w+)\}', connexion._dynreplace, filepattern) return resub(r'\{(\w+)\}', connexion._dynreplace, filepattern)
raise TftpError('Internal error, file matching pattern issue') raise TftpError(TftpError.NOT_DEF,
'Internal error, file matching pattern issue')
def get_file_filters(self): def get_file_filters(self):
patterns = [] patterns = []
@ -442,12 +465,12 @@ class TftpServer:
for pos, pattern in enumerate(self.config.options('filters'), 1): for pos, pattern in enumerate(self.config.options('filters'), 1):
value = self.config.get('filters', pattern).strip() value = self.config.get('filters', pattern).strip()
pattern = pattern.strip('\r\n \t') pattern = pattern.strip('\r\n \t')
pattern = pattern.replace('.', '\.') pattern = pattern.replace(r'.', r'\.')
pattern = pattern.replace('*', '.*').replace('?', '.') pattern = pattern.replace(r'*', r'.*').replace(r'?', r'.')
pname = 'p%d' % pos pname = 'p%d' % pos
replacements[pname] = value replacements[pname] = value
patterns.append('(?P<%s>%s)' % (pname, pattern)) patterns.append(r'(?P<%s>%s)' % (pname, pattern))
xre = '^(?:\./)?(?:%s)$' % '|'.join(patterns) xre = r'^(?:\./)?(?:%s)$' % r'|'.join(patterns)
except NoSectionError: except NoSectionError:
xre = '^$' xre = r'^$'
return (re.compile(xre), replacements) return (recompile(xre), replacements)

View File

@ -1,6 +1,4 @@
# -*- coding: utf-8 -*- # Copyright (c) 2010-2019 Emmanuel Blot <emmanuel.blot@free.fr>
#
# Copyright (c) 2010-2016 Emmanuel Blot <emmanuel.blot@free.fr>
# Copyright (c) 2010-2011 Neotion # Copyright (c) 2010-2011 Neotion
# #
# This library is free software; you can redistribute it and/or # This library is free software; you can redistribute it and/or
@ -17,23 +15,24 @@
# License along with this library; if not, write to the Free Software # License along with this library; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
from array import array from configparser import ConfigParser, InterpolationSyntaxError
import commands from logging import (DEBUG, INFO, ERROR, CRITICAL, WARNING,
import logging Formatter, FileHandler, StreamHandler, getLogger)
import re from logging.handlers import (BufferingHandler, NTEventLogHandler,
import socket SysLogHandler)
import struct from re import match
import sys from socket import inet_aton, inet_ntoa
from ConfigParser import SafeConfigParser from subprocess import run
from six import PY3, integer_types, binary_type from struct import pack as spack, unpack as sunpack
from sys import platform, stderr
try: try:
import netifaces import netifaces as nif
except ImportError: except ImportError:
import os if platform == 'darwin':
if os.uname()[0].lower() == 'darwin':
raise ImportError('netifaces package is not installed') raise ImportError('netifaces package is not installed')
netifaces = None nif = None
# String values evaluated as true boolean values # String values evaluated as true boolean values
TRUE_BOOLEANS = ['on', 'high', 'true', 'enable', 'enabled', 'yes', '1'] TRUE_BOOLEANS = ['on', 'high', 'true', 'enable', 'enabled', 'yes', '1']
@ -56,9 +55,9 @@ def to_int(value):
""" """
if not value: if not value:
return 0 return 0
if isinstance(value, integer_types): if isinstance(value, int):
return int(value) return int(value)
mo = re.match('^\s*(\d+)\s*(?:([KMkm]i?)?B?)?\s*$', value) mo = match(r'^\s*(\d+)\s*(?:([KMkm]i?)?B?)?\s*$', value)
if mo: if mo:
mult = {'K': (1000), mult = {'K': (1000),
'KI': (1 << 10), 'KI': (1 << 10),
@ -108,7 +107,7 @@ def hexline(data, sep=' '):
of the buffer data of the buffer data
""" """
try: try:
if isinstance(data, (binary_type, array)): if isinstance(data, bytes):
src = bytearray(data) src = bytearray(data)
elif isinstance(data, bytearray): elif isinstance(data, bytearray):
src = data src = data
@ -128,20 +127,19 @@ def hexline(data, sep=' '):
def logger_factory(logtype='syslog', logfile=None, level='WARNING', def logger_factory(logtype='syslog', logfile=None, level='WARNING',
logid='PXEd', format=None): logid='PXEd', format=None):
# this code has been copied from Trac (MIT modified license) # this code has been copied from Trac (MIT modified license)
logger = logging.getLogger(logid) logger = getLogger(logid)
logtype = logtype.lower() logtype = logtype.lower()
if logtype == 'file': if logtype == 'file':
hdlr = logging.FileHandler(logfile) hdlr = FileHandler(logfile)
elif logtype in ('winlog', 'eventlog', 'nteventlog'): elif logtype in ('winlog', 'eventlog', 'nteventlog'):
# Requires win32 extensions # Requires win32 extensions
hdlr = logging.handlers.NTEventLogHandler(logid, hdlr = NTEventLogHandler(logid, logtype='Application')
logtype='Application')
elif logtype in ('syslog', 'unix'): elif logtype in ('syslog', 'unix'):
hdlr = logging.handlers.SysLogHandler('/dev/log') hdlr = SysLogHandler('/dev/log')
elif logtype in ('stderr'): elif logtype in ('stderr'):
hdlr = logging.StreamHandler(sys.stderr) hdlr = StreamHandler(stderr)
else: else:
hdlr = logging.handlers.BufferingHandler(0) hdlr = BufferingHandler(0)
if not format: if not format:
format = 'PXEd[%(module)s] %(levelname)s: %(message)s' format = 'PXEd[%(module)s] %(levelname)s: %(message)s'
@ -152,45 +150,36 @@ def logger_factory(logtype='syslog', logfile=None, level='WARNING',
datefmt = '%X' datefmt = '%X'
level = level.upper() level = level.upper()
if level in ('DEBUG', 'ALL'): if level in ('DEBUG', 'ALL'):
logger.setLevel(logging.DEBUG) logger.setLevel(DEBUG)
elif level == 'INFO': elif level == 'INFO':
logger.setLevel(logging.INFO) logger.setLevel(INFO)
elif level == 'ERROR': elif level == 'ERROR':
logger.setLevel(logging.ERROR) logger.setLevel(ERROR)
elif level == 'CRITICAL': elif level == 'CRITICAL':
logger.setLevel(logging.CRITICAL) logger.setLevel(CRITICAL)
else: else:
logger.setLevel(logging.WARNING) logger.setLevel(WARNING)
formatter = logging.Formatter(format, datefmt) formatter = Formatter(format, datefmt)
hdlr.setFormatter(formatter) hdlr.setFormatter(formatter)
logger.addHandler(hdlr) logger.addHandler(hdlr)
def logerror(record):
import traceback
print_(record.msg)
print_(record.args)
traceback.print_exc()
# uncomment the following line to show logger formatting error
#hdlr.handleError = logerror
return logger return logger
def iptoint(ipstr): def iptoint(ipstr):
return struct.unpack('!I', socket.inet_aton(ipstr))[0] return sunpack('!I', inet_aton(ipstr))[0]
def inttoip(ipval): def inttoip(ipval):
return socket.inet_ntoa(struct.pack('!I', ipval)) return inet_ntoa(spack('!I', ipval))
def _netifaces_get_iface_config(address): def _netifaces_get_iface_config(address):
pool = iptoint(address) pool = iptoint(address)
for iface in netifaces.interfaces(): for iface in nif.interfaces():
ifinfo = netifaces.ifaddresses(iface) ifinfo = nif.ifaddresses(iface)
if netifaces.AF_INET not in ifinfo: if nif.AF_INET not in ifinfo:
continue continue
for inetinfo in netifaces.ifaddresses(iface)[netifaces.AF_INET]: for inetinfo in nif.ifaddresses(iface)[nif.AF_INET]:
addr_s = inetinfo.get('addr') addr_s = inetinfo.get('addr')
netmask_s = inetinfo.get('netmask') netmask_s = inetinfo.get('netmask')
if addr_s is None or netmask_s is None: if addr_s is None or netmask_s is None:
@ -213,7 +202,7 @@ def _netifaces_get_iface_config(address):
def _iproute_get_iface_config(address): def _iproute_get_iface_config(address):
pool = iptoint(address) pool = iptoint(address)
iplines = (line.strip() iplines = (line.strip()
for line in commands.getoutput("ip address show").split('\n')) for line in run("ip address show").stdout.split('\n'))
iface = None iface = None
for l in iplines: for l in iplines:
items = l.split() items = l.split()
@ -240,17 +229,55 @@ def _iproute_get_iface_config(address):
def get_iface_config(address): def get_iface_config(address):
if not address: if not address:
return None return None
if not netifaces: nifcfg = _netifaces_get_iface_config if nif else _iproute_get_iface_config
return _iproute_get_iface_config(address) return nifcfg(address)
return _netifaces_get_iface_config(address)
class EasyConfigParser(SafeConfigParser): def is_quoted(str_):
"ConfigParser extension to support default config values" """Tells whether a string is enclosed in simple- or double- quoted
markers"""
str_ = str_.strip()
return (str_.startswith('"') and str_.endswith('"')) or \
(str_.startswith("'") and str_.endswith("'"))
def get(self, section, option, default=None):
class EasyConfigParser(ConfigParser):
"""ConfigParser extension to support default config values and do not
mess with multi-line option strings"""
INDENT_SIZE = 8
InterpolationSyntaxError = InterpolationSyntaxError
def get(self, section, option, default=None, raw=True, vars=None,
fallback=None):
"""Return the section:option value if it exists, or the default value
if either the section or the option is missing"""
if not self.has_section(section): if not self.has_section(section):
return default return default
if not self.has_option(section, option): if not self.has_option(section, option):
return default return default
return SafeConfigParser.get(self, section, option) return ConfigParser.get(self, section, option, raw=raw, vars=vars,
fallback=fallback)
def write(self, filep):
"""Write an .ini-format representation of the configuration state,
with automatic line wrapping, using improved multi-line
representation.
"""
for section in self._sections:
filep.write("[%s]\n" % section)
for (key, value) in self._sections[section].items():
if key != "__name__":
filep.write("%s = %s\n" %
(key, str(value).replace('\n', '\n' +
' ' * self.INDENT_SIZE)))
filep.write("\n")
def _interpolate(self, section, option, rawval, vars):
# special overloading of SafeConfigParser._interpolate:
# do not attempt to interpolate if the string is (double-)quoted
if is_quoted(rawval):
return rawval
# cannot use 'super' here as ConfigParser is outdated
return ConfigParser._interpolate(self, section, option, rawval, vars)

View File

@ -1,7 +1,6 @@
#!/usr/bin/env python #!/usr/bin/env python3
# -*- coding: utf-8 -*-
# #
# Copyright (c) 2010-2016 Emmanuel Blot <emmanuel.blot@free.fr> # Copyright (c) 2010-2019 Emmanuel Blot <emmanuel.blot@free.fr>
# Copyright (c) 2010-2011 Neotion # Copyright (c) 2010-2011 Neotion
# #
# This library is free software; you can redistribute it and/or # This library is free software; you can redistribute it and/or
@ -18,22 +17,23 @@
# License along with this library; if not, write to the Free Software # License along with this library; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
import os
from distutils.core import setup from distutils.core import setup
from os.path import dirname, join as joinpath
from sys import platform
from pybootd import __version__
def _read(fname): def _read(fname):
return open(os.path.join(os.path.dirname(__file__), fname)).read() return open(joinpath(dirname(__file__), fname)).read()
requirements = ['six'] requirements = []
if os.uname()[0].lower() == 'darwin': if platform == 'darwin':
requirements.append('netifaces (>= 0.5)') requirements.append('netifaces (>= 0.5)')
setup( setup(
name='pybootd', name='pybootd',
version='1.6.0', version=__version__,
description='Simplified BOOTP/DHCP/PXE and TFTP server', description='Simplified BOOTP/DHCP/PXE and TFTP server',
author='Emmanuel Blot', author='Emmanuel Blot',
author_email='emmanuel.blot@free.fr', author_email='emmanuel.blot@free.fr',
@ -52,7 +52,7 @@ setup(
'Lesser General Public License (LGPL)', 'Lesser General Public License (LGPL)',
'Operating System :: MacOS :: MacOS X', 'Operating System :: MacOS :: MacOS X',
'Operating System :: POSIX', 'Operating System :: POSIX',
'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3.5',
'Topic :: Internet', 'Topic :: Internet',
'Topic :: System :: Installation/Setup', 'Topic :: System :: Installation/Setup',
'Topic :: System :: Networking', 'Topic :: System :: Networking',