1 # -*- coding: iso-8859-1 -*- 2 """ 3 MoinMoin - MoinMessage library 4 5 @copyright: 2012 by Paul Boddie <paul@boddie.org.uk> 6 @license: GNU GPL (v2 or later), see COPYING.txt for details. 7 """ 8 9 from email import message_from_string 10 from email.encoders import encode_noop 11 from email.mime.multipart import MIMEMultipart 12 from email.mime.application import MIMEApplication 13 from email.mime.base import MIMEBase 14 from email.mime.text import MIMEText 15 from subprocess import Popen, PIPE 16 from tempfile import mkstemp 17 from urlparse import urlsplit 18 import httplib 19 import os 20 21 class Message: 22 23 "An update message." 24 25 def __init__(self): 26 self.updates = [] 27 28 def add_update(self, alternatives): 29 30 """ 31 Add an update fragment to a message, providing alternative forms of the 32 update content in the given 'alternatives': a list of MIME message 33 parts, each encoding the content according to different MIME types. 34 """ 35 36 if len(alternatives) > 1: 37 part = MIMEMultipart() 38 for alternative in alternatives: 39 part.attach(alternative) 40 self.updates.append(part) 41 else: 42 self.updates.append(alternatives[0]) 43 44 def get_payload(self): 45 46 "Get the multipart payload for the message." 47 48 if len(self.updates) == 1: 49 message = self.updates[0] 50 else: 51 message = MIMEMultipart() 52 message.add_header("Update-Type", "collection") 53 for update in self.updates: 54 message.attach(update) 55 56 return message 57 58 class MoinMessageError(Exception): 59 pass 60 61 class GPG: 62 63 "A wrapper around the gpg command using a particular configuration." 64 65 def __init__(self, homedir=None): 66 self.conf_args = [] 67 68 if homedir: 69 self.conf_args += ["--homedir", homedir] 70 71 self.errors = None 72 73 def run(self, args, text=None): 74 75 """ 76 Invoke gpg with the given 'args', supplying the given 'text' to the 77 command directly or, if 'text' is omitted, using a file provided as part 78 of the 'args' if appropriate. 79 80 Failure to complete the operation will result in a MoinMessageError 81 being raised. 82 """ 83 84 cmd = Popen(["gpg"] + self.conf_args + list(args), stdin=PIPE, stdout=PIPE, stderr=PIPE) 85 86 try: 87 # Attempt to write input to the command and to read output from the 88 # command. 89 90 try: 91 if text: 92 cmd.stdin.write(text) 93 cmd.stdin.close() 94 95 text = cmd.stdout.read() 96 97 # I/O errors can indicate the failure of the command. 98 99 except IOError: 100 pass 101 102 self.errors = cmd.stderr.read() 103 104 # Test for a zero result. 105 106 if not cmd.wait(): 107 return text 108 else: 109 raise MoinMessageError, self.errors 110 111 finally: 112 cmd.stdout.close() 113 cmd.stderr.close() 114 115 def verifyMessage(self, signature, content): 116 117 "Using the given 'signature', verify the given message 'content'." 118 119 # Write the detached signature and content to files. 120 121 signature_fd, signature_filename = mkstemp() 122 content_fd, content_filename = mkstemp() 123 124 try: 125 signature_fp = os.fdopen(signature_fd, "w") 126 content_fp = os.fdopen(content_fd, "w") 127 try: 128 signature_fp.write(signature) 129 content_fp.write(content) 130 finally: 131 signature_fp.close() 132 content_fp.close() 133 134 # Verify the message text. 135 136 text = self.run(["--status-fd", "1", "--verify", signature_filename, content_filename]) 137 138 # Return the details of the signing key. 139 140 identity = None 141 fingerprint = None 142 143 for line in text.split("\n"): 144 try: 145 prefix, msgtype, digest, details = line.strip().split(" ", 3) 146 except ValueError: 147 continue 148 149 # Return the fingerprint and identity details. 150 151 if msgtype == "GOODSIG": 152 identity = details 153 elif msgtype == "VALIDSIG": 154 fingerprint = digest 155 156 if identity and fingerprint: 157 return fingerprint, identity 158 159 return None 160 161 finally: 162 os.remove(signature_filename) 163 os.remove(content_filename) 164 165 def signMessage(self, message, keyid): 166 167 """ 168 Return a signed version of 'message' using the given 'keyid'. 169 """ 170 171 text = message.as_string() 172 signature = self.run(["--armor", "-u", keyid, "--detach-sig"], text) 173 174 # Make the container for the message. 175 176 signed_message = MIMEMultipart("signed", protocol="application/pgp-signature") 177 signed_message.attach(message) 178 179 signature_part = MIMEBase("application", "pgp-signature") 180 signature_part.set_payload(signature) 181 signed_message.attach(signature_part) 182 183 return signed_message 184 185 def decryptMessage(self, message): 186 187 "Return a decrypted version of 'message'." 188 189 return self.run(["--decrypt"], message) 190 191 def encryptMessage(self, message, keyid): 192 193 """ 194 Return an encrypted version of 'message' using the given 'keyid'. 195 """ 196 197 text = message.as_string() 198 encrypted = self.run(["--armor", "-r", keyid, "--encrypt", "--trust-model", "always"], text) 199 200 # Make the container for the message. 201 202 encrypted_message = MIMEMultipart("encrypted", protocol="application/pgp-encrypted") 203 204 # For encrypted content, add the declaration and content. 205 206 declaration = MIMEBase("application", "pgp-encrypted") 207 declaration.set_payload("Version: 1") 208 encrypted_message.attach(declaration) 209 210 content = MIMEApplication(encrypted, "octet-stream", encode_noop) 211 encrypted_message.attach(content) 212 213 return encrypted_message 214 215 # Communications functions. 216 217 def sendMessage(message, url): 218 219 "Send 'message' to the given 'url." 220 221 scheme, host, port, path = parseURL(url) 222 text = message.as_string() 223 224 if scheme == "http": 225 cls = httplib.HTTPConnection 226 elif scheme == "https": 227 cls = httplib.HTTPSConnection 228 else: 229 raise MoinMessageError, "Communications protocol not supported: %s" % scheme 230 231 req = cls(host, port) 232 req.request("PUT", path, text) 233 resp = req.getresponse() 234 return resp.read() 235 236 def parseURL(url): 237 238 "Return the scheme, host, port and path for the given 'url'." 239 240 scheme, host_port, path, query, fragment = urlsplit(url) 241 host_port = host_port.split(":") 242 243 if query: 244 path += "?" + query 245 246 if len(host_port) > 1: 247 host = host_port[0] 248 port = int(host_port[1]) 249 else: 250 host = host_port[0] 251 port = 80 252 253 return scheme, host, port, path 254 255 # vim: tabstop=4 expandtab shiftwidth=4