lint: reformat python code with black
This commit is contained in:
parent
331beb01b4
commit
a406a7974b
88 changed files with 2579 additions and 1608 deletions
|
@ -5,34 +5,38 @@ from werkzeug.exceptions import HTTPException
|
|||
from app.api.onion import api_onion
|
||||
from app.api.web import api_web
|
||||
|
||||
api = Blueprint('api', __name__)
|
||||
api.register_blueprint(api_onion, url_prefix='/onion')
|
||||
api.register_blueprint(api_web, url_prefix='/web')
|
||||
api = Blueprint("api", __name__)
|
||||
api.register_blueprint(api_onion, url_prefix="/onion")
|
||||
api.register_blueprint(api_web, url_prefix="/web")
|
||||
|
||||
|
||||
@api.errorhandler(400)
|
||||
def bad_request(error: HTTPException) -> ResponseReturnValue:
|
||||
response = jsonify({'error': 'Bad Request', 'message': error.description})
|
||||
response = jsonify({"error": "Bad Request", "message": error.description})
|
||||
response.status_code = 400
|
||||
return response
|
||||
|
||||
|
||||
@api.errorhandler(401)
|
||||
def unauthorized(error: HTTPException) -> ResponseReturnValue:
|
||||
response = jsonify({'error': 'Unauthorized', 'message': error.description})
|
||||
response = jsonify({"error": "Unauthorized", "message": error.description})
|
||||
response.status_code = 401
|
||||
return response
|
||||
|
||||
|
||||
@api.errorhandler(404)
|
||||
def not_found(_: HTTPException) -> ResponseReturnValue:
|
||||
response = jsonify({'error': 'Not found', 'message': 'Resource could not be found.'})
|
||||
response = jsonify(
|
||||
{"error": "Not found", "message": "Resource could not be found."}
|
||||
)
|
||||
response.status_code = 404
|
||||
return response
|
||||
|
||||
|
||||
@api.errorhandler(500)
|
||||
def internal_server_error(_: HTTPException) -> ResponseReturnValue:
|
||||
response = jsonify({'error': 'Internal Server Error', 'message': 'An unexpected error occurred.'})
|
||||
response = jsonify(
|
||||
{"error": "Internal Server Error", "message": "An unexpected error occurred."}
|
||||
)
|
||||
response.status_code = 500
|
||||
return response
|
||||
|
|
128
app/api/onion.py
128
app/api/onion.py
|
@ -7,31 +7,37 @@ from flask import Blueprint, abort, jsonify, request
|
|||
from flask.typing import ResponseReturnValue
|
||||
from sqlalchemy import exc
|
||||
|
||||
from app.api.util import (DOMAIN_NAME_REGEX, MAX_ALLOWED_ITEMS,
|
||||
MAX_DOMAIN_NAME_LENGTH, ListFilter,
|
||||
get_single_resource, list_resources,
|
||||
validate_description)
|
||||
from app.api.util import (
|
||||
DOMAIN_NAME_REGEX,
|
||||
MAX_ALLOWED_ITEMS,
|
||||
MAX_DOMAIN_NAME_LENGTH,
|
||||
ListFilter,
|
||||
get_single_resource,
|
||||
list_resources,
|
||||
validate_description,
|
||||
)
|
||||
from app.extensions import db
|
||||
from app.models.base import Group
|
||||
from app.models.onions import Onion
|
||||
from app.util.onion import decode_onion_keys, onion_hostname
|
||||
from app.util.x509 import validate_tls_keys
|
||||
|
||||
api_onion = Blueprint('api_onion', __name__)
|
||||
api_onion = Blueprint("api_onion", __name__)
|
||||
|
||||
|
||||
@api_onion.route('/onion', methods=['GET'])
|
||||
@api_onion.route("/onion", methods=["GET"])
|
||||
def list_onions() -> ResponseReturnValue:
|
||||
domain_name_filter = request.args.get('DomainName')
|
||||
group_id_filter = request.args.get('GroupId')
|
||||
domain_name_filter = request.args.get("DomainName")
|
||||
group_id_filter = request.args.get("GroupId")
|
||||
|
||||
filters: List[ListFilter] = [
|
||||
(Onion.destroyed.is_(None))
|
||||
]
|
||||
filters: List[ListFilter] = [(Onion.destroyed.is_(None))]
|
||||
|
||||
if domain_name_filter:
|
||||
if len(domain_name_filter) > MAX_DOMAIN_NAME_LENGTH:
|
||||
abort(400, description=f"DomainName cannot exceed {MAX_DOMAIN_NAME_LENGTH} characters.")
|
||||
abort(
|
||||
400,
|
||||
description=f"DomainName cannot exceed {MAX_DOMAIN_NAME_LENGTH} characters.",
|
||||
)
|
||||
if not DOMAIN_NAME_REGEX.match(domain_name_filter):
|
||||
abort(400, description="DomainName contains invalid characters.")
|
||||
filters.append(Onion.domain_name.ilike(f"%{domain_name_filter}%"))
|
||||
|
@ -46,9 +52,9 @@ def list_onions() -> ResponseReturnValue:
|
|||
Onion,
|
||||
lambda onion: onion.to_dict(),
|
||||
filters=filters,
|
||||
resource_name='OnionsList',
|
||||
resource_name="OnionsList",
|
||||
max_allowed_items=MAX_ALLOWED_ITEMS,
|
||||
protective_marking='amber',
|
||||
protective_marking="amber",
|
||||
)
|
||||
|
||||
|
||||
|
@ -71,13 +77,26 @@ def create_onion() -> ResponseReturnValue:
|
|||
abort(400)
|
||||
|
||||
errors = []
|
||||
for field in ["DomainName", "Description", "OnionPrivateKey", "OnionPublicKey", "GroupId", "TlsPrivateKey",
|
||||
"TlsCertificate"]:
|
||||
for field in [
|
||||
"DomainName",
|
||||
"Description",
|
||||
"OnionPrivateKey",
|
||||
"OnionPublicKey",
|
||||
"GroupId",
|
||||
"TlsPrivateKey",
|
||||
"TlsCertificate",
|
||||
]:
|
||||
if not data.get(field):
|
||||
errors.append({"Error": f"{field}_missing", "Message": f"Missing required field: {field}"})
|
||||
errors.append(
|
||||
{
|
||||
"Error": f"{field}_missing",
|
||||
"Message": f"Missing required field: {field}",
|
||||
}
|
||||
)
|
||||
|
||||
onion_private_key, onion_public_key, onion_errors = decode_onion_keys(data["OnionPrivateKey"],
|
||||
data["OnionPublicKey"])
|
||||
onion_private_key, onion_public_key, onion_errors = decode_onion_keys(
|
||||
data["OnionPrivateKey"], data["OnionPublicKey"]
|
||||
)
|
||||
if onion_errors:
|
||||
errors.extend(onion_errors)
|
||||
|
||||
|
@ -85,23 +104,35 @@ def create_onion() -> ResponseReturnValue:
|
|||
return jsonify({"Errors": errors}), 400
|
||||
|
||||
if onion_private_key:
|
||||
existing_onion = db.session.query(Onion).where(
|
||||
Onion.onion_private_key == onion_private_key,
|
||||
Onion.destroyed.is_(None),
|
||||
).first()
|
||||
existing_onion = (
|
||||
db.session.query(Onion)
|
||||
.where(
|
||||
Onion.onion_private_key == onion_private_key,
|
||||
Onion.destroyed.is_(None),
|
||||
)
|
||||
.first()
|
||||
)
|
||||
if existing_onion:
|
||||
errors.append(
|
||||
{"Error": "duplicate_onion_key", "Message": "An onion service with this private key already exists."})
|
||||
{
|
||||
"Error": "duplicate_onion_key",
|
||||
"Message": "An onion service with this private key already exists.",
|
||||
}
|
||||
)
|
||||
|
||||
if "GroupId" in data:
|
||||
group = Group.query.get(data["GroupId"])
|
||||
if not group:
|
||||
errors.append({"Error": "group_id_not_found", "Message": "Invalid group ID."})
|
||||
errors.append(
|
||||
{"Error": "group_id_not_found", "Message": "Invalid group ID."}
|
||||
)
|
||||
|
||||
chain, san_list, tls_errors = validate_tls_keys(
|
||||
data["TlsPrivateKey"], data["TlsCertificate"], data.get("SkipChainVerification"),
|
||||
data["TlsPrivateKey"],
|
||||
data["TlsCertificate"],
|
||||
data.get("SkipChainVerification"),
|
||||
data.get("SkipNameVerification"),
|
||||
f"{onion_hostname(onion_public_key)}.onion"
|
||||
f"{onion_hostname(onion_public_key)}.onion",
|
||||
)
|
||||
|
||||
if tls_errors:
|
||||
|
@ -123,15 +154,21 @@ def create_onion() -> ResponseReturnValue:
|
|||
added=datetime.now(timezone.utc),
|
||||
updated=datetime.now(timezone.utc),
|
||||
cert_expiry=cert_expiry_date,
|
||||
cert_sans=",".join(san_list)
|
||||
cert_sans=",".join(san_list),
|
||||
)
|
||||
|
||||
try:
|
||||
db.session.add(onion)
|
||||
db.session.commit()
|
||||
return jsonify({"Message": "Onion service created successfully.", "Id": onion.id}), 201
|
||||
return (
|
||||
jsonify({"Message": "Onion service created successfully.", "Id": onion.id}),
|
||||
201,
|
||||
)
|
||||
except exc.SQLAlchemyError as e:
|
||||
return jsonify({"Errors": [{"Error": "database_error", "Message": str(e)}]}), 500
|
||||
return (
|
||||
jsonify({"Errors": [{"Error": "database_error", "Message": str(e)}]}),
|
||||
500,
|
||||
)
|
||||
|
||||
|
||||
class UpdateOnionRequest(TypedDict):
|
||||
|
@ -152,8 +189,19 @@ def update_onion(onion_id: int) -> ResponseReturnValue:
|
|||
|
||||
onion = Onion.query.get(onion_id)
|
||||
if not onion:
|
||||
return jsonify(
|
||||
{"Errors": [{"Error": "onion_not_found", "Message": f"No Onion service found with ID {onion_id}"}]}), 404
|
||||
return (
|
||||
jsonify(
|
||||
{
|
||||
"Errors": [
|
||||
{
|
||||
"Error": "onion_not_found",
|
||||
"Message": f"No Onion service found with ID {onion_id}",
|
||||
}
|
||||
]
|
||||
}
|
||||
),
|
||||
404,
|
||||
)
|
||||
|
||||
if "Description" in data:
|
||||
description = data["Description"]
|
||||
|
@ -161,7 +209,12 @@ def update_onion(onion_id: int) -> ResponseReturnValue:
|
|||
if validate_description(description):
|
||||
onion.description = description
|
||||
else:
|
||||
errors.append({"Error": "description_error", "Message": "Description field is invalid"})
|
||||
errors.append(
|
||||
{
|
||||
"Error": "description_error",
|
||||
"Message": "Description field is invalid",
|
||||
}
|
||||
)
|
||||
|
||||
tls_private_key_pem: Optional[str] = None
|
||||
tls_certificate_pem: Optional[str] = None
|
||||
|
@ -176,7 +229,9 @@ def update_onion(onion_id: int) -> ResponseReturnValue:
|
|||
tls_private_key_pem = onion.tls_private_key.decode("utf-8")
|
||||
|
||||
chain, san_list, tls_errors = validate_tls_keys(
|
||||
tls_private_key_pem, tls_certificate_pem, data.get("SkipChainVerification", False),
|
||||
tls_private_key_pem,
|
||||
tls_certificate_pem,
|
||||
data.get("SkipChainVerification", False),
|
||||
data.get("SkipNameVerification", False),
|
||||
f"{onion_hostname(onion.onion_public_key)}.onion",
|
||||
)
|
||||
|
@ -200,7 +255,10 @@ def update_onion(onion_id: int) -> ResponseReturnValue:
|
|||
db.session.commit()
|
||||
return jsonify({"Message": "Onion service updated successfully."}), 200
|
||||
except exc.SQLAlchemyError as e:
|
||||
return jsonify({"Errors": [{"Error": "database_error", "Message": str(e)}]}), 500
|
||||
return (
|
||||
jsonify({"Errors": [{"Error": "database_error", "Message": str(e)}]}),
|
||||
500,
|
||||
)
|
||||
|
||||
|
||||
@api_onion.route("/onion/<int:onion_id>", methods=["GET"])
|
||||
|
|
|
@ -12,7 +12,7 @@ from app.extensions import db
|
|||
|
||||
logger = logging.getLogger(__name__)
|
||||
MAX_DOMAIN_NAME_LENGTH = 255
|
||||
DOMAIN_NAME_REGEX = re.compile(r'^[a-zA-Z0-9.\-]*$')
|
||||
DOMAIN_NAME_REGEX = re.compile(r"^[a-zA-Z0-9.\-]*$")
|
||||
MAX_ALLOWED_ITEMS = 100
|
||||
ListFilter = Union[BinaryExpression[Any], ColumnElement[Any]]
|
||||
|
||||
|
@ -24,7 +24,10 @@ def validate_max_items(max_items_str: str, max_allowed: int) -> int:
|
|||
raise ValueError()
|
||||
return max_items
|
||||
except ValueError:
|
||||
abort(400, description=f"MaxItems must be a positive integer not exceeding {max_allowed}.")
|
||||
abort(
|
||||
400,
|
||||
description=f"MaxItems must be a positive integer not exceeding {max_allowed}.",
|
||||
)
|
||||
|
||||
|
||||
def validate_marker(marker_str: str) -> int:
|
||||
|
@ -47,21 +50,22 @@ TlpMarkings = Union[
|
|||
|
||||
|
||||
def list_resources( # pylint: disable=too-many-arguments,too-many-locals
|
||||
model: Type[Any],
|
||||
serialize_func: Callable[[Any], Dict[str, Any]],
|
||||
*,
|
||||
filters: Optional[List[ListFilter]] = None,
|
||||
order_by: Optional[ColumnElement[Any]] = None,
|
||||
resource_name: str = 'ResourceList',
|
||||
max_items_param: str = 'MaxItems',
|
||||
marker_param: str = 'Marker',
|
||||
max_allowed_items: int = 100,
|
||||
protective_marking: TlpMarkings = 'default',
|
||||
model: Type[Any],
|
||||
serialize_func: Callable[[Any], Dict[str, Any]],
|
||||
*,
|
||||
filters: Optional[List[ListFilter]] = None,
|
||||
order_by: Optional[ColumnElement[Any]] = None,
|
||||
resource_name: str = "ResourceList",
|
||||
max_items_param: str = "MaxItems",
|
||||
marker_param: str = "Marker",
|
||||
max_allowed_items: int = 100,
|
||||
protective_marking: TlpMarkings = "default",
|
||||
) -> ResponseReturnValue:
|
||||
try:
|
||||
marker = request.args.get(marker_param)
|
||||
max_items = validate_max_items(
|
||||
request.args.get(max_items_param, default='100'), max_allowed_items)
|
||||
request.args.get(max_items_param, default="100"), max_allowed_items
|
||||
)
|
||||
query = select(model)
|
||||
|
||||
if filters:
|
||||
|
@ -101,14 +105,21 @@ def list_resources( # pylint: disable=too-many-arguments,too-many-locals
|
|||
abort(500)
|
||||
|
||||
|
||||
def get_single_resource(model: Type[Any], id_: int, resource_name: str) -> ResponseReturnValue:
|
||||
def get_single_resource(
|
||||
model: Type[Any], id_: int, resource_name: str
|
||||
) -> ResponseReturnValue:
|
||||
try:
|
||||
resource = db.session.get(model, id_)
|
||||
if not resource:
|
||||
return jsonify({
|
||||
"Error": "resource_not_found",
|
||||
"Message": f"No {resource_name} found with ID {id_}"
|
||||
}), 404
|
||||
return (
|
||||
jsonify(
|
||||
{
|
||||
"Error": "resource_not_found",
|
||||
"Message": f"No {resource_name} found with ID {id_}",
|
||||
}
|
||||
),
|
||||
404,
|
||||
)
|
||||
return jsonify({resource_name: resource.to_dict()}), 200
|
||||
except Exception: # pylint: disable=broad-exception-caught
|
||||
logger.exception("An unexpected error occurred while retrieving the onion")
|
||||
|
|
|
@ -4,35 +4,43 @@ from typing import List
|
|||
from flask import Blueprint, abort, request
|
||||
from flask.typing import ResponseReturnValue
|
||||
|
||||
from app.api.util import (DOMAIN_NAME_REGEX, MAX_ALLOWED_ITEMS,
|
||||
MAX_DOMAIN_NAME_LENGTH, ListFilter, list_resources)
|
||||
from app.api.util import (
|
||||
DOMAIN_NAME_REGEX,
|
||||
MAX_ALLOWED_ITEMS,
|
||||
MAX_DOMAIN_NAME_LENGTH,
|
||||
ListFilter,
|
||||
list_resources,
|
||||
)
|
||||
from app.models.base import Group
|
||||
from app.models.mirrors import Origin, Proxy
|
||||
|
||||
api_web = Blueprint('web', __name__)
|
||||
api_web = Blueprint("web", __name__)
|
||||
|
||||
|
||||
@api_web.route('/group', methods=['GET'])
|
||||
@api_web.route("/group", methods=["GET"])
|
||||
def list_groups() -> ResponseReturnValue:
|
||||
return list_resources(
|
||||
Group,
|
||||
lambda group: group.to_dict(),
|
||||
resource_name='OriginGroupList',
|
||||
resource_name="OriginGroupList",
|
||||
max_allowed_items=MAX_ALLOWED_ITEMS,
|
||||
protective_marking='amber',
|
||||
protective_marking="amber",
|
||||
)
|
||||
|
||||
|
||||
@api_web.route('/origin', methods=['GET'])
|
||||
@api_web.route("/origin", methods=["GET"])
|
||||
def list_origins() -> ResponseReturnValue:
|
||||
domain_name_filter = request.args.get('DomainName')
|
||||
group_id_filter = request.args.get('GroupId')
|
||||
domain_name_filter = request.args.get("DomainName")
|
||||
group_id_filter = request.args.get("GroupId")
|
||||
|
||||
filters: List[ListFilter] = []
|
||||
|
||||
if domain_name_filter:
|
||||
if len(domain_name_filter) > MAX_DOMAIN_NAME_LENGTH:
|
||||
abort(400, description=f"DomainName cannot exceed {MAX_DOMAIN_NAME_LENGTH} characters.")
|
||||
abort(
|
||||
400,
|
||||
description=f"DomainName cannot exceed {MAX_DOMAIN_NAME_LENGTH} characters.",
|
||||
)
|
||||
if not DOMAIN_NAME_REGEX.match(domain_name_filter):
|
||||
abort(400, description="DomainName contains invalid characters.")
|
||||
filters.append(Origin.domain_name.ilike(f"%{domain_name_filter}%"))
|
||||
|
@ -47,18 +55,18 @@ def list_origins() -> ResponseReturnValue:
|
|||
Origin,
|
||||
lambda origin: origin.to_dict(),
|
||||
filters=filters,
|
||||
resource_name='OriginsList',
|
||||
resource_name="OriginsList",
|
||||
max_allowed_items=MAX_ALLOWED_ITEMS,
|
||||
protective_marking='amber',
|
||||
protective_marking="amber",
|
||||
)
|
||||
|
||||
|
||||
@api_web.route('/mirror', methods=['GET'])
|
||||
@api_web.route("/mirror", methods=["GET"])
|
||||
def list_mirrors() -> ResponseReturnValue:
|
||||
filters = []
|
||||
|
||||
twenty_four_hours_ago = datetime.now(timezone.utc) - timedelta(hours=24)
|
||||
status_filter = request.args.get('Status')
|
||||
status_filter = request.args.get("Status")
|
||||
if status_filter:
|
||||
if status_filter == "pending":
|
||||
filters.append(Proxy.url.is_(None))
|
||||
|
@ -74,13 +82,15 @@ def list_mirrors() -> ResponseReturnValue:
|
|||
if status_filter == "destroyed":
|
||||
filters.append(Proxy.destroyed > twenty_four_hours_ago)
|
||||
else:
|
||||
filters.append((Proxy.destroyed.is_(None)) | (Proxy.destroyed > twenty_four_hours_ago))
|
||||
filters.append(
|
||||
(Proxy.destroyed.is_(None)) | (Proxy.destroyed > twenty_four_hours_ago)
|
||||
)
|
||||
|
||||
return list_resources(
|
||||
Proxy,
|
||||
lambda proxy: proxy.to_dict(),
|
||||
filters=filters,
|
||||
resource_name='MirrorsList',
|
||||
resource_name="MirrorsList",
|
||||
max_allowed_items=MAX_ALLOWED_ITEMS,
|
||||
protective_marking='amber',
|
||||
protective_marking="amber",
|
||||
)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue