├── VERSION ├── .github ├── CODEOWNERS └── workflows │ ├── build.yml │ ├── publish.yml │ └── check.yml ├── installers ├── freebsd │ ├── pre │ ├── post_install │ ├── post_deinstall │ ├── make-toolchain.py │ └── build.py ├── mac │ ├── wrapper │ │ ├── Assets.xcassets │ │ │ ├── Contents.json │ │ │ ├── AppIcon.appiconset │ │ │ │ ├── icon_16x16.png │ │ │ │ ├── icon_32x32.png │ │ │ │ ├── icon_128x128.png │ │ │ │ ├── icon_16x16@2x.png │ │ │ │ ├── icon_256x256.png │ │ │ │ ├── icon_32x32@2x.png │ │ │ │ ├── icon_128x128@2x.png │ │ │ │ ├── icon_256x256@2x.png │ │ │ │ └── Contents.json │ │ │ └── AccentColor.colorset │ │ │ │ └── Contents.json │ │ ├── main.mm │ │ ├── Info.template.plist │ │ └── CMakeLists.txt │ ├── scripts │ │ ├── postinstall │ │ └── preinstall │ ├── set-github-keychain │ ├── html │ │ ├── conclusion.html │ │ ├── welcome.html │ │ └── license.html │ ├── wsddn-uninstall │ ├── notarize │ ├── distribution.xml │ └── build.py ├── deb │ ├── preinst │ ├── prerm │ ├── postrm │ ├── postinst │ ├── copyright │ └── build.py ├── docker │ ├── Dockerfile │ └── run-wsddn ├── common.py ├── openbsd │ └── build.py └── wsddn.conf ├── .gitignore ├── src ├── exc_handling.h ├── Info.template.plist ├── http_request.h ├── interface_monitor.h ├── http_server.h ├── udp_server.h ├── http_response.h ├── wsd_server.h ├── command_line.h ├── server_manager.h ├── server_manager.cpp ├── app_state.h ├── http_request.cpp ├── sys_config.h.in ├── config_mac.cpp ├── pid_file.h ├── config.h ├── http_request_parser.h ├── exc_handling.cpp ├── pch.h ├── sys_util.cpp ├── util.h ├── config.cpp ├── sys_socket.h ├── sys_util.h ├── http_response.cpp ├── main.cpp └── http_request_parser.cpp ├── config ├── openrc │ └── etc │ │ ├── logrotate.d │ │ └── wsddn │ │ └── init.d │ │ └── wsddn ├── openbsd │ └── etc │ │ └── rc.d │ │ └── wsddn ├── firewalls │ └── etc │ │ ├── ufw │ │ └── applications.d │ │ │ └── wsddn │ │ └── firewalld │ │ └── services │ │ ├── wsddn-http.xml │ │ └── wsddn.xml ├── freebsd │ └── usr │ │ └── local │ │ └── etc │ │ ├── newsyslog.conf.d │ │ └── wsddn.conf │ │ └── rc.d │ │ └── wsddn ├── systemd │ └── usr │ │ └── lib │ │ └── systemd │ │ └── system │ │ └── wsddn.service ├── mac │ └── Library │ │ └── LaunchDaemons │ │ └── io.github.gershnik.wsddn.plist ├── metadata │ ├── default.xml │ ├── other.xml │ └── README.md └── sysv │ └── etc │ └── init.d │ └── wsddn ├── tools ├── uncache ├── create-release └── hashdeps ├── cmake ├── install.cmake ├── dependencies.cmake └── detect_system.cmake ├── SECURITY.md ├── LICENSE └── dependencies.json /VERSION: -------------------------------------------------------------------------------- 1 | 1.22 2 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | /** @gershnik 2 | 3 | -------------------------------------------------------------------------------- /installers/freebsd/pre: -------------------------------------------------------------------------------- 1 | #! /bin/sh 2 | 3 | if service wsddn status > /dev/null 2>&1; then 4 | service wsddn stop 5 | fi 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | out/ 2 | __pycache__/ 3 | .vscode/ 4 | .devcontainer/ 5 | 6 | LLDBInitFile 7 | .DS_Store 8 | env.cmake 9 | 10 | -------------------------------------------------------------------------------- /installers/mac/wrapper/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /installers/freebsd/post_install: -------------------------------------------------------------------------------- 1 | #! /bin/sh 2 | 3 | set -e 4 | 5 | CONF=/usr/local/etc/wsddn.conf 6 | if [ ! -f "$CONF" ]; then 7 | cp $CONF.sample $CONF 8 | fi 9 | -------------------------------------------------------------------------------- /src/exc_handling.h: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022, Eugene Gershnik 2 | // SPDX-License-Identifier: BSD-3-Clause 3 | 4 | auto formatCaughtExceptionBacktrace() -> std::string; 5 | 6 | -------------------------------------------------------------------------------- /installers/deb/preinst: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | 4 | if [ "$1" = "install" ] && [ -n "$2" ] && [ -e "/etc/init.d/wsddn" ] ; then 5 | chmod +x "/etc/init.d/wsddn" >/dev/null || true 6 | fi 7 | -------------------------------------------------------------------------------- /installers/mac/wrapper/Assets.xcassets/AppIcon.appiconset/icon_16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gershnik/wsdd-native/HEAD/installers/mac/wrapper/Assets.xcassets/AppIcon.appiconset/icon_16x16.png -------------------------------------------------------------------------------- /installers/mac/wrapper/Assets.xcassets/AppIcon.appiconset/icon_32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gershnik/wsdd-native/HEAD/installers/mac/wrapper/Assets.xcassets/AppIcon.appiconset/icon_32x32.png -------------------------------------------------------------------------------- /installers/mac/wrapper/Assets.xcassets/AppIcon.appiconset/icon_128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gershnik/wsdd-native/HEAD/installers/mac/wrapper/Assets.xcassets/AppIcon.appiconset/icon_128x128.png -------------------------------------------------------------------------------- /installers/mac/wrapper/Assets.xcassets/AppIcon.appiconset/icon_16x16@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gershnik/wsdd-native/HEAD/installers/mac/wrapper/Assets.xcassets/AppIcon.appiconset/icon_16x16@2x.png -------------------------------------------------------------------------------- /installers/mac/wrapper/Assets.xcassets/AppIcon.appiconset/icon_256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gershnik/wsdd-native/HEAD/installers/mac/wrapper/Assets.xcassets/AppIcon.appiconset/icon_256x256.png -------------------------------------------------------------------------------- /installers/mac/wrapper/Assets.xcassets/AppIcon.appiconset/icon_32x32@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gershnik/wsdd-native/HEAD/installers/mac/wrapper/Assets.xcassets/AppIcon.appiconset/icon_32x32@2x.png -------------------------------------------------------------------------------- /config/openrc/etc/logrotate.d/wsddn: -------------------------------------------------------------------------------- 1 | /var/log/wsddn.log { 2 | rotate 5 3 | size 100k 4 | sharedscripts 5 | postrotate 6 | kill -HUP `cat /var/wsddn/wsddn.pid` 7 | endscript 8 | } 9 | -------------------------------------------------------------------------------- /installers/mac/wrapper/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gershnik/wsdd-native/HEAD/installers/mac/wrapper/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x.png -------------------------------------------------------------------------------- /installers/mac/wrapper/Assets.xcassets/AppIcon.appiconset/icon_256x256@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gershnik/wsdd-native/HEAD/installers/mac/wrapper/Assets.xcassets/AppIcon.appiconset/icon_256x256@2x.png -------------------------------------------------------------------------------- /config/openbsd/etc/rc.d/wsddn: -------------------------------------------------------------------------------- 1 | #!/bin/ksh 2 | 3 | daemon="/usr/local/bin/wsddn --unixd" 4 | daemon_flags="--config /etc/wsddn/wsddn.conf --pid-file=/var/run/wsddn.pid --log-file=/var/log/wsddn.log" 5 | 6 | . /etc/rc.d/rc.subr 7 | 8 | rc_cmd $1 9 | -------------------------------------------------------------------------------- /installers/mac/wrapper/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /config/firewalls/etc/ufw/applications.d/wsddn: -------------------------------------------------------------------------------- 1 | [wsddn] 2 | title=WS-Discovery Host Daemon 3 | description=Allows your machine to be discovered by Windows 10 and above systems and displayed by their Explorer "Network" views. 4 | ports=3702/udp|5357/tcp 5 | -------------------------------------------------------------------------------- /config/freebsd/usr/local/etc/newsyslog.conf.d/wsddn.conf: -------------------------------------------------------------------------------- 1 | # logfilename [owner:group] mode count size when flags [/pid_file] [sig_num] 2 | /var/log/wsddn.log 644 5 1000 * J /var/run/wsddn/wsddn.pid 3 | -------------------------------------------------------------------------------- /installers/freebsd/post_deinstall: -------------------------------------------------------------------------------- 1 | #! /bin/sh 2 | 3 | rm -rf /var/run/wsddn 4 | rm -rf /var/log/wsddn.* 5 | 6 | if pw usershow wsddn > /dev/null 2>&1; then 7 | pw userdel wsddn 8 | fi 9 | 10 | if pw groupshow wsddn > /dev/null 2>&1; then 11 | pw groupdel wsddn 12 | fi 13 | 14 | -------------------------------------------------------------------------------- /installers/mac/scripts/postinstall: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | 3 | set -e 4 | 5 | '/Library/Application Support/wsdd-native/wsdd-native.app/Contents/MacOS/wsdd-native' || true 6 | 7 | CONF=/etc/wsddn.conf 8 | if [[ ! -f "$CONF" ]]; then 9 | cp $CONF.sample $CONF 10 | fi 11 | 12 | /bin/launchctl load -w "/Library/LaunchDaemons/io.github.gershnik.wsddn.plist" 13 | -------------------------------------------------------------------------------- /config/firewalls/etc/firewalld/services/wsddn-http.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | WS-Discovery Host Daemon (HTTP Interface) 4 | Allows your machine to be discovered by Windows 10 and above systems and displayed by their Explorer "Network" views. 5 | 6 | -------------------------------------------------------------------------------- /installers/deb/prerm: -------------------------------------------------------------------------------- 1 | #! /bin/sh 2 | 3 | set -e 4 | 5 | if [ -z "${DPKG_ROOT:-}" ] && [ "$1" = remove ] && [ -d /run/systemd/system ] ; then 6 | if [ -x "/usr/bin/deb-systemd-invoke" ]; then 7 | deb-systemd-invoke stop 'wsddn.service' >/dev/null || true 8 | fi 9 | fi 10 | 11 | if [ "$1" = remove ] ; then 12 | service wsddn stop >/dev/null || true 13 | fi 14 | 15 | exit 0 16 | -------------------------------------------------------------------------------- /config/systemd/usr/lib/systemd/system/wsddn.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=WS-Discovery Host 3 | Documentation=man:wsdd(8) 4 | After=network-online.target 5 | Wants=network-online.target 6 | 7 | [Service] 8 | Type=notify 9 | ExecStart=/usr/bin/wsddn --systemd --config=/etc/wsddn.conf 10 | ExecReload=/usr/bin/kill -HUP $MAINPID 11 | DynamicUser=yes 12 | User=wsdd 13 | Group=wsdd 14 | 15 | [Install] 16 | WantedBy=multi-user.target 17 | -------------------------------------------------------------------------------- /config/firewalls/etc/firewalld/services/wsddn.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | WS-Discovery Host Daemon 4 | Allows your machine to be discovered by Windows 10 and above systems and displayed by their Explorer "Network" views. 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /installers/mac/wrapper/main.mm: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023, Eugene Gershnik 2 | // SPDX-License-Identifier: BSD-3-Clause 3 | 4 | #import 5 | 6 | #include 7 | 8 | int main(int argc, char ** argv) { 9 | auto bundle = NSBundle.mainBundle; 10 | auto res = LSRegisterURL((__bridge CFURLRef)bundle.bundleURL, false); 11 | if (res != noErr) { 12 | fprintf(stderr, "LSRegisterURL failed: %d", res); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /installers/docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:latest 2 | 3 | RUN < /etc/apk/keys/gershnik@hotmail.com-6643812b.rsa.pub 8 | mkdir -p /etc/apk/repositories.d 9 | echo "https://www.gershnik.com/alpine-repo/main" \ 10 | > /etc/apk/repositories.d/www.gershnik.com.list 11 | apk del wget 12 | apk update 13 | apk --no-cache add wsdd-native 14 | 15 | EOF 16 | 17 | COPY run-wsddn ./ 18 | 19 | CMD [ "./run-wsddn"] 20 | -------------------------------------------------------------------------------- /config/freebsd/usr/local/etc/rc.d/wsddn: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # PROVIDE: wsddn 4 | # BEFORE: login 5 | # REQUIRE: DAEMON 6 | # KEYWORD: shutdown 7 | 8 | . /etc/rc.subr 9 | 10 | name=wsddn 11 | rcvar=${name}_enable 12 | 13 | export PATH=$PATH:/usr/local/sbin:/usr/local/bin 14 | 15 | command="/usr/local/bin/wsddn" 16 | command_args="--unixd --config=/usr/local/etc/wsddn.conf --pid-file=/var/run/${name}/${name}.pid --log-file=/var/log/wsddn.log" 17 | pidfile="/var/run/${name}/${name}.pid" 18 | extra_commands="reload" 19 | 20 | load_rc_config $name 21 | run_rc_command "$1" 22 | -------------------------------------------------------------------------------- /installers/docker/run-wsddn: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | if [ -z "${WSDDN_HOSTNAME}" ]; then 4 | echo "You must set WSDDN_HOSTNAME environment variable." 5 | exit 1 6 | fi 7 | 8 | echo "Starting wsdd-native" 9 | 10 | if [ ! -z "${WSDDN_WORKGROUP}" ]; then 11 | exec wsddn --user wsddn --hostname "${WSDDN_HOSTNAME}" --workgroup "${WSDDN_WORKGROUP}" 12 | elif [ ! -z "${WSDDN_DOMAIN}" ]; then 13 | exec wsddn --user wsddn --hostname "${WSDDN_HOSTNAME}" --domain "${WSDDN_DOMAIN}" 14 | else 15 | exec wsddn --user wsddn --hostname "${WSDDN_HOSTNAME}" 16 | fi 17 | -------------------------------------------------------------------------------- /config/openrc/etc/init.d/wsddn: -------------------------------------------------------------------------------- 1 | #!/sbin/openrc-run 2 | 3 | depend() { 4 | need net 5 | } 6 | 7 | name=wsddn 8 | description="WS-Discovery Host Daemon" 9 | pidfile="/var/run/${RC_SVCNAME}/${RC_SVCNAME}.pid" 10 | command="/usr/bin/wsddn" 11 | command_args="--user=wsddn:wsddn --config=/etc/wsddn.conf --pid-file=${pidfile} --log-file=/var/log/${RC_SVCNAME}.log" 12 | command_args_background="--unixd" 13 | extra_started_commands="reload" 14 | 15 | reload() { 16 | ebegin "Reloading ${RC_SVCNAME}" 17 | start-stop-daemon --signal HUP --pidfile "${pidfile}" 18 | eend $? 19 | } 20 | 21 | -------------------------------------------------------------------------------- /installers/mac/set-github-keychain: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | set -u 5 | 6 | echo $SIGN_CERTIFICATE | base64 --decode > certificate.p12 7 | security create-keychain -p $KEYCHAIN_PWD build.keychain 8 | security default-keychain -s build.keychain 9 | security unlock-keychain -p $KEYCHAIN_PWD build.keychain 10 | security set-keychain-settings -u 11 | security import certificate.p12 -k build.keychain -P $SIGN_CERTIFICATE_PWD -T /usr/bin/productsign -T /usr/bin/codesign 12 | security set-key-partition-list -S apple-tool:,apple:,productsign:,codesign: -s -k $KEYCHAIN_PWD build.keychain 13 | 14 | -------------------------------------------------------------------------------- /tools/uncache: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python3 2 | 3 | import argparse 4 | from pathlib import Path 5 | 6 | parser = argparse.ArgumentParser() 7 | parser.add_argument('builddir', type=Path) 8 | parser.add_argument('--dry-run', required=False, action='store_true', default=False, dest='dryRun') 9 | args = parser.parse_args() 10 | 11 | 12 | MYPATH = Path(__file__).parent 13 | ROOT = MYPATH.parent 14 | 15 | builddir: Path = args.builddir 16 | dryRun: bool = args.dryRun 17 | 18 | for cache in builddir.glob('**/CMakeCache.txt'): 19 | if not dryRun: 20 | cache.unlink() 21 | else: 22 | print(str(cache)) 23 | -------------------------------------------------------------------------------- /installers/mac/html/conclusion.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 12 | 13 | 14 |

The installation was successful

15 |

16 | You can configure WS-Discovery Host Daemon by editing /etc/wsddn.conf 17 |

18 |

19 | You can uninstall it by running /usr/local/bin/wsddn-uninstall script 20 |

21 | 22 | -------------------------------------------------------------------------------- /cmake/install.cmake: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2022, Eugene Gershnik 2 | # SPDX-License-Identifier: BSD-3-Clause 3 | 4 | include(GNUInstallDirs) 5 | 6 | install(CODE "message(\"Prefix: '\$ENV{DESTDIR}\${CMAKE_INSTALL_PREFIX}'\")") 7 | 8 | install(TARGETS 9 | wsddn RUNTIME 10 | DESTINATION ${CMAKE_INSTALL_BINDIR} 11 | ) 12 | 13 | install(FILES 14 | ${CMAKE_CURRENT_BINARY_DIR}/doc/wsddn.8.gz 15 | DESTINATION ${CMAKE_INSTALL_MANDIR}/man8 16 | ) 17 | 18 | if (${CMAKE_SYSTEM_NAME} STREQUAL "Darwin") 19 | install(CODE " 20 | if(CMAKE_INSTALL_DO_STRIP) 21 | execute_process(COMMAND codesign --force --sign - --timestamp=none \"\$ENV{DESTDIR}\${CMAKE_INSTALL_PREFIX}/bin/wsddn\") 22 | endif() 23 | ") 24 | endif() 25 | 26 | -------------------------------------------------------------------------------- /installers/mac/html/welcome.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 12 | 13 | 14 |

15 | This package will install WS-Discovery Host Daemon for macOS. 16 |

17 |

18 | It allows your macOS machine to be discovered by Windows 10 and above 19 | systems and displayed in their Explorer "Network" views. 20 |

21 |

You will be guided through the steps necessary to install this software.

