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 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 if len(alternatives) > 1: 29 part = MIMEMultipart() 30 for alternative in alternatives: 31 part.attach(alternative) 32 self.updates.append(part) 33 else: 34 self.updates.append(alternatives[0]) 35 36 def get_payload(self): 37 if len(self.updates) == 1: 38 message = self.updates[0] 39 else: 40 message = MIMEMultipart() 41 message.add_header("Update-Type", "collection") 42 for update in self.updates: 43 message.attach(update) 44 45 return message 46 47 class MoinMessageError(Exception): 48 pass 49 50 class GPG: 51 52 "A wrapper around the gpg command using a particular configuration." 53 54 def __init__(self, homedir=None): 55 self.conf_args = [] 56 57 if homedir: 58 self.conf_args += ["--homedir", homedir] 59 60 self.errors = None 61 62 def run(self, args, text=None): 63 64 """ 65 Invoke gpg with the given 'args', supplying the given 'text' to the 66 command directly or, if 'text' is omitted, using a file provided as part 67 of the 'args' if appropriate. 68 69 Failure to complete the operation will result in a MoinMessageError 70 being raised. 71 """ 72 73 cmd = Popen(["gpg"] + self.conf_args + list(args), stdin=PIPE, stdout=PIPE, stderr=PIPE) 74 75 if text: 76 cmd.stdin.write(text) 77 cmd.stdin.close() 78 79 self.errors = cmd.stderr.read() 80 81 try: 82 text = cmd.stdout.read() 83 84 # Test for a zero result. 85 86 if not cmd.wait(): 87 return text 88 else: 89 raise MoinMessageError, self.errors 90 91 finally: 92 cmd.stdout.close() 93 cmd.stderr.close() 94 95 def verifyMessage(self, signature, content): 96 97 "Using the given 'signature', verify the given message 'content'." 98 99 # Write the detached signature and content to files. 100 101 signature_fd, signature_filename = mkstemp() 102 content_fd, content_filename = mkstemp() 103 104 try: 105 signature_fp = os.fdopen(signature_fd, "w") 106 content_fp = os.fdopen(content_fd, "w") 107 try: 108 signature_fp.write(signature) 109 content_fp.write(content) 110 finally: 111 signature_fp.close() 112 content_fp.close() 113 114 # Verify the message text. 115 116 text = self.run(["--status-fd", "1", "--verify", signature_filename, content_filename]) 117 118 # Return the details of the signing key. 119 120 for line in text.split("\n"): 121 try: 122 prefix, msgtype, fingerprint, details = line.strip().split(" ", 3) 123 except ValueError: 124 continue 125 126 # Return the fingerprint and identity details. 127 128 if msgtype == "GOODSIG": 129 return fingerprint, details 130 131 return None 132 133 finally: 134 os.remove(signature_filename) 135 os.remove(content_filename) 136 137 def signMessage(self, message, keyid): 138 139 """ 140 Return a signed version of 'message' using the given 'keyid'. 141 """ 142 143 text = message.as_string() 144 signature = self.run(["--armor", "-u", keyid, "--detach-sig"], text) 145 146 # Make the container for the message. 147 148 signed_message = MIMEMultipart("signed", protocol="application/pgp-signature") 149 signed_message.attach(message) 150 151 signature_part = MIMEBase("application", "pgp-signature") 152 signature_part.set_payload(signature) 153 signed_message.attach(signature_part) 154 155 return signed_message 156 157 def decryptMessage(self, message): 158 159 "Return a decrypted version of 'message'." 160 161 return self.run(["--decrypt"], message) 162 163 def encryptMessage(self, message, keyid): 164 165 """ 166 Return an encrypted version of 'message' using the given 'keyid'. 167 """ 168 169 text = message.as_string() 170 encrypted = self.run(["--armor", "-r", keyid, "--encrypt", "--trust-model", "always"], text) 171 172 # Make the container for the message. 173 174 encrypted_message = MIMEMultipart("encrypted", protocol="application/pgp-encrypted") 175 176 # For encrypted content, add the declaration and content. 177 178 declaration = MIMEBase("application", "pgp-encrypted") 179 declaration.set_payload("Version: 1") 180 encrypted_message.attach(declaration) 181 182 content = MIMEApplication(encrypted, "octet-stream", encode_noop) 183 encrypted_message.attach(content) 184 185 return encrypted_message 186 187 # Communications functions. 188 189 def sendMessage(message, host, path): 190 191 "Send 'message' to the given 'host' using the specified URL 'path'." 192 193 text = message.as_string() 194 195 req = httplib.HTTPConnection(host) 196 req.request("PUT", path, text) # {"Content-Length" : len(text)} 197 resp = req.getresponse() 198 return resp.read() 199 200 # vim: tabstop=4 expandtab shiftwidth=4