blob: cd76659365a3fcc0ce6cd3d5b307353cead4e150 [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>', \
Tim Peterse6ddc8b2004-07-18 05:56:09 +000033 '4': '<BINHEX>', '5': '<DOS>', '6': '<UUENCODE>', '7': '<SEARCH>', \
34 '8': '<TELNET>', '9': '<BINARY>', '+': '<REDUNDANT>', 's': '<SOUND>'}
Guido van Rossum6b8d6991992-10-25 19:18:11 +000035
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):
Tim Peterse6ddc8b2004-07-18 05:56:09 +000042 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
Guido van Rossum6b8d6991992-10-25 19:18:11 +000049
50# Send a selector to a given host and port, return a file with the reply
51def send_request(selector, host, port):
Tim Peterse6ddc8b2004-07-18 05:56:09 +000052 s = open_socket(host, port)
53 s.send(selector + CRLF)
54 s.shutdown(1)
55 return s.makefile('r')
Guido van Rossum6b8d6991992-10-25 19:18:11 +000056
57# Get a menu in the form of a list of entries
58def get_menu(selector, host, port):
Tim Peterse6ddc8b2004-07-18 05:56:09 +000059 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
Guido van Rossum6b8d6991992-10-25 19:18:11 +000086
87# Get a text file as a list of lines, with trailing CRLF stripped
88def get_textfile(selector, host, port):
Tim Peterse6ddc8b2004-07-18 05:56:09 +000089 list = []
90 get_alt_textfile(selector, host, port, list.append)
91 return list
Guido van Rossum6b8d6991992-10-25 19:18:11 +000092
93# Get a text file and pass each line to a function, with trailing CRLF stripped
94def get_alt_textfile(selector, host, port, func):
Tim Peterse6ddc8b2004-07-18 05:56:09 +000095 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()
Guido van Rossum6b8d6991992-10-25 19:18:11 +0000111
112# Get a binary file as one solid data block
113def get_binary(selector, host, port):
Tim Peterse6ddc8b2004-07-18 05:56:09 +0000114 f = send_request(selector, host, port)
115 data = f.read()
116 f.close()
117 return data
Guido van Rossum6b8d6991992-10-25 19:18:11 +0000118
119# Get a binary file and pass each block to a function
120def get_alt_binary(selector, host, port, func, blocksize):
Tim Peterse6ddc8b2004-07-18 05:56:09 +0000121 f = send_request(selector, host, port)
122 while 1:
123 data = f.read(blocksize)
124 if not data:
125 break
126 func(data)
Guido van Rossum6b8d6991992-10-25 19:18:11 +0000127
128# A *very* simple interactive browser
129
130# Browser main command, has default arguments
131def browser(*args):
Tim Peterse6ddc8b2004-07-18 05:56:09 +0000132 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]'
Guido van Rossum6b8d6991992-10-25 19:18:11 +0000151
152# Browse a menu
153def browse_menu(selector, host, port):
Tim Peterse6ddc8b2004-07-18 05:56:09 +0000154 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'
Guido van Rossum6b8d6991992-10-25 19:18:11 +0000197
198# Browse a text file
199def browse_textfile(selector, host, port):
Tim Peterse6ddc8b2004-07-18 05:56:09 +0000200 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()
Guido van Rossum6b8d6991992-10-25 19:18:11 +0000219
220# Browse a search index
221def browse_search(selector, host, port):
Tim Peterse6ddc8b2004-07-18 05:56:09 +0000222 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)
Guido van Rossum6b8d6991992-10-25 19:18:11 +0000239
240# "Browse" telnet-based information, i.e. open a telnet session
241def browse_telnet(selector, host, port):
Tim Peterse6ddc8b2004-07-18 05:56:09 +0000242 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
Guido van Rossum6b8d6991992-10-25 19:18:11 +0000249
250# "Browse" a binary file, i.e. save it to a file
251def browse_binary(selector, host, port):
Tim Peterse6ddc8b2004-07-18 05:56:09 +0000252 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()
Guido van Rossum6b8d6991992-10-25 19:18:11 +0000258
259# "Browse" a sound file, i.e. play it or save it
260def browse_sound(selector, host, port):
Tim Peterse6ddc8b2004-07-18 05:56:09 +0000261 browse_binary(selector, host, port)
Guido van Rossum6b8d6991992-10-25 19:18:11 +0000262
263# Dictionary mapping types to browser functions
264typebrowser = {'0': browse_textfile, '1': browse_menu, \
Tim Peterse6ddc8b2004-07-18 05:56:09 +0000265 '4': browse_binary, '5': browse_binary, '6': browse_textfile, \
266 '7': browse_search, \
267 '8': browse_telnet, '9': browse_binary, 's': browse_sound}
Guido van Rossum6b8d6991992-10-25 19:18:11 +0000268
269# Class used to save lines, appending a newline to each line
270class SaveLines:
Tim Peterse6ddc8b2004-07-18 05:56:09 +0000271 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
Guido van Rossum6b8d6991992-10-25 19:18:11 +0000279
280# Class used to save data while showing progress
281class SaveWithProgress:
Tim Peterse6ddc8b2004-07-18 05:56:09 +0000282 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
Guido van Rossum6b8d6991992-10-25 19:18:11 +0000293
294# Ask for and open a save file, or return None if not to save
295def open_savefile():
Tim Peterse6ddc8b2004-07-18 05:56:09 +0000296 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
Guido van Rossum6b8d6991992-10-25 19:18:11 +0000323
324# Test program
325def test():
Tim Peterse6ddc8b2004-07-18 05:56:09 +0000326 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()
Guido van Rossum6b8d6991992-10-25 19:18:11 +0000345
346# Call the test program as a main program
347test()