22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /installers/deb/postrm: -------------------------------------------------------------------------------- 1 | #! /bin/sh 2 | 3 | set -e 4 | 5 | if [ "$1" = remove ] && [ -d /run/systemd/system ] ; then 6 | if [ -x "/usr/bin/systemctl" ]; then 7 | systemctl --system daemon-reload >/dev/null || true 8 | fi 9 | fi 10 | 11 | if [ "$1" = "remove" ]; then 12 | if [ -x "/usr/bin/deb-systemd-helper" ]; then 13 | deb-systemd-helper mask 'wsddn.service' >/dev/null || true 14 | fi 15 | fi 16 | 17 | if [ "$1" = "purge" ]; then 18 | if [ -x "/usr/bin/deb-systemd-helper" ]; then 19 | deb-systemd-helper purge 'wsddn.service' >/dev/null || true 20 | deb-systemd-helper unmask 'wsddn.service' >/dev/null || true 21 | fi 22 | fi 23 | 24 | if [ -x "/usr/sbin/update-rc.d" ] ; then 25 | update-rc.d wsddn remove || true 26 | fi 27 | 28 | 29 | 30 | exit 0 31 | -------------------------------------------------------------------------------- /config/mac/Library/LaunchDaemons/io.github.gershnik.wsddn.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Label 6 | io.github.gershnik.wsddn 7 | ProgramArguments 8 | 9 | /usr/local/bin/wsddn 10 | --launchd 11 | --config=/etc/wsddn.conf 12 | --log-os-log 13 | 14 | WorkingDirectory 15 | /var/empty 16 | KeepAlive 17 | 18 | Crashed 19 | 20 | 21 | RunAtLoad 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /installers/mac/scripts/preinstall: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | 3 | set -e 4 | if /bin/launchctl list io.github.gershnik.wsddn &>/dev/null; then 5 | /bin/launchctl unload -w "/Library/LaunchDaemons/io.github.gershnik.wsddn.plist" 6 | fi 7 | 8 | # Remove pre 1.3 logging and pidfile if present 9 | rm -f /etc/newsyslog.d/wsddn.conf || true 10 | rm -rf /var/run/wsddn || true 11 | rm -f /var/log/wsddn.* || true 12 | 13 | 14 | #User and group were broken before 1.4 15 | #Let's delete them if they exist (unconditionally for good measure) 16 | dscl . -delete /Users/_wsddn || true 17 | dscl . -delete /Groups/_wsddn || true 18 | 19 | # Remove old conf if it is the same as sample 20 | if [[ -f /etc/wsddn.conf ]]; then 21 | if [ ! diff /etc/wsddn.conf /etc/wsddn.conf.sample >/dev/null 2>&1 ]; then 22 | rm -f /etc/wsddn.conf 23 | fi 24 | fi 25 | -------------------------------------------------------------------------------- /src/Info.template.plist: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | CFBundleIdentifier 10 | @WSDDN_BUNDLE_IDENTIFIER@ 11 | CFBundleName 12 | wsddn 13 | CFBundleDisplayName 14 | WS-Discovery Host Daemon 15 | CFBundleVersion 16 | @WSDDN_VERSION@ 17 | CFBundleShortVersionString 18 | @WSDDN_VERSION@ 19 | NSHumanReadableCopyright 20 | Copyright (c) 2022, Eugene Gershnik 21 | CFBundleInfoDictionaryVersion 22 | 6.0 23 | 24 | 25 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | The following versions are currently being supported with security updates. 6 | 7 | | Version | Supported | 8 | | ------- | ------------------ | 9 | | 1.x | :white_check_mark: | 10 | | 0.x | :x: | 11 | 12 | ## Reporting a Vulnerability 13 | 14 | Please report privately using Github tools 15 | ([How to guide](https://docs.github.com/en/code-security/security-advisories/guidance-on-reporting-and-writing/privately-reporting-a-security-vulnerability)) 16 | 17 | You should expect a reply to any vulnerability related communication within 1 business day (USA). 18 | If vulnerability is accepted a fix will be developed with the goal to be released within at most 14 days. 19 | If the fix takes longer than a day to develop you will receive updates on the fix progress (or lack of it) every 2 days. 20 | You will also receive an update once fix is released to the public. 21 | -------------------------------------------------------------------------------- /src/http_request.h: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022, Eugene Gershnik 2 | // SPDX-License-Identifier: BSD-3-Clause 3 | 4 | #ifndef HEADER_HTTP_REQUEST_H_INCLUDED 5 | #define HEADER_HTTP_REQUEST_H_INCLUDED 6 | 7 | struct HttpRequest { 8 | 9 | enum class HeaderError { 10 | NotUnique = 1, 11 | BadFormat 12 | }; 13 | 14 | template 15 | using Outcome = outcome::outcome; 16 | 17 | auto getUniqueHeader(const sys_string & name) const -> Outcome>; 18 | auto getHeaderList(const sys_string & name) const -> std::optional; 19 | 20 | auto getContentLength() const -> Outcome>; 21 | auto getContentType() const -> Outcome>>; 22 | auto getKeepAlive() const -> bool; 23 | 24 | sys_string method; 25 | sys_string uri; 26 | unsigned versionMajor; 27 | unsigned versionMinor; 28 | std::multimap headers; 29 | }; 30 | 31 | 32 | #endif 33 | 34 | -------------------------------------------------------------------------------- /installers/mac/wsddn-uninstall: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | 3 | if [[ $EUID -ne 0 ]]; then 4 | exec sudo "$0" "$@" 5 | fi 6 | 7 | while true; do 8 | read -p "Do you want to unsinstal WS-Discovery Host Daemon? [yN] " -r 9 | case $REPLY in 10 | ([yY]) break;; 11 | ("" | [nN]) exit 0;; 12 | (*) continue;; 13 | esac 14 | done 15 | 16 | if /bin/launchctl list "io.github.gershnik.wsddn" &> /dev/null; then 17 | /bin/launchctl bootout system/io.github.gershnik.wsddn 18 | fi 19 | 20 | rm -f /Library/LaunchDaemons/io.github.gershnik.wsddn.plist 21 | rm -f /usr/local/bin/wsddn 22 | rm -f /usr/local/share/man/man8/wsddn.8.gz 23 | rm -f /etc/wsddn.conf.sample 24 | rm -rf '/Library/Application Support/wsdd-native' 25 | 26 | if dscl . -read /Users/_wsddn > /dev/null 2>&1; then 27 | dscl . -delete /Users/_wsddn 28 | fi 29 | if dscl . -read /Groups/_wsddn > /dev/null 2>&1; then 30 | dscl . -delete /Groups/_wsddn 31 | fi 32 | 33 | if pkgutil --pkgs=io.github.gershnik.wsddn > /dev/null; then 34 | pkgutil --forget io.github.gershnik.wsddn 35 | fi 36 | 37 | rm -f "$0" 38 | -------------------------------------------------------------------------------- /installers/mac/wrapper/Info.template.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleIdentifier 6 | @WSDDN_BUNDLE_IDENTIFIER@.wrapper 7 | CFBundleName 8 | WS-Discovery Host Daemon Helper 9 | CFBundleDisplayName 10 | WS-Discovery Host Daemon Helper 11 | LSUIElement 12 | 13 | CFBundleExecutable 14 | wsdd-native 15 | CFBundlePackageType 16 | APPL 17 | CFBundleVersion 18 | @WSDDN_VERSION@ 19 | CFBundleShortVersionString 20 | @WSDDN_VERSION@ 21 | NSHumanReadableCopyright 22 | Copyright (c) 2022, Eugene Gershnik 23 | CFBundleIconFile 24 | AppIcon 25 | CFBundleIconName 26 | AppIcon 27 | CFBundleSupportedPlatforms 28 | 29 | MacOSX 30 | 31 | CFBundleInfoDictionaryVersion 32 | 6.0 33 | 34 | 35 | -------------------------------------------------------------------------------- /src/interface_monitor.h: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022, Eugene Gershnik 2 | // SPDX-License-Identifier: BSD-3-Clause 3 | 4 | #ifndef HEADER_INTERFACE_MONITOR_H_INCLUDED 5 | #define HEADER_INTERFACE_MONITOR_H_INCLUDED 6 | 7 | #include "wsd_server.h" 8 | 9 | class InterfaceMonitor : public ref_counted { 10 | friend ref_counted; 11 | public: 12 | class Handler { 13 | public: 14 | virtual void addAddress(const NetworkInterface & interface, const ip::address & addr) = 0; 15 | virtual void removeAddress(const NetworkInterface & interface, const ip::address & addr) = 0; 16 | virtual void onFatalInterfaceMonitorError(asio::error_code ec) = 0; 17 | protected: 18 | ~Handler() {} 19 | }; 20 | 21 | public: 22 | virtual void start(Handler & handler) = 0; 23 | virtual void stop() = 0; 24 | protected: 25 | InterfaceMonitor() { 26 | } 27 | virtual ~InterfaceMonitor() noexcept { 28 | } 29 | }; 30 | 31 | using InterfaceMonitorFactoryT = auto (asio::io_context & ctxt, const refcnt_ptr & config) -> refcnt_ptr; 32 | using InterfaceMonitorFactory = std::function; 33 | 34 | InterfaceMonitorFactoryT createInterfaceMonitor; 35 | 36 | #endif 37 | -------------------------------------------------------------------------------- /src/http_server.h: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022, Eugene Gershnik 2 | // SPDX-License-Identifier: BSD-3-Clause 3 | 4 | #ifndef HEADER_HTTP_SERVER_H_INCLUDED 5 | #define HEADER_HTTP_SERVER_H_INCLUDED 6 | 7 | #include "config.h" 8 | #include "xml_wrapper.h" 9 | 10 | struct NetworkInterface; 11 | 12 | class HttpServer : public ref_counted { 13 | friend ref_counted; 14 | 15 | public: 16 | class Handler { 17 | public: 18 | virtual auto handleHttpRequest(std::unique_ptr doc) -> std::optional = 0; 19 | virtual void onFatalHttpError() = 0; 20 | protected: 21 | ~Handler() {} 22 | }; 23 | public: 24 | virtual void start(Handler & handler) = 0; 25 | virtual void stop() = 0; 26 | protected: 27 | HttpServer() { 28 | } 29 | virtual ~HttpServer() noexcept { 30 | } 31 | }; 32 | 33 | using HttpServerFactoryT = auto (asio::io_context & ctxt, 34 | const refcnt_ptr & config, 35 | const NetworkInterface & iface, 36 | const ip::tcp::endpoint & endpoint) -> refcnt_ptr; 37 | using HttpServerFactory = std::function; 38 | 39 | HttpServerFactoryT createHttpServer; 40 | 41 | #endif 42 | -------------------------------------------------------------------------------- /src/udp_server.h: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022, Eugene Gershnik 2 | // SPDX-License-Identifier: BSD-3-Clause 3 | 4 | #ifndef HEADER_UDP_SERVER_H_INCLUDED 5 | #define HEADER_UDP_SERVER_H_INCLUDED 6 | 7 | #include "xml_wrapper.h" 8 | #include "util.h" 9 | #include "config.h" 10 | 11 | class UdpServer : public ref_counted { 12 | friend ref_counted; 13 | 14 | public: 15 | class Handler { 16 | public: 17 | virtual auto handleUdpRequest(std::unique_ptr doc) -> std::optional = 0; 18 | virtual void onFatalUdpError() = 0; 19 | protected: 20 | ~Handler() {} 21 | }; 22 | public: 23 | virtual void start(Handler & handler) = 0; 24 | virtual void stop() = 0; 25 | virtual void broadcast(XmlCharBuffer && data, std::function continuation = nullptr) = 0; 26 | 27 | protected: 28 | UdpServer() { 29 | } 30 | virtual ~UdpServer() noexcept { 31 | } 32 | }; 33 | 34 | using UdpServerFactoryT = auto (asio::io_context & ctxt, 35 | const refcnt_ptr & config, 36 | const NetworkInterface & iface, 37 | const ip::address & addr) -> refcnt_ptr; 38 | using UdpServerFactory = std::function; 39 | 40 | UdpServerFactoryT createUdpServer; 41 | 42 | 43 | #endif 44 | -------------------------------------------------------------------------------- /installers/common.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import subprocess 3 | import argparse 4 | from pathlib import Path 5 | 6 | def parseCommandLine() -> argparse.Namespace : 7 | parser = argparse.ArgumentParser() 8 | 9 | parser.add_argument('srcdir', type=Path) 10 | parser.add_argument('builddir', type=Path) 11 | parser.add_argument('--sign', dest='sign', action='store_true', required=False) 12 | parser.add_argument('--upload-results', dest='uploadResults', action='store_true', required=False) 13 | 14 | args = parser.parse_args() 15 | 16 | if args.uploadResults: 17 | args.sign = True 18 | 19 | return args 20 | 21 | def getVersion(builddir: Path): 22 | verRes = subprocess.run([builddir/'wsddn', '--version'], check=False, capture_output=True, encoding='utf-8') 23 | if verRes.returncode != 0: 24 | sys.exit(1) 25 | version = verRes.stdout.strip() 26 | print(f'VERSION={version}') 27 | return version 28 | 29 | def getSrcVersion(srcdir: Path): 30 | return (srcdir / 'VERSION').read_text().strip() 31 | 32 | def buildCode(builddir): 33 | subprocess.run(['cmake', '--build', builddir], check=True) 34 | 35 | def installCode(builddir, stagedir): 36 | subprocess.run(['cmake', '--install', builddir, '--prefix', stagedir, '--strip'], check=True) 37 | 38 | 39 | def copyTemplated(src, dst, map): 40 | dstdir = dst.parent 41 | dstdir.mkdir(parents=True, exist_ok=True) 42 | dst.write_text(src.read_text().format_map(map)) 43 | 44 | -------------------------------------------------------------------------------- /installers/mac/wrapper/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "icon_16x16.png", 5 | "idiom" : "mac", 6 | "scale" : "1x", 7 | "size" : "16x16" 8 | }, 9 | { 10 | "filename" : "icon_16x16@2x.png", 11 | "idiom" : "mac", 12 | "scale" : "2x", 13 | "size" : "16x16" 14 | }, 15 | { 16 | "filename" : "icon_32x32.png", 17 | "idiom" : "mac", 18 | "scale" : "1x", 19 | "size" : "32x32" 20 | }, 21 | { 22 | "filename" : "icon_32x32@2x.png", 23 | "idiom" : "mac", 24 | "scale" : "2x", 25 | "size" : "32x32" 26 | }, 27 | { 28 | "filename" : "icon_128x128.png", 29 | "idiom" : "mac", 30 | "scale" : "1x", 31 | "size" : "128x128" 32 | }, 33 | { 34 | "filename" : "icon_128x128@2x.png", 35 | "idiom" : "mac", 36 | "scale" : "2x", 37 | "size" : "128x128" 38 | }, 39 | { 40 | "filename" : "icon_256x256.png", 41 | "idiom" : "mac", 42 | "scale" : "1x", 43 | "size" : "256x256" 44 | }, 45 | { 46 | "filename" : "icon_256x256@2x.png", 47 | "idiom" : "mac", 48 | "scale" : "2x", 49 | "size" : "256x256" 50 | }, 51 | { 52 | "idiom" : "mac", 53 | "scale" : "1x", 54 | "size" : "512x512" 55 | }, 56 | { 57 | "idiom" : "mac", 58 | "scale" : "2x", 59 | "size" : "512x512" 60 | } 61 | ], 62 | "info" : { 63 | "author" : "xcode", 64 | "version" : 1 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/http_response.h: -------------------------------------------------------------------------------- 1 | 2 | // Copyright (c) 2022, Eugene Gershnik 3 | // Copyright (c) 2003-2022 Christopher M. Kohlhoff (chris at kohlhoff dot com) 4 | // SPDX-License-Identifier: BSD-3-Clause 5 | 6 | 7 | #ifndef HEADER_HTTP_RESPONSE_H_INCLUDED 8 | #define HEADER_HTTP_RESPONSE_H_INCLUDED 9 | 10 | #include "xml_wrapper.h" 11 | 12 | class HttpResponse 13 | { 14 | public: 15 | enum Status 16 | { 17 | Ok = 200, 18 | Created = 201, 19 | Accepted = 202, 20 | NoContent = 204, 21 | MultipleChoices = 300, 22 | MovedPermanently = 301, 23 | MovedTemporarily = 302, 24 | NotModified = 304, 25 | BadRequest = 400, 26 | Unauthorized = 401, 27 | Forbidden = 403, 28 | NotFound = 404, 29 | InternalServerError = 500, 30 | NotImplemented = 501, 31 | BadGateway = 502, 32 | ServiceUnavailable = 503 33 | }; 34 | 35 | public: 36 | HttpResponse(Status status = InternalServerError) : m_status(status) { 37 | } 38 | static auto makeStockResponse(Status status) -> HttpResponse; 39 | static auto makeReply(XmlCharBuffer && xml) -> HttpResponse; 40 | 41 | void addHeader(const sys_string & name, const sys_string & value); 42 | 43 | auto makeBuffers() const -> std::vector; 44 | 45 | private: 46 | auto contentLength() const -> size_t; 47 | 48 | private: 49 | Status m_status; 50 | std::variant m_content; 51 | std::vector m_headers; 52 | }; 53 | 54 | 55 | #endif 56 | -------------------------------------------------------------------------------- /src/wsd_server.h: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022, Eugene Gershnik 2 | // SPDX-License-Identifier: BSD-3-Clause 3 | 4 | #ifndef HEADER_WSD_SERVER_H_INCLUDED 5 | #define HEADER_WSD_SERVER_H_INCLUDED 6 | 7 | #include "udp_server.h" 8 | #include "http_server.h" 9 | 10 | 11 | class WsdServer : public ref_counted { 12 | friend ref_counted; 13 | public: 14 | enum State { 15 | NotStarted, 16 | Running, 17 | Stopped 18 | }; 19 | public: 20 | virtual void start() = 0; 21 | virtual void stop(bool graceful) = 0; 22 | 23 | auto state() const -> State { 24 | return m_state; 25 | } 26 | 27 | auto interface() const -> const NetworkInterface & { 28 | return m_interface; 29 | } 30 | protected: 31 | WsdServer(const NetworkInterface & iface): m_interface(iface) { 32 | } 33 | virtual ~WsdServer() noexcept { 34 | } 35 | 36 | protected: 37 | const NetworkInterface m_interface; 38 | 39 | State m_state = NotStarted; 40 | }; 41 | 42 | using WsdServerFactoryT = auto (asio::io_context & ctxt, 43 | const refcnt_ptr & config, 44 | HttpServerFactory httpFactory, 45 | UdpServerFactory udpFactory, 46 | const NetworkInterface & iface, 47 | const ip::address & addr) -> refcnt_ptr; 48 | using WsdServerFactory = std::function; 49 | 50 | WsdServerFactoryT createWsdServer; 51 | 52 | 53 | #endif 54 | -------------------------------------------------------------------------------- /installers/deb/postinst: -------------------------------------------------------------------------------- 1 | #! /bin/sh 2 | 3 | set -e 4 | 5 | if [ "$1" = "configure" ] || [ "$1" = "abort-upgrade" ] || [ "$1" = "abort-deconfigure" ] || [ "$1" = "abort-remove" ] ; then 6 | if [ -x "/usr/bin/deb-systemd-helper" ] && [ -x "/usr/bin/systemctl" ] && [ -x "/usr/bin/deb-systemd-invoke" ] ; then 7 | # This will only remove masks created by d-s-h on package removal. 8 | deb-systemd-helper unmask 'wsddn.service' >/dev/null || true 9 | 10 | # was-enabled defaults to true, so new installations run enable. 11 | if deb-systemd-helper --quiet was-enabled 'wsddn.service'; then 12 | # Enables the unit on first installation, creates new 13 | # symlinks on upgrades if the unit file has changed. 14 | deb-systemd-helper enable 'wsddn.service' >/dev/null || true 15 | else 16 | # Update the statefile to add new symlinks (if any), which need to be 17 | # cleaned up on purge. Also remove old symlinks. 18 | deb-systemd-helper update-state 'wsddn.service' >/dev/null || true 19 | fi 20 | 21 | if [ -d /run/systemd/system ]; then 22 | systemctl --system daemon-reload >/dev/null || true 23 | if [ -n "$2" ]; then 24 | _dh_action=restart 25 | else 26 | _dh_action=start 27 | fi 28 | deb-systemd-invoke $_dh_action 'wsddn.service' >/dev/null || true 29 | fi 30 | fi 31 | if [ -x "/usr/sbin/update-rc.d" ] ; then 32 | update-rc.d wsddn defaults 33 | fi 34 | fi 35 | 36 | exit 0 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2022, Eugene Gershnik 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /config/metadata/default.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | $SMB_HOST_DESCRIPTION 11 | 1.0 12 | 1 13 | 14 | 15 | 16 | 17 | wsddn 18 | wsddn 19 | 1 20 | Computers 21 | 22 | 23 | 24 | 25 | 26 | 27 | $ENDPOINT_ID 28 | 29 | pub:Computer 30 | $ENDPOINT_ID 31 | $SMB_FULL_HOST_NAME 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /src/command_line.h: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022, Eugene Gershnik 2 | // SPDX-License-Identifier: BSD-3-Clause 3 | 4 | #ifndef HEADER_COMMAND_LINE_H_INCLUDED 5 | #define HEADER_COMMAND_LINE_H_INCLUDED 6 | 7 | #include "sys_util.h" 8 | #include "util.h" 9 | 10 | struct CommandLine { 11 | std::optional configFile; 12 | std::optional daemonType; 13 | 14 | std::vector interfaces; 15 | std::optional allowedAddressFamily; 16 | std::optional hoplimit; 17 | std::optional sourcePort; 18 | 19 | std::optional uuid; 20 | std::optional hostname; 21 | std::optional memberOf; 22 | 23 | #if CAN_HAVE_SAMBA 24 | std::optional smbConf; 25 | #endif 26 | 27 | std::optional metadataFile; 28 | 29 | std::optional logLevel; 30 | std::optional logFile; 31 | #if HAVE_OS_LOG 32 | std::optional logToOsLog; 33 | #endif 34 | std::optional pidFile; 35 | std::optional runAs; 36 | std::optional chrootDir; 37 | 38 | 39 | void parse(int argc, char * argv[]); 40 | void mergeConfigFile(const std::filesystem::path & path); 41 | 42 | private: 43 | class ConfigFileError; 44 | 45 | template 46 | void parseConfigKey(std::string_view keyName, const toml::node & value, Handler h); 47 | 48 | void parseConfigKey(std::string_view keyName, const toml::node & value); 49 | 50 | template 51 | void setConfigValue(bool isSet, std::string_view keyName, const toml::node & value, Handler handler); 52 | }; 53 | 54 | #endif 55 | 56 | -------------------------------------------------------------------------------- /config/metadata/other.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | Contoso Device 11 | 1.0 12 | 1 13 | 14 | 15 | 16 | 17 | Contoso, Ltd 18 | http://www.contoso.com 19 | Contoso Fancy Device 20 | 1 21 | http://www.contoso.com 22 | http://$IP_ADDR/ 23 | Other 24 | 25 | 26 | 27 | 28 | 29 | 30 | $ENDPOINT_ID 31 | 32 | $ENDPOINT_ID 33 | something 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /src/server_manager.h: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022, Eugene Gershnik 2 | // SPDX-License-Identifier: BSD-3-Clause 3 | 4 | #ifndef HEADER_SERVER_MANAGER_H_INCLUDED 5 | #define HEADER_SERVER_MANAGER_H_INCLUDED 6 | 7 | #include "wsd_server.h" 8 | #include "interface_monitor.h" 9 | 10 | 11 | class ServerManager : public InterfaceMonitor::Handler { 12 | 13 | public: 14 | ServerManager(asio::io_context & ctxt, 15 | const refcnt_ptr & config, 16 | InterfaceMonitorFactory ifaceMonitorFactory, 17 | HttpServerFactory httpServerFactory, 18 | UdpServerFactory udpServerFactory) : 19 | m_ctxt(ctxt), 20 | m_config(config), 21 | m_interfaceMonitor(ifaceMonitorFactory(ctxt, config)), 22 | m_httpServerFactory(httpServerFactory), 23 | m_udpServerFactory(udpServerFactory) { 24 | 25 | } 26 | 27 | void start() { 28 | m_interfaceMonitor->start(*this); 29 | } 30 | 31 | void stop(bool gracefully) { 32 | m_interfaceMonitor->stop(); 33 | for(auto & [_, server]: m_serversByAddress) { 34 | if (server) 35 | server->stop(gracefully); 36 | } 37 | m_serversByAddress.clear(); 38 | } 39 | 40 | private: 41 | void addAddress(const NetworkInterface & interface, const ip::address & addr) override; 42 | void removeAddress(const NetworkInterface & interface, const ip::address & addr) override; 43 | void onFatalInterfaceMonitorError(asio::error_code ec) override; 44 | 45 | auto createServer(const NetworkInterface & interface, const ip::address & addr) -> refcnt_ptr; 46 | 47 | private: 48 | asio::io_context & m_ctxt; 49 | const refcnt_ptr m_config; 50 | refcnt_ptr m_interfaceMonitor; 51 | HttpServerFactory m_httpServerFactory; 52 | UdpServerFactory m_udpServerFactory; 53 | std::map> m_serversByAddress; 54 | }; 55 | 56 | #endif 57 | -------------------------------------------------------------------------------- /src/server_manager.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022, Eugene Gershnik 2 | // SPDX-License-Identifier: BSD-3-Clause 3 | 4 | #include "server_manager.h" 5 | #include "exc_handling.h" 6 | 7 | auto ServerManager::createServer(const NetworkInterface & interface, const ip::address & addr) -> refcnt_ptr { 8 | refcnt_ptr server; 9 | try { 10 | server = createWsdServer(m_ctxt, m_config, m_httpServerFactory, m_udpServerFactory, interface, addr); 11 | } catch(std::system_error & ex) { 12 | WSDLOG_ERROR("Unable to start WSD server on interface {}, addr {}: error: {}", interface, addr.to_string(), ex.what()); 13 | WSDLOG_DEBUG("{}", formatCaughtExceptionBacktrace()); 14 | return nullptr; 15 | } 16 | server->start(); 17 | return server; 18 | } 19 | 20 | void ServerManager::onFatalInterfaceMonitorError(asio::error_code ec) { 21 | //nothing left to do if the monitor died 22 | throw std::system_error(ec, "fatal interface monitor error"); 23 | } 24 | 25 | void ServerManager::addAddress(const NetworkInterface & interface, const ip::address & addr) { 26 | auto & server = m_serversByAddress[addr]; 27 | 28 | if (server && server->interface() == interface && server->state() == WsdServer::Running) 29 | return; 30 | 31 | WSDLOG_INFO("Adding interface {}, addr {}", interface, addr.to_string()); 32 | server = createServer(interface, addr); 33 | } 34 | 35 | void ServerManager::removeAddress(const NetworkInterface & interface, const ip::address & addr) { 36 | 37 | auto itServer = m_serversByAddress.find(addr); 38 | if (itServer == m_serversByAddress.end()) 39 | return; 40 | 41 | auto & server = itServer->second; 42 | if (server && server->interface() != interface) 43 | return; 44 | 45 | WSDLOG_INFO("Removing interface {}, addr {}", interface, addr.to_string()); 46 | if (server) 47 | server->stop(false); 48 | m_serversByAddress.erase(itServer); 49 | 50 | } 51 | -------------------------------------------------------------------------------- /installers/deb/copyright: -------------------------------------------------------------------------------- 1 | Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ 2 | Upstream-Name: wsdd-native 3 | Upstream-Contact: Eugene Gershnik 4 | Source: https://github.com/gershnik/wsdd-native 5 | 6 | Files: * 7 | Copyright: 2022, Eugene Gershnik 8 | License: BSD-3-Clause 9 | Redistribution and use in source and binary forms, with or without 10 | modification, are permitted provided that the following conditions are met: 11 | . 12 | 1. Redistributions of source code must retain the above copyright notice, this 13 | list of conditions and the following disclaimer. 14 | . 15 | 2. Redistributions in binary form must reproduce the above copyright notice, 16 | this list of conditions and the following disclaimer in the documentation 17 | and/or other materials provided with the distribution. 18 | . 19 | 3. Neither the name of the copyright holder nor the names of its 20 | contributors may be used to endorse or promote products derived from 21 | this software without specific prior written permission. 22 | . 23 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 24 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 25 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 26 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 27 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 28 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 29 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 30 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 31 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 32 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 33 | Comment: 34 | On Debian systems, the complete text of the BSD 3-clause "New" or "Revised" 35 | License can be found in `/usr/share/common-licenses/BSD'. 36 | -------------------------------------------------------------------------------- /tools/create-release: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env -S python3 -u 2 | 3 | import sys 4 | import re 5 | import subprocess 6 | 7 | from pathlib import Path 8 | from datetime import date 9 | 10 | MYPATH = Path(__file__).parent 11 | ROOT = MYPATH.parent 12 | 13 | NEW_VER = sys.argv[1] 14 | 15 | unreleased_link_pattern = re.compile(r"^\[Unreleased\]: (.*)$", re.DOTALL) 16 | lines = [] 17 | with open(ROOT / "CHANGELOG.md", "rt", encoding="utf-8") as change_log: 18 | for line in change_log.readlines(): 19 | # Move Unreleased section to new version 20 | if re.fullmatch(r"^## Unreleased.*$", line, re.DOTALL): 21 | lines.append(line) 22 | lines.append("\n") 23 | lines.append( 24 | f"## [{NEW_VER}] - {date.today().isoformat()}\n" 25 | ) 26 | else: 27 | lines.append(line) 28 | lines.append(f'[{NEW_VER}]: https://github.com/gershnik/wsdd-native/releases/v{NEW_VER}\n') 29 | 30 | with open(ROOT / "CHANGELOG.md", "wt", encoding="utf-8") as change_log: 31 | change_log.writelines(lines) 32 | 33 | (ROOT / "VERSION").write_text(f'{NEW_VER}\n') 34 | 35 | readme = (ROOT / 'README.md').read_text() 36 | readme = re.sub(r'https://github.com/gershnik/wsdd-native/releases/download/v(?:\d+(?:\.\d+)+)', 37 | f'https://github.com/gershnik/wsdd-native/releases/download/v{NEW_VER}', 38 | readme) 39 | readme = re.sub(r'wsddn-macos-(?:\d+(?:\.\d+)+)\.pkg', 40 | f'wsddn-macos-{NEW_VER}.pkg', 41 | readme) 42 | readme = re.sub(r'wsddn-(?:\d+(?:\.\d+)+)([-A-Za-z0-9]*)\.tgz', 43 | fr'wsddn-{NEW_VER}\1.tgz', 44 | readme) 45 | (ROOT / 'README.md').write_text(readme) 46 | 47 | subprocess.run(['git', 'add', 48 | ROOT / "README.md", 49 | ROOT / "CHANGELOG.md", 50 | ROOT / "VERSION"], check=True) 51 | subprocess.run(['git', 'commit', '-m', f'chore: creating version {NEW_VER}'], check=True) 52 | subprocess.run(['git', 'tag', f'v{NEW_VER}'], check=True) 53 | -------------------------------------------------------------------------------- /tools/hashdeps: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env -S python3 -u 2 | 3 | import argparse 4 | import json 5 | import hashlib 6 | 7 | from pathlib import Path 8 | from urllib.request import urlretrieve 9 | from urllib.error import HTTPError 10 | 11 | MYDIR = Path(__file__).parent 12 | 13 | 14 | 15 | def hash_url(url): 16 | try: 17 | path, _ = urlretrieve(url) 18 | except HTTPError as err: 19 | raise RuntimeError(f'Unable to download {url}: {err}') from err 20 | try: 21 | sha256 = hashlib.sha256() 22 | md5 = hashlib.md5() 23 | buffer = bytearray(4096) 24 | view = memoryview(buffer) 25 | with open(path, 'rb') as f: 26 | while True: 27 | size = f.readinto(buffer) 28 | if size == 0: 29 | break 30 | sha256.update(view[:size]) 31 | md5.update(view[:size]) 32 | return sha256.hexdigest(), md5.hexdigest() 33 | finally: 34 | Path(path).unlink() 35 | 36 | def main(): 37 | parser = argparse.ArgumentParser() 38 | 39 | parser.add_argument('name', nargs='*', type=str) 40 | args = parser.parse_args() 41 | 42 | names = set(args.name) 43 | 44 | with open(MYDIR.parent / 'dependencies.json', 'rt', encoding='utf-8') as f: 45 | deps = json.load(f) 46 | changed = False 47 | for dep, data in deps.items(): 48 | if not len(names) == 0 and not dep in names: 49 | continue 50 | version = data['version'] 51 | url: str = data['url'] 52 | url = url.replace('${version}', version) 53 | sha256, md5 = hash_url(url) 54 | if data.get('sha256', '') != sha256: 55 | data['sha256'] = sha256 56 | changed = True 57 | if data.get('md5', '') != md5: 58 | data['md5'] = md5 59 | changed = True 60 | if changed: 61 | with open(MYDIR.parent / 'dependencies.json', 'wt', encoding='utf-8') as f: 62 | json.dump(deps, f, indent=4) 63 | 64 | if __name__ == '__main__': 65 | main() 66 | -------------------------------------------------------------------------------- /src/app_state.h: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022, Eugene Gershnik 2 | // SPDX-License-Identifier: BSD-3-Clause 3 | 4 | #ifndef HEADER_APP_STATE_H_INCLUDED 5 | #define HEADER_APP_STATE_H_INCLUDED 6 | 7 | #include "xml_wrapper.h" 8 | #include "command_line.h" 9 | #include "pid_file.h" 10 | #include "config.h" 11 | 12 | class AppState { 13 | public: 14 | AppState(int argc, char ** argv, std::set untouchedSignals); 15 | 16 | void reload(); 17 | 18 | auto config() const -> const refcnt_ptr { 19 | return m_config; 20 | } 21 | 22 | auto shouldFork() const -> bool { 23 | return m_currentCommandLine.chrootDir || m_currentCommandLine.runAs; 24 | } 25 | 26 | 27 | void preFork(); 28 | 29 | void postForkInServerProcess() noexcept; 30 | 31 | enum class DaemonStatus { 32 | Ready, 33 | Reloading, 34 | Stopping 35 | }; 36 | void notify(DaemonStatus status); 37 | 38 | private: 39 | void init(); 40 | void refresh(); 41 | 42 | void daemonize(); 43 | 44 | void setLogLevel(); 45 | void setLogOutput(bool firstTime); 46 | void setPidFile(); 47 | 48 | static auto openLogFile(const std::filesystem::path & filename) -> ptl::FileDescriptor; 49 | static void redirectStdFile(FILE * from, const ptl::FileDescriptor & to); 50 | static void closeAllExcept(const int * first, const int * last); 51 | private: 52 | std::set m_untouchedSignals; 53 | CommandLine m_origCommandLine; 54 | CommandLine m_currentCommandLine; 55 | pid_t m_mainPid; 56 | 57 | bool m_isInitialized = false; 58 | std::optional m_logLevel; 59 | std::optional m_logFilePath; 60 | std::optional m_pidFilePath; 61 | #if HAVE_OS_LOG 62 | std::optional m_logToOsLog; 63 | #endif 64 | #if HAVE_SYSTEMD 65 | decltype(sd_notify) * m_sdNotify = nullptr; 66 | #endif 67 | PidFile m_pidFile; 68 | ptl::FileDescriptor m_savedStdOut; 69 | ptl::FileDescriptor m_savedStdErr; 70 | refcnt_ptr m_config; 71 | }; 72 | 73 | #endif 74 | -------------------------------------------------------------------------------- /config/sysv/etc/init.d/wsddn: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | ### BEGIN INIT INFO 3 | # Provides: wsddn 4 | # Required-Start: $network $local_fs 5 | # Required-Stop: $network $local_fs 6 | # Default-Start: 2 3 4 5 7 | # Default-Stop: 0 1 6 8 | # Short-Description: WS-Discovery Host Daemon 9 | # Description: Allows your Linux machine to be discovered by Windows 10 10 | # and above systems and displayed by their 11 | # Explorer "Network" views. 12 | ### END INIT INFO 13 | 14 | NAME="wsddn" 15 | DESC="WS-Discovery Host Daemon" 16 | DAEMON="/usr/bin/$NAME" 17 | SCRIPTNAME=/etc/init.d/$NAME 18 | PIDFILE=/var/run/$NAME/$NAME.pid 19 | LOGFILE=/var/log/$NAME.log 20 | 21 | # Gracefully exit if the package has been removed. 22 | test -x $DAEMON || exit 0 23 | 24 | . /lib/lsb/init-functions 25 | 26 | case $1 in 27 | start) 28 | log_daemon_msg "Starting $DESC" $NAME 29 | if ! start-stop-daemon --start --quiet --oknodo --pidfile $PIDFILE --exec $DAEMON -- --unixd --pid-file=$PIDFILE --log-file=$LOGFILE; then 30 | log_end_msg 1 31 | exit 1 32 | fi 33 | 34 | log_end_msg 0 35 | ;; 36 | stop) 37 | 38 | log_daemon_msg "Stopping $DESC" $NAME 39 | 40 | start-stop-daemon --stop --quiet --pidfile $PIDFILE 41 | # Wait a little and remove stale PID file in case it is left over 42 | sleep 1 43 | if [ -f $PIDFILE ] && ! ps h `cat $PIDFILE` > /dev/null 44 | then 45 | rm -f $PIDFILE 46 | fi 47 | 48 | log_end_msg 0 49 | 50 | ;; 51 | reload) 52 | log_daemon_msg "Reloading $DESC" $NAME 53 | 54 | start-stop-daemon --stop --quiet --signal HUP --pidfile $PIDFILE 55 | 56 | log_end_msg 0 57 | ;; 58 | restart|force-reload) 59 | $0 stop 60 | sleep 1 61 | $0 start 62 | ;; 63 | status) 64 | status_of_proc -p $PIDFILE $DAEMON $NAME 65 | exit $? 66 | ;; 67 | *) 68 | echo "Usage: $SCRIPTNAME {start|stop|reload|restart|force-reload|status}" 69 | exit 1 70 | ;; 71 | esac 72 | 73 | exit 0 -------------------------------------------------------------------------------- /installers/mac/notarize: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env -S python3 -u 2 | 3 | import sys 4 | import argparse 5 | import subprocess 6 | import json 7 | from pathlib import Path 8 | 9 | 10 | def callNotaryTool(cmd): 11 | fullCmd = ['xcrun', 'notarytool'] + cmd + ['-f', 'json'] 12 | output = subprocess.run(fullCmd, check=True, stdout=subprocess.PIPE).stdout.decode('utf-8') 13 | print(output) 14 | return json.loads(output) 15 | 16 | 17 | def notarize(package, username, team, password): 18 | print("Starting...") 19 | workDir = package.parent 20 | print(f"Uploading to Apple to notarize") 21 | submission = callNotaryTool(['submit', str(package), 22 | '--apple-id', username, '--team-id', team, '--password', password, 23 | '--wait']) 24 | success = (submission['status'] == 'Accepted') 25 | submissionId = submission['id'] 26 | print("Downloading log file") 27 | callNotaryTool(['log', submissionId, 28 | '--apple-id', username, '--team-id', team, '--password', password, 29 | workDir/'notarization-log.json']) 30 | log = (workDir/'notarization-log.json').read_text() 31 | print(f"Notarization log:\n{log}") 32 | if not success: 33 | sys.exit(1) 34 | print("Stapling") 35 | subprocess.check_call(['xcrun', 'stapler', 'staple', f"{package}"]) 36 | print("Done") 37 | 38 | def main(): 39 | parser = argparse.ArgumentParser(description=''' 40 | Notarize Mac software 41 | ''') 42 | parser.add_argument(dest='package', 43 | help=f'Package to notarize') 44 | parser.add_argument('--user', dest='username', type=str, required = True, 45 | help='Username') 46 | parser.add_argument('--password', dest='password', type=str, required = True, 47 | help='Application password configured for your Apple ID (not your Apple ID password)') 48 | parser.add_argument('--team', dest='team', type=str, required = True, 49 | help='Team ID') 50 | args = parser.parse_args() 51 | notarize(Path(args.package), args.username, args.team, args.password) 52 | 53 | if __name__ == "__main__": 54 | main() -------------------------------------------------------------------------------- /installers/mac/distribution.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | WS-Discovery Host Daemon 8 | io.github.gershnik 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | output.pkg 20 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 33 | 51 | -------------------------------------------------------------------------------- /src/http_request.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022, Eugene Gershnik 2 | // SPDX-License-Identifier: BSD-3-Clause 3 | 4 | #include "http_request.h" 5 | 6 | using namespace sysstr; 7 | 8 | auto HttpRequest::getUniqueHeader(const sys_string & name) const -> Outcome> { 9 | auto [first, last] = this->headers.equal_range(name); 10 | if (first == last) 11 | return std::nullopt; 12 | if (--last != first) 13 | return HeaderError::NotUnique; 14 | return first->second; 15 | } 16 | 17 | auto HttpRequest::getHeaderList(const sys_string & name) const -> std::optional { 18 | auto [first, last] = this->headers.equal_range(name); 19 | if (first == last) 20 | return std::nullopt; 21 | sys_string_builder builder; 22 | builder.append(first->second); 23 | for (++first; first == last; ++first) { 24 | builder.append(S(", ")); 25 | builder.append(first->second); 26 | } 27 | return builder.build(); 28 | } 29 | 30 | auto HttpRequest::getContentLength() const -> Outcome> { 31 | auto maybeVal = getUniqueHeader(S("Content-Length")); 32 | if (!maybeVal) 33 | return maybeVal.assume_error(); 34 | 35 | auto val = maybeVal.assume_value(); 36 | if (!val) 37 | return std::nullopt; 38 | 39 | size_t ret; 40 | auto first = val->c_str(); 41 | auto last = first + val->storage_size(); 42 | auto res = std::from_chars(first, last, ret); 43 | if (res.ec != std::errc() || res.ptr != last) { 44 | return HeaderError::BadFormat; 45 | } 46 | return ret; 47 | } 48 | 49 | auto HttpRequest::getContentType() const -> Outcome>> { 50 | auto maybeVal = getUniqueHeader(S("Content-Type")); 51 | if (!maybeVal) 52 | return maybeVal.assume_error(); 53 | 54 | auto val = maybeVal.assume_value(); 55 | 56 | if (!val) 57 | return std::nullopt; 58 | std::vector ret; 59 | val->split(std::back_inserter(ret), S("; ")); 60 | return ret; 61 | } 62 | 63 | auto HttpRequest::getKeepAlive() const -> bool { 64 | auto val = getHeaderList(S("Connection")); 65 | if (!val) 66 | return false; 67 | std::set items; 68 | val->split(std::inserter(items, items.end()), S(", ")); 69 | return items.contains(S("keep-alive")); 70 | } -------------------------------------------------------------------------------- /src/sys_config.h.in: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022, Eugene Gershnik 2 | // SPDX-License-Identifier: BSD-3-Clause 3 | 4 | #ifndef HEADER_SYS_CONFIG_H_INCLUDED 5 | #define HEADER_SYS_CONFIG_H_INCLUDED 6 | 7 | #cmakedefine01 HAVE_NETLINK 8 | #cmakedefine01 HAVE_PF_ROUTE 9 | #cmakedefine01 HAVE_SYSCTL_PF_ROUTE 10 | #cmakedefine01 HAVE_SIOCGLIFCONF 11 | #cmakedefine01 HAVE_SIOCGIFCONF 12 | #cmakedefine01 HAVE_SOCKADDR_SA_LEN 13 | #cmakedefine01 HAVE_EXECINFO_H 14 | #cmakedefine01 HAVE_CXXABI_H 15 | #cmakedefine01 HAVE_ABI_CXA_THROW 16 | #cmakedefine01 HAVE_SYSTEMD 17 | #cmakedefine01 IS_ALPINE_LINUX 18 | 19 | #cmakedefine LIBSYSTEMD_SO "${LIBSYSTEMD_SO}" 20 | 21 | #cmakedefine USERADD_PATH "${USERADD_PATH}" 22 | #cmakedefine GROUPADD_PATH "${GROUPADD_PATH}" 23 | #cmakedefine PW_PATH "${PW_PATH}" 24 | #cmakedefine ADDUSER_PATH "${ADDUSER_PATH}" 25 | #cmakedefine ADDGROUP_PATH "${ADDGROUP_PATH}" 26 | 27 | #if (defined(__APPLE__) && defined(__MACH__)) 28 | #define WSDDN_PLATFORM_APPLE 1 29 | #define ADMIN_GROUP_NAME "admin" 30 | #define WSDDN_DEFAULT_USER_NAME "_wsddn" 31 | #define WSDDN_DEFAULT_CHROOT_DIR "/var/empty" 32 | #define WSDDN_BUNDLE_IDENTIFIER "@WSDDN_BUNDLE_IDENTIFIER@" 33 | #elif defined(__FreeBSD__) 34 | #define WSDDN_PLATFORM_APPLE 0 35 | #define WSDDN_DEFAULT_USER_NAME "wsddn" 36 | #define WSDDN_DEFAULT_CHROOT_DIR "/var/empty" 37 | #elif defined(__OpenBSD__) 38 | #define WSDDN_PLATFORM_APPLE 0 39 | #define WSDDN_DEFAULT_USER_NAME "_wsddn" 40 | #define WSDDN_DEFAULT_CHROOT_DIR "/var/empty" 41 | #elif defined(__NetBSD__) 42 | #define WSDDN_PLATFORM_APPLE 0 43 | #define WSDDN_DEFAULT_USER_NAME "_wsddn" 44 | #define WSDDN_DEFAULT_CHROOT_DIR "/var/chroot/wsddn" 45 | #elif defined(__sun) || defined(__HAIKU__) 46 | #define WSDDN_PLATFORM_APPLE 0 47 | #define WSDDN_DEFAULT_USER_NAME "wsddn" 48 | #define WSDDN_DEFAULT_CHROOT_DIR "/var/empty" 49 | #else 50 | #define WSDDN_PLATFORM_APPLE 0 51 | #define WSDDN_DEFAULT_USER_NAME "wsddn" 52 | #define WSDDN_DEFAULT_CHROOT_DIR "/var/run/wsddn" 53 | #endif 54 | 55 | #define CAN_HAVE_SAMBA !WSDDN_PLATFORM_APPLE 56 | #define HAVE_APPLE_SAMBA WSDDN_PLATFORM_APPLE 57 | #define HAVE_APPLE_USER_CREATION WSDDN_PLATFORM_APPLE 58 | #define HAVE_LAUNCHD WSDDN_PLATFORM_APPLE 59 | #define HAVE_OS_LOG WSDDN_PLATFORM_APPLE 60 | 61 | 62 | #define WSDDN_VERSION "@WSDDN_VERSION@" 63 | #define WSDDN_PROGNAME "$" 64 | 65 | #endif 66 | -------------------------------------------------------------------------------- /installers/mac/html/license.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 12 | 13 | 14 |

