Compare commits
	
		
			19 Commits
		
	
	
		
			v2.5.1
			...
			87005e72f1
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 87005e72f1 | |||
| f799c2ee66 | |||
| 1a029ba493 | |||
| 5b756dd223 | |||
| 4cac599a58 | |||
| be6a7314c3 | |||
| 83ba9c2611 | |||
| 22ab472e58 | |||
| 9a77030377 | |||
| ceff285ff5 | |||
| d8bfbf0be3 | |||
| 3e6b883b38 | |||
| 47ef918128 | |||
| 5951638967 | |||
| b06e2b2273 | |||
| cc1cfe894c | |||
| da49b7a5bf | |||
| 4de6081a74 | |||
| 5a13e49803 | 
							
								
								
									
										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
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										66
									
								
								changelog.md
									
									
									
									
									
								
							
							
						
						
									
										66
									
								
								changelog.md
									
									
									
									
									
								
							@@ -1,5 +1,71 @@
 | 
				
			|||||||
# Changelog
 | 
					# Changelog
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## 2025-03-26 - 2.6.7 - fix(setup.sh)
 | 
				
			||||||
 | 
					Clarify net-snmp dependency installation message in setup.sh
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- Updated echo statement to indicate installation of net-snmp along with 2 subdependencies
 | 
				
			||||||
 | 
					- Improves clarity on dependency installation during setup
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## 2025-03-26 - 2.6.6 - fix(setup.sh)
 | 
				
			||||||
 | 
					Improve setup script to detect and execute npm-cli.js directly using the Node.js binary
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- Replace use of the npm binary with direct execution of npm-cli.js
 | 
				
			||||||
 | 
					- Add fallback logic to locate npm-cli.js when not found at the expected path
 | 
				
			||||||
 | 
					- Simplify cleanup by removing unnecessary PATH modifications
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## 2025-03-26 - 2.6.5 - fix(daemon, setup)
 | 
				
			||||||
 | 
					Improve shutdown command detection and fallback logic; update setup script to use absolute Node/npm paths
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- Use execFileAsync to execute shutdown commands reliably
 | 
				
			||||||
 | 
					- Add multiple fallback alternatives for shutdown and emergency shutdown handling
 | 
				
			||||||
 | 
					- Update setup.sh to log the Node and NPM versions using absolute paths without modifying PATH
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## 2025-03-26 - 2.6.4 - fix(setup)
 | 
				
			||||||
 | 
					Improve installation process in setup script by cleaning up package files and ensuring a minimal net-snmp dependency installation.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- Remove existing package-lock.json along with node_modules to prevent stale artifacts.
 | 
				
			||||||
 | 
					- Back up the original package.json before modifying it.
 | 
				
			||||||
 | 
					- Create a minimal package.json with only the net-snmp dependency based on the backed-up version.
 | 
				
			||||||
 | 
					- Use a clean install to guarantee that only net-snmp is installed.
 | 
				
			||||||
 | 
					- Restore the original package.json if the installation fails.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## 2025-03-26 - 2.6.3 - fix(setup)
 | 
				
			||||||
 | 
					Update setup script to install only net-snmp dependency and create a minimal package-lock.json for better dependency control.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- Removed full production dependency install in favor of installing only net-snmp@3.20.0
 | 
				
			||||||
 | 
					- Added verification step to confirm net-snmp installation
 | 
				
			||||||
 | 
					- Generate a minimal package-lock.json if one does not exist
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## 2025-03-26 - 2.6.2 - fix(setup/readme)
 | 
				
			||||||
 | 
					Improve force update instructions and dependency installation process in setup.sh and readme.md
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- Clarify force update commands with explicit paths in readme.md
 | 
				
			||||||
 | 
					- Remove existing node_modules before installing dependencies in setup.sh
 | 
				
			||||||
 | 
					- Switch from 'npm ci --only=production' to 'npm install --omit=dev' with updated error instructions
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## 2025-03-26 - 2.6.1 - fix(setup)
 | 
				
			||||||
 | 
					Update setup.sh to temporarily add vendor Node.js binary to PATH for dependency installation, log Node and npm versions, and restore the original PATH afterwards.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- Temporarily prepend vendor Node.js binary directory to PATH to ensure proper npm execution.
 | 
				
			||||||
 | 
					- Log Node.js and npm versions for debugging purposes.
 | 
				
			||||||
 | 
					- Restore the original PATH after installing dependencies.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## 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)
 | 
					## 2025-03-25 - 2.5.1 - fix(snmp)
 | 
				
			||||||
