added UI php version for enrollment

This commit is contained in:
Edmund Tan 2025-07-22 00:28:48 +08:00
parent 4ce79cdf33
commit 5e7f322877
12 changed files with 3304 additions and 1 deletions

View File

@ -26,6 +26,7 @@ NC='\033[0m' # No Color
# Default API endpoint (can be overridden) # Default API endpoint (can be overridden)
DEFAULT_API_ENDPOINT="https://backend.zitinexus.com" DEFAULT_API_ENDPOINT="https://backend.zitinexus.com"
# Initialize variables to prevent unbound variable errors # Initialize variables to prevent unbound variable errors
CALLBACK_URL="" CALLBACK_URL=""
JWT="" JWT=""
@ -37,7 +38,9 @@ CONTROLLER_ENDPOINT=""
ROLE_ATTRIBUTES="" ROLE_ATTRIBUTES=""
HASH_KEY="" HASH_KEY=""
API_ENDPOINT="" API_ENDPOINT=""
# Example modification (Automated installation , uncomment below)
#API_ENDPOINT="https://backend.zitinexus.com"
#HASH_KEY="your-hash-key-here"
# Logging function # Logging function
log() { log() {
local level=$1 local level=$1

178
UI/INSTALLATION_GUIDE.md Normal file
View File

@ -0,0 +1,178 @@
# ZitiNexus Router Enrollment UI - Installation Guide
## Quick Start
### Prerequisites
- Ubuntu 22.04 or 24.04 LTS
- Root/sudo access
- Internet connectivity
### Automated Installation
1. **Download and extract the UI files to your server**
2. **Run the installation script:**
```bash
cd UI
sudo chmod +x install.sh
sudo ./install.sh
```
3. **Follow the prompts to select your web server (Apache or Nginx)**
4. **Access the interface:**
- URL: `http://ziti-enrollment.local`
- Username: `admin`
- Password: `admin123`
### Manual Installation
If you prefer manual installation, follow the detailed steps in [README.md](README.md).
## Post-Installation Steps
### 1. Change Default Password (IMPORTANT)
Edit `/var/www/ziti-enrollment/includes/config.php`:
```php
// Change this line:
define('ADMIN_PASSWORD_HASH', password_hash('your-new-secure-password', PASSWORD_DEFAULT));
```
### 2. Configure for Production
#### Enable HTTPS
```bash
# Install SSL certificate (example with Let's Encrypt)
sudo apt install certbot python3-certbot-apache # or python3-certbot-nginx
sudo certbot --apache -d your-domain.com # or --nginx
```
#### Secure File Permissions
```bash
sudo chmod 600 /var/www/ziti-enrollment/includes/config.php
sudo chown root:www-data /var/www/ziti-enrollment/includes/config.php
```
#### Configure Firewall
```bash
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable
```
### 3. Test the Installation
1. **Access the web interface**
2. **Login with your credentials**
3. **Check system status on the dashboard**
4. **Test enrollment with a valid hash key**
## Troubleshooting
### Common Issues
#### 1. Permission Denied Errors
```bash
# Fix ownership
sudo chown -R www-data:www-data /var/www/ziti-enrollment
# Fix permissions
sudo chmod -R 755 /var/www/ziti-enrollment
sudo chmod -R 777 /var/www/ziti-enrollment/logs /var/www/ziti-enrollment/temp
```
#### 2. PHP Functions Disabled
```bash
# Check disabled functions
php -r "echo ini_get('disable_functions');"
# Edit PHP configuration
sudo nano /etc/php/8.1/apache2/php.ini # or /etc/php/8.1/fpm/php.ini
# Remove exec, shell_exec, proc_open from disable_functions line
# Restart web server
sudo systemctl restart apache2 # or nginx and php8.1-fpm
```
#### 3. Sudo Access Issues
```bash
# Test sudo access
sudo -u www-data sudo -l
# If issues, recreate sudoers file
sudo tee /etc/sudoers.d/ziti-enrollment << 'EOF'
www-data ALL=(ALL) NOPASSWD: /usr/bin/apt-get, /usr/bin/systemctl, /usr/bin/mkdir, /usr/bin/chmod, /usr/bin/chown, /usr/bin/curl, /usr/bin/gpg, /usr/bin/ziti, /usr/bin/which, /usr/bin/hostname, /usr/bin/uname, /usr/bin/lsb_release
EOF
# Validate
sudo visudo -c
```
#### 4. Web Server Not Starting
```bash
# Check status
sudo systemctl status apache2 # or nginx
# Check logs
sudo journalctl -u apache2 -f # or nginx
# Check configuration
sudo apache2ctl configtest # or nginx -t
```
### Log Files
- **UI Logs**: `/var/www/ziti-enrollment/logs/ui-enrollment.log`
- **System Logs**: `/var/log/ziti-router-enrollment.log`
- **Web Server Logs**:
- Apache: `/var/log/apache2/ziti-enrollment_error.log`
- Nginx: `/var/log/nginx/error.log`
- **PHP Logs**: `/var/log/php_errors.log`
## Security Checklist
- [ ] Changed default password
- [ ] Configured HTTPS
- [ ] Set proper file permissions
- [ ] Configured firewall
- [ ] Restricted network access (if needed)
- [ ] Regular security updates scheduled
- [ ] Log monitoring configured
## Support
For technical support:
1. Check the logs for error messages
2. Verify system requirements are met
3. Test individual components (web server, PHP, sudo access)
4. Review the troubleshooting section
5. Consult the main [README.md](README.md) for detailed information
## Uninstallation
To remove the UI:
```bash
# Stop and disable web server
sudo systemctl stop apache2 # or nginx php8.1-fpm
sudo systemctl disable apache2 # or nginx php8.1-fpm
# Remove files
sudo rm -rf /var/www/ziti-enrollment
sudo rm -f /etc/apache2/sites-available/ziti-enrollment.conf # or /etc/nginx/sites-available/ziti-enrollment
sudo rm -f /etc/apache2/sites-enabled/ziti-enrollment.conf # or /etc/nginx/sites-enabled/ziti-enrollment
sudo rm -f /etc/sudoers.d/ziti-enrollment
# Remove from hosts file
sudo sed -i '/ziti-enrollment.local/d' /etc/hosts
# Optionally remove packages
sudo apt remove apache2 php8.1 libapache2-mod-php8.1 # or nginx php8.1-fpm
sudo apt autoremove
```
---
**Note**: This UI complements the original bash script and provides the same functionality through a modern web interface. Both tools can coexist on the same system.

371
UI/README.md Normal file
View File

@ -0,0 +1,371 @@
# ZitiNexus Router Enrollment UI
A modern PHP-based web interface for enrolling OpenZiti routers using the ZitiNexus Portal. This UI provides a user-friendly alternative to the command-line enrollment script.
## Features
- **Modern Web Interface**: Clean, responsive design with real-time progress tracking
- **Authentication System**: Secure login with session management
- **System Status Monitoring**: Real-time display of Ziti CLI, service, and configuration status
- **Interactive Enrollment**: Step-by-step enrollment process with live progress updates
- **Input Validation**: Client-side and server-side validation for hash keys and API endpoints
- **Comprehensive Logging**: Detailed logs with different severity levels
- **Mobile Responsive**: Works on desktop, tablet, and mobile devices
## System Requirements
- **Operating System**: Ubuntu 22.04 or 24.04 LTS
- **Web Server**: Apache 2.4+ or Nginx 1.18+
- **PHP**: 8.0 or higher with extensions:
- curl
- json
- posix
- proc_open/exec functions enabled
- **Root Access**: Required for system operations (router installation, service management)
- **Internet Connectivity**: For downloading packages and API communication
## Installation
### 1. Install Web Server and PHP
#### For Apache:
```bash
sudo apt update
sudo apt install apache2 php8.1 php8.1-curl php8.1-json libapache2-mod-php8.1
sudo systemctl enable apache2
sudo systemctl start apache2
```
#### For Nginx:
```bash
sudo apt update
sudo apt install nginx php8.1-fpm php8.1-curl php8.1-json
sudo systemctl enable nginx php8.1-fpm
sudo systemctl start nginx php8.1-fpm
```
### 2. Deploy the UI
```bash
# Create web directory
sudo mkdir -p /var/www/ziti-enrollment
# Copy UI files
sudo cp -r UI/* /var/www/ziti-enrollment/
# Set proper permissions
sudo chown -R www-data:www-data /var/www/ziti-enrollment
sudo chmod -R 755 /var/www/ziti-enrollment
sudo chmod -R 777 /var/www/ziti-enrollment/logs
sudo chmod -R 777 /var/www/ziti-enrollment/temp
```
### 3. Configure Web Server
#### Apache Virtual Host:
```bash
sudo tee /etc/apache2/sites-available/ziti-enrollment.conf << 'EOF'
<VirtualHost *:80>
ServerName ziti-enrollment.local
DocumentRoot /var/www/ziti-enrollment/public
<Directory /var/www/ziti-enrollment/public>
AllowOverride All
Require all granted
DirectoryIndex index.php
</Directory>
ErrorLog ${APACHE_LOG_DIR}/ziti-enrollment_error.log
CustomLog ${APACHE_LOG_DIR}/ziti-enrollment_access.log combined
</VirtualHost>
EOF
# Enable site and rewrite module
sudo a2ensite ziti-enrollment.conf
sudo a2enmod rewrite
sudo systemctl reload apache2
```
#### Nginx Configuration:
```bash
sudo tee /etc/nginx/sites-available/ziti-enrollment << 'EOF'
server {
listen 80;
server_name ziti-enrollment.local;
root /var/www/ziti-enrollment/public;
index index.php;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
fastcgi_pass unix:/var/run/php/php8.1-fpm.sock;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
include fastcgi_params;
}
location ~ /\. {
deny all;
}
}
EOF
# Enable site
sudo ln -s /etc/nginx/sites-available/ziti-enrollment /etc/nginx/sites-enabled/
sudo systemctl reload nginx
```
### 4. Configure PHP for System Commands
Edit PHP configuration to allow system command execution:
```bash
# Find PHP configuration file
php --ini
# Edit php.ini (example path)
sudo nano /etc/php/8.1/apache2/php.ini
# Ensure these functions are NOT in disable_functions:
# exec, shell_exec, system, proc_open, proc_close, proc_get_status
# Restart web server
sudo systemctl restart apache2 # or nginx
```
### 5. Set Up Sudo Access for Web Server
The web server needs to run system commands as root. Create a sudoers file:
```bash
sudo tee /etc/sudoers.d/ziti-enrollment << 'EOF'
# Allow www-data to run system commands for Ziti enrollment
www-data ALL=(ALL) NOPASSWD: /usr/bin/apt-get
www-data ALL=(ALL) NOPASSWD: /usr/bin/systemctl
www-data ALL=(ALL) NOPASSWD: /usr/bin/mkdir
www-data ALL=(ALL) NOPASSWD: /usr/bin/chmod
www-data ALL=(ALL) NOPASSWD: /usr/bin/chown
www-data ALL=(ALL) NOPASSWD: /usr/bin/curl
www-data ALL=(ALL) NOPASSWD: /usr/bin/gpg
www-data ALL=(ALL) NOPASSWD: /usr/bin/ziti
www-data ALL=(ALL) NOPASSWD: /usr/bin/which
www-data ALL=(ALL) NOPASSWD: /usr/bin/hostname
www-data ALL=(ALL) NOPASSWD: /usr/bin/uname
www-data ALL=(ALL) NOPASSWD: /usr/bin/lsb_release
EOF
# Validate sudoers file
sudo visudo -c
```
### 6. Update Hosts File (Optional)
For local testing, add the domain to your hosts file:
```bash
echo "127.0.0.1 ziti-enrollment.local" | sudo tee -a /etc/hosts
```
## Configuration
### Default Credentials
- **Username**: `admin`
- **Password**: `admin123`
**⚠️ Important**: Change the default password in production by modifying `UI/includes/config.php`:
```php
define('ADMIN_PASSWORD_HASH', password_hash('your-new-password', PASSWORD_DEFAULT));
```
### API Endpoint
The default API endpoint is set to `https://backend.zitinexus.com`. You can modify this in `UI/includes/config.php`:
```php
define('DEFAULT_API_ENDPOINT', 'https://your-api-endpoint.com');
```
### File Paths
All file paths match the original bash script:
- Configuration: `/etc/zitirouter/`
- Certificates: `/etc/zitirouter/certs/`
- Logs: `/var/log/ziti-router-enrollment.log`
- Service: `/etc/systemd/system/ziti-router.service`
## Usage
1. **Access the Interface**
- Open your web browser
- Navigate to `http://ziti-enrollment.local` (or your configured domain)
- Login with the default credentials
2. **Check System Status**
- The dashboard shows current system status
- Ziti CLI installation status
- Router service status
- Configuration status
- System hostname
3. **Enroll a Router**
- Obtain a hash key from ZitiNexus Portal
- Enter the API endpoint (pre-filled with default)
- Paste the 32-character hash key
- Click "Start Enrollment"
- Monitor progress and logs in real-time
4. **Monitor Progress**
- Progress bar shows overall completion
- Step indicators show current phase
- Live log output shows detailed progress
- System status updates automatically after enrollment
## Security Considerations
### Production Deployment
1. **Change Default Password**
```php
define('ADMIN_PASSWORD_HASH', password_hash('strong-password-here', PASSWORD_DEFAULT));
```
2. **Use HTTPS**
- Configure SSL/TLS certificates
- Redirect HTTP to HTTPS
- Update virtual host configuration
3. **Restrict Access**
- Use firewall rules to limit access
- Consider VPN or IP whitelisting
- Implement additional authentication if needed
4. **File Permissions**
```bash
sudo chmod 600 /var/www/ziti-enrollment/includes/config.php
sudo chown root:www-data /var/www/ziti-enrollment/includes/config.php
```
5. **Regular Updates**
- Keep PHP and web server updated
- Monitor security advisories
- Review logs regularly
## Troubleshooting
### Common Issues
1. **Permission Denied Errors**
```bash
# Check web server user
ps aux | grep apache2 # or nginx
# Ensure proper ownership
sudo chown -R www-data:www-data /var/www/ziti-enrollment
# Check sudoers configuration
sudo -u www-data sudo -l
```
2. **PHP Function Disabled**
```bash
# Check disabled functions
php -r "echo ini_get('disable_functions');"
# Edit php.ini to remove exec, shell_exec, proc_open from disable_functions
sudo nano /etc/php/8.1/apache2/php.ini
sudo systemctl restart apache2
```
3. **System Command Failures**
```bash
# Test sudo access
sudo -u www-data sudo systemctl status ziti-router
# Check system logs
sudo tail -f /var/log/syslog
sudo tail -f /var/log/ziti-router-enrollment.log
```
4. **Web Server Issues**
```bash
# Check web server status
sudo systemctl status apache2 # or nginx
# Check error logs
sudo tail -f /var/log/apache2/error.log
sudo tail -f /var/log/nginx/error.log
```
### Debug Mode
Enable debug logging by modifying `UI/includes/config.php`:
```php
// Add at the top of config.php
error_reporting(E_ALL);
ini_set('display_errors', 1);
ini_set('log_errors', 1);
ini_set('error_log', '/var/log/php_errors.log');
```
### Log Files
- **UI Logs**: `/var/www/ziti-enrollment/logs/ui-enrollment.log`
- **System Logs**: `/var/log/ziti-router-enrollment.log`
- **Web Server Logs**: `/var/log/apache2/` or `/var/log/nginx/`
- **PHP Logs**: `/var/log/php_errors.log`
## API Compatibility
This UI is fully compatible with the ZitiNexus Portal API and replicates all functionality of the original bash script:
- Router registration with hash key validation
- JWT token and configuration download
- OpenZiti CLI installation and setup
- Router enrollment and certificate generation
- Systemd service creation and management
- Status reporting back to portal
## Development
### File Structure
```
UI/
├── public/
│ ├── index.php # Login page
│ └── dashboard.php # Main dashboard
├── includes/
│ ├── config.php # Configuration and utilities
│ ├── auth.php # Authentication handler
│ ├── api_client.php # API communication
│ └── enrollment.php # Core enrollment logic
├── assets/
│ ├── css/style.css # Styling
│ └── js/app.js # Frontend JavaScript
├── logs/ # UI-specific logs
├── temp/ # Temporary files
└── README.md # This file
```
### Contributing
1. Follow PSR-12 coding standards for PHP
2. Use modern JavaScript (ES6+)
3. Maintain responsive design principles
4. Add comprehensive error handling
5. Update documentation for any changes
## Support
For issues related to:
- **UI functionality**: Check logs and configuration
- **ZitiNexus Portal**: Contact your portal administrator
- **OpenZiti**: Visit [OpenZiti Documentation](https://docs.openziti.io/)
## License
This project follows the same license as the original ZitiNexus Router Script.

490
UI/assets/css/style.css Normal file
View File

@ -0,0 +1,490 @@
/* ZitiNexus Router Enrollment UI Styles */
:root {
--primary-color: #2563eb;
--primary-dark: #1d4ed8;
--secondary-color: #64748b;
--success-color: #10b981;
--warning-color: #f59e0b;
--error-color: #ef4444;
--background-color: #f8fafc;
--card-background: #ffffff;
--text-primary: #1e293b;
--text-secondary: #64748b;
--border-color: #e2e8f0;
--shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
background-color: var(--background-color);
color: var(--text-primary);
line-height: 1.6;
}
/* Login Page Styles */
.login-container {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 1rem;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.login-card {
background: var(--card-background);
padding: 2rem;
border-radius: 12px;
box-shadow: var(--shadow-lg);
width: 100%;
max-width: 400px;
}
.login-header {
text-align: center;
margin-bottom: 2rem;
}
.login-header h1 {
color: var(--text-primary);
font-size: 1.875rem;
font-weight: 700;
margin-bottom: 0.5rem;
}
.login-header p {
color: var(--text-secondary);
font-size: 0.875rem;
}
.form-group {
margin-bottom: 1.5rem;
}
.form-label {
display: block;
font-weight: 500;
color: var(--text-primary);
margin-bottom: 0.5rem;
font-size: 0.875rem;
}
.form-input {
width: 100%;
padding: 0.75rem 1rem;
border: 1px solid var(--border-color);
border-radius: 8px;
font-size: 1rem;
transition: border-color 0.2s, box-shadow 0.2s;
background-color: #fff;
}
.form-input:focus {
outline: none;
border-color: var(--primary-color);
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1);
}
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0.75rem 1.5rem;
border: none;
border-radius: 8px;
font-size: 0.875rem;
font-weight: 500;
text-decoration: none;
cursor: pointer;
transition: all 0.2s;
min-height: 44px;
}
.btn-primary {
background-color: var(--primary-color);
color: white;
}
.btn-primary:hover {
background-color: var(--primary-dark);
}
.btn-secondary {
background-color: var(--secondary-color);
color: white;
}
.btn-success {
background-color: var(--success-color);
color: white;
}
.btn-warning {
background-color: var(--warning-color);
color: white;
}
.btn-danger {
background-color: var(--error-color);
color: white;
}
.btn-full {
width: 100%;
}
.btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
/* Alert Styles */
.alert {
padding: 1rem;
border-radius: 8px;
margin-bottom: 1rem;
font-size: 0.875rem;
}
.alert-success {
background-color: #dcfce7;
color: #166534;
border: 1px solid #bbf7d0;
}
.alert-error {
background-color: #fef2f2;
color: #991b1b;
border: 1px solid #fecaca;
}
.alert-warning {
background-color: #fffbeb;
color: #92400e;
border: 1px solid #fed7aa;
}
.alert-info {
background-color: #eff6ff;
color: #1e40af;
border: 1px solid #bfdbfe;
}
/* Dashboard Layout */
.dashboard-container {
min-height: 100vh;
display: flex;
flex-direction: column;
}
.header {
background: var(--card-background);
border-bottom: 1px solid var(--border-color);
padding: 1rem 2rem;
box-shadow: var(--shadow);
}
.header-content {
display: flex;
justify-content: space-between;
align-items: center;
max-width: 1200px;
margin: 0 auto;
}
.header-title {
font-size: 1.5rem;
font-weight: 700;
color: var(--text-primary);
}
.header-actions {
display: flex;
align-items: center;
gap: 1rem;
}
.user-info {
color: var(--text-secondary);
font-size: 0.875rem;
}
.main-content {
flex: 1;
padding: 2rem;
max-width: 1200px;
margin: 0 auto;
width: 100%;
}
/* Card Styles */
.card {
background: var(--card-background);
border-radius: 12px;
box-shadow: var(--shadow);
overflow: hidden;
margin-bottom: 2rem;
}
.card-header {
padding: 1.5rem 2rem;
border-bottom: 1px solid var(--border-color);
background: #f8fafc;
}
.card-title {
font-size: 1.25rem;
font-weight: 600;
color: var(--text-primary);
margin: 0;
}
.card-body {
padding: 2rem;
}
/* System Status Styles */
.status-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 1rem;
margin-bottom: 2rem;
}
.status-item {
display: flex;
align-items: center;
padding: 1rem;
background: #f8fafc;
border-radius: 8px;
border: 1px solid var(--border-color);
}
.status-icon {
width: 40px;
height: 40px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin-right: 1rem;
font-size: 1.25rem;
}
.status-icon.success {
background-color: #dcfce7;
color: var(--success-color);
}
.status-icon.error {
background-color: #fef2f2;
color: var(--error-color);
}
.status-icon.warning {
background-color: #fffbeb;
color: var(--warning-color);
}
.status-content h4 {
font-size: 0.875rem;
font-weight: 500;
color: var(--text-secondary);
margin-bottom: 0.25rem;
}
.status-content p {
font-size: 1rem;
font-weight: 600;
color: var(--text-primary);
}
/* Enrollment Form Styles */
.enrollment-form {
display: grid;
gap: 1.5rem;
}
.form-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1rem;
}
.form-group-full {
grid-column: 1 / -1;
}
/* Progress Styles */
.progress-container {
margin-top: 2rem;
display: none;
}
.progress-container.active {
display: block;
}
.progress-bar {
width: 100%;
height: 8px;
background-color: #e5e7eb;
border-radius: 4px;
overflow: hidden;
margin-bottom: 1rem;
}
.progress-fill {
height: 100%;
background-color: var(--primary-color);
transition: width 0.3s ease;
width: 0%;
}
.progress-steps {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
gap: 0.5rem;
margin-bottom: 1rem;
}
.progress-step {
text-align: center;
padding: 0.5rem;
border-radius: 6px;
font-size: 0.75rem;
font-weight: 500;
background-color: #f3f4f6;
color: var(--text-secondary);
}
.progress-step.active {
background-color: var(--primary-color);
color: white;
}
.progress-step.completed {
background-color: var(--success-color);
color: white;
}
.progress-step.error {
background-color: var(--error-color);
color: white;
}
/* Log Display Styles */
.log-container {
background-color: #1f2937;
color: #f9fafb;
border-radius: 8px;
padding: 1rem;
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
font-size: 0.875rem;
line-height: 1.5;
max-height: 400px;
overflow-y: auto;
margin-top: 1rem;
}
.log-entry {
margin-bottom: 0.25rem;
word-wrap: break-word;
}
.log-entry.info {
color: #60a5fa;
}
.log-entry.success {
color: #34d399;
}
.log-entry.warning {
color: #fbbf24;
}
.log-entry.error {
color: #f87171;
}
/* Responsive Design */
@media (max-width: 768px) {
.header {
padding: 1rem;
}
.header-content {
flex-direction: column;
gap: 1rem;
align-items: flex-start;
}
.main-content {
padding: 1rem;
}
.card-body {
padding: 1rem;
}
.form-row {
grid-template-columns: 1fr;
}
.status-grid {
grid-template-columns: 1fr;
}
.progress-steps {
grid-template-columns: repeat(2, 1fr);
}
}
/* Loading Spinner */
.spinner {
display: inline-block;
width: 20px;
height: 20px;
border: 3px solid rgba(255, 255, 255, 0.3);
border-radius: 50%;
border-top-color: #fff;
animation: spin 1s ease-in-out infinite;
margin-right: 0.5rem;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
/* Utility Classes */
.text-center { text-align: center; }
.text-left { text-align: left; }
.text-right { text-align: right; }
.mt-1 { margin-top: 0.25rem; }
.mt-2 { margin-top: 0.5rem; }
.mt-3 { margin-top: 0.75rem; }
.mt-4 { margin-top: 1rem; }
.mb-1 { margin-bottom: 0.25rem; }
.mb-2 { margin-bottom: 0.5rem; }
.mb-3 { margin-bottom: 0.75rem; }
.mb-4 { margin-bottom: 1rem; }
.hidden { display: none; }
.block { display: block; }
.font-bold { font-weight: 700; }
.font-medium { font-weight: 500; }
.text-sm { font-size: 0.875rem; }
.text-xs { font-size: 0.75rem; }

415
UI/assets/js/app.js Normal file
View File

@ -0,0 +1,415 @@
/**
* ZitiNexus Router Enrollment UI JavaScript
*/
class EnrollmentUI {
constructor() {
this.enrollmentInProgress = false;
this.progressSteps = [
'INIT', 'REQUIREMENTS', 'INSTALL', 'DIRECTORIES',
'REGISTER', 'CONFIG', 'ENROLL', 'SERVICE', 'START', 'REPORT', 'COMPLETE'
];
this.currentStep = 0;
this.init();
}
init() {
this.bindEvents();
this.loadSystemStatus();
// Auto-refresh system status every 30 seconds
setInterval(() => {
if (!this.enrollmentInProgress) {
this.loadSystemStatus();
}
}, 30000);
}
bindEvents() {
// Enrollment form submission
const enrollForm = document.getElementById('enrollmentForm');
if (enrollForm) {
enrollForm.addEventListener('submit', (e) => {
e.preventDefault();
this.startEnrollment();
});
}
// Hash key validation
const hashKeyInput = document.getElementById('hashKey');
if (hashKeyInput) {
hashKeyInput.addEventListener('input', (e) => {
this.validateHashKey(e.target);
});
}
// API endpoint validation
const apiEndpointInput = document.getElementById('apiEndpoint');
if (apiEndpointInput) {
apiEndpointInput.addEventListener('input', (e) => {
this.validateApiEndpoint(e.target);
});
}
// Refresh system status button
const refreshBtn = document.getElementById('refreshStatus');
if (refreshBtn) {
refreshBtn.addEventListener('click', () => {
this.loadSystemStatus();
});
}
// Clear logs button
const clearLogsBtn = document.getElementById('clearLogs');
if (clearLogsBtn) {
clearLogsBtn.addEventListener('click', () => {
this.clearLogs();
});
}
}
validateHashKey(input) {
const value = input.value.trim();
const isValid = /^[a-fA-F0-9]{32}$/.test(value);
if (value.length === 0) {
this.setInputState(input, 'neutral');
} else if (isValid) {
this.setInputState(input, 'valid');
} else {
this.setInputState(input, 'invalid', 'Hash key must be 32 hexadecimal characters');
}
return isValid;
}
validateApiEndpoint(input) {
const value = input.value.trim();
const isValid = /^https?:\/\/.+/.test(value);
if (value.length === 0) {
this.setInputState(input, 'neutral');
} else if (isValid) {
this.setInputState(input, 'valid');
} else {
this.setInputState(input, 'invalid', 'Must be a valid HTTP/HTTPS URL');
}
return isValid;
}
setInputState(input, state, message = '') {
const formGroup = input.closest('.form-group');
const feedback = formGroup.querySelector('.form-feedback') || this.createFeedbackElement(formGroup);
// Remove existing state classes
input.classList.remove('is-valid', 'is-invalid');
feedback.classList.remove('valid-feedback', 'invalid-feedback');
switch (state) {
case 'valid':
input.classList.add('is-valid');
feedback.classList.add('valid-feedback');
feedback.textContent = 'Looks good!';
feedback.style.display = 'block';
break;
case 'invalid':
input.classList.add('is-invalid');
feedback.classList.add('invalid-feedback');
feedback.textContent = message;
feedback.style.display = 'block';
break;
case 'neutral':
default:
feedback.style.display = 'none';
break;
}
}
createFeedbackElement(formGroup) {
const feedback = document.createElement('div');
feedback.className = 'form-feedback';
feedback.style.fontSize = '0.875rem';
feedback.style.marginTop = '0.25rem';
formGroup.appendChild(feedback);
return feedback;
}
async loadSystemStatus() {
try {
const response = await fetch('dashboard.php?action=get_status', {
method: 'GET',
headers: {
'X-Requested-With': 'XMLHttpRequest'
}
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const data = await response.json();
this.updateSystemStatus(data);
} catch (error) {
console.error('Failed to load system status:', error);
this.showAlert('Failed to load system status', 'error');
}
}
updateSystemStatus(status) {
// Update hostname
const hostnameElement = document.getElementById('hostname');
if (hostnameElement) {
hostnameElement.textContent = status.hostname || 'Unknown';
}
// Update Ziti status
const zitiStatusElement = document.getElementById('zitiStatus');
const zitiStatusIcon = document.getElementById('zitiStatusIcon');
if (zitiStatusElement && zitiStatusIcon) {
if (status.ziti_status === 'installed') {
zitiStatusElement.textContent = `Installed (${status.ziti_version})`;
zitiStatusIcon.className = 'status-icon success';
zitiStatusIcon.innerHTML = '✓';
} else {
zitiStatusElement.textContent = 'Not Installed';
zitiStatusIcon.className = 'status-icon error';
zitiStatusIcon.innerHTML = '✗';
}
}
// Update service status
const serviceStatusElement = document.getElementById('serviceStatus');
const serviceStatusIcon = document.getElementById('serviceStatusIcon');
if (serviceStatusElement && serviceStatusIcon) {
if (status.service_active) {
serviceStatusElement.textContent = 'Running';
serviceStatusIcon.className = 'status-icon success';
serviceStatusIcon.innerHTML = '▶';
} else {
serviceStatusElement.textContent = 'Stopped';
serviceStatusIcon.className = 'status-icon error';
serviceStatusIcon.innerHTML = '⏹';
}
}
// Update configuration status
const configStatusElement = document.getElementById('configStatus');
const configStatusIcon = document.getElementById('configStatusIcon');
if (configStatusElement && configStatusIcon) {
if (status.config_exists && status.certificates_exist) {
configStatusElement.textContent = 'Configured';
configStatusIcon.className = 'status-icon success';
configStatusIcon.innerHTML = '⚙';
} else if (status.config_exists) {
configStatusElement.textContent = 'Partial';
configStatusIcon.className = 'status-icon warning';
configStatusIcon.innerHTML = '⚠';
} else {
configStatusElement.textContent = 'Not Configured';
configStatusIcon.className = 'status-icon error';
configStatusIcon.innerHTML = '✗';
}
}
}
async startEnrollment() {
if (this.enrollmentInProgress) {
return;
}
const hashKeyInput = document.getElementById('hashKey');
const apiEndpointInput = document.getElementById('apiEndpoint');
const enrollBtn = document.getElementById('enrollBtn');
// Validate inputs
const hashKeyValid = this.validateHashKey(hashKeyInput);
const apiEndpointValid = this.validateApiEndpoint(apiEndpointInput);
if (!hashKeyValid || !apiEndpointValid) {
this.showAlert('Please fix the validation errors before proceeding', 'error');
return;
}
// Start enrollment process
this.enrollmentInProgress = true;
this.currentStep = 0;
// Update UI
enrollBtn.disabled = true;
enrollBtn.innerHTML = '<span class="spinner"></span>Starting Enrollment...';
this.showProgressContainer();
this.clearLogs();
this.updateProgress(0, 'Initializing...');
try {
const formData = new FormData();
formData.append('action', 'enroll');
formData.append('hashKey', hashKeyInput.value.trim());
formData.append('apiEndpoint', apiEndpointInput.value.trim());
formData.append('csrf_token', document.querySelector('input[name="csrf_token"]').value);
const response = await fetch('dashboard.php', {
method: 'POST',
body: formData,
headers: {
'X-Requested-With': 'XMLHttpRequest'
}
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const result = await response.json();
if (result.success) {
this.updateProgress(100, 'Enrollment completed successfully!');
this.addLogEntry('success', `Router '${result.routerName}' enrolled successfully`);
this.showAlert(`Router enrollment completed successfully! Router: ${result.routerName}`, 'success');
// Refresh system status after successful enrollment
setTimeout(() => {
this.loadSystemStatus();
}, 2000);
} else {
throw new Error(result.error || 'Enrollment failed');
}
} catch (error) {
console.error('Enrollment failed:', error);
this.updateProgress(null, 'Enrollment failed');
this.addLogEntry('error', `Enrollment failed: ${error.message}`);
this.showAlert(`Enrollment failed: ${error.message}`, 'error');
} finally {
this.enrollmentInProgress = false;
enrollBtn.disabled = false;
enrollBtn.innerHTML = 'Start Enrollment';
}
}
showProgressContainer() {
const progressContainer = document.getElementById('progressContainer');
if (progressContainer) {
progressContainer.classList.add('active');
}
}
updateProgress(percentage, message) {
// Update progress bar
const progressFill = document.getElementById('progressFill');
if (progressFill && percentage !== null) {
progressFill.style.width = `${percentage}%`;
}
// Update current step
if (message) {
this.addLogEntry('info', message);
}
// Update progress steps
this.updateProgressSteps();
}
updateProgressSteps() {
const progressSteps = document.querySelectorAll('.progress-step');
progressSteps.forEach((step, index) => {
step.classList.remove('active', 'completed', 'error');
if (index < this.currentStep) {
step.classList.add('completed');
} else if (index === this.currentStep) {
step.classList.add('active');
}
});
}
addLogEntry(type, message) {
const logContainer = document.getElementById('logContainer');
if (!logContainer) return;
const timestamp = new Date().toLocaleTimeString();
const logEntry = document.createElement('div');
logEntry.className = `log-entry ${type}`;
logEntry.textContent = `[${timestamp}] ${message}`;
logContainer.appendChild(logEntry);
logContainer.scrollTop = logContainer.scrollHeight;
}
clearLogs() {
const logContainer = document.getElementById('logContainer');
if (logContainer) {
logContainer.innerHTML = '';
}
}
showAlert(message, type = 'info') {
// Remove existing alerts
const existingAlerts = document.querySelectorAll('.alert');
existingAlerts.forEach(alert => alert.remove());
// Create new alert
const alert = document.createElement('div');
alert.className = `alert alert-${type}`;
alert.textContent = message;
// Insert at the top of main content
const mainContent = document.querySelector('.main-content');
if (mainContent) {
mainContent.insertBefore(alert, mainContent.firstChild);
}
// Auto-remove after 5 seconds for non-error alerts
if (type !== 'error') {
setTimeout(() => {
if (alert.parentNode) {
alert.remove();
}
}, 5000);
}
}
// Utility method to format file sizes
formatFileSize(bytes) {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
// Utility method to format timestamps
formatTimestamp(timestamp) {
return new Date(timestamp * 1000).toLocaleString();
}
}
// Initialize the application when DOM is loaded
document.addEventListener('DOMContentLoaded', () => {
window.enrollmentUI = new EnrollmentUI();
});
// Add CSS classes for input validation
const style = document.createElement('style');
style.textContent = `
.form-input.is-valid {
border-color: var(--success-color);
box-shadow: 0 0 0 3px rgba(16, 185, 129, 0.1);
}
.form-input.is-invalid {
border-color: var(--error-color);
box-shadow: 0 0 0 3px rgba(239, 68, 68, 0.1);
}
.valid-feedback {
color: var(--success-color);
}
.invalid-feedback {
color: var(--error-color);
}
`;
document.head.appendChild(style);

209
UI/includes/api_client.php Normal file
View File

@ -0,0 +1,209 @@
<?php
/**
* API Client for ZitiNexus Portal communication
*/
require_once 'config.php';
class ApiClient {
private $apiEndpoint;
private $userAgent;
private $timeout;
private $maxRetries;
public function __construct($apiEndpoint = DEFAULT_API_ENDPOINT) {
$this->apiEndpoint = rtrim($apiEndpoint, '/');
$this->userAgent = 'ZitiRouter-EnrollmentUI/' . APP_VERSION;
$this->timeout = 60;
$this->maxRetries = 3;
}
/**
* Register router with ZitiNexus Portal
*/
public function registerRouter($hashKey) {
$url = $this->apiEndpoint . '/api/router/register';
$payload = json_encode(['hashKey' => $hashKey]);
logMessage('INFO', "Registering router with API: $url");
$response = $this->makeRequest('POST', $url, $payload);
if (!$response['success']) {
logMessage('ERROR', "API registration failed: " . $response['error']);
return $response;
}
$data = json_decode($response['body'], true);
if (!$data || !isset($data['success']) || !$data['success']) {
$errorMsg = isset($data['error']['message']) ? $data['error']['message'] : 'Registration failed';
logMessage('ERROR', "Registration failed: $errorMsg");
return [
'success' => false,
'error' => $errorMsg
];
}
logMessage('SUCCESS', "Router registered successfully");
return [
'success' => true,
'data' => $data['data']
];
}
/**
* Report enrollment status to portal
*/
public function reportStatus($callbackUrl, $hashKey, $status, $routerInfo = null, $errorMessage = null) {
if (empty($callbackUrl)) {
logMessage('WARNING', 'No callback URL provided, skipping status report');
return ['success' => true];
}
// Fix callback URL domain mismatch if needed
$fixedCallbackUrl = $this->fixCallbackUrl($callbackUrl);
$payload = [
'hashKey' => $hashKey,
'status' => $status
];
if ($status === 'success' && $routerInfo) {
$payload['routerInfo'] = $routerInfo;
} elseif ($status === 'failed' && $errorMessage) {
$payload['error'] = $errorMessage;
}
logMessage('INFO', "Reporting status '$status' to: $fixedCallbackUrl");
$response = $this->makeRequest('POST', $fixedCallbackUrl, json_encode($payload));
if ($response['success']) {
logMessage('SUCCESS', 'Status reported successfully');
} else {
logMessage('WARNING', 'Failed to report status: ' . $response['error']);
}
return $response;
}
/**
* Make HTTP request with retry logic
*/
private function makeRequest($method, $url, $data = null) {
$retryCount = 0;
while ($retryCount < $this->maxRetries) {
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => $url,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => $this->timeout,
CURLOPT_CONNECTTIMEOUT => 30,
CURLOPT_USERAGENT => $this->userAgent,
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
'Accept: application/json'
],
CURLOPT_SSL_VERIFYPEER => true,
CURLOPT_SSL_VERIFYHOST => 2,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_MAXREDIRS => 3
]);
if ($method === 'POST' && $data) {
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
}
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$error = curl_error($ch);
curl_close($ch);
if ($response === false) {
logMessage('ERROR', "cURL error: $error");
$retryCount++;
if ($retryCount < $this->maxRetries) {
$waitTime = $retryCount * 2;
logMessage('INFO', "Retrying in {$waitTime}s... (attempt $retryCount/$this->maxRetries)");
sleep($waitTime);
}
continue;
}
if ($httpCode === 200) {
return [
'success' => true,
'body' => $response,
'http_code' => $httpCode
];
} elseif ($httpCode === 429) {
// Rate limited
$retryCount++;
if ($retryCount < $this->maxRetries) {
$waitTime = $retryCount * 2;
logMessage('WARNING', "Rate limited. Waiting {$waitTime}s before retry $retryCount/$this->maxRetries");
sleep($waitTime);
}
continue;
} else {
$errorMsg = "HTTP $httpCode";
if ($response) {
$responseData = json_decode($response, true);
if ($responseData && isset($responseData['error']['message'])) {
$errorMsg .= ': ' . $responseData['error']['message'];
} elseif ($responseData && isset($responseData['message'])) {
$errorMsg .= ': ' . $responseData['message'];
}
}
return [
'success' => false,
'error' => $errorMsg,
'http_code' => $httpCode,
'body' => $response
];
}
}
return [
'success' => false,
'error' => 'Max retries exceeded',
'http_code' => 0
];
}
/**
* Fix callback URL domain mismatch
*/
private function fixCallbackUrl($callbackUrl) {
// Replace api.zitinexus.com with backend.zitinexus.com to match script's API endpoint
if (strpos($callbackUrl, 'api.zitinexus.com') !== false) {
$fixedUrl = str_replace('api.zitinexus.com', 'backend.zitinexus.com', $callbackUrl);
logMessage('INFO', "Fixed callback URL domain: $fixedUrl");
return $fixedUrl;
}
return $callbackUrl;
}
/**
* Validate hash key format
*/
public static function validateHashKey($hashKey) {
return preg_match('/^[a-fA-F0-9]{32}$/', $hashKey);
}
/**
* Validate API endpoint format
*/
public static function validateApiEndpoint($endpoint) {
return filter_var($endpoint, FILTER_VALIDATE_URL) &&
(strpos($endpoint, 'http://') === 0 || strpos($endpoint, 'https://') === 0);
}
}
?>

107
UI/includes/auth.php Normal file
View File

@ -0,0 +1,107 @@
<?php
/**
* Authentication handler for Ziti Router Enrollment UI
*/
require_once 'config.php';
class AuthManager {
/**
* Authenticate user with username and password
*/
public static function authenticate($username, $password) {
$username = sanitizeInput($username);
if ($username === ADMIN_USERNAME && password_verify($password, ADMIN_PASSWORD_HASH)) {
$_SESSION['authenticated'] = true;
$_SESSION['username'] = $username;
$_SESSION['last_activity'] = time();
$_SESSION['login_time'] = time();
// Generate new CSRF token
generateCSRFToken();
logMessage('INFO', "User '$username' logged in successfully from " . $_SERVER['REMOTE_ADDR']);
return true;
}
logMessage('WARNING', "Failed login attempt for user '$username' from " . $_SERVER['REMOTE_ADDR']);
return false;
}
/**
* Logout user
*/
public static function logout() {
if (isset($_SESSION['username'])) {
logMessage('INFO', "User '{$_SESSION['username']}' logged out");
}
session_destroy();
session_start();
}
/**
* Check if user is authenticated and session is valid
*/
public static function requireAuth() {
if (!isAuthenticated() || !isSessionValid()) {
header('Location: index.php?error=session_expired');
exit;
}
}
/**
* Get current user info
*/
public static function getCurrentUser() {
if (!isAuthenticated()) {
return null;
}
return [
'username' => $_SESSION['username'] ?? '',
'login_time' => $_SESSION['login_time'] ?? 0,
'last_activity' => $_SESSION['last_activity'] ?? 0
];
}
/**
* Check CSRF token for forms
*/
public static function requireCSRF() {
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$token = $_POST[CSRF_TOKEN_NAME] ?? '';
if (!verifyCSRFToken($token)) {
http_response_code(403);
die('CSRF token validation failed');
}
}
}
}
/**
* Handle login form submission
*/
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['action'] === 'login') {
$username = $_POST['username'] ?? '';
$password = $_POST['password'] ?? '';
if (AuthManager::authenticate($username, $password)) {
header('Location: dashboard.php');
exit;
} else {
$loginError = 'Invalid username or password';
}
}
/**
* Handle logout
*/
if (isset($_GET['action']) && $_GET['action'] === 'logout') {
AuthManager::logout();
header('Location: index.php?message=logged_out');
exit;
}
?>

147
UI/includes/config.php Normal file
View File

@ -0,0 +1,147 @@
<?php
/**
* Configuration file for Ziti Router Enrollment UI
*/
// Start session if not already started
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
// Application configuration
define('APP_NAME', 'ZitiNexus Router Enrollment');
define('APP_VERSION', '1.0.0');
// Default API configuration
define('DEFAULT_API_ENDPOINT', 'https://backend.zitinexus.com');
// File paths (matching the bash script)
define('CONFIG_DIR', '/etc/zitirouter');
define('CERTS_DIR', CONFIG_DIR . '/certs');
define('ROUTER_CONFIG', CONFIG_DIR . '/router.yaml');
define('JWT_FILE', CONFIG_DIR . '/enrollment.jwt');
define('LOG_FILE', '/var/log/ziti-router-enrollment.log');
define('SYSTEMD_SERVICE_FILE', '/etc/systemd/system/ziti-router.service');
// UI specific paths
define('UI_LOG_DIR', __DIR__ . '/../logs');
define('UI_TEMP_DIR', __DIR__ . '/../temp');
// Authentication
define('ADMIN_USERNAME', 'admin');
define('ADMIN_PASSWORD_HASH', password_hash('admin123', PASSWORD_DEFAULT)); // Change this in production
// Security settings
define('SESSION_TIMEOUT', 3600); // 1 hour
define('CSRF_TOKEN_NAME', 'csrf_token');
// System commands
define('SYSTEMCTL_CMD', 'systemctl');
define('HOSTNAME_CMD', 'hostname');
define('ZITI_CMD', 'ziti');
/**
* Check if user is authenticated
*/
function isAuthenticated() {
return isset($_SESSION['authenticated']) && $_SESSION['authenticated'] === true;
}
/**
* Check if session is valid
*/
function isSessionValid() {
if (!isset($_SESSION['last_activity'])) {
return false;
}
if (time() - $_SESSION['last_activity'] > SESSION_TIMEOUT) {
session_destroy();
return false;
}
$_SESSION['last_activity'] = time();
return true;
}
/**
* Generate CSRF token
*/
function generateCSRFToken() {
if (!isset($_SESSION[CSRF_TOKEN_NAME])) {
$_SESSION[CSRF_TOKEN_NAME] = bin2hex(random_bytes(32));
}
return $_SESSION[CSRF_TOKEN_NAME];
}
/**
* Verify CSRF token
*/
function verifyCSRFToken($token) {
return isset($_SESSION[CSRF_TOKEN_NAME]) && hash_equals($_SESSION[CSRF_TOKEN_NAME], $token);
}
/**
* Sanitize input
*/
function sanitizeInput($input) {
return htmlspecialchars(trim($input), ENT_QUOTES, 'UTF-8');
}
/**
* Log message to file
*/
function logMessage($level, $message) {
$timestamp = date('Y-m-d H:i:s');
$logEntry = "[$timestamp] [$level] $message" . PHP_EOL;
// Try to write to system log first
if (is_writable(dirname(LOG_FILE))) {
file_put_contents(LOG_FILE, $logEntry, FILE_APPEND | LOCK_EX);
}
// Also write to UI log
$uiLogFile = UI_LOG_DIR . '/ui-enrollment.log';
if (!is_dir(UI_LOG_DIR)) {
mkdir(UI_LOG_DIR, 0755, true);
}
file_put_contents($uiLogFile, $logEntry, FILE_APPEND | LOCK_EX);
}
/**
* Check if running as root/admin
*/
function isRunningAsRoot() {
return posix_getuid() === 0;
}
/**
* Execute system command safely
*/
function executeCommand($command, &$output = null, &$returnCode = null) {
$descriptorspec = [
0 => ['pipe', 'r'], // stdin
1 => ['pipe', 'w'], // stdout
2 => ['pipe', 'w'] // stderr
];
$process = proc_open($command, $descriptorspec, $pipes);
if (is_resource($process)) {
fclose($pipes[0]);
$stdout = stream_get_contents($pipes[1]);
$stderr = stream_get_contents($pipes[2]);
fclose($pipes[1]);
fclose($pipes[2]);
$returnCode = proc_close($process);
$output = trim($stdout . $stderr);
return $returnCode === 0;
}
return false;
}
?>

539
UI/includes/enrollment.php Normal file
View File

@ -0,0 +1,539 @@
<?php
/**
* Enrollment Manager for Ziti Router
* Replicates the functionality of the bash script
*/
require_once 'config.php';
require_once 'api_client.php';
class EnrollmentManager {
private $apiClient;
private $routerData;
private $progressCallback;
public function __construct($apiEndpoint = DEFAULT_API_ENDPOINT) {
$this->apiClient = new ApiClient($apiEndpoint);
$this->routerData = [];
}
/**
* Set progress callback function
*/
public function setProgressCallback($callback) {
$this->progressCallback = $callback;
}
/**
* Report progress
*/
private function reportProgress($step, $message, $percentage = null) {
logMessage('INFO', "[$step] $message");
if ($this->progressCallback && is_callable($this->progressCallback)) {
call_user_func($this->progressCallback, $step, $message, $percentage);
}
}
/**
* Main enrollment process
*/
public function enrollRouter($hashKey, $apiEndpoint = null) {
try {
if ($apiEndpoint) {
$this->apiClient = new ApiClient($apiEndpoint);
}
$this->reportProgress('INIT', 'Starting router enrollment process...', 0);
// Step 1: Check system requirements
$this->reportProgress('REQUIREMENTS', 'Checking system requirements...', 10);
if (!$this->checkSystemRequirements()) {
throw new Exception('System requirements check failed');
}
// Step 2: Install OpenZiti if needed
$this->reportProgress('INSTALL', 'Installing OpenZiti CLI...', 20);
if (!$this->installZiti()) {
throw new Exception('OpenZiti installation failed');
}
// Step 3: Create directories
$this->reportProgress('DIRECTORIES', 'Creating necessary directories...', 30);
if (!$this->createDirectories()) {
throw new Exception('Failed to create directories');
}
// Step 4: Register router with API
$this->reportProgress('REGISTER', 'Registering router with ZitiNexus Portal...', 40);
$result = $this->apiClient->registerRouter($hashKey);
if (!$result['success']) {
throw new Exception('Router registration failed: ' . $result['error']);
}
$this->routerData = $result['data'];
$this->reportProgress('REGISTER', 'Router registered successfully: ' . $this->routerData['routerInfo']['name'], 50);
// Step 5: Save configuration files
$this->reportProgress('CONFIG', 'Saving configuration files...', 60);
if (!$this->saveConfiguration()) {
throw new Exception('Failed to save configuration files');
}
// Step 6: Enroll router with OpenZiti
$this->reportProgress('ENROLL', 'Enrolling router with OpenZiti controller...', 70);
if (!$this->enrollWithZiti()) {
throw new Exception('Router enrollment with OpenZiti failed');
}
// Step 7: Create systemd service
$this->reportProgress('SERVICE', 'Creating systemd service...', 80);
if (!$this->createSystemdService()) {
throw new Exception('Failed to create systemd service');
}
// Step 8: Start router service
$this->reportProgress('START', 'Starting router service...', 90);
if (!$this->startRouter()) {
throw new Exception('Failed to start router service');
}
// Step 9: Report success status
$this->reportProgress('REPORT', 'Reporting enrollment status...', 95);
$this->reportSuccessStatus($hashKey);
$this->reportProgress('COMPLETE', 'Router enrollment completed successfully!', 100);
return [
'success' => true,
'routerName' => $this->routerData['routerInfo']['name'],
'routerId' => $this->routerData['routerInfo']['id'],
'message' => 'Router enrollment completed successfully'
];
} catch (Exception $e) {
$errorMsg = $e->getMessage();
logMessage('ERROR', $errorMsg);
$this->reportProgress('ERROR', $errorMsg, null);
// Report failure status
if (!empty($hashKey) && !empty($this->routerData['callbackUrl'])) {
$this->apiClient->reportStatus(
$this->routerData['callbackUrl'],
$hashKey,
'failed',
null,
$errorMsg
);
}
return [
'success' => false,
'error' => $errorMsg
];
}
}
/**
* Check system requirements
*/
private function checkSystemRequirements() {
// Check if running as root
if (!isRunningAsRoot()) {
throw new Exception('This script must be run as root (use sudo)');
}
// Check if curl is available
if (!$this->checkCommand('curl')) {
$this->reportProgress('REQUIREMENTS', 'Installing curl...');
if (!$this->installPackage('curl')) {
return false;
}
}
// Check if jq is available
if (!$this->checkCommand('jq')) {
$this->reportProgress('REQUIREMENTS', 'Installing jq...');
if (!$this->installPackage('jq')) {
return false;
}
}
// Check if systemctl is available
if (!$this->checkCommand('systemctl')) {
throw new Exception('systemctl is required but not available');
}
return true;
}
/**
* Install OpenZiti CLI
*/
private function installZiti() {
// Check if ziti is already installed
if ($this->checkCommand('ziti')) {
$output = '';
executeCommand('ziti version 2>/dev/null | head -n1', $output);
$this->reportProgress('INSTALL', 'OpenZiti CLI already installed: ' . trim($output));
return true;
}
$this->reportProgress('INSTALL', 'Setting up OpenZiti package repository...');
// Add GPG key
$gpgCommand = 'curl -sSLf https://get.openziti.io/tun/package-repos.gpg | gpg --dearmor --output /usr/share/keyrings/openziti.gpg';
if (!executeCommand($gpgCommand)) {
throw new Exception('Failed to add OpenZiti GPG key');
}
// Set proper permissions
if (!executeCommand('chmod a+r /usr/share/keyrings/openziti.gpg')) {
throw new Exception('Failed to set GPG key permissions');
}
// Add repository to sources list
$repoContent = 'deb [signed-by=/usr/share/keyrings/openziti.gpg] https://packages.openziti.org/zitipax-openziti-deb-stable debian main';
if (!file_put_contents('/etc/apt/sources.list.d/openziti-release.list', $repoContent)) {
throw new Exception('Failed to add OpenZiti repository');
}
// Update package list
$this->reportProgress('INSTALL', 'Updating package list...');
if (!executeCommand('apt-get update')) {
throw new Exception('Failed to update package list');
}
// Install openziti-router package
$this->reportProgress('INSTALL', 'Installing openziti-router package...');
if (!executeCommand('apt-get install -y openziti-router')) {
$this->reportProgress('INSTALL', 'Trying to install ziti CLI only...');
if (!executeCommand('apt-get install -y ziti')) {
throw new Exception('Failed to install OpenZiti CLI');
}
}
// Verify installation
if (!$this->checkCommand('ziti')) {
throw new Exception('OpenZiti CLI installation failed - command not found after installation');
}
$output = '';
executeCommand('ziti version 2>/dev/null | head -n1', $output);
$this->reportProgress('INSTALL', 'OpenZiti CLI installed successfully: ' . trim($output));
return true;
}
/**
* Create necessary directories
*/
private function createDirectories() {
$directories = [
CONFIG_DIR => 0755,
CERTS_DIR => 0700,
dirname(LOG_FILE) => 0755
];
foreach ($directories as $dir => $permissions) {
if (!is_dir($dir)) {
if (!mkdir($dir, $permissions, true)) {
throw new Exception("Failed to create directory: $dir");
}
}
chmod($dir, $permissions);
}
return true;
}
/**
* Save configuration files
*/
private function saveConfiguration() {
// Save JWT
if (!file_put_contents(JWT_FILE, $this->routerData['jwt'])) {
throw new Exception('Failed to save JWT file');
}
chmod(JWT_FILE, 0600);
// Save router configuration
if (!file_put_contents(ROUTER_CONFIG, $this->routerData['routerConfig']['yaml'])) {
throw new Exception('Failed to save router configuration');
}
chmod(ROUTER_CONFIG, 0644);
// Fix router configuration for proper enrollment
$this->fixRouterConfiguration();
return true;
}
/**
* Fix router configuration (replicate bash script logic)
*/
private function fixRouterConfiguration() {
// Create backup
copy(ROUTER_CONFIG, ROUTER_CONFIG . '.backup');
$routerName = $this->routerData['routerInfo']['name'];
$routerId = $this->routerData['routerInfo']['id'];
$tenantId = $this->routerData['metadata']['tenantId'];
$controllerEndpoint = $this->routerData['metadata']['controllerEndpoint'] ?? 'enroll.zitinexus.com:443';
// Add tls: prefix if not present
if (strpos($controllerEndpoint, 'tls:') !== 0) {
$controllerEndpoint = 'tls:' . $controllerEndpoint;
}
// Build role attributes
$roleAttributesSection = '# No role attributes specified';
if (!empty($this->routerData['routerInfo']['roleAttributes'])) {
$roleAttributesSection = "roleAttributes:";
foreach ($this->routerData['routerInfo']['roleAttributes'] as $attr) {
$roleAttributesSection .= "\n - \"$attr\"";
}
}
$generatedAt = date('c');
$configContent = <<<EOF
v: 3
identity:
cert: /etc/zitirouter/certs/$routerName.cert
server_cert: /etc/zitirouter/certs/$routerName.server.chain.cert
key: /etc/zitirouter/certs/$routerName.key
ca: /etc/zitirouter/certs/$routerName.cas
ctrl:
endpoint: $controllerEndpoint
link:
dialers:
- binding: transport
listeners:
# bindings of edge and tunnel requires an "edge" section below
- binding: edge
address: tls:0.0.0.0:443
options:
advertise: 127.0.0.1:443
connectTimeoutMs: 5000
getSessionTimeout: 60
- binding: tunnel
options:
mode: host
edge:
csr:
country: SG
province: SG
locality: Singapore
organization: Genworx
organizationalUnit: ZitiNexus
sans:
dns:
- localhost
- $routerName
ip:
- "127.0.0.1"
- "::1"
# Tenant-specific role attributes
$roleAttributesSection
# Router metadata
metadata:
tenantId: "$tenantId"
zitiRouterId: "$routerId"
routerType: "private-edge"
generatedAt: "$generatedAt"
generatedBy: "ZitiNexus"
EOF;
file_put_contents(ROUTER_CONFIG, $configContent);
chmod(ROUTER_CONFIG, 0644);
}
/**
* Enroll router with OpenZiti
*/
private function enrollWithZiti() {
$command = 'ziti router enroll --jwt ' . JWT_FILE . ' ' . ROUTER_CONFIG . ' 2>&1';
$output = '';
if (!executeCommand($command, $output)) {
throw new Exception('Router enrollment failed: ' . $output);
}
// Verify certificates were created
$routerName = $this->routerData['routerInfo']['name'];
$certFile = CERTS_DIR . '/' . $routerName . '.cert';
if (!file_exists($certFile)) {
throw new Exception('Router certificate not found after enrollment');
}
return true;
}
/**
* Create systemd service
*/
private function createSystemdService() {
$finalConfig = '/etc/zitirouter/zitirouter.yaml';
// Copy router config to final location
if (!copy(ROUTER_CONFIG, $finalConfig)) {
throw new Exception('Failed to copy router config to final location');
}
chmod($finalConfig, 0644);
$serviceContent = <<<EOF
[Unit]
Description=OpenZiti Router
After=network.target
[Service]
ExecStart=/usr/bin/ziti router run /etc/zitirouter/zitirouter.yaml
Restart=on-failure
User=root
WorkingDirectory=/etc/zitirouter
LimitNOFILE=65536
StandardOutput=append:/var/log/ziti-router.log
StandardError=append:/var/log/ziti-router.log
[Install]
WantedBy=multi-user.target
EOF;
if (!file_put_contents(SYSTEMD_SERVICE_FILE, $serviceContent)) {
throw new Exception('Failed to create systemd service file');
}
// Reload systemd and enable service
if (!executeCommand('systemctl daemon-reload')) {
throw new Exception('Failed to reload systemd');
}
if (!executeCommand('systemctl enable ziti-router.service')) {
throw new Exception('Failed to enable ziti-router service');
}
return true;
}
/**
* Start router service
*/
private function startRouter() {
if (!executeCommand('systemctl start ziti-router.service')) {
throw new Exception('Failed to start router service');
}
// Wait and check status
sleep(3);
$output = '';
executeCommand('systemctl is-active ziti-router.service', $output);
if (trim($output) !== 'active') {
logMessage('WARNING', 'Router service may not be running properly');
}
return true;
}
/**
* Report success status
*/
private function reportSuccessStatus($hashKey) {
if (empty($this->routerData['callbackUrl'])) {
return;
}
$hostname = '';
$arch = '';
$os = '';
$zitiVersion = '';
executeCommand('hostname', $hostname);
executeCommand('uname -m', $arch);
executeCommand('lsb_release -d 2>/dev/null | cut -f2', $os);
executeCommand('ziti version 2>/dev/null | head -n1', $zitiVersion);
$routerInfo = [
'version' => trim($zitiVersion) ?: 'unknown',
'hostname' => trim($hostname),
'arch' => trim($arch),
'os' => trim($os) ?: 'Linux'
];
$this->apiClient->reportStatus(
$this->routerData['callbackUrl'],
$hashKey,
'success',
$routerInfo
);
}
/**
* Check if command exists
*/
private function checkCommand($command) {
$output = '';
return executeCommand("which $command", $output);
}
/**
* Install package using apt
*/
private function installPackage($package) {
return executeCommand("apt-get update && apt-get install -y $package");
}
/**
* Get system status information
*/
public function getSystemStatus() {
$status = [
'hostname' => '',
'ziti_status' => 'unknown',
'ziti_version' => '',
'service_active' => false,
'config_exists' => false,
'certificates_exist' => false
];
// Get hostname
executeCommand('hostname', $status['hostname']);
$status['hostname'] = trim($status['hostname']);
// Check if ziti command exists and get version
if ($this->checkCommand('ziti')) {
executeCommand('ziti version 2>/dev/null | head -n1', $status['ziti_version']);
$status['ziti_version'] = trim($status['ziti_version']);
$status['ziti_status'] = 'installed';
} else {
$status['ziti_status'] = 'not_installed';
}
// Check service status
$output = '';
if (executeCommand('systemctl is-active ziti-router.service 2>/dev/null', $output)) {
$status['service_active'] = trim($output) === 'active';
}
// Check configuration files
$status['config_exists'] = file_exists(ROUTER_CONFIG);
// Check certificates
if (is_dir(CERTS_DIR)) {
$certFiles = glob(CERTS_DIR . '/*.cert');
$status['certificates_exist'] = !empty($certFiles);
}
return $status;
}
}
?>

399
UI/install.sh Normal file
View File

@ -0,0 +1,399 @@
#!/bin/bash
# ZitiNexus Router Enrollment UI Installation Script
# For Ubuntu 22.04/24.04 LTS
set -euo pipefail
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Configuration
WEB_DIR="/var/www/ziti-enrollment"
DOMAIN="ziti-enrollment.local"
WEB_USER="www-data"
# Logging function
log() {
local level=$1
shift
local message="$*"
local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
case $level in
"ERROR")
echo -e "${RED}[ERROR]${NC} $message" >&2
;;
"SUCCESS")
echo -e "${GREEN}[SUCCESS]${NC} $message"
;;
"WARNING")
echo -e "${YELLOW}[WARNING]${NC} $message"
;;
"INFO")
echo -e "${BLUE}[INFO]${NC} $message"
;;
*)
echo "$message"
;;
esac
}
# Error handling
error_exit() {
log "ERROR" "$1"
exit 1
}
# Check if running as root
check_root() {
if [[ $EUID -ne 0 ]]; then
error_exit "This script must be run as root (use sudo)"
fi
}
# Detect web server preference
detect_web_server() {
echo
log "INFO" "Which web server would you like to use?"
echo "1) Apache (recommended)"
echo "2) Nginx"
read -p "Enter your choice (1-2): " choice
case $choice in
1)
WEB_SERVER="apache"
;;
2)
WEB_SERVER="nginx"
;;
*)
log "WARNING" "Invalid choice, defaulting to Apache"
WEB_SERVER="apache"
;;
esac
log "INFO" "Selected web server: $WEB_SERVER"
}
# Install web server and PHP
install_web_server() {
log "INFO" "Installing web server and PHP..."
# Update package list
apt update || error_exit "Failed to update package list"
if [[ "$WEB_SERVER" == "apache" ]]; then
# Install Apache and PHP
apt install -y apache2 php8.1 php8.1-curl php8.1-json libapache2-mod-php8.1 || error_exit "Failed to install Apache and PHP"
# Enable and start Apache
systemctl enable apache2 || error_exit "Failed to enable Apache"
systemctl start apache2 || error_exit "Failed to start Apache"
log "SUCCESS" "Apache and PHP installed successfully"
elif [[ "$WEB_SERVER" == "nginx" ]]; then
# Install Nginx and PHP-FPM
apt install -y nginx php8.1-fpm php8.1-curl php8.1-json || error_exit "Failed to install Nginx and PHP"
# Enable and start services
systemctl enable nginx php8.1-fpm || error_exit "Failed to enable Nginx and PHP-FPM"
systemctl start nginx php8.1-fpm || error_exit "Failed to start Nginx and PHP-FPM"
log "SUCCESS" "Nginx and PHP-FPM installed successfully"
fi
}
# Deploy UI files
deploy_ui() {
log "INFO" "Deploying UI files..."
# Create web directory
mkdir -p "$WEB_DIR" || error_exit "Failed to create web directory"
# Copy UI files (assuming script is run from the UI directory)
if [[ -d "public" && -d "includes" && -d "assets" ]]; then
cp -r public includes assets logs temp README.md "$WEB_DIR/" || error_exit "Failed to copy UI files"
else
error_exit "UI files not found. Please run this script from the UI directory."
fi
# Set proper permissions
chown -R "$WEB_USER:$WEB_USER" "$WEB_DIR" || error_exit "Failed to set ownership"
chmod -R 755 "$WEB_DIR" || error_exit "Failed to set permissions"
chmod -R 777 "$WEB_DIR/logs" "$WEB_DIR/temp" || error_exit "Failed to set log/temp permissions"
log "SUCCESS" "UI files deployed successfully"
}
# Configure Apache
configure_apache() {
log "INFO" "Configuring Apache virtual host..."
# Create virtual host configuration
cat > "/etc/apache2/sites-available/ziti-enrollment.conf" << EOF
<VirtualHost *:80>
ServerName $DOMAIN
DocumentRoot $WEB_DIR/public
<Directory $WEB_DIR/public>
AllowOverride All
Require all granted
DirectoryIndex index.php
</Directory>
ErrorLog \${APACHE_LOG_DIR}/ziti-enrollment_error.log
CustomLog \${APACHE_LOG_DIR}/ziti-enrollment_access.log combined
</VirtualHost>
EOF
# Enable site and modules
a2ensite ziti-enrollment.conf || error_exit "Failed to enable site"
a2enmod rewrite || error_exit "Failed to enable rewrite module"
# Disable default site
a2dissite 000-default.conf || log "WARNING" "Failed to disable default site"
# Reload Apache
systemctl reload apache2 || error_exit "Failed to reload Apache"
log "SUCCESS" "Apache configured successfully"
}
# Configure Nginx
configure_nginx() {
log "INFO" "Configuring Nginx virtual host..."
# Create virtual host configuration
cat > "/etc/nginx/sites-available/ziti-enrollment" << EOF
server {
listen 80;
server_name $DOMAIN;
root $WEB_DIR/public;
index index.php;
location / {
try_files \$uri \$uri/ /index.php?\$query_string;
}
location ~ \.php$ {
fastcgi_pass unix:/var/run/php/php8.1-fpm.sock;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME \$realpath_root\$fastcgi_script_name;
include fastcgi_params;
}
location ~ /\. {
deny all;
}
}
EOF
# Enable site
ln -sf "/etc/nginx/sites-available/ziti-enrollment" "/etc/nginx/sites-enabled/" || error_exit "Failed to enable site"
# Remove default site
rm -f "/etc/nginx/sites-enabled/default" || log "WARNING" "Failed to remove default site"
# Test and reload Nginx
nginx -t || error_exit "Nginx configuration test failed"
systemctl reload nginx || error_exit "Failed to reload Nginx"
log "SUCCESS" "Nginx configured successfully"
}
# Configure PHP
configure_php() {
log "INFO" "Configuring PHP..."
# Find PHP configuration file
if [[ "$WEB_SERVER" == "apache" ]]; then
PHP_INI="/etc/php/8.1/apache2/php.ini"
else
PHP_INI="/etc/php/8.1/fpm/php.ini"
fi
# Check if exec functions are disabled
if grep -q "disable_functions.*exec" "$PHP_INI"; then
log "WARNING" "PHP exec functions may be disabled. Please check $PHP_INI"
log "INFO" "Ensure exec, shell_exec, proc_open are NOT in disable_functions"
fi
# Restart web server to apply PHP changes
if [[ "$WEB_SERVER" == "apache" ]]; then
systemctl restart apache2 || error_exit "Failed to restart Apache"
else
systemctl restart php8.1-fpm || error_exit "Failed to restart PHP-FPM"
fi
log "SUCCESS" "PHP configured successfully"
}
# Set up sudo access
setup_sudo() {
log "INFO" "Setting up sudo access for web server..."
# Create sudoers file
cat > "/etc/sudoers.d/ziti-enrollment" << 'EOF'
# Allow www-data to run system commands for Ziti enrollment
www-data ALL=(ALL) NOPASSWD: /usr/bin/apt-get
www-data ALL=(ALL) NOPASSWD: /usr/bin/systemctl
www-data ALL=(ALL) NOPASSWD: /usr/bin/mkdir
www-data ALL=(ALL) NOPASSWD: /usr/bin/chmod
www-data ALL=(ALL) NOPASSWD: /usr/bin/chown
www-data ALL=(ALL) NOPASSWD: /usr/bin/curl
www-data ALL=(ALL) NOPASSWD: /usr/bin/gpg
www-data ALL=(ALL) NOPASSWD: /usr/bin/ziti
www-data ALL=(ALL) NOPASSWD: /usr/bin/which
www-data ALL=(ALL) NOPASSWD: /usr/bin/hostname
www-data ALL=(ALL) NOPASSWD: /usr/bin/uname
www-data ALL=(ALL) NOPASSWD: /usr/bin/lsb_release
EOF
# Validate sudoers file
if visudo -c -f "/etc/sudoers.d/ziti-enrollment"; then
log "SUCCESS" "Sudo access configured successfully"
else
error_exit "Invalid sudoers configuration"
fi
}
# Update hosts file
update_hosts() {
log "INFO" "Updating hosts file..."
# Check if entry already exists
if ! grep -q "$DOMAIN" /etc/hosts; then
echo "127.0.0.1 $DOMAIN" >> /etc/hosts
log "SUCCESS" "Added $DOMAIN to hosts file"
else
log "INFO" "Domain already exists in hosts file"
fi
}
# Test installation
test_installation() {
log "INFO" "Testing installation..."
# Test web server
if [[ "$WEB_SERVER" == "apache" ]]; then
if systemctl is-active --quiet apache2; then
log "SUCCESS" "Apache is running"
else
log "ERROR" "Apache is not running"
fi
else
if systemctl is-active --quiet nginx && systemctl is-active --quiet php8.1-fpm; then
log "SUCCESS" "Nginx and PHP-FPM are running"
else
log "ERROR" "Nginx or PHP-FPM is not running"
fi
fi
# Test PHP
if php -v > /dev/null 2>&1; then
log "SUCCESS" "PHP is working"
else
log "ERROR" "PHP is not working"
fi
# Test sudo access
if sudo -u www-data sudo -n systemctl --version > /dev/null 2>&1; then
log "SUCCESS" "Sudo access is working"
else
log "WARNING" "Sudo access may not be working properly"
fi
# Test file permissions
if [[ -r "$WEB_DIR/public/index.php" ]]; then
log "SUCCESS" "File permissions are correct"
else
log "ERROR" "File permissions may be incorrect"
fi
}
# Show final information
show_final_info() {
echo
echo "=============================================="
echo " INSTALLATION COMPLETED"
echo "=============================================="
echo
log "SUCCESS" "ZitiNexus Router Enrollment UI installed successfully!"
echo
echo "Access Information:"
echo " URL: http://$DOMAIN"
echo " Username: admin"
echo " Password: admin123"
echo
echo "Important Notes:"
echo " 1. Change the default password in production"
echo " 2. Consider setting up HTTPS for production use"
echo " 3. Review security settings in $WEB_DIR/includes/config.php"
echo
echo "File Locations:"
echo " Web Directory: $WEB_DIR"
echo " Configuration: $WEB_DIR/includes/config.php"
echo " Logs: $WEB_DIR/logs/"
echo
echo "Useful Commands:"
if [[ "$WEB_SERVER" == "apache" ]]; then
echo " Check status: systemctl status apache2"
echo " View logs: tail -f /var/log/apache2/ziti-enrollment_error.log"
else
echo " Check status: systemctl status nginx php8.1-fpm"
echo " View logs: tail -f /var/log/nginx/error.log"
fi
echo " Test sudo: sudo -u www-data sudo -l"
echo
}
# Main installation function
main() {
echo "=============================================="
echo " ZitiNexus Router Enrollment UI Installer"
echo "=============================================="
echo
# Check if running as root
check_root
# Detect web server preference
detect_web_server
# Install web server and PHP
install_web_server
# Deploy UI files
deploy_ui
# Configure web server
if [[ "$WEB_SERVER" == "apache" ]]; then
configure_apache
else
configure_nginx
fi
# Configure PHP
configure_php
# Set up sudo access
setup_sudo
# Update hosts file
update_hosts
# Test installation
test_installation
# Show final information
show_final_info
}
# Run main function
main "$@"

