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