2022-08-10 16:50:50 +01:00
|
|
|
import urllib.parse
|
2024-12-06 16:08:48 +00:00
|
|
|
from datetime import datetime, timezone
|
|
|
|
from typing import List, Optional
|
2022-05-04 13:46:52 +01:00
|
|
|
|
2022-08-10 16:50:50 +01:00
|
|
|
import requests
|
2023-10-29 15:45:10 +00:00
|
|
|
import sqlalchemy
|
2024-12-06 16:08:48 +00:00
|
|
|
from flask import (Blueprint, Response, flash, redirect, render_template,
|
|
|
|
url_for)
|
2022-05-16 11:44:03 +01:00
|
|
|
from flask.typing import ResponseReturnValue
|
2022-05-04 13:46:52 +01:00
|
|
|
from flask_wtf import FlaskForm
|
|
|
|
from sqlalchemy import exc
|
2024-12-06 16:08:48 +00:00
|
|
|
from wtforms import (BooleanField, IntegerField, SelectField, StringField,
|
|
|
|
SubmitField)
|
2022-05-04 13:46:52 +01:00
|
|
|
from wtforms.validators import DataRequired
|
|
|
|
|
|
|
|
from app.extensions import db
|
|
|
|
from app.models.base import Group
|
2024-12-06 16:08:48 +00:00
|
|
|
from app.models.mirrors import Country, Origin
|
|
|
|
from app.portal.util import LifecycleForm, response_404, view_lifecycle
|
2022-05-04 13:46:52 +01:00
|
|
|
|
|
|
|
bp = Blueprint("origin", __name__)
|
|
|
|
|
|
|
|
|
2022-05-16 11:44:03 +01:00
|
|
|
class NewOriginForm(FlaskForm): # type: ignore
|
2022-05-04 13:46:52 +01:00
|
|
|
domain_name = StringField('Domain Name', validators=[DataRequired()])
|
|
|
|
description = StringField('Description', validators=[DataRequired()])
|
|
|
|
group = SelectField('Group', validators=[DataRequired()])
|
|
|
|
auto_rotate = BooleanField("Enable auto-rotation?", default=True)
|
2022-05-24 19:51:38 +01:00
|
|
|
smart_proxy = BooleanField("Requires smart proxy?", default=False)
|
2022-05-25 15:32:17 +01:00
|
|
|
asset_domain = BooleanField("Used to host assets for other domains?", default=False)
|
2022-05-04 13:46:52 +01:00
|
|
|
submit = SubmitField('Save Changes')
|
|
|
|
|
|
|
|
|
2023-10-29 15:45:10 +00:00
|
|
|
class EditOriginForm(FlaskForm): # type: ignore[misc]
|
2022-05-04 13:46:52 +01:00
|
|
|
description = StringField('Description', validators=[DataRequired()])
|
|
|
|
group = SelectField('Group', validators=[DataRequired()])
|
|
|
|
auto_rotate = BooleanField("Enable auto-rotation?")
|
2022-05-24 19:51:38 +01:00
|
|
|
smart_proxy = BooleanField("Requires smart proxy?")
|
2022-05-25 15:32:17 +01:00
|
|
|
asset_domain = BooleanField("Used to host assets for other domains?", default=False)
|
2023-10-29 15:45:10 +00:00
|
|
|
risk_level_override = BooleanField("Force Risk Level Override?")
|
|
|
|
risk_level_override_number = IntegerField("Forced Risk Level", description="Number from 0 to 20", default=0)
|
2022-05-04 13:46:52 +01:00
|
|
|
submit = SubmitField('Save Changes')
|
|
|
|
|
|
|
|
|
2023-10-29 15:45:10 +00:00
|
|
|
class CountrySelectForm(FlaskForm): # type: ignore[misc]
|
|
|
|
country = SelectField("Country", validators=[DataRequired()])
|
|
|
|
submit = SubmitField('Save Changes', render_kw={"class": "btn btn-success"})
|
|
|
|
|
|
|
|
|
2022-08-12 12:24:56 +01:00
|
|
|
def final_domain_name(domain_name: str) -> str:
|
2022-08-10 16:50:50 +01:00
|
|
|
session = requests.Session()
|
|
|
|
r = session.get(f"https://{domain_name}/", allow_redirects=True, timeout=10)
|
|
|
|
return urllib.parse.urlparse(r.url).netloc
|
|
|
|
|
|
|
|
|
2022-05-04 13:46:52 +01:00
|
|
|
@bp.route("/new", methods=['GET', 'POST'])
|
|
|
|
@bp.route("/new/<group_id>", methods=['GET', 'POST'])
|
2022-05-16 11:44:03 +01:00
|
|
|
def origin_new(group_id: Optional[int] = None) -> ResponseReturnValue:
|
2022-05-04 13:46:52 +01:00
|
|
|
form = NewOriginForm()
|
|
|
|
form.group.choices = [(x.id, x.group_name) for x in Group.query.all()]
|
|
|
|
if form.validate_on_submit():
|
|
|
|
origin = Origin()
|
|
|
|
origin.group_id = form.group.data
|
2022-08-10 16:50:50 +01:00
|
|
|
origin.domain_name = final_domain_name(form.domain_name.data)
|
2022-05-04 13:46:52 +01:00
|
|
|
origin.description = form.description.data
|
|
|
|
origin.auto_rotation = form.auto_rotate.data
|
2022-05-24 19:51:38 +01:00
|
|
|
origin.smart = form.smart_proxy.data
|
2022-05-25 15:32:17 +01:00
|
|
|
origin.assets = form.asset_domain.data
|
2024-12-06 16:08:48 +00:00
|
|
|
origin.added = datetime.now(tz=timezone.utc)
|
|
|
|
origin.updated = datetime.now(tz=timezone.utc)
|
2022-05-04 13:46:52 +01:00
|
|
|
try:
|
|
|
|
db.session.add(origin)
|
|
|
|
db.session.commit()
|
|
|
|
flash(f"Created new origin {origin.domain_name}.", "success")
|
|
|
|
return redirect(url_for("portal.origin.origin_edit", origin_id=origin.id))
|
2022-06-23 13:42:45 +01:00
|
|
|
except exc.SQLAlchemyError:
|
2022-05-04 13:46:52 +01:00
|
|
|
flash("Failed to create new origin.", "danger")
|
|
|
|
return redirect(url_for("portal.origin.origin_list"))
|
|
|
|
if group_id:
|
|
|
|
form.group.data = group_id
|
|
|
|
return render_template("new.html.j2", section="origin", form=form)
|
|
|
|
|
|
|
|
|
|
|
|
@bp.route('/edit/<origin_id>', methods=['GET', 'POST'])
|
2022-05-16 11:44:03 +01:00
|
|
|
def origin_edit(origin_id: int) -> ResponseReturnValue:
|
|
|
|
origin: Optional[Origin] = Origin.query.filter(Origin.id == origin_id).first()
|
2022-05-04 13:46:52 +01:00
|
|
|
if origin is None:
|
|
|
|
return Response(render_template("error.html.j2",
|
|
|
|
section="origin",
|
|
|
|
header="404 Origin Not Found",
|
|
|
|
message="The requested origin could not be found."),
|
|
|
|
status=404)
|
|
|
|
form = EditOriginForm(group=origin.group_id,
|
|
|
|
description=origin.description,
|
2022-05-24 19:51:38 +01:00
|
|
|
auto_rotate=origin.auto_rotation,
|
2022-05-25 15:32:17 +01:00
|
|
|
smart_proxy=origin.smart,
|
2023-10-29 15:45:10 +00:00
|
|
|
asset_domain=origin.assets,
|
|
|
|
risk_level_override=origin.risk_level_override is not None,
|
|
|
|
risk_level_override_number=origin.risk_level_override)
|
2022-05-04 13:46:52 +01:00
|
|
|
form.group.choices = [(x.id, x.group_name) for x in Group.query.all()]
|
|
|
|
if form.validate_on_submit():
|
|
|
|
origin.group_id = form.group.data
|
|
|
|
origin.description = form.description.data
|
|
|
|
origin.auto_rotation = form.auto_rotate.data
|
2022-05-24 19:51:38 +01:00
|
|
|
origin.smart = form.smart_proxy.data
|
2022-05-25 15:32:17 +01:00
|
|
|
origin.assets = form.asset_domain.data
|
2023-10-29 15:45:10 +00:00
|
|
|
if form.risk_level_override.data:
|
|
|
|
origin.risk_level_override = form.risk_level_override_number.data
|
|
|
|
else:
|
|
|
|
origin.risk_level_override = None
|
2024-12-06 16:08:48 +00:00
|
|
|
origin.updated = datetime.now(tz=timezone.utc)
|
2022-05-04 13:46:52 +01:00
|
|
|
try:
|
|
|
|
db.session.commit()
|
2023-10-29 15:45:10 +00:00
|
|
|
flash(f"Saved changes for origin {origin.domain_name}.", "success")
|
2022-05-04 13:46:52 +01:00
|
|
|
except exc.SQLAlchemyError:
|
2023-10-29 15:45:10 +00:00
|
|
|
flash("An error occurred saving the changes to the origin.", "danger")
|
2022-05-04 13:46:52 +01:00
|
|
|
return render_template("origin.html.j2",
|
|
|
|
section="origin",
|
|
|
|
origin=origin, form=form)
|
|
|
|
|
|
|
|
|
|
|
|
@bp.route("/list")
|
2022-05-16 11:44:03 +01:00
|
|
|
def origin_list() -> ResponseReturnValue:
|
|
|
|
origins: List[Origin] = Origin.query.order_by(Origin.domain_name).all()
|
2022-05-04 13:46:52 +01:00
|
|
|
return render_template("list.html.j2",
|
|
|
|
section="origin",
|
2022-05-12 17:03:26 +01:00
|
|
|
title="Web Origins",
|
2022-05-04 13:46:52 +01:00
|
|
|
item="origin",
|
|
|
|
new_link=url_for("portal.origin.origin_new"),
|
2022-05-12 17:03:26 +01:00
|
|
|
items=origins,
|
|
|
|
extra_buttons=[{
|
|
|
|
"link": url_for("portal.origin.origin_onion"),
|
|
|
|
"text": "Onion services",
|
|
|
|
"style": "onion"
|
|
|
|
}])
|
2022-05-04 13:46:52 +01:00
|
|
|
|
|
|
|
|
2022-05-04 15:36:36 +01:00
|
|
|
@bp.route("/onion")
|
2022-05-16 11:44:03 +01:00
|
|
|
def origin_onion() -> ResponseReturnValue:
|
2022-05-04 15:36:36 +01:00
|
|
|
origins = Origin.query.order_by(Origin.domain_name).all()
|
|
|
|
return render_template("list.html.j2",
|
|
|
|
section="origin",
|
|
|
|
title="Onion Sites",
|
|
|
|
item="onion service",
|
|
|
|
new_link=url_for("portal.onion.onion_new"),
|
|
|
|
items=origins)
|
|
|
|
|
|
|
|
|
2022-05-04 13:46:52 +01:00
|
|
|
@bp.route("/destroy/<origin_id>", methods=['GET', 'POST'])
|
2022-05-16 11:44:03 +01:00
|
|
|
def origin_destroy(origin_id: int) -> ResponseReturnValue:
|
2022-05-16 13:29:48 +01:00
|
|
|
origin = Origin.query.filter(Origin.id == origin_id, Origin.destroyed.is_(None)).first()
|
2022-05-04 13:46:52 +01:00
|
|
|
if origin is None:
|
|
|
|
return response_404("The requested origin could not be found.")
|
|
|
|
return view_lifecycle(
|
|
|
|
header=f"Destroy origin {origin.domain_name}",
|
|
|
|
message=origin.description,
|
|
|
|
success_message="All proxies from the destroyed origin will shortly be destroyed at their providers.",
|
2022-05-04 15:36:36 +01:00
|
|
|
success_view="portal.origin.origin_list",
|
2022-05-04 13:46:52 +01:00
|
|
|
section="origin",
|
|
|
|
resource=origin,
|
|
|
|
action="destroy"
|
|
|
|
)
|
2023-10-29 15:45:10 +00:00
|
|
|
|
|
|
|
|
|
|
|
@bp.route('/country_remove/<origin_id>/<country_id>', methods=['GET', 'POST'])
|
|
|
|
def origin_country_remove(origin_id: int, country_id: int) -> ResponseReturnValue:
|
|
|
|
origin = Origin.query.filter(Origin.id == origin_id).first()
|
|
|
|
if origin is None:
|
|
|
|
return Response(render_template("error.html.j2",
|
|
|
|
section="origin",
|
|
|
|
header="404 Pool Not Found",
|
|
|
|
message="The requested origin could not be found."),
|
|
|
|
status=404)
|
|
|
|
country = Country.query.filter(Country.id == country_id).first()
|
|
|
|
if country is None:
|
|
|
|
return Response(render_template("error.html.j2",
|
|
|
|
section="origin",
|
|
|
|
header="404 Country Not Found",
|
|
|
|
message="The requested country could not be found."),
|
|
|
|
status=404)
|
|
|
|
if country not in origin.countries:
|
|
|
|
return Response(render_template("error.html.j2",
|
|
|
|
section="origin",
|
|
|
|
header="404 Country Not In Pool",
|
|
|
|
message="The requested country could not be found in the specified origin."),
|
|
|
|
status=404)
|
|
|
|
form = LifecycleForm()
|
|
|
|
if form.validate_on_submit():
|
|
|
|
origin.countries.remove(country)
|
|
|
|
try:
|
|
|
|
db.session.commit()
|
|
|
|
flash("Saved changes to origin.", "success")
|
|
|
|
return redirect(url_for("portal.origin.origin_edit", origin_id=origin.id))
|
|
|
|
except sqlalchemy.exc.SQLAlchemyError:
|
|
|
|
flash("An error occurred saving the changes to the origin.", "danger")
|
|
|
|
return render_template("lifecycle.html.j2",
|
2023-10-29 19:18:05 +00:00
|
|
|
header=f"Remove {country.description} from the {origin.domain_name} origin?",
|
2023-10-29 15:45:10 +00:00
|
|
|
message="Stop monitoring in this country.",
|
|
|
|
section="origin",
|
|
|
|
origin=origin, form=form)
|
|
|
|
|
|
|
|
|
|
|
|
@bp.route('/country_add/<origin_id>', methods=['GET', 'POST'])
|
|
|
|
def origin_country_add(origin_id: int) -> ResponseReturnValue:
|
|
|
|
origin = Origin.query.filter(Origin.id == origin_id).first()
|
|
|
|
if origin is None:
|
|
|
|
return Response(render_template("error.html.j2",
|
|
|
|
section="origin",
|
|
|
|
header="404 Origin Not Found",
|
|
|
|
message="The requested origin could not be found."),
|
|
|
|
status=404)
|
|
|
|
form = CountrySelectForm()
|
|
|
|
form.country.choices = [(x.id, f"{x.country_code} - {x.description}") for x in Country.query.all()]
|
|
|
|
if form.validate_on_submit():
|
|
|
|
country = Country.query.filter(Country.id == form.country.data).first()
|
|
|
|
if country is None:
|
|
|
|
return Response(render_template("error.html.j2",
|
|
|
|
section="origin",
|
|
|
|
header="404 Country Not Found",
|
|
|
|
message="The requested country could not be found."),
|
|
|
|
status=404)
|
|
|
|
origin.countries.append(country)
|
|
|
|
try:
|
|
|
|
db.session.commit()
|
|
|
|
flash("Saved changes to origin.", "success")
|
|
|
|
return redirect(url_for("portal.origin.origin_edit", origin_id=origin.id))
|
|
|
|
except sqlalchemy.exc.SQLAlchemyError:
|
|
|
|
flash("An error occurred saving the changes to the origin.", "danger")
|
|
|
|
return render_template("lifecycle.html.j2",
|
|
|
|
header=f"Add a country to {origin.domain_name}",
|
|
|
|
message="Enable monitoring from this country:",
|
|
|
|
section="origin",
|
|
|
|
origin=origin, form=form)
|