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 ItemStore, getHeader, getMetadata, getWikiDict, \ 14 writeHeaders 15 from MoinMessage import GPG, Message, MoinMessageError, \ 16 MoinMessageMissingPart, MoinMessageBadContent, \ 17 is_signed, is_encrypted, getContentAndSignature 18 from email.parser import Parser 19 import time 20 21 try: 22 from cStringIO import StringIO 23 except ImportError: 24 from StringIO import StringIO 25 26 Dependencies = ['pages'] 27 28 class MoinMessageAction: 29 30 "Common message handling support for actions." 31 32 def __init__(self, pagename, request): 33 34 """ 35 On the page with the given 'pagename', use the given 'request' when 36 reading posted messages, modifying the Wiki. 37 """ 38 39 self.pagename = pagename 40 self.request = request 41 self.page = Page(request, pagename) 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 message = Parser().parse(StringIO(message_text)) 57 self.handle_message(message) 58 59 def handle_message(self, message): 60 61 "Handle the given 'message'." 62 63 # Detect PGP/GPG-encoded payloads. 64 # See: http://tools.ietf.org/html/rfc3156 65 66 # Signed payloads are checked and then passed on for further processing 67 # elsewhere. Verification is the last step in this base implementation, 68 # even if an encrypted-then-signed payload is involved. 69 70 if is_signed(message): 71 self.handle_signed_message(message) 72 73 # Encrypted payloads are decrypted and then sent back into this method 74 # for signature checking as described above. Thus, signed-then-encrypted 75 # payloads are first decrypted and then verified. 76 77 elif is_encrypted(message): 78 self.handle_encrypted_message(message) 79 80 # Reject unsigned and unencrypted payloads. 81 82 else: 83 request = self.request 84 writeHeaders(request, "text/plain", getMetadata(self.page), "415 Unsupported Media Type") 85 request.write("Only PGP/GPG-signed payloads are supported.") 86 87 def handle_encrypted_message(self, message): 88 89 "Handle the given encrypted 'message'." 90 91 request = self.request 92 93 homedir = self.get_homedir() 94 if not homedir: 95 return 96 97 gpg = GPG(homedir) 98 99 try: 100 text = gpg.decryptMessage(message) 101 102 # Reject messages without a declaration. 103 104 except MoinMessageMissingPart: 105 writeHeaders(request, "text/plain", getMetadata(self.page), "415 Unsupported Media Type") 106 request.write("There must be a declaration and a content part for encrypted uploads.") 107 return 108 109 # Reject messages without appropriate content. 110 111 except MoinMessageBadContent: 112 writeHeaders(request, "text/plain", getMetadata(self.page), "415 Unsupported Media Type") 113 request.write("Encrypted data must be provided as application/octet-stream.") 114 return 115 116 # Reject any unencryptable message. 117 118 except MoinMessageError: 119 writeHeaders(request, "text/plain", getMetadata(self.page), "403 Forbidden") 120 request.write("The message could not be decrypted.") 121 return 122 123 # Log non-fatal errors. 124 125 if gpg.errors: 126 getLogger(__name__).warning(gpg.errors) 127 128 # Handle the embedded message which may itself be a signed message. 129 130 self.handle_message_text(text) 131 132 def handle_signed_message(self, message): 133 134 "Handle the given signed 'message'." 135 136 request = self.request 137 138 # Accept any message whose sender was authenticated by the PGP method. 139 140 if request.user and request.user.valid and request.user.auth_method == "pgp": 141 142 # Handle the embedded message. 143 144 content, signature = getContentAndSignature(message) 145 self.handle_message_content(content) 146 147 # Reject any unverified message. 148 149 else: 150 writeHeaders(request, "text/plain", getMetadata(self.page), "403 Forbidden") 151 request.write("The message could not be verified. " 152 "Maybe this site is not performing authentication using PGP signatures.") 153 154 def handle_message_content(self, content): 155 156 "Handle the given message 'content'." 157 158 request = self.request 159 160 # Interpret the content as one or more updates. 161 162 message = Message() 163 message.handle_message(content) 164 165 # Test any date against the page or message store. 166 167 if message.date: 168 store_date = time.gmtime(self.store.mtime()) 169 page_date = time.gmtime(wikiutil.version2timestamp(self.page.mtime_usecs())) 170 last_date = max(store_date, page_date) 171 172 # Reject messages older than the page date. 173 174 if message.date < last_date: 175 writeHeaders(request, "text/plain", getMetadata(self.page), "403 Forbidden") 176 request.write("The message is too old: %s versus %s." % (message.date, last_date)) 177 return 178 179 # Reject messages without dates if so configured. 180 181 elif getattr(request.cfg, "moinmessage_reject_messages_without_dates", True): 182 writeHeaders(request, "text/plain", getMetadata(self.page), "403 Forbidden") 183 request.write("The message has no date information.") 184 return 185 186 # Handle the message as an object. 187 188 self.handle_message_object(message) 189 190 def get_homedir(self): 191 192 "Locate the GPG home directory." 193 194 request = self.request 195 homedir = get_homedir(self.request) 196 197 if not homedir: 198 writeHeaders(request, "text/plain", getMetadata(self.page), "415 Unsupported Media Type") 199 request.write("Encoded data cannot currently be understood. Please notify the site administrator.") 200 201 return homedir 202 203 def get_homedir(request): 204 205 "Locate the GPG home directory." 206 207 return getattr(request.cfg, "moinmessage_gpg_homedir") 208 209 def get_signing_users(request): 210 211 "Return a dictionary mapping usernames to signing keys." 212 213 return getWikiDict( 214 getattr(request.cfg, "moinmessage_gpg_signing_users_page", "MoinMessageSigningUserDict"), 215 request) 216 217 def get_recipients(request): 218 219 """ 220 Return the recipients dictionary by first obtaining the page in which it 221 is stored. This page may either be a subpage of the user's home page, if 222 stored on this wiki, or it may be relative to the site root. 223 224 The name of the subpage is defined by the configuration setting 225 'moinmessage_gpg_recipients_page', which if absent is set to 226 "MoinMessageRecipientsDict". 227 """ 228 229 subpage = getattr(request.cfg, "moinmessage_gpg_recipients_page", "MoinMessageRecipientsDict") 230 homedetails = wikiutil.getInterwikiHomePage(request) 231 232 if homedetails: 233 homewiki, homepage = homedetails 234 if homewiki == "Self": 235 recipients = getWikiDict("%s/%s" % (homepage, subpage), request) 236 if recipients: 237 return recipients 238 239 return getWikiDict(subpage, request) 240 241 # vim: tabstop=4 expandtab shiftwidth=4