replace abort(code) with proper exceptions and refactor request dispatch
This commit is contained in:
parent
2794734258
commit
74ab58167e
@ -32,7 +32,6 @@ dist = pkg_resources.get_distribution("isso")
|
|||||||
|
|
||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
import socket
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from os.path import dirname, join
|
from os.path import dirname, join
|
||||||
@ -47,8 +46,7 @@ import misaka
|
|||||||
from itsdangerous import URLSafeTimedSerializer
|
from itsdangerous import URLSafeTimedSerializer
|
||||||
|
|
||||||
from werkzeug.routing import Map, Rule
|
from werkzeug.routing import Map, Rule
|
||||||
from werkzeug.wrappers import Response, Request
|
from werkzeug.exceptions import HTTPException, InternalServerError
|
||||||
from werkzeug.exceptions import HTTPException, NotFound, MethodNotAllowed
|
|
||||||
|
|
||||||
from werkzeug.wsgi import SharedDataMiddleware
|
from werkzeug.wsgi import SharedDataMiddleware
|
||||||
from werkzeug.serving import run_simple
|
from werkzeug.serving import run_simple
|
||||||
@ -56,7 +54,7 @@ from werkzeug.contrib.fixers import ProxyFix
|
|||||||
|
|
||||||
from isso import db, migrate, views, wsgi
|
from isso import db, migrate, views, wsgi
|
||||||
from isso.core import ThreadedMixin, uWSGIMixin, Config
|
from isso.core import ThreadedMixin, uWSGIMixin, Config
|
||||||
from isso.utils import parse, http
|
from isso.utils import parse, http, JSONRequest
|
||||||
from isso.views import comment
|
from isso.views import comment
|
||||||
|
|
||||||
logging.getLogger('werkzeug').setLevel(logging.ERROR)
|
logging.getLogger('werkzeug').setLevel(logging.ERROR)
|
||||||
@ -67,25 +65,23 @@ logging.basicConfig(
|
|||||||
logger = logging.getLogger("isso")
|
logger = logging.getLogger("isso")
|
||||||
|
|
||||||
|
|
||||||
rules = Map([
|
|
||||||
Rule('/new', methods=['POST'], endpoint=views.comment.new),
|
|
||||||
|
|
||||||
Rule('/id/<int:id>', methods=['GET', 'PUT', 'DELETE'], endpoint=views.comment.single),
|
|
||||||
Rule('/id/<int:id>/like', methods=['POST'], endpoint=views.comment.like),
|
|
||||||
Rule('/id/<int:id>/dislike', methods=['POST'], endpoint=views.comment.dislike),
|
|
||||||
|
|
||||||
Rule('/', methods=['GET'], endpoint=views.comment.fetch),
|
|
||||||
Rule('/count', methods=['GET'], endpoint=views.comment.count),
|
|
||||||
Rule('/delete/<string:auth>', endpoint=views.comment.delete),
|
|
||||||
Rule('/activate/<string:auth>', endpoint=views.comment.activate),
|
|
||||||
|
|
||||||
Rule('/check-ip', endpoint=views.comment.checkip)
|
|
||||||
])
|
|
||||||
|
|
||||||
|
|
||||||
class Isso(object):
|
class Isso(object):
|
||||||
|
|
||||||
salt = b"Eech7co8Ohloopo9Ol6baimi"
|
salt = b"Eech7co8Ohloopo9Ol6baimi"
|
||||||
|
urls = Map([
|
||||||
|
Rule('/new', methods=['POST'], endpoint=views.comment.new),
|
||||||
|
|
||||||
|
Rule('/id/<int:id>', methods=['GET', 'PUT', 'DELETE'], endpoint=views.comment.single),
|
||||||
|
Rule('/id/<int:id>/like', methods=['POST'], endpoint=views.comment.like),
|
||||||
|
Rule('/id/<int:id>/dislike', methods=['POST'], endpoint=views.comment.dislike),
|
||||||
|
|
||||||
|
Rule('/', methods=['GET'], endpoint=views.comment.fetch),
|
||||||
|
Rule('/count', methods=['GET'], endpoint=views.comment.count),
|
||||||
|
Rule('/delete/<string:auth>', endpoint=views.comment.delete),
|
||||||
|
Rule('/activate/<string:auth>', endpoint=views.comment.activate),
|
||||||
|
|
||||||
|
Rule('/check-ip', endpoint=views.comment.checkip)
|
||||||
|
])
|
||||||
|
|
||||||
def __init__(self, conf):
|
def __init__(self, conf):
|
||||||
|
|
||||||
@ -102,30 +98,27 @@ class Isso(object):
|
|||||||
return self.signer.loads(obj, max_age=max_age or self.conf.getint('general', 'max-age'))
|
return self.signer.loads(obj, max_age=max_age or self.conf.getint('general', 'max-age'))
|
||||||
|
|
||||||
def markdown(self, text):
|
def markdown(self, text):
|
||||||
return misaka.html(text, extensions=misaka.EXT_STRIKETHROUGH \
|
return misaka.html(text, extensions=misaka.EXT_STRIKETHROUGH
|
||||||
| misaka.EXT_SUPERSCRIPT | misaka.EXT_AUTOLINK \
|
| misaka.EXT_SUPERSCRIPT | misaka.EXT_AUTOLINK
|
||||||
| misaka.HTML_SKIP_HTML | misaka.HTML_SKIP_IMAGES | misaka.HTML_SAFELINK)
|
| misaka.HTML_SKIP_HTML | misaka.HTML_SKIP_IMAGES | misaka.HTML_SAFELINK)
|
||||||
|
|
||||||
def dispatch(self, request, start_response):
|
def dispatch(self, request):
|
||||||
adapter = rules.bind_to_environ(request.environ)
|
adapter = Isso.urls.bind_to_environ(request.environ)
|
||||||
try:
|
try:
|
||||||
handler, values = adapter.match()
|
handler, values = adapter.match()
|
||||||
return handler(self, request.environ, request, **values)
|
|
||||||
except NotFound:
|
|
||||||
return Response('Not Found', 404)
|
|
||||||
except MethodNotAllowed:
|
|
||||||
return Response("Yup.", 200)
|
|
||||||
except HTTPException as e:
|
except HTTPException as e:
|
||||||
return e
|
return e
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
response = handler(self, request.environ, request, **values)
|
||||||
|
except HTTPException as e:
|
||||||
|
return e
|
||||||
|
except Exception:
|
||||||
|
logger.exception("%s %s", request.method, request.environ["PATH_INFO"])
|
||||||
|
return InternalServerError()
|
||||||
|
|
||||||
def wsgi_app(self, environ, start_response):
|
|
||||||
|
|
||||||
response = self.dispatch(Request(environ), start_response)
|
|
||||||
|
|
||||||
# add CORS header
|
|
||||||
if hasattr(response, 'headers') and 'HTTP_ORIGN' in environ:
|
|
||||||
for host in self.conf.getiter('general', 'host'):
|
for host in self.conf.getiter('general', 'host'):
|
||||||
if environ["HTTP_ORIGIN"] == host.rstrip("/"):
|
if request.environ.get("HTTP_ORIGIN", None) == host.rstrip("/"):
|
||||||
origin = host.rstrip("/")
|
origin = host.rstrip("/")
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
@ -137,6 +130,11 @@ class Isso(object):
|
|||||||
hdrs["Access-Control-Allow-Credentials"] = "true"
|
hdrs["Access-Control-Allow-Credentials"] = "true"
|
||||||
hdrs["Access-Control-Allow-Methods"] = "GET, POST, PUT, DELETE"
|
hdrs["Access-Control-Allow-Methods"] = "GET, POST, PUT, DELETE"
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
|
def wsgi_app(self, environ, start_response):
|
||||||
|
|
||||||
|
response = self.dispatch(JSONRequest(environ))
|
||||||
return response(environ, start_response)
|
return response(environ, start_response)
|
||||||
|
|
||||||
def __call__(self, environ, start_response):
|
def __call__(self, environ, start_response):
|
||||||
|
@ -2,11 +2,15 @@
|
|||||||
|
|
||||||
from __future__ import division
|
from __future__ import division
|
||||||
|
|
||||||
|
import json
|
||||||
import random
|
import random
|
||||||
import hashlib
|
import hashlib
|
||||||
|
|
||||||
from string import ascii_letters, digits
|
from string import ascii_letters, digits
|
||||||
|
|
||||||
|
from werkzeug.wrappers import Request
|
||||||
|
from werkzeug.exceptions import BadRequest
|
||||||
|
|
||||||
import ipaddress
|
import ipaddress
|
||||||
|
|
||||||
|
|
||||||
@ -83,3 +87,12 @@ class Bloomfilter:
|
|||||||
|
|
||||||
def __len__(self):
|
def __len__(self):
|
||||||
return self.elements
|
return self.elements
|
||||||
|
|
||||||
|
|
||||||
|
class JSONRequest(Request):
|
||||||
|
|
||||||
|
def get_json(self):
|
||||||
|
try:
|
||||||
|
return json.loads(self.get_data().decode('utf-8'))
|
||||||
|
except ValueError:
|
||||||
|
raise BadRequest('Unable to read JSON request')
|
||||||
|
@ -10,7 +10,7 @@ import sqlite3
|
|||||||
from itsdangerous import SignatureExpired, BadSignature
|
from itsdangerous import SignatureExpired, BadSignature
|
||||||
|
|
||||||
from werkzeug.wrappers import Response
|
from werkzeug.wrappers import Response
|
||||||
from werkzeug.exceptions import abort, BadRequest
|
from werkzeug.exceptions import BadRequest, Forbidden, NotFound
|
||||||
|
|
||||||
from isso.compat import text_type as str
|
from isso.compat import text_type as str
|
||||||
|
|
||||||
@ -49,19 +49,16 @@ class requires:
|
|||||||
@requires(str, 'uri')
|
@requires(str, 'uri')
|
||||||
def new(app, environ, request, uri):
|
def new(app, environ, request, uri):
|
||||||
|
|
||||||
try:
|
data = request.get_json()
|
||||||
data = json.loads(request.get_data().decode('utf-8'))
|
|
||||||
except ValueError:
|
|
||||||
return Response("No JSON object could be decoded", 400)
|
|
||||||
|
|
||||||
for field in set(data.keys()) - set(['text', 'author', 'website', 'email', 'parent']):
|
for field in set(data.keys()) - set(['text', 'author', 'website', 'email', 'parent']):
|
||||||
data.pop(field)
|
data.pop(field)
|
||||||
|
|
||||||
if not data.get("text"):
|
if not data.get("text"):
|
||||||
return Response("No text given.", 400)
|
raise BadRequest("no text given")
|
||||||
|
|
||||||
if "id" in data and not isinstance(data["id"], int):
|
if "id" in data and not isinstance(data["id"], int):
|
||||||
return Response("Parent ID must be an integer.")
|
raise BadRequest("parent id must be an integer")
|
||||||
|
|
||||||
for field in ("author", "email"):
|
for field in ("author", "email"):
|
||||||
if data.get(field):
|
if data.get(field):
|
||||||
@ -88,11 +85,8 @@ def new(app, environ, request, uri):
|
|||||||
try:
|
try:
|
||||||
with app.lock:
|
with app.lock:
|
||||||
rv = app.db.comments.add(uri, data)
|
rv = app.db.comments.add(uri, data)
|
||||||
except sqlite3.Error:
|
|
||||||
logging.exception('uncaught SQLite3 exception')
|
|
||||||
abort(400)
|
|
||||||
except db.IssoDBException:
|
except db.IssoDBException:
|
||||||
abort(403)
|
raise Forbidden
|
||||||
|
|
||||||
host = list(app.conf.getiter('general', 'host'))[0].rstrip("/")
|
host = list(app.conf.getiter('general', 'host'))[0].rstrip("/")
|
||||||
href = host + uri + "#isso-%i" % rv["id"]
|
href = host + uri + "#isso-%i" % rv["id"]
|
||||||
@ -130,7 +124,7 @@ def single(app, environ, request, id):
|
|||||||
if request.method == 'GET':
|
if request.method == 'GET':
|
||||||
rv = app.db.comments.get(id)
|
rv = app.db.comments.get(id)
|
||||||
if rv is None:
|
if rv is None:
|
||||||
abort(404)
|
raise NotFound
|
||||||
|
|
||||||
for key in set(rv.keys()) - FIELDS:
|
for key in set(rv.keys()) - FIELDS:
|
||||||
rv.pop(key)
|
rv.pop(key)
|
||||||
@ -146,23 +140,20 @@ def single(app, environ, request, id):
|
|||||||
try:
|
try:
|
||||||
rv = app.unsign(request.cookies.get('admin', ''))
|
rv = app.unsign(request.cookies.get('admin', ''))
|
||||||
except (SignatureExpired, BadSignature):
|
except (SignatureExpired, BadSignature):
|
||||||
abort(403)
|
raise Forbidden
|
||||||
|
|
||||||
if rv[0] != id:
|
if rv[0] != id:
|
||||||
abort(403)
|
raise Forbidden
|
||||||
|
|
||||||
# verify checksum, mallory might skip cookie deletion when he deletes a comment
|
# verify checksum, mallory might skip cookie deletion when he deletes a comment
|
||||||
if rv[1] != hashlib.md5(app.db.comments.get(id)["text"].encode('utf-8')).hexdigest():
|
if rv[1] != hashlib.md5(app.db.comments.get(id)["text"].encode('utf-8')).hexdigest():
|
||||||
abort(403)
|
raise Forbidden
|
||||||
|
|
||||||
if request.method == 'PUT':
|
if request.method == 'PUT':
|
||||||
try:
|
data = request.get_json()
|
||||||
data = json.loads(request.get_data().decode('utf-8'))
|
|
||||||
except ValueError:
|
|
||||||
return Response("No JSON object could be decoded", 400)
|
|
||||||
|
|
||||||
if data.get("text") is not None and len(data['text']) < 3:
|
if data.get("text") is not None and len(data['text']) < 3:
|
||||||
return Response("No text given.", 400)
|
raise BadRequest("no text given")
|
||||||
|
|
||||||
for key in set(data.keys()) - set(["text", "author", "website"]):
|
for key in set(data.keys()) - set(["text", "author", "website"]):
|
||||||
data.pop(key)
|
data.pop(key)
|
||||||
@ -203,7 +194,7 @@ def fetch(app, environ, request, uri):
|
|||||||
|
|
||||||
rv = list(app.db.comments.fetch(uri))
|
rv = list(app.db.comments.fetch(uri))
|
||||||
if not rv:
|
if not rv:
|
||||||
abort(404)
|
raise NotFound
|
||||||
|
|
||||||
for item in rv:
|
for item in rv:
|
||||||
|
|
||||||
@ -237,7 +228,7 @@ def count(app, environ, request, uri):
|
|||||||
rv = app.db.comments.count(uri)[0]
|
rv = app.db.comments.count(uri)[0]
|
||||||
|
|
||||||
if rv == 0:
|
if rv == 0:
|
||||||
abort(404)
|
raise NotFound
|
||||||
|
|
||||||
return Response(json.dumps(rv), 200, content_type='application/json')
|
return Response(json.dumps(rv), 200, content_type='application/json')
|
||||||
|
|
||||||
@ -247,7 +238,7 @@ def activate(app, environ, request, auth):
|
|||||||
try:
|
try:
|
||||||
id = app.unsign(auth, max_age=2**32)
|
id = app.unsign(auth, max_age=2**32)
|
||||||
except (BadSignature, SignatureExpired):
|
except (BadSignature, SignatureExpired):
|
||||||
abort(403)
|
raise Forbidden
|
||||||
|
|
||||||
with app.lock:
|
with app.lock:
|
||||||
app.db.comments.activate(id)
|
app.db.comments.activate(id)
|
||||||
@ -260,7 +251,7 @@ def delete(app, environ, request, auth):
|
|||||||
try:
|
try:
|
||||||
id = app.unsign(auth, max_age=2**32)
|
id = app.unsign(auth, max_age=2**32)
|
||||||
except (BadSignature, SignatureExpired):
|
except (BadSignature, SignatureExpired):
|
||||||
abort(403)
|
raise Forbidden
|
||||||
|
|
||||||
with app.lock:
|
with app.lock:
|
||||||
app.db.comments.delete(id)
|
app.db.comments.delete(id)
|
||||||
|
Loading…
Reference in New Issue
Block a user