From a5d2be5dd9a0fed4ee1ee9b1aeb64d0550bd40c1 Mon Sep 17 00:00:00 2001 From: IITG Date: Mon, 9 Sep 2024 13:28:44 +1000 Subject: [PATCH] Updated web interface to add new tests and run everything as a non root user --- Dockerfile | 19 ++++-- app.py | 159 ++++++++++++++++++++++++++++++++----------- static/app.js | 111 ++++++++++++++++++------------ templates/base.html | 2 +- templates/index.html | 117 ++++++++++++++++++++++++------- 5 files changed, 295 insertions(+), 113 deletions(-) diff --git a/Dockerfile b/Dockerfile index f03b8d2..5248afb 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,14 +2,23 @@ FROM python:3.12-alpine RUN pip install --no-cache-dir flask -RUN apk update && apk add traceroute \ +RUN apk update && apk add mtr \ + traceroute \ + bind-tools \ iperf \ iperf3 -COPY static /static/ -COPY templates /templates/ -COPY app.py . +# Security updates +RUN apk upgrade libexpat + +COPY static /app/static/ +COPY templates /app/templates/ +COPY app.py /app/ EXPOSE 5000 -CMD ["python", "app.py"] +RUN adduser -D iperf-web +RUN chown -R iperf-web:iperf-web /app + +USER iperf-web +CMD ["python", "/app/app.py"] diff --git a/app.py b/app.py index c6b331c..dccf5bc 100644 --- a/app.py +++ b/app.py @@ -1,20 +1,61 @@ from flask import Flask, render_template, request, Response import subprocess +import select import shlex import re +import os app = Flask(__name__) +# function to convert a string to boolean +def str_to_boolean(s): + return str(s).lower() == "true" + # Function to run a command and stream the output def run_command(command): - process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + try: + process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + except FileNotFoundError as e: + yield f"Error: {str(e)}
" + return + except OSError as e: + yield f"Error: {str(e)}
" + return + + yield 'Executing command: ' + ' '.join(command) + '

