Merge branch 'main' of https://git.novacow.ch/Nova/PyWebServer
This commit is contained in:
@@ -1,5 +1,12 @@
|
||||
# PyWebServer
|
||||
|
||||
# PyWebServer is undergoing major security updates!
|
||||
# Please use commit [7ac160f625](https://git.novacow.ch/Nova/PyWebServer/src/commit/7ac160f6259e637c2337a672e56f105f4cdd2d2a) as the source for now!!
|
||||
|
||||
## GitHub
|
||||
The upstream of this project is on my own [Gitea instance](https://git.novacow.ch/Nova/PyWebServer/).
|
||||
Because of that I'll mostly reply to issues and PRs there, you can submit issues and PRs on GitHub, but it might take longer before I read it.
|
||||
|
||||
## Installing
|
||||
Installing and running PyWebServer is very simple.
|
||||
Assuming you're running Linux:
|
||||
|
61
certgen.py
Normal file
61
certgen.py
Normal file
@@ -0,0 +1,61 @@
|
||||
from cryptography import x509
|
||||
from cryptography.x509.oid import NameOID
|
||||
from cryptography.hazmat.primitives import hashes, serialization
|
||||
from cryptography.hazmat.primitives.asymmetric import rsa
|
||||
import datetime
|
||||
|
||||
|
||||
class AutoCertGen:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def gen_cert():
|
||||
# Generate private key
|
||||
private_key = rsa.generate_private_key(
|
||||
public_exponent=65537,
|
||||
key_size=2048,
|
||||
)
|
||||
|
||||
# Define subject and issuer (self-signed)
|
||||
subject = issuer = x509.Name(
|
||||
[
|
||||
x509.NameAttribute(NameOID.COUNTRY_NAME, "ZZ"),
|
||||
x509.NameAttribute(NameOID.STATE_OR_PROVINCE_NAME, "Some province"),
|
||||
x509.NameAttribute(NameOID.LOCALITY_NAME, "Some place"),
|
||||
x509.NameAttribute(NameOID.ORGANIZATION_NAME, "Some org"),
|
||||
x509.NameAttribute(NameOID.COMMON_NAME, "localhost"),
|
||||
]
|
||||
)
|
||||
|
||||
# Create certificate
|
||||
certificate = (
|
||||
x509.CertificateBuilder()
|
||||
.subject_name(subject)
|
||||
.issuer_name(issuer)
|
||||
.public_key(private_key.public_key())
|
||||
.serial_number(x509.random_serial_number())
|
||||
.not_valid_before(datetime.datetime.utcnow())
|
||||
.not_valid_after(
|
||||
datetime.datetime.utcnow() + datetime.timedelta(days=365)
|
||||
) # 1 year validity
|
||||
.add_extension(
|
||||
x509.SubjectAlternativeName([x509.DNSName("localhost")]), critical=False
|
||||
)
|
||||
.sign(private_key, hashes.SHA256())
|
||||
)
|
||||
|
||||
# Save private key
|
||||
with open("key.pem", "wb") as f:
|
||||
f.write(
|
||||
private_key.private_bytes(
|
||||
encoding=serialization.Encoding.PEM,
|
||||
format=serialization.PrivateFormat.TraditionalOpenSSL,
|
||||
encryption_algorithm=serialization.NoEncryption(),
|
||||
)
|
||||
)
|
||||
|
||||
# Save certificate
|
||||
with open("certificate.pem", "wb") as f:
|
||||
f.write(certificate.public_bytes(serialization.Encoding.PEM))
|
||||
|
||||
print("Self-signed certificate and private key generated for HTTPS server!")
|
68
pywebsrv.py
68
pywebsrv.py
@@ -1,4 +1,17 @@
|
||||
"""
|
||||
License:
|
||||
PyWebServer
|
||||
Copyright (C) 2025 Nova
|
||||
|
||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
Contact:
|
||||
E-mail: nova@novacow.ch
|
||||
|
||||
This is PyWebServer, an ultra minimalist webserver, meant to still have
|
||||
a lot standard webserver features. A comprehensive list is below:
|
||||
Features:
|
||||
@@ -28,7 +41,7 @@ import signal
|
||||
import sys
|
||||
|
||||
try:
|
||||
from autocertgen import AutoCertGen
|
||||
from certgen import AutoCertGen
|
||||
except ImportError:
|
||||
print(
|
||||
"WARN: You need the AutoCertGen plugin! Please install it from\n"
|
||||
@@ -46,7 +59,6 @@ class FileHandler:
|
||||
|
||||
def __init__(self, base_dir=None):
|
||||
self.config_path = os.path.join(os.getcwd(), self.CONFIG_FILE)
|
||||
self.base_dir = self.read_config("directory")
|
||||
|
||||
def check_first_run(self):
|
||||
if not os.path.isfile(self.config_path):
|
||||
@@ -58,6 +70,9 @@ class FileHandler:
|
||||
with open(self.config_path, "w") as f:
|
||||
f.write(self.DEFAULT_CONFIG.format(cwd=os.getcwd()))
|
||||
|
||||
def didnt_confirm(self):
|
||||
os.remove(self.config_path)
|
||||
|
||||
def read_file(self, file_path):
|
||||
if "../" in file_path:
|
||||
return 403
|
||||
@@ -127,7 +142,9 @@ class FileHandler:
|
||||
return bool(int(value))
|
||||
if option == "directory":
|
||||
if value == "<Enter directory here>":
|
||||
print("FATAL: You haven't set up PyWebServer! Please edit pywebsrv.conf!")
|
||||
print(
|
||||
"FATAL: You haven't set up PyWebServer! Please edit pywebsrv.conf!"
|
||||
)
|
||||
exit(1)
|
||||
return value
|
||||
return value
|
||||
@@ -138,10 +155,7 @@ class FileHandler:
|
||||
Generate some self-signed certificates using AutoCertGen
|
||||
"""
|
||||
autocert = AutoCertGen()
|
||||
pk = autocert.generate_private_key()
|
||||
sub, iss = autocert.generate_issuer_and_subject()
|
||||
cert = autocert.build_cert(pk, iss, sub)
|
||||
autocert.write_cert(pk, cert)
|
||||
autocert.gen_cert()
|
||||
|
||||
|
||||
class RequestParser:
|
||||
@@ -318,7 +332,7 @@ class WebServer:
|
||||
response = self.handle_request(request, addr)
|
||||
|
||||
if isinstance(response, str):
|
||||
response = response.encode() # if we send text this shouldn't explode
|
||||
response = response.encode()
|
||||
|
||||
conn.sendall(response)
|
||||
except Exception as e:
|
||||
@@ -370,9 +384,10 @@ class WebServer:
|
||||
500,
|
||||
"PyWebServer has encountered a fatal error and cannot serve "
|
||||
"your request. Contact the owner with this error: FATAL_FILE_RO_ACCESS",
|
||||
) # The user did no fucky-wucky, but the server fucking exploded.
|
||||
) # When there was an issue with reading we throw this.
|
||||
|
||||
# (try to) detect binary files (eg, mp3) and serve them correctly
|
||||
# A really crude implementation of binary files. Later in 2.0 I'll actually
|
||||
# make this useful.
|
||||
if path.endswith((".mp3", ".png", ".jpg", ".jpeg", ".gif")):
|
||||
return self.build_binary_response(200, file_content, path)
|
||||
|
||||
@@ -406,12 +421,18 @@ class WebServer:
|
||||
f"Server: PyWebServer/1.1\r\n"
|
||||
f"Content-Type: {content_type}\r\n"
|
||||
f"Content-Length: {len(binary_data)}\r\n"
|
||||
f"Connection: close\r\n\r\n" # connection close bcuz im lazy
|
||||
f"Connection: close\r\n\r\n"
|
||||
# Connection close is done because it is way easier to implement.
|
||||
# It's not like this program will see production use anyway.
|
||||
)
|
||||
return headers.encode() + binary_data
|
||||
|
||||
@staticmethod
|
||||
def build_response(status_code, body):
|
||||
"""
|
||||
For textfiles we'll not have to guess MIME-types, though the other function
|
||||
build_binary_response will be merged in here anyway.
|
||||
"""
|
||||
messages = {
|
||||
200: "OK",
|
||||
304: "Not Modified", # TODO KEKL
|
||||
@@ -436,8 +457,7 @@ class WebServer:
|
||||
return headers + body
|
||||
|
||||
def shutdown(self, signum, frame):
|
||||
print(f"\nRecieved signal {signum}")
|
||||
print("\nShutting down server...")
|
||||
print("\nRecieved signal to exit!\nShutting down server...")
|
||||
self.running = False
|
||||
self.http_socket.close()
|
||||
self.https_socket.close()
|
||||
@@ -446,7 +466,27 @@ class WebServer:
|
||||
|
||||
def main():
|
||||
file_handler = FileHandler()
|
||||
file_handler.check_first_run()
|
||||
first_run = file_handler.check_first_run()
|
||||
if first_run is True:
|
||||
print(
|
||||
"*******************************************************************\n"
|
||||
"* WARNING!! *\n"
|
||||
"*******************************************************************\n"
|
||||
"You have installed PyWebServer for the first time!\n"
|
||||
"PyWebServer comes with test keys and certificates!\n"
|
||||
"THESE SHOULD UNDER NO CIRCUMSTANCE BE USED IN ANYTHING BUT LOCAL TESTING!!!\n"
|
||||
"IF YOU DON'T FOLLOW THESE INSTRUCTIONS YOU ARE PUTTING ALL YOUR TRAFFIC IN DANGER!!!\n"
|
||||
"PLEASE REMOVE THEM ASAP IF YOU'RE USING THIS IN ANY FORM OF PRODUCTION!!!\n"
|
||||
"*******************************************************************\n"
|
||||
"* WARNING!! *\n"
|
||||
"*******************************************************************\n"
|
||||
)
|
||||
confirm = input("Do you understand? [y/N] ")
|
||||
if confirm != "y":
|
||||
print("User did not confirm, exiting!")
|
||||
file_handler.didnt_confirm()
|
||||
exit(1)
|
||||
file_handler.base_dir = file_handler.read_config("directory")
|
||||
http_port = file_handler.read_config("port") or 8080
|
||||
https_port = file_handler.read_config("port-https") or 8443
|
||||
http_enabled = file_handler.read_config("http") or True
|
||||
|
Reference in New Issue
Block a user