Esempio n. 1
0
def submit_article():
    """Submit a new article for inclusion in margarine.

    Request
    -------

    ::

        POST /
        Content-Type: application/x-www-form-urlencoded

        url=http://blog.alunduil.com/posts/an-explanation-of-lvm-snapshots.html

    ::

        curl -X POST example.com/ -F url="http://blog.alunduil.com/posts/an-explanation-of-lvm-snapshots.html"

    Response
    --------

    ::

        HTTP/1.0 202 Accepted
        Location: /44d85795-248d-5899-b8ca-ac2bd8233755

    """

    logger.debug("request.form[url]: '%s'", request.form["url"])
    logger.debug("ASCII: %s", all(ord(c) < 128 for c in request.form["url"]))
    logger.debug("type(request.form[url]): %s", type(request.form["url"]))

    _id = uuid.uuid5(uuid.NAMESPACE_URL, request.form["url"].encode('ascii'))

    logger.debug("type(_id): %s", type(_id.hex))
    logger.debug("_id: %s", _id.hex)

    message_properties = pika.BasicProperties()
    message_properties.content_type = "application/json"
    message_properties.durable = False

    message = {
            "_id": str(_id.hex),
            "url": request.form["url"],
            }

    message = json.dumps(message)

    channel = get_channel()
    channel.exchange_declare(exchange = "margarine.articles.topic", type = "topic", auto_delete = False)
    channel.basic_publish(body = message, exchange = "margarine.articles.topic", properties = message_properties, routing_key = "articles.create")
    channel.close()

    response = make_response("", 202)
    response.headers["Location"] = url_for(".article", article_id = _id)

    return response
Esempio n. 2
0
def main():
    """Set us up the bomb.

    Create a consuming process for the various backend processes.

    """

    Parameters().parse()

    # TODO Manage threads for load balancing.

    while True:
        try:
            channel = get_channel()

            users.register(channel)
            articles.register(channel)

            channel.start_consuming()
        except (pika.exceptions.ChannelClosed) as e:
            logger.exception(e)
        else:
            channel.close()
Esempio n. 3
0
    def post(self, username):
        """Update the password for the user and complete the password process.

        Authentication is not handled the same for this method as it is for the
        GET method of the password mechanism.  This method can only be invoked
        properly with the verification query parameter passed from the password
        change mechanism in the GET method.

        Request
        -------

        ::

            POST /alunduil/password?verification=6e585a2d-438d-4a33-856a-8a7c086421ee
            Content-Type: multipart/form-data

            password-0=PASSWORD;password-1=PASSWORD

        Response
        --------

        ::

            HTTP/1.0 202 Accepted

        """

        verification = request.args.get("verification")

        if get_keyspace("verifications").get(verification) != username:
            logger.error("verification token not valid")
            logger.debug("verification: %s", verification)

            abort(400)

        password = request.form.get("password-0")

        if password is None or password != request.form.get("password-1"):
            logger.error("passwords did not match!")

            logger.debug("password-0: %s", password)
            logger.debug("password-1: %s", request.form.get("password-1"))

            abort(400)

        message_properties = pika.BasicProperties()
        message_properties.content_type = "application/json"
        message_properties.durable = False

        message = {"username": username, "password": password}

        message = json.dumps(message)

        channel = get_channel()
        channel.exchange_declare(exchange="margarine.users.topic", type="topic", auto_delete=False)
        channel.basic_publish(
            body=message, exchange="margarine.users.topic", properties=message_properties, routing_key="users.password"
        )
        channel.close()

        logger.info("Sent Password Update")

        get_keyspace("verifications").delete(verification)

        return "", 202