Terms And Conditions For Accessing Or Otherwise Using WS-Discovery Host Daemon

15 |

16 | WS-Discovery Host Daemon software and documentation are licensed under the 17 | 3-Clause BSD License. 18 |

19 | 20 |

The 3-Clause BSD License

21 |

Copyright © 2022, Eugene Gershnik

22 |

All rights reserved.

23 |

24 | Redistribution and use in source and binary forms, with or without 25 | modification, are permitted provided that the following conditions are met: 26 |

27 |

28 | 1. Redistributions of source code must retain the above copyright notice, this 29 | list of conditions and the following disclaimer. 30 |

31 |

32 | 2. Redistributions in binary form must reproduce the above copyright notice, 33 | this list of conditions and the following disclaimer in the documentation 34 | and/or other materials provided with the distribution. 35 |

36 |

37 | 3. Neither the name of the copyright holder nor the names of its 38 | contributors may be used to endorse or promote products derived from 39 | this software without specific prior written permission. 40 |

41 |

42 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 43 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 44 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 45 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 46 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 47 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 48 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 49 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 50 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 51 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 52 |

53 | 54 | -------------------------------------------------------------------------------- /src/config_mac.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022, Eugene Gershnik 2 | // SPDX-License-Identifier: BSD-3-Clause 3 | 4 | #if HAVE_APPLE_SAMBA 5 | 6 | #include "config.h" 7 | 8 | auto Config::detectAppleWinNetInfo(bool useNetbiosHostName) -> std::optional { 9 | 10 | cf_ptr store = cf_attach(SCDynamicStoreCreate(nullptr, CFSTR("detectWinNetInfo"), nullptr, nullptr)); 11 | if (!store) { 12 | WSDLOG_ERROR("Unable to create configuration store"); 13 | return std::nullopt; 14 | } 15 | cf_ptr smb = cf_attach(SCDynamicStoreCopyValue (store.get(), CFSTR("com.apple.smb"))); 16 | if (!smb || CFGetTypeID(smb.get()) != CFDictionaryGetTypeID()) { 17 | WSDLOG_WARN("SMB info is not present in configuration store"); 18 | return std::nullopt; 19 | } 20 | 21 | auto dict = (CFDictionaryRef)smb.get(); 22 | sys_string_cfstr workgroup = (CFStringRef)CFDictionaryGetValue(dict, CFSTR("Workgroup")); 23 | sys_string_cfstr hostname = (CFStringRef)CFDictionaryGetValue(dict, CFSTR("NetBIOSName")); 24 | sys_string_cfstr desc = (CFStringRef)CFDictionaryGetValue(dict, CFSTR("ServerDescription")); 25 | if (!workgroup.cf_str()) { 26 | WSDLOG_WARN("Workgroup is missing in configuration store"); 27 | } 28 | 29 | sys_string_cfstr domain; 30 | cf_ptr ad = cf_attach(SCDynamicStoreCopyValue (store.get(), CFSTR("com.apple.opendirectoryd.ActiveDirectory"))); 31 | if (ad && CFGetTypeID(ad.get()) != CFDictionaryGetTypeID()) { 32 | 33 | dict = (CFDictionaryRef)ad.get(); 34 | domain = (CFStringRef)CFDictionaryGetValue(dict, CFSTR("DomainNameFlat")); 35 | } 36 | 37 | if (!workgroup.cf_str() && !domain.cf_str()) { 38 | WSDLOG_WARN("Cannot detect either workgroup or domain from configuration store"); 39 | return std::nullopt; 40 | } 41 | 42 | WinNetInfo ret; 43 | sys_string_builder builder; 44 | 45 | if (domain.cf_str()) { 46 | for(auto c: sys_string_cfstr::utf8_view(domain)) 47 | builder.append(c); 48 | ret.memberOf.emplace(builder.build()); 49 | } else { 50 | for(auto c: sys_string_cfstr::utf8_view(workgroup)) 51 | builder.append(c); 52 | ret.memberOf.emplace(builder.build()); 53 | } 54 | 55 | if (useNetbiosHostName && hostname.cf_str()) { 56 | for(auto c: sys_string_cfstr::utf8_view(hostname)) 57 | builder.append(c); 58 | ret.hostName = builder.build(); 59 | } else { 60 | if (useNetbiosHostName) 61 | ret.hostName = m_simpleHostName.to_upper(); 62 | else 63 | ret.hostName = m_simpleHostName; 64 | } 65 | 66 | if (desc.cf_str()) { 67 | for(auto c: sys_string_cfstr::utf8_view(desc)) 68 | builder.append(c); 69 | ret.hostDescription = builder.build(); 70 | } else { 71 | ret.hostDescription = m_simpleHostName; 72 | } 73 | 74 | return ret; 75 | } 76 | 77 | #endif 78 | -------------------------------------------------------------------------------- /installers/mac/wrapper/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2022, Eugene Gershnik 2 | # SPDX-License-Identifier: BSD-3-Clause 3 | 4 | 5 | configure_file(Info.template.plist Info.plist @ONLY) 6 | 7 | file(GLOB_RECURSE ASSET_FILES CONFIGURE_DEPENDS Assets.xcassets/*) 8 | 9 | set(GENERATED_ASSETS 10 | ${CMAKE_CURRENT_BINARY_DIR}/assets/AppIcon.icns 11 | ${CMAKE_CURRENT_BINARY_DIR}/assets/Assets.car 12 | ) 13 | 14 | add_custom_command( 15 | OUTPUT ${GENERATED_ASSETS} 16 | DEPENDS ${ASSET_FILES} 17 | COMMAND mkdir -p ${CMAKE_CURRENT_BINARY_DIR}/assets 18 | COMMAND actool --output-format human-readable-text --notices --warnings 19 | --app-icon AppIcon --accent-color AccentColor 20 | --output-partial-info-plist ${CMAKE_CURRENT_BINARY_DIR}/assets/PartialInfo.plist 21 | --enable-on-demand-resources NO --development-region en --target-device mac 22 | --minimum-deployment-target ${CMAKE_OSX_DEPLOYMENT_TARGET} --platform macosx 23 | --compile ${CMAKE_CURRENT_BINARY_DIR}/assets 24 | ${CMAKE_CURRENT_LIST_DIR}/Assets.xcassets 25 | 26 | ) 27 | 28 | add_executable(wrapper MACOSX_BUNDLE) 29 | 30 | set_target_properties(wrapper PROPERTIES 31 | OUTPUT_NAME wsdd-native 32 | CXX_EXTENSIONS OFF 33 | CXX_STANDARD 20 34 | C_STANDARD 11 35 | CXX_STANDARD_REQUIRED True 36 | C_STANDARD_REQUIRED True 37 | MACOSX_BUNDLE_INFO_PLIST ${CMAKE_CURRENT_BINARY_DIR}/Info.plist 38 | ) 39 | 40 | target_link_libraries(wrapper 41 | PRIVATE 42 | "-framework Foundation" 43 | "-framework CoreServices" 44 | ) 45 | 46 | target_sources(wrapper 47 | PRIVATE 48 | main.mm 49 | ${GENERATED_ASSETS} 50 | ) 51 | set_source_files_properties(${GENERATED_ASSETS} PROPERTIES 52 | MACOSX_PACKAGE_LOCATION Resources 53 | ) 54 | 55 | target_link_options(wrapper 56 | PRIVATE 57 | "$<$:-Wl,-object_path_lto,${CMAKE_CURRENT_BINARY_DIR}/${CMAKE_CFG_INTDIR}/lto.o>" 58 | "$<$:-Wl,-cache_path_lto,${CMAKE_CURRENT_BINARY_DIR}/${CMAKE_CFG_INTDIR}/LTOCache>" 59 | "$<$:-Wl,-no_adhoc_codesign>" 60 | "$<$,$,13.5>>:-Wl,-reproducible>" 61 | "$<$,$,15.0>>:LINKER:-no_warn_duplicate_libraries>" #see https://gitlab.kitware.com/cmake/cmake/-/issues/25297 62 | ) 63 | 64 | if (CMAKE_GENERATOR MATCHES "Xcode") 65 | set_target_properties(wrapper PROPERTIES 66 | XCODE_ATTRIBUTE_INFOPLIST_FILE ${CMAKE_CURRENT_BINARY_DIR}/Info.plist 67 | XCODE_ATTRIBUTE_ENABLE_HARDENED_RUNTIME YES 68 | XCODE_ATTRIBUTE_GCC_GENERATE_DEBUGGING_SYMBOLS YES 69 | XCODE_ATTRIBUTE_DEBUG_INFORMATION_FORMAT dwarf-with-dsym 70 | XCODE_ATTRIBUTE_PRODUCT_BUNDLE_IDENTIFIER ${WSDDN_BUNDLE_IDENTIFIER}.wrapper 71 | ) 72 | else() 73 | add_custom_command(TARGET wrapper POST_BUILD 74 | COMMAND dsymutil "$" -o "$.dSYM" 75 | COMMAND codesign --force --sign - --timestamp=none 76 | --options runtime "$" 77 | ) 78 | endif() -------------------------------------------------------------------------------- /installers/freebsd/make-toolchain.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env -S python3 -u 2 | 3 | # Copyright (c) 2022, Eugene Gershnik 4 | # SPDX-License-Identifier: BSD-3-Clause 5 | 6 | import sys 7 | import argparse 8 | import subprocess 9 | import json 10 | import shutil 11 | import hashlib 12 | from pathlib import Path 13 | 14 | 15 | parser = argparse.ArgumentParser() 16 | 17 | parser.add_argument('platform', type=str) 18 | parser.add_argument('dir', type=Path) 19 | 20 | args = parser.parse_args() 21 | 22 | platform = args.platform 23 | outdir: Path = args.dir 24 | 25 | if platform == 'arm64': 26 | arch = 'aarch64' 27 | elif platform == 'amd64': 28 | arch = 'x86_64' 29 | else: 30 | print(f'no platform->arch mapping for {platform}', file=sys.stderr) 31 | sys.exit(1) 32 | 33 | verRes = subprocess.run(['freebsd-version'], check=False, capture_output=True, encoding='utf-8') 34 | if verRes.returncode != 0: 35 | sys.exit(1) 36 | fullVersion = verRes.stdout.strip() 37 | version = fullVersion[:fullVersion.rfind('-')] 38 | 39 | toolchain = f''' 40 | set(CMAKE_SYSROOT ${{CMAKE_CURRENT_LIST_DIR}}) 41 | set(CMAKE_C_COMPILER_TARGET {arch}-unknown-freebsd{version}) 42 | set(CMAKE_CXX_COMPILER_TARGET {arch}-unknown-freebsd{version}) 43 | 44 | set(CMAKE_FIND_ROOT_PATH ${{CMAKE_CURRENT_LIST_DIR}}) 45 | 46 | set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) 47 | set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) 48 | 49 | set(CMAKE_EXE_LINKER_FLAGS "${{CMAKE_EXE_LINKER_FLAGS}} -stdlib=libc++") 50 | 51 | set(CMAKE_C_STANDARD_INCLUDE_DIRECTORIES 52 | "${{CMAKE_CURRENT_LIST_DIR}}/usr/include" 53 | "${{CMAKE_CURRENT_LIST_DIR}}/usr/include/sys" 54 | ) 55 | set(CMAKE_CXX_STANDARD_INCLUDE_DIRECTORIES 56 | "${{CMAKE_CURRENT_LIST_DIR}}/usr/include/c++/v1" 57 | ${{CMAKE_C_STANDARD_INCLUDE_DIRECTORIES}} 58 | ) 59 | '''.lstrip() 60 | toolchainHash = hashlib.sha256(toolchain.encode('utf-8')).hexdigest() 61 | 62 | newInfo = { 63 | 'version': version, 64 | 'platform': platform, 65 | 'toolchainHash': toolchainHash 66 | } 67 | 68 | try: 69 | info = json.loads((outdir / '.info.json').read_text()) 70 | if info == newInfo: 71 | print(f'{outdir} already contains the requested toolchain') 72 | sys.exit(0) 73 | print(f'{outdir} contains non-matching existing toolchain: \n{json.dumps(info, indent=4)}\nwill overwrite') 74 | except FileNotFoundError: 75 | pass 76 | 77 | if outdir.exists(): 78 | shutil.rmtree(outdir) 79 | outdir.mkdir(parents=True, exist_ok=False) 80 | 81 | procCurl = subprocess.Popen(['curl', '-LSs', 82 | f'https://download.freebsd.org/ftp/releases/{platform}/{fullVersion}/base.txz' 83 | ], stdout=subprocess.PIPE) 84 | procTar = subprocess.Popen(['tar', 'Jxf', '-', 85 | '--include', './usr/include/*', 86 | '--include', './usr/lib/*', 87 | '--include', './lib/*' 88 | ], cwd=outdir, stdin=procCurl.stdout) 89 | procCurl.stdout.close() 90 | procTar.wait() 91 | procCurl.wait() 92 | if procTar.returncode != 0 or procCurl.returncode != 0: 93 | print('unpacking failed', file=sys.stderr) 94 | sys.exit(1) 95 | 96 | 97 | (outdir / 'toolchain.cmake').write_text(toolchain) 98 | (outdir / '.info.json').write_text(json.dumps(newInfo, indent = 4)) 99 | -------------------------------------------------------------------------------- /src/pid_file.h: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022, Eugene Gershnik 2 | // SPDX-License-Identifier: BSD-3-Clause 3 | 4 | #ifndef HEADER_PID_FILE_H_INCLUDED 5 | #define HEADER_PID_FILE_H_INCLUDED 6 | 7 | #include "sys_util.h" 8 | 9 | class PidFile { 10 | public: 11 | PidFile() = default; 12 | PidFile(PidFile &&) noexcept = default; 13 | PidFile & operator=(PidFile && src) noexcept { 14 | this->~PidFile(); 15 | new (this) PidFile(std::move(src)); 16 | return *this; 17 | } 18 | 19 | static auto open(std::filesystem::path filename, 20 | std::optional owner = std::nullopt) -> std::optional { 21 | 22 | const mode_t mode = S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH; 23 | 24 | createMissingDirs(filename.parent_path(), mode | S_IXUSR | S_IXGRP | S_IXOTH, owner); 25 | 26 | for ( ; ; ) { 27 | auto fd = ptl::FileDescriptor::open(filename, O_WRONLY | O_CREAT, mode); 28 | if (!ptl::tryLockFile(fd, ptl::FileLock::Exclusive)) 29 | return std::nullopt; 30 | struct stat openFileStatus; 31 | ptl::getStatus(fd, openFileStatus); 32 | struct stat fileSystemFileStatus; 33 | std::error_code ec; 34 | ptl::getStatus(filename, fileSystemFileStatus, ec); 35 | //if we cannot get the status from filesystem or it is a different file, 36 | //it means some other process got there before us and created a new pid file 37 | //and so we need to retry. 38 | if (ec || 39 | openFileStatus.st_dev != fileSystemFileStatus.st_dev || 40 | openFileStatus.st_ino != fileSystemFileStatus.st_ino) { 41 | 42 | continue; 43 | } 44 | 45 | auto pid = getpid(); 46 | initFile(fd, mode, owner, pid); 47 | return PidFile(std::move(fd), std::move(filename), pid); 48 | } 49 | 50 | } 51 | 52 | ~PidFile() noexcept { 53 | if (!m_fd) 54 | return; 55 | 56 | if (getpid() != m_lockingProcess) 57 | return; 58 | 59 | std::error_code ec; 60 | std::filesystem::remove(m_path, ec); 61 | if (ec) 62 | WSDLOG_ERROR("unable to remove pidfile {}, error: {}\n", m_path.c_str(), ec.message().c_str()); 63 | } 64 | private: 65 | PidFile(ptl::FileDescriptor && fd, std::filesystem::path && path, pid_t proc) noexcept: 66 | m_fd(std::move(fd)), m_path(std::move(path)), m_lockingProcess(proc) { 67 | } 68 | 69 | static void initFile(const ptl::FileDescriptor & fd, mode_t mode, const std::optional & owner, pid_t pid) { 70 | ptl::changeMode(fd, mode); 71 | if (owner) 72 | ptl::changeOwner(fd, owner->uid(), owner->gid()); 73 | ptl::truncateFile(fd, 0); 74 | auto strPid = std::to_string(pid); 75 | strPid += '\n'; 76 | if ((size_t)writeFile(fd, strPid.data(), strPid.size()) != strPid.size()) 77 | throw std::runtime_error("partial write to pid file!"); 78 | } 79 | private: 80 | ptl::FileDescriptor m_fd; 81 | std::filesystem::path m_path; 82 | pid_t m_lockingProcess = -1; 83 | }; 84 | 85 | static_assert(!std::is_copy_constructible_v); 86 | static_assert(!std::is_copy_assignable_v); 87 | 88 | 89 | #endif 90 | -------------------------------------------------------------------------------- /src/config.h: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022, Eugene Gershnik 2 | // SPDX-License-Identifier: BSD-3-Clause 3 | 4 | #ifndef HEADER_CONFIG_H_INCLUDED 5 | #define HEADER_CONFIG_H_INCLUDED 6 | 7 | #include "sys_util.h" 8 | #include "util.h" 9 | #include "xml_wrapper.h" 10 | 11 | constexpr uint16_t g_WsdUdpPort = 3702; 12 | constexpr uint16_t g_WsdHttpPort = 5357; 13 | 14 | constexpr const char * g_WsdMulticastGroupV4 = "239.255.255.250"; 15 | constexpr const char * g_WsdMulticastGroupV6 = "ff02::c"; // link-local 16 | 17 | constexpr size_t g_maxLogFileSize = 1024 * 1024; 18 | constexpr size_t g_maxRotatedLogs = 5; 19 | 20 | struct CommandLine; 21 | 22 | class Config : public ref_counted { 23 | friend ref_counted; 24 | public: 25 | struct WinNetInfo { 26 | sys_string hostName; 27 | sys_string hostDescription; 28 | MemberOf memberOf; 29 | }; 30 | 31 | struct SambaParams { 32 | std::optional workgroup; 33 | std::optional security; 34 | std::optional hostName; 35 | std::optional hostDescription; 36 | }; 37 | public: 38 | static refcnt_ptr make(const CommandLine & cmdline) { 39 | return refcnt_attach(new Config(cmdline)); 40 | } 41 | 42 | auto instanceIdentifier() const -> size_t { return m_instanceIdentifier; }; 43 | auto endpointIdentifier() const -> const sys_string & { return m_urnUuid; } 44 | auto httpPath() const -> const sys_string & { return m_strUuid; } 45 | auto winNetInfo() const -> const WinNetInfo & { return m_winNetInfo; } 46 | auto metadataDoc() const -> XmlDoc * { return m_metadataDoc.get(); } 47 | 48 | auto enableIPv4() const -> bool { return m_allowedAddressFamily != IPv6Only; } 49 | auto enableIPv6() const -> bool { return m_allowedAddressFamily != IPv4Only; } 50 | auto hopLimit() const -> int { return m_hopLimit; } 51 | auto isAllowedInterface(const sys_string & name) const -> bool { 52 | return m_interfaceWhitelist.empty() || m_interfaceWhitelist.contains(name); 53 | } 54 | auto sourcePort() const -> uint16_t { return m_sourcePort; } 55 | 56 | auto pageSize() const -> size_t { return m_pageSize; } 57 | 58 | private: 59 | Config(const CommandLine & cmdline); 60 | ~Config() {}; 61 | 62 | #if HAVE_APPLE_SAMBA 63 | auto detectAppleWinNetInfo(bool useNetbiosHostName) -> std::optional; 64 | #endif 65 | auto detectWinNetInfo(std::optional smbConf, bool useNetbiosHostName) -> std::optional; 66 | auto sambaParamsToWinNetInfo(const SambaParams & params, bool useNetbiosHostName) -> WinNetInfo; 67 | 68 | auto getHostName() const -> sys_string; 69 | 70 | auto loadMetadaFile(const std::string & filename) const -> std::unique_ptr; 71 | private: 72 | size_t m_instanceIdentifier; 73 | sys_string m_fullHostName; 74 | sys_string m_simpleHostName; 75 | Uuid m_uuid; 76 | sys_string m_strUuid; 77 | sys_string m_urnUuid; 78 | WinNetInfo m_winNetInfo; 79 | std::unique_ptr m_metadataDoc; 80 | 81 | AllowedAddressFamily m_allowedAddressFamily = BothIPv4AndIPv6; 82 | int m_hopLimit = 1; 83 | std::set m_interfaceWhitelist; 84 | uint16_t m_sourcePort; 85 | 86 | size_t m_pageSize; 87 | }; 88 | 89 | #endif 90 | -------------------------------------------------------------------------------- /dependencies.json: -------------------------------------------------------------------------------- 1 | { 2 | "argum": { 3 | "version": "v2.6", 4 | "url": "https://github.com/gershnik/argum/tarball/${version}", 5 | "homepage": "https://github.com/gershnik/argum", 6 | "sha256": "72b2b6805da7bf022e8111f3c2f3ed08ae6c23daa0ad336de56f2bd133d653c4", 7 | "md5": "59141e9e95b75097b0cf6d9d36fc6304" 8 | }, 9 | "asio": { 10 | "version": "1.36.0", 11 | "url": "https://downloads.sourceforge.net/asio/asio-${version}.tar.gz", 12 | "homepage": "https://think-async.com/Asio/", 13 | "sha256": "55d5c64e78b1bedd0004423e695c2cfc191fc71914eaaa7f042329ff99ee6155", 14 | "md5": "a3fc4f4ebd5baac595a7b3fea90f2117" 15 | }, 16 | "fmt": { 17 | "version": "12.0.0", 18 | "url": "https://github.com/fmtlib/fmt/tarball/${version}", 19 | "homepage": "https://github.com/fmtlib/fmt", 20 | "sha256": "24b8e5ecad723fa9654ccbc5e322ff7881cc70425ad52dcdf5b88f5873e6d16b", 21 | "md5": "f9d5b80495bb0c8327a988234e9e9524" 22 | }, 23 | "isptr": { 24 | "version": "v1.9", 25 | "url": "https://github.com/gershnik/intrusive_shared_ptr/tarball/${version}", 26 | "homepage": "https://github.com/gershnik/intrusive_shared_ptr", 27 | "sha256": "f9095609a2226f3aa6dbfcd4726a8521a56f4fd2f426b0898d92acd1f133aa6d", 28 | "md5": "574eb3ad0a8e5685cdc9e7712804900f" 29 | }, 30 | "LibXml2": { 31 | "version": "v2.15.0", 32 | "url": "https://gitlab.gnome.org/GNOME/libxml2/-/archive/${version}/libxml2-${version}.tar.gz", 33 | "homepage": "https://gitlab.gnome.org/GNOME/libxml2", 34 | "sha256": "027e302d24e0d136393a24e02938046cda72281a3e3620d56cbc0995524658bc", 35 | "md5": "6d29889f41d024c5b8b561067aee89fa" 36 | }, 37 | "modern-uuid": { 38 | "version": "v2.1", 39 | "url": "https://github.com/gershnik/modern-uuid/tarball/${version}", 40 | "homepage": "https://github.com/gershnik/modern-uuid", 41 | "sha256": "f52bad71a9691fe10a88982cee876f4670885bc4d533fbce8790e0013aad8752", 42 | "md5": "b07ea7836194f192b1bab0bbd446d9de" 43 | }, 44 | "outcome": { 45 | "version": "v2.2.13", 46 | "url": "https://github.com/ned14/outcome/tarball/${version}", 47 | "homepage": "https://github.com/ned14/outcome", 48 | "sha256": "3ee937017934371169c2f50d1e3742839dd6ab4fc1f23141cbd2efc3824cfd15", 49 | "md5": "e6d0ae19b6c250ba5e6ceb0ec11eaddb" 50 | }, 51 | "ptl": { 52 | "version": "v1.7", 53 | "url": "https://github.com/gershnik/ptl/tarball/${version}", 54 | "homepage": "https://github.com/gershnik/ptl", 55 | "sha256": "e3efb37f71846ba7d10165bef7f62a581dd3e7c8f4ac185bb86d4069bc4ec9ed", 56 | "md5": "f4c503429bd1fa9c8db8823a64590a4b" 57 | }, 58 | "spdlog": { 59 | "version": "v1.16.0", 60 | "url": "https://github.com/gabime/spdlog/tarball/${version}", 61 | "homepage": "https://github.com/gabime/spdlog", 62 | "sha256": "72d7aebe7730d5d5573524e11736bdba8c1891f07e8d80cc7e8f7b36ec7c046d", 63 | "md5": "85ad737bf7f042010ccfe998d1c8a566" 64 | }, 65 | "sys_string": { 66 | "version": "v2.22", 67 | "url": "https://github.com/gershnik/sys_string/tarball/${version}", 68 | "homepage": "https://github.com/gershnik/sys_string", 69 | "sha256": "774f8b4e39cbdba4b1a2617878a4deccb095c427dbfcd22493a221ba0960b8e4", 70 | "md5": "10f07a548b1dbfd4ceef3ce74de3f45b" 71 | }, 72 | "tomlplusplus": { 73 | "version": "v3.4.0", 74 | "url": "https://github.com/marzer/tomlplusplus/tarball/${version}", 75 | "homepage": "https://github.com/marzer/tomlplusplus", 76 | "sha256": "8874014da21de8d1414d9914c8f3c6b5f315c23a75951b33df46048c13dda12f", 77 | "md5": "562036440b3283550d508ef5dd85a839" 78 | } 79 | } -------------------------------------------------------------------------------- /src/http_request_parser.h: -------------------------------------------------------------------------------- 1 | 2 | // Copyright (c) 2022, Eugene Gershnik 3 | // Copyright (c) 2003-2022 Christopher M. Kohlhoff (chris at kohlhoff dot com) 4 | // SPDX-License-Identifier: BSD-3-Clause 5 | 6 | 7 | #ifndef HEADER_HTTP_REQUEST_PARSER_H_INCLUDED 8 | #define HEADER_HTTP_REQUEST_PARSER_H_INCLUDED 9 | 10 | #include "http_request.h" 11 | 12 | /** 13 | Parser for HTTP 1.x request header 14 | */ 15 | class HttpRequestParser 16 | { 17 | private: 18 | static constexpr std::tuple s_minVersion{1, 0}; 19 | static constexpr std::tuple s_maxVersion{1, 1}; 20 | static constexpr size_t s_maxMethodSize = 10; 21 | static constexpr size_t s_maxUriSize = 2048; 22 | static constexpr size_t s_maxHeadersSize = 8192; 23 | public: 24 | /// Reset to initial parser state. 25 | void reset(); 26 | 27 | /// Result of parse. 28 | enum ResultType { Good, Bad, Indeterminate }; 29 | 30 | /// Parse some data. The enum return value is Good when a complete request has 31 | /// been parsed, Bad if the data is invalid, Indeterminate when more data is 32 | /// required. The InputIterator return value indicates how much of the input 33 | /// has been consumed. 34 | template 35 | requires(sizeof(typename std::iterator_traits::value_type) == 1) 36 | auto parse(HttpRequest & req, InputIterator begin, InputIterator end) -> 37 | std::tuple 38 | { 39 | while (begin != end) 40 | { 41 | ResultType result = consume(req, uint8_t(*begin++)); 42 | if (result == Good || result == Bad) 43 | return {result, begin}; 44 | } 45 | return {Indeterminate, begin}; 46 | } 47 | 48 | private: 49 | /// Handle the next character of input. 50 | auto consume(HttpRequest & req, uint8_t input) -> ResultType; 51 | 52 | /// Check if a byte is an HTTP character. 53 | static bool isChar(uint8_t c) { 54 | return c <= 127; 55 | } 56 | 57 | /// Check if a byte is an HTTP control character. 58 | static bool isCtl(uint8_t c) { 59 | return c <= 31 || c == 127; 60 | } 61 | 62 | /// Check if a byte is defined as an HTTP tspecial character. 63 | static bool isTSpecial(uint8_t c) { 64 | switch (c) 65 | { 66 | case u8'(': case u8')': case u8'<': case u8'>': case u8'@': 67 | case u8',': case u8';': case u8':': case u8'\\': case u8'"': 68 | case u8'/': case u8'[': case u8']': case u8'?': case u8'=': 69 | case u8'{': case u8'}': case u8' ': case u8'\t': 70 | return true; 71 | default: 72 | return false; 73 | } 74 | } 75 | 76 | /// Check if a byte is a digit. 77 | static bool isDigit(char8_t c) { 78 | return c >= u8'0' && c <= u8'9'; 79 | } 80 | 81 | /// The current state of the parser. 82 | enum State 83 | { 84 | method_start, 85 | method, 86 | uri, 87 | http_version_h, 88 | http_version_t_1, 89 | http_version_t_2, 90 | http_version_p, 91 | http_version_slash, 92 | http_version_major_start, 93 | http_version_major, 94 | http_version_minor_start, 95 | http_version_minor, 96 | expecting_newline_1, 97 | header_line_start, 98 | header_lws, 99 | header_name, 100 | space_before_header_value, 101 | header_value, 102 | expecting_newline_2, 103 | expecting_newline_3 104 | } m_state = method_start; 105 | 106 | sys_string_builder m_methodBuilder; 107 | sys_string_builder m_uriBuilder; 108 | unsigned m_versionMajor = 0; 109 | unsigned m_versionMinor = 0; 110 | sys_string_builder m_headerNameBuilder; 111 | sys_string_builder m_headerValueBuilder; 112 | unsigned m_totalHeadersSize = 0; 113 | }; 114 | 115 | 116 | #endif 117 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | branches: 6 | - '*' 7 | - '*/**' 8 | paths-ignore: 9 | - 'README.md' 10 | - '.gitignore' 11 | - 'LICENSE' 12 | - 'CHANGELOG.md' 13 | - 'SECURITY.md' 14 | - 'Acknowledgements.md' 15 | - 'config/metadata/**' 16 | - '.github/workflows/publish.yml' 17 | - '.github/workflows/check.yml' 18 | - 'tools/**' 19 | 20 | 21 | jobs: 22 | selfhosted: 23 | concurrency: ${{ matrix.remote_host }} 24 | runs-on: [self-hosted, server] 25 | strategy: 26 | fail-fast: false 27 | matrix: 28 | include: 29 | - remote_host: debian-11 30 | installer: deb 31 | - remote_host: debian-11-arm 32 | installer: deb 33 | - remote_host: debian-11-armhf 34 | installer: deb 35 | - remote_host: freebsd-13.1 36 | installer: freebsd 37 | - remote_host: freebsd-14 38 | installer: freebsd 39 | - remote_host: openbsd-7-5 40 | installer: openbsd 41 | 42 | steps: 43 | - name: Run remote build 44 | run: | 45 | "$RUNNER_TOOLS_PATH"/run-agent gh-${{ matrix.remote_host }} <<'EOF' 46 | set -e 47 | if [ ! -d work/wsdd-native ]; then 48 | git clone https://github.com/gershnik/wsdd-native.git work/wsdd-native 49 | fi 50 | cd work/wsdd-native 51 | git fetch --all 52 | git fetch -f --prune --tags 53 | git reset --hard ${{ github.sha }} 54 | if [[ '${{ matrix.installer }}' == 'freebsd' ]]; then 55 | mkdir -p out 56 | echo "::group::AMD64" 57 | [ -d "out/amd64" ] && tools/uncache out/amd64 58 | cmake -S . -B out/amd64 -DCMAKE_BUILD_TYPE=RelWithDebInfo 59 | installers/freebsd/build.py . out/amd64 60 | echo "::endgroup::" 61 | 62 | echo "::group::ARM64" 63 | installers/freebsd/make-toolchain.py arm64 out/toolchain-arm64 64 | [ -d "out/arm64" ] && tools/uncache out/arm64 65 | cmake -S . -B out/arm64 -DCMAKE_TOOLCHAIN_FILE=out/toolchain-arm64/toolchain.cmake -DCMAKE_BUILD_TYPE=RelWithDebInfo 66 | installers/freebsd/build.py --arch=aarch64 . out/arm64 67 | echo "::endgroup::" 68 | elif [[ '${{ matrix.installer }}' == 'openbsd' ]]; then 69 | [ -d "out" ] && tools/uncache out 70 | cmake -S . -B out -DCMAKE_BUILD_TYPE=RelWithDebInfo 71 | installers/openbsd/build.py . out 72 | else 73 | [ -d "out" ] && tools/uncache out 74 | cmake -S . -B out -DCMAKE_BUILD_TYPE=RelWithDebInfo 75 | installers/${{ matrix.installer }}/build.py --sign . out 76 | fi 77 | EOF 78 | 79 | mac: 80 | runs-on: macos-14 81 | env: 82 | DEVELOPER_DIR: /Applications/Xcode_16.2.0.app 83 | 84 | steps: 85 | - name: Checkout 86 | uses: actions/checkout@v4 87 | 88 | - name: Collect System Info 89 | id: system-info 90 | uses: kenchan0130/actions-system-info@master 91 | 92 | - name: Configure 93 | run: | 94 | [ -d "out" ] && tools/uncache out 95 | cmake -S . -B out \ 96 | -DCMAKE_BUILD_TYPE=RelWithDebInfo \ 97 | "-DCMAKE_OSX_ARCHITECTURES=x86_64;arm64" \ 98 | -DCMAKE_IGNORE_PREFIX_PATH=/opt/local \ 99 | -DWSDDN_PREFER_SYSTEM_LIBXML2=ON 100 | 101 | - name: Build 102 | run: | 103 | export SIGN_CERTIFICATE='${{ secrets.SIGN_CERTIFICATE }}' 104 | export SIGN_CERTIFICATE_PWD='${{ secrets.SIGN_CERTIFICATE_PWD }}' 105 | export KEYCHAIN_PWD='${{ secrets.KEYCHAIN_PWD }}' 106 | export NOTARIZE_USER='${{ secrets.NOTARIZE_USER }}' 107 | export NOTARIZE_PWD='${{ secrets.NOTARIZE_PWD }}' 108 | installers/mac/set-github-keychain 109 | 110 | installers/mac/build.py --sign . out 111 | 112 | 113 | 114 | 115 | 116 | -------------------------------------------------------------------------------- /src/exc_handling.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022, Eugene Gershnik 2 | // SPDX-License-Identifier: BSD-3-Clause 3 | 4 | #include "exc_handling.h" 5 | 6 | #if __has_include() //moronic Alpine/MUSL have no backtrace and no replacement 7 | 8 | #include 9 | #include 10 | #include 11 | 12 | using CodePtr = void (*)(void); 13 | 14 | // static auto demangle_free = [](char * ptr) { 15 | // free(ptr); 16 | // }; 17 | // using demangled_string = std::unique_ptr; 18 | 19 | class Backtrace { 20 | public: 21 | Backtrace() = default; 22 | 23 | [[gnu::always_inline]] void fill(size_t skip) { 24 | 25 | int res = backtrace(reinterpret_cast(s_backtraceBuffer.data()), static_cast(s_backtraceBuffer.size())); 26 | if (res > 0 && size_t(res) > skip) { 27 | m_pointers.assign(s_backtraceBuffer.begin() + skip, s_backtraceBuffer.begin() + size_t(res) - skip); 28 | } else { 29 | m_pointers.clear(); 30 | } 31 | } 32 | 33 | template 34 | void print(OutIt dest) const { 35 | 36 | fmt::format_to(dest, "--------\n"); 37 | for (auto ptr: m_pointers) 38 | { 39 | auto addr = reinterpret_cast(ptr); 40 | fmt::format_to(dest, " {:p}", addr); 41 | Dl_info info{}; 42 | if (dladdr(addr, &info)) 43 | { 44 | // if (info.dli_sname) 45 | // { 46 | // int stat = 0; 47 | // demangled_string demangled(abi::__cxa_demangle(info.dli_sname, 0, 0, &stat), demangle_free); 48 | // size_t offset = intptr_t(addr) - intptr_t(info.dli_saddr); 49 | // fmt::format_to(dest, " {} + {}", (demangled ? demangled.get() : info.dli_sname), offset); 50 | // } 51 | if (info.dli_fname) 52 | { 53 | size_t offset = intptr_t(addr) - intptr_t(info.dli_fbase); 54 | const char * basename = strrchr(info.dli_fname, '/'); 55 | fmt::format_to(dest, " {} + 0x{:X}", (basename ? basename + 1 : info.dli_fname), offset); 56 | } 57 | } 58 | fmt::format_to(dest, "\n"); 59 | } 60 | fmt::format_to(dest, "--------\n"); 61 | } 62 | private: 63 | std::vector m_pointers; 64 | static thread_local std::array s_backtraceBuffer; 65 | }; 66 | 67 | thread_local std::array Backtrace::s_backtraceBuffer; 68 | 69 | thread_local std::vector g_backtraces; 70 | 71 | 72 | extern "C" { 73 | 74 | #if HAVE_ABI_CXA_THROW 75 | #define CXA_THROW abi::__cxa_throw 76 | #else 77 | #define CXA_THROW __cxa_throw 78 | #endif 79 | 80 | [[gnu::noinline]] __attribute__((__visibility__("default"))) __attribute__ ((noreturn)) 81 | void CXA_THROW(void * ex, std::type_info * info, void (*dest)(void *)) { 82 | 83 | auto currentIdx = std::uncaught_exceptions(); 84 | g_backtraces.resize(currentIdx + 1); 85 | g_backtraces[currentIdx].fill(/*skip=*/1); 86 | 87 | using __cxa_throw_t = decltype(&CXA_THROW); 88 | 89 | static __cxa_throw_t __attribute__ ((noreturn)) realCxaThrow = (__cxa_throw_t)dlsym(RTLD_NEXT, "__cxa_throw"); 90 | realCxaThrow(ex, info, dest); 91 | } 92 | } 93 | 94 | template 95 | static void doPrintCaughtExceptionBacktrace(OutIt dest) { 96 | auto currentIdx = std::uncaught_exceptions(); 97 | if (size_t(currentIdx) > g_backtraces.size()) { 98 | fmt::format_to(dest, " \n"); 99 | return; 100 | } 101 | 102 | const Backtrace & backtrace = g_backtraces[currentIdx]; 103 | backtrace.print(dest); 104 | } 105 | 106 | auto formatCaughtExceptionBacktrace() -> std::string { 107 | std::string ret = "Backtrace:\n"; 108 | doPrintCaughtExceptionBacktrace(std::back_inserter(ret)); 109 | return ret; 110 | } 111 | 112 | #else 113 | 114 | auto formatCaughtExceptionBacktrace() -> std::string { 115 | return ""; 116 | } 117 | 118 | #endif 119 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | 8 | jobs: 9 | prepare: 10 | runs-on: ubuntu-latest 11 | permissions: write-all 12 | steps: 13 | - name: Make release 14 | uses: softprops/action-gh-release@v2 15 | id: create_release 16 | with: 17 | draft: true 18 | prerelease: false 19 | body: ...edit me... 20 | 21 | selfhosted: 22 | concurrency: ${{ matrix.remote_host }} 23 | runs-on: [self-hosted, server] 24 | needs: prepare 25 | strategy: 26 | fail-fast: false 27 | matrix: 28 | include: 29 | - remote_host: debian-11 30 | installer: deb 31 | - remote_host: debian-11-arm 32 | installer: deb 33 | - remote_host: debian-11-armhf 34 | installer: deb 35 | - remote_host: freebsd-13.1 36 | installer: freebsd 37 | - remote_host: freebsd-14 38 | installer: freebsd 39 | - remote_host: openbsd-7-5 40 | installer: openbsd 41 | 42 | steps: 43 | - name: Run remote build 44 | run: | 45 | "$RUNNER_TOOLS_PATH"/run-agent gh-${{ matrix.remote_host }} <<'EOF' 46 | set -e 47 | if [ ! -d work/wsdd-native ]; then 48 | git clone https://github.com/gershnik/wsdd-native.git work/wsdd-native 49 | fi 50 | cd work/wsdd-native 51 | git fetch --all 52 | git fetch -f --prune --tags 53 | git reset --hard ${{ github.sha }} 54 | export GH_TOKEN=${{ secrets.GITHUB_TOKEN }} 55 | if [[ '${{ matrix.installer }}' == 'freebsd' ]]; then 56 | mkdir -p out 57 | echo "::group::AMD64" 58 | [ -d "out/amd64" ] && tools/uncache out/amd64 59 | cmake -S . -B out/amd64 -DCMAKE_BUILD_TYPE=RelWithDebInfo 60 | installers/freebsd/build.py --upload-results . out/amd64 61 | echo "::endgroup::" 62 | 63 | echo "::group::ARM64" 64 | installers/freebsd/make-toolchain.py arm64 out/toolchain-arm64 65 | [ -d "out/arm64" ] && tools/uncache out/arm64 66 | cmake -S . -B out/arm64 -DCMAKE_TOOLCHAIN_FILE=out/toolchain-arm64/toolchain.cmake -DCMAKE_BUILD_TYPE=RelWithDebInfo 67 | installers/freebsd/build.py --arch=aarch64 --upload-results . out/arm64 68 | echo "::endgroup::" 69 | else 70 | [ -d "out" ] && tools/uncache out 71 | cmake -S . -B out -DCMAKE_BUILD_TYPE=RelWithDebInfo 72 | installers/${{ matrix.installer }}/build.py --upload-results . out 73 | fi 74 | EOF 75 | 76 | mac: 77 | runs-on: macos-14 78 | needs: prepare 79 | env: 80 | DEVELOPER_DIR: /Applications/Xcode_16.2.app 81 | 82 | steps: 83 | - name: Checkout 84 | uses: actions/checkout@v4 85 | 86 | - name: Collect System Info 87 | id: system-info 88 | uses: kenchan0130/actions-system-info@master 89 | 90 | - name: Cache Build Dir 91 | id: cache-build-dir 92 | uses: actions/cache@v4 93 | with: 94 | path: out 95 | key: ${{ runner.os }}-${{ steps.system-info.outputs.release }}-out 96 | 97 | - name: Configure 98 | run: | 99 | [ -d "out" ] && tools/uncache out 100 | cmake -S . -B out \ 101 | -DCMAKE_BUILD_TYPE=RelWithDebInfo \ 102 | "-DCMAKE_OSX_ARCHITECTURES=x86_64;arm64" \ 103 | -DCMAKE_IGNORE_PREFIX_PATH=/opt/local \ 104 | -DWSDDN_PREFER_SYSTEM_LIBXML2=ON 105 | 106 | 107 | - name: Make Distribution 108 | env: 109 | AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} 110 | AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} 111 | AWS_DEFAULT_REGION: ${{ secrets.AWS_DEFAULT_REGION }} 112 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 113 | run: | 114 | export SIGN_CERTIFICATE='${{ secrets.SIGN_CERTIFICATE }}' 115 | export SIGN_CERTIFICATE_PWD='${{ secrets.SIGN_CERTIFICATE_PWD }}' 116 | export KEYCHAIN_PWD='${{ secrets.KEYCHAIN_PWD }}' 117 | export NOTARIZE_USER='${{ secrets.NOTARIZE_USER }}' 118 | export NOTARIZE_PWD='${{ secrets.NOTARIZE_PWD }}' 119 | 120 | installers/mac/set-github-keychain 121 | installers/mac/build.py --upload-results . out 122 | -------------------------------------------------------------------------------- /installers/freebsd/build.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env -S python3 -u 2 | 3 | # Copyright (c) 2022, Eugene Gershnik 4 | # SPDX-License-Identifier: BSD-3-Clause 5 | 6 | import sys 7 | import subprocess 8 | import shutil 9 | import re 10 | import argparse 11 | from pathlib import Path 12 | 13 | ARCH = subprocess.run(['uname', '-m'], check=True, capture_output=True, encoding="utf-8").stdout.strip() 14 | 15 | ABI = None 16 | for line in subprocess.run(['pkg', '-vv'], check=True, encoding="utf-8", capture_output=True).stdout.splitlines(): 17 | m = re.match(r'ABI\s*=\s*"([^"]+)";', line) 18 | if m: 19 | ABI = m.group(1) 20 | break 21 | 22 | if ABI is None: 23 | print("Unable to determine ABI", file=sys.stderr) 24 | sys.exit(1) 25 | 26 | mydir = Path(sys.argv[0]).parent 27 | 28 | sys.path.append(str(mydir.absolute().parent)) 29 | 30 | from common import getVersion, getSrcVersion, buildCode, installCode, copyTemplated 31 | 32 | parser = argparse.ArgumentParser() 33 | 34 | parser.add_argument('srcdir', type=Path) 35 | parser.add_argument('builddir', type=Path) 36 | parser.add_argument('--upload-results', dest='uploadResults', action='store_true', required=False) 37 | parser.add_argument('--arch', required=False) 38 | 39 | args = parser.parse_args() 40 | 41 | srcdir: Path = args.srcdir 42 | builddir: Path = args.builddir 43 | 44 | buildCode(builddir) 45 | 46 | if not args.arch is None: 47 | ABI = ABI[:ABI.rfind(':') + 1] + args.arch 48 | ARCH = args.arch 49 | VERSION = getSrcVersion(srcdir) 50 | else: 51 | VERSION = getVersion(builddir) 52 | 53 | workdir = builddir / 'stage/freebsd' 54 | stagedir = workdir / 'root' 55 | shutil.rmtree(workdir, ignore_errors=True) 56 | stagedir.mkdir(parents=True) 57 | 58 | installCode(builddir, stagedir / 'usr/local') 59 | 60 | shutil.copytree(srcdir / 'config/freebsd/usr', stagedir / 'usr', dirs_exist_ok=True) 61 | docdir = stagedir / 'usr/local/share/doc/wsddn' 62 | docdir.mkdir(parents=True) 63 | shutil.copy(srcdir / 'LICENSE', docdir / 'LICENSE') 64 | shutil.copy(srcdir / 'Acknowledgements.md', docdir / 'Acknowledgements.md') 65 | 66 | copyTemplated(mydir.parent / 'wsddn.conf', stagedir / 'usr/local/etc/wsddn.conf.sample', { 67 | 'SAMPLE_IFACE_NAME': "hn0", 68 | 'RELOAD_INSTRUCTIONS': """ 69 | # sudo service wsddn reload 70 | # or 71 | # sudo kill -HUP $( 82 | www: https://github.com/gershnik/wsdd-native 83 | comment: WS-Discovery Host Daemon 84 | desc: Allows your machine to be discovered by Windows 10 and above systems and displayed by their Explorer "Network" views. 85 | prefix: / 86 | """.lstrip()) 87 | 88 | with open(workdir / 'plist', 'w', encoding='utf-8') as plist: 89 | for item in stagedir.glob('**/*'): 90 | if not item.is_dir(): 91 | print(str(item.relative_to(stagedir)), file=plist) 92 | 93 | shutil.copy(mydir / 'pre', workdir / '+PRE_INSTALL') 94 | shutil.copy(mydir / 'post_install', workdir / '+POST_INSTALL') 95 | shutil.copy(mydir / 'pre', workdir / '+PRE_DEINSTALL') 96 | shutil.copy(mydir / 'post_deinstall', workdir / '+POST_DEINSTALL') 97 | 98 | subprocess.run(['pkg', 'create', '--verbose', 99 | '-m', workdir, '-r', stagedir, '-p', workdir/'plist', '-o', workdir], check=True) 100 | 101 | 102 | subprocess.run(['gzip', '--keep', '--force', builddir / 'wsddn'], check=True) 103 | 104 | if args.uploadResults: 105 | ostype, oslevel, osarch = ABI.split(':') 106 | abiMarker = '-'.join((ostype, oslevel, osarch)) 107 | 108 | subprocess.run(['aws', 's3', 'cp', 109 | workdir / f'wsddn-{VERSION}.pkg', 110 | f's3://gershnik-builds/freebsd/wsddn-{VERSION}-{ARCH}-{oslevel}.pkg'], 111 | check=True) 112 | subprocess.run(['aws', 's3', 'cp', 113 | builddir / 'wsddn.gz', 114 | f's3://wsddn-symbols/wsddn-{VERSION}-{abiMarker}.gz'], 115 | check=True) 116 | 117 | shutil.move(workdir / f'wsddn-{VERSION}.pkg', workdir / f'wsddn-{VERSION}-{abiMarker}.pkg') 118 | subprocess.run(['gh', 'release', 'upload', f'v{VERSION}', workdir / f'wsddn-{VERSION}-{abiMarker}.pkg'], 119 | check=True) 120 | -------------------------------------------------------------------------------- /src/pch.h: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022, Eugene Gershnik 2 | // SPDX-License-Identifier: BSD-3-Clause 3 | 4 | #ifndef HEADER_PCH_H_INCLUDED 5 | #define HEADER_PCH_H_INCLUDED 6 | 7 | #include 8 | 9 | #ifdef __GNUC__ 10 | #define WSDDN_SUPPRESS_WARNINGS_BEGIN _Pragma("GCC diagnostic push") 11 | #define WSDDN_SUPPRESS_WARNING_HELPER0(arg) #arg 12 | #define WSDDN_SUPPRESS_WARNING_HELPER1(name) WSDDN_SUPPRESS_WARNING_HELPER0(GCC diagnostic ignored name) 13 | #define WSDDN_SUPPRESS_WARNING_HELPER2(name) WSDDN_SUPPRESS_WARNING_HELPER1(#name) 14 | #define WSDDN_SUPPRESS_WARNING(name) _Pragma(WSDDN_SUPPRESS_WARNING_HELPER2(name)) 15 | #define WSDDN_SUPPRESS_WARNINGS_END _Pragma("GCC diagnostic pop") 16 | 17 | #define WSDDN_IGNORE_DEPRECATED_BEGIN WSDDN_SUPPRESS_WARNINGS_BEGIN \ 18 | WSDDN_SUPPRESS_WARNING(-Wdeprecated-declarations) 19 | #define WSDDN_IGNORE_DEPRECATED_END WSDDN_SUPPRESS_WARNINGS_END 20 | #else 21 | #define WSDDN_SUPPRESS_WARNINGS_BEGIN 22 | #define WSDDN_SUPPRESS_WARNING(x) 23 | #define WSDDN_SUPPRESS_WARNINGS_END 24 | 25 | #define WSDDN_IGNORE_DEPRECATED_BEGIN 26 | #define WSDDN_IGNORE_DEPRECATED_END 27 | #endif 28 | 29 | #include 30 | #include 31 | #include 32 | 33 | #include 34 | 35 | //must come before sys_string due to S macro collision 36 | #ifdef __clang__ 37 | WSDDN_SUPPRESS_WARNINGS_BEGIN 38 | WSDDN_SUPPRESS_WARNING(-Wshorten-64-to-32) 39 | #endif 40 | #include 41 | #ifdef __clang__ 42 | WSDDN_SUPPRESS_WARNINGS_END 43 | #endif 44 | 45 | #include 46 | #include 47 | 48 | #define PTL_USE_STD_FORMAT 0 49 | #include 50 | #include 51 | #include 52 | #include 53 | #include 54 | #include 55 | #include 56 | #include 57 | 58 | #include 59 | 60 | #include 61 | #include 62 | #include 63 | #include 64 | 65 | #include 66 | #include 67 | #include 68 | #include 69 | #include 70 | 71 | #include 72 | 73 | #include 74 | 75 | #include 76 | 77 | #if HAVE_SYSTEMD 78 | #include 79 | #include 80 | #endif 81 | 82 | #include 83 | #include 84 | #include 85 | #include 86 | #include 87 | #include 88 | #include 89 | #include 90 | #include 91 | #include 92 | #include 93 | #include 94 | #include 95 | 96 | #include 97 | 98 | #include 99 | 100 | #if WSDDN_PLATFORM_APPLE 101 | 102 | #include 103 | #include 104 | #include 105 | 106 | #include 107 | 108 | #include 109 | 110 | #endif 111 | 112 | #define WSDLOG_TRACE(...) do { if (spdlog::should_log(spdlog::level::trace)) spdlog::trace(__VA_ARGS__); } while(false) 113 | #define WSDLOG_DEBUG(...) do { if (spdlog::should_log(spdlog::level::debug)) spdlog::debug(__VA_ARGS__); } while(false) 114 | #define WSDLOG_INFO(...) do { if (spdlog::should_log(spdlog::level::info)) spdlog::info(__VA_ARGS__); } while(false) 115 | #define WSDLOG_WARN(...) do { if (spdlog::should_log(spdlog::level::warn)) spdlog::warn(__VA_ARGS__); } while(false) 116 | #define WSDLOG_ERROR(...) do { if (spdlog::should_log(spdlog::level::err)) spdlog::error(__VA_ARGS__); } while(false) 117 | #define WSDLOG_CRITICAL(...) do { if (spdlog::should_log(spdlog::level::critical)) spdlog::critical(__VA_ARGS__); } while(false) 118 | 119 | 120 | using namespace sysstr; 121 | using namespace isptr; 122 | 123 | using Uuid = muuid::uuid; 124 | 125 | namespace ip = asio::ip; 126 | namespace outcome = OUTCOME_V2_NAMESPACE; 127 | 128 | 129 | template <> struct fmt::formatter : private fmt::formatter { 130 | 131 | using super = fmt::formatter; 132 | using super::parse; 133 | 134 | template 135 | auto format(const sys_string & str, FormatContext & ctx) const -> decltype(ctx.out()) { 136 | return super::format(str.c_str(), ctx); 137 | } 138 | }; 139 | 140 | template 141 | inline 142 | auto sys_format(fmt::format_string fmtstr, Args &&... args) -> sys_string { 143 | sys_string_builder builder; 144 | fmt::format_to(std::back_inserter(builder.chars()), fmtstr, std::forward(args)...); 145 | return builder.build(); 146 | } 147 | 148 | 149 | #endif 150 | -------------------------------------------------------------------------------- /src/sys_util.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022, Eugene Gershnik 2 | // SPDX-License-Identifier: BSD-3-Clause 3 | 4 | #include "sys_util.h" 5 | 6 | #if HAVE_OS_LOG 7 | 8 | os_log_t OsLogHandle::s_handle = nullptr; 9 | const char * OsLogHandle::s_category = "main"; 10 | 11 | #endif 12 | 13 | #if !HAVE_APPLE_USER_CREATION 14 | 15 | static auto runCreateDaemonUserCommands([[maybe_unused]] const sys_string & name) -> bool { 16 | 17 | #if defined(__linux__) && defined(USERADD_PATH) 18 | 19 | (void)run({USERADD_PATH, "-r", "-d", WSDDN_DEFAULT_CHROOT_DIR, "-s", "/bin/false", name.c_str()}); 20 | return true; 21 | 22 | #elif defined(__linux__) && defined(IS_ALPINE_LINUX) && defined(ADDUSER_PATH) && defined(ADDGROUP_PATH) 23 | 24 | //The second addgroup instead of -G for adduser is necessary since for some reason -G doesn't 25 | //modify /etc/group when run from here 26 | (void)run({ADDGROUP_PATH, "-S", name.c_str()}); 27 | (void)run({ADDUSER_PATH, "-S", "-D", "-H", "-h", "/var/empty", "-s", "/sbin/nologin", "-g", name.c_str(), name.c_str()}); 28 | (void)run({ADDGROUP_PATH, name.c_str(), name.c_str()}); 29 | return true; 30 | 31 | #elif (defined(__OpenBSD__) || defined(__NetBSD__)) && defined(USERADD_PATH) 32 | 33 | createMissingDirs(WSDDN_DEFAULT_CHROOT_DIR, S_IRUSR | S_IWUSR | S_IXUSR | S_IRGRP | S_IXGRP, Identity::admin()); 34 | (void)run({USERADD_PATH, "-L", "daemon", "-g", "=uid", "-d", WSDDN_DEFAULT_CHROOT_DIR, "-s", "/sbin/nologin", "-c", "WS-Discovery Daemon", name.c_str()}); 35 | return true; 36 | 37 | #elif defined(__HAIKU__) && defined(USERADD_PATH) && defined(GROUPADD_PATH) 38 | 39 | (void)run({GROUPADD_PATH, name.c_str()}); 40 | (void)run({USERADD_PATH, "-g", name.c_str(), "-d", WSDDN_DEFAULT_CHROOT_DIR, "-s", "/bin/false", "-n", "WS-Discovery Daemon", name.c_str()}); 41 | return true; 42 | 43 | #elif defined(__FreeBSD__) && defined(PW_PATH) 44 | 45 | (void)run({PW_PATH, "adduser", name.c_str(), "-d", WSDDN_DEFAULT_CHROOT_DIR, "-s", "/usr/sbin/nologin", "-c", "WS-Discovery Daemon User"}); 46 | return true; 47 | 48 | #elif defined(__sun) && defined(USERADD_PATH) && defined(GROUPADD_PATH) 49 | 50 | (void)run({GROUPADD_PATH, name.c_str()}); 51 | (void)run({USERADD_PATH, "-g", name.c_str(), "-d", WSDDN_DEFAULT_CHROOT_DIR, "-s", "/bin/false", "-c", "WS-Discovery Daemon User", name.c_str()}); 52 | return true; 53 | 54 | #else 55 | 56 | return false; 57 | 58 | #endif 59 | } 60 | 61 | auto Identity::createDaemonUser(const sys_string & name) -> std::optional { 62 | 63 | if (!runCreateDaemonUserCommands(name)) 64 | return {}; 65 | auto pwd = ptl::Passwd::getByName(name); 66 | if (!pwd) 67 | throw std::runtime_error(fmt::format("unable to create user {}", name)); 68 | return Identity(pwd->pw_uid, pwd->pw_gid); 69 | } 70 | 71 | #endif 72 | 73 | int run(const ptl::StringRefArray & args) { 74 | ptl::SpawnAttr spawnAttr; 75 | #ifndef __HAIKU__ 76 | spawnAttr.setFlags(POSIX_SPAWN_SETSIGDEF); 77 | auto sigs = ptl::SignalSet::all(); 78 | sigs.del(SIGKILL); 79 | sigs.del(SIGSTOP); 80 | spawnAttr.setSigDefault(sigs); 81 | #endif 82 | 83 | auto proc = spawn(args, ptl::SpawnSettings().attr(spawnAttr).usePath()); 84 | 85 | auto stat = proc.wait().value(); 86 | if (WIFEXITED(stat)) 87 | return WEXITSTATUS(stat); 88 | if (WIFSIGNALED(stat)) 89 | return 128+WTERMSIG(stat); 90 | throw std::runtime_error(fmt::format("`{} finished with status 0x{:X}`", args, stat)); 91 | } 92 | 93 | void shell(const ptl::StringRefArray & args, bool suppressStdErr, std::function reader) { 94 | auto [read, write] = ptl::Pipe::create(); 95 | ptl::SpawnAttr spawnAttr; 96 | #ifndef __HAIKU__ 97 | spawnAttr.setFlags(POSIX_SPAWN_SETSIGDEF); 98 | auto sigs = ptl::SignalSet::all(); 99 | sigs.del(SIGKILL); 100 | sigs.del(SIGSTOP); 101 | spawnAttr.setSigDefault(sigs); 102 | #endif 103 | 104 | ptl::SpawnFileActions act; 105 | act.addDuplicateTo(write, stdout); 106 | act.addClose(read); 107 | if (suppressStdErr) { 108 | act.addOpen(stderr, "/dev/null", O_WRONLY, 0); 109 | } 110 | auto proc = spawn(args, ptl::SpawnSettings().attr(spawnAttr).fileActions(act).usePath()); 111 | write.close(); 112 | 113 | reader(read); 114 | 115 | auto stat = proc.wait().value(); 116 | if (WIFEXITED(stat)) { 117 | auto res = WEXITSTATUS(stat); 118 | if (res == 0) 119 | return; 120 | 121 | throw std::runtime_error(fmt::format("`{} exited with code {}`", args, res)); 122 | } 123 | if (WIFSIGNALED(stat)) { 124 | throw std::runtime_error(fmt::format("`{} exited due to signal {}`", args, WTERMSIG(stat))); 125 | } 126 | throw std::runtime_error(fmt::format("`{} finished with status 0x{:X}`", args, stat)); 127 | } 128 | -------------------------------------------------------------------------------- /installers/openbsd/build.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python3 2 | 3 | # Copyright (c) 2022, Eugene Gershnik 4 | # SPDX-License-Identifier: BSD-3-Clause 5 | 6 | import sys 7 | import subprocess 8 | import shutil 9 | import re 10 | import argparse 11 | from pathlib import Path 12 | 13 | ARCH = subprocess.run(['uname', '-m'], check=True, capture_output=True, encoding="utf-8").stdout.strip() 14 | 15 | mydir = Path(sys.argv[0]).parent 16 | 17 | sys.path.append(str(mydir.absolute().parent)) 18 | 19 | from common import getSrcVersion, buildCode, installCode, copyTemplated 20 | 21 | parser = argparse.ArgumentParser() 22 | 23 | parser.add_argument('srcdir', type=Path) 24 | parser.add_argument('builddir', type=Path) 25 | parser.add_argument('--upload-results', dest='uploadResults', action='store_true', required=False) 26 | #parser.add_argument('--arch', required=False) 27 | 28 | args = parser.parse_args() 29 | 30 | srcdir: Path = args.srcdir 31 | builddir: Path = args.builddir 32 | 33 | buildCode(builddir) 34 | 35 | VERSION = getSrcVersion(srcdir) 36 | 37 | workdir = builddir / 'stage/openbsd' 38 | stagedir = workdir / 'root' 39 | shutil.rmtree(workdir, ignore_errors=True) 40 | stagedir.mkdir(parents=True) 41 | 42 | installCode(builddir, stagedir / 'usr/local') 43 | 44 | shutil.copytree(srcdir / 'config/openbsd', stagedir, dirs_exist_ok=True) 45 | docdir = stagedir / 'usr/local/share/doc/wsddn' 46 | docdir.mkdir(parents=True) 47 | shutil.copy(srcdir / 'LICENSE', docdir / 'LICENSE') 48 | shutil.copy(srcdir / 'Acknowledgements.md', docdir / 'Acknowledgements.md') 49 | 50 | copyTemplated(mydir.parent / 'wsddn.conf', stagedir / 'etc/wsddn/wsddn.conf.sample', { 51 | 'SAMPLE_IFACE_NAME': "em0", 52 | 'RELOAD_INSTRUCTIONS': """ 53 | # sudo rcctl reload wsddn 54 | # or 55 | # sudo kill -HUP $( /dev/null 2>&1; then rcctl stop wsddn; fi 88 | @unexec-delete rm -rf /var/run/wsddn.pid 89 | @unexec-delete rm -rf /var/log/wsddn.* 90 | 91 | @mode 755 92 | @bin usr/local/bin/wsddn 93 | @rcscript etc/rc.d/wsddn 94 | @dir etc/wsddn 95 | 96 | @mode 644 97 | @man usr/local/man/man8/wsddn.8.gz 98 | @file etc/wsddn/wsddn.conf.sample 99 | @sample etc/wsddn/wsddn.conf 100 | 101 | @newgroup _wsddn: 102 | @newuser _wsddn::_wsddn:daemon:WS-Discovery Daemon:/var/empty:/sbin/nologin 103 | 104 | @exec-add grep -qxF '/var/log/wsddn.log' /etc/newsyslog.conf || echo '{LOGCONF}' >> /etc/newsyslog.conf 105 | @unexec-delete sed -i '/^\/var\/log\/wsddn.log/d' /etc/newsyslog.conf 106 | 107 | """.lstrip()) 108 | 109 | subprocess.run(['pkg_create', '-v', 110 | '-A', ARCH, 111 | '-d', f'-{DESC}', 112 | '-f', 'packinglist', 113 | '-D', 'FULLPKGPATH=net/wsddn', 114 | '-D', f'COMMENT={COMMENT}', 115 | '-D', f'MAINTAINER={MAINTAINER}', 116 | '-D', f'HOMEPAGE={HOMEPAGE}', 117 | '-B', stagedir.resolve(), 118 | '-p', '/'] + 119 | libs + [ 120 | f'wsddn-{VERSION}.tgz'], cwd=workdir, check=True) 121 | 122 | subprocess.run(['gzip', '--keep', '--force', builddir / 'wsddn'], check=True) 123 | 124 | if args.uploadResults: 125 | subprocess.run(['aws', 's3', 'cp', 126 | workdir / f'wsddn-{VERSION}.tgz', f's3://gershnik-builds/openbsd/wsddn-{VERSION}-{ARCH}.tgz'], 127 | check=True) 128 | subprocess.run(['aws', 's3', 'cp', 129 | builddir / 'wsddn.gz', f's3://wsddn-symbols/wsddn-openbsd-{VERSION}-{ARCH}.tgz'], check=True) 130 | 131 | shutil.move(workdir / f'wsddn-{VERSION}.tgz', workdir / f'wsddn-{VERSION}-OpenBSD-{ARCH}.tgz') 132 | subprocess.run(['gh', 'release', 'upload', f'v{VERSION}', workdir / f'wsddn-{VERSION}-OpenBSD-{ARCH}.tgz'], 133 | check=True) 134 | 135 | -------------------------------------------------------------------------------- /installers/deb/build.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env -S python3 -u 2 | 3 | # Copyright (c) 2022, Eugene Gershnik 4 | # SPDX-License-Identifier: BSD-3-Clause 5 | 6 | import sys 7 | import subprocess 8 | import shutil 9 | import hashlib 10 | import gzip 11 | from pathlib import Path 12 | 13 | RELEASE = '1' 14 | ARCH = subprocess.run(['dpkg-architecture', '-q', 'DEB_HOST_ARCH'], check=True, capture_output=True, encoding="utf-8").stdout.strip() 15 | #CODENAME = subprocess.run(['lsb_release', '-sc'], check=True, capture_output=True, encoding="utf-8").stdout.strip() 16 | 17 | mydir = Path(sys.argv[0]).parent 18 | 19 | sys.path.append(str(mydir.absolute().parent)) 20 | 21 | from common import parseCommandLine, getVersion, buildCode, installCode, copyTemplated 22 | 23 | args = parseCommandLine() 24 | srcdir: Path = args.srcdir 25 | builddir: Path = args.builddir 26 | 27 | buildCode(builddir) 28 | 29 | VERSION = getVersion(builddir) 30 | 31 | workdir = builddir / 'stage/deb' 32 | stagedir = workdir / f'wsddn_{VERSION}-{RELEASE}_{ARCH}' 33 | shutil.rmtree(workdir, ignore_errors=True) 34 | stagedir.mkdir(parents=True) 35 | 36 | installCode(builddir, stagedir / 'usr') 37 | 38 | shutil.copytree(srcdir / 'config/systemd/usr', stagedir / 'usr', dirs_exist_ok=True) 39 | shutil.copytree(srcdir / 'config/firewalls/etc/ufw', stagedir / 'etc/ufw', dirs_exist_ok=True) 40 | shutil.copytree(srcdir / 'config/firewalls/etc/firewalld', stagedir / 'usr/lib/firewalld', dirs_exist_ok=True) 41 | shutil.copytree(srcdir / 'config/sysv/etc', stagedir / 'etc', dirs_exist_ok=True) 42 | 43 | docdir = stagedir / 'usr/share/doc/wsddn' 44 | docdir.mkdir(parents=True) 45 | shutil.copy(mydir / 'copyright', docdir / 'copyright') 46 | shutil.copy(srcdir / 'Acknowledgements.md', docdir / 'Acknowledgements.md') 47 | with open(srcdir / 'CHANGELOG.md', 'rb') as f_in: 48 | with gzip.open(docdir / 'changelog.gz', 'wb') as f_out: 49 | shutil.copyfileobj(f_in, f_out) 50 | 51 | copyTemplated(mydir.parent / 'wsddn.conf', stagedir / "etc/wsddn.conf", { 52 | 'SAMPLE_IFACE_NAME': "eth0", 53 | 'RELOAD_INSTRUCTIONS': """ 54 | # sudo systemctl reload wsddn 55 | """.lstrip() 56 | }) 57 | 58 | 59 | def calc_sizes(): 60 | md5sums = '' 61 | total_size = 0 62 | buffer = bytearray(4096) 63 | view = memoryview(buffer) 64 | for item in stagedir.rglob('*'): 65 | if not item.is_file(): 66 | continue 67 | relpath = item.relative_to(stagedir) 68 | if not relpath.parts[0] == 'etc': 69 | md5 = hashlib.md5() 70 | with open(item, "rb") as f: 71 | while True: 72 | size = f.readinto(buffer) 73 | if size == 0: 74 | break 75 | total_size += size 76 | md5.update(view[:size]) 77 | md5sums += f'{md5.hexdigest()} {item.relative_to(stagedir)}\n' 78 | else: 79 | total_size += item.stat().st_size 80 | total_size = int(round(total_size / 1024.)) 81 | return total_size, md5sums 82 | 83 | total_size, md5sums = calc_sizes() 84 | 85 | debiandir = stagedir/ 'DEBIAN' 86 | debiandir.mkdir() 87 | 88 | # on case insensitive filesystem we don't need and cannot create lowercase 'debian' 89 | if not (stagedir / 'debian').exists(): 90 | (stagedir / 'debian').symlink_to(debiandir.absolute()) 91 | 92 | control = debiandir / 'control' 93 | 94 | control.write_text( 95 | f""" 96 | Package: wsddn 97 | Source: wsddn 98 | Version: {VERSION} 99 | Architecture: {ARCH} 100 | Installed-Size: {total_size} 101 | Depends: {{shlibs_Depends}} 102 | Maintainer: Eugene Gershnik 103 | Homepage: https://github.com/gershnik/wsdd-native 104 | Description: WS-Discovery Host Daemon 105 | Allows your Linux machine to be discovered by Windows 10 and above systems and displayed by their Explorer "Network" views. 106 | 107 | """.lstrip()) 108 | 109 | shutil.copy(mydir / 'preinst', debiandir / 'preinst') 110 | shutil.copy(mydir / 'prerm', debiandir / 'prerm') 111 | shutil.copy(mydir / 'postinst', debiandir / 'postinst') 112 | shutil.copy(mydir / 'postrm', debiandir / 'postrm') 113 | shutil.copy(mydir / 'copyright', debiandir / 'copyright') 114 | 115 | (debiandir / 'conffiles').write_text(""" 116 | /etc/init.d/wsddn 117 | /etc/ufw/applications.d/wsddn 118 | /etc/wsddn.conf 119 | """.lstrip()) 120 | 121 | (debiandir / 'md5sums').write_text(md5sums) 122 | 123 | deps = subprocess.run(['dpkg-shlibdeps', '-O', '-eusr/bin/wsddn'], 124 | check=True, cwd=stagedir, stdout=subprocess.PIPE, encoding="utf-8").stdout.strip() 125 | key, val = deps.split('=', 1) 126 | key = key.replace(':', "_") 127 | control.write_text(control.read_text().format_map({key: val})) 128 | 129 | if (stagedir / 'debian').is_symlink(): 130 | (stagedir / 'debian').unlink() 131 | 132 | subprocess.run(['dpkg-deb', '--build', '--root-owner-group', stagedir, workdir], check=True) 133 | 134 | subprocess.run(['gzip', '--keep', '--force', builddir / 'wsddn'], check=True) 135 | 136 | deb = list(workdir.glob('*.deb'))[0] 137 | 138 | if args.uploadResults: 139 | subprocess.run(['aws', 's3', 'cp', deb, 's3://gershnik-builds/apt/'], check=True) 140 | subprocess.run(['aws', 's3', 'cp', builddir / 'wsddn.gz', f's3://wsddn-symbols/wsddn-deb-{VERSION}-{ARCH}.gz'], check=True) 141 | subprocess.run(['gh', 'release', 'upload', f'v{VERSION}', deb], check=True) 142 | -------------------------------------------------------------------------------- /src/util.h: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022, Eugene Gershnik 2 | // SPDX-License-Identifier: BSD-3-Clause 3 | 4 | #ifndef HEADER_UTIL_H_INCLUDED 5 | #define HEADER_UTIL_H_INCLUDED 6 | 7 | /* 8 | Portable utilities 9 | */ 10 | 11 | #define WSDDN_DECLARE_MEMBER_DETECTOR(type, member, name) \ 12 | struct name##_detector { \ 13 | template \ 14 | static std::true_type detect(decltype(T::member) *); \ 15 | template \ 16 | static std::false_type detect(...); \ 17 | }; \ 18 | constexpr bool name = decltype(name##_detector::detect(nullptr))::value 19 | 20 | enum AllowedAddressFamily { 21 | BothIPv4AndIPv6, 22 | IPv4Only, 23 | IPv6Only 24 | }; 25 | 26 | struct WindowsDomain { 27 | template 28 | WindowsDomain(Args && ...args): name(std::forward(args)...) {} 29 | 30 | sys_string name; 31 | }; 32 | 33 | struct WindowsWorkgroup { 34 | template 35 | WindowsWorkgroup(Args && ...args): name(std::forward(args)...) {} 36 | 37 | sys_string name; 38 | }; 39 | 40 | using MemberOf = std::variant; 41 | 42 | enum class DaemonType { 43 | Unix 44 | #if HAVE_SYSTEMD 45 | , Systemd 46 | #endif 47 | #if HAVE_LAUNCHD 48 | , Launchd 49 | #endif 50 | }; 51 | 52 | struct NetworkInterface { 53 | NetworkInterface(int idx, const char * first, const char * last): 54 | name(first, last), 55 | index(idx) { 56 | } 57 | NetworkInterface(int idx, const sys_string & n): 58 | name(n), 59 | index(idx) { 60 | } 61 | 62 | friend auto operator<=>(const NetworkInterface & lhs, const NetworkInterface & rhs) -> std::strong_ordering { 63 | if (auto res = lhs.name <=> rhs.name; res != 0) 64 | return res; 65 | return lhs.index <=> rhs.index; 66 | } 67 | friend auto operator==(const NetworkInterface & lhs, const NetworkInterface & rhs) -> bool { 68 | return lhs.name == rhs.name && lhs.index == rhs.index; 69 | } 70 | friend auto operator!=(const NetworkInterface & lhs, const NetworkInterface & rhs) -> bool { 71 | return !(lhs == rhs); 72 | } 73 | 74 | sys_string name; 75 | int index; 76 | }; 77 | 78 | template <> struct fmt::formatter { 79 | 80 | constexpr auto parse(format_parse_context& ctx) -> decltype(ctx.begin()) { 81 | auto it = ctx.begin(), end = ctx.end(); 82 | if (it != end && *it != '}') throw format_error("invalid format"); 83 | return it; 84 | } 85 | template 86 | auto format(const NetworkInterface & iface, FormatContext & ctx) const -> decltype(ctx.out()) { 87 | return fmt::format_to(ctx.out(), "{}(idx: {})", iface.name, iface.index); 88 | } 89 | }; 90 | 91 | template 92 | class RefCountedContainerBuffer{ 93 | public: 94 | RefCountedContainerBuffer(T && data): 95 | m_dataPtr(refcnt_attach(new ref_counted_adapter(std::move(data)))), 96 | m_buffer(m_dataPtr->data(), m_dataPtr->size()) { 97 | 98 | } 99 | using value_type = asio::const_buffer; 100 | using const_iterator = const asio::const_buffer *; 101 | 102 | auto begin() const -> const_iterator { return &m_buffer; } 103 | auto end() const -> const_iterator { return &m_buffer + 1; } 104 | 105 | private: 106 | refcnt_ptr> m_dataPtr; 107 | asio::const_buffer m_buffer; 108 | }; 109 | 110 | 111 | inline auto makeHttpUrl(const ip::tcp::endpoint & endp) -> sys_string { 112 | auto addr = endp.address(); 113 | if (addr.is_v4()) { 114 | return fmt::format("http://{0}:{1}", addr.to_string(), endp.port()); 115 | } else { 116 | auto addr6 = addr.to_v6(); 117 | addr6.scope_id(0); 118 | return fmt::format("http://[{0}]:{1}", addr6.to_string(), endp.port()); 119 | } 120 | } 121 | 122 | inline sys_string to_urn(const Uuid & val) { 123 | std::array buf; 124 | val.to_chars(buf, Uuid::lowercase); 125 | 126 | sys_string_builder builder; 127 | builder.reserve_storage(46); 128 | builder.append(S("urn:uuid:")); 129 | builder.append(buf.data(), buf.size()); 130 | return builder.build(); 131 | } 132 | 133 | inline sys_string to_sys_string(const Uuid & val) { 134 | std::array buf; 135 | val.to_chars(buf, Uuid::lowercase); 136 | return sys_string(buf.data(), buf.size()); 137 | } 138 | 139 | 140 | template <> struct fmt::formatter { 141 | 142 | constexpr auto parse(format_parse_context& ctx) -> decltype(ctx.begin()) { 143 | auto it = ctx.begin(), end = ctx.end(); 144 | if (it != end && *it != '}') throw format_error("invalid format"); 145 | return it; 146 | } 147 | template 148 | auto format(const ptl::StringRefArray & args, FormatContext & ctx) const -> decltype(ctx.out()) { 149 | auto dest = ctx.out(); 150 | *dest++ = '['; 151 | if (auto * str = args.data()) { 152 | dest = fmt::format_to(dest, "\"{}\"", *str); 153 | for (++str; *str; ++str) { 154 | dest = fmt::format_to(dest, ", \"{}\"", *str); 155 | } 156 | } 157 | *dest++ = ']'; 158 | return dest; 159 | } 160 | }; 161 | 162 | template 163 | constexpr decltype(auto) makeDependentOn(Arg && arg) { 164 | return std::forward(arg); 165 | } 166 | 167 | 168 | extern std::mt19937 g_Random; 169 | 170 | 171 | #endif 172 | -------------------------------------------------------------------------------- /src/config.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022, Eugene Gershnik 2 | // SPDX-License-Identifier: BSD-3-Clause 3 | 4 | #include "config.h" 5 | #include "command_line.h" 6 | 7 | Config::Config(const CommandLine & cmdline): 8 | m_instanceIdentifier(time(nullptr)), 9 | m_pageSize(size_t(ptl::systemConfig(_SC_PAGESIZE).value_or(4096))) { 10 | 11 | m_hopLimit = cmdline.hoplimit.value_or(1); 12 | m_allowedAddressFamily = cmdline.allowedAddressFamily.value_or(BothIPv4AndIPv6); 13 | m_interfaceWhitelist.insert(cmdline.interfaces.begin(), cmdline.interfaces.end()); 14 | m_sourcePort = cmdline.sourcePort.value_or(0); 15 | 16 | m_fullHostName = getHostName(); 17 | m_simpleHostName = m_fullHostName.prefix_before_first(U'.').value_or(m_fullHostName); 18 | 19 | if (cmdline.uuid) { 20 | m_uuid = *cmdline.uuid; 21 | } else { 22 | using namespace muuid; 23 | m_uuid = uuid::generate_sha1(uuid("49DAC291-0608-41C9-941C-ED0E7ACCDE1E"), 24 | {m_fullHostName.c_str(), m_fullHostName.storage_size()}); 25 | } 26 | m_strUuid = to_sys_string(m_uuid); 27 | m_urnUuid = to_urn(m_uuid); 28 | 29 | bool useNetbiosHostName = cmdline.hostname && cmdline.hostname->empty(); 30 | 31 | std::optional systemWinNetInfo; 32 | #if HAVE_APPLE_SAMBA 33 | systemWinNetInfo = detectAppleWinNetInfo(useNetbiosHostName); 34 | #elif CAN_HAVE_SAMBA 35 | systemWinNetInfo = detectWinNetInfo(cmdline.smbConf, useNetbiosHostName); 36 | #endif 37 | 38 | if (cmdline.memberOf) { 39 | m_winNetInfo.memberOf = *cmdline.memberOf; 40 | } else if (systemWinNetInfo) { 41 | m_winNetInfo.memberOf = systemWinNetInfo->memberOf; 42 | } else { 43 | m_winNetInfo.memberOf.emplace(S("WORKGROUP")); 44 | } 45 | 46 | if (cmdline.hostname && !cmdline.hostname->empty()) { 47 | //explict hostname specified 48 | m_winNetInfo.hostName = *cmdline.hostname; 49 | } else if (systemWinNetInfo) { 50 | //we have detected hostname 51 | m_winNetInfo.hostName = systemWinNetInfo->hostName; 52 | } else { 53 | if (useNetbiosHostName) 54 | m_winNetInfo.hostName = m_simpleHostName.to_upper(); 55 | else 56 | m_winNetInfo.hostName = m_simpleHostName; 57 | } 58 | 59 | if (systemWinNetInfo) 60 | m_winNetInfo.hostDescription = systemWinNetInfo->hostDescription; 61 | 62 | if (m_winNetInfo.hostDescription.empty()) { 63 | if (cmdline.hostname && !cmdline.hostname->empty()) 64 | m_winNetInfo.hostDescription = *cmdline.hostname; 65 | else 66 | m_winNetInfo.hostDescription = m_simpleHostName; 67 | } 68 | 69 | 70 | auto [memberOfType, memberOfName] = std::visit([](auto & val) { 71 | 72 | using ArgType = std::remove_cvref_t; 73 | 74 | if constexpr (std::is_same_v) 75 | return std::make_pair(S("Workgoup"), val.name); 76 | else if constexpr (std::is_same_v) 77 | return std::make_pair(S("Domain"), val.name); 78 | else 79 | static_assert(makeDependentOn(false), "unhandled type"); 80 | 81 | }, m_winNetInfo.memberOf); 82 | 83 | if (cmdline.metadataFile) { 84 | m_metadataDoc = loadMetadaFile(cmdline.metadataFile->native()); 85 | } 86 | 87 | WSDLOG_INFO("Configuration:\n" 88 | " Hostname: {}\n" 89 | " {}: {}\n" 90 | " Description: {}\n" 91 | " Identifier: {}\n" 92 | " Metadata: {}", 93 | m_winNetInfo.hostName, 94 | memberOfType, memberOfName, 95 | m_winNetInfo.hostDescription, 96 | endpointIdentifier(), 97 | m_metadataDoc ? cmdline.metadataFile->c_str() : "default"); 98 | } 99 | 100 | auto Config::getHostName() const -> sys_string { 101 | auto size = size_t(ptl::systemConfig(_SC_HOST_NAME_MAX).value_or(_POSIX_HOST_NAME_MAX)); 102 | sys_string_builder builder; 103 | auto & buf = builder.chars(); 104 | buf.resize(size + 1); 105 | ptl::getHostName({buf.begin(), buf.end()}); 106 | builder.resize_storage(strlen(buf.begin())); 107 | return builder.build(); 108 | } 109 | 110 | auto Config::loadMetadaFile(const std::string & filename) const -> std::unique_ptr { 111 | auto file = ptl::FileDescriptor::open(filename, O_RDONLY); 112 | std::vector buf(m_pageSize); 113 | try { 114 | auto read = readFile(file, buf.data(), buf.size()); 115 | if (read < 4) 116 | throw std::runtime_error(fmt::format("metada file {} is invalid", filename)); 117 | auto templateParsingCtx = XmlParserContext::createPush(buf.data(), int(read), filename.c_str()); 118 | for ( ; ; ) { 119 | read = readFile(file, buf.data(), buf.size()); 120 | templateParsingCtx->parseChunk(buf.data(), int(read), read == 0); 121 | if (!read) { 122 | break; 123 | } 124 | } 125 | 126 | if (!templateParsingCtx->wellFormed()) 127 | throw std::runtime_error(fmt::format("metada file {} is not well formed XML", filename)); 128 | return templateParsingCtx->extractDoc(); 129 | 130 | } catch (XmlException & ex) { 131 | throw std::runtime_error(fmt::format("metada file {} is not a valid XML", filename)); 132 | } 133 | 134 | } 135 | 136 | -------------------------------------------------------------------------------- /.github/workflows/check.yml: -------------------------------------------------------------------------------- 1 | name: Check 2 | 3 | on: 4 | push: 5 | branches: 6 | - '*' 7 | - '*/**' 8 | paths-ignore: 9 | - 'README.md' 10 | - '.gitignore' 11 | - 'LICENSE' 12 | - 'CHANGELOG.md' 13 | - 'SECURITY.md' 14 | - 'Acknowledgements.md' 15 | - 'config/metadata/**' 16 | - '.github/workflows/publish.yml' 17 | - '.github/workflows/build.yml' 18 | - 'tools/**' 19 | workflow_dispatch: 20 | 21 | 22 | jobs: 23 | linux: 24 | runs-on: ${{ matrix.os }} 25 | strategy: 26 | fail-fast: false 27 | matrix: 28 | include: 29 | - {os: ubuntu-22.04, compiler: gcc, version: 11 } 30 | - {os: ubuntu-22.04, compiler: gcc, version: 12 } 31 | - {os: ubuntu-22.04, compiler: gcc, version: 13 } 32 | - {os: ubuntu-24.04, compiler: gcc, version: 14 } 33 | 34 | - {os: ubuntu-22.04, compiler: clang, version: 13 } 35 | - {os: ubuntu-22.04, compiler: clang, version: 14 } 36 | - {os: ubuntu-22.04, compiler: clang, version: 15 } 37 | - {os: ubuntu-22.04, compiler: clang, version: 16 } 38 | - {os: ubuntu-24.04, compiler: clang, version: 17 } 39 | - {os: ubuntu-24.04, compiler: clang, version: 18 } 40 | - {os: ubuntu-24.04, compiler: clang, version: 19 } 41 | - {os: ubuntu-24.04, compiler: clang, version: 20 } 42 | - {os: ubuntu-24.04, compiler: clang, version: 21 } 43 | steps: 44 | - name: Checkout 45 | uses: actions/checkout@v4 46 | 47 | - name: Setup Linux 48 | run: | 49 | sudo apt-get update 50 | sudo apt-get install -y libsystemd-dev 51 | 52 | if [[ '${{ matrix.compiler }}' == 'clang' ]]; then 53 | wget https://apt.llvm.org/llvm.sh 54 | chmod u+x llvm.sh 55 | sudo ./llvm.sh ${{ matrix.version }} 56 | sudo apt-get install -y clang-tools-${{ matrix.version }} libc++-${{ matrix.version }}-dev libc++abi-${{ matrix.version }}-dev 57 | echo "CC=clang-${{ matrix.version }}" >> $GITHUB_ENV 58 | echo "CXX=clang++-${{ matrix.version }}" >> $GITHUB_ENV 59 | echo "CXXFLAGS=-stdlib=libc++" >> $GITHUB_ENV 60 | fi 61 | 62 | if [[ '${{ matrix.compiler }}' == 'gcc' ]]; then 63 | sudo add-apt-repository -y ppa:ubuntu-toolchain-r/test 64 | sudo apt-get update 65 | sudo apt-get install -y gcc-${{ matrix.version }} g++-${{ matrix.version }} 66 | echo "CC=gcc-${{ matrix.version }}" >> $GITHUB_ENV 67 | echo "CXX=g++-${{ matrix.version }}" >> $GITHUB_ENV 68 | fi 69 | 70 | - name: Configure 71 | run: | 72 | [ -d "out" ] && tools/uncache out 73 | cmake -S . -B out \ 74 | -DCMAKE_BUILD_TYPE=RelWithDebInfo 75 | 76 | - name: Build 77 | run: | 78 | installers/deb/build.py . out 79 | 80 | container: 81 | runs-on: ubuntu-latest 82 | container: ${{ matrix.container }} 83 | strategy: 84 | fail-fast: false 85 | matrix: 86 | container: [gcc:15.1] 87 | 88 | steps: 89 | - name: Checkout 90 | uses: actions/checkout@v4 91 | 92 | - name: Install pre-requisites 93 | run: | 94 | apt-get update 95 | apt-get install -y ninja-build cmake 96 | apt-get install -y python3-dev libsystemd-dev 97 | 98 | - name: Configure 99 | shell: bash 100 | run: | 101 | [ -d "out" ] && tools/uncache out 102 | cmake -G Ninja -S . -B out \ 103 | -DCMAKE_BUILD_TYPE=RelWithDebInfo 104 | 105 | - name: Build and Test 106 | shell: bash 107 | run: | 108 | cmake --build out 109 | 110 | mac: 111 | runs-on: ${{ matrix.os }} 112 | strategy: 113 | fail-fast: false 114 | matrix: 115 | include: 116 | - { os: macos-14, xcode: '15.4.0' } 117 | - { os: macos-15, xcode: '16.4.0' } 118 | - { os: macos-15-intel, xcode: '16.4.0' } 119 | - { os: macos-26, xcode: '26.0' } 120 | env: 121 | DEVELOPER_DIR: /Applications/Xcode_${{ matrix.xcode }}.app 122 | steps: 123 | - name: Checkout 124 | uses: actions/checkout@v4 125 | 126 | - name: Configure 127 | run: | 128 | [ -d "out" ] && tools/uncache out 129 | cmake -S . -B out \ 130 | -DCMAKE_BUILD_TYPE=RelWithDebInfo \ 131 | "-DCMAKE_OSX_ARCHITECTURES=x86_64;arm64" \ 132 | -DCMAKE_IGNORE_PREFIX_PATH=/opt/local \ 133 | -DWSDDN_PREFER_SYSTEM_LIBXML2=ON 134 | 135 | - name: Build 136 | run: | 137 | installers/mac/build.py . out 138 | 139 | others: 140 | concurrency: ${{ matrix.remote_host }} 141 | runs-on: [self-hosted, server] 142 | strategy: 143 | fail-fast: false 144 | matrix: 145 | remote_host: 146 | - centos-9 147 | - alpine-3 148 | - archlinux 149 | - netbsd-10 150 | - omnios 151 | - haiku 152 | 153 | steps: 154 | - name: Run remote build 155 | run: | 156 | "$RUNNER_TOOLS_PATH"/run-agent gh-${{ matrix.remote_host }} <<'EOF' 157 | set -e 158 | if [ ! -d work/wsdd-native ]; then 159 | git clone https://github.com/gershnik/wsdd-native.git work/wsdd-native 160 | fi 161 | cd work/wsdd-native 162 | git fetch --all 163 | git fetch -f --prune --tags 164 | git reset --hard ${{ github.sha }} 165 | [ -d "out" ] && tools/uncache out 166 | cmake -S . -B out -DCMAKE_BUILD_TYPE=RelWithDebInfo 167 | cmake --build out --target wsddn 168 | EOF 169 | -------------------------------------------------------------------------------- /cmake/dependencies.cmake: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2022, Eugene Gershnik 2 | # SPDX-License-Identifier: BSD-3-Clause 3 | 4 | include(FetchContent) 5 | 6 | if (DEFINED CACHE{libxml2_SOURCE_DIR} AND NOT DEFINED CACHE{WSDDN_DEPENDENCIES_VERSION}) 7 | message(FATAL_ERROR 8 | "Your existing CMake cache cannot be used due to incompatible changes." 9 | "Please delete ${CMAKE_BINARY_DIR}/CMakeCache.txt and rebuild. (sorry!)") 10 | endif() 11 | set(WSDDN_DEPENDENCIES_VERSION 1 CACHE INTERNAL "version of dependencies config") 12 | 13 | 14 | if (NOT DEFINED WSDDN_PREFER_SYSTEM_LIBXML2) 15 | #By default prefer system libxml2 on Mac and source compiled on other platforms 16 | if (${CMAKE_SYSTEM_NAME} STREQUAL "Darwin") 17 | set(WSDDN_PREFER_SYSTEM_LIBXML2 ON) 18 | else() 19 | set(WSDDN_PREFER_SYSTEM_LIBXML2 OFF) 20 | endif() 21 | endif() 22 | 23 | file(READ dependencies.json DEPENDECIES_JSON) 24 | set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS dependencies.json) 25 | 26 | 27 | set(DECLARED_DEPENDENCIES "") 28 | 29 | function(fetch_dependency name #extras for FetchContent_Declare 30 | ) 31 | string(JSON version GET "${DEPENDECIES_JSON}" ${name} version) 32 | string(JSON url GET "${DEPENDECIES_JSON}" ${name} url) 33 | string(JSON md5 GET "${DEPENDECIES_JSON}" ${name} md5) 34 | string(REPLACE "\$\{version\}" ${version} url "${url}") 35 | string(TOUPPER ${name} uname) 36 | string(TOLOWER ${name} lname) 37 | 38 | set(extras "") 39 | foreach(i RANGE 1 ${ARGC}) 40 | list(APPEND extras ${ARGV${i}}) 41 | endforeach() 42 | 43 | if ($CACHE{LAST_WSDDN_PREFER_SYSTEM_${uname}}) 44 | set(old_prefer 1) 45 | else() 46 | set(old_prefer 0) 47 | endif() 48 | 49 | if (WSDDN_PREFER_SYSTEM_${uname}) 50 | set(new_prefer 1) 51 | else() 52 | set(new_prefer 0) 53 | endif() 54 | 55 | if (NOT ${new_prefer} EQUAL ${old_prefer}) 56 | unset(${lname}_POPULATED CACHE) 57 | unset(${lname}_SOURCE_DIR CACHE) 58 | unset(${lname}_BINARY_DIR CACHE) 59 | unset(${name}_FOUND CACHE) 60 | if (DEFINED ${lname}_DIR AND "${${lname}_DIR}" STREQUAL "${CMAKE_FIND_PACKAGE_REDIRECTS_DIR}") 61 | unset(${lname}_DIR CACHE) 62 | endif() 63 | endif() 64 | set(LAST_WSDDN_PREFER_SYSTEM_${uname} ${WSDDN_PREFER_SYSTEM_${uname}} CACHE INTERNAL "") 65 | 66 | if (WSDDN_PREFER_SYSTEM_${uname}) 67 | # string(JSON find_args ERROR_VARIABLE find_args_err GET "${DEPENDECIES_JSON}" ${name} find_args) 68 | # if (find_args_err) 69 | # unset(find_args) 70 | # endif() 71 | set(prefer_system FIND_PACKAGE_ARGS ${find_args}) 72 | else() 73 | set(prefer_system "") 74 | endif() 75 | FetchContent_Declare(${name} 76 | URL ${url} 77 | URL_HASH MD5=${md5} 78 | ${extras} 79 | ${prefer_system} 80 | ) 81 | set(deplist ${DECLARED_DEPENDENCIES}) 82 | list(APPEND deplist ${name}) 83 | set(DECLARED_DEPENDENCIES ${deplist} PARENT_SCOPE) 84 | endfunction() 85 | 86 | ################################################# 87 | 88 | fetch_dependency(argum) 89 | fetch_dependency(sys_string) 90 | fetch_dependency(isptr) 91 | fetch_dependency(ptl) 92 | fetch_dependency(modern-uuid) 93 | 94 | 95 | set(LIBXML2_WITH_ICONV OFF) 96 | set(LIBXML2_WITH_LZMA OFF) 97 | set(LIBXML2_WITH_HTML OFF) 98 | set(LIBXML2_WITH_HTTP OFF) 99 | set(LIBXML2_WITH_FTP OFF) 100 | set(LIBXML2_WITH_TESTS OFF) 101 | set(LIBXML2_WITH_ZLIB OFF) 102 | set(LIBXML2_WITH_PYTHON OFF) 103 | set(LIBXML2_WITH_LEGACY OFF) 104 | set(LIBXML2_WITH_MODULES OFF) 105 | set(LIBXML2_WITH_PROGRAMS OFF) 106 | 107 | fetch_dependency(LibXml2) 108 | 109 | set(FMT_INSTALL OFF) 110 | fetch_dependency(fmt) 111 | 112 | set(SPDLOG_NO_ATOMIC_LEVELS ON CACHE BOOL "prevent spdlog from using of std::atomic log levels (use only if your code never modifies log levels concurrently)") 113 | set(SPDLOG_NO_TLS ON CACHE BOOL "prevent spdlog from using thread local storage") 114 | set(SPDLOG_FMT_EXTERNAL ON CACHE BOOL "Use external fmt library instead of bundled") 115 | fetch_dependency(spdlog) 116 | 117 | fetch_dependency(tomlplusplus) 118 | fetch_dependency(outcome 119 | SOURCE_SUBDIR include #we don't really want to build it 120 | ) 121 | fetch_dependency(asio) 122 | 123 | ################################################# 124 | 125 | FetchContent_MakeAvailable(${DECLARED_DEPENDENCIES}) 126 | 127 | foreach(dep ${DECLARED_DEPENDENCIES}) 128 | string(TOUPPER ${dep} udep) 129 | string(TOLOWER ${dep} ldep) 130 | if (DEFINED ${ldep}_SOURCE_DIR) 131 | message(STATUS "${dep} will be built from sources and statically linked") 132 | else() 133 | if (DEFINED ${ldep}_VERSION) 134 | message(STATUS "${dep} will be used from system (current version: ${${ldep}_VERSION})") 135 | else() 136 | if (DEFINED ${udep}_VERSION_STRING) 137 | message(STATUS "${dep} will be used from system (current version: ${${udep}_VERSION_STRING})") 138 | else() 139 | message(STATUS "${dep} will be used from system") 140 | endif() 141 | endif() 142 | endif() 143 | endforeach() 144 | 145 | get_directory_property(KNOWN_SUBDIRECTORIES SUBDIRECTORIES) 146 | foreach(dir ${KNOWN_SUBDIRECTORIES}) 147 | if (IS_DIRECTORY ${dir}) 148 | foreach(dep ${DECLARED_DEPENDENCIES}) 149 | string(TOLOWER ${dep} ldep) 150 | if (DEFINED ${ldep}_SOURCE_DIR) 151 | #check if the subdirectory is "under" the dependency source dir 152 | string(FIND ${dir} ${${ldep}_SOURCE_DIR} match_pos) 153 | if (match_pos EQUAL 0) 154 | #and, if so, exclude it from all to prevent installation 155 | set_property(DIRECTORY ${dir} PROPERTY EXCLUDE_FROM_ALL YES) 156 | break() 157 | endif() 158 | endif() 159 | endforeach() 160 | endif() 161 | endforeach() 162 | -------------------------------------------------------------------------------- /installers/wsddn.conf: -------------------------------------------------------------------------------- 1 | # Uncomment/modify the following options to configure WS-Discovery Host 2 | # 3 | # The syntax of this file is TOML (https://toml.io/en/). 4 | # 5 | # Any options specified on command line take precedence over options 6 | # in this file 7 | # 8 | # After editing this file you must reload WS-Discovery Host 9 | # if it is running via: 10 | # 11 | {RELOAD_INSTRUCTIONS} 12 | 13 | ############################################################################### 14 | # 15 | # Networking options 16 | # 17 | 18 | # Specify on which interfaces wsddn will be listening on. If no interfaces 19 | # are specified, or the list is empty all suitable detected interfaces will be 20 | # used. Loop-back interfaces are never used, even when explicitly specified. 21 | # For interfaces with IPv6 addresses, only link-local addresses will be used 22 | # for announcing the host on the network. 23 | 24 | #interfaces = ["{SAMPLE_IFACE_NAME}"] 25 | 26 | # Restrict communications to the given address family. Valid values 27 | # are "IPv4" or "IPv6" case-insensitive. 28 | 29 | #allowed-address-family = "IPv4" 30 | 31 | # Set the hop limit for multicast packets. The default is 1 which should 32 | # prevent packets from leaving the local network segment. 33 | 34 | #hoplimit=1 35 | 36 | # Set the source port for outgoing multicast messages, so that replies will 37 | # use this as the destination port. 38 | # This is useful for firewalls that do not detect incoming unicast replies 39 | # to a multicast as part of the flow, so the port needs to be fixed in order 40 | # to be allowed manually. 41 | 42 | #source-port=12345 43 | 44 | ############################################################################### 45 | # 46 | # Machine information 47 | # 48 | 49 | # WS-Discovery protocol requires your machine to have a unique identifier that 50 | # is stable across reboots or changes in networks. 51 | # By default, wsddn uses UUID version 5 with private namespace and the 52 | # host name of the machine. This will remain stable as long as the hostname 53 | # doesn't change. If desired, you can override this with a fixed UUID using 54 | # this option. 55 | 56 | #uuid = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" 57 | 58 | # Hostname to be reported to Windows machines. By default the local machine's 59 | # hostname (with domain part, if any, removed) is used. 60 | # If you set the value to ":NETBIOS:" then Netbios hostname will be used. 61 | # The Netbios hostname is either detected from SMB configuration, if found, or 62 | # produced by capitalizing normal machine hostname. 63 | 64 | #hostname = "my-awesome-host" 65 | 66 | # Report whether the host is a member of a given workgroup or domain. 67 | # To specify a workgroup use "Workgroup/name" syntax. 68 | # To specify a domain use "Domain/name" 69 | # The "workgroup/" and "domain/" prefixes are not case sensitive. 70 | # If not specified workgroup/domain membership is detected from SMB 71 | # configuration. If no SMB configuration is found it is set to a workgroup 72 | # named WORKGROUP. 73 | 74 | #member-of = "Workgroup/WORKGROUP" 75 | 76 | # Path to smb.conf file to extract the SMB configuration from. This option is 77 | # not available on macOS. By default wsddn tries to locate this file on 78 | # its own. 79 | 80 | #smb-conf = "/path/to/smb.conf" 81 | 82 | # Path to a custom metadata file. Custom metadata allows you to completely 83 | # replace the information normally supplied by `wsddn` to Windows with your own. 84 | # See https://github.com/gershnik/wsdd-native/blob/master/config/metadata/README.md 85 | # for details about the metadata format and content. 86 | 87 | #metadata = "/path/to/metadata.xml" 88 | 89 | 90 | ############################################################################### 91 | # 92 | # Behavior options 93 | # 94 | 95 | # Set verbosity of the log output. The default value is 4. Log levels range 96 | # from 0 (disable logging) to 6 (detailed trace). Passing values bigger than 6 97 | # is equivalent to 6 98 | 99 | #log-level = 4 100 | 101 | # Set the path of log file. If not specified wsddn outputs the log 102 | # messages as follows 103 | # - If invoked without any daemon flags: to standard output 104 | # - If invoked with --systemd: to standard output, with systemd severity 105 | # prefixes 106 | # - If invoked with --launchd: to standard output 107 | # - If invoked with --unixd: to /dev/null (no logging) 108 | 109 | #log-file = "/path/to/log-file.log" 110 | 111 | # macOS only. Send log output to system log (visible via Console app or 112 | # log command line tool) 113 | # Setting it to true option is mutually exclusive with log-file 114 | 115 | #log-os-log = false 116 | 117 | # Set the path to PID file. If not specified no PID file is written 118 | # Send SIGHUP signal to the process ID in the PID file to reload 119 | # configuration. 120 | 121 | #pid-file = "/path/to/pidfile.pid" 122 | 123 | # Set the identity under which the process that performs network communications 124 | # will run. The value can be either just username or username:groupname. 125 | # If groupname is not specified, primary group of the username is used. 126 | # If this option is not specified then the behavior is as follows: 127 | # - If wsddn process is run under the root account it tries to use a special 128 | # unprivileged account name (_wsddn:_wsddn on macOS, wsddn:wsddn otherwise) 129 | # The user and group are created if they do not exist. Any failures in these 130 | # steps stop the process. 131 | # - Otherwise, wsddn uses the account it is run under. 132 | # The net effect of these rules is that wsddn under no circumstances will 133 | # perform network communications under root account. 134 | 135 | #user = "username:groupname" 136 | 137 | # Set the directory into which process that performs network communications 138 | # should chroot into. This further limits any impact of potential network 139 | # exploits into wsddn. If not specified the behavior is as follows 140 | # - If wsddn process is run under the root account: use /var/empty on macOS 141 | # and /var/run/wsddn on other platforms 142 | # This directory will be created if it does not exist. 143 | # - Otherwise: do not chroot 144 | # Note: do not use external methods to chroot wsddn process (e.g. using 145 | # launchd config plist). Non-networking parts of it need access to the 146 | # normal filesystem to detect things like SMB configuration etc. 147 | 148 | #chroot = "/path/to/an/empty/dir" 149 | 150 | -------------------------------------------------------------------------------- /cmake/detect_system.cmake: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2022, Eugene Gershnik 2 | # SPDX-License-Identifier: BSD-3-Clause 3 | 4 | include(CheckCXXSourceCompiles) 5 | include(CheckIncludeFiles) 6 | include(CheckLibraryExists) 7 | include(CheckFunctionExists) 8 | include(CheckStructHasMember) 9 | include(CMakePushCheckState) 10 | 11 | check_cxx_source_compiles(" 12 | #include 13 | #include 14 | int main() {}" 15 | HAVE_NETLINK) 16 | 17 | if (NOT HAVE_NETLINK) 18 | 19 | check_cxx_source_compiles(" 20 | #include 21 | #include 22 | #include 23 | #include 24 | int main() { 25 | int x = PF_ROUTE; 26 | size_t s = sizeof(rt_msghdr); 27 | }" 28 | HAVE_PF_ROUTE) 29 | 30 | check_cxx_source_compiles(" 31 | #include 32 | #include 33 | #include 34 | #include 35 | int main() { 36 | int x = NET_RT_IFLIST; 37 | }" 38 | HAVE_SYSCTL_PF_ROUTE) 39 | 40 | check_cxx_source_compiles(" 41 | #include 42 | #include 43 | #include 44 | #include 45 | int main() { 46 | int x = SIOCGLIFCONF; 47 | lifconf conf{}; 48 | }" 49 | HAVE_SIOCGLIFCONF) 50 | 51 | check_cxx_source_compiles(" 52 | #include 53 | #include 54 | #include 55 | #include 56 | int main() { 57 | int x = SIOCGIFCONF; 58 | ifconf conf{}; 59 | }" 60 | HAVE_SIOCGIFCONF) 61 | 62 | endif() 63 | 64 | check_struct_has_member("struct sockaddr" "sa_len" "sys/types.h;sys/socket.h" HAVE_SOCKADDR_SA_LEN) 65 | 66 | check_include_files(execinfo.h HAVE_EXECINFO_H) 67 | check_library_exists(execinfo backtrace "" HAVE_EXECINFO_LIB) 68 | 69 | check_cxx_source_compiles(" 70 | #include 71 | int main() { 72 | int stat; 73 | abi::__cxa_demangle(\"abc\", 0, 0, &stat); 74 | }" 75 | HAVE_CXXABI_H) 76 | 77 | 78 | check_cxx_source_compiles(" 79 | #include 80 | int main() { 81 | using x = decltype(abi::__cxa_throw); 82 | }" 83 | HAVE_ABI_CXA_THROW) 84 | 85 | if (NOT DEFINED USERADD_PATH) 86 | find_program(USERADD_PATH useradd PATHS /usr/sbin /bin NO_DEFAULT_PATH) 87 | if (USERADD_PATH) 88 | message(STATUS "Looking for useradd - found at ${USERADD_PATH}") 89 | else() 90 | message(STATUS "Looking for useradd - not found") 91 | endif() 92 | endif() 93 | 94 | if (NOT DEFINED GROUPADD_PATH) 95 | find_program(GROUPADD_PATH groupadd PATHS /usr/sbin /bin NO_DEFAULT_PATH) 96 | if (GROUPADD_PATH) 97 | message(STATUS "Looking for groupadd - found at ${GROUPADD_PATH}") 98 | else() 99 | message(STATUS "Looking for groupadd - not found") 100 | endif() 101 | endif() 102 | 103 | if (NOT DEFINED PW_PATH) 104 | find_program(PW_PATH pw PATHS /usr/sbin NO_DEFAULT_PATH) 105 | if (PW_PATH) 106 | message(STATUS "Looking for pw - found at ${PW_PATH}") 107 | else() 108 | message(STATUS "Looking for pw - not found") 109 | endif() 110 | endif() 111 | 112 | # Alpine Linux needs special treatment 113 | 114 | if (NOT DEFINED IS_ALPINE_LINUX) 115 | if(EXISTS "/etc/os-release") 116 | file(STRINGS "/etc/os-release" OS_RELEASE_CONTENTS) 117 | foreach(line IN LISTS OS_RELEASE_CONTENTS) 118 | if(line MATCHES "^ID=alpine$") 119 | set(IS_ALPINE_LINUX TRUE CACHE INTERNAL "whether this is Alpine Linux") 120 | message(STATUS "This is Alpine Linux") 121 | break() 122 | endif() 123 | endforeach() 124 | endif() 125 | if (NOT DEFINED IS_ALPINE_LINUX) 126 | set(IS_ALPINE_LINUX FALSE CACHE INTERNAL "whether this is Alpine Linux") 127 | endif() 128 | endif() 129 | 130 | if(IS_ALPINE_LINUX) 131 | if (NOT DEFINED ADDUSER_PATH) 132 | find_program(ADDUSER_PATH adduser PATHS /usr/sbin NO_DEFAULT_PATH) 133 | if (ADDUSER_PATH) 134 | message(STATUS "Looking for adduser - found at ${ADDUSER_PATH}") 135 | else() 136 | message(STATUS "Looking for adduser - not found") 137 | endif() 138 | endif() 139 | 140 | if (NOT DEFINED ADDGROUP_PATH) 141 | find_program(ADDGROUP_PATH addgroup PATHS /usr/sbin NO_DEFAULT_PATH) 142 | if (ADDGROUP_PATH) 143 | message(STATUS "Looking for addgroup - found at ${ADDGROUP_PATH}") 144 | else() 145 | message(STATUS "Looking for addgroup - not found") 146 | endif() 147 | endif() 148 | 149 | endif() 150 | 151 | if (WSDDN_WITH_SYSTEMD STREQUAL "yes" OR WSDDN_WITH_SYSTEMD STREQUAL "auto" AND NOT DEFINED CACHE{HAVE_SYSTEMD}) 152 | 153 | message(CHECK_START "Looking for systemd") 154 | 155 | find_library(LIBSYSTEMD_LIBRARY NAMES systemd systemd-daemon) 156 | 157 | if (LIBSYSTEMD_LIBRARY) 158 | if(IS_SYMLINK "${LIBSYSTEMD_LIBRARY}") 159 | file(READ_SYMLINK "${LIBSYSTEMD_LIBRARY}" LIBSYSTEMD_SO) 160 | if(NOT IS_ABSOLUTE "${LIBSYSTEMD_SO}") 161 | get_filename_component(dir "${LIBSYSTEMD_LIBRARY}" DIRECTORY) 162 | set(LIBSYSTEMD_SO "${dir}/${LIBSYSTEMD_SO}") 163 | endif() 164 | else() 165 | set(LIBSYSTEMD_SO "${LIBSYSTEMD_LIBRARY}") 166 | endif() 167 | 168 | set(LIBSYSTEMD_SO "${LIBSYSTEMD_SO}" CACHE INTERNAL "" FORCE) 169 | endif() 170 | 171 | cmake_push_check_state(RESET) 172 | set(CMAKE_REQUIRED_LIBRARIES ${LIBSYSTEMD_LIBRARY}) 173 | check_include_files(systemd/sd-daemon.h HAVE_SYSTEMD_SD_DAEMON_H) 174 | check_function_exists(sd_notify HAVE_SYSTEMD_SD_NOTIFY) 175 | cmake_pop_check_state() 176 | 177 | if (HAVE_SYSTEMD_SD_DAEMON_H AND HAVE_SYSTEMD_SD_NOTIFY AND LIBSYSTEMD_SO) 178 | 179 | set(HAVE_SYSTEMD ON CACHE INTERNAL "") 180 | message(CHECK_PASS "found in ${LIBSYSTEMD_SO}") 181 | 182 | else() 183 | 184 | message(CHECK_FAIL "not found") 185 | if (WSDDN_WITH_SYSTEMD STREQUAL "yes") 186 | message(FATAL_ERROR "systemd is required but not found") 187 | endif() 188 | 189 | endif() 190 | 191 | endif() 192 | 193 | 194 | find_program(PANDOC_PATH pandoc) 195 | find_program(GROFF_PATH groff) 196 | -------------------------------------------------------------------------------- /config/metadata/README.md: -------------------------------------------------------------------------------- 1 | # Custom metadata 2 | 3 | 4 | [wsdp]: https://specs.xmlsoap.org/ws/2006/02/devprof/devicesprofile.pdf 5 | [pnpx]: https://download.microsoft.com/download/a/f/7/af7777e5-7dcd-4800-8a0a-b18336565f5b/PnPX-spec.doc 6 | [ms-pbsd]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-pbsd/a3c6b665-a44e-41d5-98ec-d70c188378e4 7 | [ms-dpwsrp]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-dpwsrp/0a96c35a-4cd4-4274-9f42-f44334d4d893 8 | [cont-id]: https://learn.microsoft.com/en-us/windows-hardware/drivers/install/container-ids-for-dpws-devices 9 | [metadata-retrieval]: https://learn.microsoft.com/en-us/windows-hardware/drivers/install/device-metadata-retrieval-client 10 | [wsdd-print]: https://learn.microsoft.com/en-us/windows-hardware/drivers/print/ws-discovery-mobile-printing-support 11 | 12 | 13 | WS-Discovery protocol tells Windows what kind of device it is exposing by sending over "metadata" - essentially an XML document containing information about the computer. 14 | 15 | By default **wsdd-native** sends metadata describing the host it is running on as an SMB server. It is possible to change that by authoring your own metadata and telling **wsdd-native** to use that instead. 16 | 17 | To do so you need to use `--metadata PATH` or `-m PATH` command line switch or put `metadata="path"` in `wsddn.conf` file. 18 | 19 | ## Format 20 | 21 | Custom metadata must be a valid, well-formed, standalone XML file. All namespaces you use must be fully declared. 22 | 23 | ### General form 24 | 25 | The general form of the metadata is as follows. For more details see [this obscure spec][wsdp] 26 | 27 | ```xml 28 | 29 | 33 | 34 | 35 | ... 36 | ... 37 | ... 38 | 39 | 40 | 41 | 42 | ... 43 | ... 44 | ... 45 | ... 46 | 47 | 48 | 49 | 50 | ... 51 | 52 | 53 | 54 | ``` 55 | 56 | ### Placeholders 57 | 58 | You can use _placeholders_ inside XML element values and attributes. The placeholders are keywords that start with a `$` sign. 59 | 60 | The rules for placeholders are as follows: 61 | 62 | * A single `$` not followed by a placeholder is simply dropped and ignored 63 | * `$$` is replaced with a single `$` 64 | * The following placeholders are currently defined: 65 | 66 | | Placeholder | Meaning 67 | |-----------------------|-------- 68 | | $ENDPOINT_ID | Replaced with the URN in the form urn:uuid:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx where the UUID is the identifier of the host. It is auto-generated or supplied via `--uuid` option. 69 | | $IP_ADDR | The IP address of the interface on which the metadata is being sent. This allows you to create URLs that operate on the same network Windows that Windows "sees" your machine. 70 | | $SMB_HOST_DESCRIPTION | The description of the host as derived from SMB configuration. If no description is available simple SMB host name is used. 71 | | $SMB_FULL_HOST_NAME | A string in format `hostname/Workgroup:workgroup_name` or `hostname/Domain:domain_name` that provides full SMB name of the machine in WS-Discovery protocol. 72 | 73 | * Placeholders are _prefix-matched_ so $IP_ADDR_HELLO will be expanded to something like 192.168.1.1_HELLO 74 | 75 | ### Examples 76 | 77 | This directory contains some examples that might help you author your own metadata. 78 | 79 | The [default.xml](default.xml) file contains an equivalent of what **wsdd-native** sends by default with no custom metadata. This is the standard metadata of an SMB host. 80 | 81 | The [other.xml](other.xml) file contains an example of a simple HTTP server. When you click on it in Windows Explorer the browser would open pointing to that host. Where it points to is actually controlled by the line 82 | 83 | ```xml 84 | http://$IP_ADDR/ 85 | ``` 86 | which you can change to anything you want. 87 | 88 | The section and icon in Windows Explorer Network view are determined by the line 89 | 90 | ```xml 91 | Other 92 | ``` 93 | 94 | In the example it is `Other` which would result in the host being in "Other devices". You can try other things here like `HomeAutomation` or `MediaDevices`. The full(?) list of what is allowed here can be divined from [this obscure spec][pnpx] (**warning: .doc file**). See the table in "PnPX Category Definitions" section. 95 | 96 | 97 | ## More information 98 | 99 | The entire area of what the actual WS-Discovery payload should be is incredibly poorly documented by Microsoft. The useful references (some already mentioned) I could find are as follows: 100 | 101 | * [Devices Profile for Web Services][wsdp] - a horribly formatted obscure spec that leaves many questions unanswered 102 | * [PnPX: Plug and Play Extensions for Windows][pnpx] - (**warning: .doc file**). A very old spec that seems to exist only as a Word document. This sheds some light on `pnpx` stuff that can be used in metadata. 103 | * [\[MS-PBSD\]: Publication Services Data Structure][ms-pbsd] - see especially "Section 3: Structure Examples" there. This describes the metadata for an SMB server. 104 | * [\[MS-DPWSRP\]: Devices Profile for Web Services (DPWS): Shared Resource Publishing Data Structure][ms-dpwsrp] - explains the bizarre "not quite base-64" data is in the "Structure Examples" above 105 | * [Container IDs for DPWS Devices][cont-id] - explains how multiple devices can be grouped into "containers" 106 | * [Device Metadata Retrieval Client][metadata-retrieval] - indirectly explains how Windows looks up more information about devices including device-specific icons etc. Unfortunately it seems that there is no way to specify an icon via WS-Discovery, you need to register extra device metadata with Microsoft and refer to it by device ID. 107 | * [WS-Discovery mobile printing support][wsdd-print] - an example of how printers should expose themselves via WS-Discovery 108 | 109 | 110 | 111 | -------------------------------------------------------------------------------- /src/sys_socket.h: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022, Eugene Gershnik 2 | // SPDX-License-Identifier: BSD-3-Clause 3 | 4 | #ifndef HEADER_SYS_SOCKET_H_INCLUDED 5 | #define HEADER_SYS_SOCKET_H_INCLUDED 6 | 7 | #include "sys_util.h" 8 | 9 | #if __has_include() 10 | #include 11 | #endif 12 | 13 | #if __has_include() 14 | #include 15 | #endif 16 | 17 | namespace ptl { 18 | 19 | template 20 | struct FileDescriptorTraits> { 21 | [[gnu::always_inline]] static int c_fd(asio::basic_datagram_socket & socket) noexcept 22 | { return socket.native_handle();} 23 | }; 24 | 25 | } 26 | 27 | #define IN6_IS_SCOPE_LINKLOCAL(a) \ 28 | ((IN6_IS_ADDR_LINKLOCAL(a)) || \ 29 | (IN6_IS_ADDR_MC_LINKLOCAL(a))) 30 | 31 | 32 | WSDDN_DECLARE_MEMBER_DETECTOR(in6_addr, s6_addr16, in6_addr_has_s6_addr16); 33 | 34 | template T> 35 | static inline uint16_t * in6_addr_addr16(T & addr) { 36 | if constexpr (in6_addr_has_s6_addr16) 37 | return addr.s6_addr16; 38 | else 39 | return (uint16_t *)&addr.s6_addr; 40 | } 41 | 42 | 43 | inline auto makeAddress(const sockaddr_in & addr) -> ip::address_v4 { 44 | return ip::address_v4(ntohl(addr.sin_addr.s_addr)); 45 | } 46 | 47 | inline auto makeAddress(const sockaddr_in6 & addr) -> ip::address_v6 { 48 | union { 49 | ip::address_v6::bytes_type asio; 50 | in6_addr raw; 51 | } clearAddr; 52 | memcpy(&clearAddr.raw, addr.sin6_addr.s6_addr, sizeof(clearAddr.raw)); 53 | uint32_t scope = addr.sin6_scope_id; 54 | if (IN6_IS_SCOPE_LINKLOCAL(&clearAddr.raw)) { 55 | uint16_t * words = in6_addr_addr16(clearAddr.raw); 56 | if (uint32_t embeddedScope = htons(words[1])) { 57 | scope = embeddedScope; 58 | } 59 | words[1] = 0; 60 | } 61 | return ip::address_v6(clearAddr.asio, scope); 62 | } 63 | 64 | WSDDN_DECLARE_MEMBER_DETECTOR(struct ifreq, ifr_ifindex, ifreq_has_ifr_ifindex); 65 | 66 | template T> 67 | static inline void set_ifreq_ifindex(T & req, int ifIndex) { 68 | if constexpr (ifreq_has_ifr_ifindex) 69 | req.ifr_ifindex = ifIndex; 70 | else 71 | req.ifr_index = ifIndex; 72 | } 73 | 74 | template T> 75 | static inline int ifreq_ifindex(const T & req) { 76 | if constexpr (ifreq_has_ifr_ifindex) 77 | return req.ifr_ifindex; 78 | else 79 | return req.ifr_index; 80 | } 81 | 82 | template 83 | class SocketIOControl { 84 | public: 85 | constexpr auto name() const -> unsigned long { return Name; } 86 | auto data() -> void * { return &m_data; } 87 | 88 | protected: 89 | T m_data; 90 | }; 91 | 92 | #ifdef SIOCGIFFLAGS 93 | 94 | class GetInterfaceFlags : public SocketIOControl<(unsigned long)SIOCGIFFLAGS, ifreq> { 95 | 96 | public: 97 | GetInterfaceFlags(const sys_string & name) { 98 | auto copied = name.copy_data(0, m_data.ifr_name, IFNAMSIZ); 99 | memset(m_data.ifr_name + copied, 0, IFNAMSIZ - copied); 100 | } 101 | 102 | auto result() const -> std::remove_cvref_tm_data.ifr_flags)> { 103 | return m_data.ifr_flags; 104 | } 105 | }; 106 | 107 | #endif 108 | 109 | #ifdef SIOCGLIFCONF 110 | 111 | class GetLInterfaceConf : public SocketIOControl<(unsigned long)SIOCGLIFCONF, lifconf> { 112 | public: 113 | GetLInterfaceConf(sa_family_t family, lifreq * dest, size_t size) { 114 | m_data.lifc_family = family; 115 | m_data.lifc_flags = 0; 116 | m_data.lifc_len = size * sizeof(lifreq); 117 | m_data.lifc_req = dest; 118 | } 119 | 120 | auto result() const -> size_t { 121 | return m_data.lifc_len / sizeof(lifreq); 122 | } 123 | }; 124 | 125 | #endif 126 | 127 | #ifdef SIOCGIFCONF 128 | 129 | class GetInterfaceConf : public SocketIOControl<(unsigned long)SIOCGIFCONF, ifconf> { 130 | public: 131 | GetInterfaceConf(ifreq * dest, size_t size) { 132 | m_data.ifc_len = size * sizeof(ifreq); 133 | m_data.ifc_req = dest; 134 | } 135 | 136 | auto result() const -> size_t { 137 | return m_data.ifc_len / sizeof(ifreq); 138 | } 139 | }; 140 | 141 | #endif 142 | 143 | #ifdef SIOCGLIFINDEX 144 | 145 | class GetLInterfaceIndex : public SocketIOControl<(unsigned long)SIOCGLIFINDEX, lifreq> { 146 | public: 147 | GetLInterfaceIndex(const sys_string & name) { 148 | auto copied = name.copy_data(0, m_data.lifr_name, IFNAMSIZ); 149 | memset(m_data.lifr_name + copied, 0, IFNAMSIZ - copied); 150 | } 151 | 152 | auto result() const -> int { 153 | return m_data.lifr_index; 154 | } 155 | }; 156 | 157 | #endif 158 | 159 | #ifdef SIOCGIFINDEX 160 | 161 | class GetInterfaceIndex : public SocketIOControl<(unsigned long)SIOCGIFINDEX, ifreq> { 162 | public: 163 | GetInterfaceIndex(const sys_string & name) { 164 | auto copied = name.copy_data(0, m_data.ifr_name, IFNAMSIZ); 165 | memset(m_data.ifr_name + copied, 0, IFNAMSIZ - copied); 166 | } 167 | 168 | auto result() const -> int { 169 | return ifreq_ifindex(m_data); 170 | } 171 | }; 172 | 173 | #endif 174 | 175 | #ifdef SIOCGLIFFLAGS 176 | 177 | class GetLInterfaceFlags : public SocketIOControl<(unsigned long)SIOCGLIFFLAGS, lifreq> { 178 | 179 | public: 180 | GetLInterfaceFlags(const sys_string & name) { 181 | auto copied = name.copy_data(0, m_data.lifr_name, IFNAMSIZ); 182 | memset(m_data.lifr_name + copied, 0, IFNAMSIZ - copied); 183 | } 184 | 185 | auto result() const -> std::remove_cvref_tm_data.lifr_flags)> { 186 | return m_data.lifr_flags; 187 | } 188 | }; 189 | 190 | #endif 191 | 192 | 193 | #ifdef SIOCGIFNAME 194 | 195 | class GetInterfaceName : public SocketIOControl<(unsigned long)SIOCGIFNAME, ifreq> { 196 | public: 197 | GetInterfaceName(int ifIndex) { 198 | set_ifreq_ifindex(m_data, ifIndex); 199 | } 200 | 201 | auto result() const -> sys_string { 202 | auto len = strnlen(m_data.ifr_name, IFNAMSIZ); 203 | return sys_string(m_data.ifr_name, len); 204 | } 205 | 206 | }; 207 | 208 | #endif 209 | 210 | template 211 | auto ioctlSocket(asio::basic_socket & socket, Args && ...args) -> 212 | outcome::result().result())> { 213 | 214 | IoControl control(std::forward(args)...); 215 | 216 | asio::error_code ec; 217 | socket.io_control(control, ec); 218 | if (ec) 219 | return ec; 220 | return control.result(); 221 | } 222 | 223 | #endif 224 | -------------------------------------------------------------------------------- /installers/mac/build.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env -S python3 -u 2 | 3 | # Copyright (c) 2022, Eugene Gershnik 4 | # SPDX-License-Identifier: BSD-3-Clause 5 | 6 | import sys 7 | import os 8 | import subprocess 9 | import shutil 10 | import plistlib 11 | import re 12 | from pathlib import Path 13 | 14 | IDENTIFIER='io.github.gershnik.wsddn' 15 | 16 | mydir = Path(sys.argv[0]).parent 17 | 18 | sys.path.append(str(mydir.absolute().parent)) 19 | 20 | from common import parseCommandLine, getVersion, buildCode, installCode, copyTemplated 21 | 22 | args = parseCommandLine() 23 | srcdir: Path = args.srcdir 24 | builddir: Path = args.builddir 25 | 26 | buildCode(builddir) 27 | 28 | VERSION = getVersion(builddir) 29 | 30 | workdir = builddir / 'stage/mac' 31 | stagedir = workdir / 'root' 32 | shutil.rmtree(workdir, ignore_errors=True) 33 | stagedir.mkdir(parents=True) 34 | 35 | installCode(builddir, stagedir / 'usr/local') 36 | 37 | ignoreCrap = shutil.ignore_patterns('.DS_Store') 38 | 39 | supdir = stagedir / "Library/Application Support/wsdd-native" 40 | supdir.mkdir(parents=True) 41 | shutil.copytree(builddir / "wrapper/wsdd-native.app", supdir/'wsdd-native.app', ignore=ignoreCrap) 42 | subprocess.run(['/usr/bin/strip', '-u', '-r', '-no_code_signature_warning', 43 | supdir/'wsdd-native.app/Contents/MacOS/wsdd-native'], 44 | check=True) 45 | 46 | (supdir/'wsdd-native.app/Contents/Resources').mkdir(parents=True, exist_ok=True) 47 | (stagedir / 'usr/local/bin/wsddn').rename(supdir/'wsdd-native.app/Contents/Resources/wsddn') 48 | (stagedir / 'usr/local/bin/wsddn').symlink_to('/Library/Application Support/wsdd-native/wsdd-native.app/Contents/Resources/wsddn') 49 | shutil.copy(srcdir / 'LICENSE', supdir / 'LICENSE') 50 | shutil.copy(srcdir / 'Acknowledgements.md', supdir / 'Acknowledgements.md') 51 | 52 | 53 | shutil.copytree(srcdir / 'config/mac', stagedir, dirs_exist_ok=True, ignore=ignoreCrap) 54 | 55 | (stagedir / 'usr/local/bin').mkdir(parents=True, exist_ok=True) 56 | shutil.copy(mydir / 'wsddn-uninstall', stagedir / 'usr/local/bin') 57 | 58 | copyTemplated(mydir.parent / 'wsddn.conf', stagedir / 'etc/wsddn.conf.sample', { 59 | 'SAMPLE_IFACE_NAME': "en0", 60 | 'RELOAD_INSTRUCTIONS': f""" 61 | # sudo launchctl kill HUP system/{IDENTIFIER} 62 | """.lstrip() 63 | }) 64 | 65 | copyTemplated(mydir / 'distribution.xml', workdir / 'distribution.xml', { 66 | 'IDENTIFIER':IDENTIFIER, 67 | 'VERSION': VERSION 68 | }) 69 | 70 | with open(stagedir / 'Library/LaunchDaemons/io.github.gershnik.wsddn.plist', "rb") as src: 71 | daemonPlist = plistlib.load(src, fmt=plistlib.FMT_XML) 72 | daemonPlist['ProgramArguments'][0] = 'wsddn' 73 | daemonPlist['Program'] = '/Library/Application Support/wsdd-native/wsdd-native.app/Contents/Resources/wsddn' 74 | daemonPlist['AssociatedBundleIdentifiers'] = 'io.github.gershnik.wsddn.wrapper' 75 | with open(stagedir / 'Library/LaunchDaemons/io.github.gershnik.wsddn.plist', "wb") as dst: 76 | plistlib.dump(daemonPlist, dst, fmt=plistlib.FMT_XML) 77 | 78 | 79 | things_to_sign = [ 80 | 'Library/Application Support/wsdd-native/wsdd-native.app/Contents/Resources/wsddn', 81 | 'Library/Application Support/wsdd-native/wsdd-native.app' 82 | ] 83 | 84 | for to_sign in things_to_sign: 85 | if args.sign: 86 | subprocess.run(['codesign', '--force', '--sign', 'Developer ID Application', '-o', 'runtime', '--timestamp', 87 | stagedir / to_sign], check=True) 88 | else: 89 | subprocess.run(['codesign', '--force', '--sign', '-', '-o', 'runtime', '--timestamp=none', 90 | stagedir / to_sign], check=True) 91 | 92 | 93 | packagesdir = workdir / 'packages' 94 | packagesdir.mkdir() 95 | 96 | subprocess.run(['pkgbuild', 97 | '--analyze', 98 | '--root', str(stagedir), 99 | str(packagesdir/'component.plist') 100 | ], check=True) 101 | with open(packagesdir/'component.plist', "rb") as src: 102 | components = plistlib.load(src, fmt=plistlib.FMT_XML) 103 | for component in components: 104 | component['BundleIsRelocatable'] = False 105 | with open(packagesdir/'component.plist', "wb") as dest: 106 | plistlib.dump(components, dest, fmt=plistlib.FMT_XML) 107 | subprocess.run(['pkgbuild', 108 | '--root', str(stagedir), 109 | '--component-plist', str(packagesdir/'component.plist'), 110 | '--scripts', str(mydir / 'scripts'), 111 | '--identifier', IDENTIFIER, 112 | '--version', VERSION, 113 | '--ownership', 'recommended', 114 | str(packagesdir/'output.pkg') 115 | ], check=True) 116 | 117 | subprocess.run(['productbuild', 118 | '--distribution', workdir / 'distribution.xml', 119 | '--package-path', str(packagesdir), 120 | '--resources', str(mydir / 'html'), 121 | '--version', VERSION, 122 | str(workdir/'wsddn.pkg') 123 | ], check=True) 124 | 125 | installer = workdir / f'wsddn-macos-{VERSION}.pkg' 126 | 127 | if args.sign: 128 | subprocess.run(['productsign', '--sign', 'Developer ID Installer', workdir / 'wsddn.pkg', installer], check=True) 129 | pattern = re.compile(r'^\s*1. Developer ID Installer: .*\(([0-9A-Z]{10})\)$') 130 | teamId = None 131 | for line in subprocess.run(['pkgutil', '--check-signature', installer], 132 | check=True, stdout=subprocess.PIPE).stdout.decode('utf-8').splitlines(): 133 | m = pattern.match(line) 134 | if m: 135 | teamId = m.group(1) 136 | break 137 | if teamId is None: 138 | print('Unable to find team ID from signature', file=sys.stderr) 139 | sys.exit(1) 140 | subprocess.run([mydir / 'notarize', '--user', os.environ['NOTARIZE_USER'], '--password', os.environ['NOTARIZE_PWD'], 141 | '--team', teamId, installer], check=True) 142 | print('Signature Info') 143 | res1 = subprocess.run(['pkgutil', '--check-signature', installer], check=False) 144 | print('\nAssesment') 145 | res2 = subprocess.run(['spctl', '--assess', '-vvv', '--type', 'install', installer], check=False) 146 | if res1.returncode != 0 or res2.returncode != 0: 147 | sys.exit(1) 148 | 149 | if args.uploadResults: 150 | subprocess.run(['tar', '-C', builddir, '-czf', 151 | workdir.absolute() / f'wsddn-macos-{VERSION}.dSYM.tgz', 'wsddn.dSYM'], check=True) 152 | subprocess.run(['tar', '-C', builddir / 'wrapper', '-czf', 153 | workdir.absolute() / f'wsdd-native-macos-{VERSION}.app.dSYM.tgz', 'wsdd-native.app.dSYM'], check=True) 154 | subprocess.run(['aws', 's3', 'cp', 155 | workdir / f'wsddn-macos-{VERSION}.dSYM.tgz', 's3://wsddn-symbols/'], check=True) 156 | subprocess.run(['aws', 's3', 'cp', 157 | workdir / f'wsdd-native-macos-{VERSION}.app.dSYM.tgz', 's3://wsddn-symbols/'], check=True) 158 | subprocess.run(['gh', 'release', 'upload', f'v{VERSION}', installer], check=True) 159 | -------------------------------------------------------------------------------- /src/sys_util.h: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022, Eugene Gershnik 2 | // SPDX-License-Identifier: BSD-3-Clause 3 | 4 | #ifndef HEADER_SYS_UTIL_H_INCLUDED 5 | #define HEADER_SYS_UTIL_H_INCLUDED 6 | 7 | #include "util.h" 8 | 9 | /* 10 | OS-dependent (or potentially OS-dependant) utilities 11 | */ 12 | 13 | 14 | class Identity { 15 | public: 16 | Identity(uid_t uid, gid_t gid) : m_data(uid, gid) { 17 | } 18 | 19 | static auto my() -> Identity { 20 | return Identity(getuid(), getgid()); 21 | } 22 | static auto myEffective() -> Identity { 23 | return Identity(geteuid(), getegid()); 24 | } 25 | static auto admin() -> Identity { 26 | #ifdef ADMIN_GROUP_NAME 27 | auto adminGroup = ptl::Group::getByName(ADMIN_GROUP_NAME); 28 | return Identity(0, adminGroup ? adminGroup->gr_gid: 0); 29 | #else 30 | return Identity(0, 0); 31 | #endif 32 | } 33 | 34 | static auto createDaemonUser(const sys_string & name) -> std::optional; 35 | 36 | void setMyIdentity() const { 37 | ptl::setGroups({}); 38 | ptl::setGid(gid()); 39 | ptl::setUid(uid()); 40 | } 41 | 42 | auto uid() const -> uid_t { return m_data.first; } 43 | auto gid() const -> gid_t { return m_data.second; } 44 | 45 | private: 46 | std::pair m_data; 47 | }; 48 | 49 | #if HAVE_SYSTEMD 50 | 51 | class SystemdLevelFormatter : public spdlog::custom_flag_formatter 52 | { 53 | public: 54 | void format(const spdlog::details::log_msg & msg, const std::tm &, spdlog::memory_buf_t & dest) override 55 | { 56 | std::string_view level; 57 | switch(msg.level) { 58 | case SPDLOG_LEVEL_CRITICAL: level = SD_CRIT; break; 59 | case SPDLOG_LEVEL_ERROR: level = SD_ERR; break; 60 | case SPDLOG_LEVEL_WARN: level = SD_WARNING; break; 61 | case SPDLOG_LEVEL_INFO: level = SD_INFO; break; 62 | default: level = SD_DEBUG; break; 63 | } 64 | dest.append(level.data(), level.data() + level.size()); 65 | } 66 | 67 | std::unique_ptr clone() const override 68 | { 69 | return spdlog::details::make_unique(); 70 | } 71 | }; 72 | 73 | #endif 74 | 75 | #if HAVE_OS_LOG 76 | 77 | class OsLogHandle { 78 | public: 79 | static auto get() noexcept -> os_log_t { 80 | if (!s_handle) 81 | s_handle = os_log_create(WSDDN_BUNDLE_IDENTIFIER, s_category); 82 | return s_handle; 83 | } 84 | static void resetInChild() noexcept { 85 | if (s_handle) { 86 | os_release(s_handle); 87 | s_handle = nullptr; 88 | s_category = "child"; 89 | } 90 | } 91 | private: 92 | static os_log_t s_handle; 93 | static const char * s_category; 94 | }; 95 | 96 | template 97 | class OsLogSink : public spdlog::sinks::base_sink 98 | { 99 | using super = spdlog::sinks::base_sink; 100 | protected: 101 | void sink_it_(const spdlog::details::log_msg& msg) override { 102 | 103 | spdlog::memory_buf_t formatted; 104 | super::formatter_->format(msg, formatted); 105 | formatted.append(std::array{'\0'}); 106 | 107 | os_log_type_t type; 108 | switch(msg.level) { 109 | case SPDLOG_LEVEL_CRITICAL: type = OS_LOG_TYPE_FAULT; break; 110 | case SPDLOG_LEVEL_ERROR: type = OS_LOG_TYPE_ERROR; break; 111 | case SPDLOG_LEVEL_WARN: type = OS_LOG_TYPE_DEFAULT; break; 112 | case SPDLOG_LEVEL_INFO: type = OS_LOG_TYPE_INFO; break; 113 | default: type = OS_LOG_TYPE_DEBUG; break; 114 | } 115 | 116 | os_log_with_type(OsLogHandle::get(), type, "%{public}s", formatted.data()); 117 | } 118 | 119 | void flush_() override { 120 | } 121 | }; 122 | 123 | #endif 124 | 125 | 126 | inline void createMissingDirs(const std::filesystem::path & path, mode_t mode, 127 | std::optional owner) { 128 | 129 | auto absPath = absolute(path); 130 | auto start = absPath.root_path(); 131 | 132 | auto it = absPath.begin(); 133 | std::advance(it, std::distance(start.begin(), start.end())); 134 | for(auto end = absPath.end(); it != end; ++it) { 135 | 136 | start /= *it; 137 | //we need this check because makeDirectory might fail with things 138 | //other than EEXIST like permissions 139 | if (exists(start)) 140 | continue; 141 | ptl::AllowedErrors ec; //but we also need this exception to avoid TOCTOU, sigh 142 | ptl::makeDirectory(start, mode, ec); 143 | ptl::changeMode(start, mode); 144 | if (owner) 145 | ptl::changeOwner(start, owner->uid(), owner->gid()); 146 | } 147 | } 148 | 149 | template 150 | class LineReader { 151 | public: 152 | LineReader(size_t bufferSize, Sink sink): 153 | m_bufferSize(bufferSize), 154 | m_sink(sink) 155 | {} 156 | 157 | void operator()(const ptl::FileDescriptor & fd) const { 158 | std::vector buf; 159 | buf.reserve(m_bufferSize); 160 | bool ignore = false; 161 | while(true) { 162 | auto offset = buf.size(); 163 | const size_t addition = std::min(m_bufferSize - offset, m_bufferSize); 164 | assert(addition > 0); 165 | buf.resize(offset + addition); 166 | auto read_count = ptl::readFile(fd, buf.data() + offset, addition); 167 | buf.resize(offset + read_count); 168 | bool done = (read_count == 0); 169 | 170 | auto processed_end = buf.begin(); 171 | for(auto cur = processed_end, end = buf.end(); cur != end; ) { 172 | if (*cur == '\n') { 173 | if (!ignore) { 174 | std::string_view line(buf.data() + (processed_end - buf.begin()), cur - processed_end); 175 | m_sink(line); 176 | } 177 | ignore = false; 178 | processed_end = ++cur; 179 | } else { 180 | ++cur; 181 | } 182 | } 183 | buf.erase(buf.begin(), processed_end); 184 | if (buf.size() == m_bufferSize) { 185 | WSDLOG_WARN("read line is overly long, ignored"); 186 | ignore = true; 187 | buf.clear(); 188 | } 189 | 190 | if (done) 191 | break; 192 | } 193 | if (!buf.empty() && !ignore) 194 | m_sink(std::string_view(buf.data(), buf.size())); 195 | } 196 | private: 197 | size_t m_bufferSize; 198 | Sink m_sink; 199 | }; 200 | 201 | int run(const ptl::StringRefArray & args); 202 | void shell(const ptl::StringRefArray & args, bool suppressStdErr, std::function reader); 203 | 204 | 205 | #endif 206 | -------------------------------------------------------------------------------- /src/http_response.cpp: -------------------------------------------------------------------------------- 1 | 2 | // Copyright (c) 2022, Eugene Gershnik 3 | // Copyright (c) 2003-2022 Christopher M. Kohlhoff (chris at kohlhoff dot com) 4 | // SPDX-License-Identifier: BSD-3-Clause 5 | 6 | 7 | #include "http_response.h" 8 | 9 | using namespace sysstr; 10 | 11 | using StatusRecord = std::tuple; 12 | 13 | static const StatusRecord g_statuses[] = { 14 | {200, u8"HTTP/1.0 200 OK\r\n", u8""}, 15 | {201, u8"HTTP/1.0 201 Created\r\n", 16 | u8"" 17 | "Created" 18 | "

