This commit is contained in:
Matthew Scragg 2013-11-08 12:20:40 -06:00
parent ba1ec10a34
commit 36cf728862
24 changed files with 181 additions and 488 deletions

16
Vagrantfile vendored
View File

@ -1,16 +0,0 @@
VAGRANTFILE_API_VERSION = "2"
Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
config.vm.box = "precise64"
config.vm.synced_folder "srv/", "/srv/"
config.vm.provision :salt do |salt|
salt.minion_config = "srv/minion"
salt.run_highstate = true
end
end
Vagrant::Config.run do |config|
config.vm.forward_port 80, 8000
config.vm.forward_port 10000, 10000
config.vm.forward_port 20000, 20000
end

View File

@ -4,19 +4,21 @@ import time
from threading import Lock from threading import Lock
import rethinkdb as rdb import rethinkdb as rdb
from flask import Flask, request, render_template, url_for, redirect, flash from flask import Flask, g, request, render_template, url_for, redirect, flash, session
from flask.ctx import _AppCtxGlobals
from flask.ext.login import LoginManager, login_required from flask.ext.login import LoginManager, login_required
from flask.ext.assets import Environment, Bundle from flask.ext.assets import Environment, Bundle
from recaptcha.client import captcha
from werkzeug.routing import BaseConverter from werkzeug.routing import BaseConverter
from werkzeug.utils import cached_property
from session import RedisSessionInterface
import config import config
from wiki import Wiki from realms.lib.ratelimit import get_view_rate_limit, ratelimiter
from util import to_canonical, remove_ext, mkdir_safe, gravatar_url from realms.lib.session import RedisSessionInterface
from realms.lib.wiki import Wiki
from realms.lib.util import to_canonical, remove_ext, mkdir_safe, gravatar_url
from realms.lib.services import db
from models import Site, User, CurrentUser from models import Site, User, CurrentUser
from ratelimit import get_view_rate_limit, ratelimiter
from services import db
# Flask instance container # Flask instance container
instances = {} instances = {}
@ -26,6 +28,17 @@ login_manager = LoginManager()
assets = Environment() assets = Environment()
class AppCtxGlobals(_AppCtxGlobals):
@cached_property
def current_user(self):
return session.get('user') if session.get('user') else {'username': 'Anon'}
class Application(Flask):
app_ctx_globals_class = AppCtxGlobals
class SubdomainDispatcher(object): class SubdomainDispatcher(object):
""" """
Application factory Application factory
@ -90,19 +103,12 @@ def redirect_url(referrer=None):
return request.args.get('next') or referrer or url_for('index') return request.args.get('next') or referrer or url_for('index')
def validate_captcha():
response = captcha.submit(
request.form['recaptcha_challenge_field'],
request.form['recaptcha_response_field'],
config.flask['RECAPTCHA_PRIVATE_KEY'],
request.remote_addr)
return response.is_valid
def format_subdomain(s): def format_subdomain(s):
if not config.repos['enable_subrepos']:
return ""
s = s.lower() s = s.lower()
s = to_canonical(s) s = to_canonical(s)
if s in ['www', 'api']: if s in config.repos['forbidden_subrepos']:
# Not allowed # Not allowed
s = "" s = ""
return s return s
@ -116,7 +122,7 @@ def make_app(subdomain):
def create_app(subdomain=None): def create_app(subdomain=None):
app = Flask(__name__) app = Application(__name__)
app.config.update(config.flask) app.config.update(config.flask)
app.debug = (config.ENV is not 'PROD') app.debug = (config.ENV is not 'PROD')
app.secret_key = config.secret_key app.secret_key = config.secret_key
@ -199,7 +205,7 @@ def create_app(subdomain=None):
def home(): def home():
return redirect(url_for('root')) return redirect(url_for('root'))
@app.route("/account/") @app.route("/_account/")
@login_required @login_required
def account(): def account():
return render_template('account/index.html') return render_template('account/index.html')
@ -215,13 +221,13 @@ def create_app(subdomain=None):
return redirect(redirect_url()) return redirect(redirect_url())
else: else:
s = Site() s = Site()
s.create(name=wiki_name, repo=wiki_name, founder=CurrentUser.get('id')) s.create(name=wiki_name, repo=wiki_name, founder=g.current_user.get('id'))
instances.pop(wiki_name, None) instances.pop(wiki_name, None)
return redirect('http://%s.%s' % (wiki_name, config.hostname)) return redirect('http://%s.%s' % (wiki_name, config.hostname))
else: else:
return render_template('_new/index.html') return render_template('_new/index.html')
@app.route("/logout/") @app.route("/_logout/")
def logout(): def logout():
User.logout() User.logout()
return redirect(url_for('root')) return redirect(url_for('root'))
@ -234,7 +240,7 @@ def create_app(subdomain=None):
if data: if data:
return render_template('page/page.html', name=name, page=data, commit=sha) return render_template('page/page.html', name=name, page=data, commit=sha)
else: else:
return redirect('/create/'+cname) return redirect('/_create/'+cname)
@app.route("/_compare/<name>/<regex('[^.]+'):fsha><regex('\.{2,3}'):dots><regex('.+'):lsha>") @app.route("/_compare/<name>/<regex('[^.]+'):fsha><regex('\.{2,3}'):dots><regex('.+'):lsha>")
def compare(name, fsha, dots, lsha): def compare(name, fsha, dots, lsha):
@ -247,11 +253,11 @@ def create_app(subdomain=None):
name = request.form.get('name') name = request.form.get('name')
commit = request.form.get('commit') commit = request.form.get('commit')
cname = to_canonical(name) cname = to_canonical(name)
w.revert_page(name, commit, message="Reverting %s" % cname, username=CurrentUser.get('username')) w.revert_page(name, commit, message="Reverting %s" % cname, username=g.current_user.get('username'))
flash('Page reverted', 'success') flash('Page reverted', 'success')
return redirect("/" + cname) return redirect("/" + cname)
@app.route("/register", methods=['GET', 'POST']) @app.route("/_register", methods=['GET', 'POST'])
def register(): def register():
if request.method == 'POST': if request.method == 'POST':
if User.register(request.form.get('username'), request.form.get('email'), request.form.get('password')): if User.register(request.form.get('username'), request.form.get('email'), request.form.get('password')):
@ -262,14 +268,14 @@ def create_app(subdomain=None):
else: else:
return render_template('account/register.html') return render_template('account/register.html')
@app.route("/login", methods=['GET', 'POST']) @app.route("/_login", methods=['GET', 'POST'])
def login(): def login():
if request.method == 'POST': if request.method == 'POST':
if User.auth(request.form['email'], request.form['password']): if User.auth(request.form['email'], request.form['password']):
return redirect(redirect_url(referrer=url_for('root'))) return redirect(redirect_url(referrer=url_for('root')))
else: else:
flash("Email or Password invalid") flash("Email or Password invalid")
return redirect("/login") return redirect("/_login")
else: else:
return render_template('account/login.html') return render_template('account/login.html')
@ -288,7 +294,7 @@ def create_app(subdomain=None):
w.rename_page(cname, edit_cname) w.rename_page(cname, edit_cname)
w.write_page(edit_cname, request.form['content'], w.write_page(edit_cname, request.form['content'],
message=request.form['message'], message=request.form['message'],
username=CurrentUser.get('username')) username=g.current_user.get('username'))
return redirect("/" + edit_cname) return redirect("/" + edit_cname)
else: else:
if data: if data:
@ -296,7 +302,7 @@ def create_app(subdomain=None):
content = data['data'] content = data['data']
return render_template('page/edit.html', name=name, content=content) return render_template('page/edit.html', name=name, content=content)
else: else:
return redirect('/create/'+cname) return redirect('/_create/'+cname)
@app.route("/_delete/<name>", methods=['POST']) @app.route("/_delete/<name>", methods=['POST'])
@login_required @login_required
@ -316,7 +322,7 @@ def create_app(subdomain=None):
w.write_page(request.form['name'], request.form['content'], w.write_page(request.form['name'], request.form['content'],
message=request.form['message'], message=request.form['message'],
create=True, create=True,
username=CurrentUser.get('username')) username=g.current_user.get('username'))
return redirect("/" + cname) return redirect("/" + cname)
else: else:
return render_template('page/edit.html', name=cname, content="") return render_template('page/edit.html', name=cname, content="")
@ -331,6 +337,6 @@ def create_app(subdomain=None):
if data: if data:
return render_template('page/page.html', name=cname, page=data) return render_template('page/page.html', name=cname, page=data)
else: else:
return redirect('/create/'+cname) return redirect('/_create/'+cname)
return app return app

