1 #! /usr/bin/env python 2 3 # A simple gopher client. 4 # 5 # Usage: gopher [ [selector] host [port] ] 6 7 import string 8 import sys 9 import os 10 import socket 11 12 # Default selector, host and port 13 DEF_SELECTOR = '' 14 DEF_HOST = 'gopher.micro.umn.edu' 15 DEF_PORT = 70 16 17 # Recognized file types 18 T_TEXTFILE = '0' 19 T_MENU = '1' 20 T_CSO = '2' 21 T_ERROR = '3' 22 T_BINHEX = '4' 23 T_DOS = '5' 24 T_UUENCODE = '6' 25 T_SEARCH = '7' 26 T_TELNET = '8' 27 T_BINARY = '9' 28 T_REDUNDANT = '+' 29 T_SOUND = 's' 30 31 # Dictionary mapping types to strings 32 typename = {'0': '<TEXT>', '1': '<DIR>', '2': '<CSO>', '3': '<ERROR>', \ 33 '4': '<BINHEX>', '5': '<DOS>', '6': '<UUENCODE>', '7': '<SEARCH>', \ 34 '8': '<TELNET>', '9': '<BINARY>', '+': '<REDUNDANT>', 's': '<SOUND>'} 35 36 # Oft-used characters and strings 37 CRLF = '\r\n' 38 TAB = '\t' 39 40 # Open a TCP connection to a given host and port 41 def open_socket(host, port): 42 if not port: 43 port = DEF_PORT 44 elif type(port) == type(''): 45 port = string.atoi(port) 46 s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 47 s.connect((host, port)) 48 return s 49 50 # Send a selector to a given host and port, return a file with the reply 51 def send_request(selector, host, port): 52 s = open_socket(host, port) 53 s.send(selector + CRLF) 54 s.shutdown(1) 55 return s.makefile('r') 56 57 # Get a menu in the form of a list of entries 58 def get_menu(selector, host, port): 59 f = send_request(selector, host, port) 60 list = [] 61 while 1: 62 line = f.readline() 63 if not line: 64 print '(Unexpected EOF from server)' 65 break 66 if line[-2:] == CRLF: 67 line = line[:-2] 68 elif line[-1:] in CRLF: 69 line = line[:-1] 70 if line == '.': 71 break 72 if not line: 73 print '(Empty line from server)' 74 continue 75 typechar = line[0] 76 parts = string.splitfields(line[1:], TAB) 77 if len(parts) < 4: 78 print '(Bad line from server: %r)' % (line,) 79 continue 80 if len(parts) > 4: 81 print '(Extra info from server: %r)' % (parts[4:],) 82 parts.insert(0, typechar) 83 list.append(parts) 84 f.close() 85 return list 86 87 # Get a text file as a list of lines, with trailing CRLF stripped 88 def get_textfile(selector, host, port): 89 list = [] 90 get_alt_textfile(selector, host, port, list.append) 91 return list 92 93 # Get a text file and pass each line to a function, with trailing CRLF stripped 94 def get_alt_textfile(selector, host, port, func): 95 f = send_request(selector, host, port) 96 while 1: 97 line = f.readline() 98 if not line: 99 print '(Unexpected EOF from server)' 100 break 101 if line[-2:] == CRLF: 102 line = line[:-2] 103 elif line[-1:] in CRLF: 104 line = line[:-1] 105 if line == '.': 106 break 107 if line[:2] == '..': 108 line = line[1:] 109 func(line) 110 f.close() 111 112 # Get a binary file as one solid data block 113 def get_binary(selector, host, port): 114 f = send_request(selector, host, port) 115 data = f.read() 116 f.close() 117 return data 118 119 # Get a binary file and pass each block to a function 120 def get_alt_binary(selector, host, port, func, blocksize): 121 f = send_request(selector, host, port) 122 while 1: 123 data = f.read(blocksize) 124 if not data: 125 break 126 func(data) 127 128 # A *very* simple interactive browser 129 130 # Browser main command, has default arguments 131 def browser(*args): 132 selector = DEF_SELECTOR 133 host = DEF_HOST 134 port = DEF_PORT 135 n = len(args) 136 if n > 0 and args[0]: 137 selector = args[0] 138 if n > 1 and args[1]: 139 host = args[1] 140 if n > 2 and args[2]: 141 port = args[2] 142 if n > 3: 143 raise RuntimeError, 'too many args' 144 try: 145 browse_menu(selector, host, port) 146 except socket.error, msg: 147 print 'Socket error:', msg 148 sys.exit(1) 149 except KeyboardInterrupt: 150 print '\n[Goodbye]' 151 152 # Browse a menu 153 def browse_menu(selector, host, port): 154 list = get_menu(selector, host, port) 155 while 1: 156 print '----- MENU -----' 157 print 'Selector:', repr(selector) 158 print 'Host:', host, ' Port:', port 159 print 160 for i in range(len(list)): 161 item = list[i] 162 typechar, description = item[0], item[1] 163 print string.rjust(repr(i+1), 3) + ':', description, 164 if typename.has_key(typechar): 165 print typename[typechar] 166 else: 167 print '<TYPE=' + repr(typechar) + '>' 168 print 169 while 1: 170 try: 171 str = raw_input('Choice [CR == up a level]: ') 172 except EOFError: 173 print 174 return 175 if not str: 176 return 177 try: 178 choice = string.atoi(str) 179 except string.atoi_error: 180 print 'Choice must be a number; try again:' 181 continue 182 if not 0 < choice <= len(list): 183 print 'Choice out of range; try again:' 184 continue 185 break 186 item = list[choice-1] 187 typechar = item[0] 188 [i_selector, i_host, i_port] = item[2:5] 189 if typebrowser.has_key(typechar): 190 browserfunc = typebrowser[typechar] 191 try: 192 browserfunc(i_selector, i_host, i_port) 193 except (IOError, socket.error): 194 print '***', sys.exc_type, ':', sys.exc_value 195 else: 196 print 'Unsupported object type' 197 198 # Browse a text file 199 def browse_textfile(selector, host, port): 200 x = None 201 try: 202 p = os.popen('${PAGER-more}', 'w') 203 x = SaveLines(p) 204 get_alt_textfile(selector, host, port, x.writeln) 205 except IOError, msg: 206 print 'IOError:', msg 207 if x: 208 x.close() 209 f = open_savefile() 210 if not f: 211 return 212 x = SaveLines(f) 213 try: 214 get_alt_textfile(selector, host, port, x.writeln) 215 print 'Done.' 216 except IOError, msg: 217 print 'IOError:', msg 218 x.close() 219 220 # Browse a search index 221 def browse_search(selector, host, port): 222 while 1: 223 print '----- SEARCH -----' 224 print 'Selector:', repr(selector) 225 print 'Host:', host, ' Port:', port 226 print 227 try: 228 query = raw_input('Query [CR == up a level]: ') 229 except EOFError: 230 print 231 break 232 query = string.strip(query) 233 if not query: 234 break 235 if '\t' in query: 236 print 'Sorry, queries cannot contain tabs' 237 continue 238 browse_menu(selector + TAB + query, host, port) 239 240 # "Browse" telnet-based information, i.e. open a telnet session 241 def browse_telnet(selector, host, port): 242 if selector: 243 print 'Log in as', repr(selector) 244 if type(port) <> type(''): 245 port = repr(port) 246 sts = os.system('set -x; exec telnet ' + host + ' ' + port) 247 if sts: 248 print 'Exit status:', sts 249 250 # "Browse" a binary file, i.e. save it to a file 251 def browse_binary(selector, host, port): 252 f = open_savefile() 253 if not f: 254 return 255 x = SaveWithProgress(f) 256 get_alt_binary(selector, host, port, x.write, 8*1024) 257 x.close() 258 259 # "Browse" a sound file, i.e. play it or save it 260 def browse_sound(selector, host, port): 261 browse_binary(selector, host, port) 262 263 # Dictionary mapping types to browser functions 264 typebrowser = {'0': browse_textfile, '1': browse_menu, \ 265 '4': browse_binary, '5': browse_binary, '6': browse_textfile, \ 266 '7': browse_search, \ 267 '8': browse_telnet, '9': browse_binary, 's': browse_sound} 268 269 # Class used to save lines, appending a newline to each line 270 class SaveLines: 271 def __init__(self, f): 272 self.f = f 273 def writeln(self, line): 274 self.f.write(line + '\n') 275 def close(self): 276 sts = self.f.close() 277 if sts: 278 print 'Exit status:', sts 279 280 # Class used to save data while showing progress 281 class SaveWithProgress: 282 def __init__(self, f): 283 self.f = f 284 def write(self, data): 285 sys.stdout.write('#') 286 sys.stdout.flush() 287 self.f.write(data) 288 def close(self): 289 print 290 sts = self.f.close() 291 if sts: 292 print 'Exit status:', sts 293 294 # Ask for and open a save file, or return None if not to save 295 def open_savefile(): 296 try: 297 savefile = raw_input( \ 298 'Save as file [CR == don\'t save; |pipeline or ~user/... OK]: ') 299 except EOFError: 300 print 301 return None 302 savefile = string.strip(savefile) 303 if not savefile: 304 return None 305 if savefile[0] == '|': 306 cmd = string.strip(savefile[1:]) 307 try: 308 p = os.popen(cmd, 'w') 309 except IOError, msg: 310 print repr(cmd), ':', msg 311 return None 312 print 'Piping through', repr(cmd), '...' 313 return p 314 if savefile[0] == '~': 315 savefile = os.path.expanduser(savefile) 316 try: 317 f = open(savefile, 'w') 318 except IOError, msg: 319 print repr(savefile), ':', msg 320 return None 321 print 'Saving to', repr(savefile), '...' 322 return f 323 324 # Test program 325 def test(): 326 if sys.argv[4:]: 327 print 'usage: gopher [ [selector] host [port] ]' 328 sys.exit(2) 329 elif sys.argv[3:]: 330 browser(sys.argv[1], sys.argv[2], sys.argv[3]) 331 elif sys.argv[2:]: 332 try: 333 port = string.atoi(sys.argv[2]) 334 selector = '' 335 host = sys.argv[1] 336 except string.atoi_error: 337 selector = sys.argv[1] 338 host = sys.argv[2] 339 port = '' 340 browser(selector, host, port) 341 elif sys.argv[1:]: 342 browser('', sys.argv[1]) 343 else: 344 browser() 345 346 # Call the test program as a main program 347 test() 348