Fix Eaton UPS support by updating power status OID and adjusting battery runtime conversion.
 | 
					Fix Eaton UPS support by updating power status OID and adjusting battery runtime conversion.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,6 +1,6 @@
 | 
				
			|||||||
{
 | 
					{
 | 
				
			||||||
  "name": "@serve.zone/nupst",
 | 
					  "name": "@serve.zone/nupst",
 | 
				
			||||||
  "version": "2.5.1",
 | 
					  "version": "2.6.7",
 | 
				
			||||||
  "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:
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										37
									
								
								readme.md
									
									
									
									
									
								
							
							
						
						
									
										37
									
								
								readme.md
									
									
									
									
									
								
							@@ -227,8 +227,19 @@ 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
 | 
				
			||||||
 | 
					# If you're in the nupst directory:
 | 
				
			||||||
 | 
					bash ./setup.sh --force
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# If you're in another directory, specify the full path:
 | 
				
			||||||
 | 
					bash /opt/nupst/setup.sh --force
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
## Security
 | 
					## Security
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -236,10 +247,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
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -247,14 +258,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
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										103
									
								
								setup.sh
									
									
									
									
									
								
							
							
						
						
									
										103
									
								
								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,90 @@ 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"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Set up Node.js binary path
 | 
				
			||||||
 | 
					NODE_BIN_DIR="$SCRIPT_DIR/vendor/$NODE_DIR/bin"
 | 
				
			||||||
 | 
					NODE_BIN="$NODE_BIN_DIR/node"
 | 
				
			||||||
 | 
					NPM_CLI_JS="$NODE_BIN_DIR/../lib/node_modules/npm/bin/npm-cli.js"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Ensure we have executable permissions
 | 
				
			||||||
 | 
					chmod +x "$NODE_BIN"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Make sure the npm-cli.js exists
 | 
				
			||||||
 | 
					if [ ! -f "$NPM_CLI_JS" ]; then
 | 
				
			||||||
 | 
					  # Try to find npm-cli.js
 | 
				
			||||||
 | 
					  NPM_CLI_JS=$(find "$NODE_BIN_DIR/.." -name "npm-cli.js" | head -1)
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					  if [ -z "$NPM_CLI_JS" ]; then
 | 
				
			||||||
 | 
					    echo "Warning: Could not find npm-cli.js, npm commands may fail"
 | 
				
			||||||
 | 
					    # Set to a fallback value so code can continue
 | 
				
			||||||
 | 
					    NPM_CLI_JS="$NODE_BIN_DIR/npm"
 | 
				
			||||||
 | 
					  else
 | 
				
			||||||
 | 
					    echo "Found npm-cli.js at: $NPM_CLI_JS"
 | 
				
			||||||
 | 
					  fi
 | 
				
			||||||
 | 
					fi
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Display which binaries we're using
 | 
				
			||||||
 | 
					echo "Using Node binary: $NODE_BIN"
 | 
				
			||||||
 | 
					echo "Using NPM CLI JS: $NPM_CLI_JS"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Remove existing node_modules directory and package files
 | 
				
			||||||
 | 
					echo "Cleaning up existing installation..."
 | 
				
			||||||
 | 
					rm -rf "$SCRIPT_DIR/node_modules"
 | 
				
			||||||
 | 
					rm -f "$SCRIPT_DIR/package-lock.json"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Back up existing package.json if it exists
 | 
				
			||||||
 | 
					if [ -f "$SCRIPT_DIR/package.json" ]; then
 | 
				
			||||||
 | 
					  echo "Backing up existing package.json..."
 | 
				
			||||||
 | 
					  cp "$SCRIPT_DIR/package.json" "$SCRIPT_DIR/package.json.bak"
 | 
				
			||||||
 | 
					fi
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Create a clean minimal package.json with ONLY net-snmp dependency
 | 
				
			||||||
 | 
					echo "Creating minimal package.json with only net-snmp dependency..."
 | 
				
			||||||
 | 
					VERSION=$(grep -o '"version": "[^"]*"' "$SCRIPT_DIR/package.json.bak" | head -1 | cut -d'"' -f4 || echo "2.6.3")
 | 
				
			||||||
 | 
					echo '{
 | 
				
			||||||
 | 
					  "name": "@serve.zone/nupst",
 | 
				
			||||||
 | 
					  "version": "'$VERSION'",
 | 
				
			||||||
 | 
					  "description": "Node.js UPS Shutdown Tool for SNMP-enabled UPS devices",
 | 
				
			||||||
 | 
					  "main": "dist_ts/index.js",
 | 
				
			||||||
 | 
					  "type": "module",
 | 
				
			||||||
 | 
					  "bin": {
 | 
				
			||||||
 | 
					    "nupst": "bin/nupst"
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  "dependencies": {
 | 
				
			||||||
 | 
					    "net-snmp": "3.20.0"
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  "engines": {
 | 
				
			||||||
 | 
					    "node": ">=16.0.0"
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  "private": true
 | 
				
			||||||
 | 
					}' > "$SCRIPT_DIR/package.json"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Install ONLY net-snmp
 | 
				
			||||||
 | 
					echo "Installing ONLY net-snmp dependency (+ 2 subdependencies)..."
 | 
				
			||||||
 | 
					echo "Node version: $("$NODE_BIN" --version)"
 | 
				
			||||||
 | 
					echo "Executing NPM directly with Node.js"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Execute npm-cli.js directly with our Node.js binary
 | 
				
			||||||
 | 
					"$NODE_BIN" "$NPM_CLI_JS" --prefix "$SCRIPT_DIR" install --no-audit --no-fund
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					INSTALL_STATUS=$?
 | 
				
			||||||
 | 
					if [ $INSTALL_STATUS -ne 0 ]; then
 | 
				
			||||||
 | 
					  echo "Error: Failed to install net-snmp dependency. NUPST may not function correctly."
 | 
				
			||||||
 | 
					  echo "Restoring original package.json..."
 | 
				
			||||||
 | 
					  mv "$SCRIPT_DIR/package.json.bak" "$SCRIPT_DIR/package.json"
 | 
				
			||||||
 | 
					  exit 1
 | 
				
			||||||
 | 
					else
 | 
				
			||||||
 | 
					  echo "net-snmp dependency installed successfully."
 | 
				
			||||||
 | 
					  # Show what's actually installed
 | 
				
			||||||
 | 
					  echo "Installed modules:"
 | 
				
			||||||
 | 
					  find "$SCRIPT_DIR/node_modules" -maxdepth 1 -type d | grep -v "^$SCRIPT_DIR/node_modules$" | sort
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					  # Remove backup if successful
 | 
				
			||||||
 | 
					  rm -f "$SCRIPT_DIR/package.json.bak"
 | 
				
			||||||
 | 
					fi
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# No temporary files to clean up
 | 
				
			||||||
 | 
					
 | 
				
			||||||
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.5.1',
 | 
					  version: '2.6.7',
 | 
				
			||||||
  description: 'Node.js UPS Shutdown Tool for SNMP-enabled UPS devices'
 | 
					  description: 'Node.js UPS Shutdown Tool for SNMP-enabled UPS devices'
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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...');
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										171
									
								
								ts/daemon.ts
									
									
									
									
									
								
							
							
						
						
									
										171
									
								
								ts/daemon.ts
									
									
									
									
									
								
							@@ -1,10 +1,12 @@
 | 
				
			|||||||
import * as fs from 'fs';
 | 
					import * as fs from 'fs';
 | 
				
			||||||
import * as path from 'path';
 | 
					import * as path from 'path';
 | 
				
			||||||
import { exec } from 'child_process';
 | 
					import { exec, execFile } 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);
 | 
				
			||||||
 | 
					const execFileAsync = promisify(execFile);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * Configuration interface for the daemon
 | 
					 * Configuration interface for the daemon
 | 
				
			||||||
@@ -297,24 +299,102 @@ export class NupstDaemon {
 | 
				
			|||||||
    const shutdownDelayMinutes = 5;
 | 
					    const shutdownDelayMinutes = 5;
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
 | 
					      // Find shutdown command in common system paths
 | 
				
			||||||
 | 
					      const shutdownPaths = [
 | 
				
			||||||
 | 
					        '/sbin/shutdown',
 | 
				
			||||||
 | 
					        '/usr/sbin/shutdown',
 | 
				
			||||||
 | 
					        '/bin/shutdown',
 | 
				
			||||||
 | 
					        '/usr/bin/shutdown'
 | 
				
			||||||
 | 
					      ];
 | 
				
			||||||
 | 
					      
 | 
				
			||||||
 | 
					      let shutdownCmd = '';
 | 
				
			||||||
 | 
					      for (const path of shutdownPaths) {
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					          if (fs.existsSync(path)) {
 | 
				
			||||||
 | 
					            shutdownCmd = path;
 | 
				
			||||||
 | 
					            console.log(`Found shutdown command at: ${shutdownCmd}`);
 | 
				
			||||||
 | 
					            break;
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        } catch (e) {
 | 
				
			||||||
 | 
					          // Continue checking other paths
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      
 | 
				
			||||||
 | 
					      if (shutdownCmd) {
 | 
				
			||||||
        // Execute shutdown command with delay to allow for VM graceful shutdown
 | 
					        // Execute shutdown command with delay to allow for VM graceful shutdown
 | 
				
			||||||
      const { stdout } = await execAsync(`shutdown -h +${shutdownDelayMinutes} "UPS battery critical, shutting down in ${shutdownDelayMinutes} minutes"`);
 | 
					        console.log(`Executing: ${shutdownCmd} -h +${shutdownDelayMinutes} "UPS battery critical..."`);
 | 
				
			||||||
 | 
					        const { stdout } = await execFileAsync(shutdownCmd, [
 | 
				
			||||||
 | 
					          '-h', 
 | 
				
			||||||
 | 
					          `+${shutdownDelayMinutes}`, 
 | 
				
			||||||
 | 
					          `UPS battery critical, shutting down in ${shutdownDelayMinutes} minutes`
 | 
				
			||||||
 | 
					        ]);
 | 
				
			||||||
        console.log('Shutdown initiated:', stdout);
 | 
					        console.log('Shutdown initiated:', stdout);
 | 
				
			||||||
        console.log(`Allowing ${shutdownDelayMinutes} minutes for VMs to shut down safely`);
 | 
					        console.log(`Allowing ${shutdownDelayMinutes} minutes for VMs to shut down safely`);
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        // Try using the PATH to find shutdown
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					          console.log('Shutdown command not found in common paths, trying via PATH...');
 | 
				
			||||||
 | 
					          const { stdout } = await execAsync(`shutdown -h +${shutdownDelayMinutes} "UPS battery critical, shutting down in ${shutdownDelayMinutes} minutes"`, {
 | 
				
			||||||
 | 
					            env: process.env // Pass the current environment
 | 
				
			||||||
 | 
					          });
 | 
				
			||||||
 | 
					          console.log('Shutdown initiated:', stdout);
 | 
				
			||||||
 | 
					        } catch (e) {
 | 
				
			||||||
 | 
					          throw new Error(`Shutdown command not found: ${e.message}`);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
      
 | 
					      
 | 
				
			||||||
      // Monitor UPS during shutdown and force immediate shutdown if battery gets too low
 | 
					      // Monitor UPS during shutdown and force immediate shutdown if battery gets too low
 | 
				
			||||||
      console.log('Monitoring UPS during shutdown process...');
 | 
					      console.log('Monitoring UPS during shutdown process...');
 | 
				
			||||||
      await this.monitorDuringShutdown();
 | 
					      await this.monitorDuringShutdown();
 | 
				
			||||||
    } catch (error) {
 | 
					    } catch (error) {
 | 
				
			||||||
      console.error('Failed to initiate shutdown:', error);
 | 
					      console.error('Failed to initiate shutdown:', error);
 | 
				
			||||||
      // Try a different method if first one fails
 | 
					      
 | 
				
			||||||
 | 
					      // Try alternative shutdown methods
 | 
				
			||||||
 | 
					      const alternatives = [
 | 
				
			||||||
 | 
					        { cmd: 'poweroff', args: ['--force'] },
 | 
				
			||||||
 | 
					        { cmd: 'halt', args: ['-p'] },
 | 
				
			||||||
 | 
					        { cmd: 'systemctl', args: ['poweroff'] },
 | 
				
			||||||
 | 
					        { cmd: 'reboot', args: ['-p'] } // Some systems allow reboot -p for power off
 | 
				
			||||||
 | 
					      ];
 | 
				
			||||||
 | 
					      
 | 
				
			||||||
 | 
					      for (const alt of alternatives) {
 | 
				
			||||||
        try {
 | 
					        try {
 | 
				
			||||||
        console.log('Trying alternative shutdown method...');
 | 
					          // First check if command exists in common system paths
 | 
				
			||||||
        await execAsync('poweroff --force');
 | 
					          const paths = [
 | 
				
			||||||
      } catch (innerError) {
 | 
					            `/sbin/${alt.cmd}`,
 | 
				
			||||||
        console.error('All shutdown methods failed:', innerError);
 | 
					            `/usr/sbin/${alt.cmd}`,
 | 
				
			||||||
 | 
					            `/bin/${alt.cmd}`,
 | 
				
			||||||
 | 
					            `/usr/bin/${alt.cmd}`
 | 
				
			||||||
 | 
					          ];
 | 
				
			||||||
 | 
					          
 | 
				
			||||||
 | 
					          let cmdPath = '';
 | 
				
			||||||
 | 
					          for (const path of paths) {
 | 
				
			||||||
 | 
					            if (fs.existsSync(path)) {
 | 
				
			||||||
 | 
					              cmdPath = path;
 | 
				
			||||||
 | 
					              break;
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
 | 
					          
 | 
				
			||||||
 | 
					          if (cmdPath) {
 | 
				
			||||||
 | 
					            console.log(`Trying alternative shutdown method: ${cmdPath} ${alt.args.join(' ')}`);
 | 
				
			||||||
 | 
					            await execFileAsync(cmdPath, alt.args);
 | 
				
			||||||
 | 
					            return; // Exit if successful
 | 
				
			||||||
 | 
					          } else {
 | 
				
			||||||
 | 
					            // Try using PATH environment
 | 
				
			||||||
 | 
					            console.log(`Trying alternative via PATH: ${alt.cmd} ${alt.args.join(' ')}`);
 | 
				
			||||||
 | 
					            await execAsync(`${alt.cmd} ${alt.args.join(' ')}`, {
 | 
				
			||||||
 | 
					              env: process.env // Pass the current environment
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					            return; // Exit if successful
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        } catch (altError) {
 | 
				
			||||||
 | 
					          console.error(`Alternative method ${alt.cmd} failed:`, altError);
 | 
				
			||||||
 | 
					          // Continue to next method
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      
 | 
				
			||||||
 | 
					      console.error('All shutdown methods failed');
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  
 | 
					  
 | 
				
			||||||
  /**
 | 
					  /**
 | 
				
			||||||
@@ -345,10 +425,79 @@ export class NupstDaemon {
 | 
				
			|||||||
          console.log('└──────────────────────────────────────────┘');
 | 
					          console.log('└──────────────────────────────────────────┘');
 | 
				
			||||||
          
 | 
					          
 | 
				
			||||||
          try {
 | 
					          try {
 | 
				
			||||||
            await execAsync('shutdown -h now "EMERGENCY: UPS battery critically low, shutting down NOW"');
 | 
					            // Find shutdown command in common system paths
 | 
				
			||||||
 | 
					            const shutdownPaths = [
 | 
				
			||||||
 | 
					              '/sbin/shutdown',
 | 
				
			||||||
 | 
					              '/usr/sbin/shutdown',
 | 
				
			||||||
 | 
					              '/bin/shutdown',
 | 
				
			||||||
 | 
					              '/usr/bin/shutdown'
 | 
				
			||||||
 | 
					            ];
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            let shutdownCmd = '';
 | 
				
			||||||
 | 
					            for (const path of shutdownPaths) {
 | 
				
			||||||
 | 
					              if (fs.existsSync(path)) {
 | 
				
			||||||
 | 
					                shutdownCmd = path;
 | 
				
			||||||
 | 
					                console.log(`Found shutdown command at: ${shutdownCmd}`);
 | 
				
			||||||
 | 
					                break;
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            if (shutdownCmd) {
 | 
				
			||||||
 | 
					              console.log(`Executing emergency shutdown: ${shutdownCmd} -h now`);
 | 
				
			||||||
 | 
					              await execFileAsync(shutdownCmd, ['-h', 'now', 'EMERGENCY: UPS battery critically low, shutting down NOW']);
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					              // Try using the PATH to find shutdown
 | 
				
			||||||
 | 
					              console.log('Shutdown command not found in common paths, trying via PATH...');
 | 
				
			||||||
 | 
					              await execAsync('shutdown -h now "EMERGENCY: UPS battery critically low, shutting down NOW"', {
 | 
				
			||||||
 | 
					                env: process.env // Pass the current environment
 | 
				
			||||||
 | 
					              });
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
          } catch (error) {
 | 
					          } catch (error) {
 | 
				
			||||||
            console.error('Emergency shutdown failed, trying alternative method...');
 | 
					            console.error('Emergency shutdown failed, trying alternative methods...');
 | 
				
			||||||
            await execAsync('poweroff --force');
 | 
					            
 | 
				
			||||||
 | 
					            // Try alternative shutdown methods in sequence
 | 
				
			||||||
 | 
					            const alternatives = [
 | 
				
			||||||
 | 
					              { cmd: 'poweroff', args: ['--force'] },
 | 
				
			||||||
 | 
					              { cmd: 'halt', args: ['-p'] },
 | 
				
			||||||
 | 
					              { cmd: 'systemctl', args: ['poweroff'] }
 | 
				
			||||||
 | 
					            ];
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            for (const alt of alternatives) {
 | 
				
			||||||
 | 
					              try {
 | 
				
			||||||
 | 
					                // Check common paths
 | 
				
			||||||
 | 
					                const paths = [
 | 
				
			||||||
 | 
					                  `/sbin/${alt.cmd}`,
 | 
				
			||||||
 | 
					                  `/usr/sbin/${alt.cmd}`,
 | 
				
			||||||
 | 
					                  `/bin/${alt.cmd}`,
 | 
				
			||||||
 | 
					                  `/usr/bin/${alt.cmd}`
 | 
				
			||||||
 | 
					                ];
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                let cmdPath = '';
 | 
				
			||||||
 | 
					                for (const path of paths) {
 | 
				
			||||||
 | 
					                  if (fs.existsSync(path)) {
 | 
				
			||||||
 | 
					                    cmdPath = path;
 | 
				
			||||||
 | 
					                    break;
 | 
				
			||||||
 | 
					                  }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                if (cmdPath) {
 | 
				
			||||||
 | 
					                  console.log(`Emergency: using ${cmdPath} ${alt.args.join(' ')}`);
 | 
				
			||||||
 | 
					                  await execFileAsync(cmdPath, alt.args);
 | 
				
			||||||
 | 
					                  return; // Exit if successful
 | 
				
			||||||
 | 
					                } else {
 | 
				
			||||||
 | 
					                  // Try using PATH
 | 
				
			||||||
 | 
					                  console.log(`Emergency: trying ${alt.cmd} via PATH`);
 | 
				
			||||||
 | 
					                  await execAsync(`${alt.cmd} ${alt.args.join(' ')}`, {
 | 
				
			||||||
 | 
					                    env: process.env
 | 
				
			||||||
 | 
					                  });
 | 
				
			||||||
 | 
					                  return; // Exit if successful
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					              } catch (altError) {
 | 
				
			||||||
 | 
					                // Continue to next method
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            console.error('All emergency shutdown methods failed');
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
          
 | 
					          
 | 
				
			||||||
          // Stop monitoring after initiating emergency shutdown
 | 
					          // Stop monitoring after initiating emergency shutdown
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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,157 +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
 | 
					 | 
				
			||||||
      if (config.upsModel === 'cyberpower') {
 | 
					 | 
				
			||||||
        // 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 if (config.upsModel === 'eaton') {
 | 
					 | 
				
			||||||
        // Eaton UPS: xupsOutputSource values
 | 
					 | 
				
			||||||
        // 3=normal/mains, 5=battery, etc.
 | 
					 | 
				
			||||||
        if (powerStatusValue === 3) {
 | 
					 | 
				
			||||||
          powerStatus = 'online';
 | 
					 | 
				
			||||||
        } else if (powerStatusValue === 5) {
 | 
					 | 
				
			||||||
          powerStatus = 'onBattery';
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      } else {
 | 
					 | 
				
			||||||
        // Default interpretation for other UPS models
 | 
					 | 
				
			||||||
        if (powerStatusValue === 1) {
 | 
					 | 
				
			||||||
          powerStatus = 'online';
 | 
					 | 
				
			||||||
        } else if (powerStatusValue === 2) {
 | 
					 | 
				
			||||||
          powerStatus = 'onBattery';
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
      
 | 
					      
 | 
				
			||||||
      // Convert to minutes for UPS models with different time units
 | 
					      // Convert to minutes for UPS models with different time units
 | 
				
			||||||
      let processedRuntime = batteryRuntime;
 | 
					      const processedRuntime = this.processRuntimeValue(config.upsModel, batteryRuntime);
 | 
				
			||||||
      
 | 
					 | 
				
			||||||
      if (config.upsModel === 'cyberpower' && batteryRuntime > 0) {
 | 
					 | 
				
			||||||
        // CyberPower: 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`);
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      } else if (config.upsModel === 'eaton' && batteryRuntime > 0) {
 | 
					 | 
				
			||||||
        // Eaton: Runtime is in seconds, convert to minutes
 | 
					 | 
				
			||||||
        processedRuntime = Math.floor(batteryRuntime / 60);
 | 
					 | 
				
			||||||
        if (this.debug) {
 | 
					 | 
				
			||||||
          console.log(`Converting Eaton runtime from ${batteryRuntime} seconds to ${processedRuntime} minutes`);
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
      
 | 
					      
 | 
				
			||||||
      const result = {
 | 
					      const result = {
 | 
				
			||||||
        powerStatus,
 | 
					        powerStatus,
 | 
				
			||||||
@@ -414,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;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -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