0
realms/lib/__init__.py Normal file
View File

View File

@ -1,7 +1,6 @@
import rethinkdb as rdb import rethinkdb as rdb
import redis import redis
from realms import config
import config
# Default DB connection # Default DB connection

128
realms/lib/util.py Normal file
View File

@ -0,0 +1,128 @@
import re
import os
import hashlib
import json
from flask import request
from recaptcha.client import captcha
from realms import config
from realms.lib.services import cache
def cache_it(fn):
def wrap(*args, **kw):
key = "%s:%s" % (args[0].table, args[1])
data = cache.get(key)
# Assume strings are JSON encoded
try:
data = json.loads(data)
except TypeError:
pass
except ValueError:
pass
if data is not None:
return data
else:
data = fn(*args)
print data
ret = data
if data is None:
data = ''
if not isinstance(data, basestring):
try:
data = json.dumps(data, separators=(',', ':'))
except TypeError:
pass
cache.set(key, data)
return ret
return wrap
def to_json(res, first=False):
"""
Jsonify query result.
"""
res = to_dict(res, first)
return json.dumps(res, separators=(',', ':'))
def to_dict(cur, first=False):
if not cur:
return None
ret = []
for row in cur:
ret.append(row)
if ret and first:
return ret[0]
else:
return ret
def validate_captcha():
response = captcha.submit(
request.form['recaptcha_challenge_field'],
request.form['recaptcha_response_field'],
config.flask['RECAPTCHA_PRIVATE_KEY'],
request.remote_addr)
return response.is_valid
def mkdir_safe(path):
if path and not(os.path.exists(path)):
os.makedirs(path)
return path
def extract_path(file_path):
if not file_path:
return None
last_slash = file_path.rindex("/")
if last_slash:
return file_path[0, last_slash]
def clean_path(path):
if path:
if path[0] != '/':
path.insert(0, '/')
return re.sub(r"//+", '/', path)
def extract_name(file_path):
if file_path[-1] == "/":
return None
return os.path.basename(file_path)
def remove_ext(path):
return os.path.splitext(path)[0]
def clean_url(url):
if not url:
return url
url = url.replace('%2F', '/')
url = re.sub(r"^/+", "", url)
return re.sub(r"//+", '/', url)
def to_canonical(s):
"""
Double space -> single dash
Double dash -> single dash
Remove all non alphanumeric and dash
Limit to first 64 chars
"""
s = s.encode('ascii', 'ignore')
s = str(s)
s = re.sub(r"\s\s*", "-", s)
s = re.sub(r"\-\-+", "-", s)
s = re.sub(r"[^a-zA-Z0-9\-]", "", s)
s = s[:64]
return s
def gravatar_url(email):
return "https://www.gravatar.com/avatar/" + hashlib.md5(email).hexdigest()

