blob: a2ab3a2f6c495230c577a63f77f8a6f279d569a9 [file] [log] [blame]
Guido van Rossumf06ee5f1996-11-27 19:52:01 +00001#! /usr/bin/env python
Guido van Rossum6b8d6991992-10-25 19:18:11 +00002
3# A simple gopher client.
4#
5# Usage: gopher [ [selector] host [port] ]
6
7import string
8import sys
9import os
10import socket
11
12# Default selector, host and port
13DEF_SELECTOR = ''
14DEF_HOST = 'gopher.micro.umn.edu'
15DEF_PORT = 70
16
17# Recognized file types
18T_TEXTFILE = '0'
19T_MENU = '1'
20T_CSO = '2'
21T_ERROR = '3'
22T_BINHEX = '4'
23T_DOS = '5'
24T_UUENCODE = '6'
25T_SEARCH = '7'
26T_TELNET = '8'
27T_BINARY = '9'
28T_REDUNDANT = '+'
29T_SOUND = 's'
30
31# Dictionary mapping types to strings
32typename = {'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
37CRLF = '\r\n'
38TAB = '\t'
39
40# Open a TCP connection to a given host and port
41def open_socket(host, port):
Guido van Rossumd3b0f381992-11-16 16:55:48 +000042 if not port:
43 port = DEF_PORT
44 elif type(port) == type(''):
Guido van Rossum6b8d6991992-10-25 19:18:11 +000045 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
51def 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
58def 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:', `line`, ')'
79 continue
80 if len(parts) > 4:
81 print '(Extra info from server:', 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
88def 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
94def 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
113def 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
120def 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
131def 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
153def browse_menu(selector, host, port):
154 list = get_menu(selector, host, port)
155 while 1:
156 print '----- MENU -----'
157 print 'Selector:', `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(`i+1`, 3) + ':', description,
164 if typename.has_key(typechar):
165 print typename[typechar]
166 else:
167 print '<TYPE=' + `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
199def browse_textfile(selector, host, port):
200 x = None
201 try:
202 p = os.popen('${PAGER-more}', 'w')
Guido van Rossumd55f4d11993-12-17 14:39:12 +0000203 x = SaveLines(p)
Guido van Rossum6b8d6991992-10-25 19:18:11 +0000204 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
Guido van Rossumd55f4d11993-12-17 14:39:12 +0000212 x = SaveLines(f)
Guido van Rossum6b8d6991992-10-25 19:18:11 +0000213 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
221def browse_search(selector, host, port):
222 while 1:
223 print '----- SEARCH -----'
224 print 'Selector:', `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
241def browse_telnet(selector, host, port):
242 if selector:
243 print 'Log in as', `selector`
244 if type(port) <> type(''):
245 port = `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
251def browse_binary(selector, host, port):
252 f = open_savefile()
253 if not f:
254 return
Guido van Rossumd55f4d11993-12-17 14:39:12 +0000255 x = SaveWithProgress(f)
Guido van Rossum6b8d6991992-10-25 19:18:11 +0000256 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
260def browse_sound(selector, host, port):
261 browse_binary(selector, host, port)
262
263# Dictionary mapping types to browser functions
264typebrowser = {'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
270class SaveLines:
Guido van Rossumd55f4d11993-12-17 14:39:12 +0000271 def __init__(self, f):
Guido van Rossum6b8d6991992-10-25 19:18:11 +0000272 self.f = f
Guido van Rossum6b8d6991992-10-25 19:18:11 +0000273 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
281class SaveWithProgress:
Guido van Rossumd55f4d11993-12-17 14:39:12 +0000282 def __init__(self, f):
Guido van Rossum6b8d6991992-10-25 19:18:11 +0000283 self.f = f
Guido van Rossum6b8d6991992-10-25 19:18:11 +0000284 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
295def 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 `cmd`, ':', msg
311 return None
312 print 'Piping through', `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 `savefile`, ':', msg
320 return None
321 print 'Saving to', `savefile`, '...'
322 return f
323
324# Test program
325def 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
347test()