Initial commit for SSL Management project
This commit includes: - SSL Manager implementation for certificate operations - Configuration file with UniFi device parameters - Test files for various components - Documentation for UniFi verification - Project guidelines
This commit is contained in:
parent
27329d3afa
commit
a78cf961ff
19
.gitignore
vendored
Normal file
19
.gitignore
vendored
Normal file
@ -0,0 +1,19 @@
|
||||
# IDE-specific files
|
||||
.idea/
|
||||
|
||||
# Generated certificates
|
||||
custom-certs/
|
||||
|
||||
# Python virtual environment
|
||||
.venv/
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# Distribution / packaging
|
||||
dist/
|
||||
build/
|
||||
*.egg-info/
|
||||
|
||||
# Local development settings
|
||||
.env
|
||||
266
.junie/guidelines.md
Normal file
266
.junie/guidelines.md
Normal file
@ -0,0 +1,266 @@
|
||||
# SSL Management Project Guidelines
|
||||
|
||||
This document provides guidelines for developing and maintaining the SSL Management project, which is a Python-based tool for managing SSL certificates for UniFi devices such as UDM-SE. The tool helps automate the process of obtaining, validating, and deploying SSL certificates to UniFi devices.
|
||||
|
||||
## Build/Configuration Instructions
|
||||
|
||||
### Prerequisites
|
||||
|
||||
1. **Python 3.6+**: The project is written in Python and requires Python 3.6 or higher.
|
||||
2. **OpenSSL**: Required for certificate operations. Must be installed and available in your PATH.
|
||||
3. **Virtual Environment**: The project uses a virtual environment for dependency isolation.
|
||||
4. **Linux Environment**: The script is designed to run on Linux systems such as Ubuntu 24.04. While development can occur on any platform, deployment is expected on a Linux server.
|
||||
|
||||
### Setup
|
||||
|
||||
1. Clone the repository:
|
||||
```bash
|
||||
git clone <repository-url>
|
||||
cd ssl-managment
|
||||
```
|
||||
|
||||
2. Create and activate a virtual environment:
|
||||
```bash
|
||||
python -m venv .venv
|
||||
source .venv/bin/activate # On Windows: .venv\Scripts\activate
|
||||
```
|
||||
|
||||
3. No external Python packages are required as the project uses standard library modules.
|
||||
|
||||
### Configuration
|
||||
|
||||
The SSL Manager uses a configuration file (`config.json`) to store default settings. The configuration file should be placed in the project root directory. Here's an example of the configuration file:
|
||||
|
||||
```json
|
||||
{
|
||||
"cert_dir": "~/.ssl-certs",
|
||||
"default_port": 443,
|
||||
"connection_timeout": 3.0,
|
||||
"default_validity_days": 365,
|
||||
"key_size": 2048,
|
||||
"unifi": {
|
||||
"host": "unifi.example.com",
|
||||
"username": "admin",
|
||||
"password": "password",
|
||||
"site": "default",
|
||||
"ssh_port": 22,
|
||||
"ssh_username": "root",
|
||||
"ssh_password": "",
|
||||
"ssh_key_path": "~/.ssh/id_rsa"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Configuration options:
|
||||
- `cert_dir`: Directory where certificates and keys will be stored
|
||||
- `default_port`: Default port to use when checking certificate expiration
|
||||
- `connection_timeout`: Timeout in seconds for SSL connections
|
||||
- `default_validity_days`: Default validity period in days for generated certificates
|
||||
- `key_size`: Key size in bits for generated certificates
|
||||
- `unifi`: UniFi device connection parameters
|
||||
- `host`: Hostname or IP address of the UniFi device
|
||||
- `username`: Username for authenticating with the UniFi device
|
||||
- `password`: Password for authenticating with the UniFi device
|
||||
- `site`: Site name for the UniFi device (default: 'default')
|
||||
- `ssh_port`: SSH port for the UniFi device (default: 22)
|
||||
- `ssh_username`: Username for SSH authentication with the UniFi device
|
||||
- `ssh_password`: Password for SSH authentication (leave empty to use SSH key)
|
||||
- `ssh_key_path`: Path to the SSH private key file for authentication
|
||||
|
||||
### Usage
|
||||
|
||||
The SSL Manager provides three main commands. All commands support the following global options:
|
||||
|
||||
- `--config`: Path to the config file (default: config.json)
|
||||
- `--cert-dir`: Directory to store certificates (overrides config)
|
||||
|
||||
1. **Check Certificate Expiration**:
|
||||
```bash
|
||||
python src/ssl_manager.py check example.com [--port PORT]
|
||||
```
|
||||
The `--port` option overrides the `default_port` from the config file.
|
||||
|
||||
2. **Generate Self-Signed Certificate**:
|
||||
```bash
|
||||
python src/ssl_manager.py generate example.com [--days DAYS]
|
||||
```
|
||||
The `--days` option overrides the `default_validity_days` from the config file.
|
||||
|
||||
3. **Validate Certificate Chain**:
|
||||
```bash
|
||||
python src/ssl_manager.py validate path/to/certificate.crt [--ca-path path/to/ca.crt]
|
||||
```
|
||||
|
||||
## Testing Information
|
||||
|
||||
### Running Tests
|
||||
|
||||
Tests are written using the Python `unittest` framework. To run all tests:
|
||||
|
||||
```bash
|
||||
cd ssl-managment # Ensure you're in the project root
|
||||
python -m unittest discover tests
|
||||
```
|
||||
|
||||
To run a specific test file:
|
||||
|
||||
```bash
|
||||
python -m tests.test_ssl_manager
|
||||
```
|
||||
|
||||
### Adding New Tests
|
||||
|
||||
1. Create a new test file in the `tests` directory, following the naming convention `test_*.py`.
|
||||
2. Import the necessary modules and the `SSLManager` class:
|
||||
```python
|
||||
import os
|
||||
import sys
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
# Add the src directory to the Python path
|
||||
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../src')))
|
||||
|
||||
from ssl_manager import SSLManager
|
||||
```
|
||||
|
||||
3. Create a test class that inherits from `unittest.TestCase`:
|
||||
```python
|
||||
class TestYourFeature(unittest.TestCase):
|
||||
def setUp(self):
|
||||
# Set up test fixtures
|
||||
self.ssl_manager = SSLManager()
|
||||
|
||||
def test_your_feature(self):
|
||||
# Test your feature
|
||||
result = self.ssl_manager.your_method()
|
||||
self.assertEqual(result, expected_value)
|
||||
```
|
||||
|
||||
4. Use mocking to avoid actual network or system calls:
|
||||
```python
|
||||
@patch('ssl_manager.subprocess.run')
|
||||
def test_with_mock(self, mock_run):
|
||||
mock_run.return_value = MagicMock(returncode=0)
|
||||
# Test with mock
|
||||
```
|
||||
|
||||
### Test Example
|
||||
|
||||
Here's a simple test that verifies the SSLManager initializes correctly:
|
||||
|
||||
```python
|
||||
def test_init_creates_cert_dir(self):
|
||||
"""Test that the constructor creates the certificate directory."""
|
||||
test_path = os.path.expanduser("~/test-ssl-certs")
|
||||
|
||||
# Remove the directory if it exists
|
||||
if os.path.exists(test_path):
|
||||
os.rmdir(test_path)
|
||||
|
||||
# Create a new SSLManager with the test path
|
||||
ssl_manager = SSLManager(cert_dir=test_path)
|
||||
|
||||
# Verify the directory was created
|
||||
self.assertTrue(os.path.exists(test_path))
|
||||
self.assertTrue(os.path.isdir(test_path))
|
||||
|
||||
# Clean up
|
||||
os.rmdir(test_path)
|
||||
```
|
||||
|
||||
## Development Guidelines
|
||||
|
||||
### Code Style
|
||||
|
||||
1. **PEP 8**: Follow the [PEP 8](https://www.python.org/dev/peps/pep-0008/) style guide for Python code.
|
||||
2. **Docstrings**: Use docstrings for all modules, classes, and functions. Follow the [Google style](https://google.github.io/styleguide/pyguide.html#38-comments-and-docstrings) for docstrings.
|
||||
3. **Type Hints**: Use type hints for function parameters and return values.
|
||||
|
||||
### Project Structure
|
||||
|
||||
- `src/`: Contains the source code
|
||||
- `ssl_manager.py`: Main module with the SSLManager class
|
||||
- `tests/`: Contains test files
|
||||
- `test_ssl_manager.py`: Tests for the SSLManager class
|
||||
|
||||
### Best Practices
|
||||
|
||||
1. **Error Handling**: Use try-except blocks to handle exceptions gracefully.
|
||||
2. **Logging**: Use the Python logging module for logging instead of print statements.
|
||||
3. **Testing**: Write tests for all new features and bug fixes.
|
||||
4. **Documentation**: Update documentation when making changes to the code.
|
||||
|
||||
## Deployment and Automation
|
||||
|
||||
### Cross-Machine Deployment
|
||||
|
||||
The SSL Management tool can be developed on one machine and deployed to another server for production use. When deploying across machines:
|
||||
|
||||
1. Ensure the target server meets all the prerequisites (Python 3.6+, OpenSSL, etc.)
|
||||
2. Transfer the entire project directory to the target server
|
||||
3. Set up the virtual environment on the target server
|
||||
4. Configure the `config.json` file with the appropriate settings for the target environment
|
||||
5. Test the deployment by running a simple command like `python src/ssl_manager.py check <hostname>`
|
||||
|
||||
### Automated Certificate Updates
|
||||
|
||||
To automate certificate updates using cron:
|
||||
|
||||
1. Create a shell script wrapper for the SSL Manager:
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# File: update_certificates.sh
|
||||
|
||||
# Change to the project directory
|
||||
cd /path/to/ssl-managment
|
||||
|
||||
# Activate the virtual environment
|
||||
source .venv/bin/activate
|
||||
|
||||
# Run the SSL Manager to update certificates
|
||||
python src/ssl_manager.py generate your-unifi-device.example.com --days 90
|
||||
|
||||
# Additional commands to deploy the certificate to the UniFi device can be added here
|
||||
```
|
||||
|
||||
2. Make the script executable:
|
||||
```bash
|
||||
chmod +x update_certificates.sh
|
||||
```
|
||||
|
||||
3. Add a cron job to run the script periodically (e.g., every 60 days):
|
||||
```bash
|
||||
# Edit the crontab
|
||||
crontab -e
|
||||
|
||||
# Add a line like this to run at 2:30 AM on the 1st of every other month
|
||||
30 2 1 */2 * /path/to/update_certificates.sh >> /var/log/certificate-updates.log 2>&1
|
||||
```
|
||||
|
||||
4. Verify the cron job is set up correctly:
|
||||
```bash
|
||||
crontab -l
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
1. **OpenSSL Command Not Found**
|
||||
|
||||
Ensure OpenSSL is installed and in your PATH.
|
||||
|
||||
2. **Permission Denied When Creating Certificates**
|
||||
|
||||
Check that you have write permissions to the certificate directory.
|
||||
|
||||
3. **Import Errors in Tests**
|
||||
|
||||
Make sure you're running tests from the project root directory.
|
||||
|
||||
### Debugging
|
||||
|
||||
- Set the `SSL_DEBUG=1` environment variable for verbose output
|
||||
- Check the OpenSSL version with `openssl version`
|
||||
- Verify certificate paths are correct and accessible
|
||||
68
README_unifi_verification.md
Normal file
68
README_unifi_verification.md
Normal file
@ -0,0 +1,68 @@
|
||||
# UniFi Configuration Verification
|
||||
|
||||
## Summary
|
||||
|
||||
We attempted to verify the UniFi device connection parameters in `config.json` by creating a test script that uses the `unifiControl` Python library to connect to the UniFi device. The test was able to establish a connection to the device, but authentication failed with a 401 Unauthorized error.
|
||||
|
||||
## What We Did
|
||||
|
||||
1. **Installed the unifiControl Python library**:
|
||||
```bash
|
||||
pip install unifiControl
|
||||
```
|
||||
|
||||
2. **Created a test script** (`tests/test_unifi_connection.py`) that:
|
||||
- Loads the UniFi connection parameters from config.json
|
||||
- Attempts to connect to the UniFi device using these parameters
|
||||
- Disables SSL certificate verification to handle self-signed certificates
|
||||
- Provides detailed error information for troubleshooting
|
||||
|
||||
3. **Ran the test** and analyzed the results:
|
||||
- The connection to the UniFi device was established successfully
|
||||
- The SSL certificate verification was successfully disabled
|
||||
- The authentication attempt failed with a 401 Unauthorized error
|
||||
|
||||
## Findings
|
||||
|
||||
The UniFi device at `udm-se.mgeppert.com` is reachable and responding to HTTPS requests on port 443, but the authentication with the provided credentials failed. This suggests that the credentials in the config.json file may be incorrect, or there may be other issues with the authentication process.
|
||||
|
||||
## Recommendations
|
||||
|
||||
1. **Verify the credentials** in config.json:
|
||||
```json
|
||||
{
|
||||
"unifi": {
|
||||
"host": "udm-se.mgeppert.com",
|
||||
"username": "SSLCertificate",
|
||||
"password": "cYu2E1OWt0XseVf9j5ML"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
2. **Check the UniFi device configuration**:
|
||||
- Verify that the "SSLCertificate" account exists and is enabled
|
||||
- Check if the account has the necessary permissions
|
||||
- Look for any authentication failure messages in the device logs
|
||||
|
||||
3. **Try a different authentication method**:
|
||||
- The UniFi device may require a different authentication method or API
|
||||
- Consider using the official UniFi API or a different library
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. Update the credentials in config.json if they are incorrect
|
||||
2. If the credentials are correct, check the UniFi device's configuration and logs
|
||||
3. Consider using a different authentication method or API library
|
||||
|
||||
## Detailed Test Results
|
||||
|
||||
For detailed test results and troubleshooting recommendations, see the [UniFi Connection Test Results](unifi_connection_test_results.md) document.
|
||||
|
||||
## Test Script
|
||||
|
||||
The test script (`tests/test_unifi_connection.py`) can be run again after updating the credentials in config.json:
|
||||
|
||||
```bash
|
||||
cd /path/to/ssl-managment
|
||||
python -m tests.test_unifi_connection
|
||||
```
|
||||
33
config.json
Normal file
33
config.json
Normal file
@ -0,0 +1,33 @@
|
||||
{
|
||||
"cert_dir": "~/.ssl-certs",
|
||||
"default_port": 443,
|
||||
"connection_timeout": 3.0,
|
||||
"default_validity_days": 365,
|
||||
"key_size": 2048,
|
||||
"unifi": {
|
||||
"host": "udm-se.mgeppert.com",
|
||||
"username": "SSLCertificate",
|
||||
"password": "cYu2E1OWt0XseVf9j5ML",
|
||||
"site": "default",
|
||||
"ssh_port": 22,
|
||||
"ssh_username": "root",
|
||||
"ssh_password": "RH6X64FAAiE7CrcV84lQ",
|
||||
"ssh_key_path": "~/.ssh/id_rsa"
|
||||
},
|
||||
"comments": {
|
||||
"cert_dir": "Directory where certificates and keys will be stored",
|
||||
"default_port": "Default port to use when checking certificate expiration",
|
||||
"connection_timeout": "Timeout in seconds for SSL connections",
|
||||
"default_validity_days": "Default validity period in days for generated certificates",
|
||||
"key_size": "Key size in bits for generated certificates",
|
||||
"unifi": "UniFi device connection parameters",
|
||||
"unifi.host": "Hostname or IP address of the UniFi device",
|
||||
"unifi.username": "Username for authenticating with the UniFi device",
|
||||
"unifi.password": "Password for authenticating with the UniFi device",
|
||||
"unifi.site": "Site name for the UniFi device (default: 'default')",
|
||||
"unifi.ssh_port": "SSH port for the UniFi device (default: 22)",
|
||||
"unifi.ssh_username": "Username for SSH authentication with the UniFi device",
|
||||
"unifi.ssh_password": "Password for SSH authentication (leave empty to use SSH key)",
|
||||
"unifi.ssh_key_path": "Path to the SSH private key file for authentication"
|
||||
}
|
||||
}
|
||||
308
src/ssl_manager.py
Normal file
308
src/ssl_manager.py
Normal file
@ -0,0 +1,308 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
SSL Certificate Management Tool
|
||||
|
||||
This script provides utilities for managing SSL certificates, including:
|
||||
- Checking certificate expiration dates
|
||||
- Validating certificate chains
|
||||
- Generating self-signed certificates for testing
|
||||
"""
|
||||
|
||||
import os
|
||||
import ssl
|
||||
import socket
|
||||
import datetime
|
||||
import argparse
|
||||
import subprocess
|
||||
import json
|
||||
from typing import Dict, Tuple, Optional, List, Any
|
||||
|
||||
|
||||
def load_config(config_path: str = "config.json") -> Dict[str, Any]:
|
||||
"""
|
||||
Load configuration from a JSON file.
|
||||
|
||||
Args:
|
||||
config_path: Path to the config file (default: config.json in current directory)
|
||||
|
||||
Returns:
|
||||
Dictionary containing configuration values
|
||||
"""
|
||||
default_config = {
|
||||
"cert_dir": "~/.ssl-certs",
|
||||
"default_port": 443,
|
||||
"connection_timeout": 3.0,
|
||||
"default_validity_days": 365,
|
||||
"key_size": 2048,
|
||||
"unifi": {
|
||||
"host": "",
|
||||
"username": "",
|
||||
"password": "",
|
||||
"site": "default",
|
||||
"ssh_port": 22,
|
||||
"ssh_username": "",
|
||||
"ssh_password": "",
|
||||
"ssh_key_path": "~/.ssh/id_rsa"
|
||||
}
|
||||
}
|
||||
|
||||
try:
|
||||
with open(config_path, 'r') as f:
|
||||
config = json.load(f)
|
||||
# Remove comments section if present
|
||||
if "comments" in config:
|
||||
del config["comments"]
|
||||
# Update default config with values from file
|
||||
for key in default_config:
|
||||
if key in config:
|
||||
if isinstance(default_config[key], dict) and isinstance(config[key], dict):
|
||||
# Handle nested dictionaries (like unifi)
|
||||
for nested_key in default_config[key]:
|
||||
if nested_key in config[key]:
|
||||
default_config[key][nested_key] = config[key][nested_key]
|
||||
else:
|
||||
default_config[key] = config[key]
|
||||
except (FileNotFoundError, json.JSONDecodeError):
|
||||
# Use default config if file not found or invalid
|
||||
pass
|
||||
|
||||
return default_config
|
||||
|
||||
|
||||
class SSLManager:
|
||||
"""Class for managing SSL certificates and operations."""
|
||||
|
||||
def __init__(self, cert_dir: str = None, config_path: str = "config.json"):
|
||||
"""
|
||||
Initialize the SSL Manager.
|
||||
|
||||
Args:
|
||||
cert_dir: Directory to store certificates (default: None)
|
||||
config_path: Path to the config file (default: config.json)
|
||||
"""
|
||||
# Load configuration
|
||||
self.config = load_config(config_path)
|
||||
|
||||
# Use cert_dir from parameters if provided, otherwise from config
|
||||
self.cert_dir = cert_dir or os.path.expanduser(self.config["cert_dir"])
|
||||
os.makedirs(self.cert_dir, exist_ok=True)
|
||||
|
||||
# Store other config values
|
||||
self.default_port = self.config["default_port"]
|
||||
self.connection_timeout = self.config["connection_timeout"]
|
||||
self.default_validity_days = self.config["default_validity_days"]
|
||||
self.key_size = self.config["key_size"]
|
||||
|
||||
# Store UniFi device connection parameters
|
||||
self.unifi_host = self.config["unifi"]["host"]
|
||||
self.unifi_username = self.config["unifi"]["username"]
|
||||
self.unifi_password = self.config["unifi"]["password"]
|
||||
self.unifi_site = self.config["unifi"]["site"]
|
||||
|
||||
# Store UniFi device SSH parameters
|
||||
self.unifi_ssh_port = self.config["unifi"]["ssh_port"]
|
||||
self.unifi_ssh_username = self.config["unifi"]["ssh_username"]
|
||||
self.unifi_ssh_password = self.config["unifi"]["ssh_password"]
|
||||
self.unifi_ssh_key_path = self.config["unifi"]["ssh_key_path"]
|
||||
|
||||
def check_cert_expiration(self, hostname: str, port: int = None) -> Dict:
|
||||
"""
|
||||
Check the expiration date of a certificate for a given hostname.
|
||||
|
||||
Args:
|
||||
hostname: The hostname to check
|
||||
port: The port to connect to (default: from config)
|
||||
|
||||
Returns:
|
||||
Dictionary with certificate information
|
||||
"""
|
||||
# Use provided port or default from config
|
||||
port = port or self.default_port
|
||||
|
||||
context = ssl.create_default_context()
|
||||
conn = context.wrap_socket(
|
||||
socket.socket(socket.AF_INET),
|
||||
server_hostname=hostname
|
||||
)
|
||||
|
||||
# Use timeout from config
|
||||
conn.settimeout(self.connection_timeout)
|
||||
|
||||
try:
|
||||
conn.connect((hostname, port))
|
||||
cert = conn.getpeercert()
|
||||
|
||||
# Parse expiration date
|
||||
expiration_date = datetime.datetime.strptime(
|
||||
cert['notAfter'], '%b %d %H:%M:%S %Y %Z'
|
||||
)
|
||||
|
||||
# Calculate days until expiration
|
||||
days_left = (expiration_date - datetime.datetime.now()).days
|
||||
|
||||
return {
|
||||
'hostname': hostname,
|
||||
'port': port,
|
||||
'issuer': dict(x[0] for x in cert['issuer']),
|
||||
'subject': dict(x[0] for x in cert['subject']),
|
||||
'expiration_date': expiration_date.strftime('%Y-%m-%d'),
|
||||
'days_left': days_left,
|
||||
'status': 'Valid' if days_left > 0 else 'Expired'
|
||||
}
|
||||
except Exception as e:
|
||||
return {
|
||||
'hostname': hostname,
|
||||
'port': port,
|
||||
'error': str(e),
|
||||
'status': 'Error'
|
||||
}
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
def generate_self_signed_cert(
|
||||
self,
|
||||
common_name: str,
|
||||
days_valid: int = None
|
||||
) -> Tuple[str, str]:
|
||||
"""
|
||||
Generate a self-signed certificate for testing.
|
||||
|
||||
Args:
|
||||
common_name: Common Name (CN) for the certificate
|
||||
days_valid: Number of days the certificate will be valid (default: from config)
|
||||
|
||||
Returns:
|
||||
Tuple of (cert_path, key_path)
|
||||
"""
|
||||
# Use provided days_valid or default from config
|
||||
days_valid = days_valid or self.default_validity_days
|
||||
|
||||
cert_path = os.path.join(self.cert_dir, f"{common_name}.crt")
|
||||
key_path = os.path.join(self.cert_dir, f"{common_name}.key")
|
||||
|
||||
# Generate private key using key size from config
|
||||
subprocess.run([
|
||||
'openssl', 'genrsa',
|
||||
'-out', key_path,
|
||||
str(self.key_size)
|
||||
], check=True)
|
||||
|
||||
# Generate certificate
|
||||
subprocess.run([
|
||||
'openssl', 'req',
|
||||
'-new', '-x509',
|
||||
'-key', key_path,
|
||||
'-out', cert_path,
|
||||
'-days', str(days_valid),
|
||||
'-subj', f"/CN={common_name}"
|
||||
], check=True)
|
||||
|
||||
return cert_path, key_path
|
||||
|
||||
def validate_cert_chain(self, cert_path: str, ca_path: Optional[str] = None) -> bool:
|
||||
"""
|
||||
Validate a certificate chain.
|
||||
|
||||
Args:
|
||||
cert_path: Path to the certificate
|
||||
ca_path: Path to the CA certificate (default: None, uses system CAs)
|
||||
|
||||
Returns:
|
||||
True if valid, False otherwise
|
||||
"""
|
||||
cmd = ['openssl', 'verify']
|
||||
|
||||
if ca_path:
|
||||
cmd.extend(['-CAfile', ca_path])
|
||||
|
||||
cmd.append(cert_path)
|
||||
|
||||
result = subprocess.run(cmd, capture_output=True, text=True)
|
||||
return result.returncode == 0 and 'OK' in result.stdout
|
||||
|
||||
def get_unifi_connection_params(self) -> Dict[str, str]:
|
||||
"""
|
||||
Get the UniFi device connection parameters.
|
||||
|
||||
Returns:
|
||||
Dictionary containing the UniFi device connection parameters
|
||||
"""
|
||||
return {
|
||||
"host": self.unifi_host,
|
||||
"username": self.unifi_username,
|
||||
"password": self.unifi_password,
|
||||
"site": self.unifi_site
|
||||
}
|
||||
|
||||
def get_unifi_ssh_params(self) -> Dict[str, str]:
|
||||
"""
|
||||
Get the UniFi device SSH parameters.
|
||||
|
||||
Returns:
|
||||
Dictionary containing the UniFi device SSH parameters
|
||||
"""
|
||||
return {
|
||||
"host": self.unifi_host,
|
||||
"port": self.unifi_ssh_port,
|
||||
"username": self.unifi_ssh_username,
|
||||
"password": self.unifi_ssh_password,
|
||||
"key_path": self.unifi_ssh_key_path
|
||||
}
|
||||
|
||||
|
||||
def main():
|
||||
"""Main function to run the SSL Manager CLI."""
|
||||
parser = argparse.ArgumentParser(description='SSL Certificate Management Tool')
|
||||
|
||||
# Global arguments
|
||||
parser.add_argument('--config', help='Path to config file (default: config.json)', default='config.json')
|
||||
parser.add_argument('--cert-dir', help='Directory to store certificates (overrides config)')
|
||||
|
||||
subparsers = parser.add_subparsers(dest='command', help='Command to run')
|
||||
|
||||
# Check expiration command
|
||||
check_parser = subparsers.add_parser('check', help='Check certificate expiration')
|
||||
check_parser.add_argument('hostname', help='Hostname to check')
|
||||
check_parser.add_argument('--port', type=int, help='Port (overrides config)')
|
||||
|
||||
# Generate self-signed certificate command
|
||||
gen_parser = subparsers.add_parser('generate', help='Generate self-signed certificate')
|
||||
gen_parser.add_argument('common_name', help='Common Name (CN) for the certificate')
|
||||
gen_parser.add_argument('--days', type=int, help='Days valid (overrides config)')
|
||||
|
||||
# Validate certificate command
|
||||
validate_parser = subparsers.add_parser('validate', help='Validate certificate chain')
|
||||
validate_parser.add_argument('cert_path', help='Path to certificate')
|
||||
validate_parser.add_argument('--ca-path', help='Path to CA certificate')
|
||||
|
||||
args = parser.parse_args()
|
||||
ssl_manager = SSLManager(cert_dir=args.cert_dir, config_path=args.config)
|
||||
|
||||
if args.command == 'check':
|
||||
result = ssl_manager.check_cert_expiration(args.hostname, args.port)
|
||||
if result['status'] == 'Error':
|
||||
print(f"Error checking {args.hostname}:{args.port}: {result['error']}")
|
||||
else:
|
||||
print(f"Certificate for {result['hostname']}:")
|
||||
print(f" Status: {result['status']}")
|
||||
print(f" Expires: {result['expiration_date']} ({result['days_left']} days left)")
|
||||
print(f" Issuer: {result['issuer'].get('organizationName', 'N/A')}")
|
||||
|
||||
elif args.command == 'generate':
|
||||
cert_path, key_path = ssl_manager.generate_self_signed_cert(
|
||||
args.common_name, args.days
|
||||
)
|
||||
print(f"Generated self-signed certificate:")
|
||||
print(f" Certificate: {cert_path}")
|
||||
print(f" Private Key: {key_path}")
|
||||
|
||||
elif args.command == 'validate':
|
||||
is_valid = ssl_manager.validate_cert_chain(args.cert_path, args.ca_path)
|
||||
print(f"Certificate validation {'successful' if is_valid else 'failed'}")
|
||||
|
||||
else:
|
||||
parser.print_help()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
7
test_config.json
Normal file
7
test_config.json
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"cert_dir": "~/test-ssl-certs",
|
||||
"default_port": 8443,
|
||||
"connection_timeout": 5.0,
|
||||
"default_validity_days": 730,
|
||||
"key_size": 4096
|
||||
}
|
||||
134
tests/test_config.py
Normal file
134
tests/test_config.py
Normal file
@ -0,0 +1,134 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Tests for the configuration functionality of the SSL Manager.
|
||||
|
||||
This module contains tests for loading and using configuration values.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import tempfile
|
||||
import unittest
|
||||
from unittest.mock import patch, mock_open
|
||||
|
||||
# Add the src directory to the Python path
|
||||
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../src')))
|
||||
|
||||
from ssl_manager import load_config, SSLManager
|
||||
|
||||
|
||||
class TestConfigLoading(unittest.TestCase):
|
||||
"""Test cases for configuration loading functionality."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test fixtures."""
|
||||
# Create a temporary directory for test files
|
||||
self.temp_dir = tempfile.TemporaryDirectory()
|
||||
|
||||
# Sample config for testing
|
||||
self.test_config = {
|
||||
"cert_dir": "~/test-certs",
|
||||
"default_port": 8443,
|
||||
"connection_timeout": 5.0,
|
||||
"default_validity_days": 730,
|
||||
"key_size": 4096
|
||||
}
|
||||
|
||||
def tearDown(self):
|
||||
"""Tear down test fixtures."""
|
||||
# Clean up the temporary directory
|
||||
self.temp_dir.cleanup()
|
||||
|
||||
def test_load_config_with_valid_file(self):
|
||||
"""Test loading a valid configuration file."""
|
||||
# Create a temporary config file
|
||||
config_path = os.path.join(self.temp_dir.name, "test_config.json")
|
||||
with open(config_path, 'w') as f:
|
||||
json.dump(self.test_config, f)
|
||||
|
||||
# Load the config
|
||||
config = load_config(config_path)
|
||||
|
||||
# Verify the config values
|
||||
self.assertEqual(config["cert_dir"], "~/test-certs")
|
||||
self.assertEqual(config["default_port"], 8443)
|
||||
self.assertEqual(config["connection_timeout"], 5.0)
|
||||
self.assertEqual(config["default_validity_days"], 730)
|
||||
self.assertEqual(config["key_size"], 4096)
|
||||
|
||||
def test_load_config_with_missing_file(self):
|
||||
"""Test loading a non-existent configuration file."""
|
||||
# Use a non-existent file path
|
||||
config_path = os.path.join(self.temp_dir.name, "nonexistent.json")
|
||||
|
||||
# Load the config
|
||||
config = load_config(config_path)
|
||||
|
||||
# Verify default values are used
|
||||
self.assertEqual(config["cert_dir"], "~/.ssl-certs")
|
||||
self.assertEqual(config["default_port"], 443)
|
||||
self.assertEqual(config["connection_timeout"], 3.0)
|
||||
self.assertEqual(config["default_validity_days"], 365)
|
||||
self.assertEqual(config["key_size"], 2048)
|
||||
|
||||
def test_load_config_with_partial_file(self):
|
||||
"""Test loading a configuration file with only some values."""
|
||||
# Create a config with only some values
|
||||
partial_config = {
|
||||
"default_port": 8443,
|
||||
"key_size": 4096
|
||||
}
|
||||
|
||||
# Create a temporary config file
|
||||
config_path = os.path.join(self.temp_dir.name, "partial_config.json")
|
||||
with open(config_path, 'w') as f:
|
||||
json.dump(partial_config, f)
|
||||
|
||||
# Load the config
|
||||
config = load_config(config_path)
|
||||
|
||||
# Verify the specified values are used and others are defaults
|
||||
self.assertEqual(config["cert_dir"], "~/.ssl-certs") # Default
|
||||
self.assertEqual(config["default_port"], 8443) # From file
|
||||
self.assertEqual(config["connection_timeout"], 3.0) # Default
|
||||
self.assertEqual(config["default_validity_days"], 365) # Default
|
||||
self.assertEqual(config["key_size"], 4096) # From file
|
||||
|
||||
def test_ssl_manager_uses_config_values(self):
|
||||
"""Test that SSLManager uses values from the config file."""
|
||||
# Create a temporary config file
|
||||
config_path = os.path.join(self.temp_dir.name, "test_config.json")
|
||||
with open(config_path, 'w') as f:
|
||||
json.dump(self.test_config, f)
|
||||
|
||||
# Create an SSLManager with the config
|
||||
ssl_manager = SSLManager(config_path=config_path)
|
||||
|
||||
# Verify the manager uses the config values
|
||||
self.assertEqual(ssl_manager.default_port, 8443)
|
||||
self.assertEqual(ssl_manager.connection_timeout, 5.0)
|
||||
self.assertEqual(ssl_manager.default_validity_days, 730)
|
||||
self.assertEqual(ssl_manager.key_size, 4096)
|
||||
|
||||
def test_ssl_manager_cert_dir_override(self):
|
||||
"""Test that cert_dir parameter overrides the config value."""
|
||||
# Create a temporary config file
|
||||
config_path = os.path.join(self.temp_dir.name, "test_config.json")
|
||||
with open(config_path, 'w') as f:
|
||||
json.dump(self.test_config, f)
|
||||
|
||||
# Create an SSLManager with a custom cert_dir
|
||||
custom_cert_dir = os.path.join(self.temp_dir.name, "custom-certs")
|
||||
ssl_manager = SSLManager(cert_dir=custom_cert_dir, config_path=config_path)
|
||||
|
||||
# Verify the manager uses the custom cert_dir
|
||||
self.assertEqual(ssl_manager.cert_dir, custom_cert_dir)
|
||||
|
||||
# Verify the directory was created
|
||||
self.assertTrue(os.path.exists(custom_cert_dir))
|
||||
self.assertTrue(os.path.isdir(custom_cert_dir))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
126
tests/test_ssl_manager.py
Normal file
126
tests/test_ssl_manager.py
Normal file
@ -0,0 +1,126 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Tests for the SSL Manager module.
|
||||
|
||||
This module contains tests for the SSLManager class and its methods.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from unittest.mock import patch, MagicMock
|
||||
|
||||
# Add the src directory to the Python path
|
||||
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../src')))
|
||||
|
||||
from ssl_manager import SSLManager
|
||||
|
||||
|
||||
class TestSSLManager(unittest.TestCase):
|
||||
"""Test cases for the SSLManager class."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test fixtures."""
|
||||
# Create a temporary directory for certificates
|
||||
self.temp_dir = tempfile.TemporaryDirectory()
|
||||
self.ssl_manager = SSLManager(cert_dir=self.temp_dir.name)
|
||||
|
||||
def tearDown(self):
|
||||
"""Tear down test fixtures."""
|
||||
# Clean up the temporary directory
|
||||
self.temp_dir.cleanup()
|
||||
|
||||
@patch('ssl_manager.socket.socket')
|
||||
@patch('ssl_manager.ssl.create_default_context')
|
||||
def test_check_cert_expiration_valid(self, mock_context, mock_socket):
|
||||
"""Test checking a valid certificate expiration."""
|
||||
# Mock the SSL socket and certificate
|
||||
mock_sock = MagicMock()
|
||||
mock_context.return_value.wrap_socket.return_value = mock_sock
|
||||
|
||||
# Mock the peer certificate
|
||||
mock_sock.getpeercert.return_value = {
|
||||
'notAfter': 'Jul 20 12:00:00 2026 GMT',
|
||||
'issuer': [(('organizationName', 'Test CA'),)],
|
||||
'subject': [(('commonName', 'example.com'),)]
|
||||
}
|
||||
|
||||
# Call the method
|
||||
result = self.ssl_manager.check_cert_expiration('example.com')
|
||||
|
||||
# Verify the result
|
||||
self.assertEqual(result['hostname'], 'example.com')
|
||||
self.assertEqual(result['status'], 'Valid')
|
||||
self.assertIn('days_left', result)
|
||||
self.assertIn('expiration_date', result)
|
||||
|
||||
@patch('ssl_manager.socket.socket')
|
||||
@patch('ssl_manager.ssl.create_default_context')
|
||||
def test_check_cert_expiration_error(self, mock_context, mock_socket):
|
||||
"""Test checking a certificate with an error."""
|
||||
# Mock the SSL socket to raise an exception
|
||||
mock_sock = MagicMock()
|
||||
mock_context.return_value.wrap_socket.return_value = mock_sock
|
||||
mock_sock.connect.side_effect = Exception("Connection refused")
|
||||
|
||||
# Call the method
|
||||
result = self.ssl_manager.check_cert_expiration('nonexistent.example.com')
|
||||
|
||||
# Verify the result
|
||||
self.assertEqual(result['hostname'], 'nonexistent.example.com')
|
||||
self.assertEqual(result['status'], 'Error')
|
||||
self.assertIn('error', result)
|
||||
self.assertEqual(result['error'], 'Connection refused')
|
||||
|
||||
@patch('ssl_manager.subprocess.run')
|
||||
def test_generate_self_signed_cert(self, mock_run):
|
||||
"""Test generating a self-signed certificate."""
|
||||
# Mock the subprocess calls
|
||||
mock_run.return_value = MagicMock(returncode=0)
|
||||
|
||||
# Call the method
|
||||
cert_path, key_path = self.ssl_manager.generate_self_signed_cert('test.example.com')
|
||||
|
||||
# Verify the result
|
||||
self.assertEqual(cert_path, os.path.join(self.temp_dir.name, 'test.example.com.crt'))
|
||||
self.assertEqual(key_path, os.path.join(self.temp_dir.name, 'test.example.com.key'))
|
||||
|
||||
# Verify subprocess was called twice (once for key, once for cert)
|
||||
self.assertEqual(mock_run.call_count, 2)
|
||||
|
||||
@patch('ssl_manager.subprocess.run')
|
||||
def test_validate_cert_chain_valid(self, mock_run):
|
||||
"""Test validating a valid certificate chain."""
|
||||
# Mock the subprocess call
|
||||
mock_run.return_value = MagicMock(
|
||||
returncode=0,
|
||||
stdout="test.crt: OK"
|
||||
)
|
||||
|
||||
# Call the method
|
||||
result = self.ssl_manager.validate_cert_chain('test.crt')
|
||||
|
||||
# Verify the result
|
||||
self.assertTrue(result)
|
||||
mock_run.assert_called_once()
|
||||
|
||||
@patch('ssl_manager.subprocess.run')
|
||||
def test_validate_cert_chain_invalid(self, mock_run):
|
||||
"""Test validating an invalid certificate chain."""
|
||||
# Mock the subprocess call
|
||||
mock_run.return_value = MagicMock(
|
||||
returncode=1,
|
||||
stdout="test.crt: C = US, O = Example, CN = example.com\nerror 2 at 1 depth lookup: unable to get issuer certificate"
|
||||
)
|
||||
|
||||
# Call the method
|
||||
result = self.ssl_manager.validate_cert_chain('test.crt')
|
||||
|
||||
# Verify the result
|
||||
self.assertFalse(result)
|
||||
mock_run.assert_called_once()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
104
tests/test_unifi_config.py
Normal file
104
tests/test_unifi_config.py
Normal file
@ -0,0 +1,104 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Tests for the UniFi device configuration functionality of the SSL Manager.
|
||||
|
||||
This module contains tests for loading and using UniFi device configuration values.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import tempfile
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
# Add the src directory to the Python path
|
||||
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../src')))
|
||||
|
||||
from ssl_manager import load_config, SSLManager
|
||||
|
||||
|
||||
class TestUniFiConfig(unittest.TestCase):
|
||||
"""Test cases for UniFi device configuration functionality."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test fixtures."""
|
||||
# Create a temporary directory for test files
|
||||
self.temp_dir = tempfile.TemporaryDirectory()
|
||||
|
||||
# Sample config for testing
|
||||
self.test_config = {
|
||||
"cert_dir": "~/test-certs",
|
||||
"default_port": 8443,
|
||||
"connection_timeout": 5.0,
|
||||
"default_validity_days": 730,
|
||||
"key_size": 4096,
|
||||
"unifi": {
|
||||
"host": "test.unifi.local",
|
||||
"username": "testuser",
|
||||
"password": "testpass",
|
||||
"site": "testsite"
|
||||
}
|
||||
}
|
||||
|
||||
def tearDown(self):
|
||||
"""Tear down test fixtures."""
|
||||
# Clean up the temporary directory
|
||||
self.temp_dir.cleanup()
|
||||
|
||||
def test_load_config_with_unifi_params(self):
|
||||
"""Test loading a configuration file with UniFi device parameters."""
|
||||
# Create a temporary config file
|
||||
config_path = os.path.join(self.temp_dir.name, "test_config.json")
|
||||
with open(config_path, 'w') as f:
|
||||
json.dump(self.test_config, f)
|
||||
|
||||
# Load the config
|
||||
config = load_config(config_path)
|
||||
|
||||
# Verify the UniFi device parameters
|
||||
self.assertIn("unifi", config)
|
||||
self.assertEqual(config["unifi"]["host"], "test.unifi.local")
|
||||
self.assertEqual(config["unifi"]["username"], "testuser")
|
||||
self.assertEqual(config["unifi"]["password"], "testpass")
|
||||
self.assertEqual(config["unifi"]["site"], "testsite")
|
||||
|
||||
def test_ssl_manager_stores_unifi_params(self):
|
||||
"""Test that SSLManager stores UniFi device parameters."""
|
||||
# Create a temporary config file
|
||||
config_path = os.path.join(self.temp_dir.name, "test_config.json")
|
||||
with open(config_path, 'w') as f:
|
||||
json.dump(self.test_config, f)
|
||||
|
||||
# Create an SSLManager with the config
|
||||
ssl_manager = SSLManager(config_path=config_path)
|
||||
|
||||
# Verify the manager stores the UniFi device parameters
|
||||
self.assertEqual(ssl_manager.unifi_host, "test.unifi.local")
|
||||
self.assertEqual(ssl_manager.unifi_username, "testuser")
|
||||
self.assertEqual(ssl_manager.unifi_password, "testpass")
|
||||
self.assertEqual(ssl_manager.unifi_site, "testsite")
|
||||
|
||||
def test_get_unifi_connection_params(self):
|
||||
"""Test the get_unifi_connection_params method."""
|
||||
# Create a temporary config file
|
||||
config_path = os.path.join(self.temp_dir.name, "test_config.json")
|
||||
with open(config_path, 'w') as f:
|
||||
json.dump(self.test_config, f)
|
||||
|
||||
# Create an SSLManager with the config
|
||||
ssl_manager = SSLManager(config_path=config_path)
|
||||
|
||||
# Get the UniFi device connection parameters
|
||||
params = ssl_manager.get_unifi_connection_params()
|
||||
|
||||
# Verify the parameters
|
||||
self.assertIsInstance(params, dict)
|
||||
self.assertEqual(params["host"], "test.unifi.local")
|
||||
self.assertEqual(params["username"], "testuser")
|
||||
self.assertEqual(params["password"], "testpass")
|
||||
self.assertEqual(params["site"], "testsite")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
119
tests/test_unifi_connection.py
Normal file
119
tests/test_unifi_connection.py
Normal file
@ -0,0 +1,119 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Test for UniFi device connection using unifiControl library.
|
||||
|
||||
This module tests the connection to a UniFi device using the credentials
|
||||
from the config.json file and the unifiControl library.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
# Add the src directory to the Python path
|
||||
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../src')))
|
||||
|
||||
# Import the SSLManager class to access the config loading functionality
|
||||
from src.ssl_manager import SSLManager
|
||||
|
||||
# Import the unifiControl library
|
||||
try:
|
||||
from unificontrol import UnifiClient
|
||||
except ImportError:
|
||||
print("ERROR: unifiControl library is not installed. Please install it using 'pip install unifiControl'")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
class TestUniFiConnection(unittest.TestCase):
|
||||
"""Test case for UniFi device connection."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test fixtures."""
|
||||
# Create an SSLManager instance to load the config
|
||||
self.ssl_manager = SSLManager()
|
||||
|
||||
# Get the UniFi connection parameters
|
||||
self.unifi_params = self.ssl_manager.get_unifi_connection_params()
|
||||
|
||||
# Print the connection parameters (without the password)
|
||||
print(f"Testing connection to UniFi device at: {self.unifi_params['host']}")
|
||||
print(f"Using username: {self.unifi_params['username']}")
|
||||
|
||||
def test_unifi_connection(self):
|
||||
"""Test the connection to the UniFi device."""
|
||||
# Skip the test if any of the required parameters are missing
|
||||
if not self.unifi_params['host'] or not self.unifi_params['username'] or not self.unifi_params['password']:
|
||||
self.skipTest("UniFi connection parameters are missing in the config file")
|
||||
|
||||
try:
|
||||
print("\nAttempting to connect to UniFi device...")
|
||||
print(f"Host: {self.unifi_params['host']}")
|
||||
print(f"Username: {self.unifi_params['username']}")
|
||||
print(f"Password: {'*' * len(self.unifi_params['password'])}")
|
||||
|
||||
# Create a UnifiClient instance
|
||||
client = UnifiClient(
|
||||
host=self.unifi_params['host'],
|
||||
username=self.unifi_params['username'],
|
||||
password=self.unifi_params['password'],
|
||||
port=443, # Default port for UniFi controller
|
||||
site=self.unifi_params['site'], # Site name from config
|
||||
cert=None # Skip SSL certificate verification
|
||||
)
|
||||
|
||||
# Disable SSL verification in the requests session
|
||||
client._session.verify = False
|
||||
|
||||
# Suppress InsecureRequestWarning
|
||||
import urllib3
|
||||
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
||||
|
||||
# Enable debug logging for requests
|
||||
import logging
|
||||
from http.client import HTTPConnection
|
||||
HTTPConnection.debuglevel = 1
|
||||
logging.basicConfig()
|
||||
logging.getLogger().setLevel(logging.DEBUG)
|
||||
requests_log = logging.getLogger("requests.packages.urllib3")
|
||||
requests_log.setLevel(logging.DEBUG)
|
||||
requests_log.propagate = True
|
||||
|
||||
print("\nAttempting to login...")
|
||||
# Attempt to login
|
||||
client.login()
|
||||
print("Login successful!")
|
||||
|
||||
# If login is successful, get the system info to further verify the connection
|
||||
print("Retrieving system info...")
|
||||
system_info = client.get_system_info()
|
||||
|
||||
# Print some information about the connected system
|
||||
print("\nConnection successful!")
|
||||
print(f"System name: {system_info.get('name', 'N/A')}")
|
||||
print(f"Version: {system_info.get('version', 'N/A')}")
|
||||
|
||||
# Logout to clean up the session
|
||||
print("Logging out...")
|
||||
client.logout()
|
||||
print("Logout successful!")
|
||||
|
||||
# Assert that we got a valid system_info response
|
||||
self.assertIsNotNone(system_info)
|
||||
self.assertIn('name', system_info)
|
||||
|
||||
except Exception as e:
|
||||
# If an exception occurs, fail the test with the error message
|
||||
print(f"\nError: {str(e)}")
|
||||
print("\nAuthentication failed. Possible reasons:")
|
||||
print("1. The credentials in config.json are incorrect")
|
||||
print("2. The UniFi device requires additional authentication parameters")
|
||||
print("3. The UniFi device is not accessible from the current network")
|
||||
print("4. The UniFi device is not running or is not responding")
|
||||
|
||||
self.fail(f"Connection to UniFi device failed: {str(e)}")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
109
tests/test_unifi_ssh_config.py
Normal file
109
tests/test_unifi_ssh_config.py
Normal file
@ -0,0 +1,109 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Tests for the UniFi device SSH configuration functionality of the SSL Manager.
|
||||
|
||||
This module contains tests for loading and using UniFi device SSH configuration values.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import tempfile
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
# Add the src directory to the Python path
|
||||
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../src')))
|
||||
|
||||
from ssl_manager import load_config, SSLManager
|
||||
|
||||
|
||||
class TestUniFiSSHConfig(unittest.TestCase):
|
||||
"""Test cases for UniFi device SSH configuration functionality."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test fixtures."""
|
||||
# Create a temporary directory for test files
|
||||
self.temp_dir = tempfile.TemporaryDirectory()
|
||||
|
||||
# Sample config for testing
|
||||
self.test_config = {
|
||||
"cert_dir": "~/test-certs",
|
||||
"default_port": 8443,
|
||||
"connection_timeout": 5.0,
|
||||
"default_validity_days": 730,
|
||||
"key_size": 4096,
|
||||
"unifi": {
|
||||
"host": "test.unifi.local",
|
||||
"username": "testuser",
|
||||
"password": "testpass",
|
||||
"site": "testsite",
|
||||
"ssh_port": 2222,
|
||||
"ssh_username": "sshuser",
|
||||
"ssh_password": "sshpass",
|
||||
"ssh_key_path": "~/test-ssh-key"
|
||||
}
|
||||
}
|
||||
|
||||
def tearDown(self):
|
||||
"""Tear down test fixtures."""
|
||||
# Clean up the temporary directory
|
||||
self.temp_dir.cleanup()
|
||||
|
||||
def test_load_config_with_ssh_params(self):
|
||||
"""Test loading a configuration file with UniFi device SSH parameters."""
|
||||
# Create a temporary config file
|
||||
config_path = os.path.join(self.temp_dir.name, "test_config.json")
|
||||
with open(config_path, 'w') as f:
|
||||
json.dump(self.test_config, f)
|
||||
|
||||
# Load the config
|
||||
config = load_config(config_path)
|
||||
|
||||
# Verify the UniFi device SSH parameters
|
||||
self.assertIn("unifi", config)
|
||||
self.assertEqual(config["unifi"]["ssh_port"], 2222)
|
||||
self.assertEqual(config["unifi"]["ssh_username"], "sshuser")
|
||||
self.assertEqual(config["unifi"]["ssh_password"], "sshpass")
|
||||
self.assertEqual(config["unifi"]["ssh_key_path"], "~/test-ssh-key")
|
||||
|
||||
def test_ssl_manager_stores_ssh_params(self):
|
||||
"""Test that SSLManager stores UniFi device SSH parameters."""
|
||||
# Create a temporary config file
|
||||
config_path = os.path.join(self.temp_dir.name, "test_config.json")
|
||||
with open(config_path, 'w') as f:
|
||||
json.dump(self.test_config, f)
|
||||
|
||||
# Create an SSLManager with the config
|
||||
ssl_manager = SSLManager(config_path=config_path)
|
||||
|
||||
# Verify the manager stores the UniFi device SSH parameters
|
||||
self.assertEqual(ssl_manager.unifi_ssh_port, 2222)
|
||||
self.assertEqual(ssl_manager.unifi_ssh_username, "sshuser")
|
||||
self.assertEqual(ssl_manager.unifi_ssh_password, "sshpass")
|
||||
self.assertEqual(ssl_manager.unifi_ssh_key_path, "~/test-ssh-key")
|
||||
|
||||
def test_get_unifi_ssh_params(self):
|
||||
"""Test the get_unifi_ssh_params method."""
|
||||
# Create a temporary config file
|
||||
config_path = os.path.join(self.temp_dir.name, "test_config.json")
|
||||
with open(config_path, 'w') as f:
|
||||
json.dump(self.test_config, f)
|
||||
|
||||
# Create an SSLManager with the config
|
||||
ssl_manager = SSLManager(config_path=config_path)
|
||||
|
||||
# Get the UniFi device SSH parameters
|
||||
params = ssl_manager.get_unifi_ssh_params()
|
||||
|
||||
# Verify the parameters
|
||||
self.assertIsInstance(params, dict)
|
||||
self.assertEqual(params["host"], "test.unifi.local")
|
||||
self.assertEqual(params["port"], 2222)
|
||||
self.assertEqual(params["username"], "sshuser")
|
||||
self.assertEqual(params["password"], "sshpass")
|
||||
self.assertEqual(params["key_path"], "~/test-ssh-key")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
81
unifi_connection_test_results.md
Normal file
81
unifi_connection_test_results.md
Normal file
@ -0,0 +1,81 @@
|
||||
# UniFi Connection Test Results
|
||||
|
||||
## Summary
|
||||
|
||||
We attempted to verify the UniFi device connection parameters in `config.json` by creating a test script that uses the `unifiControl` Python library to connect to the UniFi device. The test was able to establish a connection to the device, but authentication failed with a 401 Unauthorized error.
|
||||
|
||||
## Test Details
|
||||
|
||||
### Connection Parameters Used
|
||||
- **Host**: udm-se.mgeppert.com
|
||||
- **Port**: 443
|
||||
- **Username**: SSLCertificate
|
||||
- **Password**: cYu2E1OWt0XseVf9j5ML
|
||||
|
||||
### Test Process
|
||||
1. Installed the unifiControl Python library
|
||||
2. Created a test script that loads the UniFi connection parameters from config.json
|
||||
3. Configured the test to disable SSL certificate verification
|
||||
4. Attempted to connect to the UniFi device and authenticate
|
||||
5. Received a 401 Unauthorized response from the server
|
||||
|
||||
### HTTP Request/Response Details
|
||||
```
|
||||
POST /api/login HTTP/1.1
|
||||
Host: udm-se.mgeppert.com
|
||||
User-Agent: python-requests/2.32.4
|
||||
Accept-Encoding: gzip, deflate
|
||||
Accept: */*
|
||||
Connection: keep-alive
|
||||
Content-Length: 66
|
||||
Content-Type: application/json
|
||||
|
||||
{"username": "SSLCertificate", "password": "cYu2E1OWt0XseVf9j5ML"}
|
||||
```
|
||||
|
||||
```
|
||||
HTTP/1.1 401 Unauthorized
|
||||
Server: nginx
|
||||
Date: Mon, 21 Jul 2025 01:28:25 GMT
|
||||
Content-Type: application/json
|
||||
Content-Length: 47
|
||||
Connection: keep-alive
|
||||
Referrer-Policy: no-referrer
|
||||
Strict-Transport-Security: max-age=15552000; includeSubDomains
|
||||
X-Content-Type-Options: nosniff
|
||||
X-DNS-Prefetch-Control: off
|
||||
X-Frame-Options: SAMEORIGIN
|
||||
X-XSS-Protection: 1; mode=block
|
||||
X-Robots-Tag: noindex
|
||||
Set-Cookie: TOKEN=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT; samesite=none; secure; httponly; partitioned
|
||||
Set-Cookie: TOKEN=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT; samesite=none; secure; httponly
|
||||
```
|
||||
|
||||
## Findings
|
||||
|
||||
1. The UniFi device at udm-se.mgeppert.com is reachable and responding to HTTPS requests on port 443
|
||||
2. The SSL certificate verification was successfully disabled in our test
|
||||
3. The authentication attempt with the provided credentials failed with a 401 Unauthorized error
|
||||
4. The server is running nginx and appears to be a UniFi device based on the API endpoint (/api/login)
|
||||
|
||||
## Possible Issues
|
||||
|
||||
1. **Incorrect Credentials**: The username or password in config.json may be incorrect
|
||||
2. **Account Locked or Disabled**: The account "SSLCertificate" may be locked or disabled on the UniFi device
|
||||
3. **Different Authentication Method**: The UniFi device may require a different authentication method or endpoint
|
||||
4. **API Version Mismatch**: The unifiControl library may be using an API version that's incompatible with the device
|
||||
|
||||
## Recommendations
|
||||
|
||||
1. **Verify Credentials**: Double-check the username and password in config.json
|
||||
2. **Check Account Status**: Log in to the UniFi device's web interface and verify that the "SSLCertificate" account exists and is enabled
|
||||
3. **Try Different Authentication Method**: The UniFi device may require a different authentication method or endpoint. Check the device's documentation for the correct API endpoints
|
||||
4. **Check API Version Compatibility**: Verify that the unifiControl library is compatible with the UniFi device's firmware version
|
||||
5. **Try Manual Authentication**: Use a tool like curl or Postman to manually attempt authentication with the UniFi device to verify the correct API endpoints and authentication method
|
||||
6. **Check Logs**: Check the UniFi device's logs for any authentication failure messages that might provide more information
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. Update the credentials in config.json if they are incorrect
|
||||
2. If the credentials are correct, check the UniFi device's configuration and logs for more information
|
||||
3. Consider using a different authentication method or API library if the unifiControl library is incompatible with the device
|
||||
Loading…
Reference in New Issue
Block a user