blob: d7c5c93814005ee12207b2657dab72732b9f98f4 [file] [log] [blame]
Benjamin Peterson90f5ba52010-03-11 22:53:45 +00001#! /usr/bin/env python3
Barry Warsaw406d46e2001-08-13 21:18:01 +00002"""An RFC 2821 smtp proxy.
Barry Warsaw7e0d9562001-01-31 22:51:35 +00003
Barry Warsaw0e8427e2001-10-04 16:27:04 +00004Usage: %(program)s [options] [localhost:localport [remotehost:remoteport]]
Barry Warsaw7e0d9562001-01-31 22:51:35 +00005
6Options:
7
8 --nosetuid
9 -n
10 This program generally tries to setuid `nobody', unless this flag is
11 set. The setuid call will fail if this program is not run as root (in
12 which case, use this flag).
13
14 --version
15 -V
16 Print the version number and exit.
17
18 --class classname
19 -c classname
Barry Warsawf267b622004-10-09 21:44:13 +000020 Use `classname' as the concrete SMTP proxy class. Uses `PureProxy' by
Barry Warsaw7e0d9562001-01-31 22:51:35 +000021 default.
22
23 --debug
24 -d
25 Turn on debugging prints.
26
27 --help
28 -h
29 Print this message and exit.
30
31Version: %(__version__)s
32
Barry Warsaw0e8427e2001-10-04 16:27:04 +000033If localhost is not given then `localhost' is used, and if localport is not
34given then 8025 is used. If remotehost is not given then `localhost' is used,
35and if remoteport is not given, then 25 is used.
Barry Warsaw7e0d9562001-01-31 22:51:35 +000036"""
37
Barry Warsaw0e8427e2001-10-04 16:27:04 +000038
Barry Warsaw7e0d9562001-01-31 22:51:35 +000039# Overview:
40#
41# This file implements the minimal SMTP protocol as defined in RFC 821. It
42# has a hierarchy of classes which implement the backend functionality for the
43# smtpd. A number of classes are provided:
44#
Guido van Rossumb8b45ea2001-04-15 13:06:04 +000045# SMTPServer - the base class for the backend. Raises NotImplementedError
Barry Warsaw7e0d9562001-01-31 22:51:35 +000046# if you try to use it.
47#
48# DebuggingServer - simply prints each message it receives on stdout.
49#
50# PureProxy - Proxies all messages to a real smtpd which does final
51# delivery. One known problem with this class is that it doesn't handle
52# SMTP errors from the backend server at all. This should be fixed
53# (contributions are welcome!).
54#
55# MailmanProxy - An experimental hack to work with GNU Mailman
56# <www.list.org>. Using this server as your real incoming smtpd, your
57# mailhost will automatically recognize and accept mail destined to Mailman
58# lists when those lists are created. Every message not destined for a list
59# gets forwarded to a real backend smtpd, as with PureProxy. Again, errors
60# are not handled correctly yet.
61#
62# Please note that this script requires Python 2.0
63#
Barry Warsawb1027642004-07-12 23:10:08 +000064# Author: Barry Warsaw <barry@python.org>
Barry Warsaw7e0d9562001-01-31 22:51:35 +000065#
66# TODO:
67#
68# - support mailbox delivery
69# - alias files
70# - ESMTP
71# - handle error codes from the backend smtpd
72
73import sys
74import os
75import errno
76import getopt
77import time
78import socket
79import asyncore
80import asynchat
81
Skip Montanaro0de65802001-02-15 22:15:14 +000082__all__ = ["SMTPServer","DebuggingServer","PureProxy","MailmanProxy"]
Barry Warsaw7e0d9562001-01-31 22:51:35 +000083
84program = sys.argv[0]
85__version__ = 'Python SMTP proxy version 0.2'
86
87
88class Devnull:
89 def write(self, msg): pass
90 def flush(self): pass
91
92
93DEBUGSTREAM = Devnull()
94NEWLINE = '\n'
95EMPTYSTRING = ''
Barry Warsaw0e8427e2001-10-04 16:27:04 +000096COMMASPACE = ', '
Barry Warsaw7e0d9562001-01-31 22:51:35 +000097
98
Barry Warsaw0e8427e2001-10-04 16:27:04 +000099
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000100def usage(code, msg=''):
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000101 print(__doc__ % globals(), file=sys.stderr)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000102 if msg:
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000103 print(msg, file=sys.stderr)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000104 sys.exit(code)
105
106
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000107
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000108class SMTPChannel(asynchat.async_chat):
109 COMMAND = 0
110 DATA = 1
111
112 def __init__(self, server, conn, addr):
113 asynchat.async_chat.__init__(self, conn)
114 self.__server = server
115 self.__conn = conn
116 self.__addr = addr
117 self.__line = []
118 self.__state = self.COMMAND
119 self.__greeting = 0
120 self.__mailfrom = None
121 self.__rcpttos = []
122 self.__data = ''
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000123 self.__fqdn = socket.getfqdn()
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000124 self.__peer = conn.getpeername()
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000125 print('Peer:', repr(self.__peer), file=DEBUGSTREAM)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000126 self.push('220 %s %s' % (self.__fqdn, __version__))
Josiah Carlsond74900e2008-07-07 04:15:08 +0000127 self.set_terminator(b'\r\n')
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000128
129 # Overrides base class for convenience
130 def push(self, msg):
Josiah Carlsond74900e2008-07-07 04:15:08 +0000131 asynchat.async_chat.push(self, bytes(msg + '\r\n', 'ascii'))
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000132
133 # Implementation of base class abstract method
134 def collect_incoming_data(self, data):
Guido van Rossum806c2462007-08-06 23:33:07 +0000135 self.__line.append(str(data, "utf8"))
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000136
137 # Implementation of base class abstract method
138 def found_terminator(self):
139 line = EMPTYSTRING.join(self.__line)
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000140 print('Data:', repr(line), file=DEBUGSTREAM)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000141 self.__line = []
142 if self.__state == self.COMMAND:
143 if not line:
144 self.push('500 Error: bad syntax')
145 return
146 method = None
147 i = line.find(' ')
148 if i < 0:
149 command = line.upper()
150 arg = None
151 else:
152 command = line[:i].upper()
153 arg = line[i+1:].strip()
154 method = getattr(self, 'smtp_' + command, None)
155 if not method:
156 self.push('502 Error: command "%s" not implemented' % command)
157 return
158 method(arg)
159 return
160 else:
Guido van Rossum4ba3d652001-03-02 06:42:34 +0000161 if self.__state != self.DATA:
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000162 self.push('451 Internal confusion')
163 return
164 # Remove extraneous carriage returns and de-transparency according
165 # to RFC 821, Section 4.5.2.
166 data = []
167 for text in line.split('\r\n'):
168 if text and text[0] == '.':
169 data.append(text[1:])
170 else:
171 data.append(text)
172 self.__data = NEWLINE.join(data)
173 status = self.__server.process_message(self.__peer,
174 self.__mailfrom,
175 self.__rcpttos,
176 self.__data)
177 self.__rcpttos = []
178 self.__mailfrom = None
179 self.__state = self.COMMAND
Josiah Carlsond74900e2008-07-07 04:15:08 +0000180 self.set_terminator(b'\r\n')
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000181 if not status:
182 self.push('250 Ok')
183 else:
184 self.push(status)
185
186 # SMTP and ESMTP commands
187 def smtp_HELO(self, arg):
188 if not arg:
189 self.push('501 Syntax: HELO hostname')
190 return
191 if self.__greeting:
192 self.push('503 Duplicate HELO/EHLO')
193 else:
194 self.__greeting = arg
195 self.push('250 %s' % self.__fqdn)
196
197 def smtp_NOOP(self, arg):
198 if arg:
199 self.push('501 Syntax: NOOP')
200 else:
201 self.push('250 Ok')
202
203 def smtp_QUIT(self, arg):
204 # args is ignored
205 self.push('221 Bye')
206 self.close_when_done()
207
208 # factored
209 def __getaddr(self, keyword, arg):
210 address = None
211 keylen = len(keyword)
212 if arg[:keylen].upper() == keyword:
213 address = arg[keylen:].strip()
Barry Warsawebf54272001-11-04 03:04:25 +0000214 if not address:
215 pass
216 elif address[0] == '<' and address[-1] == '>' and address != '<>':
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000217 # Addresses can be in the form <person@dom.com> but watch out
218 # for null address, e.g. <>
219 address = address[1:-1]
220 return address
221
222 def smtp_MAIL(self, arg):
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000223 print('===> MAIL', arg, file=DEBUGSTREAM)
Guido van Rossum8ce8a782007-11-01 19:42:39 +0000224 address = self.__getaddr('FROM:', arg) if arg else None
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000225 if not address:
226 self.push('501 Syntax: MAIL FROM:<address>')
227 return
228 if self.__mailfrom:
229 self.push('503 Error: nested MAIL command')
230 return
231 self.__mailfrom = address
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000232 print('sender:', self.__mailfrom, file=DEBUGSTREAM)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000233 self.push('250 Ok')
234
235 def smtp_RCPT(self, arg):
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000236 print('===> RCPT', arg, file=DEBUGSTREAM)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000237 if not self.__mailfrom:
238 self.push('503 Error: need MAIL command')
239 return
Guido van Rossum8ce8a782007-11-01 19:42:39 +0000240 address = self.__getaddr('TO:', arg) if arg else None
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000241 if not address:
242 self.push('501 Syntax: RCPT TO: <address>')
243 return
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000244 self.__rcpttos.append(address)
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000245 print('recips:', self.__rcpttos, file=DEBUGSTREAM)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000246 self.push('250 Ok')
247
248 def smtp_RSET(self, arg):
249 if arg:
250 self.push('501 Syntax: RSET')
251 return
252 # Resets the sender, recipients, and data, but not the greeting
253 self.__mailfrom = None
254 self.__rcpttos = []
255 self.__data = ''
256 self.__state = self.COMMAND
257 self.push('250 Ok')
258
259 def smtp_DATA(self, arg):
260 if not self.__rcpttos:
261 self.push('503 Error: need RCPT command')
262 return
263 if arg:
264 self.push('501 Syntax: DATA')
265 return
266 self.__state = self.DATA
Josiah Carlsond74900e2008-07-07 04:15:08 +0000267 self.set_terminator(b'\r\n.\r\n')
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000268 self.push('354 End data with <CR><LF>.<CR><LF>')
269
270
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000271
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000272class SMTPServer(asyncore.dispatcher):
273 def __init__(self, localaddr, remoteaddr):
274 self._localaddr = localaddr
275 self._remoteaddr = remoteaddr
276 asyncore.dispatcher.__init__(self)
Giampaolo Rodolà610aa4f2010-06-30 17:47:39 +0000277 try:
278 self.create_socket(socket.AF_INET, socket.SOCK_STREAM)
279 # try to re-use a server port if possible
280 self.set_reuse_addr()
281 self.bind(localaddr)
282 self.listen(5)
283 except:
284 self.close()
285 raise
286 else:
287 print('%s started at %s\n\tLocal addr: %s\n\tRemote addr:%s' % (
288 self.__class__.__name__, time.ctime(time.time()),
289 localaddr, remoteaddr), file=DEBUGSTREAM)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000290
291 def handle_accept(self):
292 conn, addr = self.accept()
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000293 print('Incoming connection from %s' % repr(addr), file=DEBUGSTREAM)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000294 channel = SMTPChannel(self, conn, addr)
295
296 # API for "doing something useful with the message"
297 def process_message(self, peer, mailfrom, rcpttos, data):
298 """Override this abstract method to handle messages from the client.
299
300 peer is a tuple containing (ipaddr, port) of the client that made the
301 socket connection to our smtp port.
302
303 mailfrom is the raw address the client claims the message is coming
304 from.
305
306 rcpttos is a list of raw addresses the client wishes to deliver the
307 message to.
308
309 data is a string containing the entire full text of the message,
310 headers (if supplied) and all. It has been `de-transparencied'
311 according to RFC 821, Section 4.5.2. In other words, a line
312 containing a `.' followed by other text has had the leading dot
313 removed.
314
315 This function should return None, for a normal `250 Ok' response;
316 otherwise it returns the desired response string in RFC 821 format.
317
318 """
Guido van Rossumb8b45ea2001-04-15 13:06:04 +0000319 raise NotImplementedError
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000320
Tim Peters658cba62001-02-09 20:06:00 +0000321
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000322
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000323class DebuggingServer(SMTPServer):
324 # Do something with the gathered message
325 def process_message(self, peer, mailfrom, rcpttos, data):
326 inheaders = 1
327 lines = data.split('\n')
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000328 print('---------- MESSAGE FOLLOWS ----------')
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000329 for line in lines:
330 # headers first
331 if inheaders and not line:
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000332 print('X-Peer:', peer[0])
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000333 inheaders = 0
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000334 print(line)
335 print('------------ END MESSAGE ------------')
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000336
337
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000338
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000339class PureProxy(SMTPServer):
340 def process_message(self, peer, mailfrom, rcpttos, data):
341 lines = data.split('\n')
342 # Look for the last header
343 i = 0
344 for line in lines:
345 if not line:
346 break
347 i += 1
348 lines.insert(i, 'X-Peer: %s' % peer[0])
349 data = NEWLINE.join(lines)
350 refused = self._deliver(mailfrom, rcpttos, data)
351 # TBD: what to do with refused addresses?
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000352 print('we got some refusals:', refused, file=DEBUGSTREAM)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000353
354 def _deliver(self, mailfrom, rcpttos, data):
355 import smtplib
356 refused = {}
357 try:
358 s = smtplib.SMTP()
359 s.connect(self._remoteaddr[0], self._remoteaddr[1])
360 try:
361 refused = s.sendmail(mailfrom, rcpttos, data)
362 finally:
363 s.quit()
Guido van Rossumb940e112007-01-10 16:19:56 +0000364 except smtplib.SMTPRecipientsRefused as e:
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000365 print('got SMTPRecipientsRefused', file=DEBUGSTREAM)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000366 refused = e.recipients
Guido van Rossumb940e112007-01-10 16:19:56 +0000367 except (socket.error, smtplib.SMTPException) as e:
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000368 print('got', e.__class__, file=DEBUGSTREAM)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000369 # All recipients were refused. If the exception had an associated
370 # error code, use it. Otherwise,fake it with a non-triggering
371 # exception code.
372 errcode = getattr(e, 'smtp_code', -1)
373 errmsg = getattr(e, 'smtp_error', 'ignore')
374 for r in rcpttos:
375 refused[r] = (errcode, errmsg)
376 return refused
377
378
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000379
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000380class MailmanProxy(PureProxy):
381 def process_message(self, peer, mailfrom, rcpttos, data):
Guido van Rossum68937b42007-05-18 00:51:22 +0000382 from io import StringIO
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000383 from Mailman import Utils
384 from Mailman import Message
385 from Mailman import MailList
386 # If the message is to a Mailman mailing list, then we'll invoke the
387 # Mailman script directly, without going through the real smtpd.
388 # Otherwise we'll forward it to the local proxy for disposition.
389 listnames = []
390 for rcpt in rcpttos:
391 local = rcpt.lower().split('@')[0]
392 # We allow the following variations on the theme
393 # listname
394 # listname-admin
395 # listname-owner
396 # listname-request
397 # listname-join
398 # listname-leave
399 parts = local.split('-')
400 if len(parts) > 2:
401 continue
402 listname = parts[0]
403 if len(parts) == 2:
404 command = parts[1]
405 else:
406 command = ''
407 if not Utils.list_exists(listname) or command not in (
408 '', 'admin', 'owner', 'request', 'join', 'leave'):
409 continue
410 listnames.append((rcpt, listname, command))
411 # Remove all list recipients from rcpttos and forward what we're not
412 # going to take care of ourselves. Linear removal should be fine
413 # since we don't expect a large number of recipients.
414 for rcpt, listname, command in listnames:
415 rcpttos.remove(rcpt)
Tim Peters658cba62001-02-09 20:06:00 +0000416 # If there's any non-list destined recipients left,
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000417 print('forwarding recips:', ' '.join(rcpttos), file=DEBUGSTREAM)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000418 if rcpttos:
419 refused = self._deliver(mailfrom, rcpttos, data)
420 # TBD: what to do with refused addresses?
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000421 print('we got refusals:', refused, file=DEBUGSTREAM)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000422 # Now deliver directly to the list commands
423 mlists = {}
424 s = StringIO(data)
425 msg = Message.Message(s)
426 # These headers are required for the proper execution of Mailman. All
Mark Dickinson934896d2009-02-21 20:59:32 +0000427 # MTAs in existence seem to add these if the original message doesn't
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000428 # have them.
Barry Warsaw820c1202008-06-12 04:06:45 +0000429 if not msg.get('from'):
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000430 msg['From'] = mailfrom
Barry Warsaw820c1202008-06-12 04:06:45 +0000431 if not msg.get('date'):
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000432 msg['Date'] = time.ctime(time.time())
433 for rcpt, listname, command in listnames:
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000434 print('sending message to', rcpt, file=DEBUGSTREAM)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000435 mlist = mlists.get(listname)
436 if not mlist:
437 mlist = MailList.MailList(listname, lock=0)
438 mlists[listname] = mlist
439 # dispatch on the type of command
440 if command == '':
441 # post
442 msg.Enqueue(mlist, tolist=1)
443 elif command == 'admin':
444 msg.Enqueue(mlist, toadmin=1)
445 elif command == 'owner':
446 msg.Enqueue(mlist, toowner=1)
447 elif command == 'request':
448 msg.Enqueue(mlist, torequest=1)
449 elif command in ('join', 'leave'):
450 # TBD: this is a hack!
451 if command == 'join':
452 msg['Subject'] = 'subscribe'
453 else:
454 msg['Subject'] = 'unsubscribe'
455 msg.Enqueue(mlist, torequest=1)
456
457
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000458
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000459class Options:
460 setuid = 1
461 classname = 'PureProxy'
462
463
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000464
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000465def parseargs():
466 global DEBUGSTREAM
467 try:
468 opts, args = getopt.getopt(
469 sys.argv[1:], 'nVhc:d',
470 ['class=', 'nosetuid', 'version', 'help', 'debug'])
Guido van Rossumb940e112007-01-10 16:19:56 +0000471 except getopt.error as e:
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000472 usage(1, e)
473
474 options = Options()
475 for opt, arg in opts:
476 if opt in ('-h', '--help'):
477 usage(0)
478 elif opt in ('-V', '--version'):
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000479 print(__version__, file=sys.stderr)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000480 sys.exit(0)
481 elif opt in ('-n', '--nosetuid'):
482 options.setuid = 0
483 elif opt in ('-c', '--class'):
484 options.classname = arg
485 elif opt in ('-d', '--debug'):
486 DEBUGSTREAM = sys.stderr
487
488 # parse the rest of the arguments
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000489 if len(args) < 1:
490 localspec = 'localhost:8025'
491 remotespec = 'localhost:25'
492 elif len(args) < 2:
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000493 localspec = args[0]
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000494 remotespec = 'localhost:25'
Barry Warsawebf54272001-11-04 03:04:25 +0000495 elif len(args) < 3:
496 localspec = args[0]
497 remotespec = args[1]
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000498 else:
499 usage(1, 'Invalid arguments: %s' % COMMASPACE.join(args))
500
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000501 # split into host/port pairs
502 i = localspec.find(':')
503 if i < 0:
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000504 usage(1, 'Bad local spec: %s' % localspec)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000505 options.localhost = localspec[:i]
506 try:
507 options.localport = int(localspec[i+1:])
508 except ValueError:
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000509 usage(1, 'Bad local port: %s' % localspec)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000510 i = remotespec.find(':')
511 if i < 0:
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000512 usage(1, 'Bad remote spec: %s' % remotespec)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000513 options.remotehost = remotespec[:i]
514 try:
515 options.remoteport = int(remotespec[i+1:])
516 except ValueError:
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000517 usage(1, 'Bad remote port: %s' % remotespec)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000518 return options
519
520
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000521
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000522if __name__ == '__main__':
523 options = parseargs()
524 # Become nobody
525 if options.setuid:
526 try:
527 import pwd
528 except ImportError:
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000529 print('Cannot import module "pwd"; try running with -n option.', file=sys.stderr)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000530 sys.exit(1)
531 nobody = pwd.getpwnam('nobody')[2]
532 try:
533 os.setuid(nobody)
Guido van Rossumb940e112007-01-10 16:19:56 +0000534 except OSError as e:
Guido van Rossum4ba3d652001-03-02 06:42:34 +0000535 if e.errno != errno.EPERM: raise
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000536 print('Cannot setuid "nobody"; try running with -n option.', file=sys.stderr)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000537 sys.exit(1)
Skip Montanaro90e01532004-06-26 19:18:49 +0000538 classname = options.classname
539 if "." in classname:
540 lastdot = classname.rfind(".")
541 mod = __import__(classname[:lastdot], globals(), locals(), [""])
542 classname = classname[lastdot+1:]
543 else:
544 import __main__ as mod
Skip Montanaro90e01532004-06-26 19:18:49 +0000545 class_ = getattr(mod, classname)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000546 proxy = class_((options.localhost, options.localport),
547 (options.remotehost, options.remoteport))
548 try:
549 asyncore.loop()
550 except KeyboardInterrupt:
551 pass