1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
1.2 +++ b/MoinMessageSupport.py Sun Jun 02 01:34:19 2013 +0200
1.3 @@ -0,0 +1,219 @@
1.4 +# -*- coding: iso-8859-1 -*-
1.5 +"""
1.6 + MoinMoin - MoinMessageSupport library
1.7 +
1.8 + @copyright: 2012, 2013 by Paul Boddie <paul@boddie.org.uk>
1.9 + @license: GNU GPL (v2 or later), see COPYING.txt for details.
1.10 +"""
1.11 +
1.12 +from MoinMoin.Page import Page
1.13 +from MoinMoin.log import getLogger
1.14 +from MoinMoin.user import User
1.15 +from MoinSupport import ItemStore, getHeader, getMetadata, getWikiDict, writeHeaders
1.16 +from MoinMessage import GPG, MoinMessageError
1.17 +from email.parser import Parser
1.18 +
1.19 +try:
1.20 + from cStringIO import StringIO
1.21 +except ImportError:
1.22 + from StringIO import StringIO
1.23 +
1.24 +Dependencies = ['pages']
1.25 +
1.26 +class MoinMessageAction:
1.27 +
1.28 + "Common message handling support for actions."
1.29 +
1.30 + def __init__(self, pagename, request):
1.31 +
1.32 + """
1.33 + On the page with the given 'pagename', use the given 'request' when
1.34 + reading posted messages, modifying the Wiki.
1.35 + """
1.36 +
1.37 + self.pagename = pagename
1.38 + self.request = request
1.39 + self.page = Page(request, pagename)
1.40 + self.store = ItemStore(self.page, "messages", "message-locks")
1.41 +
1.42 + def do_action(self):
1.43 + request = self.request
1.44 + content_length = getHeader(request, "Content-Length", "HTTP")
1.45 + if content_length:
1.46 + content_length = int(content_length)
1.47 +
1.48 + self.handle_message_text(request.read(content_length))
1.49 +
1.50 + def handle_message_text(self, message_text):
1.51 +
1.52 + "Handle the given 'message_text'."
1.53 +
1.54 + message = Parser().parse(StringIO(message_text))
1.55 + self.handle_message(message)
1.56 +
1.57 + def handle_message(self, message):
1.58 +
1.59 + "Handle the given 'message'."
1.60 +
1.61 + request = self.request
1.62 + mimetype = message.get_content_type()
1.63 + encoding = message.get_content_charset()
1.64 +
1.65 + # Detect PGP/GPG-encoded payloads.
1.66 + # See: http://tools.ietf.org/html/rfc3156
1.67 +
1.68 + if mimetype == "multipart/signed" and \
1.69 + message.get_param("protocol") == "application/pgp-signature":
1.70 +
1.71 + self.handle_signed_message(message)
1.72 +
1.73 + elif mimetype == "multipart/encrypted" and \
1.74 + message.get_param("protocol") == "application/pgp-encrypted":
1.75 +
1.76 + self.handle_encrypted_message(message)
1.77 +
1.78 + # Reject unsigned payloads.
1.79 +
1.80 + else:
1.81 + writeHeaders(request, "text/plain", getMetadata(self.page), "415 Unsupported Media Type")
1.82 + request.write("Only PGP/GPG-signed payloads are supported.")
1.83 +
1.84 + def handle_encrypted_message(self, message):
1.85 +
1.86 + "Handle the given encrypted 'message'."
1.87 +
1.88 + request = self.request
1.89 +
1.90 + try:
1.91 + declaration, content = message.get_payload()
1.92 + except ValueError:
1.93 + writeHeaders(request, "text/plain", getMetadata(self.page), "415 Unsupported Media Type")
1.94 + request.write("There must be a declaration and a content part for encrypted uploads.")
1.95 + return
1.96 +
1.97 + # Verify the message format.
1.98 +
1.99 + if content.get_content_type() != "application/octet-stream":
1.100 + writeHeaders(request, "text/plain", getMetadata(self.page), "415 Unsupported Media Type")
1.101 + request.write("Encrypted data must be provided as application/octet-stream.")
1.102 + return
1.103 +
1.104 + homedir = self.get_homedir()
1.105 + if not homedir:
1.106 + return
1.107 +
1.108 + gpg = GPG(homedir)
1.109 +
1.110 + # Get the decrypted message text.
1.111 +
1.112 + try:
1.113 + text = gpg.decryptMessage(content.get_payload())
1.114 +
1.115 + # Log non-fatal errors.
1.116 +
1.117 + if gpg.errors:
1.118 + getLogger(__name__).warning(gpg.errors)
1.119 +
1.120 + # Handle the embedded message.
1.121 +
1.122 + self.handle_message_text(text)
1.123 +
1.124 + # Otherwise, reject the unverified message.
1.125 +
1.126 + except MoinMessageError:
1.127 + writeHeaders(request, "text/plain", getMetadata(self.page), "403 Forbidden")
1.128 + request.write("The message could not be decrypted.")
1.129 +
1.130 + def handle_signed_message(self, message):
1.131 +
1.132 + "Handle the given signed 'message'."
1.133 +
1.134 + request = self.request
1.135 +
1.136 + # NOTE: RFC 3156 states that signed messages should employ a detached
1.137 + # NOTE: signature but then shows "BEGIN PGP MESSAGE" for signatures
1.138 + # NOTE: instead of "BEGIN PGP SIGNATURE".
1.139 + # NOTE: The "micalg" parameter is currently not supported.
1.140 +
1.141 + try:
1.142 + content, signature = message.get_payload()
1.143 + except ValueError:
1.144 + writeHeaders(request, "text/plain", getMetadata(self.page), "415 Unsupported Media Type")
1.145 + request.write("There must be a content part and a signature for signed uploads.")
1.146 + return
1.147 +
1.148 + # Verify the message format.
1.149 +
1.150 + if signature.get_content_type() != "application/pgp-signature":
1.151 + writeHeaders(request, "text/plain", getMetadata(self.page), "415 Unsupported Media Type")
1.152 + request.write("Signature data must be provided in the second part as application/pgp-signature.")
1.153 + return
1.154 +
1.155 + homedir = self.get_homedir()
1.156 + if not homedir:
1.157 + return
1.158 +
1.159 + gpg = GPG(homedir)
1.160 +
1.161 + # Verify the message.
1.162 +
1.163 + try:
1.164 + fingerprint, identity = gpg.verifyMessage(signature.get_payload(), content.as_string())
1.165 +
1.166 + # Map the fingerprint to a Wiki user.
1.167 +
1.168 + old_user = None
1.169 + request = self.request
1.170 +
1.171 + try:
1.172 + if fingerprint:
1.173 + gpg_users = getWikiDict(
1.174 + getattr(request.cfg, "moinmessage_gpg_users_page", "MoinMessageUserDict"),
1.175 + request
1.176 + )
1.177 +
1.178 + # With a user mapping and a fingerprint corresponding to a known
1.179 + # user, temporarily switch user in order to make the edit.
1.180 +
1.181 + if gpg_users and gpg_users.has_key(fingerprint):
1.182 + old_user = request.user
1.183 + request.user = User(request, auth_method="gpg", auth_username=gpg_users[fingerprint])
1.184 +
1.185 + # Log non-fatal errors.
1.186 +
1.187 + if gpg.errors:
1.188 + getLogger(__name__).warning(gpg.errors)
1.189 +
1.190 + # Handle the embedded message.
1.191 +
1.192 + self.handle_message_content(content)
1.193 +
1.194 + # Restore any user identity.
1.195 +
1.196 + finally:
1.197 + if old_user:
1.198 + request.user = old_user
1.199 +
1.200 + # Otherwise, reject the unverified message.
1.201 +
1.202 + except MoinMessageError:
1.203 + writeHeaders(request, "text/plain", getMetadata(self.page), "403 Forbidden")
1.204 + request.write("The message could not be verified.")
1.205 +
1.206 + def handle_message_content(self, content):
1.207 +
1.208 + "Handle the given message 'content'."
1.209 +
1.210 + pass
1.211 +
1.212 + def get_homedir(self):
1.213 +
1.214 + "Locate the GPG home directory."
1.215 +
1.216 + homedir = getattr(self.request.cfg, "moinmessage_gpg_homedir")
1.217 + if not homedir:
1.218 + writeHeaders(request, "text/plain", getMetadata(self.page), "415 Unsupported Media Type")
1.219 + request.write("Encoded data cannot currently be understood. Please notify the site administrator.")
1.220 + return homedir
1.221 +
1.222 +# vim: tabstop=4 expandtab shiftwidth=4