Compare commits

...

1 Commits

Author SHA1 Message Date
038efb745a add: etag support 2024-12-09 08:00:25 +00:00
5 changed files with 40 additions and 6 deletions

View File

@@ -1,11 +1,10 @@
#api/__init__.py
import base64 import base64
import os import os
from functools import wraps from functools import wraps
from cryptography import x509 from cryptography import x509
from cryptography.hazmat.primitives import serialization from cryptography.hazmat.primitives import serialization
from flask import jsonify, Blueprint, request from flask import jsonify, Blueprint, request, make_response
from flask_limiter import Limiter from flask_limiter import Limiter
from flask_limiter.util import get_remote_address from flask_limiter.util import get_remote_address
from jwt import decode, ExpiredSignatureError, InvalidTokenError, get_unverified_header from jwt import decode, ExpiredSignatureError, InvalidTokenError, get_unverified_header
@@ -14,6 +13,11 @@ import requests
from threading import Lock from threading import Lock
import env_provider import env_provider
import hashlib
import json
_public_key_cache = {} _public_key_cache = {}
_lock = Lock() _lock = Lock()
@@ -135,3 +139,24 @@ def register_blueprints(app):
app.register_blueprint(bp) app.register_blueprint(bp)
def generate_etag(data):
serialized = json.dumps(data, sort_keys=True).encode('utf-8')
return hashlib.md5(serialized).hexdigest()
def etag_response(f):
@wraps(f)
def decorator(*args, **kwargs):
response = f(*args, **kwargs)
if response[1] in (200, 201):
if isinstance(response[0], (dict, list)):
etag = generate_etag(response[0])
if_none_match = request.headers.get("if_none_match")
if if_none_match == etag:
return jsonify({}), 200
resp = make_response(response[0], response[1])
resp.headers["ETag"] = etag
return resp
return response
return decorator

View File

@@ -1,6 +1,6 @@
from flask import Blueprint, jsonify, request from flask import Blueprint, jsonify, request
from api import require_auth, rate_limits from api import require_auth, rate_limits, etag_response
import re import re
config_bp = Blueprint('config', __name__, url_prefix='/api/config') config_bp = Blueprint('config', __name__, url_prefix='/api/config')
@@ -10,6 +10,7 @@ def is_valid_rate_limit(limit):
return bool(RATE_LIMIT_REGEX.match(limit)) return bool(RATE_LIMIT_REGEX.match(limit))
@config_bp.route('/limits', methods=['GET']) @config_bp.route('/limits', methods=['GET'])
@require_auth(roles=['admin']) @require_auth(roles=['admin'])
@etag_response
def limits(): def limits():
return jsonify(rate_limits), 200 return jsonify(rate_limits), 200

View File

@@ -2,7 +2,7 @@
from flask import Blueprint, request, jsonify from flask import Blueprint, request, jsonify
import api import api
from api import require_auth from api import require_auth, etag_response
from contexts.RequestContext import RequestContext from contexts.RequestContext import RequestContext
from db import get_db from db import get_db
from db.models.Markdown import Markdown from db.models.Markdown import Markdown
@@ -14,6 +14,7 @@ markdown_bp = Blueprint('markdown', __name__, url_prefix='/api/markdown')
@markdown_bp.route('/', methods=['GET']) @markdown_bp.route('/', methods=['GET'])
@limiter.limit(api.get_rate_limit) @limiter.limit(api.get_rate_limit)
@etag_response
def get_markdowns(): def get_markdowns():
with get_db() as session: with get_db() as session:
mds = session.query(Markdown).all() mds = session.query(Markdown).all()
@@ -21,12 +22,14 @@ def get_markdowns():
@markdown_bp.route('/by_path/<int:path_id>', methods=['GET']) @markdown_bp.route('/by_path/<int:path_id>', methods=['GET'])
@limiter.limit(api.get_rate_limit) @limiter.limit(api.get_rate_limit)
@etag_response
def get_markdowns_by_path(path_id): def get_markdowns_by_path(path_id):
with get_db() as session: with get_db() as session:
markdowns = session.query(Markdown).filter(Markdown.path_id == path_id).all() markdowns = session.query(Markdown).filter(Markdown.path_id == path_id).all()
return jsonify([md.to_dict() for md in markdowns]), 200 return jsonify([md.to_dict() for md in markdowns]), 200
@markdown_bp.route('/get_index/<int:path_id>', methods=['GET']) @markdown_bp.route('/get_index/<int:path_id>', methods=['GET'])
@limiter.limit(api.get_rate_limit) @limiter.limit(api.get_rate_limit)
@etag_response
def get_index(path_id): def get_index(path_id):
with get_db() as session: with get_db() as session:
markdown = session.query(Markdown).filter(Markdown.path_id == path_id).filter(Markdown.title == "index").first() markdown = session.query(Markdown).filter(Markdown.path_id == path_id).filter(Markdown.title == "index").first()
@@ -38,6 +41,7 @@ def get_index(path_id):
@markdown_bp.route('/<int:markdown_id>', methods=['GET']) @markdown_bp.route('/<int:markdown_id>', methods=['GET'])
@limiter.limit(api.get_rate_limit) @limiter.limit(api.get_rate_limit)
@etag_response
def get_markdown(markdown_id): def get_markdown(markdown_id):
with get_db() as session: with get_db() as session:
markdown = session.query(Markdown).get(markdown_id) markdown = session.query(Markdown).get(markdown_id)

