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 MoinMoin import wikiutil 13 from MoinSupport import getHeader, getMetadata, getWikiDict, writeHeaders, \ 14 parseDictEntry 15 from ItemSupport import ItemStore 16 from MoinMessage import GPG, Message, MoinMessageError, \ 17 MoinMessageMissingPart, MoinMessageBadContent, \ 18 is_signed, is_encrypted, getContentAndSignature 19 from email.parser import Parser 20 import time 21 22 try: 23 from cStringIO import StringIO 24 except ImportError: 25 from StringIO import StringIO 26 27 Dependencies = ['pages'] 28 29 class MoinMessageAction: 30 31 "Common message handling support for actions." 32 33 def __init__(self, pagename, request): 34 35 """ 36 On the page with the given 'pagename', use the given 'request' when 37 reading posted messages, modifying the Wiki. 38 """ 39 40 self.pagename = pagename 41 self.request = request 42 self.page = Page(request, pagename) 43 self.init_store() 44 45 def init_store(self): 46 self.store = ItemStore(self.page, "messages", "message-locks") 47 48 def do_action(self): 49 request = self.request 50 content_length = getHeader(request, "Content-Length", "HTTP") 51 if content_length: 52 content_length = int(content_length) 53 54 self.handle_message_text(request.read(content_length)) 55 56 def handle_message_text(self, message_text): 57 58 "Handle the given 'message_text'." 59 60 request = self.request 61 message = Parser().parse(StringIO(message_text)) 62 63 # Detect any indicated recipient and change the target page, if 64 # appropriate. 65 66 if message.has_key("To"): 67 try: 68 parameters = get_recipient_details(request, message["To"], main=True) 69 except MoinMessageRecipientError, exc: 70 writeHeaders(request, "text/plain", getMetadata(self.page), "403 Forbidden") 71 request.write("The recipient indicated in the message is not known to this site. " 72 "Details: %s" % exc.message) 73 return 74 else: 75 if parameters["type"] == "page": 76 self.page = Page(request, parameters["location"]) 77 self.init_store() 78 79 # NOTE: Support "url". 80 81 # Handle the parsed message. 82 83 self.handle_message(message) 84 85 def handle_message(self, message): 86 87 "Handle the given 'message'." 88 89 # Detect PGP/GPG-encoded payloads. 90 # See: http://tools.ietf.org/html/rfc3156 91 92 # Signed payloads are checked and then passed on for further processing 93 # elsewhere. Verification is the last step in this base implementation, 94 # even if an encrypted-then-signed payload is involved. 95 96 if is_signed(message): 97 self.handle_signed_message(message) 98 99 # Encrypted payloads are decrypted and then sent back into this method 100 # for signature checking as described above. Thus, signed-then-encrypted 101 # payloads are first decrypted and then verified. 102 103 elif is_encrypted(message): 104 self.handle_encrypted_message(message) 105 106 # Reject unsigned and unencrypted payloads. 107 108 else: 109 request = self.request 110 writeHeaders(request, "text/plain", getMetadata(self.page), "415 Unsupported Media Type") 111 request.write("Only PGP/GPG-signed payloads are supported.") 112 113 def handle_encrypted_message(self, message): 114 115 "Handle the given encrypted 'message'." 116 117 request = self.request 118 119 homedir = self.get_homedir() 120 if not homedir: 121 return 122 123 gpg = GPG(homedir) 124 125 try: 126 text = gpg.decryptMessage(message) 127 128 # Reject messages without a declaration. 129 130 except MoinMessageMissingPart: 131 writeHeaders(request, "text/plain", getMetadata(self.page), "415 Unsupported Media Type") 132 request.write("There must be a declaration and a content part for encrypted uploads.") 133 return 134 135 # Reject messages without appropriate content. 136 137 except MoinMessageBadContent: 138 writeHeaders(request, "text/plain", getMetadata(self.page), "415 Unsupported Media Type") 139 request.write("Encrypted data must be provided as application/octet-stream.") 140 return 141 142 # Reject any unencryptable message. 143 144 except MoinMessageError: 145 writeHeaders(request, "text/plain", getMetadata(self.page), "403 Forbidden") 146 request.write("The message could not be decrypted.") 147 return 148 149 # Log non-fatal errors. 150 151 if gpg.errors: 152 getLogger(__name__).warning(gpg.errors) 153 154 # Handle the embedded message which may itself be a signed message. 155 156 self.handle_message_text(text) 157 158 def handle_signed_message(self, message): 159 160 "Handle the given signed 'message'." 161 162 request = self.request 163 164 # Accept any message whose sender was authenticated by the PGP method. 165 166 if request.user and request.user.valid and request.user.auth_method == "pgp": 167 168 # Handle the embedded message. 169 170 content, signature = getContentAndSignature(message) 171 self.handle_message_content(content) 172 173 # Reject any unverified message. 174 175 else: 176 writeHeaders(request, "text/plain", getMetadata(self.page), "403 Forbidden") 177 request.write("The message could not be verified. " 178 "Maybe this site is not performing authentication using PGP signatures.") 179 180 def handle_message_content(self, content): 181 182 "Handle the given message 'content'." 183 184 request = self.request 185 186 # Interpret the content as one or more updates. 187 188 message = Message() 189 message.handle_message(content) 190 191 # Test any date against the page or message store. 192 193 if message.date: 194 store_date = time.gmtime(self.store.mtime()) 195 page_date = time.gmtime(wikiutil.version2timestamp(self.page.mtime_usecs())) 196 last_date = max(store_date, page_date) 197 198 # Reject messages older than the page date. 199 200 if message.date < last_date: 201 writeHeaders(request, "text/plain", getMetadata(self.page), "403 Forbidden") 202 request.write("The message is too old: %s versus %s." % (message.date, last_date)) 203 return 204 205 # Reject messages without dates if so configured. 206 207 elif getattr(request.cfg, "moinmessage_reject_messages_without_dates", True): 208 writeHeaders(request, "text/plain", getMetadata(self.page), "403 Forbidden") 209 request.write("The message has no date information.") 210 return 211 212 # Handle the message as an object. 213 214 self.handle_message_object(message) 215 216 def get_homedir(self): 217 218 "Locate the GPG home directory." 219 220 request = self.request 221 homedir = get_homedir(self.request) 222 223 if not homedir: 224 writeHeaders(request, "text/plain", getMetadata(self.page), "415 Unsupported Media Type") 225 request.write("Encoded data cannot currently be understood. Please notify the site administrator.") 226 227 return homedir 228 229 class MoinMessageRecipientError(MoinMessageError): 230 pass 231 232 class MoinMessageNoRecipients(MoinMessageRecipientError): 233 pass 234 235 class MoinMessageUnknownRecipient(MoinMessageRecipientError): 236 pass 237 238 class MoinMessageBadRecipient(MoinMessageRecipientError): 239 pass 240 241 def get_homedir(request): 242 243 "Locate the GPG home directory." 244 245 return getattr(request.cfg, "moinmessage_gpg_homedir") 246 247 def get_signing_users(request): 248 249 "Return a dictionary mapping usernames to signing keys." 250 251 return getWikiDict( 252 getattr(request.cfg, "moinmessage_gpg_signing_users_page", "MoinMessageSigningUserDict"), 253 request) 254 255 def get_relays(request): 256 257 "Return a dictionary mapping relays to URLs." 258 259 return getWikiDict( 260 getattr(request.cfg, "moinmessage_gpg_relays_page", "MoinMessageRelayDict"), 261 request) 262 263 def get_recipients(request, main=False): 264 265 """ 266 Return the recipients dictionary by first obtaining the page in which it 267 is stored. This page may either be a subpage of the user's home page, if 268 stored on this wiki, or it may be relative to the site root. 269 270 When 'main' is specified and set to a true value, only a dictionary under 271 the site root is consulted. 272 273 The name of the subpage is defined by the configuration setting 274 'moinmessage_gpg_recipients_page', which if absent is set to 275 "MoinMessageRecipientsDict". 276 """ 277 278 subpage = getattr(request.cfg, "moinmessage_gpg_recipients_page", "MoinMessageRecipientsDict") 279 280 if not main: 281 homedetails = wikiutil.getInterwikiHomePage(request) 282 283 if homedetails: 284 homewiki, homepage = homedetails 285 if homewiki == "Self": 286 recipients = getWikiDict("%s/%s" % (homepage, subpage), request) 287 if recipients: 288 return recipients 289 290 return getWikiDict(subpage, request) 291 292 def get_username_for_fingerprint(request, fingerprint): 293 294 """ 295 Using the 'request', return the username corresponding to the given key 296 'fingerprint' or None if no correspondence is present in the mapping page. 297 """ 298 299 gpg_users = getWikiDict( 300 getattr(request.cfg, "moinmessage_gpg_users_page", "MoinMessageUserDict"), 301 request, 302 superuser=True # disable user test because we have no user yet 303 ) 304 305 # With a user mapping and a fingerprint corresponding to a known 306 # user, temporarily switch user in order to make the edit. 307 308 if gpg_users and gpg_users.has_key(fingerprint): 309 return gpg_users[fingerprint] 310 else: 311 return None 312 313 def get_recipient_details(request, recipient, main=False): 314 315 """ 316 Using the 'request', return a dictionary of details for the specified 317 'recipient'. If no details exist, raise a MoinMessageRecipientError 318 exception. 319 320 When 'main' is specified and set to a true value, only the recipients 321 dictionary under the site root is consulted. 322 """ 323 324 _ = request.getText 325 326 recipients = get_recipients(request, main) 327 if not recipients: 328 raise MoinMessageNoRecipients, _("No recipients page is defined for MoinMessage.") 329 330 recipient_details = recipients.get(recipient) 331 if not recipient_details: 332 raise MoinMessageUnknownRecipient, _("The specified recipient is not present in the list of known contacts.") 333 334 parameters = parseDictEntry(recipient_details, ("type", "location", "fingerprint",)) 335 336 if not parameters.has_key("type"): 337 raise MoinMessageBadRecipient, _("The recipient details are missing a destination type.") 338 339 if not parameters.has_key("location"): 340 raise MoinMessageBadRecipient, _("The recipient details are missing a location for sent messages.") 341 342 if parameters["type"] in ("url", "relay") and not parameters.has_key("fingerprint"): 343 raise MoinMessageBadRecipient, _("The recipient details are missing a fingerprint for sending messages.") 344 345 return parameters 346 347 # vim: tabstop=4 expandtab shiftwidth=4