17 Commits
1.0+u2 ... main

Author SHA1 Message Date
a36141edd0 release for 1.3.0
minor update, got ua blocking working and match statements in config
2025-07-22 16:35:39 +02:00
0b57578cc4 Updated README to reflect project updates 2025-07-08 15:02:50 +02:00
d6fdc5c63d v1.2.1: small bugfixes and welcome page. 2025-07-08 14:37:27 +02:00
590abbf649 Uuuh? 2025-06-15 13:33:52 +02:00
87a2505395 Merge branch 'main' of https://git.novacow.ch/Nova/PyWebServer 2025-06-15 13:33:14 +02:00
118a342e9d Update to v1.2, crude fix for #6 and for #5 2025-06-15 13:32:19 +02:00
9cd0528ab3 Update README.md 2025-05-04 14:54:51 +02:00
98aab9539a Made an oopsie with conf file 2025-05-03 23:49:38 +02:00
6244650180 Fix for #4, more permanent solution 2025-05-03 23:47:30 +02:00
c4a1140d83 Merge branch 'main' of https://git.novacow.ch/Nova/PyWebServer 2025-05-03 23:42:01 +02:00
22a37670f7 Updated README to reflect PyWebServer state 2025-05-03 22:16:11 +02:00
5014ff2a04 Quick #4 fix 2025-05-03 22:10:40 +02:00
e6b188196b Update README.md to reflect GitHub mirror 2025-05-03 16:28:07 +02:00
43dd3d1b44 Small #3 acknowlegdement in code 2025-04-18 22:03:30 +02:00
b1bf3825de README update 2025-04-14 08:21:51 +02:00
7ac160f625 Quick fix that adresses #1 2025-04-13 13:27:58 +02:00
5a92243bcb A quick update to config for more clarity 2025-03-10 15:17:57 +01:00
7 changed files with 339 additions and 138 deletions

View File

