1.1 --- a/actions/PostMessage.py Sat Jul 21 19:58:34 2012 +0200
1.2 +++ b/actions/PostMessage.py Sat Jul 21 21:01:16 2012 +0200
1.3 @@ -11,6 +11,8 @@
1.4 from MoinSupport import *
1.5 from email.parser import Parser
1.6 from subprocess import Popen, PIPE
1.7 +from tempfile import mkstemp
1.8 +import os
1.9
1.10 try:
1.11 from cStringIO import StringIO
1.12 @@ -56,21 +58,25 @@
1.13 # Detect PGP/GPG-encoded payloads.
1.14 # See: http://tools.ietf.org/html/rfc3156
1.15
1.16 - if mimetype == "multipart/encrypted" and \
1.17 - message.get_param("protocol") == "application/pgp-encrypted":
1.18 + # NOTE: RFC 3156 states that signed messages should employ a detached
1.19 + # NOTE: signature but then shows "BEGIN PGP MESSAGE" for signatures
1.20 + # NOTE: instead of "BEGIN PGP SIGNATURE".
1.21 +
1.22 + if mimetype == "multipart/signed" and \
1.23 + message.get_param("protocol") == "application/pgp-signature":
1.24
1.25 try:
1.26 - declaration, part = message.get_payload()
1.27 + content, signature = message.get_payload()
1.28 except ValueError:
1.29 writeHeaders(request, "text/plain", getMetadata(self.page), "415 Unsupported Media Type")
1.30 - request.write("There must be a declaration and a content part for signed uploads.")
1.31 + request.write("There must be a content part and a signature for signed uploads.")
1.32 return
1.33
1.34 # Verify the message format.
1.35
1.36 - if part.get_content_type() != "application/octet-stream":
1.37 + if signature.get_content_type() != "application/pgp-signature":
1.38 writeHeaders(request, "text/plain", getMetadata(self.page), "415 Unsupported Media Type")
1.39 - request.write("Encrypted data must be provided as application/octet-stream.")
1.40 + request.write("Signature data must be provided in the second part as application/pgp-signature.")
1.41 return
1.42
1.43 # Locate the keyring.
1.44 @@ -78,40 +84,53 @@
1.45 homedir = getattr(request.cfg, "postmessage_gpg_homedir")
1.46 if not homedir:
1.47 writeHeaders(request, "text/plain", getMetadata(self.page), "415 Unsupported Media Type")
1.48 - request.write("Encrypted data cannot currently be understood. Please notify the site administrator.")
1.49 + request.write("Encoded data cannot currently be understood. Please notify the site administrator.")
1.50 return
1.51
1.52 - # Decrypt the message text.
1.53 -
1.54 - cmd = Popen(["gpg", "--homedir", homedir, "--decrypt"],
1.55 - stdin=PIPE, stdout=PIPE, stderr=PIPE)
1.56 + # Write the detached signature and content to files.
1.57
1.58 - cmd.stdin.write(part.get_payload())
1.59 - cmd.stdin.close()
1.60 + signature_fd, signature_filename = mkstemp()
1.61 + content_fd, content_filename = mkstemp()
1.62 + try:
1.63 + signature_fp = os.fdopen(signature_fd, "w")
1.64 + content_fp = os.fdopen(content_fd, "w")
1.65 + try:
1.66 + signature_fp.write(signature.get_payload())
1.67 + content_fp.write(content.as_string())
1.68 + finally:
1.69 + signature_fp.close()
1.70 + content_fp.close()
1.71
1.72 - errors = cmd.stderr.read()
1.73 - if errors:
1.74 - getLogger(__name__).warning(errors)
1.75 + # Verify the message text.
1.76
1.77 - # Handle the embedded message.
1.78 + cmd = Popen(["gpg", "--homedir", homedir, "--verify", signature_filename, content_filename],
1.79 + stdout=PIPE, stderr=PIPE)
1.80
1.81 - try:
1.82 - message_text = cmd.stdout.read()
1.83 + errors = cmd.stderr.read()
1.84 + if errors:
1.85 + getLogger(__name__).warning(errors)
1.86
1.87 - # With a zero return code, accept the message.
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_plaintext_message(StringIO(message_text))
1.95 + if not cmd.wait():
1.96 + self.handle_parsed_message(content)
1.97 +
1.98 + # Otherwise, reject the unverified message.
1.99
1.100 - # Otherwise, reject the unverified message.
1.101 + else:
1.102 + writeHeaders(request, "text/plain", getMetadata(self.page), "403 Forbidden")
1.103 + request.write("The message could not be verified.")
1.104
1.105 - else:
1.106 - writeHeaders(request, "text/plain", getMetadata(self.page), "403 Forbidden")
1.107 - request.write("The message could not be verified.")
1.108 + finally:
1.109 + cmd.stdout.close()
1.110 + cmd.stderr.close()
1.111
1.112 finally:
1.113 - cmd.stdout.close()
1.114 - cmd.stderr.close()
1.115 + os.remove(signature_filename)
1.116 + os.remove(content_filename)
1.117
1.118 # Reject unsigned payloads.
1.119
1.120 @@ -123,8 +142,14 @@
1.121
1.122 "Handle the given 'message_text'."
1.123
1.124 + message = Parser().parse(message_text)
1.125 + self.handle_parsed_message(message)
1.126 +
1.127 + def handle_parsed_message(self, message):
1.128 +
1.129 + "Handle the given 'message_text'."
1.130 +
1.131 request = self.request
1.132 - message = Parser().parse(message_text)
1.133
1.134 # Handle a single part.
1.135
2.1 --- a/tests/test_post.py Sat Jul 21 19:58:34 2012 +0200
2.2 +++ b/tests/test_post.py Sat Jul 21 21:01:16 2012 +0200
2.3 @@ -4,22 +4,51 @@
2.4 from email.mime.application import MIMEApplication
2.5 from email.mime.base import MIMEBase
2.6 from email.encoders import encode_noop
2.7 +from email import message_from_string
2.8 import httplib
2.9 import sys
2.10
2.11 if __name__ == "__main__":
2.12 host = sys.argv[1]
2.13 path = sys.argv[2] + "?action=PostMessage"
2.14 - text = sys.stdin.read()
2.15
2.16 - message = MIMEMultipart("encrypted", protocol="application/pgp-encrypted")
2.17 + try:
2.18 + message = sys.argv[3]
2.19 + text = open(message).read()
2.20 + signature = sys.stdin.read()
2.21 + protocol = "application/pgp-signature"
2.22 + subtype = "signed"
2.23 + except IndexError:
2.24 + text = sys.stdin.read()
2.25 + signature = None
2.26 + protocol = "application/pgp-encrypted"
2.27 + subtype = "encrypted"
2.28 +
2.29 + # Make the container for the message.
2.30 +
2.31 + message = MIMEMultipart(subtype, protocol=protocol)
2.32 +
2.33 + # For encrypted content, add the declaration and content.
2.34
2.35 - declaration = MIMEBase("application", "pgp-encrypted")
2.36 - declaration.set_payload("Version: 1")
2.37 - message.attach(declaration)
2.38 + if not signature:
2.39 + declaration = MIMEBase("application", "pgp-encrypted")
2.40 + declaration.set_payload("Version: 1")
2.41 + message.attach(declaration)
2.42 +
2.43 + content = MIMEApplication(text, "octet-stream", encode_noop)
2.44 + message.attach(content)
2.45 +
2.46 + # For signed content,
2.47
2.48 - content = MIMEApplication(text, "octet-stream", encode_noop)
2.49 - message.attach(content)
2.50 + else:
2.51 + submessage = message_from_string(text)
2.52 + message.attach(submessage)
2.53 +
2.54 + signature_part = MIMEBase("application", "pgp-signature")
2.55 + signature_part.set_payload(signature)
2.56 + message.attach(signature_part)
2.57 +
2.58 + # Show the resulting message text.
2.59
2.60 text = message.as_string()
2.61