├── LICENSE ├── README.md └── ip-tracker.py /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Eric Labrador Sainz 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![image](https://github.com/e1abrador/Burp-IP-Logger/assets/74373745/0850a68e-a3e3-450b-baca-3946ca3c87ec) 2 | 3 | ## TL;DR 4 | 5 | ### Installation 6 | 7 | Go to the Extensions -> Installed -> Add -> iptracker.py 8 | 9 | ### Why? 10 | 11 | Usually in a red team engagement it is needed to log all IP addresses used in the exercise. By using this extension, Burp will log all IP addresses used automatically, so the auditor can focus on testing and stop caring about what IP address he has used. 12 | 13 | ## Using 14 | 15 | Once the extension is loaded, it will start logging your IP address: 16 | 17 | ![image](https://github.com/e1abrador/Burp-IP-Logger/assets/74373745/592eb967-d84c-4195-9b93-3f0f0adf5f8e) 18 | 19 | It will show the IP Address used + the first and last time the IP was detected. 20 | 21 | By default the extension will not save the IP logs, so you will need to save the logs in a .txt format and when you open back Burp load them from that .txt file. 22 | In case is the end of the exercise, you can export the logs as a CSV file and format the .csv as a table in an easy way. 23 | 24 |

If you have any idea: https://github.com/e1abrador/Burp-IP-Tracker/pulls

25 |

If you have any issue with the extension: https://github.com/e1abrador/Burp-IP-Tracker/issues

26 | 27 | Good luck and good hunting! 28 | If you really love the tool (or any others), or they helped you find an awesome bounty, consider [BUYING ME A COFFEE!](https://www.buymeacoffee.com/e1abrador) ☕ (I could use the caffeine!) 29 | 30 | ## TODO 31 | 32 | - Implement a "CLEAR" button. 33 | - Implement a "Export to CSV" button. [DONE] 34 | 35 | ## Thanks 36 | 37 | Thanks to Ricardo Pelaz for the awesome idea! 38 | - https://github.com/varandinawer 39 | - https://twitter.com/Varandinawer 40 | 41 | ## Advisory 42 | 43 | This Burp Suite extension should be used for authorized penetration testing and/or educational purposes only. Any misuse of this software will not be the responsibility of the author or of any other collaborator. Use it at your own networks and/or with the network owner's permission. 44 | -------------------------------------------------------------------------------- /ip-tracker.py: -------------------------------------------------------------------------------- 1 | from burp import IBurpExtender, ITab, IExtensionStateListener, IHttpListener 2 | from javax.swing import JPanel, JTable, JScrollPane, BoxLayout, JButton, JFileChooser, BorderFactory 3 | from javax.swing import JLabel, SwingConstants, Box, JSeparator, JComboBox, JTextField, GroupLayout 4 | from javax.swing.table import DefaultTableModel, DefaultTableCellRenderer 5 | from javax.swing.border import EmptyBorder, CompoundBorder, TitledBorder 6 | from java.awt import Color, Font, FlowLayout, BorderLayout, Dimension, GridLayout 7 | from java.awt.event import ActionListener, MouseAdapter, MouseEvent 8 | from java.util import Timer, TimerTask 9 | from java.io import File 10 | import java.awt.Dimension as Dimension 11 | import java.lang.System as System 12 | import java.text.SimpleDateFormat as SimpleDateFormat 13 | import java.util.ArrayList as ArrayList 14 | import json 15 | import socket 16 | import struct 17 | import random 18 | import threading 19 | import time 20 | 21 | # Clase personalizada para DefaultTableModel 22 | class ReadOnlyTableModel(DefaultTableModel): 23 | def isCellEditable(self, row, column): 24 | return False 25 | 26 | class BurpExtender(IBurpExtender, IExtensionStateListener, IHttpListener): 27 | def registerExtenderCallbacks(self, callbacks): 28 | self.callbacks = callbacks 29 | self.helpers = callbacks.getHelpers() 30 | callbacks.setExtensionName("IP Tracker") 31 | 32 | # Register listeners 33 | callbacks.registerExtensionStateListener(self) 34 | callbacks.registerHttpListener(self) 35 | 36 | # Initialize persistence 37 | self.project_file = None 38 | self.setupProjectState() 39 | 40 | # Create UI after we have the project state 41 | self.ui = IPLoggerUI(callbacks, self) 42 | callbacks.customizeUiComponent(self.ui.getUiComponent()) 43 | callbacks.addSuiteTab(self.ui) 44 | 45 | print "IP Tracker extension loaded successfully" 46 | 47 | def extensionUnloaded(self): 48 | # Save state before unloading 49 | try: 50 | self.ui.saveState() 51 | print "IP Tracker extension unloaded" 52 | except: 53 | print "Error saving state during unload" 54 | 55 | def processHttpMessage(self, toolFlag, messageIsRequest, messageInfo): 56 | # This is where we could extract IP information from HTTP traffic 57 | # Currently we're using STUN for IP detection, but this could be an alternative 58 | pass 59 | 60 | def setupProjectState(self): 61 | # Try to get current project identifier 62 | try: 63 | # Get a unique identifier for the current project file - using hash of project name or path 64 | # In a real implementation, you might use a better way to identify the current project 65 | # For now, we'll just use "default" as a placeholder 66 | project_id = "default_project" 67 | self.project_file = project_id 68 | 69 | # Load project-specific data if it exists 70 | project_data = self.callbacks.loadExtensionSetting("ip_tracker_" + project_id) 71 | if project_data is not None: 72 | # We have project-specific data, load it as the current data 73 | self.callbacks.saveExtensionSetting("ip_logs", project_data) 74 | print "Loaded project-specific IP tracking data" 75 | else: 76 | print "No project-specific IP tracking data found" 77 | except Exception as e: 78 | print "Error setting up project state:", e 79 | 80 | class IPLoggerUI(ITab): 81 | def __init__(self, callbacks, extender): 82 | self.callbacks = callbacks 83 | self.extender = extender 84 | 85 | # Define colors for a professional look 86 | self.BACKGROUND_COLOR = Color(245, 245, 247) 87 | self.HEADER_COLOR = Color(66, 66, 76) 88 | self.ACCENT_COLOR = Color(88, 157, 246) 89 | self.BUTTON_COLOR = Color(88, 157, 246) 90 | self.TEXT_COLOR = Color(50, 50, 50) 91 | self.ALT_ROW_COLOR = Color(240, 240, 242) 92 | self.ERROR_COLOR = Color(217, 83, 79) 93 | self.SUCCESS_COLOR = Color(92, 184, 92) 94 | 95 | # Create main panel 96 | self.panel = JPanel(BorderLayout()) 97 | self.panel.setBackground(self.BACKGROUND_COLOR) 98 | self.panel.setBorder(EmptyBorder(15, 15, 15, 15)) 99 | 100 | # Create header panel with logo 101 | headerPanel = JPanel(BorderLayout()) 102 | headerPanel.setBackground(self.BACKGROUND_COLOR) 103 | headerPanel.setBorder(EmptyBorder(0, 0, 10, 0)) 104 | 105 | # Add title 106 | titleLabel = JLabel("IP Tracker", SwingConstants.LEFT) 107 | titleLabel.setFont(Font("Arial", Font.BOLD, 20)) 108 | titleLabel.setForeground(self.HEADER_COLOR) 109 | headerPanel.add(titleLabel, BorderLayout.WEST) 110 | 111 | # Add status info 112 | self.statusLabel = JLabel("Initializing IP tracking...") 113 | self.statusLabel.setFont(Font("Arial", Font.ITALIC, 12)) 114 | self.statusLabel.setForeground(Color(100, 100, 100)) 115 | headerPanel.add(self.statusLabel, BorderLayout.EAST) 116 | 117 | # Add header to main panel 118 | self.panel.add(headerPanel, BorderLayout.NORTH) 119 | 120 | # Create table panel with border 121 | tablePanel = JPanel(BorderLayout()) 122 | tablePanel.setBackground(self.BACKGROUND_COLOR) 123 | tablePanel.setBorder(CompoundBorder( 124 | TitledBorder(BorderFactory.createLineBorder(self.ACCENT_COLOR, 1), 125 | "IP Address History", 126 | TitledBorder.LEFT, 127 | TitledBorder.TOP, 128 | Font("Arial", Font.BOLD, 14), 129 | self.ACCENT_COLOR), 130 | EmptyBorder(10, 10, 10, 10))) 131 | 132 | # Create table with custom rendering - using our ReadOnlyTableModel class 133 | self.model = ReadOnlyTableModel( 134 | ["IP Address", "First Detection Time", "Last Detection Time", "Duration"], 135 | 0 136 | ) 137 | 138 | self.table = JTable(self.model) 139 | self.table.setRowHeight(28) 140 | self.table.setFont(Font("Arial", Font.PLAIN, 13)) 141 | self.table.getTableHeader().setFont(Font("Arial", Font.BOLD, 13)) 142 | self.table.getTableHeader().setBackground(self.HEADER_COLOR) 143 | self.table.getTableHeader().setForeground(Color.WHITE) 144 | self.table.setShowGrid(False) 145 | self.table.setIntercellSpacing(Dimension(0, 0)) 146 | self.table.setFillsViewportHeight(True) 147 | self.table.setSelectionBackground(self.ACCENT_COLOR.brighter()) 148 | self.table.setSelectionForeground(Color.WHITE) 149 | 150 | # Custom renderer for alternating row colors 151 | class CustomCellRenderer(DefaultTableCellRenderer): 152 | def getTableCellRendererComponent(self, table, value, isSelected, hasFocus, row, column): 153 | component = DefaultTableCellRenderer.getTableCellRendererComponent( 154 | self, table, value, isSelected, hasFocus, row, column) 155 | 156 | if not isSelected: 157 | if row % 2 == 0: 158 | component.setBackground(Color.WHITE) 159 | else: 160 | component.setBackground(self.ALT_ROW_COLOR) 161 | 162 | component.setBorder(EmptyBorder(5, 10, 5, 10)) 163 | return component 164 | 165 | renderer = CustomCellRenderer() 166 | renderer.ALT_ROW_COLOR = self.ALT_ROW_COLOR 167 | for i in range(self.table.getColumnCount()): 168 | self.table.getColumnModel().getColumn(i).setCellRenderer(renderer) 169 | 170 | # Set preferred column widths 171 | self.table.getColumnModel().getColumn(0).setPreferredWidth(150) # IP 172 | self.table.getColumnModel().getColumn(1).setPreferredWidth(180) # First detection 173 | self.table.getColumnModel().getColumn(2).setPreferredWidth(180) # Last detection 174 | self.table.getColumnModel().getColumn(3).setPreferredWidth(120) # Duration 175 | 176 | # Add table to scroll pane 177 | scrollPane = JScrollPane(self.table) 178 | scrollPane.setBorder(BorderFactory.createLineBorder(Color.LIGHT_GRAY, 1)) 179 | tablePanel.add(scrollPane, BorderLayout.CENTER) 180 | 181 | # Add table panel to main panel 182 | self.panel.add(tablePanel, BorderLayout.CENTER) 183 | 184 | # Create button panel 185 | buttonPanel = JPanel(BorderLayout()) 186 | buttonPanel.setBackground(self.BACKGROUND_COLOR) 187 | buttonPanel.setBorder(EmptyBorder(10, 0, 0, 0)) 188 | 189 | # Create info panel on the left side 190 | infoPanel = JPanel(FlowLayout(FlowLayout.LEFT)) 191 | infoPanel.setBackground(self.BACKGROUND_COLOR) 192 | 193 | # Add version info 194 | versionLabel = JLabel("v1.2.0") 195 | versionLabel.setFont(Font("Arial", Font.ITALIC, 11)) 196 | versionLabel.setForeground(Color(120, 120, 120)) 197 | infoPanel.add(versionLabel) 198 | 199 | buttonPanel.add(infoPanel, BorderLayout.WEST) 200 | 201 | # Create right side panel for buttons 202 | rightPanel = JPanel(FlowLayout(FlowLayout.RIGHT)) 203 | rightPanel.setBackground(self.BACKGROUND_COLOR) 204 | 205 | # Create styled buttons 206 | self.refreshButton = self.createStyledButton("Refresh Now", "Force an immediate IP check") 207 | self.refreshButton.addActionListener(RefreshListener(self)) 208 | 209 | self.exportCSVButton = self.createStyledButton("Export as CSV", "Save IP history to CSV file") 210 | self.exportCSVButton.addActionListener(ExportListener(self)) 211 | 212 | self.clearButton = self.createStyledButton("Clear History", "Clear all IP history data") 213 | self.clearButton.addActionListener(ClearHistoryListener(self)) 214 | 215 | # Add buttons to right panel 216 | rightPanel.add(self.refreshButton) 217 | rightPanel.add(Box.createRigidArea(Dimension(10, 0))) 218 | rightPanel.add(self.clearButton) 219 | rightPanel.add(Box.createRigidArea(Dimension(10, 0))) 220 | rightPanel.add(self.exportCSVButton) 221 | 222 | # Add right panel to button panel 223 | buttonPanel.add(rightPanel, BorderLayout.EAST) 224 | 225 | # Add button panel to main panel 226 | self.panel.add(buttonPanel, BorderLayout.SOUTH) 227 | 228 | # Initialize data and timer 229 | self.last_ip = None 230 | self.error_count = 0 # Track consecutive errors 231 | self.auto_save_interval = 30000 # 30 seconds 232 | 233 | # Load data (with project awareness) 234 | self.load_persisted_data() 235 | 236 | # Start timers 237 | self.timer = Timer(True) 238 | self.timer.scheduleAtFixedRate(IPCheckTask(self), 0, 5000) # Check every 5 seconds 239 | self.timer.scheduleAtFixedRate(AutoSaveTask(self), self.auto_save_interval, self.auto_save_interval) # Auto-save 240 | 241 | def createStyledButton(self, text, tooltip): 242 | button = JButton(text) 243 | button.setBackground(self.BUTTON_COLOR) 244 | button.setForeground(Color.WHITE) 245 | button.setFont(Font("Arial", Font.BOLD, 12)) 246 | button.setFocusPainted(False) 247 | button.setBorder(BorderFactory.createEmptyBorder(8, 15, 8, 15)) 248 | button.setToolTipText(tooltip) 249 | 250 | # Add hover effect 251 | class ButtonMouseListener(MouseAdapter): 252 | def mouseEntered(self, e): 253 | button.setBackground(Color(88, 157, 246).brighter()) 254 | 255 | def mouseExited(self, e): 256 | button.setBackground(self.BUTTON_COLOR) 257 | 258 | listener = ButtonMouseListener() 259 | listener.BUTTON_COLOR = self.BUTTON_COLOR 260 | button.addMouseListener(listener) 261 | 262 | return button 263 | 264 | def getTabCaption(self): 265 | return "IP Tracker" 266 | 267 | def getUiComponent(self): 268 | return self.panel 269 | 270 | def load_persisted_data(self): 271 | try: 272 | persisted_data = self.callbacks.loadExtensionSetting("ip_logs") 273 | if persisted_data: 274 | data = json.loads(persisted_data) 275 | for entry in data: 276 | # Skip error entries 277 | if entry["ip"].startswith("Error"): 278 | continue 279 | 280 | # Calculate duration if available 281 | duration = "" 282 | if entry.get("last_detection_time", ""): 283 | try: 284 | fmt = SimpleDateFormat("yyyy-MM-dd HH:mm:ss") 285 | first_time = fmt.parse(entry["detection_time"]) 286 | last_time = fmt.parse(entry["last_detection_time"]) 287 | diff_ms = last_time.getTime() - first_time.getTime() 288 | 289 | # Format duration 290 | if diff_ms < 60000: # Less than a minute 291 | duration = str(int(diff_ms / 1000)) + " seconds" 292 | elif diff_ms < 3600000: # Less than an hour 293 | duration = str(int(diff_ms / 60000)) + " minutes" 294 | else: # Hours or more 295 | duration = str(round(diff_ms / 3600000.0, 1)) + " hours" 296 | except Exception, e: 297 | print "Error calculating duration:", e 298 | 299 | self.model.addRow([ 300 | entry["ip"], 301 | entry["detection_time"], 302 | entry.get("last_detection_time", ""), 303 | duration 304 | ]) 305 | 306 | if data: 307 | # Find last valid IP 308 | for entry in reversed(data): 309 | if not entry["ip"].startswith("Error"): 310 | self.last_ip = entry["ip"] 311 | break 312 | self.updateStatusLabel() 313 | except Exception, e: 314 | print "Error loading persisted data:", e 315 | self.setErrorStatus("Error loading data: " + str(e)) 316 | 317 | def saveState(self): 318 | """Save state for current project""" 319 | try: 320 | data = [] 321 | for row in range(self.model.getRowCount()): 322 | data.append({ 323 | "ip": str(self.model.getValueAt(row, 0)), 324 | "detection_time": str(self.model.getValueAt(row, 1)), 325 | "last_detection_time": str(self.model.getValueAt(row, 2)), 326 | # Duration is calculated on load, no need to store it 327 | }) 328 | 329 | # Save general data 330 | json_data = json.dumps(data) 331 | self.callbacks.saveExtensionSetting("ip_logs", json_data) 332 | 333 | # Save project-specific data if we have a project identifier 334 | if self.extender.project_file: 335 | self.callbacks.saveExtensionSetting( 336 | "ip_tracker_" + str(self.extender.project_file), 337 | json_data 338 | ) 339 | 340 | # Show autosave indicator briefly (but only if we're visible) 341 | if hasattr(self, 'panel') and self.panel.isShowing(): 342 | self.showStatusMessage("Data saved automatically", self.SUCCESS_COLOR, 2000) 343 | except Exception, e: 344 | print "Error saving data:", e 345 | if hasattr(self, 'panel') and self.panel.isShowing(): 346 | self.setErrorStatus("Error saving data: " + str(e)) 347 | 348 | def write_to_csv(self, file): 349 | try: 350 | writer = open(file.getAbsolutePath(), 'w') 351 | try: 352 | # Write header 353 | headers = [] 354 | for col in range(self.model.getColumnCount()): 355 | headers.append(self.model.getColumnName(col)) 356 | writer.write(",".join(headers) + "\n") 357 | 358 | # Write data 359 | for row in range(self.model.getRowCount()): 360 | line = [ 361 | '"' + str(self.model.getValueAt(row, col)).replace('"', '""') + '"' # Quote each field 362 | for col in range(self.model.getColumnCount()) 363 | ] 364 | writer.write(",".join(line) + "\n") 365 | 366 | self.showStatusMessage("Export successful: " + file.getName(), self.SUCCESS_COLOR, 3000) 367 | finally: 368 | writer.close() 369 | except Exception, e: 370 | print "Error writing to CSV file:", e 371 | self.setErrorStatus("Error exporting to CSV: " + str(e)) 372 | 373 | def clear_history(self): 374 | self.model.setRowCount(0) 375 | self.last_ip = None 376 | self.saveState() # Save the empty state 377 | self.showStatusMessage("History cleared successfully", self.SUCCESS_COLOR, 3000) 378 | 379 | def log_current_ip(self): 380 | try: 381 | # Use STUN to fetch public IP 382 | ip = self.get_public_ip_via_stun() 383 | current_time = SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(System.currentTimeMillis()) 384 | 385 | # Reset error count on success 386 | self.error_count = 0 387 | 388 | if ip != self.last_ip: 389 | if self.last_ip is not None and self.model.getRowCount() > 0: 390 | # Update last detection time for previous IP 391 | row_idx = self.model.getRowCount() - 1 392 | self.model.setValueAt(current_time, row_idx, 2) 393 | 394 | # Calculate and update duration 395 | try: 396 | fmt = SimpleDateFormat("yyyy-MM-dd HH:mm:ss") 397 | first_time = fmt.parse(str(self.model.getValueAt(row_idx, 1))) 398 | last_time = fmt.parse(current_time) 399 | diff_ms = last_time.getTime() - first_time.getTime() 400 | 401 | # Format duration 402 | if diff_ms < 60000: # Less than a minute 403 | duration = str(int(diff_ms / 1000)) + " seconds" 404 | elif diff_ms < 3600000: # Less than an hour 405 | duration = str(int(diff_ms / 60000)) + " minutes" 406 | else: # Hours or more 407 | duration = str(round(diff_ms / 3600000.0, 1)) + " hours" 408 | 409 | self.model.setValueAt(duration, row_idx, 3) 410 | except Exception, e: 411 | print "Error calculating duration:", e 412 | 413 | # Add new IP to table 414 | self.model.addRow([ip, current_time, "", ""]) 415 | self.last_ip = ip 416 | 417 | # Auto-save 418 | self.saveState() 419 | 420 | # Update status with new IP 421 | self.showStatusMessage("New IP detected: " + ip, self.ACCENT_COLOR, 5000) 422 | else: 423 | self.updateStatusLabel() 424 | 425 | except Exception, e: 426 | # Instead of adding error to table, just show in status 427 | self.error_count += 1 428 | self.setErrorStatus("Error fetching IP: " + str(e)) 429 | 430 | # Log error to console only 431 | print "Error fetching IP:", e 432 | 433 | def setErrorStatus(self, message): 434 | """Set error message in status label""" 435 | if hasattr(self, 'statusLabel'): # Ensure statusLabel exists 436 | self.statusLabel.setText(message + " (Retrying...)") 437 | self.statusLabel.setForeground(self.ERROR_COLOR) 438 | 439 | def showStatusMessage(self, message, color, duration_ms): 440 | """Show a temporary status message with custom color""" 441 | if not hasattr(self, 'statusLabel'): # Safety check 442 | return 443 | 444 | self.statusLabel.setText(message) 445 | self.statusLabel.setForeground(color) 446 | 447 | # Reset after specified duration 448 | timer_task = ResetLabelTask(self) 449 | reset_timer = Timer() 450 | reset_timer.schedule(timer_task, duration_ms) 451 | 452 | def updateStatusLabel(self): 453 | """Update the status label with default status""" 454 | if not hasattr(self, 'statusLabel'): # Safety check 455 | return 456 | 457 | if self.last_ip: 458 | self.statusLabel.setText("Monitoring IP: " + self.last_ip) 459 | self.statusLabel.setForeground(Color(100, 100, 100)) # Normal color 460 | else: 461 | self.statusLabel.setText("Waiting to detect your public IP address...") 462 | self.statusLabel.setForeground(Color(100, 100, 100)) # Normal color 463 | 464 | def get_public_ip_via_stun(self, stun_server="stun.l.google.com", stun_port=19302): 465 | """Get public IP address using STUN protocol""" 466 | # Try multiple STUN servers if needed 467 | stun_servers = [ 468 | ("stun1.l.google.com", 19302), 469 | ("stun.ekiga.net", 3478), 470 | ("stun.ideasip.com", 3478), 471 | ("stun.voiparound.com", 3478), 472 | ] 473 | 474 | # Start with the default server 475 | servers_to_try = [(stun_server, stun_port)] + stun_servers 476 | 477 | last_error = None 478 | s = None 479 | 480 | for server, port in servers_to_try: 481 | try: 482 | if s: 483 | try: 484 | s.close() 485 | except: 486 | pass 487 | 488 | s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 489 | s.settimeout(2) # Timeout for response 490 | 491 | # Generate random 16-byte transaction ID 492 | tid = ''.join(chr(random.randint(0, 255)) for _ in range(16)) 493 | 494 | # STUN binding request (type 0x0001, length 0) 495 | request = struct.pack("!HH", 0x0001, 0) + tid 496 | s.sendto(request, (server, port)) 497 | 498 | data, addr = s.recvfrom(1024) 499 | 500 | # Parse header 501 | msg_type, msg_len = struct.unpack("!HH", data[:4]) 502 | if msg_type != 0x0101: 503 | raise ValueError("Not a STUN binding response") 504 | if data[4:20] != tid: 505 | raise ValueError("Transaction ID mismatch") 506 | 507 | # Parse attributes 508 | pos = 20 509 | while pos < 20 + msg_len: 510 | attr_type, attr_len = struct.unpack("!HH", data[pos:pos+4]) 511 | pos += 4 512 | value = data[pos:pos + attr_len] 513 | pos += attr_len 514 | 515 | # Pad to 4-byte boundary 516 | if pos % 4 != 0: 517 | pos += 4 - (pos % 4) 518 | 519 | if attr_type == 0x0001: # MAPPED-ADDRESS 520 | unused, family, port = struct.unpack("!BBH", value[:4]) 521 | if family == 1: # IPv4 522 | ip_bytes = struct.unpack("!BBBB", value[4:8]) 523 | ip_address = ".".join(map(str, ip_bytes)) 524 | 525 | # Close the socket before returning 526 | try: 527 | s.close() 528 | except: 529 | pass 530 | 531 | return ip_address 532 | # IPv6 (family 2) can be added if needed 533 | 534 | raise ValueError("No mapped address attribute found in response") 535 | 536 | except Exception, e: 537 | last_error = e 538 | continue 539 | 540 | # Make sure to close the socket if we're exiting with an error 541 | if s: 542 | try: 543 | s.close() 544 | except: 545 | pass 546 | 547 | # If we get here, all servers failed 548 | if last_error: 549 | raise last_error 550 | else: 551 | raise ValueError("All STUN servers failed") 552 | 553 | class RefreshListener(ActionListener): 554 | """Listener for manual refresh button""" 555 | def __init__(self, ui): 556 | self.ui = ui 557 | 558 | def actionPerformed(self, event): 559 | # Disable button temporarily 560 | self.ui.refreshButton.setEnabled(False) 561 | 562 | try: 563 | # Update status 564 | self.ui.statusLabel.setText("Checking IP address...") 565 | self.ui.statusLabel.setForeground(self.ui.ACCENT_COLOR) 566 | 567 | # Force an IP check 568 | self.ui.log_current_ip() 569 | 570 | finally: 571 | # Re-enable button after a short delay 572 | refresh_timer = Timer() 573 | refresh_timer.schedule(EnableButtonTask(self.ui.refreshButton), 2000) 574 | 575 | class EnableButtonTask(TimerTask): 576 | """Task to re-enable a button after a delay""" 577 | def __init__(self, button): 578 | self.button = button 579 | 580 | def run(self): 581 | try: 582 | self.button.setEnabled(True) 583 | except: 584 | pass # Ignore errors if button no longer exists 585 | 586 | class ExportListener(ActionListener): 587 | """Listener for export CSV button""" 588 | def __init__(self, ui): 589 | self.ui = ui 590 | 591 | def actionPerformed(self, event): 592 | chooser = JFileChooser() 593 | chooser.setDialogTitle("Save IP History as CSV") 594 | ret = chooser.showSaveDialog(self.ui.panel) 595 | if ret == JFileChooser.APPROVE_OPTION: 596 | file = chooser.getSelectedFile() 597 | # Add .csv extension if not present 598 | if not file.getName().lower().endswith(".csv"): 599 | file = File(file.getAbsolutePath() + ".csv") 600 | self.ui.write_to_csv(file) 601 | 602 | class ClearHistoryListener(ActionListener): 603 | """Listener for clear history button""" 604 | def __init__(self, ui): 605 | self.ui = ui 606 | 607 | def actionPerformed(self, event): 608 | self.ui.clear_history() 609 | 610 | class IPCheckTask(TimerTask): 611 | """Task to periodically check IP address""" 612 | def __init__(self, ip_logger_ui): 613 | self.ip_logger_ui = ip_logger_ui 614 | 615 | def run(self): 616 | try: 617 | self.ip_logger_ui.log_current_ip() 618 | except Exception, e: 619 | # Capture any errors to prevent the timer from stopping 620 | print "Error in IP check task:", e 621 | 622 | class AutoSaveTask(TimerTask): 623 | """Task to periodically save state""" 624 | def __init__(self, ip_logger_ui): 625 | self.ip_logger_ui = ip_logger_ui 626 | 627 | def run(self): 628 | try: 629 | self.ip_logger_ui.saveState() 630 | except Exception, e: 631 | # Capture any errors to prevent the timer from stopping 632 | print "Error in auto-save task:", e 633 | 634 | class ResetLabelTask(TimerTask): 635 | """Task to reset status label after a delay""" 636 | def __init__(self, ui): 637 | self.ui = ui 638 | 639 | def run(self): 640 | try: 641 | if hasattr(self.ui, 'updateStatusLabel'): 642 | self.ui.updateStatusLabel() 643 | except Exception, e: 644 | # Capture any errors 645 | print "Error in reset label task:", e 646 | --------------------------------------------------------------------------------