2016-08-01 18:04:32 +00:00
|
|
|
import enum
|
2016-07-12 14:11:58 +00:00
|
|
|
import os
|
|
|
|
import select
|
|
|
|
import socket
|
|
|
|
import sys
|
|
|
|
import threading
|
|
|
|
import time
|
|
|
|
|
|
|
|
class config: None
|
|
|
|
|
2016-07-12 14:33:59 +00:00
|
|
|
config.max_threads = 8192
|
2016-07-12 14:11:58 +00:00
|
|
|
config.port = 7777
|
|
|
|
config.recognised_selectors = ['0', '1', '5', '9', 'g', 'h', 'I', 's']
|
2016-07-12 14:33:59 +00:00
|
|
|
config.request_max_size = 8192
|
|
|
|
config.socket_timeout = 1
|
2016-07-12 14:11:58 +00:00
|
|
|
|
|
|
|
# error(message)
|
|
|
|
# Print error message to stderr
|
|
|
|
def error(message):
|
|
|
|
program_name = os.path.basename(sys.argv[0])
|
|
|
|
print('%s: Error: %s' % (program_name, message), file = sys.stderr)
|
|
|
|
|
|
|
|
# die(message, status = 1) -> (Never returns)
|
|
|
|
# Print error message to stderr and exit with status code
|
|
|
|
def die(message, status = 1):
|
|
|
|
error(message)
|
|
|
|
sys.exit(status)
|
|
|
|
|
|
|
|
# bind(port, backlog = 1) → [sockets...]
|
|
|
|
# Binds to all available (TCP) interfaces on specified port and returns the sockets
|
|
|
|
# backlog controls how many connections allowed to wait handling before system drops new ones
|
|
|
|
def bind(port, backlog = 1):
|
|
|
|
# Based on code in https://docs.python.org/3/library/socket.html
|
|
|
|
sockets = []
|
|
|
|
for res in socket.getaddrinfo(None, port, socket.AF_UNSPEC, socket.SOCK_STREAM, 0, socket.AI_PASSIVE):
|
|
|
|
af, socktype, proto, canonname, sa = res
|
|
|
|
|
|
|
|
try:
|
|
|
|
s = socket.socket(af, socktype, proto)
|
|
|
|
except OSError:
|
|
|
|
continue
|
2016-07-12 14:16:21 +00:00
|
|
|
# Make IPv6 socket only bind on IPv6 address, otherwise may clash with IPv4 and not get enabled
|
|
|
|
if af == socket.AF_INET6:
|
|
|
|
try:
|
|
|
|
s.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 1)
|
|
|
|
except OSError:
|
|
|
|
pass
|
2016-07-12 14:11:58 +00:00
|
|
|
try:
|
|
|
|
s.bind(sa)
|
|
|
|
s.listen(backlog)
|
|
|
|
except OSError:
|
|
|
|
s.close()
|
|
|
|
continue
|
|
|
|
|
|
|
|
sockets.append(s)
|
|
|
|
|
|
|
|
return sockets
|
|
|
|
|
|
|
|
|
|
|
|
# drop_privileges()
|
|
|
|
# Drops set[ug]id, die()s if unsuccesful
|
|
|
|
def drop_privileges():
|
|
|
|
try:
|
|
|
|
uid = os.getuid()
|
|
|
|
gid = os.getgid()
|
|
|
|
os.setresgid(gid, gid, gid)
|
|
|
|
os.setresuid(uid, uid, uid)
|
|
|
|
except:
|
|
|
|
die('Unable to drop privileges')
|
|
|
|
|
2016-08-01 18:04:32 +00:00
|
|
|
class Protocol(enum.Enum):
|
2016-07-12 14:11:58 +00:00
|
|
|
gopher, http = range(2)
|
|
|
|
|
2016-07-12 14:38:53 +00:00
|
|
|
class RequestError(Exception):
|
2016-07-12 14:11:58 +00:00
|
|
|
def __init__(self, message):
|
|
|
|
self.message = message
|
|
|
|
def __str__(self):
|
|
|
|
return 'Error with handling request: ' + self.message
|
|
|
|
|
|
|
|
# extract_selector_path(selector_path) -> selector, path
|
|
|
|
# Extract selector and path components from a HTTP path
|
|
|
|
def extract_selector_path(selector_path):
|
|
|
|
if len(selector_path) > 0 and selector_path[0] == '/':
|
|
|
|
selector_path = selector_path[1:]
|
|
|
|
|
|
|
|
if len(selector_path) == 0: # / is by default of type 1
|
|
|
|
selector = '1'
|
|
|
|
path = selector_path
|
|
|
|
elif selector_path[0] in config.recognised_selectors: # Requested path has a selector we recognise, extract it
|
|
|
|
selector = selector_path[0]
|
|
|
|
path = selector_path[1:]
|
|
|
|
else: # We couldn't recognise any selector, return None for it
|
|
|
|
selector = None
|
|
|
|
path = selector_path
|
|
|
|
|
|
|
|
return selector, path
|
|
|
|
|
|
|
|
# get_request(sock) -> path, protocol, rest
|
|
|
|
# Read request from socket and parse it.
|
|
|
|
# path is the requested path, protocol is Protocol.gopher or Protocol.http depending on the request protocol
|
|
|
|
# rest is protocol-dependant information
|
|
|
|
def get_request(sock):
|
|
|
|
request = b''
|
|
|
|
while True:
|
2016-07-12 14:33:59 +00:00
|
|
|
try:
|
|
|
|
data = sock.recv(1024)
|
|
|
|
except socket.timeout:
|
2016-07-12 14:38:53 +00:00
|
|
|
raise RequestError('Remote end timed out')
|
2016-07-12 14:11:58 +00:00
|
|
|
if not data: # Other end hung up before sending a header
|
2016-07-12 14:38:53 +00:00
|
|
|
raise RequestError('Remote end hung up unexpectedly')
|
|
|
|
if len(request) >= config.request_max_size:
|
|
|
|
raise RequestError('Request too long')
|
2016-07-12 14:11:58 +00:00
|
|
|
|
|
|
|
request += data
|
|
|
|
|
2016-07-12 14:40:24 +00:00
|
|
|
if b'\n' in data: # First line has been sent, all we care about for now
|
2016-07-12 14:11:58 +00:00
|
|
|
break
|
|
|
|
|
|
|
|
request = request.decode('utf-8')
|
|
|
|
first_line = request.split('\n')[0]
|
|
|
|
if first_line[-1] == '\r':
|
|
|
|
first_line = first_line[:-1]
|
|
|
|
first_line = first_line.split(' ')
|
|
|
|
|
2016-07-12 15:02:36 +00:00
|
|
|
if len(first_line) >= 2 and first_line[0] == 'GET':
|
2016-07-12 14:11:58 +00:00
|
|
|
selector_path = first_line[1]
|
|
|
|
selector, path = extract_selector_path(selector_path)
|
|
|
|
return path, Protocol.http, selector
|
|
|
|
else:
|
|
|
|
if len(first_line) >= 1:
|
|
|
|
path = first_line[0]
|
|
|
|
else:
|
|
|
|
path = ''
|
|
|
|
return path, Protocol.gopher, None
|
|
|
|
|
|
|
|
# Global variables to keep track of the amount of running worker threads
|
|
|
|
threads_amount = 0
|
|
|
|
threads_lock = threading.Lock()
|
|
|
|
|
|
|
|
# Worker thread implementation
|
|
|
|
class Serve(threading.Thread):
|
|
|
|
def __init__(self, sock, address):
|
|
|
|
self.sock = sock
|
|
|
|
self.address = address
|
|
|
|
threading.Thread.__init__(self)
|
|
|
|
|
|
|
|
def handle_request(self):
|
2016-07-12 14:18:00 +00:00
|
|
|
path, protocol, rest = get_request(self.sock)
|
2016-07-12 14:14:01 +00:00
|
|
|
answer = str((path, protocol, rest))+'\n'
|
|
|
|
self.sock.sendall(answer.encode('utf-8'))
|
2016-07-12 14:11:58 +00:00
|
|
|
|
|
|
|
def run(self):
|
|
|
|
global threads_amount, threads_lock
|
|
|
|
|
|
|
|
try:
|
|
|
|
self.handle_request()
|
2016-07-12 14:23:58 +00:00
|
|
|
except BaseException as err: # Catch and log exceptions instead of letting to crash, as we need to update the worker thread count on abnormal exit as well
|
2016-07-12 14:45:55 +00:00
|
|
|
error('Worker thread (%s) died with: %s' % (self.address, err))
|
2016-07-12 14:11:58 +00:00
|
|
|
finally:
|
|
|
|
self.sock.close()
|
|
|
|
with threads_lock:
|
|
|
|
threads_amount -= 1
|
|
|
|
|
|
|
|
# spawn_thread(sock, address)
|
|
|
|
# Spawn a new thread to serve a connection if possible, do nothing if not
|
|
|
|
def spawn_thread(sock, address):
|
|
|
|
global threads_amount, threads_lock
|
|
|
|
|
|
|
|
# See if we can spawn a new thread. If not, log an error, close the socket and return. If yes, increment the amount of threads running
|
|
|
|
with threads_lock:
|
|
|
|
if threads_amount >= config.max_threads:
|
|
|
|
error('Could not serve a request from %s, worker thread limit exhausted' % address)
|
|
|
|
sock.close()
|
|
|
|
return
|
|
|
|
else:
|
|
|
|
threads_amount += 1
|
|
|
|
|
|
|
|
# Spawn a new worker thread
|
|
|
|
Serve(sock, address).start()
|
|
|
|
|
|
|
|
# listen(port) -> (Never returns)
|
|
|
|
# Binds itself to all interfaces on designated port and listens on incoming connections
|
|
|
|
# Spawns worker threads to handle the connections
|
|
|
|
def listen(port):
|
|
|
|
# Get sockets that we listen to
|
|
|
|
listening_sockets = bind(port)
|
|
|
|
# Drop privileges, we don't need them after this
|
|
|
|
drop_privileges()
|
|
|
|
|
|
|
|
# If we got no sockets to listen to, die
|
|
|
|
if listening_sockets == []:
|
|
|
|
die('Could not bind to port %i' % port)
|
|
|
|
|
|
|
|
# Create a poll object for the listening sockets and a fd->socket map
|
|
|
|
listening = select.poll()
|
2016-07-12 14:18:00 +00:00
|
|
|
sock_by_fd = {}
|
2016-07-12 14:11:58 +00:00
|
|
|
for s in listening_sockets:
|
|
|
|
listening.register(s, select.POLLIN)
|
|
|
|
sock_by_fd[s.fileno()] = s
|
|
|
|
del listening_sockets
|
|
|
|
|
|
|
|
while True:
|
|
|
|
# Wait for listening sockets to get activity
|
|
|
|
events = listening.poll()
|
|
|
|
for fd,event in events:
|
|
|
|
assert(event == select.POLLIN)
|
2016-07-12 14:18:00 +00:00
|
|
|
# Get socket from table established previously
|
2016-07-12 14:11:58 +00:00
|
|
|
s = sock_by_fd[fd]
|
|
|
|
# Accept and handle the connection
|
2016-07-12 14:18:00 +00:00
|
|
|
conn, addr = s.accept()
|
2016-07-12 14:11:58 +00:00
|
|
|
|
2016-07-12 14:33:59 +00:00
|
|
|
# Set timeout for socket
|
2016-07-12 14:42:36 +00:00
|
|
|
conn.settimeout(config.socket_timeout)
|
2016-07-12 14:33:59 +00:00
|
|
|
|
2016-07-12 14:11:58 +00:00
|
|
|
spawn_thread(conn, addr[0])
|
|
|
|
|
|
|
|
listen(config.port)
|