├── .gitignore ├── ACKNOWLEDGEMENTS ├── COPYING ├── README.md ├── install.sh ├── libpydhcpserver ├── MANIFEST ├── debian │ ├── changelog │ ├── compat │ ├── control │ ├── copyright │ ├── rules │ └── source │ │ └── format ├── doc │ ├── Makefile │ ├── api │ │ └── index.rst │ ├── conf.py │ ├── examples │ │ └── index.rst │ ├── index.rst │ └── types │ │ ├── constants.rst │ │ ├── dhcp.rst │ │ ├── functions.rst │ │ └── index.rst ├── examples │ └── hardcoded_server.py ├── libpydhcpserver │ ├── __init__.py │ ├── dhcp.py │ ├── dhcp_types │ │ ├── __init__.py │ │ ├── constants.py │ │ ├── conversion.py │ │ ├── ipv4.py │ │ ├── mac.py │ │ ├── packet.py │ │ └── rfc.py │ └── getifaddrslib.py └── setup.py ├── makedebs.sh └── staticDHCPd ├── MANIFEST ├── README ├── conf ├── conf.py.sample └── staticDHCPd_extensions │ └── HOWTO ├── control-scripts ├── ca.uguu.puukusoft.staticDHCPd.plist └── staticDHCPd.service ├── databases ├── dhcp.ini ├── mysql.sql ├── oracle.sql ├── postgres.sql └── sqlite.sql ├── debian ├── changelog ├── compat ├── control ├── copyright ├── docs ├── examples ├── rules └── source │ └── format ├── doc ├── Makefile ├── api │ ├── databases.rst │ ├── index.rst │ ├── logging.rst │ ├── statistics.rst │ └── web.rst ├── commentary │ ├── database.rst │ ├── faq.rst │ ├── index.rst │ └── setups.rst ├── conf.py ├── customisation │ ├── configuration.rst │ ├── extensions.rst │ ├── index.rst │ └── scripting.rst ├── examples │ └── index.rst └── index.rst ├── extensions ├── README └── official │ ├── dynamism.py │ ├── feedservice.py │ ├── httpdb.py │ ├── recent_activity.py │ ├── redis_dynamic.py │ ├── redis_static-utils │ ├── dhcp-static-add │ ├── dhcp-static-list │ └── dhcp-static-remove │ ├── redis_static.py │ └── statistics.py ├── setup.py ├── staticDHCPd └── staticdhcpdlib ├── __init__.py ├── config.py ├── databases ├── __init__.py ├── _caching.py ├── _ini.py ├── _sql.py └── generic.py ├── dhcp.py ├── logging_handlers.py ├── statistics.py ├── system.py └── web ├── __init__.py ├── _resources.py ├── _templates.py ├── functions.py ├── headers.py ├── methods.py └── server.py /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | dist/ 3 | *.pyc 4 | -------------------------------------------------------------------------------- /ACKNOWLEDGEMENTS: -------------------------------------------------------------------------------- 1 | staticDHCPd makes use of only GPLv3-compatible resoruces. However, as with 2 | almost any good open source project, some of these resources came from other 3 | places. 4 | 5 | This file exists to document the origins of these borrowed resources, in case 6 | anyone might wish to research them on their own. 7 | 8 | -------------------- 9 | 10 | pydhcplib : http://pydhcplib.tuxfamily.org/pmwiki/ 11 | Mathieu Ignacio's pure-Python implementation of the packet format used in 12 | the DHCP protocol, plus a simple handler for receiving and sending packets 13 | via UDP. 14 | 15 | staticDHCPd features a completely overhauled, fully updated, and 16 | stripped-down version of this library, rebranded as 'libpydhcpserver'. 17 | -------------------------------------------------------------------------------- /install.sh: -------------------------------------------------------------------------------- 1 | #Runs both libpydhcpserver and staticDHCPd's installation scripts 2 | cd libpydhcpserver 3 | /usr/bin/env python3 setup.py install 4 | cd .. 5 | 6 | cd staticDHCPd 7 | /usr/bin/env python3 setup.py install 8 | cd .. 9 | -------------------------------------------------------------------------------- /libpydhcpserver/MANIFEST: -------------------------------------------------------------------------------- 1 | setup.py 2 | libpydhcpserver/__init__.py 3 | libpydhcpserver/dhcp.py 4 | libpydhcpserver/getifaddrslib.py 5 | libpydhcpserver/dhcp_types/__init__.py 6 | libpydhcpserver/dhcp_types/constants.py 7 | libpydhcpserver/dhcp_types/conversion.py 8 | libpydhcpserver/dhcp_types/ipv4.py 9 | libpydhcpserver/dhcp_types/mac.py 10 | libpydhcpserver/dhcp_types/packet.py 11 | libpydhcpserver/dhcp_types/rfc.py 12 | -------------------------------------------------------------------------------- /libpydhcpserver/debian/changelog: -------------------------------------------------------------------------------- 1 | libpydhcpserver (3.0.0) unstable; urgency=low 2 | 3 | * Ported to Python 3. 4 | 5 | -- Neil Tallim Sun, 28 Mar 2021 17:33:00 +0000 6 | 7 | libpydhcpserver (2.0.0) unstable; urgency=low 8 | 9 | * Finally refactored DHCPPacket. 10 | 11 | * Options can now be set with Python data-types, rather than lists of bytes. 12 | 13 | * Internal memory-management for packst should now be tighter and faster. 14 | 15 | -- Neil Tallim Fri, 27 Sep 2013 07:00:00 -0600 16 | 17 | libpydhcpserver (1.4.0~beta) unstable; urgency=low 18 | 19 | * Refactoring and relocation of code, cleaning up dhcp a little and 20 | concentrating all functions in conversion. 21 | 22 | -- Neil Tallim Wed, 25 Sep 2013 07:00:00 -0600 23 | 24 | libpydhcpserver (1.3.0~beta) unstable; urgency=low 25 | 26 | * Huge rewrite to the networking logic, allowing for layer 2 packets and 27 | moving all server-related logic into libpydhcpserver 28 | - Support for qtags (vlans) now provided for raw packets 29 | 30 | * Renamed modules and made what should be the last significant API change. 31 | 32 | -- Neil Tallim Fri, 13 Sep 2013 07:00:00 -0600 33 | 34 | libpydhcpserver (1.2.0) unstable; urgency=low 35 | 36 | * Massive refactoring and cleanup of all modules 37 | 38 | -- Neil Tallim Fri, 21 Jun 2013 07:00:00 -0600 39 | 40 | libpydhcpserver (1.1.0) unstable; urgency=low 41 | 42 | * Massive internal refactoring, rewriting hwaddr and ipv4 as MAC and IPv4, 43 | intelligent classes that offer a lot of flexibility 44 | 45 | -- Neil Tallim Fri, 24 May 2013 07:00:00 -0600 46 | 47 | libpydhcpserver (1.0.0) stable; urgency=low 48 | 49 | * Refactored pydhcplib from staticDHCPd into libpydhcpserver 50 | 51 | -- Neil Tallim Sat, 20 Nov 2010 00:00:00 -0600 52 | -------------------------------------------------------------------------------- /libpydhcpserver/debian/compat: -------------------------------------------------------------------------------- 1 | 12 2 | -------------------------------------------------------------------------------- /libpydhcpserver/debian/control: -------------------------------------------------------------------------------- 1 | Source: libpydhcpserver 2 | Maintainer: Neil Tallim 3 | Section: python 4 | Priority: optional 5 | Build-Depends: python3 (>= 3.5), debhelper (>= 12), dh-python 6 | Standards-Version: 4.5.1 7 | 8 | Package: python3-libpydhcpserver 9 | Architecture: all 10 | Depends: ${misc:Depends}, ${python3:Depends}, python3 (>= 3.5) 11 | Description: Pure-Python, spec-compliant DHCP-packet-processing and networking library 12 | libpydhcpserver provides the implementation for staticDHCPd's DHCP-processing 13 | needs, but has a stable API and may be used by other applications that have a 14 | reason to work with DHCP packets and perform server-oriented functions. 15 | -------------------------------------------------------------------------------- /libpydhcpserver/debian/copyright: -------------------------------------------------------------------------------- 1 | Files: * 2 | Copyright: 2021 Neil Tallim 3 | License: GPL-3+ 4 | 5 | License: GPL-3+ 6 | libpydhcpserver is free software; you can redistribute it and/or modify it 7 | under the terms of the GNU General Public License as published by the Free 8 | Software Foundation; either version 3 of the License, or (at your option) any 9 | later version. 10 | 11 | This program is distributed in the hope that it will be useful, but WITHOUT 12 | ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 13 | FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . 17 | 18 | On Debian systems, the full text of the GNU General Public License version 3 19 | can be found in the file /usr/share/common-licenses/GPL-3. 20 | 21 | -------------------------------------------------------------------------------- /libpydhcpserver/debian/rules: -------------------------------------------------------------------------------- 1 | #!/usr/bin/make -f 2 | 3 | export PYBUILD_DISABLE=test 4 | export PYBUILD_NAME=libpydhcpserver 5 | 6 | %: 7 | dh $@ --with=python3 --buildsystem=pybuild 8 | 9 | 10 | -------------------------------------------------------------------------------- /libpydhcpserver/debian/source/format: -------------------------------------------------------------------------------- 1 | 3.0 (native) 2 | -------------------------------------------------------------------------------- /libpydhcpserver/doc/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | 9 | # Internal variables. 10 | PAPEROPT_a4 = -D latex_paper_size=a4 11 | PAPEROPT_letter = -D latex_paper_size=letter 12 | ALLSPHINXOPTS = -d _build/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 13 | 14 | .PHONY: help clean html dirhtml pickle json htmlhelp qthelp latex changes linkcheck doctest 15 | 16 | help: 17 | @echo "Please use \`make ' where is one of" 18 | @echo " html to make standalone HTML files" 19 | @echo " dirhtml to make HTML files named index.html in directories" 20 | @echo " pickle to make pickle files" 21 | @echo " json to make JSON files" 22 | @echo " htmlhelp to make HTML files and a HTML help project" 23 | @echo " qthelp to make HTML files and a qthelp project" 24 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 25 | @echo " changes to make an overview of all changed/added/deprecated items" 26 | @echo " linkcheck to check all external links for integrity" 27 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 28 | 29 | clean: 30 | -rm -rf _build/* 31 | 32 | html: 33 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) _build/html 34 | @echo 35 | @echo "Build finished. The HTML pages are in _build/html." 36 | 37 | dirhtml: 38 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) _build/dirhtml 39 | @echo 40 | @echo "Build finished. The HTML pages are in _build/dirhtml." 41 | 42 | pickle: 43 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) _build/pickle 44 | @echo 45 | @echo "Build finished; now you can process the pickle files." 46 | 47 | json: 48 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) _build/json 49 | @echo 50 | @echo "Build finished; now you can process the JSON files." 51 | 52 | htmlhelp: 53 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) _build/htmlhelp 54 | @echo 55 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 56 | ".hhp project file in _build/htmlhelp." 57 | 58 | qthelp: 59 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) _build/qthelp 60 | @echo 61 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 62 | ".qhcp project file in _build/qthelp, like this:" 63 | @echo "# qcollectiongenerator _build/qthelp/pystrix.qhcp" 64 | @echo "To view the help file:" 65 | @echo "# assistant -collectionFile _build/qthelp/pystrix.qhc" 66 | 67 | latex: 68 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) _build/latex 69 | @echo 70 | @echo "Build finished; the LaTeX files are in _build/latex." 71 | @echo "Run \`make all-pdf' or \`make all-ps' in that directory to" \ 72 | "run these through (pdf)latex." 73 | 74 | changes: 75 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) _build/changes 76 | @echo 77 | @echo "The overview file is in _build/changes." 78 | 79 | linkcheck: 80 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) _build/linkcheck 81 | @echo 82 | @echo "Link check complete; look for any errors in the above output " \ 83 | "or in _build/linkcheck/output.txt." 84 | 85 | doctest: 86 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) _build/doctest 87 | @echo "Testing of doctests in the sources finished, look at the " \ 88 | "results in _build/doctest/output.txt." 89 | -------------------------------------------------------------------------------- /libpydhcpserver/doc/api/index.rst: -------------------------------------------------------------------------------- 1 | Framework overview 2 | ================== 3 | *libpydhcpserver* is fundamentally a framework on which a DHCP server may be 4 | built. Its :doc:`types <../types/index>` may be of value to developers working 5 | with DHCP in Python, but some will be crazy enough to need to build a server of 6 | their own and this is a good place to start. 7 | 8 | For reference, consider studying 9 | `staticDHCPd `_, a complete server built on 10 | top of *libpydhcpserver*. 11 | 12 | 13 | API: all you need to know 14 | ------------------------- 15 | The core operation of a DHCP server is relatively straightforward: a request 16 | packet is received and analysed, then a response packet is emitted. Between the 17 | :doc:`types <../types/index>` *libpydhcpserver* provides (which include an 18 | object-oriented packet interface) and the 19 | :class:`DHCPServer ` class described below, all that's left 20 | for you to do is customise the analysis part. 21 | 22 | .. module:: dhcp 23 | 24 | Constants 25 | +++++++++ 26 | .. autodata:: IP_UNSPECIFIED_FILTER 27 | 28 | Named Tuples 29 | ++++++++++++ 30 | .. autodata:: Address 31 | :annotation: 32 | 33 | Classes 34 | +++++++ 35 | .. autoclass:: DHCPServer 36 | :members: 37 | :private-members: 38 | -------------------------------------------------------------------------------- /libpydhcpserver/doc/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import sys, os, re 3 | 4 | sys.path.append(os.path.abspath('..')) 5 | import libpydhcpserver as module 6 | sys.path.remove(os.path.abspath('..')) 7 | sys.path.append(os.path.abspath('../libpydhcpserver')) 8 | 9 | extensions = [ 10 | 'sphinx.ext.autodoc', 11 | 'sphinx.ext.todo', 12 | 'sphinx.ext.coverage', 13 | 'sphinx.ext.viewcode', 14 | ] 15 | templates_path = ['_templates'] 16 | source_suffix = '.rst' 17 | master_doc = 'index' 18 | 19 | project = 'libpydhcpserver' 20 | copyright = module.COPYRIGHT 21 | version = re.match('^(\d+\.\d+)', module.VERSION).group(1) 22 | release = module.VERSION 23 | 24 | exclude_trees = ['_build'] 25 | 26 | pygments_style = 'sphinx' 27 | 28 | autodoc_member_order = 'bysource' 29 | autoclass_content = 'init' 30 | 31 | html_theme = 'default' 32 | html_static_path = ['_static'] 33 | html_show_sourcelink = False 34 | 35 | htmlhelp_basename = 'libpydhcpserverdoc' 36 | 37 | latex_documents = [ 38 | ('index', 'libpydhcpserver.tex', 'libpydhcpserver documentation', 39 | re.search(', (.*?) <', module.COPYRIGHT).group(1), 'manual'), 40 | ] 41 | -------------------------------------------------------------------------------- /libpydhcpserver/doc/examples/index.rst: -------------------------------------------------------------------------------- 1 | Primer on manipulating DHCP packets 2 | =================================== 3 | If you want to do anything with *libpydhcpserver*, it will be necessary to 4 | modify DHCP packets at some point. Fortunately, it's really not difficult. 5 | 6 | Useful background knowledge 7 | --------------------------- 8 | It will be necessary to think in terms of the types of data involved in the 9 | operation you wish to perform. :ref:`constants-types` covers the various 10 | primitive data-types used in the system. 11 | 12 | :ref:`constants-fields` and :ref:`constants-options` describe how these types 13 | fit into packets and demystify a great deal of what's presented in the remainder 14 | of this page, but involve some research. It is recommended that you study these 15 | examples first, then attempt to learn how they work. 16 | 17 | Data-access Conventions 18 | ----------------------- 19 | As much as possible, *libpydhcpserver* tries to keep things consistent and easy 20 | to predict. 21 | 22 | Setting values 23 | ++++++++++++++ 24 | With a few exceptions for :ref:`special RFC requirements `, all 25 | DHCP options are set as follows, where ``x`` is the option's ID or name and 26 | ``packet`` is a :class:`DHCPPacket `: 27 | 28 | * **TYPE_IPV4**:: 29 | 30 | packet.setOption(x, IPv4('127.0.0.1')) #Using the libpydhcpserver IPv4 type 31 | packet.setOption(x, '127.0.0.1') 32 | packet.setOption(x, [127,0,0,1]) 33 | packet.setOption(x, 2130706433) 34 | 35 | * **TYPE_IPV4_PLUS**:: 36 | 37 | packet.setOption(x, '127.0.0.1,192.168.0.1') 38 | packet.setOption(x, [[127,0,0,1], '192.168.0.1']) 39 | 40 | * **TYPE_IPV4_MULT**:: 41 | 42 | packet.setOption(x, []) #Having no IPs is permitted 43 | packet.setOption(x, '127.0.0.1,192.168.0.1') 44 | 45 | * **TYPE_BOOL**:: 46 | 47 | packet.setOption(x, True) 48 | packet.setOption(x, False) 49 | packet.setOption(x, 1) 50 | packet.setOption(x, 0) 51 | 52 | 53 | * **TYPE_BYTE**:: 54 | 55 | packet.setOption(x, 127) #Must be 0-255 56 | 57 | * **TYPE_BYTE_PLUS**:: 58 | 59 | packet.setOption(x, [0, 127, 255]) #Must be 0-255 60 | 61 | * **TYPE_STRING**:: 62 | 63 | packet.setOption(x, 'hello') 64 | 65 | * **TYPE_INT**:: 66 | 67 | packet.setOption(x, 32768) #Must be 0-65,535 68 | 69 | * **TYPE_INT_PLUS**:: 70 | 71 | packet.setOption(x, [0, 32768, 65535]) #Must be 0-65,535 72 | 73 | * **TYPE_LONG**:: 74 | 75 | packet.setOption(x, 2147483648) #Must be 0-4,294,967,295 76 | 77 | * **TYPE_LONG_PLUS**:: 78 | 79 | packet.setOption(x, [0, 2147483648, 4294967295]) #Must be 0-4,294,967,295 80 | 81 | **Note:** All integers may be specified in hex (``0xF0``), octal (``020``), and 82 | binary (``0b10001000``). 83 | 84 | **Note:** Anything, including RFC values, may be passed as a list or tuple of 85 | bytes, too, if you know what you want to set. 86 | 87 | Getting values 88 | ++++++++++++++ 89 | Except for RFC values, which are returned bytes-only, everything attached to a 90 | packet can be retreieved in a format that is either efficient (bytes) or 91 | friendly (like the first form of everything that can be used in the setting 92 | examples above). While there is a performance difference, it isn't significant 93 | enough for you to obsess over. 94 | 95 | :: 96 | 97 | >>> packet.getOption(15) #The domain-name 98 | [117, 103, 117, 117, 46, 99, 97] 99 | 100 | >>> packet.getOption('domain_name', convert=True) 101 | 'uguu.ca' 102 | 103 | Examples 104 | -------- 105 | The interesting part of this document: how to apply this stuff. Before that, 106 | though, quickly familiarise yourself with 107 | :class:`DHCPPacket `. 108 | 109 | Options 110 | +++++++ 111 | DHCP options are accessed exactly as described above, so here are some practical 112 | examples. 113 | 114 | Set renewal T1 to 60 seconds:: 115 | 116 | packet.setOption('renewal_time_value', 60) 117 | packet.setOption(58, 60) #The same thing, but using the numeric ID 118 | 119 | See if the client requested a specific option:: 120 | 121 | if packet.isRequestedOption('router'): #Option 3 122 | print("The client wants 'router'") 123 | 124 | Using numeric IDs is *slightly* more efficient, but, really, unless you know 125 | what you're doing, the gains aren't worth the headaches. 126 | 127 | Fields 128 | ++++++ 129 | DHCP fields are accessed the same way as are options, through 130 | :func:`setOption `. 131 | 132 | Unless you're working with PXE, which makes **FIELD_FILE** relevant, the only 133 | things you are likely to want to manipulate are **FIELD_CIADDR**, 134 | **FIELD_YIADDR**, **FIELD_SIADDR**, and **FIELD_GIADDR**. 135 | 136 | All of them work with IPv4 data, so the example here will be modifying the 137 | server's address:: 138 | 139 | ip = packet.getOption(FIELD_SIADDR, convert=True) #IPv4('192.168.0.1') 140 | ip = list(ip) #[192, 168, 0, 1] 141 | ip[3] = 2 #[192, 168, 0, 2] 142 | packet.setOption(FIELD_SIADDR, ip) 143 | 144 | RFC options 145 | +++++++++++ 146 | RFC values can be pretty complex. *libpydhcpserver* implements convenient 147 | handlers for a lot of them, though. 148 | 149 | :rfc:`2610` 150 | ||||||||||| 151 | Set :class:`Option 78 ` with the following pattern:: 152 | 153 | packet.setOption('directory_agent', rfc2610_78('192.168.1.1,192.168.1.2')) 154 | 155 | There are no limits on the number of comma-delimited values you may specify. 156 | 157 | Set :class:`Option 79 ` with the following pattern:: 158 | 159 | packet.setOption('service_scope', rfc2610_79(u'slp-scope-string')) 160 | 161 | Where ``slp-scope-string`` is the scope you want to set. 162 | 163 | :rfc:`3361` 164 | ||||||||||| 165 | Set :class:`Option 120 ` with either of the 166 | following patterns:: 167 | 168 | packet.setOption('sip_servers', rfc3361_120('example.org,uguu.ca')) 169 | packet.setOption('sip_servers', rfc3361_120('192.168.1.1')) 170 | 171 | There are no limits on the number of comma-delimited values you may specify. 172 | The only restriction is that either names xor IPs may be used, never both. 173 | 174 | :rfc:`3397` 175 | ||||||||||| 176 | Set :class:`Option 119 ` with the following 177 | pattern:: 178 | 179 | packet.setOption('domain_search', rfc3397_119('example.org,uguu.ca')) 180 | 181 | There are no limits on the number of comma-delimited values you may specify. 182 | 183 | :rfc:`3925` 184 | ||||||||||| 185 | Set :class:`Option 124 ` with the following 186 | pattern:: 187 | 188 | packet.setOption('vendor_class', rfc3925_124([(0x00000001, strToList('hello'))])) 189 | 190 | Set :class:`Option 125 ` with the following 191 | pattern:: 192 | 193 | packet.setOption('vendor_specific', rfc3925_125([(0x00000001, [(45, strToList('hello'))])])) 194 | 195 | :rfc:`4174` 196 | ||||||||||| 197 | Set :class:`Option 83 ` with the following 198 | pattern:: 199 | 200 | isns_functions = 0b0000000000000111 201 | dd_access = 0b0000000000111111 202 | admin_flags = 0b0000000000001111 203 | isns_security = 0b00000000000000000000000001111111 204 | packet.setOption('internet_storage_name_service', rfc4174_83( 205 | isns_functions, dd_access, admin_flags, isns_security, 206 | '192.168.1.1,192.168.1.2,192.168.1.3' 207 | )) 208 | 209 | There are no limits on the number of comma-delimited values you may specify, 210 | but you may require at least two, depending on the rest of your configuration. 211 | 212 | :rfc:`4280` 213 | ||||||||||| 214 | Set :class:`Option 88 ` with the following 215 | pattern:: 216 | 217 | packet.setOption('bcmcs_domain_list', rfc4280_88('example.org,uguu.ca')) 218 | 219 | There are no limits on the number of comma-delimited values you may specify. 220 | 221 | Set :class:`Option 89` as you would any other **TYPE_IPV4_PLUS** value. 222 | 223 | :rfc:`5223` 224 | ||||||||||| 225 | Set :class:`Option 137 ` with the following 226 | pattern:: 227 | 228 | packet.setOption('v4_lost', rfc5223_137('example.org,uguu.ca')) 229 | 230 | There are no limits on the number of comma-delimited values you may specify. 231 | 232 | :rfc:`5678` 233 | ||||||||||| 234 | Set :class:`Option 139 ` with the following 235 | pattern:: 236 | 237 | packet.setOption('ipv4_mos', rfc5678_139( 238 | (1, '127.0.0.1,192.168.1.1'), 239 | (2, '10.0.0.1'), 240 | )) 241 | 242 | There are no limits on the number of comma-delimited values you may specify. 243 | 244 | Set :class:`Option 140 ` with the following 245 | pattern:: 246 | 247 | packet.setOption('fqdn_mos', rfc5678_140( 248 | (1, 'example.org,uguu.ca'), 249 | (2, 'example.ca,google.com'), 250 | )) 251 | 252 | There are no limits on the number of comma-delimited values you may specify. 253 | -------------------------------------------------------------------------------- /libpydhcpserver/doc/index.rst: -------------------------------------------------------------------------------- 1 | libpydhcpserver 2 | =============== 3 | *libpydhcpserver* is an open framework for developing DHCP servers in Python. 4 | It is also a project with strong ties (but no forward-coupling) to 5 | `staticDHCPd `_, and it is very likely that 6 | you are viewing this documentation because you want to do something on the 7 | packet-level from *staticDHCPd*. If so, you're in the right place. 8 | 9 | This documentation provides enough information for sysadmins to tweak setups to 10 | their liking and for developers to build new DHCP servers for special 11 | use-cases. The table-of-contents below progresses from basic information to 12 | hardcore internals, so just read until you've found what you need. 13 | 14 | .. toctree:: 15 | :maxdepth: 2 16 | 17 | examples/index.rst 18 | types/index.rst 19 | api/index.rst 20 | 21 | -------------------------------------------------------------------------------- /libpydhcpserver/doc/types/constants.rst: -------------------------------------------------------------------------------- 1 | Constants 2 | ========= 3 | There's a lot of stuff going on behind the scenes in DHCP, and while this 4 | library tries to make it accessible, it is impossible to hide. At least, not 5 | without removing what makes the library worth using. 6 | 7 | Keep this around as a reference when messing with internals, but most of this is 8 | used internally to do the right thing with whatever you throw at a packet. 9 | Check back when things don't go as expected, but go with your instinct first. 10 | 11 | .. module:: dhcp_types.constants 12 | 13 | .. _constants-fields: 14 | 15 | DHCP fields 16 | ----------- 17 | .. autodata:: FIELD_OP 18 | :annotation: 19 | 20 | .. autodata:: FIELD_HTYPE 21 | :annotation: 22 | 23 | .. autodata:: FIELD_HLEN 24 | :annotation: 25 | 26 | .. autodata:: FIELD_HOPS 27 | :annotation: 28 | 29 | .. autodata:: FIELD_XID 30 | :annotation: 31 | 32 | .. autodata:: FIELD_SECS 33 | :annotation: 34 | 35 | .. autodata:: FIELD_FLAGS 36 | :annotation: 37 | 38 | .. autodata:: FIELD_CIADDR 39 | :annotation: 40 | 41 | .. autodata:: FIELD_YIADDR 42 | :annotation: 43 | 44 | .. autodata:: FIELD_SIADDR 45 | :annotation: 46 | 47 | .. autodata:: FIELD_GIADDR 48 | :annotation: 49 | 50 | .. autodata:: FIELD_CHADDR 51 | :annotation: 52 | 53 | .. autodata:: FIELD_SNAME 54 | :annotation: 55 | 56 | .. autodata:: FIELD_FILE 57 | :annotation: 58 | 59 | .. autodata:: DHCP_FIELDS 60 | :annotation: 61 | 62 | .. autodata:: DHCP_FIELDS_SPECS 63 | :annotation: 64 | 65 | .. autodata:: DHCP_FIELDS_TYPES 66 | :annotation: 67 | 68 | Reading the `source <../_modules/dhcp_types/constants.html>`_ for this 69 | element is VERY strongly recommended. 70 | 71 | .. _constants-options: 72 | 73 | DHCP options 74 | ------------ 75 | .. autodata:: DHCP_OPTIONS_TYPES 76 | :annotation: 77 | 78 | Reading the `source <../_modules/dhcp_types/constants.html>`_ for this 79 | element is VERY strongly recommended. 80 | 81 | .. autodata:: DHCP_OPTIONS 82 | :annotation: 83 | 84 | Reading the `source <../_modules/dhcp_types/constants.html>`_ for this 85 | element is VERY strongly recommended. 86 | 87 | .. autodata:: DHCP_OPTIONS_REVERSE 88 | :annotation: 89 | 90 | DHCP miscellany 91 | --------------- 92 | .. autodata:: DHCP_OP_NAMES 93 | :annotation: 94 | 95 | .. autodata:: DHCP_TYPE_NAMES 96 | :annotation: 97 | 98 | .. autodata:: DHCP_FIELDS_TEXT 99 | :annotation: 100 | 101 | .. autodata:: MAGIC_COOKIE 102 | :annotation: 103 | 104 | .. autodata:: MAGIC_COOKIE_ARRAY 105 | :annotation: 106 | 107 | .. _constants-types: 108 | 109 | Type-definitions 110 | ---------------- 111 | .. autodata:: TYPE_IPV4 112 | :annotation: 113 | 114 | .. autodata:: TYPE_IPV4_PLUS 115 | :annotation: 116 | 117 | .. autodata:: TYPE_IPV4_MULT 118 | :annotation: 119 | 120 | .. autodata:: TYPE_BYTE 121 | :annotation: 122 | 123 | .. autodata:: TYPE_BYTE_PLUS 124 | :annotation: 125 | 126 | .. autodata:: TYPE_STRING 127 | :annotation: 128 | 129 | .. autodata:: TYPE_BOOL 130 | :annotation: 131 | 132 | .. autodata:: TYPE_INT 133 | :annotation: 134 | 135 | .. autodata:: TYPE_INT_PLUS 136 | :annotation: 137 | 138 | .. autodata:: TYPE_LONG 139 | :annotation: 140 | 141 | .. autodata:: TYPE_LONG_PLUS 142 | :annotation: 143 | 144 | .. autodata:: TYPE_IDENTIFIER 145 | :annotation: 146 | 147 | .. autodata:: TYPE_NONE 148 | :annotation: 149 | -------------------------------------------------------------------------------- /libpydhcpserver/doc/types/dhcp.rst: -------------------------------------------------------------------------------- 1 | DHCP data-types 2 | =============== 3 | Networking is a complex subject, with lots of different ways to communicate the 4 | same information. To make this easier, abstract encapsulations are provided that 5 | behave like all of the most common expressions simultaneously. 6 | 7 | If you ever find yourself needing to pass or work with an 8 | :class:`IPv4 ` or :class:`MAC ` 9 | address, wrap it with one of these. If it's coming out of the library, chances 10 | are it's one already. 11 | 12 | IPv4 addresses 13 | -------------- 14 | A fairly straightforward representation of a conventional IP address. It can be 15 | coerced into a familiar dotted-quad string, an efficient sequence of bytes, or 16 | a somewhat intimidating unsigned, 32-bit integer. It can also be assembled from 17 | any of these things, as well as other instances of this class. 18 | 19 | .. module:: dhcp_types.ipv4 20 | 21 | .. autoclass:: IPv4 22 | :members: 23 | 24 | MAC addresses 25 | ------------- 26 | A simple, friendly representation of a standard six-octet MAC address. It can be 27 | coerced into a string of colon-delimited hex-values, an efficient sequence of 28 | bytes, or a scary unsigned integer. It can be built from any of these things, 29 | too, other instances of the class, or hex-strings that may or may not contain 30 | any delimiters you like. 31 | 32 | .. module:: dhcp_types.mac 33 | 34 | .. autoclass:: MAC 35 | :members: 36 | 37 | DHCP packets 38 | ------------ 39 | The heart of the library, data-structure-wise, a DHCP packet to be examined, 40 | modified, and serialised for transmission. 41 | 42 | .. module:: dhcp_types.packet 43 | 44 | Constants 45 | +++++++++ 46 | .. autodata:: FLAGBIT_BROADCAST 47 | :annotation: 48 | 49 | Classes 50 | +++++++ 51 | .. autoclass:: DHCPPacket 52 | :members: 53 | -------------------------------------------------------------------------------- /libpydhcpserver/doc/types/functions.rst: -------------------------------------------------------------------------------- 1 | Conversion functions 2 | ==================== 3 | For efficiency purposes, packet-data is conceptualised as sequences of bytes, 4 | since it has very limited interaction with human operators. This is great for 5 | performance, but not so great when writing code. 6 | 7 | To maintain efficiency and avoid errors, a number of optimised convenience 8 | routines are provided and described below. However, they are all encapsulated 9 | within the :meth:`getOption ` and 10 | :meth:`setOption ` methods of 11 | :class:`DHCPPacket `, so it is rare that you will 12 | need to invoke them directly. 13 | 14 | Note also that the terminology used here is derived from BOOTP's 16-bit origins: 15 | "int" means "16-bit integer" and "long" is "32-bit integer". Rather than 16 | conflict with existing literature and domain references, this has been 17 | preserved as part of the nomenclature. 18 | 19 | 20 | .. _conversion-type: 21 | 22 | Type-conversion 23 | --------------- 24 | DHCP needs to encode a wide variety of data and it can be easy to confuse 25 | byte-ordering. That's where the conversion-functions come into play. 26 | 27 | .. module:: dhcp_types.conversion 28 | 29 | Decoders 30 | |||||||| 31 | The following functions convert from byte-sequences into more familiar 32 | data-types. They are additionally bound via autoconversion. 33 | 34 | .. autofunction:: listToNumber 35 | .. autofunction:: listToInt 36 | .. autofunction:: listToInts 37 | .. autofunction:: listToLong 38 | .. autofunction:: listToLongs 39 | .. autofunction:: listToStr 40 | .. autofunction:: listToIP 41 | .. autofunction:: listToIPs 42 | 43 | Encoders 44 | |||||||| 45 | When writing values to a packet, the following methods, additionally bound via 46 | autoconversion, will handle the hard part. 47 | 48 | .. autofunction:: intToList 49 | .. autofunction:: intsToList 50 | .. autofunction:: longToList 51 | .. autofunction:: longsToList 52 | .. autofunction:: strToList 53 | .. autofunction:: strToPaddedList 54 | .. autofunction:: ipToList 55 | .. autofunction:: ipsToList 56 | 57 | 58 | .. _conversion-rfc: 59 | 60 | RFC conversions 61 | --------------- 62 | DHCP has many extending RFCs, and many of those have their own data-formats. 63 | 64 | Where possible, `libpydhcpserver` provides routines for processing their 65 | content, letting you focus on logic, not bitwise shifts. 66 | 67 | .. module:: dhcp_types.rfc 68 | 69 | Decoders 70 | |||||||| 71 | The following functions decode RFC options, providing easy-to-process data. 72 | They are bound and invoked, where appriopriate, via autoconversion. 73 | 74 | .. autofunction:: rfc3046_decode 75 | .. autofunction:: rfc3925_decode 76 | .. autofunction:: rfc3925_125_decode 77 | 78 | Encoders 79 | |||||||| 80 | For setting RFC options, the following classes can be passed in place of 81 | byte-sequences, handling all logic internally. 82 | 83 | .. autoclass:: RFC 84 | :members: 85 | 86 | .. autoclass:: rfc1035_plus 87 | :show-inheritance: 88 | 89 | .. autoclass:: rfc2610_78 90 | :show-inheritance: 91 | 92 | .. autoclass:: rfc2610_79 93 | :show-inheritance: 94 | 95 | .. autoclass:: rfc3361_120 96 | :show-inheritance: 97 | 98 | .. autoclass:: rfc3397_119 99 | :show-inheritance: 100 | 101 | .. autoclass:: rfc3442_121 102 | :show-inheritance: 103 | 104 | .. autoclass:: rfc3925_124 105 | :show-inheritance: 106 | 107 | .. autoclass:: rfc3925_125 108 | :show-inheritance: 109 | 110 | .. autoclass:: rfc4174_83 111 | :show-inheritance: 112 | 113 | .. autoclass:: rfc4280_88 114 | :show-inheritance: 115 | 116 | .. autoclass:: rfc5223_137 117 | :show-inheritance: 118 | 119 | .. autoclass:: rfc5678_139 120 | :show-inheritance: 121 | 122 | .. autoclass:: rfc5678_140 123 | :show-inheritance: 124 | 125 | -------------------------------------------------------------------------------- /libpydhcpserver/doc/types/index.rst: -------------------------------------------------------------------------------- 1 | DHCP-specific data 2 | ================== 3 | Details about data-types, structures, and the constants that support them. 4 | 5 | .. toctree:: 6 | :maxdepth: 2 7 | 8 | dhcp.rst 9 | functions.rst 10 | constants.rst 11 | -------------------------------------------------------------------------------- /libpydhcpserver/examples/hardcoded_server.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- encoding: utf-8 -*- 3 | """ 4 | This is a minimal example showing how to use libpydhcpserver with well-behaved 5 | clients. 6 | 7 | It's entirely unsuitable for production work, but if you want to create a new 8 | server or experiment with a network, it should give you enough to get started. 9 | 10 | It will handle offers, renew, and rebind requests, so it should be enough to 11 | bring a few hosts online for learning purposes. 12 | 13 | Check the documentation for more information. 14 | """ 15 | import select 16 | import traceback 17 | 18 | import libpydhcpserver.dhcp 19 | from libpydhcpserver.dhcp_types.ipv4 import IPv4 20 | from libpydhcpserver.dhcp_types.mac import MAC 21 | 22 | _HARDCODED_MACS_TO_IPS = { 23 | MAC('00:11:22:33:44:55'): IPv4('192.168.0.100'), 24 | } 25 | _SUBNET_MASK = IPv4('255.255.255.0') 26 | _LEASE_TIME = 120 #seconds 27 | 28 | class _DHCPServer(libpydhcpserver.dhcp.DHCPServer): 29 | def __init__(self, server_address, server_port, client_port, proxy_port, response_interface, response_interface_qtags): 30 | libpydhcpserver.dhcp.DHCPServer.__init__( 31 | self, server_address, server_port, client_port, proxy_port, 32 | response_interface=response_interface, 33 | response_interface_qtags=response_interface_qtags, 34 | ) 35 | 36 | def _handleDHCPDiscover(self, packet, source_address, port): 37 | mac = packet.getHardwareAddress() 38 | ip = _HARDCODED_MACS_TO_IPS.get(mac) 39 | if ip: 40 | packet.transformToDHCPOfferPacket() 41 | packet.setOption('yiaddr', ip) 42 | packet.setOption(1, _SUBNET_MASK) 43 | packet.setOption(51, _LEASE_TIME) 44 | 45 | self._emitDHCPPacket( 46 | packet, source_address, port, 47 | mac, ip, 48 | ) 49 | 50 | def _handleDHCPRequest(self, packet, source_address, port): 51 | sid = packet.extractIPOrNone("server_identifier") 52 | ciaddr = packet.extractIPOrNone("ciaddr") 53 | riaddr = packet.extractIPOrNone("requested_ip_address") 54 | 55 | mac = packet.getHardwareAddress() 56 | ip = _HARDCODED_MACS_TO_IPS.get(mac) 57 | 58 | if sid and not ciaddr: #SELECTING 59 | if ip and sid == self._server_address: #SELECTING; our offer was chosen 60 | packet.transformToDHCPAckPacket() 61 | packet.setOption('yiaddr', ip) 62 | packet.setOption(1, _SUBNET_MASK) 63 | packet.setOption(51, _LEASE_TIME) 64 | 65 | self._emitDHCPPacket( 66 | packet, source_address, port, 67 | mac, ip, 68 | ) 69 | elif not sid and ciaddr and not riaddr: #RENEWING or REBINDING 70 | if ip and ip == ciaddr: 71 | packet.transformToDHCPAckPacket() 72 | packet.setOption('yiaddr', ip) 73 | packet.setOption(1, _SUBNET_MASK) 74 | packet.setOption(51, _LEASE_TIME) 75 | 76 | self._emitDHCPPacket( 77 | packet, source_address, port, 78 | mac, ip, 79 | ) 80 | 81 | def _emitDHCPPacket(self, packet, address, port, mac, client_ip): 82 | packet.setOption(54, self._server_address) #server_identifier 83 | 84 | (bytes_sent, address) = self._sendDHCPPacket(packet, address, port) 85 | return bytes_sent 86 | 87 | def getNextDHCPPacket(self): 88 | (dhcp_received, source_address) = self._getNextDHCPPacket() 89 | print((dhcp_received, source_address)) 90 | 91 | 92 | if __name__ == '__main__': 93 | dhcp_server = _DHCPServer( 94 | server_address=IPv4('192.168.0.1'), 95 | server_port=67, 96 | client_port=68, 97 | proxy_port=None, 98 | response_interface=None, 99 | response_interface_qtags=None, 100 | ) 101 | 102 | while True: 103 | try: 104 | dhcp_server.getNextDHCPPacket() 105 | except select.error: 106 | pass #non-fatal error; occurs with some kernel configs 107 | except Exception: 108 | print("Unhandled exception:\n{}".format(traceback.format_exc())) 109 | -------------------------------------------------------------------------------- /libpydhcpserver/libpydhcpserver/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | """ 3 | libpydhcpserver 4 | =============== 5 | Provides all functionality and definitions needed to implement a DHCP server or 6 | packet-management library in Python. 7 | 8 | Legal 9 | ----- 10 | This file is part of libpydhcpserver. 11 | libpydhcpserver is free software; you can redistribute it and/or modify 12 | it under the terms of the GNU General Public License as published by 13 | the Free Software Foundation; either version 3 of the License, or 14 | (at your option) any later version. 15 | 16 | This program is distributed in the hope that it will be useful, 17 | but WITHOUT ANY WARRANTY; without even the implied warranty of 18 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 19 | GNU General Public License for more details. 20 | 21 | You should have received a copy of the GNU General Public License 22 | along with this program. If not, see . 23 | 24 | (C) Neil Tallim, 2014 25 | (C) Mathieu Ignacio, 2008 26 | """ 27 | VERSION = '3.0.0-beta1' 28 | URL = 'https://github.com/flan/staticdhcpd' 29 | COPYRIGHT = '2021, Neil Tallim ' 30 | -------------------------------------------------------------------------------- /libpydhcpserver/libpydhcpserver/dhcp_types/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | """ 3 | libpydhcpserver.dhcp_types 4 | ========================== 5 | Collects definitions of types specific to libpydhcpserver. 6 | 7 | Legal 8 | ===== 9 | This file is part of libpydhcpserver. 10 | libpydhcpserver is free software; you can redistribute it and/or modify 11 | it under the terms of the GNU General Public License as published by 12 | the Free Software Foundation; either version 3 of the License, or 13 | (at your option) any later version. 14 | 15 | This program is distributed in the hope that it will be useful, 16 | but WITHOUT ANY WARRANTY; without even the implied warranty of 17 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 18 | GNU General Public License for more details. 19 | 20 | You should have received a copy of the GNU General Public License 21 | along with this program. If not, see . 22 | 23 | (C) Neil Tallim, 2021 24 | (C) Mathieu Ignacio, 2008 25 | """ 26 | -------------------------------------------------------------------------------- /libpydhcpserver/libpydhcpserver/dhcp_types/conversion.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | """ 3 | libpydhcpserver.dhcp_types.conversion 4 | ===================================== 5 | Provides convenience functions used to convert from friendly data-types into 6 | packet-insertable data-types and vice-versa. 7 | 8 | Legal 9 | ----- 10 | This file is part of libpydhcpserver. 11 | libpydhcpserver is free software; you can redistribute it and/or modify 12 | it under the terms of the GNU General Public License as published by 13 | the Free Software Foundation; either version 3 of the License, or 14 | (at your option) any later version. 15 | 16 | This program is distributed in the hope that it will be useful, 17 | but WITHOUT ANY WARRANTY; without even the implied warranty of 18 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 19 | GNU General Public License for more details. 20 | 21 | You should have received a copy of the GNU General Public License 22 | along with this program. If not, see . 23 | 24 | (C) Neil Tallim, 2021 25 | """ 26 | _IPv4 = None #: Placeholder for a deferred import to avoid a circular reference. 27 | 28 | def listToNumber(l): 29 | """ 30 | Sums a sequence of bytes in big-endian order, producing an integer. 31 | 32 | :param sequence l: A sequence of ints, between ``0`` and ``255``. 33 | :return int: The corresponding value. 34 | """ 35 | value = 0 36 | for (i, v) in enumerate(reversed(l)): 37 | value += v << (8 * i) 38 | return value 39 | 40 | def listToInt(l): 41 | """ 42 | Converts a pair of bytes, in big-endian order, into an integer. 43 | 44 | :param sequence l: A sequence of ints, between ``0`` and ``255``. If longer 45 | than two, only the head is used; if less than two, zero-padded to LSD. 46 | :return int: The corresponding value. 47 | """ 48 | return listToNumber(l[:2]) 49 | 50 | def listToInts(l): 51 | """ 52 | Converts pairs of bytes, in big-endian order, into integers. 53 | 54 | :param sequence l: A sequence of ints, between ``0`` and ``255``. If not a 55 | multiple of two, zero-padded to LSD. 56 | :return list: A list of ints corresponding to the byte-pairs. 57 | """ 58 | ints = [] 59 | for i in range(len(l) >> 1): 60 | p = i * 2 61 | ints.append(listToInt(l[p:p + 2])) 62 | return ints 63 | 64 | def listToLong(l): 65 | """ 66 | Converts a quartet of bytes, in big-endian order, into an integer. 67 | 68 | :param sequence l: A sequence of ints, between ``0`` and ``255``. If longer 69 | than four, only the head is used; if less than four, zero-padded to LSD. 70 | :return int: The corresponding value. 71 | """ 72 | return listToNumber(l[:4]) 73 | 74 | def listToLongs(l): 75 | """ 76 | Converts quartets of bytes, in big-endian order, into integers. 77 | 78 | :param sequence l: A sequence of ints, between ``0`` and ``255``. If not a 79 | multiple of four, zero-padded to LSD. 80 | :return list: A list of ints corresponding to the byte-quartets. 81 | """ 82 | longs = [] 83 | for i in range(len(l) >> 2): 84 | p = i * 4 85 | longs.append(listToLong(l[p:p + 4])) 86 | return longs 87 | 88 | def intToList(i): 89 | """ 90 | Converts an integer into a pair of bytes in big-endian order. 91 | 92 | :param int i: The integer to be converted. If outside the range of ``0`` to 93 | ``65535``, only the low-order sixteen bits are considered. 94 | :return list(2): The converted value. 95 | """ 96 | return [ 97 | i >> 8 & 0xFF, 98 | i & 0xFF, 99 | ] 100 | 101 | def intsToList(l): 102 | """ 103 | Converts a sequence of integers into pairs of bytes in big-endian order. 104 | 105 | :param sequence l: The sequence to be converted. If any values are outside 106 | the range of ``0`` to ``65535``, only the low-order sixteen bits are 107 | considered. 108 | :return list: The converted values. 109 | """ 110 | pairs = [] 111 | for i in l: 112 | pairs += intToList(i) 113 | return pairs 114 | 115 | def longToList(l): 116 | """ 117 | Converts an integer into a quartet of bytes in big-endian order. 118 | 119 | :param int l: The integer to be converted. If outside the range of ``0`` to 120 | ``4294967296``, only the low-order thirty-two bits are considered. 121 | :return list(4): The converted value. 122 | """ 123 | return [ 124 | l >> 24 & 0xFF, 125 | l >> 16 & 0xFF, 126 | l >> 8 & 0xFF, 127 | l & 0xFF, 128 | ] 129 | 130 | def longsToList(l): 131 | """ 132 | Converts a sequence of integers into quartets of bytes in big-endian order. 133 | 134 | :param sequence l: The sequence to be converted. If any values are outside 135 | the range of ``0`` to ``4294967296``, only the low-order thirty-two 136 | bits are considered. 137 | :return list: The converted values. 138 | """ 139 | bytes = [] 140 | for i in l: 141 | bytes += longToList(i) 142 | return bytes 143 | 144 | def listToStr(l): 145 | """ 146 | Converts a sequence of bytes into a byte-string. 147 | 148 | :param sequence l: The bytes to be converted. 149 | :return str: The converted byte-string. 150 | """ 151 | return bytes(l) 152 | 153 | def strToList(s): 154 | """ 155 | Converts a string into a sequence of bytes. 156 | 157 | This will also process unicode strings, so sanitise all input. 158 | 159 | :param str s: The string to be converted. 160 | :return list: A sequence of bytes. 161 | """ 162 | if isinstance(s, str): 163 | s = s.encode('utf-8') 164 | return list(s) 165 | 166 | def strToPaddedList(s, l): 167 | """ 168 | Converts a string into a sequence of bytes, with a fixed length. 169 | 170 | This will also handle unicode strings, so sanitise all input. 171 | 172 | :param str s: The string to be converted. 173 | :param int l: The length of the resulting list. 174 | :return list: A sequence of bytes of length ``l``, truncated or null-padded 175 | as appropriate. 176 | """ 177 | padded_list = strToList(s) 178 | if len(padded_list) < l: 179 | padded_list.extend(0 for i in range(l - len(padded_list))) 180 | else: 181 | padded_list = padded_list[:l] 182 | return padded_list 183 | 184 | def listToIP(l): 185 | """ 186 | Converts almost anything into an IPv4 address. 187 | 188 | :param sequence(4) l: The bytes to be converted. 189 | :return: The equivalent IPv4 address. 190 | :except ValueError: The list could not be processed. 191 | """ 192 | global _IPv4 193 | if not _IPv4: 194 | from .ipv4 import IPv4 195 | _IPv4 = IPv4 196 | 197 | return _IPv4(l) 198 | 199 | def listToIPs(l): 200 | """ 201 | Converts almost anything into IPv4 addresses. 202 | 203 | :param sequence l: The bytes to be converted, as a flat sequence with 204 | length a multiple of four. 205 | :return list: The equivalent IPv4 addresses. 206 | :except ValueError: The list could not be processed. 207 | """ 208 | ips = [] 209 | for i in range(len(l) // 4): 210 | p = i * 4 211 | ips.append(listToIP(l[p:p + 4])) 212 | return ips 213 | 214 | def ipToList(ip): 215 | """ 216 | Converts an IPv4 address into a list of four bytes in big-endian order. 217 | 218 | :param object ip: Any valid IPv4 format (string, 32-bit integer, list of 219 | bytes, :class:`IPv4 `). 220 | :return list(4): The converted address. 221 | :except ValueError: The IP could not be processed. 222 | """ 223 | global _IPv4 224 | if not _IPv4: 225 | from .ipv4 import IPv4 226 | _IPv4 = IPv4 227 | 228 | if not isinstance(ip, _IPv4): 229 | ip = _IPv4(ip) 230 | return list(ip) 231 | 232 | def ipsToList(ips): 233 | """ 234 | Converts a IPv4 addresses into a flat list of multiples of four bytes in 235 | big-endian order. 236 | 237 | :param list ips: A list of any valid IPv4 formats (string, 32-bit integer, 238 | list of bytes, :class:`IPv4 `). 239 | :return list: The converted addresses. 240 | :except ValueError: The IPs could not be processed. 241 | """ 242 | if isinstance(ips, str): 243 | tokens = ips.split(',') 244 | elif isinstance(ips, bytes): 245 | tokens = ips.split(b',') 246 | else: 247 | tokens = ips 248 | 249 | output = [] 250 | for ip in tokens: 251 | output += ipToList(ip) 252 | return output 253 | 254 | -------------------------------------------------------------------------------- /libpydhcpserver/libpydhcpserver/dhcp_types/ipv4.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | """ 3 | libpydhcpserver.dhcp_types.ipv4 4 | =============================== 5 | Defines a standard way of representing IPv4s within the library. 6 | 7 | Legal 8 | ----- 9 | This file is part of libpydhcpserver. 10 | libpydhcpserver is free software; you can redistribute it and/or modify 11 | it under the terms of the GNU General Public License as published by 12 | the Free Software Foundation; either version 3 of the License, or 13 | (at your option) any later version. 14 | 15 | This program is distributed in the hope that it will be useful, 16 | but WITHOUT ANY WARRANTY; without even the implied warranty of 17 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 18 | GNU General Public License for more details. 19 | 20 | You should have received a copy of the GNU General Public License 21 | along with this program. If not, see . 22 | 23 | (C) Neil Tallim, 2021 24 | (C) Mathieu Ignacio, 2008 25 | """ 26 | from .conversion import (longToList, listToLong) 27 | 28 | _MAX_IP_INT = 4294967295 29 | 30 | class IPv4(object): 31 | """ 32 | An abstract IPv4 address that can be realised as a sequence of bytes, a 33 | dotted quad, or an unsigned, 32-bit integer, as needed. 34 | """ 35 | _ip = None #: An IPv4 as an integer. 36 | _ip_tuple = None #: An IPv4 as a quadruple of bytes. 37 | _ip_string = None #: An IPv4 as a dotted quad. 38 | 39 | def __init__(self, address): 40 | """ 41 | Constructs an IPv4 abstraction from a concrete representation. 42 | 43 | :param address: An IPv4, which may be a dotted quad, a quadruple of 44 | bytes, or a 32-bit, unsigned integer. 45 | :except ValueError: The address could not be processed. 46 | """ 47 | if isinstance(address, int): 48 | if not 0 <= address <= _MAX_IP_INT: 49 | raise ValueError("'{ip}' is not a valid IP: not a 32-bit unsigned integer".format( 50 | ip=address, 51 | )) 52 | self._ip = int(address) 53 | self._ip_tuple = tuple(longToList(self._ip)) 54 | else: 55 | if isinstance(address, bytes): 56 | address = address.decode('utf-8') 57 | 58 | if isinstance(address, str): 59 | octets = (i.strip() for i in address.split('.')) 60 | else: 61 | octets = address 62 | 63 | try: 64 | octets = [int(i) for i in octets][:4] 65 | except Exception: 66 | raise ValueError("{ip!r} is not a valid IPv4: non-integer data supplied".format( 67 | ip=address, 68 | )) 69 | else: 70 | if len(octets) < 4: 71 | raise ValueError("{ip} is not a valid IPv4: length < 4".format( 72 | ip=address, 73 | )) 74 | 75 | if any(True for i in octets if i < 0 or i > 255): 76 | raise ValueError("{ip} is not a valid IPv4: non-byte values present".format( 77 | ip=address, 78 | )) 79 | 80 | self._ip_tuple = tuple(octets) 81 | 82 | def __eq__(self, other): 83 | if not other and not isinstance(other, IPv4): 84 | return False 85 | 86 | if isinstance(other, str): 87 | other = IPv4(other) 88 | elif isinstance(other, int): 89 | return int(self) == other 90 | return self._ip_tuple == tuple(other) 91 | 92 | def __hash__(self): 93 | return hash(self._ip_tuple) 94 | 95 | def __getitem__(self, index): 96 | return self._ip_tuple[index] 97 | 98 | def __bool__(self): 99 | return any(self._ip_tuple) 100 | 101 | def __int__(self): 102 | if self._ip is None: 103 | self._ip = listToLong(self._ip_tuple) 104 | return self._ip 105 | 106 | def __repr__(self): 107 | return "IPv4({})".format(self) 108 | 109 | def __bytes__(self): 110 | return bytes(self._ip_tuple) 111 | 112 | def __str__(self): 113 | if not self._ip_string: 114 | self._ip_string = "{}.{}.{}.{}".format(*self._ip_tuple) 115 | return self._ip_string 116 | 117 | def isSubnetMember(self, address, prefix): 118 | """ 119 | Evaluates whether this IPv4 address is a member of the specifed subnet. 120 | 121 | :param address: An IPv4, which may be a dotted quad, a quadruple of 122 | bytes, or a 32-bit, unsigned integer. 123 | :param prefix: A subnet mask or CIDR prefix, like `'255.255.255.0'` 124 | or `24`. 125 | :return bool: `True` if this IPv4 is a member of the subnet. 126 | :except ValueError: The address or prefix could not be processed. 127 | """ 128 | if isinstance(prefix, int): 129 | if 0 <= prefix <= 32: 130 | mask = (_MAX_IP_INT << (32 - prefix)) 131 | else: 132 | raise ValueError("Invalid CIDR prefix: {prefix}".format( 133 | prefix=prefix, 134 | )) 135 | else: 136 | mask = int(IPv4(prefix)) 137 | return mask & int(IPv4(address)) == mask & int(self) 138 | 139 | @classmethod 140 | def parseSubnet(cls, subnet): 141 | """ 142 | Splits a subnet-specifier written in common "ip/mask" notation into its 143 | constituent parts, allowing patterns like 144 | `(address, prefix) = IPv4.parseSubnet("10.50.0.0/255.255.0.0")` and 145 | `.isSubnetMember(*.parseSubnet("192.168.0.0/24"))`. 146 | 147 | :param subnet: A string, using dotted-quad-slash-notation, with either 148 | an IPv4 mask or a CIDR integer as its complement. 149 | :return tuple(2): The address and prefix components of the subnet. 150 | :except ValueError: The subnet could not be interpreted. 151 | """ 152 | (address, prefix) = subnet.split('/', 1) 153 | if prefix.isdigit(): 154 | return (address, int(prefix)) 155 | return (address, prefix) 156 | 157 | -------------------------------------------------------------------------------- /libpydhcpserver/libpydhcpserver/dhcp_types/mac.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | """ 3 | libpydhcpserver.dhcp_types.mac 4 | ============================== 5 | Defines a standard way of representing MACs within the library. 6 | 7 | Legal 8 | ----- 9 | This file is part of libpydhcpserver. 10 | libpydhcpserver is free software; you can redistribute it and/or modify 11 | it under the terms of the GNU General Public License as published by 12 | the Free Software Foundation; either version 3 of the License, or 13 | (at your option) any later version. 14 | 15 | This program is distributed in the hope that it will be useful, 16 | but WITHOUT ANY WARRANTY; without even the implied warranty of 17 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 18 | GNU General Public License for more details. 19 | 20 | You should have received a copy of the GNU General Public License 21 | along with this program. If not, see . 22 | 23 | (C) Neil Tallim, 2021 24 | (C) Mathieu Ignacio, 2008 25 | """ 26 | from .conversion import (listToNumber) 27 | 28 | class MAC(object): 29 | """ 30 | Provides a standardised way of representing MACs. 31 | """ 32 | _mac = None #: The MAC encapsulated by this object, as a tuple of bytes. 33 | _mac_integer = None #: The MAC as an integer. 34 | _mac_string = None #: The MAC as a colon-delimited, lower-case string. 35 | 36 | def __init__(self, address): 37 | """ 38 | Constructs a MAC abstraction from a concrete representation. 39 | 40 | :param address: A MAC, which may be a string of twelve hex digits, 41 | optionally separated by non-hex characters, like ':', 42 | '.', or '-', a sequence of six bytes, or an unsigned 43 | integer. 44 | :except ValueError: The address could not be processed. 45 | """ 46 | if isinstance(address, int): 47 | if not 0 <= address <= 281474976710655: 48 | raise ValueError("'{ip}' is not a valid IP: not a 32-bit unsigned integer".format( 49 | ip=address, 50 | )) 51 | self._mac_integer = int(address) 52 | self._mac = ( 53 | self._mac_integer >> 40 & 0xFF, 54 | self._mac_integer >> 32 & 0xFF, 55 | self._mac_integer >> 24 & 0xFF, 56 | self._mac_integer >> 16 & 0xFF, 57 | self._mac_integer >> 8 & 0xFF, 58 | self._mac_integer & 0xFF, 59 | ) 60 | else: 61 | if isinstance(address, bytes): 62 | address = address.decode('utf-8') 63 | 64 | if isinstance(address, str): 65 | address = [c for c in address.lower() if c.isdigit() or 'a' <= c <= 'f'] 66 | if len(address) != 12: 67 | raise ValueError("Expected twelve hex digits as a MAC identifier; received {}".format(len(address))) 68 | 69 | mac = [] 70 | while address: 71 | mac.append(int(address.pop(0), 16) * 16 + int(address.pop(0), 16)) 72 | self._mac = tuple(mac) 73 | else: 74 | self._mac = tuple(address) 75 | if len(self._mac) != 6 or any((type(d) is not int or d < 0 or d > 255) for d in self._mac): 76 | raise ValueError("Expected a sequence of six bytes as a MAC identifier; received {!r}".format(self._mac)) 77 | 78 | def __eq__(self, other): 79 | if not other and not isinstance(other, MAC): 80 | return False 81 | 82 | if isinstance(other, str): 83 | other = MAC(other) 84 | elif isinstance(other, int): 85 | return int(self) == other 86 | return self._mac == tuple(other) 87 | 88 | def __hash__(self): 89 | return hash(self._mac) 90 | 91 | def __getitem__(self, index): 92 | return self._mac[index] 93 | 94 | def __bool__(self): 95 | return any(self._mac) 96 | 97 | def __int__(self): 98 | if self._mac_integer is None: 99 | self._mac_integer = listToNumber(self._mac) 100 | return self._mac_integer 101 | 102 | def __repr__(self): 103 | return "MAC(%r)" % (str(self)) 104 | 105 | def __bytes__(self): 106 | return bytes(self._mac) 107 | 108 | def __str__(self): 109 | if self._mac_string is None: 110 | self._mac_string = "{:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}".format(*self._mac) 111 | return self._mac_string 112 | 113 | -------------------------------------------------------------------------------- /libpydhcpserver/libpydhcpserver/getifaddrslib.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | """ 3 | getifaddrslib 4 | ============= 5 | Support for resolving an IPv4 address to a network interface and retrieving the 6 | MAC address for an interface by name, in pure, stdlib CPython. 7 | 8 | Suitable for use with Linux, FreeBSD, OpenBSD, NetBSD, DragonflyBSD, and 9 | Mac OS X. 10 | 11 | Legal 12 | ===== 13 | This is free and unencumbered software released into the public domain. 14 | 15 | Anyone is free to copy, modify, publish, use, compile, sell, or 16 | distribute this software, either in source code form or as a compiled 17 | binary, for any purpose, commercial or non-commercial, and via any 18 | medium. 19 | 20 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 21 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 22 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 23 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 24 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 25 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 26 | OTHER DEALINGS IN THE SOFTWARE. 27 | 28 | Authors 29 | ======= 30 | Neil Tallim 31 | """ 32 | import ctypes 33 | import ctypes.util 34 | import socket 35 | 36 | #Linux constants that might not be present in Python 37 | _AF_PACKET = (hasattr(socket, 'AF_PACKET') and socket.AF_PACKET) or 17 38 | #BSD constants that might not be present in Python 39 | _AF_LINK = (hasattr(socket, 'AF_LINK') and socket.AF_LINK) or 18 40 | 41 | _LIBC = ctypes.CDLL(ctypes.util.find_library('c')) 42 | 43 | class struct_sockaddr(ctypes.Structure): 44 | _fields_ = ( 45 | ('sa_family', ctypes.c_ushort), 46 | ) 47 | 48 | class struct_sockaddr_in(ctypes.Structure): 49 | _fields_ = ( 50 | ('sin_family', ctypes.c_ushort), 51 | ('sin_port', ctypes.c_uint16), 52 | ('sin_addr', ctypes.c_ubyte * 4), 53 | ) 54 | 55 | class struct_ifaddrs(ctypes.Structure): pass 56 | struct_ifaddrs._fields_ = ( 57 | ('ifa_next', ctypes.POINTER(struct_ifaddrs)), 58 | ('ifa_name', ctypes.c_char_p), 59 | ('ifa_flags', ctypes.c_uint32), 60 | ('ifa_addr', ctypes.POINTER(struct_sockaddr)), 61 | ) #Linux diverges from BSD for the rest, but it's safe to omit the tail 62 | 63 | #AF_LINK 64 | class struct_sockaddr_dl(ctypes.Structure): 65 | _fields_ = ( 66 | ('sdl_len', ctypes.c_byte), 67 | ('sdl_family', ctypes.c_byte), 68 | ('sdl_index', ctypes.c_ushort), 69 | ('sdl_type', ctypes.c_ubyte), 70 | ('sdl_nlen', ctypes.c_ubyte), 71 | ('sdl_alen', ctypes.c_ubyte), 72 | ('sdl_slen', ctypes.c_ubyte), 73 | ('sdl_data', ctypes.c_ubyte * 12), 74 | ) 75 | #AF_PACKET 76 | class struct_sockaddr_ll(ctypes.Structure): 77 | _fields_ = ( 78 | ('sll_family', ctypes.c_ushort), 79 | ('sll_protocol', ctypes.c_ushort), 80 | ('sll_ifindex', ctypes.c_int32), 81 | ('sll_hatype', ctypes.c_ushort), 82 | ('sll_pkttype', ctypes.c_ubyte), 83 | ('sll_halen', ctypes.c_ubyte), 84 | ('sll_addr', ctypes.c_ubyte * 8), 85 | ) 86 | 87 | def _evaluate_ipv4(ifaddr, ipv4): 88 | if ifaddr.ifa_addr: 89 | sockaddr = ifaddr.ifa_addr.contents 90 | if sockaddr.sa_family == socket.AF_INET: #IPv4 address 91 | sockaddr_in = ctypes.cast(ctypes.pointer(sockaddr), ctypes.POINTER(struct_sockaddr_in)).contents 92 | return socket.inet_ntop(socket.AF_INET, sockaddr_in.sin_addr) == ipv4 93 | return False 94 | 95 | def _extract_ipv4(ifaddr): 96 | return ifaddr.ifa_name.decode("utf-8") 97 | 98 | def _evaluate_mac(ifaddr, iface): 99 | if ifaddr.ifa_name.decode("utf-8") == iface: 100 | sockaddr = ifaddr.ifa_addr.contents 101 | if sockaddr.sa_family == _AF_PACKET: 102 | return True 103 | sockaddr_dl = ctypes.cast(ctypes.pointer(sockaddr), ctypes.POINTER(struct_sockaddr_dl)).contents 104 | return sockaddr_dl.sdl_family == _AF_LINK 105 | return False 106 | 107 | def _extract_mac(ifaddr): 108 | sockaddr = ifaddr.ifa_addr.contents 109 | if sockaddr.sa_family == _AF_PACKET: 110 | sockaddr_ll = ctypes.cast(ctypes.pointer(sockaddr), ctypes.POINTER(struct_sockaddr_ll)).contents 111 | mac = sockaddr_ll.sll_addr[:sockaddr_ll.sll_halen] 112 | else: 113 | sockaddr_dl = ctypes.cast(ctypes.pointer(sockaddr), ctypes.POINTER(struct_sockaddr_dl)).contents 114 | mac = sockaddr_dl.sdl_data[sockaddr_dl.sdl_nlen:sockaddr_dl.sdl_nlen + sockaddr_dl.sdl_alen] 115 | return ':'.join('{:02x}'.format(b) for b in mac) 116 | 117 | def _evaluate_ifaddrs(evaluator, extractor): 118 | ifap = ctypes.POINTER(struct_ifaddrs)() 119 | if _LIBC.getifaddrs(ctypes.pointer(ifap)): #Non-zero response 120 | raise OSError(ctypes.get_errno()) 121 | 122 | try: 123 | ifaddr = ifap.contents 124 | while True: 125 | if evaluator(ifaddr): 126 | return extractor(ifaddr) 127 | if not ifaddr.ifa_next: 128 | break 129 | ifaddr = ifaddr.ifa_next.contents 130 | finally: 131 | _LIBC.freeifaddrs(ifap) 132 | return None 133 | 134 | def get_network_interface(ipv4): 135 | ipv4 = str(ipv4) 136 | interface = _evaluate_ifaddrs( 137 | lambda ifaddr : _evaluate_ipv4(ifaddr, ipv4), 138 | _extract_ipv4, 139 | ) 140 | if interface is None: 141 | raise ValueError("Unable to find an interface with IP address {}".format(ipv4)) 142 | 143 | #Handle aliased interfaces, like 'eth0:1' 144 | return interface.split(':', 1)[0] 145 | 146 | def get_mac_address(iface): 147 | return _evaluate_ifaddrs( 148 | lambda ifaddr : _evaluate_mac(ifaddr, iface), 149 | _extract_mac, 150 | ) 151 | 152 | if __name__ == '__main__': 153 | import sys 154 | iface_name = get_network_interface(sys.argv[1]) 155 | print(iface_name) 156 | print(get_mac_address(iface_name)) 157 | -------------------------------------------------------------------------------- /libpydhcpserver/setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- encoding: utf-8 -*- 3 | """ 4 | Deployment script for libpydhcpserver. 5 | """ 6 | from distutils.core import setup 7 | import re 8 | 9 | import libpydhcpserver 10 | 11 | setup( 12 | name='libpydhcpserver', 13 | version=libpydhcpserver.VERSION, 14 | description='Pure-Python, spec-compliant DHCP-packet-processing and networking library', 15 | long_description=( 16 | 'libpydhcpserver provides the implementation for staticDHCPd\'s DHCP-processing' 17 | ' needs, but has a stable API and may be used by other applications that have a' 18 | ' reason to work with DHCP packets and perform server-oriented functions.' 19 | ), 20 | author=re.search(', (.*?) <', libpydhcpserver.COPYRIGHT).group(1), 21 | author_email=re.search('<(.*?)>', libpydhcpserver.COPYRIGHT).group(1), 22 | license='GPLv3', 23 | url=libpydhcpserver.URL, 24 | packages=[ 25 | 'libpydhcpserver', 26 | 'libpydhcpserver.dhcp_types', 27 | ], 28 | ) 29 | -------------------------------------------------------------------------------- /makedebs.sh: -------------------------------------------------------------------------------- 1 | #Runs both libpydhcpserver and staticDHCPd's Debian scripts 2 | cd libpydhcpserver 3 | /usr/bin/debuild -uc -us 4 | cd .. 5 | 6 | cd staticDHCPd 7 | /usr/bin/debuild -uc -us 8 | cd .. 9 | 10 | rm *.dsc 11 | rm *.changes 12 | rm *.build 13 | rm *.buildinfo 14 | rm *.tar.xz 15 | -------------------------------------------------------------------------------- /staticDHCPd/MANIFEST: -------------------------------------------------------------------------------- 1 | README 2 | setup.py 3 | conf/conf.py.sample 4 | conf/extensions/HOWTO 5 | staticDHCPd 6 | staticdhcpdlib/__init__.py 7 | staticdhcpdlib/config.py 8 | staticdhcpdlib/dhcp.py 9 | staticdhcpdlib/logging_handlers.py 10 | staticdhcpdlib/statistics.py 11 | staticdhcpdlib/system.py 12 | staticdhcpdlib/databases/__init__.py 13 | staticdhcpdlib/databases/_caching.py 14 | staticdhcpdlib/databases/_ini.py 15 | staticdhcpdlib/databases/_sql.py 16 | staticdhcpdlib/databases/generic.py 17 | staticdhcpdlib/web/__init__.py 18 | staticdhcpdlib/web/_resources.py 19 | staticdhcpdlib/web/_templates.py 20 | staticdhcpdlib/web/functions.py 21 | staticdhcpdlib/web/headers.py 22 | staticdhcpdlib/web/methods.py 23 | staticdhcpdlib/web/server.py 24 | -------------------------------------------------------------------------------- /staticDHCPd/README: -------------------------------------------------------------------------------- 1 | Thirty-second upgrade guide for people who hate using diff: 2 | Install and go. Except in very rare cases, which will be documented here, 3 | your old conf.py, extensions, and scripting logic will be 4 | forwards-compatible with this version. 5 | 6 | If coming from anything before 1.6.1, move conf.py into the conf/ directory 7 | or create /etc/staticDHCPd/ and move it there. Your old file needs no TLC. 8 | 9 | ------------------------------------------------------------------------------- 10 | 11 | 12 | Installation instructions: 13 | Run install.sh with privileges that can create content in 14 | /etc and /usr/local/bin. Follow the resulting on-screen text to integrate 15 | the server with your OS's daemon-management engine. 16 | 17 | Just remember to set up conf.py and everything should Just Work(TM). Before 18 | installing the server, though, run through the five-minute quickstart 19 | described below; it doesn't require that you make any permanent changes to 20 | your host and it'll run out of the source distribution as-is. 21 | 22 | 23 | ------------------------------------------------------------------------------- 24 | 25 | 26 | Five-minute "does this really work?" setup guide for busy administrators 27 | Uses an INI file or sqlite3 to avoid unnecessary installations 28 | 29 | (If you need more information, see the project page at 30 | https://github.com/flan/staticdhcpd) 31 | 32 | 33 | Step 1: Gather resources 34 | You need the code, which came with this lovely text file, and a computer 35 | on which to run it. Since this is a UNIX-formatted file, you've probably 36 | already got that, too. (Also Python 3.5+, but no modern UNIX-like system is 37 | without that) 38 | 39 | The last thing you need is enough access to bind to the DHCP ports. 40 | Since there's no way you're going to just run this server on a production 41 | box without testing it first, you've almost certainly satisfied this 42 | requirement, too. 43 | 44 | So you're done. That was easy. 45 | 46 | Step 2: Set up the DHCP database 47 | (This example assumes your network is similar to that of a typical home 48 | user; if this is not the case, you will need to adjust things, but you 49 | probably wouldn't be playing with a DHCP server if you were a typical home 50 | user anyway) 51 | 52 | The example values below will give the MAC 'aa:bb:cc:dd:ee:ff' the IP 53 | '192.168.0.197' and no hostname. You'll notice that non-host-specific 54 | parameters are inherited from its subnet-classification, specifically 55 | things like lease-time and basic routing parameters. DNS, NTP, and 56 | other properties aren't specified in this example, but are in the samples/ 57 | directory. 58 | 59 | (The term "subnet" is used loosely here: the only thing that matters is that 60 | the "subnet" and "serial" values match for inheritance -- you could put 61 | "floor 3" in as a "subnet" if you wanted to. The term "subnet" was chosen 62 | because it seemed like the most likely classification system for 63 | administrators to use and recognise; similarly, "serial" is also up to you, 64 | it just allows for multiple definitions within the same "subnet" -- you 65 | might want to use the VLAN, or maybe you'll just always make it 0) 66 | 67 | INI method: 68 | Create a file with the following contents; the name is up to you. 69 | 70 | [192.168.0.0/24|0] 71 | lease-time: 14400 72 | gateway: 192.168.0.1 73 | subnet-mask: 255.255.255.0 74 | broadcast-address: 192.168.0.255 75 | 76 | [aa:bb:cc:dd:ee:ff] 77 | ip: 192.168.0.197 78 | subnet: 192.168.0.0/24 79 | serial: 0 80 | 81 | SQLite method: 82 | Open a terminal and run `sqlite3 dhcp.sqlite3` 83 | 84 | Copy and paste the contents of databases/sqlite.sql into the prompt. 85 | 86 | Now that your database is ready to go (SQLite is easy), add some rules. 87 | 88 | INSERT INTO subnets ( 89 | subnet, 90 | serial, 91 | lease_time, 92 | gateway, 93 | subnet_mask, 94 | broadcast_address, 95 | ntp_servers, 96 | domain_name_servers, 97 | domain_name 98 | ) VALUES ( 99 | '192.168.0.0/24', 100 | 0, 101 | 14400, 102 | '192.168.0.1', 103 | '255.255.255.0', 104 | '192.168.0.255', 105 | NULL, 106 | NULL, 107 | NULL 108 | ); 109 | 110 | INSERT INTO maps ( 111 | mac, 112 | ip, 113 | hostname, 114 | subnet, 115 | serial 116 | ) VALUES ( 117 | 'aa:bb:cc:dd:ee:ff', 118 | '192.168.0.197', 119 | NULL, 120 | '192.168.0.0/24', 121 | 0 122 | ); 123 | 124 | Step 3: Set up conf.py 125 | Copy 'conf/conf.py.sample' to 'conf/conf.py'. 126 | 127 | Edit the file and make the following changes: 128 | Set DHCP_SERVER_IP to whichever IP you want to listen on. 129 | 130 | If you are working with clients that do not understand the DHCP 131 | broadcast bit (mostly embedded devices running busybox/udhcpc), read 132 | about the DHCP_RESPONSE_INTERFACE option in the configuration doc. If 133 | not, don't worry about it. 134 | 135 | INI method: 136 | Set DATABASE_ENGINE to 'INI'; capitalization matters. 137 | 138 | Add the line "INI_FILE = '/home/you/ini-file-you-created'" 139 | 140 | SQLite method: 141 | Set DATABASE_ENGINE to 'SQLite'; capitalization matters. 142 | 143 | Add the line "SQLITE_FILE = '/home/you/sqlite-file-you-created'" 144 | 145 | Step 4: Start the server 146 | Run `sudo python staticDHCPd`. 147 | 148 | You should see a bunch of lines appear, explaining that the server is now 149 | running. 150 | 151 | Tell the device with the MAC given in step 3 to request an address and 152 | everything should Just Work(tm). 153 | 154 | Go to http://localhost:30880/ if you want to check out the web interface. 155 | 156 | Step 5: Kill the process 157 | When satisifed that the system works, hit ^C or send SIGTERM (15) to the 158 | process. 159 | 160 | 161 | You now have proof that what you have in your hands is a functional, 162 | ready-to-customise DHCP server. 163 | -------------------------------------------------------------------------------- /staticDHCPd/conf/conf.py.sample: -------------------------------------------------------------------------------- 1 | #Copy this file to one of the following locations, then rename it to conf.py: 2 | #/etc/staticDHCPd/, ./conf/ 3 | 4 | #For a full overview of what these parameters mean, and to further customise 5 | #your system, please consult the configuration and scripting guides in the 6 | #standard documentation 7 | 8 | 9 | # Whether to daemonise on startup (you don't want this during initial setup 10 | # or when using systemd) 11 | DAEMON = False 12 | 13 | #WARNING: The default UID and GID are those of root. THIS IS NOT GOOD! 14 | #If testing, set them to your id, which you can find using `id` in a terminal. 15 | #If going into production, if no standard exists in your environment, use the 16 | #values of "nobody": `id nobody` 17 | #The UID this server will use after initial setup 18 | UID = 0 19 | #The GID this server will use after initial setup 20 | GID = 0 21 | 22 | #The IP of the interface to use for DHCP traffic 23 | DHCP_SERVER_IP = '192.168.1.1' 24 | 25 | #The database-engine to use 26 | #For details, see the configuration guide in the documentation. 27 | DATABASE_ENGINE = None 28 | -------------------------------------------------------------------------------- /staticDHCPd/conf/staticDHCPd_extensions/HOWTO: -------------------------------------------------------------------------------- 1 | To make extension-modules available for import in conf.py, copy or link them into this directory. 2 | -------------------------------------------------------------------------------- /staticDHCPd/control-scripts/ca.uguu.puukusoft.staticDHCPd.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Label 5 | ca.uguu.puukusoft.staticDHCPd 6 | ProgramArguments 7 | 8 | /usr/bin/env 9 | python 10 | -OO 11 | /usr/local/bin/staticDHCPd 12 | 13 | KeepAlive 14 | 15 | RunAtLoad 16 | 17 | ServiceDescription 18 | Provides staticDHCPd 19 | 20 | 21 | -------------------------------------------------------------------------------- /staticDHCPd/control-scripts/staticDHCPd.service: -------------------------------------------------------------------------------- 1 | [Service] 2 | Type=simple 3 | 4 | ExecStart=/usr/bin/staticDHCPd --config /etc/staticDHCPd/conf.py 5 | ExecReload=/bin/kill -HUP $MAINPID 6 | ExecStop=/bin/kill -TERM $MAINPID 7 | 8 | Restart=always 9 | RestartSec=1s 10 | 11 | AmbientCapabilities=CAP_NET_BIND_SERVICE 12 | #User=something-safe 13 | #Group=something-safe 14 | 15 | [Install] 16 | WantedBy=default.target 17 | -------------------------------------------------------------------------------- /staticDHCPd/databases/dhcp.ini: -------------------------------------------------------------------------------- 1 | #An example of what an INI file should look like 2 | 3 | #These files should be relatively small. If you expect to have enough devices 4 | #that you will require runtime modification, programmatic management, or 5 | #database-based reporting, consider using SQLite as an alternative 6 | 7 | #Sections take the form of either a MAC address or a subnet/serial pair, 8 | #separated by a pipe-character 9 | ;This is a subnet declaration; note that the value after the pipe must be an 10 | ;integer, while there are no restrictions on the content before the pipe, 11 | ;including other pipes 12 | [192.168.0.0/24|0] 13 | ;The number of seconds a "lease" is good for 14 | lease-time: 87840 15 | ;A comma-separated list of IPv4 gateways to supply to clients; may be null 16 | gateway: 192.168.0.1 17 | ;The IPv4 subnet mask to supply to clients; may be omitted 18 | subnet-mask: 255.255.255.0 19 | ;The IPv4 broadcast address to supply to clients; may be omitted 20 | broadcast-address: 192.168.0.255 21 | ;A comma-separated list of IPv4 addresses pointing to NTP servers; limit 3; may be omitted 22 | ntp-servers: 192.168.0.1, 192.168.0.2, 192.168.0.3 23 | ;A comma-separated list of IPv4 addresses pointing to DNS servers; limit 3; may be omitted 24 | domain-name-servers: 192.168.0.1, 192.168.0.2, 192.168.0.3 25 | ;The name of the search domain to be provided to clients; may be omitted 26 | domain-name: "example.org" 27 | 28 | ;This is a MAC declaration; delimiters, like colons, and case are optional, so 29 | ;[0800272c494b] and [08.00-27;2C:49,4B] are equivalent, but harder to read 30 | [08:00:27:2c:49:4b] 31 | ;The IPv4 address to provide to the client 32 | ip: 192.168.0.200 33 | ;The hostname to assign to the client; may be omitted 34 | hostname: testbox 35 | ;A human-readable subnet-identifier, used in conjunction with the serial 36 | subnet: 192.168.0.0/24 37 | ;Together with the serial, this identifies the options to pass to the client 38 | serial: 0 39 | 40 | 41 | #So a really minimal file, in which no gateway, NTP, or DNS are needed, might 42 | #look like this: 43 | [192.168.1.0/24|0] 44 | lease-time: 87840 45 | 46 | [08:00:27:2c:49:4c] 47 | ip: 192.168.1.200 48 | subnet: 192.168.1.0/24 49 | serial: 0 50 | 51 | #This would give 08:00:27:2c:49:4c the IP address 192.168.1.200 for one day 52 | -------------------------------------------------------------------------------- /staticDHCPd/databases/mysql.sql: -------------------------------------------------------------------------------- 1 | CREATE DATABASE dhcp; 2 | USE dhcp; 3 | 4 | CREATE TABLE subnets ( 5 | subnet CHAR(18) NOT NULL, -- A human-readable subnet-identifier, large enough to hold a CIDR mask. 6 | serial SMALLINT UNSIGNED NOT NULL DEFAULT 0, -- A means of allowing a subnet to be reused, just in case you have two 192.168.1.0/24s. 7 | lease_time MEDIUMINT UNSIGNED NOT NULL, -- The number of seconds a "lease" is good for. This can be massive unless properties change often. 8 | gateway TEXT DEFAULT NULL, -- A comma-separated list of IPv4 gateways to supply to clients; may be null. 9 | subnet_mask CHAR(15), -- The IPv4 subnet mask to supply to clients; may be null. 10 | broadcast_address CHAR(15), -- The IPv4 broadcast address to supply to clients; may be null. 11 | ntp_servers CHAR(50), -- A comma-separated list of IPv4 addresses pointing to NTP servers; limit 3; may be null. 12 | domain_name_servers CHAR(50), -- A comma-separated list of IPv4 addresses pointing to DNS servers; limit 3; may be null. 13 | domain_name CHAR(128), -- The name of the search domain to be provided to clients. 14 | PRIMARY KEY(subnet, serial) 15 | ); 16 | 17 | CREATE TABLE maps ( 18 | mac CHAR(17) PRIMARY KEY, -- The MAC address of the client to whom the IP and associated options will be passed. 19 | ip CHAR(15) NOT NULL, -- The IPv4 address to provide to the client identified by the associated MAC. 20 | hostname CHAR(32), -- The hostname to assign to the client; may be null. 21 | subnet CHAR(18) NOT NULL, -- A human-readable subnet-identifier, used in conjunction with the serial. 22 | serial SMALLINT UNSIGNED NOT NULL DEFAULT 0, -- Together with the serial, this identifies the options to pass to the client. 23 | UNIQUE (ip, subnet, serial), 24 | FOREIGN KEY (subnet, serial) REFERENCES subnets (subnet, serial) 25 | ); 26 | 27 | delimiter | 28 | CREATE PROCEDURE cleanup() 29 | BEGIN 30 | OPTIMIZE LOCAL TABLE subnets, maps; 31 | END; 32 | | 33 | delimiter ; 34 | 35 | /* staticDHCPd requires an account with SELECT access; the first of these lines grants that against its 36 | default config settings; the second provides a management account so you don't have to use root. 37 | How you get entries into the database is up to you, however. 38 | GRANT SELECT ON dhcp.* TO 'dhcp_user'@'localhost' IDENTIFIED BY 'dhcp_pass'; 39 | GRANT SELECT, INSERT, DELETE, UPDATE, EXECUTE ON dhcp.* TO 'dhcp_maintainer'@'localhost' IDENTIFIED by 'dhcp_pass'; 40 | */ 41 | -------------------------------------------------------------------------------- /staticDHCPd/databases/oracle.sql: -------------------------------------------------------------------------------- 1 | CREATE DATABASE dhcp; 2 | ALTER SESSION SET CURRENT_SCHEMA = dhcp; 3 | 4 | CREATE TABLE subnets ( 5 | subnet VARCHAR2(18) NOT NULL, -- A human-readable subnet-identifier, large enough to hold a CIDR mask. 6 | serial NUMBER (15,0) DEFAULT 0 NOT NULL, -- A means of allowing a subnet to be reused, just in case you have two 192.168.1.0/24s. 7 | lease_time NUMBER (15,0) NOT NULL, -- The number of seconds a "lease" is good for. This can be massive unless properties change often. 8 | gateway VARCHAR2(512), -- A comma-separated list of IPv4 gateways to supply to clients; may be null. 9 | subnet_mask VARCHAR2(15), -- The IPv4 subnet mask to supply to clients; may be null. 10 | broadcast_address VARCHAR2(15), -- The IPv4 broadcast address to supply to clients; may be null. 11 | ntp_servers VARCHAR2(50), -- A comma-separated list of IPv4 addresses pointing to NTP servers; limit 3; may be null. 12 | domain_name_servers VARCHAR2(50), -- A comma-separated list of IPv4 addresses pointing to DNS servers; limit 3; may be null. 13 | domain_name VARCHAR2(128), -- The name of the search domain to be provided to clients. 14 | PRIMARY KEY(subnet, serial) 15 | ); 16 | 17 | CREATE TABLE maps ( 18 | mac CHAR(17) PRIMARY KEY, -- The MAC address of the client to whom the IP and associated options will be passed. 19 | ip VARCHAR2(15) NOT NULL, -- The IPv4 address to provide to the client identified by the associated MAC. 20 | hostname VARCHAR2(32), -- The hostname to assign to the client; may be null. 21 | subnet VARCHAR2(18) NOT NULL, -- A human-readable subnet-identifier, used in conjunction with the serial. 22 | serial NUMBER (15,0) DEFAULT 0 NOT NULL, -- Together with the serial, this identifies the options to pass to the client. 23 | UNIQUE (ip, subnet, serial), 24 | FOREIGN KEY (subnet, serial) REFERENCES subnets (subnet, serial) 25 | ); 26 | 27 | /* staticDHCPd requires an account with SELECT access; if anyone can provide a sane description of 28 | how to set this up under Oracle, it would be very much appreciated. 29 | */ 30 | 31 | -- Case-insensitive MAC-lookups may be handled in-database using the following method: 32 | -- - Include the following index 33 | CREATE INDEX case_insensitive_macs ON maps ((lower(mac))); -------------------------------------------------------------------------------- /staticDHCPd/databases/postgres.sql: -------------------------------------------------------------------------------- 1 | CREATE DATABASE dhcp; 2 | \connect dhcp; 3 | 4 | CREATE TABLE subnets ( 5 | subnet VARCHAR(18) NOT NULL, -- A human-readable subnet-identifier, large enough to hold a CIDR mask. 6 | serial SMALLINT NOT NULL DEFAULT 0, -- A means of allowing a subnet to be reused, just in case you have two 192.168.1.0/24s. 7 | lease_time INTEGER NOT NULL, -- The number of seconds a "lease" is good for. This can be massive unless properties change often. 8 | gateway VARCHAR, -- A comma-separated list of IPv4 gateways to supply to clients; may be null. 9 | subnet_mask VARCHAR(15), -- The IPv4 subnet mask to supply to clients; may be null. 10 | broadcast_address VARCHAR(15), -- The IPv4 broadcast address to supply to clients; may be null. 11 | ntp_servers VARCHAR(50), -- A comma-separated list of IPv4 addresses pointing to NTP servers; limit 3; may be null. 12 | domain_name_servers VARCHAR(50), -- A comma-separated list of IPv4 addresses pointing to DNS servers; limit 3; may be null. 13 | domain_name VARCHAR(128), -- The name of the search domain to be provided to clients. 14 | PRIMARY KEY(subnet, serial) 15 | ); 16 | 17 | CREATE TABLE maps ( 18 | mac CHAR(17) PRIMARY KEY, -- The MAC address of the client to whom the IP and associated options will be passed. 19 | ip VARCHAR(15) NOT NULL, -- The IPv4 address to provide to the client identified by the associated MAC. 20 | hostname VARCHAR(32), -- The hostname to assign to the client; may be null. 21 | subnet VARCHAR(18) NOT NULL, -- A human-readable subnet-identifier, used in conjunction with the serial. 22 | serial SMALLINT NOT NULL DEFAULT 0, -- Together with the serial, this identifies the options to pass to the client. 23 | UNIQUE (ip, subnet, serial), 24 | FOREIGN KEY (subnet, serial) REFERENCES subnets (subnet, serial) 25 | ); 26 | 27 | /* staticDHCPd requires an account with SELECT access; the first two of these lines grants that against its 28 | default config settings; the second pair provides a management account so you don't have to use root. 29 | How you get entries into the database is up to you, however. 30 | CREATE USER 'dhcp_user' WITH password 'dhcp_pass'; 31 | GRANT SELECT ON TABLE subnets, maps TO 'dhcp_user'; 32 | 33 | CREATE USER 'dhcp_maintainer' WITH password 'dhcp_pass'; 34 | GRANT SELECT, INSERT, DELETE, UPDATE, EXECUTE ON TABLE subnets, maps TO 'dhcp_maintainer'; 35 | */ 36 | 37 | -- Case-insensitive MAC-lookups may be handled in-database using the following method: 38 | -- - Include the following index 39 | CREATE INDEX case_insensitive_macs ON maps ((lower(mac))); 40 | -------------------------------------------------------------------------------- /staticDHCPd/databases/sqlite.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE subnets ( 2 | subnet TEXT NOT NULL, -- A human-readable subnet-identifier, typically a CIDR mask. 3 | serial INTEGER NOT NULL DEFAULT 0, -- A means of allowing a subnet to be reused, just in case you have two 192.168.1.0/24s. 4 | lease_time INTEGER NOT NULL, -- The number of seconds a "lease" is good for. This can be massive unless properties change often. 5 | gateway TEXT, -- The IPv4 gateway to supply to clients; may be null. 6 | subnet_mask TEXT, -- The IPv4 subnet mask to supply to clients; may be null. 7 | broadcast_address TEXT, -- The IPv4 broadcast address to supply to clients; may be null. 8 | ntp_servers TEXT, -- A comma-separated list of IPv4 addresses pointing to NTP servers; limit 3; may be null. 9 | domain_name_servers TEXT, -- A comma-separated list of IPv4 addresses pointing to DNS servers; limit 3; may be null. 10 | domain_name TEXT, -- The name of the search domain to be provided to clients. 11 | PRIMARY KEY(subnet, serial) 12 | ); 13 | 14 | CREATE TABLE maps ( 15 | mac TEXT PRIMARY KEY NOT NULL, -- The MAC address of the client to whom the IP and associated options will be passed. 16 | ip TEXT NOT NULL, -- The IPv4 address to provide to the client identified by the associated MAC. 17 | hostname TEXT, -- The hostname to assign to the client; may be null. 18 | subnet TEXT NOT NULL, -- A human-readable subnet-identifier, used in conjunction with the serial. 19 | serial INTEGER NOT NULL DEFAULT 0, -- Together with the serial, this identifies the options to pass to the client. 20 | UNIQUE (ip, subnet, serial), 21 | FOREIGN KEY (subnet, serial) REFERENCES subnets (subnet, serial) 22 | ); 23 | 24 | -- Case-insensitive MAC-lookups may be handled in-database using either of the following methods: 25 | -- - Put "COLLATE NOCASE" in the column-definition in maps:mac 26 | -- - Include the following index 27 | CREATE INDEX case_insensitive_macs ON maps (mac COLLATE NOCASE); 28 | -------------------------------------------------------------------------------- /staticDHCPd/debian/compat: -------------------------------------------------------------------------------- 1 | 12 2 | -------------------------------------------------------------------------------- /staticDHCPd/debian/control: -------------------------------------------------------------------------------- 1 | Source: staticdhcpd 2 | Maintainer: Neil Tallim 3 | Section: python 4 | Priority: optional 5 | Build-Depends: python3 (>= 3.5), debhelper (>= 12), dh-python 6 | Standards-Version: 4.5.1 7 | 8 | Package: staticdhcpd 9 | Architecture: all 10 | Depends: ${misc:Depends}, ${python3:Depends}, python3 (>= 3.5), 11 | python3-libpydhcpserver (>=3.0.0) 12 | Suggests: python3-scapy (>= 2.0.1), python3-mysqldb (>= 1.2), python3-psycopg2 (>=2.2) 13 | Description: Highly customisable, static-lease-focused DHCP server 14 | staticDHCPd is an extensively customisable, high-performance, 15 | RFC-spec-compliant DHCP server, well-suited to labs, LAN parties, home and 16 | small-office networks, and specialised networks of vast size. 17 | . 18 | It supports all major DHCP extension RFCs and features a rich, plugin-oriented 19 | web-interface, has a variety of modules, ranging from statistics-management 20 | to notification services to dynamic address-provisioning and 21 | network-auto-discovery. 22 | . 23 | Multiple backend databases are supported, from INI files to RDBMS SQL servers, 24 | with examples of how to write and integrate your own, such as a REST-JSON 25 | endpoint, simple enough to complete in minutes. 26 | -------------------------------------------------------------------------------- /staticDHCPd/debian/copyright: -------------------------------------------------------------------------------- 1 | Files: * 2 | Copyright: 2021 Neil Tallim 3 | License: GPL-3+ 4 | 5 | License: GPL-3+ 6 | staticDHCPd is free software; you can redistribute it and/or modify it 7 | under the terms of the GNU General Public License as published by the Free 8 | Software Foundation; either version 3 of the License, or (at your option) any 9 | later version. 10 | 11 | This program is distributed in the hope that it will be useful, but WITHOUT 12 | ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 13 | FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . 17 | 18 | On Debian systems, the full text of the GNU General Public License version 3 19 | can be found in the file /usr/share/common-licenses/GPL-3. 20 | -------------------------------------------------------------------------------- /staticDHCPd/debian/docs: -------------------------------------------------------------------------------- 1 | README 2 | doc/customisation/configuration.rst 3 | doc/customisation/scripting.rst 4 | -------------------------------------------------------------------------------- /staticDHCPd/debian/examples: -------------------------------------------------------------------------------- 1 | conf/conf.py.sample 2 | extensions 3 | databases 4 | -------------------------------------------------------------------------------- /staticDHCPd/debian/rules: -------------------------------------------------------------------------------- 1 | #!/usr/bin/make -f 2 | 3 | export PYBUILD_DISABLE=test 4 | export PYBUILD_NAME=staticdhcpd 5 | 6 | %: 7 | dh $@ --with=python3 --buildsystem=pybuild 8 | -------------------------------------------------------------------------------- /staticDHCPd/debian/source/format: -------------------------------------------------------------------------------- 1 | 3.0 (native) 2 | -------------------------------------------------------------------------------- /staticDHCPd/doc/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = /usr/share/sphinx/scripts/python2/sphinx-build 7 | PAPER = 8 | 9 | # Internal variables. 10 | PAPEROPT_a4 = -D latex_paper_size=a4 11 | PAPEROPT_letter = -D latex_paper_size=letter 12 | ALLSPHINXOPTS = -d _build/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 13 | 14 | .PHONY: help clean html dirhtml pickle json htmlhelp qthelp latex changes linkcheck doctest 15 | 16 | help: 17 | @echo "Please use \`make ' where is one of" 18 | @echo " html to make standalone HTML files" 19 | @echo " dirhtml to make HTML files named index.html in directories" 20 | @echo " pickle to make pickle files" 21 | @echo " json to make JSON files" 22 | @echo " htmlhelp to make HTML files and a HTML help project" 23 | @echo " qthelp to make HTML files and a qthelp project" 24 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 25 | @echo " changes to make an overview of all changed/added/deprecated items" 26 | @echo " linkcheck to check all external links for integrity" 27 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 28 | 29 | clean: 30 | -rm -rf _build/* 31 | 32 | html: 33 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) _build/html 34 | @echo 35 | @echo "Build finished. The HTML pages are in _build/html." 36 | 37 | dirhtml: 38 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) _build/dirhtml 39 | @echo 40 | @echo "Build finished. The HTML pages are in _build/dirhtml." 41 | 42 | pickle: 43 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) _build/pickle 44 | @echo 45 | @echo "Build finished; now you can process the pickle files." 46 | 47 | json: 48 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) _build/json 49 | @echo 50 | @echo "Build finished; now you can process the JSON files." 51 | 52 | htmlhelp: 53 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) _build/htmlhelp 54 | @echo 55 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 56 | ".hhp project file in _build/htmlhelp." 57 | 58 | qthelp: 59 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) _build/qthelp 60 | @echo 61 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 62 | ".qhcp project file in _build/qthelp, like this:" 63 | @echo "# qcollectiongenerator _build/qthelp/pystrix.qhcp" 64 | @echo "To view the help file:" 65 | @echo "# assistant -collectionFile _build/qthelp/pystrix.qhc" 66 | 67 | latex: 68 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) _build/latex 69 | @echo 70 | @echo "Build finished; the LaTeX files are in _build/latex." 71 | @echo "Run \`make all-pdf' or \`make all-ps' in that directory to" \ 72 | "run these through (pdf)latex." 73 | 74 | changes: 75 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) _build/changes 76 | @echo 77 | @echo "The overview file is in _build/changes." 78 | 79 | linkcheck: 80 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) _build/linkcheck 81 | @echo 82 | @echo "Link check complete; look for any errors in the above output " \ 83 | "or in _build/linkcheck/output.txt." 84 | 85 | doctest: 86 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) _build/doctest 87 | @echo "Testing of doctests in the sources finished, look at the " \ 88 | "results in _build/doctest/output.txt." 89 | -------------------------------------------------------------------------------- /staticDHCPd/doc/api/databases.rst: -------------------------------------------------------------------------------- 1 | Database resources 2 | ================== 3 | If you have a specialised site or want to integrate staticDHCPd into an 4 | existing framework (please don't build your framework around it -- it's 5 | designed to fit into your architecture, not to *be* your architecture), you 6 | may find that you need to create a custom database handler. 7 | 8 | It's really not that hard: subclass the type of database you want, override 9 | ``lookupMac()``, and you're done. Everything you need to know is described 10 | below. 11 | 12 | Classes 13 | ------- 14 | .. autoclass:: databases.generic.Definition 15 | :members: 16 | 17 | .. autoclass:: databases.generic.Database 18 | :members: 19 | 20 | .. autoclass:: databases.generic.CachingDatabase 21 | :show-inheritance: 22 | :members: 23 | -------------------------------------------------------------------------------- /staticDHCPd/doc/api/index.rst: -------------------------------------------------------------------------------- 1 | Programming interfaces 2 | ====================== 3 | *staticDHCPd* is designed to be easy to extend. To this end, official 4 | :doc:`../customisation/extensions` are provided, covering all of the major 5 | ways in which internal data may be accessed. 6 | 7 | This section exists as an API reference for anyone who needs to dig deeper 8 | than what the sample extensions provide. 9 | 10 | .. toctree:: 11 | :maxdepth: 1 12 | 13 | databases.rst 14 | logging.rst 15 | statistics.rst 16 | web.rst 17 | -------------------------------------------------------------------------------- /staticDHCPd/doc/api/logging.rst: -------------------------------------------------------------------------------- 1 | Logging facilities 2 | ================== 3 | *staticDHCPd* uses the native Python `logging` subsystem, so if you want to 4 | work with it, just tap into that. 5 | 6 | It does, however, define custom logging handlers. 7 | 8 | Classes 9 | ------- 10 | .. autoclass:: logging_handlers.FIFOHandler 11 | :show-inheritance: 12 | 13 | .. automethod:: logging_handlers.FIFOHandler.flush 14 | 15 | .. automethod:: logging_handlers.FIFOHandler.readContents 16 | -------------------------------------------------------------------------------- /staticDHCPd/doc/api/statistics.rst: -------------------------------------------------------------------------------- 1 | Statistics structures 2 | ===================== 3 | Like most of *staticDHCPd*, the statistics interface is well-documented in the 4 | :doc:`scripting guide <../customisation/scripting>`. 5 | 6 | But it wouldn't really make sense to clutter that with data-types, so those 7 | are collected here. 8 | 9 | Named tuples 10 | ------------ 11 | .. autodata:: statistics.Statistics 12 | :annotation: 13 | -------------------------------------------------------------------------------- /staticDHCPd/doc/api/web.rst: -------------------------------------------------------------------------------- 1 | Web-interface functions 2 | ======================= 3 | Like most of *staticDHCPd*, the web interface is well-documented in the 4 | :doc:`scripting guide <../customisation/scripting>`. 5 | 6 | There are some methods that you might want to use in your own code that would 7 | be a waste of effort to reimplement, however, and those are described here. 8 | 9 | Methods 10 | ------- 11 | .. autofunction:: web.functions.sanitise 12 | -------------------------------------------------------------------------------- /staticDHCPd/doc/commentary/database.rst: -------------------------------------------------------------------------------- 1 | General database structure and concept 2 | ====================================== 3 | staticDHCPd's database structure is meant to be highly normalised, while 4 | capturing all of the most important attributes of a DHCP lease. 5 | 6 | However, its design, for legacy reasons, has some design features tied to its 7 | origins as an ISP's server in 2009. This section will provide some insight into 8 | why certain choices were made. 9 | 10 | "tables" 11 | -------- 12 | *staticDHCPd* was originally built against a MySQL database, since that's what 13 | shipped with OS X 10.6, which was the core of the server infrastructure on which 14 | it had to run. Its database access routines were built to be decoupled early in 15 | its lifetime, but the concept of using REST APIs to serve leases wasn't on the 16 | radar (SOAP was still fairly dominant at the time, which would have been 17 | unacceptably slow, especially for thousands of clients), and No-SQL models were 18 | still too new. The net effect of all of this is that the design was 19 | conceptualised from an SQL viewpoint and there was no need to change. 20 | 21 | By the time that the framework became mature enough to allow for non-SQL 22 | database backends, the architecture was too well-established (and, to be fair, 23 | well-vetted, without complications) to change. 24 | 25 | Built-in versus extension interfaces 26 | ------------------------------------ 27 | SQL-type database modules are built-in only to maintain backwards-compatibility. 28 | They will not be removed until a compelling reason is raised to force 29 | ``conf.py`` to be updated as part of an upgrade. 30 | 31 | All future database development will occur using extension-modules, because 32 | they're more flexible and can do whatever a site needs. 33 | 34 | Integration 35 | ----------- 36 | If you have an existing database that provides all or most of the information 37 | below in an SQL setup, it might be worthwhile to create a view to transform it 38 | into *staticDHCPd*'s format to reduce the amount of work you will need to do. 39 | 40 | "subnets" table 41 | --------------- 42 | A "subnet" is used to collect settings applicable to multiple clients. Its name 43 | comes from the idea that members of a subnet will typically share the same 44 | gateway, DNS servers, and other like details. 45 | 46 | .. data:: subnet(text) 47 | 48 | While it's generally a good idea to use a value like ``192.168.0.0/24`` so 49 | you know, at a glance, what subnet its clients should be on, it is perfectly 50 | legal to set a value like ``my subnet``: this field is just free-form text. 51 | 52 | The origin of this field's name is directly related to the origin of the 53 | name of the table. 54 | 55 | .. data:: serial(int) 56 | 57 | This field may be used to separate a subnet into partitions to do things 58 | like set different default gateways to reduce load on network hardware. 59 | 60 | Its name reflects the idea that, within a single subnet, there may be 61 | multiple configurations that are generated as environmental needs evolve. 62 | 63 | .. data:: lease_time(int) 64 | 65 | The number of seconds for which clients will believe their "leases" to be 66 | valid; by default, T1 is half of this, so stable clients may update their 67 | information in as little as half this time. 68 | 69 | .. data:: gateway(text) 70 | 71 | May be a list of comma-delimited IPv4 addresses or ``NULL`` to avoid 72 | setting the corresponding DHCP option. Normally, you will only specify one. 73 | 74 | .. data:: subnet_mask(test) 75 | 76 | May be an IPv4 address or ``NULL`` to avoid setting the corresponding DHCP 77 | option. CIDR notation is not supported at this time. 78 | 79 | .. data:: broadcast_address(text) 80 | 81 | May be an IPv4 address or ``NULL`` to avoid setting the corresponding DHCP 82 | option. 83 | 84 | .. data:: ntp_servers(text) 85 | 86 | May be a list of comma-delimited IPv4 addresses or ``NULL`` to avoid 87 | setting the corresponding DHCP option. Up to three may be specified. 88 | 89 | .. data:: domain_name_servers(text) 90 | 91 | May be a list of comma-delimited IPv4 addresses or ``NULL`` to avoid 92 | setting the corresponding DHCP option. Up to three may be specified. 93 | 94 | .. data:: domain_name(text) 95 | 96 | May be any arbitrary, FQDN-valid string or ``NULL`` to avoid setting the 97 | corresponding DHCP option. 98 | 99 | "maps" table 100 | ------------ 101 | Shortened from "mappings", this is where MACs are bound to specific leases. 102 | 103 | .. data:: mac(string) 104 | 105 | A lower-case, colon-separated MAC address. 106 | 107 | .. data:: ip(string) 108 | 109 | A dot-separated IPv4 address. 110 | 111 | .. data:: hostname(string) 112 | 113 | May be a string or ``NULL`` to avoid setting the corresponding DHCP option. 114 | 115 | .. data:: subnet(string) 116 | 117 | Must correspond to an entry in the `subnets` table. 118 | 119 | .. data:: serial(int) 120 | 121 | Must correspond to an entry in the `subnets` table. 122 | -------------------------------------------------------------------------------- /staticDHCPd/doc/commentary/faq.rst: -------------------------------------------------------------------------------- 1 | Frequently asked questions and feature notes 2 | ============================================ 3 | What platforms are supported? 4 | ----------------------------- 5 | Almost anything POSIX-compliant should work as a server, which includes Linux, 6 | the BSD family, and OS X. DHCP is a protocol, so it will serve Windows, your 7 | smartphone, and your Vita just fine. 8 | 9 | All core development is done in `Debian `_ and 10 | `Ubuntu `_ environments. 11 | 12 | It *should* be possible to run *staticDHCPd* on Windows, but it will require 13 | some significant rework of the sockets layer, particularly with raw L2 access, 14 | and nobody central to the project has the skills, time, or resources for it. If 15 | you make it work, please do not submit a patch unless you are also willing to 16 | maintain Windows support indefinitely. 17 | 18 | Can I do dynamic provisioning? 19 | ------------------------------ 20 | Support for limited, non-spec-compliant dynamic provisioning is provided through 21 | the :ref:`dynamism extension module `. It works pretty well for 22 | most cases. 23 | 24 | Can I connect to a non-standard database, like a webservice? 25 | ------------------------------------------------------------ 26 | Absolutely. 27 | 28 | You can define your own database engine, as long as it implements 29 | *staticDHCPd*'s database interface, then reference it in ``conf.py``, with no 30 | need to carry patches against the core codebase. 31 | 32 | An example is provided as the `httpdb` 33 | :doc:`extension module <../customisation/extensions>`. 34 | 35 | I want to change the order of elements in the dashboard 36 | ------------------------------------------------------- 37 | And you should want to do this. Customisation is good. 38 | 39 | Every dashboard element has an ordering-bias value; those with smaller values 40 | appear first. If you have logging at a debug level, you'll see this information 41 | printed when each one gets registered; every built-in element is 42 | :doc:`configurable <../customisation/configuration>` via ``conf.py``, and you 43 | can set your own bias values in any modules you write. 44 | 45 | I want to use my own CSS/JS/favicon for the web interface 46 | --------------------------------------------------------- 47 | Sure, that's not a problem at all. 48 | 49 | You can inject your own lines into ```` by using a tiny bit of code in 50 | :ref:`scripting-init`:: 51 | 52 | def init(): 53 | #... 54 | def myHeaders(path, queryargs, mimetype, data, headers): 55 | return "" 56 | 57 | callbacks.webAddHeader(myHeaders) 58 | #... 59 | 60 | You can use lambdas, too, if that's your thing. However you do it, this will add 61 | another block to the ```` section, so you can add a link to your own 62 | stylesheet or just embed code directly. As with the other 63 | :ref:`web-callbacks `, the standard set of parameters are 64 | passed to your function, so you can do different things depending on what was 65 | requested or what was received via `POST`; you just have to return a string or 66 | ``None``, which suppresses output. 67 | 68 | Anything you add, like a CSS class or JavaScript function, should be prefixed 69 | with a leading underscore, where possible, to avoid potential future conflicts 70 | with *staticDHCPd*'s core code. 71 | 72 | If you want to replace the CSS or favicon completely, you'll find their 73 | definitions in :mod:`web.resources` and handlers in :mod:`web.methods`. 74 | Just implement your own equivalent method, then, in :ref:`scripting-init`, do 75 | something similar to the following:: 76 | 77 | callbacks.webRemoveMethod('/css') #Get rid of the old one 78 | callbacks.webAddMethod('/css', _YOUR_METHOD_HERE_, cacheable=True) 79 | 80 | Replacing the favicon is pretty much identical. Replacing the JavaScript is 81 | discouraged, but also roughly the same; extend that, rather than replace it. 82 | 83 | Platform-specific questions 84 | --------------------------- 85 | On Ubuntu, I get these ``non-fatal select()`` error messages in my logs at 86 | startup. Why? 87 | 88 | Actually, we're not quite sure why, either. It seems as though Ubuntu's default 89 | configuration hits the process, when it starts, with a signal that generates an 90 | interrupt, which wakes the ``select()`` operations prematurely and causes them 91 | to throw an error because no handlers were invoked. No handlers were invoked 92 | because the nature of the interrupt is unknown, so to ensure normal operation, 93 | the error is semi-silently discarded and ``select()`` is invoked again, which is 94 | what would normally happen after each wakeup event. No requests can possibly be 95 | lost as a result of this error, so it's completely benign. 96 | 97 | That said, if you see this message appear after the initial startup, then you 98 | should start investigating the cause. 99 | 100 | Further information: 101 | 102 | This is actually more of a Python issue than an Ubuntu issue (it would have 103 | been fixed if it were reasonably easy): Python's ``select()`` receives 104 | ``SIGINT``, as it should, but there's no clear way to actually handle the 105 | signal gracefully -- although handling it properly would require knowledge 106 | of why it's actually being sent. 107 | 108 | Release errata 109 | -------------- 110 | :rfc:`4388`: "LEASEQUERY" 111 | +++++++++++++++++++++++++ 112 | The featureset described by this RFC is untested, yet was included in versions 113 | 1.4.0+, before removal in 1.6.3, because its implementation was wrong. It will 114 | return if there is demand, but better to leave out bad code than try to hack it 115 | into a semi-working state. 116 | 117 | Unsupported features 118 | -------------------- 119 | :rfc:`3004`: User class 120 | +++++++++++++++++++++++ 121 | *staticDHCPd* requires that each client be known ahead of time, precluding any 122 | need for the notion of dynamic assignment from pools based on clases. 123 | 124 | :rfc:`3011`: Subnet selection 125 | +++++++++++++++++++++++++++++ 126 | This feature is not required in a purely static environment. 127 | 128 | :rfc:`3118`: DHCP Authentication 129 | ++++++++++++++++++++++++++++++++ 130 | This feature is not supported because of the large number of clients that ignore 131 | the option. 132 | 133 | It is also unnecessary in any environment in which *staticDHCPd* should be used: 134 | if administrators do not have absolute control of their network, *staticDHCPd* 135 | is not the right choice. 136 | 137 | :rfc:`3203`: "FORCERENEW" 138 | +++++++++++++++++++++++++ 139 | This feature explicitly depends on :rfc:`3118`. 140 | 141 | It also poses problems related to authority and shouldn't be necessary in an 142 | all-static environment. It will be implemented if anyone makes a solid case for 143 | its inclusion, though. 144 | -------------------------------------------------------------------------------- /staticDHCPd/doc/commentary/index.rst: -------------------------------------------------------------------------------- 1 | Use-cases and hints 2 | =================== 3 | A system as flexible as *staticDHCPd* is also hard to cover in detail without 4 | getting all the way down to the read-the-code level. Questions from users and 5 | experience over time has led to lots of good ideas and interesting 6 | configurations, the more general of which are collected here. 7 | 8 | .. toctree:: 9 | :maxdepth: 2 10 | 11 | faq.rst 12 | database.rst 13 | setups.rst 14 | -------------------------------------------------------------------------------- /staticDHCPd/doc/commentary/setups.rst: -------------------------------------------------------------------------------- 1 | Specialised and non-static configurations 2 | ========================================= 3 | *staticDHCPd* is meant to help administrators easily configure static 4 | environments, with easy-to-integrate provisioning facilities. However, special 5 | cases arise and that's what makes the software truly powerful. Some of the more 6 | interesting setups in the wild will be documented here. 7 | 8 | .. _setups-dynamic: 9 | 10 | Dynamic hybrids 11 | --------------- 12 | The motivating case for adding support for dynamic provisioning to *staticDHCPd* 13 | was a LAN party context in which guests need to register their systems before 14 | they can be given a static mapping by site administration. Using the `dynamism` 15 | extension, unknown clients can be given configuration that puts them into an 16 | isolated subnet on a short lease so they can access a registration system, and 17 | the DHCP server itself can send notification of the new arrival to a webservice 18 | to streamline the operators' work. 19 | 20 | The rest of this article outlines how to use the sample extensions provided 21 | with *staticDHCPd*. Any site seeking to use dynamic services will almost 22 | certainly need to do some customisation, though, so consider at least basic 23 | Python knowledge to be a pre-requisite. 24 | 25 | Be aware also that, unlike dynamic-provisioning-focused servers, like the ISC's, 26 | not all provisioning semantics are respected and that, unlike *staticDHCPd*'s 27 | static behaviour, this facet of the system is not RFC-compliant. It probably 28 | won't do anything environment-breaking, but be prepared for some weird things; 29 | feedback, if you encounter anything curious, is very welcome. 30 | 31 | Setup 32 | +++++ 33 | For the common case, it is enough to 34 | :doc:`install <../customisation/extensions>` ``dynamism.py`` normally. 35 | 36 | If you want to do anything cooler, like send a JSON message to a webservice when 37 | an unknown MAC appears or block clients after they renew five times, subclass 38 | ``DynamicPool`` or just hack it in-place. It's simple code and it's your 39 | environment, so just apply what you find in tutorials on the Internet and have 40 | fun! 41 | 42 | How stable are dynamic leases? 43 | ++++++++++++++++++++++++++++++ 44 | They should be pretty consistent: when IPs are added to the pool, if 45 | `scapy `_ is available, and if the 46 | scan option is enabled, the server will ARP for each address (in parallel, so 47 | it's not slow), setting up leases as hits are found, making your network a 48 | living database. Additionally, if a client requests a specific IP after the 49 | server is already online (clients often do this when rebooting), that address 50 | will be plucked if available. 51 | 52 | If scapy is unavailable, you'll probably get a lot of DECLINEs, but your network 53 | will stabilise before too long. 54 | 55 | Is DST a factor with leases? 56 | ++++++++++++++++++++++++++++ 57 | No, DST shouldn't be relevant. Internally, leases are managed as offsets against 58 | UTC, so timezones are only applied when formatting the timestamps for 59 | presentation to operators. 60 | 61 | .. _setups-pxe: 62 | 63 | PXE support 64 | ----------- 65 | In general, it should be sufficient to test for option 60 66 | (`vendor_class_identifier`) in :ref:`scripting-loadDHCPPacket` to see if it 67 | matches the device-type you want to net-boot and set options 60, 66 68 | (`tftp_server_name`), and 67 (`bootfile_name`) accordingly, as demonstrated in 69 | the following example:: 70 | 71 | vendor_class_identifier = source_packet.getOption('vendor_class_identifier', convert=True) 72 | if vendor_class_identifier and vendor_class_identifier.startswith('PXEClient'): 73 | #The device may look for a specific value; check your manual 74 | packet.setOption('vendor_class_identifier', 'PXEServer:staticDHCPd') 75 | #Tell it where to get its bootfile; IPs are valid, too 76 | packet.setOption('tftp_server_name', 'bootserver.example.org') 77 | #Have the device ask for its own MAC, stripped of colons and uppercased 78 | packet.setOption('bootfile_name', str(mac).replace(':', '').upper() + '.cfg') 79 | 80 | Those working with systems derived from BOOTP, rather than DHCP, like embedded 81 | BIOS-level stacks, will probably want to do something more like this:: 82 | 83 | vendor_class_identifier = source_packet.getOption('vendor_class_identifier', convert=True) 84 | if vendor_class_identifier and vendor_class_identifier.startswith('PXEClient'): 85 | #Tell it where to get its bootfile; your device probably isn't 86 | #DNS-aware if it's using BOOTP, but the field is free-form text 87 | packet.setOption('siaddr', DHCP_SERVER_IP) #The same address defined earlier in conf.py 88 | #Tell it which file to look for; pxelinux.0 is pretty common 89 | packet.setOption('file', 'pxelinux.0') 90 | 91 | The two approaches are not mutually exclusive and well-behaved clients should 92 | only look at the fields they understand. But it's probably safest to use ``if`` 93 | clauses to be sure that you're not at risk of confusing a partial 94 | implementation. 95 | 96 | Of course, you can use other criteria to evaluate whether an option should be 97 | set and what its value should be. 98 | 99 | In the event that the client tries to hit a ProxyDHCP port (4011, by 100 | convention), you'll need to edit ``conf.py`` and assign the port number to 101 | **PROXY_PORT**. This will cause *staticDHCPd* to bind another port on the same 102 | interface(s) as the main DHCP port; full DHCP service will be provided on that 103 | port, too, including IP assignment. 104 | 105 | The ``port`` parameter in :ref:`scripting-loadDHCPPacket` and other functions 106 | will allow site-specific code to respond differently depending on how the packet 107 | was received; you can use simple tests like this to apply appropriate logic:: 108 | 109 | if port == PROXY_PORT: #The address defined in conf.py 110 | #set special fields 111 | 112 | Chances are, in most cases, the client will have been assigned an IP over the 113 | standard DHCP port already, testable with ``packet.getOption('ciaddr')``, and 114 | though it's highly unlikely, the device may complain if the response contains an 115 | IP offer; ``packet.deleteOption('yiaddr')`` takes care of this. 116 | -------------------------------------------------------------------------------- /staticDHCPd/doc/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import sys, os, re 3 | 4 | sys.path.append(os.path.abspath('../staticdhcpdlib')) 5 | sys.path.append(os.path.abspath('..')) 6 | import staticdhcpdlib as module 7 | 8 | extensions = [ 9 | 'sphinx.ext.autodoc', 10 | 'sphinx.ext.todo', 11 | 'sphinx.ext.coverage', 12 | 'sphinx.ext.viewcode', 13 | ] 14 | templates_path = ['_templates'] 15 | source_suffix = '.rst' 16 | master_doc = 'index' 17 | 18 | project = 'staticDHCPd' 19 | copyright = module.COPYRIGHT 20 | version = re.match('^(\d+\.\d+)', module.VERSION).group(1) 21 | release = module.VERSION 22 | 23 | exclude_trees = ['_build'] 24 | 25 | pygments_style = 'sphinx' 26 | 27 | autodoc_member_order = 'bysource' 28 | autoclass_content = 'init' 29 | 30 | html_theme = 'default' 31 | html_static_path = ['_static'] 32 | html_show_sourcelink = False 33 | 34 | htmlhelp_basename = 'staticDHCPddoc' 35 | 36 | latex_documents = [( 37 | 'index', 38 | 'staticDHCPd.tex', 39 | 'staticDHCPd documentation', 40 | re.search(', (.*?) <', module.COPYRIGHT).group(1), 41 | 'manual', 42 | )] 43 | -------------------------------------------------------------------------------- /staticDHCPd/doc/customisation/extensions.rst: -------------------------------------------------------------------------------- 1 | Working with extension modules 2 | ============================== 3 | All included extension modules provide specific usage instructions in their 4 | header sections. 5 | 6 | However, because including common information in each one would be redundant, 7 | the general flow of installing one is the following: 8 | 9 | #. Ensure that there exists an ``staticDHCPd_extensions/`` subdirectory in the same 10 | directory as ``conf.py`` 11 | #. Copy or link the extension-file into ``staticDHCPd_extensions/`` 12 | #. Follow its instructions to install hooks in ``conf.py`` 13 | #. (re)Start *staticDHCPd* 14 | 15 | Configuring modules 16 | ------------------- 17 | Since your module is independent code that you explicitly hook into 18 | *staticDHCPd*, and can therefore run independently, you are free to configure 19 | it any way you would like; if you are its sole consumer, constants defined 20 | in-file are likely the simplest method and more than good enough. 21 | 22 | Supplying configuration through `conf.py` 23 | +++++++++++++++++++++++++++++++++++++++++ 24 | While it is entirely possible to contain all extension configuration within the 25 | module itself, if you plan to share, it is convenient for users to define 26 | values in ``conf.py``. 27 | 28 | To make use of this facility, all you need to do is instruct your users to add 29 | lines like the following in :ref:`scripting-init`:: 30 | 31 | staticDHCPd_extensions.your_module.REFRESH_INTERVAL = 5 32 | staticDHCPd_extensions.your_module.SOME_DICT = { 33 | 1: 2, 34 | 'a': 'c', 35 | } 36 | 37 | Or like this, so that it's clear, at a glance, where your module's parameters 38 | are set, by encouraging uniform indentation:: 39 | 40 | with staticDHCPd_extensions.your_module as x: 41 | x.TIMEOUT = 0.25 42 | 43 | If, however, you are working with a module for which loading in 44 | :ref:`scripting-init` is too late, the convention to avoid conflicting with 45 | future *staticDHCPd* built-in variables is to use ``X_YOURMODULE_VARIABLE``:: 46 | 47 | X_HTTPDB_URI = 'http://example.org/dhcp' 48 | 49 | Accessing configuration data 50 | ++++++++++++++++++++++++++++ 51 | Within your module, you then have a few ways of accessing this data. They'll 52 | basically all start with importing the ``staticDHCPd_extensions`` namespace:: 53 | 54 | import staticdhcpdlib.config 55 | _config = staticdhcpdlib.config.conf.staticDHCPd_extensions.your_module 56 | 57 | You can then extract data from ``_config`` as needed; you'll probably want to 58 | use one of the parsing methods it exposes to create a dictionary to avoid 59 | testing to see if every value is set or not, but how to use it is entirely up 60 | to you. 61 | 62 | .. automethod:: config._Namespace.extension_config_merge 63 | 64 | .. automethod:: config._Namespace.extension_config_dict 65 | 66 | .. automethod:: config._Namespace.extension_config_iter 67 | 68 | For performance reasons, it may be a good idea to assign the namespace's 69 | data during module setup, then discard it and any intermediate structures, 70 | like dictionaries compiled using these methods:: 71 | 72 | del _config #Removes the reference and lets staticDHCPd reclaim resources 73 | del YOUR_CONFIG_DICTIONARY #Allows for normal Python garbage-collection 74 | 75 | Of course, if keeping a dictionary or the namespace around is how you want to 76 | access information, that's perfectly valid and the structures are pretty 77 | efficient by themselves. 78 | 79 | In the early-bind case, the following will work, and you may streamline the code 80 | as you see fit:: 81 | 82 | import staticdhcpdlib.config 83 | URI = getattr(staticdhcpdlib.config, 'X_HTTPDB_URI', 'http://default/value') 84 | 85 | Developing your own module 86 | -------------------------- 87 | The best way to start is by studying the provided modules. They range from 88 | simple to fairly complex, but all of them are practical. 89 | 90 | Simple -> Complex: 91 | 92 | * `recent_activity` 93 | 94 | * Simple, self-installing web-interface dashboard element that shows 95 | the last several DHCP events. 96 | 97 | * `httpdb` 98 | 99 | * Basic REST/JSON-based database interface. 100 | 101 | * `feedservice` 102 | 103 | * Self-installing ATOM-feed interface to the logging system, using 104 | web-methods to extend the webserver. 105 | 106 | * `statistics` 107 | 108 | * Self-installing web-interface dashboard elements that display DHCP activity 109 | and, if necessary packages are available, an activity graph. 110 | 111 | * `dynamism` 112 | 113 | * Robust dynamic DHCP facilities that can enhance or completely supplant 114 | static behaviour. 115 | 116 | No matter what you want to build, though, understanding how it will interact 117 | with *staticDHCPd* is crucial. You will almost certainly be making use of 118 | :ref:`callbacks `, and some combination of the 119 | :ref:`scripting-init`, :ref:`scripting-filterPacket`, 120 | :ref:`scripting-handleUnknownMAC`, and :ref:`scripting-loadDHCPPacket` 121 | functions. 122 | -------------------------------------------------------------------------------- /staticDHCPd/doc/customisation/index.rst: -------------------------------------------------------------------------------- 1 | Customisation guidance 2 | ====================== 3 | While quite usable out-of-the-box, especially for its intended purpose, which 4 | is serving static DHCP "leases", different sites have different needs, and some 5 | sites want as few frills as possible. 6 | 7 | This section exists to cover the various bells and whistles available. 8 | 9 | .. toctree:: 10 | :maxdepth: 2 11 | 12 | configuration.rst 13 | scripting.rst 14 | extensions.rst 15 | 16 | For the sysadmins out there working in acutely memory-constrained environments 17 | (that still have enough space to support a Python interpreter), as a general 18 | design guideline, *staticDHCPd* avoids loading anything it doesn't absolutely 19 | need: if you choose not to enable the `web` subsystem, for example, it won't 20 | ever be read from disk. 21 | 22 | Additionally, staticDHCPd is pretty open to tuning, so if you know a lot about 23 | the sort of load your environment will handle, you can change properties like 24 | ``checkinterval`` to adjust threading and resource priotisation and 25 | responsiveness. 26 | -------------------------------------------------------------------------------- /staticDHCPd/doc/examples/index.rst: -------------------------------------------------------------------------------- 1 | Practical recipies that everyone can use! 2 | ========================================= 3 | While there's a lot of stuff that you can do with the 4 | :doc:`scripting <../customisation/scripting>` toolset, figuring out how to get started, 5 | especially if you're not already familiar with Python, can be a bit 6 | overwhelming. That's what this document is for: loaded with examples, it 7 | serves as a crash-course for tweaking your environment to do special things 8 | that will really help to make your life easier. 9 | 10 | Pre-requisites 11 | -------------- 12 | There are a few things that you will need to understand before diving into 13 | these examples. Nothing difficult or long, but things that are essential 14 | nonetheless. 15 | 16 | *libpydhcpserver* 17 | +++++++++++++++++ 18 | *staticDHCPd* unapologetically uses resources from *libpydhcpserver*. Reading 19 | the examples section of its documentation, which is always distributed alongside 20 | this one, should be considered necessary. 21 | 22 | Python 23 | ++++++ 24 | staticDHCPd's configuration, ``conf.py``, is a living, breathing chunk of 25 | `Python `_ source code. As such, when working with it, 26 | Python coding conventions must be followed. 27 | 28 | If anything mentioned here doesn't make sense, search the Internet for a 29 | "hello, world!" Python script and do a bit of exploratory hacking. 30 | 31 | Whitespace 32 | |||||||||| 33 | Python is whitespace-sensitive. All this means, really, is that putting spaces 34 | before every line you write is important and that the number of spaces must be 35 | consistent. (And it's something you should do anyway, since indented code is 36 | much easier to read) 37 | 38 | When adding code to a scripting method, the standard convention is to indent it 39 | with four spaces, like this:: 40 | 41 | def loadDHCPPacket(...): 42 | packet.setOption('renewal_time_value', 60) 43 | if packet.isOption('router'): 44 | packet.setOption('domain_name', 'uguu.ca') 45 | logger.info("domain name set to 'uguu.ca'") 46 | 47 | #blank lines, like the one above, are optional; your code should be readable 48 | logger.info("processing done") 49 | return True 50 | 51 | Strings 52 | ||||||| 53 | A string is a sequence of bytes, usually text, like ``'hello'``. It may be 54 | single- or double-quoted, and, if you need to put the same type of quotation 55 | you used to start the string somewhere in the middle, it can be "escaped" by 56 | prefixing it with a backslash: 57 | ``"Static assignments aren't really \"leases\"."``. 58 | 59 | Numbers 60 | ||||||| 61 | Integers, referred to as "ints", should be familiar, and "floating-point" 62 | values, also known as "floats", are just numbers with a decimal component, like 63 | ``64.53``. 64 | 65 | Conditionals 66 | |||||||||||| 67 | You probably won't want logic to execute in all cases and that's what the ``if`` 68 | statement is for. Rather than trying to learn from an explanation, just read the 69 | examples below and its use will become apparent quickly. 70 | 71 | Comparators like ``>``, ``<=``, and ``!=`` should be pretty obvious, but you 72 | will need to use ``==`` to test equality, since a single ``=`` is used for 73 | assignment. 74 | 75 | Comments 76 | |||||||| 77 | Anything prefixed with a hash-mark (``#``) is a comment and will not be 78 | processed. 79 | 80 | Sequences 81 | ||||||||| 82 | Lists, tuples, arrays, strings... Whatever they are, they are indexible, meaning 83 | that you can access any individual element if you know its position. The only 84 | real catch here is that everything starts at ``0``, not ``1``:: 85 | 86 | x = [1, 2, 8, 'hello'] 87 | x[0] #This is the value 1 88 | x[2] #This is the value 8 89 | 90 | Evaluation 91 | |||||||||| 92 | In Python, it is common to see things like the following:: 93 | 94 | clients = some_function_that_returns_a_number_or_a_sequence() 95 | if not clients: 96 | #do something 97 | 98 | When ``x`` is evaluated, it is asked if it holds a meaningful value and this 99 | is used to determine whether it is equivalent to ``True`` or ``False`` for the 100 | comparison. Numbers are ``False`` if equal to ``0``, sequences are ``False`` 101 | when empty, and ``None`` is always ``False``. The ``not`` keyword is a more 102 | readable variant of ``!``, meaning that ``True``/``False`` should be flipped. 103 | 104 | Returns 105 | ||||||| 106 | A ``return`` statement may be placed anywhere inside of a function. Its purpose 107 | is to end execution and report a result. 108 | 109 | The convention within *staticDHCPd* is to have ``return True`` indicate that 110 | everything is good and processing should continue, while ``return False`` means 111 | that the packet should be rejected. For your own sanity, when rejecting a 112 | packet, you should :ref:`log the reason why `. 113 | 114 | Examples 115 | -------- 116 | This section will grow as new examples are created; if you let us know how to do 117 | something cool or you ask a question and the result of the exchange boils down 118 | to a handy snippet, it will probably show up here. 119 | 120 | Gateway configuration 121 | +++++++++++++++++++++ 122 | Tell all clients with an IP address ending in a multiple of 3 to use 123 | 192.168.1.254 as their default gateway:: 124 | 125 | def loadDHCPPacket(...): 126 | #... 127 | if definition.ip[3] % 3 == 0: 128 | logger.info("I'm a log message. Please use me!") 129 | packet.setOption('router', '192.168.1.254') 130 | #... 131 | return True 132 | 133 | Here, the modulus-by-3 of the last octet (zero-based array) of the IP address to 134 | associate with the client is checked to see if it is zero. If so, the "router" 135 | option (DHCP option 3) is set to 192.168.1.254 136 | 137 | Prevent clients in all ``"192.168.0.0/24"`` subnets from having a default 138 | gateway:: 139 | 140 | def loadDHCPPacket(...): 141 | #... 142 | if definition.subnet == '192.168.0.0/24': 143 | packet.deleteOption('router') 144 | #... 145 | return True 146 | 147 | "subnet", which is the database's "subnet" field, not that of the client's 148 | IP/netmask, is checked to see if it matches. If so, then the "router" option is 149 | discarded. 150 | 151 | Override renewal times 152 | ++++++++++++++++++++++ 153 | Set T1 to 60 seconds:: 154 | 155 | def loadDHCPPacket(...): 156 | #... 157 | packet.setOption('renewal_time_value', 60) 158 | #... 159 | return True 160 | 161 | Adjust domain names 162 | +++++++++++++++++++ 163 | Set the client's domain name to "example.com" if the request was relayed, but 164 | refuse to respond if it was relayed from 10.0.0.1:: 165 | 166 | def loadDHCPPacket(...): 167 | #... 168 | if relay_ip: #The request was relayed 169 | if relay_ip == "10.0.0.1": 170 | return False #Abort processing 171 | packet.setOption('domain_name', 'example.com') 172 | #... 173 | return True 174 | 175 | Here, ``relay_ip`` (DHCP field "giaddr"), is checked to see if it was set, 176 | indicating that this request was relayed. The IP of the relay server is then 177 | compared and, if it matches, "domain_name" is set to "example.com". 178 | 179 | Working with option 82 180 | ++++++++++++++++++++++ 181 | Refuse relays without "relay_agent" (DHCP option 82)'s agent-ID set to 182 | [1, 2, 3]:: 183 | 184 | def loadDHCPPacket(...): 185 | #... 186 | if relay_ip: #The request was relayed 187 | relay_agent = packet.getOption('relay_agent') 188 | if relay_agent and not rfc3046_decode(relay_agent)[1] == [1, 2, 3]: 189 | logger.warn("Rejecting relayed request from [1, 2, 3]") 190 | return False 191 | #... 192 | return True 193 | 194 | This allows any non-relayed requests to pass through. Any relayed requests 195 | missing option 82 will be allowed (more on this below); any instances of option 196 | 82 with an invalid agent-ID (sub-option 1) will be ignored. Any instances of 197 | option 82 missing sub-option 1 will generate an error (described in the next 198 | example). 199 | 200 | Even relay agents configured to set option 82 will omit it if the resulting DHCP 201 | packet would be too large. For this reason, it's important to limit the relay 202 | IPs allowed in the config settings. 203 | 204 | Managing errors 205 | +++++++++++++++ 206 | Do something to generate an error for testing purposes:: 207 | 208 | def loadDHCPPacket(...): 209 | #... 210 | if not packet.setOption('router', [192])): 211 | raise Exception("192 is not a valid IP") 212 | #... 213 | return True 214 | 215 | The reason why this fails should be obvious, though it is worth noting that 216 | ``setOption()`` returns ``False`` on failure, rather than raising an exception 217 | of its own. This was done because it seemed easier for scripting novices to 218 | work with while *staticDHCPd* was still in its infancy. 219 | 220 | What's important here is that raising any sort of exception in 221 | ``loadDHCPPacket()`` prevents the DHCP response from being sent, but it will 222 | help to debug problems by printing or e-mailing a thorough description of the 223 | exception that occurred. 224 | -------------------------------------------------------------------------------- /staticDHCPd/doc/index.rst: -------------------------------------------------------------------------------- 1 | staticDHCPd user documentation 2 | ============================== 3 | 4 | staticDHCPd is an all-Python, :rfc:`2131`-compliant DHCP server, with support 5 | for most common DHCP extensions and extensive site-specific customisation. 6 | 7 | .. toctree:: 8 | :maxdepth: 2 9 | 10 | customisation/index.rst 11 | examples/index.rst 12 | commentary/index.rst 13 | api/index.rst 14 | -------------------------------------------------------------------------------- /staticDHCPd/extensions/README: -------------------------------------------------------------------------------- 1 | staticDHCPd ships with a number of extension-modules in its standard 2 | distribution; these are qualified as either 'contrib' or 'official', depending 3 | on how well-supported they are. 4 | 5 | official: 6 | - guaranteed to work with the release with which they ship 7 | - receive rapid response to bugfixes and patches 8 | - will be dropped as gracefully as possible, if deprecated for any reason 9 | - distributed under the same license as staticDHCPd 10 | - documented as reference examples 11 | 12 | contrib: 13 | - may not be thoroughly tested for new releases of staticDHCPd 14 | - updated at the maintainer's discretion 15 | - may differ from the maintainer's version 16 | - may disappear from future releases without notice 17 | - may be distributed under any Debian-compliant open license 18 | - may be entirely free of non-essential documentation 19 | 20 | common to both: 21 | - screened for security, to prevent malicious code from being shared 22 | - well-documented usage information 23 | - contain contact information for the author or primary maintainer 24 | -------------------------------------------------------------------------------- /staticDHCPd/extensions/official/feedservice.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | """ 3 | Integrates an Atom feed into the webservice module, allowing you to subscribe to 4 | events of importance, without all the noise that often accompanies e-mail 5 | updates, especially when something fails on every single request, like someone 6 | pulling a cable they shouldn't have touched. 7 | 8 | To use this module, configure whatever is required in conf.py, inside of init(), 9 | like this: 10 | with extensions.feedservice as x: 11 | #FEED_ID is something you should set uniquely on each server you run 12 | x.FEED_ID = 'bcd2dbbc-105b-4533-bb6d-01c2333cc55e' 13 | #This is usually intelligently inferred, but you might need to set it 14 | #Trailing slash is optional 15 | x.SERVER_BASE = 'http://someserver:someport' 16 | 17 | For a list of all parameters you may define, see below. 18 | 19 | Then add the following to conf.py's init() function: 20 | import staticDHCPd_extensions.feedservice 21 | 22 | Like staticDHCPd, this module under the GNU General Public License v3 23 | (C) Neil Tallim, 2021 24 | """ 25 | from staticdhcpdlib import config 26 | _config = config.conf.extensions.feedservice 27 | _CONFIG = _config.extension_config_merge(defaults={ 28 | #The address at which your server can be reached 29 | #Defaults to the given web-address, if not '0.0.0.0'; 'localhost' otherwise 30 | 'SERVER_BASE': 'http://{}:{}'.format(config.WEB_IP != '0.0.0.0' and config.WEB_IP or 'localhost', config.WEB_PORT), 31 | 32 | #The minimum severity of events to include 33 | #Choices: 'DEBUG', 'INFO', 'WARN', 'ERROR', 'CRITICAL' 34 | 'LOGGING_LEVEL': 'ERROR', 35 | 36 | #The number of events to serve in a feed 37 | 'MAX_EVENTS_FEED': 10, 38 | #The number of events to retain for linking purposes 39 | 'MAX_EVENTS_TOTAL': 100, 40 | #The maximum age of an event that can be included in a feed 41 | 'MAX_AGE': 60 * 60 * 24, 42 | 43 | #Whether feed items should be removed when the system is reinitialised 44 | 'CLEAR_ON_REINIT': True, 45 | 46 | #The name to give the feed 47 | 'FEED_TITLE': config.SYSTEM_NAME, 48 | 49 | #The path at which individual records will be served 50 | 'PATH_RECORD': '/ca/uguu/puukusoft/staticDHCPd/extension/feedservice/record', 51 | 52 | #The path at which to serve Atom (None to disable) 53 | 'PATH_ATOM': '/ca/uguu/puukusoft/staticDHCPd/extension/feedservice/atom', 54 | 55 | #Whether feeds should be advertised in the web interface's headers 56 | 'ADVERTISE': True, 57 | 58 | #The ID that uniquely identifies this feed 59 | #If you are running multiple instances of staticDHCPd in your environment, 60 | #you MUST generate a new feed-ID for EACH server. To do this, execute the 61 | #following code in Python: import uuid; str(uuid.uuid4()) 62 | 'FEED_ID': 'bcd2dbbc-105b-4533-bb6d-01c2333cc55e', 63 | }, required=[ 64 | ]) 65 | del _config 66 | 67 | #Do not touch anything below this line 68 | ################################################################################ 69 | import collections 70 | import datetime 71 | import logging 72 | import time 73 | import traceback 74 | import uuid 75 | import xml.etree.ElementTree as ET 76 | 77 | _logger = logging.getLogger('extension.feedservice') 78 | 79 | if _CONFIG['SERVER_BASE'].endswith('/'): 80 | _CONFIG['SERVER_BASE'] = _CONFIG['SERVER_BASE'][:-1] 81 | _SERVER_ROOT = '{}/'.format(_CONFIG['SERVER_BASE']) 82 | _RECORD_URI = _CONFIG['SERVER_BASE'] + _CONFIG['PATH_RECORD'] + "?uid={uid}" 83 | 84 | _last_update = int(time.time()) 85 | 86 | _Event = collections.namedtuple('Event', ('summary', 'severity', 'timestamp', 'module', 'line', 'uid')) 87 | 88 | class _FeedHandler(logging.Handler): 89 | """ 90 | A handler that holds a fixed number of records, allowing index-based 91 | retrieval. 92 | """ 93 | def __init__(self, capacity): 94 | logging.Handler.__init__(self) 95 | self._records = collections.deque(maxlen=capacity) 96 | 97 | def emit(self, record): 98 | global _last_update 99 | _last_update = int(record.created) 100 | self.acquire() 101 | try: 102 | self._records.appendleft((record, str(uuid.uuid4()))) 103 | finally: 104 | self.release() 105 | 106 | def flush(self): 107 | self.acquire() 108 | try: 109 | self._records.clear() 110 | finally: 111 | self.release() 112 | 113 | def close(self): 114 | self.flush() 115 | logging.Handler.close(self) 116 | 117 | def presentRecord(self, path, queryargs, mimetype, data, headers): 118 | """ 119 | Non-Handler method: renders the requested record for the web interface. 120 | """ 121 | uids = queryargs.get('uid') 122 | if uids: 123 | uid = uids[0] 124 | else: 125 | return 'No UID specified' 126 | 127 | record = None 128 | self.acquire() 129 | try: 130 | #This should, in general, be efficient enough 131 | #The overhead of a dictionary seems unnecessary for all practical 132 | #use-cases 133 | for (record, uuid) in self._records: 134 | if uuid == uid: 135 | break 136 | else: 137 | return 'Specified UID was not found; record may have expired' 138 | finally: 139 | self.release() 140 | 141 | return self.format(record) 142 | 143 | def enumerateRecords(self, limit): 144 | """ 145 | Non-Handler method: Enumerates every tracked record, up to `limit`. 146 | """ 147 | events = [] 148 | self.acquire() 149 | try: 150 | for i in range(min(limit, len(self._records))): 151 | (record, uid) = self._records[i] 152 | events.append(_Event(record.msg, record.levelname, record.created, record.name, record.lineno, uid)) 153 | finally: 154 | self.release() 155 | return events 156 | 157 | def _format_title(element): 158 | """ 159 | Formats the given `element`, returning a string suitable for display in a 160 | feed's header. 161 | """ 162 | return "{severity} at {time} in {module}:{line}".format( 163 | severity=element.severity, 164 | time=time.ctime(element.timestamp), 165 | module=element.module, 166 | line=element.line, 167 | ) 168 | 169 | def _feed_presenter(feed_type): 170 | """ 171 | A decorator that handles exceptions. 172 | """ 173 | def decorator(f): 174 | def function(*args, **kwargs): 175 | try: 176 | return f(*args, **kwargs) 177 | except Exception: 178 | _logger.error("Unable to render {} feed:\n{}".format(feed_type, traceback.format_exc())) 179 | raise 180 | return function 181 | return decorator 182 | 183 | _ATOM_ID_FORMAT = 'urn:uuid:{id}' 184 | _FEED_ID = _ATOM_ID_FORMAT.format(id=_CONFIG['FEED_ID']) 185 | @_feed_presenter('Atom') 186 | def _present_atom(logger): 187 | """ 188 | Assembles an Atom-compliant feed, drawing elements from `logger`. 189 | """ 190 | feed = ET.Element('feed') 191 | feed.attrib['xmlns'] = 'http://www.w3.org/2005/Atom' 192 | 193 | title = ET.SubElement(feed, 'title') 194 | title.text = _CONFIG['FEED_TITLE'] 195 | link = ET.SubElement(feed, 'link') 196 | link.attrib['href'] = _SERVER_ROOT 197 | updated = ET.SubElement(feed, 'updated') 198 | updated.text = datetime.datetime.fromtimestamp(_last_update).isoformat() 199 | id = ET.SubElement(feed, 'id') 200 | id.text = _FEED_ID 201 | 202 | global _ATOM_ID_FORMAT 203 | max_age = time.time() - _CONFIG['MAX_AGE'] 204 | for element in logger.enumerateRecords(_CONFIG['MAX_EVENTS_FEED']): 205 | if element.timestamp < max_age: #Anything after this is also too old 206 | break 207 | 208 | entry = ET.SubElement(feed, 'entry') 209 | 210 | title = ET.SubElement(entry, 'title') 211 | title.text = _format_title(element) 212 | link = ET.SubElement(entry, 'link') 213 | link.attrib['href'] = _RECORD_URI.format(uid=element.uid) 214 | id = ET.SubElement(entry, 'id') 215 | id.text = _ATOM_ID_FORMAT.format(id=element.uid) 216 | updated = ET.SubElement(entry, 'updated') 217 | updated.text = datetime.datetime.fromtimestamp(element.timestamp).isoformat() 218 | summary = ET.SubElement(entry, 'summary') 219 | summary.text = element.summary 220 | return ('application/atom+xml', '' + ET.tostring(feed)) 221 | 222 | #Setup happens here 223 | ################################################################################ 224 | _LOGGER = _FeedHandler(_CONFIG['MAX_EVENTS_TOTAL']) 225 | _logger.info("Prepared feed-handler") 226 | _LOGGER.setFormatter(logging.Formatter("""%(asctime)s : %(levelname)s : %(name)s:%(lineno)d[%(threadName)s] 227 |

