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