[Author Prev][Author Next][Thread Prev][Thread Next][Author Index][Thread Index]
[or-cvs] r21927: {} Restructure: Add setup files, move data files to data dir, b (in weather/trunk: . data lib lib/weather)
Author: kaner
Date: 2010-03-12 15:38:47 +0000 (Fri, 12 Mar 2010)
New Revision: 21927
Added:
weather/trunk/MANIFEST.in
weather/trunk/Weather.py
weather/trunk/data/
weather/trunk/data/favicon.ico
weather/trunk/data/stylesheet.css
weather/trunk/data/subscribe.template
weather/trunk/data/top-left.png
weather/trunk/data/top-middle.png
weather/trunk/data/top-right.png
weather/trunk/lib/
weather/trunk/lib/weather/
weather/trunk/lib/weather/__init__.py
weather/trunk/lib/weather/config.py
weather/trunk/lib/weather/constants.py
weather/trunk/lib/weather/poller.py
weather/trunk/lib/weather/queries.py
weather/trunk/lib/weather/torping.py
weather/trunk/setup.cfg
weather/trunk/setup.py
Removed:
weather/trunk/config.py
weather/trunk/favicon.ico
weather/trunk/poll.py
weather/trunk/stylesheet.css
weather/trunk/subscribe.template
weather/trunk/top-left.png
weather/trunk/top-middle.png
weather/trunk/top-right.png
weather/trunk/weather.py
Log:
Restructure: Add setup files, move data files to data dir, break up with the "one python file does it all" approach
Added: weather/trunk/MANIFEST.in
===================================================================
--- weather/trunk/MANIFEST.in (rev 0)
+++ weather/trunk/MANIFEST.in 2010-03-12 15:38:47 UTC (rev 21927)
@@ -0,0 +1,2 @@
+include README top-left.png top-middle.png top-right.png stylesheet.css subscribe.template
+
Copied: weather/trunk/Weather.py (from rev 21925, weather/trunk/weather.py)
===================================================================
--- weather/trunk/Weather.py (rev 0)
+++ weather/trunk/Weather.py 2010-03-12 15:38:47 UTC (rev 21927)
@@ -0,0 +1,254 @@
+#!/usr/bin/python
+
+import os
+import re
+import sys
+import DNS
+import base64
+import smtplib
+import socket
+from twisted.web import resource, static, server, http
+from twisted.enterprise import adbapi
+from twisted.internet import reactor
+from twisted.internet.task import LoopingCall
+from email.mime.multipart import MIMEMultipart
+from email.mime.base import MIMEBase
+from email.mime.text import MIMEText
+
+import weather.constants
+from weather.queries import CHECK_SUBS_Q, INSERT_SUBS_Q, CHECK_SUBS_AUTH_Q, ACK_SUB_Q, UNSUBSCRIBE_Q
+from weather.poller import WeatherPoller
+from weather.config import pollPeriod, URLbase
+
+class WeatherIndex(resource.Resource):
+ def render(self, request):
+ return open("subscribe.template").read()
+
+class SubscribeRequest(resource.Resource):
+ def __init__(self, dbConn):
+ self.dbConn = dbConn
+ resource.Resource.__init__(self)
+
+ def render(self, request):
+ self.request = request
+ self.email = request.args['email'][0]
+ self.node = request.args['node'][0]
+
+ if not self._checkMail():
+ return "Error: Bad email address '%s'" % self.email
+
+ self.subs_auth = self._getRandString()
+ self.unsubs_auth = self._getRandString()
+ self._isSubscribedAlready()
+ return server.NOT_DONE_YET
+
+ def _isSubscribedAlready(self):
+ dbQuery = CHECK_SUBS_Q % (self.email, self.node)
+ q = self.dbConn.runQuery(dbQuery)
+ q.addCallback(self._checkHasSubRet)
+ q.addErrback(self._errback)
+
+ def _checkHasSubRet(self, result):
+ # Do we already have a subscription for this address for this node?
+ if len(result) is not 0:
+ self.request.setResponseCode(http.OK)
+ self.request.write("Error: Already subscribed.")
+ self.request.finish()
+ else:
+ # Alright, subscribe it
+ return self._runSaveQuery()
+
+ def _runSaveQuery(self):
+ dbQuery = INSERT_SUBS_Q % (self.email, self.node, \
+ self.subs_auth, self.unsubs_auth)
+ q = self.dbConn.runOperation(dbQuery)
+ q.addCallback(self._saved)
+ q.addErrback(self._errback)
+ return q
+
+ def _saved(self, result):
+ url = URLbase + "/confirm-subscribe?auth=" + self.subs_auth
+ try:
+ self._sendConfirmationMail(url)
+ except Exception, e:
+ self.error = "Unknown error while sending confirmation mail." + \
+ "Please try again later." + \
+ "[Exception %s]" % sys.exc_info()[0]
+ self._rollBack()
+ return
+
+ self.request.setResponseCode(http.OK)
+ self.request.write(THANKS_OUT % self.email)
+ self.request.finish()
+
+ def _rollBack(self):
+ dbQuery = CHECK_SUBS_Q % (self.email, self.node)
+ q = self.dbConn.runQuery(dbQuery)
+ q.addCallback(self._errOut)
+ q.addErrback(self._errback)
+
+ def _errOut(self, result):
+ self.request.setResponseCode(http.INTERNAL_SERVER_ERROR)
+ self.request.write(self.error)
+ self.request.finish()
+
+ def _errback(self, failure):
+ self.error = "Error: %s" % (failure.getErrorMessage())
+ self._errOut()
+
+ def _sendConfirmationMail(self, url):
+ message = MIMEMultipart()
+ message['Subject'] = "Tor Weather Subscription Request"
+ message['To'] = self.email
+ message['From'] = mailFrom
+
+ messageText = CONFIRMATION_MAIL % (self.node, url)
+ text = MIMEText(messageText, _subtype="plain", _charset="utf-8")
+ # Add text part
+ message.attach(text)
+
+ # Try to send
+ smtp = smtplib.SMTP("localhost:25")
+ smtp.sendmail(mailFrom, self.email, message.as_string())
+ smtp.quit()
+
+ def _getRandString(self):
+ """Produce a random alphanumeric string for authentication"""
+ r = base64.urlsafe_b64encode(os.urandom(18))[:-1]
+ # some email clients don't like URLs ending in -
+ if r[-1] == "-":
+ r.replace("-", "x")
+ return r
+
+ def _checkMail(self):
+ # Unsure if this is enough
+ mailValidator = "^[a-zA-Z0-9._%-+]+@([a-zA-Z0-9._%-]+\\.[a-zA-Z]{2,6}$)"
+ mailOk = re.compile(mailValidator)
+ match = mailOk.match(self.email)
+ if match:
+ mailDomain = match.group(1)
+ return self._doDNSLookup(mailDomain)
+ else:
+ return False
+
+ def _doDNSLookup(self, mailDomain):
+ DNS.DiscoverNameServers()
+ querinator = DNS.Request(qtype='mx')
+ try:
+ dnsquery = querinator.req(mailDomain)
+ except DNS.DNSError, type:
+ if type == 'Timeout':
+ return False
+ else:
+ raise
+ if not dnsquery.answers:
+ # No DNS MX records for this domain
+ return False
+
+ return True
+
+ def _checkNode(self):
+ nodeOk = re.compile("(0x)?[a-fA-F0-9]{40}\Z")
+ if nodeOk.match(self.node):
+ return True
+ else:
+ return False
+
+class ConfirmSubscribeRequest(resource.Resource):
+ def __init__(self, dbConn):
+ self.dbConn = dbConn
+ resource.Resource.__init__(self)
+
+ def render(self, request):
+ self.subs_auth = request.args['auth'][0]
+ self._lookupSubsAuth(request)
+ return server.NOT_DONE_YET
+
+ def _lookupSubsAuth(self, request):
+ dbQuery = CHECK_SUBS_AUTH_Q % self.subs_auth
+ q = self.dbConn.runQuery(dbQuery)
+ q.addCallback(self._checkRet, request)
+ q.addErrback(self._errback, request)
+
+ def _checkRet(self, result, request):
+ if len(result) is 0:
+ request.setResponseCode(http.OK)
+ request.write("Error: No subscription with your auth code.")
+ request.finish()
+ else:
+ self.unsubs_auth = str(result[0][0])
+ return self._ackSubscription(request)
+
+ def _ackSubscription(self, request):
+ dbQuery = ACK_SUB_Q % self.subs_auth
+ q = self.dbConn.runQuery(dbQuery)
+ q.addCallback(self._subDone, request)
+ q.addErrback(self._errback, request)
+
+ def _subDone(self, result, request):
+ url = URLbase + "/unsubscribe?auth=" + self.unsubs_auth
+ link = "<a href=\"" + url + "\">" + url + "</a>"
+ request.write("<p>Subscription finished. Thank you very much.")
+ request.write("You can unsubscribe anytime with the following link: ")
+ request.write(link)
+ #request.write(URLbase + "/unsubscribe?auth=" + self.unsubs_auth)
+ request.write("</p>")
+ request.finish()
+
+ def _errback(self, failure, request):
+ request.setResponseCode(http.INTERNAL_SERVER_ERROR)
+ request.write("Error: %s" % (failure.getErrorMessage()))
+ request.finish()
+
+class UnsubscribeRequest(resource.Resource):
+ def __init__(self, dbConn):
+ self.dbConn = dbConn
+ resource.Resource.__init__(self)
+
+ def render(self, request):
+ self.unsubs_auth = request.args['auth'][0]
+ self._deleteSub(request)
+ return server.NOT_DONE_YET
+
+ def _deleteSub(self, request):
+ dbQuery = UNSUBSCRIBE_Q % self.unsubs_auth
+ q = self.dbConn.runQuery(dbQuery)
+ q.addCallback(self._deleteDone, request)
+ q.addErrback(self._errback, request)
+
+ def _deleteDone(self, result, request):
+ request.setResponseCode(http.OK)
+ request.write("Subscription deleted. Goodbye.")
+ request.finish()
+
+ def _errback(self, failure, request):
+ request.setResponseCode(http.INTERNAL_SERVER_ERROR)
+ request.write("Error: %s" % (failure.getErrorMessage()))
+ request.finish()
+
+class RootResource(resource.Resource):
+ def __init__(self, dbConn):
+ resource.Resource.__init__(self)
+ self.putChild('top-left.png', static.File("./top-left.png"))
+ self.putChild('top-middle.png', static.File("./top-middle.png"))
+ self.putChild('top-right.png', static.File("./top-right.png"))
+ self.putChild('stylesheet.css', static.File("./stylesheet.css"))
+ self.putChild('', WeatherIndex())
+ self.putChild('subscribe', SubscribeRequest(dbConn))
+ self.putChild('confirm-subscribe', ConfirmSubscribeRequest(dbConn))
+ self.putChild('unsubscribe', UnsubscribeRequest(dbConn))
+
+def main():
+ # Set up database connection
+ dbConn = adbapi.ConnectionPool("sqlite3", "subscriptions.db")
+ # Set up polling timer
+ weatherPoller = WeatherPoller(dbConn)
+ pollTimer = LoopingCall(weatherPoller.poller)
+ pollTimer.start(pollPeriod)
+ # Set up webserver
+ weatherSite = server.Site(RootResource(dbConn))
+ reactor.listenTCP(8000, weatherSite)
+ reactor.run()
+
+if __name__ == "__main__":
+ main()
Deleted: weather/trunk/config.py
===================================================================
--- weather/trunk/config.py 2010-03-12 15:33:26 UTC (rev 21926)
+++ weather/trunk/config.py 2010-03-12 15:38:47 UTC (rev 21927)
@@ -1,20 +0,0 @@
-#!/usr/bin/env python2.5
-
-URLbase = "https://weather.torproject.org"
-
-weather_storage = "/var/lib/torweather/"
-
-authenticator = open(weather_storage + "auth_token").read().strip()
-
-weather_email = "tor-ops@xxxxxxxxxxxxxx"
-
-# these respond to pings (for now!) and are geographically dispersed
-
-ping_targets = ["google.com", "telstra.com.au", "yahoo.co.uk"]
-
-failure_threshold = 4 # this number of failures in a row counts as being
- # down
-
-poll_period = 1800 # try to wait this number of seconds in between polling
-
-apache_fcgi = True # set this if we're trying to operate in an apache / fastCGI
Copied: weather/trunk/data/favicon.ico (from rev 21335, weather/trunk/favicon.ico)
===================================================================
(Binary files differ)
Copied: weather/trunk/data/stylesheet.css (from rev 21335, weather/trunk/stylesheet.css)
===================================================================
--- weather/trunk/data/stylesheet.css (rev 0)
+++ weather/trunk/data/stylesheet.css 2010-03-12 15:38:47 UTC (rev 21927)
@@ -0,0 +1,382 @@
+body {
+ background-color: #FFFFFF;
+ margin-top: 0px;
+ font-family: Arial, Helvetica, sans-serif;
+ font-size: 1em;
+ font-style: normal;
+ color: #000000;
+ padding-top: 0px;
+}
+
+/* images */
+
+img {
+ border: 0;
+}
+
+
+li {
+ margin: .2em .2em .2em 1em;
+
+}
+
+/* this centers the page */
+
+.center {
+ text-align: center;
+ background-color: white;
+ margin: 0px auto 0 auto;
+ width: 85%;
+}
+
+.center table {
+ margin-left: auto;
+ margin-right: auto;
+ text-align: left;
+}
+
+/* for the shadow box */
+
+table.shadowbox {
+ width: 788px;
+ border-collapse: collapse;
+ padding: 0;
+ margin-bottom: 2em;
+}
+
+table.shadowbox td {
+ margin: 0;
+ padding: 0;
+}
+
+/* spacer */
+
+td.spacer {
+ width: 110px;
+}
+
+
+
+div.banner {
+ text-align: center;
+ height: 79px;
+ margin-bottom: 10px;
+ width:100%;
+}
+
+table.table-banner {
+ margin: 0 auto 0 auto;
+ background-image: url("tor_mast.gif");
+ background-repeat: no-repeat;
+}
+
+
+
+
+div.bottom {
+ font-size: 0.8em;
+ margin-top: 2cm;
+ margin-left: 1em;
+ margin-right: 1em;
+ text-align: right;
+}
+
+/* the sidebar */
+
+div.sidebar {
+ float: right;
+ padding-top: 10px;
+ padding-right: 10px;
+ padding-bottom: 15px;
+ padding-left: 10px;
+ width: 260px;
+ text-align: center;
+}
+
+
+/* The main column (left text) */
+
+div.main-column {
+ padding: 15px 0 10px 10px;
+ text-indent: 0pt;
+ font-size: 1em;
+ text-align: left;
+}
+
+/* formatting styles */
+
+h1 {
+ font-size: 1.6em;
+ margin-bottom: 0.5em;
+}
+
+h2 {
+ font-size: 1.4em;
+ margin-bottom: 0em;
+ font-weight: bold;
+ margin-top: 0;
+}
+
+h3 {
+ font-size: 1.2em;
+ margin-bottom: 0em;
+ font-weight: bold;
+ margin-top: 0;
+}
+
+h4 {
+ font-size: 1.1em;
+ margin-bottom: 0em;
+ font-weight: bold;
+ margin-top: 0;
+}
+
+h5 {
+ font-size: 1.0em;
+ margin-bottom: 0em;
+ font-weight: bold;
+ margin-top: 0;
+}
+
+p {
+ margin-top: 0;
+ margin-bottom: 1em;
+}
+
+a:link {
+ color: blue;
+ font-size: 1em;
+}
+
+a:visited {
+ color: purple;
+ font-size: 1em;
+}
+
+
+a.anchor:link {
+ font-size: 1em;
+ color: black;
+ font-weight: bold;
+ text-decoration: none;
+}
+
+a.anchor:visited {
+ font-size: 1em;
+ color: black;
+ font-weight: bold;
+ text-decoration: none;
+}
+
+a.anchor {
+ font-size: 1em;
+ color: black;
+ font-weight: bold;
+ text-decoration: none;
+}
+
+td {
+ vertical-align: top;
+}
+
+a.smalllink {
+ font-size: 0.8em;
+}
+
+/* the banner */
+
+table.banner {
+ width: 100%;
+ height: 79px;
+ margin-left: auto;
+ margin-right: auto;
+}
+
+td.banner-left {
+ /* This is done with an <img> in the HTML so it can be clickable
+ background-image: url("top-left.png");
+ background-repeat: no-repeat; */
+ width: 193px;
+}
+
+td.banner-middle {
+ background-color: #00802B;
+ background-image: url("top-middle.png");
+ background-repeat: repeat-x;
+ vertical-align: bottom;
+ padding-bottom: 10px;
+ color: white;
+ font-weight: bold;
+ font-size: 1em;
+}
+
+td.banner-middle a, td.banner-middle a:visited {
+ color: white;
+ font-weight: bold;
+ font-size: 1em;
+}
+
+td.banner-middle a:hover {
+ color: #FF7F00;
+ font-weight: bold;
+ font-size: 1em;
+}
+
+td.banner-right {
+ background-image: url("top-right.png");
+ background-repeat: no-repeat;
+ width: 150px;
+ background-position: right;
+ padding-top: 8px;
+}
+
+.banner-middle a.current {
+ text-decoration: none;
+ color: #FF7F00;
+ font-weight: bold;
+ font-size: 1em;
+ width: auto;
+ text-align: auto;
+ left: -50px;
+}
+
+.donatebutton {
+ width: auto;
+ text-align: center;
+}
+
+.donatebutton a {
+ margin: 10px 0 0 0;
+ font-weight: bold;
+ display: block;
+ padding: 6px;
+ background-color: #00802B;
+ border-top: 1px solid #00A838;
+ border-left: 1px solid #00A838;
+ border-bottom: 1px solid #00591E;
+ border-right: 1px solid #00591E;
+ color: #FFFFFF;
+}
+
+.donatebutton a:hover {
+ color: orange;
+}
+
+.donatebutton a:active {
+ color: orange;
+}
+
+/* these styles are for the menu on the gui contest pages */
+
+.guileft {
+ width: 25%;
+ float: left;
+ padding: 0;
+ margin: 0;
+}
+
+.guimenu {
+ border: 1px solid #AAA6AB;
+ background-color: #E2DFE3;
+ margin: 0 15px 15px 0;
+ padding: 0;
+}
+
+.guimenuinner a {
+ display: block;
+ text-decoration: none;
+ padding: 2px 0px 0px 12px;
+ margin: 0 0 0 0px;
+ color: #333333;
+}
+
+.guimenuinner a:visited {
+ color: #333333;
+}
+
+.guimenuinner a:hover {
+ background-image: url(gui/img/arrow.png);
+ background-repeat: no-repeat;
+ background-position: left;
+ color: #EF8012;
+}
+
+.guimenuinner a.on {
+ background-image: url(gui/img/arrow.png);
+ background-repeat: no-repeat;
+ background-position: left;
+ color: #EF8012;
+}
+
+
+.guimenu h1 {
+ width: 85%;
+ font-size: 16px;
+ margin: 0 0 8px 0;
+ padding: 0;
+ border-bottom: 1px solid #AAA6AB;
+}
+
+.curveleft {
+ background-image: url(gui/img/corner-topleft.png);
+ background-repeat: no-repeat;
+ background-position: top left;
+ margin: -1px;
+}
+
+.curveright {
+ background-image: url(gui/img/corner-topright.png);
+ background-repeat: no-repeat;
+ background-position: top right;
+}
+
+.guimenuinner {
+
+ padding: 0 10px 0 10px;
+}
+
+//.wiki {
+ padding: 5px 40px 0 0;
+ display: block;
+ text-align: right;
+}
+
+
+.curvebottomleft {
+ background-image: url(gui/img/corner-bottomleft.png);
+ background-repeat: no-repeat;
+ background-position: bottom left;
+ margin: -1px;
+}
+
+.curvebottomright {
+ background-image: url(gui/img/corner-bottomright.png);
+ background-repeat: no-repeat;
+ background-position: bottom right;
+}
+
+
+table.mirrors {
+ margin: 0 auto;
+ border-width: 3px;
+ border-color: gray;
+ border-style: ridge;
+ border-collapse: collapse;
+}
+table.mirrors th {
+ border: 1px solid gray;
+ background-color: #DDDDDD;
+}
+table.mirrors td {
+ border: 1px solid gray;
+ padding: 4px;
+}
+
+acronym {
+ border-bottom: none;
+}
+
+dt {
+ font-weight: bolder;
+ font-style: italic;
+}
Copied: weather/trunk/data/subscribe.template (from rev 21335, weather/trunk/subscribe.template)
===================================================================
--- weather/trunk/data/subscribe.template (rev 0)
+++ weather/trunk/data/subscribe.template 2010-03-12 15:38:47 UTC (rev 21927)
@@ -0,0 +1,61 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
+<html>
+<head>
+ <title>Tor Weather</title>
+ <link rel="stylesheet" type="text/css" href="./stylesheet.css">
+</head>
+<body>
+<div class="center">
+<table class="banner" border="0" cellpadding="0" cellspacing="0" summary="">
+ <tr>
+
+ <td class="banner-left"><a href="https://www.torproject.org/"><img src="top-left.png" alt="Click to go to home page" width="193" height="79"></a></td>
+ <td class="banner-middle">
+
+ </td>
+ <td class="banner-right">
+ </td>
+ </tr>
+</table>
+<div class="main-column">
+<h2>Tor Weather</h2>
+
+<hr>
+
+<h3>Sign Up!</h3>
+
+<br>
+<form method="post" action="/subscribe">
+You can use this form to request status updates to tell you when a particular
+Tor node has become unreachable for a sustained period of time.
+<br><br>
+
+<p>
+Email:<br>
+<input type="text" name="email" size="50" maxlength="255" value="Enter one email address" onclick="if (this.value == 'Enter one email address') {this.value = ''}" />
+</p><p>
+Node fingerprint:<br>
+<input type="text" name="node" size="50" maxlength="255" value="Enter one Tor node ID" onclick="if (this.value == 'Enter one Tor node ID') {this.value = ''}" />
+</p><p>
+<input type="submit" class="submit" value="Subscribe to Tor Weather" name="sa"/>
+</p>
+<hr>
+<h3>FAQ</h3>
+<br>
+<p>Q: <b>Where can I find the fingerprint for my server?</b></p>
+<p>A: <i>Often your node fingerprint can be found on unix-like machines in the file: <tt>/var/lib/tor/fingerprint</tt></i></p>
+<p>Q: <b>Will I be overloaded with alerts that I cannot suppress?</b></p>
+<p>A: <i>No.</i>
+<p>Q: <b>Can I unsubscribe easily?</b></p>
+<p>A: <i>Yes.</i>
+<p>Q: <b>I'm having a problem, can you help me?</b></p>
+<p>A: <i>Yes. Send an email to <tt>tor-weather @ torproject dot org</tt></i>
+<br>
+<hr>
+<br>
+<p><i>Please note that while we won't ever intentionally publish them, the address/node pairs sent to this server are not protected against SMTP eavesdropping, hacking, or lawyers.</i>
+</form>
+</div>
+</div>
+</body>
+</html>
Copied: weather/trunk/data/top-left.png (from rev 21335, weather/trunk/top-left.png)
===================================================================
(Binary files differ)
Copied: weather/trunk/data/top-middle.png (from rev 21335, weather/trunk/top-middle.png)
===================================================================
(Binary files differ)
Copied: weather/trunk/data/top-right.png (from rev 21335, weather/trunk/top-right.png)
===================================================================
(Binary files differ)
Deleted: weather/trunk/favicon.ico
===================================================================
(Binary files differ)
Added: weather/trunk/lib/weather/__init__.py
===================================================================
--- weather/trunk/lib/weather/__init__.py (rev 0)
+++ weather/trunk/lib/weather/__init__.py 2010-03-12 15:38:47 UTC (rev 21927)
@@ -0,0 +1 @@
+# :)
Copied: weather/trunk/lib/weather/config.py (from rev 21335, weather/trunk/config.py)
===================================================================
--- weather/trunk/lib/weather/config.py (rev 0)
+++ weather/trunk/lib/weather/config.py 2010-03-12 15:38:47 UTC (rev 21927)
@@ -0,0 +1,16 @@
+#!/usr/bin/env python2.5
+
+URLbase = "https://weather.torproject.org"
+
+authenticator = open("auth_token").read().strip()
+
+mailFrom = "tor-ops@xxxxxxxxxxxxxx"
+
+# these respond to pings (for now!) and are geographically dispersed
+
+pingTargets = ["google.com", "telstra.com.au", "yahoo.co.uk"]
+
+failureThreshold = 4 # this number of failures in a row counts as being
+ # down
+
+pollPeriod = 10 # try to wait this number of seconds in between polling
Added: weather/trunk/lib/weather/constants.py
===================================================================
--- weather/trunk/lib/weather/constants.py (rev 0)
+++ weather/trunk/lib/weather/constants.py 2010-03-12 15:38:47 UTC (rev 21927)
@@ -0,0 +1,37 @@
+#!/usr/bin/python
+# Some constants needed in weather
+
+THANKS_OUT = """
+ Thanks for using tor weather. A confirmation request has been sent to '%s'.
+"""
+
+REPORT_MAIL = """
+ This is a Tor Weather report.
+
+ It appears that a tor node you elected to monitor,
+
+ (node id: %s)
+
+ has been uncontactable through the Tor network for a while. You may wish
+ to look at it to see why. The last error message from our code while
+ trying to contact it is included below. You may or may not find it helpful!
+
+ (You can unsubscribe from these reports at any time by visiting the
+ following url:
+
+ %s )
+"""
+
+CONFIRMATION_MAIL = """
+ Dear human, this is the Tor Weather Report system.
+
+ Somebody (possibly you) has requested that status monitoring information
+ about a tor node (id: %s) be sent to this email address.
+
+ If you wish to confirm this request, please visit the following link:
+
+ %s
+
+ If you do *not* wish to receive Tor Weather Reports, you do not need to do
+ anything.
+"""
Added: weather/trunk/lib/weather/poller.py
===================================================================
--- weather/trunk/lib/weather/poller.py (rev 0)
+++ weather/trunk/lib/weather/poller.py 2010-03-12 15:38:47 UTC (rev 21927)
@@ -0,0 +1,76 @@
+#!/usr/bin/python
+
+import smtplib
+
+from email.mime.multipart import MIMEMultipart
+from email.mime.base import MIMEBase
+from email.mime.text import MIMEText
+
+from twisted.web import server
+from weather.queries import GETALL_SUBS_Q
+from weather.torping import TorPing
+from weather.config import URLbase, mailFrom
+from weather.constants import REPORT_MAIL
+
+class WeatherPoller():
+ def __init__(self, dbConn):
+ self.dbConn = dbConn
+
+ def poller(self):
+ print "Polling.."
+ self._checkAll()
+ return server.NOT_DONE_YET
+
+ def _checkAll(self):
+ dbQuery = GETALL_SUBS_Q
+ q = self.dbConn.runQuery(dbQuery)
+ q.addCallback(self._checkRet)
+ q.addErrback(self._errBack)
+
+ def _checkRet(self, resultList):
+ # Loop through result list and check each node
+ for result in resultList:
+ print "Result: ", result
+ checkHost = result[2]
+ if not self._checkHost(checkHost):
+ print "Server %s seems to be offline" % checkHost
+ self._handleOfflineNode(result)
+ else:
+ print "Server %s is ok" % checkHost
+
+ def _errBack(self, failure):
+ print "Error: ", failure.getErrorMessage()
+
+ def _checkHost(self, hostID):
+ print "Checking host %s" % hostID
+ torPing = TorPing()
+ return torPing.ping(hostID)
+
+ def _handleOfflineNode(self, dbRow):
+ # Log, mail
+ if self._decideNotice(dbRow):
+ self._sendNotice(dbRow)
+
+ def _decideNotice(self, dbRow):
+ # This is just a placeholder for now. We'll decide later what
+ # conditions we want to check
+ return True
+
+ def _sendNotice(self, dbRow):
+ nodeId = dbRow[2]
+ unsubsURL = URLbase + "/unsubscribe?auth=" + str(dbRow[4])
+ message = MIMEMultipart()
+ message['Subject'] = "Tor Weather Subscription Request"
+ message['To'] = dbRow[1]
+ message['From'] = mailFrom
+
+ messageText = REPORT_MAIL % (nodeId, unsubsURL)
+ text = MIMEText(messageText, _subtype="plain", _charset="ascii")
+ # Add text part
+ message.attach(text)
+
+ # Try to send
+ smtp = smtplib.SMTP("localhost:25")
+ smtp.sendmail(mailFrom, dbRow[1], message.as_string())
+ smtp.quit()
+
Added: weather/trunk/lib/weather/queries.py
===================================================================
--- weather/trunk/lib/weather/queries.py (rev 0)
+++ weather/trunk/lib/weather/queries.py 2010-03-12 15:38:47 UTC (rev 21927)
@@ -0,0 +1,12 @@
+#!/usr/bin/python
+
+# Query strings
+CHECK_SUBS_Q = "SELECT id FROM subscriptions WHERE email='%s' and node='%s'"
+INSERT_SUBS_Q = """
+ INSERT INTO subscriptions (email, node, subs_auth, unsubs_auth, subscribed) VALUES ('%s', '%s', '%s', '%s', 0)
+"""
+CHECK_SUBS_AUTH_Q = "SELECT unsubs_auth FROM subscriptions WHERE subs_auth='%s'"
+ACK_SUB_Q = "UPDATE subscriptions SET subscribed=1 WHERE subs_auth='%s'"
+UNSUBSCRIBE_Q = "DELETE from subscriptions where unsubs_auth='%s'"
+GETALL_SUBS_Q = "SELECT * from subscriptions WHERE subscribed=1"
+
Added: weather/trunk/lib/weather/torping.py
===================================================================
--- weather/trunk/lib/weather/torping.py (rev 0)
+++ weather/trunk/lib/weather/torping.py 2010-03-12 15:38:47 UTC (rev 21927)
@@ -0,0 +1,52 @@
+#!/usr/bin/python
+# Taken from the old weather code
+
+import socket
+from TorCtl import TorCtl
+from weather.config import authenticator
+
+debugfile = open("debug", "w")
+
+class TorPing:
+ "Check to see if various tor nodes respond to SSL hanshakes"
+ def __init__(self, control_host = "127.0.0.1", control_port = 9051):
+ "Keep the connection to the control port lying around"
+ self.control_host = control_host
+ self.control_port = control_port
+ self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ self.sock.connect((control_host,control_port))
+ self.control = TorCtl.Connection(self.sock)
+ self.control.authenticate(authenticator)
+ self.control.debug(debugfile)
+
+ def __del__(self):
+ self.sock.close()
+ del self.sock
+ self.sock = None # prevents double deletion exceptions
+
+ # it would be better to fix TorCtl!
+ try:
+ self.control.close()
+ except:
+ pass
+
+ del self.control
+ self.control = None
+
+ def ping(self, nodeId):
+ "Let's see if this tor node is up by only asking Tor."
+ try:
+ info = self.control.get_info(str("ns/id/" + nodeId))
+ except TorCtl.ErrorReply:
+ # If we're getting here, we're likely seeing:
+ # ErrorReply: 552 Unrecognized key "ns/id/46D9..."
+ # This means that the node isn't recognized by
+ # XXX: Log
+ return False
+
+ except:
+ # XXX: Log
+ return False
+
+ # If we're here, we were able to fetch information about the router
+ return True
Deleted: weather/trunk/poll.py
===================================================================
--- weather/trunk/poll.py 2010-03-12 15:33:26 UTC (rev 21926)
+++ weather/trunk/poll.py 2010-03-12 15:38:47 UTC (rev 21927)
@@ -1,285 +0,0 @@
-#!/usr/bin/env python2.5
-import socket
-import sys
-import os
-import gdbm
-import re
-import time
-import threading
-from datetime import datetime
-from traceback import print_exception
-from subprocess import Popen, PIPE
-import TorCtl.TorCtl as TorCtl
-
-from config import authenticator, URLbase, weather_email, failure_threshold
-from config import poll_period, ping_targets, weather_storage
-from weather import parse_subscriptions
-
-# Lets debug this
-import traceback
-
-debug = 0
-
-
-debugfile = open(weather_storage + "/torctl-debug","w")
-class TorPing:
- "Check to see if various tor nodes respond to SSL hanshakes"
- def __init__(self, control_host = "127.0.0.1", control_port = 9051):
- "Keep the connection to the control port lying around"
- self.control_host = control_host
- self.control_port = control_port
- self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- self.sock.connect((control_host,control_port))
- self.control = TorCtl.Connection(self.sock)
- self.control.authenticate(authenticator)
- self.control.debug(debugfile)
-
- def __del__(self):
- self.sock.close()
- del self.sock
- self.sock = None # prevents double deletion exceptions
-
- # it would be better to fix TorCtl!
- try:
- self.control.close()
- except:
- pass
-
- del self.control
- self.control = None
-
- def ping(self, node_id):
- "Let's see if this tor node is up by only asking Tor."
- string = "ns/id/" + node_id
- try:
- info = self.control.get_info(string)
- except TorCtl.ErrorReply:
- # If we're getting here, we're likely seeing:
- # ErrorReply: 552 Unrecognized key "ns/id/46D9..."
- # This means that the node isn't recognized by
- x = traceback.format_exc()
- fh = file("/tmp/tor-ctl-failed-ping-log", "a")
- fh.write(x)
- fh.close()
- info = None
- return False
-
- except:
- # Remove this, it's a hack to debug this specific bug
- x = traceback.format_exc()
- fh = file("/tmp/misc-failed-ping-log", "a")
- fh.write(x)
- fh.close()
- info = None
- return False
-
- # If we're here, we were able to fetch information about the router
- return True
-
- # info looks like this:
- # {'ns/id/FFCB46DB1339DA84674C70D7CB586434C4370441': 'r moria1 /8tG2xM52oRnTHDXy1hkNMQ3BEE pavoLDqxMvw+T1VHR5hmmgpr9self 2007-10-10 21:12:08 128.31.0.34 9001 9031\ns Authority Fast Named Running Valid V2Dir\n'}
- ##ip,port = info[string].split()[6:8]
- # throw exceptions like confetti if this isn't legit
- ##socket.inet_aton(ip)
- # ensure port is a kosher string
- ##assert 0 < int(port) < 65536
-
- ##if debug: print "contacting node at %s:%s" % (ip,port)
-
- # XXX check: could non-blocking io be used to make us safe from
- # answer-very-slowly DOSes? or do we need to spawn threads here?
-
- ##cmd = ["openssl", "s_client", "-connect", ip + ':' + port]
- ##ssl_handshake = Popen( args = cmd, stdout = PIPE, stderr = PIPE, stdin=PIPE)
- ##ssl_handshake.stdin.close()
- ##safe_from_DOS = 10000 # moria1's response is ~1500 chars long
- ##output = ssl_handshake.stdout.read(safe_from_DOS)
- ##n = output.find("Server public key is 1024 bit")
- ##if n > 0:
- ## return True
- ##else:
- ## return False
-
- def test(self):
- "Check that the connection to the Tor Control port is still okay."
- try:
- self.control.get_info("version")
- return True
- except:
- if debug: print "Respawning control port connection..."
- self.__del__()
- try:
- self.__init__(self.control_host, self.control_port)
- return True
- except:
- if debug: print "Respawn failed"
- return False
-
-
-report_text = \
-"""This is a Tor Weather report.
-
-It appears that a tor node you elected to monitor,
-
-(node id: %s)
-
-has been uncontactable through the Tor network for a while. You may wish
-to look at it to see why. The last error message from our code while trying to
-contact it is included below. You may or may not find it helpful!
-
-(You can unsubscribe from these reports at any time by visiting the
-following url:
-
-%s )
-
-The last error message was as follows:
---------------------------------------
-%s"""
-
-class WeatherPoller(threading.Thread):
- "This thread sits around, checking to see if tor nodes are up."
-
- def __init__(self, subscriptions, failures, lock):
- self.gdbm_lock = lock
- self.subscriptions = subscriptions
- self.failure_counts = failures
- self.failure_counts.reorganize() # just in case
- if debug:
- print "failure counts"
- for node in self.failure_counts.keys():
- print node, self.failure_counts[node]
- self.tp = TorPing()
- threading.Thread.__init__(self)
-
- def run(self):
- "Keep polling nodes... forever."
- while True:
- stamp = time.time()
- self.ping_all()
- offset = time.time() - stamp
- if offset < poll_period:
- time.sleep(poll_period - offset)
-
- def ping_all(self):
- if debug: print "starting a new round of polls"
- #self.tp = TorPing()
- if not self.tp.test():
- return False
- print 'Timestamp', datetime.now().isoformat('-')
-
- try:
- self.gdbm_lock.acquire()
- node = self.subscriptions.firstkey()
- while node != None:
- # nodes stay in the subscription db even if nobody is subscribed to them
- # anymore
- if self.subscriptions[node] != "":
- #self.gdbm_lock.release()
- try:
- self.ping(node) # this is time consuming ; don't hold the lock
- finally:
- print "pinging node finished "
- # self.gdbm_lock.acquire()
- node = self.subscriptions.nextkey(node)
-
- finally:
- self.gdbm_lock.release()
- if debug:
- print "Ending the last round of polls"
- print 'Timestamp', datetime.now().isoformat('-')
-
- #del self.tp # this minimises the chance of confusion a local tor control
- # port crash with a remote node being down
- if debug:
- print "Ping_all finished"
- print "Ending the last round of polls"
- print 'Timestamp', datetime.now().isoformat('-')
-
- def ping(self, node):
- if debug: print "pinging", node
- try:
- assert self.tp.ping(node)
- # Okay we can see this node. Zero its count, if it has one
- if debug: print node, "is okay"
- try:
- if int(self.failure_counts[node]) != 0:
- self.failure_counts[node] = "0"
- except KeyError:
- pass
- except AssertionError:
- # for /some/ reason, we can't contact this tor node
- ex1,ex2,ex3 = sys.exc_info()
- if self.internet_looks_okay():
- # But we can ping the net. That's bad.
- # This doesn't work as you might expect, it returned None
- #reason = print_exception(ex1,ex2,ex3)
- reason = "Unable to ping node"
- if (debug):
- print "logging a strike against node", node, "because of:"
- print reason
- self.strike_against(node, reason)
- else:
- if (debug):
- print "I would have concluded that tor node", node, "was down;"
- print "The problem looked like this:"
- print print_exception(ex1,ex2,ex3)
- print "But I couldn't ping %s!" % (self.ping_failure)
-
- good_ping = re.compile("0% packet loss")
-
- def internet_looks_okay(self):
- cmd = ["ping", "-c", "3", "x"]
- pings = []
- for host in ping_targets:
- cmd[3] = host
- pings.append((Popen(args=cmd,stdout=PIPE,stdin=PIPE,stderr=PIPE), host))
- for ping,host in pings:
- output = ping.stdout.read()
- ping.stdin.close()
- if not self.good_ping.search(output):
- self.ping_failure = host
- return False
- return True
-
- def strike_against(self, node, reason):
- "Increment the failure count for this node"
- # gdbm is string based
- if not self.failure_counts.has_key(node):
- self.failure_counts[node] = "1"
- else:
- count = int(self.failure_counts[node]) + 1
- self.failure_counts[node] = "%d" %(count)
- if count == failure_threshold:
- self.send_failure_email(node, reason)
-
- def send_failure_email(self, node, reason):
- import smtplib
- from email.mime.text import MIMEText
-
- # Send the message via our own SMTP server, but don"t include the
- # envelope header.
- s = smtplib.SMTP()
- s.connect()
- self.gdbm_lock.acquire()
- try:
- list = parse_subscriptions(node,self.subscriptions)
- finally:
- self.gdbm_lock.release()
- for address, unsub_token in list:
-
- unsub_url = URLbase+"/unsubscribe/" + unsub_token
- msg= MIMEText(report_text % (node, unsub_url, reason))
-
- sender = weather_email
- msg["Subject"] = "Tor weather report"
- msg["From"] = sender
- msg["To"] = address
- msg["List-Unsubscribe"] = unsub_url
- s.sendmail(sender, [address], msg.as_string())
- s.close()
-
-def ping_test():
- x = NodePoller()
- print x.internet_looks_okay()
- x.send_failure_email()
-
Added: weather/trunk/setup.cfg
===================================================================
--- weather/trunk/setup.cfg (rev 0)
+++ weather/trunk/setup.cfg 2010-03-12 15:38:47 UTC (rev 21927)
@@ -0,0 +1,4 @@
+[install]
+install-purelib=~/opt/weather
+install-scripts=~/opt/weather
+install-data=~/opt/weather
Added: weather/trunk/setup.py
===================================================================
--- weather/trunk/setup.py (rev 0)
+++ weather/trunk/setup.py 2010-03-12 15:38:47 UTC (rev 21927)
@@ -0,0 +1,32 @@
+#!/usr/bin/python
+# (c) 2009 The Tor project
+# Tor weather installer & packer
+
+
+from distutils.core import setup
+
+setup(name='Weather',
+ version='0.1',
+ description='Weather checks if subscribed nodes are available',
+ author='Jacob Appelbaum, Christian Fromme',
+ author_email='jacob at appelbaum dot net, kaner at strace dot org',
+ url='https://weather.torproject.org/',
+ package_dir={'': 'lib',},
+ packages=['weather'],
+ data_files = [('', ['data/top-left.png',
+ 'data/top-middle.png',
+ 'data/top-right.png',
+ 'data/stylesheet.css',
+ 'data/subscribe.template',
+ 'data/favicon.ico']),
+ ('TorCtl',['TorCtl/PathSupport.py',
+ 'TorCtl/ScanSupport.py',
+ 'TorCtl/SQLSupport.py',
+ 'TorCtl/StatsSupport.py',
+ 'TorCtl/TorCtl.py',
+ 'TorCtl/TorUtil.py',
+ 'TorCtl/__init__.py'
+ ])],
+ scripts = ["Weather.py"],
+ long_description = """Really long text here."""
+ )
Deleted: weather/trunk/stylesheet.css
===================================================================
--- weather/trunk/stylesheet.css 2010-03-12 15:33:26 UTC (rev 21926)
+++ weather/trunk/stylesheet.css 2010-03-12 15:38:47 UTC (rev 21927)
@@ -1,382 +0,0 @@
-body {
- background-color: #FFFFFF;
- margin-top: 0px;
- font-family: Arial, Helvetica, sans-serif;
- font-size: 1em;
- font-style: normal;
- color: #000000;
- padding-top: 0px;
-}
-
-/* images */
-
-img {
- border: 0;
-}
-
-
-li {
- margin: .2em .2em .2em 1em;
-
-}
-
-/* this centers the page */
-
-.center {
- text-align: center;
- background-color: white;
- margin: 0px auto 0 auto;
- width: 85%;
-}
-
-.center table {
- margin-left: auto;
- margin-right: auto;
- text-align: left;
-}
-
-/* for the shadow box */
-
-table.shadowbox {
- width: 788px;
- border-collapse: collapse;
- padding: 0;
- margin-bottom: 2em;
-}
-
-table.shadowbox td {
- margin: 0;
- padding: 0;
-}
-
-/* spacer */
-
-td.spacer {
- width: 110px;
-}
-
-
-
-div.banner {
- text-align: center;
- height: 79px;
- margin-bottom: 10px;
- width:100%;
-}
-
-table.table-banner {
- margin: 0 auto 0 auto;
- background-image: url("tor_mast.gif");
- background-repeat: no-repeat;
-}
-
-
-
-
-div.bottom {
- font-size: 0.8em;
- margin-top: 2cm;
- margin-left: 1em;
- margin-right: 1em;
- text-align: right;
-}
-
-/* the sidebar */
-
-div.sidebar {
- float: right;
- padding-top: 10px;
- padding-right: 10px;
- padding-bottom: 15px;
- padding-left: 10px;
- width: 260px;
- text-align: center;
-}
-
-
-/* The main column (left text) */
-
-div.main-column {
- padding: 15px 0 10px 10px;
- text-indent: 0pt;
- font-size: 1em;
- text-align: left;
-}
-
-/* formatting styles */
-
-h1 {
- font-size: 1.6em;
- margin-bottom: 0.5em;
-}
-
-h2 {
- font-size: 1.4em;
- margin-bottom: 0em;
- font-weight: bold;
- margin-top: 0;
-}
-
-h3 {
- font-size: 1.2em;
- margin-bottom: 0em;
- font-weight: bold;
- margin-top: 0;
-}
-
-h4 {
- font-size: 1.1em;
- margin-bottom: 0em;
- font-weight: bold;
- margin-top: 0;
-}
-
-h5 {
- font-size: 1.0em;
- margin-bottom: 0em;
- font-weight: bold;
- margin-top: 0;
-}
-
-p {
- margin-top: 0;
- margin-bottom: 1em;
-}
-
-a:link {
- color: blue;
- font-size: 1em;
-}
-
-a:visited {
- color: purple;
- font-size: 1em;
-}
-
-
-a.anchor:link {
- font-size: 1em;
- color: black;
- font-weight: bold;
- text-decoration: none;
-}
-
-a.anchor:visited {
- font-size: 1em;
- color: black;
- font-weight: bold;
- text-decoration: none;
-}
-
-a.anchor {
- font-size: 1em;
- color: black;
- font-weight: bold;
- text-decoration: none;
-}
-
-td {
- vertical-align: top;
-}
-
-a.smalllink {
- font-size: 0.8em;
-}
-
-/* the banner */
-
-table.banner {
- width: 100%;
- height: 79px;
- margin-left: auto;
- margin-right: auto;
-}
-
-td.banner-left {
- /* This is done with an <img> in the HTML so it can be clickable
- background-image: url("top-left.png");
- background-repeat: no-repeat; */
- width: 193px;
-}
-
-td.banner-middle {
- background-color: #00802B;
- background-image: url("top-middle.png");
- background-repeat: repeat-x;
- vertical-align: bottom;
- padding-bottom: 10px;
- color: white;
- font-weight: bold;
- font-size: 1em;
-}
-
-td.banner-middle a, td.banner-middle a:visited {
- color: white;
- font-weight: bold;
- font-size: 1em;
-}
-
-td.banner-middle a:hover {
- color: #FF7F00;
- font-weight: bold;
- font-size: 1em;
-}
-
-td.banner-right {
- background-image: url("top-right.png");
- background-repeat: no-repeat;
- width: 150px;
- background-position: right;
- padding-top: 8px;
-}
-
-.banner-middle a.current {
- text-decoration: none;
- color: #FF7F00;
- font-weight: bold;
- font-size: 1em;
- width: auto;
- text-align: auto;
- left: -50px;
-}
-
-.donatebutton {
- width: auto;
- text-align: center;
-}
-
-.donatebutton a {
- margin: 10px 0 0 0;
- font-weight: bold;
- display: block;
- padding: 6px;
- background-color: #00802B;
- border-top: 1px solid #00A838;
- border-left: 1px solid #00A838;
- border-bottom: 1px solid #00591E;
- border-right: 1px solid #00591E;
- color: #FFFFFF;
-}
-
-.donatebutton a:hover {
- color: orange;
-}
-
-.donatebutton a:active {
- color: orange;
-}
-
-/* these styles are for the menu on the gui contest pages */
-
-.guileft {
- width: 25%;
- float: left;
- padding: 0;
- margin: 0;
-}
-
-.guimenu {
- border: 1px solid #AAA6AB;
- background-color: #E2DFE3;
- margin: 0 15px 15px 0;
- padding: 0;
-}
-
-.guimenuinner a {
- display: block;
- text-decoration: none;
- padding: 2px 0px 0px 12px;
- margin: 0 0 0 0px;
- color: #333333;
-}
-
-.guimenuinner a:visited {
- color: #333333;
-}
-
-.guimenuinner a:hover {
- background-image: url(gui/img/arrow.png);
- background-repeat: no-repeat;
- background-position: left;
- color: #EF8012;
-}
-
-.guimenuinner a.on {
- background-image: url(gui/img/arrow.png);
- background-repeat: no-repeat;
- background-position: left;
- color: #EF8012;
-}
-
-
-.guimenu h1 {
- width: 85%;
- font-size: 16px;
- margin: 0 0 8px 0;
- padding: 0;
- border-bottom: 1px solid #AAA6AB;
-}
-
-.curveleft {
- background-image: url(gui/img/corner-topleft.png);
- background-repeat: no-repeat;
- background-position: top left;
- margin: -1px;
-}
-
-.curveright {
- background-image: url(gui/img/corner-topright.png);
- background-repeat: no-repeat;
- background-position: top right;
-}
-
-.guimenuinner {
-
- padding: 0 10px 0 10px;
-}
-
-//.wiki {
- padding: 5px 40px 0 0;
- display: block;
- text-align: right;
-}
-
-
-.curvebottomleft {
- background-image: url(gui/img/corner-bottomleft.png);
- background-repeat: no-repeat;
- background-position: bottom left;
- margin: -1px;
-}
-
-.curvebottomright {
- background-image: url(gui/img/corner-bottomright.png);
- background-repeat: no-repeat;
- background-position: bottom right;
-}
-
-
-table.mirrors {
- margin: 0 auto;
- border-width: 3px;
- border-color: gray;
- border-style: ridge;
- border-collapse: collapse;
-}
-table.mirrors th {
- border: 1px solid gray;
- background-color: #DDDDDD;
-}
-table.mirrors td {
- border: 1px solid gray;
- padding: 4px;
-}
-
-acronym {
- border-bottom: none;
-}
-
-dt {
- font-weight: bolder;
- font-style: italic;
-}
Deleted: weather/trunk/subscribe.template
===================================================================
--- weather/trunk/subscribe.template 2010-03-12 15:33:26 UTC (rev 21926)
+++ weather/trunk/subscribe.template 2010-03-12 15:38:47 UTC (rev 21927)
@@ -1,61 +0,0 @@
-<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
-<html>
-<head>
- <title>Tor Weather</title>
- <link rel="stylesheet" type="text/css" href="./stylesheet.css">
-</head>
-<body>
-<div class="center">
-<table class="banner" border="0" cellpadding="0" cellspacing="0" summary="">
- <tr>
-
- <td class="banner-left"><a href="https://www.torproject.org/"><img src="top-left.png" alt="Click to go to home page" width="193" height="79"></a></td>
- <td class="banner-middle">
-
- </td>
- <td class="banner-right">
- </td>
- </tr>
-</table>
-<div class="main-column">
-<h2>Tor Weather</h2>
-
-<hr>
-
-<h3>Sign Up!</h3>
-
-<br>
-<form method="post" action="/subscribe">
-You can use this form to request status updates to tell you when a particular
-Tor node has become unreachable for a sustained period of time.
-<br><br>
-
-<p>
-Email:<br>
-<input type="text" name="email" size="50" maxlength="255" value="Enter one email address" onclick="if (this.value == 'Enter one email address') {this.value = ''}" />
-</p><p>
-Node fingerprint:<br>
-<input type="text" name="node" size="50" maxlength="255" value="Enter one Tor node ID" onclick="if (this.value == 'Enter one Tor node ID') {this.value = ''}" />
-</p><p>
-<input type="submit" class="submit" value="Subscribe to Tor Weather" name="sa"/>
-</p>
-<hr>
-<h3>FAQ</h3>
-<br>
-<p>Q: <b>Where can I find the fingerprint for my server?</b></p>
-<p>A: <i>Often your node fingerprint can be found on unix-like machines in the file: <tt>/var/lib/tor/fingerprint</tt></i></p>
-<p>Q: <b>Will I be overloaded with alerts that I cannot suppress?</b></p>
-<p>A: <i>No.</i>
-<p>Q: <b>Can I unsubscribe easily?</b></p>
-<p>A: <i>Yes.</i>
-<p>Q: <b>I'm having a problem, can you help me?</b></p>
-<p>A: <i>Yes. Send an email to <tt>tor-weather @ torproject dot org</tt></i>
-<br>
-<hr>
-<br>
-<p><i>Please note that while we won't ever intentionally publish them, the address/node pairs sent to this server are not protected against SMTP eavesdropping, hacking, or lawyers.</i>
-</form>
-</div>
-</div>
-</body>
-</html>
Deleted: weather/trunk/top-left.png
===================================================================
(Binary files differ)
Deleted: weather/trunk/top-middle.png
===================================================================
(Binary files differ)
Deleted: weather/trunk/top-right.png
===================================================================
(Binary files differ)
Deleted: weather/trunk/weather.py
===================================================================
--- weather/trunk/weather.py 2010-03-12 15:33:26 UTC (rev 21926)
+++ weather/trunk/weather.py 2010-03-12 15:38:47 UTC (rev 21927)
@@ -1,413 +0,0 @@
-#!/usr/bin/python
-
-import os
-import re
-import sys
-import DNS
-import base64
-import smtplib
-import socket
-from twisted.web import resource, static, server, http
-from twisted.enterprise import adbapi
-from twisted.internet import reactor
-from twisted.internet.task import LoopingCall
-from email.mime.multipart import MIMEMultipart
-from email.mime.base import MIMEBase
-from email.mime.text import MIMEText
-
-import traceback
-
-import TorCtl.TorCtl as TorCtl
-
-# Globals
-URLbase = "https://weather.torproject.org"
-mailFrom = "tor-ops@xxxxxxxxxxxxxx"
-pollPeriod = 10
-
-debugfile = open("debug", "w")
-
-# Text strings
-THANKS_OUT = """
- Thanks for using tor weather. A confirmation request has been sent to '%s'.
-"""
-REPORT_MAIL = """This is a Tor Weather report.
-
-It appears that a tor node you elected to monitor,
-
-(node id: %s)
-
-has been uncontactable through the Tor network for a while. You may wish
-to look at it to see why. The last error message from our code while trying to
-contact it is included below. You may or may not find it helpful!
-
-(You can unsubscribe from these reports at any time by visiting the
-following url:
-
-%s )"""
-
-CONFIRMATION_MAIL = """
- Dear human, this is the Tor Weather Report system.
-
- Somebody (possibly you) has requested that status monitoring information
- about a tor node (id: %s) be sent to this email address.
-
- If you wish to confirm this request, please visit the following link:
-
- %s
-
- If you do *not* wish to receive Tor Weather Reports, you do not need to do
- anything.
-"""
-
-# Query strings
-CHECK_SUBS_Q = "SELECT id FROM subscriptions WHERE email='%s' and node='%s'"
-INSERT_SUBS_Q = """
- INSERT INTO subscriptions (email, node, subs_auth, unsubs_auth, subscribed) VALUES ('%s', '%s', '%s', '%s', 0)
-"""
-CHECK_SUBS_AUTH_Q = "SELECT unsubs_auth FROM subscriptions WHERE subs_auth='%s'"
-ACK_SUB_Q = "UPDATE subscriptions SET subscribed=1 WHERE subs_auth='%s'"
-UNSUBSCRIBE_Q = "DELETE from subscriptions where unsubs_auth='%s'"
-GETALL_SUBS_Q = "SELECT * from subscriptions WHERE subscribed=1"
-
-class WeatherIndex(resource.Resource):
- def render(self, request):
- return open("subscribe.template").read()
-
-class SubscribeRequest(resource.Resource):
- def __init__(self, dbConn):
- self.dbConn = dbConn
- resource.Resource.__init__(self)
-
- def render(self, request):
- self.request = request
- self.email = request.args['email'][0]
- self.node = request.args['node'][0]
-
- if not self._checkMail():
- return "Error: Bad email address '%s'" % self.email
-
- self.subs_auth = self._getRandString()
- self.unsubs_auth = self._getRandString()
- self._isSubscribedAlready()
- return server.NOT_DONE_YET
-
- def _isSubscribedAlready(self):
- dbQuery = CHECK_SUBS_Q % (self.email, self.node)
- q = self.dbConn.runQuery(dbQuery)
- q.addCallback(self._checkHasSubRet)
- q.addErrback(self._errback)
-
- def _checkHasSubRet(self, result):
- # Do we already have a subscription for this address for this node?
- if len(result) is not 0:
- self.request.setResponseCode(http.OK)
- self.request.write("Error: Already subscribed.")
- self.request.finish()
- else:
- # Alright, subscribe it
- return self._runSaveQuery()
-
- def _runSaveQuery(self):
- dbQuery = INSERT_SUBS_Q % (self.email, self.node, \
- self.subs_auth, self.unsubs_auth)
- q = self.dbConn.runOperation(dbQuery)
- q.addCallback(self._saved)
- q.addErrback(self._errback)
- return q
-
- def _saved(self, result):
- # Back to index
- #request.redirect("/")
- url = URLbase + "/confirm-subscribe?auth=" + self.subs_auth
- try:
- self._sendConfirmationMail(url)
- except Exception, e:
- self.error = "Unknown error while sending confirmation mail." + \
- "Please try again later." + \
- "[Exception %s]" % sys.exc_info()[0]
- self._rollBack()
- return
-
- self.request.setResponseCode(http.OK)
- self.request.write(THANKS_OUT % self.email)
- self.request.finish()
-
- def _rollBack(self):
- dbQuery = CHECK_SUBS_Q % (self.email, self.node)
- q = self.dbConn.runQuery(dbQuery)
- q.addCallback(self._errOut)
- q.addErrback(self._errback)
-
- def _errOut(self, result):
- self.request.setResponseCode(http.INTERNAL_SERVER_ERROR)
- self.request.write(self.error)
- self.request.finish()
-
- def _errback(self, failure):
- self.error = "Error: %s" % (failure.getErrorMessage())
- self._errOut()
-
- def _sendConfirmationMail(self, url):
- message = MIMEMultipart()
- message['Subject'] = "Tor Weather Subscription Request"
- message['To'] = self.email
- message['From'] = mailFrom
-
- messageText = CONFIRMATION_MAIL % (self.node, url)
- text = MIMEText(messageText, _subtype="plain", _charset="utf-8")
- # Add text part
- message.attach(text)
-
- # Try to send
- smtp = smtplib.SMTP("localhost:25")
- smtp.sendmail(mailFrom, self.email, message.as_string())
- smtp.quit()
-
- def _getRandString(self):
- """Produce a random alphanumeric string for authentication"""
- r = base64.urlsafe_b64encode(os.urandom(18))[:-1]
- # some email clients don't like URLs ending in -
- if r[-1] == "-":
- r.replace("-", "x")
- return r
-
- def _checkMail(self):
- # Unsure if this is enough
- mailValidator = "^[a-zA-Z0-9._%-+]+@([a-zA-Z0-9._%-]+\\.[a-zA-Z]{2,6}$)"
- mailOk = re.compile(mailValidator)
- match = mailOk.match(self.email)
- if match:
- mailDomain = match.group(1)
- return self._doDNSLookup(mailDomain)
- else:
- return False
-
- def _doDNSLookup(self, mailDomain):
- DNS.DiscoverNameServers()
- querinator = DNS.Request(qtype='mx')
- try:
- dnsquery = querinator.req(mailDomain)
- except DNS.DNSError, type:
- if type == 'Timeout':
- return False
- else:
- raise
- if not dnsquery.answers:
- # No DNS MX records for this domain
- return False
-
- return True
-
- def _checkNode(self):
- nodeOk = re.compile("(0x)?[a-fA-F0-9]{40}\Z")
- if nodeOk.match(self.node):
- return True
- else:
- return False
-
-class ConfirmSubscribeRequest(resource.Resource):
- def __init__(self, dbConn):
- self.dbConn = dbConn
- resource.Resource.__init__(self)
-
- def render(self, request):
- self.subs_auth = request.args['auth'][0]
- self._lookupSubsAuth(request)
-
- return server.NOT_DONE_YET
-
- def _lookupSubsAuth(self, request):
- dbQuery = CHECK_SUBS_AUTH_Q % self.subs_auth
- q = self.dbConn.runQuery(dbQuery)
- q.addCallback(self._checkRet, request)
- q.addErrback(self._errback, request)
-
- def _checkRet(self, result, request):
- if len(result) is 0:
- request.setResponseCode(http.OK)
- request.write("Error: No subscription with your auth code.")
- request.finish()
- else:
- self.unsubs_auth = str(result[0][0])
- return self._ackSubscription(request)
-
- def _ackSubscription(self, request):
- dbQuery = ACK_SUB_Q % self.subs_auth
- q = self.dbConn.runQuery(dbQuery)
- q.addCallback(self._subDone, request)
- q.addErrback(self._errback, request)
-
- def _subDone(self, result, request):
- url = URLbase + "/unsubscribe?auth=" + self.unsubs_auth
- link = "<a href=\"" + url + "\">" + url + "</a>"
- request.write("<p>Subscription finished. Thank you very much.")
- request.write("You can unsubscribe anytime with the following link: ")
- request.write(link)
- #request.write(URLbase + "/unsubscribe?auth=" + self.unsubs_auth)
- request.write("</p>")
- request.finish()
-
- def _errback(self, failure, request):
- request.setResponseCode(http.INTERNAL_SERVER_ERROR)
- request.write("Error: %s" % (failure.getErrorMessage()))
- request.finish()
-
-class UnsubscribeRequest(resource.Resource):
- def __init__(self, dbConn):
- self.dbConn = dbConn
- resource.Resource.__init__(self)
-
- def render(self, request):
- self.unsubs_auth = request.args['auth'][0]
- self._deleteSub(request)
-
- return server.NOT_DONE_YET
-
- def _deleteSub(self, request):
- dbQuery = UNSUBSCRIBE_Q % self.unsubs_auth
- q = self.dbConn.runQuery(dbQuery)
- q.addCallback(self._deleteDone, request)
- q.addErrback(self._errback, request)
-
- def _deleteDone(self, result, request):
- request.setResponseCode(http.OK)
- request.write("Subscription deleted. Goodbye.")
- request.finish()
-
- def _errback(self, failure, request):
- request.setResponseCode(http.INTERNAL_SERVER_ERROR)
- request.write("Error: %s" % (failure.getErrorMessage()))
- request.finish()
-
-class RootResource(resource.Resource):
- def __init__(self, dbConn):
- resource.Resource.__init__(self)
- self.putChild('top-left.png', static.File("./top-left.png"))
- self.putChild('top-middle.png', static.File("./top-middle.png"))
- self.putChild('top-right.png', static.File("./top-right.png"))
- self.putChild('stylesheet.css', static.File("./stylesheet.css"))
- self.putChild('', WeatherIndex())
- self.putChild('subscribe', SubscribeRequest(dbConn))
- self.putChild('confirm-subscribe', ConfirmSubscribeRequest(dbConn))
- self.putChild('unsubscribe', UnsubscribeRequest(dbConn))
-
-class WeatherPoller():
- def __init__(self, dbConn):
- self.dbConn = dbConn
-
- def poller(self):
- print "Polling.."
- self._checkAll()
- return server.NOT_DONE_YET
-
- def _checkAll(self):
- dbQuery = GETALL_SUBS_Q
- q = self.dbConn.runQuery(dbQuery)
- q.addCallback(self._checkRet)
- q.addErrback(self._errBack)
-
- def _checkRet(self, resultList):
- # Loop through result list and check each node
- for result in resultList:
- print "Result: ", result
- checkHost = result[2]
- if not self._checkHost(checkHost):
- print "Server %s seems to be offline" % checkHost
- self._handleOfflineNode(result)
- else:
- print "Server %s is ok" % checkHost
-
- def _errBack(self, failure):
- print "Error: ", failure.getErrorMessage()
-
- def _checkHost(self, hostID):
- print "Checking host %s" % hostID
- torPing = TorPing()
- return torPing.ping(hostID)
-
- def _handleOfflineNode(self, dbRow):
- # Log, mail
- if self._decideNotice(dbRow):
- self._sendNotice(dbRow)
-
- def _decideNotice(self, dbRow):
- # This is just a placeholder for now. We'll decide later what
- # conditions we want to check
- return True
-
- def _sendNotice(self, dbRow):
- nodeId = dbRow[2]
- unsubsURL = URLbase + "/unsubscribe?auth=" + str(dbRow[4])
- message = MIMEMultipart()
- message['Subject'] = "Tor Weather Subscription Request"
- message['To'] = dbRow[1]
- message['From'] = mailFrom
-
- messageText = REPORT_MAIL % (nodeId, unsubsURL)
- text = MIMEText(messageText, _subtype="plain", _charset="ascii")
- # Add text part
- message.attach(text)
-
- # Try to send
- smtp = smtplib.SMTP("localhost:25")
- smtp.sendmail(mailFrom, dbRow[1], message.as_string())
- smtp.quit()
-
-class TorPing:
- "Check to see if various tor nodes respond to SSL hanshakes"
- def __init__(self, control_host = "127.0.0.1", control_port = 9051):
- "Keep the connection to the control port lying around"
- self.control_host = control_host
- self.control_port = control_port
- self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- self.sock.connect((control_host,control_port))
- self.control = TorCtl.Connection(self.sock)
- self.control.authenticate("")
- self.control.debug(debugfile)
-
- def __del__(self):
- self.sock.close()
- del self.sock
- self.sock = None # prevents double deletion exceptions
-
- # it would be better to fix TorCtl!
- try:
- self.control.close()
- except:
- pass
-
- del self.control
- self.control = None
-
- def ping(self, nodeId):
- "Let's see if this tor node is up by only asking Tor."
- try:
- info = self.control.get_info(str("ns/id/" + nodeId))
- except TorCtl.ErrorReply:
- # If we're getting here, we're likely seeing:
- # ErrorReply: 552 Unrecognized key "ns/id/46D9..."
- # This means that the node isn't recognized by
- # XXX: Log
- return False
-
- except:
- # XXX: Log
- return False
-
- # If we're here, we were able to fetch information about the router
- return True
-
-def main():
- # Set up database connection
- dbConn = adbapi.ConnectionPool("sqlite3", "subscriptions.db")
- # Set up polling timer
- weatherPoller = WeatherPoller(dbConn)
- pollTimer = LoopingCall(weatherPoller.poller)
- pollTimer.start(pollPeriod)
- # Set up webserver
- weatherSite = server.Site(RootResource(dbConn))
- reactor.listenTCP(8000, weatherSite)
- reactor.run()
-
-if __name__ == "__main__":
- main()