Esempio n. 4
0
    def get(self, username):
        """Begin a password change or continue after email verification.

        If an X-Auth-Token header is found, no further validation needs to be
        performed and the password change mechanism is returned.

        If there is no X-Auth-Token header but a query parameter or header with
        an emailed verification code is found (examples shown below), then no
        further validation needs to be performed and the password change
        mechanism is again returned.

        Otherwise, the process is simply initiated and an email is sent with
        a valid URL (including validation token) to continue this process.

        Authenticated Requests
        ----------------------

        ::

            GET /alunduil/password
            X-Auth-Token: 6e585a2d-438d-4a33-856a-8a7c086421ee

        ::

            GET /alunduil/password
            X-Validation-Token: 6e585a2d-438d-4a33-856a-8a7c086421ee

        ::
            
            GET /alunduil/password?verification=6e585a2d-438d-4a33-856a-8a7c086421ee

        Authenticated Response
        ----------------------

        ::

            HTTP/1.0 200 OK

            <!DOCTYPE html>
            <html>
              <body>
                <form name="password" action="/alunduil/password?verification=6e585a2d-438d-4a33-856a-8a7c086421ee" method="post">
                  Password: <input type="password" name="password-0">
                  Password: <input type="password" name="password-1">
                  <input type="submit" value="password-submit">
                </form>
              </body>
            </html>

        Unauthenticated Requests
        ------------------------

        ::

            GET /alunduil/password

        Unauthenticated Response
        ------------------------

        ::

            HTTP/1.0 202 Accepted

        """

        tokens = [
            _
            for _ in [
                request.headers.get("X-Auth-Token"),
                request.headers.get("X-Verification-Token"),
                request.args.get("verification"),
            ]
            if _ is not None
        ]

        logger.debug("len(tokens): %s", len(tokens))

        if len(tokens) > 1:
            abort(400)
        elif len(tokens) == 1:
            verification = tokens[0]

            if get_keyspace("tokens").get(verification) == username:
                get_keyspace("verifications").setex(verification, username, datetime.timedelta(minutes=3))

            logger.debug("verification token: %s", verification)

            return render_template("password_mechanism.html", username=username, verification=verification)

        message_properties = pika.BasicProperties()
        message_properties.content_type = "application/json"
        message_properties.durable = False

        message = {"username": username}
        message = json.dumps(message)

        channel = get_channel()
        channel.exchange_declare(exchange="margarine.users.topic", type="topic", auto_delete=False)
        channel.basic_publish(
            body=message, exchange="margarine.users.topic", properties=message_properties, routing_key="users.email"
        )
        channel.close()

        return "", 202
Esempio n. 5
0
    def put(self, username):
        """Create an User or modify an existing User.

        Create an User
        =============

        To create a new user in the system, perform a PUT on the particular
        user's URL that want created with any parameters (required and
        optional) specified in the form data.

        Request
        -------

        ::
        
            PUT /alunduil
            Content-Type: application/x-www-form-urlencoded

            email=alunduil%40alunduil.com
            name=Alex%20Brandt

        Response
        --------

        ::

            HTTP/1.0 202 Accepted

        Modify an User
        ==============

        This method can also be used to modify an existing user—not just for
        creating new users.

        Request
        -------

        ::

            PUT /alunduil
            Content-Type: application/x-www-form-urlencoded
            X-Auth-Token: 6e585a2d-438d-4a33-856a-8a7c086421ee

            email=alunduil%40alunduil.com

        Response
        --------

        ::

            HTTP/1.0 200 OK

        Possible Errors
        ===============

        :400: Bad Request—A required option was not passed or is improperly
              formatted
        :401: Unauthorized—An attempt to create an existing user was detected

        The following are also used when updating a user:

        :409: Conflict—The new username requested is already in use.

        """

        user = get_collection("users").find_one({"username": username})

        logger.debug("user: %s", user)

        message_properties = pika.BasicProperties()
        message_properties.content_type = "application/json"
        message_properties.durable = False

        message = {
            "username": request.form.get("username", username),
            "email": request.form.get("email"),
            "name": request.form.get("name"),
        }

        routing_key = "users.create"

        if user is not None:
            routing_key = "users.update"

            message["original_username"] = username

            logger.debug("X-Auth-Token: %s", request.headers.get("X-Auth-Token"))

            if get_keyspace("tokens").get(request.headers.get("X-Auth-Token")) != username:
                # TODO Redirect to token URL?
                raise UnauthorizedError(username=username)

        if message["email"] is None and routing_key == "users.create":
            abort(400)

        message = json.dumps(message)

        channel = get_channel()
        channel.exchange_declare(exchange="margarine.users.topic", type="topic", auto_delete=False)
        channel.basic_publish(
            body=message, exchange="margarine.users.topic", properties=message_properties, routing_key=routing_key
        )
        channel.close()

        return "", 202