201 Created

" 19 | ""}, 20 | {202, u8"HTTP/1.0 202 Accepted\r\n", 21 | u8"" 22 | "Accepted" 23 | "

202 Accepted

" 24 | ""}, 25 | {204, u8"HTTP/1.0 204 No Content\r\n", 26 | u8"" 27 | "No Content" 28 | "

204 Content

" 29 | ""}, 30 | {300, u8"HTTP/1.0 300 Multiple Choices\r\n", 31 | u8"" 32 | "Multiple Choices" 33 | "

300 Multiple Choices

" 34 | ""}, 35 | {301, u8"HTTP/1.0 301 Moved Permanently\r\n", 36 | u8"" 37 | "Moved Permanently" 38 | "

301 Moved Permanently

" 39 | ""}, 40 | {302, u8"HTTP/1.0 302 Moved Temporarily\r\n", 41 | u8"" 42 | "Moved Temporarily" 43 | "

302 Moved Temporarily

" 44 | ""}, 45 | {304, u8"HTTP/1.0 304 Not Modified\r\n", 46 | u8"" 47 | "Not Modified" 48 | "

304 Not Modified

" 49 | ""}, 50 | {400, u8"HTTP/1.0 400 Bad Request\r\n", 51 | u8"" 52 | "Bad Request" 53 | "

