#!/usr/bin/python
# The contents of this file are subject to the Common Public Attribution
# License Version 1.0. (the "License"); you may not use this file except in
# compliance with the License. You may obtain a copy of the License at
# http://code.reddit.com/LICENSE. The License is based on the Mozilla Public
# License Version 1.1, but Sections 14 and 15 have been added to cover use of
# software over a computer network and provide for limited attribution for the
# Original Developer. In addition, Exhibit A has been modified to be consistent
# with Exhibit B.
#
# Software distributed under the License is distributed on an "AS IS" basis,
# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for
# the specific language governing rights and limitations under the License.
#
# The Original Code is reddit.
#
# The Original Developer is the Initial Developer.  The Initial Developer of
# the Original Code is reddit Inc.
#
# All portions of the code written by reddit are Copyright (c) 2006-2015 reddit
# Inc. All Rights Reserved.
###############################################################################
"""
This is a tiny Flask app used for a couple of self-serve ad tracking
mechanisms. The URLs it provides are:

/click

    Promoted links have their URL replaced with a /click URL by the JS
    (after a call to /fetch-trackers). Redirect to the actual URL after logging
    the click. This must be run in a place whose logs are stored for traffic
    analysis.

For convenience, the script can compile itself into a Zip archive suitable for
use on Amazon Elastic Beanstalk (and possibly other systems).

"""


import cStringIO
import hashlib
import hmac
import time
import urllib
from urlparse import parse_qsl, urlparse, urlunparse

from ConfigParser import RawConfigParser
from wsgiref.handlers import format_date_time

from flask import Flask, request, json, make_response, abort, redirect


application = Flask(__name__)
REQUIRED_PACKAGES = [
    "flask",
]


class ApplicationConfig(object):
    """A thin wrapper around ConfigParser that remembers what we read.

    The remembered settings can then be written out to a minimal config file
    when building the Elastic Beanstalk zipfile.

    """
    def __init__(self):
        self.input = RawConfigParser()
        with open("production.ini") as f:
            self.input.readfp(f)
        self.output = RawConfigParser()

    def get(self, section, key):
        value = self.input.get(section, key)

        # remember that we needed this configuration value
        if (section.upper() != "DEFAULT" and
            not self.output.has_section(section)):
            self.output.add_section(section)
        self.output.set(section, key, value)

        return value

    def to_config(self):
        io = cStringIO.StringIO()
        self.output.write(io)
        return io.getvalue()


config = ApplicationConfig()
tracking_secret = config.get('DEFAULT', 'tracking_secret')


@application.route("/")
def healthcheck():
    return "I am healthy."


@application.route('/click')
def click_redirect():
    destination = request.args['url'].encode('utf-8')
    fullname = request.args['id'].encode('utf-8')
    observed_mac = request.args['hash']

    expected_hashable = ''.join((destination, fullname))
    expected_mac = hmac.new(
            tracking_secret, expected_hashable, hashlib.sha1).hexdigest()

    if not constant_time_compare(expected_mac, observed_mac):
        abort(403)

    # fix encoding in the query string of the destination
    destination = urllib.unquote(destination)
    u = urlparse(destination)
    if u.query:
        query_dict = dict(parse_qsl(u.query))

        # this effectively calls urllib.quote_plus on every query value
        query = urllib.urlencode(query_dict)
        destination = urlunparse(
            (u.scheme, u.netloc, u.path, u.params, query, u.fragment))

    now = format_date_time(time.time())
    response = redirect(destination)
    response.headers['Cache-control'] = 'no-cache'
    response.headers['Pragma'] = 'no-cache'
    response.headers['Date'] = now
    response.headers['Expires'] = now
    return response


# copied from r2.lib.utils
def constant_time_compare(actual, expected):
    """
    Returns True if the two strings are equal, False otherwise

    The time taken is dependent on the number of characters provided
    instead of the number of characters that match.
    """
    actual_len   = len(actual)
    expected_len = len(expected)
    result = actual_len ^ expected_len
    if expected_len > 0:
        for i in xrange(actual_len):
            result |= ord(actual[i]) ^ ord(expected[i % expected_len])
    return result == 0


if __name__ == "__main__":
    # package up for elastic beanstalk
    import zipfile

    with zipfile.ZipFile("/tmp/tracker.zip", "w", zipfile.ZIP_DEFLATED) as zip:
        zip.write(__file__, "application.py")
        zip.writestr("production.ini", config.to_config())
        zip.writestr("requirements.txt", "\n".join(REQUIRED_PACKAGES) + "\n")
