From 1a08aade00f5fe6f513bf0c21ae2575194c93235 Mon Sep 17 00:00:00 2001 From: Matthew Scragg Date: Fri, 4 Oct 2013 16:42:45 -0500 Subject: [PATCH] subdomain dispatcher --- app.py | 30 +------ realms/__init__.py | 133 ++++++++++++++++++---------- realms/models.py | 64 ++++++++----- realms/ratelimit.py | 20 +---- realms/services.py | 11 +++ realms/templates/_new/index.html | 2 +- realms/templates/account/index.html | 14 +-- realms/wiki.py | 12 ++- 8 files changed, 157 insertions(+), 129 deletions(-) create mode 100644 realms/services.py diff --git a/app.py b/app.py index 35f7712..9942101 100644 --- a/app.py +++ b/app.py @@ -1,35 +1,9 @@ from gevent import monkey, pywsgi monkey.patch_all() -from realms import create_app, config -from threading import Lock +from realms import config, init_db, make_app, SubdomainDispatcher -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__': + init_db(config.db['dbname']) app = SubdomainDispatcher(config.domain, make_app) pywsgi.WSGIServer(('', config.port), app).serve_forever() diff --git a/realms/__init__.py b/realms/__init__.py index b71e9b0..ff3b124 100644 --- a/realms/__init__.py +++ b/realms/__init__.py @@ -1,18 +1,60 @@ import logging import os import time +from threading import Lock -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 +import rethinkdb as rdb +from flask import Flask, request, render_template, url_for, redirect, flash +from flask.ext.login import LoginManager, login_required 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 +from ratelimit import get_view_rate_limit, ratelimiter +from services import db + + +instances = {} + +class SubdomainDispatcher(object): + def __init__(self, domain, create_app): + self.domain = domain + self.create_app = create_app + self.lock = Lock() + + 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 = instances.get(subdomain) + if app is None: + app = self.create_app(subdomain) + instances[subdomain] = app + return app + + def __call__(self, environ, start_response): + app = self.get_application(environ['HTTP_HOST']) + return app(environ, start_response) + + +def init_db(dbname): + if not dbname in rdb.db_list().run(db): + print "Creating DB %s" % dbname + rdb.db_create(dbname).run(db) + + for tbl in ['sites', 'users', 'pages']: + if not tbl in rdb.table_list().run(db): + rdb.table_create(tbl).run(db) + + s = Site() + if not s.get_by_name('_'): + s.create(name='_', repo='_') class RegexConverter(BaseConverter): @@ -21,8 +63,10 @@ class RegexConverter(BaseConverter): self.regex = items[0] -def redirect_url(): - return request.args.get('next') or request.referrer or url_for('index') +def redirect_url(referrer=None): + if not referrer: + referrer = request.referrer + return request.args.get('next') or referrer or url_for('index') def validate_captcha(): @@ -34,6 +78,12 @@ def validate_captcha(): return response.is_valid +def make_app(subdomain): + if subdomain and not Wiki.is_registered(subdomain): + return redirect("http://%s/_new/?site=%s" % (config.hostname, subdomain)) + return create_app(subdomain) + + def create_app(subdomain=None): app = Flask(__name__) app.config.update(config.flask) @@ -43,10 +93,9 @@ def create_app(subdomain=None): app.session_interface = RedisSessionInterface() app.url_map.converters['regex'] = RegexConverter - bcrypt = Bcrypt(app) - login_manager = LoginManager() login_manager.init_app(app) + login_manager.login_view = 'login' @login_manager.user_loader def load_user(user_id): @@ -56,60 +105,65 @@ def create_app(subdomain=None): assets.url = app.static_url_path assets.directory = app.static_folder - - main_repo_dir = config.repos['main'] repo_dir = config.repos['dir'] + repo_name = subdomain if subdomain else "_" - w = Wiki(main_repo_dir) if not subdomain else Wiki(repo_dir + "/" + subdomain) + w = Wiki(repo_dir + "/" + repo_name) + @app.after_request + def inject_x_rate_headers(response): + limit = get_view_rate_limit() + if limit and limit.send_x_headers: + h = response.headers + h.add('X-RateLimit-Remaining', str(limit.remaining)) + h.add('X-RateLimit-Limit', str(limit.limit)) + h.add('X-RateLimit-Reset', str(limit.reset)) + return response @app.template_filter('datetime') def _jinja2_filter_datetime(ts): return time.strftime('%b %d, %Y %I:%M %p', time.localtime(ts)) - @app.errorhandler(404) def page_not_found(e): return render_template('errors/404.html'), 404 - @app.errorhandler(500) def page_error(e): logging.exception(e) return render_template('errors/500.html'), 500 - @app.route("/") + @ratelimiter(limit=50, per=60) def root(): return render('home') #return redirect('/home') - @app.route("/account/") + @login_required def account(): return render_template('account/index.html') - @app.route("/_new/", methods=['GET', 'POST']) + @login_required 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): + wiki_name = to_canonical(request.form['name']) + + if Wiki.is_registered(wiki_name): flash("Site already exists") return redirect(redirect_url()) else: - Wiki(repo_dir + "/" + wiki_name) + s = Site() + s.create(name=wiki_name, repo=wiki_name) + instances.pop(wiki_name, None) return redirect('http://%s.%s' % (wiki_name, config.hostname)) else: return render_template('_new/index.html') - @app.route("/logout/") def logout(): - logout_user() - del session['user'] + User.logout() return redirect(url_for('root')) @app.route("/commit//") @@ -122,55 +176,40 @@ def create_app(subdomain=None): 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("/") + if User.register(request.form.get('username'), request.form.get('email'), request.form.get('password')): + return redirect(url_for('root')) + else: + # Login failed + return redirect(url_for('register')) 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("/") + return redirect(redirect_url(referrer=url_for('root'))) 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']) + @login_required def edit(name): data = w.get_page(name) cname = to_canonical(name) @@ -188,14 +227,14 @@ def create_app(subdomain=None): else: return redirect('/create/'+cname) - @app.route("/delete/", methods=['POST']) + @login_required def delete(name): pass - @app.route("/create/", methods=['GET', 'POST']) @app.route("/create/", methods=['GET', 'POST']) + @login_required def create(name=None): cname = "" if name: diff --git a/realms/models.py b/realms/models.py index 5e7278d..a022f97 100644 --- a/realms/models.py +++ b/realms/models.py @@ -1,27 +1,13 @@ import rethinkdb as rdb import bcrypt -import redis -from flask import session -from flask.ext.login import login_user +from flask import session, flash +from flask.ext.login import login_user, logout_user from rethinkORM import RethinkModel -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']) +from util import gravatar_url +from services import db -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): +def to_dict(cur, first): ret = [] for row in cur: ret.append(row) @@ -33,9 +19,11 @@ def to_dict(cur, first=False): class BaseModel(RethinkModel): + _conn = db + def __init__(self, **kwargs): if not kwargs.get('conn'): - kwargs['conn'] = conn + kwargs['conn'] = db super(BaseModel, self).__init__(**kwargs) @classmethod @@ -66,7 +54,7 @@ class CurrentUser(): return self.id def is_active(self): - return True + return True if self.id else False def is_anonymous(self): return False if self.id else True @@ -100,8 +88,36 @@ class User(BaseModel): return False if bcrypt.checkpw(password, data['password']): - login_user(CurrentUser(data['id'])) - session['user'] = data + cls.login(data['id'], data) return True else: - return False \ No newline at end of file + return False + + @classmethod + def register(cls, username, email, password): + user = User() + email = email.lower() + if user.get_by_email(email): + flash('Email is already taken') + return False + if user.get_by_username(username): + flash('Username is already taken') + return False + + # Create user and login + u = User.create(email=email, + username=username, + password=bcrypt.hashpw(password, bcrypt.gensalt(10)), + avatar=gravatar_url(email)) + + User.login(u.id, user.get_one(u.id, 'id')) + + @classmethod + def login(cls, id, data=None): + login_user(CurrentUser(id), True) + session['user'] = data + + @classmethod + def logout(cls): + logout_user() + session.pop('user', None) \ No newline at end of file diff --git a/realms/ratelimit.py b/realms/ratelimit.py index 115600e..fd66e44 100644 --- a/realms/ratelimit.py +++ b/realms/ratelimit.py @@ -1,8 +1,7 @@ import time from functools import update_wrapper from flask import request, g -from realms import app -from models import cache +from services import cache class RateLimit(object): @@ -28,10 +27,10 @@ def get_view_rate_limit(): def on_over_limit(limit): - return 'You hit the rate limit', 400 + return 'Slow it down', 400 -def ratelimit(limit, per=300, send_x_headers=True, +def ratelimiter(limit, per=300, send_x_headers=True, over_limit=on_over_limit, scope_func=lambda: request.remote_addr, key_func=lambda: request.endpoint): @@ -44,15 +43,4 @@ def ratelimit(limit, per=300, send_x_headers=True, return over_limit(rlimit) return f(*args, **kwargs) return update_wrapper(rate_limited, f) - return decorator - - -@app.after_request -def inject_x_rate_headers(response): - limit = get_view_rate_limit() - if limit and limit.send_x_headers: - h = response.headers - h.add('X-RateLimit-Remaining', str(limit.remaining)) - h.add('X-RateLimit-Limit', str(limit.limit)) - h.add('X-RateLimit-Reset', str(limit.reset)) - return response \ No newline at end of file + return decorator \ No newline at end of file diff --git a/realms/services.py b/realms/services.py new file mode 100644 index 0000000..bbe6f27 --- /dev/null +++ b/realms/services.py @@ -0,0 +1,11 @@ +import rethinkdb as rdb +import redis + +import config + + +# Default DB connection +db = 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']) \ No newline at end of file diff --git a/realms/templates/_new/index.html b/realms/templates/_new/index.html index 429756f..9f8840a 100644 --- a/realms/templates/_new/index.html +++ b/realms/templates/_new/index.html @@ -7,7 +7,7 @@
- + .realms.io
diff --git a/realms/templates/account/index.html b/realms/templates/account/index.html index 91cd377..88ac50b 100644 --- a/realms/templates/account/index.html +++ b/realms/templates/account/index.html @@ -3,18 +3,12 @@

Account

-
+
- -
- -
+ +
-
-
- -
-
+
{% endblock %} \ No newline at end of file diff --git a/realms/wiki.py b/realms/wiki.py index f511c4e..ff9ae23 100644 --- a/realms/wiki.py +++ b/realms/wiki.py @@ -4,8 +4,10 @@ from lxml.html.clean import clean_html import ghdiff from gittle import Gittle +from dulwich.repo import NotGitRepository from util import to_canonical +from models import Site class MyGittle(Gittle): @@ -54,13 +56,17 @@ class Wiki(): def __init__(self, path): try: - self.repo = MyGittle.init(path) - except OSError: - # Repo already exists self.repo = MyGittle(path) + except NotGitRepository: + self.repo = MyGittle.init(path) self.path = path + @staticmethod + def is_registered(name): + s = Site() + return True if s.get_by_name(name) else False + def write_page(self, name, content, message=None, create=False, username=None, email=None): content = clean_html(content) filename = self.cname_to_filename(to_canonical(name))