# HG changeset patch # User Paul Boddie # Date 1342907579 -7200 # Node ID 29c65bed990b2ea59d08176d4d4a009c47610334 # Parent 30c3b8b61b26dc17aad629a59d606407df023f63 Restructured the action so that both signed and signed/encrypted messages can be processed. Split the posting test program into separate signing, encryption and posting programs. Added a default mask command to the initialisation script. diff -r 30c3b8b61b26 -r 29c65bed990b actions/PostMessage.py --- a/actions/PostMessage.py Sat Jul 21 21:25:09 2012 +0200 +++ b/actions/PostMessage.py Sat Jul 21 23:52:59 2012 +0200 @@ -42,96 +42,35 @@ if content_length: content_length = int(content_length) - # Get the message. + self.handle_message_text(request.read(content_length)) - self.handle_message(StringIO(request.read(content_length))) - - def handle_message(self, message_text): + def handle_message_text(self, message_text): "Handle the given 'message_text'." + message = Parser().parse(StringIO(message_text)) + self.handle_message(message) + + def handle_message(self, message): + + "Handle the given 'message'." + request = self.request - message = Parser().parse(message_text) mimetype = message.get_content_type() encoding = message.get_content_charset() # Detect PGP/GPG-encoded payloads. # See: http://tools.ietf.org/html/rfc3156 - # NOTE: RFC 3156 states that signed messages should employ a detached - # NOTE: signature but then shows "BEGIN PGP MESSAGE" for signatures - # NOTE: instead of "BEGIN PGP SIGNATURE". - # NOTE: The "micalg" parameter is currently not supported. - if mimetype == "multipart/signed" and \ message.get_param("protocol") == "application/pgp-signature": - try: - content, signature = message.get_payload() - except ValueError: - writeHeaders(request, "text/plain", getMetadata(self.page), "415 Unsupported Media Type") - request.write("There must be a content part and a signature for signed uploads.") - return - - # Verify the message format. - - if signature.get_content_type() != "application/pgp-signature": - writeHeaders(request, "text/plain", getMetadata(self.page), "415 Unsupported Media Type") - request.write("Signature data must be provided in the second part as application/pgp-signature.") - return - - # Locate the keyring. - - homedir = getattr(request.cfg, "postmessage_gpg_homedir") - if not homedir: - writeHeaders(request, "text/plain", getMetadata(self.page), "415 Unsupported Media Type") - request.write("Encoded data cannot currently be understood. Please notify the site administrator.") - return - - # Write the detached signature and content to files. + self.handle_signed_message(message) - signature_fd, signature_filename = mkstemp() - content_fd, content_filename = mkstemp() - try: - signature_fp = os.fdopen(signature_fd, "w") - content_fp = os.fdopen(content_fd, "w") - try: - signature_fp.write(signature.get_payload()) - content_fp.write(content.as_string()) - finally: - signature_fp.close() - content_fp.close() - - # Verify the message text. - - cmd = Popen(["gpg", "--homedir", homedir, "--verify", signature_filename, content_filename], - stdout=PIPE, stderr=PIPE) - - errors = cmd.stderr.read() - if errors: - getLogger(__name__).warning(errors) + elif mimetype == "multipart/encrypted" and \ + message.get_param("protocol") == "application/pgp-encrypted": - # Handle the embedded message. - - try: - # With a zero return code, accept the message. - - if not cmd.wait(): - self.handle_parsed_message(content) - - # Otherwise, reject the unverified message. - - else: - writeHeaders(request, "text/plain", getMetadata(self.page), "403 Forbidden") - request.write("The message could not be verified.") - - finally: - cmd.stdout.close() - cmd.stderr.close() - - finally: - os.remove(signature_filename) - os.remove(content_filename) + self.handle_encrypted_message(message) # Reject unsigned payloads. @@ -139,16 +78,138 @@ writeHeaders(request, "text/plain", getMetadata(self.page), "415 Unsupported Media Type") request.write("Only PGP/GPG-signed payloads are supported.") - def handle_plaintext_message(self, message_text): + def handle_encrypted_message(self, message): + + "Handle the given encrypted 'message'." + + request = self.request + + try: + declaration, content = message.get_payload() + except ValueError: + writeHeaders(request, "text/plain", getMetadata(self.page), "415 Unsupported Media Type") + request.write("There must be a declaration and a content part for encrypted uploads.") + return + + # Verify the message format. + + if content.get_content_type() != "application/octet-stream": + writeHeaders(request, "text/plain", getMetadata(self.page), "415 Unsupported Media Type") + request.write("Encrypted data must be provided as application/octet-stream.") + return + + homedir = self.get_homedir() + if not homedir: + return + + cmd = Popen(["gpg", "--homedir", homedir, "--decrypt"], + stdin=PIPE, stdout=PIPE, stderr=PIPE) + + cmd.stdin.write(content.get_payload()) + cmd.stdin.close() - "Handle the given 'message_text'." + errors = cmd.stderr.read() + if errors: + getLogger(__name__).warning(errors) + + # Handle the embedded message. + + try: + # Get the decrypted message text. + + text = cmd.stdout.read() + + # With a zero return code, accept the message. + + if not cmd.wait(): + self.handle_message_text(text) + + # Otherwise, reject the unverified message. + + else: + writeHeaders(request, "text/plain", getMetadata(self.page), "403 Forbidden") + request.write("The message could not be decrypted.") + + finally: + cmd.stdout.close() + cmd.stderr.close() + + def handle_signed_message(self, message): + + "Handle the given signed 'message'." + + request = self.request - message = Parser().parse(message_text) - self.handle_parsed_message(message) + # NOTE: RFC 3156 states that signed messages should employ a detached + # NOTE: signature but then shows "BEGIN PGP MESSAGE" for signatures + # NOTE: instead of "BEGIN PGP SIGNATURE". + # NOTE: The "micalg" parameter is currently not supported. + + try: + content, signature = message.get_payload() + except ValueError: + writeHeaders(request, "text/plain", getMetadata(self.page), "415 Unsupported Media Type") + request.write("There must be a content part and a signature for signed uploads.") + return + + # Verify the message format. + + if signature.get_content_type() != "application/pgp-signature": + writeHeaders(request, "text/plain", getMetadata(self.page), "415 Unsupported Media Type") + request.write("Signature data must be provided in the second part as application/pgp-signature.") + return + + homedir = self.get_homedir() + if not homedir: + return + + # Write the detached signature and content to files. - def handle_parsed_message(self, message): + signature_fd, signature_filename = mkstemp() + content_fd, content_filename = mkstemp() + try: + signature_fp = os.fdopen(signature_fd, "w") + content_fp = os.fdopen(content_fd, "w") + try: + signature_fp.write(signature.get_payload()) + content_fp.write(content.as_string()) + finally: + signature_fp.close() + content_fp.close() + + # Verify the message text. + + cmd = Popen(["gpg", "--homedir", homedir, "--verify", signature_filename, content_filename], + stderr=PIPE) + + errors = cmd.stderr.read() + if errors: + getLogger(__name__).warning(errors) + + # Handle the embedded message. - "Handle the given 'message_text'." + try: + # With a zero return code, accept the message. + + if not cmd.wait(): + self.handle_message_content(content) + + # Otherwise, reject the unverified message. + + else: + writeHeaders(request, "text/plain", getMetadata(self.page), "403 Forbidden") + request.write("The message could not be verified.") + + finally: + cmd.stderr.close() + + finally: + os.remove(signature_filename) + os.remove(content_filename) + + def handle_message_content(self, message): + + "Handle the given 'message'." request = self.request @@ -205,6 +266,16 @@ page_editor = PageEditor(self.request, self.pagename) page_editor.saveText("\n\n".join(body), 0) + def get_homedir(self): + + "Locate the GPG home directory." + + homedir = getattr(self.request.cfg, "postmessage_gpg_homedir") + if not homedir: + writeHeaders(request, "text/plain", getMetadata(self.page), "415 Unsupported Media Type") + request.write("Encoded data cannot currently be understood. Please notify the site administrator.") + return homedir + def is_collection(message): return message.get("Update-Type") == "collection" diff -r 30c3b8b61b26 -r 29c65bed990b scripts/init_wiki_keyring.sh --- a/scripts/init_wiki_keyring.sh Sat Jul 21 21:25:09 2012 +0200 +++ b/scripts/init_wiki_keyring.sh Sat Jul 21 23:52:59 2012 +0200 @@ -20,3 +20,4 @@ setfacl -m u:$USER:rwx $WIKI/gnupg setfacl -m m:rwx $WIKI/gnupg setfacl -m d:u:$USER:rwx $WIKI/gnupg +setfacl -m d:m:rwx $WIKI/gnupg diff -r 30c3b8b61b26 -r 29c65bed990b tests/test_encrypt.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/test_encrypt.py Sat Jul 21 23:52:59 2012 +0200 @@ -0,0 +1,34 @@ +#!/usr/bin/env python + +from email.mime.multipart import MIMEMultipart +from email.mime.application import MIMEApplication +from email.mime.base import MIMEBase +from email.encoders import encode_noop +import sys + +if __name__ == "__main__": + text = sys.stdin.read() + signature = None + protocol = "application/pgp-encrypted" + subtype = "encrypted" + + # Make the container for the message. + + message = MIMEMultipart(subtype, protocol=protocol) + + # For encrypted content, add the declaration and content. + + declaration = MIMEBase("application", "pgp-encrypted") + declaration.set_payload("Version: 1") + message.attach(declaration) + + content = MIMEApplication(text, "octet-stream", encode_noop) + message.attach(content) + + # Show the resulting message text. + + text = message.as_string() + + print text + +# vim: tabstop=4 expandtab shiftwidth=4 diff -r 30c3b8b61b26 -r 29c65bed990b tests/test_post.py --- a/tests/test_post.py Sat Jul 21 21:25:09 2012 +0200 +++ b/tests/test_post.py Sat Jul 21 23:52:59 2012 +0200 @@ -1,10 +1,5 @@ #!/usr/bin/env python -from email.mime.multipart import MIMEMultipart -from email.mime.application import MIMEApplication -from email.mime.base import MIMEBase -from email.encoders import encode_noop -from email import message_from_string import httplib import sys @@ -12,48 +7,7 @@ host = sys.argv[1] path = sys.argv[2] + "?action=PostMessage" - try: - message = sys.argv[3] - text = open(message).read() - signature = sys.stdin.read() - protocol = "application/pgp-signature" - subtype = "signed" - except IndexError: - text = sys.stdin.read() - signature = None - protocol = "application/pgp-encrypted" - subtype = "encrypted" - - # Make the container for the message. - - message = MIMEMultipart(subtype, protocol=protocol) - - # For encrypted content, add the declaration and content. - - if not signature: - declaration = MIMEBase("application", "pgp-encrypted") - declaration.set_payload("Version: 1") - message.attach(declaration) - - content = MIMEApplication(text, "octet-stream", encode_noop) - message.attach(content) - - # For signed content, - - else: - submessage = message_from_string(text) - message.attach(submessage) - - signature_part = MIMEBase("application", "pgp-signature") - signature_part.set_payload(signature) - message.attach(signature_part) - - # Show the resulting message text. - - text = message.as_string() - - print text - print + text = sys.stdin.read() req = httplib.HTTPConnection(host) req.request("PUT", path, text) # {"Content-Length" : len(text)} diff -r 30c3b8b61b26 -r 29c65bed990b tests/test_sign.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/test_sign.py Sat Jul 21 23:52:59 2012 +0200 @@ -0,0 +1,33 @@ +#!/usr/bin/env python + +from email.mime.multipart import MIMEMultipart +from email.mime.application import MIMEApplication +from email.mime.base import MIMEBase +from email import message_from_string +import sys + +if __name__ == "__main__": + message = sys.argv[1] + text = open(message).read() + signature = sys.stdin.read() + protocol = "application/pgp-signature" + subtype = "signed" + + # Make the container for the message. + + message = MIMEMultipart(subtype, protocol=protocol) + + submessage = message_from_string(text) + message.attach(submessage) + + signature_part = MIMEBase("application", "pgp-signature") + signature_part.set_payload(signature) + message.attach(signature_part) + + # Show the resulting message text. + + text = message.as_string() + + print text + +# vim: tabstop=4 expandtab shiftwidth=4