# HG changeset patch # User Paul Boddie # Date 1350837698 -7200 # Node ID 408d78d241c2764e825c932a75c38f2d7e429e47 # Parent ceadc72b61774edeed75a79130a6fd9000e75763 Changed the sendMessage function to use URLs instead of hosts and paths. Added a SendMessage action for use within MoinMoin. Added documentation about the different configuration settings and pages. Attempted to improve the homedir initialisation script to fix permissions issues. diff -r ceadc72b6177 -r 408d78d241c2 MoinMessage.py --- a/MoinMessage.py Sun Oct 21 00:43:40 2012 +0200 +++ b/MoinMessage.py Sun Oct 21 18:41:38 2012 +0200 @@ -14,6 +14,7 @@ from email.mime.text import MIMEText from subprocess import Popen, PIPE from tempfile import mkstemp +from urlparse import urlsplit import httplib import os @@ -203,15 +204,42 @@ # Communications functions. -def sendMessage(message, host, path): +def sendMessage(message, url): - "Send 'message' to the given 'host' using the specified URL 'path'." + "Send 'message' to the given 'url." + scheme, host, port, path = parseURL(url) text = message.as_string() - req = httplib.HTTPConnection(host) - req.request("PUT", path, text) # {"Content-Length" : len(text)} + if scheme == "http": + cls = httplib.HTTPConnection + elif scheme == "https": + cls = httplib.HTTPSConnection + else: + raise MoinMessageError, "Communications protocol not supported: %s" % scheme + + req = cls(host, port) + req.request("PUT", path, text) resp = req.getresponse() return resp.read() +def parseURL(url): + + "Return the scheme, host, port and path for the given 'url'." + + scheme, host_port, path, query, fragment = urlsplit(url) + host_port = host_port.split(":") + + if query: + path += "?" + query + + if len(host_port) > 1: + host = host_port[0] + port = int(host_port[1]) + else: + host = host_port[0] + port = 80 + + return scheme, host, port, path + # vim: tabstop=4 expandtab shiftwidth=4 diff -r ceadc72b6177 -r 408d78d241c2 README.txt --- a/README.txt Sun Oct 21 00:43:40 2012 +0200 +++ b/README.txt Sun Oct 21 18:41:38 2012 +0200 @@ -2,7 +2,7 @@ ------------ MoinMessage provides a library for creating, signing, encrypting, decrypting, -verifying PGP/GPG content in Python along with mechanisms for updating +and verifying PGP/GPG content in Python along with mechanisms for updating MoinMoin Wiki instances with such content such that contributors can be identified from their PGP signatures and such details used to authenticate their contributions. @@ -13,7 +13,13 @@ Initialise a homedir for GPG and configure it using ACL (access control list) properties: -./scripts/init_wiki_keyring.sh +./scripts/init_wiki_keyring.sh WIKI WEBUSER + +Here, WIKI should be replaced by the top-level Wiki instance directory, and +WEBUSER should be the name of the user under which the Web server operates. + +Note that this script may need re-running after the homedir has been changed +by gpg operations as gpg likes to remove permissions from various files. To be in any way useful, signing keys must be made available within this homedir so that incoming messages can have their senders verified. @@ -38,6 +44,10 @@ gpg --homedir wiki/gnupg --gen-key +For the Wiki environment to be able to use the key, password access must be +disabled. This can be done by either not specifying a password or by removing +it later using the --edit-key option. + Export the Wiki's key for encrypting messages sent to the Wiki: gpg --homedir wiki/gnupg --armor --output 0891463A.asc --export 0891463A @@ -57,25 +67,66 @@ moinmessage_gpg_users_page (optional, default is MoinMessageUserDict) This provides a mapping from key fingerprints to Moin usernames. + moinmessage_gpg_signing_users_page (optional, default is MoinMessageSigningUserDict) + This provides a mapping from Moin usernames to key fingerprints. + + moinmessage_gpg_recipients_page (optional, default is MoinMessageRecipientsDict) + This provides a mapping from recipients to remote URLs and key fingerprints. + +Fingerprints and Keys +--------------------- + +All fingerprints mentioned in the various configuration pages must exclude +space characters - that is, the letters and digits must appear together in a +continuous block of text - and refer to keys available in the Wiki homedir. + The Fingerprint-to-Username Mapping ----------------------------------- -The mapping from fingerprints to usernames is a WikiDict page having the -following general format: +The mapping from fingerprints to usernames typically defined by the +MoinMessageUserDict page is a WikiDict having the following general format: fingerprint:: username -Each fingerprint must exclude space characters and correspond to the -fingerprint shown for a key in the available key listing generated above. +Each fingerprint corresponds to a key used by a person wanting to send +messages to the Wiki to sign such messages. Each username must correspond to a registered user in the Wiki. +The Username-to-Signing Key Mapping +----------------------------------- + +The mapping from usernames to fingerprints typically defined by the +MoinMessageSigningUserDict page is a WikiDict having the following general +format: + + username:: fingerprint + +Each fingerprint corresponds to a key available in the Wiki's GPG homedir +generated for the purpose of signing the specified user's messages. Such a key +is not the same as one used by a person to send messages to the Wiki since +only the public key used to verify such messages should be known to the Wiki. + +The Recipients Mapping +---------------------- + +The mapping from recipients to remote URLs and fingerprints typically defined +by the MoinMessageRecipientsDict page is a WikiDict having the following +general format: + + recipient:: URL fingerprint + +Each URL must refer to a resource that can accept MoinMessage content. + +Each fingerprint corresponds to a key used by the remote site (as identified +by the URL) for the decryption of messages. + Quick Start: Signing, Encrypting and Sending Messages ----------------------------------------------------- To send a message signed and encrypted to a resource on localhost: -python tests/test_send.py 1C1AAF83 0891463A localhost /wiki/ShareTest \ +python tests/test_send.py 1C1AAF83 0891463A http://localhost/wiki/ShareTest \ 'An update to the Wiki.' 'Another update.' Here, the first identifier is a reference to the signing key (over which you @@ -83,7 +134,8 @@ encryption key (which is a public key published for the Wiki). This needs password protection to be removed from the secret key in the Web -server environment, and so uses a modified trust model when invoking gpg. +server environment. It also uses a modified trust model when invoking gpg in +order to avoid complaints about the identity of the sender during encryption. Below, the mechanisms employed are illustrated through the use of the other test programs. @@ -126,9 +178,7 @@ Signing and Encrypting ---------------------- -Send a message signed and encrypted: - -python tests/test_send.py 1C1AAF83 0891463A localhost /wiki/ShareTest +Sign and encrypt a message: python tests/test_message.py 'An update to the Wiki.' 'Another update.' \ | python tests/test_sign.py 1C1AAF83 \ @@ -150,7 +200,7 @@ To post a signed and/or encrypted message, output from the above activities can be piped into the following command: -python tests/test_post.py localhost /wiki/ShareTest +python tests/test_post.py http://localhost/wiki/ShareTest Here, the resource "/wiki/ShareTest" on localhost is presented with the message. diff -r ceadc72b6177 -r 408d78d241c2 actions/PostMessage.py --- a/actions/PostMessage.py Sun Oct 21 00:43:40 2012 +0200 +++ b/actions/PostMessage.py Sun Oct 21 18:41:38 2012 +0200 @@ -281,6 +281,6 @@ # Action function. def execute(pagename, request): - PostMessage(pagename, request).do_action() + PostMessage(pagename, request).do_action() # instead of render # vim: tabstop=4 expandtab shiftwidth=4 diff -r ceadc72b6177 -r 408d78d241c2 actions/SendMessage.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/actions/SendMessage.py Sun Oct 21 18:41:38 2012 +0200 @@ -0,0 +1,163 @@ +# -*- coding: iso-8859-1 -*- +""" + MoinMoin - SendMessage Action + + @copyright: 2012 by Paul Boddie + @license: GNU GPL (v2 or later), see COPYING.txt for details. +""" + +from MoinMoin.action import ActionBase +from MoinMoin.log import getLogger +from MoinMessage import GPG, MoinMessageError, Message, sendMessage +from MoinSupport import * +from MoinMoin.wikiutil import escape +from email.mime.text import MIMEText + +Dependencies = [] + +class SendMessage(ActionBase, ActionSupport): + + "An action that can send a message to another site." + + def get_form_html(self, buttons_html): + + "Present an interface for message sending." + + _ = self._ + request = self.request + form = self.get_form() + + message = form.get("message", [""])[0] + recipient = form.get("recipient", [""])[0] + + # Get a list of potential recipients. + + recipients = self.get_recipients() + + # Prepare the recipients list, selecting the specified recipients. + + recipients_list = [] + + if recipients: + recipients_list += self.get_option_list(recipient, recipients) or [] + + recipients_list.sort() + + # Fill in the fields and labels. + + d = { + "buttons_html" : buttons_html, + "recipient_label" : _("Recipient"), + "recipients_list" : "\n".join(recipients_list), + "message_label" : _("Message text"), + "message_default" : escattr(message), + } + + # Prepare the output HTML. + + html = ''' + + + + + + + + + + + + + +
+ +
+ +
+ %(buttons_html)s +
''' % d + + return html + + def do_action(self): + + "Attempt to send the message." + + _ = self._ + request = self.request + form = self.get_form() + + text = form.get("message", [None])[0] + recipient = form.get("recipient", [None])[0] + + if not text: + return 0, _("A message must be given.") + + if not recipient: + return 0, _("A recipient must be given.") + + homedir = self.get_homedir() + if not homedir: + return 0, _("MoinMessage has not been set up: a GPG homedir is not defined.") + + gpg = GPG(homedir) + + # Construct a message from the request. + + message = Message() + message.add_update([MIMEText(text, "moin")]) + + # Get the sender details for signing messages. + # This is not the same as the details for authenticating users in the + # PostMessage action since the fingerprints refer to public keys. + + signing_users = getWikiDict(getattr(request.cfg, "moinmessage_gpg_signing_users_page", "MoinMessageSigningUserDict"), request) + signer = signing_users and signing_users.get(request.user.name) + + # Get the recipient details. + + recipients = self.get_recipients() + if not recipients: + return 0, _("No recipients page is defined for MoinMessage.") + + recipient_details = recipients.get(recipient) + if not recipient_details: + return 0, _("The specified recipient is not present in the list of known contacts.") + + try: + url, fingerprint = recipient_details.split() + except ValueError: + return 0, _("The recipient details were not in the correct format: url, fingerprint.") + + # Sign, encrypt and send the message. + + try: + message = message.get_payload() + if signer: + message = gpg.signMessage(message, signer) + + message = gpg.encryptMessage(message, fingerprint) + sendMessage(message, url) + + except MoinMessageError, exc: + return 0, "%s: %s" % (_("The message could not be prepared and sent:"), exc) + + return 1, None + + def get_homedir(self): + + "Locate the GPG home directory." + + return getattr(self.request.cfg, "moinmessage_gpg_homedir") + + def get_recipients(self): + return getWikiDict(getattr(self.request.cfg, "moinmessage_gpg_recipients_page", "MoinMessageRecipientsDict"), self.request) + +# Action function. + +def execute(pagename, request): + SendMessage(pagename, request).render() + +# vim: tabstop=4 expandtab shiftwidth=4 diff -r ceadc72b6177 -r 408d78d241c2 scripts/init_wiki_keyring.sh --- a/scripts/init_wiki_keyring.sh Sun Oct 21 00:43:40 2012 +0200 +++ b/scripts/init_wiki_keyring.sh Sun Oct 21 18:41:38 2012 +0200 @@ -15,9 +15,13 @@ # Initialise and configure ACLs on the gpg "homedir". -mkdir $WIKI/gnupg +if [ ! -e "$WIKI/gnupg" ]; then + mkdir $WIKI/gnupg +fi + chmod go-rwx $WIKI/gnupg setfacl -m u:$USER:rwx $WIKI/gnupg setfacl -m m:rwx $WIKI/gnupg setfacl -m d:u:$USER:rwx $WIKI/gnupg setfacl -m d:m:rwx $WIKI/gnupg +setfacl -m m:rw $WIKI/gnupg/* diff -r ceadc72b6177 -r 408d78d241c2 tests/test_post.py --- a/tests/test_post.py Sun Oct 21 00:43:40 2012 +0200 +++ b/tests/test_post.py Sun Oct 21 18:41:38 2012 +0200 @@ -1,15 +1,20 @@ #!/usr/bin/env python +from MoinMessage import parseURL import httplib import sys if __name__ == "__main__": - host = sys.argv[1] - path = sys.argv[2] + "?action=PostMessage" + url = sys.argv[1] + "?action=PostMessage" text = sys.stdin.read() + scheme, host, port, path = parseURL(url) - req = httplib.HTTPConnection(host) + if scheme != "http": + print >>sys.stderr, "Only http URLs are supported." + sys.exit(1) + + req = httplib.HTTPConnection(host, port) req.request("PUT", path, text) # {"Content-Length" : len(text)} resp = req.getresponse() print resp.read() diff -r ceadc72b6177 -r 408d78d241c2 tests/test_send.py --- a/tests/test_send.py Sun Oct 21 00:43:40 2012 +0200 +++ b/tests/test_send.py Sun Oct 21 18:41:38 2012 +0200 @@ -7,9 +7,8 @@ if __name__ == "__main__": signer = sys.argv[1] recipient = sys.argv[2] - host = sys.argv[3] - path = sys.argv[4] + "?action=PostMessage" - args = sys.argv[5:] + url = sys.argv[3] + "?action=PostMessage" + args = sys.argv[4:] if not args: print >>sys.stderr, "Need some updates as arguments to this program." @@ -26,7 +25,7 @@ try: signed_message = gpg.signMessage(email_message, signer) encrypted_message = gpg.encryptMessage(signed_message, recipient) - print sendMessage(encrypted_message, host, path) + print sendMessage(encrypted_message, url) except MoinMessageError, exc: print exc