@@ -1,2 +1,51 @@
# PyWebServer # PyWebServer
## 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:
```bash
git clone https://git.novacow.ch/Nova/PyWebServer.git
cd ./PyWebServer/
```
Windows users, make sure you have installed Git, from there:
```powershell
git clone https://git.novacow.ch/Nova/PyWebServer.git
Set-Location .\PyWebServer\
```
Then, open `pywebsrv.conf` in your favorite text editor and change the `directory` key to the full path where your files are stored.
After that, put your files in and run this:
Linux:
```bash
python3 /path/to/pywebsrv.py
```
Windows:
```powershell
# If you have installed Python via the Microsoft Store:
python3 \path\to\pywebsrv.py
# Via the python.org website:
py \path\to\pywebsrv.py
```
## SSL Support
PyWebServer supports SSL/TLS for authentication via HTTPS. In the config file, you should enable the HTTPS port. After that you need to create the certificate.
Currently PyWebServer looks for the `cert.pem` and the `key.pem` files in the root directory of the installation.
## HTTP support
Currently PyWebServer only supports HTTP/1.1, this is very unlikely to change, as most of the modern web today still uses HTTP/1.1.
For methods PyWebServer only supports `GET`, this is being reworked though, check issue [#3](https://git.novacow.ch/Nova/PyWebServer/issues/3) for progress.
## Files support
Unlike other small web servers, PyWebServer has full support for binary files being sent and received (once that logic is put in) over HTTP(S).
## Support
PyWebServer will follow a standard support scheme.
### 1.x
For every 1.x version there will be support until 2 newer versions come out.
So that means that 1.0 will still be supported when 1.1 comes out, but no longer be supported when 1.2 comes out.
### 2.x
I am planning on releasing a 2.x version with will have a lot more advanced features, like nginx's server block emulation amongst other things.
When 2.0 will come out, the last version of 1.x will be supported for a while longer, but no new features will be added.

View File

@@ -1,21 +0,0 @@
-----BEGIN CERTIFICATE-----
MIIDfTCCAmWgAwIBAgIUbZA2WZ1Q7ZGmYttO+f6w5tFXZLMwDQYJKoZIhvcNAQEL
BQAwazELMAkGA1UEBhMCTk8xETAPBgNVBAgMCE5vcmRsYW5kMQ4wDAYDVQQHDAVC
b2TDuDEXMBUGA1UECgwOTm92YSdzIHRlc3QgQ0ExIDAeBgNVBAMMF05vdmEncyB0
ZXN0aW5nIENBIENlcnQuMB4XDTI1MDMwNDIyMjMwNFoXDTI2MDMwNDIyMjMwNFow
bDELMAkGA1UEBhMCWloxDzANBgNVBAgMBkdsb2JhbDEPMA0GA1UEBwwGR2xvYmFs
MSAwHgYDVQQKDBdOb3ZhJ3MgdGVzdCBjZXJ0aWZpY2F0ZTEZMBcGA1UEAwwQTm92
YSdzIHRlc3QgY2VydDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANmh
gdz5oi+Z1ci0oA1q4NeSeU7b58TkRPvz7g2th4x1OjOhyEA2qG2sOKpjwZ9FB7Ce
TPenZ3M3ISq5MQxGJdHB5tzP86d4fbnRldqS3hs+XW+OYvVWcIonHr8OQXsx1qFP
2yJGIVRMDcxarFg4ZnIk/M5LsgogrYnhOVhg9mi58tLKp+Q+D10RwDPppi0/e5Ud
XM4qrkysY0rA1DwiAgj5MSWwnDCTeUbZDA+znBV5b521VS2XkoVhy49A3lCO2YHc
zAdoyLwAUl84lDN5oQPlqkMN2kEDJw2UDxpCFmzdVvMX30uuQY+vpYI0suwrDBye
0VxkAwX5qI454SLydE8CAwEAAaMYMBYwFAYDVR0RBA0wC4IJbG9jYWxob3N0MA0G
CSqGSIb3DQEBCwUAA4IBAQCIRzTVzeRxWFmBg2wo1W9QXdVorAALw+xcceypHdrA
GYTW7WYLmxXHTSy414p0KFdQ9/CgUpXE0LxwD1gLmWlKEheqlh2T9FPBUK/axZvG
00o/YtAaSDHtiC+OcEzPfTFxEpdOoMMBoCpyLBt+0CgfV1BJFRK9Hw7ZOaVQ2eLC
nxBypEKf3hv0gtGaKnm+vFYDm4Az3+CojtzJiR07WUsPn5HvbOgH6k7jmKuFiR2w
FpPrErVbbLMCZB7+uxfaJyQaEc9DmUf+LDFLbVkM7gk1o249WLjRR5d8MatkwEPN
auYdVlrb/CpxTbNzzipFCX+hnFojuFjXp266woplKleW
-----END CERTIFICATE-----

61
certgen.py Normal file
View 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(self):
# 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("cert.pem", "wb") as f:
f.write(certificate.public_bytes(serialization.Encoding.PEM))
print("Self-signed certificate and private key generated for HTTPS server!")

13
html/index.html Normal file
View File

@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html>
<head>
<title>Test page</title>
</head>
<body>
<h1>Hey there!</h1>
<h2>You're seeing this page because you haven't set up PyWebServer yet!</h2>
<h2>This page confirms that PyWebServer can read and serve files from your PC.</h2>
<h2>To make this go away, please edit the file `pywebsrv.conf` and edit the `directory` key to your directory of choice!</h2>
<p>Here you can simulate a 404 error: <a href="/uuh">Click me for a 404 error!</a></p>
</body>
</html>

27
key.pem
View File

@@ -1,27 +0,0 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEpQIBAAKCAQEA2aGB3PmiL5nVyLSgDWrg15J5TtvnxORE+/PuDa2HjHU6M6HI
QDaobaw4qmPBn0UHsJ5M96dnczchKrkxDEYl0cHm3M/zp3h9udGV2pLeGz5db45i
9VZwiicevw5BezHWoU/bIkYhVEwNzFqsWDhmciT8zkuyCiCtieE5WGD2aLny0sqn
5D4PXRHAM+mmLT97lR1cziquTKxjSsDUPCICCPkxJbCcMJN5RtkMD7OcFXlvnbVV
LZeShWHLj0DeUI7ZgdzMB2jIvABSXziUM3mhA+WqQw3aQQMnDZQPGkIWbN1W8xff
S65Bj6+lgjSy7CsMHJ7RXGQDBfmojjnhIvJ0TwIDAQABAoIBAACkfu8pl4Z/dEei
7OQNQDuytYP7lzwYFnIN/tJwhDlwcSsM27wAzU+Blis+nyg6unKVjRGgH2iSLZlk
MZZhMKRlZ6qYPJZufySIz2H1VA2NihYVvAoQZsWppugWgS/9bi5Mv49i2J9YmCPV
0rNx+y90F4D+bTilbw28qgAuTRvzCzTYqcOLnBvjfHfhh1gzOADB4zHGjEb4qwWd
GCPGs85tzfT2Bez6GTCvzNEf8kmGO8EwynZk20SPkswcMIQhES1S6wC3zOi0C9+Y
B4dVnfgtukvsgG+AAtBo8rx6iVIKlGMU3xex9+aZPiJ8O8A/zOJU34IpdDMf7Oha
bK44pgUCgYEA9mK6tmSgN6Sqji9r7hWSse+tX6faeIeFSDIo33pre2jNyM1qTBHY
VZ54CoGa02PLRclqci0TsRaN5Gh+wLVzLW2NDLMEZFXIELF4vGexGnOWLYrxp4hm
uk8rQskoa7/pE7gyjgbYjXqn+wM2ifyc/XXFwTbjbFrj7zPkEdhNmV0CgYEA4h+I
MLn+4PvABojLekU8EHVLAjnWbKYie/a0ELYDz+DZiGgtU21q4HgaOI8SSRA/UvFW
l1i75NmKALT/d89Bok0THmfWAIIPzbsboRJe8f8uce9ICwdmbYKHCJwmgDyhq4ic
UoDzWAuUQa144tcC91Mop5VYa5Ee8TYswIuybZsCgYEA0oQw/D6mFmT/xVUHZvnP
yXD8Ncr5hBpm6vTQr4Gt7Ffz3CqHNE/bA+zOrEtouk1+FTavWLbjKGAZBJu0EXv3
2UzNQ5iBnCkfNAQvIOuICw3Pt0IMkBSfkXirgfjWLJpgz5SGvYtj5B51AKgSJXxN
ttK2EQyQ7LgMIQm5SPYD95ECgYEAlJykpWGYYcUTLzg4guN91lNAOPZKNp35i/9X
2KPHXZgpX70YDPycgWpt0T42hk5nT9vNTSrEUmOmj1BllhhgyopdRl54B11zhYKz
Zejs/Z74p2jbsGPsrYxbswztQNqYZmQiWRbm17bEeWXJTUyCZooA7iL5Objm3SD9
yI4HdoECgYEAzNAa7QJy/bgjuaP8fNx+vfsgMWQ9WT12IXnoiJN4I6mKBrSoJGJ2
EeM41K1lRglI70WDHFPVn7AQvLiFgfWRoI0ucT65VUzYHT+m2g4p7wwb4wPLkhoj
nMLthqEoO+CMIrdGSUVmOwlQ4SnKn9G9a2R4yAEJzkItsYDWD+/hzTc=
-----END RSA PRIVATE KEY-----

View File

@@ -1,17 +1,33 @@
# Using NSCL 1.3 # Using NSCL 1.3
# Port defenition. What ports to use.
# port is the HTTP port, port-https is the HTTPS port
port:8080 port:8080
directory:/home/nova/Documents/html
host:localhost
# DANGER: NEVER EVER TURN THIS ON IN PROD!!!!!!!!!!!!
allow-all:1
# DANGER!!
port-https:8443 port-https:8443
# Here you choose what directory PyWebServer looks in for files.
directory:<Enter directory here>
# Host defenition, what hosts you can connect via.
# You can use FQDNs, IP-addresses and localhost,
# Support for multiple hosts is coming.
host:localhost
# Enables HTTP support. (Only enables/disables the HTTP port.)
http:1 http:1
# Enables HTTPS support. (Only enables/disables the HTTPS port.)
https:1 https:1
allow-localhost:0 # Allows the use of localhost to connect.
# for use in libraries # The default is on, this is seperate of the host defenition.
# disable-autocertgen:0 allow-localhost:1
# If you're using the webserver in a library form,
# you can disable the AutoCertGen and never trigger it.
disable-autocertgen:0
# If you wish to block IP-addresses, this function is coming though.
# block-ip:0.0.0.0,1.1.1.1,2.2.2.2 # block-ip:0.0.0.0,1.1.1.1,2.2.2.2
# If you wish to block User-Agents, this function is coming though.
# block-ua:(NULL) # block-ua:(NULL)
# allow-nohost:0
# In libraries you can disable everything you don't need. # TEST: experimental non-defined keys go here:
# keyfile key
key-file:/home/nova/PyWebServer/key.pem
# certfile keys
cert-file:/home/nova/PyWebServer/cert.pem
# allowed-methods, csv's
allowed-methods:GET

View File

@@ -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 This is PyWebServer, an ultra minimalist webserver, meant to still have
a lot standard webserver features. A comprehensive list is below: a lot standard webserver features. A comprehensive list is below:
Features: Features:
@@ -18,9 +31,12 @@ Simple to understand and mod codebase.
All GNU GPL-3-or-above license. (Do with it what you want.) All GNU GPL-3-or-above license. (Do with it what you want.)
Library aswell as a standalone script: Library aswell as a standalone script:
You can easily get access to other parts of the script if you need it. You can easily get access to other parts of the script if you need it.
TODO: actually put normal comments in
""" """
import os import os
import mimetypes
import threading import threading
import ssl import ssl
import socket import socket
@@ -28,7 +44,7 @@ import signal
import sys import sys
try: try:
from autocertgen import AutoCertGen from certgen import AutoCertGen
except ImportError: except ImportError:
print( print(
"WARN: You need the AutoCertGen plugin! Please install it from\n" "WARN: You need the AutoCertGen plugin! Please install it from\n"
@@ -41,12 +57,12 @@ class FileHandler:
DEFAULT_CONFIG = ( DEFAULT_CONFIG = (
"port:8080\nport-https:8443\nhttp:1" "port:8080\nport-https:8443\nhttp:1"
"\nhttps:0\ndirectory:{cwd}\nhost:localhost" "\nhttps:0\ndirectory:{cwd}\nhost:localhost"
"allow-all:1\nallow-localhost:1" "\nallow-localhost:1"
) )
def __init__(self, base_dir=None): def __init__(self, base_dir=None):
self.base_dir = base_dir or os.path.join(os.getcwd(), "html")
self.config_path = os.path.join(os.getcwd(), self.CONFIG_FILE) self.config_path = os.path.join(os.getcwd(), self.CONFIG_FILE)
self.base_dir = self.read_config("directory")
def check_first_run(self): def check_first_run(self):
if not os.path.isfile(self.config_path): if not os.path.isfile(self.config_path):
@@ -60,18 +76,19 @@ class FileHandler:
def read_file(self, file_path): def read_file(self, file_path):
if "../" in file_path: if "../" in file_path:
return 403 return 403, None
full_path = os.path.join(self.base_dir, file_path.lstrip("/")) full_path = os.path.join(self.base_dir, file_path.lstrip("/"))
if not os.path.isfile(full_path): if not os.path.isfile(full_path):
return 404 return 404, None
try: try:
mimetype = mimetypes.guess_type(full_path)
with open(full_path, "rb") as f: with open(full_path, "rb") as f:
return f.read() return f.read(), mimetype
except Exception as e: except Exception as e:
print(f"Error reading file {full_path}: {e}") print(f"Error reading file {full_path}: {e}")
return 500 return 500, None
def write_file(self, file_path, data): def write_file(self, file_path, data):
if "../" in file_path: if "../" in file_path:
@@ -94,9 +111,11 @@ class FileHandler:
"http", "http",
"https", "https",
"port-https", "port-https",
"allow-all",
"allow-localhost", "allow-localhost",
"disable-autocertgen", "disable-autocertgen",
"key-file",
"cert-file",
"block-ua"
] ]
if option not in valid_options: if option not in valid_options:
return None return None
@@ -111,21 +130,36 @@ class FileHandler:
key = key.lower() key = key.lower()
if key == option: if key == option:
if option == "host": if option == "host":
seperated_values = value.split(",", 0) seperated_values = value.split(",", -1)
return [value.lower() for value in seperated_values] return [value.lower() for value in seperated_values]
if option == "block-ua":
seperated_values = value.split(",", -1)
host_to_match = []
literal_blocks = []
for val in seperated_values:
if val.startswith("match(") and val.endswith(")"):
idx = val.index("(")
idx2 = val.index(")")
ua_to_match = val[idx+1:idx2]
host_to_match.append(ua_to_match)
else:
literal_blocks.append(val)
return host_to_match, literal_blocks
if option == "port" or option == "port-https": if option == "port" or option == "port-https":
return int(value) return int(value)
if ( if (
option == "http" option == "http"
or option == "https" or option == "https"
or option == "allow-all"
or option == "allow-localhost" or option == "allow-localhost"
or option == "disable-autocertgen" or option == "disable-autocertgen"
): ):
print(
f"option: {option}, val: {value}, ret: {bool(int(value))}"
)
return bool(int(value)) return bool(int(value))
if option == "directory":
if value == "<Enter directory here>":
return os.path.join(os.getcwd(), "html")
if value.endswith("/"):
value = value.rstrip("/")
return value
return value return value
return None return None
@@ -134,28 +168,33 @@ class FileHandler:
Generate some self-signed certificates using AutoCertGen Generate some self-signed certificates using AutoCertGen
""" """
autocert = AutoCertGen() autocert = AutoCertGen()
pk = autocert.generate_private_key() autocert.gen_cert()
sub, iss = autocert.generate_issuer_and_subject()
cert = autocert.build_cert(pk, iss, sub)
autocert.write_cert(pk, cert)
class RequestParser: class RequestParser:
def __init__(self): def __init__(self):
self.allowed_methods_file = "allowedmethods.conf"
self.file_handler = FileHandler() self.file_handler = FileHandler()
self.hosts = self.file_handler.read_config("host") self.hosts = self.file_handler.read_config("host")
self.all_allowed = self.file_handler.read_config("allow-all")
def parse_request_line(self, line): def parse_request_line(self, line):
"""Parses the HTTP request line.""" """Parses the HTTP request line."""
try: try:
method, path, version = line.split(" ") method, path, version = line.split(" ")
except ValueError:
return None, None, None
if path.endswith("/"): if path.endswith("/"):
path += "index.html" path += "index.html"
return method, path, version return method, path, version
except ValueError:
return None, None, None def ua_blocker(self, ua):
"""Parses and matches UA to block"""
match, literal = self.file_handler.read_config("block-ua")
if ua in literal:
return False
for _ua in match:
if _ua.lower() in ua.lower():
return False
return True
def is_method_allowed(self, method): def is_method_allowed(self, method):
""" """
@@ -165,9 +204,10 @@ class RequestParser:
Should (for now) only be GET as I haven't implemented the logic for PUT Should (for now) only be GET as I haven't implemented the logic for PUT
""" """
allowed_methods = ["GET"] allowed_methods = ["GET"]
if os.path.isfile(self.allowed_methods_file): # While the logic for PUT, DELETE, etc. is not added, we shouldn't
with open(self.allowed_methods_file, "r") as f: # allow for it to attempt it.
allowed_methods = [line.strip() for line in f] # Prepatched for new update.
# allowed_methods = self.file_handler.read_config("allowed-methods")
return method in allowed_methods return method in allowed_methods
def host_parser(self, host): def host_parser(self, host):
@@ -175,17 +215,18 @@ class RequestParser:
Parses the host and makes sure it's allowed in Parses the host and makes sure it's allowed in
Mfw im in an ugly code writing contest and my opponent is nova while writing a side project Mfw im in an ugly code writing contest and my opponent is nova while writing a side project
""" """
host = str(host) host = f"{host}"
if ":" in host: if ":" in host:
host = host.split(":", 1)[0] host = host.split(":", 1)[0]
host = host.lstrip() host = host.lstrip()
host = host.rstrip()
if ( if (
host == "localhost" or host == "127.0.0.1" host == "localhost" or host == "127.0.0.1"
) and self.file_handler.read_config("allow-localhost"): ) and self.file_handler.read_config("allow-localhost"):
return True return True
if host not in self.hosts and self.all_allowed is False: if host not in self.hosts:
return False return False
elif host not in self.hosts and self.all_allowed is True: else:
return True return True
@@ -203,6 +244,13 @@ class WebServer:
# me when no certificate and key file # me when no certificate and key file
if not os.path.exists(self.cert_file) or not os.path.exists(self.key_file): if not os.path.exists(self.cert_file) or not os.path.exists(self.key_file):
if not os.path.exists(self.cert_file) and not os.path.exists(self.key_file):
pass
# maybe warn users we purge their key/cert files? xdd
elif not os.path.exists(self.cert_file):
os.remove(self.key_file)
elif not os.path.exists(self.key_file):
os.remove(self.cert_file)
print("WARN: No HTTPS certificate was found!") print("WARN: No HTTPS certificate was found!")
if self.file_handler.read_config("disable-autocertgen") is True: if self.file_handler.read_config("disable-autocertgen") is True:
print("WARN: AutoCertGen is disabled, ignoring...") print("WARN: AutoCertGen is disabled, ignoring...")
@@ -214,11 +262,8 @@ class WebServer:
else: else:
self.skip_ssl = True self.skip_ssl = True
# TODO: change this to something like oh no you fucked up, go fix idiot
self.no_host_req_response = ( self.no_host_req_response = (
"Connecting via this host is disallowed\r\n" "This host cannot be reached without sending a `Host` header."
"You may also be using a very old browser!\r\n"
"Ask the owner of this website to set allow-all to 1!"
) )
# ipv6 when????/??//?????//? # ipv6 when????/??//?????//?
@@ -240,6 +285,22 @@ class WebServer:
self.https_socket, server_side=True self.https_socket, server_side=True
) )
self.http_404_html = (
"<html><head><title>HTTP 404 - PyWebServer</title></head>"
"<body><center><h1>HTTP 404 - Not Found!</h1><p>Running PyWebServer/1.2.1</p>"
"</center></body></html>"
)
self.http_403_html = (
"<html><head><title>HTTP 403 - PyWebServer</title></head>"
"<body><center><h1>HTTP 403 - Forbidden</h1><p>Running PyWebServer/1.2.1</p>"
"</center></body></html>"
)
self.http_405_html = (
"<html><head><title>HTTP 405 - PyWebServer</title></head>"
"<body><center><h1>HTTP 405 - Method not allowed</h1><p>Running PyWebServer/1.2.1</p>"
"</center></body></html>"
)
self.running = True self.running = True
def start(self, http, https): def start(self, http, https):
@@ -250,14 +311,13 @@ class WebServer:
https_thread = threading.Thread(target=self.start_https, daemon=True) https_thread = threading.Thread(target=self.start_https, daemon=True)
if https is True: if https is True:
if self.skip_ssl is True:
print("WARN: You have enabled HTTPS without SSL!!")
yn = input("Is this intended behaviour? [y/N] ")
https_thread.start() https_thread.start()
if http is True: if http is True:
http_thread.start() http_thread.start()
print(
f"Server running:\n - HTTP on port {self.http_port}\n - HTTPS on port {self.https_port}"
)
http_thread.join() http_thread.join()
https_thread.join() https_thread.join()
@@ -267,7 +327,6 @@ class WebServer:
while self.running: while self.running:
try: try:
conn, addr = self.http_socket.accept() conn, addr = self.http_socket.accept()
print(f"HTTP connection received from {addr}")
self.handle_connection(conn, addr) self.handle_connection(conn, addr)
except Exception as e: except Exception as e:
print(f"HTTP error: {e}") print(f"HTTP error: {e}")
@@ -280,7 +339,6 @@ class WebServer:
while self.running: while self.running:
try: try:
conn, addr = self.https_socket.accept() conn, addr = self.https_socket.accept()
print(f"HTTPS connection received from {addr}")
self.handle_connection(conn, addr) self.handle_connection(conn, addr)
except Exception as e: except Exception as e:
print( print(
@@ -291,12 +349,17 @@ class WebServer:
def handle_connection(self, conn, addr): def handle_connection(self, conn, addr):
try: try:
data = conn.recv(512) # why? well internet and tutiorials data = conn.recv(512)
request = data.decode(errors="ignore") request = data.decode(errors="ignore")
if not data:
response = self.build_response(400, "Bad Request") # user did fucky-wucky
elif len(data) > 8192:
response = self.build_response(413, "Request too long")
else:
response = self.handle_request(request, addr) response = self.handle_request(request, addr)
if isinstance(response, str): if isinstance(response, str):
response = response.encode() # if we send text this shouldn't explode response = response.encode()
conn.sendall(response) conn.sendall(response)
except Exception as e: except Exception as e:
@@ -305,9 +368,7 @@ class WebServer:
conn.close() conn.close()
def handle_request(self, data, addr): def handle_request(self, data, addr):
if not data: print(f"data: {data}")
return self.build_response(400, "Bad Request") # user did fucky-wucky
request_line = data.splitlines()[0] request_line = data.splitlines()[0]
# Extract host from headers, never works though # Extract host from headers, never works though
@@ -321,92 +382,141 @@ class WebServer:
) )
break break
else: else:
if (
self.file_handler.read_config("allow-nohost") is True
): # no host is stupid
pass
return self.build_response( return self.build_response(
403, self.no_host_req_response.encode() 400, self.no_host_req_response.encode()
) # the default (i hope to god) )
for line in data.splitlines():
if "User-Agent" in line:
ua = line.split(":", 1)[1].strip()
allowed = self.parser.ua_blocker(ua)
if not allowed:
return self.build_response(
403, "This UA has been blocked by the owner of this site."
)
break
else:
return self.build_response(
400, "You cannot connect without a User-Agent."
)
method, path, version = self.parser.parse_request_line(request_line) method, path, version = self.parser.parse_request_line(request_line)
if not all([method, path, version]) or not self.parser.is_method_allowed( if not all([method, path, version]):
return self.build_response(400, "Bad Request")
# Figure out a better way to reload config
if path == "/?pywebsrv_reload_conf=1":
print("Got reload command! Reloading configuration...")
self.file_handler = FileHandler()
self.parser = RequestParser()
return self.build_response(302, "")
if not self.parser.is_method_allowed(
method method
): ):
return self.build_response(405, "Method Not Allowed") return self.build_response(405, self.http_405_html)
file_content = self.file_handler.read_file(path) file_content, mimetype = self.file_handler.read_file(path)
if file_content == 403: if file_content == 403:
print("WARN: Directory traversal attack prevented.") # look ma, security!! print("WARN: Directory traversal attack prevented.") # look ma, security!!
return self.build_response(403, "Forbidden") return self.build_response(403, self.http_403_html)
if file_content == 404: if file_content == 404:
return self.build_response(404, "Not Found") return self.build_response(404, self.http_404_html)
if file_content == 500: if file_content == 500:
return self.build_response( return self.build_response(
500, 500,
"PyWebServer has encountered a fatal error and cannot serve " "PyWebServer has encountered a fatal error and cannot serve "
"your request. Contact the owner with this error: FATAL_FILE_RO_ACCESS", "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
if path.endswith((".mp3", ".png", ".jpg", ".jpeg", ".gif")): # make this useful.
return self.build_binary_response(200, file_content, path) mimetype = mimetype[0]
if mimetype is None:
# We have to assume it's binary.
return self.build_binary_response(200, file_content, "application/octet-stream")
if "text/" not in mimetype:
return self.build_binary_response(200, file_content, mimetype)
return self.build_response(200, file_content) return self.build_response(200, file_content)
@staticmethod @staticmethod
def build_binary_response(status_code, binary_data, filename): def build_binary_response(status_code, binary_data, content_type):
"""Handles binary files like MP3s.""" """Handles binary files like MP3s."""
messages = { messages = {
200: "OK", 200: "OK",
403: "Forbidden", 403: "Forbidden",
404: "Not Found", 404: "Not Found",
405: "Method Not Allowed", 405: "Method Not Allowed",
500: "Internal Server Error", 500: "Internal Server Error"
} }
status_message = messages.get(status_code) status_message = messages.get(status_code)
# In the spirit of keeping stuff small, we'll just guess and see.
content_type = "application/octet-stream"
if filename.endswith(".mp3"):
content_type = "audio/mpeg"
elif filename.endswith(".png"):
content_type = "image/png"
elif filename.endswith(".jpg") or filename.endswith(".jpeg"):
content_type = "image/jpeg"
elif filename.endswith(".gif"):
content_type = "image/gif"
headers = ( headers = (
f"HTTP/1.1 {status_code} {status_message}\r\n" f"HTTP/1.1 {status_code} {status_message}\r\n"
f"Server: PyWebServer/1.0\r\n" f"Server: PyWebServer/1.2.1\r\n"
f"Content-Type: {content_type}\r\n" f"Content-Type: {content_type}\r\n"
f"Content-Length: {len(binary_data)}\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.
# Tbh when i'll implement HTTP2
) )
return headers.encode() + binary_data return headers.encode() + binary_data
@staticmethod def build_response(self, status_code, body):
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 = { messages = {
200: "OK", 200: "OK",
204: "No Content",
302: "Found",
304: "Not Modified", # TODO KEKL 304: "Not Modified", # TODO KEKL
400: "Bad Request", 400: "Bad Request",
403: "Forbidden", 403: "Forbidden",
404: "Not Found", 404: "Not Found",
405: "Method Not Allowed", 405: "Method Not Allowed",
413: "Payload Too Large",
500: "Internal Server Error", 500: "Internal Server Error",
635: "Go Away"
} }
status_message = messages.get(status_code) status_message = messages.get(status_code)
if isinstance(body, str): if isinstance(body, str):
body = body.encode() body = body.encode()
# TODO: dont encode yet, and i encode. awesome comments here.
# Don't encode yet, if 302 status code we have to include location.
headers = ( headers = (
f"HTTP/1.1 {status_code} {status_message}\r\n" f"HTTP/1.1 {status_code} {status_message}\r\n"
f"Server: PyWebServer/1.0\r\n" f"Server: PyWebServer/1.2.1\r\n"
f"Content-Length: {len(body)}\r\n"
f"Connection: close\r\n\r\n"
).encode()
if status_code == 302:
# 302 currently only happens when the reload is triggered.
# Why not 307, Moved Permanently? Because browsers will cache the
# response and not send the reload command.
host = self.file_handler.read_config("host")[0]
port = self.file_handler.read_config("port-https") or self.file_handler.read_config("port")
if port != 80 and port != 443:
if port == 8443:
host = f"https://{host}:{port}/"
else:
host = f"http://{host}:{port}/"
else:
if port == 443:
host = f"https://{host}/"
else:
host = f"http://{host}/"
headers = (
f"HTTP/1.1 {status_code} {status_message}\r\n"
f"Location: {host}\r\n"
f"Server: PyWebServer/1.2.1\r\n"
f"Content-Length: {len(body)}\r\n" f"Content-Length: {len(body)}\r\n"
f"Connection: close\r\n\r\n" f"Connection: close\r\n\r\n"
).encode() ).encode()
@@ -414,8 +524,7 @@ class WebServer:
return headers + body return headers + body
def shutdown(self, signum, frame): def shutdown(self, signum, frame):
print(f"\nRecieved signal {signum}") print("\nRecieved signal to exit!\nShutting down server...")
print("\nShutting down server...")
self.running = False self.running = False
self.http_socket.close() self.http_socket.close()
self.https_socket.close() self.https_socket.close()
@@ -425,6 +534,7 @@ class WebServer:
def main(): def main():
file_handler = FileHandler() file_handler = FileHandler()
file_handler.check_first_run() file_handler.check_first_run()
file_handler.base_dir = file_handler.read_config("directory")
http_port = file_handler.read_config("port") or 8080 http_port = file_handler.read_config("port") or 8080
https_port = file_handler.read_config("port-https") or 8443 https_port = file_handler.read_config("port-https") or 8443
http_enabled = file_handler.read_config("http") or True http_enabled = file_handler.read_config("http") or True