diff --git a/front/src/app.py b/front/src/app.py index 1a1f1c80..3e0c2194 100644 --- a/front/src/app.py +++ b/front/src/app.py @@ -19,6 +19,7 @@ CreateCheckTaskView, ) from miminet_auth import ( + animation_filters, google_callback, google_login, insert_test_user, @@ -158,7 +159,9 @@ view_func=upload_network_picture, ) app.add_url_rule("/network/copy_network", methods=["POST"], view_func=copy_network) - +app.add_url_rule( + "/user/animation_filters", methods=["POST"], view_func=animation_filters +) # Simulation app.add_url_rule("/run_simulation", methods=["POST"], view_func=run_simulation) app.add_url_rule("/check_simulation", methods=["GET"], view_func=check_simulation) diff --git a/front/src/miminet_auth.py b/front/src/miminet_auth.py index 8012655c..1bd84e9c 100644 --- a/front/src/miminet_auth.py +++ b/front/src/miminet_auth.py @@ -9,7 +9,7 @@ import google.auth.transport.requests import requests -from flask import flash, redirect, render_template, request, session, url_for +from flask import flash, redirect, render_template, request, session, url_for, jsonify from flask_login import ( LoginManager, current_user, @@ -27,6 +27,11 @@ from sqlalchemy.exc import SQLAlchemyError from werkzeug.security import check_password_hash, generate_password_hash +DEFAULT_USER_CONFIG = { + "hideARP": False, + "hideSTP": False, +} + # Global variables UPLOAD_FOLDER = "static/avatar/" UPLOAD_TMP_FOLDER = "static/tmp/avatar/" @@ -155,6 +160,43 @@ def user_profile(): return render_template("auth/profile.html", user=user) +def _load_user_config(user: User) -> dict: + try: + parsed = json.loads(user.config) if user.config else {} + except (TypeError, ValueError): + parsed = {} + + return parsed if isinstance(parsed, dict) else {} + + +@login_required +def animation_filters(): + user = current_user + user_config = _load_user_config(user) + + # Ensure defaults are present + merged_config = {**DEFAULT_USER_CONFIG, **user_config} + merged_config["hideARP"] = bool(merged_config.get("hideARP", False)) + merged_config["hideSTP"] = bool(merged_config.get("hideSTP", False)) + + if request.method == "POST": + payload = request.get_json(silent=True) or {} + + if not isinstance(payload, dict): + return jsonify({"error": "Invalid payload"}), 400 + + for key in ("hideARP", "hideSTP"): + if key in payload: + merged_config[key] = bool(payload.get(key, False)) + + user.config = json.dumps(merged_config) + db.session.commit() + + return jsonify(merged_config), 200 + + return jsonify({"error": "Invalid request"}), 405 + + def password_recovery(): return render_template("auth/password_recovery.html") diff --git a/front/src/miminet_model.py b/front/src/miminet_model.py index 1b59d313..af7c5399 100644 --- a/front/src/miminet_model.py +++ b/front/src/miminet_model.py @@ -38,6 +38,8 @@ class User(db.Model, UserMixin): # type:ignore[name-defined] yandex_id = db.Column(Text, nullable=True) tg_id = db.Column(Text, nullable=True) + config = db.Column(Text, nullable=True) + class Network(db.Model): # type:ignore[name-defined] id = db.Column(BigInteger, primary_key=True, autoincrement=True) diff --git a/front/src/static/config_vxlan.js b/front/src/static/config_vxlan.js index 6c1e3aec..9a44d519 100644 --- a/front/src/static/config_vxlan.js +++ b/front/src/static/config_vxlan.js @@ -7,14 +7,14 @@ const ConfigVxlan = function (currentDevice) { var buttonElem = document.getElementById('config_button_vxlan_script'); var modalElem = document.getElementById('config_modal_vxlan_script'); var tableElem = document.getElementById('config_table_vxlan_script'); - + if (!buttonElem || !modalElem || !tableElem) { return; } - + var buttonHTML = buttonElem.innerHTML; var modalHTML = modalElem.innerHTML; - var tableHTML = tableElem.innerHTML; + var tableHTML = tableElem.innerHTML; modalHTML = modalHTML.replace('id="VxlanModal"', 'id="' + modalId + '"'); tableHTML = tableHTML.replace('id="config_table_vxlan"', 'id="' + tableId + '"'); @@ -59,7 +59,7 @@ function setupVxlanEventHandlers(currentDevice, modalId, tableId) { PostNodesEdges(); }); - $('#' + modalId).find('#vxlanConfigrationSubmit').on('click', function () { + $('#' + modalId).find('#vxlanConfigurationSubmit').on('click', function () { $('#' + modalId).modal('hide'); }); @@ -274,7 +274,7 @@ function addClientVxlanInterface(currentDevice, tableId, modalId) { return item.id === deviceEntry; }); if (iface) { - if (iface.vxlan_connection_type === 0 && iface.vxlan_vni !== null && iface.vxlan_vni !== undefined) { + if (iface.vxlan_connection_type === 0 && iface.vxlan_vni !== null && iface.vxlan_vni !== undefined) { showAlert("Этот интерфейс уже привязан к VNI: " + String(iface.vxlan_vni), "warning", modalId); return; } @@ -414,4 +414,4 @@ function showAlert(message, type = 'info', modalId) { setTimeout(() => { $(`#${alertId}`).alert('close'); }, 5000); -} \ No newline at end of file +} diff --git a/front/src/static/netfront_f.js b/front/src/static/netfront_f.js index 1e546c8a..80786296 100644 --- a/front/src/static/netfront_f.js +++ b/front/src/static/netfront_f.js @@ -5,6 +5,12 @@ var NetworkUpdateTimeoutId = -1; let NetworkCache = []; let lastSimulationId = 0 +let packetsNotFiltered = null; +let packetFilterState = { + hideARP: false, + hideSTP: false, +}; + const uid = function(){ return Date.now().toString(36) + Math.random().toString(36).substr(2); } @@ -1312,7 +1318,10 @@ const CheckSimulation = function (simulation_id) { packets = JSON.parse(data.packets); pcaps = data.pcaps; - SetNetworkPlayerState(0); + + // Set filters + packetsNotFiltered = null; + SetPacketFilter(); const answerButton = document.querySelector('button[name="answerQuestion"]'); if (answerButton) { @@ -1836,6 +1845,93 @@ const RunSimulation = function (network_guid) }); } +const FilterPackets = function () { + packets = packets + .map((step) => + step.filter( + (pkt) => + !( + (packetFilterState.hideARP && + pkt.data.label.startsWith("ARP")) || + packetFilterState.hideSTP && + (pkt.data.label.startsWith("STP") || + pkt.data.label.startsWith("RSTP")) + ) + ) + ) + .filter((step) => step.length > 0); +}; + +const UpdateFilterStates = function (settings) { + if (!settings) { + return; + } + + Object.assign(packetFilterState, settings); + $("#ARPFilterCheckbox").prop("checked", packetFilterState.hideARP); + $("#STPFilterCheckbox").prop("checked", packetFilterState.hideSTP); +}; + +const SaveAnimationFilters = function () { + if (!window.isAuthenticated) { + return; + } + + const payload = { + hideARP: Boolean(packetFilterState.hideARP), + hideSTP: Boolean(packetFilterState.hideSTP), + }; + + $.ajax({ + type: "POST", + url: "/user/animation_filters", + data: JSON.stringify(payload), + contentType: "application/json; charset=utf-8", + dataType: "json", + success: function (data) { + if (!data) { + return; + } + + const saved = { + hideARP: Boolean(data.hideARP), + hideSTP: Boolean(data.hideSTP), + }; + + UpdateFilterStates(saved); + }, + error: function (xhr) { + console.log("Cannot save animation filters"); + console.log(xhr); + }, + }); +}; + +const SetPacketFilter = function () { + // If network player UI is absent (e.g., not on network page), skip. + if (!document.getElementById("NetworkPlayer") || !document.getElementById("PacketSliderInput")) { + return; + } + + console.log("Packet filter call"); + // SetPacketFilter first call on emulated network + if (packets && !packetsNotFiltered) { + packetsNotFiltered = JSON.parse(JSON.stringify(packets)); // Array deep copy + } + // Numerous filter call, we grab our packets copy to filter it + else if (packetsNotFiltered) { + packets = JSON.parse(JSON.stringify(packetsNotFiltered)); + } + + packetFilterState.hideARP = $("#ARPFilterCheckbox").is(":checked"); + packetFilterState.hideSTP = $("#STPFilterCheckbox").is(":checked"); + + if (packets) { + FilterPackets(); + SetNetworkPlayerState(0); + } +}; + // 2 states: // Do we need emulation // We have a packets and ready to play packets @@ -1843,6 +1939,7 @@ const SetNetworkPlayerState = function (simulation_id) { // Reset? if (simulation_id === -1) { + packetsNotFiltered = null; packets = null; pcaps = []; SetNetworkPlayerState(0); @@ -1860,6 +1957,10 @@ const SetNetworkPlayerState = function (simulation_id) { PacketPlayer.getInstance().InitPlayer(packets); // Configure the slider + if (!$('#PacketSliderInput')[0] || !$('#PacketSliderInput')[0].noUiSlider) { + return; + } + $('#PacketSliderInput')[0].noUiSlider.updateOptions({ start: [1], range: { diff --git a/front/src/templates/base.html b/front/src/templates/base.html index 698f7973..21de9a01 100644 --- a/front/src/templates/base.html +++ b/front/src/templates/base.html @@ -61,7 +61,7 @@ - + {% else %}