310
UI/public/dashboard.php Normal file
View File

@ -0,0 +1,310 @@
<?php
/**
* Main dashboard for ZitiNexus Router Enrollment UI
*/
require_once '../includes/auth.php';
require_once '../includes/enrollment.php';
// Require authentication
AuthManager::requireAuth();
// Get current user
$currentUser = AuthManager::getCurrentUser();
// Initialize enrollment manager
$enrollmentManager = new EnrollmentManager();
// Handle AJAX requests
if (isset($_SERVER['HTTP_X_REQUESTED_WITH']) && strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) === 'xmlhttprequest') {
header('Content-Type: application/json');
if (isset($_GET['action']) && $_GET['action'] === 'get_status') {
// Get system status
$status = $enrollmentManager->getSystemStatus();
echo json_encode($status);
exit;
}
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['action'] === 'enroll') {
// Handle enrollment request
AuthManager::requireCSRF();
$hashKey = sanitizeInput($_POST['hashKey'] ?? '');
$apiEndpoint = sanitizeInput($_POST['apiEndpoint'] ?? DEFAULT_API_ENDPOINT);
// Validate inputs
if (!ApiClient::validateHashKey($hashKey)) {
echo json_encode(['success' => false, 'error' => 'Invalid hash key format']);
exit;
}
if (!ApiClient::validateApiEndpoint($apiEndpoint)) {
echo json_encode(['success' => false, 'error' => 'Invalid API endpoint format']);
exit;
}
// Start enrollment
$result = $enrollmentManager->enrollRouter($hashKey, $apiEndpoint);
echo json_encode($result);
exit;
}
}
// Get system status for initial page load
$systemStatus = $enrollmentManager->getSystemStatus();
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?php echo APP_NAME; ?> - Dashboard</title>
<link rel="stylesheet" href="../assets/css/style.css">
<link rel="icon" type="image/x-icon" href="../assets/images/favicon.ico">
</head>
<body>
<div class="dashboard-container">
<!-- Header -->
<header class="header">
<div class="header-content">
<h1 class="header-title"><?php echo APP_NAME; ?></h1>
<div class="header-actions">
<div class="user-info">
Welcome, <strong><?php echo htmlspecialchars($currentUser['username']); ?></strong>
<span class="text-xs">
| Logged in: <?php echo date('M j, Y g:i A', $currentUser['login_time']); ?>
</span>
</div>
<button id="refreshStatus" class="btn btn-secondary" title="Refresh System Status">
🔄 Refresh
</button>
<a href="index.php?action=logout" class="btn btn-secondary">
Logout
</a>
</div>
</div>
</header>
<!-- Main Content -->
<main class="main-content">
<!-- System Status Card -->
<div class="card">
<div class="card-header">
<h2 class="card-title">System Status</h2>
</div>
<div class="card-body">
<div class="status-grid">
<div class="status-item">
<div id="zitiStatusIcon" class="status-icon <?php echo $systemStatus['ziti_status'] === 'installed' ? 'success' : 'error'; ?>">
<?php echo $systemStatus['ziti_status'] === 'installed' ? '✓' : '✗'; ?>
</div>
<div class="status-content">
<h4>Ziti CLI Status</h4>
<p id="zitiStatus">
<?php
if ($systemStatus['ziti_status'] === 'installed') {
echo 'Installed (' . htmlspecialchars($systemStatus['ziti_version']) . ')';
} else {
echo 'Not Installed';
}
?>
</p>
</div>
</div>
<div class="status-item">
<div id="serviceStatusIcon" class="status-icon <?php echo $systemStatus['service_active'] ? 'success' : 'error'; ?>">
<?php echo $systemStatus['service_active'] ? '▶' : '⏹'; ?>
</div>
<div class="status-content">
<h4>Router Service</h4>
<p id="serviceStatus">
<?php echo $systemStatus['service_active'] ? 'Running' : 'Stopped'; ?>
</p>
</div>
</div>
<div class="status-item">
<div id="configStatusIcon" class="status-icon <?php
if ($systemStatus['config_exists'] && $systemStatus['certificates_exist']) {
echo 'success';
} elseif ($systemStatus['config_exists']) {
echo 'warning';
} else {
echo 'error';
}
?>">
<?php
if ($systemStatus['config_exists'] && $systemStatus['certificates_exist']) {
echo '⚙';
} elseif ($systemStatus['config_exists']) {
echo '⚠';
} else {
echo '✗';
}
?>
</div>
<div class="status-content">
<h4>Configuration</h4>
<p id="configStatus">
<?php
if ($systemStatus['config_exists'] && $systemStatus['certificates_exist']) {
echo 'Configured';
} elseif ($systemStatus['config_exists']) {
echo 'Partial';
} else {
echo 'Not Configured';
}
?>
</p>
</div>
</div>
<div class="status-item">
<div class="status-icon success">
🖥
</div>
<div class="status-content">
<h4>Hostname</h4>
<p id="hostname"><?php echo htmlspecialchars($systemStatus['hostname']); ?></p>
</div>
</div>
</div>
</div>
</div>
<!-- Enrollment Form Card -->
<div class="card">
<div class="card-header">
<h2 class="card-title">Router Enrollment</h2>
</div>
<div class="card-body">
<form id="enrollmentForm" class="enrollment-form">
<input type="hidden" name="csrf_token" value="<?php echo generateCSRFToken(); ?>">
<div class="form-row">
<div class="form-group">
<label for="apiEndpoint" class="form-label">API Endpoint</label>
<input
type="url"
id="apiEndpoint"
name="apiEndpoint"
class="form-input"
value="<?php echo DEFAULT_API_ENDPOINT; ?>"
placeholder="https://backend.zitinexus.com"
required
>
<small class="text-sm text-secondary">ZitiNexus Portal API endpoint</small>
</div>
<div class="form-group">
<label for="hashKey" class="form-label">Hash Key</label>
<input
type="text"
id="hashKey"
name="hashKey"
class="form-input"
placeholder="32-character hexadecimal hash key"
maxlength="32"
pattern="[a-fA-F0-9]{32}"
required
>
<small class="text-sm text-secondary">Router enrollment hash key from ZitiNexus Portal</small>
</div>
</div>
<div class="form-group-full">
<button type="submit" id="enrollBtn" class="btn btn-primary">
Start Enrollment
</button>
<button type="button" id="clearLogs" class="btn btn-secondary" style="margin-left: 1rem;">
Clear Logs
</button>
</div>
</form>
<!-- Progress Container -->
<div id="progressContainer" class="progress-container">
<div class="progress-bar">
<div id="progressFill" class="progress-fill"></div>
</div>
<div class="progress-steps">
<div class="progress-step">Initialize</div>
<div class="progress-step">Requirements</div>
<div class="progress-step">Install</div>
<div class="progress-step">Directories</div>
<div class="progress-step">Register</div>
<div class="progress-step">Configure</div>
<div class="progress-step">Enroll</div>
<div class="progress-step">Service</div>
<div class="progress-step">Start</div>
<div class="progress-step">Report</div>
<div class="progress-step">Complete</div>
</div>
<div id="logContainer" class="log-container">
<!-- Log entries will be added here dynamically -->
</div>
</div>
</div>
</div>
<!-- Information Card -->
<div class="card">
<div class="card-header">
<h2 class="card-title">Information</h2>
</div>
<div class="card-body">
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 2rem;">
<div>
<h4 class="font-medium mb-2">How to Use</h4>
<ol class="text-sm text-secondary" style="padding-left: 1.5rem;">
<li>Obtain a hash key from the ZitiNexus Portal by creating a router enrollment</li>
<li>Enter the API endpoint (default is pre-filled)</li>
<li>Paste the 32-character hash key</li>
<li>Click "Start Enrollment" to begin the process</li>
<li>Monitor the progress and logs for status updates</li>
</ol>
</div>
<div>
<h4 class="font-medium mb-2">System Requirements</h4>
<ul class="text-sm text-secondary" style="padding-left: 1.5rem;">
<li>Ubuntu 22.04 or 24.04 LTS</li>
<li>Root/sudo access required</li>
<li>Internet connectivity</li>
<li>systemctl available</li>
<li>curl and jq packages (auto-installed)</li>
</ul>
</div>
<div>
<h4 class="font-medium mb-2">File Locations</h4>
<ul class="text-sm text-secondary" style="padding-left: 1.5rem;">
<li><code>/etc/zitirouter/</code> - Configuration directory</li>
<li><code>/etc/zitirouter/certs/</code> - Certificates</li>
<li><code>/var/log/ziti-router-enrollment.log</code> - Enrollment log</li>
<li><code>/etc/systemd/system/ziti-router.service</code> - Service file</li>
</ul>
</div>
<div>
<h4 class="font-medium mb-2">Service Management</h4>
<ul class="text-sm text-secondary" style="padding-left: 1.5rem;">
<li><code>systemctl status ziti-router</code> - Check status</li>
<li><code>systemctl start ziti-router</code> - Start service</li>
<li><code>systemctl stop ziti-router</code> - Stop service</li>
<li><code>journalctl -u ziti-router -f</code> - View logs</li>
</ul>
</div>
</div>
</div>
</div>
</main>
</div>
<script src="../assets/js/app.js"></script>
</body>
</html>

