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, 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 self.run(["--verify", signature_filename, content_filename]) 117 118 finally: 119 os.remove(signature_filename) 120 os.remove(content_filename) 121 122 def signMessage(self, message, keyid): 123 124 """ 125 Return a signed version of 'message' using the given 'keyid'. 126 """ 127 128 text = message.as_string() 129 signature = self.run(["--armor", "-u", keyid, "--detach-sig"], text) 130 131 # Make the container for the message. 132 133 signed_message = MIMEMultipart("signed", protocol="application/pgp-signature") 134 signed_message.attach(message) 135 136 signature_part = MIMEBase("application", "pgp-signature") 137 signature_part.set_payload(signature) 138 signed_message.attach(signature_part) 139 140 return signed_message 141 142 def decryptMessage(self, message): 143 144 "Return a decrypted version of 'message'." 145 146 return self.run(["--decrypt"], message) 147 148 def encryptMessage(self, message, keyid): 149 150 """ 151 Return an encrypted version of 'message' using the given 'keyid'. 152 """ 153 154 text = message.as_string() 155 encrypted = self.run(["--armor", "-r", keyid, "--encrypt", "--trust-model", "always"], text) 156 157 # Make the container for the message. 158 159 encrypted_message = MIMEMultipart("encrypted", protocol="application/pgp-encrypted") 160 161 # For encrypted content, add the declaration and content. 162 163 declaration = MIMEBase("application", "pgp-encrypted") 164 declaration.set_payload("Version: 1") 165 encrypted_message.attach(declaration) 166 167 content = MIMEApplication(encrypted, "octet-stream", encode_noop) 168 encrypted_message.attach(content) 169 170 return encrypted_message 171 172 # Communications functions. 173 174 def sendMessage(message, host, path): 175 176 "Send 'message' to the given 'host' using the specified URL 'path'." 177 178 text = message.as_string() 179 180 req = httplib.HTTPConnection(host) 181 req.request("PUT", path, text) # {"Content-Length" : len(text)} 182 resp = req.getresponse() 183 return resp.read() 184 185 # vim: tabstop=4 expandtab shiftwidth=4