View File

@ -7,8 +7,8 @@ import ghdiff
import gittle.utils import gittle.utils
from gittle import Gittle from gittle import Gittle
from dulwich.repo import NotGitRepository from dulwich.repo import NotGitRepository
from werkzeug.utils import escape, unescape
from util import to_canonical, escape_repl, unescape_repl from util import to_canonical
from models import Site from models import Site
@ -80,6 +80,14 @@ class Wiki():
def write_page(self, name, content, message=None, create=False, username=None, email=None): def write_page(self, name, content, message=None, create=False, username=None, email=None):
def escape_repl(m):
if m.group(1):
return "```" + escape(m.group(1)) + "```"
def unescape_repl(m):
if m.group(1):
return "```" + unescape(m.group(1)) + "```"
# prevents p tag from being added, we remove this later # prevents p tag from being added, we remove this later
content = '<div>' + content + '</div>' content = '<div>' + content + '</div>'
content = re.sub(r"```(.*?)```", escape_repl, content, flags=re.DOTALL) content = re.sub(r"```(.*?)```", escape_repl, content, flags=re.DOTALL)
@ -93,9 +101,11 @@ class Wiki():
# post processing to fix errors # post processing to fix errors
content = content[5:-6] content = content[5:-6]
# FIXME this is for block quotes, doesn't work for double ">" # FIXME this is for block quotes, doesn't work for double ">"
content = re.sub(r"(\n&gt;)", "\n>", content) content = re.sub(r"(\n&gt;)", "\n>", content)
content = re.sub(r"(^&gt;)", ">", content) content = re.sub(r"(^&gt;)", ">", content)
content = re.sub(r"```(.*?)```", unescape_repl, content, flags=re.DOTALL) content = re.sub(r"```(.*?)```", unescape_repl, content, flags=re.DOTALL)
filename = self.cname_to_filename(to_canonical(name)) filename = self.cname_to_filename(to_canonical(name))
@ -129,7 +139,7 @@ class Wiki():
files=[old_name]) files=[old_name])
def get_page(self, name, sha='HEAD'): def get_page(self, name, sha='HEAD'):
commit = gittle.utils.git.commit_info(self.repo[sha]) # commit = gittle.utils.git.commit_info(self.repo[sha])
name = self.cname_to_filename(name) name = self.cname_to_filename(name)
try: try:
return self.repo.get_commit_files(sha, paths=[name]).get(name) return self.repo.get_commit_files(sha, paths=[name]).get(name)

