diff --git a/README.md b/README.md index a8ffd40..27068b9 100644 --- a/README.md +++ b/README.md @@ -39,10 +39,10 @@ to offer itself, so he can get **wurf** and offer a file to you. have used it successfully on Windows within the cygwin environment. ``` - Usage: wurf [-i ] [-p ] [-c ] - wurf [-i ] [-p ] [-c ] [-z|-j|-Z|-u] - wurf [-i ] [-p ] [-c ] -s - wurf [-i ] [-p ] [-c ] -U + Usage: wurf [-i ] [-p ] [-c ] [-t [--cert ] [--key ] [--keypass ]] + wurf [-i ] [-p ] [-c ] [-t [--cert ] [--key ] [--keypass ]] [-z|-j|-Z|-u] + wurf [-i ] [-p ] [-c ] [-t [--cert ] [--key ] [--keypass ]] -s + wurf [-i ] [-p ] [-c ] [-t [--cert ] [--key ] [--keypass ]] -U wurf @@ -54,6 +54,8 @@ have used it successfully on Windows within the cygwin environment. You can configure your default compression method in the configuration file described below. + When -t is specified, wurf will use TLS to secure the connection. You must pass both a certificate and key in PEM format. + When -s is specified instead of a filename, wurf distributes itself. When -U is specified, wurf provides an upload form, allowing file uploads. @@ -75,6 +77,12 @@ have used it successfully on Windows within the cygwin environment. count = 2 ip = 127.0.0.1 compressed = gz + tls = on + + [tls] + cert = /etc/letsencrypt/live/example.com/fullchain.pem + key = /etc/letsencrypt/live/example.com/privkey.pem + keypass = my_password ``` ## Credits diff --git a/doc/wurf.1 b/doc/wurf.1 index d158cef..29631dc 100644 --- a/doc/wurf.1 +++ b/doc/wurf.1 @@ -1,4 +1,4 @@ -.TH "wurf" "1" "Last Modified: October 18, 2024" +.TH "wurf" "1" "Last Modified: June 25, 2025" .SH NAME \fBwurf\fP \- A small, simple, stupid webserver to share files @@ -45,6 +45,18 @@ Used on a directory, it creates a tarball with no compression .B \-s Used to distribute wurf itself .TP +.B \-t +Enables TLS mode +.TP +.B \--cert +With -t, specifies a pem formatted certificate file +.TP +.B \--key +With -t, specifies a pem formatted key file +.TP +.B \--keypass +With -t, specifies the key password +.TP .B \-U wurf provides an upload form and allows uploading files @@ -61,6 +73,12 @@ precedence. The compression methods are "off", "gz", "bz2" or "zip". count = 2 ip = 127.0.0.1 compressed = gz + tls = on + + [tls] + cert = /etc/letsencrypt/live/example.com/fullchain.pem + key = /etc/letsencrypt/live/example.com/privkey.pem + keypass = my_password .SH AUTHOR wurf is maintained by Sébastien Santoro , diff --git a/src/wurf.py b/src/wurf.py index 8bd5304..cacfbc8 100755 --- a/src/wurf.py +++ b/src/wurf.py @@ -14,11 +14,16 @@ import shutil, tarfile, zipfile import struct from io import BytesIO, StringIO +import ssl maxdownloads = 1 cpid = -1 compressed = "gz" upload = False +tls = False +cert = "" +key = "" +keypass = "" # Utility function to guess the IP (as a string) where the server can be @@ -325,31 +330,46 @@ def serve_files(filename, maxdown=1, ip_addr="", port=8080): "cannot bind to IP address '%s' port %d" % (ip_addr, port), file=sys.stderr ) sys.exit(1) - + listen_protocol = "https" if tls else "http" if not ip_addr: ip_addr = find_ip() if ip_addr: if filename: - location = f"http://{ip_addr}:{httpd.server_port}/" + urllib.parse.quote( + location = f"{listen_protocol}://{ip_addr}:{httpd.server_port}/" + urllib.parse.quote( os.path.basename(filename + archive_ext) ) else: - location = "http://%s:%s/" % (ip_addr, httpd.server_port) + location = "%s://%s:%s/" % (listen_protocol, ip_addr, httpd.server_port) print("Now serving on %s" % location) - - while cpid != 0 and maxdownloads > 0: - httpd.handle_request() + if tls : + context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) + try: + context.load_cert_chain(cert, key, keypass) + except ssl.SSLError: + print("Unable to load certificate or key. Possibly missing or incorrect password.") + sys.exit(1) + except FileNotFoundError: + print("Certificate or Key file is inaccessible or incorrect.") + sys.exit(1) + + with context.wrap_socket(httpd.socket, server_side=True) as ssock: + httpd.socket = ssock + while cpid != 0 and maxdownloads > 0: + httpd.handle_request() + else: + while cpid != 0 and maxdownloads > 0: + httpd.handle_request() def usage(defport, defmaxdown, errmsg=None): name = os.path.basename(sys.argv[0]) print( """ - Usage: %s [-i ] [-p ] [-c ] - %s [-i ] [-p ] [-c ] [-z|-j|-Z|-u] - %s [-i ] [-p ] [-c ] -s - %s [-i ] [-p ] [-c ] -U + Usage: %s [-i ] [-p ] [-c ] [-t [--cert ] [--key ] [--keypass ]] + %s [-i ] [-p ] [-c ] [-t [--cert ] [--key ] [--keypass ]] [-z|-j|-Z|-u] + %s [-i ] [-p ] [-c ] [-t [--cert ] [--key ] [--keypass ]] -s + %s [-i ] [-p ] [-c ] [-t [--cert ] [--key ] [--keypass ]] -U %s @@ -361,6 +381,8 @@ def usage(defport, defmaxdown, errmsg=None): You can configure your default compression method in the configuration file described below. + When -t is specified, wurf will use TLS to secure the connection. You must pass both a certificate and key in PEM format. + When -s is specified instead of a filename, %s distributes itself. When -U is specified, wurf provides an upload form, allowing file uploads. @@ -382,6 +404,12 @@ def usage(defport, defmaxdown, errmsg=None): count = 2 ip = 127.0.0.1 compressed = gz + tls = on + + [tls] + cert = /etc/letsencrypt/live/example.com/fullchain.pem + key = /etc/letsencrypt/live/example.com/privkey.pem + keypass = my_password """ % (name, name, name, name, name, name, defmaxdown, defport), file=sys.stderr, @@ -472,7 +500,7 @@ def wurf_client(url): def main(): - global cpid, upload, compressed + global cpid, upload, compressed, tls, cert, key, keypass maxdown = 1 port = 8080 @@ -510,11 +538,32 @@ def main(): compressed = config.get("main", "compressed") compressed = formats.get(compressed, "gz") + if config.has_option("main", "tls"): + affirm = { + "yes": True, + "true": True, + "enabled": True, + "on": True + } + tls_setting = config.get("main", "tls") + tls = affirm.get(tls_setting, False) + if not config.has_option("main", "port"): + port = 8443 + + if config.has_option("tls", "cert"): + cert = config.get("tls", "cert") + + if config.has_option("tls", "key"): + key = config.get("tls", "key") + + if config.has_option("tls", "keypass"): + keypass = config.get("tls", "keypass") + defaultport = port defaultmaxdown = maxdown try: - options, filenames = getopt.gnu_getopt(sys.argv[1:], "hUszjZui:c:p:") + options, filenames = getopt.gnu_getopt(sys.argv[1:], "hUszjZuti:c:p:", ["cert=", "key=", "keypass="]) except getopt.GetoptError as desc: usage(defaultport, defaultmaxdown, desc) @@ -563,6 +612,17 @@ def main(): elif option == "-u": compressed = "" + elif option == "-t": + tls = True + if '-p' not in dict(options) and not config.has_option("main", "port"): + port = 8443 + elif option == "--cert": + cert = val + elif option == "--key": + key = val + elif option == "--keypass": + keypass = val + else: usage(defaultport, defaultmaxdown, "Unknown option: %r" % option) @@ -600,6 +660,14 @@ def main(): "%s: Neither file nor directory" % filenames[0], ) + if tls: + if cert == "" or key == "": + usage( + defaultport, + defaultmaxdown, + "Missing option, TLS requested but certificate/key pair not provided", + ) + serve_files(filename, maxdown, ip_addr, port) # wait for child processes to terminate