From 2906b79dfc600151d4408cb00ebe1de6e89acc61 Mon Sep 17 00:00:00 2001 From: Matthew Scragg Date: Thu, 3 Oct 2013 21:57:19 -0500 Subject: [PATCH] subdomain dispatcher --- app.py | 33 ++++- realms/__init__.py | 344 +++++++++++++++++++++----------------------- realms/models.py | 20 ++- realms/ratelimit.py | 3 +- 4 files changed, 215 insertions(+), 185 deletions(-) diff --git a/app.py b/app.py index 180f510..35f7712 100644 --- a/app.py +++ b/app.py @@ -1,8 +1,35 @@ from gevent import monkey, pywsgi monkey.patch_all() -import logging -from realms import app, config +from realms import create_app, config +from threading import Lock + +class SubdomainDispatcher(object): + + def __init__(self, domain, create_app): + self.domain = domain + self.create_app = create_app + self.lock = Lock() + self.instances = {} + + def get_application(self, host): + host = host.split(':')[0] + assert host.endswith(self.domain), 'Configuration error' + subdomain = host[:-len(self.domain)].rstrip('.') + with self.lock: + app = self.instances.get(subdomain) + if app is None: + app = self.create_app(subdomain) + self.instances[subdomain] = app + return app + + def __call__(self, environ, start_response): + app = self.get_application(environ['HTTP_HOST']) + return app(environ, start_response) + + +def make_app(subdomain): + return create_app(subdomain) if __name__ == '__main__': - app.logger.setLevel(logging.INFO) + app = SubdomainDispatcher(config.domain, make_app) pywsgi.WSGIServer(('', config.port), app).serve_forever() diff --git a/realms/__init__.py b/realms/__init__.py index d2864fb..b71e9b0 100644 --- a/realms/__init__.py +++ b/realms/__init__.py @@ -2,19 +2,17 @@ import logging import os import time -import redis -import rethinkdb as rdb from flask import Flask, request, render_template, url_for, redirect, flash, session from flask.ext.bcrypt import Bcrypt from flask.ext.login import LoginManager, login_user, logout_user from flask.ext.assets import Environment from recaptcha.client import captcha from werkzeug.routing import BaseConverter - from session import RedisSessionInterface import config from wiki import Wiki from util import to_canonical, remove_ext, mkdir_safe, gravatar_url +from models import Site, User, CurrentUser class RegexConverter(BaseConverter): @@ -22,48 +20,6 @@ class RegexConverter(BaseConverter): super(RegexConverter, self).__init__(url_map) self.regex = items[0] -app = Flask(__name__) -app.config.update(config.flask) -app.debug = (config.ENV is not 'PROD') -app.secret_key = config.secret_key -app.static_path = os.sep + 'static' -app.session_interface = RedisSessionInterface() -app.url_map.converters['regex'] = RegexConverter - -bcrypt = Bcrypt(app) - -login_manager = LoginManager() -login_manager.init_app(app) - -assets = Environment(app) -assets.url = app.static_url_path -assets.directory = app.static_folder - -cache = redis.StrictRedis(host=config.cache['host'], port=config.cache['port']) - -conn = rdb.connect(config.db['host'], config.db['port'], db=config.db['dbname']) - -if not config.db['dbname'] in rdb.db_list().run(conn) and config.ENV is not 'PROD': - # Create default db and repo - print "Creating DB %s" % config.db['dbname'] - rdb.db_create(config.db['dbname']).run(conn) - for tbl in ['sites', 'users', 'pages']: - rdb.table_create(tbl).run(conn) - -main_repo_dir = config.repos['main'] -repo_dir = config.repos['dir'] - -# This is down here because of dependencies above -from models import Site, User, CurrentUser - - -@login_manager.user_loader -def load_user(user_id): - return CurrentUser(user_id) - - -w = Wiki(main_repo_dir) - def redirect_url(): return request.args.get('next') or request.referrer or url_for('index') @@ -73,167 +29,197 @@ def validate_captcha(): response = captcha.submit( request.form['recaptcha_challenge_field'], request.form['recaptcha_response_field'], - app.config['RECAPTCHA_PRIVATE_KEY'], + config.flask['RECAPTCHA_PRIVATE_KEY'], request.remote_addr) return response.is_valid -@app.template_filter('datetime') -def _jinja2_filter_datetime(ts): - return time.strftime('%b %d, %Y %I:%M %p', time.localtime(ts)) + +def create_app(subdomain=None): + app = Flask(__name__) + app.config.update(config.flask) + app.debug = (config.ENV is not 'PROD') + app.secret_key = config.secret_key + app.static_path = os.sep + 'static' + app.session_interface = RedisSessionInterface() + app.url_map.converters['regex'] = RegexConverter + + bcrypt = Bcrypt(app) + + login_manager = LoginManager() + login_manager.init_app(app) + + @login_manager.user_loader + def load_user(user_id): + return CurrentUser(user_id) + + assets = Environment(app) + assets.url = app.static_url_path + assets.directory = app.static_folder -@app.errorhandler(404) -def page_not_found(e): - return render_template('errors/404.html'), 404 + main_repo_dir = config.repos['main'] + repo_dir = config.repos['dir'] + + w = Wiki(main_repo_dir) if not subdomain else Wiki(repo_dir + "/" + subdomain) -@app.errorhandler(500) -def page_error(e): - logging.exception(e) - return render_template('errors/500.html'), 500 + @app.template_filter('datetime') + def _jinja2_filter_datetime(ts): + return time.strftime('%b %d, %Y %I:%M %p', time.localtime(ts)) -@app.route("/") -def root(): - return render('home') - #return redirect('/home') + @app.errorhandler(404) + def page_not_found(e): + return render_template('errors/404.html'), 404 -@app.route("/account/") -def account(): - return render_template('account/index.html') + @app.errorhandler(500) + def page_error(e): + logging.exception(e) + return render_template('errors/500.html'), 500 -@app.route("/_new/", methods=['GET', 'POST']) -def new_wiki(): - if request.method == 'POST': - # TODO validate wiki name - wiki_name = request.form['name'] - s = Site() - if s.get_by_name(wiki_name): - flash("Site already exists") - return redirect(redirect_url()) + @app.route("/") + def root(): + return render('home') + #return redirect('/home') + + + @app.route("/account/") + def account(): + return render_template('account/index.html') + + + @app.route("/_new/", methods=['GET', 'POST']) + def new_wiki(): + if request.method == 'POST': + # TODO validate wiki name + wiki_name = request.form['name'] + s = Site() + if s.get_by_name(wiki_name): + flash("Site already exists") + return redirect(redirect_url()) + else: + Wiki(repo_dir + "/" + wiki_name) + return redirect('http://%s.%s' % (wiki_name, config.hostname)) else: - Wiki(repo_dir + "/" + wiki_name) - return redirect('http://%s.%s' % (wiki_name, config.hostname)) - else: - return render_template('_new/index.html') + return render_template('_new/index.html') -@app.route("/logout/") -def logout(): - logout_user() - del session['user'] - return redirect(url_for('root')) + @app.route("/logout/") + def logout(): + logout_user() + del session['user'] + return redirect(url_for('root')) -@app.route("/commit//") -def commit_sha(name, sha): - cname = to_canonical(name) + @app.route("/commit//") + def commit_sha(name, sha): + cname = to_canonical(name) - data = w.get_page(cname, sha=sha) - if data: - return render_template('page/page.html', page=data) - else: - return redirect('/create/'+cname) - - -@app.route("/compare//") -def compare(name, fsha, dots, lsha): - diff = w.compare(name, fsha, lsha) - return render_template('page/compare.html', name=name, diff=diff) - - -@app.route("/register", methods=['GET', 'POST']) -def register(): - if request.method == 'POST': - user = User() - if user.get_by_email(request.form['email']): - flash('Email is already taken') - return redirect('/register') - if user.get_by_username(request.form['username']): - flash('Username is already taken') - return redirect('/register') - - email = request.form['email'].lower() - # Create user and login - u = User.create(email=email, - username=request.form['username'], - password=bcrypt.generate_password_hash(request.form['password']), - avatar=gravatar_url(email)) - login_user(CurrentUser(u.id)) - return redirect("/") - else: - return render_template('account/register.html') - - -@app.route("/login", methods=['GET', 'POST']) -def login(): - if request.method == 'POST': - if User.auth(request.form['email'], request.form['password']): - return redirect("/") - else: - flash("Email or Password invalid") - return redirect("/login") - else: - return render_template('account/login.html') - - -@app.route("/history/") -def history(name): - history = w.get_history(name) - return render_template('page/history.html', name=name, history=history) - - -@app.route("/edit/", methods=['GET', 'POST']) -def edit(name): - data = w.get_page(name) - cname = to_canonical(name) - if request.method == 'POST': - edit_cname = to_canonical(request.form['name']) - if edit_cname != cname: - w.rename_page(cname, edit_cname) - w.write_page(edit_cname, request.form['content'], message=request.form['message']) - return redirect("/" + edit_cname) - else: + data = w.get_page(cname, sha=sha) if data: - name = remove_ext(data['name']) - content = data['data'] - return render_template('page/edit.html', name=name, content=content) + return render_template('page/page.html', page=data) else: return redirect('/create/'+cname) -@app.route("/delete/", methods=['POST']) -def delete(name): - pass + @app.route("/compare//") + def compare(name, fsha, dots, lsha): + diff = w.compare(name, fsha, lsha) + return render_template('page/compare.html', name=name, diff=diff) -@app.route("/create/", methods=['GET', 'POST']) -@app.route("/create/", methods=['GET', 'POST']) -def create(name=None): - cname = "" - if name: + @app.route("/register", methods=['GET', 'POST']) + def register(): + if request.method == 'POST': + user = User() + if user.get_by_email(request.form['email']): + flash('Email is already taken') + return redirect('/register') + if user.get_by_username(request.form['username']): + flash('Username is already taken') + return redirect('/register') + + email = request.form['email'].lower() + # Create user and login + u = User.create(email=email, + username=request.form['username'], + password=bcrypt.generate_password_hash(request.form['password']), + avatar=gravatar_url(email)) + login_user(CurrentUser(u.id)) + return redirect("/") + else: + return render_template('account/register.html') + + + @app.route("/login", methods=['GET', 'POST']) + def login(): + if request.method == 'POST': + if User.auth(request.form['email'], request.form['password']): + return redirect("/") + else: + flash("Email or Password invalid") + return redirect("/login") + else: + return render_template('account/login.html') + + + @app.route("/history/") + def history(name): + history = w.get_history(name) + return render_template('page/history.html', name=name, history=history) + + + @app.route("/edit/", methods=['GET', 'POST']) + def edit(name): + data = w.get_page(name) cname = to_canonical(name) - if w.get_page(cname): - # Page exists, edit instead - return redirect("/edit/" + cname) - if request.method == 'POST': - w.write_page(request.form['name'], request.form['content'], message=request.form['message'], create=True) - return redirect("/" + cname) - else: - return render_template('page/edit.html', name=cname, content="") + if request.method == 'POST': + edit_cname = to_canonical(request.form['name']) + if edit_cname != cname: + w.rename_page(cname, edit_cname) + w.write_page(edit_cname, request.form['content'], message=request.form['message']) + return redirect("/" + edit_cname) + else: + if data: + name = remove_ext(data['name']) + content = data['data'] + return render_template('page/edit.html', name=name, content=content) + else: + return redirect('/create/'+cname) -@app.route("/") -def render(name): - cname = to_canonical(name) - if cname != name: - return redirect('/' + cname) + @app.route("/delete/", methods=['POST']) + def delete(name): + pass - data = w.get_page(cname) - if data: - return render_template('page/page.html', name=cname, page=data) - else: - return redirect('/create/'+cname) -import ratelimit + @app.route("/create/", methods=['GET', 'POST']) + @app.route("/create/", methods=['GET', 'POST']) + def create(name=None): + cname = "" + if name: + cname = to_canonical(name) + if w.get_page(cname): + # Page exists, edit instead + return redirect("/edit/" + cname) + if request.method == 'POST': + w.write_page(request.form['name'], request.form['content'], message=request.form['message'], create=True) + return redirect("/" + cname) + else: + return render_template('page/edit.html', name=cname, content="") + + + @app.route("/") + def render(name): + cname = to_canonical(name) + if cname != name: + return redirect('/' + cname) + + data = w.get_page(cname) + if data: + return render_template('page/page.html', name=cname, page=data) + else: + return redirect('/create/'+cname) + + return app \ No newline at end of file diff --git a/realms/models.py b/realms/models.py index 12ffa3c..5e7278d 100644 --- a/realms/models.py +++ b/realms/models.py @@ -1,9 +1,25 @@ import rethinkdb as rdb +import bcrypt +import redis from flask import session from flask.ext.login import login_user from rethinkORM import RethinkModel -from realms import conn, bcrypt +from realms import config +# Default DB connection +conn = rdb.connect(config.db['host'], config.db['port'], db=config.db['dbname']) + +# Default Cache connection +cache = redis.StrictRedis(host=config.cache['host'], port=config.cache['port']) + + +def init_db(): + if not config.db['dbname'] in rdb.db_list().run(conn) and config.ENV is not 'PROD': + # Create default db and repo + print "Creating DB %s" % config.db['dbname'] + rdb.db_create(config.db['dbname']).run(conn) + for tbl in ['sites', 'users', 'pages']: + rdb.table_create(tbl).run(conn) def to_dict(cur, first=False): ret = [] @@ -83,7 +99,7 @@ class User(BaseModel): if not data: return False - if bcrypt.check_password_hash(data['password'], password): + if bcrypt.checkpw(password, data['password']): login_user(CurrentUser(data['id'])) session['user'] = data return True diff --git a/realms/ratelimit.py b/realms/ratelimit.py index 856530c..115600e 100644 --- a/realms/ratelimit.py +++ b/realms/ratelimit.py @@ -1,7 +1,8 @@ import time from functools import update_wrapper from flask import request, g -from realms import app, cache +from realms import app +from models import cache class RateLimit(object):