add ipv4-ipv6 override

This commit is contained in:
Micky 2024-12-25 18:59:37 +11:00
parent 7961a6d1a9
commit acb7cff1e4
6 changed files with 95 additions and 110 deletions

View File

@ -1,5 +1,5 @@
FROM python:3.11-slim-bookworm FROM python:3.11-slim
# Install Node.js # Install Node.js
RUN apt-get update && apt-get install -y nodejs npm \ RUN apt-get update && apt-get install -y nodejs npm \
@ -16,7 +16,7 @@ COPY app/requirements.txt /app/
RUN pip install --no-cache-dir -r /app/requirements.txt RUN pip install --no-cache-dir -r /app/requirements.txt
# Install Node.js dependencies and build TailwindCSS # Install Node.js dependencies and build TailwindCSS
COPY package*.json /app/ COPY app/package*.json /app/
RUN npm install RUN npm install
COPY app/ /app/ COPY app/ /app/

View File

@ -50,7 +50,7 @@ def establish_connection(device_config):
logger.error(f"Failed to establish connection to {device_config['host']}: {e}") logger.error(f"Failed to establish connection to {device_config['host']}: {e}")
raise raise
def execute_command(device, command_config, target): def execute_command(device, command_format, target, ip_version):
device_config = { device_config = {
'device_type': device['type'], 'device_type': device['type'],
'host': device['host'], 'host': device['host'],
@ -68,7 +68,8 @@ def execute_command(device, command_config, target):
try: try:
with establish_connection(device_config) as connection: with establish_connection(device_config) as connection:
# Format the command # Format the command
command = str(command_config.get('format').format(target=target).strip()) print(command_format)
command = str(command_format.format(ip_version=ip_version, target=target).strip())
# Execute the command # Execute the command
output = connection.send_command( output = connection.send_command(

8
app/package.json Normal file
View File

@ -0,0 +1,8 @@
{
"name": "photonglass",
"version": "1.0.0",
"description": "Looking Glass",
"dependencies": {
"tailwindcss": "^3.4.16"
}
}

View File

@ -5,8 +5,8 @@ const app = Vue.createApp({
selectedDevice: '', selectedDevice: '',
selectedCommand: '', selectedCommand: '',
targetIp: '', targetIp: '',
selectedIpVersion: 'IPv4', // Default to IPv4
isLoading: false, isLoading: false,
isLoadingIp: false,
commandResult: '', commandResult: '',
devices: window.initialData?.devices || [], devices: window.initialData?.devices || [],
commands: window.initialData?.commands || [], commands: window.initialData?.commands || [],
@ -22,23 +22,40 @@ const app = Vue.createApp({
}, },
mounted() { mounted() {
this.updateThemeClass(); this.updateThemeClass();
// Make sure commands are loaded
if (window.initialData?.commands) {
console.log('Commands loaded:', this.commands);
}
}, },
watch: { watch: {
selectedCommand: { selectedCommand: {
handler(newVal) { handler(newVal) {
this.currentCommand = this.commands.find(cmd => cmd.id === newVal); this.currentCommand = this.commands.find(cmd => cmd.id === newVal);
console.log("Current command updated:", this.currentCommand);
}, },
immediate: true immediate: true
} }
}, },
computed: { computed: {
showIpVersionSelector() {
if (!this.targetIp) return true;
// Check if the input is not a valid IP address
const ipv4Regex = /^(\d{1,3}\.){3}\d{1,3}$/;
const ipv6Regex = /^([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$|^([0-9a-fA-F]{1,4}:){0,7}:([0-9a-fA-F]{1,4}:){0,7}[0-9a-fA-F]{1,4}$|^::1$|^::$|^::ffff:\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/;
// Detect IP version
if (ipv6Regex.test(this.targetIp)) {
this.selectedIpVersion = 'IPv6';
return false; // Disable button for valid IPv6
} else if (ipv4Regex.test(this.targetIp)) {
this.selectedIpVersion = 'IPv4';
return false; // Disable button for valid IPv4
}
return true; // Enable button for non-IP input
},
isValidInput() { isValidInput() {
if (!this.currentCommand || !this.targetIp) return false; if (!this.currentCommand || !this.targetIp) return false;
// If IP version selector is shown, any input is valid as it will be resolved
if (this.showIpVersionSelector) return true;
if (this.currentCommand.field?.validation) { if (this.currentCommand.field?.validation) {
const pattern = new RegExp(this.currentCommand.field.validation); const pattern = new RegExp(this.currentCommand.field.validation);
return pattern.test(this.targetIp); return pattern.test(this.targetIp);
@ -47,22 +64,8 @@ const app = Vue.createApp({
} }
}, },
methods: { methods: {
async getMyIp() { toggleIpVersion() {
if (this.isLoadingIp) return; this.selectedIpVersion = this.selectedIpVersion === 'IPv4' ? 'IPv6' : 'IPv4';
this.isLoadingIp = true;
try {
const response = await fetch('/my-ip');
const data = await response.json();
if (response.ok && data.ip) {
this.targetIp = data.ip;
} else {
console.error('Failed to get IP address');
}
} catch (error) {
console.error('Error fetching IP:', error);
} finally {
this.isLoadingIp = false;
}
}, },
toggleTheme() { toggleTheme() {
this.isDark = !this.isDark; this.isDark = !this.isDark;
@ -111,7 +114,8 @@ const app = Vue.createApp({
body: JSON.stringify({ body: JSON.stringify({
device: this.selectedDevice, device: this.selectedDevice,
command: this.selectedCommand, command: this.selectedCommand,
target: this.targetIp target: this.targetIp,
ipVersion: this.selectedIpVersion
}) })
}); });
@ -125,10 +129,10 @@ const app = Vue.createApp({
if (data.error) { if (data.error) {
// Handle error responses with specific error types // Handle error responses with specific error types
const errorPrefix = data.error_type === 'timeout' ? '🕒 Timeout Error: ' : const errorPrefix = data.error_type === 'timeout' ? '🕒 Timeout Error: ' :
data.error_type === 'auth' ? '🔒 Authentication Error: ' : data.error_type === 'auth' ? '🔒 Authentication Error: ' :
data.error_type === 'connection' ? '🔌 Connection Error: ' : data.error_type === 'connection' ? '🔌 Connection Error: ' :
data.error_type === 'no_output' ? '📭 No Output Error: ' : data.error_type === 'no_output' ? '📭 No Output Error: ' :
'❌ Error: '; '❌ Error: ';
this.commandResult = errorPrefix + data.message; this.commandResult = errorPrefix + data.message;
return; return;
} }

View File

@ -38,9 +38,9 @@
leave-to-class="opacity-0"> leave-to-class="opacity-0">
<div v-if="selectedDevice" class="mb-8 max-w-4xl mx-auto"> <div v-if="selectedDevice" class="mb-8 max-w-4xl mx-auto">
<div class="mb-6"></div> <div class="mb-6"></div>
<div class="grid grid-cols-1 md:grid-cols-12 gap-4"> <div class="flex flex-col md:flex-row gap-4">
<!-- Query Type Dropdown --> <!-- Query Type Dropdown -->
<div :class="['relative', selectedCommand ? 'md:col-span-5' : 'md:col-span-12']" @click.outside="closeDropdown"> <div class="relative flex-1" @click.outside="closeDropdown">
<button type="button" <button type="button"
@click.stop="toggleDropdown" @click.stop="toggleDropdown"
class="relative w-full h-12 bg-white dark:bg-gray-900 border border-gray-300 dark:border-gray-700 rounded-lg shadow-sm pl-3 pr-10 text-left cursor-pointer focus:outline-none focus:ring-1 focus:ring-gray-500 focus:border-gray-500 sm:text-sm transition-all duration-200 ease-in-out hover:border-gray-400 dark:hover:border-gray-600"> class="relative w-full h-12 bg-white dark:bg-gray-900 border border-gray-300 dark:border-gray-700 rounded-lg shadow-sm pl-3 pr-10 text-left cursor-pointer focus:outline-none focus:ring-1 focus:ring-gray-500 focus:border-gray-500 sm:text-sm transition-all duration-200 ease-in-out hover:border-gray-400 dark:hover:border-gray-600">
@ -65,19 +65,19 @@
<div v-if="isOpen" <div v-if="isOpen"
class="absolute z-10 mt-1 w-full bg-white dark:bg-gray-800 shadow-lg max-h-60 rounded-lg py-1 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm"> class="absolute z-10 mt-1 w-full bg-white dark:bg-gray-800 shadow-lg max-h-60 rounded-lg py-1 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm">
{% for command in commands %} {% for command in commands %}
<div @click.stop.prevent="selectCommand('{{ command.id }}')" <div @click.stop.prevent="selectCommand('{{ command.id }}')"
:class="['cursor-pointer select-none relative py-2 pl-3 pr-9 transition-all duration-200 ease-in-out hover:bg-gray-200 dark:hover:bg-gray-600 hover:text-gray-900 dark:hover:text-white', :class="['cursor-pointer select-none relative py-2 pl-3 pr-9 transition-all duration-200 ease-in-out hover:bg-gray-200 dark:hover:bg-gray-600 hover:text-gray-900 dark:hover:text-white',
selectedCommand === '{{ command.id }}' ? 'bg-gray-100 dark:bg-gray-700 text-gray-900 dark:text-white' : 'text-gray-700 dark:text-gray-300']"> selectedCommand === '{{ command.id }}' ? 'bg-gray-100 dark:bg-gray-700 text-gray-900 dark:text-white' : 'text-gray-700 dark:text-gray-300']">
<div class="flex items-center"> <div class="flex items-center">
<span class="block truncate font-medium">{{ command.display_name }}</span> <span class="block truncate font-medium">{{ command.display_name }}</span>
</div>
<span v-if="selectedCommand === '{{ command.id }}'"
class="absolute inset-y-0 right-0 flex items-center pr-4 text-gray-600 dark:text-gray-400">
<svg class="h-5 w-5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd" />
</svg>
</span>
</div> </div>
<span v-if="selectedCommand === '{{ command.id }}'"
class="absolute inset-y-0 right-0 flex items-center pr-4 text-gray-600 dark:text-gray-400">
<svg class="h-5 w-5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd" />
</svg>
</span>
</div>
{% endfor %} {% endfor %}
</div> </div>
</transition> </transition>
@ -92,61 +92,51 @@
leave-from-class="transform opacity-100 scale-100" leave-from-class="transform opacity-100 scale-100"
leave-to-class="transform opacity-0 scale-95"> leave-to-class="transform opacity-0 scale-95">
<div v-if="selectedCommand && currentCommand" <div v-if="selectedCommand && currentCommand"
class="md:col-span-7 relative flex items-center gap-2 transform transition-all duration-200 ease-in-out"> class="flex-1 flex items-center gap-2">
<!-- Target Input Row -->
<!-- Text Input Field --> <div class="flex items-center gap-2 w-full">
<div class="flex-1 relative"> <div class="flex-1 relative">
<input <input
v-model="targetIp" v-model="targetIp"
type="text" type="text"
@keyup.enter="isValidInput && executeCommand()" @keyup.enter="isValidInput && executeCommand()"
class="w-full h-12 rounded-lg px-3 pr-20 border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-900 text-gray-700 dark:text-gray-300 focus:border-gray-500 dark:focus:border-gray-500 transition-all duration-200 ease-in-out" class="w-full h-12 rounded-lg px-3 pr-24 border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-900 text-gray-700 dark:text-gray-300 focus:border-gray-500 dark:focus:border-gray-500 transition-all duration-200 ease-in-out"
:class="{'pr-32': targetIp}" :placeholder="currentCommand?.field?.placeholder || 'Enter target'"
:placeholder="currentCommand?.field?.placeholder || 'Enter target'" :pattern="currentCommand?.field?.validation || '.*'"
:pattern="currentCommand?.field?.validation || '.*'" required>
required> <div class="absolute right-2 top-1/2 -translate-y-1/2 flex items-center gap-2">
<button
type="button"
@click="toggleIpVersion"
:disabled="!showIpVersionSelector"
class="px-2 py-1 text-xs bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 rounded hover:bg-gray-200 dark:hover:bg-gray-600 transition-all duration-200 ease-in-out disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:bg-gray-100 dark:disabled:hover:bg-gray-700">
${selectedIpVersion}
</button>
</div>
</div>
<button <button
type="button" @click="executeCommand"
@click="getMyIp" :disabled="isLoading || !isValidInput || !targetIp"
:disabled="isLoadingIp" class="h-12 px-4 flex-shrink-0 rounded-lg border border-gray-300 dark:border-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800 hover:border-gray-400 dark:hover:border-gray-600 focus:outline-none focus:ring-2 focus:ring-gray-500 disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center transform transition-all duration-200 ease-in-out">
class="absolute right-2 top-1/2 -translate-y-1/2 px-2 py-1 text-xs bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 rounded hover:bg-gray-200 dark:hover:bg-gray-600 transition-all duration-200 ease-in-out disabled:opacity-50"
:class="{'opacity-0 translate-x-2': targetIp}">
<span v-if="isLoadingIp">...</span>
<span v-else>My IP</span>
</button>
</div>
<!-- Execute Button -->
<transition
enter-active-class="transition-all ease-in-out duration-200"
enter-from-class="opacity-0 scale-90 -translate-x-2"
enter-to-class="opacity-100 scale-100 translate-x-0"
leave-active-class="transition-all ease-in-out duration-200"
leave-from-class="opacity-100 scale-100 translate-x-0"
leave-to-class="opacity-0 scale-90 translate-x-2">
<button v-if="targetIp"
@click="executeCommand"
:disabled="isLoading"
class="h-12 px-4 rounded-lg border border-gray-300 dark:border-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800 hover:border-gray-400 dark:hover:border-gray-600 focus:outline-none focus:ring-2 focus:ring-gray-500 disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center transform transition-all duration-200 ease-in-out">
<transition <transition
enter-active-class="transition-all ease-in-out duration-200" enter-active-class="transition-all ease-in-out duration-200"
enter-from-class="opacity-0 rotate-180 scale-0" enter-from-class="opacity-0 rotate-180 scale-0"
enter-to-class="opacity-100 rotate-0 scale-100" enter-to-class="opacity-100 rotate-0 scale-100"
leave-active-class="transition-all ease-in-out duration-200" leave-active-class="transition-all ease-in-out duration-200"
leave-from-class="opacity-100 rotate-0 scale-100" leave-from-class="transform opacity-100 rotate-0 scale-100"
leave-to-class="opacity-0 rotate-180 scale-0"> leave-to-class="transform opacity-0 rotate-180 scale-0">
<!-- Loading Spinner --> <!-- Loading Spinner -->
<svg v-if="isLoading" class="animate-spin h-5 w-5" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"> <svg v-if="isLoading" class="animate-spin h-5 w-5" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle> <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path> <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg> </svg>
<!-- Search Icon --> <!-- Execute Icon -->
<svg v-else xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg v-else xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg> </svg>
</transition> </transition>
</button> </button>
</transition> </div>
</div> </div>
</transition> </transition>
</div> </div>

View File

@ -25,27 +25,6 @@ def index():
return render_template('index.html', config=config, devices=devices, commands=commands) return render_template('index.html', config=config, devices=devices, commands=commands)
@bp.route('/my-ip', methods=['GET'])
@exception_handler
def get_my_ip():
# Check headers in order of reliability
client_ip = None
forwarded_for = request.headers.get('X-Forwarded-For')
if forwarded_for:
client_ip = forwarded_for.split(',')[0].strip()
if not client_ip:
real_ip = request.headers.get('X-Real-IP')
if real_ip:
client_ip = real_ip
if not client_ip:
client_ip = request.remote_addr
return jsonify({'ip': client_ip})
@bp.route('/execute', methods=['POST']) @bp.route('/execute', methods=['POST'])
@exception_handler @exception_handler
def execute(): def execute():
@ -53,8 +32,9 @@ def execute():
device = data.get('device') device = data.get('device')
command = data.get('command') command = data.get('command')
target = data.get('target') target = data.get('target')
ip_version = data.get('ipVersion')
if not all([device, command, target]): if not all([device, command, target, ip_version]):
raise Exception("Missing required parameters") raise Exception("Missing required parameters")
# Load configurations # Load configurations
@ -72,9 +52,11 @@ def execute():
if command['id'] not in device.get('commands', []): if command['id'] not in device.get('commands', []):
raise Exception("Command not allowed for this device") raise Exception("Command not allowed for this device")
ip_version = 6 if ip_version == "IPv6" else 4
try: try:
# Execute the command using network_utils # Execute the command using network_utils
result = execute_command(device, command, target) result = execute_command(device, command['format'], target, ip_version)
if not result: if not result:
error_msg = 'No response from command execution' error_msg = 'No response from command execution'