mirror of
https://github.com/eblot/pybootd.git
synced 2024-09-11 22:17:44 +03:00
Add a very basic HTTP server
Rework daemon initialization and termination Rename HTTPd test server Update documentation
This commit is contained in:
parent
a1cdddd8fa
commit
be1195b25f
51
README.rst
51
README.rst
@ -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
|
||||
.....................
|
||||
|
||||
|
@ -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:
|
||||
|
@ -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
99
pybootd/httpd.py
Executable 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()
|
@ -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 = {}
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user