gophersrv-backup/gophersrv.py

315 lines
8.4 KiB
Python

#!/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()