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
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()
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
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
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
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
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)