[or-cvs] r21927: {} Restructure: Add setup files, move data files to data dir, b (in weather/trunk: . data lib lib/weather)

Christian Fromme kaner at strace.org
Fri Mar 12 15:38:47 UTC 2010


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 at torproject.org"
-
-# 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">
+        &nbsp;
+        </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 at torproject.org"
+
+# 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">
-        &nbsp;
-        </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 at torproject.org"
-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()



More information about the tor-commits mailing list