1.1 --- a/MoinMessage.py Thu May 30 21:41:23 2013 +0200
1.2 +++ b/MoinMessage.py Sun Jun 02 01:34:19 2013 +0200
1.3 @@ -21,6 +21,12 @@
1.4 def is_collection(message):
1.5 return message.get("Update-Type") == "collection"
1.6
1.7 +def to_replace(message):
1.8 + return message.get("Update-Action") == "replace"
1.9 +
1.10 +def to_store(message):
1.11 + return message.get("Update-Action") == "store"
1.12 +
1.13 class Message:
1.14
1.15 "An update message."
2.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
2.2 +++ b/MoinMessageSupport.py Sun Jun 02 01:34:19 2013 +0200
2.3 @@ -0,0 +1,219 @@
2.4 +# -*- coding: iso-8859-1 -*-
2.5 +"""
2.6 + MoinMoin - MoinMessageSupport library
2.7 +
2.8 + @copyright: 2012, 2013 by Paul Boddie <paul@boddie.org.uk>
2.9 + @license: GNU GPL (v2 or later), see COPYING.txt for details.
2.10 +"""
2.11 +
2.12 +from MoinMoin.Page import Page
2.13 +from MoinMoin.log import getLogger
2.14 +from MoinMoin.user import User
2.15 +from MoinSupport import ItemStore, getHeader, getMetadata, getWikiDict, writeHeaders
2.16 +from MoinMessage import GPG, MoinMessageError
2.17 +from email.parser import Parser
2.18 +
2.19 +try:
2.20 + from cStringIO import StringIO
2.21 +except ImportError:
2.22 + from StringIO import StringIO
2.23 +
2.24 +Dependencies = ['pages']
2.25 +
2.26 +class MoinMessageAction:
2.27 +
2.28 + "Common message handling support for actions."
2.29 +
2.30 + def __init__(self, pagename, request):
2.31 +
2.32 + """
2.33 + On the page with the given 'pagename', use the given 'request' when
2.34 + reading posted messages, modifying the Wiki.
2.35 + """
2.36 +
2.37 + self.pagename = pagename
2.38 + self.request = request
2.39 + self.page = Page(request, pagename)
2.40 + self.store = ItemStore(self.page, "messages", "message-locks")
2.41 +
2.42 + def do_action(self):
2.43 + request = self.request
2.44 + content_length = getHeader(request, "Content-Length", "HTTP")
2.45 + if content_length:
2.46 + content_length = int(content_length)
2.47 +
2.48 + self.handle_message_text(request.read(content_length))
2.49 +
2.50 + def handle_message_text(self, message_text):
2.51 +
2.52 + "Handle the given 'message_text'."
2.53 +
2.54 + message = Parser().parse(StringIO(message_text))
2.55 + self.handle_message(message)
2.56 +
2.57 + def handle_message(self, message):
2.58 +
2.59 + "Handle the given 'message'."
2.60 +
2.61 + request = self.request
2.62 + mimetype = message.get_content_type()
2.63 + encoding = message.get_content_charset()
2.64 +
2.65 + # Detect PGP/GPG-encoded payloads.
2.66 + # See: http://tools.ietf.org/html/rfc3156
2.67 +
2.68 + if mimetype == "multipart/signed" and \
2.69 + message.get_param("protocol") == "application/pgp-signature":
2.70 +
2.71 + self.handle_signed_message(message)
2.72 +
2.73 + elif mimetype == "multipart/encrypted" and \
2.74 + message.get_param("protocol") == "application/pgp-encrypted":
2.75 +
2.76 + self.handle_encrypted_message(message)
2.77 +
2.78 + # Reject unsigned payloads.
2.79 +
2.80 + else:
2.81 + writeHeaders(request, "text/plain", getMetadata(self.page), "415 Unsupported Media Type")
2.82 + request.write("Only PGP/GPG-signed payloads are supported.")
2.83 +
2.84 + def handle_encrypted_message(self, message):
2.85 +
2.86 + "Handle the given encrypted 'message'."
2.87 +
2.88 + request = self.request
2.89 +
2.90 + try:
2.91 + declaration, content = message.get_payload()
2.92 + except ValueError:
2.93 + writeHeaders(request, "text/plain", getMetadata(self.page), "415 Unsupported Media Type")
2.94 + request.write("There must be a declaration and a content part for encrypted uploads.")
2.95 + return
2.96 +
2.97 + # Verify the message format.
2.98 +
2.99 + if content.get_content_type() != "application/octet-stream":
2.100 + writeHeaders(request, "text/plain", getMetadata(self.page), "415 Unsupported Media Type")
2.101 + request.write("Encrypted data must be provided as application/octet-stream.")
2.102 + return
2.103 +
2.104 + homedir = self.get_homedir()
2.105 + if not homedir:
2.106 + return
2.107 +
2.108 + gpg = GPG(homedir)
2.109 +
2.110 + # Get the decrypted message text.
2.111 +
2.112 + try:
2.113 + text = gpg.decryptMessage(content.get_payload())
2.114 +
2.115 + # Log non-fatal errors.
2.116 +
2.117 + if gpg.errors:
2.118 + getLogger(__name__).warning(gpg.errors)
2.119 +
2.120 + # Handle the embedded message.
2.121 +
2.122 + self.handle_message_text(text)
2.123 +
2.124 + # Otherwise, reject the unverified message.
2.125 +
2.126 + except MoinMessageError:
2.127 + writeHeaders(request, "text/plain", getMetadata(self.page), "403 Forbidden")
2.128 + request.write("The message could not be decrypted.")
2.129 +
2.130 + def handle_signed_message(self, message):
2.131 +
2.132 + "Handle the given signed 'message'."
2.133 +
2.134 + request = self.request
2.135 +
2.136 + # NOTE: RFC 3156 states that signed messages should employ a detached
2.137 + # NOTE: signature but then shows "BEGIN PGP MESSAGE" for signatures
2.138 + # NOTE: instead of "BEGIN PGP SIGNATURE".
2.139 + # NOTE: The "micalg" parameter is currently not supported.
2.140 +
2.141 + try:
2.142 + content, signature = message.get_payload()
2.143 + except ValueError:
2.144 + writeHeaders(request, "text/plain", getMetadata(self.page), "415 Unsupported Media Type")
2.145 + request.write("There must be a content part and a signature for signed uploads.")
2.146 + return
2.147 +
2.148 + # Verify the message format.
2.149 +
2.150 + if signature.get_content_type() != "application/pgp-signature":
2.151 + writeHeaders(request, "text/plain", getMetadata(self.page), "415 Unsupported Media Type")
2.152 + request.write("Signature data must be provided in the second part as application/pgp-signature.")
2.153 + return
2.154 +
2.155 + homedir = self.get_homedir()
2.156 + if not homedir:
2.157 + return
2.158 +
2.159 + gpg = GPG(homedir)
2.160 +
2.161 + # Verify the message.
2.162 +
2.163 + try:
2.164 + fingerprint, identity = gpg.verifyMessage(signature.get_payload(), content.as_string())
2.165 +
2.166 + # Map the fingerprint to a Wiki user.
2.167 +
2.168 + old_user = None
2.169 + request = self.request
2.170 +
2.171 + try:
2.172 + if fingerprint:
2.173 + gpg_users = getWikiDict(
2.174 + getattr(request.cfg, "moinmessage_gpg_users_page", "MoinMessageUserDict"),
2.175 + request
2.176 + )
2.177 +
2.178 + # With a user mapping and a fingerprint corresponding to a known
2.179 + # user, temporarily switch user in order to make the edit.
2.180 +
2.181 + if gpg_users and gpg_users.has_key(fingerprint):
2.182 + old_user = request.user
2.183 + request.user = User(request, auth_method="gpg", auth_username=gpg_users[fingerprint])
2.184 +
2.185 + # Log non-fatal errors.
2.186 +
2.187 + if gpg.errors:
2.188 + getLogger(__name__).warning(gpg.errors)
2.189 +
2.190 + # Handle the embedded message.
2.191 +
2.192 + self.handle_message_content(content)
2.193 +
2.194 + # Restore any user identity.
2.195 +
2.196 + finally:
2.197 + if old_user:
2.198 + request.user = old_user
2.199 +
2.200 + # Otherwise, reject the unverified message.
2.201 +
2.202 + except MoinMessageError:
2.203 + writeHeaders(request, "text/plain", getMetadata(self.page), "403 Forbidden")
2.204 + request.write("The message could not be verified.")
2.205 +
2.206 + def handle_message_content(self, content):
2.207 +
2.208 + "Handle the given message 'content'."
2.209 +
2.210 + pass
2.211 +
2.212 + def get_homedir(self):
2.213 +
2.214 + "Locate the GPG home directory."
2.215 +
2.216 + homedir = getattr(self.request.cfg, "moinmessage_gpg_homedir")
2.217 + if not homedir:
2.218 + writeHeaders(request, "text/plain", getMetadata(self.page), "415 Unsupported Media Type")
2.219 + request.write("Encoded data cannot currently be understood. Please notify the site administrator.")
2.220 + return homedir
2.221 +
2.222 +# vim: tabstop=4 expandtab shiftwidth=4
3.1 --- a/actions/PostMessage.py Thu May 30 21:41:23 2013 +0200
3.2 +++ b/actions/PostMessage.py Sun Jun 02 01:34:19 2013 +0200
3.3 @@ -8,201 +8,18 @@
3.4
3.5 from MoinMoin.Page import Page
3.6 from MoinMoin.PageEditor import PageEditor
3.7 -from MoinMoin.log import getLogger
3.8 -from MoinMoin.user import User
3.9 from MoinMoin import wikiutil
3.10 -from MoinSupport import ItemStore, getHeader, getMetadata, getWikiDict, writeHeaders
3.11 -from MoinMessage import GPG, Message, MoinMessageError, is_collection
3.12 -from email.parser import Parser
3.13 +from MoinSupport import getMetadata, writeHeaders
3.14 +from MoinMessage import Message, is_collection, to_replace, to_store
3.15 +from MoinMessageSupport import MoinMessageAction
3.16 import time
3.17
3.18 -try:
3.19 - from cStringIO import StringIO
3.20 -except ImportError:
3.21 - from StringIO import StringIO
3.22 -
3.23 Dependencies = ['pages']
3.24
3.25 -class PostMessage:
3.26 +class PostMessage(MoinMessageAction):
3.27
3.28 "A posted message handler."
3.29
3.30 - def __init__(self, pagename, request):
3.31 -
3.32 - """
3.33 - On the page with the given 'pagename', use the given 'request' when
3.34 - reading posted messages, modifying the Wiki.
3.35 - """
3.36 -
3.37 - self.pagename = pagename
3.38 - self.request = request
3.39 - self.page = Page(request, pagename)
3.40 - self.store = ItemStore(self.page, "messages", "message-locks")
3.41 -
3.42 - def do_action(self):
3.43 - request = self.request
3.44 - content_length = getHeader(request, "Content-Length", "HTTP")
3.45 - if content_length:
3.46 - content_length = int(content_length)
3.47 -
3.48 - self.handle_message_text(request.read(content_length))
3.49 -
3.50 - def handle_message_text(self, message_text):
3.51 -
3.52 - "Handle the given 'message_text'."
3.53 -
3.54 - message = Parser().parse(StringIO(message_text))
3.55 - self.handle_message(message)
3.56 -
3.57 - def handle_message(self, message):
3.58 -
3.59 - "Handle the given 'message'."
3.60 -
3.61 - request = self.request
3.62 - mimetype = message.get_content_type()
3.63 - encoding = message.get_content_charset()
3.64 -
3.65 - # Detect PGP/GPG-encoded payloads.
3.66 - # See: http://tools.ietf.org/html/rfc3156
3.67 -
3.68 - if mimetype == "multipart/signed" and \
3.69 - message.get_param("protocol") == "application/pgp-signature":
3.70 -
3.71 - self.handle_signed_message(message)
3.72 -
3.73 - elif mimetype == "multipart/encrypted" and \
3.74 - message.get_param("protocol") == "application/pgp-encrypted":
3.75 -
3.76 - self.handle_encrypted_message(message)
3.77 -
3.78 - # Reject unsigned payloads.
3.79 -
3.80 - else:
3.81 - writeHeaders(request, "text/plain", getMetadata(self.page), "415 Unsupported Media Type")
3.82 - request.write("Only PGP/GPG-signed payloads are supported.")
3.83 -
3.84 - def handle_encrypted_message(self, message):
3.85 -
3.86 - "Handle the given encrypted 'message'."
3.87 -
3.88 - request = self.request
3.89 -
3.90 - try:
3.91 - declaration, content = message.get_payload()
3.92 - except ValueError:
3.93 - writeHeaders(request, "text/plain", getMetadata(self.page), "415 Unsupported Media Type")
3.94 - request.write("There must be a declaration and a content part for encrypted uploads.")
3.95 - return
3.96 -
3.97 - # Verify the message format.
3.98 -
3.99 - if content.get_content_type() != "application/octet-stream":
3.100 - writeHeaders(request, "text/plain", getMetadata(self.page), "415 Unsupported Media Type")
3.101 - request.write("Encrypted data must be provided as application/octet-stream.")
3.102 - return
3.103 -
3.104 - homedir = self.get_homedir()
3.105 - if not homedir:
3.106 - return
3.107 -
3.108 - gpg = GPG(homedir)
3.109 -
3.110 - # Get the decrypted message text.
3.111 -
3.112 - try:
3.113 - text = gpg.decryptMessage(content.get_payload())
3.114 -
3.115 - # Log non-fatal errors.
3.116 -
3.117 - if gpg.errors:
3.118 - getLogger(__name__).warning(gpg.errors)
3.119 -
3.120 - # Handle the embedded message.
3.121 -
3.122 - self.handle_message_text(text)
3.123 -
3.124 - # Otherwise, reject the unverified message.
3.125 -
3.126 - except MoinMessageError:
3.127 - writeHeaders(request, "text/plain", getMetadata(self.page), "403 Forbidden")
3.128 - request.write("The message could not be decrypted.")
3.129 -
3.130 - def handle_signed_message(self, message):
3.131 -
3.132 - "Handle the given signed 'message'."
3.133 -
3.134 - request = self.request
3.135 -
3.136 - # NOTE: RFC 3156 states that signed messages should employ a detached
3.137 - # NOTE: signature but then shows "BEGIN PGP MESSAGE" for signatures
3.138 - # NOTE: instead of "BEGIN PGP SIGNATURE".
3.139 - # NOTE: The "micalg" parameter is currently not supported.
3.140 -
3.141 - try:
3.142 - content, signature = message.get_payload()
3.143 - except ValueError:
3.144 - writeHeaders(request, "text/plain", getMetadata(self.page), "415 Unsupported Media Type")
3.145 - request.write("There must be a content part and a signature for signed uploads.")
3.146 - return
3.147 -
3.148 - # Verify the message format.
3.149 -
3.150 - if signature.get_content_type() != "application/pgp-signature":
3.151 - writeHeaders(request, "text/plain", getMetadata(self.page), "415 Unsupported Media Type")
3.152 - request.write("Signature data must be provided in the second part as application/pgp-signature.")
3.153 - return
3.154 -
3.155 - homedir = self.get_homedir()
3.156 - if not homedir:
3.157 - return
3.158 -
3.159 - gpg = GPG(homedir)
3.160 -
3.161 - # Verify the message.
3.162 -
3.163 - try:
3.164 - fingerprint, identity = gpg.verifyMessage(signature.get_payload(), content.as_string())
3.165 -
3.166 - # Map the fingerprint to a Wiki user.
3.167 -
3.168 - old_user = None
3.169 - request = self.request
3.170 -
3.171 - try:
3.172 - if fingerprint:
3.173 - gpg_users = getWikiDict(
3.174 - getattr(request.cfg, "moinmessage_gpg_users_page", "MoinMessageUserDict"),
3.175 - request
3.176 - )
3.177 -
3.178 - # With a user mapping and a fingerprint corresponding to a known
3.179 - # user, temporarily switch user in order to make the edit.
3.180 -
3.181 - if gpg_users and gpg_users.has_key(fingerprint):
3.182 - old_user = request.user
3.183 - request.user = User(request, auth_method="gpg", auth_username=gpg_users[fingerprint])
3.184 -
3.185 - # Log non-fatal errors.
3.186 -
3.187 - if gpg.errors:
3.188 - getLogger(__name__).warning(gpg.errors)
3.189 -
3.190 - # Handle the embedded message.
3.191 -
3.192 - self.handle_message_content(content)
3.193 -
3.194 - # Restore any user identity.
3.195 -
3.196 - finally:
3.197 - if old_user:
3.198 - request.user = old_user
3.199 -
3.200 - # Otherwise, reject the unverified message.
3.201 -
3.202 - except MoinMessageError:
3.203 - writeHeaders(request, "text/plain", getMetadata(self.page), "403 Forbidden")
3.204 - request.write("The message could not be verified.")
3.205 -
3.206 def handle_message_content(self, content):
3.207
3.208 "Handle the given message 'content'."
3.209 @@ -295,22 +112,6 @@
3.210
3.211 self.page = Page(self.request, self.pagename)
3.212
3.213 - def get_homedir(self):
3.214 -
3.215 - "Locate the GPG home directory."
3.216 -
3.217 - homedir = getattr(self.request.cfg, "moinmessage_gpg_homedir")
3.218 - if not homedir:
3.219 - writeHeaders(request, "text/plain", getMetadata(self.page), "415 Unsupported Media Type")
3.220 - request.write("Encoded data cannot currently be understood. Please notify the site administrator.")
3.221 - return homedir
3.222 -
3.223 -def to_replace(message):
3.224 - return message.get("Update-Action") == "replace"
3.225 -
3.226 -def to_store(message):
3.227 - return message.get("Update-Action") == "store"
3.228 -
3.229 # Action function.
3.230
3.231 def execute(pagename, request):
4.1 --- a/setup.py Thu May 30 21:41:23 2013 +0200
4.2 +++ b/setup.py Sun Jun 02 01:34:19 2013 +0200
4.3 @@ -9,5 +9,5 @@
4.4 author_email = "paul@boddie.org.uk",
4.5 url = "http://hgweb.boddie.org.uk/MoinMessage",
4.6 version = "0.1",
4.7 - py_modules = ["MoinMessage"]
4.8 + py_modules = ["MoinMessage", "MoinMessageSupport"]
4.9 )