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