52
.htaccess
Archivo normal
52
.htaccess
Archivo normal
@@ -0,0 +1,52 @@
|
|||||||
|
# AleShell .htaccess Configuration
|
||||||
|
# This file helps with URL routing and security
|
||||||
|
|
||||||
|
# Enable URL rewriting
|
||||||
|
RewriteEngine On
|
||||||
|
|
||||||
|
# Security Headers
|
||||||
|
<IfModule mod_headers.c>
|
||||||
|
Header always set X-Content-Type-Options nosniff
|
||||||
|
Header always set X-Frame-Options DENY
|
||||||
|
Header always set X-XSS-Protection "1; mode=block"
|
||||||
|
Header always set Referrer-Policy "strict-origin-when-cross-origin"
|
||||||
|
</IfModule>
|
||||||
|
|
||||||
|
# Prevent access to sensitive files
|
||||||
|
<FilesMatch "\.(md|json|lock|yml|yaml|xml|log)$">
|
||||||
|
Require all denied
|
||||||
|
</FilesMatch>
|
||||||
|
|
||||||
|
# Protect src directory - Alternative method for .htaccess
|
||||||
|
RewriteRule ^src/ - [F,L]
|
||||||
|
|
||||||
|
# API and Auth routing
|
||||||
|
RewriteCond %{REQUEST_FILENAME} !-f
|
||||||
|
RewriteCond %{REQUEST_FILENAME} !-d
|
||||||
|
RewriteRule ^(api|auth)/(.*)$ index.php/$1/$2 [L,QSA]
|
||||||
|
|
||||||
|
# General routing for non-existent files
|
||||||
|
RewriteCond %{REQUEST_FILENAME} !-f
|
||||||
|
RewriteCond %{REQUEST_FILENAME} !-d
|
||||||
|
RewriteRule ^(.*)$ index.php/$1 [L,QSA]
|
||||||
|
|
||||||
|
# Disable server signature
|
||||||
|
ServerSignature Off
|
||||||
|
|
||||||
|
# Prevent directory browsing
|
||||||
|
Options -Indexes
|
||||||
|
|
||||||
|
# Cache static files
|
||||||
|
<IfModule mod_expires.c>
|
||||||
|
ExpiresActive On
|
||||||
|
ExpiresByType text/css "access plus 1 month"
|
||||||
|
ExpiresByType application/javascript "access plus 1 month"
|
||||||
|
ExpiresByType image/png "access plus 1 month"
|
||||||
|
ExpiresByType image/jpg "access plus 1 month"
|
||||||
|
ExpiresByType image/jpeg "access plus 1 month"
|
||||||
|
ExpiresByType image/gif "access plus 1 month"
|
||||||
|
ExpiresByType image/ico "access plus 1 month"
|
||||||
|
ExpiresByType image/icon "access plus 1 month"
|
||||||
|
ExpiresByType text/ico "access plus 1 month"
|
||||||
|
ExpiresByType image/x-icon "access plus 1 month"
|
||||||
|
</IfModule>
|
||||||
51
LICENSE
Archivo normal
51
LICENSE
Archivo normal
@@ -0,0 +1,51 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2024 AleShell Contributors
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Additional Terms for AleShell
|
||||||
|
|
||||||
|
### Intended Use
|
||||||
|
This software is intended for legitimate system administration and authorized
|
||||||
|
security testing purposes only. Users must ensure they have proper authorization
|
||||||
|
before using this software on any system they do not own.
|
||||||
|
|
||||||
|
### Responsibility
|
||||||
|
Users are solely responsible for ensuring their use of this software complies
|
||||||
|
with all applicable laws, regulations, and policies. The authors and contributors
|
||||||
|
of AleShell disclaim any responsibility for misuse of this software.
|
||||||
|
|
||||||
|
### Security Notice
|
||||||
|
This software provides administrative access to systems. Users must:
|
||||||
|
- Secure the installation with strong passwords
|
||||||
|
- Use HTTPS in production environments
|
||||||
|
- Restrict access to authorized personnel only
|
||||||
|
- Monitor and log access appropriately
|
||||||
|
- Remove the software when no longer needed
|
||||||
|
|
||||||
|
### Acknowledgments
|
||||||
|
This project is inspired by and builds upon the work of the b374k project and
|
||||||
|
other web shell implementations. We acknowledge the contributions of all
|
||||||
|
developers in the security tools community.
|
||||||
|
|
||||||
|
### Disclaimer
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND. USE AT YOUR OWN RISK.
|
||||||
282
README.md
Archivo normal
282
README.md
Archivo normal
@@ -0,0 +1,282 @@
|
|||||||
|
# 🚀 AleShell - Modern PHP Web Shell
|
||||||
|
|
||||||
|
AleShell is a powerful, secure, and modern web shell built with PHP. It's a complete rewrite and modernization of the b374k project, featuring a beautiful responsive interface, enhanced security, and modern development practices.
|
||||||
|
|
||||||
|
## ✨ Features
|
||||||
|
|
||||||
|
### 🔐 Security
|
||||||
|
- **Advanced Authentication** with password hashing and session management
|
||||||
|
- **CSRF Protection** for all state-changing operations
|
||||||
|
- **Rate Limiting** to prevent brute force attacks
|
||||||
|
- **IP Whitelisting/Blacklisting** for access control
|
||||||
|
- **Path Traversal Protection** to prevent unauthorized file access
|
||||||
|
- **Command Filtering** to block dangerous system commands
|
||||||
|
- **Session Security** with secure cookies and session regeneration
|
||||||
|
|
||||||
|
### 🎨 Modern Interface
|
||||||
|
- **Responsive Design** that works on desktop, tablet, and mobile
|
||||||
|
- **Dark/Light Theme** toggle with system preference detection
|
||||||
|
- **Keyboard Shortcuts** for power users
|
||||||
|
- **Real-time Updates** for system information
|
||||||
|
- **Smooth Animations** and transitions
|
||||||
|
- **Modular Architecture** with lazy-loaded components
|
||||||
|
|
||||||
|
### 📁 File Manager
|
||||||
|
- **Complete File Operations** (create, read, update, delete, copy, move)
|
||||||
|
- **Drag & Drop Upload** with progress indicators
|
||||||
|
- **Syntax Highlighting** for code files
|
||||||
|
- **File Permissions** management
|
||||||
|
- **Archive Support** (zip, tar, tar.gz)
|
||||||
|
- **Large File Handling** with streaming
|
||||||
|
- **File Search** and filtering capabilities
|
||||||
|
|
||||||
|
### 💻 Terminal
|
||||||
|
- **Interactive Terminal** with command history
|
||||||
|
- **Built-in Commands** (cd, pwd, help, etc.)
|
||||||
|
- **Command Auto-completion**
|
||||||
|
- **Output Streaming** for long-running commands
|
||||||
|
- **Multiple Terminal Tabs**
|
||||||
|
- **Configurable Timeout** for command execution
|
||||||
|
|
||||||
|
### 📝 Code Editor
|
||||||
|
- **Syntax Highlighting** for multiple languages
|
||||||
|
- **Line Numbers** and code folding
|
||||||
|
- **Find & Replace** functionality
|
||||||
|
- **Auto-indentation** and code formatting
|
||||||
|
- **Multiple Editor Themes**
|
||||||
|
- **File Type Detection**
|
||||||
|
|
||||||
|
### ⚡ System Monitoring
|
||||||
|
- **Real-time System Stats** (CPU, Memory, Disk, Network)
|
||||||
|
- **Process Manager** with kill capabilities
|
||||||
|
- **Network Tools** (ping, traceroute, port scan)
|
||||||
|
- **System Information** display
|
||||||
|
- **Load Average** monitoring
|
||||||
|
|
||||||
|
### 🗄️ Database Tools
|
||||||
|
- **Multi-Database Support** (MySQL, PostgreSQL, SQLite)
|
||||||
|
- **SQL Query Execution** with result formatting
|
||||||
|
- **Database Browser** with table structure
|
||||||
|
- **Export/Import** capabilities
|
||||||
|
- **Connection Management**
|
||||||
|
|
||||||
|
## 🚀 Installation
|
||||||
|
|
||||||
|
### Requirements
|
||||||
|
- PHP 7.4 or higher
|
||||||
|
- Web server (Apache, Nginx, etc.)
|
||||||
|
- PHP extensions: json, mbstring, openssl
|
||||||
|
|
||||||
|
### Quick Install
|
||||||
|
1. Download the latest release
|
||||||
|
2. Extract to your web directory
|
||||||
|
3. Access via web browser
|
||||||
|
4. Default password: `aleshell`
|
||||||
|
|
||||||
|
### From Source
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/yourusername/aleshell.git
|
||||||
|
cd aleshell
|
||||||
|
# Upload to your web server
|
||||||
|
```
|
||||||
|
|
||||||
|
### 📦 Packed Version (Recommended)
|
||||||
|
For easy deployment, use the **AleShell Packer** to generate a single encrypted PHP file:
|
||||||
|
|
||||||
|
#### Web Interface
|
||||||
|
1. Access `pack.php` in your browser
|
||||||
|
2. Configure options (password, modules, compression)
|
||||||
|
3. Click "Generate AleShell Packed"
|
||||||
|
4. Upload the generated file to any PHP server
|
||||||
|
|
||||||
|
#### Command Line
|
||||||
|
```bash
|
||||||
|
# Basic packed version
|
||||||
|
php pack.php -o shell.php -p your_password --encrypt
|
||||||
|
|
||||||
|
# Advanced packed version
|
||||||
|
php pack.php -o advanced.php -p secure_pass --encrypt --minify --obfuscate
|
||||||
|
```
|
||||||
|
|
||||||
|
**Benefits of Packed Version:**
|
||||||
|
- ✅ Single file deployment
|
||||||
|
- ✅ Encrypted and compressed
|
||||||
|
- ✅ No external dependencies
|
||||||
|
- ✅ Customizable features
|
||||||
|
- ✅ Built-in security options
|
||||||
|
|
||||||
|
## ⚙️ Configuration
|
||||||
|
|
||||||
|
AleShell can be configured by creating a `src/config/config.php` file:
|
||||||
|
|
||||||
|
```php
|
||||||
|
<?php
|
||||||
|
return [
|
||||||
|
'security' => [
|
||||||
|
'password' => password_hash('your_secure_password', PASSWORD_DEFAULT),
|
||||||
|
'session_timeout' => 3600, // 1 hour
|
||||||
|
'allowed_ips' => [], // Empty = allow all
|
||||||
|
'max_attempts' => 5,
|
||||||
|
'lockout_time' => 300 // 5 minutes
|
||||||
|
],
|
||||||
|
'features' => [
|
||||||
|
'file_manager' => true,
|
||||||
|
'terminal' => true,
|
||||||
|
'code_editor' => true,
|
||||||
|
'process_manager' => true,
|
||||||
|
'network_tools' => true,
|
||||||
|
'database_tools' => true
|
||||||
|
],
|
||||||
|
'ui' => [
|
||||||
|
'theme' => 'dark', // 'dark' or 'light'
|
||||||
|
'language' => 'en',
|
||||||
|
'items_per_page' => 50
|
||||||
|
],
|
||||||
|
'limits' => [
|
||||||
|
'max_file_size' => 50 * 1024 * 1024, // 50MB
|
||||||
|
'max_upload_size' => 100 * 1024 * 1024, // 100MB
|
||||||
|
'command_timeout' => 30
|
||||||
|
]
|
||||||
|
];
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔒 Security Considerations
|
||||||
|
|
||||||
|
AleShell is designed with security in mind, but you should still follow best practices:
|
||||||
|
|
||||||
|
1. **Change the default password** immediately after installation
|
||||||
|
2. **Use HTTPS** in production environments
|
||||||
|
3. **Restrict access** using IP whitelisting when possible
|
||||||
|
4. **Monitor access logs** for suspicious activity
|
||||||
|
5. **Keep PHP updated** to the latest stable version
|
||||||
|
6. **Remove from production** when not needed
|
||||||
|
|
||||||
|
## 🌐 Browser Support
|
||||||
|
|
||||||
|
AleShell supports all modern browsers:
|
||||||
|
- Chrome 60+
|
||||||
|
- Firefox 55+
|
||||||
|
- Safari 12+
|
||||||
|
- Edge 79+
|
||||||
|
- Opera 47+
|
||||||
|
|
||||||
|
## 📱 Mobile Support
|
||||||
|
|
||||||
|
The interface is fully responsive and optimized for mobile devices with:
|
||||||
|
- Touch-friendly controls
|
||||||
|
- Responsive navigation
|
||||||
|
- Optimized layouts
|
||||||
|
- Gesture support
|
||||||
|
|
||||||
|
## 🎯 Keyboard Shortcuts
|
||||||
|
|
||||||
|
- `Ctrl+1` - Dashboard
|
||||||
|
- `Ctrl+2` - File Manager
|
||||||
|
- `Ctrl+3` - Terminal
|
||||||
|
- `Ctrl+4` - Code Editor
|
||||||
|
- `Ctrl+L` - Clear terminal
|
||||||
|
- `Ctrl+S` - Save file (in editor)
|
||||||
|
|
||||||
|
## 🔧 Development
|
||||||
|
|
||||||
|
### Architecture
|
||||||
|
|
||||||
|
AleShell follows modern PHP development practices:
|
||||||
|
|
||||||
|
- **PSR-4 Autoloading** for clean class organization
|
||||||
|
- **MVC Pattern** with controllers and views
|
||||||
|
- **Modular Design** for easy extensibility
|
||||||
|
- **RESTful API** for all operations
|
||||||
|
- **Security-first** approach
|
||||||
|
|
||||||
|
### File Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
aleshell/
|
||||||
|
├── index.php # Entry point
|
||||||
|
├── src/ # Source code
|
||||||
|
│ ├── core/ # Core framework classes
|
||||||
|
│ ├── controllers/ # Request handlers
|
||||||
|
│ ├── security/ # Security components
|
||||||
|
│ ├── modules/ # Feature modules
|
||||||
|
│ ├── themes/ # UI themes
|
||||||
|
│ ├── config/ # Configuration
|
||||||
|
│ └── utils/ # Utility classes
|
||||||
|
├── uploads/ # File uploads (create if needed)
|
||||||
|
├── logs/ # Application logs
|
||||||
|
└── README.md
|
||||||
|
```
|
||||||
|
|
||||||
|
### Adding Modules
|
||||||
|
|
||||||
|
Create a new module by:
|
||||||
|
|
||||||
|
1. Creating a directory in `src/modules/`
|
||||||
|
2. Adding a `module.json` configuration file
|
||||||
|
3. Implementing the module class
|
||||||
|
4. Registering routes if needed
|
||||||
|
|
||||||
|
Example module structure:
|
||||||
|
```
|
||||||
|
src/modules/mymodule/
|
||||||
|
├── module.json
|
||||||
|
├── MyModule.php
|
||||||
|
├── assets/
|
||||||
|
│ ├── style.css
|
||||||
|
│ └── script.js
|
||||||
|
└── templates/
|
||||||
|
└── index.html
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🐛 Troubleshooting
|
||||||
|
|
||||||
|
### Common Issues
|
||||||
|
|
||||||
|
1. **Permission Errors**
|
||||||
|
- Ensure PHP has read/write permissions
|
||||||
|
- Check file ownership and permissions
|
||||||
|
|
||||||
|
2. **Session Issues**
|
||||||
|
- Verify session directory is writable
|
||||||
|
- Check PHP session configuration
|
||||||
|
|
||||||
|
3. **Command Execution Fails**
|
||||||
|
- Verify exec functions are enabled
|
||||||
|
- Check system PATH configuration
|
||||||
|
|
||||||
|
### Debugging
|
||||||
|
|
||||||
|
Enable debug mode in configuration:
|
||||||
|
```php
|
||||||
|
'app' => [
|
||||||
|
'debug' => true
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📄 License
|
||||||
|
|
||||||
|
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
||||||
|
|
||||||
|
## 🤝 Contributing
|
||||||
|
|
||||||
|
Contributions are welcome! Please feel free to submit a Pull Request.
|
||||||
|
|
||||||
|
1. Fork the project
|
||||||
|
2. Create your feature branch (`git checkout -b feature/AmazingFeature`)
|
||||||
|
3. Commit your changes (`git commit -m 'Add some AmazingFeature'`)
|
||||||
|
4. Push to the branch (`git push origin feature/AmazingFeature`)
|
||||||
|
5. Open a Pull Request
|
||||||
|
|
||||||
|
## 🙏 Acknowledgments
|
||||||
|
|
||||||
|
- Based on the original [b374k](https://github.com/b374k/b374k) project
|
||||||
|
- Inspired by modern web development practices
|
||||||
|
- Thanks to all contributors and testers
|
||||||
|
|
||||||
|
## ⚠️ Disclaimer
|
||||||
|
|
||||||
|
This tool is intended for legitimate system administration purposes only. Users are responsible for ensuring compliance with applicable laws and regulations. The authors are not responsible for any misuse of this software.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**AleShell v2.0.0** - Built with ❤️ for system administrators
|
||||||
86
index.php
Archivo normal
86
index.php
Archivo normal
@@ -0,0 +1,86 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* AleShell - A Modern PHP Web Shell
|
||||||
|
*
|
||||||
|
* A powerful, secure, and modern web shell based on b374k but completely rewritten
|
||||||
|
* with modern architecture, improved security, and enhanced features.
|
||||||
|
*
|
||||||
|
* @author Ale
|
||||||
|
* @version 2.0.0
|
||||||
|
* @license MIT
|
||||||
|
* @package AleShell
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Prevent direct browser access for security
|
||||||
|
if (!isset($_SERVER['HTTP_USER_AGENT']) || empty($_SERVER['HTTP_USER_AGENT'])) {
|
||||||
|
http_response_code(403);
|
||||||
|
die('Access Denied');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Basic security headers
|
||||||
|
header('X-Content-Type-Options: nosniff');
|
||||||
|
header('X-Frame-Options: DENY');
|
||||||
|
header('X-XSS-Protection: 1; mode=block');
|
||||||
|
header('Referrer-Policy: strict-origin-when-cross-origin');
|
||||||
|
|
||||||
|
// Define version and basic constants
|
||||||
|
define('ALESHELL_VERSION', '2.0.0');
|
||||||
|
define('ALESHELL_ROOT', __DIR__);
|
||||||
|
define('ALESHELL_SRC', ALESHELL_ROOT . '/src');
|
||||||
|
define('ALESHELL_CORE', ALESHELL_SRC . '/core');
|
||||||
|
|
||||||
|
// Error reporting
|
||||||
|
error_reporting(E_ALL & ~E_NOTICE);
|
||||||
|
ini_set('display_errors', 1);
|
||||||
|
ini_set('log_errors', 1);
|
||||||
|
|
||||||
|
// Manual class loading for critical files
|
||||||
|
function loadAleShellClass($className) {
|
||||||
|
// Remove namespace prefix
|
||||||
|
$className = str_replace('AleShell\\', '', $className);
|
||||||
|
|
||||||
|
// Convert namespace to path
|
||||||
|
$classFile = str_replace('\\', '/', $className) . '.php';
|
||||||
|
$fullPath = ALESHELL_SRC . '/' . $classFile;
|
||||||
|
|
||||||
|
if (file_exists($fullPath)) {
|
||||||
|
require_once $fullPath;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register autoloader
|
||||||
|
spl_autoload_register('loadAleShellClass');
|
||||||
|
|
||||||
|
// Include the autoloader and bootstrap
|
||||||
|
require_once ALESHELL_CORE . '/Autoloader.php';
|
||||||
|
|
||||||
|
// Initialize AleShell
|
||||||
|
try {
|
||||||
|
// Ensure bootstrap class is loaded
|
||||||
|
if (!class_exists('AleShell\\Core\\Bootstrap')) {
|
||||||
|
require_once ALESHELL_CORE . '/Bootstrap.php';
|
||||||
|
}
|
||||||
|
|
||||||
|
$aleShell = new AleShell\Core\Bootstrap();
|
||||||
|
$aleShell->initialize();
|
||||||
|
$aleShell->run();
|
||||||
|
} catch (Exception $e) {
|
||||||
|
// Log error and show user-friendly message
|
||||||
|
error_log('AleShell Error: ' . $e->getMessage());
|
||||||
|
http_response_code(500);
|
||||||
|
|
||||||
|
if (defined('ALESHELL_DEBUG') && ALESHELL_DEBUG) {
|
||||||
|
echo '<h1>AleShell Error</h1>';
|
||||||
|
echo '<p><strong>Message:</strong> ' . htmlspecialchars($e->getMessage()) . '</p>';
|
||||||
|
echo '<p><strong>File:</strong> ' . htmlspecialchars($e->getFile()) . '</p>';
|
||||||
|
echo '<p><strong>Line:</strong> ' . $e->getLine() . '</p>';
|
||||||
|
echo '<pre>' . htmlspecialchars($e->getTraceAsString()) . '</pre>';
|
||||||
|
} else {
|
||||||
|
echo '<h1>Service Temporarily Unavailable</h1>';
|
||||||
|
echo '<p>Please try again later.</p>';
|
||||||
|
echo '<p><small>Error: ' . htmlspecialchars($e->getMessage()) . '</small></p>';
|
||||||
|
}
|
||||||
|
}
|
||||||
3
src/.htaccess
Archivo normal
3
src/.htaccess
Archivo normal
@@ -0,0 +1,3 @@
|
|||||||
|
# Protect AleShell source directory
|
||||||
|
# Deny access to all files in src/
|
||||||
|
Require all denied
|
||||||
59
src/config/config.php
Archivo normal
59
src/config/config.php
Archivo normal
@@ -0,0 +1,59 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Basic Configuration for AleShell
|
||||||
|
*
|
||||||
|
* Minimal config to get AleShell running
|
||||||
|
*/
|
||||||
|
|
||||||
|
return [
|
||||||
|
'app' => [
|
||||||
|
'name' => 'AleShell',
|
||||||
|
'version' => '2.0.0',
|
||||||
|
'debug' => true, // Enable for troubleshooting
|
||||||
|
'timezone' => 'UTC'
|
||||||
|
],
|
||||||
|
|
||||||
|
'security' => [
|
||||||
|
// Default password: "password"
|
||||||
|
'password' => '$2y$12$IyAHk858LpyxJaqMOgHiJOHdR.GWbAoGVYUpQh1Ec1ogTGNxyRWRe',
|
||||||
|
'session_timeout' => 3600,
|
||||||
|
'csrf_protection' => true,
|
||||||
|
'rate_limiting' => false, // Disabled for testing
|
||||||
|
'max_attempts' => 5,
|
||||||
|
'lockout_time' => 300,
|
||||||
|
'allowed_ips' => [],
|
||||||
|
'blocked_ips' => []
|
||||||
|
],
|
||||||
|
|
||||||
|
'features' => [
|
||||||
|
'file_manager' => true,
|
||||||
|
'terminal' => true,
|
||||||
|
'code_editor' => true,
|
||||||
|
'process_manager' => true,
|
||||||
|
'network_tools' => true,
|
||||||
|
'database_tools' => true,
|
||||||
|
'system_info' => true,
|
||||||
|
'log_viewer' => true
|
||||||
|
],
|
||||||
|
|
||||||
|
'ui' => [
|
||||||
|
'theme' => 'dark',
|
||||||
|
'language' => 'en',
|
||||||
|
'items_per_page' => 50,
|
||||||
|
'auto_refresh' => false,
|
||||||
|
'show_hidden_files' => false
|
||||||
|
],
|
||||||
|
|
||||||
|
'limits' => [
|
||||||
|
'max_file_size' => 50 * 1024 * 1024, // 50MB
|
||||||
|
'max_upload_size' => 100 * 1024 * 1024, // 100MB
|
||||||
|
'max_output_lines' => 1000,
|
||||||
|
'command_timeout' => 30
|
||||||
|
],
|
||||||
|
|
||||||
|
'paths' => [
|
||||||
|
'temp_dir' => sys_get_temp_dir(),
|
||||||
|
'upload_dir' => dirname(__DIR__) . '/uploads',
|
||||||
|
'log_dir' => dirname(__DIR__) . '/logs'
|
||||||
|
]
|
||||||
|
];
|
||||||
60
src/controllers/AuthController.php
Archivo normal
60
src/controllers/AuthController.php
Archivo normal
@@ -0,0 +1,60 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Authentication Controller
|
||||||
|
*
|
||||||
|
* Handles user authentication
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace AleShell\Controllers;
|
||||||
|
|
||||||
|
use AleShell\Controllers\BaseController;
|
||||||
|
|
||||||
|
class AuthController extends BaseController
|
||||||
|
{
|
||||||
|
public function login(): void
|
||||||
|
{
|
||||||
|
$data = $this->getRequestData();
|
||||||
|
$password = $data['password'] ?? '';
|
||||||
|
|
||||||
|
if (empty($password)) {
|
||||||
|
$this->errorResponse('Password is required');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if ($this->security->authenticate($password)) {
|
||||||
|
$this->successResponse([
|
||||||
|
'authenticated' => true,
|
||||||
|
'csrf_token' => $this->security->generateCSRFToken(),
|
||||||
|
'redirect' => '/dashboard'
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
$this->errorResponse('Invalid password', 401);
|
||||||
|
}
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$this->errorResponse($e->getMessage(), 401);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function logout(): void
|
||||||
|
{
|
||||||
|
$this->security->logout();
|
||||||
|
$this->successResponse(['logged_out' => true]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function status(): void
|
||||||
|
{
|
||||||
|
// Use the working direct output but with proper API structure
|
||||||
|
$token = $this->security->generateCSRFToken();
|
||||||
|
$data = [
|
||||||
|
'authenticated' => $this->security->isAuthenticated(),
|
||||||
|
'csrf_token' => $token
|
||||||
|
];
|
||||||
|
|
||||||
|
$response = ['success' => true, 'data' => $data];
|
||||||
|
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
echo json_encode($response);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
}
|
||||||
121
src/controllers/BaseController.php
Archivo normal
121
src/controllers/BaseController.php
Archivo normal
@@ -0,0 +1,121 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Base Controller
|
||||||
|
*
|
||||||
|
* Base class for all controllers
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace AleShell\Controllers;
|
||||||
|
|
||||||
|
use AleShell\Core\ConfigManager;
|
||||||
|
use AleShell\Security\SecurityManager;
|
||||||
|
|
||||||
|
abstract class BaseController
|
||||||
|
{
|
||||||
|
protected ConfigManager $config;
|
||||||
|
protected SecurityManager $security;
|
||||||
|
|
||||||
|
public function __construct(ConfigManager $config)
|
||||||
|
{
|
||||||
|
$this->config = $config;
|
||||||
|
$this->security = new SecurityManager($config);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function jsonResponse($data, int $status = 200): void
|
||||||
|
{
|
||||||
|
// Aggressive output buffering cleanup
|
||||||
|
while (ob_get_level()) {
|
||||||
|
ob_end_clean();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure no buffering
|
||||||
|
ini_set('output_buffering', '0');
|
||||||
|
ini_set('implicit_flush', '1');
|
||||||
|
|
||||||
|
$json = json_encode($data);
|
||||||
|
|
||||||
|
if (!headers_sent()) {
|
||||||
|
http_response_code($status);
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
header('Content-Length: ' . strlen($json));
|
||||||
|
header('Cache-Control: no-cache, no-store, must-revalidate');
|
||||||
|
// Disable error display for API responses to prevent JSON corruption
|
||||||
|
ini_set('display_errors', 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Direct output
|
||||||
|
echo $json;
|
||||||
|
flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function errorResponse(string $message, int $status = 400): void
|
||||||
|
{
|
||||||
|
$this->jsonResponse(['error' => $message], $status);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function successResponse($data = null): void
|
||||||
|
{
|
||||||
|
$response = ['success' => true];
|
||||||
|
if ($data !== null) {
|
||||||
|
$response['data'] = $data;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use direct output approach that works
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
echo json_encode($response);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function validateCSRF(?array $requestData = null): bool
|
||||||
|
{
|
||||||
|
if (!$this->config->get('security.csrf_protection', true)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
$token = '';
|
||||||
|
|
||||||
|
// Check in request data (for JSON requests)
|
||||||
|
if ($requestData !== null) {
|
||||||
|
$token = $requestData['csrf_token'] ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check in POST data (form submissions)
|
||||||
|
if (empty($token)) {
|
||||||
|
$token = $_POST['csrf_token'] ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check in HTTP header
|
||||||
|
if (empty($token)) {
|
||||||
|
$token = $_SERVER['HTTP_X_CSRF_TOKEN'] ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->security->validateCSRFToken($token);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function requireAuth(): void
|
||||||
|
{
|
||||||
|
if (!$this->security->isAuthenticated()) {
|
||||||
|
$this->errorResponse('Authentication required', 401);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function sanitizeInput($input)
|
||||||
|
{
|
||||||
|
return $this->security->sanitizeInput($input);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getRequestData(): array
|
||||||
|
{
|
||||||
|
$contentType = $_SERVER['CONTENT_TYPE'] ?? '';
|
||||||
|
|
||||||
|
if (strpos($contentType, 'application/json') !== false) {
|
||||||
|
$json = file_get_contents('php://input');
|
||||||
|
$data = json_decode($json, true) ?? [];
|
||||||
|
} else {
|
||||||
|
$data = $_POST;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->sanitizeInput($data);
|
||||||
|
}
|
||||||
|
}
|
||||||
382
src/controllers/DatabaseController.php
Archivo normal
382
src/controllers/DatabaseController.php
Archivo normal
@@ -0,0 +1,382 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Database Controller
|
||||||
|
*
|
||||||
|
* Handles database operations and connections
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace AleShell\Controllers;
|
||||||
|
|
||||||
|
use AleShell\Controllers\BaseController;
|
||||||
|
use AleShell\Core\ConfigManager;
|
||||||
|
|
||||||
|
class DatabaseController extends BaseController
|
||||||
|
{
|
||||||
|
private $connections = [];
|
||||||
|
|
||||||
|
public function __construct(ConfigManager $config)
|
||||||
|
{
|
||||||
|
parent::__construct($config);
|
||||||
|
$this->restoreConnections();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function restoreConnections(): void
|
||||||
|
{
|
||||||
|
if (!isset($_SESSION['db_connections'])) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($_SESSION['db_connections'] as $connectionId => $connData) {
|
||||||
|
try {
|
||||||
|
// Recreate the connection
|
||||||
|
$connection = $this->createConnection(
|
||||||
|
$connData['type'],
|
||||||
|
$connData['host'] ?? 'localhost',
|
||||||
|
$connData['port'] ?? '',
|
||||||
|
$connData['database'],
|
||||||
|
$connData['username'] ?? '',
|
||||||
|
$connData['password'] ?? ''
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->connections[$connectionId] = [
|
||||||
|
'connection' => $connection,
|
||||||
|
'type' => $connData['type'],
|
||||||
|
'host' => $connData['host'] ?? 'localhost',
|
||||||
|
'database' => $connData['database'],
|
||||||
|
'connected_at' => $connData['connected_at']
|
||||||
|
];
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
// Remove invalid connection from session
|
||||||
|
unset($_SESSION['db_connections'][$connectionId]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function connect(): void
|
||||||
|
{
|
||||||
|
$this->requireAuth();
|
||||||
|
|
||||||
|
$data = $this->getRequestData();
|
||||||
|
$type = $data['type'] ?? '';
|
||||||
|
$host = $data['host'] ?? 'localhost';
|
||||||
|
$port = $data['port'] ?? '';
|
||||||
|
$database = $data['database'] ?? '';
|
||||||
|
$username = $data['username'] ?? '';
|
||||||
|
$password = $data['password'] ?? '';
|
||||||
|
|
||||||
|
if (empty($type) || empty($database)) {
|
||||||
|
$this->errorResponse('Database type and name are required');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$connection = $this->createConnection($type, $host, $port, $database, $username, $password);
|
||||||
|
$connectionId = uniqid('db_');
|
||||||
|
|
||||||
|
$this->connections[$connectionId] = [
|
||||||
|
'connection' => $connection,
|
||||||
|
'type' => $type,
|
||||||
|
'host' => $host,
|
||||||
|
'database' => $database,
|
||||||
|
'connected_at' => time()
|
||||||
|
];
|
||||||
|
|
||||||
|
// Store in session for persistence
|
||||||
|
$_SESSION['db_connections'][$connectionId] = [
|
||||||
|
'type' => $type,
|
||||||
|
'host' => $host,
|
||||||
|
'port' => $port,
|
||||||
|
'database' => $database,
|
||||||
|
'username' => $username,
|
||||||
|
'password' => $password,
|
||||||
|
'connected_at' => time()
|
||||||
|
];
|
||||||
|
|
||||||
|
$this->successResponse([
|
||||||
|
'connection_id' => $connectionId,
|
||||||
|
'type' => $type,
|
||||||
|
'database' => $database,
|
||||||
|
'host' => $host
|
||||||
|
]);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$this->errorResponse('Database connection failed: ' . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function disconnect(): void
|
||||||
|
{
|
||||||
|
$this->requireAuth();
|
||||||
|
|
||||||
|
$data = $this->getRequestData();
|
||||||
|
$connectionId = $data['connection_id'] ?? null;
|
||||||
|
|
||||||
|
if ($connectionId === "current" || $connectionId === null) {
|
||||||
|
// Disconnect all connections
|
||||||
|
$disconnected = 0;
|
||||||
|
foreach ($this->connections as $id => $conn) {
|
||||||
|
try {
|
||||||
|
$this->closeConnection($id);
|
||||||
|
if (isset($_SESSION['db_connections'][$id])) {
|
||||||
|
unset($_SESSION['db_connections'][$id]);
|
||||||
|
}
|
||||||
|
$disconnected++;
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
// Continue with other connections
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$this->successResponse(['disconnected' => $disconnected, 'message' => 'All connections closed']);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isset($this->connections[$connectionId])) {
|
||||||
|
$this->errorResponse('Invalid connection ID');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$this->closeConnection($connectionId);
|
||||||
|
unset($_SESSION['db_connections'][$connectionId]);
|
||||||
|
|
||||||
|
$this->successResponse(['disconnected' => true]);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$this->errorResponse('Failed to disconnect: ' . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getDatabases(): void
|
||||||
|
{
|
||||||
|
$this->requireAuth();
|
||||||
|
|
||||||
|
$connectionId = $_GET['connection_id'] ?? '';
|
||||||
|
|
||||||
|
if (empty($connectionId) || !isset($this->connections[$connectionId])) {
|
||||||
|
$this->errorResponse('Invalid connection ID');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$conn = $this->connections[$connectionId];
|
||||||
|
$databases = $this->listDatabases($conn);
|
||||||
|
|
||||||
|
$this->successResponse([
|
||||||
|
'databases' => $databases,
|
||||||
|
'total' => count($databases)
|
||||||
|
]);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$this->errorResponse('Failed to get databases: ' . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getTables(): void
|
||||||
|
{
|
||||||
|
$this->requireAuth();
|
||||||
|
|
||||||
|
$connectionId = $_GET['connection_id'] ?? '';
|
||||||
|
$database = $_GET['database'] ?? '';
|
||||||
|
|
||||||
|
if (empty($connectionId) || !isset($this->connections[$connectionId])) {
|
||||||
|
$this->errorResponse('Invalid connection ID');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$conn = $this->connections[$connectionId];
|
||||||
|
$tables = $this->listTables($conn, $database);
|
||||||
|
|
||||||
|
$this->successResponse([
|
||||||
|
'tables' => $tables,
|
||||||
|
'database' => $database,
|
||||||
|
'total' => count($tables)
|
||||||
|
]);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$this->errorResponse('Failed to get tables: ' . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function executeQuery(): void
|
||||||
|
{
|
||||||
|
$this->requireAuth();
|
||||||
|
|
||||||
|
$data = $this->getRequestData();
|
||||||
|
$connectionId = $data['connection_id'] ?? '';
|
||||||
|
$query = trim($data['query'] ?? '');
|
||||||
|
$database = $data['database'] ?? '';
|
||||||
|
|
||||||
|
if (empty($connectionId) || !isset($this->connections[$connectionId])) {
|
||||||
|
$this->errorResponse('Invalid connection ID');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($query)) {
|
||||||
|
$this->errorResponse('Query is required');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$conn = $this->connections[$connectionId];
|
||||||
|
$result = $this->executeDatabaseQuery($conn, $query, $database);
|
||||||
|
|
||||||
|
$this->successResponse($result);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$this->errorResponse('Query execution failed: ' . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getConnections(): void
|
||||||
|
{
|
||||||
|
$this->requireAuth();
|
||||||
|
|
||||||
|
$connections = [];
|
||||||
|
foreach ($this->connections as $id => $conn) {
|
||||||
|
$connections[] = [
|
||||||
|
'id' => $id,
|
||||||
|
'type' => $conn['type'],
|
||||||
|
'host' => $conn['host'],
|
||||||
|
'database' => $conn['database'],
|
||||||
|
'connected_at' => $conn['connected_at']
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->successResponse([
|
||||||
|
'connections' => $connections,
|
||||||
|
'total' => count($connections)
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function createConnection(string $type, string $host, string $port, string $database, string $username, string $password)
|
||||||
|
{
|
||||||
|
switch (strtolower($type)) {
|
||||||
|
case 'mysql':
|
||||||
|
case 'mariadb':
|
||||||
|
$port = $port ?: '3306';
|
||||||
|
$dsn = "mysql:host=$host;port=$port;dbname=$database;charset=utf8mb4";
|
||||||
|
return new \PDO($dsn, $username, $password, [
|
||||||
|
\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION,
|
||||||
|
\PDO::ATTR_DEFAULT_FETCH_MODE => \PDO::FETCH_ASSOC
|
||||||
|
]);
|
||||||
|
|
||||||
|
case 'postgresql':
|
||||||
|
$port = $port ?: '5432';
|
||||||
|
$dsn = "pgsql:host=$host;port=$port;dbname=$database";
|
||||||
|
return new \PDO($dsn, $username, $password, [
|
||||||
|
\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION,
|
||||||
|
\PDO::ATTR_DEFAULT_FETCH_MODE => \PDO::FETCH_ASSOC
|
||||||
|
]);
|
||||||
|
|
||||||
|
case 'sqlite':
|
||||||
|
$dsn = "sqlite:$database";
|
||||||
|
return new \PDO($dsn, null, null, [
|
||||||
|
\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION,
|
||||||
|
\PDO::ATTR_DEFAULT_FETCH_MODE => \PDO::FETCH_ASSOC
|
||||||
|
]);
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new \Exception("Unsupported database type: $type");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function closeConnection(string $connectionId): void
|
||||||
|
{
|
||||||
|
if (isset($this->connections[$connectionId])) {
|
||||||
|
$this->connections[$connectionId]['connection'] = null;
|
||||||
|
unset($this->connections[$connectionId]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function listDatabases($conn): array
|
||||||
|
{
|
||||||
|
$type = $conn['type'];
|
||||||
|
|
||||||
|
switch (strtolower($type)) {
|
||||||
|
case 'mysql':
|
||||||
|
case 'mariadb':
|
||||||
|
$stmt = $conn['connection']->query("SHOW DATABASES");
|
||||||
|
return $stmt->fetchAll(\PDO::FETCH_COLUMN);
|
||||||
|
|
||||||
|
case 'postgresql':
|
||||||
|
$stmt = $conn['connection']->query("SELECT datname FROM pg_database WHERE datistemplate = false");
|
||||||
|
return $stmt->fetchAll(\PDO::FETCH_COLUMN);
|
||||||
|
|
||||||
|
case 'sqlite':
|
||||||
|
return ['main']; // SQLite has only one database
|
||||||
|
|
||||||
|
default:
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function listTables($conn, string $database): array
|
||||||
|
{
|
||||||
|
$type = $conn['type'];
|
||||||
|
|
||||||
|
// Switch to the specified database if needed
|
||||||
|
if (!empty($database) && strtolower($type) !== 'sqlite') {
|
||||||
|
$conn['connection']->exec("USE `$database`");
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (strtolower($type)) {
|
||||||
|
case 'mysql':
|
||||||
|
case 'mariadb':
|
||||||
|
$stmt = $conn['connection']->query("SHOW TABLES");
|
||||||
|
return $stmt->fetchAll(\PDO::FETCH_COLUMN);
|
||||||
|
|
||||||
|
case 'postgresql':
|
||||||
|
$stmt = $conn['connection']->query("SELECT tablename FROM pg_tables WHERE schemaname = 'public'");
|
||||||
|
return $stmt->fetchAll(\PDO::FETCH_COLUMN);
|
||||||
|
|
||||||
|
case 'sqlite':
|
||||||
|
$stmt = $conn['connection']->query("SELECT name FROM sqlite_master WHERE type='table'");
|
||||||
|
return $stmt->fetchAll(\PDO::FETCH_COLUMN);
|
||||||
|
|
||||||
|
default:
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function executeDatabaseQuery($conn, string $query, string $database): array
|
||||||
|
{
|
||||||
|
// Switch to the specified database if needed
|
||||||
|
if (!empty($database) && strtolower($conn['type']) !== 'sqlite') {
|
||||||
|
$conn['connection']->exec("USE `$database`");
|
||||||
|
}
|
||||||
|
|
||||||
|
$startTime = microtime(true);
|
||||||
|
$stmt = $conn['connection']->query($query);
|
||||||
|
$executionTime = round((microtime(true) - $startTime) * 1000, 2);
|
||||||
|
|
||||||
|
$result = [
|
||||||
|
'query' => $query,
|
||||||
|
'execution_time' => $executionTime,
|
||||||
|
'affected_rows' => 0,
|
||||||
|
'columns' => [],
|
||||||
|
'rows' => []
|
||||||
|
];
|
||||||
|
|
||||||
|
// Check if it's a SELECT query or similar
|
||||||
|
$queryType = strtoupper(substr(trim($query), 0, 6));
|
||||||
|
if ($queryType === 'SELECT' || $queryType === 'SHOW' || $queryType === 'DESCR') {
|
||||||
|
$rows = $stmt->fetchAll(\PDO::FETCH_ASSOC);
|
||||||
|
$result['rows'] = $rows;
|
||||||
|
$result['row_count'] = count($rows);
|
||||||
|
|
||||||
|
if (!empty($rows)) {
|
||||||
|
$result['columns'] = array_keys($rows[0]);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// For INSERT, UPDATE, DELETE, etc.
|
||||||
|
$result['affected_rows'] = $stmt->rowCount();
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function __destruct()
|
||||||
|
{
|
||||||
|
// Close all connections
|
||||||
|
foreach ($this->connections as $conn) {
|
||||||
|
if ($conn['connection']) {
|
||||||
|
$conn['connection'] = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
482
src/controllers/FileController.php
Archivo normal
482
src/controllers/FileController.php
Archivo normal
@@ -0,0 +1,482 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* File Controller
|
||||||
|
*
|
||||||
|
* Handles file system operations
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace AleShell\Controllers;
|
||||||
|
|
||||||
|
use AleShell\Controllers\BaseController;
|
||||||
|
|
||||||
|
class FileController extends BaseController
|
||||||
|
{
|
||||||
|
public function listFiles(): void
|
||||||
|
{
|
||||||
|
$this->requireAuth();
|
||||||
|
|
||||||
|
$data = $this->getRequestData();
|
||||||
|
$path = $data['path'] ?? getcwd();
|
||||||
|
$showHidden = $data['show_hidden'] ?? false;
|
||||||
|
$sortBy = $data['sort_by'] ?? 'name';
|
||||||
|
$sortOrder = $data['sort_order'] ?? 'asc';
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!$this->security->isPathAllowed($path)) {
|
||||||
|
$this->errorResponse('Access denied to this path', 403);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!is_dir($path)) {
|
||||||
|
$this->errorResponse('Path is not a directory');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$files = $this->scanDirectory($path, $showHidden);
|
||||||
|
$files = $this->sortFiles($files, $sortBy, $sortOrder);
|
||||||
|
|
||||||
|
$this->successResponse([
|
||||||
|
'path' => realpath($path),
|
||||||
|
'files' => $files,
|
||||||
|
'total' => count($files)
|
||||||
|
]);
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$this->errorResponse('Failed to list files: ' . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function readFile(): void
|
||||||
|
{
|
||||||
|
$this->requireAuth();
|
||||||
|
|
||||||
|
$data = $this->getRequestData();
|
||||||
|
$path = $data['path'] ?? '';
|
||||||
|
$encoding = $data['encoding'] ?? 'utf-8';
|
||||||
|
$maxSize = $this->config->get('limits.max_file_size', 50 * 1024 * 1024);
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!$this->security->isPathAllowed($path)) {
|
||||||
|
$this->errorResponse('Access denied to this file', 403);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!file_exists($path)) {
|
||||||
|
$this->errorResponse('File not found', 404);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!is_file($path)) {
|
||||||
|
$this->errorResponse('Path is not a file');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$size = filesize($path);
|
||||||
|
if ($size > $maxSize) {
|
||||||
|
$this->errorResponse('File too large to read');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$content = file_get_contents($path);
|
||||||
|
if ($content === false) {
|
||||||
|
$this->errorResponse('Failed to read file');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to detect encoding if not specified
|
||||||
|
if ($encoding === 'auto') {
|
||||||
|
$encoding = mb_detect_encoding($content, ['UTF-8', 'ISO-8859-1', 'ASCII'], true) ?: 'UTF-8';
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->successResponse([
|
||||||
|
'content' => $content,
|
||||||
|
'size' => $size,
|
||||||
|
'encoding' => $encoding,
|
||||||
|
'mime_type' => mime_content_type($path),
|
||||||
|
'last_modified' => filemtime($path),
|
||||||
|
'permissions' => substr(sprintf('%o', fileperms($path)), -4)
|
||||||
|
]);
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$this->errorResponse('Failed to read file: ' . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function writeFile(): void
|
||||||
|
{
|
||||||
|
$this->requireAuth();
|
||||||
|
|
||||||
|
$data = $this->getRequestData();
|
||||||
|
|
||||||
|
if (!$this->validateCSRF($data)) {
|
||||||
|
$this->errorResponse('Invalid CSRF token', 403);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$path = $data['path'] ?? '';
|
||||||
|
$content = $data['content'] ?? '';
|
||||||
|
$backup = $data['backup'] ?? true;
|
||||||
|
$encoding = $data['encoding'] ?? 'utf-8';
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!$this->security->isPathAllowed($path)) {
|
||||||
|
$this->errorResponse('Access denied to this file', 403);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create backup if requested and file exists
|
||||||
|
if ($backup && file_exists($path)) {
|
||||||
|
$backupPath = $path . '.backup.' . time();
|
||||||
|
if (!copy($path, $backupPath)) {
|
||||||
|
$this->errorResponse('Failed to create backup');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure directory exists
|
||||||
|
$dir = dirname($path);
|
||||||
|
if (!is_dir($dir)) {
|
||||||
|
if (!mkdir($dir, 0755, true)) {
|
||||||
|
$this->errorResponse('Failed to create directory');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert encoding if needed
|
||||||
|
if ($encoding !== 'utf-8') {
|
||||||
|
$content = mb_convert_encoding($content, $encoding, 'UTF-8');
|
||||||
|
}
|
||||||
|
|
||||||
|
$bytesWritten = file_put_contents($path, $content, LOCK_EX);
|
||||||
|
if ($bytesWritten === false) {
|
||||||
|
$this->errorResponse('Failed to write file');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->successResponse([
|
||||||
|
'bytes_written' => $bytesWritten,
|
||||||
|
'last_modified' => filemtime($path)
|
||||||
|
]);
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$this->errorResponse('Failed to write file: ' . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function deleteFile(): void
|
||||||
|
{
|
||||||
|
$this->requireAuth();
|
||||||
|
|
||||||
|
$data = $this->getRequestData();
|
||||||
|
|
||||||
|
if (!$this->validateCSRF($data)) {
|
||||||
|
$this->errorResponse('Invalid CSRF token', 403);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$path = $data['path'] ?? '';
|
||||||
|
$recursive = $data['recursive'] ?? false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!$this->security->isPathAllowed($path)) {
|
||||||
|
$this->errorResponse('Access denied to this path', 403);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!file_exists($path)) {
|
||||||
|
$this->errorResponse('File or directory not found', 404);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_dir($path)) {
|
||||||
|
if ($recursive) {
|
||||||
|
$deleted = $this->deleteDirectory($path);
|
||||||
|
} else {
|
||||||
|
$deleted = rmdir($path);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$deleted = unlink($path);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$deleted) {
|
||||||
|
$this->errorResponse('Failed to delete');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->successResponse(['deleted' => true]);
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$this->errorResponse('Failed to delete: ' . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function createDirectory(): void
|
||||||
|
{
|
||||||
|
$this->requireAuth();
|
||||||
|
|
||||||
|
$data = $this->getRequestData();
|
||||||
|
|
||||||
|
if (!$this->validateCSRF($data)) {
|
||||||
|
$this->errorResponse('Invalid CSRF token', 403);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$path = $data['path'] ?? '';
|
||||||
|
$permissions = $data['permissions'] ?? 0755;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!$this->security->isPathAllowed($path)) {
|
||||||
|
$this->errorResponse('Access denied to this path', 403);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (file_exists($path)) {
|
||||||
|
$this->errorResponse('Path already exists');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!mkdir($path, $permissions, true)) {
|
||||||
|
$this->errorResponse('Failed to create directory');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->successResponse(['created' => true]);
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$this->errorResponse('Failed to create directory: ' . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function renameFile(): void
|
||||||
|
{
|
||||||
|
$this->requireAuth();
|
||||||
|
|
||||||
|
$data = $this->getRequestData();
|
||||||
|
|
||||||
|
if (!$this->validateCSRF($data)) {
|
||||||
|
$this->errorResponse('Invalid CSRF token', 403);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$oldPath = $data['old_path'] ?? '';
|
||||||
|
$newPath = $data['new_path'] ?? '';
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!$this->security->isPathAllowed($oldPath) || !$this->security->isPathAllowed($newPath)) {
|
||||||
|
$this->errorResponse('Access denied to one or both paths', 403);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!file_exists($oldPath)) {
|
||||||
|
$this->errorResponse('Source file or directory not found', 404);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (file_exists($newPath)) {
|
||||||
|
$this->errorResponse('Destination already exists');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!rename($oldPath, $newPath)) {
|
||||||
|
$this->errorResponse('Failed to rename');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->successResponse(['renamed' => true]);
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$this->errorResponse('Failed to rename: ' . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function uploadFile(): void
|
||||||
|
{
|
||||||
|
$this->requireAuth();
|
||||||
|
|
||||||
|
if (!$this->validateCSRF()) {
|
||||||
|
$this->errorResponse('Invalid CSRF token', 403);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$targetPath = $_POST['path'] ?? getcwd();
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!$this->security->isPathAllowed($targetPath)) {
|
||||||
|
$this->errorResponse('Access denied to target path', 403);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!is_dir($targetPath)) {
|
||||||
|
$this->errorResponse('Target path is not a directory');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isset($_FILES['file'])) {
|
||||||
|
$this->errorResponse('No file uploaded');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$file = $_FILES['file'];
|
||||||
|
|
||||||
|
if ($file['error'] !== UPLOAD_ERR_OK) {
|
||||||
|
$this->errorResponse('Upload error: ' . $this->getUploadErrorMessage($file['error']));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$fileName = basename($file['name']);
|
||||||
|
$targetFile = $targetPath . DIRECTORY_SEPARATOR . $fileName;
|
||||||
|
|
||||||
|
if (file_exists($targetFile)) {
|
||||||
|
$this->errorResponse('File already exists');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!move_uploaded_file($file['tmp_name'], $targetFile)) {
|
||||||
|
$this->errorResponse('Failed to move uploaded file');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->successResponse([
|
||||||
|
'uploaded' => true,
|
||||||
|
'file_name' => $fileName,
|
||||||
|
'file_size' => filesize($targetFile)
|
||||||
|
]);
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$this->errorResponse('Failed to upload file: ' . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getUploadErrorMessage(int $errorCode): string
|
||||||
|
{
|
||||||
|
switch ($errorCode) {
|
||||||
|
case UPLOAD_ERR_INI_SIZE:
|
||||||
|
return 'The uploaded file exceeds the upload_max_filesize directive in php.ini';
|
||||||
|
case UPLOAD_ERR_FORM_SIZE:
|
||||||
|
return 'The uploaded file exceeds the MAX_FILE_SIZE directive in the HTML form';
|
||||||
|
case UPLOAD_ERR_PARTIAL:
|
||||||
|
return 'The uploaded file was only partially uploaded';
|
||||||
|
case UPLOAD_ERR_NO_FILE:
|
||||||
|
return 'No file was uploaded';
|
||||||
|
case UPLOAD_ERR_NO_TMP_DIR:
|
||||||
|
return 'Missing a temporary folder';
|
||||||
|
case UPLOAD_ERR_CANT_WRITE:
|
||||||
|
return 'Failed to write file to disk';
|
||||||
|
case UPLOAD_ERR_EXTENSION:
|
||||||
|
return 'A PHP extension stopped the file upload';
|
||||||
|
default:
|
||||||
|
return 'Unknown upload error';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function scanDirectory(string $path, bool $showHidden): array
|
||||||
|
{
|
||||||
|
$files = [];
|
||||||
|
$items = scandir($path);
|
||||||
|
|
||||||
|
if ($items === false) {
|
||||||
|
return $files;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($items as $item) {
|
||||||
|
if ($item === '.' || $item === '..') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$showHidden && $item[0] === '.') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$fullPath = $path . DIRECTORY_SEPARATOR . $item;
|
||||||
|
$stat = stat($fullPath);
|
||||||
|
|
||||||
|
$files[] = [
|
||||||
|
'name' => $item,
|
||||||
|
'path' => $fullPath,
|
||||||
|
'type' => is_dir($fullPath) ? 'directory' : 'file',
|
||||||
|
'size' => is_file($fullPath) ? filesize($fullPath) : 0,
|
||||||
|
'permissions' => substr(sprintf('%o', fileperms($fullPath)), -4),
|
||||||
|
'owner' => $this->getFileOwner($fullPath),
|
||||||
|
'group' => $this->getFileGroup($fullPath),
|
||||||
|
'last_modified' => filemtime($fullPath),
|
||||||
|
'last_accessed' => fileatime($fullPath),
|
||||||
|
'created' => filectime($fullPath),
|
||||||
|
'readable' => is_readable($fullPath),
|
||||||
|
'writable' => is_writable($fullPath),
|
||||||
|
'executable' => is_executable($fullPath),
|
||||||
|
'mime_type' => is_file($fullPath) && is_readable($fullPath) ? mime_content_type($fullPath) : null
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $files;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function sortFiles(array $files, string $sortBy, string $sortOrder): array
|
||||||
|
{
|
||||||
|
usort($files, function($a, $b) use ($sortBy, $sortOrder) {
|
||||||
|
// Directories first
|
||||||
|
if ($a['type'] !== $b['type']) {
|
||||||
|
return $a['type'] === 'directory' ? -1 : 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = 0;
|
||||||
|
switch ($sortBy) {
|
||||||
|
case 'name':
|
||||||
|
$result = strcasecmp($a['name'], $b['name']);
|
||||||
|
break;
|
||||||
|
case 'size':
|
||||||
|
$result = $a['size'] <=> $b['size'];
|
||||||
|
break;
|
||||||
|
case 'modified':
|
||||||
|
$result = $a['last_modified'] <=> $b['last_modified'];
|
||||||
|
break;
|
||||||
|
case 'type':
|
||||||
|
$result = strcasecmp($a['mime_type'] ?? '', $b['mime_type'] ?? '');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $sortOrder === 'desc' ? -$result : $result;
|
||||||
|
});
|
||||||
|
|
||||||
|
return $files;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getFileOwner(string $path): string
|
||||||
|
{
|
||||||
|
if (function_exists('posix_getpwuid')) {
|
||||||
|
$owner = posix_getpwuid(fileowner($path));
|
||||||
|
return $owner['name'] ?? 'unknown';
|
||||||
|
}
|
||||||
|
return 'unknown';
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getFileGroup(string $path): string
|
||||||
|
{
|
||||||
|
if (function_exists('posix_getgrgid')) {
|
||||||
|
$group = posix_getgrgid(filegroup($path));
|
||||||
|
return $group['name'] ?? 'unknown';
|
||||||
|
}
|
||||||
|
return 'unknown';
|
||||||
|
}
|
||||||
|
|
||||||
|
private function deleteDirectory(string $path): bool
|
||||||
|
{
|
||||||
|
if (!is_dir($path)) {
|
||||||
|
return unlink($path);
|
||||||
|
}
|
||||||
|
|
||||||
|
$files = scandir($path);
|
||||||
|
if ($files === false) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($files as $file) {
|
||||||
|
if ($file === '.' || $file === '..') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$fullPath = $path . DIRECTORY_SEPARATOR . $file;
|
||||||
|
if (!$this->deleteDirectory($fullPath)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return rmdir($path);
|
||||||
|
}
|
||||||
|
}
|
||||||
82
src/controllers/MainController.php
Archivo normal
82
src/controllers/MainController.php
Archivo normal
@@ -0,0 +1,82 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Main Controller
|
||||||
|
*
|
||||||
|
* Handles main application routes
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace AleShell\Controllers;
|
||||||
|
|
||||||
|
use AleShell\Controllers\BaseController;
|
||||||
|
|
||||||
|
class MainController extends BaseController
|
||||||
|
{
|
||||||
|
public function index(): void
|
||||||
|
{
|
||||||
|
// This is handled by the Router class
|
||||||
|
// Just ensure user is authenticated
|
||||||
|
$this->requireAuth();
|
||||||
|
$this->successResponse(['message' => 'AleShell is running']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function dashboard(): void
|
||||||
|
{
|
||||||
|
$this->requireAuth();
|
||||||
|
|
||||||
|
// Return dashboard data
|
||||||
|
$this->successResponse([
|
||||||
|
'user' => $this->getCurrentUser(),
|
||||||
|
'system' => $this->getSystemOverview(),
|
||||||
|
'modules' => $this->getAvailableModules(),
|
||||||
|
'config' => $this->getUserConfig()
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getCurrentUser(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'name' => $_SESSION['user_name'] ?? 'admin',
|
||||||
|
'ip' => $_SESSION['user_ip'] ?? $_SERVER['REMOTE_ADDR'],
|
||||||
|
'login_time' => $_SESSION['login_time'] ?? time(),
|
||||||
|
'last_activity' => $_SESSION['last_activity'] ?? time()
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getSystemOverview(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'hostname' => gethostname(),
|
||||||
|
'os' => php_uname('s') . ' ' . php_uname('r'),
|
||||||
|
'php_version' => PHP_VERSION,
|
||||||
|
'current_path' => getcwd(),
|
||||||
|
'disk_free' => disk_free_space('.'),
|
||||||
|
'disk_total' => disk_total_space('.')
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getAvailableModules(): array
|
||||||
|
{
|
||||||
|
$features = $this->config->get('features', []);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'files' => $features['file_manager'] ?? true,
|
||||||
|
'terminal' => $features['terminal'] ?? true,
|
||||||
|
'editor' => $features['code_editor'] ?? true,
|
||||||
|
'processes' => $features['process_manager'] ?? true,
|
||||||
|
'network' => $features['network_tools'] ?? true,
|
||||||
|
'database' => $features['database_tools'] ?? true,
|
||||||
|
'system' => $features['system_info'] ?? true,
|
||||||
|
'logs' => $features['log_viewer'] ?? true
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getUserConfig(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'theme' => $_SESSION['theme'] ?? $this->config->get('ui.theme', 'dark'),
|
||||||
|
'language' => $_SESSION['language'] ?? $this->config->get('ui.language', 'en'),
|
||||||
|
'items_per_page' => $this->config->get('ui.items_per_page', 50),
|
||||||
|
'show_hidden_files' => $this->config->get('ui.show_hidden_files', false)
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
25
src/controllers/ModuleController.php
Archivo normal
25
src/controllers/ModuleController.php
Archivo normal
@@ -0,0 +1,25 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Module Controller - Simplified
|
||||||
|
*
|
||||||
|
* Handles module loading
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace AleShell\Controllers;
|
||||||
|
|
||||||
|
use AleShell\Controllers\BaseController;
|
||||||
|
|
||||||
|
class ModuleController extends BaseController
|
||||||
|
{
|
||||||
|
public function load($matches = []): void
|
||||||
|
{
|
||||||
|
$this->requireAuth();
|
||||||
|
|
||||||
|
$module = $matches['module'] ?? 'dashboard';
|
||||||
|
|
||||||
|
$this->successResponse([
|
||||||
|
'module' => $module,
|
||||||
|
'content' => "<div>Module {$module} loaded</div>"
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
334
src/controllers/NetworkController.php
Archivo normal
334
src/controllers/NetworkController.php
Archivo normal
@@ -0,0 +1,334 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Network Controller
|
||||||
|
*
|
||||||
|
* Handles network operations and tools
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace AleShell\Controllers;
|
||||||
|
|
||||||
|
use AleShell\Controllers\BaseController;
|
||||||
|
|
||||||
|
class NetworkController extends BaseController
|
||||||
|
{
|
||||||
|
public function getConnections(): void
|
||||||
|
{
|
||||||
|
$this->requireAuth();
|
||||||
|
|
||||||
|
try {
|
||||||
|
$connections = $this->getNetworkConnections();
|
||||||
|
$this->successResponse([
|
||||||
|
'connections' => $connections,
|
||||||
|
'total' => count($connections)
|
||||||
|
]);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$this->errorResponse('Failed to get connections: ' . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function ping(): void
|
||||||
|
{
|
||||||
|
$this->requireAuth();
|
||||||
|
|
||||||
|
if (!$this->validateCSRF()) {
|
||||||
|
$this->errorResponse('Invalid CSRF token', 403);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = $this->getRequestData();
|
||||||
|
$host = $data['host'] ?? '';
|
||||||
|
$count = min((int) ($data['count'] ?? 4), 10);
|
||||||
|
|
||||||
|
if (empty($host)) {
|
||||||
|
$this->errorResponse('Host is required');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$result = $this->executePing($host, $count);
|
||||||
|
$this->successResponse($result);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$this->errorResponse('Ping failed: ' . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function traceroute(): void
|
||||||
|
{
|
||||||
|
$this->requireAuth();
|
||||||
|
|
||||||
|
if (!$this->validateCSRF()) {
|
||||||
|
$this->errorResponse('Invalid CSRF token', 403);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = $this->getRequestData();
|
||||||
|
$host = $data['host'] ?? '';
|
||||||
|
$maxHops = min((int) ($data['max_hops'] ?? 30), 50);
|
||||||
|
|
||||||
|
if (empty($host)) {
|
||||||
|
$this->errorResponse('Host is required');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$result = $this->executeTraceroute($host, $maxHops);
|
||||||
|
$this->successResponse($result);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$this->errorResponse('Traceroute failed: ' . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function portScan(): void
|
||||||
|
{
|
||||||
|
$this->requireAuth();
|
||||||
|
|
||||||
|
if (!$this->validateCSRF()) {
|
||||||
|
$this->errorResponse('Invalid CSRF token', 403);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = $this->getRequestData();
|
||||||
|
$host = $data['host'] ?? '';
|
||||||
|
$ports = $data['ports'] ?? '1-1000';
|
||||||
|
$timeout = min((int) ($data['timeout'] ?? 1), 5);
|
||||||
|
|
||||||
|
if (empty($host)) {
|
||||||
|
$this->errorResponse('Host is required');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$result = $this->executePortScan($host, $ports, $timeout);
|
||||||
|
$this->successResponse($result);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$this->errorResponse('Port scan failed: ' . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getInterfaces(): void
|
||||||
|
{
|
||||||
|
$this->requireAuth();
|
||||||
|
|
||||||
|
try {
|
||||||
|
$interfaces = $this->getNetworkInterfaces();
|
||||||
|
$this->successResponse([
|
||||||
|
'interfaces' => $interfaces,
|
||||||
|
'total' => count($interfaces)
|
||||||
|
]);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$this->errorResponse('Failed to get interfaces: ' . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getNetworkConnections(): array
|
||||||
|
{
|
||||||
|
$connections = [];
|
||||||
|
|
||||||
|
if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') {
|
||||||
|
// Windows implementation
|
||||||
|
exec('netstat -ano', $output);
|
||||||
|
foreach ($output as $line) {
|
||||||
|
if (preg_match('/^\s*(\w+)\s+([\d\.]+|\[::\]):(\d+)\s+([\d\.]+|\[::\]):(\d+)\s+(\w+)/', $line, $matches)) {
|
||||||
|
$connections[] = [
|
||||||
|
'protocol' => $matches[1],
|
||||||
|
'local_address' => $matches[2],
|
||||||
|
'local_port' => $matches[3],
|
||||||
|
'remote_address' => $matches[4],
|
||||||
|
'remote_port' => $matches[5],
|
||||||
|
'state' => $matches[6],
|
||||||
|
'pid' => null
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Linux/Unix implementation
|
||||||
|
exec('netstat -tunapl 2>/dev/null || ss -tunapl 2>/dev/null', $output);
|
||||||
|
foreach ($output as $line) {
|
||||||
|
if (preg_match('/^\w+\s+\w+\s+\w+\s+([\d\.\[\]:]+)\s+([\d\.\[\]:]+)\s+(.+)/', $line, $matches)) {
|
||||||
|
$local = $this->parseAddress($matches[1]);
|
||||||
|
$remote = $this->parseAddress($matches[2]);
|
||||||
|
$extra = $matches[3];
|
||||||
|
|
||||||
|
$connections[] = [
|
||||||
|
'protocol' => strpos($line, 'tcp') !== false ? 'tcp' : 'udp',
|
||||||
|
'local_address' => $local['address'],
|
||||||
|
'local_port' => $local['port'],
|
||||||
|
'remote_address' => $remote['address'],
|
||||||
|
'remote_port' => $remote['port'],
|
||||||
|
'state' => $this->extractState($extra),
|
||||||
|
'pid' => $this->extractPid($extra)
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $connections;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function executePing(string $host, int $count): array
|
||||||
|
{
|
||||||
|
$output = [];
|
||||||
|
$success = false;
|
||||||
|
|
||||||
|
if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') {
|
||||||
|
exec("ping -n $count $host", $output, $returnCode);
|
||||||
|
} else {
|
||||||
|
exec("ping -c $count $host", $output, $returnCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
$success = $returnCode === 0;
|
||||||
|
|
||||||
|
return [
|
||||||
|
'host' => $host,
|
||||||
|
'count' => $count,
|
||||||
|
'success' => $success,
|
||||||
|
'output' => implode("\n", $output)
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function executeTraceroute(string $host, int $maxHops): array
|
||||||
|
{
|
||||||
|
$output = [];
|
||||||
|
$success = false;
|
||||||
|
|
||||||
|
if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') {
|
||||||
|
exec("tracert -h $maxHops $host", $output, $returnCode);
|
||||||
|
} else {
|
||||||
|
exec("traceroute -m $maxHops $host 2>/dev/null || tracepath $host 2>/dev/null", $output, $returnCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
$success = $returnCode === 0;
|
||||||
|
|
||||||
|
return [
|
||||||
|
'host' => $host,
|
||||||
|
'max_hops' => $maxHops,
|
||||||
|
'success' => $success,
|
||||||
|
'output' => implode("\n", $output)
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function executePortScan(string $host, string $ports, int $timeout): array
|
||||||
|
{
|
||||||
|
$openPorts = [];
|
||||||
|
$closedPorts = [];
|
||||||
|
|
||||||
|
// Parse port range
|
||||||
|
$portList = $this->parsePortRange($ports);
|
||||||
|
|
||||||
|
foreach ($portList as $port) {
|
||||||
|
$connection = @fsockopen($host, $port, $errno, $errstr, $timeout);
|
||||||
|
if ($connection) {
|
||||||
|
$openPorts[] = $port;
|
||||||
|
fclose($connection);
|
||||||
|
} else {
|
||||||
|
$closedPorts[] = $port;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'host' => $host,
|
||||||
|
'ports_scanned' => $ports,
|
||||||
|
'open_ports' => $openPorts,
|
||||||
|
'closed_ports' => $closedPorts,
|
||||||
|
'total_open' => count($openPorts),
|
||||||
|
'total_closed' => count($closedPorts)
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getNetworkInterfaces(): array
|
||||||
|
{
|
||||||
|
$interfaces = [];
|
||||||
|
|
||||||
|
if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') {
|
||||||
|
exec('ipconfig /all', $output);
|
||||||
|
$currentInterface = null;
|
||||||
|
foreach ($output as $line) {
|
||||||
|
if (preg_match('/^(\w[^:]*):/', $line, $matches)) {
|
||||||
|
$currentInterface = ['name' => trim($matches[1]), 'details' => []];
|
||||||
|
$interfaces[] = $currentInterface;
|
||||||
|
} elseif ($currentInterface && preg_match('/^\s*([^:]+):\s*(.+)$/', $line, $matches)) {
|
||||||
|
$currentInterface['details'][trim($matches[1])] = trim($matches[2]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
exec('ip addr show 2>/dev/null || ifconfig -a 2>/dev/null', $output);
|
||||||
|
$currentInterface = null;
|
||||||
|
foreach ($output as $line) {
|
||||||
|
if (preg_match('/^\d+:\s*([^:]+):/', $line, $matches)) {
|
||||||
|
$currentInterface = [
|
||||||
|
'name' => $matches[1],
|
||||||
|
'addresses' => [],
|
||||||
|
'details' => []
|
||||||
|
];
|
||||||
|
$interfaces[] = $currentInterface;
|
||||||
|
} elseif ($currentInterface && preg_match('/\s*inet\s+([^\/\s]+)/', $line, $matches)) {
|
||||||
|
$currentInterface['addresses'][] = $matches[1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $interfaces;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function parseAddress(string $address): array
|
||||||
|
{
|
||||||
|
if (preg_match('/\[([^\]]+)\]:(\d+)/', $address, $matches)) {
|
||||||
|
return ['address' => $matches[1], 'port' => $matches[2]];
|
||||||
|
} elseif (preg_match('/([\d\.]+):(\d+)/', $address, $matches)) {
|
||||||
|
return ['address' => $matches[1], 'port' => $matches[2]];
|
||||||
|
} else {
|
||||||
|
return ['address' => $address, 'port' => '0'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function extractState(string $extra): string
|
||||||
|
{
|
||||||
|
if (preg_match('/(\w+)\s*$/', $extra, $matches)) {
|
||||||
|
return $matches[1];
|
||||||
|
}
|
||||||
|
return 'UNKNOWN';
|
||||||
|
}
|
||||||
|
|
||||||
|
private function extractPid(string $extra): ?int
|
||||||
|
{
|
||||||
|
if (preg_match('/pid=(\d+)/', $extra, $matches)) {
|
||||||
|
return (int) $matches[1];
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function parsePortRange(string $ports): array
|
||||||
|
{
|
||||||
|
$portList = [];
|
||||||
|
|
||||||
|
// Handle ranges like "1-1000"
|
||||||
|
if (strpos($ports, '-') !== false) {
|
||||||
|
list($start, $end) = explode('-', $ports, 2);
|
||||||
|
$start = (int) $start;
|
||||||
|
$end = (int) $end;
|
||||||
|
for ($i = $start; $i <= $end; $i++) {
|
||||||
|
$portList[] = $i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Handle comma-separated list like "22,80,443"
|
||||||
|
elseif (strpos($ports, ',') !== false) {
|
||||||
|
$portStrings = explode(',', $ports);
|
||||||
|
foreach ($portStrings as $portStr) {
|
||||||
|
$port = (int) trim($portStr);
|
||||||
|
if ($port > 0 && $port <= 65535) {
|
||||||
|
$portList[] = $port;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Single port
|
||||||
|
else {
|
||||||
|
$port = (int) $ports;
|
||||||
|
if ($port > 0 && $port <= 65535) {
|
||||||
|
$portList[] = $port;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Limit to reasonable number
|
||||||
|
return array_slice($portList, 0, 1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
208
src/controllers/ProcessController.php
Archivo normal
208
src/controllers/ProcessController.php
Archivo normal
@@ -0,0 +1,208 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Process Controller
|
||||||
|
*
|
||||||
|
* Handles process management operations
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace AleShell\Controllers;
|
||||||
|
|
||||||
|
use AleShell\Controllers\BaseController;
|
||||||
|
|
||||||
|
class ProcessController extends BaseController
|
||||||
|
{
|
||||||
|
public function listProcesses(): void
|
||||||
|
{
|
||||||
|
$this->requireAuth();
|
||||||
|
|
||||||
|
try {
|
||||||
|
$processes = $this->getProcessList();
|
||||||
|
$this->successResponse([
|
||||||
|
'processes' => $processes,
|
||||||
|
'total' => count($processes)
|
||||||
|
]);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$this->errorResponse('Failed to list processes: ' . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function killProcess(): void
|
||||||
|
{
|
||||||
|
$this->requireAuth();
|
||||||
|
|
||||||
|
if (!$this->validateCSRF()) {
|
||||||
|
$this->errorResponse('Invalid CSRF token', 403);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = $this->getRequestData();
|
||||||
|
$pid = (int) ($data['pid'] ?? 0);
|
||||||
|
$signal = $data['signal'] ?? 'TERM';
|
||||||
|
|
||||||
|
if ($pid <= 0) {
|
||||||
|
$this->errorResponse('Invalid process ID');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$result = $this->sendSignal($pid, $signal);
|
||||||
|
$this->successResponse($result);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$this->errorResponse('Failed to kill process: ' . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getProcessInfo(): void
|
||||||
|
{
|
||||||
|
$this->requireAuth();
|
||||||
|
|
||||||
|
$data = $this->getRequestData();
|
||||||
|
$pid = (int) ($data['pid'] ?? 0);
|
||||||
|
|
||||||
|
if ($pid <= 0) {
|
||||||
|
$this->errorResponse('Invalid process ID');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$process = $this->getProcessDetails($pid);
|
||||||
|
if ($process) {
|
||||||
|
$this->successResponse($process);
|
||||||
|
} else {
|
||||||
|
$this->errorResponse('Process not found', 404);
|
||||||
|
}
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$this->errorResponse('Failed to get process info: ' . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getProcessList(): array
|
||||||
|
{
|
||||||
|
$processes = [];
|
||||||
|
|
||||||
|
if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') {
|
||||||
|
// Windows implementation
|
||||||
|
exec('tasklist /FO CSV /NH', $output);
|
||||||
|
foreach ($output as $line) {
|
||||||
|
$parts = str_getcsv($line);
|
||||||
|
if (count($parts) >= 5) {
|
||||||
|
$processes[] = [
|
||||||
|
'pid' => (int) $parts[1],
|
||||||
|
'name' => $parts[0],
|
||||||
|
'cpu' => 0, // Windows tasklist doesn't provide CPU %
|
||||||
|
'memory' => 0, // Would need additional parsing
|
||||||
|
'user' => 'N/A',
|
||||||
|
'status' => 'Running',
|
||||||
|
'command' => $parts[0]
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Linux/Unix implementation
|
||||||
|
exec('ps aux --no-headers', $output);
|
||||||
|
foreach ($output as $line) {
|
||||||
|
$parts = preg_split('/\s+/', $line, 11);
|
||||||
|
if (count($parts) >= 11) {
|
||||||
|
$processes[] = [
|
||||||
|
'pid' => (int) $parts[1],
|
||||||
|
'user' => $parts[0],
|
||||||
|
'cpu' => (float) $parts[2],
|
||||||
|
'memory' => (float) $parts[3],
|
||||||
|
'vsz' => (int) $parts[4],
|
||||||
|
'rss' => (int) $parts[5],
|
||||||
|
'tty' => $parts[6],
|
||||||
|
'stat' => $parts[7],
|
||||||
|
'start' => $parts[8],
|
||||||
|
'time' => $parts[9],
|
||||||
|
'command' => $parts[10],
|
||||||
|
'name' => basename($parts[10])
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $processes;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getProcessDetails(int $pid): ?array
|
||||||
|
{
|
||||||
|
if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') {
|
||||||
|
// Windows implementation
|
||||||
|
exec("tasklist /FI \"PID eq $pid\" /FO CSV /NH", $output);
|
||||||
|
if (!empty($output)) {
|
||||||
|
$parts = str_getcsv($output[0]);
|
||||||
|
return [
|
||||||
|
'pid' => $pid,
|
||||||
|
'name' => $parts[0] ?? 'Unknown',
|
||||||
|
'cpu' => 0,
|
||||||
|
'memory' => 0,
|
||||||
|
'user' => 'N/A',
|
||||||
|
'status' => 'Running',
|
||||||
|
'command' => $parts[0] ?? 'Unknown'
|
||||||
|
];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Linux/Unix implementation
|
||||||
|
exec("ps -p $pid -o pid,user,pcpu,pmem,vsz,rss,tty,stat,start,time,command --no-headers", $output);
|
||||||
|
if (!empty($output)) {
|
||||||
|
$parts = preg_split('/\s+/', $output[0], 11);
|
||||||
|
if (count($parts) >= 11) {
|
||||||
|
return [
|
||||||
|
'pid' => (int) $parts[0],
|
||||||
|
'user' => $parts[1],
|
||||||
|
'cpu' => (float) $parts[2],
|
||||||
|
'memory' => (float) $parts[3],
|
||||||
|
'vsz' => (int) $parts[4],
|
||||||
|
'rss' => (int) $parts[5],
|
||||||
|
'tty' => $parts[6],
|
||||||
|
'stat' => $parts[7],
|
||||||
|
'start' => $parts[8],
|
||||||
|
'time' => $parts[9],
|
||||||
|
'command' => $parts[10],
|
||||||
|
'name' => basename($parts[10])
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function sendSignal(int $pid, string $signal): array
|
||||||
|
{
|
||||||
|
$signalNum = $this->getSignalNumber($signal);
|
||||||
|
|
||||||
|
if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') {
|
||||||
|
// Windows implementation
|
||||||
|
exec("taskkill /PID $pid /F", $output, $returnCode);
|
||||||
|
$success = $returnCode === 0;
|
||||||
|
} else {
|
||||||
|
// Linux/Unix implementation
|
||||||
|
exec("kill -$signalNum $pid 2>&1", $output, $returnCode);
|
||||||
|
$success = $returnCode === 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'success' => $success,
|
||||||
|
'pid' => $pid,
|
||||||
|
'signal' => $signal,
|
||||||
|
'signal_num' => $signalNum,
|
||||||
|
'output' => implode("\n", $output)
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getSignalNumber(string $signal): int
|
||||||
|
{
|
||||||
|
$signals = [
|
||||||
|
'HUP' => 1, 'INT' => 2, 'QUIT' => 3, 'ILL' => 4, 'TRAP' => 5,
|
||||||
|
'ABRT' => 6, 'BUS' => 7, 'FPE' => 8, 'KILL' => 9, 'USR1' => 10,
|
||||||
|
'SEGV' => 11, 'USR2' => 12, 'PIPE' => 13, 'ALRM' => 14, 'TERM' => 15,
|
||||||
|
'STKFLT' => 16, 'CHLD' => 17, 'CONT' => 18, 'STOP' => 19, 'TSTP' => 20,
|
||||||
|
'TTIN' => 21, 'TTOU' => 22, 'URG' => 23, 'XCPU' => 24, 'XFSZ' => 25,
|
||||||
|
'VTALRM' => 26, 'PROF' => 27, 'WINCH' => 28, 'IO' => 29, 'PWR' => 30,
|
||||||
|
'SYS' => 31
|
||||||
|
];
|
||||||
|
|
||||||
|
return $signals[strtoupper($signal)] ?? 15; // Default to TERM
|
||||||
|
}
|
||||||
|
}
|
||||||
307
src/controllers/SystemController.php
Archivo normal
307
src/controllers/SystemController.php
Archivo normal
@@ -0,0 +1,307 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* System Controller
|
||||||
|
*
|
||||||
|
* Handles system information and monitoring
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace AleShell\Controllers;
|
||||||
|
|
||||||
|
use AleShell\Controllers\BaseController;
|
||||||
|
|
||||||
|
class SystemController extends BaseController
|
||||||
|
{
|
||||||
|
public function getInfo(): void
|
||||||
|
{
|
||||||
|
$this->requireAuth();
|
||||||
|
|
||||||
|
try {
|
||||||
|
$info = [
|
||||||
|
'os' => php_uname('s') . ' ' . php_uname('r'),
|
||||||
|
'hostname' => gethostname(),
|
||||||
|
'php_version' => PHP_VERSION,
|
||||||
|
'server_software' => $_SERVER['SERVER_SOFTWARE'] ?? 'Unknown',
|
||||||
|
'document_root' => $_SERVER['DOCUMENT_ROOT'] ?? '',
|
||||||
|
'load_average' => $this->getLoadAverage(),
|
||||||
|
'memory' => $this->getMemoryInfo(),
|
||||||
|
'disk' => $this->getDiskInfo(),
|
||||||
|
'network' => $this->getNetworkInfo(),
|
||||||
|
'processes' => $this->getProcessCount(),
|
||||||
|
'uptime' => $this->getUptime(),
|
||||||
|
'current_user' => $this->getCurrentUser(),
|
||||||
|
'current_path' => getcwd(),
|
||||||
|
'timestamp' => time(),
|
||||||
|
'timezone' => date_default_timezone_get()
|
||||||
|
];
|
||||||
|
|
||||||
|
$this->successResponse($info);
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$this->errorResponse('Failed to get system information: ' . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getConfig(): void
|
||||||
|
{
|
||||||
|
$this->requireAuth();
|
||||||
|
|
||||||
|
try {
|
||||||
|
$config = [
|
||||||
|
'version' => ALESHELL_VERSION,
|
||||||
|
'theme' => $_SESSION['theme'] ?? $this->config->get('ui.theme', 'dark'),
|
||||||
|
'language' => $_SESSION['language'] ?? $this->config->get('ui.language', 'en'),
|
||||||
|
'features' => [
|
||||||
|
'file_manager' => $this->config->get('features.file_manager', true),
|
||||||
|
'terminal' => $this->config->get('features.terminal', true),
|
||||||
|
'code_editor' => $this->config->get('features.code_editor', true),
|
||||||
|
'process_manager' => $this->config->get('features.process_manager', true),
|
||||||
|
'network_tools' => $this->config->get('features.network_tools', true),
|
||||||
|
'database_tools' => $this->config->get('features.database_tools', true),
|
||||||
|
'system_info' => $this->config->get('features.system_info', true),
|
||||||
|
'log_viewer' => $this->config->get('features.log_viewer', true)
|
||||||
|
],
|
||||||
|
'limits' => [
|
||||||
|
'max_file_size' => $this->config->get('limits.max_file_size', 10485760), // 10MB
|
||||||
|
'max_upload_size' => $this->config->get('limits.max_upload_size', 52428800), // 50MB
|
||||||
|
'session_timeout' => $this->config->get('security.session_timeout', 3600)
|
||||||
|
],
|
||||||
|
'paths' => [
|
||||||
|
'current' => getcwd(),
|
||||||
|
'home' => $_SERVER['HOME'] ?? '/home',
|
||||||
|
'temp' => sys_get_temp_dir()
|
||||||
|
]
|
||||||
|
];
|
||||||
|
|
||||||
|
$this->successResponse($config);
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$this->errorResponse('Failed to get configuration: ' . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getStats(): void
|
||||||
|
{
|
||||||
|
$this->requireAuth();
|
||||||
|
|
||||||
|
try {
|
||||||
|
$stats = [
|
||||||
|
'cpu' => $this->getCPUUsage(),
|
||||||
|
'memory' => $this->getMemoryUsage(),
|
||||||
|
'disk' => $this->getDiskUsage(),
|
||||||
|
'network' => $this->getNetworkStats(),
|
||||||
|
'timestamp' => time()
|
||||||
|
];
|
||||||
|
|
||||||
|
$this->successResponse($stats);
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$this->errorResponse('Failed to get system stats: ' . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getLoadAverage(): array
|
||||||
|
{
|
||||||
|
if (function_exists('sys_getloadavg')) {
|
||||||
|
$load = sys_getloadavg();
|
||||||
|
return [
|
||||||
|
'1min' => round($load[0], 2),
|
||||||
|
'5min' => round($load[1], 2),
|
||||||
|
'15min' => round($load[2], 2)
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback for systems without sys_getloadavg
|
||||||
|
if (is_readable('/proc/loadavg')) {
|
||||||
|
$load = file_get_contents('/proc/loadavg');
|
||||||
|
$values = explode(' ', $load);
|
||||||
|
return [
|
||||||
|
'1min' => (float)($values[0] ?? 0),
|
||||||
|
'5min' => (float)($values[1] ?? 0),
|
||||||
|
'15min' => (float)($values[2] ?? 0)
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return ['1min' => 0, '5min' => 0, '15min' => 0];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getMemoryInfo(): array
|
||||||
|
{
|
||||||
|
$info = [
|
||||||
|
'total' => 0,
|
||||||
|
'free' => 0,
|
||||||
|
'used' => 0,
|
||||||
|
'cached' => 0,
|
||||||
|
'usage_percent' => 0
|
||||||
|
];
|
||||||
|
|
||||||
|
if (is_readable('/proc/meminfo')) {
|
||||||
|
$meminfo = file_get_contents('/proc/meminfo');
|
||||||
|
preg_match('/MemTotal:\\s+(\\d+)\\s+kB/', $meminfo, $matches);
|
||||||
|
$info['total'] = ($matches[1] ?? 0) * 1024;
|
||||||
|
|
||||||
|
preg_match('/MemFree:\\s+(\\d+)\\s+kB/', $meminfo, $matches);
|
||||||
|
$memFree = ($matches[1] ?? 0) * 1024;
|
||||||
|
|
||||||
|
preg_match('/Cached:\\s+(\\d+)\\s+kB/', $meminfo, $matches);
|
||||||
|
$info['cached'] = ($matches[1] ?? 0) * 1024;
|
||||||
|
|
||||||
|
$info['free'] = $memFree + $info['cached'];
|
||||||
|
$info['used'] = $info['total'] - $info['free'];
|
||||||
|
|
||||||
|
if ($info['total'] > 0) {
|
||||||
|
$info['usage_percent'] = round(($info['used'] / $info['total']) * 100, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $info;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getDiskInfo(): array
|
||||||
|
{
|
||||||
|
$info = [];
|
||||||
|
$path = getcwd();
|
||||||
|
|
||||||
|
$total = disk_total_space($path);
|
||||||
|
$free = disk_free_space($path);
|
||||||
|
|
||||||
|
if ($total !== false && $free !== false) {
|
||||||
|
$used = $total - $free;
|
||||||
|
$info = [
|
||||||
|
'total' => $total,
|
||||||
|
'free' => $free,
|
||||||
|
'used' => $used,
|
||||||
|
'usage_percent' => $total > 0 ? round(($used / $total) * 100, 1) : 0
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $info;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getNetworkInfo(): array
|
||||||
|
{
|
||||||
|
$interfaces = [];
|
||||||
|
|
||||||
|
// Try to get network interfaces from /proc/net/dev
|
||||||
|
if (is_readable('/proc/net/dev')) {
|
||||||
|
$content = file_get_contents('/proc/net/dev');
|
||||||
|
$lines = explode("\n", $content);
|
||||||
|
|
||||||
|
foreach ($lines as $line) {
|
||||||
|
if (strpos($line, ':') !== false) {
|
||||||
|
$parts = explode(':', $line);
|
||||||
|
$interface = trim($parts[0]);
|
||||||
|
if ($interface && $interface !== 'lo' && $interface !== 'Inter-|' && $interface !== ' face') {
|
||||||
|
$interfaces[] = $interface;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ['interfaces' => $interfaces];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getProcessCount(): int
|
||||||
|
{
|
||||||
|
if (is_readable('/proc')) {
|
||||||
|
$dirs = glob('/proc/[0-9]*', GLOB_ONLYDIR);
|
||||||
|
return count($dirs);
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getUptime(): array
|
||||||
|
{
|
||||||
|
if (is_readable('/proc/uptime')) {
|
||||||
|
$uptime = file_get_contents('/proc/uptime');
|
||||||
|
$seconds = (float)explode(' ', $uptime)[0];
|
||||||
|
|
||||||
|
$days = intval(floor($seconds / 86400));
|
||||||
|
$hours = intval(floor(fmod($seconds, 86400) / 3600));
|
||||||
|
$minutes = intval(floor(fmod($seconds, 3600) / 60));
|
||||||
|
|
||||||
|
return [
|
||||||
|
'seconds' => $seconds,
|
||||||
|
'formatted' => sprintf('%dd %dh %dm', $days, $hours, $minutes)
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return ['seconds' => 0, 'formatted' => 'Unknown'];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getCurrentUser(): string
|
||||||
|
{
|
||||||
|
if (function_exists('posix_getpwuid') && function_exists('posix_geteuid')) {
|
||||||
|
$user = posix_getpwuid(posix_geteuid());
|
||||||
|
return $user['name'] ?? 'unknown';
|
||||||
|
}
|
||||||
|
|
||||||
|
return $_SERVER['USER'] ?? $_SERVER['USERNAME'] ?? 'unknown';
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getCPUUsage(): float
|
||||||
|
{
|
||||||
|
// Simple CPU usage calculation
|
||||||
|
if (is_readable('/proc/stat')) {
|
||||||
|
static $lastStats = null;
|
||||||
|
|
||||||
|
$stats = file_get_contents('/proc/stat');
|
||||||
|
preg_match('/cpu\\s+(\\d+)\\s+(\\d+)\\s+(\\d+)\\s+(\\d+)/', $stats, $matches);
|
||||||
|
|
||||||
|
if (count($matches) >= 5) {
|
||||||
|
$idle = intval($matches[4]);
|
||||||
|
$total = array_sum(array_slice($matches, 1, 4));
|
||||||
|
|
||||||
|
if ($lastStats !== null) {
|
||||||
|
$idleDiff = $idle - $lastStats['idle'];
|
||||||
|
$totalDiff = $total - $lastStats['total'];
|
||||||
|
|
||||||
|
if ($totalDiff > 0) {
|
||||||
|
$usage = 100 - (($idleDiff / $totalDiff) * 100);
|
||||||
|
$lastStats = ['idle' => $idle, 'total' => $total];
|
||||||
|
return round($usage, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$lastStats = ['idle' => $idle, 'total' => $total];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getMemoryUsage(): array
|
||||||
|
{
|
||||||
|
return $this->getMemoryInfo();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getDiskUsage(): array
|
||||||
|
{
|
||||||
|
return $this->getDiskInfo();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getNetworkStats(): array
|
||||||
|
{
|
||||||
|
$stats = ['rx_bytes' => 0, 'tx_bytes' => 0];
|
||||||
|
|
||||||
|
if (is_readable('/proc/net/dev')) {
|
||||||
|
$content = file_get_contents('/proc/net/dev');
|
||||||
|
$lines = explode("\n", $content);
|
||||||
|
|
||||||
|
foreach ($lines as $line) {
|
||||||
|
if (strpos($line, ':') !== false && strpos($line, 'lo:') === false) {
|
||||||
|
$lineParts = explode(':', $line);
|
||||||
|
if (count($lineParts) >= 2) {
|
||||||
|
$parts = preg_split('/\s+/', trim($lineParts[1]));
|
||||||
|
if (count($parts) >= 9) {
|
||||||
|
$stats['rx_bytes'] += intval($parts[0]);
|
||||||
|
$stats['tx_bytes'] += intval($parts[8]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $stats;
|
||||||
|
}
|
||||||
|
}
|
||||||
403
src/controllers/TerminalController.php
Archivo normal
403
src/controllers/TerminalController.php
Archivo normal
@@ -0,0 +1,403 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Terminal Controller
|
||||||
|
*
|
||||||
|
* Handles command execution and terminal operations
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace AleShell\Controllers;
|
||||||
|
|
||||||
|
use AleShell\Controllers\BaseController;
|
||||||
|
|
||||||
|
class TerminalController extends BaseController
|
||||||
|
{
|
||||||
|
private array $commandHistory = [];
|
||||||
|
private string $currentDirectory;
|
||||||
|
|
||||||
|
public function __construct($config)
|
||||||
|
{
|
||||||
|
parent::__construct($config);
|
||||||
|
$this->currentDirectory = getcwd();
|
||||||
|
|
||||||
|
// Load command history from session
|
||||||
|
if (isset($_SESSION['command_history'])) {
|
||||||
|
$this->commandHistory = $_SESSION['command_history'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function execute(): void
|
||||||
|
{
|
||||||
|
$this->requireAuth();
|
||||||
|
|
||||||
|
$data = $this->getRequestData();
|
||||||
|
$command = trim($data['command'] ?? '');
|
||||||
|
$workingDir = $data['working_dir'] ?? $this->currentDirectory;
|
||||||
|
$timeout = min($data['timeout'] ?? 30, $this->config->get('limits.command_timeout', 30));
|
||||||
|
|
||||||
|
if (empty($command)) {
|
||||||
|
$this->errorResponse('No command provided');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Add to history
|
||||||
|
$this->addToHistory($command);
|
||||||
|
|
||||||
|
// Check for built-in commands
|
||||||
|
if ($this->isBuiltinCommand($command)) {
|
||||||
|
$result = $this->executeBuiltinCommand($command, $workingDir);
|
||||||
|
} else {
|
||||||
|
$result = $this->executeSystemCommand($command, $workingDir, $timeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->successResponse($result);
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$this->errorResponse('Command execution failed: ' . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getHistory(): void
|
||||||
|
{
|
||||||
|
$this->requireAuth();
|
||||||
|
|
||||||
|
$this->successResponse([
|
||||||
|
'history' => $this->commandHistory,
|
||||||
|
'current_directory' => $this->currentDirectory
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function clearHistory(): void
|
||||||
|
{
|
||||||
|
$this->requireAuth();
|
||||||
|
|
||||||
|
$this->commandHistory = [];
|
||||||
|
$_SESSION['command_history'] = [];
|
||||||
|
|
||||||
|
$this->successResponse(['cleared' => true]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function executeSystemCommand(string $command, string $workingDir, int $timeout): array
|
||||||
|
{
|
||||||
|
// Sanitize command
|
||||||
|
$command = $this->sanitizeCommand($command);
|
||||||
|
|
||||||
|
// Sin restricciones de comandos - ejecutar todo
|
||||||
|
// if (!$this->isCommandAllowed($command)) {
|
||||||
|
// throw new \Exception('Command not allowed by security policy');
|
||||||
|
// }
|
||||||
|
|
||||||
|
// Change to working directory if valid
|
||||||
|
if ($this->security->isPathAllowed($workingDir) && is_dir($workingDir)) {
|
||||||
|
$originalDir = getcwd();
|
||||||
|
chdir($workingDir);
|
||||||
|
$this->currentDirectory = getcwd();
|
||||||
|
} else {
|
||||||
|
$originalDir = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$startTime = microtime(true);
|
||||||
|
$output = '';
|
||||||
|
$exitCode = 0;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Intentar ejecutar como root si está habilitado
|
||||||
|
$runAsRoot = $this->config->get('terminal.run_as_root', false);
|
||||||
|
if ($runAsRoot) {
|
||||||
|
// Intentar usar sudo si está disponible
|
||||||
|
if ($this->commandExists('sudo')) {
|
||||||
|
$command = 'sudo ' . $command;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use proc_open for better control
|
||||||
|
$descriptors = [
|
||||||
|
0 => ['pipe', 'r'], // stdin
|
||||||
|
1 => ['pipe', 'w'], // stdout
|
||||||
|
2 => ['pipe', 'w'] // stderr
|
||||||
|
];
|
||||||
|
|
||||||
|
$env = null;
|
||||||
|
if ($this->config->get('terminal.preserve_environment', true)) {
|
||||||
|
$env = $_ENV;
|
||||||
|
$env['PATH'] = '/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin';
|
||||||
|
$env['USER'] = 'root';
|
||||||
|
$env['HOME'] = '/root';
|
||||||
|
}
|
||||||
|
|
||||||
|
$process = proc_open($command, $descriptors, $pipes, $this->currentDirectory, $env);
|
||||||
|
|
||||||
|
if (!is_resource($process)) {
|
||||||
|
throw new \Exception('Failed to start process');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close stdin
|
||||||
|
fclose($pipes[0]);
|
||||||
|
|
||||||
|
// Set non-blocking mode
|
||||||
|
stream_set_blocking($pipes[1], false);
|
||||||
|
stream_set_blocking($pipes[2], false);
|
||||||
|
|
||||||
|
$stdout = '';
|
||||||
|
$stderr = '';
|
||||||
|
$timeoutReached = false;
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
$status = proc_get_status($process);
|
||||||
|
|
||||||
|
// Check timeout
|
||||||
|
if (microtime(true) - $startTime > $timeout) {
|
||||||
|
proc_terminate($process, SIGKILL);
|
||||||
|
$timeoutReached = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read output
|
||||||
|
$stdout .= stream_get_contents($pipes[1]);
|
||||||
|
$stderr .= stream_get_contents($pipes[2]);
|
||||||
|
|
||||||
|
// Check if process finished
|
||||||
|
if (!$status['running']) {
|
||||||
|
$exitCode = $status['exitcode'];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
usleep(10000); // Sleep 10ms
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get any remaining output
|
||||||
|
$stdout .= stream_get_contents($pipes[1]);
|
||||||
|
$stderr .= stream_get_contents($pipes[2]);
|
||||||
|
|
||||||
|
fclose($pipes[1]);
|
||||||
|
fclose($pipes[2]);
|
||||||
|
proc_close($process);
|
||||||
|
|
||||||
|
if ($timeoutReached) {
|
||||||
|
$output = $stdout . $stderr . "\\n[Command timed out after {$timeout} seconds]";
|
||||||
|
$exitCode = 124; // Standard timeout exit code
|
||||||
|
} else {
|
||||||
|
$output = $stdout . $stderr;
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
throw new \Exception('Command execution failed: ' . $e->getMessage());
|
||||||
|
} finally {
|
||||||
|
// Restore original directory
|
||||||
|
if ($originalDir !== null) {
|
||||||
|
chdir($originalDir);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$executionTime = round((microtime(true) - $startTime) * 1000, 2);
|
||||||
|
|
||||||
|
// Limit output size
|
||||||
|
$maxLines = $this->config->get('limits.max_output_lines', 1000);
|
||||||
|
$lines = explode("\\n", $output);
|
||||||
|
if (count($lines) > $maxLines) {
|
||||||
|
$lines = array_slice($lines, 0, $maxLines);
|
||||||
|
$lines[] = "[Output truncated - showing first {$maxLines} lines]";
|
||||||
|
$output = implode("\\n", $lines);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'command' => $command,
|
||||||
|
'output' => $output,
|
||||||
|
'exit_code' => $exitCode,
|
||||||
|
'execution_time' => $executionTime,
|
||||||
|
'working_directory' => $this->currentDirectory,
|
||||||
|
'timestamp' => time()
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function executeBuiltinCommand(string $command, string $workingDir): array
|
||||||
|
{
|
||||||
|
$parts = explode(' ', $command, 2);
|
||||||
|
$cmd = $parts[0];
|
||||||
|
$args = $parts[1] ?? '';
|
||||||
|
|
||||||
|
$output = '';
|
||||||
|
$exitCode = 0;
|
||||||
|
|
||||||
|
switch ($cmd) {
|
||||||
|
case 'cd':
|
||||||
|
$output = $this->handleCdCommand($args);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'pwd':
|
||||||
|
$output = $this->currentDirectory;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'help':
|
||||||
|
$output = $this->getHelpText();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'history':
|
||||||
|
$output = $this->formatHistoryOutput();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'clear':
|
||||||
|
$output = '[Terminal cleared]';
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
$output = "Unknown built-in command: {$cmd}";
|
||||||
|
$exitCode = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'command' => $command,
|
||||||
|
'output' => $output,
|
||||||
|
'exit_code' => $exitCode,
|
||||||
|
'execution_time' => 0,
|
||||||
|
'working_directory' => $this->currentDirectory,
|
||||||
|
'timestamp' => time()
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function isBuiltinCommand(string $command): bool
|
||||||
|
{
|
||||||
|
$cmd = explode(' ', $command)[0];
|
||||||
|
return in_array($cmd, ['cd', 'pwd', 'help', 'history', 'clear']);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function handleCdCommand(string $path): string
|
||||||
|
{
|
||||||
|
if (empty($path)) {
|
||||||
|
$path = $_SERVER['HOME'] ?? '/';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle relative paths
|
||||||
|
if ($path[0] !== '/') {
|
||||||
|
$path = $this->currentDirectory . '/' . $path;
|
||||||
|
}
|
||||||
|
|
||||||
|
$realPath = realpath($path);
|
||||||
|
|
||||||
|
if ($realPath === false) {
|
||||||
|
return "cd: no such file or directory: {$path}";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!is_dir($realPath)) {
|
||||||
|
return "cd: not a directory: {$path}";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$this->security->isPathAllowed($realPath)) {
|
||||||
|
return "cd: permission denied: {$path}";
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->currentDirectory = $realPath;
|
||||||
|
$_SESSION['current_directory'] = $this->currentDirectory;
|
||||||
|
|
||||||
|
return $this->currentDirectory;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function sanitizeCommand(string $command): string
|
||||||
|
{
|
||||||
|
// Minimal sanitization - allow all command sequences
|
||||||
|
// This permits pipes, redirections, command chaining, etc.
|
||||||
|
$command = trim($command);
|
||||||
|
|
||||||
|
// No filtering applied - full command execution freedom
|
||||||
|
return $command;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function isCommandAllowed(string $command): bool
|
||||||
|
{
|
||||||
|
// ⚠️ TODAS LAS RESTRICCIONES DE SEGURIDAD REMOVIDAS ⚠️
|
||||||
|
// PERMITIR EJECUCIÓN DE CUALQUIER COMANDO DEL SISTEMA
|
||||||
|
// Esto incluye comandos peligrosos como rm, sudo, chmod, etc.
|
||||||
|
return true;
|
||||||
|
|
||||||
|
// Additional security checks can be added here - DESHABILITADO
|
||||||
|
}
|
||||||
|
|
||||||
|
private function addToHistory(string $command): void
|
||||||
|
{
|
||||||
|
$this->commandHistory[] = [
|
||||||
|
'command' => $command,
|
||||||
|
'timestamp' => time(),
|
||||||
|
'directory' => $this->currentDirectory
|
||||||
|
];
|
||||||
|
|
||||||
|
// Keep only last 100 commands
|
||||||
|
if (count($this->commandHistory) > 100) {
|
||||||
|
$this->commandHistory = array_slice($this->commandHistory, -100);
|
||||||
|
}
|
||||||
|
|
||||||
|
$_SESSION['command_history'] = $this->commandHistory;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function formatHistoryOutput(): string
|
||||||
|
{
|
||||||
|
if (empty($this->commandHistory)) {
|
||||||
|
return "╔══════════════════════════════════════════════════════════════════════════════╗\n" .
|
||||||
|
"║ Command History (Empty) ║\n" .
|
||||||
|
"╚══════════════════════════════════════════════════════════════════════════════╝\n\n" .
|
||||||
|
"No commands in history yet. Start typing commands to build your history!";
|
||||||
|
}
|
||||||
|
|
||||||
|
$output = "╔══════════════════════════════════════════════════════════════════════════════╗\n" .
|
||||||
|
"║ Command History ║\n" .
|
||||||
|
"╚══════════════════════════════════════════════════════════════════════════════╝\n\n";
|
||||||
|
|
||||||
|
$maxIndex = count($this->commandHistory);
|
||||||
|
$indexWidth = strlen((string)$maxIndex);
|
||||||
|
|
||||||
|
foreach ($this->commandHistory as $index => $item) {
|
||||||
|
$lineNumber = str_pad($index + 1, $indexWidth, ' ', STR_PAD_LEFT);
|
||||||
|
$timestamp = date('H:i:s', $item['timestamp']);
|
||||||
|
$command = $item['command'];
|
||||||
|
|
||||||
|
// Truncate long commands for better display
|
||||||
|
if (strlen($command) > 60) {
|
||||||
|
$command = substr($command, 0, 57) . '...';
|
||||||
|
}
|
||||||
|
|
||||||
|
$output .= sprintf(" %{$indexWidth}s │ %s │ %s\n", $lineNumber, $timestamp, $command);
|
||||||
|
}
|
||||||
|
|
||||||
|
$output .= "\n┌─ Summary ──────────────────────────────────────────────────────────────────┐\n" .
|
||||||
|
sprintf("│ Total commands: %-56s │\n", count($this->commandHistory)) .
|
||||||
|
"│ Use ↑/↓ arrows to navigate history │\n" .
|
||||||
|
"└───────────────────────────────────────────────────────────────────────────┘";
|
||||||
|
|
||||||
|
return $output;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getHelpText(): string
|
||||||
|
{
|
||||||
|
return "╔══════════════════════════════════════════════════════════════════════════════╗\n" .
|
||||||
|
"║ AleShell Help ║\n" .
|
||||||
|
"╚══════════════════════════════════════════════════════════════════════════════╝\n\n" .
|
||||||
|
"Available Commands:\n\n" .
|
||||||
|
"┌─ Built-in Commands ────────────────────────────────────────────────────────┐\n" .
|
||||||
|
"│ help Show this help message │\n" .
|
||||||
|
"│ history Show command history │\n" .
|
||||||
|
"│ clear Clear the terminal screen │\n" .
|
||||||
|
"│ cd [directory] Change current directory │\n" .
|
||||||
|
"│ pwd Print current working directory │\n" .
|
||||||
|
"└───────────────────────────────────────────────────────────────────────────┘\n\n" .
|
||||||
|
"┌─ System Commands ──────────────────────────────────────────────────────────┐\n" .
|
||||||
|
"│ Any system command is supported (ls, ps, top, etc.) │\n" .
|
||||||
|
"│ Use pipes, redirections, and command chaining as usual │\n" .
|
||||||
|
"│ Examples: │\n" .
|
||||||
|
"│ ls -la | grep php │\n" .
|
||||||
|
"│ ps aux | head -10 │\n" .
|
||||||
|
"│ find /var -name '*.log' 2>/dev/null │\n" .
|
||||||
|
"└───────────────────────────────────────────────────────────────────────────┘\n\n" .
|
||||||
|
"┌─ Navigation ──────────────────────────────────────────────────────────────┐\n" .
|
||||||
|
"│ ↑/↓ arrows Navigate through command history │\n" .
|
||||||
|
"│ Tab Auto-complete (when implemented) │\n" .
|
||||||
|
"│ Ctrl+C Cancel current command (when implemented) │\n" .
|
||||||
|
"└───────────────────────────────────────────────────────────────────────────┘\n\n" .
|
||||||
|
"┌─ Features ────────────────────────────────────────────────────────────────┐\n" .
|
||||||
|
"│ • Multi-tab terminal support │\n" .
|
||||||
|
"│ • Command history with timestamps │\n" .
|
||||||
|
"│ • Working directory tracking │\n" .
|
||||||
|
"│ • Output formatting and truncation │\n" .
|
||||||
|
"│ • Security restrictions (configurable) │\n" .
|
||||||
|
"└───────────────────────────────────────────────────────────────────────────┘\n\n" .
|
||||||
|
"For more information about system commands, use 'man <command>' or '<command> --help'";
|
||||||
|
}
|
||||||
|
}
|
||||||
96
src/core/Autoloader.php
Archivo normal
96
src/core/Autoloader.php
Archivo normal
@@ -0,0 +1,96 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* AleShell Autoloader
|
||||||
|
*
|
||||||
|
* Simple PSR-4 compatible autoloader for AleShell classes
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace AleShell\Core;
|
||||||
|
|
||||||
|
class Autoloader
|
||||||
|
{
|
||||||
|
private static $instance = null;
|
||||||
|
private array $prefixes = [];
|
||||||
|
|
||||||
|
private function __construct()
|
||||||
|
{
|
||||||
|
// Register AleShell namespace
|
||||||
|
$this->addNamespace('AleShell\\', dirname(__DIR__) . '/');
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getInstance(): self
|
||||||
|
{
|
||||||
|
if (self::$instance === null) {
|
||||||
|
self::$instance = new self();
|
||||||
|
}
|
||||||
|
return self::$instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function register(): void
|
||||||
|
{
|
||||||
|
spl_autoload_register([$this, 'loadClass']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function addNamespace(string $prefix, string $baseDir): void
|
||||||
|
{
|
||||||
|
$prefix = trim($prefix, '\\') . '\\';
|
||||||
|
$baseDir = rtrim($baseDir, DIRECTORY_SEPARATOR) . '/';
|
||||||
|
|
||||||
|
if (!isset($this->prefixes[$prefix])) {
|
||||||
|
$this->prefixes[$prefix] = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
array_push($this->prefixes[$prefix], $baseDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function loadClass(string $class): ?string
|
||||||
|
{
|
||||||
|
$prefix = $class;
|
||||||
|
|
||||||
|
while (false !== $pos = strrpos($prefix, '\\')) {
|
||||||
|
$prefix = substr($class, 0, $pos + 1);
|
||||||
|
$relativeClass = substr($class, $pos + 1);
|
||||||
|
|
||||||
|
$mappedFile = $this->loadMappedFile($prefix, $relativeClass);
|
||||||
|
if ($mappedFile) {
|
||||||
|
return $mappedFile;
|
||||||
|
}
|
||||||
|
|
||||||
|
$prefix = rtrim($prefix, '\\');
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function loadMappedFile(string $prefix, string $relativeClass): ?string
|
||||||
|
{
|
||||||
|
if (!isset($this->prefixes[$prefix])) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($this->prefixes[$prefix] as $baseDir) {
|
||||||
|
$file = $baseDir . str_replace('\\', '/', $relativeClass) . '.php';
|
||||||
|
|
||||||
|
if ($this->requireFile($file)) {
|
||||||
|
return $file;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function requireFile(string $file): bool
|
||||||
|
{
|
||||||
|
if (file_exists($file)) {
|
||||||
|
require_once $file;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-register the autoloader
|
||||||
|
if (!class_exists('AleShell\\Core\\Autoloader', false)) {
|
||||||
|
$autoloader = Autoloader::getInstance();
|
||||||
|
$autoloader->register();
|
||||||
|
}
|
||||||
405
src/core/Bootstrap.php
Archivo normal
405
src/core/Bootstrap.php
Archivo normal
@@ -0,0 +1,405 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* AleShell Bootstrap
|
||||||
|
*
|
||||||
|
* Main application bootstrap class that initializes and runs AleShell
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace AleShell\Core;
|
||||||
|
|
||||||
|
class Bootstrap
|
||||||
|
{
|
||||||
|
private $config;
|
||||||
|
private $security;
|
||||||
|
private $session;
|
||||||
|
private $router;
|
||||||
|
private $moduleManager;
|
||||||
|
private bool $initialized = false;
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
// Load dependencies with error handling
|
||||||
|
try {
|
||||||
|
// Ensure all required classes are loaded
|
||||||
|
$this->loadDependencies();
|
||||||
|
|
||||||
|
$this->config = new ConfigManager();
|
||||||
|
$this->security = new \AleShell\Security\SecurityManager($this->config);
|
||||||
|
$this->session = new SessionManager($this->config);
|
||||||
|
$this->router = new Router($this->config);
|
||||||
|
$this->moduleManager = new ModuleManager($this->config);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$this->handleBootstrapError($e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function loadDependencies(): void
|
||||||
|
{
|
||||||
|
$requiredFiles = [
|
||||||
|
'ConfigManager.php',
|
||||||
|
'../security/SecurityManager.php',
|
||||||
|
'SessionManager.php',
|
||||||
|
'Router.php',
|
||||||
|
'ModuleManager.php'
|
||||||
|
];
|
||||||
|
|
||||||
|
$baseDir = __DIR__;
|
||||||
|
|
||||||
|
foreach ($requiredFiles as $file) {
|
||||||
|
$fullPath = $baseDir . '/' . $file;
|
||||||
|
if (file_exists($fullPath)) {
|
||||||
|
require_once $fullPath;
|
||||||
|
} else {
|
||||||
|
throw new \Exception("Required file not found: {$fullPath}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load controllers in proper order (BaseController first)
|
||||||
|
$controllerDir = dirname($baseDir) . '/controllers';
|
||||||
|
if (is_dir($controllerDir)) {
|
||||||
|
// Load BaseController first
|
||||||
|
$baseController = $controllerDir . '/BaseController.php';
|
||||||
|
if (file_exists($baseController)) {
|
||||||
|
require_once $baseController;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Then load other controllers
|
||||||
|
$controllers = glob($controllerDir . '/*.php');
|
||||||
|
foreach ($controllers as $controller) {
|
||||||
|
// Skip BaseController as it's already loaded
|
||||||
|
if (basename($controller) !== 'BaseController.php') {
|
||||||
|
require_once $controller;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function initialize(): void
|
||||||
|
{
|
||||||
|
if ($this->initialized) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start output buffering to prevent headers already sent errors
|
||||||
|
if (!ob_get_level()) {
|
||||||
|
ob_start();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load configuration
|
||||||
|
$this->config->load();
|
||||||
|
|
||||||
|
// Initialize security
|
||||||
|
$this->security->initialize();
|
||||||
|
|
||||||
|
// Start session management
|
||||||
|
$this->session->start();
|
||||||
|
|
||||||
|
// Handle static assets first (before authentication check)
|
||||||
|
if ($this->isAssetRequest()) {
|
||||||
|
$this->serveStaticAsset();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle login request first
|
||||||
|
if ($this->isLoginRequest()) {
|
||||||
|
$this->handleLogin();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allow auth routes without authentication
|
||||||
|
if ($this->isAuthRoute()) {
|
||||||
|
// Initialize modules and set up routing for auth routes
|
||||||
|
$this->moduleManager->initialize();
|
||||||
|
$this->setupRoutes();
|
||||||
|
$this->initialized = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check authentication
|
||||||
|
if (!$this->security->isAuthenticated()) {
|
||||||
|
$this->showLoginForm();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize modules
|
||||||
|
$this->moduleManager->initialize();
|
||||||
|
|
||||||
|
// Set up routing
|
||||||
|
$this->setupRoutes();
|
||||||
|
|
||||||
|
$this->initialized = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function run(): void
|
||||||
|
{
|
||||||
|
if (!$this->initialized) {
|
||||||
|
throw new \Exception('Bootstrap not initialized');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle the request
|
||||||
|
$this->router->dispatch();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function isAuthRoute(): bool
|
||||||
|
{
|
||||||
|
$uri = $_SERVER['REQUEST_URI'] ?? '';
|
||||||
|
$path = parse_url($uri, PHP_URL_PATH);
|
||||||
|
|
||||||
|
// Remove base path if present
|
||||||
|
$basePath = rtrim(dirname($_SERVER['SCRIPT_NAME']), '/');
|
||||||
|
if ($basePath && strpos($path, $basePath) === 0) {
|
||||||
|
$path = substr($path, strlen($basePath));
|
||||||
|
}
|
||||||
|
|
||||||
|
$path = '/' . trim($path, '/');
|
||||||
|
|
||||||
|
// Allow auth routes without authentication
|
||||||
|
return strpos($path, '/auth/') === 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function isLoginRequest(): bool
|
||||||
|
{
|
||||||
|
return isset($_POST['action']) && $_POST['action'] === 'login';
|
||||||
|
}
|
||||||
|
|
||||||
|
private function isAssetRequest(): bool
|
||||||
|
{
|
||||||
|
$uri = $_SERVER['REQUEST_URI'] ?? '';
|
||||||
|
return strpos($uri, '/assets/') === 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function serveStaticAsset(): void
|
||||||
|
{
|
||||||
|
$uri = $_SERVER['REQUEST_URI'];
|
||||||
|
|
||||||
|
// Remove query string
|
||||||
|
$path = parse_url($uri, PHP_URL_PATH);
|
||||||
|
|
||||||
|
// Remove leading slash and get file path
|
||||||
|
$filePath = ltrim($path, '/');
|
||||||
|
|
||||||
|
// Security check - only allow assets directory
|
||||||
|
if (strpos($filePath, 'assets/') !== 0) {
|
||||||
|
http_response_code(403);
|
||||||
|
echo 'Access denied';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$fullPath = ALESHELL_ROOT . '/' . $filePath;
|
||||||
|
|
||||||
|
// Check if file exists
|
||||||
|
if (!file_exists($fullPath) || !is_file($fullPath)) {
|
||||||
|
http_response_code(404);
|
||||||
|
echo 'File not found';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set appropriate content type
|
||||||
|
$extension = strtolower(pathinfo($fullPath, PATHINFO_EXTENSION));
|
||||||
|
$contentTypes = [
|
||||||
|
'js' => 'application/javascript',
|
||||||
|
'css' => 'text/css',
|
||||||
|
'png' => 'image/png',
|
||||||
|
'jpg' => 'image/jpeg',
|
||||||
|
'jpeg' => 'image/jpeg',
|
||||||
|
'gif' => 'image/gif',
|
||||||
|
'svg' => 'image/svg+xml',
|
||||||
|
'ico' => 'image/x-icon'
|
||||||
|
];
|
||||||
|
|
||||||
|
if (isset($contentTypes[$extension])) {
|
||||||
|
header('Content-Type: ' . $contentTypes[$extension]);
|
||||||
|
} else {
|
||||||
|
header('Content-Type: application/octet-stream');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cache headers for static assets
|
||||||
|
header('Cache-Control: public, max-age=3600');
|
||||||
|
|
||||||
|
// Output file content
|
||||||
|
readfile($fullPath);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function handleLogin(): void
|
||||||
|
{
|
||||||
|
$password = $_POST['password'] ?? '';
|
||||||
|
|
||||||
|
if (empty($password)) {
|
||||||
|
$this->showLoginForm('Password is required');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if ($this->security->authenticate($password)) {
|
||||||
|
// Login successful - redirect to main interface
|
||||||
|
header('Location: ' . $_SERVER['REQUEST_URI']);
|
||||||
|
exit;
|
||||||
|
} else {
|
||||||
|
$this->showLoginForm('Invalid password');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$this->showLoginForm('Authentication error: ' . $e->getMessage());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function showLoginForm(string $errorMessage = ''): void
|
||||||
|
{
|
||||||
|
// Simple login form for now
|
||||||
|
echo $this->getLoginHTML($errorMessage);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getLoginHTML(string $errorMessage = ''): string
|
||||||
|
{
|
||||||
|
$errorDiv = '';
|
||||||
|
if (!empty($errorMessage)) {
|
||||||
|
$errorDiv = '<div class="error-message" style="background: #fee; color: #c33; padding: 0.75rem; border-radius: 6px; margin-bottom: 1rem; border: 1px solid #fcc; font-size: 0.9rem;">' . htmlspecialchars($errorMessage) . '</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
return '<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>AleShell - Login</title>
|
||||||
|
<style>
|
||||||
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.login-container {
|
||||||
|
background: white;
|
||||||
|
padding: 2rem;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 20px 40px rgba(0,0,0,0.1);
|
||||||
|
min-width: 320px;
|
||||||
|
}
|
||||||
|
.login-header {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
.login-header h1 {
|
||||||
|
color: #333;
|
||||||
|
font-size: 2rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
.login-header p {
|
||||||
|
color: #666;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
.form-group label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
color: #333;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
.form-group input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.75rem;
|
||||||
|
border: 2px solid #e1e5e9;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 1rem;
|
||||||
|
transition: border-color 0.3s;
|
||||||
|
}
|
||||||
|
.form-group input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #667eea;
|
||||||
|
}
|
||||||
|
.login-btn {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.75rem;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 1rem;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: transform 0.2s;
|
||||||
|
}
|
||||||
|
.login-btn:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
.version {
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 1rem;
|
||||||
|
color: #999;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="login-container">
|
||||||
|
<div class="login-header">
|
||||||
|
<h1>🚀 AleShell</h1>
|
||||||
|
<p>Modern Web Shell Interface</p>
|
||||||
|
</div>
|
||||||
|
' . $errorDiv . '
|
||||||
|
<form method="post">
|
||||||
|
<input type="hidden" name="action" value="login">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="password">Password</label>
|
||||||
|
<input type="password" id="password" name="password" required autocomplete="current-password">
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="login-btn">Access Shell</button>
|
||||||
|
</form>
|
||||||
|
<div class="version">v' . ALESHELL_VERSION . '</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>';
|
||||||
|
}
|
||||||
|
|
||||||
|
private function setupRoutes(): void
|
||||||
|
{
|
||||||
|
// API routes
|
||||||
|
$this->router->addRoute('GET', '/api/system/info', 'SystemController@getInfo');
|
||||||
|
$this->router->addRoute('GET', '/api/config', 'SystemController@getConfig');
|
||||||
|
$this->router->addRoute('POST', '/api/files/list', 'FileController@listFiles');
|
||||||
|
$this->router->addRoute('POST', '/api/files/read', 'FileController@readFile');
|
||||||
|
$this->router->addRoute('POST', '/api/files/write', 'FileController@writeFile');
|
||||||
|
$this->router->addRoute('POST', '/api/files/delete', 'FileController@deleteFile');
|
||||||
|
$this->router->addRoute('POST', '/api/files/create-directory', 'FileController@createDirectory');
|
||||||
|
$this->router->addRoute('POST', '/api/files/rename', 'FileController@renameFile');
|
||||||
|
$this->router->addRoute('POST', '/api/files/upload', 'FileController@uploadFile');
|
||||||
|
$this->router->addRoute('POST', '/api/terminal/execute', 'TerminalController@execute');
|
||||||
|
$this->router->addRoute('GET', '/api/terminal/history', 'TerminalController@getHistory');
|
||||||
|
$this->router->addRoute('POST', '/api/terminal/clear-history', 'TerminalController@clearHistory');
|
||||||
|
$this->router->addRoute('GET', '/api/processes/list', 'ProcessController@listProcesses');
|
||||||
|
$this->router->addRoute('POST', '/api/processes/kill', 'ProcessController@killProcess');
|
||||||
|
$this->router->addRoute('GET', '/api/processes/info', 'ProcessController@getProcessInfo');
|
||||||
|
$this->router->addRoute('GET', '/api/network/connections', 'NetworkController@getConnections');
|
||||||
|
$this->router->addRoute('POST', '/api/network/ping', 'NetworkController@ping');
|
||||||
|
$this->router->addRoute('POST', '/api/network/traceroute', 'NetworkController@traceroute');
|
||||||
|
$this->router->addRoute('POST', '/api/network/portscan', 'NetworkController@portScan');
|
||||||
|
$this->router->addRoute('GET', '/api/network/interfaces', 'NetworkController@getInterfaces');
|
||||||
|
$this->router->addRoute('POST', '/api/database/connect', 'DatabaseController@connect');
|
||||||
|
$this->router->addRoute('POST', '/api/database/disconnect', 'DatabaseController@disconnect');
|
||||||
|
$this->router->addRoute('GET', '/api/database/databases', 'DatabaseController@getDatabases');
|
||||||
|
$this->router->addRoute('GET', '/api/database/tables', 'DatabaseController@getTables');
|
||||||
|
$this->router->addRoute('POST', '/api/database/query', 'DatabaseController@executeQuery');
|
||||||
|
$this->router->addRoute('GET', '/api/database/connections', 'DatabaseController@getConnections');
|
||||||
|
|
||||||
|
// Module routes
|
||||||
|
$this->router->addRoute('GET', '/modules/*', 'ModuleController@load');
|
||||||
|
|
||||||
|
// Dashboard API route (not main route)
|
||||||
|
$this->router->addRoute('GET', '/api/dashboard', 'MainController@dashboard');
|
||||||
|
|
||||||
|
// Authentication routes
|
||||||
|
$this->router->addRoute('POST', '/auth/login', 'AuthController@login');
|
||||||
|
$this->router->addRoute('POST', '/auth/logout', 'AuthController@logout');
|
||||||
|
$this->router->addRoute('GET', '/auth/status', 'AuthController@status');
|
||||||
|
|
||||||
|
// Note: '/' route is handled by Router's default showMainInterface() method
|
||||||
|
}
|
||||||
|
}
|
||||||
129
src/core/ConfigManager.php
Archivo normal
129
src/core/ConfigManager.php
Archivo normal
@@ -0,0 +1,129 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Configuration Manager
|
||||||
|
*
|
||||||
|
* Manages application configuration and settings
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace AleShell\Core;
|
||||||
|
|
||||||
|
class ConfigManager
|
||||||
|
{
|
||||||
|
private array $config = [];
|
||||||
|
private string $configFile;
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->configFile = ALESHELL_SRC . '/config/config.php';
|
||||||
|
$this->setDefaults();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function load(): void
|
||||||
|
{
|
||||||
|
if (file_exists($this->configFile)) {
|
||||||
|
$userConfig = require $this->configFile;
|
||||||
|
if (is_array($userConfig)) {
|
||||||
|
$this->config = array_merge($this->config, $userConfig);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function get(string $key, $default = null)
|
||||||
|
{
|
||||||
|
$keys = explode('.', $key);
|
||||||
|
$value = $this->config;
|
||||||
|
|
||||||
|
foreach ($keys as $k) {
|
||||||
|
if (!isset($value[$k])) {
|
||||||
|
return $default;
|
||||||
|
}
|
||||||
|
$value = $value[$k];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function set(string $key, $value): void
|
||||||
|
{
|
||||||
|
$keys = explode('.', $key);
|
||||||
|
$config = &$this->config;
|
||||||
|
|
||||||
|
foreach ($keys as $k) {
|
||||||
|
if (!isset($config[$k]) || !is_array($config[$k])) {
|
||||||
|
$config[$k] = [];
|
||||||
|
}
|
||||||
|
$config = &$config[$k];
|
||||||
|
}
|
||||||
|
|
||||||
|
$config = $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function save(): bool
|
||||||
|
{
|
||||||
|
$configContent = "<?php\n/**\n * AleShell Configuration\n * Generated on " . date('Y-m-d H:i:s') . "\n */\n\nreturn " . var_export($this->config, true) . ";\n";
|
||||||
|
|
||||||
|
$dir = dirname($this->configFile);
|
||||||
|
if (!is_dir($dir)) {
|
||||||
|
mkdir($dir, 0755, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
return file_put_contents($this->configFile, $configContent) !== false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function setDefaults(): void
|
||||||
|
{
|
||||||
|
$this->config = [
|
||||||
|
'app' => [
|
||||||
|
'name' => 'AleShell',
|
||||||
|
'version' => ALESHELL_VERSION,
|
||||||
|
'debug' => false,
|
||||||
|
'timezone' => 'UTC',
|
||||||
|
'max_execution_time' => 300,
|
||||||
|
'memory_limit' => '512M'
|
||||||
|
],
|
||||||
|
'security' => [
|
||||||
|
'password' => password_hash('aleshell', PASSWORD_DEFAULT), // Default password
|
||||||
|
'session_timeout' => 3600, // 1 hour
|
||||||
|
'csrf_protection' => true,
|
||||||
|
'rate_limiting' => true,
|
||||||
|
'max_attempts' => 5,
|
||||||
|
'lockout_time' => 300, // 5 minutes
|
||||||
|
'allowed_ips' => [], // Empty = allow all
|
||||||
|
'blocked_ips' => []
|
||||||
|
],
|
||||||
|
'features' => [
|
||||||
|
'file_manager' => true,
|
||||||
|
'terminal' => true,
|
||||||
|
'code_editor' => true,
|
||||||
|
'process_manager' => true,
|
||||||
|
'network_tools' => true,
|
||||||
|
'database_tools' => true,
|
||||||
|
'system_info' => true,
|
||||||
|
'log_viewer' => true
|
||||||
|
],
|
||||||
|
'ui' => [
|
||||||
|
'theme' => 'dark',
|
||||||
|
'language' => 'en',
|
||||||
|
'items_per_page' => 50,
|
||||||
|
'auto_refresh' => false,
|
||||||
|
'show_hidden_files' => false
|
||||||
|
],
|
||||||
|
'limits' => [
|
||||||
|
'max_file_size' => 50 * 1024 * 1024, // 50MB
|
||||||
|
'max_upload_size' => 100 * 1024 * 1024, // 100MB
|
||||||
|
'max_output_lines' => 1000,
|
||||||
|
'command_timeout' => 30
|
||||||
|
],
|
||||||
|
'paths' => [
|
||||||
|
'temp_dir' => sys_get_temp_dir(),
|
||||||
|
'upload_dir' => ALESHELL_ROOT . '/uploads',
|
||||||
|
'log_dir' => ALESHELL_ROOT . '/logs'
|
||||||
|
]
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getAll(): array
|
||||||
|
{
|
||||||
|
return $this->config;
|
||||||
|
}
|
||||||
|
}
|
||||||
162
src/core/ModuleManager.php
Archivo normal
162
src/core/ModuleManager.php
Archivo normal
@@ -0,0 +1,162 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Module Manager
|
||||||
|
*
|
||||||
|
* Manages module loading, dependencies, and lifecycle
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace AleShell\Core;
|
||||||
|
|
||||||
|
class ModuleManager
|
||||||
|
{
|
||||||
|
private ConfigManager $config;
|
||||||
|
private array $modules = [];
|
||||||
|
private array $loadedModules = [];
|
||||||
|
private string $modulesPath;
|
||||||
|
|
||||||
|
public function __construct(ConfigManager $config)
|
||||||
|
{
|
||||||
|
$this->config = $config;
|
||||||
|
$this->modulesPath = ALESHELL_SRC . '/modules';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function initialize(): void
|
||||||
|
{
|
||||||
|
$this->discoverModules();
|
||||||
|
$this->loadEnabledModules();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function loadModule(string $name): bool
|
||||||
|
{
|
||||||
|
if (isset($this->loadedModules[$name])) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isset($this->modules[$name])) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$moduleInfo = $this->modules[$name];
|
||||||
|
|
||||||
|
// Check dependencies
|
||||||
|
if (!empty($moduleInfo['dependencies'])) {
|
||||||
|
foreach ($moduleInfo['dependencies'] as $dependency) {
|
||||||
|
if (!$this->loadModule($dependency)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load module file
|
||||||
|
$moduleFile = $this->modulesPath . '/' . $name . '/' . $name . '.php';
|
||||||
|
if (file_exists($moduleFile)) {
|
||||||
|
require_once $moduleFile;
|
||||||
|
|
||||||
|
$moduleClass = "AleShell\\Modules\\" . ucfirst($name) . "\\Module";
|
||||||
|
if (class_exists($moduleClass)) {
|
||||||
|
$this->loadedModules[$name] = new $moduleClass($this->config);
|
||||||
|
$this->loadedModules[$name]->initialize();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getModule(string $name)
|
||||||
|
{
|
||||||
|
return $this->loadedModules[$name] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getLoadedModules(): array
|
||||||
|
{
|
||||||
|
return array_keys($this->loadedModules);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getAvailableModules(): array
|
||||||
|
{
|
||||||
|
return array_keys($this->modules);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isModuleLoaded(string $name): bool
|
||||||
|
{
|
||||||
|
return isset($this->loadedModules[$name]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function unloadModule(string $name): bool
|
||||||
|
{
|
||||||
|
if (!isset($this->loadedModules[$name])) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if other modules depend on this one
|
||||||
|
foreach ($this->loadedModules as $loadedName => $module) {
|
||||||
|
if ($loadedName !== $name) {
|
||||||
|
$info = $this->modules[$loadedName];
|
||||||
|
if (in_array($name, $info['dependencies'] ?? [])) {
|
||||||
|
return false; // Cannot unload, other modules depend on it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call cleanup if method exists
|
||||||
|
if (method_exists($this->loadedModules[$name], 'cleanup')) {
|
||||||
|
$this->loadedModules[$name]->cleanup();
|
||||||
|
}
|
||||||
|
|
||||||
|
unset($this->loadedModules[$name]);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function discoverModules(): void
|
||||||
|
{
|
||||||
|
if (!is_dir($this->modulesPath)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$directories = glob($this->modulesPath . '/*', GLOB_ONLYDIR);
|
||||||
|
|
||||||
|
foreach ($directories as $dir) {
|
||||||
|
$moduleName = basename($dir);
|
||||||
|
$configFile = $dir . '/module.json';
|
||||||
|
|
||||||
|
if (file_exists($configFile)) {
|
||||||
|
$config = json_decode(file_get_contents($configFile), true);
|
||||||
|
if ($config) {
|
||||||
|
$this->modules[$moduleName] = array_merge([
|
||||||
|
'name' => $moduleName,
|
||||||
|
'version' => '1.0.0',
|
||||||
|
'description' => '',
|
||||||
|
'author' => '',
|
||||||
|
'dependencies' => [],
|
||||||
|
'enabled' => true
|
||||||
|
], $config);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Default module info if no config file
|
||||||
|
$this->modules[$moduleName] = [
|
||||||
|
'name' => $moduleName,
|
||||||
|
'version' => '1.0.0',
|
||||||
|
'description' => "Module $moduleName",
|
||||||
|
'author' => 'AleShell',
|
||||||
|
'dependencies' => [],
|
||||||
|
'enabled' => true
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function loadEnabledModules(): void
|
||||||
|
{
|
||||||
|
$enabledFeatures = $this->config->get('features', []);
|
||||||
|
|
||||||
|
foreach ($this->modules as $name => $info) {
|
||||||
|
$featureName = strtolower($name);
|
||||||
|
$featureName = str_replace(['_', '-'], '_', $featureName);
|
||||||
|
|
||||||
|
if ($info['enabled'] && ($enabledFeatures[$featureName] ?? true)) {
|
||||||
|
$this->loadModule($name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
3693
src/core/Router.php
Archivo normal
3693
src/core/Router.php
Archivo normal
La diferencia del archivo ha sido suprimido porque es demasiado grande
Cargar Diff
108
src/core/SessionManager.php
Archivo normal
108
src/core/SessionManager.php
Archivo normal
@@ -0,0 +1,108 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Session Manager
|
||||||
|
*
|
||||||
|
* Handles session management and security
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace AleShell\Core;
|
||||||
|
|
||||||
|
class SessionManager
|
||||||
|
{
|
||||||
|
private ConfigManager $config;
|
||||||
|
|
||||||
|
public function __construct(ConfigManager $config)
|
||||||
|
{
|
||||||
|
$this->config = $config;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function start(): void
|
||||||
|
{
|
||||||
|
if (session_status() === PHP_SESSION_NONE) {
|
||||||
|
// Configure session settings
|
||||||
|
ini_set('session.cookie_httponly', '1');
|
||||||
|
ini_set('session.use_only_cookies', '1');
|
||||||
|
ini_set('session.cookie_secure', $this->isHTTPS() ? '1' : '0');
|
||||||
|
ini_set('session.cookie_samesite', 'Strict');
|
||||||
|
|
||||||
|
// Set session timeout
|
||||||
|
$timeout = $this->config->get('security.session_timeout', 3600);
|
||||||
|
ini_set('session.gc_maxlifetime', $timeout);
|
||||||
|
|
||||||
|
// Generate unique session name
|
||||||
|
session_name('ALESHELL_' . substr(md5(__FILE__), 0, 8));
|
||||||
|
|
||||||
|
// Start session
|
||||||
|
session_start();
|
||||||
|
|
||||||
|
// Regenerate session ID periodically for security
|
||||||
|
if (!isset($_SESSION['created'])) {
|
||||||
|
$_SESSION['created'] = time();
|
||||||
|
} elseif (time() - $_SESSION['created'] > 1800) { // 30 minutes
|
||||||
|
session_regenerate_id(true);
|
||||||
|
$_SESSION['created'] = time();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function destroy(): void
|
||||||
|
{
|
||||||
|
if (session_status() === PHP_SESSION_ACTIVE) {
|
||||||
|
$_SESSION = [];
|
||||||
|
|
||||||
|
if (isset($_COOKIE[session_name()])) {
|
||||||
|
setcookie(session_name(), '', time() - 3600, '/');
|
||||||
|
}
|
||||||
|
|
||||||
|
session_destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function regenerateId(): void
|
||||||
|
{
|
||||||
|
if (session_status() === PHP_SESSION_ACTIVE) {
|
||||||
|
session_regenerate_id(true);
|
||||||
|
$_SESSION['created'] = time();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function get(string $key, $default = null)
|
||||||
|
{
|
||||||
|
return $_SESSION[$key] ?? $default;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function set(string $key, $value): void
|
||||||
|
{
|
||||||
|
$_SESSION[$key] = $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function has(string $key): bool
|
||||||
|
{
|
||||||
|
return isset($_SESSION[$key]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function remove(string $key): void
|
||||||
|
{
|
||||||
|
unset($_SESSION[$key]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function flash(string $key, $value = null)
|
||||||
|
{
|
||||||
|
if ($value === null) {
|
||||||
|
// Get and remove flash message
|
||||||
|
$message = $_SESSION['flash'][$key] ?? null;
|
||||||
|
unset($_SESSION['flash'][$key]);
|
||||||
|
return $message;
|
||||||
|
} else {
|
||||||
|
// Set flash message
|
||||||
|
$_SESSION['flash'][$key] = $value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function isHTTPS(): bool
|
||||||
|
{
|
||||||
|
return (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ||
|
||||||
|
$_SERVER['SERVER_PORT'] == 443 ||
|
||||||
|
(isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https');
|
||||||
|
}
|
||||||
|
}
|
||||||
248
src/security/SecurityManager.php
Archivo normal
248
src/security/SecurityManager.php
Archivo normal
@@ -0,0 +1,248 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Security Manager
|
||||||
|
*
|
||||||
|
* Handles authentication, authorization, and security features
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace AleShell\Security;
|
||||||
|
|
||||||
|
use AleShell\Core\ConfigManager;
|
||||||
|
|
||||||
|
class SecurityManager
|
||||||
|
{
|
||||||
|
private ConfigManager $config;
|
||||||
|
private array $failedAttempts = [];
|
||||||
|
|
||||||
|
public function __construct(ConfigManager $config)
|
||||||
|
{
|
||||||
|
$this->config = $config;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function initialize(): void
|
||||||
|
{
|
||||||
|
// Set security headers
|
||||||
|
$this->setSecurityHeaders();
|
||||||
|
|
||||||
|
// Check IP restrictions
|
||||||
|
$this->checkIPRestrictions();
|
||||||
|
|
||||||
|
// Clean old failed attempts
|
||||||
|
$this->cleanFailedAttempts();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isAuthenticated(): bool
|
||||||
|
{
|
||||||
|
if (!isset($_SESSION['authenticated'])) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check session timeout
|
||||||
|
if (isset($_SESSION['last_activity'])) {
|
||||||
|
$timeout = $this->config->get('security.session_timeout', 3600);
|
||||||
|
if (time() - $_SESSION['last_activity'] > $timeout) {
|
||||||
|
$this->logout();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$_SESSION['last_activity'] = time();
|
||||||
|
return $_SESSION['authenticated'] === true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function authenticate(string $password): bool
|
||||||
|
{
|
||||||
|
$clientIP = $this->getClientIP();
|
||||||
|
|
||||||
|
// Check rate limiting
|
||||||
|
if ($this->isRateLimited($clientIP)) {
|
||||||
|
throw new \Exception('Too many failed attempts. Please try again later.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$hashedPassword = $this->config->get('security.password');
|
||||||
|
|
||||||
|
if (password_verify($password, $hashedPassword)) {
|
||||||
|
// Successful authentication
|
||||||
|
$_SESSION['authenticated'] = true;
|
||||||
|
$_SESSION['login_time'] = time();
|
||||||
|
$_SESSION['last_activity'] = time();
|
||||||
|
$_SESSION['user_ip'] = $clientIP;
|
||||||
|
$_SESSION['csrf_token'] = $this->generateCSRFToken();
|
||||||
|
|
||||||
|
// Clear failed attempts for this IP
|
||||||
|
unset($this->failedAttempts[$clientIP]);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
// Failed authentication
|
||||||
|
$this->recordFailedAttempt($clientIP);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function logout(): void
|
||||||
|
{
|
||||||
|
$_SESSION = [];
|
||||||
|
|
||||||
|
if (isset($_COOKIE[session_name()])) {
|
||||||
|
setcookie(session_name(), '', time() - 3600, '/');
|
||||||
|
}
|
||||||
|
|
||||||
|
session_destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function generateCSRFToken(): string
|
||||||
|
{
|
||||||
|
if (!isset($_SESSION['csrf_token'])) {
|
||||||
|
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
|
||||||
|
}
|
||||||
|
return $_SESSION['csrf_token'];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function validateCSRFToken(string $token): bool
|
||||||
|
{
|
||||||
|
if (!$this->config->get('security.csrf_protection', true)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return isset($_SESSION['csrf_token']) && hash_equals($_SESSION['csrf_token'], $token);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function sanitizeInput($input)
|
||||||
|
{
|
||||||
|
if (is_array($input)) {
|
||||||
|
return array_map([$this, 'sanitizeInput'], $input);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_string($input)) {
|
||||||
|
// Remove null bytes
|
||||||
|
$input = str_replace(chr(0), '', $input);
|
||||||
|
|
||||||
|
// Basic sanitization
|
||||||
|
$input = trim($input);
|
||||||
|
|
||||||
|
return $input;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $input;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function sanitizeOutput(string $output): string
|
||||||
|
{
|
||||||
|
return htmlspecialchars($output, ENT_QUOTES | ENT_HTML5, 'UTF-8');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isPathAllowed(string $path): bool
|
||||||
|
{
|
||||||
|
$realPath = realpath($path);
|
||||||
|
|
||||||
|
if ($realPath === false) {
|
||||||
|
// Allow even non-existent paths for creation
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allow access to entire filesystem - no path restrictions
|
||||||
|
// This permits access to system directories like /etc, /bin, /usr, etc.
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function setSecurityHeaders(): void
|
||||||
|
{
|
||||||
|
header('X-Content-Type-Options: nosniff');
|
||||||
|
header('X-Frame-Options: DENY');
|
||||||
|
header('X-XSS-Protection: 1; mode=block');
|
||||||
|
header('Referrer-Policy: strict-origin-when-cross-origin');
|
||||||
|
header('Content-Security-Policy: default-src \'self\'; script-src \'self\' \'unsafe-inline\'; style-src \'self\' \'unsafe-inline\' data:;');
|
||||||
|
}
|
||||||
|
|
||||||
|
private function checkIPRestrictions(): void
|
||||||
|
{
|
||||||
|
$clientIP = $this->getClientIP();
|
||||||
|
|
||||||
|
// Check blocked IPs
|
||||||
|
$blockedIPs = $this->config->get('security.blocked_ips', []);
|
||||||
|
if (in_array($clientIP, $blockedIPs)) {
|
||||||
|
http_response_code(403);
|
||||||
|
die('Access denied from this IP address.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check allowed IPs (if configured)
|
||||||
|
$allowedIPs = $this->config->get('security.allowed_ips', []);
|
||||||
|
if (!empty($allowedIPs) && !in_array($clientIP, $allowedIPs)) {
|
||||||
|
http_response_code(403);
|
||||||
|
die('Access denied from this IP address.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getClientIP(): string
|
||||||
|
{
|
||||||
|
$headers = [
|
||||||
|
'HTTP_X_FORWARDED_FOR',
|
||||||
|
'HTTP_X_REAL_IP',
|
||||||
|
'HTTP_CLIENT_IP',
|
||||||
|
'REMOTE_ADDR'
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($headers as $header) {
|
||||||
|
if (!empty($_SERVER[$header])) {
|
||||||
|
$ips = explode(',', $_SERVER[$header]);
|
||||||
|
$ip = trim($ips[0]);
|
||||||
|
if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
|
||||||
|
return $ip;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0';
|
||||||
|
}
|
||||||
|
|
||||||
|
private function isRateLimited(string $ip): bool
|
||||||
|
{
|
||||||
|
if (!$this->config->get('security.rate_limiting', true)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$maxAttempts = $this->config->get('security.max_attempts', 5);
|
||||||
|
$lockoutTime = $this->config->get('security.lockout_time', 300);
|
||||||
|
|
||||||
|
if (!isset($this->failedAttempts[$ip])) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$attempts = $this->failedAttempts[$ip];
|
||||||
|
|
||||||
|
if (count($attempts) >= $maxAttempts) {
|
||||||
|
$lastAttempt = end($attempts);
|
||||||
|
if (time() - $lastAttempt < $lockoutTime) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function recordFailedAttempt(string $ip): void
|
||||||
|
{
|
||||||
|
if (!isset($this->failedAttempts[$ip])) {
|
||||||
|
$this->failedAttempts[$ip] = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->failedAttempts[$ip][] = time();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function cleanFailedAttempts(): void
|
||||||
|
{
|
||||||
|
$lockoutTime = $this->config->get('security.lockout_time', 300);
|
||||||
|
$currentTime = time();
|
||||||
|
|
||||||
|
foreach ($this->failedAttempts as $ip => $attempts) {
|
||||||
|
$this->failedAttempts[$ip] = array_filter($attempts, function($timestamp) use ($currentTime, $lockoutTime) {
|
||||||
|
return ($currentTime - $timestamp) < $lockoutTime;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (empty($this->failedAttempts[$ip])) {
|
||||||
|
unset($this->failedAttempts[$ip]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Referencia en una nueva incidencia
Block a user