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 writeHeaders(request, "text/plain", getMetadata(self.page), "403 Forbidden") 122 request.write("This site is not configured for this request.") 123 return 124 125 gpg = GPG(homedir) 126 127 try: 128 text = gpg.decryptMessage(message) 129 130 # Reject messages without a declaration. 131 132 except MoinMessageMissingPart: 133 writeHeaders(request, "text/plain", getMetadata(self.page), "415 Unsupported Media Type") 134 request.write("There must be a declaration and a content part for encrypted uploads.") 135 return 136 137 # Reject messages without appropriate content. 138 139 except MoinMessageBadContent: 140 writeHeaders(request, "text/plain", getMetadata(self.page), "415 Unsupported Media Type") 141 request.write("Encrypted data must be provided as application/octet-stream.") 142 return 143 144 # Reject any unencryptable message. 145 146 except MoinMessageError: 147 writeHeaders(request, "text/plain", getMetadata(self.page), "403 Forbidden") 148 request.write("The message could not be decrypted.") 149 return 150 151 # Log non-fatal errors. 152 153 if gpg.errors: 154 getLogger(__name__).warning(gpg.errors) 155 156 # Handle the embedded message which may itself be a signed message. 157 158 self.handle_message_text(text) 159 160 def handle_signed_message(self, message): 161 162 "Handle the given signed 'message'." 163 164 request = self.request 165 166 # Accept any message whose sender was authenticated by the PGP method. 167 168 if request.user and request.user.valid and request.user.auth_method == "pgp": 169 170 # Handle the embedded message. 171 172 content, signature = getContentAndSignature(message) 173 self.handle_message_content(content) 174 175 # Reject any unverified message. 176 177 else: 178 writeHeaders(request, "text/plain", getMetadata(self.page), "403 Forbidden") 179 request.write("The message could not be verified. " 180 "Maybe this site is not performing authentication using PGP signatures.") 181 182 def handle_message_content(self, content): 183 184 "Handle the given message 'content'." 185 186 request = self.request 187 188 # Interpret the content as one or more updates. 189 190 message = Message() 191 message.handle_message(content) 192 193 # Test any date against the page or message store. 194 195 if message.date: 196 store_date = time.gmtime(self.store.mtime()) 197 page_date = time.gmtime(wikiutil.version2timestamp(self.page.mtime_usecs())) 198 last_date = max(store_date, page_date) 199 200 # Reject messages older than the page date. 201 202 if message.date < last_date: 203 writeHeaders(request, "text/plain", getMetadata(self.page), "403 Forbidden") 204 request.write("The message is too old: %s versus %s." % (message.date, last_date)) 205 return 206 207 # Reject messages without dates if so configured. 208 209 elif getattr(request.cfg, "moinmessage_reject_messages_without_dates", True): 210 writeHeaders(request, "text/plain", getMetadata(self.page), "403 Forbidden") 211 request.write("The message has no date information.") 212 return 213 214 # Handle the message as an object. 215 216 self.handle_message_object(message) 217 218 def get_homedir(self): 219 220 "Locate the GPG home directory." 221 222 request = self.request 223 homedir = get_homedir(self.request) 224 225 if not homedir: 226 writeHeaders(request, "text/plain", getMetadata(self.page), "415 Unsupported Media Type") 227 request.write("Encoded data cannot currently be understood. Please notify the site administrator.") 228 229 return homedir 230 231 class MoinMessageRecipientError(MoinMessageError): 232 pass 233 234 class MoinMessageNoRecipients(MoinMessageRecipientError): 235 pass 236 237 class MoinMessageUnknownRecipient(MoinMessageRecipientError): 238 pass 239 240 class MoinMessageBadRecipient(MoinMessageRecipientError): 241 pass 242 243 def get_homedir(request): 244 245 "Locate the GPG home directory." 246 247 return getattr(request.cfg, "moinmessage_gpg_homedir") 248 249 def get_signing_users(request): 250 251 "Return a dictionary mapping usernames to signing keys." 252 253 return getWikiDict( 254 getattr(request.cfg, "moinmessage_gpg_signing_users_page", "MoinMessageSigningUserDict"), 255 request) 256 257 def get_relays(request): 258 259 "Return a dictionary mapping relays to URLs." 260 261 return getWikiDict( 262 getattr(request.cfg, "moinmessage_gpg_relays_page", "MoinMessageRelayDict"), 263 request) 264 265 def get_recipients(request, main=False, sending=True, fetching=True): 266 267 """ 268 Return the recipients dictionary by first obtaining the page in which it 269 is stored. This page may either be a subpage of the user's home page, if 270 stored on this wiki, or it may be relative to the site root. 271 272 When 'main' is specified and set to a true value, only a dictionary under 273 the site root is consulted. 274 275 When 'sending' or 'fetching' is specified and set to a false value, any 276 recipients of the indicated type will be excluded from the result of this 277 function. 278 279 The name of the subpage is defined by the configuration setting 280 'moinmessage_gpg_recipients_page', which if absent is set to 281 "MoinMessageRecipientsDict". 282 """ 283 284 subpage = getattr(request.cfg, "moinmessage_gpg_recipients_page", "MoinMessageRecipientsDict") 285 286 if not main: 287 homedetails = wikiutil.getInterwikiHomePage(request) 288 289 if homedetails: 290 homewiki, homepage = homedetails 291 if homewiki == "Self": 292 recipients = getWikiDict("%s/%s" % (homepage, subpage), request) 293 if recipients: 294 return filter_recipients(recipients, sending, fetching) 295 296 return filter_recipients(getWikiDict(subpage, request), sending, fetching) 297 298 def get_username_for_fingerprint(request, fingerprint): 299 300 """ 301 Using the 'request', return the username corresponding to the given key 302 'fingerprint' or None if no correspondence is present in the mapping page. 303 """ 304 305 gpg_users = getWikiDict( 306 getattr(request.cfg, "moinmessage_gpg_users_page", "MoinMessageUserDict"), 307 request, 308 superuser=True # disable user test because we have no user yet 309 ) 310 311 # With a user mapping and a fingerprint corresponding to a known 312 # user, temporarily switch user in order to make the edit. 313 314 if gpg_users and gpg_users.has_key(fingerprint): 315 return gpg_users[fingerprint] 316 else: 317 return None 318 319 def get_recipient_details(request, recipient, main=False, fetching=False): 320 321 """ 322 Using the 'request', return a dictionary of details for the specified 323 'recipient'. If no details exist, raise a MoinMessageRecipientError 324 exception. 325 326 When 'main' is specified and set to a true value, only the recipients 327 dictionary under the site root is consulted. 328 329 When 'fetching' is specified and set to a true value, the recipient need 330 not have a "type" or "location" defined, but it must have a "fingerprint" 331 defined. 332 """ 333 334 _ = request.getText 335 336 recipients = get_recipients(request, main) 337 if not recipients: 338 raise MoinMessageNoRecipients, _("No recipients page is defined for MoinMessage.") 339 340 recipient_details = recipients.get(recipient) 341 if not recipient_details: 342 raise MoinMessageUnknownRecipient, _("The specified recipient is not present in the list of known contacts.") 343 344 parameters = parseDictEntry(recipient_details, ("type", "location", "fingerprint",)) 345 346 type = parameters.get("type") 347 location = parameters.get("location") 348 fingerprint = parameters.get("fingerprint") 349 350 if type in (None, "none") and not fetching: 351 raise MoinMessageBadRecipient, _("The recipient details are missing a destination type.") 352 353 if type not in (None, "none") and not location: 354 raise MoinMessageBadRecipient, _("The recipient details are missing a location for sent messages.") 355 356 if type in ("url", "relay", None, "none") and not fingerprint: 357 raise MoinMessageBadRecipient, _("The recipient details are missing a fingerprint for sending messages.") 358 359 return parameters 360 361 def filter_recipients(recipients, sending, fetching): 362 363 """ 364 Return a copy of the given 'recipients' dictionary retaining all entries 365 that apply to the given 'sending' and 'fetching' criteria. 366 """ 367 368 result = {} 369 for recipient, parameters in recipients.items(): 370 if not fetching and parameters.get("type") in (None, "none"): 371 continue 372 if not sending and not parameters.get("fingerprint"): 373 continue 374 result[recipient] = parameters 375 return result 376 377 # vim: tabstop=4 expandtab shiftwidth=4