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