1 # -*- coding: iso-8859-1 -*- 2 """ 3 MoinMoin - MoinMessageSupport library 4 5 @copyright: 2012, 2013 by Paul Boddie <paul@boddie.org.uk> 6 @license: GNU GPL (v2 or later), see COPYING.txt for details. 7 """ 8 9 from MoinMoin.Page import Page 10 from MoinMoin.log import getLogger 11 from MoinMoin.user import User 12 from MoinSupport import ItemStore, getHeader, getMetadata, getWikiDict, writeHeaders 13 from MoinMessage import GPG, MoinMessageError 14 from email.parser import Parser 15 16 try: 17 from cStringIO import StringIO 18 except ImportError: 19 from StringIO import StringIO 20 21 Dependencies = ['pages'] 22 23 class MoinMessageAction: 24 25 "Common message handling support for actions." 26 27 def __init__(self, pagename, request): 28 29 """ 30 On the page with the given 'pagename', use the given 'request' when 31 reading posted messages, modifying the Wiki. 32 """ 33 34 self.pagename = pagename 35 self.request = request 36 self.page = Page(request, pagename) 37 self.store = ItemStore(self.page, "messages", "message-locks") 38 39 def do_action(self): 40 request = self.request 41 content_length = getHeader(request, "Content-Length", "HTTP") 42 if content_length: 43 content_length = int(content_length) 44 45 self.handle_message_text(request.read(content_length)) 46 47 def handle_message_text(self, message_text): 48 49 "Handle the given 'message_text'." 50 51 message = Parser().parse(StringIO(message_text)) 52 self.handle_message(message) 53 54 def handle_message(self, message): 55 56 "Handle the given 'message'." 57 58 request = self.request 59 mimetype = message.get_content_type() 60 encoding = message.get_content_charset() 61 62 # Detect PGP/GPG-encoded payloads. 63 # See: http://tools.ietf.org/html/rfc3156 64 65 if mimetype == "multipart/signed" and \ 66 message.get_param("protocol") == "application/pgp-signature": 67 68 self.handle_signed_message(message) 69 70 elif mimetype == "multipart/encrypted" and \ 71 message.get_param("protocol") == "application/pgp-encrypted": 72 73 self.handle_encrypted_message(message) 74 75 # Reject unsigned payloads. 76 77 else: 78 writeHeaders(request, "text/plain", getMetadata(self.page), "415 Unsupported Media Type") 79 request.write("Only PGP/GPG-signed payloads are supported.") 80 81 def handle_encrypted_message(self, message): 82 83 "Handle the given encrypted 'message'." 84 85 request = self.request 86 87 try: 88 declaration, content = message.get_payload() 89 except ValueError: 90 writeHeaders(request, "text/plain", getMetadata(self.page), "415 Unsupported Media Type") 91 request.write("There must be a declaration and a content part for encrypted uploads.") 92 return 93 94 # Verify the message format. 95 96 if content.get_content_type() != "application/octet-stream": 97 writeHeaders(request, "text/plain", getMetadata(self.page), "415 Unsupported Media Type") 98 request.write("Encrypted data must be provided as application/octet-stream.") 99 return 100 101 homedir = self.get_homedir() 102 if not homedir: 103 return 104 105 gpg = GPG(homedir) 106 107 # Get the decrypted message text. 108 109 try: 110 text = gpg.decryptMessage(content.get_payload()) 111 112 # Log non-fatal errors. 113 114 if gpg.errors: 115 getLogger(__name__).warning(gpg.errors) 116 117 # Handle the embedded message. 118 119 self.handle_message_text(text) 120 121 # Otherwise, reject the unverified message. 122 123 except MoinMessageError: 124 writeHeaders(request, "text/plain", getMetadata(self.page), "403 Forbidden") 125 request.write("The message could not be decrypted.") 126 127 def handle_signed_message(self, message): 128 129 "Handle the given signed 'message'." 130 131 request = self.request 132 133 # NOTE: RFC 3156 states that signed messages should employ a detached 134 # NOTE: signature but then shows "BEGIN PGP MESSAGE" for signatures 135 # NOTE: instead of "BEGIN PGP SIGNATURE". 136 # NOTE: The "micalg" parameter is currently not supported. 137 138 try: 139 content, signature = message.get_payload() 140 except ValueError: 141 writeHeaders(request, "text/plain", getMetadata(self.page), "415 Unsupported Media Type") 142 request.write("There must be a content part and a signature for signed uploads.") 143 return 144 145 # Verify the message format. 146 147 if signature.get_content_type() != "application/pgp-signature": 148 writeHeaders(request, "text/plain", getMetadata(self.page), "415 Unsupported Media Type") 149 request.write("Signature data must be provided in the second part as application/pgp-signature.") 150 return 151 152 homedir = self.get_homedir() 153 if not homedir: 154 return 155 156 gpg = GPG(homedir) 157 158 # Verify the message. 159 160 try: 161 fingerprint, identity = gpg.verifyMessage(signature.get_payload(), content.as_string()) 162 163 # Map the fingerprint to a Wiki user. 164 165 old_user = None 166 request = self.request 167 168 try: 169 if fingerprint: 170 gpg_users = getWikiDict( 171 getattr(request.cfg, "moinmessage_gpg_users_page", "MoinMessageUserDict"), 172 request 173 ) 174 175 # With a user mapping and a fingerprint corresponding to a known 176 # user, temporarily switch user in order to make the edit. 177 178 if gpg_users and gpg_users.has_key(fingerprint): 179 old_user = request.user 180 request.user = User(request, auth_method="gpg", auth_username=gpg_users[fingerprint]) 181 182 # Log non-fatal errors. 183 184 if gpg.errors: 185 getLogger(__name__).warning(gpg.errors) 186 187 # Handle the embedded message. 188 189 self.handle_message_content(content) 190 191 # Restore any user identity. 192 193 finally: 194 if old_user: 195 request.user = old_user 196 197 # Otherwise, reject the unverified message. 198 199 except MoinMessageError: 200 writeHeaders(request, "text/plain", getMetadata(self.page), "403 Forbidden") 201 request.write("The message could not be verified.") 202 203 def handle_message_content(self, content): 204 205 "Handle the given message 'content'." 206 207 pass 208 209 def get_homedir(self): 210 211 "Locate the GPG home directory." 212 213 homedir = getattr(self.request.cfg, "moinmessage_gpg_homedir") 214 if not homedir: 215 writeHeaders(request, "text/plain", getMetadata(self.page), "415 Unsupported Media Type") 216 request.write("Encoded data cannot currently be understood. Please notify the site administrator.") 217 return homedir 218 219 # vim: tabstop=4 expandtab shiftwidth=4