135
UI/public/index.php Normal file
View File

@ -0,0 +1,135 @@
<?php
/**
* Login page for ZitiNexus Router Enrollment UI
*/
require_once '../includes/auth.php';
// Redirect if already authenticated
if (isAuthenticated() && isSessionValid()) {
header('Location: dashboard.php');
exit;
}
// Handle messages
$message = '';
$messageType = '';
if (isset($_GET['error'])) {
switch ($_GET['error']) {
case 'session_expired':
$message = 'Your session has expired. Please log in again.';
$messageType = 'warning';
break;
default:
$message = 'An error occurred. Please try again.';
$messageType = 'error';
break;
}
}
if (isset($_GET['message'])) {
switch ($_GET['message']) {
case 'logged_out':
$message = 'You have been logged out successfully.';
$messageType = 'info';
break;
}
}
if (isset($loginError)) {
$message = $loginError;
$messageType = 'error';
}
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?php echo APP_NAME; ?> - Login</title>
<link rel="stylesheet" href="../assets/css/style.css">
<link rel="icon" type="image/x-icon" href="../assets/images/favicon.ico">
</head>
<body>
<div class="login-container">
<div class="login-card">
<div class="login-header">
<h1><?php echo APP_NAME; ?></h1>
<p>Router Enrollment Management Interface</p>
<p class="text-sm text-secondary">Version <?php echo APP_VERSION; ?></p>
</div>
<?php if ($message): ?>
<div class="alert alert-<?php echo $messageType; ?>">
<?php echo htmlspecialchars($message); ?>
</div>
<?php endif; ?>
<form method="POST" action="index.php">
<input type="hidden" name="action" value="login">
<div class="form-group">
<label for="username" class="form-label">Username</label>
<input
type="text"
id="username"
name="username"
class="form-input"
required
autocomplete="username"
value="<?php echo isset($_POST['username']) ? htmlspecialchars($_POST['username']) : ''; ?>"
>
</div>
<div class="form-group">
<label for="password" class="form-label">Password</label>
<input
type="password"
id="password"
name="password"
class="form-input"
required
autocomplete="current-password"
>
</div>
<button type="submit" class="btn btn-primary btn-full">
Sign In
</button>
</form>
<div style="margin-top: 2rem; padding-top: 1rem; border-top: 1px solid var(--border-color); text-align: center;">
<p class="text-sm text-secondary">
Default credentials: <strong>admin</strong> / <strong>admin123</strong>
</p>
<p class="text-xs text-secondary" style="margin-top: 0.5rem;">
Please change the default password in production
</p>
</div>
</div>
</div>
<script>
// Auto-focus on username field
document.addEventListener('DOMContentLoaded', function() {
const usernameField = document.getElementById('username');
if (usernameField && !usernameField.value) {
usernameField.focus();
} else {
const passwordField = document.getElementById('password');
if (passwordField) {
passwordField.focus();
}
}
});
// Handle form submission
document.querySelector('form').addEventListener('submit', function(e) {
const submitBtn = this.querySelector('button[type="submit"]');
submitBtn.disabled = true;
submitBtn.innerHTML = '<span class="spinner"></span>Signing In...';
});
</script>
</body>
</html>