View File

@ -1,60 +1,9 @@
import json
import rethinkdb as rdb import rethinkdb as rdb
import bcrypt import bcrypt
from flask import session, flash from flask import session, flash
from flask.ext.login import login_user, logout_user from flask.ext.login import login_user, logout_user
from util import gravatar_url from realms.lib.util import gravatar_url, to_dict, cache_it
from services import db, cache from realms.lib.services import db
def to_json(res, first=False):
"""
Jsonify query result.
"""
res = to_dict(res, first)
return json.dumps(res, separators=(',',':'))
def to_dict(cur, first=False):
if not cur:
return None
ret = []
for row in cur:
ret.append(row)
if ret and first:
return ret[0]
else:
return ret
def cache_it(fn):
def wrap(*args, **kw):
key = "%s:%s" % (args[0].table, args[1])
data = cache.get(key)
# Assume strings are JSON encoded
try:
data = json.loads(data)
except TypeError:
pass
except ValueError:
pass
if data is not None:
return data
else:
data = fn(*args)
print data
ret = data
if data is None:
data = ''
if not isinstance(data, basestring):
try:
data = json.dumps(data, separators=(',', ':'))
except TypeError:
pass
cache.set(key, data)
return ret
return wrap
class BaseModel(): class BaseModel():
@ -162,7 +111,7 @@ class User(BaseModel):
User.login(u.id) User.login(u.id)
@classmethod @classmethod
def login(cls, id, data=None): def login(cls, id):
login_user(CurrentUser(id), True) login_user(CurrentUser(id), True)
@classmethod @classmethod

View File