' + + # Monitor both stdout and stderr streams while True: - output = process.stdout.readline().decode('utf-8') - if output: - yield output + '
' - elif process.poll() is not None: + # Use select to monitor stdout and stderr + reads = [process.stdout, process.stderr] + readable, _, _ = select.select(reads, [], []) + + for stream in readable: + if stream == process.stdout: + stdout_line = process.stdout.readline().decode('utf-8') + if stdout_line: + yield stdout_line + '
' + + if stream == process.stderr: + stderr_line = process.stderr.readline().decode('utf-8') + if stderr_line: + yield f"{stderr_line}
" + + # Break if the process has terminated and there's no more output + if process.poll() is not None: break + # Ensure that any remaining output is read after the process terminates + remaining_stdout = process.stdout.read().decode('utf-8') + if remaining_stdout: + yield remaining_stdout + '
' + + remaining_stderr = process.stderr.read().decode('utf-8') + if remaining_stderr: + yield f"{remaining_stderr}
" + + yield '
Execution finished!
' + # Helper function to sanitize parameters def sanitize_parameters(parameters): sanitized = shlex.split(parameters) # Split safely without executing @@ -27,6 +68,16 @@ def validate_target(target): return True return False +def is_valid_port(port): + try: + port = int(port) + return 1 <= port <= 65535 + except (ValueError, TypeError): + return False + +app_port = os.getenv('IPERF_WEB_PORT', '5000') +debug_mode = str_to_boolean(os.getenv('IPERF_WEB_DEBUG_MODE', False)) + # Route to display the page @app.route('/') def index(): @@ -35,49 +86,79 @@ def index(): # Route to handle form submission and execute selected test @app.route('/run_test', methods=['POST']) def run_test(): - print("Form submitted") # Debugging statement test_type = request.form.get('test_type') - + output = "" + sanitized_params = "" command = [] - if test_type == 'ping': - target = request.form.get('ping_target') + # Only run tests for valid test types + TEST_TYPES = {'dig', 'iperf', 'mtr', 'nc', 'nslookup', 'ping', 'traceroute'} + if test_type in TEST_TYPES: + target = request.form.get(test_type + '_target') # Validate the target address if not validate_target(target): - return Response("Invalid target address.", mimetype='text/plain') - parameters = request.form.get('ping_parameters', '') - sanitized_params = sanitize_parameters(parameters) - command = ['ping'] + sanitized_params + [target] - elif test_type == 'traceroute': - target = request.form.get('traceroute_target') - # Validate the target address - if not validate_target(target): - return Response("Invalid target address.", mimetype='text/plain') - parameters = request.form.get('traceroute_parameters', '') - sanitized_params = sanitize_parameters(parameters) - command = ['traceroute'] + sanitized_params + [target] - elif test_type == 'iperf': - server = request.form.get('iperf_server') - # Validate the target address - if not validate_target(server): - return Response("Invalid server address.", mimetype='text/plain') - iperf_version = request.form.get('iperf_version') - port = request.form.get('port') - conn_type = request.form.get('conn_type') - parameters = request.form.get('iperf_parameters', '-f m -i 5 -t 10') + return Response("'" + str(target) + "' is not a valid target address.", mimetype='text/plain') + parameters = request.form.get(test_type + '_parameters', '') + + # MTR specific parameters + if test_type == 'mtr': + mtr_reportcycles = int(request.form.get('mtr_reportcycles', '200')) + parameters += ' --report --report-wide --report-cycles ' + str(mtr_reportcycles) + + # netcat specific parameters + if test_type == 'nc': + port = request.form.get('nc_port') + + if not is_valid_port(port): + return Response("'" + str(port) + "' is not a valid TCP port.", mimetype='text/plain') + + command = [test_type, '-vz', target, port] + + # netcat specific parameters + if test_type == 'nslookup': + server = request.form.get('nslookup_dns_server') + if server: + parameters = server + sanitized_params = sanitize_parameters(parameters) + command = [test_type, target] + sanitized_params + + # ping specific parameters + if test_type == 'ping': + count = int(request.form.get('ping_count', '4')) + parameters += ' -c' + str(count) + + # iperf specific parameters + if test_type == 'iperf': + iperf_version = request.form.get('iperf_version') + port = request.form.get('iperf_port') + + if not is_valid_port(port): + port = 5001 + + conn_type = request.form.get('iperf_conn_type') + + # Choose the correct iperf command based on version + base_command = 'iperf3' if iperf_version == '3' else 'iperf' + + timeout = int(request.form.get('iperf_timeout', '10')) + + sanitized_params = sanitize_parameters(parameters) + + command = [base_command, '-c', target, '-p', port, '--forceflush', '-t', str(timeout)] + sanitized_params + + # Add -u flag for UDP connection type + if conn_type == 'UDP': + command.append('-u') - sanitized_params = sanitize_parameters(parameters) + if not sanitized_params: + sanitized_params = sanitize_parameters(parameters) - # Choose the correct iperf command based on version - base_command = 'iperf3' if iperf_version == '3' else 'iperf' - command = [base_command, '-c', server, '-p', port, '--forceflush'] + sanitized_params + if not command: + command = [test_type] + sanitized_params + [target] - # Add -u flag for UDP connection type - if conn_type == 'UDP': - command.append('-u') + print(f"Executing command: {' '.join(command)}") # Debugging statement - print(f"Executing command: {' '.join(command)}") # Debugging statement return Response(run_command(command), mimetype='text/html') if __name__ == '__main__': - app.run(debug=True,host='0.0.0.0') + app.run(debug=debug_mode,host='0.0.0.0',port=app_port) \ No newline at end of file diff --git a/static/app.js b/static/app.js index fcd3114..8ab40d6 100644 --- a/static/app.js +++ b/static/app.js @@ -1,54 +1,81 @@ function showTestFields() { + // Map of test types to the associated fields and required attributes + const fieldMap = { + 'dig': { + fieldsToShow: ['dig_fields'], + requiredFields: ['dig_target'], + defaults: { 'dig_parameters': '+short' } + }, + 'iperf': { + fieldsToShow: ['iperf_fields'], + requiredFields: ['iperf_target', 'iperf_port', 'iperf_timeout'], + defaults: { 'iperf_parameters': '--format m', 'iperf_timeout': '10' } + }, + 'mtr': { + fieldsToShow: ['mtr_fields'], + requiredFields: ['mtr_target', 'mtr_reportcycles'], + defaults: { 'mtr_reportcycles': '200', 'mtr_parameters': '-n -T' } + }, + 'nc': { + fieldsToShow: ['nc_fields'], + requiredFields: ['nc_target', 'nc_port'], + defaults: {} + }, + 'nslookup': { + fieldsToShow: ['nslookup_fields'], + requiredFields: ['nslookup_target'], + defaults: {} + }, + 'ping': { + fieldsToShow: ['ping_fields'], + requiredFields: ['ping_target', 'ping_count'], + defaults: { 'ping_count': '4' } + }, + 'traceroute': { + fieldsToShow: ['traceroute_fields'], + requiredFields: ['traceroute_target'], + defaults: { 'traceroute_parameters': '--icmp'} + } + }; + + // Get the selected test type var testType = document.getElementById("test_type").value; - if (testType === 'ping') { - document.getElementById("ping_fields").style.display = 'block'; - document.getElementById("traceroute_fields").style.display = 'none'; - document.getElementById("iperf_fields").style.display = 'none'; - - // Reset parameters and target for Ping - document.getElementById("ping_target").value = ''; - document.getElementById("ping_parameters").value = '-c4'; - - // Enable required for Ping, disable for others - document.getElementById("ping_target").setAttribute('required', 'required'); - document.getElementById("traceroute_target").removeAttribute('required'); - document.getElementById("iperf_server").removeAttribute('required'); - document.getElementById("port").removeAttribute('required'); - } else if (testType === 'traceroute') { - document.getElementById("ping_fields").style.display = 'none'; - document.getElementById("traceroute_fields").style.display = 'block'; - document.getElementById("iperf_fields").style.display = 'none'; - - // Reset parameters and target for Traceroute - document.getElementById("traceroute_target").value = ''; - document.getElementById("traceroute_parameters").value = ''; - - // Enable required for Traceroute, disable for others - document.getElementById("traceroute_target").setAttribute('required', 'required'); - document.getElementById("ping_target").removeAttribute('required'); - document.getElementById("iperf_server").removeAttribute('required'); - document.getElementById("port").removeAttribute('required'); - } else if (testType === 'iperf') { - document.getElementById("ping_fields").style.display = 'none'; - document.getElementById("traceroute_fields").style.display = 'none'; - document.getElementById("iperf_fields").style.display = 'block'; - - // Set default parameters for IPerf - document.getElementById("iperf_parameters").value = '-f m -i 5 -t 30'; - - // Enable required for IPerf, disable for others - document.getElementById("iperf_server").setAttribute('required', 'required'); - document.getElementById("port").setAttribute('required', 'required'); - document.getElementById("ping_target").removeAttribute('required'); - document.getElementById("traceroute_target").removeAttribute('required'); + // Hide all fields initially + const allFields = ['dig_fields', 'iperf_fields', 'mtr_fields', 'nc_fields', 'nslookup_fields', 'ping_fields', 'traceroute_fields']; + allFields.forEach(field => { + document.getElementById(field).style.display = 'none'; + }); + + // Remove 'required' from all input fields + const allInputFields = ['dig_target', 'iperf_target', 'iperf_port', 'iperf_timeout', 'mtr_target', 'mtr_reportcycles', 'nc_target', 'nc_port', 'nslookup_target', 'ping_target', 'ping_count', 'traceroute_target']; + allInputFields.forEach(field => { + document.getElementById(field).removeAttribute('required'); + }); + + // Set defaults and show relevant fields based on the selected test type + if (fieldMap[testType]) { + // Show the relevant fields + fieldMap[testType].fieldsToShow.forEach(field => { + document.getElementById(field).style.display = 'block'; + }); + + // Set required attributes for relevant fields + fieldMap[testType].requiredFields.forEach(field => { + document.getElementById(field).setAttribute('required', 'required'); + }); + + // Apply default values if any + for (const [field, value] of Object.entries(fieldMap[testType].defaults)) { + document.getElementById(field).value = value; + } } } // Initialize fields visibility on page load window.onload = showTestFields; - +// Scroll the iframe automatically function scrollIframe() { var iframe = document.getElementById("output_frame"); iframe.contentWindow.scrollTo(0, iframe.contentWindow.document.body.scrollHeight); diff --git a/templates/base.html b/templates/base.html index 84c2fa4..933590d 100644 --- a/templates/base.html +++ b/templates/base.html @@ -58,7 +58,7 @@
{% block content %} {% endblock %}
diff --git a/templates/index.html b/templates/index.html index dbf8cbe..2909f7f 100644 --- a/templates/index.html +++ b/templates/index.html @@ -5,33 +5,26 @@

Network Test Utility

-
- - - -
- - + +
- - + +
- -
+
+ + +
- + +
+ + + + + + + + + + + + + + + +