r17513@catbus: nickm | 2008-01-07 15:58:01 -0500
[tor-bridgedb/rransom.git] / lib / bridgedb / Server.py
bloba24dfe3f5d89d7fd24f93ca1e0b32fffd4cc68cd
1 # BridgeDB by Nick Mathewson.
2 # Copyright (c) 2007, The Tor Project, Inc.
3 # See LICENSE for licensing information
5 """
6 This module implements the web and email interfaces to the bridge database.
7 """
9 from cStringIO import StringIO
10 import MimeWriter
11 import rfc822
12 import time
13 import logging
15 from zope.interface import implements
17 from twisted.internet import reactor
18 from twisted.internet.defer import Deferred
19 import twisted.web.resource
20 import twisted.web.server
21 import twisted.mail.smtp
23 import bridgedb.Dist
25 HTML_MESSAGE_TEMPLATE = """
26 <html><body>
27 <p>Here are your bridge relays:
28 <pre id="bridges">
30 </pre>
31 </p>
32 <p>Bridge relays (or "bridges" for short) are Tor relays that aren't listed
33 in the main directory. Since there is no complete public list of them,
34 even if your ISP is filtering connections to all the known Tor relays,
35 they probably won't be able to block all the bridges.</p>
36 <p>To use the above lines, go to Vidalia's Network settings page, and click
37 "My ISP blocks connections to the Tor network". Then add each bridge
38 address one at a time.</p>
39 <p>Configuring more than one bridge address will make your Tor connection
40 more stable, in case some of the bridges become unreachable.</p>
41 <p>Another way to find public bridge addresses is to send mail to
42 bridges@torproject.org with the line "get bridges" by itself in the body
43 of the mail. However, so we can make it harder for an attacker to learn
44 lots of bridge addresses, you must send this request from a gmail or
45 yahoo account.</p>
46 </body></html>
47 """.strip()
49 EMAIL_MESSAGE_TEMPLATE = """\
50 [This is an automated message; please do not reply.]
52 Here are your bridge relays:
56 Bridge relays (or "bridges" for short) are Tor relays that aren't listed
57 in the main directory. Since there is no complete public list of them,
58 even if your ISP is filtering connections to all the known Tor relays,
59 they probably won't be able to block all the bridges.
61 To use the above lines, go to Vidalia's Network settings page, and click
62 "My ISP blocks connections to the Tor network". Then add each bridge
63 address one at a time.
65 Configuring more than one bridge address will make your Tor connection
66 more stable, in case some of the bridges become unreachable.
68 Another way to find public bridge addresses is to visit
69 https://bridges.torproject.org/. The answers you get from that page
70 will change every few days, so check back periodically if you need more
71 bridge addresses.
72 """
74 class WebResource(twisted.web.resource.Resource):
75 """This resource is used by Twisted Web to give a web page with some
76 bridges in response to a request."""
77 isLeaf = True
79 def __init__(self, distributor, schedule, N=1):
80 """Create a new WebResource.
81 distributor -- an IPBasedDistributor object
82 schedule -- an IntervalSchedule object
83 N -- the number of bridges to hand out per query.
84 """
85 twisted.web.resource.Resource.__init__(self)
86 self.distributor = distributor
87 self.schedule = schedule
88 self.nBridgesToGive = N
90 def render_GET(self, request):
91 interval = self.schedule.getInterval(time.time())
92 ip = request.getClientIP()
93 bridges = self.distributor.getBridgesForIP(ip, interval,
94 self.nBridgesToGive)
95 if bridges:
96 answer = "".join("%s\n" % b.getConfigLine() for b in bridges)
97 else:
98 answer = "No bridges available."
100 logging.info("Replying to web request from %s", ip)
101 return HTML_MESSAGE_TEMPLATE % answer
103 def addWebServer(cfg, dist, sched):
104 """Set up a web server.
105 cfg -- a configuration object from Main. We use these options:
106 HTTPS_N_BRIDGES_PER_ANSWER
107 HTTP_UNENCRYPTED_PORT
108 HTTP_UNENCRYPTED_BIND_IP
109 HTTPS_PORT
110 HTTPS_BIND_IP
111 dist -- an IPBasedDistributor object.
112 sched -- an IntervalSchedule object.
114 Site = twisted.web.server.Site
115 resource = WebResource(dist, sched, cfg.HTTPS_N_BRIDGES_PER_ANSWER)
116 site = Site(resource)
117 if cfg.HTTP_UNENCRYPTED_PORT:
118 ip = cfg.HTTP_UNENCRYPTED_BIND_IP or ""
119 reactor.listenTCP(cfg.HTTP_UNENCRYPTED_PORT, site, interface=ip)
120 if cfg.HTTPS_PORT:
121 from twisted.internet.ssl import DefaultOpenSSLContextFactory
122 #from OpenSSL.SSL import SSLv3_METHOD
123 ip = cfg.HTTPS_BIND_IP or ""
124 factory = DefaultOpenSSLContextFactory(cfg.HTTPS_KEY_FILE,
125 cfg.HTTPS_CERT_FILE)
126 reactor.listenSSL(cfg.HTTPS_PORT, site, factory, interface=ip)
127 return site
129 class MailFile:
130 """A file-like object used to hand rfc822.Message a list of lines
131 as though it were reading them from a file."""
132 def __init__(self, lines):
133 self.lines = lines
134 self.idx = 0
135 def readline(self):
136 try :
137 line = self.lines[self.idx]
138 self.idx += 1
139 return line
140 except IndexError:
141 return ""
143 def getMailResponse(lines, ctx):
144 """Given a list of lines from an incoming email message, and a
145 MailContext object, parse the email and decide what to do in response.
146 If we want to answer, return a 2-tuple containing the address that
147 will receive the response, and a readable filelike object containing
148 the response. Return None,None if we shouldn't answer.
150 # Extract data from the headers.
151 msg = rfc822.Message(MailFile(lines))
152 subject = msg.getheader("Subject", None)
153 if not subject: subject = "[no subject]"
154 clientFromAddr = msg.getaddr("From")
155 clientSenderAddr = msg.getaddr("Sender")
156 msgID = msg.getheader("Message-ID")
157 if clientSenderAddr and clientSenderAddr[1]:
158 clientAddr = clientSenderAddr[1]
159 elif clientFromAddr and clientFromAddr[1]:
160 clientAddr = clientFromAddr[1]
161 else:
162 logging.info("No From or Sender header on incoming mail.")
163 return None,None
165 _, addrdomain = bridgedb.Dist.extractAddrSpec(clientAddr.lower())
166 if not addrdomain:
167 logging.info("Couldn't parse domain from %r", clientAddr)
168 if addrdomain and ctx.cfg.EMAIL_DOMAIN_MAP:
169 addrdomain = ctx.cfg.EMAIL_DOMAIN_MAP.get(addrdomain, addrdomain)
170 rules = ctx.cfg.EMAIL_DOMAIN_RULES.get(addrdomain, [])
171 if 'dkim' in rules:
172 # getheader() returns the last of a given kind of header; we want
173 # to get the first, so we use getheaders() instead.
174 dkimHeaders = msg.getheaders("X-DKIM-Authentication-Result")
175 dkimHeader = "<no header>"
176 if dkimHeaders: dkimHeader = dkimHeaders[0]
177 if not dkimHeader.startswith("pass"):
178 logging.info("Got a bad dkim header (%r) on an incoming mail; "
179 "rejecting it.", dkimHeader)
180 return None, None
182 # Was the magic string included
183 for ln in lines:
184 if ln.strip().lower() in ("get bridges", "subject: get bridges"):
185 break
186 else:
187 logging.info("Got a mail from %r with no bridge request; dropping",
188 clientAddr)
189 return None,None
191 # Figure out which bridges to send
192 try:
193 interval = ctx.schedule.getInterval(time.time())
194 bridges = ctx.distributor.getBridgesForEmail(clientAddr,
195 interval, ctx.N)
196 except bridgedb.Dist.BadEmail, e:
197 logging.info("Got a mail from a bad email address %r: %s.",
198 clientAddr, e)
199 return None, None
201 # Generate the message.
202 f = StringIO()
203 w = MimeWriter.MimeWriter(f)
204 w.addheader("From", ctx.fromAddr)
205 w.addheader("To", clientAddr)
206 w.addheader("Message-ID", twisted.mail.smtp.messageid())
207 if not subject.startswith("Re:"): subject = "Re: %s"%subject
208 w.addheader("Subject", subject)
209 if msgID:
210 w.addheader("In-Reply-To", msgID)
211 w.addheader("Date", twisted.mail.smtp.rfc822date())
212 body = w.startbody("text/plain")
214 if bridges:
215 answer = "".join(" %s\n" % b.getConfigLine() for b in bridges)
216 else:
217 answer = "(no bridges currently available)"
218 body.write(EMAIL_MESSAGE_TEMPLATE % answer)
220 f.seek(0)
221 return clientAddr, f
223 def replyToMail(lines, ctx):
224 """Given a list of lines from an incoming email message, and a
225 MailContext object, possibly send a reply.
227 logging.info("Got a completed email; attempting to reply.")
228 sendToUser, response = getMailResponse(lines, ctx)
229 if response is None:
230 return
231 response.seek(0)
232 d = Deferred()
233 factory = twisted.mail.smtp.SMTPSenderFactory(
234 ctx.smtpFromAddr,
235 sendToUser,
236 response,
238 reactor.connectTCP(ctx.smtpServer, ctx.smtpPort, factory)
239 logging.info("Sending reply to %r", sendToUser)
240 return d
242 class MailContext:
243 """Helper object that holds information used by email subsystem."""
244 def __init__(self, cfg, dist, sched):
245 # Reject any RCPT TO lines that aren't to this user.
246 self.username = "bridges"
247 # Reject any mail longer than this.
248 self.maximumSize = 32*1024
249 # Use this server for outgoing mail.
250 self.smtpServer = "127.0.0.1"
251 self.smtpPort = 25
252 # Use this address in the MAIL FROM line for outgoing mail.
253 self.smtpFromAddr = (cfg.EMAIL_SMTP_FROM_ADDR or
254 "bridges@torproject.org")
255 # Use this address in the "From:" header for outgoing mail.
256 self.fromAddr = (cfg.EMAIL_FROM_ADDR or
257 "bridges@torproject.org")
258 # An EmailBasedDistributor object
259 self.distributor = dist
260 # An IntervalSchedule object
261 self.schedule = sched
262 # The number of bridges to send for each email.
263 self.N = cfg.EMAIL_N_BRIDGES_PER_ANSWER
265 class MailMessage:
266 """Plugs into the Twisted Mail and receives an incoming message.
267 Once the message is in, we reply or we don't. """
268 implements(twisted.mail.smtp.IMessage)
270 def __init__(self, ctx):
271 """Create a new MailMessage from a MailContext."""
272 self.ctx = ctx
273 self.lines = []
274 self.nBytes = 0
275 self.ignoring = False
277 def lineReceived(self, line):
278 """Called when we get another line of an incoming message."""
279 self.nBytes += len(line)
280 if self.nBytes > self.ctx.maximumSize:
281 self.ignoring = True
282 else:
283 self.lines.append(line)
285 def eomReceived(self):
286 """Called when we receive the end of a message."""
287 if not self.ignoring:
288 replyToMail(self.lines, self.ctx)
289 return twisted.internet.defer.succeed(None)
291 def connectionLost(self):
292 """Called if we die partway through reading a message."""
293 pass
295 class MailDelivery:
296 """Plugs into Twisted Mail and handles SMTP commands."""
297 implements(twisted.mail.smtp.IMessageDelivery)
298 def setBridgeDBContext(self, ctx):
299 self.ctx = ctx
300 def receivedHeader(self, helo, origin, recipients):
301 #XXXX what is this for? what should it be?
302 return "Received: BridgeDB"
303 def validateFrom(self, helo, origin):
304 return origin
305 def validateTo(self, user):
306 if user.dest.local != self.ctx.username:
307 raise twisted.mail.smtp.SMTPBadRcpt(user)
308 return lambda: MailMessage(self.ctx)
310 class MailFactory(twisted.mail.smtp.SMTPFactory):
311 """Plugs into Twisted Mail; creates a new MailDelivery whenever we get
312 a connection on the SMTP port."""
313 def __init__(self, *a, **kw):
314 twisted.mail.smtp.SMTPFactory.__init__(self, *a, **kw)
315 self.delivery = MailDelivery()
317 def setBridgeDBContext(self, ctx):
318 self.ctx = ctx
319 self.delivery.setBridgeDBContext(ctx)
321 def buildProtocol(self, addr):
322 p = twisted.mail.smtp.SMTPFactory.buildProtocol(self, addr)
323 p.delivery = self.delivery
324 return p
326 def addSMTPServer(cfg, dist, sched):
327 """Set up a smtp server.
328 cfg -- a configuration object from Main. We use these options:
329 EMAIL_BIND_IP
330 EMAIL_PORT
331 EMAIL_N_BRIDGES_PER_ANSWER
332 EMAIL_DOMAIN_RULES
333 dist -- an EmailBasedDistributor object.
334 sched -- an IntervalSchedule object.
336 ctx = MailContext(cfg, dist, sched)
337 factory = MailFactory()
338 factory.setBridgeDBContext(ctx)
339 ip = cfg.EMAIL_BIND_IP or ""
340 reactor.listenTCP(cfg.EMAIL_PORT, factory, interface=ip)
341 return factory
343 def runServers():
344 """Start all the servers that we've configured. Exits when they do."""
345 reactor.run()