Esempio n. 6
0
    def put(self, username):
        '''Create an User or modify an existing User.

        Create an User
        =============

        To create a new user in the system, perform a PUT on the particular
        user's URL that want created with any parameters (required and
        optional) specified in the form data.

        Request
        -------

        ::

            PUT /alunduil
            Content-Type: application/x-www-form-urlencoded

            email=alunduil%40alunduil.com
            name=Alex%20Brandt

        Response
        --------

        ::

            HTTP/1.0 202 Accepted

        Modify an User
        ==============

        This method can also be used to modify an existing user—not just for
        creating new users.

        Request
        -------

        ::

            PUT /alunduil
            Content-Type: application/x-www-form-urlencoded
            X-Auth-Token: 6e585a2d-438d-4a33-856a-8a7c086421ee

            email=alunduil%40alunduil.com

        Response
        --------

        ::

            HTTP/1.0 200 OK

        Possible Errors
        ===============

        :400: Bad Request—A required option was not passed or is improperly
              formatted
        :401: Unauthorized—An attempt to create an existing user was detected

        The following are also used when updating a user:

        :409: Conflict—The new username requested is already in use.

        '''

        user = get_collection('users').find_one({ 'username': username })

        routing_key = 'users.create'

        if user is not None:
            routing_key = 'users.update'

            if get_keyspace('tokens').get(request.headers.get('X-Auth-Token')) != username:
                # TODO Redirect to token URL?
                raise UnauthorizedError(username = username)

        message = {
            'username': username,
            'requested_username': request.form.get('username', username),
            'email': request.form.get('email', user.get('email')),
            'name': request.form.get('name', user.get('name')),
        }

        if message['email'] is None and routing_key == 'users.create':
            logger.error('400—Creation of a new user, %s, without an email', username)
            abort(400)

        message = json.dumps(message)

        message_properties = pika.BasicProperties()
        message_properties.content_type = 'application/json'  # TODO Switch to binary format?
        message_properties.durable = False

        logger.info('blend.user.PUT—Sending Message (Type: %s)', routing_key)

        channel = get_channel()
        channel.exchange_declare(exchange = 'margarine.users.topic', type = 'topic', auto_delete = False)
        channel.basic_publish(body = message, exchange = 'margarine.users.topic', properties = message_properties, routing_key = routing_key)
        channel.close()

        return '', 202
Esempio n. 7
0
def create_article_consumer(channel, method, header, body):
    """Create an article—completing the bottom half of article creation.

    This takes the UUID of the article to create and the URL passed through the
    message queue and fills out the rest of the meta-information as well as
    submitting a job to sanitize the HTML.

    This process should be idempotent and therefore not have any ill-effect if
    invoked multiple times (i.e. POSTed by mutliple users).

    Performs the following specific actions:

    * Updates the etag for the article with a HEAD request.
    * Initializes parsed_at to Null until parsing is complete.

    The following actions should be performed in parallel by a fanout:

    * Submits a reference job to update automatic notations in this and others.
    * Submits the sanitization request for the HTML body. 

    .. note::
        The article will need a pre-allocated space in MongoDB for performance
        reasons.  The way we start with the bare necesities and then add
        information at a later time means we may get fragmentation in the data
        stored if we don't pre-allocate enough space.

    """

    article = json.loads(body)

    logger.debug("article: %s", article)

    _id = article.pop("_id")

    articles = get_collection("articles")

    _ = articles.find_one({ "_id": _id })

    logger.debug("Found: %s", _)

    if _ is None or "created_at" not in _:
        article["created_at"] = datetime.datetime.now()

    article = dict([ (k, v) for k, v in article.iteritems() if _ is None or k not in _ or v != _[k] ])

    logger.debug("article: %s", article)
    logger.debug("_id: %s", _id)

    get_collection("articles").update({ "_id": _id }, { "$set": article }, upsert = True)

    message_properties = pika.BasicProperties()
    message_properties.content_type = "application/json"
    message_properties.durable = False

    message = json.dumps({ "_id": _id })

    _ = get_channel()
    _.exchange_declare(exchange = "margarine.articles.create", type = "fanout", auto_delete = False)
    _.basic_publish(body = message, exchange = "margarine.articles.create", properties = message_properties, routing_key = "articles.create")
    _.close()

    channel.basic_ack(delivery_tag = method.delivery_tag)