|
#! /usr/bin/env python |
|
|
|
# A simple gopher client. |
|
# |
|
# Usage: gopher [ [selector] host [port] ] |
|
|
|
import string |
|
import sys |
|
import os |
|
import socket |
|
|
|
# Default selector, host and port |
|
DEF_SELECTOR = '' |
|
DEF_HOST = 'gopher.micro.umn.edu' |
|
DEF_PORT = 70 |
|
|
|
# Recognized file types |
|
T_TEXTFILE = '0' |
|
T_MENU = '1' |
|
T_CSO = '2' |
|
T_ERROR = '3' |
|
T_BINHEX = '4' |
|
T_DOS = '5' |
|
T_UUENCODE = '6' |
|
T_SEARCH = '7' |
|
T_TELNET = '8' |
|
T_BINARY = '9' |
|
T_REDUNDANT = '+' |
|
T_SOUND = 's' |
|
|
|
# Dictionary mapping types to strings |
|
typename = {'0': '<TEXT>', '1': '<DIR>', '2': '<CSO>', '3': '<ERROR>', \ |
|
'4': '<BINHEX>', '5': '<DOS>', '6': '<UUENCODE>', '7': '<SEARCH>', \ |
|
'8': '<TELNET>', '9': '<BINARY>', '+': '<REDUNDANT>', 's': '<SOUND>'} |
|
|
|
# Oft-used characters and strings |
|
CRLF = '\r\n' |
|
TAB = '\t' |
|
|
|
# Open a TCP connection to a given host and port |
|
def open_socket(host, port): |
|
if not port: |
|
port = DEF_PORT |
|
elif type(port) == type(''): |
|
port = string.atoi(port) |
|
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) |
|
s.connect((host, port)) |
|
return s |
|
|
|
# Send a selector to a given host and port, return a file with the reply |
|
def send_request(selector, host, port): |
|
s = open_socket(host, port) |
|
s.send(selector + CRLF) |
|
s.shutdown(1) |
|
return s.makefile('r') |
|
|
|
# Get a menu in the form of a list of entries |
|
def get_menu(selector, host, port): |
|
f = send_request(selector, host, port) |
|
list = [] |
|
while 1: |
|
line = f.readline() |
|
if not line: |
|
print '(Unexpected EOF from server)' |
|
break |
|
if line[-2:] == CRLF: |
|
line = line[:-2] |
|
elif line[-1:] in CRLF: |
|
line = line[:-1] |
|
if line == '.': |
|
break |
|
if not line: |
|
print '(Empty line from server)' |
|
continue |
|
typechar = line[0] |
|
parts = string.splitfields(line[1:], TAB) |
|
if len(parts) < 4: |
|
print '(Bad line from server: %r)' % (line,) |
|
continue |
|
if len(parts) > 4: |
|
print '(Extra info from server: %r)' % (parts[4:],) |
|
parts.insert(0, typechar) |
|
list.append(parts) |
|
f.close() |
|
return list |
|
|
|
# Get a text file as a list of lines, with trailing CRLF stripped |
|
def get_textfile(selector, host, port): |
|
list = [] |
|
get_alt_textfile(selector, host, port, list.append) |
|
return list |
|
|
|
# Get a text file and pass each line to a function, with trailing CRLF stripped |
|
def get_alt_textfile(selector, host, port, func): |
|
f = send_request(selector, host, port) |
|
while 1: |
|
line = f.readline() |
|
if not line: |
|
print '(Unexpected EOF from server)' |
|
break |
|
if line[-2:] == CRLF: |
|
line = line[:-2] |
|
elif line[-1:] in CRLF: |
|
line = line[:-1] |
|
if line == '.': |
|
break |
|
if line[:2] == '..': |
|
line = line[1:] |
|
func(line) |
|
f.close() |
|
|
|
# Get a binary file as one solid data block |
|
def get_binary(selector, host, port): |
|
f = send_request(selector, host, port) |
|
data = f.read() |
|
f.close() |
|
return data |
|
|
|
# Get a binary file and pass each block to a function |
|
def get_alt_binary(selector, host, port, func, blocksize): |
|
f = send_request(selector, host, port) |
|
while 1: |
|
data = f.read(blocksize) |
|
if not data: |
|
break |
|
func(data) |
|
|
|
# A *very* simple interactive browser |
|
|
|
# Browser main command, has default arguments |
|
def browser(*args): |
|
selector = DEF_SELECTOR |
|
host = DEF_HOST |
|
port = DEF_PORT |
|
n = len(args) |
|
if n > 0 and args[0]: |
|
selector = args[0] |
|
if n > 1 and args[1]: |
|
host = args[1] |
|
if n > 2 and args[2]: |
|
port = args[2] |
|
if n > 3: |
|
raise RuntimeError, 'too many args' |
|
try: |
|
browse_menu(selector, host, port) |
|
except socket.error, msg: |
|
print 'Socket error:', msg |
|
sys.exit(1) |
|
except KeyboardInterrupt: |
|
print '\n[Goodbye]' |
|
|
|
# Browse a menu |
|
def browse_menu(selector, host, port): |
|
list = get_menu(selector, host, port) |
|
while 1: |
|
print '----- MENU -----' |
|
print 'Selector:', repr(selector) |
|
print 'Host:', host, ' Port:', port |
|
print |
|
for i in range(len(list)): |
|
item = list[i] |
|
typechar, description = item[0], item[1] |
|
print string.rjust(repr(i+1), 3) + ':', description, |
|
if typename.has_key(typechar): |
|
print typename[typechar] |
|
else: |
|
print '<TYPE=' + repr(typechar) + '>' |
|
print |
|
while 1: |
|
try: |
|
str = raw_input('Choice [CR == up a level]: ') |
|
except EOFError: |
|
print |
|
return |
|
if not str: |
|
return |
|
try: |
|
choice = string.atoi(str) |
|
except string.atoi_error: |
|
print 'Choice must be a number; try again:' |
|
continue |
|
if not 0 < choice <= len(list): |
|
print 'Choice out of range; try again:' |
|
continue |
|
break |
|
item = list[choice-1] |
|
typechar = item[0] |
|
[i_selector, i_host, i_port] = item[2:5] |
|
if typebrowser.has_key(typechar): |
|
browserfunc = typebrowser[typechar] |
|
try: |
|
browserfunc(i_selector, i_host, i_port) |
|
except (IOError, socket.error): |
|
print '***', sys.exc_type, ':', sys.exc_value |
|
else: |
|
print 'Unsupported object type' |
|
|
|
# Browse a text file |
|
def browse_textfile(selector, host, port): |
|
x = None |
|
try: |
|
p = os.popen('${PAGER-more}', 'w') |
|
x = SaveLines(p) |
|
get_alt_textfile(selector, host, port, x.writeln) |
|
except IOError, msg: |
|
print 'IOError:', msg |
|
if x: |
|
x.close() |
|
f = open_savefile() |
|
if not f: |
|
return |
|
x = SaveLines(f) |
|
try: |
|
get_alt_textfile(selector, host, port, x.writeln) |
|
print 'Done.' |
|
except IOError, msg: |
|
print 'IOError:', msg |
|
x.close() |
|
|
|
# Browse a search index |
|
def browse_search(selector, host, port): |
|
while 1: |
|
print '----- SEARCH -----' |
|
print 'Selector:', repr(selector) |
|
print 'Host:', host, ' Port:', port |
|
print |
|
try: |
|
query = raw_input('Query [CR == up a level]: ') |
|
except EOFError: |
|
print |
|
break |
|
query = string.strip(query) |
|
if not query: |
|
break |
|
if '\t' in query: |
|
print 'Sorry, queries cannot contain tabs' |
|
continue |
|
browse_menu(selector + TAB + query, host, port) |
|
|
|
# "Browse" telnet-based information, i.e. open a telnet session |
|
def browse_telnet(selector, host, port): |
|
if selector: |
|
print 'Log in as', repr(selector) |
|
if type(port) <> type(''): |
|
port = repr(port) |
|
sts = os.system('set -x; exec telnet ' + host + ' ' + port) |
|
if sts: |
|
print 'Exit status:', sts |
|
|
|
# "Browse" a binary file, i.e. save it to a file |
|
def browse_binary(selector, host, port): |
|
f = open_savefile() |
|
if not f: |
|
return |
|
x = SaveWithProgress(f) |
|
get_alt_binary(selector, host, port, x.write, 8*1024) |
|
x.close() |
|
|
|
# "Browse" a sound file, i.e. play it or save it |
|
def browse_sound(selector, host, port): |
|
browse_binary(selector, host, port) |
|
|
|
# Dictionary mapping types to browser functions |
|
typebrowser = {'0': browse_textfile, '1': browse_menu, \ |
|
'4': browse_binary, '5': browse_binary, '6': browse_textfile, \ |
|
'7': browse_search, \ |
|
'8': browse_telnet, '9': browse_binary, 's': browse_sound} |
|
|
|
# Class used to save lines, appending a newline to each line |
|
class SaveLines: |
|
def __init__(self, f): |
|
self.f = f |
|
def writeln(self, line): |
|
self.f.write(line + '\n') |
|
def close(self): |
|
sts = self.f.close() |
|
if sts: |
|
print 'Exit status:', sts |
|
|
|
# Class used to save data while showing progress |
|
class SaveWithProgress: |
|
def __init__(self, f): |
|
self.f = f |
|
def write(self, data): |
|
sys.stdout.write('#') |
|
sys.stdout.flush() |
|
self.f.write(data) |
|
def close(self): |
|
print |
|
sts = self.f.close() |
|
if sts: |
|
print 'Exit status:', sts |
|
|
|
# Ask for and open a save file, or return None if not to save |
|
def open_savefile(): |
|
try: |
|
savefile = raw_input( \ |
|
'Save as file [CR == don\'t save; |pipeline or ~user/... OK]: ') |
|
except EOFError: |
|
print |
|
return None |
|
savefile = string.strip(savefile) |
|
if not savefile: |
|
return None |
|
if savefile[0] == '|': |
|
cmd = string.strip(savefile[1:]) |
|
try: |
|
p = os.popen(cmd, 'w') |
|
except IOError, msg: |
|
print repr(cmd), ':', msg |
|
return None |
|
print 'Piping through', repr(cmd), '...' |
|
return p |
|
if savefile[0] == '~': |
|
savefile = os.path.expanduser(savefile) |
|
try: |
|
f = open(savefile, 'w') |
|
except IOError, msg: |
|
print repr(savefile), ':', msg |
|
return None |
|
print 'Saving to', repr(savefile), '...' |
|
return f |
|
|
|
# Test program |
|
def test(): |
|
if sys.argv[4:]: |
|
print 'usage: gopher [ [selector] host [port] ]' |
|
sys.exit(2) |
|
elif sys.argv[3:]: |
|
browser(sys.argv[1], sys.argv[2], sys.argv[3]) |
|
elif sys.argv[2:]: |
|
try: |
|
port = string.atoi(sys.argv[2]) |
|
selector = '' |
|
host = sys.argv[1] |
|
except string.atoi_error: |
|
selector = sys.argv[1] |
|
host = sys.argv[2] |
|
port = '' |
|
browser(selector, host, port) |
|
elif sys.argv[1:]: |
|
browser('', sys.argv[1]) |
|
else: |
|
browser() |
|
|
|
# Call the test program as a main program |
|
test() |