From dbc7f824edde8639506c33903f6db3a6207de2a0 Mon Sep 17 00:00:00 2001 From: Juhani Haverinen Date: Tue, 12 Jul 2016 17:11:58 +0300 Subject: [PATCH] First commit --- UNLICENSE | 24 +++++++ neomi.py | 203 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 227 insertions(+) create mode 100644 UNLICENSE create mode 100644 neomi.py diff --git a/UNLICENSE b/UNLICENSE new file mode 100644 index 0000000..69843e4 --- /dev/null +++ b/UNLICENSE @@ -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] diff --git a/neomi.py b/neomi.py new file mode 100644 index 0000000..2c261c0 --- /dev/null +++ b/neomi.py @@ -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)