From 8145769b7a780f2883fd100f6c9b63ec0b42d545 Mon Sep 17 00:00:00 2001 From: Brikwerk Date: Wed, 22 Sep 2021 22:55:57 -0700 Subject: [PATCH] Added SSL capabilities to the webapp --- .gitignore | 4 ++- nxbt/cli.py | 9 ++++- nxbt/web/app.py | 54 +++++++++++++++++++++++++++-- nxbt/web/cert.py | 88 ++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 151 insertions(+), 4 deletions(-) create mode 100644 nxbt/web/cert.py diff --git a/.gitignore b/.gitignore index 2ad7aac..c58b552 100644 --- a/.gitignore +++ b/.gitignore @@ -142,4 +142,6 @@ cython_debug/ secrets.txt messages.txt Vagrantfile -.vagrant \ No newline at end of file +.vagrant +cert.pem +key.pem \ No newline at end of file diff --git a/nxbt/cli.py b/nxbt/cli.py index fbc0eba..e06479b 100644 --- a/nxbt/cli.py +++ b/nxbt/cli.py @@ -46,6 +46,12 @@ parser.add_argument('-i', '--ip', required=False, default="0.0.0.0", type=str, help="""Specifies the IP to run the webapp at. Defaults to 0.0.0.0""") parser.add_argument('-p', '--port', required=False, default=8000, type=int, help="""Specifies the port to run the webapp at. Defaults to 8000""") +parser.add_argument('--usessl', required=False, default=False, action='store_true', + help="""Enables or disables SSL use in the webapp""") +parser.add_argument('--certpath', required=False, default=None, type=str, + help="""Specifies the folder location for SSL certificates used + in the webapp. Certificates in this folder should be in the form of + a 'cert.pem' and 'key.pem' pair.""") args = parser.parse_args() @@ -313,7 +319,8 @@ def main(): if args.command == 'webapp': from .web import start_web_app - start_web_app(ip=args.ip, port=args.port) + start_web_app(ip=args.ip, port=args.port, + usessl=args.usessl, cert_path=args.certpath) elif args.command == 'demo': demo() elif args.command == 'macro': diff --git a/nxbt/web/app.py b/nxbt/web/app.py index 3cb5625..9cd2ce2 100644 --- a/nxbt/web/app.py +++ b/nxbt/web/app.py @@ -1,7 +1,9 @@ import json import os from threading import RLock +import time +from .cert import generate_cert from ..nxbt import Nxbt, PRO_CONTROLLER from flask import Flask, render_template, request from flask_socketio import SocketIO, emit @@ -88,6 +90,7 @@ def on_create_controller(): @sio.on('input') def handle_input(message): + # print("Webapp Input", time.perf_counter()) message = json.loads(message) index = message[0] input_packet = message[1] @@ -102,8 +105,55 @@ def handle_macro(message): nxbt.macro(index, macro) -def start_web_app(ip='0.0.0.0', port=8000): - eventlet.wsgi.server(eventlet.listen((ip, port)), app) +def start_web_app(ip='0.0.0.0', port=8000, usessl=False, cert_path=None): + if usessl: + if cert_path is None: + # Store certs in the package directory + cert_path = os.path.join( + os.path.dirname(__file__), "cert.pem" + ) + key_path = os.path.join( + os.path.dirname(__file__), "key.pem" + ) + else: + # If specified, store certs at the user's preferred location + cert_path = os.path.join( + cert_path, "cert.pem" + ) + key_path = os.path.join( + cert_path, "key.pem" + ) + if not os.path.isfile(cert_path) or not os.path.isfile(key_path): + print( + "\n" + "-----------------------------------------\n" + "---------------->WARNING<----------------\n" + "The NXBT webapp is being run with self-\n" + "signed SSL certificates for use on your\n" + "local network.\n" + "\n" + "These certificates ARE NOT safe for\n" + "production use. Please generate valid\n" + "SSL certificates if you plan on using the\n" + "NXBT webapp anywhere other than your own\n" + "network.\n" + "-----------------------------------------\n" + "\n" + "The above warning will only be shown once\n" + "on certificate generation." + "\n" + ) + print("Generating certificates...") + cert, key = generate_cert('localhost') + with open(cert_path, "wb") as f: + f.write(cert) + with open(key_path, "wb") as f: + f.write(key) + + eventlet.wsgi.server(eventlet.wrap_ssl(eventlet.listen((ip, port)), + certfile=cert_path, keyfile=key_path), app) + else: + eventlet.wsgi.server(eventlet.listen((ip, port)), app) if __name__ == "__main__": diff --git a/nxbt/web/cert.py b/nxbt/web/cert.py new file mode 100644 index 0000000..40f7b4b --- /dev/null +++ b/nxbt/web/cert.py @@ -0,0 +1,88 @@ +# Copyright 2018 Simon Davy +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +# WARNING: the code in the gist generates self-signed certs, for the purposes of testing in development. +# Do not use these certs in production, or You Will Have A Bad Time. +# +# Caveat emptor +# + +from datetime import datetime, timedelta +import ipaddress + +from cryptography import x509 +from cryptography.x509.oid import NameOID +from cryptography.hazmat.primitives import hashes +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives import serialization +from cryptography.hazmat.primitives.asymmetric import rsa + +def generate_cert(hostname, ip_addresses=None, key=None): + """Generates self signed certificate for a hostname, and optional IP addresses.""" + + # Generate our key + if key is None: + key = rsa.generate_private_key( + public_exponent=65537, + key_size=2048, + backend=default_backend(), + ) + + name = x509.Name([ + x509.NameAttribute(NameOID.COMMON_NAME, hostname) + ]) + + # best practice seem to be to include the hostname in the SAN, which *SHOULD* mean COMMON_NAME is ignored. + alt_names = [x509.DNSName(hostname)] + + # allow addressing by IP, for when you don't have real DNS (common in most testing scenarios + if ip_addresses: + for addr in ip_addresses: + # openssl wants DNSnames for ips... + alt_names.append(x509.DNSName(addr)) + # ... whereas golang's crypto/tls is stricter, and needs IPAddresses + # note: older versions of cryptography do not understand ip_address objects + alt_names.append(x509.IPAddress(ipaddress.ip_address(addr))) + + san = x509.SubjectAlternativeName(alt_names) + + # path_len=0 means this cert can only sign itself, not other certs. + basic_contraints = x509.BasicConstraints(ca=True, path_length=0) + now = datetime.utcnow() + cert = ( + x509.CertificateBuilder() + .subject_name(name) + .issuer_name(name) + .public_key(key.public_key()) + .serial_number(1000) + .not_valid_before(now - timedelta(days=10*365)) + .not_valid_after(now - timedelta(days=9*365)) + .add_extension(basic_contraints, False) + .add_extension(san, False) + .sign(key, hashes.SHA256(), default_backend()) + ) + cert_pem = cert.public_bytes(encoding=serialization.Encoding.PEM) + key_pem = key.private_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PrivateFormat.TraditionalOpenSSL, + encryption_algorithm=serialization.NoEncryption(), + ) + + return cert_pem, key_pem \ No newline at end of file