diff --git a/facilitator/Makefile.am b/facilitator/Makefile.am index 6d4325b..6a3cdfc 100644 --- a/facilitator/Makefile.am +++ b/facilitator/Makefile.am @@ -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 @@ -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-* diff --git a/facilitator/default/facilitator-email-poller b/facilitator/default/facilitator-email-poller index 42f4dc6..b71503f 100644 --- a/facilitator/default/facilitator-email-poller +++ b/facilitator/default/facilitator-email-poller @@ -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" diff --git a/facilitator/examples/reg-email.pass b/facilitator/examples/reg-email.pass new file mode 100644 index 0000000..6f34e06 --- /dev/null +++ b/facilitator/examples/reg-email.pass @@ -0,0 +1,10 @@ +# This file should contain "[] " on a single line, +# separated by whitespace. If is omitted, it defaults to +# imap.( 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 diff --git a/facilitator/fac.py b/facilitator/fac.py index 70d482d..695cc29 100644 --- a/facilitator/fac.py +++ b/facilitator/fac.py @@ -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") @@ -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 @@ -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: diff --git a/facilitator/facilitator-email-poller b/facilitator/facilitator-email-poller index 32dc2d4..d4e1e16 100755 --- a/facilitator/facilitator-email-poller +++ b/facilitator/facilitator-email-poller @@ -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" @@ -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 @@ -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 "[] " on a + single line, separated by whitespace. If is + omitted, it defaults to imap.( 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, } @@ -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", @@ -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": @@ -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." @@ -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 [] . +""" % (options.password_filename, str(e)) + sys.exit(1) finally: password_file.close() @@ -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() @@ -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 diff --git a/facilitator/facilitator-test.py b/facilitator/facilitator-test.py index e5ed843..e00ea5e 100755 --- a/facilitator/facilitator-test.py +++ b/facilitator/facilitator-test.py @@ -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, "")