228 | %(message)s""")) 229 | _LOGGER.setLevel(getattr(logging, _CONFIG['LOGGING_LEVEL'])) 230 | _logger.info("Feed-handler logging-level set to {}".format(_CONFIG['LOGGING_LEVEL'])) 231 | _logger.root.addHandler(_LOGGER) 232 | 233 | if _CONFIG['CLEAR_ON_REINIT']: 234 | _logger.info("Registering callback handler to clear feeds on reinitialisation...") 235 | config.callbacks.systemAddReinitHandler(_LOGGER.flush) 236 | 237 | _logger.info("Registering record-access-point at '{}'...".format(_CONFIG['PATH_RECORD'])) 238 | config.callbacks.webAddMethod( 239 | _CONFIG['PATH_RECORD'], _LOGGER.presentRecord, 240 | display_mode=config.callbacks.WEB_METHOD_TEMPLATE, 241 | ) 242 | 243 | if _CONFIG['PATH_ATOM']: 244 | _logger.info("Registering Atom feed at '{}'...".format(_CONFIG['PATH_ATOM'])) 245 | config.callbacks.webAddMethod( 246 | _CONFIG['PATH_ATOM'], lambda *args, **kwargs: _present_atom(_LOGGER), 247 | display_mode=config.callbacks.WEB_METHOD_RAW, 248 | ) 249 | if _CONFIG['ADVERTISE']: 250 | _logger.info("Adding reference to Atom feed to headers...") 251 | atom_header = ''.format( 252 | _CONFIG['PATH_ATOM'], 253 | config.SYSTEM_NAME, 254 | ) 255 | config.callbacks.webAddHeader(lambda *args, **kwargs: atom_header) 256 | -------------------------------------------------------------------------------- /staticDHCPd/extensions/official/httpdb.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | """ 3 | Provides a basic HTTP(S)-based database for sites that work using RESTful models. 4 | 5 | Specifically, this module implements a very generic REST-JSON system, with 6 | optional support for caching. To implement another protocol, only one method 7 | needs to be rewritten, so just look for the comments. 8 | 9 | To use this module without making any code changes, make the following changes 10 | to conf.py; if anything more sophisticated is required, fork it and hack away: 11 | Locate the DATABASE_ENGINE line and replace it with the following two lines: 12 | import staticDHCPd_extensions.httpdb as httpdb 13 | DATABASE_ENGINE = httpdb.HTTPDatabase #or httpdb.HTTPCachingDatabase 14 | 15 | Anywhere above the 'import httpdb' line, define any of the following 16 | parameters that you need to override: 17 | #The address of your webservice; MUST BE SET 18 | X_HTTPDB_URI = 'http://example.org/lookup' 19 | #Additional parameters to be passed with the request, DEFAULTS TO {} 20 | X_HTTPDB_PARAMETERS = { 21 | 'some_request_thing': 7002, 22 | } 23 | #The parameter-key for the MAC; defaults to 'mac' 24 | X_HTTPDB_PARAMETER_KEY_MAC = 'hwaddr' 25 | #Whether the parameters should be serialised to JSON and POSTed, like 26 | #{"mac": "aa:bb:cc:dd:ee:ff"}, or encoded in the query-string, like 27 | #"mac=aa%3Abb%3Acc%3Add%3Aee%3Aff"; DEFAULTS TO True 28 | X_HTTPDB_POST = True 29 | #Any custom HTTP headers your service requires; DEFAULTS TO {} 30 | X_HTTPDB_HEADERS = { 31 | 'Your-Site-Token': "hello", 32 | } 33 | #If using HTTPCachingDatabase, the maximum number of requests to run 34 | #at a time; successive requests will block; DEFAULTS TO (effectively) 35 | #INFINITE 36 | X_HTTPDB_CONCURRENCY_LIMIT = 10 37 | 38 | For a list of all parameters you may define, see below. 39 | 40 | If concurrent connections to your HTTP server should be limited, use 41 | HTTPCachingDatabase instead of HTTPDatabase. 42 | 43 | Like staticDHCPd, this module under the GNU General Public License v3 44 | (C) Neil Tallim, 2021 45 | 46 | Created in response to a request from Aleksandr Chusov. 47 | Enhanced with feedback from Helios de Creisquer. 48 | """ 49 | ################################################################################ 50 | #Rewrite _parse_server_response() as needed to work with your service. 51 | ################################################################################ 52 | def _parse_server_response(json_data): 53 | """ 54 | Transforms a server-response that looks like this... 55 | { 56 | "ip": "192.168.0.1", 57 | "hostname": "any-valid-hostname", //may be omitted or null 58 | "gateway": "192.168.0.1", //may be omitted or null 59 | "subnet_mask": "255.255.255.0", //may be omitted or null 60 | "broadcast_address": "192.168.0.255", //may be omitted or null 61 | "domain_name": "example.org", //may be omitted or null 62 | "domain_name_servers": ["192.168.0.1", "192.168.0.2", "192.168.0.3"], //may be omitted or null 63 | "ntp_servers": ["192.168.0.1", "192.168.0.2", "192.168.0.3"], //may be omitted or null 64 | "lease_time": 3600, 65 | "subnet": "subnet-id", 66 | "serial": 0, 67 | "extra": {...}, //any extra attributes you would like in the lease-definition; may be omitted or null 68 | } 69 | ...into a Definition-object. 70 | """ 71 | return Definition( 72 | ip=json_data['ip'], lease_time=json_data['lease_time'], 73 | subnet=json_data['subnet'], serial=json_data['serial'], 74 | hostname=json_data.get('hostname'), 75 | gateways=json_data.get('gateway'), 76 | subnet_mask=json_data.get('subnet_mask'), 77 | broadcast_address=json_data.get('broadcast_address'), 78 | domain_name=json_data.get('domain_name'), 79 | domain_name_servers=json_data.get('domain_name_servers'), 80 | ntp_servers=json_data.get('ntp_servers'), 81 | extra=json_data.get('extra'), 82 | ) 83 | 84 | 85 | import json 86 | import logging 87 | import urllib.request, urllib.parse 88 | 89 | from staticdhcpdlib.databases.generic import (Definition, Database, CachingDatabase) 90 | 91 | _logger = logging.getLogger("extension.httpdb") 92 | 93 | #This class implements your lookup method; to customise this module for your 94 | #site, all you should need to do is edit this section. 95 | class _HTTPLogic(object): 96 | def __init__(self): 97 | from staticdhcpdlib import config 98 | 99 | try: 100 | self._uri = config.X_HTTPDB_URI 101 | except AttributeError: 102 | raise AttributeError("X_HTTPDB_URI must be specified in conf.py") 103 | self._headers = getattr(config, 'X_HTTPDB_HEADERS', {}) 104 | self._parameters = getattr(config, 'X_HTTPDB_PARAMETERS', {}) 105 | self._parameter_key_mac = getattr(config, 'X_HTTPDB_PARAMETER_KEY_MAC', 'mac') 106 | self._post = getattr(config, 'X_HTTPDB_POST', True) 107 | self._default_name_servers = getattr(config, 'X_HTTPDB_DEFAULT_NAME_SERVERS', '') 108 | self._default_lease_time = getattr(config, 'X_HTTPDB_DEFAULT_LEASE_TIME', 0) 109 | self._default_serial = getattr(config, 'X_HTTPDB_DEFAULT_SERIAL', 0) 110 | 111 | def _lookupMAC(self, mac): 112 | """ 113 | Performs the actual lookup operation; this is the first thing you should 114 | study when customising for your site. 115 | """ 116 | global _parse_server_response 117 | #If you need to generate per-request headers, add them here 118 | headers = self._headers.copy() 119 | 120 | #To alter the parameters supplied with the request, alter this 121 | parameters = self._parameters.copy() 122 | #Dynamic items 123 | parameters.update({ 124 | self._parameter_key_mac: str(mac), 125 | }) 126 | 127 | #You can usually ignore this if-block, though you could strip out whichever method you don't use 128 | if self._post: 129 | data = json.dumps(parameters).encode('utf-8') 130 | 131 | headers.update({ 132 | 'Content-Length': str(len(data)), 133 | 'Content-Type': 'application/json', 134 | }) 135 | 136 | request = urllib.request.Request( 137 | self._uri, data=data, 138 | headers=headers, 139 | ) 140 | else: 141 | request = urllib.request.Request( 142 | "{}?{}".format(self._uri, urllib.parse.urlencode(parameters, doseq=True)), 143 | headers=headers, 144 | ) 145 | 146 | _logger.debug("Sending request to '{}' for '{}'...".format(self._uri, parameters)) 147 | 148 | try: 149 | response = urllib.request.urlopen(request) 150 | _logger.debug("MAC response received from '{}' for '{}'".format(self._uri, mac)) 151 | results = json.loads(response.read()) 152 | except Exception as e: 153 | _logger.error("Failed to lookup '{}' on '{}': {}".format(mac, self._uri, e)) 154 | raise 155 | else: 156 | if results: 157 | _logger.debug("Known MAC response from '{}' for '{}': {!r}".format(self._uri, mac, results)) 158 | 159 | if isinstance(results, list): #Multi-definition response 160 | return [_parse_server_response(self._set_defaults(result)) for result in results] 161 | return _parse_server_response(self._set_defaults(results)) 162 | else: #The server sent back 'null' or an empty object 163 | _logger.debug("Unknown MAC response from '{}' for '{}': {!r}".format(self._uri, mac, results)) 164 | return None 165 | 166 | def _set_defaults(self, json_data): 167 | """ 168 | Set the default values on a server response if they do not 169 | already have usable values 170 | 171 | :param dictionary json_data: Dictionary containing response data 172 | :return dictionary: The modified dictionary with defaults 173 | """ 174 | if not json_data.get('serial'): 175 | json_data['serial'] = self._default_serial 176 | if not json_data.get('domain_name_servers'): 177 | json_data['domain_name_servers'] = self._default_name_servers 178 | if not json_data.get('lease_time'): 179 | json_data['lease_time'] = self._default_lease_time 180 | return json_data 181 | 182 | class HTTPDatabase(Database, _HTTPLogic): 183 | def __init__(self): 184 | _HTTPLogic.__init__(self) 185 | 186 | def lookupMAC(self, mac): 187 | return self._lookupMAC(mac) 188 | 189 | class HTTPCachingDatabase(CachingDatabase, _HTTPLogic): 190 | def __init__(self): 191 | from staticdhcpdlib import config 192 | if hasattr(config, 'X_HTTPDB_CONCURRENCY_LIMIT'): 193 | CachingDatabase.__init__(self, concurrency_limit=config.X_HTTPDB_CONCURRENCY_LIMIT) 194 | else: 195 | CachingDatabase.__init__(self) 196 | _HTTPLogic.__init__(self) 197 | -------------------------------------------------------------------------------- /staticDHCPd/extensions/official/recent_activity.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | """ 3 | Provides a means of easily tracking recent DHCP activity, so you can see if a 4 | device is self-configuring properly without needing to read through the logs. 5 | 6 | To use this module, configure whatever is required in conf.py, inside of init(), 7 | like this: 8 | with extensions.recent_activity as x: 9 | x.MAX_EVENTS = 5 10 | 11 | For a list of all parameters you may define, see below. 12 | 13 | Then add the following to conf.py's init() function: 14 | import staticDHCPd_extensions.recent_activity 15 | 16 | Like staticDHCPd, this module under the GNU General Public License v3 17 | (C) Neil Tallim, 2021 18 | 19 | Inspiration derived from a discussion with John Stowers 20 | """ 21 | from staticdhcpdlib import config 22 | _config = config.conf.extensions.recent_activity 23 | _CONFIG = _config.extension_config_merge(defaults={ 24 | #The number of events to track; if None, no limit will be applied 25 | 'MAX_EVENTS': 10, 26 | #The maximum age of an event to track; if None, no limit will be applied 27 | 'MAX_AGE': 60 * 5, 28 | 29 | #Whether feed items should be removed when the system is reinitialised 30 | 'CLEAR_ON_REINIT': True, 31 | 32 | #Whether the list should be part of the dashboard; if False, it will appear 33 | #in the methods list 34 | 'DISPLAY_IN_DASHBOARD': True, 35 | #The positioning bias of this element in the dashboard, as an integer; if 36 | #None, it will appear at the end 37 | 'DASHBOARD_ORDERING': None, 38 | #If registering as a method, this is where its callback will be registered 39 | 'METHOD_PATH': '/ca/uguu/puukusoft/staticDHCPd/extension/recent-activity/render', 40 | 41 | #If either MODULE or NAME are None and the module is not to be rendered in 42 | #the dashboard, the method-link will be hidden 43 | 44 | #The name of the module to which this element belongs 45 | 'MODULE': 'recent activity', 46 | #The name of this component 47 | 'NAME': 'dhcp', 48 | }, required=[ 49 | ]) 50 | del _config 51 | 52 | #Do not touch anything below this line 53 | ################################################################################ 54 | import collections 55 | import logging 56 | import threading 57 | import time 58 | 59 | _logger = logging.getLogger('extension.recent_activity') 60 | 61 | _events = collections.deque(maxlen=_CONFIG['MAX_EVENTS']) 62 | _lock = threading.Lock() 63 | 64 | _Event = collections.namedtuple('Event', ('time', 'mac', 'ip', 'subnet', 'serial', 'method', 'port')) 65 | 66 | def _drop_old_events(): 67 | """ 68 | Clears out any events older than `MAX_AGE`. 69 | """ 70 | max_age = time.time() - _CONFIG['MAX_AGE'] 71 | dropped = 0 72 | with _lock: 73 | while _events: 74 | if _events[-1].time < max_age: 75 | _events.pop() 76 | dropped += 1 77 | else: 78 | break 79 | if dropped: 80 | _logger.debug("Dropped {} events from recent activity due to age".format(dropped)) 81 | 82 | def _flush(): 83 | with _lock: 84 | _events.clear() 85 | 86 | def _render(*args, **kwargs): 87 | """ 88 | Provides a dashboard-embeddable rendering of all recent activity. 89 | """ 90 | _drop_old_events() 91 | with _lock: 92 | if not _events: 93 | return "No activity in the last {} seconds".format(_CONFIG['MAX_AGE']) 94 | 95 | elements = [] 96 | for event in _events: 97 | elements.append(""" 98 | 99 | {event} 100 | {port} 101 | {mac} 102 | {ip} 103 | {subnet} 104 | {serial} 105 | {time} 106 | """.format( 107 | event=event.method, 108 | port=event.port, 109 | mac=event.mac, 110 | ip=event.ip or '-', 111 | subnet=event.subnet, 112 | serial=event.serial, 113 | time=time.ctime(event.time), 114 | )) 115 | return """ 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | {content} 130 | 131 |
EventPortMACIPSubnetSerialTime
""".format( 132 | content='\n'.join(elements), 133 | ) 134 | 135 | def _update(statistics): 136 | """ 137 | Removes any prior event from `mac`, then adds the event to the collection. 138 | """ 139 | mac = str(statistics.mac) 140 | with _lock: 141 | for (i, event) in enumerate(_events): 142 | if event.mac == mac: 143 | del _events[i] 144 | break 145 | 146 | _events.appendleft(_Event(time.time(), mac, statistics.ip, statistics.subnet, statistics.serial, statistics.method, statistics.port)) 147 | 148 | #Setup happens here 149 | ################################################################################ 150 | config.callbacks.statsAddHandler(_update) 151 | _logger.info("Prepared recent-activity-tracker for up to {} events, {} seconds old".format(_CONFIG['MAX_EVENTS'], _CONFIG['MAX_AGE'])) 152 | 153 | if _CONFIG['CLEAR_ON_REINIT']: 154 | _logger.info("Registering callback handler to clear activity on reinitialisation...") 155 | config.callbacks.systemAddReinitHandler(_flush) 156 | 157 | if _CONFIG['DISPLAY_IN_DASHBOARD']: 158 | _logger.info("Registering activity-tracker as a dashboard element, with ordering={}".format(_CONFIG['DASHBOARD_ORDERING'])) 159 | config.callbacks.webAddDashboard( 160 | _CONFIG['MODULE'], _CONFIG['NAME'], _render, 161 | ordering=_CONFIG['DASHBOARD_ORDERING'], 162 | ) 163 | else: 164 | _logger.info("Registering activity-tracker at '{}'".format(_CONFIG['METHOD_PATH'])) 165 | config.callbacks.webAddMethod( 166 | _CONFIG['METHOD_PATH'], _render, 167 | hidden=(_CONFIG['MODULE'] is None or _CONFIG['NAME'] is None), 168 | module=_CONFIG['MODULE'], 169 | name=_CONFIG['NAME'], 170 | display_mode=config.callbacks.WEB_METHOD_TEMPLATE, 171 | ) 172 | -------------------------------------------------------------------------------- /staticDHCPd/extensions/official/redis_static-utils/dhcp-static-add: -------------------------------------------------------------------------------- 1 | #!/bin/python3 2 | import argparse 3 | import ipaddress 4 | import json 5 | import re 6 | 7 | import redis 8 | 9 | REDIS_HOST = '1.2.3.4' 10 | REDIS_DB = 0 11 | REDIS_PASSWORD = 'very secret' 12 | 13 | SUBNET_SERIAL_MAP = { 14 | ipaddress.IPv4Network('10.0.0.0/24'): ('10.40.0.0/24', 0), 15 | } 16 | 17 | parser = argparse.ArgumentParser( 18 | description='Configure static IP reservation via DHCP', 19 | ) 20 | parser.add_argument('mac', type=str, help="the MAC address to bind") 21 | parser.add_argument('ip', type=str, help="the IP address to bind") 22 | parser.add_argument('--lease-time', type=int, help="the number of seconds for which to hold the lease [overrides subnet|serial]") 23 | parser.add_argument('--hostname', type=str, help="the name to offer the host [optional, rarely used]") 24 | parser.add_argument('--subnet-mask', type=str, help="the subnet-mask to assign [overrides subnet|serial]") 25 | parser.add_argument('--gateway', type=str, help="the gateway to assign [overrides subnet|serial]") 26 | parser.add_argument('--broadcast-address', type=str, help="the broadcast-address to assign [overrides subnet|serial]") 27 | parser.add_argument('--domain-name', type=str, help="the domain-name to assign [overrides subnet|serial]") 28 | parser.add_argument('--domain-name-server', type=str, action='append', help="a DNS server IP to assign; may be repeated up to three times [overrides subnet|serial]") 29 | parser.add_argument('--ntp-server', type=str, action='append', help="an NTP server IP to assign; may be repeated up to three times [overrides subnet|serial]") 30 | parser.add_argument('--extra', type=str, help="JSON-encoded metadata to assign [overrides subnet|serial]") 31 | args = parser.parse_args() 32 | 33 | if not re.match('^(?:[0-9a-f]{2}:){5}[0-9a-f]{2}$', args.mac): 34 | raise ValueError("MAC address is not in the format 'aa:bb:cc:dd:ee:ff'") 35 | target_ip = ipaddress.IPv4Address(args.ip) 36 | for (supernet, (subnet, serial)) in SUBNET_SERIAL_MAP.items(): 37 | if target_ip in supernet: 38 | break 39 | else: 40 | raise ValueError("Target IP address is not in a recognised subnet") 41 | 42 | assignment = { 43 | 'ip': str(target_ip), 44 | 'subnet': subnet, 45 | 'serial': serial, 46 | } 47 | 48 | if args.lease_time is not None: 49 | if args.lease_time < 300 or args.lease_time > 50400: 50 | raise ValueError("Lease-times of less than 5 minutes or greater than 2 weeks are unsupported") 51 | assignment['lease_time'] = args.lease_time 52 | 53 | if args.hostname is not None: 54 | assignment['hostname'] = args.hostname 55 | 56 | if args.subnet_mask is not None: 57 | assignment['subnet_mask'] = str(ipaddress.IPv4Address(args.subnet_mask)) 58 | if args.gateway is not None: 59 | assignment['gateway'] = str(ipaddress.IPv4Address(args.gateway)) 60 | if args.broadcast_address is not None: 61 | assignment['broadcast_address'] = str(ipaddress.IPv4Address(args.broadcast_address)) 62 | 63 | if args.domain_name is not None: 64 | assignment['domain_name'] = args.domain_name 65 | if args.domain_name_server is not None: 66 | assignment['domain_name_servers'] = ','.join(str(ipaddress.IPv4Address(addr)) for addr in args.domain_name_server) 67 | 68 | if args.ntp_server is not None: 69 | assignment['ntp_servers'] = ','.join(str(ipaddress.IPv4Address(addr)) for addr in args.ntp_server) 70 | 71 | if args.extra is not None: 72 | assignment['extra'] = json.dumps(json.loads(args.extra), separators=(',', ':')) 73 | 74 | 75 | redis_client = redis.Redis( 76 | host=REDIS_HOST, db=REDIS_DB, 77 | password=REDIS_PASSWORD, 78 | decode_responses=True, 79 | ) 80 | 81 | if redis_client.exists(args.mac): 82 | raise ValueError('Target MAC is already bound and will not be overwritten') 83 | 84 | redis_client.hset(args.mac, mapping=assignment) 85 | -------------------------------------------------------------------------------- /staticDHCPd/extensions/official/redis_static-utils/dhcp-static-list: -------------------------------------------------------------------------------- 1 | #!/bin/python3 2 | import argparse 3 | import ipaddress 4 | import json 5 | import re 6 | import sys 7 | 8 | import redis 9 | 10 | REDIS_HOST = '1.2.3.4' 11 | REDIS_DB = 0 12 | REDIS_PASSWORD = 'very secret' 13 | 14 | MAC_RE = re.compile('^(?:[0-9a-f]{2}:){5}[0-9a-f]{2}$') 15 | 16 | parser = argparse.ArgumentParser( 17 | description='Enumerate static IP reservation via DHCP', 18 | ) 19 | parser.add_argument('--filter-mac', type=str, action='append', help="the MAC address to look up; may be repeated") 20 | parser.add_argument('--filter-ip', type=str, action='append', help="the IP address to look up; may be repeated") 21 | args = parser.parse_args() 22 | 23 | macs = set() 24 | if args.filter_mac is not None: 25 | for mac in args.filter_mac: 26 | if MAC_RE.match(mac): 27 | macs.add(mac) 28 | else: 29 | raise ValueError("MAC address {} is not in the format 'aa:bb:cc:dd:ee:ff'".format(mac)) 30 | 31 | ips = set() 32 | if args.filter_ip is not None: 33 | for ip in args.filter_ip: 34 | ips.add(str(ipaddress.IPv4Address(ip))) 35 | 36 | redis_client = redis.Redis( 37 | host=REDIS_HOST, db=REDIS_DB, 38 | password=REDIS_PASSWORD, 39 | decode_responses=True, 40 | ) 41 | 42 | 43 | subnet_serials = {} 44 | matches = {} 45 | 46 | for key in redis_client.scan_iter(): 47 | if MAC_RE.match(key): 48 | if macs and not ips and key not in macs: 49 | continue #no need to look any deeper 50 | record = redis_client.hgetall(key) 51 | 52 | if not macs and not ips: 53 | matches[key] = record 54 | elif key in macs or record['ip'] in ips: 55 | matches[key] = record 56 | 57 | for (mac, match) in matches.items(): 58 | subnet_serial = (match['subnet'], match['serial']) 59 | if subnet_serial not in subnet_serials: 60 | print('retrieving {}|{}...'.format(*subnet_serial), file=sys.stderr) 61 | subnet_serials[subnet_serial] = redis_client.hgetall('{}|{}'.format(*subnet_serial)) 62 | inherited = subnet_serials[subnet_serial] 63 | 64 | if 'extra' in match: 65 | match['extra'] = json.loads(match['extra']) 66 | for (key, value) in inherited.items(): 67 | if key not in match: 68 | if key == 'extra': 69 | value = json.loads(value) 70 | match[':{}'.format(key)] = value 71 | 72 | print('a leading colon means the value was inherited from its subnet|serial definition', file=sys.stderr) 73 | print(json.dumps(matches, sort_keys=True, indent=4)) 74 | -------------------------------------------------------------------------------- /staticDHCPd/extensions/official/redis_static-utils/dhcp-static-remove: -------------------------------------------------------------------------------- 1 | #!/bin/python3 2 | import argparse 3 | import re 4 | 5 | import redis 6 | 7 | REDIS_HOST = '1.2.3.4' 8 | REDIS_DB = 0 9 | REDIS_PASSWORD = 'very secret' 10 | 11 | parser = argparse.ArgumentParser( 12 | description='Drop static IP reservation via DHCP', 13 | ) 14 | parser.add_argument('mac', type=str, help="the MAC address to unbind") 15 | args = parser.parse_args() 16 | 17 | if not re.match('^(?:[0-9a-f]{2}:){5}[0-9a-f]{2}$', args.mac): 18 | raise ValueError("MAC address is not in the format 'aa:bb:cc:dd:ee:ff'") 19 | 20 | 21 | redis_client = redis.Redis( 22 | host=REDIS_HOST, db=REDIS_DB, 23 | password=REDIS_PASSWORD, 24 | decode_responses=True, 25 | ) 26 | 27 | redis_client.delete(args.mac) 28 | -------------------------------------------------------------------------------- /staticDHCPd/extensions/official/redis_static.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | """ 3 | Provides a means of using a Redis server to implement static addressing in a 4 | manner that may be shared by multiple StaticDHCPd instances. 5 | 6 | The redis-py package is required. 7 | This module does not support Sentinel or Cluster operation. Please feel free to 8 | modify it to meet your own needs if this is something you require. 9 | 10 | To use this module without making any code changes, make the following changes 11 | to conf.py; if anything more sophisticated is required, fork it and hack away: 12 | Locate the DATABASE_ENGINE line and replace it with the following two lines: 13 | import staticDHCPd_extensions.redis_static as redis_static 14 | DATABASE_ENGINE = redis_static.RedisDatabase #or redis_static.RedisCachingDatabase 15 | 16 | Anywhere above the 'import redis_static' line, specify any Redis connection parameters 17 | that you need to override, such as the following (see redis-py for a full list): 18 | X_REDISDB_KWARGS = { 19 | 'host': '1.2.3.4', 20 | 'port': 1234, 21 | 'db': 0, 22 | } 23 | #If using RedisCachingDatabase, the maximum number of requests to run 24 | #at a time; successive requests will block; DEFAULTS TO (effectively) 25 | #INFINITE 26 | X_REDISDB_CONCURRENCY_LIMIT = 10 27 | 28 | Your database is expected to allow for MAC addresses to be used as keys for hashes, 29 | like '11:aa:22:bb:33:cc', within which assigned values will be keyed: 30 | { 31 | "ip": "192.168.0.1", 32 | "subnet": "subnet-id", 33 | "serial": 0, 34 | "lease_time": 3600, //may be omitted 35 | "hostname": "any-valid-hostname", //may be omitted 36 | "subnet_mask": "255.255.255.0", //may be omitted 37 | "gateway": "192.168.0.1", //may be omitted 38 | "broadcast_address": "192.168.0.255", //may be omitted 39 | "domain_name": "example.org", //may be omitted 40 | "domain_name_servers": "192.168.0.1,192.168.0.2, 192.168.0.3", //may be omitted; limit: 3 entries 41 | "ntp_servers": "192.168.0.1,192.168.0.2, 192.168.0.3", //may be omitted; limit: 3 entries 42 | "extra": , //any extra attributes you would like in the lease-definition; may be omitted 43 | } 44 | 45 | Every (subnet, serial) pair associated with a MAC must be defined under another key, 46 | identified as '|' like '10.0.0.0/24|0', within which default values 47 | will be keyed: 48 | { 49 | "lease_time": 3600, //value in seconds 50 | "subnet_mask": "255.255.255.0", //may be omitted 51 | "gateway": "192.168.0.1", //may be omitted 52 | "broadcast_address": "192.168.0.255", //may be omitted 53 | "domain_name": "example.org", //may be omitted 54 | "domain_name_servers": "192.168.0.1,192.168.0.2, 192.168.0.3", //may be omitted; limit: 3 entries 55 | "ntp_servers": "192.168.0.1,192.168.0.2, 192.168.0.3", //may be omitted; limit: 3 entries 56 | "extra": , //any extra attributes you would like in the lease-definition; may be omitted 57 | } 58 | 59 | If concurrent connections to your Redis server should be limited, use 60 | RedisCachingDatabase instead of RedisDatabase. 61 | 62 | Like staticDHCPd, this module under the GNU General Public License v3 63 | (C) Neil Tallim, 2023 64 | """ 65 | 66 | import json 67 | import logging 68 | import uuid 69 | 70 | import redis 71 | 72 | from staticdhcpdlib.databases.generic import (Definition, Database, CachingDatabase) 73 | 74 | _logger = logging.getLogger("extension.redisdb") 75 | 76 | class _RedisLogic(object): 77 | _redis_client = None #a connection-pool behind the scenes 78 | 79 | def __init__(self): 80 | from staticdhcpdlib import config 81 | 82 | self._redis_client = redis.Redis( 83 | decode_responses=True, 84 | **getattr(config, 'X_REDISDB_KWARGS', {}), 85 | ) 86 | 87 | def _lookupMAC(self, mac): 88 | details = self._redis_client.hgetall(str(mac)) 89 | if not details: 90 | _logger.debug("Unknown MAC response for '{}'".format(mac)) 91 | return None 92 | _logger.debug("Known MAC response for '{}': {!r}".format(mac, details)) 93 | 94 | subnet_serial = '{}|{}'.format(details['subnet'], details['serial']) 95 | details_ss = self._redis_client.hgetall(subnet_serial) 96 | if not details_ss: 97 | _logger.warning("Unknown subnet|serial: '{}'".format(subnet_serial)) 98 | return None 99 | _logger.debug("Known subnet|serial response for '{}': {!r}".format(subnet_serial, details_ss)) 100 | 101 | #prepare response 102 | 103 | extra = details_ss.get('extra') 104 | combined_extra = extra and json.loads(extra) or {} 105 | extra = details.get('extra') 106 | combined_extra.update(extra and json.loads(extra) or {}) 107 | if not combined_extra: 108 | combined_extra = None 109 | 110 | domain_name_servers = details.get('domain_name_servers', details_ss.get('domain_name_servers')) 111 | if domain_name_servers: 112 | domain_name_servers = [v.strip() for v in domain_name_servers.split(',')][:3] 113 | 114 | ntp_servers = details.get('ntp_servers', details_ss.get('ntp_servers')) 115 | if ntp_servers: 116 | ntp_servers = [v.strip() for v in ntp_servers.split(',')][:3] 117 | 118 | return Definition( 119 | ip=details['ip'], lease_time=details.get('lease_time', details_ss['lease_time']), 120 | subnet=details['subnet'], serial=details['serial'], 121 | hostname=details.get('hostname'), 122 | gateways=details.get('gateway', details_ss.get('gateway')), 123 | subnet_mask=details.get('subnet_mask', details_ss.get('subnet_mask')), 124 | broadcast_address=details.get('broadcast_address', details_ss.get('broadcast_address')), 125 | domain_name=details.get('domain_name', details_ss.get('domain_name')), 126 | domain_name_servers=domain_name_servers, 127 | ntp_servers=ntp_servers, 128 | extra=combined_extra, 129 | ) 130 | 131 | class RedisDatabase(Database, _RedisLogic): 132 | def __init__(self): 133 | _RedisLogic.__init__(self) 134 | 135 | def lookupMAC(self, mac): 136 | return self._lookupMAC(mac) 137 | 138 | class RedisCachingDatabase(CachingDatabase, _RedisLogic): 139 | def __init__(self): 140 | from staticdhcpdlib import config 141 | 142 | if hasattr(config, 'X_REDISDB_CONCURRENCY_LIMIT'): 143 | CachingDatabase.__init__(self, concurrency_limit=config.X_REDISDB_CONCURRENCY_LIMIT) 144 | else: 145 | CachingDatabase.__init__(self) 146 | 147 | _RedisLogic.__init__(self) 148 | -------------------------------------------------------------------------------- /staticDHCPd/setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- encoding: utf-8 -*- 3 | """ 4 | Deployment script for staticDHCPd. 5 | """ 6 | from distutils.core import setup 7 | import os 8 | import platform 9 | import re 10 | import sys 11 | 12 | import staticdhcpdlib 13 | 14 | setup( 15 | name='staticDHCPd', 16 | version=staticdhcpdlib.VERSION, 17 | description='Highly customisable, static-lease-focused DHCP server', 18 | long_description=( 19 | 'staticDHCPd is an extensively customisable, high-performance,' 20 | ' RFC-spec-compliant DHCP server, well-suited to labs, LAN parties, home and' 21 | ' small-office networks, and specialised networks of vast size.' 22 | '\n\n' 23 | 'It supports all major DHCP extension RFCs and features a rich, plugin-oriented' 24 | ' web-interface, has a variety of modules, ranging from statistics-management' 25 | ' to notification services to dynamic address-provisioning and' 26 | ' network-auto-discovery.' 27 | '\n\n' 28 | 'Multiple backend databases are supported, from INI files to RDBMS SQL servers,' 29 | ' with examples of how to write and integrate your own, such as a REST-JSON' 30 | ' endpoint, simple enough to complete in minutes.' 31 | ), 32 | author=re.search(', (.*?) <', staticdhcpdlib.COPYRIGHT).group(1), 33 | author_email=re.search('<(.*?)>', staticdhcpdlib.COPYRIGHT).group(1), 34 | license='GPLv3', 35 | url=staticdhcpdlib.URL, 36 | packages=[ 37 | 'staticdhcpdlib', 38 | 'staticdhcpdlib.databases', 39 | 'staticdhcpdlib.web', 40 | ], 41 | data_files=[ 42 | ('/etc/staticDHCPd', [ 43 | 'conf/conf.py.sample', 44 | ]), 45 | ('/etc/staticDHCPd/staticDHCPd_extensions', [ 46 | 'conf/staticDHCPd_extensions/HOWTO', 47 | ]), 48 | ], 49 | scripts=[ 50 | 'staticDHCPd', 51 | ], 52 | ) 53 | 54 | #Post-installation stuff 55 | if os.getenv('DEBUILD_MODE') != 'yes' and not 'build' in sys.argv and not any('rpm' in i for i in sys.argv): #Don't print confusing stuff when building packages 56 | instructions = [ 57 | "", 58 | "", 59 | "Before you can run 'staticDHCPd', which should now be in PATH, you must copy", 60 | "/etc/staticDHCPd/conf.py.sample to /etc/staticDHCPd/conf.py and configure it", 61 | "", 62 | "Perform the following tasks to configure staticDHCPd to launch on system startup", 63 | ] 64 | if os.path.isfile('/etc/debian_version'): 65 | instructions.extend(( 66 | "Debian-like", 67 | "\tCopy control-scripts/staticDHCPd.service to /etc/systemd/system/", 68 | "\tRun 'systemctl enable staticDHCPd.service'", 69 | )) 70 | elif platform.mac_ver()[0]: 71 | instructions.extend(( 72 | "OS X", 73 | "\tCopy control-scripts/ca.uguu.puukusoft.staticDHCPd.plist to /Library/LaunchDaemons/", 74 | )) 75 | else: 76 | instructions.extend(( 77 | "Instructions relevant to your platform are unavailable; please contribute documentation", 78 | )) 79 | 80 | for i in instructions: 81 | print(i) 82 | 83 | -------------------------------------------------------------------------------- /staticDHCPd/staticdhcpdlib/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | """ 3 | staticdhcpdlib 4 | ============== 5 | Provides the logical implementation of a staticDHCPd daemon. 6 | 7 | Legal 8 | ----- 9 | This file is part of staticDHCPd. 10 | staticDHCPd is free software; you can redistribute it and/or modify 11 | it under the terms of the GNU General Public License as published by 12 | the Free Software Foundation; either version 3 of the License, or 13 | (at your option) any later version. 14 | 15 | This program is distributed in the hope that it will be useful, 16 | but WITHOUT ANY WARRANTY; without even the implied warranty of 17 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 18 | GNU General Public License for more details. 19 | 20 | You should have received a copy of the GNU General Public License 21 | along with this program. If not, see . 22 | 23 | (C) Neil Tallim, 2021 24 | """ 25 | VERSION = '3.0.0-beta1' 26 | URL = 'https://github.com/flan/staticdhcpd' 27 | COPYRIGHT = '2021, Neil Tallim ' 28 | -------------------------------------------------------------------------------- /staticDHCPd/staticdhcpdlib/databases/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | """ 3 | staticdhcpdlib.databases 4 | ======================== 5 | Templates and implementations of database interfaces for staticDHCPd. 6 | 7 | Legal 8 | ----- 9 | This file is part of staticDHCPd. 10 | staticDHCPd is free software; you can redistribute it and/or modify 11 | it under the terms of the GNU General Public License as published by 12 | the Free Software Foundation; either version 3 of the License, or 13 | (at your option) any later version. 14 | 15 | This program is distributed in the hope that it will be useful, 16 | but WITHOUT ANY WARRANTY; without even the implied warranty of 17 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 18 | GNU General Public License for more details. 19 | 20 | You should have received a copy of the GNU General Public License 21 | along with this program. If not, see . 22 | 23 | (C) Neil Tallim, 2021 24 | """ 25 | import logging 26 | 27 | _logger = logging.getLogger('databases') 28 | 29 | def get_database(): 30 | """ 31 | Assembles and returns a database-interface object. 32 | 33 | :return :class:`Database `: A database interface, usable 34 | to access DHCP information. 35 | """ 36 | #Deferred import to avoid circular issues when defining custom databases that import from generic 37 | from .. import config 38 | 39 | if callable(config.DATABASE_ENGINE): 40 | _logger.debug("Custom database engine supplied; initialising...") 41 | return config.DATABASE_ENGINE() 42 | 43 | _logger.debug("Loading database of type {!r}...".format(config.DATABASE_ENGINE)) 44 | 45 | if not config.DATABASE_ENGINE: 46 | from .generic import Null 47 | return Null() 48 | elif config.DATABASE_ENGINE == 'SQLite': 49 | from ._sql import SQLite 50 | return SQLite() 51 | elif config.DATABASE_ENGINE == 'PostgreSQL': 52 | from ._sql import PostgreSQL 53 | return PostgreSQL() 54 | elif config.DATABASE_ENGINE == 'MySQL': 55 | from ._sql import MySQL 56 | return MySQL() 57 | elif config.DATABASE_ENGINE == 'Oracle': 58 | from ._sql import Oracle 59 | return Oracle() 60 | elif config.DATABASE_ENGINE == 'INI': 61 | from ._ini import INI 62 | return INI() 63 | 64 | raise ValueError("Unknown database engine: {}".format(config.DATABASE_ENGINE)) 65 | -------------------------------------------------------------------------------- /staticDHCPd/staticdhcpdlib/databases/_ini.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | """ 3 | staticdhcpdlib.databases._ini 4 | ============================= 5 | Provides a uniform datasource API, implementing an INI-file-based backend. 6 | 7 | Legal 8 | ----- 9 | This file is part of staticDHCPd. 10 | staticDHCPd is free software; you can redistribute it and/or modify 11 | it under the terms of the GNU General Public License as published by 12 | the Free Software Foundation; either version 3 of the License, or 13 | (at your option) any later version. 14 | 15 | This program is distributed in the hope that it will be useful, 16 | but WITHOUT ANY WARRANTY; without even the implied warranty of 17 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 18 | GNU General Public License for more details. 19 | 20 | You should have received a copy of the GNU General Public License 21 | along with this program. If not, see . 22 | 23 | (C) Neil Tallim, 2021 24 | Inspiration derived from a discussion with John Stowers 25 | """ 26 | import configparser 27 | import logging 28 | import re 29 | import threading 30 | 31 | from libpydhcpserver.dhcp_types.mac import MAC 32 | 33 | from .. import config 34 | 35 | from .generic import (Definition, Database) 36 | 37 | _logger = logging.getLogger("databases._ini") 38 | 39 | class _Config(configparser.RawConfigParser): 40 | """ 41 | A simple wrapper around RawConfigParser to extend it with support for default values. 42 | """ 43 | def get(self, section, option, default): 44 | """ 45 | Returns a custom value, if one is found. Otherwise, returns ``default``. 46 | 47 | :param basestring section: The section to be queried. 48 | :param basestring option: The option to be queried. 49 | :param basestring default: The value to be returned, if the requested 50 | option is undefined. 51 | :return basestring : Either the requested value or the given default. 52 | """ 53 | try: 54 | return configparser.RawConfigParser.get(self, section, option) 55 | except configparser.Error: 56 | return default 57 | 58 | def getint(self, section, option, default): 59 | """ 60 | Returns a custom value, if one is found. Otherwise, returns ``default``. 61 | 62 | :param basestring section: The section to be queried. 63 | :param basestring option: The option to be queried. 64 | :param int default: The value to be returned, if the requested option 65 | is undefined. 66 | :return int: Either the requested value or the given default. 67 | :except ValueError: The value to be returned could not be converted to 68 | an ``int``. 69 | """ 70 | return int(self.get(section, option, default)) 71 | 72 | def getfloat(self, section, option, default): 73 | """ 74 | Returns a custom value, if one is found. Otherwise, returns ``default``. 75 | 76 | :param basestring section: The section to be queried. 77 | :param basestring option: The option to be queried. 78 | :param float default: The value to be returned, if the requested 79 | option is undefined. 80 | :return float: Either the requested value or the given default. 81 | :except ValueError: The value to be returned could not be converted to 82 | a ``float``. 83 | """ 84 | return float(self.get(section, option, default)) 85 | 86 | def getboolean(self, section, option, default): 87 | """ 88 | Returns a custom value, if one is found. Otherwise, returns ``default``. 89 | 90 | :param basestring section: The section to be queried. 91 | :param basestring option: The option to be queried. 92 | :param bool default: The value to be returned, if the requested option 93 | is undefined. 94 | :return bool: Either the requested value or the given default. 95 | """ 96 | return bool(str(self.get(section, option, default)).lower().strip() in ( 97 | 'y', 'yes', 98 | 't', 'true', 99 | 'ok', 'okay', 100 | '1', 101 | )) 102 | 103 | class INI(Database): 104 | """ 105 | Implements an INI broker. 106 | """ 107 | _maps = None #: A dictionary of MAC-associations 108 | _subnets = None #: A dictionary of subnet/serial associations 109 | _lock = None #: A lock to avoid race-conditions 110 | 111 | def __init__(self): 112 | """ 113 | Constructs the broker. 114 | """ 115 | self._maps = {} 116 | self._subnets = {} 117 | self._lock = threading.Lock() 118 | 119 | self.reinitialise() 120 | 121 | def _parse_extra_option(self, reader, section, option): 122 | method = reader.get 123 | none_on_error = False 124 | if option[1] == ':': 125 | l_option = option[0].lower() 126 | none_on_error = l_option != option[0] 127 | if l_option == 's': 128 | pass 129 | elif l_option == 'i': 130 | method = reader.getint 131 | elif l_option == 'f': 132 | method = reader.getfloat 133 | elif l_option == 'b': 134 | method = reader.getboolean 135 | 136 | real_option = option[2:] 137 | try: 138 | value = method(section, option, None) 139 | except ValueError: 140 | if none_on_error: 141 | return (real_option, None) 142 | raise 143 | else: 144 | return (real_option, value) 145 | 146 | def _parse_extra(self, reader, section, omitted, section_type): 147 | extra = {} 148 | for option in reader.options(section): 149 | if not option in omitted: 150 | (option, value) = self._parse_extra_option(section, option) 151 | extra['{}.{}'.format(section_type, option)] = value 152 | return extra or None 153 | 154 | def _parse_ini(self): 155 | """ 156 | Creates an optimal in-memory representation of the data in the INI file. 157 | """ 158 | _logger.info("Preparing to read '{}'...".format(config.INI_FILE)) 159 | reader = _Config() 160 | if not reader.read(config.INI_FILE): 161 | raise ValueError("Unable to read '{}'".format(config.INI_FILE)) 162 | 163 | subnet_re = re.compile(r"^(?P.+?)\|(?P\d+)$") 164 | 165 | for section in reader.sections(): 166 | m = subnet_re.match(section) 167 | if m: 168 | self._process_subnet(reader, section, m.group('subnet'), int(m.group('serial'))) 169 | else: 170 | try: 171 | mac = MAC(section) 172 | except Exception: 173 | _logger.warn("Unrecognised section encountered: {}".format(section)) 174 | else: 175 | self._process_map(reader, section, mac) 176 | 177 | self._validate_references() 178 | 179 | def _process_subnet(self, reader, section, subnet, serial): 180 | _logger.debug("Processing subnet: {}".format(section)) 181 | 182 | lease_time = reader.getint(section, 'lease-time', None) 183 | if not lease_time: 184 | raise ValueError("Field 'lease-time' unspecified for '{}'".format(section)) 185 | gateway = reader.get(section, 'gateway', None) 186 | subnet_mask = reader.get(section, 'subnet-mask', None) 187 | broadcast_address = reader.get(section, 'broadcast-address', None) 188 | ntp_servers = reader.get(section, 'ntp-servers', None) 189 | domain_name_servers = reader.get(section, 'domain-name-servers', None) 190 | domain_name = reader.get(section, 'domain-name', None) 191 | 192 | extra = self._parse_extra(reader, section, ( 193 | 'lease-time', 'gateway', 'subnet-mask', 'broadcast-address', 194 | 'ntp-servers', 'domain-name-servers', 'domain-name', 195 | ), 'subnets') 196 | 197 | self._subnets[(subnet, serial)] = ( 198 | lease_time, 199 | gateway, subnet_mask, broadcast_address, 200 | ntp_servers, domain_name_servers, domain_name, 201 | extra, 202 | ) 203 | 204 | def _process_map(self, reader, section, mac): 205 | _logger.debug("Processing map: {}".format(section)) 206 | 207 | ip = reader.get(section, 'ip', None) 208 | if not ip: 209 | raise ValueError("Field 'ip' unspecified for '{}'".format(section)) 210 | hostname = reader.get(section, 'hostname', None) 211 | subnet = reader.get(section, 'subnet', None) 212 | if not subnet: 213 | raise ValueError("Field 'subnet' unspecified for '{}'".format(section)) 214 | serial = reader.getint(section, 'serial', None) 215 | if serial is None: 216 | raise ValueError("Field 'serial' unspecified for '{}'".format(section)) 217 | 218 | extra = self._parse_extra(reader, section, ( 219 | 'ip', 'hostname', 220 | 'subnet', 'serial', 221 | ), 'maps') 222 | 223 | self._maps[int(mac)] = (ip, hostname, (subnet, serial), extra) 224 | 225 | def _validate_references(self): 226 | """ 227 | Effectively performs foreign-key checking, to avoid deferred errors. 228 | """ 229 | for (mac, (_, _, subnet, _)) in self._maps.items(): 230 | if subnet not in self._subnets: 231 | raise ValueError("MAC '{}' references unknown subnet '{}|{}'".format(MAC(mac), subnet[0], subnet[1])) 232 | 233 | def lookupMAC(self, mac): 234 | mac = int(mac) 235 | with self._lock: 236 | map = self._maps.get(mac) 237 | if not map: 238 | return None 239 | subnet = self._subnets.get(map[2]) 240 | 241 | extra_map = map[3] 242 | extra_subnet = subnet[7] 243 | if extra_map and extra_subnet: 244 | extra = extra_map.copy() 245 | extra.update(extra_subnet) 246 | else: 247 | extra = (extra_map and extra_map.copy()) or (extra_subnet and extra_subnet.copy()) 248 | 249 | return Definition( 250 | ip=map[0], lease_time=subnet[0], subnet=map[2][0], serial=map[2][1], 251 | hostname=map[1], 252 | gateways=subnet[1], subnet_mask=subnet[2], broadcast_address=subnet[3], 253 | domain_name=subnet[6], domain_name_servers=subnet[5], ntp_servers=subnet[4], 254 | extra=extra, 255 | ) 256 | 257 | def reinitialise(self): 258 | with self._lock: 259 | self._maps.clear() 260 | self._subnets.clear() 261 | self._parse_ini() 262 | _logger.info("INI-file contents parsed and loaded into memory") 263 | 264 | -------------------------------------------------------------------------------- /staticDHCPd/staticdhcpdlib/logging_handlers.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | """ 3 | staticdhcpdlib.logging_handlers 4 | =============================== 5 | Provides application-specific implementations of logging-friendly handlers. 6 | 7 | Legal 8 | ----- 9 | This file is part of staticDHCPd. 10 | staticDHCPd is free software; you can redistribute it and/or modify 11 | it under the terms of the GNU General Public License as published by 12 | the Free Software Foundation; either version 3 of the License, or 13 | (at your option) any later version. 14 | 15 | This program is distributed in the hope that it will be useful, 16 | but WITHOUT ANY WARRANTY; without even the implied warranty of 17 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 18 | GNU General Public License for more details. 19 | 20 | You should have received a copy of the GNU General Public License 21 | along with this program. If not, see . 22 | 23 | (C) Neil Tallim, 2021 24 | """ 25 | import collections 26 | import logging 27 | 28 | class FIFOHandler(logging.Handler): 29 | """ 30 | A handler that holds a fixed number of records, with FIFO behaviour. 31 | """ 32 | def __init__(self, capacity): 33 | """ 34 | Initialises the handler in a blank state. 35 | 36 | :param int capacity: The number of records the handler can hold. 37 | """ 38 | logging.Handler.__init__(self) 39 | self._records = collections.deque(maxlen=capacity) 40 | 41 | def emit(self, record): 42 | """ 43 | Called by the logging subsystem whenever new data is received. 44 | 45 | :param record: A logging record. 46 | """ 47 | self.acquire() 48 | try: 49 | self._records.appendleft(record) 50 | finally: 51 | self.release() 52 | 53 | def flush(self): 54 | """ 55 | Discards all logged records. 56 | """ 57 | self.acquire() 58 | try: 59 | self._records.clear() 60 | finally: 61 | self.release() 62 | 63 | def close(self): 64 | """ 65 | Called by the logging subsystem whenever the handler is closed. 66 | """ 67 | self.flush() 68 | logging.Handler.close(self) 69 | 70 | def readContents(self): 71 | """ 72 | Produces the current log. 73 | 74 | :return list(str): The logged records, in human-readable form. 75 | """ 76 | self.acquire() 77 | try: 78 | return [(record.levelno, self.format(record)) for record in self._records] 79 | finally: 80 | self.release() 81 | 82 | -------------------------------------------------------------------------------- /staticDHCPd/staticdhcpdlib/statistics.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | """ 3 | staticdhcpdlib.statistics 4 | ========================= 5 | Defines statistics-delegation methods and structures. 6 | 7 | Legal 8 | ----- 9 | This file is part of staticDHCPd. 10 | staticDHCPd is free software; you can redistribute it and/or modify 11 | it under the terms of the GNU General Public License as published by 12 | the Free Software Foundation; either version 3 of the License, or 13 | (at your option) any later version. 14 | 15 | This program is distributed in the hope that it will be useful, 16 | but WITHOUT ANY WARRANTY; without even the implied warranty of 17 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 18 | GNU General Public License for more details. 19 | 20 | You should have received a copy of the GNU General Public License 21 | along with this program. If not, see . 22 | 23 | (C) Neil Tallim, 2021 24 | """ 25 | import collections 26 | import logging 27 | import threading 28 | import traceback 29 | 30 | _logger = logging.getLogger('statistics') 31 | 32 | _stats_lock = threading.Lock() 33 | _stats_callbacks = [] 34 | 35 | Statistics = collections.namedtuple("Statistics", ( 36 | 'source_address', 37 | 'mac', 'ip', 38 | 'subnet', 'serial', 39 | 'method', 40 | 'processing_time', 'processed', 41 | 'port', 42 | )) 43 | """ 44 | Statistics associated with a DHCP event. 45 | 46 | .. py:attribute:: source_address 47 | :noindex: 48 | 49 | An :class:`libpydhcpserver.dhcp.Address` containing the IP and port of the 50 | client. 51 | 52 | .. py:attribute:: mac 53 | :noindex: 54 | 55 | A :class:`libpydhcpserver.dhcp_types.mac.MAC` containing the MAC of the 56 | client; None if the event was not due to a DHCP packet. 57 | 58 | .. py:attribute:: ip 59 | :noindex: 60 | 61 | An :class:`libpydhcpserver.dhcp_types.ipv4.IPv4` containing the address 62 | assigned to the client, if any. 63 | 64 | .. py:attribute:: subnet 65 | :noindex: 66 | 67 | The database-subnet associated with this event. 68 | 69 | .. py:attribute:: serial 70 | :noindex: 71 | 72 | The database-serial associated with this event. 73 | 74 | .. py:attribute:: method 75 | :noindex: 76 | 77 | The DHCP method of the received packet. 78 | 79 | .. py:attribute:: processing_time 80 | :noindex: 81 | 82 | The number of seconds required to finish processing the event. 83 | 84 | .. py:attribute:: processed 85 | :noindex: 86 | 87 | Whether the packet was fully processed (``False`` if non-DHCP or 88 | blocklisted). 89 | 90 | .. py:attribute:: port 91 | :noindex: 92 | 93 | The port on which the request was received. 94 | """ 95 | 96 | def emit(statistics): 97 | """ 98 | Invokes every registered stats handler to deliver new information. 99 | 100 | :param :class:`Statistics` statistics: The statistics to report. 101 | """ 102 | with _stats_lock: 103 | for callback in _stats_callbacks: 104 | try: 105 | callback(statistics) 106 | except Exception: 107 | _logger.critical("Unable to deliver statistics:\n{}".format(traceback.format_exc())) 108 | 109 | def registerStatsCallback(callback): 110 | """ 111 | Registers a statistics callback. 112 | 113 | :param callable callback: A callable that takes :data:`Statistics` as its 114 | argument; if already present, it will not be 115 | registered a second time. This function must never 116 | block for any significant amount of time. 117 | """ 118 | with _stats_lock: 119 | if callback in _stats_callbacks: 120 | _logger.error("Callback {!r} is already registered".format(callback)) 121 | else: 122 | _stats_callbacks.append(callback) 123 | _logger.debug("Registered stats-callback {!r}".format(callback)) 124 | 125 | def unregisterStatsCallback(callback): 126 | """ 127 | Unregisters a statistics callback. 128 | 129 | :param callable callback: The callable to be removed. 130 | :return bool: True if a callback was removed. 131 | """ 132 | with _stats_lock: 133 | try: 134 | _stats_callbacks.remove(callback) 135 | except ValueError: 136 | _logger.error("Callback {!r} is not registered".format(callback)) 137 | return False 138 | else: 139 | _logger.debug("Unregistered stats-callback {!r}".format(callback)) 140 | return True 141 | 142 | -------------------------------------------------------------------------------- /staticDHCPd/staticdhcpdlib/system.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | """ 3 | staticdhcpdlib.system 4 | ===================== 5 | Provides a centralised gathering point for system-level resources. 6 | 7 | Legal 8 | ----- 9 | This file is part of staticDHCPd. 10 | staticDHCPd is free software; you can redistribute it and/or modify 11 | it under the terms of the GNU General Public License as published by 12 | the Free Software Foundation; either version 3 of the License, or 13 | (at your option) any later version. 14 | 15 | This program is distributed in the hope that it will be useful, 16 | but WITHOUT ANY WARRANTY; without even the implied warranty of 17 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 18 | GNU General Public License for more details. 19 | 20 | You should have received a copy of the GNU General Public License 21 | along with this program. If not, see . 22 | 23 | (C) Neil Tallim, 2021 24 | """ 25 | import logging 26 | import threading 27 | import time 28 | import traceback 29 | 30 | _logger = logging.getLogger('system') 31 | 32 | ALIVE = True #: True until the system is ready to shut down. 33 | 34 | _reinitialisation_lock = threading.Lock() 35 | _reinitialisation_callbacks = [] 36 | 37 | _tick_lock = threading.Lock() 38 | _tick_callbacks = [] 39 | 40 | def reinitialise(): 41 | """ 42 | Invokes every registered reinitialisation handler. 43 | 44 | :return float: The number of seconds required to complete the operation. 45 | :except Exception: A callback failed to handle the reinitilisation request; 46 | this exception may be logged, but the system treats this 47 | as a fatal condition and will shut down. 48 | """ 49 | start = time.time() 50 | _logger.warn("System reinitilisation commencing...") 51 | with _reinitialisation_lock: 52 | for callback in _reinitialisation_callbacks: 53 | try: 54 | callback() 55 | except Exception: 56 | global ALIVE 57 | ALIVE = False 58 | _logger.critical("System shutdown triggered by unhandled exception:\n{}".format(traceback.format_exc())) 59 | raise 60 | _logger.warn("System reinitilisation complete") 61 | return time.time() - start 62 | 63 | def registerReinitialisationCallback(callback): 64 | """ 65 | Registers a reinitialisation callback. 66 | 67 | :param callable callback: A callable that takes no arguments; if already 68 | present, it will not be registered a second time. 69 | """ 70 | with _reinitialisation_lock: 71 | if callback in _reinitialisation_callbacks: 72 | _logger.error("Callback {!r} is already registered".format(callback)) 73 | else: 74 | _reinitialisation_callbacks.append(callback) 75 | _logger.debug("Registered reinitialisation {!r}".format(callback)) 76 | 77 | def unregisterReinitialisationCallback(callback): 78 | """ 79 | Unregisters a reinitialisation callback. 80 | 81 | :param callable callback: The callback to remove. 82 | :return bool: True if a callback was removed. 83 | """ 84 | with _reinitialisation_lock: 85 | try: 86 | _reinitialisation_callbacks.remove(callback) 87 | except ValueError: 88 | _logger.error("Callback {!r} is not registered".format(callback)) 89 | return False 90 | else: 91 | _logger.debug("Unregistered reinitialisation {!r}".format(callback)) 92 | return True 93 | 94 | def tick(): 95 | """ 96 | Invokes every registered tick handler. 97 | """ 98 | with _tick_lock: 99 | for callback in _tick_callbacks: 100 | try: 101 | callback() 102 | except Exception: 103 | _logger.critical("Unable to process tick-callback:\n{}".format(traceback.format_exc())) 104 | 105 | def registerTickCallback(callback): 106 | """ 107 | Registers a tick callback. Tick callbacks are invoked approximately once per 108 | second, but should treat this as a wake-up, not a metronome, and query the 109 | system-clock if performing any time-sensitive operations. 110 | 111 | :param callable callback: A callable that takes no arguments; if already 112 | present, it will not be registered a second time. 113 | The given callable must not block for any 114 | significant amount of time. 115 | """ 116 | with _tick_lock: 117 | if callback in _tick_callbacks: 118 | _logger.error("Callback {!r} is already registered".format(callback)) 119 | _logger.debug("Registered tick {!r}".format(callback)) 120 | else: 121 | _tick_callbacks.append(callback) 122 | 123 | def unregisterTickCallback(callback): 124 | """ 125 | Unregisters a tick callback. 126 | 127 | :param callable callback: The callback to remove. 128 | :return bool: True if a callback was removed. 129 | """ 130 | with _tick_lock: 131 | try: 132 | _tick_callbacks.remove(callback) 133 | except ValueError: 134 | _logger.error("Callback {!r} is not registered".format(callback)) 135 | return False 136 | else: 137 | _logger.debug("Unregistered tick {!r}".format(callback)) 138 | return True 139 | 140 | -------------------------------------------------------------------------------- /staticDHCPd/staticdhcpdlib/web/_templates.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | """ 3 | staticdhcpdlib.web._templated 4 | ============================= 5 | Handles all core templating requirements for rendering things like the 6 | dashboard. 7 | 8 | Legal 9 | ----- 10 | This file is part of staticDHCPd. 11 | staticDHCPd is free software; you can redistribute it and/or modify 12 | it under the terms of the GNU General Public License as published by 13 | the Free Software Foundation; either version 3 of the License, or 14 | (at your option) any later version. 15 | 16 | This program is distributed in the hope that it will be useful, 17 | but WITHOUT ANY WARRANTY; without even the implied warranty of 18 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 19 | GNU General Public License for more details. 20 | 21 | You should have received a copy of the GNU General Public License 22 | along with this program. If not, see . 23 | 24 | (C) Neil Tallim, 2021 25 | """ 26 | import logging 27 | import datetime 28 | import traceback 29 | 30 | from .. import config 31 | from . import functions 32 | 33 | import staticdhcpdlib 34 | import libpydhcpserver 35 | 36 | _logger = logging.getLogger('web.server') 37 | 38 | from staticdhcpdlib.web import ( 39 | retrieveHeaderCallbacks, 40 | retrieveDashboardCallbacks, 41 | retrieveVisibleMethodCallbacks 42 | ) 43 | 44 | _SYSTEM_NAME = functions.sanitise(config.SYSTEM_NAME) #: The name of the system 45 | _FOOTER = 'staticDHCPd v{} | libpydhcpserver v{}'.format( 46 | functions.sanitise(staticdhcpdlib.URL), 47 | functions.sanitise(staticdhcpdlib.VERSION), 48 | functions.sanitise(libpydhcpserver.URL), 49 | functions.sanitise(libpydhcpserver.VERSION), 50 | ) #: The footer's HTML fragment 51 | _BOOT_TIME = datetime.datetime.now().replace(microsecond=0) #: The time at which the system was started 52 | 53 | def _renderHeaders(path, queryargs, mimetype, data, headers): 54 | """ 55 | Renders all HTML headers. 56 | 57 | :param basestring path: The requested path. 58 | :param dict queryargs: All query arguments. 59 | :param basestring mimetype: The MIME-type of any accompanying data. 60 | :param str data: Any data uploaded by the client. 61 | :param headers: All HTTP headers. 62 | 63 | :return str: An HTML fragment. 64 | """ 65 | output = [] 66 | for callback in retrieveHeaderCallbacks(): 67 | try: 68 | content = callback(path, queryargs, mimetype, data, headers) 69 | except Exception: 70 | _logger.error("Unable to execute header-element {!r}:\n{}".format(callback, traceback.format_exc())) 71 | else: 72 | if content: 73 | output.append(content) 74 | return '\n'.join(output) 75 | 76 | def _renderHeader(): 77 | """ 78 | Renders the header section of the web interface. 79 | 80 | :return str: An HTML fragment. 81 | """ 82 | current_time = datetime.datetime.now().replace(microsecond=0) 83 | return """
Page generated {}
84 | {} online for {}, since {}""".format( 85 | current_time.ctime(), 86 | _SYSTEM_NAME, 87 | (current_time - _BOOT_TIME), 88 | _BOOT_TIME.ctime(), 89 | ) 90 | 91 | def _renderFooter(): 92 | """ 93 | Renders the footer section of the web interface. 94 | 95 | :return str: An HTML fragment. 96 | """ 97 | return _FOOTER 98 | 99 | def _renderMain(elements, path, queryargs, mimetype, data, headers): 100 | """ 101 | Renders the main section of the web interface. 102 | 103 | :param elements: The elements to render. 104 | :param basestring path: The requested path. 105 | :param dict queryargs: All query arguments. 106 | :param basestring mimetype: The MIME-type of any accompanying data. 107 | :param str data: Any data uploaded by the client. 108 | :param headers: All HTTP headers. 109 | 110 | :return str: An HTML fragment. 111 | """ 112 | output = [] 113 | for element in elements: 114 | if not element: 115 | output.append('
') 116 | continue 117 | 118 | try: 119 | result = element.callback(path=path, queryargs=queryargs, mimetype=mimetype, data=data, headers=headers) 120 | except Exception: 121 | _logger.error("Unable to render dashboard element '{}' '{}':\n{}".format( 122 | element.module, 123 | element.name, 124 | traceback.format_exc(), 125 | )) 126 | else: 127 | if result is not None: 128 | output.append('

{} | {}

'.format( 129 | element.module, 130 | element.name, 131 | )) 132 | output.append('
') 133 | output.append(result) 134 | output.append('
') 135 | return '\n'.join(output) 136 | 137 | def _renderMethods(): 138 | """ 139 | Renders the methods section of the web interface. 140 | 141 | :return str: An HTML fragment. 142 | """ 143 | output = [] 144 | module = None 145 | for (element, path) in retrieveVisibleMethodCallbacks(): 146 | if element.module != module: 147 | if module is not None: 148 | output.append('') 149 | module = element.module 150 | output.append('

{}

'.format(element.module)) 151 | output.append('
') 152 | output.append('{}
'.format( 153 | path, 154 | element.confirm and ' onclick="return confirm(\'"{} | {}" requested that you confirm your intent to proceed\');"'.format( 155 | element.module, 156 | element.name, 157 | ) or '', 158 | element.name, 159 | )) 160 | else: 161 | if module is not None: 162 | output.append('
') 163 | return '\n'.join(output) 164 | 165 | def _renderTemplate(elements, path, queryargs, mimetype, data, headers, rewrite_location=False): 166 | """ 167 | Renders the web interface. 168 | 169 | :param elements: The elements to render. 170 | :param basestring path: The requested path. 171 | :param dict queryargs: All query arguments. 172 | :param basestring mimetype: The MIME-type of any accompanying data. 173 | :param str data: Any data uploaded by the client. 174 | :param headers: All HTTP headers. 175 | :param bool rewrite_location: Whether the URI should be rewritten to point 176 | at the dashboard. 177 | 178 | :return str: An HTML fragment. 179 | """ 180 | return ('application/xhtml+xml; charset=utf-8', 181 | """ 183 | 184 | {} 185 | 186 |
187 | 188 |
{}
189 |
{}
190 | 191 |
192 | 193 | 194 | """.format( 195 | _renderHeaders(path, queryargs, mimetype, data, headers), 196 | rewrite_location and ' onload="rewriteLocation(\'/\');"' or '', 197 | _renderHeader(), 198 | _renderMethods(), 199 | _renderMain(elements, path, queryargs, mimetype, data, headers), 200 | _renderFooter(), 201 | )) 202 | 203 | def renderTemplate(path, queryargs, mimetype, data, headers, element): 204 | """ 205 | Renders a single-element view. 206 | 207 | :param basestring path: The requested path. 208 | :param dict queryargs: All query arguments. 209 | :param basestring mimetype: The MIME-type of any accompanying data. 210 | :param str data: Any data uploaded by the client. 211 | :param headers: All HTTP headers. 212 | :param :class:`WebMethod ` element: The element to render. 213 | 214 | :return str: An HTML fragment. 215 | """ 216 | return _renderTemplate((element,), path=path, queryargs=queryargs, mimetype=mimetype, data=data, headers=headers) 217 | 218 | def renderDashboard(path, queryargs, mimetype, data, headers, featured_element=None): 219 | """ 220 | Renders the dashboard view. 221 | 222 | :param basestring path: The requested path. 223 | :param dict queryargs: All query arguments. 224 | :param basestring mimetype: The MIME-type of any accompanying data. 225 | :param str data: Any data uploaded by the client. 226 | :param headers: All HTTP headers. 227 | :param :class:`WebMethod ` featured_element: The element to 228 | present at the start of the dashboard. 229 | 230 | :return str: An HTML fragment. 231 | """ 232 | elements = retrieveDashboardCallbacks() 233 | if featured_element: 234 | elements = [featured_element, None] + list(elements) 235 | 236 | return _renderTemplate(elements, path, queryargs, mimetype, data, headers, rewrite_location=bool(featured_element)) 237 | 238 | -------------------------------------------------------------------------------- /staticDHCPd/staticdhcpdlib/web/functions.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | """ 3 | staticdhcpdlib.web.functions 4 | ============================ 5 | Provides functions required to transform content for web-presentation. 6 | 7 | Legal 8 | ----- 9 | This file is part of staticDHCPd. 10 | staticDHCPd is free software; you can redistribute it and/or modify 11 | it under the terms of the GNU General Public License as published by 12 | the Free Software Foundation; either version 3 of the License, or 13 | (at your option) any later version. 14 | 15 | This program is distributed in the hope that it will be useful, 16 | but WITHOUT ANY WARRANTY; without even the implied warranty of 17 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 18 | GNU General Public License for more details. 19 | 20 | You should have received a copy of the GNU General Public License 21 | along with this program. If not, see . 22 | 23 | (C) Neil Tallim, 2021 24 | """ 25 | import html 26 | 27 | def sanitise(string): 28 | """ 29 | Ensures that the string is usable anywhere in an HTML5 body. 30 | 31 | :param basestring string: The string to sanitise. 32 | :return basestring: The sanitised string, or None if nothing was provided. 33 | """ 34 | return string and html.escape(string).replace('"', '"') 35 | 36 | -------------------------------------------------------------------------------- /staticDHCPd/staticdhcpdlib/web/headers.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | """ 3 | staticdhcpdlib.web.headers 4 | ========================== 5 | Provides implementations of the default elements. 6 | 7 | Legal 8 | ----- 9 | This file is part of staticDHCPd. 10 | staticDHCPd is free software; you can redistribute it and/or modify 11 | it under the terms of the GNU General Public License as published by 12 | the Free Software Foundation; either version 3 of the License, or 13 | (at your option) any later version. 14 | 15 | This program is distributed in the hope that it will be useful, 16 | but WITHOUT ANY WARRANTY; without even the implied warranty of 17 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 18 | GNU General Public License for more details. 19 | 20 | You should have received a copy of the GNU General Public License 21 | along with this program. If not, see . 22 | 23 | (C) Neil Tallim, 2021 24 | """ 25 | import logging 26 | 27 | from .. import config 28 | from . import functions 29 | 30 | _logger = logging.getLogger('web.headers') 31 | 32 | def contentType(*args, **kwargs): 33 | """ 34 | Provides the default content-type HTML header. 35 | 36 | :return str: The content-type header. 37 | """ 38 | return '' 39 | 40 | _TITLE = '' + functions.sanitise(config.SYSTEM_NAME) + '' #: The title of the web interface 41 | def title(*args, **kwargs): 42 | """ 43 | Provides the default title HTML header. 44 | 45 | :return str: The title header. 46 | """ 47 | return _TITLE 48 | 49 | def css(*args, **kwargs): 50 | """ 51 | Provides the default CSS HTML header. 52 | 53 | :return str: The CSS header. 54 | """ 55 | return '' 56 | 57 | def favicon(*args, **kwargs): 58 | """ 59 | Provides the default favicon HTML header. 60 | 61 | :return str: The favicon header. 62 | """ 63 | return '' 64 | 65 | def javascript(*args, **kwargs): 66 | """ 67 | Provides the default JavaScript HTML header. 68 | 69 | :return str: The JavaScript header. 70 | """ 71 | return '' 72 | 73 | -------------------------------------------------------------------------------- /staticDHCPd/staticdhcpdlib/web/methods.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | """ 3 | statidhcpdlib.web.methods 4 | ========================= 5 | Provides implementations of several helpful web-components. 6 | 7 | Legal 8 | ----- 9 | This file is part of staticDHCPd. 10 | staticDHCPd is free software; you can redistribute it and/or modify 11 | it under the terms of the GNU General Public License as published by 12 | the Free Software Foundation; either version 3 of the License, or 13 | (at your option) any later version. 14 | 15 | This program is distributed in the hope that it will be useful, 16 | but WITHOUT ANY WARRANTY; without even the implied warranty of 17 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 18 | GNU General Public License for more details. 19 | 20 | You should have received a copy of the GNU General Public License 21 | along with this program. If not, see . 22 | 23 | (C) Neil Tallim, 2021 24 | """ 25 | import logging 26 | 27 | from .. import config 28 | from .. import logging_handlers 29 | from ..import system 30 | from . import functions 31 | from . import _resources 32 | 33 | _logger = logging.getLogger('web.methods') 34 | 35 | _SEVERITY_MAP = { 36 | logging.DEBUG: 'debug', 37 | logging.INFO: 'info', 38 | logging.WARN: 'warn', 39 | logging.ERROR: 'error', 40 | logging.CRITICAL: 'critical', 41 | } #: Mappings to show logging data in the web interface 42 | 43 | class Logger(object): 44 | """ 45 | A logging mechanism to provide a web-interface view of what's going on in 46 | the system. 47 | """ 48 | _handler = None #: The loging handler 49 | 50 | def __init__(self): 51 | """ 52 | Initialises and registers the logging handler. 53 | """ 54 | _logger.info("Configuring web-accessible logging...") 55 | self._handler = logging_handlers.FIFOHandler(config.WEB_LOG_HISTORY) 56 | self._handler.setLevel(getattr(logging, config.WEB_LOG_SEVERITY)) 57 | if config.DEBUG: 58 | self._handler.setFormatter(logging.Formatter("%(asctime)s : %(levelname)s : %(name)s : %(message)s")) 59 | else: 60 | self._handler.setFormatter(logging.Formatter("%(asctime)s : %(message)s")) 61 | _logger.root.addHandler(self._handler) 62 | _logger.info("Web-accessible logging online; buffer-size={}".format(config.WEB_LOG_HISTORY)) 63 | 64 | def render(self, *args, **kwargs): 65 | """ 66 | Generates a view of the current log. 67 | 68 | :return str: An HTML fragment, containing the log. 69 | """ 70 | max_height = config.WEB_LOG_MAX_HEIGHT and 'max-height:{}px;'.format(config.WEB_LOG_MAX_HEIGHT) 71 | 72 | global _SEVERITY_MAP 73 | output = [] 74 | for (severity, line) in self._handler.readContents(): 75 | output.append('{}'.format(_SEVERITY_MAP[severity], functions.sanitise(line).replace('\n', '
'))) 76 | return "
{}
".format(max_height, '
\n'.join(output)) 77 | 78 | def reinitialise(*args, **kwargs): 79 | """ 80 | Runs the reinitialisation sequence over the system. 81 | 82 | :return str: An HTML fragment, describing the result. 83 | """ 84 | try: 85 | time_elapsed = system.reinitialise() 86 | except Exception as e: 87 | return 'Reinitilisation failed: {}'.format(e) 88 | else: 89 | return 'System reinitilisation completed in {:.4f} seconds'.format(time_elapsed) 90 | 91 | def css(*args, **kwargs): 92 | """ 93 | Produces the default CSS. 94 | 95 | :return str: The CSS byte-string. 96 | """ 97 | return ('text/css', _resources.CSS) 98 | 99 | def javascript(*args, **kwargs): 100 | """ 101 | Produces the default JavaScript. 102 | 103 | :return str: The JavaScript byte-string. 104 | """ 105 | return ('text/javascript', _resources.JS) 106 | 107 | def favicon(*args, **kwargs): 108 | """ 109 | Produces the default favicon. 110 | 111 | :return str: The favicon byte-string. 112 | """ 113 | return ('image/x-icon', _resources.FAVICON) 114 | 115 | --------------------------------------------------------------------------------