Skip to content

Commit

Permalink
email-poller: configure user data (imap_host, email, password) in the…
Browse files Browse the repository at this point in the history
… same file

- means we don't depend on initscripts for user config, and easier to explain
- breaks backwards compatibility, but since the email was hardcoded we only had one user anyway
- smarter default IMAP host; and resolve DNS by default - previously one had to use an IP address
- add a nameOk option to parse_addr_spec as certificate verification requires imap host to be a name
  • Loading branch information
Ximin Luo committed Nov 4, 2013
1 parent d736b4b commit a310c1d
Show file tree
Hide file tree
Showing 6 changed files with 51 additions and 40 deletions.
7 changes: 2 additions & 5 deletions facilitator/Makefile.am
Expand Up @@ -20,7 +20,7 @@ dist_initconf_DATA = default/facilitator default/facilitator-email-poller defaul
endif

dist_doc_DATA = doc/appengine-howto.txt doc/facilitator-howto.txt doc/gmail-howto.txt README
dist_example_DATA = examples/fp-facilitator
dist_example_DATA = examples/fp-facilitator examples/reg-email.pass
dist_appengine_DATA = appengine/app.yaml appengine/config.go appengine/fp-reg.go appengine/README
appengineconf_DATA = appengine/config.go

Expand Down Expand Up @@ -87,10 +87,7 @@ install-secrets:
openssl rsa -pubout > $(pkgconfdir)/reg-daemon.pub; }
test -f $(pkgconfdir)/reg-email.pass || { \
install -m 600 /dev/null $(pkgconfdir)/reg-email.pass && \
echo >> $(pkgconfdir)/reg-email.pass \
"Replace this file's contents with your Gmail app-specific password;" && \
echo >> $(pkgconfdir)/reg-email.pass \
"see gmail-howto.txt in this package's documentation for details."; }
cat $(exampledir)/reg-email.pass > $(pkgconfdir)/reg-email.pass; }

remove-secrets:
rm -f $(pkgconfdir)/reg-*
Expand Down
8 changes: 0 additions & 8 deletions facilitator/default/facilitator-email-poller
Expand Up @@ -5,11 +5,3 @@ RUN_DAEMON="no"
# This may be useful for debugging or diagnosing functional problems, but
# should be avoided in a high-risk environment.
#UNSAFE_LOGGING="yes"

# Replace this with the email address for your facilitator.
# You should also edit the reg-email.pass file as needed.
FACILITATOR_EMAIL_ADDR="invalid"

# Set the host:port for the remote IMAP service to contact
# If not set, uses the default (imap.gmail.com:993).
#IMAPADDR="imap.gmail.com:993"
10 changes: 10 additions & 0 deletions facilitator/examples/reg-email.pass
@@ -0,0 +1,10 @@
# This file should contain "[<imap_host>] <email> <password>" on a single line,
# separated by whitespace. If <imap_host> is omitted, it defaults to
# imap.(<email> domain):993.
#
# If your email provider supports it, we advise you to use an app-specific
# password rather than your account password; see gmail-howto.txt in this
# package's documentation for details on how to do this for a Google account.
#
#imap.gmail.com:993 flashproxyreg.a@gmail.com topsecret11!one
#flashproxyreg.a@gmail.com passwords with spaces are ok too
11 changes: 7 additions & 4 deletions facilitator/fac.py
Expand Up @@ -46,7 +46,7 @@ def ret(self, *args):
raise
return ret

def parse_addr_spec(spec, defhost = None, defport = None, resolve = False):
def parse_addr_spec(spec, defhost = None, defport = None, resolve = False, nameOk = False):
"""Parse a host:port specification and return a 2-tuple ("host", port) as
understood by the Python socket functions.
>>> parse_addr_spec("192.168.0.1:9999")
Expand All @@ -67,9 +67,9 @@ def parse_addr_spec(spec, defhost = None, defport = None, resolve = False):
>>> parse_addr_spec("", defhost="192.168.0.1", defport=9999)
('192.168.0.1', 9999)
If resolve is true, then the host in the specification or the defhost may be
a domain name, which will be resolved. If resolve is false, then the host
must be a numeric IPv4 or IPv6 address.
If nameOk is true, then the host in the specification or the defhost may be
a domain name. Otherwise, it must be a numeric IPv4 or IPv6 address.
If resolve is true, this implies nameOk, and the host will be resolved.
IPv6 addresses must be enclosed in square brackets."""
host = None
Expand Down Expand Up @@ -107,6 +107,9 @@ def parse_addr_spec(spec, defhost = None, defport = None, resolve = False):
# done only if resolve is true; otherwise the address must be numeric.
if resolve:
flags = 0
elif nameOk:
# don't pass through the getaddrinfo numeric check, just return directly
return host, int(port)
else:
flags = socket.AI_NUMERICHOST
try:
Expand Down
50 changes: 27 additions & 23 deletions facilitator/facilitator-email-poller
Expand Up @@ -21,7 +21,6 @@ import fac
from hashlib import sha1
from M2Crypto import SSL, X509

DEFAULT_IMAP_HOST = "imap.gmail.com"
DEFAULT_IMAP_PORT = 993
DEFAULT_LOG_FILENAME = "facilitator-email-poller.log"

