1
1
mirror of https://github.com/eblot/pybootd.git synced 2024-08-17 18:00:33 +03:00

Add a very basic HTTP server

Rework daemon initialization and termination
Rename HTTPd test server
Update documentation
This commit is contained in:
Emmanuel Blot 2019-09-06 16:20:54 +02:00
parent a1cdddd8fa
commit be1195b25f
7 changed files with 248 additions and 47 deletions

View File

@ -5,16 +5,20 @@ PyBootd
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.
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
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
physical support such as a USB key or a CD/DVD.
Requirements
~~~~~~~~~~~~
@ -36,17 +40,21 @@ Permissions
- DHCP protocol requires the daemon to listen on port 67.
- 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
are required on Unix hosts (Linux, Mac OS X, ...) to start up these daemons.
Status
~~~~~~
This project is in beta development stage.
Supported features
~~~~~~~~~~~~~~~~~~
- Access control:
1. None (any remote host can be served)
@ -70,6 +78,20 @@ Supported features
- It is possible to use pybootd with only one of the services, either TFTP or
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
~~~
@ -104,8 +126,9 @@ Options:
-h, --help show this help message and exit
-c CONFIG, --config CONFIG
configuration file
-p, --pxe enable BOOTP/DHCP/PXE server only
-t, --tftp enable TFTP server only
-p, --pxe only enable BOOTP/DHCP/PXE server
-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
@ -315,6 +338,28 @@ differentiation is useless, both options may refer to the same path.
- 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
.....................

View File

@ -20,12 +20,14 @@
"""Tiny BOOTP/DHCP/TFTP/PXE server"""
from argparse import ArgumentParser
from collections import OrderedDict
from os.path import isfile
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
@ -35,49 +37,72 @@ from .util import logger_factory, EasyConfigParser
#pylint: disable-msg=invalid-name
class BootpDaemon(Thread):
class Daemon(Thread):
def __init__(self, logger, config):
super(BootpDaemon, self).__init__(name="BootpDeamon")
self.daemon = True
def __init__(self, debug):
super(Daemon, self).__init__(name=self.__class__.__name__, 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)
def get_netconfig(self):
return self._server.get_netconfig()
def is_managed_ip(self, ip):
return self._server.is_managed_ip(ip)
def get_filename(self, 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, bootpd=None):
super(TftpDaemon, self).__init__(name="TftpDeamon")
self.daemon = True
def __init__(self, logger, config, debug, bootpd=None):
super(TftpDaemon, self).__init__(debug)
self._server = TftpServer(logger=logger, config=config, bootpd=bootpd)
def run(self):
self._server.bind()
self._server.forever()
class HttpDaemon(Daemon):
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():
debug = False
try:
argparser = ArgumentParser(description=modules[__name__].__doc__)
argparser.add_argument('-c', '--config', dest='config',
argparser.add_argument('-c', '--config',
default='pybootd/etc/pybootd.ini',
help='configuration file')
argparser.add_argument('-p', '--pxe', dest='pxe',
action='store_true',
help='enable BOOTP/DHCP/PXE server only')
argparser.add_argument('-t', '--tftp', dest='tftp',
action='store_true',
help='enable TFTP server only')
argparser.add_argument('-p', '--pxe', action='store_true',
help='only enable BOOTP/DHCP/PXE server')
argparser.add_argument('-t', '--tftp', action='store_true',
help='only enable TFTP server')
argparser.add_argument('-H', '--http', action='store_true',
help='enable HTTP server (default: disabled)')
argparser.add_argument('-d', '--debug', action='store_true',
help='enable debug mode')
args = argparser.parse_args()
@ -100,18 +125,33 @@ def main():
'info'))
logger.info('-'.join(('pybootd', __version__)))
daemon = None
daemons = OrderedDict()
if not args.tftp:
daemon = BootpDaemon(logger, cfgparser)
daemon = BootpDaemon(logger, cfgparser, debug)
daemon.start()
daemons['bootp'] = daemon
if not args.pxe:
daemon = TftpDaemon(logger, cfgparser, daemon)
daemon = TftpDaemon(logger, cfgparser, debug,
daemons.get('bootp', None))
daemon.start()
if daemon:
while True:
daemon.join(0.5)
daemons['tftp'] = daemon
if args.http:
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():
break
logger.warn('%s daemon terminated', name)
zombies.add(name)
resume = False
for name in zombies:
del daemons[name]
except Exception as exc:
print('\nError: %s' % exc, file=stderr)
if debug:

View File

@ -37,6 +37,12 @@ always_check = disable
root = ./images
;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]
;pxelinux.cfg/* = pybootd/etc/pxe.cfg
egg.info = [{filename}]

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

@ -255,7 +255,10 @@ class BootpServer:
def get_netconfig(self):
return self.netconfig
def bind(self):
def is_managed_ip(self, address):
return address in self.ippool.values()
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))
@ -265,11 +268,12 @@ class BootpServer:
self.sock.append(sock)
self.log.info('Listening to %s:%s' % (host, port))
sock.bind((host, int(port)))
def forever(self):
while True:
self._resume = True
while self._resume:
try:
r = select(self.sock, [], self.sock)[0]
r = select(self.sock, [], self.sock, 0.25)[0]
if not r:
continue
for sock in r:
data, addr = sock.recvfrom(556)
self.handle(sock, addr, data)
@ -277,6 +281,9 @@ class BootpServer:
self.log.critical('%s\n%s' % (exc, format_exc()))
sleep(1)
def stop(self):
self._resume = False
def parse_options(self, tail):
self.log.debug('Parsing DHCP options')
dhcp_tags = {}

View File

@ -416,8 +416,9 @@ class TftpServer:
self.root = self.config.get(self.TFTP_SECTION, 'root', os.getcwd())
self.fcre, self.filepatterns = self.get_file_filters()
self.genfilecre = recompile(r'\[(?P<name>[\w\.\-]+)\]')
self._resume = False
def bind(self):
def start(self):
netconfig = self.bootpd and self.bootpd.get_netconfig()
host = self.config.get(self.TFTP_SECTION, 'address',
netconfig and netconfig['server'])
@ -427,20 +428,23 @@ class TftpServer:
sock = socket(AF_INET, SOCK_DGRAM)
self.sock.append(sock)
sock.bind((host, port))
def forever(self):
while True:
if self.bootpd:
if not self.bootpd.is_alive():
self.log.info('Bootp daemon is dead, exiting')
break
r = select(self.sock, [], self.sock)[0]
self.log.info('Listening to %s:%s' % (host, port))
self._resume = True
while self._resume:
r = select(self.sock, [], self.sock, 0.25)[0]
if not r:
continue
for sock in r:
data, addr = sock.recvfrom(516)
tc = TftpConnection(self)
thread = Thread(target=tc.connect, args=(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):
# extract the position of the matching pattern, then extract the
# conversion string from the file convertion sequence