diff --git a/board/common/nginx.conf b/board/common/nginx.conf new file mode 100644 index 00000000..06f1eddd --- /dev/null +++ b/board/common/nginx.conf @@ -0,0 +1,76 @@ +worker_processes 1; + +events { + worker_connections 1024; +} + +http { + include mime.types; + default_type application/octet-stream; + + sendfile on; + #tcp_nopush on; + + #keepalive_timeout 0; + keepalive_timeout 65; + + #gzip on; + + server { + listen 80; + listen [::]:80; + server_name _; + + location / { + root html; + index index.html index.htm; + } + + #error_page 404 /404.html; + + # redirect server error pages to the static page /50x.html + # + error_page 500 502 503 504 /50x.html; + location = /50x.html { + root html; + } + + location /browse { + include fastcgi_params; + fastcgi_pass unix:/tmp/netbrowse.sock; + } + } + + server { + listen 80; + listen [::]:80; + server_name network.local; + + location / { + include fastcgi_params; + fastcgi_pass unix:/tmp/netbrowse.sock; + } + } + + # + # HTTPS server + # + # server { + # listen 443 ssl; + # server_name localhost; + + # ssl_certificate cert.pem; + # ssl_certificate_key cert.key; + + # ssl_session_cache shared:SSL:1m; + # ssl_session_timeout 5m; + + # ssl_ciphers HIGH:!aNULL:!MD5; + # ssl_prefer_server_ciphers on; + + # location / { + # root html; + # index index.html index.htm; + # } + # } +} diff --git a/board/common/post-build.sh b/board/common/post-build.sh index bb9bc62e..f8e3be73 100755 --- a/board/common/post-build.sh +++ b/board/common/post-build.sh @@ -1,5 +1,6 @@ #!/bin/sh # shellcheck disable=SC1090,SC1091 +common=$(dirname "$(readlink -f "$0")") . "$BR2_CONFIG" 2>/dev/null . "$TARGET_DIR/usr/lib/os-release" @@ -79,6 +80,37 @@ grep -qsE '^/bin/true$$' "$TARGET_DIR/etc/shells" \ || echo "/bin/true" >> "$TARGET_DIR/etc/shells" grep -qsE '^/bin/false$$' "$TARGET_DIR/etc/shells" \ || echo "/bin/false" >> "$TARGET_DIR/etc/shells" + # Allow clish (symlink to /usr/bin/klish) to be a login shell grep -qsE '^/bin/clish$$' "$TARGET_DIR/etc/shells" \ || echo "/bin/clish" >> "$TARGET_DIR/etc/shells" + +if [ -n "$BR2_PACKAGE_NGINX" ]; then + cp "$common/nginx.conf" "$TARGET_DIR/etc/nginx/" + ln -sf ../available/nginx.conf "$TARGET_DIR/etc/finit.d/enabled/nginx.conf" + + cat < "$TARGET_DIR/etc/avahi/services/http.service" + + + + %h + + _http._tcp + 80 + product=$INFIX_NAME + + +EOF + cat < "$TARGET_DIR/etc/avahi/services/https.service" + + + + %h + + _https._tcp + 443 + product=$INFIX_NAME + + +EOF +fi diff --git a/configs/aarch64_defconfig b/configs/aarch64_defconfig index 13779ef2..87603cf8 100644 --- a/configs/aarch64_defconfig +++ b/configs/aarch64_defconfig @@ -80,6 +80,9 @@ BR2_PACKAGE_NETCALC=y BR2_PACKAGE_NETCAT_OPENBSD=y BR2_PACKAGE_NETSNMP=y BR2_PACKAGE_NFTABLES=y +BR2_PACKAGE_NGINX=y +BR2_PACKAGE_NGINX_HTTP_SSL_MODULE=y +BR2_PACKAGE_NGINX_HTTP_V2_MODULE=y BR2_PACKAGE_NMAP=y BR2_PACKAGE_NMAP_NCAT=y BR2_PACKAGE_NMAP_NMAP=y @@ -137,12 +140,13 @@ BR2_PACKAGE_K8S_LOGGER=y BR2_PACKAGE_KEYACK=y BR2_PACKAGE_KLISH_PLUGIN_INFIX=y BR2_PACKAGE_LOWDOWN=y +BR2_PACKAGE_MCD=y +BR2_PACKAGE_NETBROWSE=y BR2_PACKAGE_PODMAN=y BR2_PACKAGE_PODMAN_DRIVER_BTRFS=y BR2_PACKAGE_PODMAN_DRIVER_DEVICEMAPPER=y BR2_PACKAGE_PODMAN_DRIVER_VFS=y BR2_PACKAGE_TETRIS=y -BR2_PACKAGE_MCD=y BR2_PACKAGE_LIBINPUT=y DISK_IMAGE_BOOT_BIN=y GNS3_APPLIANCE_RAM=512 diff --git a/configs/r2s_defconfig b/configs/r2s_defconfig index 6bb90b0a..9b238ffc 100644 --- a/configs/r2s_defconfig +++ b/configs/r2s_defconfig @@ -63,6 +63,7 @@ BR2_PACKAGE_UBOOT_TOOLS_FIT_SIGNATURE_SUPPORT=y BR2_PACKAGE_UBOOT_TOOLS_FIT_CHECK_SIGN=y BR2_PACKAGE_UBOOT_TOOLS_MKENVIMAGE=y BR2_PACKAGE_PYTHON3=y +BR2_PACKAGE_PYTHON_CRYPTOGRAPHY=y BR2_PACKAGE_CA_CERTIFICATES=y BR2_PACKAGE_LIBSSH_OPENSSL=y BR2_PACKAGE_LIBSSH2=y @@ -91,6 +92,9 @@ BR2_PACKAGE_NETCALC=y BR2_PACKAGE_NETCAT_OPENBSD=y BR2_PACKAGE_NETSNMP=y BR2_PACKAGE_NFTABLES=y +BR2_PACKAGE_NGINX=y +BR2_PACKAGE_NGINX_HTTP_SSL_MODULE=y +BR2_PACKAGE_NGINX_HTTP_V2_MODULE=y BR2_PACKAGE_NMAP=y BR2_PACKAGE_NMAP_NCAT=y BR2_PACKAGE_NMAP_NMAP=y @@ -165,12 +169,13 @@ BR2_PACKAGE_K8S_LOGGER=y BR2_PACKAGE_KEYACK=y BR2_PACKAGE_KLISH_PLUGIN_INFIX=y BR2_PACKAGE_LOWDOWN=y +BR2_PACKAGE_MCD=y +BR2_PACKAGE_NETBROWSE=y BR2_PACKAGE_PODMAN=y BR2_PACKAGE_PODMAN_DRIVER_BTRFS=y BR2_PACKAGE_PODMAN_DRIVER_DEVICEMAPPER=y BR2_PACKAGE_PODMAN_DRIVER_VFS=y BR2_PACKAGE_TETRIS=y -BR2_PACKAGE_MCD=y BR2_PACKAGE_LIBINPUT=y # SIGN_ENABLED is not set # GNS3_APPLIANCE is not set diff --git a/configs/x86_64_defconfig b/configs/x86_64_defconfig index ce8173c2..abec241b 100644 --- a/configs/x86_64_defconfig +++ b/configs/x86_64_defconfig @@ -75,6 +75,9 @@ BR2_PACKAGE_NETCALC=y BR2_PACKAGE_NETCAT_OPENBSD=y BR2_PACKAGE_NETSNMP=y BR2_PACKAGE_NFTABLES=y +BR2_PACKAGE_NGINX=y +BR2_PACKAGE_NGINX_HTTP_SSL_MODULE=y +BR2_PACKAGE_NGINX_HTTP_V2_MODULE=y BR2_PACKAGE_NMAP=y BR2_PACKAGE_NMAP_NCAT=y BR2_PACKAGE_NMAP_NMAP=y @@ -140,11 +143,12 @@ BR2_PACKAGE_K8S_LOGGER=y BR2_PACKAGE_KEYACK=y BR2_PACKAGE_KLISH_PLUGIN_INFIX=y BR2_PACKAGE_LOWDOWN=y +BR2_PACKAGE_MCD=y +BR2_PACKAGE_NETBROWSE=y BR2_PACKAGE_PODMAN=y BR2_PACKAGE_PODMAN_DRIVER_BTRFS=y BR2_PACKAGE_PODMAN_DRIVER_DEVICEMAPPER=y BR2_PACKAGE_PODMAN_DRIVER_VFS=y BR2_PACKAGE_TETRIS=y -BR2_PACKAGE_MCD=y GNS3_APPLIANCE_RAM=512 GNS3_APPLIANCE_IFNUM=10 diff --git a/package/Config.in b/package/Config.in index cd3aa802..6b1db6ba 100644 --- a/package/Config.in +++ b/package/Config.in @@ -15,6 +15,7 @@ source "$BR2_EXTERNAL_INFIX_PATH/package/klish-plugin-sysrepo/Config.in" source "$BR2_EXTERNAL_INFIX_PATH/package/libsrx/Config.in" source "$BR2_EXTERNAL_INFIX_PATH/package/lowdown/Config.in" source "$BR2_EXTERNAL_INFIX_PATH/package/mcd/Config.in" +source "$BR2_EXTERNAL_INFIX_PATH/package/netbrowse/Config.in" source "$BR2_EXTERNAL_INFIX_PATH/package/podman/Config.in" source "$BR2_EXTERNAL_INFIX_PATH/package/skeleton-init-finit/Config.in" source "$BR2_EXTERNAL_INFIX_PATH/package/tetris/Config.in" diff --git a/package/netbrowse/Config.in b/package/netbrowse/Config.in new file mode 100644 index 00000000..d50befce --- /dev/null +++ b/package/netbrowse/Config.in @@ -0,0 +1,10 @@ +config BR2_PACKAGE_NETBROWSE + bool netbrowse + select BR2_PACKAGE_DBUS + select BR2_PACKAGE_DBUS_PYTHON + select BR2_PACKAGE_PYTHON_FLUP + help + Advertises the initial $(HOSTNAME).local and network.local to + give your device a http://network.local mDNS/SD browser. + + UNIX socket available for fastcgi at /tmp/netbrowse.sock diff --git a/package/netbrowse/netbrowse.mk b/package/netbrowse/netbrowse.mk new file mode 100644 index 00000000..c2900c27 --- /dev/null +++ b/package/netbrowse/netbrowse.mk @@ -0,0 +1,25 @@ +################################################################################ +# +# netbrowse +# +################################################################################ + +NETBROWSE_VERSION = 1.0 +NETBROWSE_SITE_METHOD = local +NETBROWSE_SITE = $(BR2_EXTERNAL_INFIX_PATH)/src/netbrowse +NETBROWSE_SETUP_TYPE = setuptools +NETBROWSE_LICENSE = MIT +NETBROWSE_LICENSE_FILES = LICENSE +NETBROWSE_REDISTRIBUTE = NO + +define NETBROWSE_INSTALL_EXTRA + $(INSTALL) -D -m 0644 $(NETBROWSE_PKGDIR)/netbrowse.svc \ + $(FINIT_D)/available/netbrowse.conf + ln -sf ../available/netbrowse.conf $(FINIT_D)/enabled/netbrowse.conf + $(INSTALL) -d -m 755 $(TARGET_DIR)/etc/default + echo "NETBROWSE_ARGS=\"network.local $(BR2_TARGET_GENERIC_HOSTNAME).local\"" \ + > $(TARGET_DIR)/etc/default/netbrowse +endef +NETBROWSE_POST_INSTALL_TARGET_HOOKS += NETBROWSE_INSTALL_EXTRA + +$(eval $(python-package)) diff --git a/package/netbrowse/netbrowse.svc b/package/netbrowse/netbrowse.svc new file mode 100644 index 00000000..002b7f84 --- /dev/null +++ b/package/netbrowse/netbrowse.svc @@ -0,0 +1,3 @@ +service env:-/etc/default/netbrowse log:prio:daemon.debug,tag:mdns \ + @www-data:www-data \ + [2345] netbrowse $NETBROWSE_ARGS -- Network browser diff --git a/src/netbrowse/.gitignore b/src/netbrowse/.gitignore new file mode 100644 index 00000000..ba0430d2 --- /dev/null +++ b/src/netbrowse/.gitignore @@ -0,0 +1 @@ +__pycache__/ \ No newline at end of file diff --git a/src/netbrowse/LICENSE b/src/netbrowse/LICENSE new file mode 100644 index 00000000..b67cb375 --- /dev/null +++ b/src/netbrowse/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Joachim Wiberg + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/src/netbrowse/README.md b/src/netbrowse/README.md new file mode 100644 index 00000000..c15a5233 --- /dev/null +++ b/src/netbrowse/README.md @@ -0,0 +1,38 @@ +mDNS Network Browser +==================== + +This Python program both advertises CNAMEs (using D-Bus to Avahi) and +provides an mDNS browser over fastcgi for, e.g., Nginx. + +To start the program: + + netbrowse $hostname.local network.local + +A UNIX socket, for fastcgi, is created in `/tmp/netbrowse.sock` with +permissions 0660 as the user and group the program is started as. + +When using Finit this can be achieved with + + service @www-data:www-data netbrowse network.local + +In your Nginx server configuration, add: + + location /browse { + include fastcgi_params; + fastcgi_pass unix:/tmp/netbrowse.sock; + } + +For more a elaborate setup, you can have another server block: + +``` +server { + listen 80; + listen [::]:80; + server_name network.local; + + location / { + include fastcgi_params; + fastcgi_pass unix:/tmp/netbrowse.sock; + } +} +``` diff --git a/src/netbrowse/avahi_alias/__init__.py b/src/netbrowse/avahi_alias/__init__.py new file mode 100644 index 00000000..8ba9476b --- /dev/null +++ b/src/netbrowse/avahi_alias/__init__.py @@ -0,0 +1,6 @@ +""" + Avahi CNAME service over D-Bus advertiser +""" + +from .avahi_alias import AvahiAlias +__all__ = ['AvahiAlias'] diff --git a/src/netbrowse/avahi_alias/avahi_alias.py b/src/netbrowse/avahi_alias/avahi_alias.py new file mode 100644 index 00000000..c86ec061 --- /dev/null +++ b/src/netbrowse/avahi_alias/avahi_alias.py @@ -0,0 +1,56 @@ +"""Avahi CNAME alias class""" + +from encodings.idna import ToASCII +import dbus + +class AvahiAlias: + """Import to your project and use publish_cname() for each name.""" + DBUS_NAME = 'org.freedesktop.Avahi' + CLASS_IN = 0x01 + TYPE_CNAME = 0x05 + TTL = 60 + DBUS_PATH_SERVER = '/' + DBUS_INTERFACE_SERVER = DBUS_NAME + '.Server' + DBUS_INTERFACE_ENTRY_GROUP = DBUS_NAME + '.EntryGroup' + IF_UNSPEC = -1 + PROTO_UNSPEC = -1 + + def publish_cname(self, cname): + """Call this method for each alias to publish as a CNAME record.""" + bus = dbus.SystemBus() + server = dbus.Interface(bus.get_object(self.DBUS_NAME, self.DBUS_PATH_SERVER), + self.DBUS_INTERFACE_SERVER) + group = dbus.Interface(bus.get_object(self.DBUS_NAME, server.EntryGroupNew()), + self.DBUS_INTERFACE_ENTRY_GROUP) + + rdata = self.create_rr(server.GetHostNameFqdn()) + cname = self.encode_dns(cname) + + group.AddRecord(self.IF_UNSPEC, self.PROTO_UNSPEC, dbus.UInt32(0), + cname, self.CLASS_IN, self.TYPE_CNAME, self.TTL, rdata) + group.Commit() + print("Published " + cname.decode()) + + @staticmethod + def encode_dns(name): + """Internal""" + out = [] + name = name.decode() + for part in str(name).split('.'): + if len(part) == 0: + continue + out.append(ToASCII(part)) + return b'.'.join(out) + + @staticmethod + def create_rr(name): + """Internal""" + out = [] + for part in name.split('.'): + if len(part) == 0: + continue + out.append(chr(len(part)).encode()) + out.append(ToASCII(part)) + out.append(b'\0') + + return b''.join(out) diff --git a/src/netbrowse/mdns_hosts/__init__.py b/src/netbrowse/mdns_hosts/__init__.py new file mode 100644 index 00000000..d9b98056 --- /dev/null +++ b/src/netbrowse/mdns_hosts/__init__.py @@ -0,0 +1,6 @@ +""" + Very basic mDNS scanner with HTML table renderer +""" + +from .mdns_hosts import MdnsHosts +__all__ = ['MdnsHosts'] diff --git a/src/netbrowse/mdns_hosts/mdns_hosts.py b/src/netbrowse/mdns_hosts/mdns_hosts.py new file mode 100644 index 00000000..be08604d --- /dev/null +++ b/src/netbrowse/mdns_hosts/mdns_hosts.py @@ -0,0 +1,152 @@ +""" + Very basic mDNS scanner with HTML table renderer +""" +import subprocess + +class MdnsHosts: + """mDNS scanner class using avahi-browse""" + def hask(self): + """Check if avahi-browse has -k option""" + try: + result = subprocess.run(['avahi-browse', '--help'], check=True, + stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) + return '-k' in result.stdout + except subprocess.CalledProcessError: + return False + + def scan(self): + """Perform mDNS scan and return list of hosts.""" + services = { + '_daap._tcp': ('iTunes Server', None), + '_device-info._tcp': ('Device info', None), + '_http._tcp': ('HTTP', 'http://{address}'), + '_https._tcp': ('HTTPS', 'https://{address}'), + '_ipp._tcp': ('Printer', None), + '_ipps._tcp': ('Printer', None), + '_netconf-ssh._tcp': ('NETCONF', None), + '_raop._tcp': ('AirPlay ', None), + '_ssh._tcp': ('SSH', None), + '_sftp-ssh._tcp': ('SFTP', None), + '_workstation._tcp': ('Workstation', None), + } + + result = subprocess.run(['avahi-browse', '-tarpk' if self.hask() else '-tarp'], + stdout=subprocess.PIPE, text=True) + lines = result.stdout.strip().split('\n') + hosts_services = {} # Key: address, Value: list of dicts with service details + + for line in lines: + print(f"{line}") + parts = line.split(';') + if len(parts) <= 8 or parts[0] != '=': + continue + if (parts[2] != 'IPv4' and parts[2] != 'IPv6'): + continue + + service_type = parts[4] + if service_type not in services: + continue + + service_name = parts[3] + address = parts[7] + identifier, url_template = services[service_type] + url = url_template.format(address=address) if url_template else None + + service_details = { + 'name': self.decode(service_name), + 'type': identifier, + 'link': parts[6], + 'url': url + } + if address not in hosts_services: + hosts_services[address] = {'services': [service_details], 'type': parts[2]} + else: + hosts_services[address]['services'].append(service_details) + + return hosts_services + + def decode(self, name): + """Decode escape sequences like \032 and \040 in service names""" + name = name.replace('\\032', ' ') + name = name.replace('\\040', '(') + name = name.replace('\\041', ')') + return bytes(name, "utf-8").decode("unicode_escape") + + def html(self): + """Generate a HTML table of the mDNS scan results""" + hosts_services = self.scan() + html_content = """ + + + mDNS Hosts - Services + + + +
+ + + + """ + + for address, info in hosts_services.items(): + for service in info['services']: + service_name = service['name'] + service_type = service['type'] + link = service['link'] + url = service['url'] + if url: + url = url.replace("127.0.0.1", link) + link_html = f'{link}' + else: + link_html = f'{link}' + + html_content += f"" + html_content += f"\n" + + html_content += """ +
IP AddressService NameLinkService Type
{address}{service_name}{link_html}{service_type}
+
+ + + + """ + return html_content diff --git a/src/netbrowse/netbrowse b/src/netbrowse/netbrowse new file mode 100755 index 00000000..529384b5 --- /dev/null +++ b/src/netbrowse/netbrowse @@ -0,0 +1,47 @@ +#!/usr/bin/env python3 +""" + Main server loop. +""" +import os +import signal +import sys +from flup.server.fcgi import WSGIServer + +from avahi_alias import AvahiAlias +from mdns_hosts import MdnsHosts + +SOCK_FILE='/tmp/netbrowse.sock' + +def app(_, start_response): + """The /browse network.local application""" + mdns_hosts = MdnsHosts() + content = mdns_hosts.html().encode('utf-8') + + start_response('200 OK', [('Content-Type', 'text/html'), ('Content-Length', str(len(content)))]) + return [content] + +def cleanup(): + """Clean up on server shutdown""" + try: + os.remove(SOCK_FILE) + except OSError: + pass + sys.exit(0) + +def __sigcb(_, __): + cleanup() + +if __name__ == '__main__': + avahi_aliases = AvahiAlias() + for each in sys.argv[1:]: + avahi_aliases.publish_cname(str(each).encode("utf-8", "strict")) + + signal.signal(signal.SIGINT, __sigcb) + signal.signal(signal.SIGTERM, __sigcb) + + try: + # For testing on PC as $USER, add: , umask=0 + WSGIServer(app, bindAddress=SOCK_FILE).run() + except KeyboardInterrupt: + cleanup() + print("Exiting") diff --git a/src/netbrowse/setup.cfg b/src/netbrowse/setup.cfg new file mode 100644 index 00000000..488cd068 --- /dev/null +++ b/src/netbrowse/setup.cfg @@ -0,0 +1,5 @@ +[metadata] +name = Network Browser +description = An mDNS CNAME advertiser over D-Bus and mDNS Browser (fcgi) +version = 1.0 +license_files = LICENSE diff --git a/src/netbrowse/setup.py b/src/netbrowse/setup.py new file mode 100644 index 00000000..0403ad61 --- /dev/null +++ b/src/netbrowse/setup.py @@ -0,0 +1,10 @@ +from setuptools import setup, find_packages + +setup( + packages=find_packages(), + scripts=['netbrowse'], + install_requires=[ + 'dbus-python', + 'flup' + ], +)