400 Bad Request

" 54 | ""}, 55 | {401, u8"HTTP/1.0 401 Unauthorized\r\n", 56 | u8"" 57 | "Unauthorized" 58 | "

401 Unauthorized

" 59 | ""}, 60 | {403, u8"HTTP/1.0 403 Forbidden\r\n", 61 | u8"" 62 | "Forbidden" 63 | "

403 Forbidden

" 64 | ""}, 65 | {404, u8"HTTP/1.0 404 Not Found\r\n", 66 | u8"" 67 | "Not Found" 68 | "

404 Not Found

" 69 | ""}, 70 | {500, u8"HTTP/1.0 500 Internal Server Error\r\n", 71 | u8"" 72 | "Internal Server Error" 73 | "

500 Internal Server Error

" 74 | ""}, 75 | {501, u8"HTTP/1.0 501 Not Implemented\r\n", 76 | u8"" 77 | "Not Implemented" 78 | "

501 Not Implemented

" 79 | ""}, 80 | {502, u8"HTTP/1.0 502 Bad Gateway\r\n", 81 | u8"" 82 | "Bad Gateway" 83 | "

502 Bad Gateway

" 84 | ""}, 85 | {503, u8"HTTP/1.0 503 Service Unavailable\r\n", 86 | u8"" 87 | "Service Unavailable" 88 | "

