From d4c4aaac0be5a52d5fcca67596fa522d57553acf Mon Sep 17 00:00:00 2001 From: Mike Crute Date: Sun, 29 Jul 2012 13:39:23 -0400 Subject: Initial import --- email_gateway.cfg | 38 +++++++++++++++++ email_gateway.py | 84 ++++++++++++++++++++++++++++++++++++ ip_reflector.py | 11 +++++ thumbnailer.py | 125 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 258 insertions(+) create mode 100644 email_gateway.cfg create mode 100755 email_gateway.py create mode 100755 ip_reflector.py create mode 100755 thumbnailer.py diff --git a/email_gateway.cfg b/email_gateway.cfg new file mode 100644 index 0000000..31a3691 --- /dev/null +++ b/email_gateway.cfg @@ -0,0 +1,38 @@ +; ===================== +; Example Configuration +; ===================== +; [form-key] +; to = you@example.com +; subject = My awesome contact form +; from = Contact Form +; message = First line of the message +; redirect = /contact-thanks.html +; site = http://(?:www\.)?example.com + +; ====================== +; Required Configuration +; ====================== +; INI section is the form key to be passed by the form +; +; These must all be specified in some form or the app breaks +; +; to -- Email address (comma separated list) to receive the email +; subject -- Email subject +; from -- Email sent from email +; message -- Static message subject +; redirect -- URL to redirect to +; site -- Regex to match against referrer header + +; =================== +; Special Form Fields +; =================== +; mailer.form-key -- Form key from config file +; mailer.redirect -- Target for a redirect +; mailer.fields.ignore -- Ignored fields +; mailer.labels.* -- Labels for form fields + +; mailer.fields.message -- Message field +; mailer.fields.subject -- Subject field + +; mailer.subject -- Subject of message +; mailer.message -- Message diff --git a/email_gateway.py b/email_gateway.py new file mode 100755 index 0000000..e73e3e2 --- /dev/null +++ b/email_gateway.py @@ -0,0 +1,84 @@ +#!/usr/bin/env python2.6 + +import os +import smtplib +import re +import urlparse +from cStringIO import StringIO +from email.mime.text import MIMEText +from ConfigParser import SafeConfigParser as ConfigParser, NoSectionError + + +config = ConfigParser() +with open("/etc/email_gateway.cfg") as fp: + config.readfp(fp) + + +def send_message(text, subject, to, from_email): + msg = MIMEText(text.getvalue(), "plain") + + msg['Subject'] = subject + msg['From'] = from_email + msg['To'] = to + + p = os.popen("/usr/sbin/sendmail -t", "w") + p.write(msg.as_string()) + p.close() + + +def email_app(environ, start_response): + ignored_fields = [] + useful_fields = [] + form_key = None + message_buffer = StringIO() + + context = {} + + fields = urlparse.parse_qsl(environ["wsgi.input"].read()) + for key, value in fields: + if key == "mailer.form-key": + form_key = value + elif key == "mailer.redirect": + context["redirect"] = value + elif key == "mailer.subject": + context["subject"] = value + elif key == "mailer.message": + context["message"] = value + elif key == "mailer.fields.ignore": + ignored_fields = value.split(",") + else: + useful_fields.append((key, value)) + + try: + my_config = dict(config.items(form_key)) + site_matcher = re.compile(my_config["site"]) + except NoSectionError: + start_response('403 Forbidden', [('Content-Type', 'text/plain')]) + return "Invalid form key!" + + if not site_matcher.match(environ["HTTP_REFERER"]): + start_response('403 Forbidden', [('Content-Type', 'text/plain')]) + return "Invalid send!" + + useful_fields = ["{0}: {1}".format(*f) + for f in useful_fields + if f[0] not in ignored_fields] + + message_buffer.write(context.get("message", my_config["message"])) + message_buffer.write("\n\n") + message_buffer.write("\n".join(useful_fields)) + + send_message(message_buffer, + context.get("subject", my_config["subject"]), + my_config["to"], + my_config["from"]) + + redirect_location = context.get("redirect", my_config["redirect"]) + start_response('302 Found', [('Location', redirect_location)]) + + return "" + + +if __name__ == "__main__": + from flup.server.fcgi_fork import WSGIServer + WSGIServer(email_app, maxSpare=1).run() diff --git a/ip_reflector.py b/ip_reflector.py new file mode 100755 index 0000000..8ccd9f4 --- /dev/null +++ b/ip_reflector.py @@ -0,0 +1,11 @@ +#!/usr/bin/env python2.6 + + +def reflector_app(environ, start_response): + start_response('200 OK', [('Content-Type', 'text/plain')]) + return environ['REMOTE_ADDR'] + + +if __name__ == "__main__": + from flup.server.fcgi_fork import WSGIServer + WSGIServer(reflector_app, maxSpare=1).run() diff --git a/thumbnailer.py b/thumbnailer.py new file mode 100755 index 0000000..cebbf72 --- /dev/null +++ b/thumbnailer.py @@ -0,0 +1,125 @@ +#!/usr/bin/env python2.6 + +import hmac +import urllib2 +import urlparse +from cStringIO import StringIO + +from PIL import Image, ImageOps +from PIL.ImageFileIO import ImageFileIO + + +def crop_image(img, width, height): + src_width, src_height = img.size + src_ratio = float(src_width) / float(src_height) + dst_width, dst_height = int(width), int(height) + dst_ratio = float(dst_width) / float(dst_height) + + if dst_ratio < src_ratio: + crop_height = src_height + crop_width = crop_height * dst_ratio + x_offset = float(src_width - crop_width) / 2 + y_offset = 0 + else: + crop_width = src_width + crop_height = crop_width / dst_ratio + x_offset = 0 + y_offset = float(src_height - crop_height) / 3 + + img = img.crop((x_offset, y_offset, x_offset+int(crop_width), y_offset+int(crop_height))) + + return img + + +def calculate_size(img, long_side): + width, height = img.size + width, height = float(width), float(height) + + if height > width: + width = (width / height) * long_side + height = long_side + else: + height = (height / width) * long_side + width = long_side + + return width, height + + +def load_remote_image(url): + return Image.open(ImageFileIO(urllib2.urlopen(url))) + + +def create_thumbnail(img, long_side): + width, height = calculate_size(img, int(long_side)) + img.thumbnail((width, height), Image.ANTIALIAS) + return img + + +def desaturate_image(img): + img = ImageOps.grayscale(img) + return img + + +KEY = "" + +OPERATIONS = { + "thumb": (create_thumbnail, 1), + "crop": (crop_image, 2), + "desaturate": (desaturate_image, 0), +} + +def generate_sig(query): + clean = ["{0}{1}".format(k,v) for k, v in query if k != "sig"] + return hmac.new(KEY, "".join(clean)).hexdigest() + + +def thumbnailer_app(environ, start_response): + to_do = [] + qs = [] + for tup in urlparse.parse_qsl(environ.get('QUERY_STRING', '')): + if tup[0] == "sig": + sig = tup[1] + else: + qs.append(tup) + + # Validate + if generate_sig(qs) != sig: + start_response('401 Forbidden', [('Content-Type', 'text/plain')]) + return ["You may not use the thumbnailer for that site."] + + need_values = 0 + for key, value in qs: + if need_values > 0: + to_do[-1].append(value) + need_values -= 1 + continue + + if key == "image": + path = value + elif key == "op": + op, args = OPERATIONS[value] + need_values = args + to_do.append([op]) + + # Start the web response + start_response('200 OK', [('Content-Type', 'image/jpeg')]) + + img = load_remote_image(path) + + for action in to_do: + func = action.pop(0) + img = func(img, *action) + + return img.tostring('jpeg', img.mode) + + +if __name__ == "__main__": + import sys + + if len(sys.argv) == 2: + qs = urlparse.parse_qsl(urlparse.urlparse(sys.argv[-1]).query) + print "{0}&sig={1}".format(sys.argv[-1], generate_sig(qs)) + sys.exit(0) + + from flup.server.fcgi_fork import WSGIServer + WSGIServer(thumbnailer_app, debug=True, maxSpare=1).run() -- cgit v1.2.3