1.1 --- a/actions/PostMessage.py Sat Jul 21 21:25:09 2012 +0200
1.2 +++ b/actions/PostMessage.py Sat Jul 21 23:52:59 2012 +0200
1.3 @@ -42,96 +42,35 @@
1.4 if content_length:
1.5 content_length = int(content_length)
1.6
1.7 - # Get the message.
1.8 + self.handle_message_text(request.read(content_length))
1.9
1.10 - self.handle_message(StringIO(request.read(content_length)))
1.11 -
1.12 - def handle_message(self, message_text):
1.13 + def handle_message_text(self, message_text):
1.14
1.15 "Handle the given 'message_text'."
1.16
1.17 + message = Parser().parse(StringIO(message_text))
1.18 + self.handle_message(message)
1.19 +
1.20 + def handle_message(self, message):
1.21 +
1.22 + "Handle the given 'message'."
1.23 +
1.24 request = self.request
1.25 - message = Parser().parse(message_text)
1.26 mimetype = message.get_content_type()
1.27 encoding = message.get_content_charset()
1.28
1.29 # Detect PGP/GPG-encoded payloads.
1.30 # See: http://tools.ietf.org/html/rfc3156
1.31
1.32 - # NOTE: RFC 3156 states that signed messages should employ a detached
1.33 - # NOTE: signature but then shows "BEGIN PGP MESSAGE" for signatures
1.34 - # NOTE: instead of "BEGIN PGP SIGNATURE".
1.35 - # NOTE: The "micalg" parameter is currently not supported.
1.36 -
1.37 if mimetype == "multipart/signed" and \
1.38 message.get_param("protocol") == "application/pgp-signature":
1.39
1.40 - try:
1.41 - content, signature = message.get_payload()
1.42 - except ValueError:
1.43 - writeHeaders(request, "text/plain", getMetadata(self.page), "415 Unsupported Media Type")
1.44 - request.write("There must be a content part and a signature for signed uploads.")
1.45 - return
1.46 -
1.47 - # Verify the message format.
1.48 -
1.49 - if signature.get_content_type() != "application/pgp-signature":
1.50 - writeHeaders(request, "text/plain", getMetadata(self.page), "415 Unsupported Media Type")
1.51 - request.write("Signature data must be provided in the second part as application/pgp-signature.")
1.52 - return
1.53 -
1.54 - # Locate the keyring.
1.55 -
1.56 - homedir = getattr(request.cfg, "postmessage_gpg_homedir")
1.57 - if not homedir:
1.58 - writeHeaders(request, "text/plain", getMetadata(self.page), "415 Unsupported Media Type")
1.59 - request.write("Encoded data cannot currently be understood. Please notify the site administrator.")
1.60 - return
1.61 -
1.62 - # Write the detached signature and content to files.
1.63 + self.handle_signed_message(message)
1.64
1.65 - signature_fd, signature_filename = mkstemp()
1.66 - content_fd, content_filename = mkstemp()
1.67 - try:
1.68 - signature_fp = os.fdopen(signature_fd, "w")
1.69 - content_fp = os.fdopen(content_fd, "w")
1.70 - try:
1.71 - signature_fp.write(signature.get_payload())
1.72 - content_fp.write(content.as_string())
1.73 - finally:
1.74 - signature_fp.close()
1.75 - content_fp.close()
1.76 -
1.77 - # Verify the message text.
1.78 -
1.79 - cmd = Popen(["gpg", "--homedir", homedir, "--verify", signature_filename, content_filename],
1.80 - stdout=PIPE, stderr=PIPE)
1.81 -
1.82 - errors = cmd.stderr.read()
1.83 - if errors:
1.84 - getLogger(__name__).warning(errors)
1.85 + elif mimetype == "multipart/encrypted" and \
1.86 + message.get_param("protocol") == "application/pgp-encrypted":
1.87
1.88 - # Handle the embedded message.
1.89 -
1.90 - try:
1.91 - # With a zero return code, accept the message.
1.92 -
1.93 - if not cmd.wait():
1.94 - self.handle_parsed_message(content)
1.95 -
1.96 - # Otherwise, reject the unverified message.
1.97 -
1.98 - else:
1.99 - writeHeaders(request, "text/plain", getMetadata(self.page), "403 Forbidden")
1.100 - request.write("The message could not be verified.")
1.101 -
1.102 - finally:
1.103 - cmd.stdout.close()
1.104 - cmd.stderr.close()
1.105 -
1.106 - finally:
1.107 - os.remove(signature_filename)
1.108 - os.remove(content_filename)
1.109 + self.handle_encrypted_message(message)
1.110
1.111 # Reject unsigned payloads.
1.112
1.113 @@ -139,16 +78,138 @@
1.114 writeHeaders(request, "text/plain", getMetadata(self.page), "415 Unsupported Media Type")
1.115 request.write("Only PGP/GPG-signed payloads are supported.")
1.116
1.117 - def handle_plaintext_message(self, message_text):
1.118 + def handle_encrypted_message(self, message):
1.119 +
1.120 + "Handle the given encrypted 'message'."
1.121 +
1.122 + request = self.request
1.123 +
1.124 + try:
1.125 + declaration, content = message.get_payload()
1.126 + except ValueError:
1.127 + writeHeaders(request, "text/plain", getMetadata(self.page), "415 Unsupported Media Type")
1.128 + request.write("There must be a declaration and a content part for encrypted uploads.")
1.129 + return
1.130 +
1.131 + # Verify the message format.
1.132 +
1.133 + if content.get_content_type() != "application/octet-stream":
1.134 + writeHeaders(request, "text/plain", getMetadata(self.page), "415 Unsupported Media Type")
1.135 + request.write("Encrypted data must be provided as application/octet-stream.")
1.136 + return
1.137 +
1.138 + homedir = self.get_homedir()
1.139 + if not homedir:
1.140 + return
1.141 +
1.142 + cmd = Popen(["gpg", "--homedir", homedir, "--decrypt"],
1.143 + stdin=PIPE, stdout=PIPE, stderr=PIPE)
1.144 +
1.145 + cmd.stdin.write(content.get_payload())
1.146 + cmd.stdin.close()
1.147
1.148 - "Handle the given 'message_text'."
1.149 + errors = cmd.stderr.read()
1.150 + if errors:
1.151 + getLogger(__name__).warning(errors)
1.152 +
1.153 + # Handle the embedded message.
1.154 +
1.155 + try:
1.156 + # Get the decrypted message text.
1.157 +
1.158 + text = cmd.stdout.read()
1.159 +
1.160 + # With a zero return code, accept the message.
1.161 +
1.162 + if not cmd.wait():
1.163 + self.handle_message_text(text)
1.164 +
1.165 + # Otherwise, reject the unverified message.
1.166 +
1.167 + else:
1.168 + writeHeaders(request, "text/plain", getMetadata(self.page), "403 Forbidden")
1.169 + request.write("The message could not be decrypted.")
1.170 +
1.171 + finally:
1.172 + cmd.stdout.close()
1.173 + cmd.stderr.close()
1.174 +
1.175 + def handle_signed_message(self, message):
1.176 +
1.177 + "Handle the given signed 'message'."
1.178 +
1.179 + request = self.request
1.180
1.181 - message = Parser().parse(message_text)
1.182 - self.handle_parsed_message(message)
1.183 + # NOTE: RFC 3156 states that signed messages should employ a detached
1.184 + # NOTE: signature but then shows "BEGIN PGP MESSAGE" for signatures
1.185 + # NOTE: instead of "BEGIN PGP SIGNATURE".
1.186 + # NOTE: The "micalg" parameter is currently not supported.
1.187 +
1.188 + try:
1.189 + content, signature = message.get_payload()
1.190 + except ValueError:
1.191 + writeHeaders(request, "text/plain", getMetadata(self.page), "415 Unsupported Media Type")
1.192 + request.write("There must be a content part and a signature for signed uploads.")
1.193 + return
1.194 +
1.195 + # Verify the message format.
1.196 +
1.197 + if signature.get_content_type() != "application/pgp-signature":
1.198 + writeHeaders(request, "text/plain", getMetadata(self.page), "415 Unsupported Media Type")
1.199 + request.write("Signature data must be provided in the second part as application/pgp-signature.")
1.200 + return
1.201 +
1.202 + homedir = self.get_homedir()
1.203 + if not homedir:
1.204 + return
1.205 +
1.206 + # Write the detached signature and content to files.
1.207
1.208 - def handle_parsed_message(self, message):
1.209 + signature_fd, signature_filename = mkstemp()
1.210 + content_fd, content_filename = mkstemp()
1.211 + try:
1.212 + signature_fp = os.fdopen(signature_fd, "w")
1.213 + content_fp = os.fdopen(content_fd, "w")
1.214 + try:
1.215 + signature_fp.write(signature.get_payload())
1.216 + content_fp.write(content.as_string())
1.217 + finally:
1.218 + signature_fp.close()
1.219 + content_fp.close()
1.220 +
1.221 + # Verify the message text.
1.222 +
1.223 + cmd = Popen(["gpg", "--homedir", homedir, "--verify", signature_filename, content_filename],
1.224 + stderr=PIPE)
1.225 +
1.226 + errors = cmd.stderr.read()
1.227 + if errors:
1.228 + getLogger(__name__).warning(errors)
1.229 +
1.230 + # Handle the embedded message.
1.231
1.232 - "Handle the given 'message_text'."
1.233 + try:
1.234 + # With a zero return code, accept the message.
1.235 +
1.236 + if not cmd.wait():
1.237 + self.handle_message_content(content)
1.238 +
1.239 + # Otherwise, reject the unverified message.
1.240 +
1.241 + else:
1.242 + writeHeaders(request, "text/plain", getMetadata(self.page), "403 Forbidden")
1.243 + request.write("The message could not be verified.")
1.244 +
1.245 + finally:
1.246 + cmd.stderr.close()
1.247 +
1.248 + finally:
1.249 + os.remove(signature_filename)
1.250 + os.remove(content_filename)
1.251 +
1.252 + def handle_message_content(self, message):
1.253 +
1.254 + "Handle the given 'message'."
1.255
1.256 request = self.request
1.257
1.258 @@ -205,6 +266,16 @@
1.259 page_editor = PageEditor(self.request, self.pagename)
1.260 page_editor.saveText("\n\n".join(body), 0)
1.261
1.262 + def get_homedir(self):
1.263 +
1.264 + "Locate the GPG home directory."
1.265 +
1.266 + homedir = getattr(self.request.cfg, "postmessage_gpg_homedir")
1.267 + if not homedir:
1.268 + writeHeaders(request, "text/plain", getMetadata(self.page), "415 Unsupported Media Type")
1.269 + request.write("Encoded data cannot currently be understood. Please notify the site administrator.")
1.270 + return homedir
1.271 +
1.272 def is_collection(message):
1.273 return message.get("Update-Type") == "collection"
1.274
4.1 --- a/tests/test_post.py Sat Jul 21 21:25:09 2012 +0200
4.2 +++ b/tests/test_post.py Sat Jul 21 23:52:59 2012 +0200
4.3 @@ -1,10 +1,5 @@
4.4 #!/usr/bin/env python
4.5
4.6 -from email.mime.multipart import MIMEMultipart
4.7 -from email.mime.application import MIMEApplication
4.8 -from email.mime.base import MIMEBase
4.9 -from email.encoders import encode_noop
4.10 -from email import message_from_string
4.11 import httplib
4.12 import sys
4.13
4.14 @@ -12,48 +7,7 @@
4.15 host = sys.argv[1]
4.16 path = sys.argv[2] + "?action=PostMessage"
4.17
4.18 - try:
4.19 - message = sys.argv[3]
4.20 - text = open(message).read()
4.21 - signature = sys.stdin.read()
4.22 - protocol = "application/pgp-signature"
4.23 - subtype = "signed"
4.24 - except IndexError:
4.25 - text = sys.stdin.read()
4.26 - signature = None
4.27 - protocol = "application/pgp-encrypted"
4.28 - subtype = "encrypted"
4.29 -
4.30 - # Make the container for the message.
4.31 -
4.32 - message = MIMEMultipart(subtype, protocol=protocol)
4.33 -
4.34 - # For encrypted content, add the declaration and content.
4.35 -
4.36 - if not signature:
4.37 - declaration = MIMEBase("application", "pgp-encrypted")
4.38 - declaration.set_payload("Version: 1")
4.39 - message.attach(declaration)
4.40 -
4.41 - content = MIMEApplication(text, "octet-stream", encode_noop)
4.42 - message.attach(content)
4.43 -
4.44 - # For signed content,
4.45 -
4.46 - else:
4.47 - submessage = message_from_string(text)
4.48 - message.attach(submessage)
4.49 -
4.50 - signature_part = MIMEBase("application", "pgp-signature")
4.51 - signature_part.set_payload(signature)
4.52 - message.attach(signature_part)
4.53 -
4.54 - # Show the resulting message text.
4.55 -
4.56 - text = message.as_string()
4.57 -
4.58 - print text
4.59 - print
4.60 + text = sys.stdin.read()
4.61
4.62 req = httplib.HTTPConnection(host)
4.63 req.request("PUT", path, text) # {"Content-Length" : len(text)}