You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
314 lines
8.4 KiB
314 lines
8.4 KiB
#!/usr/bin/python |
|
# Software written by Juhani Haverinen (nortti). Influenced in idea and |
|
# some implementation details by https://github.com/puckipedia/pyGopher/ |
|
# ----------------------------------------------------------------------- |
|
# 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. |
|
# ----------------------------------------------------------------------- |
|
# NOTE: Requires python 2 |
|
|
|
import os |
|
import socket |
|
import stat |
|
import subprocess |
|
import threading |
|
import time |
|
|
|
# Config |
|
port = 7777 |
|
gopherroot = os.environ['HOME']+'/gopher' |
|
blacklistfile = os.environ['HOME']+'/gopher_blacklist_1' |
|
|
|
# Set up socket |
|
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) |
|
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) #DEBG |
|
sock.bind(('', port)) |
|
sock.listen(1) |
|
|
|
# Helper functions |
|
def exist(path): |
|
return os.access(path, os.F_OK) |
|
|
|
def isdir(path): |
|
st = os.stat(path) |
|
return stat.S_ISDIR(st.st_mode) |
|
|
|
def isexecutable(path): |
|
return os.access(path, os.X_OK) |
|
|
|
def normalizepath(path): |
|
path = path.split('/') |
|
while '' in path: |
|
path.remove('') |
|
|
|
while '..' in path: |
|
i = path.index('..') |
|
if i == 0: |
|
return None # Attempted to access something outside gopherroot |
|
path = path[:i-1] + path[i+1:] |
|
|
|
return '/'.join(path) |
|
|
|
# Error handling |
|
def error(conn, ishttp, text, code): |
|
sendheader(conn, ishttp, '1', code) |
|
if ishttp: |
|
conn.sendall('<!DOCTYPE html>\n' |
|
'\t<head>\n' |
|
'\t\t<title>%s</title>\n' |
|
'\t</head>\n' |
|
'\t<body>\n' |
|
'\t\t<p>%s</p>\n' |
|
'\t</body>\n' |
|
'</html>' % (code, text)) |
|
else: |
|
conn.sendall('3%s\t/\t(null)\t0\n' % text) |
|
|
|
def notfounderror(conn, path, ishttp): |
|
error(conn, ishttp, '"%s" does not exist' % path, '404 Not Found') |
|
|
|
def notallowederror(conn, path, ishttp): |
|
error(conn, ishttp, 'Access denied', '403 Forbidden') |
|
|
|
# Server implementation |
|
def getrequest(conn): |
|
ishttp = False |
|
data = '' |
|
while True: |
|
chunk = conn.recv(1024) |
|
|
|
if not chunk: |
|
return None, ishttp |
|
|
|
data += chunk |
|
|
|
if data[-1] == '\n': |
|
break |
|
|
|
while len(data) > 0 and data[-1] in ('\r', '\n'): |
|
data = data[:-1] |
|
|
|
# Minimal HTTP support |
|
if len(data) >= 4 and data[:4] == 'GET ': |
|
data = data.split()[1] |
|
ishttp = True |
|
|
|
return data.split('\t'), ishttp |
|
|
|
def getselector(request): # If a HTTP request with selector is used, this extracts the selector |
|
if len(request) < 1: |
|
return (request, None) |
|
|
|
req = request[0] |
|
if len(req) >= 1 and req[0] == '/': |
|
req = req[1:] |
|
args = request[1:] |
|
|
|
if len(req) >= 1 and req[0] in ['0', '1', '5', '9', 'g', 'h', 'I', 's']: # Supported selectors |
|
reqpath = req[1:] |
|
selector = req[0] |
|
elif len(req) == 0: # Root is by default of type 1 |
|
reqpath = '/' |
|
selector = '1' |
|
else: |
|
reqpath = req |
|
selector = None |
|
|
|
return ([reqpath] + args, selector) |
|
|
|
def sendheader(conn, ishttp, selector, code = '200 OK'): |
|
if ishttp: |
|
# All others can safely be made text/plain |
|
contenttypes = {'1': 'text/html; charset=utf-8', |
|
'5': 'application/octet-stream', |
|
'9': 'application/octet-stream', |
|
'g': 'image/gif', |
|
'h': 'text/html; charset=utf-8', |
|
'I': 'application/octet-stream', |
|
's': 'application/octet-stream'} |
|
|
|
if selector is not None and selector in contenttypes: |
|
contenttype = contenttypes[selector] |
|
else: |
|
contenttype = 'text/plain; charset=utf-8' # Default to text/plain |
|
|
|
conn.sendall('HTTP/1.1 %s\r\n' |
|
'Content-type: %s\r\n' |
|
'\r\n' % (code, contenttype)) |
|
|
|
def serveurlredirect(conn, path): |
|
path = path[4:] |
|
conn.sendall('<!DOCTYPE html>\n' |
|
'<html>\n' |
|
'\t<head>\n' |
|
'\t\t<meta http-equiv="refresh" content="1;URL=%s">\n' |
|
'\t</head>\n' |
|
'\t<body>\n' |
|
'\t\t<p><a href="%s">Redirect to %s</a></p>\n' |
|
'\t</body>\n' |
|
'</html>' % (path, path, path)) |
|
|
|
def servecommon(conn, fd): |
|
for line in fd: |
|
conn.sendall(line) |
|
|
|
fd.close() |
|
|
|
def servehtmlgophermap(conn, fd): |
|
conn.sendall('<!DOCTYPE html>\n' |
|
'<html>\n' |
|
'\t<head>\n' |
|
'\t\t<title>Gophermap</title>\n' |
|
'\t</head>\n' |
|
'\t<body>\n' |
|
'\t\t<p>\n' |
|
'\t\t\t<a href="..">..</a> <a href="/">/</a><br/>\n') |
|
|
|
for line in fd: |
|
while len(line) > 0 and line[-1] == '\n': |
|
line = line[:-1] |
|
|
|
if line != '.' and line != '': |
|
text, path, server, port = line.split('\t') |
|
port = int(port) |
|
selector, text = text[0], text[1:] |
|
if selector == 'i' or selector == '3': |
|
conn.sendall('\t\t\t%s<br/>\n' % text) |
|
else: |
|
if len(path) >= 4 and path[:4] == 'URL:': |
|
conn.sendall('\t\t\t<a href="%s">%s</a><br/>\n' % (path[4:], text)) |
|
else: |
|
conn.sendall('\t\t\t<a href="http://%s:%s/%s%s">%s</a><br/>\n' % (server, port, selector, path, text)) |
|
|
|
conn.sendall('\t\t</p>\n' |
|
'\t</body>\n' |
|
'</html>') |
|
|
|
def servecgi(conn, path, servefunc = servecommon): |
|
proc = subprocess.Popen([path], stdout=subprocess.PIPE) |
|
servefunc(conn, proc.stdout) |
|
|
|
def servefile(conn, path, servefunc = servecommon): |
|
f = open(path, 'r') |
|
servefunc(conn, f) |
|
|
|
def serverequest(conn, request, ishttp): |
|
# Extract selector if needed |
|
if ishttp: |
|
request, selector = getselector(request) |
|
else: |
|
selector = None |
|
|
|
# URL link extension |
|
if len(request[0]) >= 4 and request[0][:4] == 'URL:': |
|
return serveurlredirect(conn, request[0]) |
|
|
|
reqpath = normalizepath(request[0]) |
|
|
|
if reqpath == None: |
|
return notallowederror(conn, reqpath, ishttp) |
|
|
|
path = gopherroot + '/' + reqpath |
|
|
|
if not exist(path): |
|
return notfounderror(conn, reqpath, ishttp) |
|
|
|
if isdir(path): |
|
if exist(path + '/gophermap'): |
|
path = path + '/gophermap' |
|
else: |
|
return notfounderror(conn, reqpath, ishttp) |
|
|
|
sendheader(conn, ishttp, selector) |
|
|
|
if ishttp and selector == '1': |
|
servefunc = servehtmlgophermap |
|
else: |
|
servefunc = servecommon |
|
|
|
if isexecutable(path): |
|
servecgi(conn, path, servefunc) |
|
else: |
|
servefile(conn, path, servefunc) |
|
|
|
class Serve(threading.Thread): |
|
def __init__(self, conn): |
|
self.conn = conn |
|
threading.Thread.__init__(self) |
|
def run(self): |
|
try: |
|
(request, ishttp) = getrequest(self.conn) |
|
|
|
if not request: |
|
self.conn.shutdown(socket.SHUT_RDWR) |
|
self.conn.close() |
|
return |
|
|
|
serverequest(self.conn, request, ishttp) |
|
|
|
self.conn.shutdown(socket.SHUT_RDWR) |
|
self.conn.close() |
|
except socket.error: |
|
self.conn.close() |
|
|
|
def toint(addr): |
|
a1, a2, a3, a4 = [int(i) for i in addr.split('.')] |
|
return a1<<24 | a2<<16 | a3<<8 | a4 |
|
|
|
try: |
|
f = open(blacklistfile, 'r') |
|
except IOError: |
|
blacklist = [] |
|
else: |
|
blacklist = [] |
|
for line in f: |
|
if len(line) > 0 and line[-1] == '\n': |
|
line = line[:-1] |
|
|
|
line = line.split('/') |
|
if len(line) == 1: |
|
addr = toint(line[0]) |
|
upto = 32 |
|
elif len(line) == 2: |
|
addr = toint(line[0]) |
|
upto = int(line[1]) |
|
else: |
|
assert(not 'Invalid line format') |
|
|
|
blacklist.append((addr, upto)) |
|
|
|
f.close() |
|
|
|
def matchaddr(addr, blacklist_entry): |
|
blacklist_addr, upto = blacklist_entry |
|
shift = 32 - upto |
|
return addr >> shift == blacklist_addr >> shift |
|
|
|
while True: |
|
conn, addr = sock.accept() |
|
ip, port = addr |
|
if not any(map(lambda x: matchaddr(toint(ip), x), blacklist)): |
|
Serve(conn).start() |
|
else: |
|
print '%s: Blacklisted IP %s' % (time.strftime('%Y-%m-%d %H:%M'), ip) |
|
conn.close()
|
|
|