@ -1,95 +0,0 @@
import re
import os
import hashlib
def escape_repl(m):
print "group 0"
print m.group(0)
print "group 1"
print m.group(1)
if m.group(1):
return "```" + escape_html(m.group(1)) + "```"
def unescape_repl(m):
if m.group(1):
return "```" + unescape_html(m.group(1)) + "```"
def escape_html(s):
s = s.replace("&", '&amp;')
s = s.replace("<", '&lt;')
s = s.replace(">", '&gt;')
s = s.replace('"', '&quot;')
s = s.replace("'", '&#39;')
return s
def unescape_html(s):
s = s.replace('&amp;', "&")
s = s.replace('&lt;', "<")
s = s.replace('&gt;', ">")
s = s.replace('&quot;', '"')
s = s.replace('&#39;', "'")
return s
def mkdir_safe(path):
if path and not(os.path.exists(path)):
os.makedirs(path)
return path
def extract_path(file_path):
if not file_path:
return None
last_slash = file_path.rindex("/")
if last_slash:
return file_path[0, last_slash]
def clean_path(path):
if path:
if path[0] != '/':
path.insert(0, '/')
return re.sub(r"//+", '/', path)
def extract_name(file_path):
if file_path[-1] == "/":
return None
return os.path.basename(file_path)
def remove_ext(path):
return os.path.splitext(path)[0]
def clean_url(url):
if not url:
return url
url = url.replace('%2F', '/')
url = re.sub(r"^/+", "", url)
return re.sub(r"//+", '/', url)
def to_canonical(s):
"""
Double space -> single dash
Double dash -> single dash
Remove all non alphanumeric and dash
Limit to first 64 chars
"""
s = s.encode('ascii', 'ignore')
s = str(s)
s = re.sub(r"\s\s*", "-", s)
s = re.sub(r"\-\-+", "-", s)
s = re.sub(r"[^a-zA-Z0-9\-]", "", s)
s = s[:64]
return s
def gravatar_url(email):
return "https://www.gravatar.com/avatar/" + hashlib.md5(email).hexdigest()

View File

@ -1,2 +0,0 @@
master: localhost
file_client: local

View File

@ -1,30 +0,0 @@
extra-repos:
pkgrepo.managed:
- ppa: chris-lea/python-redis
- ppa: brianmercer/redis
- ppa: chris-lea/node.js
- ppa: nginx/stable
common-pkgs:
pkg.installed:
- pkgs:
- python
- vim
- build-essential
- screen
- htop
- git
- ntp
- libpcre3-dev
- libevent-dev
- iptraf
- python-software-properties
- python-pip
- python-virtualenv
- python-dev
- pkg-config
- curl
- libxml2-dev
- libxslt-dev
- require:
- pkgrepo.managed: extra-repos

View File

@ -1,9 +0,0 @@
nginx:
pkg:
- installed
service:
- running
- enable: True
- reload: True
- require:
- pkg: nginx

View File

@ -1,70 +0,0 @@
{% from 'config.sls' import root %}
gzip_proxied any;
gzip_types text/plain text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript;
upstream web {
fair;
server 127.0.0.1:10000;
}
server {
listen 80;
# Allow file uploads
client_max_body_size 50M;
location ^~ /static/ {
root {{ root }};
expires max;
}
location = /favicon.ico {
rewrite (.*) /static/favicon.ico;
}
location = /robots.txt {
rewrite (.*) /static/robots.txt;
}
location / {
proxy_pass_header Server;
proxy_redirect off;
proxy_buffering off;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Scheme $scheme;
proxy_set_header Host $http_host;
proxy_pass http://web;
error_page 502 = /maintenance.html;
}
location /maintenance.html {
root {{ root }}/templates/;
add_header Cache-Control private;
expires epoch;
}
}
{% if ssl_certificate %}
server {
listen 443;
ssl on;
ssl_certificate {{ ssl_certificate }};
ssl_certificate_key {{ ssl_certificate_key }};
location ^~ /static/ {
root {{ root }};
expires max;
}
location / {
proxy_pass_header Server;
proxy_redirect off;
proxy_buffering off;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Scheme $scheme;
proxy_set_header Host $http_host;
proxy_pass http://web;
error_page 502 = /maintenance.html;
}
}
{% endif %}

View File

@ -1,13 +0,0 @@
nodejs:
pkg.installed
nodejs-dev:
pkg.installed
npm:
pkg.installed
bower:
npm.installed:
- require:
- pkg.installed: npm

