|
import socket
|
|
import os
|
|
from datetime import datetime
|
|
import pathlib
|
|
import html
|
|
server_start = datetime.now()
|
|
import pgs
|
|
print(pgs.add(1,2))
|
|
|
|
def get_server_uptime():
|
|
seconds = (datetime.now() - server_start).total_seconds()
|
|
if seconds > 60*60: # hour
|
|
return f"{int(seconds/60/60)} hours"
|
|
elif seconds > 60: # minute
|
|
return f"{int(seconds/60)} minutes"
|
|
return f"{seconds} seconds"
|
|
|
|
# Hostname, only resolved at startup.
|
|
hostname = socket.gethostname()
|
|
|
|
# Specify environment based on hostname.
|
|
env = "dev" if hostname.startswith("retoor") else "prod"
|
|
|
|
# This dict will contain the connections in this format: {downstream_fd:upstream_fd]
|
|
streams = {}
|
|
|
|
# This is debug variable. It holds the number of connections total made.
|
|
counter = 0
|
|
|
|
def is_ssh(header_bytes):
|
|
return b'SSH' in header_bytes
|
|
|
|
def is_http(header_bytes):
|
|
"""
|
|
Check if the header is an HTTP request.
|
|
"""
|
|
return b'HTTP/1.1' in header_bytes or b'HTTP/1.0' in header_bytes or b'HTTP/2.0' in header_bytes or b'HTTP/3.0' in header_bytes or b'Connection: ' in header_bytes
|
|
|
|
def is_https(header_bytes):
|
|
return not any([is_ssh(header_bytes), is_http(header_bytes)])
|
|
|
|
def route(downstream,upstream):
|
|
"""
|
|
This is a connection router which will be called by the server every
|
|
time a client connects. This function will be used to determine
|
|
the upstream. The downstream and upstream are file descriptors.
|
|
The upstream is not connected yet, it only holds a file descriptor.
|
|
The connection will be made in this function. The connection will be
|
|
set non blocking after this function by pgs.
|
|
This way of routing is so dynamic that you can:
|
|
- Create [your-site].localhost redirects without configuring DNS.
|
|
- Run multiple services on the same port. It is possible to run ssh
|
|
on the same port as your HTTPS server. This is a good idea in sense
|
|
of security. Very unique, who does / expects that?
|
|
- Rewrite urls in general.
|
|
- Make clients always connect to the same upstream. Servers only have
|
|
to manage their own session instead of having to communicate with
|
|
redis.
|
|
- You can inject headers in the request.
|
|
- You can add HTTP Basic Authentication to protect all your services
|
|
in a very early stage.
|
|
- This server is quick, it can act as ddos protection.
|
|
- You can make your server act as a load balancer.
|
|
- You can make your server act as a reverse proxy.
|
|
- You can apply rate limits.
|
|
- You can cache responses.
|
|
- You can implement a complete custom protocol here. Complete own
|
|
design. This feature will probably moved in the future.
|
|
- You can do static file serving.
|
|
- You can protect sensitive data not leaving the network by
|
|
intercepting it.
|
|
- You can call AI to make modications.
|
|
- You can call databases.
|
|
- You can save statistics.
|
|
"""
|
|
|
|
global streams
|
|
global counter
|
|
|
|
counter += 1
|
|
|
|
print("Connection nr.", counter)
|
|
u = socket.fromfd(upstream, socket.AF_INET, socket.SOCK_STREAM)
|
|
#u = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
#print("FD:",u.fileno())
|
|
peek = pgs.read(downstream, 4096).tobytes()
|
|
|
|
|
|
if pgs.is_ssh(peek):
|
|
print("Forwarding to ssh molodetz")
|
|
u.connect(("molodetz.nl", 22))
|
|
elif pgs.is_http(peek):
|
|
|
|
if b'/random' in peek or b'random.' in peek:
|
|
|
|
print("Forwarding to 127.0.0.1:3028.")
|
|
peek = peek.replace(b'/random', b'/')
|
|
peek = peek.replace(b'random.', b'')
|
|
u.connect(("127.0.0.1", 3028))
|
|
elif b'molodetz.local' in peek:
|
|
print("Forwarding to 127.0.0.1:8082.")
|
|
peek = peek.replace(b'molodetz.local', b'localhost')
|
|
u.connect(("127.0.0.1", 8082))
|
|
elif b'bench.local' in peek:
|
|
print("Responding with bench page.")
|
|
body = f"""<html>\n<head>\n<title>Benchmark page.</title>\n</head>\n<body>\n<h1>Bench</h1>\n<p>{counter}</p>\n</body>\n</html>\n""".encode()
|
|
|
|
s = socket.fromfd(downstream, socket.AF_INET, socket.SOCK_STREAM)
|
|
s.sendall(
|
|
b'HTTP/1.1 200 Pretty Good Server.\r\n'
|
|
+ b'Content-Length: ' + str(len(body)).encode() + b'\r\n'
|
|
+ b'Content-Type: text/html\r\n\r\n'
|
|
+ body
|
|
)
|
|
u = None
|
|
s.shutdown(socket.SHUT_RDWR)
|
|
else:
|
|
# 404
|
|
if env == "prod":
|
|
pgs.write(downstream,
|
|
b'HTTP/1.1 403 Authorization Required.\r\n\r\n')
|
|
else:
|
|
pgscript_source = html.escape(pathlib.Path(__file__).read_text())
|
|
content = f"""<pre>
|
|
Server: Pretty Good Server
|
|
Environment: {env}
|
|
Total connections: {counter}
|
|
Local hostname: {hostname}
|
|
Downstream FD: {downstream}
|
|
Upstream FD: {upstream}
|
|
Current time server: {datetime.now()}
|
|
Server started on: {server_start}
|
|
Server uptime: {get_server_uptime()}
|
|
</pre>
|
|
<h3>Source code of pgscript.py</h3>
|
|
<i>Location: {pathlib.Path(__file__).resolve()}</i>
|
|
<pre style="color:blue;">
|
|
{pgscript_source}
|
|
</pre>
|
|
"""
|
|
body = f"""<html>\n<head>\n<title>Debug page.</title>\n</head>\n<body>\n<h1>Pretty Good Server</h1>\n<h3>Debugging information</h3>\n<p>{content}</p>\n</body>\n</html>\n"""
|
|
headers = ["HTTP/1.1 200 Pretty Good Server.",
|
|
"Content-Length: {len(body)}",
|
|
"Content-Type: text/html",
|
|
""
|
|
]
|
|
headers = "\r\n".join(headers)
|
|
response = f"{headers}{body}"
|
|
|
|
pgs.write(downstream,response)
|
|
|
|
# Unset socket so the server will close it.
|
|
# Do not disconnect in python!
|
|
# Instead of a 404, we also could've displayed a custom page.
|
|
# Maybe some server statistics?
|
|
u = None
|
|
|
|
elif is_https(peek) and env == "prod":
|
|
print("Forwarding to dev.to")
|
|
u.connect(("devrant.com", 443))
|
|
peek = peek.replace(b'localhost', b'devrant.com')
|
|
peek = peek.replace(b'molodetz.nl', b'devrant.com')
|
|
else:
|
|
# Error.
|
|
print("Could not find upstream for header content.")
|
|
print(f"Closing connection. Your current environment: {env}")
|
|
# Don't have to close socket, pgs will do that himself.
|
|
# Socket handling is done at one place to avoid race conditions.
|
|
u = None
|
|
|
|
if not u:
|
|
return -1
|
|
|
|
# Remove reference to the socket so it doesn't get garbage collected.
|
|
# This could break the connection. This way, it stays open.
|
|
u = None
|
|
|
|
os.write(upstream,peek)
|
|
|
|
# Keep track of connections. Not sure if this is needed.
|
|
streams[downstream] = upstream
|
|
streams[upstream] = downstream
|
|
|
|
# Return exact same value as what is given as parameter.
|
|
return upstream
|