|
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() |