View File

@ -1,40 +0,0 @@
python-dev:
pkg.installed
python-pip:
pkg.installed
build-essential:
pkg.installed
realms-repo:
git.latest:
- unless: test -e /vagrant
- name: git@github.com:scragg0x/realms.git
- target: /home/deploy
- rev: master
- user: deploy
- identity: /home/deploy/.ssh/id_rsa
realms-link:
cmd.run:
- onlyif: test -e /vagrant
- name: ln -s /vagrant /home/deploy/realms
/home/deploy/virtualenvs/realms:
file.directory:
- user: deploy
- group: deploy
- makedirs: True
- recurse:
- user
- group
- require:
- user.present: deploy
virtualenv.managed:
- name: /home/deploy/virtualenvs/realms
- requirements: /home/deploy/realms/requirements.txt
- watch:
- git: realms-repo
- require:
- file.directory: /home/deploy/virtualenvs/realms

View File

@ -1,9 +0,0 @@
redis-server:
pkg:
- installed
service:
- running
- enable: True
- reload: True
- require:
- pkg: redis-server

View File

@ -1,23 +0,0 @@
rethink-repo:
pkgrepo.managed:
- ppa: rethinkdb/ppa
rethinkdb:
user.present:
- shell: /bin/bash
- home: /home/rethinkdb
pkg:
- installed
rethinkdb-pip:
pip:
- name: rethinkdb
- installed
- require:
- pkg: python-pip
- pkg: rethinkdb
- pkg: build-essential
/etc/rethinkdb/rdb0.conf:
file.managed:
- source: salt://rethinkdb/rdb0.conf

View File

@ -1,10 +0,0 @@
runuser=rethinkdb
rungroup=rethinkdb
pid-file=/home/rethinkdb/rdb0/rethinkdb.pid
directory=/home/rethinkdb/rdb0
bind=all
driver-port=28015
cluster-port=29015
port-offset=0
http-port=20000
cores=2

View File

@ -1,19 +0,0 @@
/etc/supervisord.conf:
file.managed:
- source: salt://supervisor/supervisord.conf
supervisor-pip:
pip:
- name: supervisor
- installed
- require:
- pkg.installed: python-pip
supervisor-run:
cmd.run:
- unless: test -e /tmp/supervisord.pid
- name: /usr/local/bin/supervisord
- require:
- file.managed: /etc/supervisord.conf
- file.managed: /etc/rethinkdb/rdb0.conf

View File

@ -1,33 +0,0 @@
[rpcinterface:supervisor]
supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface
[unix_http_server]
file = /tmp/supervisor.sock
chmod = 0777
[supervisorctl]
serverurl = unix:///tmp/supervisor.sock
[supervisord]
logfile = /tmp/supervisord.log
logfile_maxbytes = 50MB
logfile_backups=10
loglevel = info
pidfile = /tmp/supervisord.pid
nodaemon = false
minfds = 1024
minprocs = 200
umask = 022
user = root
identifier = supervisor
directory = /tmp
nocleanup = true
childlogdir = /tmp
strip_ansi = false
[program:realms]
command=/home/deploy/virtualenvs/realms/bin/python /home/deploy/realms/app.py
[program:rethinkdb]
command=/usr/bin/rethinkdb --config-file /etc/rethinkdb/rdb0.conf
user=root

View File

@ -1,10 +0,0 @@
base:
'*':
- common
- users
- nodejs
- redis
- nginx
- rethinkdb
- realms
- supervisor

View File

@ -1,19 +0,0 @@
deploy:
user.present:
- shell: /bin/bash
- home: /home/deploy
- fullname: Deploy
sudo:
pkg:
- installed
/etc/sudoes.d/mysudoers:
file:
- managed
- source: salt://users/mysudoers
- mode: 440
- user: root
- group: root
- require:
- pkg: sudo

View File

@ -1 +0,0 @@
deploy ALL=(ALL) NOPASSWD:ALL