First commit

This commit is contained in:
Juhani Haverinen 2016-07-12 17:11:58 +03:00
commit dbc7f824ed
2 changed files with 227 additions and 0 deletions

24
UNLICENSE Normal file
View File

@ -0,0 +1,24 @@
This is free and unencumbered software released into the public domain.
Anyone is free to copy, modify, publish, use, compile, sell, or
distribute this software, either in source code form or as a compiled
binary, for any purpose, commercial or non-commercial, and by any
means.
In jurisdictions that recognize copyright laws, the author or authors
of this software dedicate any and all copyright interest in the
software to the public domain. We make this dedication for the benefit
of the public at large and to the detriment of our heirs and
successors. We intend this dedication to be an overt act of
relinquishment in perpetuity of all present and future rights to this
software under copyright law.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
For more information, please refer to [http://unlicense.org]

203
neomi.py Normal file
View File

@ -0,0 +1,203 @@
import os
import select
import socket
import sys
import threading
import time
class config: None
config.port = 7777
config.max_threads = 1024
config.recognised_selectors = ['0', '1', '5', '9', 'g', 'h', 'I', 's']
# 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
try:
s.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 1)
except OSError:
pass
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')
class Protocol:
gopher, http = range(2)
class RequestEerror(Exception):
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:
data = sock.recv(1024)
if not data: # Other end hung up before sending a header
raise RequestEerror('Remote end hung up unexpectedly')
request += data
if b'\n' in request: # First line has been sent, all we care about for now
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(' ')
if len(first_line) >= 1 and first_line[0] == 'GET':
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):
path, protocol, rest = get_request(self.sock)
self.sock.sendall(str((path, protocol, rest)).encode('utf-8'))
def run(self):
global threads_amount, threads_lock
try:
self.handle_request()
#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
# error('Worker thread died with: %s' % err)
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()
sock_by_fd={}
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)
# Get socked from table established previously
s = sock_by_fd[fd]
# Accept and handle the connection
conn,addr = s.accept()
spawn_thread(conn, addr[0])
listen(config.port)