Compare commits
	
		
			15 Commits
		
	
	
		
			v2.4.5
			...
			b06e2b2273
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| b06e2b2273 | |||
| cc1cfe894c | |||
| da49b7a5bf | |||
| 4de6081a74 | |||
| 5a13e49803 | |||
| 2737fca294 | |||
| 896233914f | |||
| 5bb775b17d | |||
| ae8219acf7 | |||
| 4ad383884c | |||
| 65a9d1c798 | |||
| f583e1466f | |||
| 9d893a97b6 | |||
| aa52d5e9f6 | |||
| 623b7ee51f | 
							
								
								
									
										53
									
								
								bin/nupst
									
									
									
									
									
								
							
							
						
						
									
										53
									
								
								bin/nupst
									
									
									
									
									
								
							| @@ -22,16 +22,63 @@ fi | |||||||
| # For debugging | # For debugging | ||||||
| # echo "Project root: $PROJECT_ROOT" | # echo "Project root: $PROJECT_ROOT" | ||||||
|  |  | ||||||
| # Set Node.js binary path directly | # Detect architecture and OS | ||||||
|  | ARCH=$(uname -m) | ||||||
|  | OS=$(uname -s) | ||||||
|  |  | ||||||
|  | # Determine Node.js binary location based on architecture and OS | ||||||
|  | NODE_BIN="" | ||||||
|  | case "$OS" in | ||||||
|  |   Linux) | ||||||
|  |     case "$ARCH" in | ||||||
|  |       x86_64) | ||||||
|         NODE_BIN="$PROJECT_ROOT/vendor/node-linux-x64/bin/node" |         NODE_BIN="$PROJECT_ROOT/vendor/node-linux-x64/bin/node" | ||||||
|  |         ;; | ||||||
|  |       aarch64|arm64) | ||||||
|  |         NODE_BIN="$PROJECT_ROOT/vendor/node-linux-arm64/bin/node" | ||||||
|  |         ;; | ||||||
|  |       *) | ||||||
|  |         # Use system Node as fallback for other architectures | ||||||
|  |         if command -v node &> /dev/null; then | ||||||
|  |           NODE_BIN="node" | ||||||
|  |           echo "Using system Node.js installation for unsupported architecture: $ARCH" | ||||||
|  |         fi | ||||||
|  |         ;; | ||||||
|  |     esac | ||||||
|  |     ;; | ||||||
|  |   Darwin) | ||||||
|  |     case "$ARCH" in | ||||||
|  |       x86_64) | ||||||
|  |         NODE_BIN="$PROJECT_ROOT/vendor/node-darwin-x64/bin/node" | ||||||
|  |         ;; | ||||||
|  |       arm64) | ||||||
|  |         NODE_BIN="$PROJECT_ROOT/vendor/node-darwin-arm64/bin/node" | ||||||
|  |         ;; | ||||||
|  |       *) | ||||||
|  |         # Use system Node as fallback for other architectures | ||||||
|  |         if command -v node &> /dev/null; then | ||||||
|  |           NODE_BIN="node" | ||||||
|  |           echo "Using system Node.js installation for unsupported architecture: $ARCH" | ||||||
|  |         fi | ||||||
|  |         ;; | ||||||
|  |     esac | ||||||
|  |     ;; | ||||||
|  |   *) | ||||||
|  |     # Use system Node as fallback for other operating systems | ||||||
|  |     if command -v node &> /dev/null; then | ||||||
|  |       NODE_BIN="node" | ||||||
|  |       echo "Using system Node.js installation for unsupported OS: $OS" | ||||||
|  |     fi | ||||||
|  |     ;; | ||||||
|  | esac | ||||||
|  |  | ||||||
| # If binary doesn't exist, try system Node as fallback | # If binary doesn't exist, try system Node as fallback | ||||||
| if [ ! -f "$NODE_BIN" ]; then | if [ -z "$NODE_BIN" ] || [ ! -f "$NODE_BIN" ]; then | ||||||
|   if command -v node &> /dev/null; then |   if command -v node &> /dev/null; then | ||||||
|     NODE_BIN="node" |     NODE_BIN="node" | ||||||
|     echo "Using system Node.js installation" |     echo "Using system Node.js installation" | ||||||
|   else   |   else   | ||||||
|     echo "Error: Node.js binary not found at $NODE_BIN" |     echo "Error: Node.js binary not found for $OS-$ARCH" | ||||||
|     echo "Please run the setup script or install Node.js manually." |     echo "Please run the setup script or install Node.js manually." | ||||||
|     exit 1 |     exit 1 | ||||||
|   fi |   fi | ||||||
|   | |||||||
							
								
								
									
										46
									
								
								changelog.md
									
									
									
									
									
								
							
							
						
						
									
										46
									
								
								changelog.md
									
									
									
									
									
								
							| @@ -1,5 +1,51 @@ | |||||||
| # Changelog | # Changelog | ||||||
|  |  | ||||||
|  | ## 2025-03-26 - 2.6.0 - feat(setup) | ||||||
|  | Add --force update flag to setup script and update installation instructions | ||||||
|  |  | ||||||
|  | - Implemented --force option in setup.sh to force-update Node.js binary and dependencies | ||||||
|  | - Updated readme.md to document the --force flag and revised update steps | ||||||
|  | - Modified ts/cli.ts update command to pass the --force flag to setup.sh | ||||||
|  |  | ||||||
|  | ## 2025-03-26 - 2.5.2 - fix(installer) | ||||||
|  | Improve Node.js binary detection, dependency management, and SNMPv3 fallback logic | ||||||
|  |  | ||||||
|  | - Enhanced bin/nupst to detect OS and architecture (Linux and Darwin) and fall back to system Node.js for unsupported platforms | ||||||
|  | - Moved net-snmp from devDependencies to dependencies in package.json | ||||||
|  | - Updated setup.sh to install production dependencies and handle installation errors gracefully | ||||||
|  | - Refined SNMPv3 user configuration and fallback mechanism in ts/snmp/manager.ts | ||||||
|  | - Revised README to clarify minimal runtime dependencies and secure SNMP features | ||||||
|  |  | ||||||
|  | ## 2025-03-25 - 2.5.1 - fix(snmp) | ||||||
|  | Fix Eaton UPS support by updating power status OID and adjusting battery runtime conversion. | ||||||
|  |  | ||||||
|  | - Updated Eaton UPS power status OID to '1.3.6.1.4.1.534.1.4.4.0' to correctly detect online/battery status. | ||||||
|  | - Added conversion for Eaton UPS battery runtime from seconds to minutes in SNMP manager. | ||||||
|  |  | ||||||
|  | ## 2025-03-25 - 2.5.0 - feat(cli) | ||||||
|  | Automatically restart running NUPST service after configuration changes in interactive setup | ||||||
|  |  | ||||||
|  | - Added restartServiceIfRunning() to check and restart the service if it's active. | ||||||
|  | - Invoked the restart function post-setup to apply configuration changes immediately. | ||||||
|  |  | ||||||
|  | ## 2025-03-25 - 2.4.8 - fix(installer) | ||||||
|  | Improve Git dependency handling and repository cloning in install.sh | ||||||
|  |  | ||||||
|  | - Add explicit check for git installation and prompt the user interactively if git is missing. | ||||||
|  | - Auto-install git when '-y' flag is provided in non-interactive mode. | ||||||
|  | - Ensure proper cloning of the repository when running the installer outside the repo. | ||||||
|  |  | ||||||
|  | ## 2025-03-25 - 2.4.7 - fix(readme) | ||||||
|  | Update installation instructions to combine download and execution into a single command for clarity | ||||||
|  |  | ||||||
|  | - Method 1 now uses a unified one-line command to download and run the install script | ||||||
|  |  | ||||||
|  | ## 2025-03-25 - 2.4.6 - fix(installer) | ||||||
|  | Improve installation instructions for interactive and non-interactive setups | ||||||
|  |  | ||||||
|  | - Changed install.sh to require explicit download of the install script and updated error messages for non-interactive modes | ||||||
|  | - Updated readme.md to include three distinct installation methods with clear command examples | ||||||
|  |  | ||||||
| ## 2025-03-25 - 2.4.5 - fix(install) | ## 2025-03-25 - 2.4.5 - fix(install) | ||||||
| Improve interactive terminal detection and update installation instructions | Improve interactive terminal detection and update installation instructions | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										51
									
								
								install.sh
									
									
									
									
									
								
							
							
						
						
									
										51
									
								
								install.sh
									
									
									
									
									
								
							| @@ -74,9 +74,11 @@ if [ ! -t 0 ] || [ ! -t 1 ]; then | |||||||
