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