503 Service Unavailable

" 89 | ""} 90 | }; 91 | 92 | static const char8_t g_crlf[] = { u8'\r', u8'\n' }; 93 | 94 | static auto findStatusRecord(HttpResponse::Status status) -> const StatusRecord * { 95 | auto ptr = std::lower_bound(std::begin(g_statuses), std::end(g_statuses), status, 96 | [] (const StatusRecord & val, HttpResponse::Status st) { 97 | 98 | return std::get<0>(val) < st; 99 | }); 100 | if (ptr != std::end(g_statuses) && std::get<0>(*ptr) == status) 101 | return ptr; 102 | return nullptr; 103 | } 104 | 105 | static const StatusRecord & g_defaultStatusRecord = [](){ 106 | auto ret = findStatusRecord(HttpResponse::InternalServerError); 107 | assert(ret); 108 | return *ret; 109 | }(); 110 | 111 | 112 | static asio::const_buffer makeBuffer(HttpResponse::Status status) { 113 | auto ptr = findStatusRecord(status); 114 | if (!ptr) 115 | ptr = &g_defaultStatusRecord; 116 | return asio::buffer(std::get<1>(*ptr), std::char_traits::length(std::get<1>(*ptr))); 117 | } 118 | 119 | auto HttpResponse::makeStockResponse(Status status) -> HttpResponse { 120 | HttpResponse ret(status); 121 | auto ptr = findStatusRecord(status); 122 | if (!ptr) 123 | ptr = &g_defaultStatusRecord; 124 | auto content = std::u8string_view(std::get<2>(*ptr), std::char_traits::length(std::get<2>(*ptr))); 125 | ret.m_content = content; 126 | 127 | ret.m_headers.reserve(2); 128 | ret.addHeader(S("Content-Type"), S("text/html")); 129 | ret.addHeader(S("Content-Length"), std::to_string(content.size())); 130 | return ret; 131 | } 132 | 133 | auto HttpResponse::makeReply(XmlCharBuffer && xml) -> HttpResponse { 134 | HttpResponse ret(Ok); 135 | 136 | ret.m_headers.reserve(2); 137 | ret.addHeader(S("Content-Type"), S("application/soap+xml")); 138 | ret.addHeader(S("Content-Length"), std::to_string(xml.size())); 139 | ret.m_content = std::move(xml); 140 | return ret; 141 | } 142 | 143 | void HttpResponse::addHeader(const sys_string & name, const sys_string & value) { 144 | 145 | sys_string_builder builder; 146 | builder.append(name); 147 | builder.append(u8": "); 148 | builder.append(value); 149 | builder.append(g_crlf, std::size(g_crlf)); 150 | m_headers.emplace_back(builder.build()); 151 | } 152 | 153 | 154 | auto HttpResponse::makeBuffers() const -> std::vector 155 | { 156 | std::vector buffers; 157 | buffers.push_back(makeBuffer(m_status)); 158 | for (auto & header: m_headers) { 159 | buffers.emplace_back(asio::buffer(header.data(), header.storage_size())); 160 | } 161 | buffers.push_back(asio::buffer(g_crlf)); 162 | std::visit([&buffers](const auto & val) { 163 | using ContentType = std::remove_cvref_t; 164 | if constexpr (std::is_same_v) { 165 | 166 | buffers.push_back(asio::buffer(val.data(), val.storage_size())); 167 | 168 | } else { 169 | 170 | buffers.push_back(asio::buffer(val.data(), val.size())); 171 | } 172 | 173 | }, m_content); 174 | return buffers; 175 | } 176 | 177 | auto HttpResponse::contentLength() const -> size_t { 178 | return std::visit([](const auto & val) { 179 | using ContentType = std::remove_cvref_t; 180 | if constexpr (std::is_same_v) { 181 | 182 | return val.storage_size(); 183 | 184 | } else { 185 | 186 | return val.size(); 187 | } 188 | 189 | }, m_content); 190 | } 191 | 192 | 193 | -------------------------------------------------------------------------------- /src/main.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022, Eugene Gershnik 2 | // SPDX-License-Identifier: BSD-3-Clause 3 | 4 | #include "app_state.h" 5 | #include "server_manager.h" 6 | #include "exc_handling.h" 7 | 8 | #define EXIT_RELOAD 2 9 | 10 | static_assert(EXIT_RELOAD != EXIT_FAILURE); 11 | 12 | 13 | static std::random_device g_RandomDevice; 14 | std::mt19937 g_Random(g_RandomDevice()); 15 | 16 | static std::optional g_maybeChildProcess; 17 | static std::atomic g_reload = 0; 18 | static ptl::SignalSet g_controlSignals; 19 | 20 | 21 | static inline void blockSignals() { 22 | ptl::setSignalProcessMask(SIG_BLOCK, g_controlSignals); 23 | } 24 | static inline void unblockSignals() { 25 | ptl::setSignalProcessMask(SIG_UNBLOCK, g_controlSignals); 26 | } 27 | 28 | static auto waitForChild() -> std::optional { 29 | 30 | WSDLOG_INFO("Waiting for child"); 31 | 32 | auto oldSigInt = ptl::setSignalHandler(SIGINT, [](int) { 33 | assert(g_maybeChildProcess); 34 | (void)::kill(g_maybeChildProcess->get(), SIGINT); 35 | }); 36 | auto oldSigTerm = ptl::setSignalHandler(SIGTERM, [](int) { 37 | assert(g_maybeChildProcess); 38 | (void)::kill(g_maybeChildProcess->get(), SIGTERM); 39 | }); 40 | auto oldSigHup = ptl::setSignalHandler(SIGHUP, [](int) { 41 | g_reload = 1; 42 | assert(g_maybeChildProcess); 43 | (void)::kill(g_maybeChildProcess->get(), SIGINT); 44 | }); 45 | 46 | unblockSignals(); 47 | 48 | int status = 0; 49 | for ( ; ; ) { 50 | ptl::AllowedErrors ec; 51 | auto maybeStatus = g_maybeChildProcess->wait(ec); 52 | if (maybeStatus) { 53 | status = *maybeStatus; 54 | break; 55 | } 56 | } 57 | ptl::setSignalHandler(SIGINT, oldSigInt); 58 | ptl::setSignalHandler(SIGINT, oldSigTerm); 59 | ptl::setSignalHandler(SIGINT, oldSigHup); 60 | 61 | assert(!*g_maybeChildProcess); 62 | g_maybeChildProcess = std::nullopt; 63 | 64 | if (WIFSIGNALED(status)) { //child exited by signal 65 | int termsig = WTERMSIG(status); 66 | if (termsig == SIGINT) { 67 | WSDLOG_INFO("Child killed by SIGINT"); 68 | return std::nullopt; 69 | } 70 | WSDLOG_INFO("Child killed by signal {} - exiting", ptl::signalName(termsig)); 71 | return EXIT_FAILURE; 72 | } 73 | 74 | if (int exitCode = WEXITSTATUS(status); exitCode != 0) { //child exited with failure 75 | if (exitCode == EXIT_RELOAD) { 76 | WSDLOG_INFO("Child exited with EXIT_RELOAD"); 77 | g_reload = 1; 78 | return std::nullopt; 79 | } 80 | WSDLOG_INFO("Child exited with code {} - exiting", exitCode); 81 | return exitCode; 82 | } 83 | 84 | WSDLOG_INFO("Child exited successfully"); 85 | return std::nullopt; 86 | } 87 | 88 | 89 | static void serve(const refcnt_ptr & config, ptl::FileDescriptor * monitorDesc) { 90 | 91 | static char dummyBuffer[1]; 92 | 93 | WSDLOG_INFO("Starting processing"); 94 | 95 | asio::io_context ctxt; 96 | 97 | ServerManager serverManager(ctxt, config, createInterfaceMonitor, createHttpServer, createUdpServer); 98 | 99 | std::shared_ptr monitorPipe; 100 | asio::signal_set signals(ctxt, SIGINT, SIGTERM, SIGHUP); 101 | 102 | signals.async_wait([&](const asio::error_code & ec, int signo){ 103 | if (ec) 104 | throw std::system_error(ec, "async waiting for signal failed"); 105 | WSDLOG_INFO("Received signal: {}", ptl::signalName(signo)); 106 | serverManager.stop(true); 107 | if (monitorPipe) 108 | monitorPipe.reset(); 109 | if (signo == SIGHUP) 110 | g_reload = 1; 111 | }); 112 | unblockSignals(); 113 | 114 | 115 | if (monitorDesc) { 116 | monitorPipe = std::make_shared(ctxt, monitorDesc->get()); 117 | monitorDesc->detach(); 118 | monitorPipe->async_read_some(asio::buffer(dummyBuffer), [&](asio::error_code /*ec*/, size_t /*bytesRead*/) { 119 | if (!monitorPipe) 120 | return; 121 | WSDLOG_INFO("Parent process exited"); 122 | kill(getpid(), SIGINT); 123 | }); 124 | } 125 | 126 | serverManager.start(); 127 | 128 | ctxt.run(); 129 | 130 | WSDLOG_INFO("Stopped processing"); 131 | } 132 | 133 | auto runServer(AppState & appState) -> int { 134 | 135 | try { 136 | for ( ; ; ) { 137 | 138 | ptl::Pipe monitorPipe; 139 | 140 | blockSignals(); 141 | 142 | appState.reload(); 143 | g_reload = 0; 144 | 145 | if (appState.shouldFork()) { 146 | 147 | WSDLOG_INFO("Starting child"); 148 | 149 | monitorPipe = ptl::Pipe::create(); 150 | 151 | appState.preFork(); 152 | 153 | g_maybeChildProcess = ptl::forkProcess(); 154 | } 155 | 156 | if (!g_maybeChildProcess) { //standalone 157 | 158 | appState.notify(AppState::DaemonStatus::Ready); 159 | serve(appState.config(), nullptr); 160 | 161 | if (!g_reload) { 162 | appState.notify(AppState::DaemonStatus::Stopping); 163 | return EXIT_SUCCESS; 164 | } 165 | 166 | } else if (!*g_maybeChildProcess) { //child 167 | 168 | #if HAVE_OS_LOG 169 | OsLogHandle::resetInChild(); 170 | #endif 171 | 172 | WSDLOG_INFO("Child started"); 173 | 174 | appState.postForkInServerProcess(); 175 | 176 | monitorPipe.writeEnd.close(); 177 | 178 | serve(appState.config(), &monitorPipe.readEnd); 179 | 180 | return g_reload ? EXIT_RELOAD : EXIT_SUCCESS; 181 | 182 | } else { //parent 183 | 184 | monitorPipe.readEnd.close(); 185 | 186 | appState.notify(AppState::DaemonStatus::Ready); 187 | if (auto res = waitForChild()) 188 | return *res; 189 | 190 | if (!g_reload) { 191 | appState.notify(AppState::DaemonStatus::Stopping); 192 | return EXIT_SUCCESS; 193 | } 194 | } 195 | 196 | appState.notify(AppState::DaemonStatus::Reloading); 197 | WSDLOG_INFO("Reloading configuration"); 198 | } 199 | 200 | } catch (std::exception & ex) { 201 | WSDLOG_ERROR("Exception: {}", ex.what()); 202 | WSDLOG_ERROR("{}", formatCaughtExceptionBacktrace()); 203 | } 204 | return EXIT_FAILURE; 205 | } 206 | 207 | 208 | int main(int argc, char * argv[]) { 209 | 210 | try { 211 | g_controlSignals.add(SIGINT); 212 | g_controlSignals.add(SIGTERM); 213 | g_controlSignals.add(SIGHUP); 214 | blockSignals(); 215 | 216 | //set default handlers 217 | ptl::setSignalHandler(SIGINT, [](int) { 218 | exit(EXIT_SUCCESS); 219 | }); 220 | ptl::setSignalHandler(SIGTERM, [](int) { 221 | exit(EXIT_SUCCESS); 222 | }); 223 | ptl::setSignalHandler(SIGHUP, SIG_IGN); 224 | 225 | umask(S_IRWXG | S_IRWXO); 226 | 227 | AppState appState(argc, argv, {SIGINT, SIGTERM, SIGHUP}); 228 | 229 | return runServer(appState); 230 | 231 | } catch (std::exception & ex) { 232 | fmt::print(stderr, "Exception: {}\n", ex.what()); 233 | fmt::print(stderr, "{}", formatCaughtExceptionBacktrace()); 234 | } 235 | return EXIT_FAILURE; 236 | } 237 | -------------------------------------------------------------------------------- /src/http_request_parser.cpp: -------------------------------------------------------------------------------- 1 | 2 | // Copyright (c) 2022, Eugene Gershnik 3 | // Copyright (c) 2003-2022 Christopher M. Kohlhoff (chris at kohlhoff dot com) 4 | // SPDX-License-Identifier: BSD-3-Clause 5 | 6 | #include "http_request_parser.h" 7 | 8 | static inline 9 | auto boundedAddDigit(unsigned & value, unsigned digit, unsigned maxVal) -> bool { 10 | 11 | unsigned val = value; 12 | if (maxVal / 10 < val) 13 | return false; 14 | val *= 10; 15 | if (maxVal - digit < val) 16 | return false; 17 | val += digit; 18 | 19 | value = val; 20 | return true; 21 | } 22 | 23 | void HttpRequestParser::reset() { 24 | m_state = method_start; 25 | m_methodBuilder.clear(); 26 | m_uriBuilder.clear(); 27 | m_versionMajor = 0; 28 | m_versionMinor = 0; 29 | m_headerNameBuilder.clear(); 30 | m_headerValueBuilder.clear(); 31 | m_totalHeadersSize = 0; 32 | } 33 | 34 | auto HttpRequestParser::consume(HttpRequest & req, uint8_t input) -> ResultType { 35 | switch (m_state) { 36 | case method_start: 37 | if (!isChar(input) || isCtl(input) || isTSpecial(input)) 38 | return Bad; 39 | 40 | m_methodBuilder.append(input); 41 | m_state = method; 42 | return Indeterminate; 43 | 44 | case method: 45 | if (input == u8' ') { 46 | req.method = m_methodBuilder.build(); 47 | m_state = uri; 48 | return Indeterminate; 49 | } 50 | if (!isChar(input) || isCtl(input) || isTSpecial(input)) 51 | return Bad; 52 | 53 | if (m_methodBuilder.storage_size() == s_maxMethodSize) 54 | return Bad; 55 | m_methodBuilder.append(input); 56 | return Indeterminate; 57 | 58 | case uri: 59 | if (input == u8' ') { 60 | if (m_uriBuilder.empty()) 61 | return Bad; 62 | 63 | req.uri = m_uriBuilder.build(); 64 | m_state = http_version_h; 65 | return Indeterminate; 66 | } 67 | if (isCtl(input)) 68 | return Bad; 69 | 70 | if (m_uriBuilder.storage_size() == s_maxUriSize) 71 | return Bad; 72 | m_uriBuilder.append(input); 73 | return Indeterminate; 74 | 75 | case http_version_h: 76 | if (input == u8'H') { 77 | m_state = http_version_t_1; 78 | return Indeterminate; 79 | } 80 | return Bad; 81 | 82 | case http_version_t_1: 83 | if (input == u8'T') { 84 | m_state = http_version_t_2; 85 | return Indeterminate; 86 | } 87 | return Bad; 88 | 89 | case http_version_t_2: 90 | if (input == u8'T') { 91 | m_state = http_version_p; 92 | return Indeterminate; 93 | } 94 | return Bad; 95 | 96 | case http_version_p: 97 | if (input == u8'P') { 98 | m_state = http_version_slash; 99 | return Indeterminate; 100 | } 101 | return Bad; 102 | 103 | case http_version_slash: 104 | if (input == u8'/') { 105 | m_state = http_version_major_start; 106 | return Indeterminate; 107 | } 108 | return Bad; 109 | 110 | case http_version_major_start: 111 | if (isDigit(input)) { 112 | unsigned digit = input - '0'; 113 | if (digit == 0 || digit > std::get<0>(s_maxVersion)) 114 | return Bad; 115 | m_versionMajor = digit; 116 | m_state = http_version_major; 117 | return Indeterminate; 118 | } 119 | return Bad; 120 | 121 | case http_version_major: 122 | if (input == u8'.') { 123 | if (m_versionMajor < std::get<0>(s_minVersion)) 124 | return Bad; 125 | m_state = http_version_minor_start; 126 | return Indeterminate; 127 | } 128 | if (isDigit(input)) { 129 | unsigned digit = input - '0'; 130 | unsigned maxMajor = std::get<0>(s_maxVersion); 131 | if (!boundedAddDigit(m_versionMajor, digit, maxMajor)) 132 | return Bad; 133 | return Indeterminate; 134 | } 135 | return Bad; 136 | 137 | case http_version_minor_start: 138 | if (isDigit(input)) { 139 | unsigned digit = input - '0'; 140 | if (m_versionMajor == std::get<0>(s_maxVersion) && digit > std::get<1>(s_maxVersion)) 141 | return Bad; 142 | m_versionMinor = digit; 143 | m_state = http_version_minor; 144 | return Indeterminate; 145 | } 146 | return Bad; 147 | 148 | case http_version_minor: 149 | if (input == u8'\r') { 150 | if (m_versionMajor == std::get<0>(s_minVersion) && m_versionMinor < std::get<1>(s_minVersion)) 151 | return Bad; 152 | req.versionMajor = m_versionMajor; 153 | req.versionMinor = m_versionMinor; 154 | m_state = expecting_newline_1; 155 | return Indeterminate; 156 | } 157 | if (isDigit(input)) { 158 | unsigned digit = input - '0'; 159 | unsigned maxMinor = m_versionMajor == std::get<0>(s_maxVersion) ? std::get<1>(s_maxVersion) : std::numeric_limits::max(); 160 | if (!boundedAddDigit(m_versionMinor, digit, maxMinor)) 161 | return Bad; 162 | return Indeterminate; 163 | } 164 | return Bad; 165 | 166 | case expecting_newline_1: 167 | if (input == u8'\n') { 168 | m_state = header_line_start; 169 | return Indeterminate; 170 | } 171 | return Bad; 172 | 173 | case header_line_start: 174 | if (input == u8'\r') { 175 | m_state = expecting_newline_3; 176 | return Indeterminate; 177 | } 178 | if (!m_headerValueBuilder.empty() && (input == u8' ' || input == u8'\t')) { 179 | m_state = header_lws; 180 | return Indeterminate; 181 | } 182 | if (!isChar(input) || isCtl(input) || isTSpecial(input)) 183 | return Bad; 184 | 185 | if (m_totalHeadersSize == s_maxHeadersSize) 186 | return Bad; 187 | m_headerNameBuilder.append(input); 188 | ++m_totalHeadersSize; 189 | m_state = header_name; 190 | return Indeterminate; 191 | 192 | case header_lws: 193 | if (input == u8'\r') { 194 | m_state = expecting_newline_2; 195 | return Indeterminate; 196 | } 197 | if (input == u8' ' || input == u8'\t') 198 | return Indeterminate; 199 | 200 | if (isCtl(input)) 201 | return Bad; 202 | 203 | m_state = header_value; 204 | if (m_totalHeadersSize == s_maxHeadersSize) 205 | return Bad; 206 | m_headerValueBuilder.append(input); 207 | ++m_totalHeadersSize; 208 | return Indeterminate; 209 | 210 | case header_name: 211 | if (input == u8':') { 212 | 213 | m_state = space_before_header_value; 214 | return Indeterminate; 215 | } 216 | if (!isChar(input) || isCtl(input) || isTSpecial(input)) 217 | return Bad; 218 | 219 | if (m_totalHeadersSize == s_maxHeadersSize) 220 | return Bad; 221 | m_headerNameBuilder.append(input); 222 | ++m_totalHeadersSize; 223 | return Indeterminate; 224 | 225 | case space_before_header_value: 226 | if (input == u8' ') { 227 | m_state = header_value; 228 | return Indeterminate; 229 | } 230 | return Bad; 231 | 232 | case header_value: 233 | if (input == u8'\r') { 234 | m_state = expecting_newline_2; 235 | return Indeterminate; 236 | } 237 | if (isCtl(input)) 238 | return Bad; 239 | 240 | if (m_totalHeadersSize == s_maxHeadersSize) 241 | return Bad; 242 | m_headerValueBuilder.append(input); 243 | ++m_totalHeadersSize; 244 | return Indeterminate; 245 | 246 | case expecting_newline_2: 247 | if (input == u8'\n') { 248 | req.headers.emplace(m_headerNameBuilder.build(), m_headerValueBuilder.build()); 249 | m_state = header_line_start; 250 | return Indeterminate; 251 | } 252 | return Bad; 253 | 254 | case expecting_newline_3: 255 | return (input == u8'\n') ? Good : Bad; 256 | 257 | } 258 | 259 | return Bad; 260 | } 261 | 262 | 263 | --------------------------------------------------------------------------------