|     if [ $INTERACTIVE -eq 0 ]; then |     if [ $INTERACTIVE -eq 0 ]; then | ||||||
|       echo "ERROR: No controlling terminal available for interactive prompts." |       echo "ERROR: No controlling terminal available for interactive prompts." | ||||||
|       echo "For interactive installation (RECOMMENDED):" |       echo "For interactive installation (RECOMMENDED):" | ||||||
|       echo "  sudo bash <(curl -sSL https://code.foss.global/serve.zone/nupst/raw/branch/main/install.sh)" |       echo "  curl -sSL https://code.foss.global/serve.zone/nupst/raw/branch/main/install.sh -o nupst-install.sh" | ||||||
|  |       echo "  sudo bash nupst-install.sh" | ||||||
|  |       echo "" | ||||||
|       echo "For non-interactive installation with automatic dependency installation:" |       echo "For non-interactive installation with automatic dependency installation:" | ||||||
|       echo "  sudo bash <(curl -sSL https://code.foss.global/serve.zone/nupst/raw/branch/main/install.sh) -y" |       echo "  curl -sSL https://code.foss.global/serve.zone/nupst/raw/branch/main/install.sh | sudo bash -s -- -y" | ||||||
|       exit 1 |       exit 1 | ||||||
|     else |     else | ||||||
|       echo "Interactive terminal found, continuing with prompts..." |       echo "Interactive terminal found, continuing with prompts..." | ||||||
| @@ -160,10 +162,7 @@ install_git() { | |||||||
| INSTALL_DIR="/opt/nupst" | INSTALL_DIR="/opt/nupst" | ||||||
| REPO_URL="https://code.foss.global/serve.zone/nupst.git" | REPO_URL="https://code.foss.global/serve.zone/nupst.git" | ||||||
|  |  | ||||||
| if [ $PIPED -eq 1 ]; then | # Check if git is installed - needed for both piped and direct execution | ||||||
|   echo "Installing NUPST from remote repository..." |  | ||||||
|    |  | ||||||
|   # Check if git is installed |  | ||||||
| if ! command -v git &> /dev/null; then | if ! command -v git &> /dev/null; then | ||||||
|   echo "Git is required but not installed." |   echo "Git is required but not installed." | ||||||
|    |    | ||||||
| @@ -192,6 +191,9 @@ if [ $PIPED -eq 1 ]; then | |||||||
|   fi |   fi | ||||||
| fi | fi | ||||||
|  |  | ||||||
|  | if [ $PIPED -eq 1 ]; then | ||||||
|  |   echo "Installing NUPST from remote repository..." | ||||||
|  |    | ||||||
|   # Check if installation directory exists |   # Check if installation directory exists | ||||||
|   if [ -d "$INSTALL_DIR" ] && [ -d "$INSTALL_DIR/.git" ]; then |   if [ -d "$INSTALL_DIR" ] && [ -d "$INSTALL_DIR/.git" ]; then | ||||||
|     echo "Existing installation found at $INSTALL_DIR. Updating..." |     echo "Existing installation found at $INSTALL_DIR. Updating..." | ||||||
| @@ -233,12 +235,47 @@ if [ $PIPED -eq 1 ]; then | |||||||
|   # Set script directory to the cloned repo |   # Set script directory to the cloned repo | ||||||
|   SCRIPT_DIR="$INSTALL_DIR" |   SCRIPT_DIR="$INSTALL_DIR" | ||||||
| else | else | ||||||
|   # Running directly from within the repo |   # Running directly from within the repo or downloaded script | ||||||
|   SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" |   SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" | ||||||
|  |    | ||||||
|  |   # When running from a downloaded script in a different location  | ||||||
|  |   # we need to clone the repository first | ||||||
|  |   if [ ! -f "$SCRIPT_DIR/setup.sh" ]; then | ||||||
|  |     echo "Running installer from downloaded script outside repository." | ||||||
|  |     echo "Will clone the repository to $INSTALL_DIR..." | ||||||
|  |      | ||||||
|  |     # Create installation directory if needed | ||||||
|  |     if [ -d "$INSTALL_DIR" ]; then | ||||||
|  |       echo "Removing previous installation at $INSTALL_DIR..." | ||||||
|  |       rm -rf "$INSTALL_DIR" | ||||||
|  |     fi | ||||||
|  |      | ||||||
|  |     mkdir -p "$INSTALL_DIR" | ||||||
|  |      | ||||||
|  |     # Clone the repository | ||||||
|  |     echo "Cloning NUPST repository to $INSTALL_DIR..." | ||||||
|  |     git clone --depth 1 $REPO_URL "$INSTALL_DIR" | ||||||
|  |      | ||||||
|  |     if [ $? -ne 0 ]; then | ||||||
|  |       echo "Failed to clone repository. Please check your internet connection." | ||||||
|  |       exit 1 | ||||||
|  |     fi | ||||||
|  |      | ||||||
|  |     # Update script directory to use the cloned repo | ||||||
|  |     SCRIPT_DIR="$INSTALL_DIR" | ||||||
|  |   fi | ||||||
| fi | fi | ||||||
|  |  | ||||||
| # Run setup script | # Run setup script | ||||||
| echo "Running setup script..." | echo "Running setup script..." | ||||||
|  | if [ ! -f "$SCRIPT_DIR/setup.sh" ]; then | ||||||
|  |   echo "ERROR: Setup script not found at $SCRIPT_DIR/setup.sh" | ||||||
|  |   echo "Current directory: $(pwd)" | ||||||
|  |   echo "Script directory: $SCRIPT_DIR" | ||||||
|  |   ls -la "$SCRIPT_DIR" | ||||||
|  |   exit 1 | ||||||
|  | fi | ||||||
|  |  | ||||||
| bash "$SCRIPT_DIR/setup.sh" | bash "$SCRIPT_DIR/setup.sh" | ||||||
|  |  | ||||||
| # Install globally | # Install globally | ||||||
|   | |||||||
| @@ -1,6 +1,6 @@ | |||||||
| { | { | ||||||
|   "name": "@serve.zone/nupst", |   "name": "@serve.zone/nupst", | ||||||
|   "version": "2.4.5", |   "version": "2.6.0", | ||||||
|   "description": "Node.js UPS Shutdown Tool for SNMP-enabled UPS devices", |   "description": "Node.js UPS Shutdown Tool for SNMP-enabled UPS devices", | ||||||
|   "main": "dist/index.js", |   "main": "dist/index.js", | ||||||
|   "bin": { |   "bin": { | ||||||
| @@ -36,7 +36,9 @@ | |||||||
|   ], |   ], | ||||||
|   "author": "", |   "author": "", | ||||||
|   "license": "MIT", |   "license": "MIT", | ||||||
|   "dependencies": {}, |   "dependencies": { | ||||||
|  |     "net-snmp": "3.20.0" | ||||||
|  |   }, | ||||||
|   "devDependencies": { |   "devDependencies": { | ||||||
|     "@git.zone/tsbuild": "^2.3.2", |     "@git.zone/tsbuild": "^2.3.2", | ||||||
|     "@git.zone/tsrun": "^1.3.3", |     "@git.zone/tsrun": "^1.3.3", | ||||||
|   | |||||||
							
								
								
									
										17
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										17
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							| @@ -7,6 +7,10 @@ settings: | |||||||
| importers: | importers: | ||||||
|  |  | ||||||
|   .: |   .: | ||||||
|  |     dependencies: | ||||||
|  |       net-snmp: | ||||||
|  |         specifier: ^3.20.0 | ||||||
|  |         version: 3.20.0 | ||||||
|     devDependencies: |     devDependencies: | ||||||
|       '@git.zone/tsbuild': |       '@git.zone/tsbuild': | ||||||
|         specifier: ^2.3.2 |         specifier: ^2.3.2 | ||||||
| @@ -1647,6 +1651,9 @@ packages: | |||||||
|     resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} |     resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} | ||||||
|     engines: {node: '>=8'} |     engines: {node: '>=8'} | ||||||
|  |  | ||||||
|  |   asn1-ber@1.2.2: | ||||||
|  |     resolution: {integrity: sha512-CbNem/7hxrjSiOAOOTX4iZxu+0m3jiLqlsERQwwPM1IDR/22M8IPpA1VVndCLw5KtjRYyRODbvAEIfuTogNDng==} | ||||||
|  |  | ||||||
|   ast-types@0.13.4: |   ast-types@0.13.4: | ||||||
|     resolution: {integrity: sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==} |     resolution: {integrity: sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==} | ||||||
|     engines: {node: '>=4'} |     engines: {node: '>=4'} | ||||||
| @@ -3303,6 +3310,9 @@ packages: | |||||||
|     resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} |     resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} | ||||||
|     engines: {node: '>= 0.6'} |     engines: {node: '>= 0.6'} | ||||||
|  |  | ||||||
|  |   net-snmp@3.20.0: | ||||||
|  |     resolution: {integrity: sha512-4Cp8ODkzgVXjUrIQFfL9Vo6qVsz+8OuAjUvkRGsSZOKSpoxpy9YWjVgNs+/a9N4Hd9MilIy90Zhw3EZlUUZB6A==} | ||||||
|  |  | ||||||
|   netmask@2.0.2: |   netmask@2.0.2: | ||||||
|     resolution: {integrity: sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==} |     resolution: {integrity: sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==} | ||||||
|     engines: {node: '>= 0.4.0'} |     engines: {node: '>= 0.4.0'} | ||||||
| @@ -7181,6 +7191,8 @@ snapshots: | |||||||
|  |  | ||||||
|   array-union@2.1.0: {} |   array-union@2.1.0: {} | ||||||
|  |  | ||||||
|  |   asn1-ber@1.2.2: {} | ||||||
|  |  | ||||||
|   ast-types@0.13.4: |   ast-types@0.13.4: | ||||||
|     dependencies: |     dependencies: | ||||||
|       tslib: 2.8.1 |       tslib: 2.8.1 | ||||||
| @@ -9133,6 +9145,11 @@ snapshots: | |||||||
|  |  | ||||||
|   negotiator@0.6.3: {} |   negotiator@0.6.3: {} | ||||||
|  |  | ||||||
|  |   net-snmp@3.20.0: | ||||||
|  |     dependencies: | ||||||
|  |       asn1-ber: 1.2.2 | ||||||
|  |       smart-buffer: 4.2.0 | ||||||
|  |  | ||||||
|   netmask@2.0.2: {} |   netmask@2.0.2: {} | ||||||
|  |  | ||||||
|   new-find-package-json@2.0.0: |   new-find-package-json@2.0.0: | ||||||
|   | |||||||
							
								
								
									
										52
									
								
								readme.md
									
									
									
									
									
								
							
							
						
						
									
										52
									
								
								readme.md
									
									
									
									
									
								
							| @@ -19,18 +19,21 @@ NUPST is a command-line tool that monitors SNMP-enabled UPS devices and initiate | |||||||
| ### Quick Install (One-line command) | ### Quick Install (One-line command) | ||||||
|  |  | ||||||
| ```bash | ```bash | ||||||
| # RECOMMENDED: Install interactively using process substitution (requires root privileges) | # Method 1: Download and run (most reliable across all environments) | ||||||
| # This ensures proper interactive prompts for dependencies | curl -sSL https://code.foss.global/serve.zone/nupst/raw/branch/main/install.sh -o nupst-install.sh && sudo bash nupst-install.sh && rm nupst-install.sh | ||||||
| sudo bash <(curl -sSL https://code.foss.global/serve.zone/nupst/raw/branch/main/install.sh) | ``` | ||||||
|  |  | ||||||
| # Alternative: Install with auto-yes for dependencies (will install git automatically if needed) | ```bash | ||||||
| # Use this for automated/non-interactive installations | # Method 2: Pipe with automatic yes for dependencies (non-interactive) | ||||||
| sudo bash <(curl -sSL https://code.foss.global/serve.zone/nupst/raw/branch/main/install.sh) -y |  | ||||||
|  |  | ||||||
| # For systems where process substitution is not available (e.g., some older shells) |  | ||||||
| curl -sSL https://code.foss.global/serve.zone/nupst/raw/branch/main/install.sh | sudo bash -s -- -y | curl -sSL https://code.foss.global/serve.zone/nupst/raw/branch/main/install.sh | sudo bash -s -- -y | ||||||
| ``` | ``` | ||||||
|  |  | ||||||
|  | ```bash | ||||||
|  | # Method 3: Process substitution (only on systems that support /dev/fd/) | ||||||
|  | # Note: This may fail on some systems with "No such file or directory" errors | ||||||
|  | sudo bash <(curl -sSL https://code.foss.global/serve.zone/nupst/raw/branch/main/install.sh) | ||||||
|  | ``` | ||||||
|  |  | ||||||
| ### Direct from Git | ### Direct from Git | ||||||
|  |  | ||||||
| ```bash | ```bash | ||||||
| @@ -224,8 +227,15 @@ sudo nupst update | |||||||
| This will: | This will: | ||||||
| 1. Pull the latest changes from the git repository | 1. Pull the latest changes from the git repository | ||||||
| 2. Run the installation scripts | 2. Run the installation scripts | ||||||
| 3. Refresh the systemd service configuration | 3. Force-update Node.js and all dependencies, even if they already exist | ||||||
| 4. Restart the service if it was running | 4. Refresh the systemd service configuration | ||||||
|  | 5. Restart the service if it was running | ||||||
|  |  | ||||||
|  | You can also manually run the setup script with the force flag to update Node.js and dependencies without updating the application code: | ||||||
|  |  | ||||||
|  | ```bash | ||||||
|  | bash setup.sh --force | ||||||
|  | ``` | ||||||
|  |  | ||||||
| ## Security | ## Security | ||||||
|  |  | ||||||
| @@ -233,10 +243,10 @@ NUPST was designed with security in mind: | |||||||
|  |  | ||||||
| ### Minimal Dependencies | ### Minimal Dependencies | ||||||
|  |  | ||||||
| - **Zero Runtime NPM Dependencies**: NUPST is built without any external NPM packages to minimize the attack surface and avoid supply chain risks. | - **Minimal Runtime Dependencies**: NUPST uses only one carefully selected NPM package (net-snmp) to minimize the attack surface and avoid supply chain risks while providing robust SNMP functionality. | ||||||
| - **Self-contained Node.js**: NUPST ships with its own Node.js binary, isolated from the system's Node.js installation. This ensures: | - **Self-contained Node.js**: NUPST ships with its own Node.js binary, isolated from the system's Node.js installation. This ensures: | ||||||
|   - No dependency on system Node.js versions |   - No dependency on system Node.js versions | ||||||
|   - Zero external libraries that could become compromised |   - Minimal external libraries that could become compromised | ||||||
|   - Consistent, tested environment for execution |   - Consistent, tested environment for execution | ||||||
|   - Reduced risk of dependency-based attacks |   - Reduced risk of dependency-based attacks | ||||||
|  |  | ||||||
| @@ -244,14 +254,30 @@ NUPST was designed with security in mind: | |||||||
|  |  | ||||||
| - **Privilege Separation**: Only specific commands that require elevated permissions (`enable`, `disable`, `update`) check for root access; all other functionality runs with minimal privileges. | - **Privilege Separation**: Only specific commands that require elevated permissions (`enable`, `disable`, `update`) check for root access; all other functionality runs with minimal privileges. | ||||||
| - **Limited Network Access**: NUPST only communicates with the UPS device over SNMP and contacts npmjs.org only to check for updates. | - **Limited Network Access**: NUPST only communicates with the UPS device over SNMP and contacts npmjs.org only to check for updates. | ||||||
| - **Secure SNMPv3 Support**: Supports encrypted authentication and privacy for secure communication with the UPS device. |  | ||||||
| - **Isolated Execution**: The application runs in its working directory (`/opt/nupst`) or specified installation location, minimizing the impact on the rest of the system. | - **Isolated Execution**: The application runs in its working directory (`/opt/nupst`) or specified installation location, minimizing the impact on the rest of the system. | ||||||
|  |  | ||||||
|  | ### SNMP Security Features | ||||||
|  |  | ||||||
|  | - **SNMPv3 Support with Secure Authentication and Privacy**: | ||||||
|  |   - Three security levels available: | ||||||
|  |     - `noAuthNoPriv`: No authentication or encryption (basic access) | ||||||
|  |     - `authNoPriv`: Authentication without encryption (verifies identity) | ||||||
|  |     - `authPriv`: Full authentication and encryption (most secure) | ||||||
|  |   - Authentication protocols: MD5 or SHA | ||||||
|  |   - Privacy/encryption protocols: DES or AES | ||||||
|  |   - Automatic fallback mechanisms for compatibility | ||||||
|  |   - Context support for segmented SNMP deployments | ||||||
|  |   - Configurable timeouts based on security level | ||||||
|  | - **Graceful degradation**: If authentication or privacy details are missing or invalid, NUPST will automatically fall back to a lower security level while logging appropriate warnings. | ||||||
|  | - **Interactive setup**: Guided setup process to properly configure SNMPv3 security settings with clear explanations of each security option. | ||||||
|  |  | ||||||
| ### Installation Security | ### Installation Security | ||||||
|  |  | ||||||
| - The installation script can be reviewed before execution (`curl -sSL [url] | less`) | - The installation script can be reviewed before execution (`curl -sSL [url] | less`) | ||||||
| - All setup scripts download only verified versions and check integrity | - All setup scripts download only verified versions and check integrity | ||||||
| - Installation is transparent and places files in standard locations (`/opt/nupst`, `/usr/local/bin`, `/etc/systemd/system`) | - Installation is transparent and places files in standard locations (`/opt/nupst`, `/usr/local/bin`, `/etc/systemd/system`) | ||||||
|  | - Automatically detects platform architecture and OS for proper binary selection | ||||||
|  | - Installs production dependencies locally without requiring global npm packages | ||||||
|  |  | ||||||
| ### Audit and Review | ### Audit and Review | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										38
									
								
								setup.sh
									
									
									
									
									
								
							
							
						
						
									
										38
									
								
								setup.sh
									
									
									
									
									
								
							| @@ -2,6 +2,22 @@ | |||||||
|  |  | ||||||
| # NUPST Setup Script | # NUPST Setup Script | ||||||
| # Downloads the appropriate Node.js binary for the current platform | # Downloads the appropriate Node.js binary for the current platform | ||||||
|  | # and installs production dependencies | ||||||
|  |  | ||||||
|  | # Parse command line arguments | ||||||
|  | FORCE_UPDATE=0 | ||||||
|  |  | ||||||
|  | for arg in "$@"; do | ||||||
|  |   case $arg in | ||||||
|  |     --force|-f) | ||||||
|  |       FORCE_UPDATE=1 | ||||||
|  |       shift | ||||||
|  |       ;; | ||||||
|  |     *) | ||||||
|  |       # Unknown option | ||||||
|  |       ;; | ||||||
|  |   esac | ||||||
|  | done | ||||||
|  |  | ||||||
| # Find the directory where this script is located | # Find the directory where this script is located | ||||||
| SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" | SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" | ||||||
| @@ -74,8 +90,9 @@ case "$OS" in | |||||||
| esac | esac | ||||||
|  |  | ||||||
| # Check if we already have the Node.js binary | # Check if we already have the Node.js binary | ||||||
| if [ -f "$SCRIPT_DIR/vendor/$NODE_DIR/bin/node" ]; then | if [ -f "$SCRIPT_DIR/vendor/$NODE_DIR/bin/node" ] && [ $FORCE_UPDATE -eq 0 ]; then | ||||||
|   echo "Node.js binary already exists for $OS-$ARCH. Skipping download." |   echo "Node.js binary already exists for $OS-$ARCH. Skipping download." | ||||||
|  |   echo "Use --force or -f to force update Node.js." | ||||||
| else | else | ||||||
|   echo "Downloading Node.js v$NODE_VERSION for $OS-$ARCH..." |   echo "Downloading Node.js v$NODE_VERSION for $OS-$ARCH..." | ||||||
|    |    | ||||||
| @@ -222,6 +239,25 @@ echo "dist_ts directory successfully downloaded from npm registry." | |||||||
| # Make launcher script executable | # Make launcher script executable | ||||||
| chmod +x "$SCRIPT_DIR/bin/nupst" | chmod +x "$SCRIPT_DIR/bin/nupst" | ||||||
|  |  | ||||||
|  | # Install production dependencies | ||||||
|  | echo "Installing production dependencies..." | ||||||
|  | "$SCRIPT_DIR/vendor/$NODE_DIR/bin/npm" --prefix "$SCRIPT_DIR" ci --only=production --no-audit --no-fund | ||||||
|  |  | ||||||
|  | if [ $? -ne 0 ]; then | ||||||
|  |   echo "Warning: Failed to install dependencies with 'npm ci'. Trying 'npm install'..." | ||||||
|  |   "$SCRIPT_DIR/vendor/$NODE_DIR/bin/npm" --prefix "$SCRIPT_DIR" install --only=production --no-audit --no-fund | ||||||
|  |    | ||||||
|  |   if [ $? -ne 0 ]; then | ||||||
|  |     echo "Error: Failed to install dependencies. NUPST may not function correctly." | ||||||
|  |     echo "You can try to install dependencies manually by running:" | ||||||
|  |     echo "cd $SCRIPT_DIR && npm install --only=production" | ||||||
|  |   else | ||||||
|  |     echo "Dependencies installed successfully with 'npm install'." | ||||||
|  |   fi | ||||||
|  | else | ||||||
|  |   echo "Dependencies installed successfully with 'npm ci'." | ||||||
|  | fi | ||||||
|  |  | ||||||
| echo "NUPST setup completed successfully." | echo "NUPST setup completed successfully." | ||||||
| echo "You can now run NUPST using: $SCRIPT_DIR/bin/nupst" | echo "You can now run NUPST using: $SCRIPT_DIR/bin/nupst" | ||||||
| echo "To install NUPST globally, run: sudo ln -s $SCRIPT_DIR/bin/nupst /usr/local/bin/nupst" | echo "To install NUPST globally, run: sudo ln -s $SCRIPT_DIR/bin/nupst /usr/local/bin/nupst" | ||||||
|   | |||||||
							
								
								
									
										329
									
								
								test/test.ts
									
									
									
									
									
								
							
							
						
						
									
										329
									
								
								test/test.ts
									
									
									
									
									
								
							| @@ -1,9 +1,6 @@ | |||||||
| import { tap, expect } from '@push.rocks/tapbundle'; | import { tap, expect } from '@push.rocks/tapbundle'; | ||||||
| import { NupstSnmp } from '../ts/snmp.js'; | import { NupstSnmp } from '../ts/snmp/manager.js'; | ||||||
| import type { SnmpConfig, UpsStatus } from '../ts/snmp.js'; | import type { ISnmpConfig, IUpsStatus } from '../ts/snmp/types.js'; | ||||||
| import { SnmpEncoder } from '../ts/snmp/encoder.js'; |  | ||||||
| import { SnmpPacketCreator } from '../ts/snmp/packet-creator.js'; |  | ||||||
| import { SnmpPacketParser } from '../ts/snmp/packet-parser.js'; |  | ||||||
|  |  | ||||||
| import * as qenv from '@push.rocks/qenv'; | import * as qenv from '@push.rocks/qenv'; | ||||||
| const testQenv = new qenv.Qenv('./', '.nogit/'); | const testQenv = new qenv.Qenv('./', '.nogit/'); | ||||||
| @@ -12,295 +9,57 @@ const testQenv = new qenv.Qenv('./', '.nogit/'); | |||||||
| const snmp = new NupstSnmp(true); | const snmp = new NupstSnmp(true); | ||||||
|  |  | ||||||
| // Load the test configuration from .nogit/env.json  | // Load the test configuration from .nogit/env.json  | ||||||
| const testConfig = await testQenv.getEnvVarOnDemandAsObject('testConfig'); | const testConfigV1 = await testQenv.getEnvVarOnDemandAsObject('testConfigV1'); | ||||||
|  | const testConfigV3 = await testQenv.getEnvVarOnDemandAsObject('testConfigV3'); | ||||||
|  |  | ||||||
| tap.test('should log config', async () => { | tap.test('should log config', async () => { | ||||||
|   console.log(testConfig); |   console.log(testConfigV1); | ||||||
| }); |  | ||||||
|  |  | ||||||
| tap.test('SNMP packet creation and parsing test', async () => { |  | ||||||
|   // We'll test the internal methods that are now in separate classes |  | ||||||
|    |  | ||||||
|   // Test OID conversion |  | ||||||
|   const oidStr = '1.3.6.1.4.1.3808.1.1.1.4.1.1.0'; |  | ||||||
|   const oidArray = SnmpEncoder.oidToArray(oidStr); |  | ||||||
|   console.log('OID array length:', oidArray.length); |  | ||||||
|   console.log('OID array:', oidArray); |  | ||||||
|   // The OID has 14 elements after splitting |  | ||||||
|   expect(oidArray.length).toEqual(14); |  | ||||||
|   expect(oidArray[0]).toEqual(1); |  | ||||||
|   expect(oidArray[1]).toEqual(3); |  | ||||||
|    |  | ||||||
|   // Test OID encoding |  | ||||||
|   const encodedOid = SnmpEncoder.encodeOID(oidArray); |  | ||||||
|   expect(encodedOid).toBeInstanceOf(Buffer); |  | ||||||
|    |  | ||||||
|   // Test SNMP request creation |  | ||||||
|   const request = SnmpPacketCreator.createSnmpGetRequest(oidStr, 'public', true); |  | ||||||
|   expect(request).toBeInstanceOf(Buffer); |  | ||||||
|   expect(request.length).toBeGreaterThan(20); |  | ||||||
|    |  | ||||||
|   // Log the request for debugging |  | ||||||
|   console.log('SNMP Request buffer:', request.toString('hex')); |  | ||||||
|    |  | ||||||
|   // Test integer encoding |  | ||||||
|   const int = SnmpEncoder.encodeInteger(42); |  | ||||||
|   expect(int).toBeInstanceOf(Buffer); |  | ||||||
|   expect(int.length).toBeGreaterThanOrEqual(1); |  | ||||||
|    |  | ||||||
|   // Test SNMPv3 engine ID discovery message |  | ||||||
|   const discoveryMsg = SnmpPacketCreator.createDiscoveryMessage(testConfig, 1); |  | ||||||
|   expect(discoveryMsg).toBeInstanceOf(Buffer); |  | ||||||
|   expect(discoveryMsg.length).toBeGreaterThan(20); |  | ||||||
|    |  | ||||||
|   console.log('SNMPv3 Discovery message:', discoveryMsg.toString('hex')); |  | ||||||
| }); |  | ||||||
|  |  | ||||||
| tap.test('SNMP response parsing simulation', async () => { |  | ||||||
|   // Create a simulated SNMP response for parsing |  | ||||||
|    |  | ||||||
|   // Simulate an INTEGER response (battery capacity) |  | ||||||
|   const intResponse = Buffer.from([ |  | ||||||
|     0x30, 0x29, // Sequence, length 41 |  | ||||||
|     0x02, 0x01, 0x00, // Integer (version), value 0 |  | ||||||
|     0x04, 0x06, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, // Octet String (community), value "public" |  | ||||||
|     0xa2, 0x1c, // GetResponse |  | ||||||
|     0x02, 0x01, 0x01, // Integer (request ID), value 1 |  | ||||||
|     0x02, 0x01, 0x00, // Integer (error status), value 0 |  | ||||||
|     0x02, 0x01, 0x00, // Integer (error index), value 0 |  | ||||||
|     0x30, 0x11, // Sequence (varbinds) |  | ||||||
|     0x30, 0x0f, // Sequence (varbind) |  | ||||||
|     0x06, 0x0b, 0x2b, 0x06, 0x01, 0x04, 0x01, 0x8c, 0x10, 0x01, 0x02, 0x01, 0x00, // OID (example) |  | ||||||
|     0x02, 0x01, 0x64 // Integer (value), value 100 (100%) |  | ||||||
|   ]); |  | ||||||
|    |  | ||||||
|   // Simulate a Gauge32 response (battery capacity) |  | ||||||
|   const gauge32Response = Buffer.from([ |  | ||||||
|     0x30, 0x29, // Sequence, length 41 |  | ||||||
|     0x02, 0x01, 0x00, // Integer (version), value 0 |  | ||||||
|     0x04, 0x06, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, // Octet String (community), value "public" |  | ||||||
|     0xa2, 0x1c, // GetResponse |  | ||||||
|     0x02, 0x01, 0x01, // Integer (request ID), value 1 |  | ||||||
|     0x02, 0x01, 0x00, // Integer (error status), value 0 |  | ||||||
|     0x02, 0x01, 0x00, // Integer (error index), value 0 |  | ||||||
|     0x30, 0x11, // Sequence (varbinds) |  | ||||||
|     0x30, 0x0f, // Sequence (varbind) |  | ||||||
|     0x06, 0x0b, 0x2b, 0x06, 0x01, 0x04, 0x01, 0x8c, 0x10, 0x01, 0x02, 0x01, 0x00, // OID (example) |  | ||||||
|     0x42, 0x01, 0x64 // Gauge32 (value), value 100 (100%) |  | ||||||
|   ]); |  | ||||||
|    |  | ||||||
|   // Simulate a TimeTicks response (battery runtime) |  | ||||||
|   const timeTicksResponse = Buffer.from([ |  | ||||||
|     0x30, 0x29, // Sequence, length 41 |  | ||||||
|     0x02, 0x01, 0x00, // Integer (version), value 0 |  | ||||||
|     0x04, 0x06, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, // Octet String (community), value "public" |  | ||||||
|     0xa2, 0x1c, // GetResponse |  | ||||||
|     0x02, 0x01, 0x01, // Integer (request ID), value 1 |  | ||||||
|     0x02, 0x01, 0x00, // Integer (error status), value 0 |  | ||||||
|     0x02, 0x01, 0x00, // Integer (error index), value 0 |  | ||||||
|     0x30, 0x11, // Sequence (varbinds) |  | ||||||
|     0x30, 0x0f, // Sequence (varbind) |  | ||||||
|     0x06, 0x0b, 0x2b, 0x06, 0x01, 0x04, 0x01, 0x8c, 0x10, 0x01, 0x02, 0x01, 0x00, // OID (example) |  | ||||||
|     0x43, 0x01, 0x0f // TimeTicks (value), value 15 (0.15 seconds or 15/100 seconds) |  | ||||||
|   ]); |  | ||||||
|    |  | ||||||
|   // Test parsing INTEGER response |  | ||||||
|   const intValue = SnmpPacketParser.parseSnmpResponse(intResponse, testConfig, true); |  | ||||||
|   console.log('Parsed INTEGER value:', intValue); |  | ||||||
|   expect(intValue).toEqual(100); |  | ||||||
|    |  | ||||||
|   // Test parsing Gauge32 response |  | ||||||
|   const gauge32Value = SnmpPacketParser.parseSnmpResponse(gauge32Response, testConfig, true); |  | ||||||
|   console.log('Parsed Gauge32 value:', gauge32Value); |  | ||||||
|   expect(gauge32Value).toEqual(100); |  | ||||||
|    |  | ||||||
|   // Test parsing TimeTicks response |  | ||||||
|   const timeTicksValue = SnmpPacketParser.parseSnmpResponse(timeTicksResponse, testConfig, true); |  | ||||||
|   console.log('Parsed TimeTicks value:', timeTicksValue); |  | ||||||
|   expect(timeTicksValue).toEqual(15); |  | ||||||
| }); |  | ||||||
|  |  | ||||||
| tap.test('CyberPower TimeTicks conversion', async () => { |  | ||||||
|   // Test the conversion of TimeTicks to minutes for CyberPower UPS |  | ||||||
|    |  | ||||||
|   // Set up a config for CyberPower |  | ||||||
|   const cyberPowerConfig: SnmpConfig = { |  | ||||||
|     ...testConfig, |  | ||||||
|     upsModel: 'cyberpower' |  | ||||||
|   }; |  | ||||||
|    |  | ||||||
|   // Create a simulated TimeTicks response with a value of 104 (104/100 seconds) |  | ||||||
|   const ticksResponse = Buffer.from([ |  | ||||||
|     0x30, 0x29, // Sequence |  | ||||||
|     0x02, 0x01, 0x00, // Integer (version), value 0 |  | ||||||
|     0x04, 0x06, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, // Octet String (community), value "public" |  | ||||||
|     0xa2, 0x1c, // GetResponse |  | ||||||
|     0x02, 0x01, 0x01, // Integer (request ID), value 1 |  | ||||||
|     0x02, 0x01, 0x00, // Integer (error status), value 0 |  | ||||||
|     0x02, 0x01, 0x00, // Integer (error index), value 0 |  | ||||||
|     0x30, 0x11, // Sequence (varbinds) |  | ||||||
|     0x30, 0x0f, // Sequence (varbind) |  | ||||||
|     0x06, 0x0b, 0x2b, 0x06, 0x01, 0x04, 0x01, 0x8c, 0x10, 0x01, 0x02, 0x04, 0x00, // OID (battery runtime) |  | ||||||
|     0x43, 0x01, 0x68 // TimeTicks (value), value 104 (104/100 seconds) |  | ||||||
|   ]); |  | ||||||
|    |  | ||||||
|   // Mock the getUpsStatus function to test our TimeTicks conversion logic |  | ||||||
|   const mockGetUpsStatus = async () => { |  | ||||||
|     // Parse the TimeTicks value from the response |  | ||||||
|     const runtime = SnmpPacketParser.parseSnmpResponse(ticksResponse, testConfig, true); |  | ||||||
|     console.log('Raw runtime value:', runtime); |  | ||||||
|      |  | ||||||
|     // Create a sample UPS status result |  | ||||||
|     const result = { |  | ||||||
|       powerStatus: 'onBattery', |  | ||||||
|       batteryCapacity: 100, |  | ||||||
|       batteryRuntime: 0, |  | ||||||
|       raw: { |  | ||||||
|         powerStatus: 2, |  | ||||||
|         batteryCapacity: 100, |  | ||||||
|         batteryRuntime: runtime, |  | ||||||
|       }, |  | ||||||
|     }; |  | ||||||
|      |  | ||||||
|     // Convert TimeTicks to minutes for CyberPower |  | ||||||
|     if (cyberPowerConfig.upsModel === 'cyberpower' && runtime > 0) { |  | ||||||
|       result.batteryRuntime = Math.floor(runtime / 6000); |  | ||||||
|       console.log(`Converting CyberPower runtime from ${runtime} ticks to ${result.batteryRuntime} minutes`); |  | ||||||
|     } else { |  | ||||||
|       result.batteryRuntime = runtime; |  | ||||||
|     } |  | ||||||
|      |  | ||||||
|     return result; |  | ||||||
|   }; |  | ||||||
|    |  | ||||||
|   // Call our mock function |  | ||||||
|   const status = await mockGetUpsStatus(); |  | ||||||
|    |  | ||||||
|   // Assert the conversion worked correctly |  | ||||||
|   console.log('Final status object:', status); |  | ||||||
|   expect(status.batteryRuntime).toEqual(0); // 104 ticks / 6000 = 0.0173... rounds to 0 minutes |  | ||||||
| }); |  | ||||||
|  |  | ||||||
| tap.test('Simulate fully charged online UPS', async () => { |  | ||||||
|   // Test a realistic scenario of an online UPS with high battery capacity and ~30 mins runtime |  | ||||||
|    |  | ||||||
|   // Create simulated responses for power status (online), battery capacity (95%), runtime (30 min) |  | ||||||
|    |  | ||||||
|   // Power Status = 2 (online for CyberPower) |  | ||||||
|   const powerStatusResponse = Buffer.from([ |  | ||||||
|     0x30, 0x29, // Sequence |  | ||||||
|     0x02, 0x01, 0x00, // Integer (version), value 0 |  | ||||||
|     0x04, 0x06, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, // Octet String (community), value "public" |  | ||||||
|     0xa2, 0x1c, // GetResponse |  | ||||||
|     0x02, 0x01, 0x01, // Integer (request ID), value 1 |  | ||||||
|     0x02, 0x01, 0x00, // Integer (error status), value 0 |  | ||||||
|     0x02, 0x01, 0x00, // Integer (error index), value 0 |  | ||||||
|     0x30, 0x11, // Sequence (varbinds) |  | ||||||
|     0x30, 0x0f, // Sequence (varbind) |  | ||||||
|     0x06, 0x0b, 0x2b, 0x06, 0x01, 0x04, 0x01, 0xed, 0x08, 0x01, 0x01, 0x01, 0x00, // OID (power status) |  | ||||||
|     0x02, 0x01, 0x02 // Integer (value), value 2 (online) |  | ||||||
|   ]); |  | ||||||
|    |  | ||||||
|   // Battery Capacity = 95% (as Gauge32) |  | ||||||
|   const batteryCapacityResponse = Buffer.from([ |  | ||||||
|     0x30, 0x29, // Sequence |  | ||||||
|     0x02, 0x01, 0x00, // Integer (version), value 0 |  | ||||||
|     0x04, 0x06, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, // Octet String (community), value "public" |  | ||||||
|     0xa2, 0x1c, // GetResponse |  | ||||||
|     0x02, 0x01, 0x02, // Integer (request ID), value 2 |  | ||||||
|     0x02, 0x01, 0x00, // Integer (error status), value 0 |  | ||||||
|     0x02, 0x01, 0x00, // Integer (error index), value 0 |  | ||||||
|     0x30, 0x11, // Sequence (varbinds) |  | ||||||
|     0x30, 0x0f, // Sequence (varbind) |  | ||||||
|     0x06, 0x0b, 0x2b, 0x06, 0x01, 0x04, 0x01, 0xed, 0x08, 0x01, 0x02, 0x01, 0x00, // OID (battery capacity) |  | ||||||
|     0x42, 0x01, 0x5F // Gauge32 (value), value 95 (95%) |  | ||||||
|   ]); |  | ||||||
|    |  | ||||||
|   // Battery Runtime = 30 minutes (as TimeTicks) |  | ||||||
|   // 30 minutes = 1800 seconds = 180000 ticks (in 1/100 seconds) |  | ||||||
|   const batteryRuntimeResponse = Buffer.from([ |  | ||||||
|     0x30, 0x2c, // Sequence |  | ||||||
|     0x02, 0x01, 0x00, // Integer (version), value 0 |  | ||||||
|     0x04, 0x06, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, // Octet String (community), value "public" |  | ||||||
|     0xa2, 0x1f, // GetResponse |  | ||||||
|     0x02, 0x01, 0x03, // Integer (request ID), value 3 |  | ||||||
|     0x02, 0x01, 0x00, // Integer (error status), value 0 |  | ||||||
|     0x02, 0x01, 0x00, // Integer (error index), value 0 |  | ||||||
|     0x30, 0x14, // Sequence (varbinds) |  | ||||||
|     0x30, 0x12, // Sequence (varbind) |  | ||||||
|     0x06, 0x0b, 0x2b, 0x06, 0x01, 0x04, 0x01, 0xed, 0x08, 0x01, 0x02, 0x04, 0x00, // OID (battery runtime) |  | ||||||
|     0x43, 0x04, 0x00, 0x02, 0xBF, 0x20 // TimeTicks (value), value 180000 (1800 seconds = 30 minutes) |  | ||||||
|   ]); |  | ||||||
|    |  | ||||||
|   // Mock the getUpsStatus function to test with our simulated data |  | ||||||
|   const mockGetUpsStatus = async () => { |  | ||||||
|     console.log('Simulating UPS status request with synthetic data'); |  | ||||||
|      |  | ||||||
|     // Create a config that specifies this is a CyberPower UPS |  | ||||||
|     const upsConfig: SnmpConfig = { |  | ||||||
|       host: '192.168.1.1', |  | ||||||
|       port: 161, |  | ||||||
|       version: 1, |  | ||||||
|       community: 'public', |  | ||||||
|       timeout: 5000, |  | ||||||
|       upsModel: 'cyberpower', |  | ||||||
|     }; |  | ||||||
|      |  | ||||||
|     // Parse each simulated response |  | ||||||
|     const powerStatus = SnmpPacketParser.parseSnmpResponse(powerStatusResponse, upsConfig, true); |  | ||||||
|     console.log('Power status value:', powerStatus); |  | ||||||
|      |  | ||||||
|     const batteryCapacity = SnmpPacketParser.parseSnmpResponse(batteryCapacityResponse, upsConfig, true); |  | ||||||
|     console.log('Battery capacity value:', batteryCapacity); |  | ||||||
|      |  | ||||||
|     const batteryRuntime = SnmpPacketParser.parseSnmpResponse(batteryRuntimeResponse, upsConfig, true); |  | ||||||
|     console.log('Battery runtime value:', batteryRuntime); |  | ||||||
|      |  | ||||||
|     // Convert TimeTicks to minutes for CyberPower UPSes |  | ||||||
|     const runtimeMinutes = Math.floor(batteryRuntime / 6000); |  | ||||||
|     console.log(`Converting ${batteryRuntime} ticks to ${runtimeMinutes} minutes`); |  | ||||||
|      |  | ||||||
|     // Interpret power status for CyberPower |  | ||||||
|     // CyberPower: 2=online, 3=on battery |  | ||||||
|     let powerStatusText: 'online' | 'onBattery' | 'unknown' = 'unknown'; |  | ||||||
|     if (powerStatus === 2) { |  | ||||||
|       powerStatusText = 'online'; |  | ||||||
|     } else if (powerStatus === 3) { |  | ||||||
|       powerStatusText = 'onBattery'; |  | ||||||
|     } |  | ||||||
|      |  | ||||||
|     // Create the status result |  | ||||||
|     const result: UpsStatus = { |  | ||||||
|       powerStatus: powerStatusText, |  | ||||||
|       batteryCapacity: batteryCapacity, |  | ||||||
|       batteryRuntime: runtimeMinutes, |  | ||||||
|       raw: { |  | ||||||
|         powerStatus, |  | ||||||
|         batteryCapacity, |  | ||||||
|         batteryRuntime, |  | ||||||
|       }, |  | ||||||
|     }; |  | ||||||
|      |  | ||||||
|     return result; |  | ||||||
|   }; |  | ||||||
|    |  | ||||||
|   // Call our mock function |  | ||||||
|   const status = await mockGetUpsStatus(); |  | ||||||
|    |  | ||||||
|   // Assert that the values match our expectations |  | ||||||
|   console.log('UPS Status Result:', status); |  | ||||||
|   expect(status.powerStatus).toEqual('online'); |  | ||||||
|   expect(status.batteryCapacity).toEqual(95); |  | ||||||
|   expect(status.batteryRuntime).toEqual(30); |  | ||||||
| }); | }); | ||||||
|  |  | ||||||
| // Test with real UPS using the configuration from .nogit/env.json | // Test with real UPS using the configuration from .nogit/env.json | ||||||
| tap.test('Real UPS test', async () => { | tap.test('Real UPS test v1', async () => { | ||||||
|   try { |   try { | ||||||
|     console.log('Testing with real UPS configuration...'); |     console.log('Testing with real UPS configuration...'); | ||||||
|      |      | ||||||
|     // Extract the correct SNMP config from the test configuration |     // Extract the correct SNMP config from the test configuration | ||||||
|     const snmpConfig = testConfig.snmp; |     const snmpConfig = testConfigV1.snmp; | ||||||
|  |     console.log('SNMP Config:'); | ||||||
|  |     console.log(`  Host: ${snmpConfig.host}:${snmpConfig.port}`); | ||||||
|  |     console.log(`  Version: SNMPv${snmpConfig.version}`); | ||||||
|  |     console.log(`  UPS Model: ${snmpConfig.upsModel}`); | ||||||
|  |      | ||||||
|  |     // Use a short timeout for testing | ||||||
|  |     const testSnmpConfig = {  | ||||||
|  |       ...snmpConfig, | ||||||
|  |       timeout: Math.min(snmpConfig.timeout, 10000) // Use at most 10 seconds for testing | ||||||
|  |     }; | ||||||
|  |      | ||||||
|  |     // Try to get the UPS status | ||||||
|  |     const status = await snmp.getUpsStatus(testSnmpConfig); | ||||||
|  |      | ||||||
|  |     console.log('UPS Status:'); | ||||||
|  |     console.log(`  Power Status: ${status.powerStatus}`); | ||||||
|  |     console.log(`  Battery Capacity: ${status.batteryCapacity}%`); | ||||||
|  |     console.log(`  Runtime Remaining: ${status.batteryRuntime} minutes`); | ||||||
|  |      | ||||||
|  |     // Just make sure we got valid data types back | ||||||
|  |     expect(status).toBeTruthy(); | ||||||
|  |     expect(['online', 'onBattery', 'unknown']).toContain(status.powerStatus); | ||||||
|  |     expect(typeof status.batteryCapacity).toEqual('number'); | ||||||
|  |     expect(typeof status.batteryRuntime).toEqual('number'); | ||||||
|  |   } catch (error) { | ||||||
|  |     console.log('Real UPS test failed:', error); | ||||||
|  |     // Skip the test if we can't connect to the real UPS | ||||||
|  |     console.log('Skipping this test since the UPS might not be available'); | ||||||
|  |   } | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | tap.test('Real UPS test v3', async () => { | ||||||
|  |   try { | ||||||
|  |     console.log('Testing with real UPS configuration...'); | ||||||
|  |      | ||||||
|  |     // Extract the correct SNMP config from the test configuration | ||||||
|  |     const snmpConfig = testConfigV3.snmp; | ||||||
|     console.log('SNMP Config:'); |     console.log('SNMP Config:'); | ||||||
|     console.log(`  Host: ${snmpConfig.host}:${snmpConfig.port}`); |     console.log(`  Host: ${snmpConfig.host}:${snmpConfig.port}`); | ||||||
|     console.log(`  Version: SNMPv${snmpConfig.version}`); |     console.log(`  Version: SNMPv${snmpConfig.version}`); | ||||||
|   | |||||||
| @@ -3,6 +3,6 @@ | |||||||
|  */ |  */ | ||||||
| export const commitinfo = { | export const commitinfo = { | ||||||
|   name: '@serve.zone/nupst', |   name: '@serve.zone/nupst', | ||||||
|   version: '2.4.5', |   version: '2.6.0', | ||||||
|   description: 'Node.js UPS Shutdown Tool for SNMP-enabled UPS devices' |   description: 'Node.js UPS Shutdown Tool for SNMP-enabled UPS devices' | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										47
									
								
								ts/cli.ts
									
									
									
									
									
								
							
							
						
						
									
										47
									
								
								ts/cli.ts
									
									
									
									
									
								
							| @@ -418,9 +418,9 @@ Options: | |||||||
|         console.log('│ Running install.sh to update NUPST...'); |         console.log('│ Running install.sh to update NUPST...'); | ||||||
|         execSync(`cd ${installDir} && bash ./install.sh`, { stdio: 'pipe' }); |         execSync(`cd ${installDir} && bash ./install.sh`, { stdio: 'pipe' }); | ||||||
|          |          | ||||||
|         // 3. Run the setup.sh script  |         // 3. Run the setup.sh script with force flag to update Node.js and dependencies | ||||||
|         console.log('│ Running setup.sh to update dependencies...'); |         console.log('│ Running setup.sh to update Node.js and dependencies...'); | ||||||
|         execSync(`cd ${installDir} && bash ./setup.sh`, { stdio: 'pipe' }); |         execSync(`cd ${installDir} && bash ./setup.sh --force`, { stdio: 'pipe' }); | ||||||
|          |          | ||||||
|         // 4. Refresh the systemd service |         // 4. Refresh the systemd service | ||||||
|         console.log('│ Refreshing systemd service...'); |         console.log('│ Refreshing systemd service...'); | ||||||
| @@ -533,6 +533,9 @@ Options: | |||||||
|     // Test the connection if requested |     // Test the connection if requested | ||||||
|     await this.optionallyTestConnection(config, prompt); |     await this.optionallyTestConnection(config, prompt); | ||||||
|      |      | ||||||
|  |     // Check if service is running and restart it if needed | ||||||
|  |     await this.restartServiceIfRunning(); | ||||||
|  |      | ||||||
|     console.log('\nSetup complete!'); |     console.log('\nSetup complete!'); | ||||||
|     await this.optionallyEnableService(prompt); |     await this.optionallyEnableService(prompt); | ||||||
|   } |   } | ||||||
| @@ -833,6 +836,44 @@ Options: | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * Check if the systemd service is running and restart it if it is | ||||||
|  |    * This is useful after configuration changes | ||||||
|  |    */ | ||||||
|  |   private async restartServiceIfRunning(): Promise<void> { | ||||||
|  |     try { | ||||||
|  |       // Check if the service is active | ||||||
|  |       const isActive = execSync('systemctl is-active nupst.service || true').toString().trim() === 'active'; | ||||||
|  |        | ||||||
|  |       if (isActive) { | ||||||
|  |         // Service is running, restart it | ||||||
|  |         console.log('┌─ Service Update ─────────────────────────┐'); | ||||||
|  |         console.log('│ Configuration has changed.'); | ||||||
|  |         console.log('│ Restarting NUPST service to apply changes...'); | ||||||
|  |          | ||||||
|  |         try { | ||||||
|  |           if (process.getuid && process.getuid() === 0) { | ||||||
|  |             // We have root access, restart directly | ||||||
|  |             execSync('systemctl restart nupst.service'); | ||||||
|  |             console.log('│ Service restarted successfully.'); | ||||||
|  |           } else { | ||||||
|  |             // No root access, show instructions | ||||||
|  |             console.log('│ Please restart the service with:'); | ||||||
|  |             console.log('│   sudo systemctl restart nupst.service'); | ||||||
|  |           } | ||||||
|  |         } catch (error) { | ||||||
|  |           console.log(`│ Error restarting service: ${error.message}`); | ||||||
|  |           console.log('│ You may need to restart the service manually:'); | ||||||
|  |           console.log('│   sudo systemctl restart nupst.service'); | ||||||
|  |         } | ||||||
|  |          | ||||||
|  |         console.log('└──────────────────────────────────────────┘'); | ||||||
|  |       } | ||||||
|  |     } catch (error) { | ||||||
|  |       // Ignore errors checking service status | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|   /** |   /** | ||||||
|    * Optionally enable and start systemd service |    * Optionally enable and start systemd service | ||||||
|    * @param prompt Function to prompt for user input |    * @param prompt Function to prompt for user input | ||||||
|   | |||||||
| @@ -2,7 +2,8 @@ import * as fs from 'fs'; | |||||||
| import * as path from 'path'; | import * as path from 'path'; | ||||||
| import { exec } from 'child_process'; | import { exec } from 'child_process'; | ||||||
| import { promisify } from 'util'; | import { promisify } from 'util'; | ||||||
| import { NupstSnmp, type ISnmpConfig } from './snmp.js'; | import { NupstSnmp } from './snmp/manager.js'; | ||||||
|  | import type { ISnmpConfig } from './snmp/types.js'; | ||||||
|  |  | ||||||
| const execAsync = promisify(exec); | const execAsync = promisify(exec); | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,4 +1,4 @@ | |||||||
| import { NupstSnmp } from './snmp.js'; | import { NupstSnmp } from './snmp/manager.js'; | ||||||
| import { NupstDaemon } from './daemon.js'; | import { NupstDaemon } from './daemon.js'; | ||||||
| import { NupstSystemd } from './systemd.js'; | import { NupstSystemd } from './systemd.js'; | ||||||
| import { commitinfo } from './00_commitinfo_data.js'; | import { commitinfo } from './00_commitinfo_data.js'; | ||||||
|   | |||||||
| @@ -1,6 +0,0 @@ | |||||||
| /** |  | ||||||
|  * Re-export from the snmp module |  | ||||||
|  * This file is kept for backward compatibility |  | ||||||
|  */ |  | ||||||
|  |  | ||||||
| export * from './snmp/index.js'; |  | ||||||
| @@ -1,98 +0,0 @@ | |||||||
| /** |  | ||||||
|  * SNMP encoding utilities |  | ||||||
|  * Contains helper methods for encoding SNMP data |  | ||||||
|  */ |  | ||||||
| export class SnmpEncoder { |  | ||||||
|   /** |  | ||||||
|    * Convert OID string to array of integers |  | ||||||
|    * @param oid OID string in dotted notation (e.g. "1.3.6.1.2.1") |  | ||||||
|    * @returns Array of integers representing the OID |  | ||||||
|    */ |  | ||||||
|   public static oidToArray(oid: string): number[] { |  | ||||||
|     return oid.split('.').map(n => parseInt(n, 10)); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   /** |  | ||||||
|    * Encode an SNMP integer |  | ||||||
|    * @param value Integer value to encode |  | ||||||
|    * @returns Buffer containing the encoded integer |  | ||||||
|    */ |  | ||||||
|   public static encodeInteger(value: number): Buffer { |  | ||||||
|     const buf = Buffer.alloc(4); |  | ||||||
|     buf.writeInt32BE(value, 0); |  | ||||||
|      |  | ||||||
|     // Find first non-zero byte |  | ||||||
|     let start = 0; |  | ||||||
|     while (start < 3 && buf[start] === 0) { |  | ||||||
|       start++; |  | ||||||
|     } |  | ||||||
|      |  | ||||||
|     // Handle negative values |  | ||||||
|     if (value < 0 && buf[start] === 0) { |  | ||||||
|       start--; |  | ||||||
|     } |  | ||||||
|      |  | ||||||
|     return buf.slice(start); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   /** |  | ||||||
|    * Encode an OID |  | ||||||
|    * @param oid Array of integers representing the OID |  | ||||||
|    * @returns Buffer containing the encoded OID |  | ||||||
|    */ |  | ||||||
|   public static encodeOID(oid: number[]): Buffer { |  | ||||||
|     // First two numbers are encoded as 40*x+y |  | ||||||
|     let encodedOid = Buffer.from([40 * (oid[0] || 0) + (oid[1] || 0)]); |  | ||||||
|      |  | ||||||
|     // Encode remaining numbers |  | ||||||
|     for (let i = 2; i < oid.length; i++) { |  | ||||||
|       const n = oid[i]; |  | ||||||
|        |  | ||||||
|       if (n < 128) { |  | ||||||
|         // Simple case: number fits in one byte |  | ||||||
|         encodedOid = Buffer.concat([encodedOid, Buffer.from([n])]); |  | ||||||
|       } else { |  | ||||||
|         // Number needs multiple bytes |  | ||||||
|         const bytes = []; |  | ||||||
|         let value = n; |  | ||||||
|          |  | ||||||
|         // Create bytes array in reverse order |  | ||||||
|         do { |  | ||||||
|           bytes.unshift(value & 0x7F); |  | ||||||
|           value >>= 7; |  | ||||||
|         } while (value > 0); |  | ||||||
|          |  | ||||||
|         // Set high bit on all but the last byte |  | ||||||
|         for (let j = 0; j < bytes.length - 1; j++) { |  | ||||||
|           bytes[j] |= 0x80; |  | ||||||
|         } |  | ||||||
|          |  | ||||||
|         encodedOid = Buffer.concat([encodedOid, Buffer.from(bytes)]); |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|      |  | ||||||
|     return encodedOid; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   /** |  | ||||||
|    * Decode an ASN.1 integer |  | ||||||
|    * @param buffer Buffer containing the encoded integer |  | ||||||
|    * @param offset Offset in the buffer |  | ||||||
|    * @param length Length of the integer in bytes |  | ||||||
|    * @returns Decoded integer value |  | ||||||
|    */ |  | ||||||
|   public static decodeInteger(buffer: Buffer, offset: number, length: number): number { |  | ||||||
|     if (length === 1) { |  | ||||||
|       return buffer[offset]; |  | ||||||
|     } else if (length === 2) { |  | ||||||
|       return buffer.readInt16BE(offset); |  | ||||||
|     } else if (length === 3) { |  | ||||||
|       return (buffer[offset] << 16) | (buffer[offset + 1] << 8) | buffer[offset + 2]; |  | ||||||
|     } else if (length === 4) { |  | ||||||
|       return buffer.readInt32BE(offset); |  | ||||||
|     } else { |  | ||||||
|       // For longer integers, we'll just return a simple value |  | ||||||
|       return buffer[offset]; |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| @@ -1,8 +1,7 @@ | |||||||
| import * as dgram from 'dgram'; | import * as dgram from 'dgram'; | ||||||
|  | import * as snmp from 'net-snmp'; | ||||||
| import type { IOidSet, ISnmpConfig, TUpsModel, IUpsStatus } from './types.js'; | import type { IOidSet, ISnmpConfig, TUpsModel, IUpsStatus } from './types.js'; | ||||||
| import { UpsOidSets } from './oid-sets.js'; | import { UpsOidSets } from './oid-sets.js'; | ||||||
| import { SnmpPacketCreator } from './packet-creator.js'; |  | ||||||
| import { SnmpPacketParser } from './packet-parser.js'; |  | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * Class for SNMP communication with UPS devices |  * Class for SNMP communication with UPS devices | ||||||
| @@ -13,6 +12,8 @@ export class NupstSnmp { | |||||||
|   private activeOIDs: IOidSet; |   private activeOIDs: IOidSet; | ||||||
|   // Reference to the parent Nupst instance |   // Reference to the parent Nupst instance | ||||||
|   private nupst: any; // Type 'any' to avoid circular dependency |   private nupst: any; // Type 'any' to avoid circular dependency | ||||||
|  |   // Debug mode flag | ||||||
|  |   private debug: boolean = false; | ||||||
|  |  | ||||||
|   // Default SNMP configuration |   // Default SNMP configuration | ||||||
|   private readonly DEFAULT_CONFIG: ISnmpConfig = { |   private readonly DEFAULT_CONFIG: ISnmpConfig = { | ||||||
| @@ -24,13 +25,6 @@ export class NupstSnmp { | |||||||
|     upsModel: 'cyberpower', // Default UPS model |     upsModel: 'cyberpower', // Default UPS model | ||||||
|   }; |   }; | ||||||
|  |  | ||||||
|   // SNMPv3 engine ID and counters |  | ||||||
|   private engineID: Buffer = Buffer.from([0x80, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06]); |  | ||||||
|   private engineBoots: number = 0; |  | ||||||
|   private engineTime: number = 0; |  | ||||||
|   private requestID: number = 1; |  | ||||||
|   private debug: boolean = false; // Enable for debug output |  | ||||||
|  |  | ||||||
|   /** |   /** | ||||||
|    * Create a new SNMP manager |    * Create a new SNMP manager | ||||||
|    * @param debug Whether to enable debug mode |    * @param debug Whether to enable debug mode | ||||||
| @@ -56,6 +50,14 @@ export class NupstSnmp { | |||||||
|     return this.nupst; |     return this.nupst; | ||||||
|   } |   } | ||||||
|    |    | ||||||
|  |   /** | ||||||
|  |    * Enable debug mode | ||||||
|  |    */ | ||||||
|  |   public enableDebug(): void { | ||||||
|  |     this.debug = true; | ||||||
|  |     console.log('SNMP debug mode enabled - detailed logs will be shown'); | ||||||
|  |   } | ||||||
|  |  | ||||||
|   /** |   /** | ||||||
|    * Set active OID set based on UPS model |    * Set active OID set based on UPS model | ||||||
|    * @param config SNMP configuration |    * @param config SNMP configuration | ||||||
| @@ -80,119 +82,188 @@ export class NupstSnmp { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   /** |   /** | ||||||
|    * Enable debug mode |    * Send an SNMP GET request using the net-snmp package | ||||||
|    */ |  | ||||||
|   public enableDebug(): void { |  | ||||||
|     this.debug = true; |  | ||||||
|     console.log('SNMP debug mode enabled - detailed logs will be shown'); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   /** |  | ||||||
|    * Send an SNMP GET request |  | ||||||
|    * @param oid OID to query |    * @param oid OID to query | ||||||
|    * @param config SNMP configuration |    * @param config SNMP configuration | ||||||
|  |    * @param retryCount Current retry count (unused in this implementation) | ||||||
|    * @returns Promise resolving to the SNMP response value |    * @returns Promise resolving to the SNMP response value | ||||||
|    */ |    */ | ||||||
|   public async snmpGet(oid: string, config = this.DEFAULT_CONFIG): Promise<any> { |   public async snmpGet( | ||||||
|  |     oid: string,  | ||||||
|  |     config = this.DEFAULT_CONFIG,  | ||||||
|  |     retryCount = 0 | ||||||
|  |   ): Promise<any> { | ||||||
|     return new Promise((resolve, reject) => { |     return new Promise((resolve, reject) => { | ||||||
|       const socket = dgram.createSocket('udp4'); |       if (this.debug) { | ||||||
|  |         console.log(`Sending SNMP v${config.version} GET request for OID ${oid} to ${config.host}:${config.port}`); | ||||||
|  |         console.log('Using community:', config.community); | ||||||
|  |       } | ||||||
|  |  | ||||||
|       // Create appropriate request based on SNMP version |       // Create SNMP options based on configuration | ||||||
|       let request: Buffer; |       const options: any = { | ||||||
|       if (config.version === 3) { |         port: config.port, | ||||||
|         request = SnmpPacketCreator.createSnmpV3GetRequest( |         retries: 2, // Number of retries | ||||||
|           oid,  |         timeout: config.timeout, | ||||||
|           config,  |         transport: 'udp4', | ||||||
|           this.engineID,  |         idBitsSize: 32, | ||||||
|           this.engineBoots,  |         context: config.context || '' | ||||||
|           this.engineTime,  |       }; | ||||||
|           this.requestID++, |  | ||||||
|           this.debug |       // Set version based on config | ||||||
|         ); |       if (config.version === 1) { | ||||||
|  |         options.version = snmp.Version1; | ||||||
|  |       } else if (config.version === 2) { | ||||||
|  |         options.version = snmp.Version2c; | ||||||
|       } else { |       } else { | ||||||
|         request = SnmpPacketCreator.createSnmpGetRequest(oid, config.community || 'public', this.debug); |         options.version = snmp.Version3; | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       if (this.debug) { |       // Create appropriate session based on SNMP version | ||||||
|         console.log(`Sending SNMP ${config.version === 3 ? 'v3' : ('v' + config.version)} request to ${config.host}:${config.port}`); |       let session; | ||||||
|         console.log('Request length:', request.length); |  | ||||||
|         console.log('First 16 bytes of request:', request.slice(0, 16).toString('hex')); |  | ||||||
|         console.log('Full request hex:', request.toString('hex')); |  | ||||||
|       } |  | ||||||
|        |        | ||||||
|       // Set timeout - add extra logging for debugging |  | ||||||
|       const timeout = setTimeout(() => { |  | ||||||
|         socket.close(); |  | ||||||
|         if (this.debug) { |  | ||||||
|           console.error('---------------------------------------'); |  | ||||||
|           console.error('SNMP request timed out after', config.timeout, 'ms'); |  | ||||||
|           console.error('SNMP Version:', config.version); |  | ||||||
|       if (config.version === 3) { |       if (config.version === 3) { | ||||||
|             console.error('SNMPv3 Security Level:', config.securityLevel); |         // For SNMPv3, we need to set up authentication and privacy | ||||||
|             console.error('SNMPv3 Username:', config.username); |         // For SNMPv3, we need a valid security level | ||||||
|             console.error('SNMPv3 Auth Protocol:', config.authProtocol || 'None'); |         const securityLevel = config.securityLevel || 'noAuthNoPriv'; | ||||||
|             console.error('SNMPv3 Privacy Protocol:', config.privProtocol || 'None'); |  | ||||||
|           } |  | ||||||
|           console.error('OID:', oid); |  | ||||||
|           console.error('Host:', config.host); |  | ||||||
|           console.error('Port:', config.port); |  | ||||||
|           console.error('---------------------------------------'); |  | ||||||
|         } |  | ||||||
|         reject(new Error(`SNMP request timed out after ${config.timeout}ms`)); |  | ||||||
|       }, config.timeout); |  | ||||||
|          |          | ||||||
|       // Listen for responses |         // Create the user object with required structure for net-snmp | ||||||
|       socket.on('message', (message, rinfo) => { |         const user: any = { | ||||||
|         clearTimeout(timeout); |           name: config.username || '' | ||||||
|  |         }; | ||||||
|  |          | ||||||
|  |         // Set security level | ||||||
|  |         if (securityLevel === 'noAuthNoPriv') { | ||||||
|  |           user.level = snmp.SecurityLevel.noAuthNoPriv; | ||||||
|  |         } else if (securityLevel === 'authNoPriv') { | ||||||
|  |           user.level = snmp.SecurityLevel.authNoPriv; | ||||||
|  |            | ||||||
|  |           // Set auth protocol - must provide both protocol and key | ||||||
|  |           if (config.authProtocol && config.authKey) { | ||||||
|  |             if (config.authProtocol === 'MD5') { | ||||||
|  |               user.authProtocol = snmp.AuthProtocols.md5; | ||||||
|  |             } else if (config.authProtocol === 'SHA') { | ||||||
|  |               user.authProtocol = snmp.AuthProtocols.sha; | ||||||
|  |             } | ||||||
|  |             user.authKey = config.authKey; | ||||||
|  |           } else { | ||||||
|  |             // Fallback to noAuthNoPriv if auth details missing | ||||||
|  |             user.level = snmp.SecurityLevel.noAuthNoPriv; | ||||||
|  |             if (this.debug) { | ||||||
|  |               console.log('Warning: Missing authProtocol or authKey, falling back to noAuthNoPriv'); | ||||||
|  |             } | ||||||
|  |           } | ||||||
|  |         } else if (securityLevel === 'authPriv') { | ||||||
|  |           user.level = snmp.SecurityLevel.authPriv; | ||||||
|  |            | ||||||
|  |           // Set auth protocol - must provide both protocol and key | ||||||
|  |           if (config.authProtocol && config.authKey) { | ||||||
|  |             if (config.authProtocol === 'MD5') { | ||||||
|  |               user.authProtocol = snmp.AuthProtocols.md5; | ||||||
|  |             } else if (config.authProtocol === 'SHA') { | ||||||
|  |               user.authProtocol = snmp.AuthProtocols.sha; | ||||||
|  |             } | ||||||
|  |             user.authKey = config.authKey; | ||||||
|  |              | ||||||
|  |             // Set privacy protocol - must provide both protocol and key | ||||||
|  |             if (config.privProtocol && config.privKey) { | ||||||
|  |               if (config.privProtocol === 'DES') { | ||||||
|  |                 user.privProtocol = snmp.PrivProtocols.des; | ||||||
|  |               } else if (config.privProtocol === 'AES') { | ||||||
|  |                 user.privProtocol = snmp.PrivProtocols.aes; | ||||||
|  |               } | ||||||
|  |               user.privKey = config.privKey; | ||||||
|  |             } else { | ||||||
|  |               // Fallback to authNoPriv if priv details missing | ||||||
|  |               user.level = snmp.SecurityLevel.authNoPriv; | ||||||
|  |               if (this.debug) { | ||||||
|  |                 console.log('Warning: Missing privProtocol or privKey, falling back to authNoPriv'); | ||||||
|  |               } | ||||||
|  |             } | ||||||
|  |           } else { | ||||||
|  |             // Fallback to noAuthNoPriv if auth details missing | ||||||
|  |             user.level = snmp.SecurityLevel.noAuthNoPriv; | ||||||
|  |             if (this.debug) { | ||||||
|  |               console.log('Warning: Missing authProtocol or authKey, falling back to noAuthNoPriv'); | ||||||
|  |             } | ||||||
|  |           } | ||||||
|  |         } | ||||||
|          |          | ||||||
|         if (this.debug) { |         if (this.debug) { | ||||||
|           console.log(`Received SNMP response from ${rinfo.address}:${rinfo.port}`); |           console.log('SNMPv3 user configuration:', { | ||||||
|           console.log('Response length:', message.length); |             name: user.name, | ||||||
|           console.log('First 16 bytes of response:', message.slice(0, 16).toString('hex')); |             level: Object.keys(snmp.SecurityLevel).find(key => snmp.SecurityLevel[key] === user.level), | ||||||
|           console.log('Full response hex:', message.toString('hex')); |             authProtocol: user.authProtocol ? 'Set' : 'Not Set', | ||||||
|         } |             authKey: user.authKey ? 'Set' : 'Not Set', | ||||||
|          |             privProtocol: user.privProtocol ? 'Set' : 'Not Set', | ||||||
|         try { |             privKey: user.privKey ? 'Set' : 'Not Set' | ||||||
|           const result = SnmpPacketParser.parseSnmpResponse(message, config, this.debug); |  | ||||||
|            |  | ||||||
|           if (this.debug) { |  | ||||||
|             console.log('Parsed SNMP response:', result); |  | ||||||
|           } |  | ||||||
|            |  | ||||||
|           socket.close(); |  | ||||||
|           resolve(result); |  | ||||||
|         } catch (error) { |  | ||||||
|           if (this.debug) { |  | ||||||
|             console.error('Error parsing SNMP response:', error); |  | ||||||
|           } |  | ||||||
|           socket.close(); |  | ||||||
|           reject(error); |  | ||||||
|         } |  | ||||||
|           }); |           }); | ||||||
|        |  | ||||||
|       // Handle errors |  | ||||||
|       socket.on('error', (error) => { |  | ||||||
|         clearTimeout(timeout); |  | ||||||
|         socket.close(); |  | ||||||
|         if (this.debug) { |  | ||||||
|           console.error('Socket error during SNMP request:', error); |  | ||||||
|         } |         } | ||||||
|         reject(error); |  | ||||||
|       }); |  | ||||||
|          |          | ||||||
|       // First send the request directly without binding to a specific port |         session = snmp.createV3Session(config.host, user, options); | ||||||
|       // This lets the OS pick an available port instead of trying to bind to one |       } else { | ||||||
|       socket.send(request, 0, request.length, config.port, config.host, (error) => { |         // For SNMPv1/v2c, we use the community string | ||||||
|  |         session = snmp.createSession(config.host, config.community || 'public', options); | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       // Convert the OID string to an array of OIDs if multiple OIDs are needed | ||||||
|  |       const oids = [oid]; | ||||||
|  |  | ||||||
|  |       // Send the GET request | ||||||
|  |       session.get(oids, (error: any, varbinds: any[]) => { | ||||||
|  |         // Close the session to release resources | ||||||
|  |         session.close(); | ||||||
|  |  | ||||||
|         if (error) { |         if (error) { | ||||||
|           clearTimeout(timeout); |  | ||||||
|           socket.close(); |  | ||||||
|           if (this.debug) { |           if (this.debug) { | ||||||
|             console.error('Error sending SNMP request:', error); |             console.error('SNMP GET error:', error); | ||||||
|           } |           } | ||||||
|           reject(error); |           reject(new Error(`SNMP GET error: ${error.message || error}`)); | ||||||
|         } else if (this.debug) { |           return; | ||||||
|           console.log('SNMP request sent successfully'); |  | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  |         if (!varbinds || varbinds.length === 0) { | ||||||
|  |           if (this.debug) { | ||||||
|  |             console.error('No varbinds returned in response'); | ||||||
|  |           } | ||||||
|  |           reject(new Error('No varbinds returned in response')); | ||||||
|  |           return; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // Check for SNMP errors in the response | ||||||
|  |         if (varbinds[0].type === snmp.ObjectType.NoSuchObject || | ||||||
|  |             varbinds[0].type === snmp.ObjectType.NoSuchInstance || | ||||||
|  |             varbinds[0].type === snmp.ObjectType.EndOfMibView) { | ||||||
|  |           if (this.debug) { | ||||||
|  |             console.error('SNMP error:', snmp.ObjectType[varbinds[0].type]); | ||||||
|  |           } | ||||||
|  |           reject(new Error(`SNMP error: ${snmp.ObjectType[varbinds[0].type]}`)); | ||||||
|  |           return; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // Process the response value based on its type | ||||||
|  |         let value = varbinds[0].value; | ||||||
|  |  | ||||||
|  |         // Handle specific types that might need conversion | ||||||
|  |         if (Buffer.isBuffer(value)) { | ||||||
|  |           // If value is a Buffer, try to convert it to a string if it's printable ASCII | ||||||
|  |           const isPrintableAscii = value.every(byte => byte >= 32 && byte <= 126); | ||||||
|  |           if (isPrintableAscii) { | ||||||
|  |             value = value.toString(); | ||||||
|  |           } | ||||||
|  |         } else if (typeof value === 'bigint') { | ||||||
|  |           // Convert BigInt to a normal number or string if needed | ||||||
|  |           value = Number(value); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if (this.debug) { | ||||||
|  |           console.log('SNMP response:', { | ||||||
|  |             oid: varbinds[0].oid, | ||||||
|  |             type: varbinds[0].type, | ||||||
|  |             value: value | ||||||
|  |           }); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         resolve(value); | ||||||
|       }); |       }); | ||||||
|     }); |     }); | ||||||
|   } |   } | ||||||
| @@ -230,142 +301,16 @@ export class NupstSnmp { | |||||||
|         console.log('---------------------------------------'); |         console.log('---------------------------------------'); | ||||||
|       } |       } | ||||||
|        |        | ||||||
|       // For SNMPv3, we need to discover the engine ID first |  | ||||||
|       if (config.version === 3) { |  | ||||||
|         if (this.debug) { |  | ||||||
|           console.log('SNMPv3 detected, starting engine ID discovery'); |  | ||||||
|         } |  | ||||||
|          |  | ||||||
|         try { |  | ||||||
|           const discoveredEngineId = await this.discoverEngineId(config); |  | ||||||
|           if (discoveredEngineId) { |  | ||||||
|             this.engineID = discoveredEngineId; |  | ||||||
|             if (this.debug) { |  | ||||||
|               console.log('Using discovered engine ID:', this.engineID.toString('hex')); |  | ||||||
|             } |  | ||||||
|           } |  | ||||||
|         } catch (error) { |  | ||||||
|           if (this.debug) { |  | ||||||
|             console.warn('Engine ID discovery failed, using default:', error); |  | ||||||
|           } |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|        |  | ||||||
|       // Helper function to get SNMP value with retry |  | ||||||
|       const getSNMPValueWithRetry = async (oid: string, description: string) => { |  | ||||||
|         if (oid === '') { |  | ||||||
|           if (this.debug) { |  | ||||||
|             console.log(`No OID provided for ${description}, skipping`); |  | ||||||
|           } |  | ||||||
|           return 0; |  | ||||||
|         } |  | ||||||
|          |  | ||||||
|         if (this.debug) { |  | ||||||
|           console.log(`Getting ${description} OID: ${oid}`); |  | ||||||
|         } |  | ||||||
|          |  | ||||||
|         try { |  | ||||||
|           const value = await this.snmpGet(oid, config); |  | ||||||
|           if (this.debug) { |  | ||||||
|             console.log(`${description} value:`, value); |  | ||||||
|           } |  | ||||||
|           return value; |  | ||||||
|         } catch (error) { |  | ||||||
|           if (this.debug) { |  | ||||||
|             console.error(`Error getting ${description}:`, error.message); |  | ||||||
|           } |  | ||||||
|            |  | ||||||
|           // If we got a timeout and it's SNMPv3, try with different security levels |  | ||||||
|           if (error.message.includes('timed out') && config.version === 3) { |  | ||||||
|             if (this.debug) { |  | ||||||
|               console.log(`Retrying ${description} with fallback settings...`); |  | ||||||
|             } |  | ||||||
|              |  | ||||||
|             // Create a retry config with lower security level |  | ||||||
|             if (config.securityLevel === 'authPriv') { |  | ||||||
|               const retryConfig = { ...config, securityLevel: 'authNoPriv' as 'authNoPriv' }; |  | ||||||
|               try { |  | ||||||
|                 if (this.debug) { |  | ||||||
|                   console.log(`Retrying with authNoPriv security level`); |  | ||||||
|                 } |  | ||||||
|                 const value = await this.snmpGet(oid, retryConfig); |  | ||||||
|                 if (this.debug) { |  | ||||||
|                   console.log(`${description} retry value:`, value); |  | ||||||
|                 } |  | ||||||
|                 return value; |  | ||||||
|               } catch (retryError) { |  | ||||||
|                 if (this.debug) { |  | ||||||
|                   console.error(`Retry failed for ${description}:`, retryError.message); |  | ||||||
|                 } |  | ||||||
|               } |  | ||||||
|             } |  | ||||||
|           } |  | ||||||
|            |  | ||||||
|           // If we're still having trouble, try with standard OIDs |  | ||||||
|           if (config.upsModel !== 'custom') { |  | ||||||
|             try { |  | ||||||
|               // Try RFC 1628 standard UPS MIB OIDs |  | ||||||
|               const standardOIDs = UpsOidSets.getStandardOids(); |  | ||||||
|                |  | ||||||
|               if (this.debug) { |  | ||||||
|                 console.log(`Trying standard RFC 1628 OID for ${description}: ${standardOIDs[description]}`); |  | ||||||
|               } |  | ||||||
|                |  | ||||||
|               const standardValue = await this.snmpGet(standardOIDs[description], config); |  | ||||||
|               if (this.debug) { |  | ||||||
|                 console.log(`${description} standard OID value:`, standardValue); |  | ||||||
|               } |  | ||||||
|               return standardValue; |  | ||||||
|             } catch (stdError) { |  | ||||||
|               if (this.debug) { |  | ||||||
|                 console.error(`Standard OID retry failed for ${description}:`, stdError.message); |  | ||||||
|               } |  | ||||||
|             } |  | ||||||
|           } |  | ||||||
|            |  | ||||||
|           // Return a default value if all attempts fail |  | ||||||
|           if (this.debug) { |  | ||||||
|             console.log(`Using default value 0 for ${description}`); |  | ||||||
|           } |  | ||||||
|           return 0; |  | ||||||
|         } |  | ||||||
|       }; |  | ||||||
|        |  | ||||||
|       // Get all values with independent retry logic |       // Get all values with independent retry logic | ||||||
|       const powerStatusValue = await getSNMPValueWithRetry(this.activeOIDs.POWER_STATUS, 'power status'); |       const powerStatusValue = await this.getSNMPValueWithRetry(this.activeOIDs.POWER_STATUS, 'power status', config); | ||||||
|       const batteryCapacity = await getSNMPValueWithRetry(this.activeOIDs.BATTERY_CAPACITY, 'battery capacity') || 0; |       const batteryCapacity = await this.getSNMPValueWithRetry(this.activeOIDs.BATTERY_CAPACITY, 'battery capacity', config) || 0; | ||||||
|       const batteryRuntime = await getSNMPValueWithRetry(this.activeOIDs.BATTERY_RUNTIME, 'battery runtime') || 0; |       const batteryRuntime = await this.getSNMPValueWithRetry(this.activeOIDs.BATTERY_RUNTIME, 'battery runtime', config) || 0; | ||||||
|        |        | ||||||
|       // Determine power status - handle different values for different UPS models |       // Determine power status - handle different values for different UPS models | ||||||
|       let powerStatus: 'online' | 'onBattery' | 'unknown' = 'unknown'; |       const powerStatus = this.determinePowerStatus(config.upsModel, powerStatusValue); | ||||||
|        |        | ||||||
|       // Different UPS models use different values for power status |       // Convert to minutes for UPS models with different time units | ||||||
|       if (config.upsModel === 'cyberpower') { |       const processedRuntime = this.processRuntimeValue(config.upsModel, batteryRuntime); | ||||||
|         // CyberPower RMCARD205: upsBaseOutputStatus values |  | ||||||
|         // 2=onLine, 3=onBattery, 4=onBoost, 5=onSleep, 6=off, etc. |  | ||||||
|         if (powerStatusValue === 2) { |  | ||||||
|           powerStatus = 'online'; |  | ||||||
|         } else if (powerStatusValue === 3) { |  | ||||||
|           powerStatus = 'onBattery'; |  | ||||||
|         } |  | ||||||
|       } else { |  | ||||||
|         // Default interpretation for other UPS models |  | ||||||
|         if (powerStatusValue === 1) { |  | ||||||
|           powerStatus = 'online'; |  | ||||||
|         } else if (powerStatusValue === 2) { |  | ||||||
|           powerStatus = 'onBattery'; |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|        |  | ||||||
|       // Convert TimeTicks to minutes for CyberPower runtime (value is in 1/100 seconds) |  | ||||||
|       let processedRuntime = batteryRuntime; |  | ||||||
|       if (config.upsModel === 'cyberpower' && batteryRuntime > 0) { |  | ||||||
|         // TimeTicks is in 1/100 seconds, convert to minutes |  | ||||||
|         processedRuntime = Math.floor(batteryRuntime / 6000); // 6000 ticks = 1 minute |  | ||||||
|         if (this.debug) { |  | ||||||
|           console.log(`Converting CyberPower runtime from ${batteryRuntime} ticks to ${processedRuntime} minutes`); |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|        |        | ||||||
|       const result = { |       const result = { | ||||||
|         powerStatus, |         powerStatus, | ||||||
| @@ -399,109 +344,231 @@ export class NupstSnmp { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   /** |   /** | ||||||
|    * Discover SNMP engine ID (for SNMPv3) |    * Helper method to get SNMP value with retry and fallback logic | ||||||
|    * Sends a proper discovery message to get the engine ID from the device |    * @param oid OID to query | ||||||
|  |    * @param description Description of the value for logging | ||||||
|    * @param config SNMP configuration |    * @param config SNMP configuration | ||||||
|    * @returns Promise resolving to the discovered engine ID |    * @returns Promise resolving to the SNMP value | ||||||
|    */ |    */ | ||||||
|   public async discoverEngineId(config: ISnmpConfig): Promise<Buffer> { |   private async getSNMPValueWithRetry( | ||||||
|     return new Promise((resolve, reject) => { |     oid: string,  | ||||||
|       const socket = dgram.createSocket('udp4'); |     description: string,  | ||||||
|        |     config: ISnmpConfig | ||||||
|       // Create a proper discovery message (SNMPv3 with noAuthNoPriv) |   ): Promise<any> { | ||||||
|       const discoveryConfig: ISnmpConfig = { |     if (oid === '') { | ||||||
|         ...config, |  | ||||||
|         securityLevel: 'noAuthNoPriv', |  | ||||||
|         username: '',  // Empty username for discovery |  | ||||||
|       }; |  | ||||||
|        |  | ||||||
|       // Create a simple GetRequest for sysDescr (a commonly available OID) |  | ||||||
|       const request = SnmpPacketCreator.createDiscoveryMessage(discoveryConfig, this.requestID++); |  | ||||||
|        |  | ||||||
|       if (this.debug) { |       if (this.debug) { | ||||||
|         console.log('Sending SNMPv3 discovery message'); |         console.log(`No OID provided for ${description}, skipping`); | ||||||
|         console.log('SNMPv3 Discovery message:', request.toString('hex')); |       } | ||||||
|  |       return 0; | ||||||
|     } |     } | ||||||
|      |      | ||||||
|       // Set timeout - use a longer timeout for discovery phase |  | ||||||
|       const discoveryTimeout = Math.max(config.timeout, 15000); // At least 15 seconds for discovery |  | ||||||
|       const timeout = setTimeout(() => { |  | ||||||
|         socket.close(); |  | ||||||
|         // Fall back to default engine ID if discovery fails |  | ||||||
|     if (this.debug) { |     if (this.debug) { | ||||||
|           console.error('---------------------------------------'); |       console.log(`Getting ${description} OID: ${oid}`); | ||||||
|           console.error('Engine ID discovery timed out after', discoveryTimeout, 'ms'); |  | ||||||
|           console.error('SNMPv3 settings:'); |  | ||||||
|           console.error('  Username:', config.username); |  | ||||||
|           console.error('  Security Level:', config.securityLevel); |  | ||||||
|           console.error('  Host:', config.host); |  | ||||||
|           console.error('  Port:', config.port); |  | ||||||
|           console.error('Using default engine ID:', this.engineID.toString('hex')); |  | ||||||
|           console.error('---------------------------------------'); |  | ||||||
|         } |  | ||||||
|         resolve(this.engineID); |  | ||||||
|       }, discoveryTimeout); |  | ||||||
|        |  | ||||||
|       // Listen for responses |  | ||||||
|       socket.on('message', (message, rinfo) => { |  | ||||||
|         clearTimeout(timeout); |  | ||||||
|          |  | ||||||
|         if (this.debug) { |  | ||||||
|           console.log(`Received SNMPv3 discovery response from ${rinfo.address}:${rinfo.port}`); |  | ||||||
|           console.log('Response:', message.toString('hex')); |  | ||||||
|     } |     } | ||||||
|      |      | ||||||
|     try { |     try { | ||||||
|           // Extract engine ID from response |       const value = await this.snmpGet(oid, config); | ||||||
|           const engineId = SnmpPacketParser.extractEngineId(message, this.debug); |  | ||||||
|           if (engineId) { |  | ||||||
|             this.engineID = engineId; // Update the engine ID |  | ||||||
|       if (this.debug) { |       if (this.debug) { | ||||||
|               console.log('Discovered engine ID:', engineId.toString('hex')); |         console.log(`${description} value:`, value); | ||||||
|             } |  | ||||||
|             socket.close(); |  | ||||||
|             resolve(engineId); |  | ||||||
|           } else { |  | ||||||
|             if (this.debug) { |  | ||||||
|               console.log('Could not extract engine ID, using default'); |  | ||||||
|             } |  | ||||||
|             socket.close(); |  | ||||||
|             resolve(this.engineID); |  | ||||||
|       } |       } | ||||||
|  |       return value; | ||||||
|     } catch (error) { |     } catch (error) { | ||||||
|       if (this.debug) { |       if (this.debug) { | ||||||
|             console.error('Error extracting engine ID:', error); |         console.error(`Error getting ${description}:`, error.message); | ||||||
|       } |       } | ||||||
|           socket.close(); |  | ||||||
|           resolve(this.engineID); // Fall back to default engine ID |  | ||||||
|         } |  | ||||||
|       }); |  | ||||||
|        |        | ||||||
|       // Handle errors |       // If we're using SNMPv3, try with different security levels | ||||||
|       socket.on('error', (error) => { |       if (config.version === 3) { | ||||||
|         clearTimeout(timeout); |         return await this.tryFallbackSecurityLevels(oid, description, config); | ||||||
|         socket.close(); |       } | ||||||
|  |        | ||||||
|  |       // Try with standard OIDs as fallback | ||||||
|  |       if (config.upsModel !== 'custom') { | ||||||
|  |         return await this.tryStandardOids(oid, description, config); | ||||||
|  |       } | ||||||
|  |        | ||||||
|  |       // Return a default value if all attempts fail | ||||||
|       if (this.debug) { |       if (this.debug) { | ||||||
|           console.error('Engine ID discovery socket error:', error); |         console.log(`Using default value 0 for ${description}`); | ||||||
|  |       } | ||||||
|  |       return 0; | ||||||
|  |     } | ||||||
|   } |   } | ||||||
|         resolve(this.engineID); // Fall back to default engine ID |  | ||||||
|       }); |  | ||||||
|  |  | ||||||
|       // Send request directly without binding |   /** | ||||||
|       socket.send(request, 0, request.length, config.port, config.host, (error) => { |    * Try fallback security levels for SNMPv3 | ||||||
|         if (error) { |    * @param oid OID to query | ||||||
|           clearTimeout(timeout); |    * @param description Description of the value for logging | ||||||
|           socket.close(); |    * @param config SNMP configuration | ||||||
|  |    * @returns Promise resolving to the SNMP value | ||||||
|  |    */ | ||||||
|  |   private async tryFallbackSecurityLevels( | ||||||
|  |     oid: string,  | ||||||
|  |     description: string,  | ||||||
|  |     config: ISnmpConfig | ||||||
|  |   ): Promise<any> { | ||||||
|     if (this.debug) { |     if (this.debug) { | ||||||
|             console.error('Error sending discovery message:', error); |       console.log(`Retrying ${description} with fallback security level...`); | ||||||
|           } |  | ||||||
|           resolve(this.engineID); // Fall back to default engine ID |  | ||||||
|         } else if (this.debug) { |  | ||||||
|           console.log('Discovery message sent successfully'); |  | ||||||
|         } |  | ||||||
|       }); |  | ||||||
|     }); |  | ||||||
|     } |     } | ||||||
|      |      | ||||||
|   // initiateShutdown method has been moved to the NupstDaemon class |     // Try with authNoPriv if current level is authPriv | ||||||
|  |     if (config.securityLevel === 'authPriv') { | ||||||
|  |       const retryConfig = { ...config, securityLevel: 'authNoPriv' as 'authNoPriv' }; | ||||||
|  |       try { | ||||||
|  |         if (this.debug) { | ||||||
|  |           console.log(`Retrying with authNoPriv security level`); | ||||||
|  |         } | ||||||
|  |         const value = await this.snmpGet(oid, retryConfig); | ||||||
|  |         if (this.debug) { | ||||||
|  |           console.log(`${description} retry value:`, value); | ||||||
|  |         } | ||||||
|  |         return value; | ||||||
|  |       } catch (retryError) { | ||||||
|  |         if (this.debug) { | ||||||
|  |           console.error(`Retry failed for ${description}:`, retryError.message); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     // Try with noAuthNoPriv as a last resort | ||||||
|  |     if (config.securityLevel === 'authPriv' || config.securityLevel === 'authNoPriv') { | ||||||
|  |       const retryConfig = { ...config, securityLevel: 'noAuthNoPriv' as 'noAuthNoPriv' }; | ||||||
|  |       try { | ||||||
|  |         if (this.debug) { | ||||||
|  |           console.log(`Retrying with noAuthNoPriv security level`); | ||||||
|  |         } | ||||||
|  |         const value = await this.snmpGet(oid, retryConfig); | ||||||
|  |         if (this.debug) { | ||||||
|  |           console.log(`${description} retry value:`, value); | ||||||
|  |         } | ||||||
|  |         return value; | ||||||
|  |       } catch (retryError) { | ||||||
|  |         if (this.debug) { | ||||||
|  |           console.error(`Retry failed for ${description}:`, retryError.message); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     return 0; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * Try standard OIDs as fallback | ||||||
|  |    * @param oid OID to query | ||||||
|  |    * @param description Description of the value for logging | ||||||
|  |    * @param config SNMP configuration | ||||||
|  |    * @returns Promise resolving to the SNMP value | ||||||
|  |    */ | ||||||
|  |   private async tryStandardOids( | ||||||
|  |     oid: string,  | ||||||
|  |     description: string,  | ||||||
|  |     config: ISnmpConfig | ||||||
|  |   ): Promise<any> { | ||||||
|  |     try { | ||||||
|  |       // Try RFC 1628 standard UPS MIB OIDs | ||||||
|  |       const standardOIDs = UpsOidSets.getStandardOids(); | ||||||
|  |        | ||||||
|  |       if (this.debug) { | ||||||
|  |         console.log(`Trying standard RFC 1628 OID for ${description}: ${standardOIDs[description]}`); | ||||||
|  |       } | ||||||
|  |        | ||||||
|  |       const standardValue = await this.snmpGet(standardOIDs[description], config); | ||||||
|  |       if (this.debug) { | ||||||
|  |         console.log(`${description} standard OID value:`, standardValue); | ||||||
|  |       } | ||||||
|  |       return standardValue; | ||||||
|  |     } catch (stdError) { | ||||||
|  |       if (this.debug) { | ||||||
|  |         console.error(`Standard OID retry failed for ${description}:`, stdError.message); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     return 0; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * Determine power status based on UPS model and raw value | ||||||
|  |    * @param upsModel UPS model | ||||||
|  |    * @param powerStatusValue Raw power status value | ||||||
|  |    * @returns Standardized power status | ||||||
|  |    */ | ||||||
|  |   private determinePowerStatus( | ||||||
|  |     upsModel: TUpsModel | undefined,  | ||||||
|  |     powerStatusValue: number | ||||||
|  |   ): 'online' | 'onBattery' | 'unknown' { | ||||||
|  |     if (upsModel === 'cyberpower') { | ||||||
|  |       // CyberPower RMCARD205: upsBaseOutputStatus values | ||||||
|  |       // 2=onLine, 3=onBattery, 4=onBoost, 5=onSleep, 6=off, etc. | ||||||
|  |       if (powerStatusValue === 2) { | ||||||
|  |         return 'online'; | ||||||
|  |       } else if (powerStatusValue === 3) { | ||||||
|  |         return 'onBattery'; | ||||||
|  |       } | ||||||
|  |     } else if (upsModel === 'eaton') { | ||||||
|  |       // Eaton UPS: xupsOutputSource values | ||||||
|  |       // 3=normal/mains, 5=battery, etc. | ||||||
|  |       if (powerStatusValue === 3) { | ||||||
|  |         return 'online'; | ||||||
|  |       } else if (powerStatusValue === 5) { | ||||||
|  |         return 'onBattery'; | ||||||
|  |       } | ||||||
|  |     } else if (upsModel === 'apc') { | ||||||
|  |       // APC UPS: upsBasicOutputStatus values | ||||||
|  |       // 2=online, 3=onBattery, etc. | ||||||
|  |       if (powerStatusValue === 2) { | ||||||
|  |         return 'online'; | ||||||
|  |       } else if (powerStatusValue === 3) { | ||||||
|  |         return 'onBattery'; | ||||||
|  |       } | ||||||
|  |     } else { | ||||||
|  |       // Default interpretation for other UPS models | ||||||
|  |       if (powerStatusValue === 1) { | ||||||
|  |         return 'online'; | ||||||
|  |       } else if (powerStatusValue === 2) { | ||||||
|  |         return 'onBattery'; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     return 'unknown'; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * Process runtime value based on UPS model | ||||||
|  |    * @param upsModel UPS model | ||||||
|  |    * @param batteryRuntime Raw battery runtime value | ||||||
|  |    * @returns Processed runtime in minutes | ||||||
|  |    */ | ||||||
|  |   private processRuntimeValue( | ||||||
|  |     upsModel: TUpsModel | undefined,  | ||||||
|  |     batteryRuntime: number | ||||||
|  |   ): number { | ||||||
|  |     if (this.debug) { | ||||||
|  |       console.log('Raw runtime value:', batteryRuntime); | ||||||
|  |     } | ||||||
|  |    | ||||||
|  |     if (upsModel === 'cyberpower' && batteryRuntime > 0) { | ||||||
|  |       // CyberPower: TimeTicks is in 1/100 seconds, convert to minutes | ||||||
|  |       const minutes = Math.floor(batteryRuntime / 6000); // 6000 ticks = 1 minute | ||||||
|  |       if (this.debug) { | ||||||
|  |         console.log(`Converting CyberPower runtime from ${batteryRuntime} ticks to ${minutes} minutes`); | ||||||
|  |       } | ||||||
|  |       return minutes; | ||||||
|  |     } else if (upsModel === 'eaton' && batteryRuntime > 0) { | ||||||
|  |       // Eaton: Runtime is in seconds, convert to minutes | ||||||
|  |       const minutes = Math.floor(batteryRuntime / 60); | ||||||
|  |       if (this.debug) { | ||||||
|  |         console.log(`Converting Eaton runtime from ${batteryRuntime} seconds to ${minutes} minutes`); | ||||||
|  |       } | ||||||
|  |       return minutes; | ||||||
|  |     } else if (batteryRuntime > 10000) { | ||||||
|  |       // Generic conversion for large tick values (likely TimeTicks) | ||||||
|  |       const minutes = Math.floor(batteryRuntime / 6000); | ||||||
|  |       if (this.debug) { | ||||||
|  |         console.log(`Converting ${batteryRuntime} ticks to ${minutes} minutes`); | ||||||
|  |       } | ||||||
|  |       return minutes; | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     return batteryRuntime; | ||||||
|  |   } | ||||||
| } | } | ||||||
| @@ -25,9 +25,9 @@ export class UpsOidSets { | |||||||
|      |      | ||||||
|     // Eaton OIDs |     // Eaton OIDs | ||||||
|     eaton: { |     eaton: { | ||||||
|       POWER_STATUS: '1.3.6.1.4.1.534.1.1.2.0', // Power status |       POWER_STATUS: '1.3.6.1.4.1.534.1.4.4.0', // xupsOutputSource (3=normal/mains, 5=battery) | ||||||
|       BATTERY_CAPACITY: '1.3.6.1.4.1.534.1.2.4.0', // Battery capacity in percentage |       BATTERY_CAPACITY: '1.3.6.1.4.1.534.1.2.4.0', // xupsBatCapacity (percentage) | ||||||
|       BATTERY_RUNTIME: '1.3.6.1.4.1.534.1.2.1.0', // Remaining runtime in minutes |       BATTERY_RUNTIME: '1.3.6.1.4.1.534.1.2.1.0', // xupsBatTimeRemaining (seconds) | ||||||
|     }, |     }, | ||||||
|      |      | ||||||
|     // TrippLite OIDs |     // TrippLite OIDs | ||||||
|   | |||||||
| @@ -1,651 +0,0 @@ | |||||||
| import * as crypto from 'crypto'; |  | ||||||
| import type { ISnmpConfig, ISnmpV3SecurityParams } from './types.js'; |  | ||||||
| import { SnmpEncoder } from './encoder.js'; |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * SNMP packet creation utilities |  | ||||||
|  * Creates SNMP request packets for different SNMP versions |  | ||||||
|  */ |  | ||||||
| export class SnmpPacketCreator { |  | ||||||
|   /** |  | ||||||
|    * Create an SNMPv1 GET request |  | ||||||
|    * @param oid OID to query |  | ||||||
|    * @param community Community string |  | ||||||
|    * @param debug Whether to enable debug output |  | ||||||
|    * @returns Buffer containing the SNMP request |  | ||||||
|    */ |  | ||||||
|   public static createSnmpGetRequest(oid: string, community: string, debug: boolean = false): Buffer { |  | ||||||
|     const oidArray = SnmpEncoder.oidToArray(oid); |  | ||||||
|     const encodedOid = SnmpEncoder.encodeOID(oidArray); |  | ||||||
|      |  | ||||||
|     if (debug) { |  | ||||||
|       console.log('OID array length:', oidArray.length); |  | ||||||
|       console.log('OID array:', oidArray); |  | ||||||
|     } |  | ||||||
|      |  | ||||||
|     // SNMP message structure |  | ||||||
|     // Sequence |  | ||||||
|     //   Version (Integer) |  | ||||||
|     //   Community (String) |  | ||||||
|     //   PDU (GetRequest) |  | ||||||
|     //     Request ID (Integer) |  | ||||||
|     //     Error Status (Integer) |  | ||||||
|     //     Error Index (Integer) |  | ||||||
|     //     Variable Bindings (Sequence) |  | ||||||
|     //       Variable (Sequence) |  | ||||||
|     //         OID (ObjectIdentifier) |  | ||||||
|     //         Value (Null) |  | ||||||
|      |  | ||||||
|     // Use the standard method from our test that is known to work |  | ||||||
|     // Create a fixed request ID (0x00000001) to ensure deterministic behavior |  | ||||||
|     const requestId = Buffer.from([0x00, 0x00, 0x00, 0x01]); |  | ||||||
|      |  | ||||||
|     // Encode values |  | ||||||
|     const versionBuf = Buffer.concat([ |  | ||||||
|       Buffer.from([0x02, 0x01]), // ASN.1 Integer, length 1 |  | ||||||
|       Buffer.from([0x00])        // SNMP version 1 (0) |  | ||||||
|     ]); |  | ||||||
|      |  | ||||||
|     const communityBuf = Buffer.concat([ |  | ||||||
|       Buffer.from([0x04, community.length]), // ASN.1 Octet String, length |  | ||||||
|       Buffer.from(community)                // Community string |  | ||||||
|     ]); |  | ||||||
|      |  | ||||||
|     const requestIdBuf = Buffer.concat([ |  | ||||||
|       Buffer.from([0x02, 0x04]), // ASN.1 Integer, length 4 |  | ||||||
|       requestId                  // Fixed Request ID |  | ||||||
|     ]); |  | ||||||
|      |  | ||||||
|     const errorStatusBuf = Buffer.concat([ |  | ||||||
|       Buffer.from([0x02, 0x01]), // ASN.1 Integer, length 1 |  | ||||||
|       Buffer.from([0x00])        // Error Status (0 = no error) |  | ||||||
|     ]); |  | ||||||
|      |  | ||||||
|     const errorIndexBuf = Buffer.concat([ |  | ||||||
|       Buffer.from([0x02, 0x01]), // ASN.1 Integer, length 1 |  | ||||||
|       Buffer.from([0x00])        // Error Index (0) |  | ||||||
|     ]); |  | ||||||
|      |  | ||||||
|     const oidValueBuf = Buffer.concat([ |  | ||||||
|       Buffer.from([0x30]),       // ASN.1 Sequence |  | ||||||
|       Buffer.from([encodedOid.length + 2]), // Length |  | ||||||
|       Buffer.from([0x06]),       // ASN.1 Object Identifier |  | ||||||
|       Buffer.from([encodedOid.length]), // Length |  | ||||||
|       encodedOid,                // OID |  | ||||||
|       Buffer.from([0x05, 0x00])  // Null value |  | ||||||
|     ]); |  | ||||||
|      |  | ||||||
|     const varBindingsBuf = Buffer.concat([ |  | ||||||
|       Buffer.from([0x30]),       // ASN.1 Sequence |  | ||||||
|       Buffer.from([oidValueBuf.length]), // Length |  | ||||||
|       oidValueBuf                // Variable binding |  | ||||||
|     ]); |  | ||||||
|      |  | ||||||
|     const pduBuf = Buffer.concat([ |  | ||||||
|       Buffer.from([0xa0]),       // ASN.1 Context-specific Constructed 0 (GetRequest) |  | ||||||
|       Buffer.from([requestIdBuf.length + errorStatusBuf.length + errorIndexBuf.length + varBindingsBuf.length]), // Length |  | ||||||
|       requestIdBuf,              // Request ID |  | ||||||
|       errorStatusBuf,            // Error Status |  | ||||||
|       errorIndexBuf,             // Error Index |  | ||||||
|       varBindingsBuf             // Variable Bindings |  | ||||||
|     ]); |  | ||||||
|      |  | ||||||
|     const messageBuf = Buffer.concat([ |  | ||||||
|       Buffer.from([0x30]),       // ASN.1 Sequence |  | ||||||
|       Buffer.from([versionBuf.length + communityBuf.length + pduBuf.length]), // Length |  | ||||||
|       versionBuf,                // Version |  | ||||||
|       communityBuf,              // Community |  | ||||||
|       pduBuf                     // PDU |  | ||||||
|     ]); |  | ||||||
|      |  | ||||||
|     if (debug) { |  | ||||||
|       console.log('SNMP Request buffer:', messageBuf.toString('hex')); |  | ||||||
|     } |  | ||||||
|      |  | ||||||
|     return messageBuf; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   /** |  | ||||||
|    * Create an SNMPv3 GET request |  | ||||||
|    * @param oid OID to query |  | ||||||
|    * @param config SNMP configuration |  | ||||||
|    * @param engineID Engine ID |  | ||||||
|    * @param engineBoots Engine boots counter |  | ||||||
|    * @param engineTime Engine time counter |  | ||||||
|    * @param requestID Request ID |  | ||||||
|    * @param debug Whether to enable debug output |  | ||||||
|    * @returns Buffer containing the SNMP request |  | ||||||
|    */ |  | ||||||
|   public static createSnmpV3GetRequest( |  | ||||||
|     oid: string,  |  | ||||||
|     config: ISnmpConfig,  |  | ||||||
|     engineID: Buffer, |  | ||||||
|     engineBoots: number, |  | ||||||
|     engineTime: number, |  | ||||||
|     requestID: number, |  | ||||||
|     debug: boolean = false |  | ||||||
|   ): Buffer { |  | ||||||
|     if (debug) { |  | ||||||
|       console.log('Creating SNMPv3 GET request for OID:', oid); |  | ||||||
|       console.log('With config:', { |  | ||||||
|         ...config, |  | ||||||
|         authKey: config.authKey ? '***' : undefined, |  | ||||||
|         privKey: config.privKey ? '***' : undefined |  | ||||||
|       }); |  | ||||||
|     } |  | ||||||
|      |  | ||||||
|     const oidArray = SnmpEncoder.oidToArray(oid); |  | ||||||
|     const encodedOid = SnmpEncoder.encodeOID(oidArray); |  | ||||||
|      |  | ||||||
|     if (debug) { |  | ||||||
|       console.log('Using engine ID:', engineID.toString('hex')); |  | ||||||
|       console.log('Engine boots:', engineBoots); |  | ||||||
|       console.log('Engine time:', engineTime); |  | ||||||
|       console.log('Request ID:', requestID); |  | ||||||
|     } |  | ||||||
|      |  | ||||||
|     // Create security parameters |  | ||||||
|     const securityParams: ISnmpV3SecurityParams = { |  | ||||||
|       msgAuthoritativeEngineID: engineID, |  | ||||||
|       msgAuthoritativeEngineBoots: engineBoots, |  | ||||||
|       msgAuthoritativeEngineTime: engineTime, |  | ||||||
|       msgUserName: config.username || '', |  | ||||||
|       msgAuthenticationParameters: Buffer.alloc(12, 0), // Will be filled in later for auth |  | ||||||
|       msgPrivacyParameters: Buffer.alloc(8, 0),  // For privacy |  | ||||||
|     }; |  | ||||||
|  |  | ||||||
|     // Create the PDU (Protocol Data Unit) |  | ||||||
|     // This is wrapped within the security parameters |  | ||||||
|     const requestIdBuf = Buffer.concat([ |  | ||||||
|       Buffer.from([0x02, 0x04]), // ASN.1 Integer, length 4 |  | ||||||
|       SnmpEncoder.encodeInteger(requestID) // Request ID |  | ||||||
|     ]); |  | ||||||
|      |  | ||||||
|     const errorStatusBuf = Buffer.concat([ |  | ||||||
|       Buffer.from([0x02, 0x01]), // ASN.1 Integer, length 1 |  | ||||||
|       Buffer.from([0x00])        // Error Status (0 = no error) |  | ||||||
|     ]); |  | ||||||
|      |  | ||||||
|     const errorIndexBuf = Buffer.concat([ |  | ||||||
|       Buffer.from([0x02, 0x01]), // ASN.1 Integer, length 1 |  | ||||||
|       Buffer.from([0x00])        // Error Index (0) |  | ||||||
|     ]); |  | ||||||
|      |  | ||||||
|     const oidValueBuf = Buffer.concat([ |  | ||||||
|       Buffer.from([0x30]),       // ASN.1 Sequence |  | ||||||
|       Buffer.from([encodedOid.length + 2]), // Length |  | ||||||
|       Buffer.from([0x06]),       // ASN.1 Object Identifier |  | ||||||
|       Buffer.from([encodedOid.length]), // Length |  | ||||||
|       encodedOid,                // OID |  | ||||||
|       Buffer.from([0x05, 0x00])  // Null value |  | ||||||
|     ]); |  | ||||||
|      |  | ||||||
|     const varBindingsBuf = Buffer.concat([ |  | ||||||
|       Buffer.from([0x30]),       // ASN.1 Sequence |  | ||||||
|       Buffer.from([oidValueBuf.length]), // Length |  | ||||||
|       oidValueBuf                // Variable binding |  | ||||||
|     ]); |  | ||||||
|      |  | ||||||
|     const pduBuf = Buffer.concat([ |  | ||||||
|       Buffer.from([0xa0]),       // ASN.1 Context-specific Constructed 0 (GetRequest) |  | ||||||
|       Buffer.from([requestIdBuf.length + errorStatusBuf.length + errorIndexBuf.length + varBindingsBuf.length]), // Length |  | ||||||
|       requestIdBuf,              // Request ID |  | ||||||
|       errorStatusBuf,            // Error Status |  | ||||||
|       errorIndexBuf,             // Error Index |  | ||||||
|       varBindingsBuf             // Variable Bindings |  | ||||||
|     ]); |  | ||||||
|  |  | ||||||
|     // Create the security parameters |  | ||||||
|     const engineIdBuf = Buffer.concat([ |  | ||||||
|       Buffer.from([0x04, securityParams.msgAuthoritativeEngineID.length]), // ASN.1 Octet String |  | ||||||
|       securityParams.msgAuthoritativeEngineID |  | ||||||
|     ]); |  | ||||||
|      |  | ||||||
|     const engineBootsBuf = Buffer.concat([ |  | ||||||
|       Buffer.from([0x02, 0x04]), // ASN.1 Integer, length 4 |  | ||||||
|       SnmpEncoder.encodeInteger(securityParams.msgAuthoritativeEngineBoots) |  | ||||||
|     ]); |  | ||||||
|      |  | ||||||
|     const engineTimeBuf = Buffer.concat([ |  | ||||||
|       Buffer.from([0x02, 0x04]), // ASN.1 Integer, length 4 |  | ||||||
|       SnmpEncoder.encodeInteger(securityParams.msgAuthoritativeEngineTime) |  | ||||||
|     ]); |  | ||||||
|      |  | ||||||
|     const userNameBuf = Buffer.concat([ |  | ||||||
|       Buffer.from([0x04, securityParams.msgUserName.length]), // ASN.1 Octet String |  | ||||||
|       Buffer.from(securityParams.msgUserName) |  | ||||||
|     ]); |  | ||||||
|      |  | ||||||
|     const authParamsBuf = Buffer.concat([ |  | ||||||
|       Buffer.from([0x04, securityParams.msgAuthenticationParameters.length]), // ASN.1 Octet String |  | ||||||
|       securityParams.msgAuthenticationParameters |  | ||||||
|     ]); |  | ||||||
|      |  | ||||||
|     const privParamsBuf = Buffer.concat([ |  | ||||||
|       Buffer.from([0x04, securityParams.msgPrivacyParameters.length]), // ASN.1 Octet String |  | ||||||
|       securityParams.msgPrivacyParameters |  | ||||||
|     ]); |  | ||||||
|      |  | ||||||
|     // Security parameters sequence |  | ||||||
|     const securityParamsBuf = Buffer.concat([ |  | ||||||
|       Buffer.from([0x30]), // ASN.1 Sequence |  | ||||||
|       Buffer.from([engineIdBuf.length + engineBootsBuf.length + engineTimeBuf.length +  |  | ||||||
|                    userNameBuf.length + authParamsBuf.length + privParamsBuf.length]), // Length |  | ||||||
|       engineIdBuf, |  | ||||||
|       engineBootsBuf, |  | ||||||
|       engineTimeBuf, |  | ||||||
|       userNameBuf, |  | ||||||
|       authParamsBuf, |  | ||||||
|       privParamsBuf |  | ||||||
|     ]); |  | ||||||
|  |  | ||||||
|     // Determine security level flags |  | ||||||
|     let securityFlags = 0; |  | ||||||
|     if (config.securityLevel === 'authNoPriv' || config.securityLevel === 'authPriv') { |  | ||||||
|       securityFlags |= 0x01; // Authentication flag |  | ||||||
|     } |  | ||||||
|     if (config.securityLevel === 'authPriv') { |  | ||||||
|       securityFlags |= 0x02; // Privacy flag |  | ||||||
|     } |  | ||||||
|      |  | ||||||
|     // Set reportable flag - required for SNMPv3 |  | ||||||
|     securityFlags |= 0x04; // Reportable flag |  | ||||||
|  |  | ||||||
|     // Create SNMPv3 header |  | ||||||
|     const msgIdBuf = Buffer.concat([ |  | ||||||
|       Buffer.from([0x02, 0x04]), // ASN.1 Integer, length 4 |  | ||||||
|       SnmpEncoder.encodeInteger(requestID) // Message ID (same as request ID for simplicity) |  | ||||||
|     ]); |  | ||||||
|      |  | ||||||
|     const msgMaxSizeBuf = Buffer.concat([ |  | ||||||
|       Buffer.from([0x02, 0x04]), // ASN.1 Integer, length 4 |  | ||||||
|       SnmpEncoder.encodeInteger(65507) // Max message size |  | ||||||
|     ]); |  | ||||||
|      |  | ||||||
|     const msgFlagsBuf = Buffer.concat([ |  | ||||||
|       Buffer.from([0x04, 0x01]), // ASN.1 Octet String, length 1 |  | ||||||
|       Buffer.from([securityFlags]) |  | ||||||
|     ]); |  | ||||||
|      |  | ||||||
|     const msgSecModelBuf = Buffer.concat([ |  | ||||||
|       Buffer.from([0x02, 0x01]), // ASN.1 Integer, length 1 |  | ||||||
|       Buffer.from([0x03]) // Security model (3 = USM) |  | ||||||
|     ]); |  | ||||||
|  |  | ||||||
|     // SNMPv3 header |  | ||||||
|     const msgHeaderBuf = Buffer.concat([ |  | ||||||
|       Buffer.from([0x30]), // ASN.1 Sequence |  | ||||||
|       Buffer.from([msgIdBuf.length + msgMaxSizeBuf.length + msgFlagsBuf.length + msgSecModelBuf.length]), // Length |  | ||||||
|       msgIdBuf, |  | ||||||
|       msgMaxSizeBuf, |  | ||||||
|       msgFlagsBuf, |  | ||||||
|       msgSecModelBuf |  | ||||||
|     ]); |  | ||||||
|  |  | ||||||
|     // SNMPv3 security parameters |  | ||||||
|     const msgSecurityBuf = Buffer.concat([ |  | ||||||
|       Buffer.from([0x04]), // ASN.1 Octet String |  | ||||||
|       Buffer.from([securityParamsBuf.length]), // Length |  | ||||||
|       securityParamsBuf |  | ||||||
|     ]); |  | ||||||
|  |  | ||||||
|     // Create scopedPDU |  | ||||||
|     // In SNMPv3, the PDU is wrapped in a "scoped PDU" structure |  | ||||||
|     const contextEngineBuf = Buffer.concat([ |  | ||||||
|       Buffer.from([0x04, engineID.length]), // ASN.1 Octet String |  | ||||||
|       engineID |  | ||||||
|     ]); |  | ||||||
|      |  | ||||||
|     const contextNameBuf = Buffer.concat([ |  | ||||||
|       Buffer.from([0x04, 0x00]), // ASN.1 Octet String, length 0 (empty context name) |  | ||||||
|     ]); |  | ||||||
|      |  | ||||||
|     const scopedPduBuf = Buffer.concat([ |  | ||||||
|       Buffer.from([0x30]), // ASN.1 Sequence |  | ||||||
|       Buffer.from([contextEngineBuf.length + contextNameBuf.length + pduBuf.length]), // Length |  | ||||||
|       contextEngineBuf, |  | ||||||
|       contextNameBuf, |  | ||||||
|       pduBuf |  | ||||||
|     ]); |  | ||||||
|  |  | ||||||
|     // For authPriv, we need to encrypt the scopedPDU |  | ||||||
|     let encryptedPdu = scopedPduBuf; |  | ||||||
|     if (config.securityLevel === 'authPriv' && config.privKey) { |  | ||||||
|       // In a real implementation, encryption would be applied here |  | ||||||
|       // For this example, we'll just simulate it |  | ||||||
|       encryptedPdu = this.simulateEncryption(scopedPduBuf, config); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     // Final scopedPDU (encrypted or not) |  | ||||||
|     const finalScopedPduBuf = Buffer.concat([ |  | ||||||
|       Buffer.from([0x04]), // ASN.1 Octet String |  | ||||||
|       Buffer.from([encryptedPdu.length]), // Length |  | ||||||
|       encryptedPdu |  | ||||||
|     ]); |  | ||||||
|  |  | ||||||
|     // Combine everything for the final message |  | ||||||
|     const versionBuf = Buffer.concat([ |  | ||||||
|       Buffer.from([0x02, 0x01]), // ASN.1 Integer, length 1 |  | ||||||
|       Buffer.from([0x03])        // SNMP version 3 (3) |  | ||||||
|     ]); |  | ||||||
|      |  | ||||||
|     const messageBuf = Buffer.concat([ |  | ||||||
|       Buffer.from([0x30]), // ASN.1 Sequence |  | ||||||
|       Buffer.from([versionBuf.length + msgHeaderBuf.length + msgSecurityBuf.length + finalScopedPduBuf.length]), // Length |  | ||||||
|       versionBuf, |  | ||||||
|       msgHeaderBuf, |  | ||||||
|       msgSecurityBuf, |  | ||||||
|       finalScopedPduBuf |  | ||||||
|     ]); |  | ||||||
|  |  | ||||||
|     // If using authentication, calculate and insert the authentication parameters |  | ||||||
|     if ((config.securityLevel === 'authNoPriv' || config.securityLevel === 'authPriv') &&  |  | ||||||
|         config.authKey && config.authProtocol) { |  | ||||||
|       const authenticatedMsg = this.addAuthentication(messageBuf, config, authParamsBuf); |  | ||||||
|        |  | ||||||
|       if (debug) { |  | ||||||
|         console.log('Created authenticated SNMPv3 message'); |  | ||||||
|         console.log('Final message length:', authenticatedMsg.length); |  | ||||||
|       } |  | ||||||
|        |  | ||||||
|       return authenticatedMsg; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     if (debug) { |  | ||||||
|       console.log('Created SNMPv3 message without authentication'); |  | ||||||
|       console.log('Final message length:', messageBuf.length); |  | ||||||
|     } |  | ||||||
|      |  | ||||||
|     return messageBuf; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   /** |  | ||||||
|    * Simulate encryption for authPriv security level |  | ||||||
|    * In a real implementation, this would use the specified privacy protocol (DES/AES) |  | ||||||
|    * @param data Data to encrypt |  | ||||||
|    * @param config SNMP configuration |  | ||||||
|    * @returns Encrypted data |  | ||||||
|    */ |  | ||||||
|   private static simulateEncryption(data: Buffer, config: ISnmpConfig): Buffer { |  | ||||||
|     // This is a placeholder - in a real implementation, you would: |  | ||||||
|     // 1. Generate an initialization vector (IV) |  | ||||||
|     // 2. Use the privacy key derived from the privKey |  | ||||||
|     // 3. Apply the appropriate encryption algorithm (DES/AES) |  | ||||||
|      |  | ||||||
|     // For demonstration purposes only |  | ||||||
|     if (config.privProtocol === 'AES' && config.privKey) { |  | ||||||
|       try { |  | ||||||
|         // Create a deterministic IV for demo purposes (not secure for production) |  | ||||||
|         const iv = Buffer.alloc(16, 0); |  | ||||||
|         const engineID = Buffer.from([0x80, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06]); |  | ||||||
|         for (let i = 0; i < 8; i++) { |  | ||||||
|           iv[i] = engineID[i % engineID.length]; |  | ||||||
|         } |  | ||||||
|          |  | ||||||
|         // Create a key from the privKey (proper key localization should be used in production) |  | ||||||
|         const key = crypto.createHash('md5').update(config.privKey).digest(); |  | ||||||
|          |  | ||||||
|         // Create cipher and encrypt |  | ||||||
|         const cipher = crypto.createCipheriv('aes-128-cfb', key, iv); |  | ||||||
|         const encrypted = Buffer.concat([cipher.update(data), cipher.final()]); |  | ||||||
|          |  | ||||||
|         return encrypted; |  | ||||||
|       } catch (error) { |  | ||||||
|         console.warn('AES encryption failed, falling back to plaintext:', error); |  | ||||||
|         return data; |  | ||||||
|       } |  | ||||||
|     } else if (config.privProtocol === 'DES' && config.privKey) { |  | ||||||
|       try { |  | ||||||
|         // Create a deterministic IV for demo purposes (not secure for production) |  | ||||||
|         const iv = Buffer.alloc(8, 0); |  | ||||||
|         const engineID = Buffer.from([0x80, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06]); |  | ||||||
|         for (let i = 0; i < 8; i++) { |  | ||||||
|           iv[i] = engineID[i % engineID.length]; |  | ||||||
|         } |  | ||||||
|          |  | ||||||
|         // Create a key from the privKey (proper key localization should be used in production) |  | ||||||
|         const key = crypto.createHash('md5').update(config.privKey).digest().slice(0, 8); |  | ||||||
|          |  | ||||||
|         // Create cipher and encrypt |  | ||||||
|         const cipher = crypto.createCipheriv('des-cbc', key, iv); |  | ||||||
|         const encrypted = Buffer.concat([cipher.update(data), cipher.final()]); |  | ||||||
|          |  | ||||||
|         return encrypted; |  | ||||||
|       } catch (error) { |  | ||||||
|         console.warn('DES encryption failed, falling back to plaintext:', error); |  | ||||||
|         return data; |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|      |  | ||||||
|     return data; // Return unencrypted data as fallback |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   /** |  | ||||||
|    * Add authentication to SNMPv3 message |  | ||||||
|    * @param message Message to authenticate |  | ||||||
|    * @param config SNMP configuration |  | ||||||
|    * @param authParamsBuf Authentication parameters buffer |  | ||||||
|    * @returns Authenticated message |  | ||||||
|    */ |  | ||||||
|   private static addAuthentication(message: Buffer, config: ISnmpConfig, authParamsBuf: Buffer): Buffer { |  | ||||||
|     // In a real implementation, this would: |  | ||||||
|     // 1. Zero out the authentication parameters field |  | ||||||
|     // 2. Calculate HMAC-MD5 or HMAC-SHA1 over the entire message |  | ||||||
|     // 3. Insert the HMAC into the authentication parameters field |  | ||||||
|      |  | ||||||
|     if (!config.authKey) { |  | ||||||
|       return message; |  | ||||||
|     } |  | ||||||
|      |  | ||||||
|     try { |  | ||||||
|       // Find position of auth parameters in the message |  | ||||||
|       // This is a more reliable way to find the exact position |  | ||||||
|       let authParamsPos = -1; |  | ||||||
|       for (let i = 0; i < message.length - 16; i++) { |  | ||||||
|         // Look for the auth params pattern: 0x04 0x0C 0x00 0x00... |  | ||||||
|         if (message[i] === 0x04 && message[i + 1] === 0x0C) { |  | ||||||
|           // Check if next 12 bytes are all zeros |  | ||||||
|           let allZeros = true; |  | ||||||
|           for (let j = 0; j < 12; j++) { |  | ||||||
|             if (message[i + 2 + j] !== 0) { |  | ||||||
|               allZeros = false; |  | ||||||
|               break; |  | ||||||
|             } |  | ||||||
|           } |  | ||||||
|           if (allZeros) { |  | ||||||
|             authParamsPos = i; |  | ||||||
|             break; |  | ||||||
|           } |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|        |  | ||||||
|       if (authParamsPos === -1) { |  | ||||||
|         return message; |  | ||||||
|       } |  | ||||||
|        |  | ||||||
|       // Create a copy of the message with zeroed auth parameters |  | ||||||
|       const msgCopy = Buffer.from(message); |  | ||||||
|        |  | ||||||
|       // Prepare the authentication key according to RFC3414 |  | ||||||
|       // We should use the standard key localization process |  | ||||||
|       const localizedKey = this.localizeAuthKey(config.authKey,  |  | ||||||
|         Buffer.from([0x80, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06]), |  | ||||||
|         config.authProtocol); |  | ||||||
|        |  | ||||||
|       // Calculate HMAC |  | ||||||
|       let hmac; |  | ||||||
|       if (config.authProtocol === 'SHA') { |  | ||||||
|         hmac = crypto.createHmac('sha1', localizedKey).update(msgCopy).digest().slice(0, 12); |  | ||||||
|       } else { |  | ||||||
|         // Default to MD5 |  | ||||||
|         hmac = crypto.createHmac('md5', localizedKey).update(msgCopy).digest().slice(0, 12); |  | ||||||
|       } |  | ||||||
|        |  | ||||||
|       // Copy HMAC into original message |  | ||||||
|       hmac.copy(message, authParamsPos + 2); |  | ||||||
|        |  | ||||||
|       return message; |  | ||||||
|     } catch (error) { |  | ||||||
|       console.warn('Authentication failed:', error); |  | ||||||
|       return message; |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|    |  | ||||||
|   /** |  | ||||||
|    * Localize authentication key according to RFC3414 |  | ||||||
|    * @param key Authentication key |  | ||||||
|    * @param engineId Engine ID |  | ||||||
|    * @param authProtocol Authentication protocol |  | ||||||
|    * @returns Localized key |  | ||||||
|    */ |  | ||||||
|   private static localizeAuthKey(key: string, engineId: Buffer, authProtocol: string = 'MD5'): Buffer { |  | ||||||
|     try { |  | ||||||
|       // Convert password to key using hash |  | ||||||
|       let initialHash; |  | ||||||
|       if (authProtocol === 'SHA') { |  | ||||||
|         initialHash = crypto.createHash('sha1'); |  | ||||||
|       } else { |  | ||||||
|         initialHash = crypto.createHash('md5'); |  | ||||||
|       } |  | ||||||
|        |  | ||||||
|       // Generate the initial key - repeated hashing of password + padding |  | ||||||
|       const password = Buffer.from(key); |  | ||||||
|       let passwordIndex = 0; |  | ||||||
|        |  | ||||||
|       // Create a buffer of 1MB (1048576 bytes) filled with the password |  | ||||||
|       const buffer = Buffer.alloc(1048576); |  | ||||||
|       for (let i = 0; i < 1048576; i++) { |  | ||||||
|         buffer[i] = password[passwordIndex]; |  | ||||||
|         passwordIndex = (passwordIndex + 1) % password.length; |  | ||||||
|       } |  | ||||||
|        |  | ||||||
|       initialHash.update(buffer); |  | ||||||
|       let initialKey = initialHash.digest(); |  | ||||||
|        |  | ||||||
|       // Localize the key with engine ID |  | ||||||
|       let localHash; |  | ||||||
|       if (authProtocol === 'SHA') { |  | ||||||
|         localHash = crypto.createHash('sha1'); |  | ||||||
|       } else { |  | ||||||
|         localHash = crypto.createHash('md5'); |  | ||||||
|       } |  | ||||||
|        |  | ||||||
|       localHash.update(initialKey); |  | ||||||
|       localHash.update(engineId); |  | ||||||
|       localHash.update(initialKey); |  | ||||||
|        |  | ||||||
|       return localHash.digest(); |  | ||||||
|     } catch (error) { |  | ||||||
|       console.error('Error localizing auth key:', error); |  | ||||||
|       // Return a fallback key |  | ||||||
|       return Buffer.from(key); |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   /** |  | ||||||
|    * Create a discovery message for SNMPv3 engine ID discovery |  | ||||||
|    * @param config SNMP configuration |  | ||||||
|    * @param requestID Request ID |  | ||||||
|    * @returns Discovery message |  | ||||||
|    */ |  | ||||||
|   public static createDiscoveryMessage(config: ISnmpConfig, requestID: number): Buffer { |  | ||||||
|     // Basic SNMPv3 header for discovery |  | ||||||
|     const msgIdBuf = Buffer.concat([ |  | ||||||
|       Buffer.from([0x02, 0x04]), // ASN.1 Integer, length 4 |  | ||||||
|       SnmpEncoder.encodeInteger(requestID) |  | ||||||
|     ]); |  | ||||||
|      |  | ||||||
|     const msgMaxSizeBuf = Buffer.concat([ |  | ||||||
|       Buffer.from([0x02, 0x04]), // ASN.1 Integer, length 4 |  | ||||||
|       SnmpEncoder.encodeInteger(65507) // Max message size |  | ||||||
|     ]); |  | ||||||
|      |  | ||||||
|     const msgFlagsBuf = Buffer.concat([ |  | ||||||
|       Buffer.from([0x04, 0x01]), // ASN.1 Octet String, length 1 |  | ||||||
|       Buffer.from([0x00]) // No authentication or privacy |  | ||||||
|     ]); |  | ||||||
|      |  | ||||||
|     const msgSecModelBuf = Buffer.concat([ |  | ||||||
|       Buffer.from([0x02, 0x01]), // ASN.1 Integer, length 1 |  | ||||||
|       Buffer.from([0x03]) // Security model (3 = USM) |  | ||||||
|     ]); |  | ||||||
|  |  | ||||||
|     // SNMPv3 header |  | ||||||
|     const msgHeaderBuf = Buffer.concat([ |  | ||||||
|       Buffer.from([0x30]), // ASN.1 Sequence |  | ||||||
|       Buffer.from([msgIdBuf.length + msgMaxSizeBuf.length + msgFlagsBuf.length + msgSecModelBuf.length]), // Length |  | ||||||
|       msgIdBuf, |  | ||||||
|       msgMaxSizeBuf, |  | ||||||
|       msgFlagsBuf, |  | ||||||
|       msgSecModelBuf |  | ||||||
|     ]); |  | ||||||
|      |  | ||||||
|     // Simple security parameters for discovery |  | ||||||
|     const securityBuf = Buffer.concat([ |  | ||||||
|       Buffer.from([0x04, 0x00]), // Empty octet string |  | ||||||
|     ]); |  | ||||||
|      |  | ||||||
|     // Simple Get request for discovery |  | ||||||
|     const requestIdBuf = Buffer.concat([ |  | ||||||
|       Buffer.from([0x02, 0x04]), // ASN.1 Integer, length 4 |  | ||||||
|       SnmpEncoder.encodeInteger(requestID + 1) |  | ||||||
|     ]); |  | ||||||
|      |  | ||||||
|     const errorStatusBuf = Buffer.concat([ |  | ||||||
|       Buffer.from([0x02, 0x01]), // ASN.1 Integer, length 1 |  | ||||||
|       Buffer.from([0x00])        // Error Status (0 = no error) |  | ||||||
|     ]); |  | ||||||
|      |  | ||||||
|     const errorIndexBuf = Buffer.concat([ |  | ||||||
|       Buffer.from([0x02, 0x01]), // ASN.1 Integer, length 1 |  | ||||||
|       Buffer.from([0x00])        // Error Index (0) |  | ||||||
|     ]); |  | ||||||
|      |  | ||||||
|     // Empty varbinds for discovery |  | ||||||
|     const varBindingsBuf = Buffer.concat([ |  | ||||||
|       Buffer.from([0x30, 0x00]), // Empty sequence |  | ||||||
|     ]); |  | ||||||
|      |  | ||||||
|     const pduBuf = Buffer.concat([ |  | ||||||
|       Buffer.from([0xa0]), // GetRequest |  | ||||||
|       Buffer.from([requestIdBuf.length + errorStatusBuf.length + errorIndexBuf.length + varBindingsBuf.length]), |  | ||||||
|       requestIdBuf, |  | ||||||
|       errorStatusBuf, |  | ||||||
|       errorIndexBuf, |  | ||||||
|       varBindingsBuf |  | ||||||
|     ]); |  | ||||||
|      |  | ||||||
|     // Context data |  | ||||||
|     const contextEngineBuf = Buffer.concat([ |  | ||||||
|       Buffer.from([0x04, 0x00]), // Empty octet string |  | ||||||
|     ]); |  | ||||||
|      |  | ||||||
|     const contextNameBuf = Buffer.concat([ |  | ||||||
|       Buffer.from([0x04, 0x00]), // Empty octet string |  | ||||||
|     ]); |  | ||||||
|      |  | ||||||
|     const scopedPduBuf = Buffer.concat([ |  | ||||||
|       Buffer.from([0x30]), |  | ||||||
|       Buffer.from([contextEngineBuf.length + contextNameBuf.length + pduBuf.length]), |  | ||||||
|       contextEngineBuf, |  | ||||||
|       contextNameBuf, |  | ||||||
|       pduBuf |  | ||||||
|     ]); |  | ||||||
|      |  | ||||||
|     // Version |  | ||||||
|     const versionBuf = Buffer.concat([ |  | ||||||
|       Buffer.from([0x02, 0x01]), // ASN.1 Integer, length 1 |  | ||||||
|       Buffer.from([0x03])        // SNMP version 3 (3) |  | ||||||
|     ]); |  | ||||||
|      |  | ||||||
|     // Complete message |  | ||||||
|     return Buffer.concat([ |  | ||||||
|       Buffer.from([0x30]), |  | ||||||
|       Buffer.from([versionBuf.length + msgHeaderBuf.length + securityBuf.length + scopedPduBuf.length]), |  | ||||||
|       versionBuf, |  | ||||||
|       msgHeaderBuf, |  | ||||||
|       securityBuf, |  | ||||||
|       scopedPduBuf |  | ||||||
|     ]); |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| @@ -1,553 +0,0 @@ | |||||||
| import type { ISnmpConfig } from './types.js'; |  | ||||||
| import { SnmpEncoder } from './encoder.js'; |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * SNMP packet parsing utilities |  | ||||||
|  * Parses SNMP response packets |  | ||||||
|  */ |  | ||||||
| export class SnmpPacketParser { |  | ||||||
|   /** |  | ||||||
|    * Parse an SNMP response |  | ||||||
|    * @param buffer Response buffer |  | ||||||
|    * @param config SNMP configuration |  | ||||||
|    * @param debug Whether to enable debug output |  | ||||||
|    * @returns Parsed value or null if parsing failed |  | ||||||
|    */ |  | ||||||
|   public static parseSnmpResponse(buffer: Buffer, config: ISnmpConfig, debug: boolean = false): any { |  | ||||||
|     // Check if we have a response packet |  | ||||||
|     if (buffer[0] !== 0x30) { |  | ||||||
|       throw new Error('Invalid SNMP response format'); |  | ||||||
|     } |  | ||||||
|      |  | ||||||
|     // For SNMPv3, we need to handle the message differently |  | ||||||
|     if (config.version === 3) { |  | ||||||
|       return this.parseSnmpV3Response(buffer, debug); |  | ||||||
|     } |  | ||||||
|      |  | ||||||
|     if (debug) { |  | ||||||
|       console.log('Parsing SNMPv1/v2 response: ', buffer.toString('hex')); |  | ||||||
|     } |  | ||||||
|      |  | ||||||
|     try { |  | ||||||
|       // Enhanced structured parsing approach |  | ||||||
|       // SEQUENCE header |  | ||||||
|       let pos = 0; |  | ||||||
|       if (buffer[pos] !== 0x30) { |  | ||||||
|         throw new Error('Missing SEQUENCE at start of response'); |  | ||||||
|       } |  | ||||||
|       // Skip SEQUENCE header - assume length is in single byte for simplicity |  | ||||||
|       // In a more robust implementation, we'd handle multi-byte lengths |  | ||||||
|       pos += 2; |  | ||||||
|        |  | ||||||
|       // VERSION |  | ||||||
|       if (buffer[pos] !== 0x02) { |  | ||||||
|         throw new Error('Missing INTEGER for version'); |  | ||||||
|       } |  | ||||||
|       const versionLength = buffer[pos + 1]; |  | ||||||
|       pos += 2 + versionLength; |  | ||||||
|        |  | ||||||
|       // COMMUNITY STRING |  | ||||||
|       if (buffer[pos] !== 0x04) { |  | ||||||
|         throw new Error('Missing OCTET STRING for community'); |  | ||||||
|       } |  | ||||||
|       const communityLength = buffer[pos + 1]; |  | ||||||
|       pos += 2 + communityLength; |  | ||||||
|        |  | ||||||
|       // PDU TYPE - should be RESPONSE (0xA2) |  | ||||||
|       if (buffer[pos] !== 0xA2) { |  | ||||||
|         throw new Error(`Unexpected PDU type: 0x${buffer[pos].toString(16)}, expected 0xA2`); |  | ||||||
|       } |  | ||||||
|       // Skip PDU header |  | ||||||
|       pos += 2; |  | ||||||
|        |  | ||||||
|       // REQUEST ID |  | ||||||
|       if (buffer[pos] !== 0x02) { |  | ||||||
|         throw new Error('Missing INTEGER for request ID'); |  | ||||||
|       } |  | ||||||
|       const requestIdLength = buffer[pos + 1]; |  | ||||||
|       pos += 2 + requestIdLength; |  | ||||||
|        |  | ||||||
|       // ERROR STATUS |  | ||||||
|       if (buffer[pos] !== 0x02) { |  | ||||||
|         throw new Error('Missing INTEGER for error status'); |  | ||||||
|       } |  | ||||||
|       const errorStatusLength = buffer[pos + 1]; |  | ||||||
|       const errorStatus = SnmpEncoder.decodeInteger(buffer, pos + 2, errorStatusLength); |  | ||||||
|        |  | ||||||
|       if (errorStatus !== 0) { |  | ||||||
|         throw new Error(`SNMP error status: ${errorStatus}`); |  | ||||||
|       } |  | ||||||
|       pos += 2 + errorStatusLength; |  | ||||||
|        |  | ||||||
|       // ERROR INDEX |  | ||||||
|       if (buffer[pos] !== 0x02) { |  | ||||||
|         throw new Error('Missing INTEGER for error index'); |  | ||||||
|       } |  | ||||||
|       const errorIndexLength = buffer[pos + 1]; |  | ||||||
|       pos += 2 + errorIndexLength; |  | ||||||
|        |  | ||||||
|       // VARBIND LIST |  | ||||||
|       if (buffer[pos] !== 0x30) { |  | ||||||
|         throw new Error('Missing SEQUENCE for varbind list'); |  | ||||||
|       } |  | ||||||
|       // Skip varbind list header |  | ||||||
|       pos += 2; |  | ||||||
|        |  | ||||||
|       // VARBIND |  | ||||||
|       if (buffer[pos] !== 0x30) { |  | ||||||
|         throw new Error('Missing SEQUENCE for varbind'); |  | ||||||
|       } |  | ||||||
|       // Skip varbind header |  | ||||||
|       pos += 2; |  | ||||||
|        |  | ||||||
|       // OID |  | ||||||
|       if (buffer[pos] !== 0x06) { |  | ||||||
|         throw new Error('Missing OBJECT IDENTIFIER for OID'); |  | ||||||
|       } |  | ||||||
|       const oidLength = buffer[pos + 1]; |  | ||||||
|       pos += 2 + oidLength; |  | ||||||
|        |  | ||||||
|       // VALUE - this is what we want |  | ||||||
|       const valueType = buffer[pos]; |  | ||||||
|       const valueLength = buffer[pos + 1]; |  | ||||||
|        |  | ||||||
|       if (debug) { |  | ||||||
|         console.log(`Found value type: 0x${valueType.toString(16)}, length: ${valueLength}`); |  | ||||||
|       } |  | ||||||
|        |  | ||||||
|       return this.parseValueByType(valueType, valueLength, buffer, pos, debug); |  | ||||||
|     } catch (error) { |  | ||||||
|       if (debug) { |  | ||||||
|         console.error('Error in structured parsing:', error); |  | ||||||
|         console.error('Falling back to scan-based parsing method'); |  | ||||||
|       } |  | ||||||
|        |  | ||||||
|       return this.scanBasedParsing(buffer, debug); |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   /** |  | ||||||
|    * Parse value by ASN.1 type |  | ||||||
|    * @param valueType ASN.1 type |  | ||||||
|    * @param valueLength Value length |  | ||||||
|    * @param buffer Buffer containing the value |  | ||||||
|    * @param pos Position of the value in the buffer |  | ||||||
|    * @param debug Whether to enable debug output |  | ||||||
|    * @returns Parsed value |  | ||||||
|    */ |  | ||||||
|   private static parseValueByType( |  | ||||||
|     valueType: number,  |  | ||||||
|     valueLength: number,  |  | ||||||
|     buffer: Buffer,  |  | ||||||
|     pos: number,  |  | ||||||
|     debug: boolean |  | ||||||
|   ): any { |  | ||||||
|     switch (valueType) { |  | ||||||
|       case 0x02: // INTEGER |  | ||||||
|         { |  | ||||||
|           const value = SnmpEncoder.decodeInteger(buffer, pos + 2, valueLength); |  | ||||||
|           if (debug) { |  | ||||||
|             console.log('Parsed INTEGER value:', value); |  | ||||||
|           } |  | ||||||
|           return value; |  | ||||||
|         } |  | ||||||
|          |  | ||||||
|       case 0x04: // OCTET STRING |  | ||||||
|         { |  | ||||||
|           const value = buffer.slice(pos + 2, pos + 2 + valueLength).toString(); |  | ||||||
|           if (debug) { |  | ||||||
|             console.log('Parsed OCTET STRING value:', value); |  | ||||||
|           } |  | ||||||
|           return value; |  | ||||||
|         } |  | ||||||
|          |  | ||||||
|       case 0x05: // NULL |  | ||||||
|         if (debug) { |  | ||||||
|           console.log('Parsed NULL value'); |  | ||||||
|         } |  | ||||||
|         return null; |  | ||||||
|          |  | ||||||
|       case 0x06: // OBJECT IDENTIFIER (rare in a value position) |  | ||||||
|         { |  | ||||||
|           // Usually this would be encoded as a string representation |  | ||||||
|           const value = buffer.slice(pos + 2, pos + 2 + valueLength).toString('hex'); |  | ||||||
|           if (debug) { |  | ||||||
|             console.log('Parsed OBJECT IDENTIFIER value (hex):', value); |  | ||||||
|           } |  | ||||||
|           return value; |  | ||||||
|         } |  | ||||||
|          |  | ||||||
|       case 0x40: // IP ADDRESS |  | ||||||
|         { |  | ||||||
|           if (valueLength !== 4) { |  | ||||||
|             throw new Error(`Invalid IP address length: ${valueLength}, expected 4`); |  | ||||||
|           } |  | ||||||
|           const octets = []; |  | ||||||
|           for (let i = 0; i < 4; i++) { |  | ||||||
|             octets.push(buffer[pos + 2 + i]); |  | ||||||
|           } |  | ||||||
|           const value = octets.join('.'); |  | ||||||
|           if (debug) { |  | ||||||
|             console.log('Parsed IP ADDRESS value:', value); |  | ||||||
|           } |  | ||||||
|           return value; |  | ||||||
|         } |  | ||||||
|          |  | ||||||
|       case 0x41: // COUNTER |  | ||||||
|       case 0x42: // GAUGE32 |  | ||||||
|       case 0x43: // TIMETICKS |  | ||||||
|       case 0x44: // OPAQUE |  | ||||||
|         { |  | ||||||
|           // All these are essentially unsigned 32-bit integers |  | ||||||
|           const value = SnmpEncoder.decodeInteger(buffer, pos + 2, valueLength); |  | ||||||
|           if (debug) { |  | ||||||
|             console.log(`Parsed ${valueType === 0x41 ? 'COUNTER'  |  | ||||||
|                         : valueType === 0x42 ? 'GAUGE32' |  | ||||||
|                         : valueType === 0x43 ? 'TIMETICKS' |  | ||||||
|                         : 'OPAQUE'} value:`, value); |  | ||||||
|           } |  | ||||||
|           return value; |  | ||||||
|         } |  | ||||||
|          |  | ||||||
|       default: |  | ||||||
|         if (debug) { |  | ||||||
|           console.log(`Unknown value type: 0x${valueType.toString(16)}`); |  | ||||||
|         } |  | ||||||
|         return null; |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   /** |  | ||||||
|    * Fallback scan-based parsing method |  | ||||||
|    * @param buffer Buffer containing the SNMP response |  | ||||||
|    * @param debug Whether to enable debug output |  | ||||||
|    * @returns Parsed value or null if parsing failed |  | ||||||
|    */ |  | ||||||
|   private static scanBasedParsing(buffer: Buffer, debug: boolean): any { |  | ||||||
|     // Look for various data types in the response |  | ||||||
|     // The value is near the end of the packet after the OID |  | ||||||
|      |  | ||||||
|     // We're looking for one of these: |  | ||||||
|     // 0x02 - Integer - can be at the end of a varbind |  | ||||||
|     // 0x04 - OctetString |  | ||||||
|     // 0x05 - Null |  | ||||||
|     // 0x42 - Gauge32 - special type for unsigned 32-bit integers |  | ||||||
|     // 0x43 - Timeticks - special type for time values |  | ||||||
|  |  | ||||||
|     // This algorithm performs a thorough search for data types |  | ||||||
|     // by iterating from the start and watching for varbind structures |  | ||||||
|      |  | ||||||
|     // Walk through the buffer looking for varbinds |  | ||||||
|     let i = 0; |  | ||||||
|      |  | ||||||
|     // First, find the varbinds section (0x30 sequence) |  | ||||||
|     while (i < buffer.length - 2) { |  | ||||||
|       // Look for a varbinds sequence |  | ||||||
|       if (buffer[i] === 0x30) { |  | ||||||
|         const varbindsLength = buffer[i + 1]; |  | ||||||
|         const varbindsEnd = i + 2 + varbindsLength; |  | ||||||
|          |  | ||||||
|         // Now search within the varbinds for the value |  | ||||||
|         let j = i + 2; |  | ||||||
|         while (j < varbindsEnd - 2) { |  | ||||||
|           // Look for a varbind (0x30 sequence) |  | ||||||
|           if (buffer[j] === 0x30) { |  | ||||||
|             const varbindLength = buffer[j + 1]; |  | ||||||
|             const varbindEnd = j + 2 + varbindLength; |  | ||||||
|              |  | ||||||
|             // Skip over the OID and find the value within this varbind |  | ||||||
|             let k = j + 2; |  | ||||||
|             while (k < varbindEnd - 1) { |  | ||||||
|               // First find the OID |  | ||||||
|               if (buffer[k] === 0x06) { // OID |  | ||||||
|                 const oidLength = buffer[k + 1]; |  | ||||||
|                 k += 2 + oidLength; // Skip past the OID |  | ||||||
|                  |  | ||||||
|                 // We should now be at the value |  | ||||||
|                 // Check what type it is |  | ||||||
|                 if (k < varbindEnd - 1) { |  | ||||||
|                   return this.parseValueAtPosition(buffer, k, debug); |  | ||||||
|                 } |  | ||||||
|                  |  | ||||||
|                 // If we didn't find a value, move to next byte |  | ||||||
|                 k++; |  | ||||||
|               } else { |  | ||||||
|                 // Move to next byte |  | ||||||
|                 k++; |  | ||||||
|               } |  | ||||||
|             } |  | ||||||
|              |  | ||||||
|             // Move to next varbind |  | ||||||
|             j = varbindEnd; |  | ||||||
|           } else { |  | ||||||
|             // Move to next byte |  | ||||||
|             j++; |  | ||||||
|           } |  | ||||||
|         } |  | ||||||
|          |  | ||||||
|         // Move to next sequence |  | ||||||
|         i = varbindsEnd; |  | ||||||
|       } else { |  | ||||||
|         // Move to next byte |  | ||||||
|         i++; |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|      |  | ||||||
|     if (debug) { |  | ||||||
|       console.log('No valid value found in SNMP response'); |  | ||||||
|     } |  | ||||||
|     return null; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   /** |  | ||||||
|    * Parse value at a specific position in the buffer |  | ||||||
|    * @param buffer Buffer containing the SNMP response |  | ||||||
|    * @param pos Position of the value in the buffer |  | ||||||
|    * @param debug Whether to enable debug output |  | ||||||
|    * @returns Parsed value or null if parsing failed |  | ||||||
|    */ |  | ||||||
|   private static parseValueAtPosition(buffer: Buffer, pos: number, debug: boolean): any { |  | ||||||
|     if (buffer[pos] === 0x02) { // Integer |  | ||||||
|       const valueLength = buffer[pos + 1]; |  | ||||||
|       const value = SnmpEncoder.decodeInteger(buffer, pos + 2, valueLength); |  | ||||||
|       if (debug) { |  | ||||||
|         console.log('Found Integer value:', value); |  | ||||||
|       } |  | ||||||
|       return value; |  | ||||||
|     } else if (buffer[pos] === 0x42) { // Gauge32 |  | ||||||
|       const valueLength = buffer[pos + 1]; |  | ||||||
|       const value = SnmpEncoder.decodeInteger(buffer, pos + 2, valueLength); |  | ||||||
|       if (debug) { |  | ||||||
|         console.log('Found Gauge32 value:', value); |  | ||||||
|       } |  | ||||||
|       return value; |  | ||||||
|     } else if (buffer[pos] === 0x43) { // TimeTicks |  | ||||||
|       const valueLength = buffer[pos + 1]; |  | ||||||
|       const value = SnmpEncoder.decodeInteger(buffer, pos + 2, valueLength); |  | ||||||
|       if (debug) { |  | ||||||
|         console.log('Found Timeticks value:', value); |  | ||||||
|       } |  | ||||||
|       return value; |  | ||||||
|     } else if (buffer[pos] === 0x04) { // OctetString |  | ||||||
|       const valueLength = buffer[pos + 1]; |  | ||||||
|       if (debug) { |  | ||||||
|         console.log('Found OctetString value'); |  | ||||||
|       } |  | ||||||
|       // Just return the string value as-is |  | ||||||
|       return buffer.slice(pos + 2, pos + 2 + valueLength).toString(); |  | ||||||
|     } else if (buffer[pos] === 0x05) { // Null |  | ||||||
|       if (debug) { |  | ||||||
|         console.log('Found Null value'); |  | ||||||
|       } |  | ||||||
|       return null; |  | ||||||
|     } |  | ||||||
|      |  | ||||||
|     return null; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   /** |  | ||||||
|    * Parse an SNMPv3 response |  | ||||||
|    * @param buffer Buffer containing the SNMP response |  | ||||||
|    * @param debug Whether to enable debug output |  | ||||||
|    * @returns Parsed value or null if parsing failed |  | ||||||
|    */ |  | ||||||
|   public static parseSnmpV3Response(buffer: Buffer, debug: boolean = false): any { |  | ||||||
|     // SNMPv3 parsing is complex. In a real implementation, we would: |  | ||||||
|     // 1. Parse the header and get the security parameters |  | ||||||
|     // 2. Verify authentication if used |  | ||||||
|     // 3. Decrypt the PDU if privacy was used |  | ||||||
|     // 4. Extract the PDU and parse it |  | ||||||
|      |  | ||||||
|     if (debug) { |  | ||||||
|       console.log('Parsing SNMPv3 response: ', buffer.toString('hex')); |  | ||||||
|     } |  | ||||||
|      |  | ||||||
|     // Find the scopedPDU - it should be the last OCTET STRING in the message |  | ||||||
|     let scopedPduPos = -1; |  | ||||||
|     for (let i = buffer.length - 50; i >= 0; i--) { |  | ||||||
|       if (buffer[i] === 0x04 && buffer[i + 1] > 10) { // OCTET STRING with reasonable length |  | ||||||
|         scopedPduPos = i; |  | ||||||
|         break; |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|      |  | ||||||
|     if (scopedPduPos === -1) { |  | ||||||
|       if (debug) { |  | ||||||
|         console.log('Could not find scoped PDU in SNMPv3 response'); |  | ||||||
|       } |  | ||||||
|       return null; |  | ||||||
|     } |  | ||||||
|      |  | ||||||
|     // Skip to the PDU content |  | ||||||
|     let pduContent = buffer.slice(scopedPduPos + 2); // Skip OCTET STRING header |  | ||||||
|      |  | ||||||
|     // This improved algorithm performs a more thorough search for varbinds  |  | ||||||
|     // in the scoped PDU |  | ||||||
|      |  | ||||||
|     // First, look for the response PDU (sequence with tag 0xa2) |  | ||||||
|     let responsePdu = null; |  | ||||||
|     for (let i = 0; i < pduContent.length - 3; i++) { |  | ||||||
|       if (pduContent[i] === 0xa2) { |  | ||||||
|         // Found the response PDU |  | ||||||
|         const pduLength = pduContent[i + 1]; |  | ||||||
|         responsePdu = pduContent.slice(i, i + 2 + pduLength); |  | ||||||
|         break; |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|      |  | ||||||
|     if (!responsePdu) { |  | ||||||
|       // Try to find the varbinds directly |  | ||||||
|       for (let i = 0; i < pduContent.length - 3; i++) { |  | ||||||
|         if (pduContent[i] === 0x30) { |  | ||||||
|           const seqLength = pduContent[i + 1]; |  | ||||||
|           if (i + 2 + seqLength <= pduContent.length) { |  | ||||||
|             // Check if this sequence might be the varbinds |  | ||||||
|             const possibleVarbinds = pduContent.slice(i, i + 2 + seqLength); |  | ||||||
|              |  | ||||||
|             // Look for varbind structure inside |  | ||||||
|             for (let j = 0; j < possibleVarbinds.length - 3; j++) { |  | ||||||
|               if (possibleVarbinds[j] === 0x30) { |  | ||||||
|                 // Might be a varbind - look for an OID inside |  | ||||||
|                 for (let k = j; k < j + 10 && k < possibleVarbinds.length - 1; k++) { |  | ||||||
|                   if (possibleVarbinds[k] === 0x06) { |  | ||||||
|                     // Found an OID, so this is likely the varbinds sequence |  | ||||||
|                     responsePdu = possibleVarbinds; |  | ||||||
|                     break; |  | ||||||
|                   } |  | ||||||
|                 } |  | ||||||
|                  |  | ||||||
|                 if (responsePdu) break; |  | ||||||
|               } |  | ||||||
|             } |  | ||||||
|              |  | ||||||
|             if (responsePdu) break; |  | ||||||
|           } |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|      |  | ||||||
|     if (!responsePdu) { |  | ||||||
|       if (debug) { |  | ||||||
|         console.log('Could not find response PDU in SNMPv3 response'); |  | ||||||
|       } |  | ||||||
|       return null; |  | ||||||
|     } |  | ||||||
|      |  | ||||||
|     // Now that we have the response PDU, search for varbinds |  | ||||||
|     // Skip the first few bytes to get past the header fields |  | ||||||
|     let varbindsPos = -1; |  | ||||||
|     for (let i = 10; i < responsePdu.length - 3; i++) { |  | ||||||
|       if (responsePdu[i] === 0x30) { |  | ||||||
|         // Check if this is the start of the varbinds |  | ||||||
|         // by seeing if it contains a varbind sequence |  | ||||||
|         for (let j = i + 2; j < i + 10 && j < responsePdu.length - 3; j++) { |  | ||||||
|           if (responsePdu[j] === 0x30) { |  | ||||||
|             varbindsPos = i; |  | ||||||
|             break; |  | ||||||
|           } |  | ||||||
|         } |  | ||||||
|          |  | ||||||
|         if (varbindsPos !== -1) break; |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|      |  | ||||||
|     if (varbindsPos === -1) { |  | ||||||
|       if (debug) { |  | ||||||
|         console.log('Could not find varbinds in SNMPv3 response'); |  | ||||||
|       } |  | ||||||
|       return null; |  | ||||||
|     } |  | ||||||
|      |  | ||||||
|     // Get the varbinds |  | ||||||
|     const varbindsLength = responsePdu[varbindsPos + 1]; |  | ||||||
|     const varbinds = responsePdu.slice(varbindsPos, varbindsPos + 2 + varbindsLength); |  | ||||||
|      |  | ||||||
|     // Now search for values inside the varbinds |  | ||||||
|     for (let i = 2; i < varbinds.length - 3; i++) { |  | ||||||
|       // Look for a varbind sequence |  | ||||||
|       if (varbinds[i] === 0x30) { |  | ||||||
|         const varbindLength = varbinds[i + 1]; |  | ||||||
|         const varbind = varbinds.slice(i, i + 2 + varbindLength); |  | ||||||
|          |  | ||||||
|         // Inside the varbind, look for the OID and then the value |  | ||||||
|         for (let j = 0; j < varbind.length - 3; j++) { |  | ||||||
|           if (varbind[j] === 0x06) { // OID |  | ||||||
|             const oidLength = varbind[j + 1]; |  | ||||||
|              |  | ||||||
|             // The value should be right after the OID |  | ||||||
|             const valuePos = j + 2 + oidLength; |  | ||||||
|             if (valuePos < varbind.length - 1) { |  | ||||||
|               // Check what type of value it is |  | ||||||
|               if (varbind[valuePos] === 0x02) { // INTEGER |  | ||||||
|                 const valueLength = varbind[valuePos + 1]; |  | ||||||
|                 const value = SnmpEncoder.decodeInteger(varbind, valuePos + 2, valueLength); |  | ||||||
|                 if (debug) { |  | ||||||
|                   console.log('Found INTEGER value in SNMPv3 response:', value); |  | ||||||
|                 } |  | ||||||
|                 return value; |  | ||||||
|               } else if (varbind[valuePos] === 0x42) { // Gauge32 |  | ||||||
|                 const valueLength = varbind[valuePos + 1]; |  | ||||||
|                 const value = SnmpEncoder.decodeInteger(varbind, valuePos + 2, valueLength); |  | ||||||
|                 if (debug) { |  | ||||||
|                   console.log('Found Gauge32 value in SNMPv3 response:', value); |  | ||||||
|                 } |  | ||||||
|                 return value; |  | ||||||
|               } else if (varbind[valuePos] === 0x43) { // TimeTicks |  | ||||||
|                 const valueLength = varbind[valuePos + 1]; |  | ||||||
|                 const value = SnmpEncoder.decodeInteger(varbind, valuePos + 2, valueLength); |  | ||||||
|                 if (debug) { |  | ||||||
|                   console.log('Found TimeTicks value in SNMPv3 response:', value); |  | ||||||
|                 } |  | ||||||
|                 return value; |  | ||||||
|               } else if (varbind[valuePos] === 0x04) { // OctetString |  | ||||||
|                 const valueLength = varbind[valuePos + 1]; |  | ||||||
|                 const value = varbind.slice(valuePos + 2, valuePos + 2 + valueLength).toString(); |  | ||||||
|                 if (debug) { |  | ||||||
|                   console.log('Found OctetString value in SNMPv3 response:', value); |  | ||||||
|                 } |  | ||||||
|                 return value; |  | ||||||
|               } |  | ||||||
|             } |  | ||||||
|           } |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|      |  | ||||||
|     if (debug) { |  | ||||||
|       console.log('No valid value found in SNMPv3 response'); |  | ||||||
|     } |  | ||||||
|     return null; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   /** |  | ||||||
|    * Extract engine ID from SNMPv3 response |  | ||||||
|    * @param buffer Buffer containing the SNMP response |  | ||||||
|    * @param debug Whether to enable debug output |  | ||||||
|    * @returns Extracted engine ID or null if extraction failed |  | ||||||
|    */ |  | ||||||
|   public static extractEngineId(buffer: Buffer, debug: boolean = false): Buffer | null { |  | ||||||
|     try { |  | ||||||
|       // Simple parsing to find the engine ID |  | ||||||
|       // Look for the first octet string with appropriate length |  | ||||||
|       for (let i = 0; i < buffer.length - 10; i++) { |  | ||||||
|         if (buffer[i] === 0x04) { // Octet string |  | ||||||
|           const len = buffer[i + 1]; |  | ||||||
|           if (len >= 5 && len <= 32) { // Engine IDs are typically 5-32 bytes |  | ||||||
|             // Verify this looks like an engine ID (usually starts with 0x80) |  | ||||||
|             if (buffer[i + 2] === 0x80) { |  | ||||||
|               if (debug) { |  | ||||||
|                 console.log('Found engine ID at position', i); |  | ||||||
|                 console.log('Engine ID:', buffer.slice(i + 2, i + 2 + len).toString('hex')); |  | ||||||
|               } |  | ||||||
|               return buffer.slice(i + 2, i + 2 + len); |  | ||||||
|             } |  | ||||||
|           } |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|       return null; |  | ||||||
|     } catch (error) { |  | ||||||
|       console.error('Error extracting engine ID:', error); |  | ||||||
|       return null; |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| @@ -46,6 +46,8 @@ export interface ISnmpConfig { | |||||||
|   /** Timeout in milliseconds */ |   /** Timeout in milliseconds */ | ||||||
|   timeout: number; |   timeout: number; | ||||||
|  |  | ||||||
|  |   context?: string; | ||||||
|  |    | ||||||
|   // SNMPv1/v2c |   // SNMPv1/v2c | ||||||
|   /** Community string for SNMPv1/v2c */ |   /** Community string for SNMPv1/v2c */ | ||||||
|   community?: string; |   community?: string; | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user