View File

@@ -1,7 +1,7 @@
from flask import Blueprint, request, jsonify from flask import Blueprint, request, jsonify
import api import api
from api import require_auth from api import require_auth, etag_response
from db import get_db from db import get_db
from db.models.Markdown import Markdown from db.models.Markdown import Markdown
from db.models.Path import Path from db.models.Path import Path
@@ -13,6 +13,7 @@ path_bp = Blueprint('path', __name__, url_prefix='/api/path')
@path_bp.route('/', methods=['GET']) @path_bp.route('/', methods=['GET'])
@limiter.limit(api.get_rate_limit) @limiter.limit(api.get_rate_limit)
@etag_response
def get_root_paths(): def get_root_paths():
with get_db() as session: with get_db() as session:
paths = session.query(Path).filter(Path.parent_id == 1) paths = session.query(Path).filter(Path.parent_id == 1)
@@ -20,6 +21,7 @@ def get_root_paths():
@path_bp.route('/<int:path_id>', methods=['GET']) @path_bp.route('/<int:path_id>', methods=['GET'])
@limiter.limit(api.get_rate_limit) @limiter.limit(api.get_rate_limit)
@etag_response
def get_path(path_id): def get_path(path_id):
with get_db() as session: with get_db() as session:
path = session.query(Path).get(path_id) path = session.query(Path).get(path_id)
@@ -29,6 +31,7 @@ def get_path(path_id):
@path_bp.route('/parent/<int:parent_id>', methods=['GET']) @path_bp.route('/parent/<int:parent_id>', methods=['GET'])
@limiter.limit(api.get_rate_limit) @limiter.limit(api.get_rate_limit)
@etag_response
def get_path_by_parent(parent_id): def get_path_by_parent(parent_id):
with get_db() as session: with get_db() as session:
paths = session.query(Path).filter(Path.parent_id == parent_id).all() paths = session.query(Path).filter(Path.parent_id == parent_id).all()

View File

@@ -4,12 +4,13 @@ from flask import Blueprint, jsonify, request
from contexts.RequestContext import RequestContext from contexts.RequestContext import RequestContext
from db import get_db from db import get_db
from db.models.Resource import Resource from db.models.Resource import Resource
from api import require_auth, limiter from api import require_auth, limiter, etag_response
import logging import logging
resource_bp = Blueprint('resource', __name__, url_prefix='/api/resource') resource_bp = Blueprint('resource', __name__, url_prefix='/api/resource')
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@resource_bp.route('/<identifier>', methods=['GET']) @resource_bp.route('/<identifier>', methods=['GET'])
@limiter.limit(api.get_rate_limit) @limiter.limit(api.get_rate_limit)
def get_resource(identifier): def get_resource(identifier):
with get_db() as session: with get_db() as session:
resource = session.query(Resource).get(identifier) resource = session.query(Resource).get(identifier)