Expand Down Expand Up @@ -74,8 +73,6 @@ PUBKEY_SHA1 = (
LOG_DATE_FORMAT = "%Y-%m-%d %H:%M:%S"

class options(object):
email_addr = None
imap_addr = None
password_filename = None
log_filename = DEFAULT_LOG_FILENAME
log_file = sys.stdout
Expand Down Expand Up @@ -132,18 +129,18 @@ them, and forwards the registrations to the facilitator.
-d, --debug don't daemonize, log to stdout.
--disable-pin don't check server public key against a known pin.
-e, --email=ADDRESS log in as ADDRESS
-h, --help show this help.
-i, --imap=HOST[:PORT] use the given IMAP server (default "%(imap_addr)s").
--imaplib-debug show raw IMAP messages (will include email password).
-l, --log FILENAME write log to FILENAME (default \"%(log)s\").
-p, --pass=PASSFILE use the email password contained in PASSFILE.
-p, --pass=PASSFILE use the email/password contained in PASSFILE. This file
should contain "[<imap_host>] <email> <password>" on a
single line, separated by whitespace. If <imap_host> is
omitted, it defaults to imap.(<email> domain):993.
--pidfile FILENAME write PID to FILENAME after daemonizing.
--privdrop-user USER switch UID and GID to those of USER.
--unsafe-logging don't scrub email password and IP addresses from logs.\
""" % {
"progname": sys.argv[0],
"imap_addr": fac.format_addr((DEFAULT_IMAP_HOST, DEFAULT_IMAP_PORT)),
"log": DEFAULT_LOG_FILENAME,
}

Expand All @@ -158,9 +155,6 @@ def log(msg):
print >> options.log_file, (u"%s %s" % (time.strftime(LOG_DATE_FORMAT), msg)).encode("UTF-8")
options.log_file.flush()

options.email_addr = None
options.imap_addr = (DEFAULT_IMAP_HOST, DEFAULT_IMAP_PORT)

opts, args = getopt.gnu_getopt(sys.argv[1:], "de:hi:l:p:", [
"debug",
"disable-pin",
Expand All @@ -180,13 +174,9 @@ for o, a in opts:
options.log_filename = None
elif o == "--disable-pin":
options.use_certificate_pin = False
elif o == "-e" or o == "--email":
options.email_addr = a
elif o == "-h" or o == "--help":
usage()
sys.exit()
elif o == "-i" or o == "--imap":
options.imap_addr = fac.parse_addr_spec(a, DEFAULT_IMAP_HOST, DEFAULT_IMAP_PORT)
if o == "--imaplib-debug":
options.imaplib_debug = True
elif o == "-l" or o == "--log":
Expand All @@ -204,11 +194,6 @@ if len(args) != 0:
usage(sys.stderr)
sys.exit(1)

# Check the email
if not options.email_addr or '@' not in options.email_addr:
print >> sys.stderr, "The --email option is required and must be an email address."
sys.exit(1)

# Load the email password.
if options.password_filename is None:
print >> sys.stderr, "The --pass option is required."
Expand All @@ -225,7 +210,26 @@ try:
print >> sys.stderr, "Refusing to run with group- or world-readable password file. Try"
print >> sys.stderr, "\tchmod 600 %s" % options.password_filename
sys.exit(1)
email_password = password_file.read().strip()
for line in password_file.readlines():
line = line.strip("\n")
if not line or line.startswith('#'): continue
# we do this stricter regex match because passwords might have spaces in
res = re.match(r"(?:(\S+)\s)?(\S+@\S+)\s(.+)", line)
if not res:
raise ValueError("could not find email or password: %s" % line)
(imap_addr_spec, email_addr, email_password) = res.groups()
default_imap_host = "imap.%s" % (email_addr.split('@', 1)[1])
imap_addr = fac.parse_addr_spec(
imap_addr_spec or "", default_imap_host, DEFAULT_IMAP_PORT, nameOk=True)
break
else:
raise ValueError("no email line found")
except Exception, e:
print >> sys.stderr, """\
Failed to parse password file "%s": %s.
Syntax is [<imap_host>] <email> <password>.
""" % (options.password_filename, str(e))
sys.exit(1)
finally:
password_file.close()

Expand Down Expand Up @@ -375,7 +379,7 @@ def imap_login():
try:
ca_certs_file.write(CA_CERTS)
ca_certs_file.flush()
imap = IMAP4_SSL_REQUIRED(options.imap_addr[0], options.imap_addr[1],
imap = IMAP4_SSL_REQUIRED(imap_addr[0], imap_addr[1],
None, ca_certs_file.name)
finally:
ca_certs_file.close()
Expand All @@ -393,8 +397,8 @@ def imap_login():
expected = "(" + ", ".join(x.encode("hex") for x in PUBKEY_SHA1) + ")"
raise ValueError("Public key does not match pin: got %s but expected any of %s" % (found, expected))

log(u"logging in as %s" % options.email_addr)
imap.login(options.email_addr, email_password)
log(u"logging in as %s" % email_addr)
imap.login(email_addr, email_password)

return imap

Expand Down
5 changes: 5 additions & 0 deletions facilitator/facilitator-test.py
Expand Up @@ -156,6 +156,11 @@ def test_noresolve(self):
"""Test that parse_addr_spec does not do DNS resolution by default."""
self.assertRaises(ValueError, fac.parse_addr_spec, "example.com")

def test_noresolve_nameok(self):
"""Test that nameok passes through a domain name without resolving it."""
self.assertEqual(fac.parse_addr_spec("example.com:8888", defhost="other.com", defport=9999, nameOk=True), ("example.com", 8888))
self.assertEqual(fac.parse_addr_spec("", defhost="other.com", defport=9999, nameOk=True), ("other.com", 9999))

class ParseTransactionTest(unittest.TestCase):
def test_empty_string(self):
self.assertRaises(ValueError, fac.parse_transaction, "")
Expand Down

0 comments on commit a310c1d

Please sign in to comment.