| #! /usr/bin/env python |
| |
| # A simple gopher client. |
| # |
| # Usage: gopher [ [selector] host [port] ] |
| |
| 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 = int(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 = line[1:].split(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 as 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 repr(i+1).rjust(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 = int(str) |
| except ValueError: |
| 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): |
| t, v, tb = sys.exc_info() |
| print '***', t, ':', v |
| 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 as 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 as msg: |
| print 'IOError:', msg |
| x.close() |
| |
| def raw_input(prompt): |
| sys.stdout.write(prompt) |
| sys.stdout.flush() |
| return sys.stdin.readline() |
| |
| # 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 = query.strip() |
| 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 = savefile.strip() |
| if not savefile: |
| return None |
| if savefile[0] == '|': |
| cmd = savefile[1:].strip() |
| try: |
| p = os.popen(cmd, 'w') |
| except IOError as 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 as 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 = int(sys.argv[2]) |
| selector = '' |
| host = sys.argv[1] |
| except ValueError: |
| 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() |