blob: 57ad08984192ecd641ed0206c169ce785e9912e3 [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Ă ed2ce462010-08-23 22:34:37 +0000300 try:
301 conn, addr = self.accept()
302 except TypeError:
303 # sometimes accept() might return None
304 return
305 except socket.error as err:
306 # ECONNABORTED might be thrown
307 if err.args[0] != errno.ECONNABORTED:
308 raise
309 return
310 else:
311 # sometimes addr == None instead of (ip, port)
312 if addr == None:
313 return
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000314 print('Incoming connection from %s' % repr(addr), file=DEBUGSTREAM)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000315 channel = SMTPChannel(self, conn, addr)
316
317 # API for "doing something useful with the message"
318 def process_message(self, peer, mailfrom, rcpttos, data):
319 """Override this abstract method to handle messages from the client.
320
321 peer is a tuple containing (ipaddr, port) of the client that made the
322 socket connection to our smtp port.
323
324 mailfrom is the raw address the client claims the message is coming
325 from.
326
327 rcpttos is a list of raw addresses the client wishes to deliver the
328 message to.
329
330 data is a string containing the entire full text of the message,
331 headers (if supplied) and all. It has been `de-transparencied'
332 according to RFC 821, Section 4.5.2. In other words, a line
333 containing a `.' followed by other text has had the leading dot
334 removed.
335
336 This function should return None, for a normal `250 Ok' response;
337 otherwise it returns the desired response string in RFC 821 format.
338
339 """
Guido van Rossumb8b45ea2001-04-15 13:06:04 +0000340 raise NotImplementedError
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000341
Tim Peters658cba62001-02-09 20:06:00 +0000342
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000343
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000344class DebuggingServer(SMTPServer):
345 # Do something with the gathered message
346 def process_message(self, peer, mailfrom, rcpttos, data):
347 inheaders = 1
348 lines = data.split('\n')
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000349 print('---------- MESSAGE FOLLOWS ----------')
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000350 for line in lines:
351 # headers first
352 if inheaders and not line:
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000353 print('X-Peer:', peer[0])
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000354 inheaders = 0
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000355 print(line)
356 print('------------ END MESSAGE ------------')
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000357
358
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000359
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000360class PureProxy(SMTPServer):
361 def process_message(self, peer, mailfrom, rcpttos, data):
362 lines = data.split('\n')
363 # Look for the last header
364 i = 0
365 for line in lines:
366 if not line:
367 break
368 i += 1
369 lines.insert(i, 'X-Peer: %s' % peer[0])
370 data = NEWLINE.join(lines)
371 refused = self._deliver(mailfrom, rcpttos, data)
372 # TBD: what to do with refused addresses?
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000373 print('we got some refusals:', refused, file=DEBUGSTREAM)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000374
375 def _deliver(self, mailfrom, rcpttos, data):
376 import smtplib
377 refused = {}
378 try:
379 s = smtplib.SMTP()
380 s.connect(self._remoteaddr[0], self._remoteaddr[1])
381 try:
382 refused = s.sendmail(mailfrom, rcpttos, data)
383 finally:
384 s.quit()
Guido van Rossumb940e112007-01-10 16:19:56 +0000385 except smtplib.SMTPRecipientsRefused as e:
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000386 print('got SMTPRecipientsRefused', file=DEBUGSTREAM)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000387 refused = e.recipients
Guido van Rossumb940e112007-01-10 16:19:56 +0000388 except (socket.error, smtplib.SMTPException) as e:
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000389 print('got', e.__class__, file=DEBUGSTREAM)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000390 # All recipients were refused. If the exception had an associated
391 # error code, use it. Otherwise,fake it with a non-triggering
392 # exception code.
393 errcode = getattr(e, 'smtp_code', -1)
394 errmsg = getattr(e, 'smtp_error', 'ignore')
395 for r in rcpttos:
396 refused[r] = (errcode, errmsg)
397 return refused
398
399
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000400
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000401class MailmanProxy(PureProxy):
402 def process_message(self, peer, mailfrom, rcpttos, data):
Guido van Rossum68937b42007-05-18 00:51:22 +0000403 from io import StringIO
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000404 from Mailman import Utils
405 from Mailman import Message
406 from Mailman import MailList
407 # If the message is to a Mailman mailing list, then we'll invoke the
408 # Mailman script directly, without going through the real smtpd.
409 # Otherwise we'll forward it to the local proxy for disposition.
410 listnames = []
411 for rcpt in rcpttos:
412 local = rcpt.lower().split('@')[0]
413 # We allow the following variations on the theme
414 # listname
415 # listname-admin
416 # listname-owner
417 # listname-request
418 # listname-join
419 # listname-leave
420 parts = local.split('-')
421 if len(parts) > 2:
422 continue
423 listname = parts[0]
424 if len(parts) == 2:
425 command = parts[1]
426 else:
427 command = ''
428 if not Utils.list_exists(listname) or command not in (
429 '', 'admin', 'owner', 'request', 'join', 'leave'):
430 continue
431 listnames.append((rcpt, listname, command))
432 # Remove all list recipients from rcpttos and forward what we're not
433 # going to take care of ourselves. Linear removal should be fine
434 # since we don't expect a large number of recipients.
435 for rcpt, listname, command in listnames:
436 rcpttos.remove(rcpt)
Tim Peters658cba62001-02-09 20:06:00 +0000437 # If there's any non-list destined recipients left,
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000438 print('forwarding recips:', ' '.join(rcpttos), file=DEBUGSTREAM)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000439 if rcpttos:
440 refused = self._deliver(mailfrom, rcpttos, data)
441 # TBD: what to do with refused addresses?
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000442 print('we got refusals:', refused, file=DEBUGSTREAM)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000443 # Now deliver directly to the list commands
444 mlists = {}
445 s = StringIO(data)
446 msg = Message.Message(s)
447 # These headers are required for the proper execution of Mailman. All
Mark Dickinson934896d2009-02-21 20:59:32 +0000448 # MTAs in existence seem to add these if the original message doesn't
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000449 # have them.
Barry Warsaw820c1202008-06-12 04:06:45 +0000450 if not msg.get('from'):
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000451 msg['From'] = mailfrom
Barry Warsaw820c1202008-06-12 04:06:45 +0000452 if not msg.get('date'):
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000453 msg['Date'] = time.ctime(time.time())
454 for rcpt, listname, command in listnames:
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000455 print('sending message to', rcpt, file=DEBUGSTREAM)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000456 mlist = mlists.get(listname)
457 if not mlist:
458 mlist = MailList.MailList(listname, lock=0)
459 mlists[listname] = mlist
460 # dispatch on the type of command
461 if command == '':
462 # post
463 msg.Enqueue(mlist, tolist=1)
464 elif command == 'admin':
465 msg.Enqueue(mlist, toadmin=1)
466 elif command == 'owner':
467 msg.Enqueue(mlist, toowner=1)
468 elif command == 'request':
469 msg.Enqueue(mlist, torequest=1)
470 elif command in ('join', 'leave'):
471 # TBD: this is a hack!
472 if command == 'join':
473 msg['Subject'] = 'subscribe'
474 else:
475 msg['Subject'] = 'unsubscribe'
476 msg.Enqueue(mlist, torequest=1)
477
478
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000479
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000480class Options:
481 setuid = 1
482 classname = 'PureProxy'
483
484
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000485
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000486def parseargs():
487 global DEBUGSTREAM
488 try:
489 opts, args = getopt.getopt(
490 sys.argv[1:], 'nVhc:d',
491 ['class=', 'nosetuid', 'version', 'help', 'debug'])
Guido van Rossumb940e112007-01-10 16:19:56 +0000492 except getopt.error as e:
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000493 usage(1, e)
494
495 options = Options()
496 for opt, arg in opts:
497 if opt in ('-h', '--help'):
498 usage(0)
499 elif opt in ('-V', '--version'):
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000500 print(__version__, file=sys.stderr)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000501 sys.exit(0)
502 elif opt in ('-n', '--nosetuid'):
503 options.setuid = 0
504 elif opt in ('-c', '--class'):
505 options.classname = arg
506 elif opt in ('-d', '--debug'):
507 DEBUGSTREAM = sys.stderr
508
509 # parse the rest of the arguments
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000510 if len(args) < 1:
511 localspec = 'localhost:8025'
512 remotespec = 'localhost:25'
513 elif len(args) < 2:
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000514 localspec = args[0]
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000515 remotespec = 'localhost:25'
Barry Warsawebf54272001-11-04 03:04:25 +0000516 elif len(args) < 3:
517 localspec = args[0]
518 remotespec = args[1]
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000519 else:
520 usage(1, 'Invalid arguments: %s' % COMMASPACE.join(args))
521
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000522 # split into host/port pairs
523 i = localspec.find(':')
524 if i < 0:
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000525 usage(1, 'Bad local spec: %s' % localspec)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000526 options.localhost = localspec[:i]
527 try:
528 options.localport = int(localspec[i+1:])
529 except ValueError:
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000530 usage(1, 'Bad local port: %s' % localspec)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000531 i = remotespec.find(':')
532 if i < 0:
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000533 usage(1, 'Bad remote spec: %s' % remotespec)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000534 options.remotehost = remotespec[:i]
535 try:
536 options.remoteport = int(remotespec[i+1:])
537 except ValueError:
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000538 usage(1, 'Bad remote port: %s' % remotespec)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000539 return options
540
541
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000542
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000543if __name__ == '__main__':
544 options = parseargs()
545 # Become nobody
546 if options.setuid:
547 try:
548 import pwd
549 except ImportError:
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000550 print('Cannot import module "pwd"; try running with -n option.', file=sys.stderr)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000551 sys.exit(1)
552 nobody = pwd.getpwnam('nobody')[2]
553 try:
554 os.setuid(nobody)
Guido van Rossumb940e112007-01-10 16:19:56 +0000555 except OSError as e:
Guido van Rossum4ba3d652001-03-02 06:42:34 +0000556 if e.errno != errno.EPERM: raise
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000557 print('Cannot setuid "nobody"; try running with -n option.', file=sys.stderr)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000558 sys.exit(1)
Skip Montanaro90e01532004-06-26 19:18:49 +0000559 classname = options.classname
560 if "." in classname:
561 lastdot = classname.rfind(".")
562 mod = __import__(classname[:lastdot], globals(), locals(), [""])
563 classname = classname[lastdot+1:]
564 else:
565 import __main__ as mod
Skip Montanaro90e01532004-06-26 19:18:49 +0000566 class_ = getattr(mod, classname)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000567 proxy = class_((options.localhost, options.localport),
568 (options.remotehost, options.remoteport))
569 try:
570 asyncore.loop()
571 except KeyboardInterrupt:
572 pass