blob: ab3e0e209b0aa5bbf84dd428848514fc65f6eac0 [file] [log] [blame]
Barry Warsaw7e0d9562001-01-31 22:51:35 +00001#! /usr/bin/env python
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()
Giampaolo RodolĂ ed2ce462010-08-23 22:34:37 +0000124 try:
125 self.__peer = conn.getpeername()
126 except socket.error as err:
127 # a race condition may occur if the other end is closing
128 # before we can get the peername
129 self.close()
130 if err.args[0] != errno.ENOTCONN:
131 raise
132 return
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000133 print('Peer:', repr(self.__peer), file=DEBUGSTREAM)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000134 self.push('220 %s %s' % (self.__fqdn, __version__))
Josiah Carlsond74900e2008-07-07 04:15:08 +0000135 self.set_terminator(b'\r\n')
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000136
137 # Overrides base class for convenience
138 def push(self, msg):
Josiah Carlsond74900e2008-07-07 04:15:08 +0000139 asynchat.async_chat.push(self, bytes(msg + '\r\n', 'ascii'))
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000140
141 # Implementation of base class abstract method
142 def collect_incoming_data(self, data):
Guido van Rossum806c2462007-08-06 23:33:07 +0000143 self.__line.append(str(data, "utf8"))
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000144
145 # Implementation of base class abstract method
146 def found_terminator(self):
147 line = EMPTYSTRING.join(self.__line)
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000148 print('Data:', repr(line), file=DEBUGSTREAM)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000149 self.__line = []
150 if self.__state == self.COMMAND:
151 if not line:
152 self.push('500 Error: bad syntax')
153 return
154 method = None
155 i = line.find(' ')
156 if i < 0:
157 command = line.upper()
158 arg = None
159 else:
160 command = line[:i].upper()
161 arg = line[i+1:].strip()
162 method = getattr(self, 'smtp_' + command, None)
163 if not method:
164 self.push('502 Error: command "%s" not implemented' % command)
165 return
166 method(arg)
167 return
168 else:
Guido van Rossum4ba3d652001-03-02 06:42:34 +0000169 if self.__state != self.DATA:
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000170 self.push('451 Internal confusion')
171 return
172 # Remove extraneous carriage returns and de-transparency according
173 # to RFC 821, Section 4.5.2.
174 data = []
175 for text in line.split('\r\n'):
176 if text and text[0] == '.':
177 data.append(text[1:])
178 else:
179 data.append(text)
180 self.__data = NEWLINE.join(data)
181 status = self.__server.process_message(self.__peer,
182 self.__mailfrom,
183 self.__rcpttos,
184 self.__data)
185 self.__rcpttos = []
186 self.__mailfrom = None
187 self.__state = self.COMMAND
Josiah Carlsond74900e2008-07-07 04:15:08 +0000188 self.set_terminator(b'\r\n')
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000189 if not status:
190 self.push('250 Ok')
191 else:
192 self.push(status)
193
194 # SMTP and ESMTP commands
195 def smtp_HELO(self, arg):
196 if not arg:
197 self.push('501 Syntax: HELO hostname')
198 return
199 if self.__greeting:
200 self.push('503 Duplicate HELO/EHLO')
201 else:
202 self.__greeting = arg
203 self.push('250 %s' % self.__fqdn)
204
205 def smtp_NOOP(self, arg):
206 if arg:
207 self.push('501 Syntax: NOOP')
208 else:
209 self.push('250 Ok')
210
211 def smtp_QUIT(self, arg):
212 # args is ignored
213 self.push('221 Bye')
214 self.close_when_done()
215
216 # factored
217 def __getaddr(self, keyword, arg):
218 address = None
219 keylen = len(keyword)
220 if arg[:keylen].upper() == keyword:
221 address = arg[keylen:].strip()
Barry Warsawebf54272001-11-04 03:04:25 +0000222 if not address:
223 pass
224 elif address[0] == '<' and address[-1] == '>' and address != '<>':
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000225 # Addresses can be in the form <person@dom.com> but watch out
226 # for null address, e.g. <>
227 address = address[1:-1]
228 return address
229
230 def smtp_MAIL(self, arg):
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000231 print('===> MAIL', arg, file=DEBUGSTREAM)
Guido van Rossum8ce8a782007-11-01 19:42:39 +0000232 address = self.__getaddr('FROM:', arg) if arg else None
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000233 if not address:
234 self.push('501 Syntax: MAIL FROM:<address>')
235 return
236 if self.__mailfrom:
237 self.push('503 Error: nested MAIL command')
238 return
239 self.__mailfrom = address
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000240 print('sender:', self.__mailfrom, file=DEBUGSTREAM)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000241 self.push('250 Ok')
242
243 def smtp_RCPT(self, arg):
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000244 print('===> RCPT', arg, file=DEBUGSTREAM)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000245 if not self.__mailfrom:
246 self.push('503 Error: need MAIL command')
247 return
Guido van Rossum8ce8a782007-11-01 19:42:39 +0000248 address = self.__getaddr('TO:', arg) if arg else None
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000249 if not address:
250 self.push('501 Syntax: RCPT TO: <address>')
251 return
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000252 self.__rcpttos.append(address)
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000253 print('recips:', self.__rcpttos, file=DEBUGSTREAM)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000254 self.push('250 Ok')
255
256 def smtp_RSET(self, arg):
257 if arg:
258 self.push('501 Syntax: RSET')
259 return
260 # Resets the sender, recipients, and data, but not the greeting
261 self.__mailfrom = None
262 self.__rcpttos = []
263 self.__data = ''
264 self.__state = self.COMMAND
265 self.push('250 Ok')
266
267 def smtp_DATA(self, arg):
268 if not self.__rcpttos:
269 self.push('503 Error: need RCPT command')
270 return
271 if arg:
272 self.push('501 Syntax: DATA')
273 return
274 self.__state = self.DATA
Josiah Carlsond74900e2008-07-07 04:15:08 +0000275 self.set_terminator(b'\r\n.\r\n')
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000276 self.push('354 End data with <CR><LF>.<CR><LF>')
277
278
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000279
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000280class SMTPServer(asyncore.dispatcher):
281 def __init__(self, localaddr, remoteaddr):
282 self._localaddr = localaddr
283 self._remoteaddr = remoteaddr
284 asyncore.dispatcher.__init__(self)
Giampaolo RodolĂ d68ed932010-06-30 17:50:20 +0000285 try:
286 self.create_socket(socket.AF_INET, socket.SOCK_STREAM)
287 # try to re-use a server port if possible
288 self.set_reuse_addr()
289 self.bind(localaddr)
290 self.listen(5)
291 except:
292 self.close()
293 raise
294 else:
295 print('%s started at %s\n\tLocal addr: %s\n\tRemote addr:%s' % (
296 self.__class__.__name__, time.ctime(time.time()),
297 localaddr, remoteaddr), file=DEBUGSTREAM)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000298
299 def handle_accept(self):
Giampaolo RodolĂ 5ea3d0f2010-11-01 15:18:09 +0000300 pair = self.accept()
301 if pair is not None:
302 conn, addr = pair
303 print('Incoming connection from %s' % repr(addr), file=DEBUGSTREAM)
304 channel = SMTPChannel(self, conn, addr)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000305
306 # API for "doing something useful with the message"
307 def process_message(self, peer, mailfrom, rcpttos, data):
308 """Override this abstract method to handle messages from the client.
309
310 peer is a tuple containing (ipaddr, port) of the client that made the
311 socket connection to our smtp port.
312
313 mailfrom is the raw address the client claims the message is coming
314 from.
315
316 rcpttos is a list of raw addresses the client wishes to deliver the
317 message to.
318
319 data is a string containing the entire full text of the message,
320 headers (if supplied) and all. It has been `de-transparencied'
321 according to RFC 821, Section 4.5.2. In other words, a line
322 containing a `.' followed by other text has had the leading dot
323 removed.
324
325 This function should return None, for a normal `250 Ok' response;
326 otherwise it returns the desired response string in RFC 821 format.
327
328 """
Guido van Rossumb8b45ea2001-04-15 13:06:04 +0000329 raise NotImplementedError
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000330
Tim Peters658cba62001-02-09 20:06:00 +0000331
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000332
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000333class DebuggingServer(SMTPServer):
334 # Do something with the gathered message
335 def process_message(self, peer, mailfrom, rcpttos, data):
336 inheaders = 1
337 lines = data.split('\n')
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000338 print('---------- MESSAGE FOLLOWS ----------')
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000339 for line in lines:
340 # headers first
341 if inheaders and not line:
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000342 print('X-Peer:', peer[0])
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000343 inheaders = 0
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000344 print(line)
345 print('------------ END MESSAGE ------------')
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000346
347
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000348
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000349class PureProxy(SMTPServer):
350 def process_message(self, peer, mailfrom, rcpttos, data):
351 lines = data.split('\n')
352 # Look for the last header
353 i = 0
354 for line in lines:
355 if not line:
356 break
357 i += 1
358 lines.insert(i, 'X-Peer: %s' % peer[0])
359 data = NEWLINE.join(lines)
360 refused = self._deliver(mailfrom, rcpttos, data)
361 # TBD: what to do with refused addresses?
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000362 print('we got some refusals:', refused, file=DEBUGSTREAM)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000363
364 def _deliver(self, mailfrom, rcpttos, data):
365 import smtplib
366 refused = {}
367 try:
368 s = smtplib.SMTP()
369 s.connect(self._remoteaddr[0], self._remoteaddr[1])
370 try:
371 refused = s.sendmail(mailfrom, rcpttos, data)
372 finally:
373 s.quit()
Guido van Rossumb940e112007-01-10 16:19:56 +0000374 except smtplib.SMTPRecipientsRefused as e:
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000375 print('got SMTPRecipientsRefused', file=DEBUGSTREAM)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000376 refused = e.recipients
Guido van Rossumb940e112007-01-10 16:19:56 +0000377 except (socket.error, smtplib.SMTPException) as e:
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000378 print('got', e.__class__, file=DEBUGSTREAM)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000379 # All recipients were refused. If the exception had an associated
380 # error code, use it. Otherwise,fake it with a non-triggering
381 # exception code.
382 errcode = getattr(e, 'smtp_code', -1)
383 errmsg = getattr(e, 'smtp_error', 'ignore')
384 for r in rcpttos:
385 refused[r] = (errcode, errmsg)
386 return refused
387
388
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000389
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000390class MailmanProxy(PureProxy):
391 def process_message(self, peer, mailfrom, rcpttos, data):
Guido van Rossum68937b42007-05-18 00:51:22 +0000392 from io import StringIO
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000393 from Mailman import Utils
394 from Mailman import Message
395 from Mailman import MailList
396 # If the message is to a Mailman mailing list, then we'll invoke the
397 # Mailman script directly, without going through the real smtpd.
398 # Otherwise we'll forward it to the local proxy for disposition.
399 listnames = []
400 for rcpt in rcpttos:
401 local = rcpt.lower().split('@')[0]
402 # We allow the following variations on the theme
403 # listname
404 # listname-admin
405 # listname-owner
406 # listname-request
407 # listname-join
408 # listname-leave
409 parts = local.split('-')
410 if len(parts) > 2:
411 continue
412 listname = parts[0]
413 if len(parts) == 2:
414 command = parts[1]
415 else:
416 command = ''
417 if not Utils.list_exists(listname) or command not in (
418 '', 'admin', 'owner', 'request', 'join', 'leave'):
419 continue
420 listnames.append((rcpt, listname, command))
421 # Remove all list recipients from rcpttos and forward what we're not
422 # going to take care of ourselves. Linear removal should be fine
423 # since we don't expect a large number of recipients.
424 for rcpt, listname, command in listnames:
425 rcpttos.remove(rcpt)
Tim Peters658cba62001-02-09 20:06:00 +0000426 # If there's any non-list destined recipients left,
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000427 print('forwarding recips:', ' '.join(rcpttos), file=DEBUGSTREAM)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000428 if rcpttos:
429 refused = self._deliver(mailfrom, rcpttos, data)
430 # TBD: what to do with refused addresses?
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000431 print('we got refusals:', refused, file=DEBUGSTREAM)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000432 # Now deliver directly to the list commands
433 mlists = {}
434 s = StringIO(data)
435 msg = Message.Message(s)
436 # These headers are required for the proper execution of Mailman. All
Mark Dickinson934896d2009-02-21 20:59:32 +0000437 # MTAs in existence seem to add these if the original message doesn't
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000438 # have them.
Barry Warsaw820c1202008-06-12 04:06:45 +0000439 if not msg.get('from'):
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000440 msg['From'] = mailfrom
Barry Warsaw820c1202008-06-12 04:06:45 +0000441 if not msg.get('date'):
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000442 msg['Date'] = time.ctime(time.time())
443 for rcpt, listname, command in listnames:
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000444 print('sending message to', rcpt, file=DEBUGSTREAM)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000445 mlist = mlists.get(listname)
446 if not mlist:
447 mlist = MailList.MailList(listname, lock=0)
448 mlists[listname] = mlist
449 # dispatch on the type of command
450 if command == '':
451 # post
452 msg.Enqueue(mlist, tolist=1)
453 elif command == 'admin':
454 msg.Enqueue(mlist, toadmin=1)
455 elif command == 'owner':
456 msg.Enqueue(mlist, toowner=1)
457 elif command == 'request':
458 msg.Enqueue(mlist, torequest=1)
459 elif command in ('join', 'leave'):
460 # TBD: this is a hack!
461 if command == 'join':
462 msg['Subject'] = 'subscribe'
463 else:
464 msg['Subject'] = 'unsubscribe'
465 msg.Enqueue(mlist, torequest=1)
466
467
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000468
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000469class Options:
470 setuid = 1
471 classname = 'PureProxy'
472
473
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000474
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000475def parseargs():
476 global DEBUGSTREAM
477 try:
478 opts, args = getopt.getopt(
479 sys.argv[1:], 'nVhc:d',
480 ['class=', 'nosetuid', 'version', 'help', 'debug'])
Guido van Rossumb940e112007-01-10 16:19:56 +0000481 except getopt.error as e:
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000482 usage(1, e)
483
484 options = Options()
485 for opt, arg in opts:
486 if opt in ('-h', '--help'):
487 usage(0)
488 elif opt in ('-V', '--version'):
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000489 print(__version__, file=sys.stderr)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000490 sys.exit(0)
491 elif opt in ('-n', '--nosetuid'):
492 options.setuid = 0
493 elif opt in ('-c', '--class'):
494 options.classname = arg
495 elif opt in ('-d', '--debug'):
496 DEBUGSTREAM = sys.stderr
497
498 # parse the rest of the arguments
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000499 if len(args) < 1:
500 localspec = 'localhost:8025'
501 remotespec = 'localhost:25'
502 elif len(args) < 2:
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000503 localspec = args[0]
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000504 remotespec = 'localhost:25'
Barry Warsawebf54272001-11-04 03:04:25 +0000505 elif len(args) < 3:
506 localspec = args[0]
507 remotespec = args[1]
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000508 else:
509 usage(1, 'Invalid arguments: %s' % COMMASPACE.join(args))
510
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000511 # split into host/port pairs
512 i = localspec.find(':')
513 if i < 0:
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000514 usage(1, 'Bad local spec: %s' % localspec)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000515 options.localhost = localspec[:i]
516 try:
517 options.localport = int(localspec[i+1:])
518 except ValueError:
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000519 usage(1, 'Bad local port: %s' % localspec)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000520 i = remotespec.find(':')
521 if i < 0:
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000522 usage(1, 'Bad remote spec: %s' % remotespec)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000523 options.remotehost = remotespec[:i]
524 try:
525 options.remoteport = int(remotespec[i+1:])
526 except ValueError:
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000527 usage(1, 'Bad remote port: %s' % remotespec)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000528 return options
529
530
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000531
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000532if __name__ == '__main__':
533 options = parseargs()
534 # Become nobody
535 if options.setuid:
536 try:
537 import pwd
538 except ImportError:
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000539 print('Cannot import module "pwd"; try running with -n option.', file=sys.stderr)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000540 sys.exit(1)
541 nobody = pwd.getpwnam('nobody')[2]
542 try:
543 os.setuid(nobody)
Guido van Rossumb940e112007-01-10 16:19:56 +0000544 except OSError as e:
Guido van Rossum4ba3d652001-03-02 06:42:34 +0000545 if e.errno != errno.EPERM: raise
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000546 print('Cannot setuid "nobody"; try running with -n option.', file=sys.stderr)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000547 sys.exit(1)
Skip Montanaro90e01532004-06-26 19:18:49 +0000548 classname = options.classname
549 if "." in classname:
550 lastdot = classname.rfind(".")
551 mod = __import__(classname[:lastdot], globals(), locals(), [""])
552 classname = classname[lastdot+1:]
553 else:
554 import __main__ as mod
Skip Montanaro90e01532004-06-26 19:18:49 +0000555 class_ = getattr(mod, classname)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000556 proxy = class_((options.localhost, options.localport),
557 (options.remotehost, options.remoteport))
558 try:
559 asyncore.loop()
560 except KeyboardInterrupt:
561 pass