diff options
Diffstat (limited to 'contrib')
-rwxr-xr-x | contrib/gettor/gettor.py | 141 | ||||
-rwxr-xr-x | contrib/gettor/gettor_blacklist.py | 116 | ||||
-rwxr-xr-x | contrib/gettor/gettor_requests.py | 86 | ||||
-rwxr-xr-x | contrib/gettor/gettor_responses.py | 72 | ||||
-rw-r--r-- | contrib/gettor/sample-emails/negative-DKIM-header-package-sample.eml | 39 | ||||
-rw-r--r-- | contrib/gettor/sample-emails/negative-DKIM-header.eml | 39 | ||||
-rw-r--r-- | contrib/gettor/sample-emails/positive-DKIM-header.eml | 57 |
7 files changed, 550 insertions, 0 deletions
diff --git a/contrib/gettor/gettor.py b/contrib/gettor/gettor.py new file mode 100755 index 0000000000..c687665a2f --- /dev/null +++ b/contrib/gettor/gettor.py @@ -0,0 +1,141 @@ +#!/usr/bin/python2.5 +# -*- coding: utf-8 -*- +""" + + gettor.py by Jacob Appelbaum <jacob@appelbaum.net> + This program will hand out Tor via email to supported systems. + This program is Free Software released under the GPLv3. + + It is intended to be used in a .forward file as part of a pipe like so: + + cat <<'EOF'> .forward + |/usr/local/bin/gettor.py + EOF + + You should have a dist/current/ mirror in a directory that gettor can read. + Such a mirror can be created like so: + + cd /usr/local/ + rsync -av rsync://rsync.torproject.org/tor/dist/current tor-dist-current/ + + You can keep it updated with a cronjob like so: + + MirrorDir=/usr/local/tor-dist-current/ + 0 3 * * * rsync -a rsync://rsync.torproject.org/tor/dist/current/ $MirrorDir + + You should ensure that for each file and signature pair you wish to + distribute, you have created a zip file containing both. + + While this program isn't written in a threaded manner per se, it is designed to function + as if it will be called as a pipe many times at once. There is a slight + desynchronization with blacklist entry checking and may result in false + negatives. This isn't perfect but it is designed to be lightweight. It could + be fixed easily with a shared locking system but this isn't implemented yet. + +""" + +__program__ = 'gettor.py' +__version__ = '20080713.00' +__url__ = 'https://tor-svn.freehaven.net/svn/tor/trunk/contrib/gettor/' +__author__ = 'Jacob Appelbaum <jacob@appelbaum.net>' +__copyright__ = 'Copyright (c) 2008, Jacob Appelbaum' +__license__ = 'See LICENSE for licensing information' + +try: + from future import antigravity +except ImportError: + antigravity = None + +import syslog +import gettor_blacklist +import gettor_requests +import gettor_responses + +if __name__ == "__main__": + + rawMessage = gettor_requests.getMessage() + parsedMessage = gettor_requests.parseMessage(rawMessage) + + if not parsedMessage: + syslog.syslog("gettor: No parsed message. Dropping message.") + print "gettor: No parsed message. Dropping message." + exit(1) + + signature = False + signature = gettor_requests.verifySignature(rawMessage) + print "Signature is : " + str(signature) + replyTo = False + srcEmail = "gettor@torproject.org" + + # TODO XXX: + # Make the zip files and ensure they match packageList + # Make each zip file like so: + # zip -9 windows-bindle.z \ + # vidalia-bundle-0.2.0.29-rc-0.1.6.exe \ + # vidalia-bundle-0.2.0.29-rc-0.1.6.exe.asc + # + packageList = { + "windows-bundle": "/tmp/windows-bundle.z", + "macosx-bundle": "/tmp/macosx-bundle.z", + "linux-bundle": "/tmp/linux-bundle.z", + "source-bundle": "/tmp/source-bundle.z" + } + + # XXX TODO: Ensure we have a proper replyTO or bail out (majorly malformed mail). + replyTo = gettor_requests.parseReply(parsedMessage) + + if not signature: + # Check to see if we've helped them to understand that they need DKIM in the past + previouslyHelped = gettor_blacklist.blackList(replyTo) + + if not replyTo: + syslog.syslog("No help dispatched. Invalid reply address for user.") + print "No help dispatched. Invalid reply address for user." + exit(1) + + if not signature and previouslyHelped: + syslog.syslog("gettor: Unsigned messaged to gettor by blacklisted user dropped.") + print "No help dispatched. Unsigned and unhelped for blacklisted user." + exit(1) + + if not signature and not previouslyHelped: + # Reply with some help and bail out + # Someday call blackList(replyTo) + message = """ + You should try your request again with a provider that implements DKIM. Sorry. + """ + gettor_responses.sendHelp(message, srcEmail, replyTo) + print "attempting to send email from: " + srcEmail + "The mail is sent to: " + replyTo + syslog.syslog("gettor: Unsigned messaged to gettor. We issued some help about using DKIM.") + print "gettor: Unsigned messaged to gettor. We issued some help about using DKIM." + exit(0) + + if signature: + syslog.syslog("gettor: Signed messaged to gettor.") + print "gettor: Signed messaged to gettor." + + try: + print "gettor: Parsing now." + package = gettor_requests.parseRequest(parsedMessage, packageList) + except: + package = None + + if package == "windows-bundle": + print "gettor: " + package + " selected." + syslog.syslog("gettor: " + package + " selected.") + message = "Here's your requested software as a zip file. Please \ + verify the signature." + print "attempting to send email from: " + + srcEmail + "The mail is sent to: " + replyTo + gettor_responses.sendPackage(message, srcEmail, replyTo, packageList[package]) + exit(0) + else: + print "Package request is unknown: " + package + message = " Your request was misunderstood. Please select one of the \ + following packages: " + packageList.keys() + + gettor_responses.sendHelp(message, srcEmail, replyTo) + print "attempting to send email from: " + srcEmail + "The mail is sent to: " + replyTo + syslog.syslog("gettor: Signed messaged to gettor. We issued some help about proper email formatting.") + print "gettor: Signed messaged to gettor. We issued some help about proper email formatting." + exit(0) diff --git a/contrib/gettor/gettor_blacklist.py b/contrib/gettor/gettor_blacklist.py new file mode 100755 index 0000000000..f31327ead3 --- /dev/null +++ b/contrib/gettor/gettor_blacklist.py @@ -0,0 +1,116 @@ +#!/usr/bin/python2.5 +""" +This library implements all of the black listing features needed for gettor. +""" + +import hashlib +import os + +stateDir = "/tmp/gettor/" +blStateDir = stateDir + "bl/" + +def blackList(address, createEntry=False): + """ + Check to see if an address is on our blacklist. If it is - we'll return true. + If requested, we'll attempt to create a blacklist entry and return true if + everything works out. + """ + # XXX TODO: Eventually we may want to sort entries with netcom + # style /tmp/gettor/2-digits-of-hash/2-more-digits/rest-of-hash files + + privateAddress = makeAddressPrivate(address) + blackListed = lookupBlackListEntry(privateAddress) + + if blackListed: + return True + elif createEntry: + return createBlackListEntry(privateAddress) + + return False + +def lookupBlackListEntry(privateAddress, stateDir="/tmp/gettor/bl/"): + """ Check to see if we have a blacklist entry for the given address. """ + entry = stateDir + str(privateAddress) + try: + entry = os.stat(entry) + except OSError: + return False + return True + +def createBlackListEntry(privateAddress, stateDir="/tmp/gettor/bl/"): + """ Create a zero byte file that represents the address in our blacklist. """ + entry = stateDir + str(privateAddress) + stat = None + try: + stat = os.stat(entry) + except OSError: + try: + fd = open(entry, 'w') + fd.close() + return True + except: + print "Entry not found. We were unable to create an entry." + return False + print "It appears that we already had an entry." + return False + +def removeBlackListEntry(privateAddress, stateDir="/tmp/gettor/bl/"): + """ Remove the zero byte file that represents an entry in our blacklist.""" + entry = stateDir + str(privateAddress) + stat = None + try: + entry = os.unlink(entry) + except OSError: + return False + return True + +def makeAddressPrivate(address): + """ Creates a unique identifier for the user. """ + hash = hashlib.sha1(address) + privateAddress = hash.hexdigest() + return privateAddress + +def prepBLStateDir(stateDir = "/tmp/gettor/bl/"): + print "Preparing the state directory for gettor." + stat = None + try: + stat = os.stat(stateDir) + print "We found a state directory" + return True + except OSError: + try: + os.mkdir(stateDir) + print "No state directory was found, we created one" + return True + except: + print "Unable to make a state directory" + return False + +def blackListtests(address): + """ This is a basic evaluation of our blacklist functionality """ + prepBLStateDir() + privateAddress = makeAddressPrivate(address) + print "We have a private address of: " + privateAddress + print "Testing creation of blacklist entry: " + blackListEntry = createBlackListEntry(privateAddress) + print blackListEntry + print "We're testing a lookup of a known positive blacklist entry: " + blackListEntry = lookupBlackListEntry(privateAddress) + print blackListEntry + print "We're testing removal of a known blacklist entry: " + blackListEntry = removeBlackListEntry(privateAddress) + print blackListEntry + print "We're testing a lookup of a known false blacklist entry: " + blackListEntry = lookupBlackListEntry(privateAddress) + print blackListEntry + print "Now we'll test the higher level blacklist functionality..." + print "This should not find an entry in the blacklist: " + print blackList(address) + print "This should add an entry to the blacklist: " + print blackList(address, True) + print "This should find the previous addition to the blacklist: " + print blackList(address) + print "Please ensure the tests match what you would expect for your environment." + +if __name__ == "__main__" : + blackListtests("requestingUser@example.com") diff --git a/contrib/gettor/gettor_requests.py b/contrib/gettor/gettor_requests.py new file mode 100755 index 0000000000..06df1c5424 --- /dev/null +++ b/contrib/gettor/gettor_requests.py @@ -0,0 +1,86 @@ +#!/usr/bin/python2.5 +# -*- coding: utf-8 -*- +""" +This library implements all of the email parsing features needed for gettor. +""" + +import sys +import email +import dkim +import re + +def getMessage(): + """ Read the message into a buffer and return it """ + rawMessage = sys.stdin.read() + return rawMessage + +def verifySignature(rawMessage): + """ Attempt to verify the DKIM signature of a message and return a positive or negative status """ + signature = False + + # TODO XXX: + # This should catch DNS exceptions and fail to verify if we have a + # dns timeout + if dkim.verify(rawMessage): + signature = True + return signature + else: + return signature + +def parseMessage(message): + """ parse an email message and return a parsed email object """ + return email.message_from_string(message) + +def parseReply(parsedMessage): + """ Return an email address that we want to email """ + # TODO XXX: + # Scrub this data + address = parsedMessage["from"] + return address + +def parseRequest(parsedMessage, packages): + """ This parses the request and returns the first specific package name for + sending. If we do not understand the request, we return None as the package + name.""" + # XXX TODO: + # Should we pick only the first line of the email body. Drop the rest? + # It may be too unfriendly to our users + for line in email.Iterators.body_line_iterator(parsedMessage): + for package in packages.keys(): + print "Line is: " + line + print "Package is " + package + match = re.match(package, line) + if match: + return package + # If we get here, we didn't find a package we're currently serving + return None + +if __name__ == "__main__" : + """ Give us an email to understand what we think of it. """ + packageList = { + "windows-bundle": "/tmp/windows-bundle.z", + "macosx-bundle": "/tmp/macosx-bundle.z", + "linux-bundle": "/tmp/linux-bundle.z", + "source-bundle": "/tmp/source-bundle.z" + } + + print "Fetching raw message." + rawMessage = getMessage() + # This doesn't work without DNS ( no wifi on board current airplane ) + print "Verifying signature of message." + signature = verifySignature(rawMessage) + print "Parsing Message." + parsedMessage = parseMessage(rawMessage) + print "Parsing reply." + parsedReply = parseReply(parsedMessage) + print "Parsing package request." + package = parseRequest(parsedMessage, packageList) + if package == None: + package = "help" + else: + package = packageList[package] + + print "The signature status of the email is: " + str(signature) + print "The email requested the following reply address: " + parsedReply + print "It looks like the email requested the following package: " + package + print "We would select the following package file: " + package diff --git a/contrib/gettor/gettor_responses.py b/contrib/gettor/gettor_responses.py new file mode 100755 index 0000000000..4e6ba80ba0 --- /dev/null +++ b/contrib/gettor/gettor_responses.py @@ -0,0 +1,72 @@ +#!/usr/bin/python2.5 +# -*- coding: utf-8 -*- +""" This library implements all of the email replying features needed for gettor. """ + +import smtplib +import MimeWriter +import syslog +import StringIO +import base64 + +def sendHelp(message, source, destination): + """ Send a helpful message to the user interacting with us """ + help = constructMessage(message, source, destination) + try: + print "Attempting to send the following message: " + status = sendMessage(help, source, destination) + except: + print "Message sending failed." + status = False + return status + +def sendPackage(message, source, destination, filelist): + """ Send a message with an attachment to the user interacting with us """ + package = constructMessage(message, source, destination, filelist) + try: + print "Attempting to send the following message: " + status = sendMessage(package, destination) + except: + print "Message sending failed." + status = False + return status + +def constructMessage(messageText, ourAddress, recipient, fileList=None, fileName="requested-files.z"): + """ Construct a multi-part mime message, including only the first part + with plaintext.""" + + message = StringIO.StringIO() + mime = MimeWriter.MimeWriter(message) + mime.addheader('MIME-Version', '1.0') + mime.addheader('Subject', 'Your request has been processed') + mime.addheader('To', recipient) + mime.addheader('From', ourAddress) + mime.startmultipartbody('mixed') + + firstPart = mime.nextpart() + emailBody = firstPart.startbody('text/plain') + emailBody.write(messageText) + + # Add a file if we have one + if fileList: + # XXX TODO: Iterate over each file eventually + filePart = mime.nextpart() + filePart.addheader('Content-Transfer-Encoding', 'base64') + emailBody = filePart.startbody('application/zip; name=%s' % fileName) + base64.encode(open(fileList, 'rb'), emailBody) + + # Now end the mime messsage + mime.lastpart() + return message + +def sendMessage(message, dst, src="gettor@torproject.org", smtpserver="localhost:2700"): + try: + smtp = smtplib.SMTP(smtpserver) + smtp.sendmail(src, dst, message.getvalue()) + smtp.quit() + status = True + except: + return False + + return status + + diff --git a/contrib/gettor/sample-emails/negative-DKIM-header-package-sample.eml b/contrib/gettor/sample-emails/negative-DKIM-header-package-sample.eml new file mode 100644 index 0000000000..ac113e88b0 --- /dev/null +++ b/contrib/gettor/sample-emails/negative-DKIM-header-package-sample.eml @@ -0,0 +1,39 @@ +X-Account-Key: account6 +X-UIDL: 1215752284.31537.faustus,S=1469 +X-Mozilla-Status: 0001 +X-Mozilla-Status2: 02000000 +Return-Path: <jacob@appelbaum.net> +Delivered-To: jpopped@appelbaum.net +Received: (qmail 31535 invoked by uid 89); 11 Jul 2008 04:58:04 -0000 +Delivered-To: appelbaum.net-jacob@appelbaum.net +Received: (qmail 31531 invoked by uid 89); 11 Jul 2008 04:58:04 -0000 +Received: from unknown (HELO moria.seul.org) (128.31.0.34) + by 0 with (DHE-RSA-AES256-SHA encrypted) SMTP; 11 Jul 2008 04:58:04 -0000 +Received-SPF: none (0: domain at appelbaum.net does not designate permitted sender hosts) +Received: by moria.seul.org (Postfix) + id F09581415CD3; Fri, 11 Jul 2008 00:55:45 -0400 (EDT) +Delivered-To: gettor@seul.org +Received: from mail.lostinthenoise.net (mail.lostinthenoise.net [64.142.98.226]) + (using TLSv1 with cipher DHE-RSA-AES256-SHA (256/256 bits)) + (No client certificate requested) + by moria.seul.org (Postfix) with ESMTP id C464A140F476 + for <gettor@torproject.org>; Fri, 11 Jul 2008 00:55:45 -0400 (EDT) +Received: (qmail 31524 invoked by uid 89); 11 Jul 2008 04:58:02 -0000 +Received: from unknown (HELO ?127.0.0.1?) (64.142.98.226) + by 0 with (DHE-RSA-AES256-SHA encrypted) SMTP; 11 Jul 2008 04:58:02 -0000 +Message-ID: <4876E7D0.5050201@appelbaum.net> +Date: Thu, 10 Jul 2008 21:55:44 -0700 +From: Jacob Appelbaum <jacob@appelbaum.net> +User-Agent: Icedove 1.5.0.14eol (X11/20080509) +MIME-Version: 1.0 +To: gettor@torproject.org +Subject: negative DKIM signature +X-Enigmail-Version: 0.94.2.0 +OpenPGP: id=9D0FACE4; + url=http://www.appelbaum.net/gpg.asc +Content-Type: text/plain; charset=ISO-8859-1 +Content-Transfer-Encoding: 7bit + +source-bundle + + diff --git a/contrib/gettor/sample-emails/negative-DKIM-header.eml b/contrib/gettor/sample-emails/negative-DKIM-header.eml new file mode 100644 index 0000000000..543d695e2a --- /dev/null +++ b/contrib/gettor/sample-emails/negative-DKIM-header.eml @@ -0,0 +1,39 @@ +X-Account-Key: account6 +X-UIDL: 1215752284.31537.faustus,S=1469 +X-Mozilla-Status: 0001 +X-Mozilla-Status2: 02000000 +Return-Path: <jacob@appelbaum.net> +Delivered-To: jpopped@appelbaum.net +Received: (qmail 31535 invoked by uid 89); 11 Jul 2008 04:58:04 -0000 +Delivered-To: appelbaum.net-jacob@appelbaum.net +Received: (qmail 31531 invoked by uid 89); 11 Jul 2008 04:58:04 -0000 +Received: from unknown (HELO moria.seul.org) (128.31.0.34) + by 0 with (DHE-RSA-AES256-SHA encrypted) SMTP; 11 Jul 2008 04:58:04 -0000 +Received-SPF: none (0: domain at appelbaum.net does not designate permitted sender hosts) +Received: by moria.seul.org (Postfix) + id F09581415CD3; Fri, 11 Jul 2008 00:55:45 -0400 (EDT) +Delivered-To: gettor@seul.org +Received: from mail.lostinthenoise.net (mail.lostinthenoise.net [64.142.98.226]) + (using TLSv1 with cipher DHE-RSA-AES256-SHA (256/256 bits)) + (No client certificate requested) + by moria.seul.org (Postfix) with ESMTP id C464A140F476 + for <gettor@torproject.org>; Fri, 11 Jul 2008 00:55:45 -0400 (EDT) +Received: (qmail 31524 invoked by uid 89); 11 Jul 2008 04:58:02 -0000 +Received: from unknown (HELO ?127.0.0.1?) (64.142.98.226) + by 0 with (DHE-RSA-AES256-SHA encrypted) SMTP; 11 Jul 2008 04:58:02 -0000 +Message-ID: <4876E7D0.5050201@appelbaum.net> +Date: Thu, 10 Jul 2008 21:55:44 -0700 +From: Jacob Appelbaum <jacob@appelbaum.net> +User-Agent: Icedove 1.5.0.14eol (X11/20080509) +MIME-Version: 1.0 +To: gettor@torproject.org +Subject: negative DKIM signature +X-Enigmail-Version: 0.94.2.0 +OpenPGP: id=9D0FACE4; + url=http://www.appelbaum.net/gpg.asc +Content-Type: text/plain; charset=ISO-8859-1 +Content-Transfer-Encoding: 7bit + +testing + + diff --git a/contrib/gettor/sample-emails/positive-DKIM-header.eml b/contrib/gettor/sample-emails/positive-DKIM-header.eml new file mode 100644 index 0000000000..6ada7d7572 --- /dev/null +++ b/contrib/gettor/sample-emails/positive-DKIM-header.eml @@ -0,0 +1,57 @@ +X-Account-Key: account6 +X-UIDL: 1214981061.25808.faustus,S=2285 +X-Mozilla-Status: 0001 +X-Mozilla-Status2: 02000000 +Return-Path: <ioerror@gmail.com> +Delivered-To: jpopped@appelbaum.net +Received: (qmail 25806 invoked by uid 89); 2 Jul 2008 06:44:21 -0000 +Delivered-To: appelbaum.net-jacob@appelbaum.net +Received: (qmail 25804 invoked by uid 89); 2 Jul 2008 06:44:21 -0000 +Received: from unknown (HELO wa-out-1112.google.com) (209.85.146.180) + by 0 with SMTP; 2 Jul 2008 06:44:21 -0000 +Received-SPF: pass (0: SPF record at _spf.google.com designates 209.85.146.180 as permitted sender) +Received: by wa-out-1112.google.com with SMTP id j40so170432wah.1 + for <jacob@appelbaum.net>; Tue, 01 Jul 2008 23:42:01 -0700 (PDT) +DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; + d=gmail.com; s=gamma; + h=domainkey-signature:received:received:message-id:date:from:to + :subject:mime-version:content-type; + bh=IvFqNkffeoST7vamh2ytuq/b7GpLhg2hStTrQq3I3rE=; + b=xQR0hE/J4AXpAqH1UDXTtDrU9Izc6WM8vtFudRBzldWYyRx3Vvfh2I2Opu8+O6wbAv + jlDi18anUMbZqlIGSgGOxvXW4CltpX/SFZm1aGL4AisQ1Bi5xEqlrc8OnX3sA2xKeM3g + KWsWm+GVSMI4XAqnY9FYAfPx4DmOAfkdMyWCU= +DomainKey-Signature: a=rsa-sha1; c=nofws; + d=gmail.com; s=gamma; + h=message-id:date:from:to:subject:mime-version:content-type; + b=kyzDtGRDbiC5y4Bz/ylQjyHOChiOP2A6QDzybsVXc0C1hjHLImOQYR8gOxcRY+mRkN + 1xpBaEF4UloZAxTb79khRRp4TWmjT1DagtLx2MFzIj/F6awtdE/9U3p4QyKr8S43tGcE + ET26BSfT5u9zrXblVVAP3JedMPZ8mlIGQxyDs= +Received: by 10.115.90.1 with SMTP id s1mr6711509wal.51.1214980921268; + Tue, 01 Jul 2008 23:42:01 -0700 (PDT) +Received: by 10.114.184.16 with HTTP; Tue, 1 Jul 2008 23:41:57 -0700 (PDT) +Message-ID: <7fadd8130807012341n3b3af401mbdb4a29c80310bd3@mail.gmail.com> +Date: Tue, 1 Jul 2008 23:41:57 -0700 +From: "Jacob Applebaum" <ioerror@gmail.com> +To: jacob@appelbaum.net +Subject: Positive DKIM header +MIME-Version: 1.0 +Content-Type: multipart/alternative; + boundary="----=_Part_462_28562233.1214980917793" + +------=_Part_462_28562233.1214980917793 +Content-Type: text/plain; charset=ISO-8859-1 +Content-Transfer-Encoding: 7bit +Content-Disposition: inline + +This email should have a positive DKIM header. + +------=_Part_462_28562233.1214980917793 +Content-Type: text/html; charset=ISO-8859-1 +Content-Transfer-Encoding: 7bit +Content-Disposition: inline + +This email should have a positive DKIM header.<br> + +------=_Part_462_28562233.1214980917793-- + + |