From 4ec6d55406cb7c431e633e89c19c16a3af30a909 Mon Sep 17 00:00:00 2001 From: Jeremy Blum Date: Tue, 16 Feb 2021 09:40:25 -0800 Subject: [PATCH 01/11] RC1 Commit of v3 Beta Adds an automated installer script that takes out all of the guess work. Tested and working on the latest versions of Raspbian, Apache, and PHP. Adds full support for lets-encrypt signed SSL certificates with automatic setup. Improves security of password hashing approach. Simplifies configuration of dynamic DNS updating functionality. --- .gitignore | 3 +- README.md | 9 +- apache2_configs/000-default_http.conf | 24 + .../000-default_self-https.conf | 2 + apache2_configs/ssl.conf | 20 + config_sample.php | 50 -- setup.py | 832 ++++++++++++++++++ ssl.conf | 19 - .htaccess => www/html/.htaccess | 0 .../bootstrap}/css/bootstrap-responsive.css | 0 .../html/bootstrap}/css/bootstrap.css | 0 .../ico/apple-touch-icon-114-precomposed.png | Bin .../ico/apple-touch-icon-144-precomposed.png | Bin .../ico/apple-touch-icon-57-precomposed.png | Bin .../ico/apple-touch-icon-72-precomposed.png | Bin .../html/bootstrap}/ico/favicon.png | Bin .../html/bootstrap}/js/bootstrap.min.js | 0 www/html/config_sample.php | 59 ++ index.php => www/html/index.php | 32 +- www/html/knock_knock.txt | 4 + 20 files changed, 966 insertions(+), 88 deletions(-) create mode 100644 apache2_configs/000-default_http.conf rename 000-default.conf => apache2_configs/000-default_self-https.conf (90%) create mode 100644 apache2_configs/ssl.conf delete mode 100644 config_sample.php create mode 100644 setup.py delete mode 100644 ssl.conf rename .htaccess => www/html/.htaccess (100%) rename {bootstrap => www/html/bootstrap}/css/bootstrap-responsive.css (100%) rename {bootstrap => www/html/bootstrap}/css/bootstrap.css (100%) rename {bootstrap => www/html/bootstrap}/ico/apple-touch-icon-114-precomposed.png (100%) rename {bootstrap => www/html/bootstrap}/ico/apple-touch-icon-144-precomposed.png (100%) rename {bootstrap => www/html/bootstrap}/ico/apple-touch-icon-57-precomposed.png (100%) rename {bootstrap => www/html/bootstrap}/ico/apple-touch-icon-72-precomposed.png (100%) rename {bootstrap => www/html/bootstrap}/ico/favicon.png (100%) rename {bootstrap => www/html/bootstrap}/js/bootstrap.min.js (100%) create mode 100644 www/html/config_sample.php rename index.php => www/html/index.php (94%) create mode 100644 www/html/knock_knock.txt diff --git a/.gitignore b/.gitignore index b73f9d1..798a79a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ config.php -Thumbs.db \ No newline at end of file +Thumbs.db +.setup_progress* \ No newline at end of file diff --git a/README.md b/README.md index b2537af..78e0c64 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,18 @@ REMOTE WAKE/SLEEP-ON-LAN SERVER *(RWSOLS)* ========================================== -The Remote Wake/Sleep-on-LAN Server (RWSOLS) is a simple webapp that runs on your Raspberry Pi to turn it into a remotely accessible Wake/Sleep-On-LAN Server. This is very useful when you have a high-powered machine that you don't want to keep on all the time, but that you want to keep remotely accessible for Remote Desktop, SSH, FTP, etc. Wake-On-LAN packets cannot be forwarded through a router, so to wake up a remote machine behind a router, you need to have something on its local network to wake it up. That's where RWSOLS comes in. RWSOLS can control an unlimited number of remote machines on its local network, and is capable of waking them up (any OS) or putting them to sleep (only Windows remote machines). It can be configured to use SSL encryption or it can be run over traditional HTTP. +The Remote Wake/Sleep-on-LAN Server (RWSOLS) is a simple webapp that runs on your Raspberry Pi to turn it into a remotely accessible Wake/Sleep-On-LAN Server. This is very useful when you have a high-powered machine that you don't want to keep on all the time, but that you want to keep remotely accessible for Remote Desktop, SSH, FTP, etc. Wake-On-LAN packets cannot be forwarded through a router, so to wake up a remote machine behind a router, you need to have something on its local network to wake it up. That's where RWSOLS comes in. RWSOLS can control an unlimited number of remote machines on its local network, and is capable of waking them up (any OS) or putting them to sleep (only Windows remote machines). It can be configured to use signed or unsigned SSL encryption or it can be run over traditional HTTP. A very detailed set of [installation instructions](https://github.com/sciguy14/Remote-Wake-Sleep-On-LAN-Server/wiki) can be found in the GitHub Wiki. You'll also find a description of [how it works](https://github.com/sciguy14/Remote-Wake-Sleep-On-LAN-Server/wiki/How-it-Works), [an FAQ](https://github.com/sciguy14/Remote-Wake-Sleep-On-LAN-Server/wiki/Notes-and-FAQs), and a list of [relevant terminology](https://github.com/sciguy14/Remote-Wake-Sleep-On-LAN-Server/wiki/Terminology) on the Wiki. -For more info, see [my blog post about RWSOLS](http://www.jeremyblum.com/2013/07/14/rpi-wol-server/) on my website. +For more info, see [my blog post about RWSOLS](https://www.jeremyblum.com/2013/07/14/rpi-wol-server/) on my website. -If you're having problems with getting RWSOLS working, check the [FAQ](https://github.com/sciguy14/Remote-Wake-Sleep-On-LAN-Server/wiki/Notes-and-FAQs) first, or [the comments](http://www.jeremyblum.com/2013/07/14/rpi-wol-server/#comments) on my blog. If you still can't get it to work, please [create a GitHub issue](https://github.com/sciguy14/Remote-Wake-Sleep-On-LAN-Server/issues) with specific details. +If you're having problems with getting RWSOLS working, check the [FAQ](https://github.com/sciguy14/Remote-Wake-Sleep-On-LAN-Server/wiki/Notes-and-FAQs) first, or [the comments](https://www.jeremyblum.com/2013/07/14/rpi-wol-server/#comments) on my blog. If you still can't get it to work, please [create a GitHub issue](https://github.com/sciguy14/Remote-Wake-Sleep-On-LAN-Server/issues) with specific details. + x86 Docker Image (Alternate Installation Option) --------------------------------------------- +------------------------------------------------ GitHub user [ex0nuss](https://github.com/ex0nuss) has created an x86 Docker Image for RWSOLS, that you may wish to try out. I have not independently validated its functionality, but it does pull directly from this Repo. You may find the setup of a Docker image to be faster and easier than following the instructions in the Wiki to do a native installation of this application on a Desktop (this is for x86, not ARM). You can find the GitHub Repo for the Docker Image [here](https://github.com/ex0nuss/Remote-Wake-Sleep-On-LAN-Docker), and the DockerHub link [here](https://hub.docker.com/r/ex0nuss/remote-wake-sleep-on-lan-docker). License diff --git a/apache2_configs/000-default_http.conf b/apache2_configs/000-default_http.conf new file mode 100644 index 0000000..4517385 --- /dev/null +++ b/apache2_configs/000-default_http.conf @@ -0,0 +1,24 @@ +# The RWSOLS setup script will overwrite the /etc/apache2/sites-available/000-default.conf file with this one when doing an HTTP-only setup. +# The RWSOLS setup script will overwrite the /etc/apache2/sites-available/000-default.conf file with this one right before running the certbot auto-setup. + + + # Non-TLS/SSL configuration. + ServerAdmin webmaster@localhost + + DocumentRoot /var/www/html + + AllowOverride AuthConfig FileInfo + Order allow,deny + allow from all + Options -Indexes + + + ErrorLog ${APACHE_LOG_DIR}/error.log + + # Possible values include: debug, info, notice, warn, error, crit, + # alert, emerg. + LogLevel error + + CustomLog ${APACHE_LOG_DIR}/access.log combined + + diff --git a/000-default.conf b/apache2_configs/000-default_self-https.conf similarity index 90% rename from 000-default.conf rename to apache2_configs/000-default_self-https.conf index bc27fcc..30b62f5 100644 --- a/000-default.conf +++ b/apache2_configs/000-default_self-https.conf @@ -1,3 +1,5 @@ +# The RWSOLS setup script will overwrite the /etc/apache2/sites-available/000-default.conf file with this one when doing a self-signed HTTPS setup. + # TLS/SSL configuration, this is for the use of crypto. diff --git a/apache2_configs/ssl.conf b/apache2_configs/ssl.conf new file mode 100644 index 0000000..6c0677f --- /dev/null +++ b/apache2_configs/ssl.conf @@ -0,0 +1,20 @@ +# RWSOLS setup script will copy this file to the apache2 mods-available folder if using a self-signed certificate. + + + SSLRandomSeed startup builtin + SSLRandomSeed startup file:/dev/urandom 512 + SSLRandomSeed connect builtin + SSLRandomSeed connect file:/dev/urandom 512 + + AddType application/x-x509-ca-cert .crt + AddType application/x-pkcs7-crl .crl + + SSLPassPhraseDialog builtin + + SSLSessionCache shmcb:${APACHE_RUN_DIR}/ssl_scache(512000) + SSLSessionCacheTimeout 300 + + SSLCipherSuite HIGH:!MEDIUM:!LOW:!aNULL:!MD5 + + SSLProtocol all -SSLv2 -SSLv3 + diff --git a/config_sample.php b/config_sample.php deleted file mode 100644 index c6a57f8..0000000 --- a/config_sample.php +++ /dev/null @@ -1,50 +0,0 @@ - diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..47f4793 --- /dev/null +++ b/setup.py @@ -0,0 +1,832 @@ +#! /usr/bin/python3 + +# Remote Wake/Sleep-On-LAN Server [SETUP SCRIPT] +# https://github.com/sciguy14/Remote-Wake-Sleep-On-LAN-Server +# Author: Jeremy E. Blum (https://www.jeremyblum.com) +# License: GPL v3 (http://www.gnu.org/licenses/gpl.html) + +# Import Libraries +import subprocess +import pathlib +import os +import signal +import sys +import urllib.request +import urllib.error +import re +import getpass +import socket +import time +import ssl +import fileinput + +# Exit Handler +def signal_handler(sig, frame): + print('\n\nABORTING SCRIPT.\n') + sys.exit(1) +signal.signal(signal.SIGINT, signal_handler) + +# Global Variables +script_dir = pathlib.Path(__file__).parent.absolute() +public_ipv4 = "127.0.0.1" +urls = [] +encryption_mode="none" # Options: "none","self","certbot","skip" +ddns_temp_config=pathlib.Path('/tmp/ddclient.conf') +ddns_real_config=pathlib.Path('/var/snap/ddclient-snap/current/etc/ddclient/ddclient.conf') +rwsols_config_sample = script_dir.joinpath('www/html/config_sample.php') +rwsols_config_user = script_dir.joinpath('www/html/config.php') + +# Colors +black = lambda text: '\033[0;30m' + text + '\033[0m' +red = lambda text: '\033[0;31m' + text + '\033[0m' +green = lambda text: '\033[0;32m' + text + '\033[0m' +yellow = lambda text: '\033[0;33m' + text + '\033[0m' +blue = lambda text: '\033[0;34m' + text + '\033[0m' +magenta = lambda text: '\033[0;35m' + text + '\033[0m' +cyan = lambda text: '\033[0;36m' + text + '\033[0m' +white = lambda text: '\033[0;37m' + text + '\033[0m' + +# Main Logic +def main(): + # Intro + print("") + print(cyan(">>> <<<")) + print(cyan(">>> Remote-Wake-Sleep-On-LAN-Server (RWSOLS) Setup Script <<<")) + print(cyan(">>> <<<\n")) + + # Warn the user that internet scripts are dangerous + print(yellow("This script will install the required packages and configure permissions as necessary for RWSOLS to work properly.")) + print(yellow("As with all scripts that you find on the internet, I strongly suggest you read its contents before blindly executing it.")) + print(yellow("This script assumes your user is a passwordless sudo user, which is the default for the 'pi' user on Raspbian. It executes commands with sudo.\n")) + print(yellow("If you want to abort this script to inspect its contents, press Ctrl+C now.")) + input(yellow("If you have read and understood what this script is doing, Press Enter to continue with the automatic setup.")) + print("") + + # Add note about skipping completed steps + print("Note: One-time steps that have already been executed will be automatically skipped.") + print("If you need to re-run them, delete the relevant dot files in this folder.\n") + time.sleep(1) + + # Install prerequisites + run_step('_01_install_prereqs', 'Install Prequisite Software') + + # Ping Permissions + run_step('_02_ping_permissions', 'Grant Ping Permission') + + # Symlink the Webroot to our files + run_step('_03_symlink_webroot', 'Symlink Apache2 Webroot') + + # ddclient DDNS Setup + run_step('_04_setup_ddns', 'Setup DDNS') + + # Get Public IPv4 Address + run_step('_05_get_ip', 'Get Public IPv4 Address', False) + + # Confirm functional URL + run_step('_06_check_urls', 'Check DNS URL Resolution', False) + + # Check if our server is up and serving content on port 80 + run_step('_07_server_check', 'Check if RWSOLS is accessible on port 80 and/or port 443', False) + + # Setup requested encryption options + if encryption_mode == "certbot": + run_step('_08_certbot_setup', 'Certbot-Signed SSL Certificate Setup', False) + elif encryption_mode == "self": + run_step('_08_selfcert_setup', 'Self-Signed SSL Certificate Setup', False) + elif encryption_mode == "none": + run_step('_08_unencrypted_setup', 'Unencrypted Setup', False) + else: + print("No Apache2 encryption setting changes will be made.\n") + + # Security-Harden PHP installation + run_step('_09_secure_php', 'Secure the PHP Installation') + + # Create user copy of config file + run_step('_10_prep_config_file', 'Generate Default WOL Config File', False) + + # Create user copy of config file + run_step('_11_modify_config_file', 'Modify WOL Config File', False) + + + print("Automatic Setup is now complete.") + + +# Function for encapsulating parts of the setup process that can be skipped on script re-run +# handle: short name for setup step +# description: human-understandable setup setup name +# dot_file_skippable: If True, this step will be skipped if already completed in previous run +# Returns True if the setup step should be run, False if it was already previously completed +def run_step(handle, description, dot_file_skippable=True): + if script_dir.joinpath("." + handle).exists() and dot_file_skippable: + print(blue("'" + description + "' has already been completed. Skipping.\n")) + else: + print(cyan("Running: ") + description) + if eval(handle + '()'): + print(green("Done: ") + description + "\n") + if dot_file_skippable: + script_dir.joinpath("." + handle).touch() + else: + print(red("Script Exiting due to failure in '" + description + "' setup step.\n")) + sys.exit(1) + +# Setup Step 1: Install Prequisite Software +def _01_install_prereqs(): + try: + subprocess.run(['sudo', 'apt-get', 'update'], check=True) + subprocess.run(['sudo', 'apt-get', '-y', 'install', 'wakeonlan', 'git', 'apache2', 'php7.3', 'php7.3-curl', 'libapache2-mod-php7.3', 'snapd'], check=True) + subprocess.run(['sudo', 'snap', 'install', 'core'], check=True) + subprocess.run(['sudo', 'snap', 'refresh', 'core'], check=True) + subprocess.run(['sudo', 'apt-get', '-y', 'remove', 'certbot']) #Remove any existing installations from package managers + subprocess.run(['sudo', 'snap', 'install', '--classic', 'certbot'], check=True) + subprocess.run(['sudo', 'ln', '-sf', '/snap/bin/certbot', '/usr/bin/certbot'], check=True) + except subprocess.CalledProcessError as e: + print(yellow("Error installing prerequisite software.")) + return False + return True + +# Setup Step 2: The PHP server uses the built-in ping command to check if the remote machine is awake or not. Give all users on the pi permission to ping. +def _02_ping_permissions(): + try: + subprocess.run(['sudo', 'chmod', 'u+s', '/usr/bin/ping'], check=True) + except subprocess.CalledProcessError as e: + print(yellow("Error setting ping permissions.")) + return False + return True + +# Setup Step 3: Symlink Apache2 Webroot +def _03_symlink_webroot(): + try: + apache_www_dir = pathlib.Path('/var/www') + wol_www_dir = script_dir.joinpath('www') + if apache_www_dir.exists(): + # If there is a directory here that isn't symlinked, we back it up. + if not apache_www_dir.is_symlink(): + subprocess.run(['sudo', 'mv', str(apache_www_dir), str(apache_www_dir) + "_backup"], check=True) + subprocess.run(['sudo', 'ln', '-sf', str(wol_www_dir), str(apache_www_dir)], check=True) + except Exception as e: + print(yellow("Error symlinking apache webroot to local folder.")) + print(yellow(str(e))) + return False + return True + +# Setup Step 4: DDNS Configuration +def _04_setup_ddns(): + ddns_option, _ = multi_choice('Will you handle Dynamic DNS updates from your router, or would you like to setup Dynamic DNS updating on this Pi?', ['I will handle DDNS on my router or elesewhere.','I want to configure DDNS updates from this Pi.']) + print ("") + if ddns_option == 1: + print("Ok. If you haven't already, go configure your router or other device to update your DDNS service of choice, now.") + input("Once you've done that, press Enter to proceed, and the URL/IP will be checked.") + return True + elif ddns_option == 2: + # Setup ddclient so that our URL points to our public IP. If we don't do this, certbot won't be able to complete the port 80 challenge. + + input("ddclient will be installed via snap. apt-installed version will be removed if present. Press enter to proceed.") + print("") + print("Installing ddclient and required packages...") + try: + subprocess.run(['sudo', 'apt', 'install', '-y', 'libio-socket-ssl-perl'], check=True) + # We install ddclient via snap, as the latest version isn't in the apt repo as of Jan 2021. + # Also, we want to bypass the broken debconf wizard, and this does that too. + subprocess.run(['sudo', 'apt-get', '-y', 'remove', 'ddclient']) #Remove any existing installations from package managers + subprocess.run(['sudo', 'snap', 'refresh', 'core'], check=True) + subprocess.run(['sudo', 'snap', 'install', 'ddclient-snap',], check=True) + subprocess.run(['sudo', 'ln', '-sf', '/snap/bin/ddclient-snap.exec', '/usr/sbin/ddclient'], check=True) + except subprocess.CalledProcessError as e: + print(yellow("Error installing ddclient and/or libio-socket-ssl-perl.")) + print(yellow(str(e))) + return False + + + print("\nNow we need to setup ddclient on this device.") + ddclient_config_option, _ = multi_choice("Would you like to use this script's wizard to help you generate your ddclient config file, or would you like to provide your own config file?", ['Please help me generate the config file.','I will manually create the ddclient config file (advanced!).']) + if ddclient_config_option == 1: + print("\nGreat! Answer the following questions to generate your config file. You'll have a chance to review it and re-answer if you make a mistake.") + # Now, get the info necessary to create the ddclient.conf file + while(True): + with open(ddns_temp_config, 'w') as f: + f.write('# Generated by RWSOLS Setup Script: https://github.com/sciguy14/Remote-Wake-Sleep-On-LAN-Server\n') + f.write('use=web\n') # We are running this behind a router, so we must use the web method to get the IP + f.write('ssl=yes\n') # Don't send our password in cleartext! + i, v = multi_choice('Choose a DDNS Protocol.', ['dyndns2','noip','googledomains','cloudflare','namecheap','zoneedit1','easydns','nfsn','yandex','ovh','dinahosting']) + f.write('protocol=' + v + '\n') + while(True): + v = input('Enter your Server URL (ex: members.dyndns.org). Leave blank if not needed by your service/protocol: ') + if v == "": + break # No Server value provided + elif is_valid_hostname(v): + f.write('server=' + v + '\n') + break + else: + print ("'" + v + "' is not a valid server. Please try again.") + v = input('Enter your login username for the service: ') + f.write('login=' + v + '\n') + v = getpass.getpass('Enter your login password for the service: ') + f.write('password=' + v + '\n') + global urls + urls = enter_urls('Enter all your Hostname(s) to update (ex: myhome.com)(multi ex: sub1.myhome.com, myhome.com): ') + f.write(",".join(urls) + '\n') + print("\nYour config file contents will be written as follows:") + with open(ddns_temp_config, 'r') as f: + for line in f: + print("\t" + line.strip()) + config_okay, _ = multi_choice('Does this look okay?',['Yes Please write this configuration file', 'No. I want to do the wizard again.']) + if config_okay == 1: + break + else: + print("\nOkay. The wizard will run again.\n") + try: + subprocess.run(['sudo', 'cp', str(ddns_temp_config), str(ddns_real_config)], check=True) + except subprocess.CalledProcessError as e: + print(yellow("Error copying config file.")) + return False + elif ddclient_config_option == 2: + print("Okay. It recommend that you set use=web and ssl=yes. When you're done editing, save and exit nano to proceed.") + input("Press enter to open the config file with nano.") + try: + subprocess.run(['sudo', 'nano', str(ddns_real_config)], check=True) + except subprocess.CalledProcessError as e: + print(yellow("Error editing config file.")) + return False + print("The config file has been written.\n") + + print("Launching ddclient daemon...") + try: + subprocess.run(['sudo', 'snap', 'restart', 'ddclient-snap.daemon'], check=True) + except subprocess.CalledProcessError as e: + print(yellow("Error launching ddclient daemon.")) + return False + + print("Waiting 5 seconds for DNS update to execute before proceeding...") + time.sleep(5) + return True + +# Setup Step 5: Get Public IP - we'll need it for checking existing DDNS configs, or for configuring a new one +def _05_get_ip(): + global public_ipv4 + try: + public_ipv4 = urllib.request.urlopen('https://v4.ident.me').read().decode('utf8') + except urllib.error.URLError as e: + if hasattr(e, 'reason'): + print(yellow('Failed to reach the ident.me server.')) + print(yellow('Reason: ' + e.reason)) + elif hasattr(e, 'code'): + print(yellow('The ident.me server couldn\'t fulfill the request.')) + print(yellow('Error code: ' + e.code)) + return False + print("The public-facing IPv4 address of this Pi's network was detected as " + cyan(public_ipv4) + ".") + return True + +# Setup Step 6: Confirm that the DNS update suceeded +def _06_check_urls(): + fill_urls_var() + max_attempts = 5 + backoff_exponent = 2 + + # Now we check to see if each of the URLs resolves to our public-facing IP address. + failures = False + for url in urls: + try: + resolved_ip = socket.gethostbyname(url) + except socket.gaierror as e: + print(yellow("Failed to resolve " + url + "! Is it a valid URL?")) + failures = True + else: + attempt = 1 + for attempt in range(1, max_attempts+1): + if resolved_ip != public_ipv4: + if attempt < max_attempts: + print("[Attempt " + str(attempt) + "/" + str(max_attempts) + "] " + yellow(url + " resolves to " + resolved_ip + " instead of detected public IP of " + public_ipv4 + "! ") + "Checking again in " + str(attempt**backoff_exponent) + " seconds.") + time.sleep(attempt**backoff_exponent) + else: + print("[Attempt " + str(attempt) + "/" + str(max_attempts) + "] " + yellow(url + " resolves to " + resolved_ip + " instead of detected public IP of " + public_ipv4 + "! DDNS configured incorrectly, or it hasn't updated yet.")) + failures = True + else: + print("[Attempt " + str(attempt) + "/" + str(max_attempts) + "] " + cyan(url) + " correctly resolves to " + cyan(public_ipv4) + "!") + break + else: + print(yellow(url + " resolves to " + resolved_ip + " instead of detected public IP of " + public_ipv4 + "! ")) + if failures: + print(yellow("One or more of your URLs failed to resolve! Try running this script again after checking your domains and your DDNS configuration.")) + print(yellow("Delete the setup_ddns dot file in this directory if you need to reconfigure local DDNS updating.")) + return False + return True + +# Setup Step 7: Check to see if port 80 and/or port 443 is currently serving RWSOLs to correctly display options for cert setup +def _07_server_check(): + global encryption_mode + no_encryption_option_text = 'I do not wish to enable encryption. (You can still manually setup certbot later using a Port 80 or DNS challenege).' + disable_encryption_option_text = 'I wish to disable encryption, making RWSOLS accessible over an unencrypted connection only.' + switch_to_unsigned_option_text = 'I wish to change from a signed certificate to an unsigned certificate.' + switch_to_signed_option_text = 'I wish to change from an unsigned certificate to a signed certificate (RECOMMENDED OPTION).' + retain_encryption_settings_option_text = 'I wish to retain the existing encryption settings.' + certbot_encryption_option_text = 'Proceed with automatic certbot signed encryption setup (STRONGLY RECOMMENDED OPTION)' + self_signed_encryption_option_text = 'I would like this script to automatically configure encryption using a self-signed certificate.' + exit_and_fix_option_text = 'Please exit the script - I will forward port 80 and run the script again once I have done that.' + + http_server, http_rwsols, https_server, https_rwsols, https_signed = rwsols_serving_status() + print("") + + # RWSOLS is already running with a signed cert + if https_rwsols and https_signed: + print("Your RWSOLS is already acceessible over an encrypted connection with a valid signed certificate.") + c, _ = multi_choice('How would you like to proceed?', [retain_encryption_settings_option_text, switch_to_unsigned_option_text, disable_encryption_option_text]) + if c == 1: + encryption_mode="skip" + elif c == 2: + encryption_mode="self" + else: + encryption_mode="none" + return True + + # RWSOLS is already running with a self-signed cert, and port 80 is accessible + elif http_rwsols and https_rwsols and not https_signed: + print("Your RWSOLS is already acceessible over an encrypted connection, but it is using a self-signed certificate.") + print("Port 80 is also correctly resolving to RWSOLS, so automatic certbot configuration should be possible, and is recommended.") + c, _ = multi_choice('How would you like to proceed?', [switch_to_signed_option_text, retain_encryption_settings_option_text, disable_encryption_option_text]) + if c == 1: + encryption_mode="certbot" + elif c == 2: + encryption_mode="skip" + else: + encryption_mode="none" + return True + + # RWSOLS is already running with a self-signed cert and port 80 is not accessible + elif not http_rwsols and https_rwsols and not https_signed: + print("Your RWSOLS is already acceessible over an encrypted connection, but it is using a self-signed certificate.") + print("Port 80 does not appear to be open and pointed to RWSOLS, so automatic certbot configuration is not possible.") + c, _ = multi_choice('How would you like to proceed?', [exit_and_fix_option_text, retain_encryption_settings_option_text, disable_encryption_option_text]) + if c == 1: + local_ip = get_local_ip() + if local_ip: + print("\nOk! Forward ports 80 and 443 to this Pi (your Pi's local IP that you should forward to is " + magenta(local_ip) + ").") + return False + elif c == 2: + encryption_mode="skip" + else: + encryption_mode="none" + return True + + # RWSOLS is running on port 80, but doesn't have encryption setup + elif http_rwsols: + print("It looks like RWSOLS is active on port 80, but HTTPS is not configured.") + print("Certbot should be able to automatically issue and configure a signed, auto-renewing encryption certificate.") + c, _ = multi_choice('How would you like to proceed?', [certbot_encryption_option_text, self_signed_encryption_option_text, no_encryption_option_text]) + if c == 1: + encryption_mode="certbot" + elif c == 2: + encryption_mode="self" + else: + encryption_mode="none" + return True + + # RWSOLS is not currently accessible + else: + print("In order to automatically configure SSL Encryption, port 80 must be open on your router and forwarded to port 80 on this Pi.") + print("If you are unable to forward port 80 because you are using it for another service, or because your ISP blocks it, you can configure certbot to perform a DNS challenge instead, or this script can setup unsigned encryption.") + c, _ = multi_choice('How would you like to proceed?', [exit_and_fix_option_text , self_signed_encryption_text, no_encryption_option_text]) + if c == 1: + local_ip = get_local_ip() + if local_ip: + print("\nOk! Forward ports 80 and 443 to this Pi (your Pi's local IP that you should forward to is " + magenta(local_ip) + ").") + return False + elif c == 2: + encryption_mode="self" + else: + encryption_mode="none" + return True + + +# Setup Step 8: Certbot config +def _08_certbot_setup(): + # Let's Encrypt! + print("A valid email address is required so that the Let's Encrypt Certificate Authority can email you if there is problem with the automatic renewal of your certificate.") + print("This email will NOT be signed up for a mailing list.") + while True: + email = input("Please provide a valid email: ") + if not re.fullmatch(r"[^@]+@[^@]+\.[^@]+", email): + print(yellow("That doesn't look like a valid email...")) + else: + break + fill_urls_var() + + # In case setup has previously run and has copied over the site config with the SSL port configured, we restore the the HTTP-only config so that there is only one virtualhost for certbot + try: + bak = copy_config_with_backup(script_dir.joinpath('apache2_configs/000-default_http.conf'), '/etc/apache2/sites-available/000-default.conf') + subprocess.run(['sudo', 'service', 'apache2', 'restart'], check=True) + except subprocess.CalledProcessError as e: + print(yellow("Error getting Apache2 site config into deafult state before running certbot.")) + print(yellow(str(e))) + return False + + # Run certbot non-interactively + try: + subprocess.run(['sudo', 'certbot', '--apache', '--non-interactive', '--agree-tos', '--redirect', '--uir', '--hsts', '--staple-ocsp', '--must-staple', '-d', ','.join(urls), '--email', email], check=True) + except subprocess.CalledProcessError as e: + print(yellow("Error running certbot.")) + print(yellow(str(e))) + + # Restore the config file that we replaced in the event of a failure running certbot + copy_config_with_backup(bak, '/etc/apache2/sites-available/000-default.conf') + + return False + return True + +# Alternate Setup Step 8: Self-Signed Cert Setup +def _08_selfcert_setup(): + fill_urls_var() + try: + # Delete existing Certbot Entries + for url in urls: + subprocess.run(['sudo', 'certbot', 'delete', '-q', '--cert-name', url], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + + # Disable the certbot site if enabled from previously being setup with the script + subprocess.run(['sudo', 'a2dissite', '000-default-le-ssl'], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + + subprocess.run(['sudo', 'mkdir', '-p', '/etc/apache2/ssl'], check=True) + subprocess.run(['sudo', 'openssl', 'req', '-new', '-newkey', 'rsa:4096', '-days', '365', '-nodes', '-x509', '-subj', '/C=AQ/ST=Unlisted/L=Unlisted/O=RWSOLS/CN=%s' % urls[0], '-keyout', '/etc/apache2/ssl/wol.key', '-out', '/etc/apache2/ssl/wol.crt'], check=True) + subprocess.run(['sudo', 'a2enmod', 'ssl'], check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + subprocess.run(['sudo', 'a2enmod', 'headers'], check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + copy_config_with_backup(script_dir.joinpath('apache2_configs/ssl.conf'), '/etc/apache2/mods-available/ssl.conf') + copy_config_with_backup(script_dir.joinpath('apache2_configs/000-default_self-https.conf'), '/etc/apache2/sites-available/000-default.conf') + subprocess.run(['sudo', 'service', 'apache2', 'restart'], check=True) + except subprocess.CalledProcessError as e: + print(yellow("Error creating self-signed key and/or enabling SSL engine.")) + print(yellow(str(e))) + return False + return True + +# Alternate Setup Step 8: Unencrypted Config (We still copy over the 000-default file because it has some security improvements, but the 443 virtual host is ignored because ssl mod is disabled) +def _08_unencrypted_setup(): + fill_urls_var() + try: + # Delete existing Certbot Entries + for url in urls: + subprocess.run(['sudo', 'certbot', 'delete', '-q', '--cert-name', url], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + + # Disable the certbot site if enabled from previously being setup with the script + subprocess.run(['sudo', 'a2dissite', '000-default-le-ssl'], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + + subprocess.run(['sudo', 'a2dismod', 'ssl'], check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + subprocess.run(['sudo', 'a2enmod', 'headers'], check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + copy_config_with_backup(script_dir.joinpath('apache2_configs/000-default_http.conf'), '/etc/apache2/sites-available/000-default.conf') + subprocess.run(['sudo', 'service', 'apache2', 'restart'], check=True) + except subprocess.CalledProcessError as e: + print(yellow("Error configuring Apache2.")) + print(yellow(str(e))) + return False + return True + +# Step 9: Make some configuration adjustments to the PHP/Apache installation to improve its security +def _09_secure_php(): + # Get Apache's php.ini file location regardless of our PHP version. + try: + r = subprocess.run(['sudo', 'find', '/etc/', '-name', 'php.ini'], capture_output=True, check=True) + php_ini_locations = r.stdout.decode('utf8').splitlines() + php_ini_apache2_location = None + for location in php_ini_locations: + if "apache2" in location: + php_ini_apache2_location = location + break + if php_ini_apache2_location is None: + print(yellow("Apache2 php.ini could not be located.")) + return False + subprocess.run(['sudo', 'sed', '-i.%s.bak' % str(int(time.time())), 's/expose_php = On/expose_php = Off/g', php_ini_apache2_location], check=True) # Note that in PHP7, this should be already disabled, but this ensures that it is. + subprocess.run(['sudo', 'sed', '-i.%s.bak' % str(int(time.time())), 's/E_ALL & ~E_NOTICE & ~E_STRICT & ~E_DEPRECATED/error_reporting = E_ERROR/g', php_ini_apache2_location], check=True) + except subprocess.CalledProcessError as e: + print(yellow("Error updating apache2 php.ini.")) + print(yellow(str(e))) + return False + try: + security_conf_location = '/etc/apache2/conf-available/security.conf' + subprocess.run(['sudo', 'sed', '-i.%s.bak' % str(int(time.time())), 's/ServerSignature On/ServerSignature Off/g', security_conf_location], check=True) + subprocess.run(['sudo', 'sed', '-i.%s.bak' % str(int(time.time())), 's/ServerTokens OS/ServerTokens Prod/g', security_conf_location], check=True) + except subprocess.CalledProcessError as e: + print(yellow("Error updating security.conf.")) + print(yellow(str(e))) + return False + try: + subprocess.run(['sudo', 'service', 'apache2', 'restart'], check=True) + except subprocess.CalledProcessError as e: + print(yellow("Error Restarting Apache.")) + print(yellow(str(e))) + return False + return True + +# Step 10: Generate default config file for WOL page if one doesn't already exist +def _10_prep_config_file(): + if rwsols_config_user.exists(): + print("An existing user config file already exists, so a default one will not be created.") + return True + + try: + subprocess.run(['cp', str(rwsols_config_sample), str(rwsols_config_user)], check=True) + except subprocess.CalledProcessError as e: + print(yellow("Error creating default config file.")) + return False + + print("A default WOL config file has been created for you.") + return True + +# Step 11: Help with editing of the config file +def _11_modify_config_file(): + if not rwsols_config_user.exists(): + print(yellow("Config file couldn't be located for editing!")) + return False + + # Check for the variables we are going to edit + use_https_found = False + approved_hash_found = False + with open(rwsols_config_user) as f: + for line in f: + if "$APPROVED_HASH" in line: + approved_hash_found = True + if "$USE_HTTPS" in line: + use_https_found = True + if not use_https_found or not approved_hash_found: + print(yellow("The RWSOLS config file is improperly formatted. Consider deleting your config.php file and re-running this script to recreate the default config.")) + return False + + + # If we're encrypted, pre-set that value correctly in the config file. + http_server, http_rwsols, https_server, https_rwsols, https_signed = rwsols_serving_status() + if https_rwsols: + try: + subprocess.run(['sudo', 'sed', '-i', 's/.*$USE_HTTPS.*/$USE_HTTPS = true;/g', rwsols_config_user], check=True) + except subprocess.CalledProcessError as e: + print(yellow("Error setting USE_HTTPS variable.")) + print(yellow(str(e))) + return False + else: + try: + subprocess.run(['sudo', 'sed', '-i', 's/.*$USE_HTTPS.*/$USE_HTTPS = false;/g', rwsols_config_user], check=True) + except subprocess.CalledProcessError as e: + print(yellow("Error setting USE_HTTPS variable.")) + print(yellow(str(e))) + return False + + # Generate Password Hash for user. If one already appears to be set, ask if they want to change it or leave it. + print("") + get_password = False + with open(rwsols_config_user) as f: + for line in f: + if "$APPROVED_HASH" in line: + if "NULL" in line: + get_password = True + else: + pw_option, _ = multi_choice('An existing password is already configured.', ['I would like to keep my currently configured password.','I want to change my password.']) + if pw_option == 1: + print("Ok, the currently set password will not be changed.") + else: + get_password = True + break + + if get_password: + while True: + p1 = getpass.getpass('Please enter the password that you would like to use to access your RWSOLS Web Interface: ') + p2 = getpass.getpass('Please re-enter the password to confirm: ') + if p1 != p2: + print("The provided passwords do not match. Please try again...\n") + else: + break + + # We use PHP's password hashing function, since PHP will be responsible for checking the password hash when received by the website + # Note that this must be in single quotes so that PHP doesn't expand variables inside of the string + try: + r = subprocess.run(['php', '-r', 'echo password_hash(\'' + p1 + '\', PASSWORD_DEFAULT);'], capture_output=True, check=True) + pw_hash = r.stdout.decode('utf8').strip() + except subprocess.CalledProcessError as e: + print(yellow("Error hashing password.")) + print(yellow(str(e))) + return False + + # Save the Hash, Using the fileinput method here, because sed is a nightmare with escape characters + # Note that this must be in single quotes so that PHP doesn't expand variables inside of the string + pw_updated = False + with fileinput.FileInput(rwsols_config_user, inplace = True) as f: + for line in f: + if "$APPROVED_HASH" in line: + print('$APPROVED_HASH = \'' + pw_hash + '\';', end="\n") + pw_updated = True + else: + print(line, end="") + if pw_updated: + print('Password hashed securely and added to configuration file.') + else: + print(yellow('The password could not be updated.')) + return False + + print("\nFor the remainder of the configuration, it's easiest for you to edit the file directly.") + input("Press enter to open the config file with nano for editing so you can input your values. Save and exit when done.") + try: + subprocess.run(['nano', str(rwsols_config_user)], check=True) + except subprocess.CalledProcessError as e: + print(yellow("Error editing config file.")) + return False + print("The config file has been written.\n") + return True + + + +# Prompt Function for Multiple Choice Questions. +# query: question string to be asked. e.g. "What is the best sandwich ingredient?" +# options: list of possible choices. e.g. ['bacon', 'lettuce', 'tomato'] +# Returns the 1-indexed selection number (1 if you choose bacon), and the value as a tuple (bacon) +def multi_choice(query, options): + while True: + print(query) + + for i in range(len(options)): + if i >=9: + print(" " + str(i+1) + ": " + options[i]) + else: + print(" " + str(i+1) + ": " + options[i]) + + + try: + choice = int(input("Choose an number from the list (1-" + str(len(options)) + "): ")) + if choice in range(1, len(options)+1): + return choice, options[choice-1] + else: + raise ValueError + except ValueError: + print(yellow("Please choose a number from the list!\n")) + +# Ensures the urls global list var is populated appropriately +def fill_urls_var(): + global urls + # If urls variable isn't already filled, but ddclient is running locally, we can check it for the URL, otherwise, we must ask the user. + if len(urls) == 0: + try: + # Copy to tmp file that python has permission to view/parse + subprocess.run(['sudo', 'cp', str(ddns_real_config), str(ddns_temp_config)], check=True) + subprocess.run(['sudo', 'chown', os.environ.get('USER'), str(ddns_temp_config)], check=True) + with open(ddns_temp_config) as f: + last = None + for last in (line for line in f if line.rstrip('\n')): + pass + if ',' in last: + hosts = last.split(",") + urls = [host.strip() for host in hosts] + else: + urls = [last.strip()] + print("The following URLs will be checked: " + " | ".join(urls)) + except (subprocess.CalledProcessError, OSError, IOError) as e: + print(yellow("Could not automatically determine the desired URL(s) from ddclient.")) + print(yellow(str(e))) + print("\nIf you ARE running ddclient locally (i.e. Configured by this script), but this failed, it's possible your configuration file is incorrect.") + print("You can always re-run that step by delecting the 'setup_ddns' dot file in this folder and re-running this script.") + print("Or, edit your ddclient config file manually at " + str(ddns_real_config) + ".\n") + # If we couldn't grab it from ddclient config file either, then we must ask user + if len(urls) == 0: + print("If you're running your DDNS updating service elsewhere, then don't worry about that and enter the URL(s) manually for checking.") + urls = enter_urls('Enter all the URLs that should be pointing at your public IP (seperate with commas if multiple): ') + +# Prompt Function for entering multiple comma seperated, proper-format FQDNs +# query: Request to be presented. e.g. Please enter one or more domain names, comma seperated +# Returns a list of validated FQDNs +def enter_urls(query): + while(True): + v = input(query) + if v == "": + print(yellow("A hostname is required. Please try again.")) + continue + if ',' in v: + hosts = v.split(",") + hosts = [host.strip() for host in hosts] + else: + hosts = [v] + invalid_hosts = [] + for host in hosts: + if not is_valid_hostname(host): + print(yellow("'" + host + "' is an invalid hostname.")) + invalid_hosts.append(host) + if len(invalid_hosts) > 0: + print("Please try again.\n") + continue + else: + return hosts + +# Function for identifying a valid hostname +# https://stackoverflow.com/a/2532344 +def is_valid_hostname(hostname): + if len(hostname) > 255: + return False + if hostname[-1] == ".": + hostname = hostname[:-1] # strip exactly one dot from the right, if present + allowed = re.compile("(?!-)[A-Z\d-]{1,63}(? -SSLRandomSeed startup builtin -SSLRandomSeed startup file:/dev/urandom 512 -SSLRandomSeed connect builtin -SSLRandomSeed connect file:/dev/urandom 512 - -AddType application/x-x509-ca-cert .crt -AddType application/x-pkcs7-crl .crl - -SSLPassPhraseDialog builtin - -SSLSessionCache shmcb:${APACHE_RUN_DIR}/ssl_scache(512000) -SSLSessionCacheTimeout 300 - -SSLCipherSuite HIGH:!MEDIUM:!LOW:!aNULL:!MD5 - -SSLProtocol all -SSLv2 -SSLv3 - - diff --git a/.htaccess b/www/html/.htaccess similarity index 100% rename from .htaccess rename to www/html/.htaccess diff --git a/bootstrap/css/bootstrap-responsive.css b/www/html/bootstrap/css/bootstrap-responsive.css similarity index 100% rename from bootstrap/css/bootstrap-responsive.css rename to www/html/bootstrap/css/bootstrap-responsive.css diff --git a/bootstrap/css/bootstrap.css b/www/html/bootstrap/css/bootstrap.css similarity index 100% rename from bootstrap/css/bootstrap.css rename to www/html/bootstrap/css/bootstrap.css diff --git a/bootstrap/ico/apple-touch-icon-114-precomposed.png b/www/html/bootstrap/ico/apple-touch-icon-114-precomposed.png similarity index 100% rename from bootstrap/ico/apple-touch-icon-114-precomposed.png rename to www/html/bootstrap/ico/apple-touch-icon-114-precomposed.png diff --git a/bootstrap/ico/apple-touch-icon-144-precomposed.png b/www/html/bootstrap/ico/apple-touch-icon-144-precomposed.png similarity index 100% rename from bootstrap/ico/apple-touch-icon-144-precomposed.png rename to www/html/bootstrap/ico/apple-touch-icon-144-precomposed.png diff --git a/bootstrap/ico/apple-touch-icon-57-precomposed.png b/www/html/bootstrap/ico/apple-touch-icon-57-precomposed.png similarity index 100% rename from bootstrap/ico/apple-touch-icon-57-precomposed.png rename to www/html/bootstrap/ico/apple-touch-icon-57-precomposed.png diff --git a/bootstrap/ico/apple-touch-icon-72-precomposed.png b/www/html/bootstrap/ico/apple-touch-icon-72-precomposed.png similarity index 100% rename from bootstrap/ico/apple-touch-icon-72-precomposed.png rename to www/html/bootstrap/ico/apple-touch-icon-72-precomposed.png diff --git a/bootstrap/ico/favicon.png b/www/html/bootstrap/ico/favicon.png similarity index 100% rename from bootstrap/ico/favicon.png rename to www/html/bootstrap/ico/favicon.png diff --git a/bootstrap/js/bootstrap.min.js b/www/html/bootstrap/js/bootstrap.min.js similarity index 100% rename from bootstrap/js/bootstrap.min.js rename to www/html/bootstrap/js/bootstrap.min.js diff --git a/www/html/config_sample.php b/www/html/config_sample.php new file mode 100644 index 0000000..4826ab9 --- /dev/null +++ b/www/html/config_sample.php @@ -0,0 +1,59 @@ + +// >> +// >>> +// >> +// > + +// The rest of the value should be set by you based on your configuration of computers to wake/sleep +// ------------------------------- + +// This is the number of times that the WOL server will try to ping the target computer to check if it has woken up. Default = 15. +$MAX_PINGS = 15; + +// This is the number of seconds to wait between pings commands when waking up or sleeping. Waking from shutdown or sleep will impact this. +$SLEEP_TIME = 5; + +// This is the Name of the computers to appear in the drop down +$COMPUTER_NAME = array("computer1","computer2"); + +// This is the MAC address of the Network Interface on the computer you are trying to wake. +$COMPUTER_MAC = array("00:00:00:00:00:00","00:00:00:00:00:00"); + +// This is the LOCAL IP address of the computer you are trying to wake. Use a reserved DHCP through your router's administration interface to ensure it doesn't change. +$COMPUTER_LOCAL_IP = array("192.168.0.1","192.168.0.2"); + +// This is the Port being used by the Windows SleepOnLan Utility to initiate a Sleep State +// http://www.ireksoftware.com/SleepOnLan/ +// Alternate Download Link: http://www.jeremyblum.com/wp-content/uploads/2013/07/SleepOnLan.zip +$COMPUTER_SLEEP_CMD_PORT = 7760; + +// Command to be issued by the windows sleeponlan utility +// options are suspend, hibernate, logoff, poweroff, forcepoweroff, lock, reboot +// You can create a windows scheduled task that starts sleeponlan.exe on boot with following startup parameters /auto /port=7760 +$COMPUTER_SLEEP_CMD = "suspend"; + +// This is the location of the bootstrap style folder relative to your index and config file. Default = "" (Same folder as this file) +// Directory must be called "bootstrap". You may wish to move if this WOL script is the "child" of a larger web project on your Pi, that will also use bootstrap styling. +// If if it on directory up, for example, you would set this to "../" +// Two directories up? Set too "../../" +// etc... +$BOOTSTRAP_LOCATION_PREFIX = ""; +// ------------------------------- + +?> diff --git a/index.php b/www/html/index.php similarity index 94% rename from index.php rename to www/html/index.php index 55b62e0..b09d645 100644 --- a/index.php +++ b/www/html/index.php @@ -1,7 +1,7 @@ Date: Sun, 21 Feb 2021 15:49:16 -0800 Subject: [PATCH 02/11] Add robots.txt to block crawling of this page. --- www/html/robots.txt | 1 + 1 file changed, 1 insertion(+) create mode 100644 www/html/robots.txt diff --git a/www/html/robots.txt b/www/html/robots.txt new file mode 100644 index 0000000..f677fae --- /dev/null +++ b/www/html/robots.txt @@ -0,0 +1 @@ +User-agent: * Disallow: / \ No newline at end of file From 1fad0fd430b6f6e94674a84e3ba9219aa65804b9 Mon Sep 17 00:00:00 2001 From: Jeremy Blum Date: Sun, 21 Feb 2021 16:50:08 -0800 Subject: [PATCH 03/11] Minor updates to README. --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 78e0c64..7d70cbc 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ REMOTE WAKE/SLEEP-ON-LAN SERVER *(RWSOLS)* ========================================== The Remote Wake/Sleep-on-LAN Server (RWSOLS) is a simple webapp that runs on your Raspberry Pi to turn it into a remotely accessible Wake/Sleep-On-LAN Server. This is very useful when you have a high-powered machine that you don't want to keep on all the time, but that you want to keep remotely accessible for Remote Desktop, SSH, FTP, etc. Wake-On-LAN packets cannot be forwarded through a router, so to wake up a remote machine behind a router, you need to have something on its local network to wake it up. That's where RWSOLS comes in. RWSOLS can control an unlimited number of remote machines on its local network, and is capable of waking them up (any OS) or putting them to sleep (only Windows remote machines). It can be configured to use signed or unsigned SSL encryption or it can be run over traditional HTTP. -A very detailed set of [installation instructions](https://github.com/sciguy14/Remote-Wake-Sleep-On-LAN-Server/wiki) can be found in the GitHub Wiki. +A very detailed set of [installation instructions](https://github.com/sciguy14/Remote-Wake-Sleep-On-LAN-Server/wiki) can be found in the GitHub Wiki. V3 of this software adds an auto-installed script that makes installation very easy, and handles automatic configuration of signed SSL certificate. You'll also find a description of [how it works](https://github.com/sciguy14/Remote-Wake-Sleep-On-LAN-Server/wiki/How-it-Works), [an FAQ](https://github.com/sciguy14/Remote-Wake-Sleep-On-LAN-Server/wiki/Notes-and-FAQs), and a list of [relevant terminology](https://github.com/sciguy14/Remote-Wake-Sleep-On-LAN-Server/wiki/Terminology) on the Wiki. @@ -18,7 +18,7 @@ GitHub user [ex0nuss](https://github.com/ex0nuss) has created an x86 Docker Imag License ------- This work is licensed under the [GNU GPL v3](http://www.gnu.org/licenses/gpl.html). -Please share improvements or remixes with the community, and attribute me (Jeremy Blum, ) when reusing portions of my code. +Please share improvements or remixes with the community, and attribute me (Jeremy Blum, ) when reusing portions of my code. Other contributors to this work include: - Felix Ryan (https://www.felixrr.pro) From 94ed462793c8fa519670a428e4cdbd708059ba30 Mon Sep 17 00:00:00 2001 From: Alfred Gaillard Date: Thu, 27 Oct 2022 23:25:35 +0200 Subject: [PATCH 04/11] Updating php to 7.4 because 7.3 is no longer supported with raspberry apt repos --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 47f4793..894ac11 100644 --- a/setup.py +++ b/setup.py @@ -133,7 +133,7 @@ def run_step(handle, description, dot_file_skippable=True): def _01_install_prereqs(): try: subprocess.run(['sudo', 'apt-get', 'update'], check=True) - subprocess.run(['sudo', 'apt-get', '-y', 'install', 'wakeonlan', 'git', 'apache2', 'php7.3', 'php7.3-curl', 'libapache2-mod-php7.3', 'snapd'], check=True) + subprocess.run(['sudo', 'apt-get', '-y', 'install', 'wakeonlan', 'git', 'apache2', 'php7.4', 'php7.4-curl', 'libapache2-mod-php7.4', 'snapd'], check=True) subprocess.run(['sudo', 'snap', 'install', 'core'], check=True) subprocess.run(['sudo', 'snap', 'refresh', 'core'], check=True) subprocess.run(['sudo', 'apt-get', '-y', 'remove', 'certbot']) #Remove any existing installations from package managers From a4a9b5092136d9617478ff557de6f3477cd633dd Mon Sep 17 00:00:00 2001 From: Jeremy Blum Date: Sat, 4 Feb 2023 19:04:45 -0800 Subject: [PATCH 05/11] Readme Updates --- README.md | 10 ++++++---- setup.py | 1 + 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 7d70cbc..a5c8cd7 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ REMOTE WAKE/SLEEP-ON-LAN SERVER *(RWSOLS)* ========================================== The Remote Wake/Sleep-on-LAN Server (RWSOLS) is a simple webapp that runs on your Raspberry Pi to turn it into a remotely accessible Wake/Sleep-On-LAN Server. This is very useful when you have a high-powered machine that you don't want to keep on all the time, but that you want to keep remotely accessible for Remote Desktop, SSH, FTP, etc. Wake-On-LAN packets cannot be forwarded through a router, so to wake up a remote machine behind a router, you need to have something on its local network to wake it up. That's where RWSOLS comes in. RWSOLS can control an unlimited number of remote machines on its local network, and is capable of waking them up (any OS) or putting them to sleep (only Windows remote machines). It can be configured to use signed or unsigned SSL encryption or it can be run over traditional HTTP. -A very detailed set of [installation instructions](https://github.com/sciguy14/Remote-Wake-Sleep-On-LAN-Server/wiki) can be found in the GitHub Wiki. V3 of this software adds an auto-installed script that makes installation very easy, and handles automatic configuration of signed SSL certificate. +A very detailed set of [installation instructions](https://github.com/sciguy14/Remote-Wake-Sleep-On-LAN-Server/wiki) can be found in the GitHub Wiki. V3 of this software adds an auto-installer script that makes installation very easy, and handles automatic configuration of signed SSL certificate. You'll also find a description of [how it works](https://github.com/sciguy14/Remote-Wake-Sleep-On-LAN-Server/wiki/How-it-Works), [an FAQ](https://github.com/sciguy14/Remote-Wake-Sleep-On-LAN-Server/wiki/Notes-and-FAQs), and a list of [relevant terminology](https://github.com/sciguy14/Remote-Wake-Sleep-On-LAN-Server/wiki/Terminology) on the Wiki. @@ -15,10 +15,12 @@ x86 Docker Image (Alternate Installation Option) ------------------------------------------------ GitHub user [ex0nuss](https://github.com/ex0nuss) has created an x86 Docker Image for RWSOLS, that you may wish to try out. I have not independently validated its functionality, but it does pull directly from this Repo. You may find the setup of a Docker image to be faster and easier than following the instructions in the Wiki to do a native installation of this application on a Desktop (this is for x86, not ARM). You can find the GitHub Repo for the Docker Image [here](https://github.com/ex0nuss/Remote-Wake-Sleep-On-LAN-Docker), and the DockerHub link [here](https://hub.docker.com/r/ex0nuss/remote-wake-sleep-on-lan-docker). + License ------- -This work is licensed under the [GNU GPL v3](http://www.gnu.org/licenses/gpl.html). -Please share improvements or remixes with the community, and attribute me (Jeremy Blum, ) when reusing portions of my code. - +Copyright 2023 [Jeremy Blum](https://www.jeremyblum.com), [Blum Idea Labs, LLC.](https://www.blumidealabs.com) +This project is licensed under the GPLv3 license (see LICENSE.md for details). +Please share improvements or remixes with the community, and attribute me (Jeremy Blum, ) when reusing portions of my code. + Other contributors to this work include: - Felix Ryan (https://www.felixrr.pro) diff --git a/setup.py b/setup.py index 894ac11..17e3066 100644 --- a/setup.py +++ b/setup.py @@ -2,6 +2,7 @@ # Remote Wake/Sleep-On-LAN Server [SETUP SCRIPT] # https://github.com/sciguy14/Remote-Wake-Sleep-On-LAN-Server +# (c) 2023 Blum Idea Labs, LLC. # Author: Jeremy E. Blum (https://www.jeremyblum.com) # License: GPL v3 (http://www.gnu.org/licenses/gpl.html) From e134369c6bbab803d5fe540f144ad7cfb1581981 Mon Sep 17 00:00:00 2001 From: Jeremy Blum Date: Sat, 4 Feb 2023 19:10:15 -0800 Subject: [PATCH 06/11] Fix incorrect selection variable name --- setup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 17e3066..236afc0 100644 --- a/setup.py +++ b/setup.py @@ -384,8 +384,8 @@ def _07_server_check(): # RWSOLS is not currently accessible else: print("In order to automatically configure SSL Encryption, port 80 must be open on your router and forwarded to port 80 on this Pi.") - print("If you are unable to forward port 80 because you are using it for another service, or because your ISP blocks it, you can configure certbot to perform a DNS challenge instead, or this script can setup unsigned encryption.") - c, _ = multi_choice('How would you like to proceed?', [exit_and_fix_option_text , self_signed_encryption_text, no_encryption_option_text]) + print("If you are unable to forward port 80 because you are using it for another service, or because your ISP blocks it, you can manually configure certbot to perform a DNS challenge instead, or this script can setup unsigned encryption.") + c, _ = multi_choice('How would you like to proceed?', [exit_and_fix_option_text , self_signed_encryption_option_text, no_encryption_option_text]) if c == 1: local_ip = get_local_ip() if local_ip: From 236d4d328021133f35d28c82f83edc280cac887e Mon Sep 17 00:00:00 2001 From: Jeremy Blum Date: Sat, 4 Feb 2023 19:19:25 -0800 Subject: [PATCH 07/11] Error message clean up --- setup.py | 24 +++++++++--------------- 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/setup.py b/setup.py index 236afc0..5bcc019 100644 --- a/setup.py +++ b/setup.py @@ -762,11 +762,9 @@ def rwsols_serving_status(): except ValueError as e: print(yellow("The HTTP URL is of an invalid format.")) except urllib.error.HTTPError as e: - print(yellow("Couldn't connect to the server on port 80 (HTTP Error).")) - print(yellow(str(e))) + print(yellow(f"Couldn't connect to port 80 due to a HTTP Error ({str(e)}).")) except urllib.error.URLError as e: - print(yellow("Couldn't connect to the server on port 80 (URL Error).")) - print(yellow(str(e))) + print(yellow(f"Couldn't connect to port 80 due to a URL Error ({str(e)}).")) else: http_server = True if txt.startswith("hello?"): @@ -781,12 +779,10 @@ def rwsols_serving_status(): except ValueError as e: print(yellow("The HTTPS URL is of an invalid format.")) except urllib.error.HTTPError as e: - print(yellow("Couldn't connect to the server on port 443 (HTTP Error).")) - print(yellow(str(e))) + print(yellow(f"Couldn't connect to port 443 due to an HTTP Error ({str(e)}).")) except (urllib.error.URLError, ssl.SSLCertVerificationError) as e: - print(yellow("Couldn't to connect to a server at port 443 with strict checking for signed certs.")) - # print(yellow(str(e))) - # There was a SSL Cert Error, which means that it still might active here with a self-signed cert + print(yellow("Couldn't to connect to port 443 with strict checking for signed certs.")) + # There was a SSL Cert Error, which means that it still might work with a self-signed cert # So, we check again, after allow self-signed certs: try: self_signed_ssl_context = ssl.create_default_context() @@ -796,13 +792,11 @@ def rwsols_serving_status(): except ValueError as e: print(yellow("The HTTPs URL is of an invalid format.")) except urllib.error.HTTPError as e: - print(yellow("Couldn't connect to the server on port 443, even when allowing self-signed certs (HTTP Error)")) - print(yellow(str(e))) + print(yellow(f"Couldn't connect to port 443, even when allowing self-signed certs, due to a HTTP Error ({str(e)})")) except urllib.error.URLError as e: - print(yellow("Couldn't connect to the server on port 443, even when allowing self-signed certs (URL Error).")) - print(yellow(str(e))) + print(yellow(f"Couldn't connect to port 443, even when allowing self-signed certs, due to a URL Error ({str(e)}).")) except ssl.SSLError as e: - print(yellow("An SSL error was encountered while trying to check if the server would respond (with self-signed certs allowed).")) + print(yellow("The following SSL error was encountered while trying to check if the server would respond (with self-signed certs allowed):")) print(yellow(str(e))) else: https_server = True @@ -812,7 +806,7 @@ def rwsols_serving_status(): else: print(yellow("Something was found at " + urls[0] + " port 443, but it was not RWSOLS.")) except ssl.SSLError as e: - print(yellow("An SSL error was encountered while trying to verify the server.")) + print(yellow("The following SSL error was encountered while trying to verify the server:")) print(yellow(str(e))) else: https_server = True From e6c5c22d7dc3db55384b527376ba0aabb29ccda2 Mon Sep 17 00:00:00 2001 From: Jeremy Blum Date: Sat, 4 Feb 2023 20:15:59 -0800 Subject: [PATCH 08/11] Fix IPv4 Lookup error handling --- setup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 5bcc019..66280ea 100644 --- a/setup.py +++ b/setup.py @@ -269,10 +269,10 @@ def _05_get_ip(): except urllib.error.URLError as e: if hasattr(e, 'reason'): print(yellow('Failed to reach the ident.me server.')) - print(yellow('Reason: ' + e.reason)) + print(yellow('Reason: ' + str(e.reason))) elif hasattr(e, 'code'): print(yellow('The ident.me server couldn\'t fulfill the request.')) - print(yellow('Error code: ' + e.code)) + print(yellow('Error code: ' + str(e.code))) return False print("The public-facing IPv4 address of this Pi's network was detected as " + cyan(public_ipv4) + ".") return True From a955b5a62f5aef1c972f058036c3682a15657ea1 Mon Sep 17 00:00:00 2001 From: Jeremy Blum Date: Sat, 4 Feb 2023 22:24:21 -0800 Subject: [PATCH 09/11] Prevent leaking of computer state info by requiring the passphrase to check status in addition to requiring it for wake/sleep commands --- www/html/index.php | 49 ++++++++++++++++++++++++++++++---------------- 1 file changed, 32 insertions(+), 17 deletions(-) diff --git a/www/html/index.php b/www/html/index.php index b09d645..dd94174 100644 --- a/www/html/index.php +++ b/www/html/index.php @@ -104,8 +104,10 @@