initial commit

Signed-off-by: ale <ale@manalejandro.com>
Este commit está contenido en:
ale
2025-12-11 03:51:36 +01:00
commit 8f5b9f3dae
Se han modificado 37 ficheros con 10238 adiciones y 0 borrados

15
.gitignore vendido Archivo normal
Ver fichero

@@ -0,0 +1,15 @@
.DS_Store
*.log
*.tmp
.env
.env.local
config.php
packed/*.php
!packed/.gitkeep
vendor/
node_modules/
.idea/
.vscode/
*.swp
*.swo
*~

128
CHANGELOG.md Archivo normal
Ver fichero

@@ -0,0 +1,128 @@
# Changelog
All notable changes to AleShell2 will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [1.0.0] - 2024-01-XX
### Added
- **Core Framework**
- Modern MVC architecture with PSR-4 style organization
- Request/Response handling with JSON and file download support
- Session management with timeout and regeneration
- CSRF protection on all forms
- Rate limiting for login attempts
- IP whitelist/blacklist support
- **Authentication**
- Secure password hashing with bcrypt
- Session-based authentication
- Automatic logout on inactivity
- Brute-force protection with lockout
- **File Manager**
- Browse directories with icons
- Create/edit/delete files and folders
- Upload multiple files
- Download files
- Copy/move operations
- Permission management (chmod)
- File search
- Syntax-aware icons for file types
- **Terminal**
- Full command execution
- Working directory tracking
- Command history (arrow keys)
- Built-in commands: cd, pwd, clear, help
- Quick command buttons
- Keyboard shortcuts
- **Code Editor**
- Open and edit any text file
- Tab indentation support
- Save with Ctrl+S
- Line/column tracking
- Save As functionality
- Direct download
- **Process Manager**
- List all running processes
- CPU and memory usage display
- Kill processes with signal selection
- Process filtering
- Auto-refresh
- **Network Tools**
- View network connections
- Ping hosts
- Traceroute
- Port scanner (up to 100 ports)
- Common port identification
- **Database Manager**
- MySQL/MariaDB support
- PostgreSQL support
- SQLite support
- Browse databases and tables
- Execute SQL queries
- Export to SQL/CSV
- Query results with pagination
- **System Information**
- Server details (hostname, OS, uptime)
- Hardware info (CPU, memory, disk)
- PHP configuration
- Loaded extensions
- Environment variables
- Disabled functions display
- **API Endpoints**
- Status endpoint
- CSRF token endpoint
- Health check
- Quick command execution
- PHP eval endpoint
- File upload/download API
- **Packer**
- Single-file generation
- Custom password support
- Minification option
- All views embedded
- No external dependencies
- **UI/UX**
- Dark theme (GitHub-inspired)
- Responsive design
- Keyboard shortcuts
- Toast notifications
- Modal dialogs
- Context menus
- Loading indicators
### Security
- Password stored as bcrypt hash
- Session tokens regenerated on login
- CSRF tokens required for state-changing operations
- Input sanitization throughout
- Output escaping (XSS prevention)
- Security headers (X-Frame-Options, X-XSS-Protection, etc.)
- IP restriction support
## [Unreleased]
### Planned
- Two-factor authentication
- Audit logging
- File encryption
- SSH tunnel support
- Cron job manager
- Log viewer
- Backup manager
- Plugin system

33
LICENSE Archivo normal
Ver fichero

@@ -0,0 +1,33 @@
MIT License
Copyright (c) 2024 AleShell Team
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.
---
DISCLAIMER
This software is provided for educational and legitimate administrative purposes
only. The authors do not condone or support any illegal activities. Users are
solely responsible for ensuring their use of this software complies with all
applicable laws and regulations.
Unauthorized access to computer systems is illegal and punishable by law. Always
obtain proper authorization before using this tool on any system.

191
PACKER_GUIDE.md Archivo normal
Ver fichero

@@ -0,0 +1,191 @@
# AleShell2 Packer Guide
## Overview
The AleShell2 packer (`pack.php`) combines all source files into a single, self-contained PHP file that can be deployed anywhere without additional dependencies.
## Quick Start
```bash
# Basic usage - creates packed/aleshell.php with default password "aleshell"
php pack.php
# Custom output file and password
php pack.php --output=/path/to/webshell.php --password=mysecretpassword
# Minified output (smaller file size)
php pack.php --minify
# Show help
php pack.php --help
```
## Options
| Option | Description | Default |
|--------|-------------|---------|
| `--output=<file>` | Output file path | `packed/aleshell.php` |
| `--password=<pass>` | Login password | `aleshell` |
| `--minify` | Remove comments and extra whitespace | disabled |
| `--help` | Show help message | - |
## Examples
### Basic Pack
```bash
php pack.php
```
Creates `packed/aleshell.php` with password `aleshell`.
### Custom Password
```bash
php pack.php --password=MySecureP@ssw0rd!
```
### Custom Output Location
```bash
php pack.php --output=../public/admin.php --password=admin123
```
### Production Build (Minified)
```bash
php pack.php --minify --output=dist/shell.php --password=prod_password
```
## Deployment
1. **Pack the application:**
```bash
php pack.php --password=your_secure_password
```
2. **Upload to server:**
Upload `packed/aleshell.php` to your web server via FTP, SCP, or your preferred method.
3. **Set permissions:**
```bash
chmod 644 aleshell.php
```
4. **Access via browser:**
Navigate to `https://your-server.com/path/to/aleshell.php`
5. **Login:**
Use the password you set during packing.
## Security Recommendations
### Strong Password
Always use a strong, unique password:
```bash
php pack.php --password="$(openssl rand -base64 32)"
```
### Rename the File
Use a non-obvious filename:
```bash
php pack.php --output=./admin_tools_$(date +%s).php
```
### IP Restrictions
Edit the packed file to add IP restrictions:
```php
$ALESHELL_CONFIG = [
// ... other config ...
'ip_whitelist' => ['192.168.1.100', '10.0.0.50'],
];
```
### Delete After Use
Remove the webshell when no longer needed:
```bash
rm aleshell.php
```
## What Gets Packed
The packer combines:
- Core framework classes (Application, Router, Request, Response, View)
- Security classes (Session, Auth)
- All module controllers:
- Dashboard
- File Manager
- Terminal
- Code Editor
- Process Manager
- Network Tools
- Database Manager
- System Info
- API endpoints
- All view templates (embedded as strings)
- CSS styles (embedded in layout)
- JavaScript (embedded in views)
## Troubleshooting
### "Class not found" errors
Ensure all source files exist before packing:
```bash
ls -la src/Core/
ls -la src/Security/
ls -la src/Modules/
ls -la src/Views/
```
### Large file size
Use the `--minify` option:
```bash
php pack.php --minify
```
### View rendering issues
Views are stored as PHP strings and evaluated with `eval()`. Ensure view files have valid PHP syntax.
### Password not working
The password is hashed during packing. You cannot recover it from the packed file. Re-pack with a known password:
```bash
php pack.php --password=newpassword
```
## Technical Details
### Class Renaming
To avoid namespace conflicts, classes are renamed during packing:
- `AleShell2\Core\Request` → `AleShell2_Request`
- `AleShell2\Security\Auth` → `AleShell2_Auth`
- etc.
### View Storage
Views are stored in the `$ALESHELL_VIEWS` global array and rendered via `eval()`.
### Configuration
Configuration is stored in the `$ALESHELL_CONFIG` global array at the top of the packed file.
## License
MIT License - See LICENSE file for details.

326
README.md Archivo normal
Ver fichero

@@ -0,0 +1,326 @@
# 🚀 AleShell2 - Modern PHP Web Shell
[![PHP Version](https://img.shields.io/badge/PHP-8.0+-blue.svg)](https://php.net)
[![License](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE)
[![Version](https://img.shields.io/badge/Version-2.0.0-orange.svg)](CHANGELOG.md)
AleShell2 is a powerful, secure, and modern web shell built with PHP. It's designed to be deployed as a **single monolithic PHP file** while maintaining a clean, modular architecture during development.
## ✨ Features
### 🔐 Security
- **Password Authentication** with secure hashing (bcrypt)
- **Session Management** with timeout protection
- **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** for dangerous system commands
- **Self-destruct Mode** after N accesses (optional)
### 🎨 Modern Interface
- **Responsive Design** works on desktop, tablet, and mobile
- **Dark/Light Theme** toggle with system preference detection
- **Multiple Color Themes** (Dark, Light, Matrix, Ocean, etc.)
- **Keyboard Shortcuts** for power users
- **Real-time Updates** for system information
- **Smooth Animations** and transitions
- **Single Page Application** experience
### 📁 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 (chmod)
- **Archive Support** (zip, tar, tar.gz)
- **Large File Handling** with streaming
- **File Search** and filtering capabilities
- **Breadcrumb Navigation**
### 💻 Terminal
- **Interactive Terminal** with command history
- **Built-in Commands** (cd, pwd, clear, help, etc.)
- **Command Auto-completion**
- **Output Streaming** for long-running commands
- **Multiple Terminal Tabs**
- **Configurable Timeout** for command execution
- **Color-coded Output**
### 📝 Code Editor
- **Syntax Highlighting** for 20+ languages
- **Line Numbers** and code folding
- **Find & Replace** functionality
- **Auto-indentation** and code formatting
- **Multiple Editor Themes**
- **File Type Detection**
- **Unsaved Changes Warning**
### ⚡ System Monitoring (Dashboard)
- **Real-time System Stats** (CPU, Memory, Disk, Network)
- **Process Manager** with kill capabilities
- **System Load Average** monitoring
- **PHP Information** display
- **Server Time** display
### 🔧 Process Manager
- **List All Processes** with details
- **Search/Filter Processes**
- **Kill Processes** (single or batch)
- **CPU & Memory Usage** per process
- **Auto-refresh** capability
### 🌐 Network Tools
- **Active Connections** list (netstat)
- **Ping** utility
- **Traceroute** utility
- **Port Scanner** (basic)
- **DNS Lookup**
- **Interface Information**
### 🗄️ Database Tools
- **Multi-Database Support** (MySQL, PostgreSQL, SQLite)
- **SQL Query Execution** with result formatting
- **Database Browser** with table structure
- **Export/Import** capabilities (SQL dump)
- **Connection Management**
- **Query History**
## 🚀 Installation
### Requirements
- PHP 8.0 or higher
- Web server (Apache, Nginx, LiteSpeed, etc.)
- PHP extensions: json, mbstring, openssl (optional: pdo, mysqli, pgsql)
### Quick Install (Packed Version)
1. Generate a packed version using `pack.php`
2. Upload the single `aleshell.php` file to your server
3. Access via web browser
4. Default password: `aleshell`
### From Source (Development)
```bash
git clone https://github.com/yourusername/aleshell2.git
cd aleshell2
# Copy configuration
cp src/Config/config.example.php src/Config/config.php
# Edit configuration
nano src/Config/config.php
# Access index.php via your web server
```
## 📦 Generating Packed Version
AleShell2 can be packed into a single PHP file for easy deployment:
### Web Interface
```bash
# Access pack.php in your browser
http://your-server/aleshell2/pack.php
```
### Command Line
```bash
# Basic packed version
php pack.php --output=shell.php --password=your_password
# Full options
php pack.php \
--output=shell.php \
--password=secure_pass \
--encrypt \
--minify \
--obfuscate \
--theme=dark \
--modules=files,terminal,editor,processes,network,database
```
### Packer Options
| Option | Description | Default |
|--------|-------------|---------|
| `--output` | Output filename | `aleshell.php` |
| `--password` | Access password | `aleshell` |
| `--theme` | Default theme | `dark` |
| `--modules` | Modules to include | all |
| `--encrypt` | Encrypt with base64+compression | false |
| `--minify` | Minify code | false |
| `--obfuscate` | Obfuscate variable names | false |
| `--compression` | Compression type | `gzdeflate` |
| `--allowed-ips` | IP whitelist | empty |
| `--self-destruct` | Delete after N accesses | disabled |
## ⚙️ Configuration
Edit `src/Config/config.php`:
```php
<?php
return [
// Application settings
'app' => [
'name' => 'AleShell2',
'version' => '2.0.0',
'debug' => false,
'timezone' => 'UTC',
],
// Security settings
'security' => [
'password' => password_hash('your_password', PASSWORD_BCRYPT),
'session_timeout' => 3600,
'max_attempts' => 5,
'lockout_time' => 300,
'csrf_protection' => true,
'allowed_ips' => [],
'blocked_ips' => [],
],
// Feature toggles
'features' => [
'file_manager' => true,
'terminal' => true,
'code_editor' => true,
'process_manager' => true,
'network_tools' => true,
'database_tools' => true,
'system_info' => true,
],
// UI settings
'ui' => [
'theme' => 'dark',
'language' => 'en',
'items_per_page' => 50,
],
// Limits
'limits' => [
'max_file_size' => 50 * 1024 * 1024,
'max_upload_size' => 100 * 1024 * 1024,
'command_timeout' => 30,
'max_history' => 100,
],
];
```
## 🔒 Security Considerations
⚠️ **WARNING**: This tool provides full system access. Use responsibly!
1. **Always change the default password** immediately
2. **Use HTTPS** in production environments
3. **Restrict access** using IP whitelisting when possible
4. **Use self-destruct** for temporary access
5. **Delete the file** when not needed
6. **Monitor access logs** for suspicious activity
7. **Keep PHP updated** to the latest stable version
## 🎯 Keyboard Shortcuts
| Shortcut | Action |
|----------|--------|
| `Ctrl+1` | Dashboard |
| `Ctrl+2` | File Manager |
| `Ctrl+3` | Terminal |
| `Ctrl+4` | Code Editor |
| `Ctrl+5` | Processes |
| `Ctrl+6` | Network |
| `Ctrl+7` | Database |
| `Ctrl+L` | Clear terminal |
| `Ctrl+S` | Save file (in editor) |
| `Escape` | Close modal |
## 🌐 Browser Support
- Chrome 80+
- Firefox 75+
- Safari 13+
- Edge 80+
- Opera 67+
## 📱 Mobile Support
The interface is fully responsive with:
- Touch-friendly controls
- Swipe navigation
- Responsive layouts
- Mobile-optimized terminal
## 📁 Project Structure
```
aleshell2/
├── index.php # Entry point
├── pack.php # Packer utility
├── README.md # Documentation
├── CHANGELOG.md # Version history
├── LICENSE # MIT License
├── src/
│ ├── Config/
│ │ ├── config.example.php
│ │ └── config.php
│ ├── Core/
│ │ ├── Application.php
│ │ ├── Router.php
│ │ ├── Request.php
│ │ ├── Response.php
│ │ └── View.php
│ ├── Security/
│ │ ├── Auth.php
│ │ ├── Session.php
│ │ └── Csrf.php
│ ├── Modules/
│ │ ├── Dashboard/
│ │ ├── Files/
│ │ ├── Terminal/
│ │ ├── Editor/
│ │ ├── Processes/
│ │ ├── Network/
│ │ └── Database/
│ └── Views/
│ ├── layouts/
│ ├── components/
│ └── modules/
└── packed/ # Generated packed files
```
## 🔧 Development
### Adding a New Module
1. Create directory: `src/Modules/MyModule/`
2. Create controller: `MyModuleController.php`
3. Create view: `src/Views/modules/mymodule.php`
4. Register route in `src/Core/Router.php`
5. Add to navigation in `src/Views/layouts/main.php`
### Code Style
- PSR-12 coding standard
- Type hints for parameters and return values
- PHPDoc comments for all public methods
- Meaningful variable and function names
## 📄 License
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
## 🙏 Acknowledgments
- Based on concepts from the original b374k shell
- Inspired by modern web development practices
- Built with ❤️ for system administrators
## ⚠️ 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. Unauthorized access to computer systems is illegal.
---
**AleShell2 v2.0.0** - Modern PHP Web Shell

154
index.php Archivo normal
Ver fichero

@@ -0,0 +1,154 @@
<?php
/**
* AleShell2 - Modern PHP Web Shell
*
* A powerful, secure, and modern web shell that can be packed into
* a single monolithic PHP file for easy deployment.
*
* @author AleShell Team
* @version 2.0.0
* @license MIT
* @package AleShell2
* @link https://github.com/yourusername/aleshell2
*/
declare(strict_types=1);
// Prevent direct execution without proper context
if (php_sapi_name() === 'cli' && basename(__FILE__) === basename($argv[0] ?? '')) {
echo "AleShell2 v2.0.0 - Web Shell\n";
echo "This file should be accessed via a web server.\n";
exit(1);
}
// =============================================================================
// CONSTANTS AND CONFIGURATION
// =============================================================================
define('ALESHELL_VERSION', '2.0.0');
define('ALESHELL_ROOT', __DIR__);
define('ALESHELL_SRC', ALESHELL_ROOT . '/src');
define('ALESHELL_START_TIME', microtime(true));
define('ALESHELL_PACKED', false);
// =============================================================================
// ERROR HANDLING
// =============================================================================
error_reporting(E_ALL);
ini_set('display_errors', '0');
ini_set('log_errors', '1');
set_error_handler(function ($severity, $message, $file, $line) {
if (!(error_reporting() & $severity)) {
return false;
}
throw new ErrorException($message, 0, $severity, $file, $line);
});
set_exception_handler(function (Throwable $e) {
http_response_code(500);
if (defined('ALESHELL_DEBUG') && ALESHELL_DEBUG) {
echo "<h1>Error</h1>";
echo "<p><strong>Message:</strong> " . htmlspecialchars($e->getMessage()) . "</p>";
echo "<p><strong>File:</strong> " . htmlspecialchars($e->getFile()) . ":" . $e->getLine() . "</p>";
echo "<pre>" . htmlspecialchars($e->getTraceAsString()) . "</pre>";
} else {
echo "<h1>Internal Server Error</h1>";
echo "<p>An error occurred. Please try again later.</p>";
}
exit(1);
});
// =============================================================================
// AUTOLOADER
// =============================================================================
spl_autoload_register(function (string $class): bool {
// Only handle AleShell2 namespace
if (strpos($class, 'AleShell2\\') !== 0) {
return false;
}
// Convert namespace to path
$relativePath = str_replace('AleShell2\\', '', $class);
$relativePath = str_replace('\\', DIRECTORY_SEPARATOR, $relativePath);
$filePath = ALESHELL_SRC . DIRECTORY_SEPARATOR . $relativePath . '.php';
if (file_exists($filePath)) {
require_once $filePath;
return true;
}
return false;
});
// =============================================================================
// SECURITY HEADERS
// =============================================================================
header('X-Content-Type-Options: nosniff');
header('X-Frame-Options: SAMEORIGIN');
header('X-XSS-Protection: 1; mode=block');
header('Referrer-Policy: strict-origin-when-cross-origin');
header('Permissions-Policy: geolocation=(), microphone=(), camera=()');
// =============================================================================
// BOOTSTRAP APPLICATION
// =============================================================================
try {
// Load and run application
$app = new AleShell2\Core\Application();
$app->run();
} catch (Throwable $e) {
// Log error
error_log(sprintf(
'AleShell2 Error: %s in %s:%d',
$e->getMessage(),
$e->getFile(),
$e->getLine()
));
// Show error page
http_response_code(500);
echo "<!DOCTYPE html>
<html lang='en'>
<head>
<meta charset='UTF-8'>
<meta name='viewport' content='width=device-width, initial-scale=1.0'>
<title>Error - AleShell2</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: #1a202c; color: #f7fafc;
display: flex; align-items: center; justify-content: center;
min-height: 100vh; margin: 0;
}
.error-box {
background: #2d3748; padding: 2rem; border-radius: 12px;
max-width: 500px; text-align: center;
box-shadow: 0 10px 25px rgba(0,0,0,0.3);
}
h1 { color: #fc8181; margin: 0 0 1rem 0; }
p { color: #a0aec0; margin: 0; }
code {
display: block; margin-top: 1rem; padding: 1rem;
background: #1a202c; border-radius: 6px;
font-size: 0.875rem; text-align: left;
overflow-x: auto; color: #ed8936;
}
</style>
</head>
<body>
<div class='error-box'>
<h1>⚠️ Application Error</h1>
<p>An error occurred while loading AleShell2.</p>" .
(defined('ALESHELL_DEBUG') && ALESHELL_DEBUG
? "<code>" . htmlspecialchars($e->getMessage()) . "</code>"
: "") .
"</div>
</body>
</html>";
}

654
pack.php Archivo normal
Ver fichero

@@ -0,0 +1,654 @@
<?php
/**
* AleShell2 Packer
*
* This script packs the entire AleShell2 application into a single monolithic PHP file.
*
* Usage:
* php pack.php [options]
*
* Options:
* --output=<file> Output file path (default: packed/aleshell.php)
* --password=<pass> Set custom password (default: aleshell)
* --minify Minify the output (remove comments and extra whitespace)
* --help Show this help message
*
* @package AleShell2
*/
declare(strict_types=1);
// Parse command line arguments
$options = getopt('', ['output:', 'password:', 'minify', 'help']);
if (isset($options['help'])) {
echo <<<HELP
AleShell2 Packer
================
This script packs the entire AleShell2 application into a single monolithic PHP file.
Usage:
php pack.php [options]
Options:
--output=<file> Output file path (default: packed/aleshell.php)
--password=<pass> Set custom password (default: aleshell)
--minify Minify the output (remove comments and extra whitespace)
--help Show this help message
Examples:
php pack.php
php pack.php --output=../webshell.php --password=mysecret
php pack.php --minify
HELP;
exit(0);
}
$outputFile = $options['output'] ?? __DIR__ . '/packed/aleshell.php';
$password = $options['password'] ?? 'aleshell';
$minify = isset($options['minify']);
echo "AleShell2 Packer\n";
echo "================\n\n";
// Create output directory if needed
$outputDir = dirname($outputFile);
if (!is_dir($outputDir)) {
mkdir($outputDir, 0755, true);
echo "Created directory: {$outputDir}\n";
}
// Generate password hash
$passwordHash = password_hash($password, PASSWORD_BCRYPT);
echo "Password: {$password}\n";
echo "Hash: {$passwordHash}\n\n";
// Collect all source files
$sources = [];
// Core files order matters for dependencies
$coreFiles = [
'src/Core/Request.php',
'src/Core/Response.php',
'src/Core/View.php',
'src/Core/Router.php',
'src/Core/Application.php',
];
// Security files
$securityFiles = [
'src/Security/Session.php',
'src/Security/Auth.php',
];
// Module files
$moduleFiles = [
'src/Modules/BaseController.php',
'src/Modules/Auth/AuthController.php',
'src/Modules/Dashboard/DashboardController.php',
'src/Modules/Files/FilesController.php',
'src/Modules/Terminal/TerminalController.php',
'src/Modules/Editor/EditorController.php',
'src/Modules/Processes/ProcessesController.php',
'src/Modules/Network/NetworkController.php',
'src/Modules/Database/DatabaseController.php',
'src/Modules/System/SystemController.php',
'src/Modules/Api/ApiController.php',
];
// View files
$viewFiles = [
'src/Views/layouts/main.php',
'src/Views/auth/login.php',
'src/Views/modules/dashboard.php',
'src/Views/modules/files.php',
'src/Views/modules/terminal.php',
'src/Views/modules/editor.php',
'src/Views/modules/processes.php',
'src/Views/modules/network.php',
'src/Views/modules/database.php',
'src/Views/modules/system.php',
];
echo "Collecting source files...\n";
/**
* Extract PHP code from file (without opening tag and namespace)
*/
function extractPhpCode(string $filePath, bool $minify = false): string {
$content = file_get_contents($filePath);
// Remove opening PHP tag
$content = preg_replace('/^<\?php\s*/s', '', $content);
// Remove declare strict types (we'll add it once at the top)
$content = preg_replace('/declare\s*\(\s*strict_types\s*=\s*1\s*\)\s*;?\s*/i', '', $content);
// Remove namespace declarations (we'll inline everything)
$content = preg_replace('/namespace\s+[^;]+;\s*/i', '', $content);
// Remove use statements (we'll use fully qualified names)
$content = preg_replace('/use\s+[^;]+;\s*/i', '', $content);
if ($minify) {
// Remove multi-line comments but preserve strings
$content = preg_replace('/\/\*(?!.*?\*\/).*?\*\//s', '', $content);
// Remove single-line comments (be careful with URLs)
$content = preg_replace('/(?<!:)\/\/.*$/m', '', $content);
// Remove extra whitespace
$content = preg_replace('/\n\s*\n/', "\n", $content);
}
return trim($content);
}
/**
* Extract view content (PHP template)
*/
function extractViewContent(string $filePath): string {
return file_get_contents($filePath);
}
// Build the monolithic file
$output = <<<'HEADER'
<?php
/**
* AleShell2 - Web Administration Shell
*
* A powerful, single-file PHP webshell with modern features:
* - File Manager with upload/download/edit capabilities
* - Terminal emulator with command execution
* - Code Editor with syntax highlighting
* - Process Manager with kill functionality
* - Network Tools (ping, traceroute, port scan)
* - Database Manager (MySQL, PostgreSQL, SQLite)
* - System Information viewer
*
* @package AleShell2
* @version 1.0.0
* @author AleShell Team
* @license MIT
*
* WARNING: This tool is intended for legitimate server administration only.
* Unauthorized access to computer systems is illegal.
*/
declare(strict_types=1);
// ============================================================================
// CONFIGURATION
// ============================================================================
HEADER;
// Add configuration
$output .= "\n\$ALESHELL_CONFIG = [\n";
$output .= " 'password_hash' => '{$passwordHash}',\n";
$output .= " 'session_timeout' => 3600,\n";
$output .= " 'max_login_attempts' => 5,\n";
$output .= " 'lockout_duration' => 900,\n";
$output .= " 'ip_whitelist' => [],\n";
$output .= " 'ip_blacklist' => [],\n";
$output .= "];\n\n";
// Add embedded views
$output .= "// ============================================================================\n";
$output .= "// EMBEDDED VIEWS\n";
$output .= "// ============================================================================\n\n";
$output .= "\$ALESHELL_VIEWS = [];\n\n";
foreach ($viewFiles as $viewFile) {
$path = __DIR__ . '/' . $viewFile;
if (file_exists($path)) {
$viewName = str_replace(['src/Views/', '.php'], '', $viewFile);
$viewName = str_replace('/', '.', $viewName);
$viewContent = extractViewContent($path);
// Escape for heredoc
$viewContent = addcslashes($viewContent, '\\\'');
$output .= "\$ALESHELL_VIEWS['{$viewName}'] = '" . $viewContent . "';\n\n";
echo " Added view: {$viewName}\n";
}
}
$output .= "\n// ============================================================================\n";
$output .= "// CORE CLASSES\n";
$output .= "// ============================================================================\n\n";
// Process core files
foreach ($coreFiles as $file) {
$path = __DIR__ . '/' . $file;
if (file_exists($path)) {
$code = extractPhpCode($path, $minify);
// Replace namespace references with inline
$code = str_replace('AleShell2\\Core\\', 'AleShell2_', $code);
$code = str_replace('AleShell2\\Security\\', 'AleShell2_', $code);
$code = str_replace('AleShell2\\Modules\\', 'AleShell2_', $code);
// Rename classes to avoid namespace issues
$className = basename($file, '.php');
$code = preg_replace('/class\s+' . $className . '/', 'class AleShell2_' . $className, $code);
$output .= "// Source: {$file}\n";
$output .= $code . "\n\n";
echo " Added core: {$className}\n";
}
}
$output .= "\n// ============================================================================\n";
$output .= "// SECURITY CLASSES\n";
$output .= "// ============================================================================\n\n";
// Process security files
foreach ($securityFiles as $file) {
$path = __DIR__ . '/' . $file;
if (file_exists($path)) {
$code = extractPhpCode($path, $minify);
$code = str_replace('AleShell2\\Core\\', 'AleShell2_', $code);
$code = str_replace('AleShell2\\Security\\', 'AleShell2_', $code);
$code = str_replace('AleShell2\\Modules\\', 'AleShell2_', $code);
$className = basename($file, '.php');
$code = preg_replace('/class\s+' . $className . '/', 'class AleShell2_' . $className, $code);
$output .= "// Source: {$file}\n";
$output .= $code . "\n\n";
echo " Added security: {$className}\n";
}
}
$output .= "\n// ============================================================================\n";
$output .= "// MODULE CONTROLLERS\n";
$output .= "// ============================================================================\n\n";
// Process module files
foreach ($moduleFiles as $file) {
$path = __DIR__ . '/' . $file;
if (file_exists($path)) {
$code = extractPhpCode($path, $minify);
$code = str_replace('AleShell2\\Core\\', 'AleShell2_', $code);
$code = str_replace('AleShell2\\Security\\', 'AleShell2_', $code);
$code = str_replace('AleShell2\\Modules\\', 'AleShell2_', $code);
$code = str_replace('AleShell2\\Modules\\Auth\\', 'AleShell2_', $code);
$code = str_replace('AleShell2\\Modules\\Dashboard\\', 'AleShell2_', $code);
$code = str_replace('AleShell2\\Modules\\Files\\', 'AleShell2_', $code);
$code = str_replace('AleShell2\\Modules\\Terminal\\', 'AleShell2_', $code);
$code = str_replace('AleShell2\\Modules\\Editor\\', 'AleShell2_', $code);
$code = str_replace('AleShell2\\Modules\\Processes\\', 'AleShell2_', $code);
$code = str_replace('AleShell2\\Modules\\Network\\', 'AleShell2_', $code);
$code = str_replace('AleShell2\\Modules\\Database\\', 'AleShell2_', $code);
$code = str_replace('AleShell2\\Modules\\System\\', 'AleShell2_', $code);
$code = str_replace('AleShell2\\Modules\\Api\\', 'AleShell2_', $code);
$className = basename($file, '.php');
$code = preg_replace('/class\s+' . $className . '/', 'class AleShell2_' . $className, $code);
$code = preg_replace('/abstract\s+class\s+' . $className . '/', 'abstract class AleShell2_' . $className, $code);
$code = preg_replace('/extends\s+' . $className . '/', 'extends AleShell2_' . $className, $code);
$code = preg_replace('/extends\s+BaseController/', 'extends AleShell2_BaseController', $code);
$output .= "// Source: {$file}\n";
$output .= $code . "\n\n";
echo " Added module: {$className}\n";
}
}
// Add bootstrap code
$output .= <<<'BOOTSTRAP'
// ============================================================================
// BOOTSTRAP
// ============================================================================
// Override View class to use embedded views
class AleShell2_PackedView extends AleShell2_View {
public function render(string $template, array $data = []): string {
global $ALESHELL_VIEWS;
$templateKey = $template;
if (!isset($ALESHELL_VIEWS[$templateKey])) {
return "View not found: {$template}";
}
$viewContent = $ALESHELL_VIEWS[$templateKey];
// Extract data to local scope
extract($data);
// Capture output
ob_start();
eval('?>' . $viewContent);
$content = ob_get_clean();
// If using layout, wrap content
if ($template !== 'layouts.main' && $template !== 'auth.login') {
if (isset($ALESHELL_VIEWS['layouts.main'])) {
$layoutContent = $ALESHELL_VIEWS['layouts.main'];
ob_start();
eval('?>' . $layoutContent);
return ob_get_clean();
}
}
return $content;
}
}
// Modified Application for packed version
class AleShell2_PackedApplication {
private AleShell2_Request $request;
private AleShell2_Response $response;
private AleShell2_Router $router;
private AleShell2_Session $session;
private AleShell2_Auth $auth;
private AleShell2_PackedView $view;
private array $config;
public function __construct() {
global $ALESHELL_CONFIG;
$this->config = $ALESHELL_CONFIG;
}
public function run(): void {
try {
// Initialize components
$this->request = new AleShell2_Request();
$this->response = new AleShell2_Response();
$this->session = new AleShell2_Session($this->config['session_timeout'] ?? 3600);
$this->auth = new AleShell2_Auth(
$this->config['password_hash'],
$this->session,
$this->config['max_login_attempts'] ?? 5,
$this->config['lockout_duration'] ?? 900
);
$this->view = new AleShell2_PackedView();
$this->router = new AleShell2_Router($this);
// Set security headers
$this->setSecurityHeaders();
// Check IP restrictions
if (!$this->checkIpAccess()) {
http_response_code(403);
die('Access denied');
}
// Start session
$this->session->start();
// Generate CSRF token if needed
if (!isset($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = $this->auth->generateCsrfToken();
}
// Setup routes
$this->setupRoutes();
// Dispatch request
$this->router->dispatch($this->request, $this->response);
} catch (\Throwable $e) {
$this->handleError($e);
}
}
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');
}
private function checkIpAccess(): bool {
$clientIp = $_SERVER['REMOTE_ADDR'] ?? '';
// Check blacklist
if (!empty($this->config['ip_blacklist'])) {
if (in_array($clientIp, $this->config['ip_blacklist'])) {
return false;
}
}
// Check whitelist
if (!empty($this->config['ip_whitelist'])) {
return in_array($clientIp, $this->config['ip_whitelist']);
}
return true;
}
private function setupRoutes(): void {
// Auth routes (public)
$this->router->get('login', function($req, $res, $params) {
$controller = new AleShell2_AuthController($this);
$controller->showLogin($req, $res, $params);
});
$this->router->post('login', function($req, $res, $params) {
$controller = new AleShell2_AuthController($this);
$controller->login($req, $res, $params);
});
$this->router->get('logout', function($req, $res, $params) {
$controller = new AleShell2_AuthController($this);
$controller->logout($req, $res, $params);
});
// Protected routes
$authMiddleware = function($handler) {
return function($req, $res, $params) use ($handler) {
if (!$this->auth->isAuthenticated()) {
$res->redirect('?action=login');
return;
}
$handler($req, $res, $params);
};
};
// Dashboard
$this->router->get('dashboard', $authMiddleware(function($req, $res, $params) {
$controller = new AleShell2_DashboardController($this);
$controller->index($req, $res, $params);
}));
// Files
$this->router->get('files', $authMiddleware(function($req, $res, $params) {
$controller = new AleShell2_FilesController($this);
$action = $req->get('action', 'index');
match($action) {
'list' => $controller->list($req, $res, $params),
'read' => $controller->read($req, $res, $params),
'download' => $controller->download($req, $res, $params),
'search' => $controller->search($req, $res, $params),
default => $controller->index($req, $res, $params),
};
}));
$this->router->post('files', $authMiddleware(function($req, $res, $params) {
$controller = new AleShell2_FilesController($this);
$action = $req->get('action', 'index');
match($action) {
'write' => $controller->write($req, $res, $params),
'delete' => $controller->delete($req, $res, $params),
'mkdir' => $controller->mkdir($req, $res, $params),
'copy' => $controller->copy($req, $res, $params),
'move' => $controller->move($req, $res, $params),
'chmod' => $controller->chmod($req, $res, $params),
'upload' => $controller->upload($req, $res, $params),
default => $controller->index($req, $res, $params),
};
}));
// Terminal
$this->router->get('terminal', $authMiddleware(function($req, $res, $params) {
$controller = new AleShell2_TerminalController($this);
$controller->index($req, $res, $params);
}));
$this->router->post('terminal', $authMiddleware(function($req, $res, $params) {
$controller = new AleShell2_TerminalController($this);
$action = $req->get('action', 'exec');
if ($action === 'exec') {
$controller->execute($req, $res, $params);
}
}));
// Editor
$this->router->get('editor', $authMiddleware(function($req, $res, $params) {
$controller = new AleShell2_EditorController($this);
$controller->index($req, $res, $params);
}));
$this->router->post('editor', $authMiddleware(function($req, $res, $params) {
$controller = new AleShell2_EditorController($this);
$controller->save($req, $res, $params);
}));
// Processes
$this->router->get('processes', $authMiddleware(function($req, $res, $params) {
$controller = new AleShell2_ProcessesController($this);
$action = $req->get('action', 'index');
if ($action === 'list') {
$controller->list($req, $res, $params);
} else {
$controller->index($req, $res, $params);
}
}));
$this->router->post('processes', $authMiddleware(function($req, $res, $params) {
$controller = new AleShell2_ProcessesController($this);
$action = $req->get('action', 'kill');
if ($action === 'kill') {
$controller->kill($req, $res, $params);
}
}));
// Network
$this->router->get('network', $authMiddleware(function($req, $res, $params) {
$controller = new AleShell2_NetworkController($this);
$action = $req->get('action', 'index');
if ($action === 'connections') {
$controller->connections($req, $res, $params);
} else {
$controller->index($req, $res, $params);
}
}));
$this->router->post('network', $authMiddleware(function($req, $res, $params) {
$controller = new AleShell2_NetworkController($this);
$action = $req->get('action');
match($action) {
'ping' => $controller->ping($req, $res, $params),
'traceroute' => $controller->traceroute($req, $res, $params),
'portscan' => $controller->portscan($req, $res, $params),
default => $controller->index($req, $res, $params),
};
}));
// Database
$this->router->get('database', $authMiddleware(function($req, $res, $params) {
$controller = new AleShell2_DatabaseController($this);
$action = $req->get('action', 'index');
match($action) {
'tables' => $controller->tables($req, $res, $params),
'structure' => $controller->structure($req, $res, $params),
'export' => $controller->export($req, $res, $params),
default => $controller->index($req, $res, $params),
};
}));
$this->router->post('database', $authMiddleware(function($req, $res, $params) {
$controller = new AleShell2_DatabaseController($this);
$action = $req->get('action');
match($action) {
'connect' => $controller->connect($req, $res, $params),
'disconnect' => $controller->disconnect($req, $res, $params),
'query' => $controller->query($req, $res, $params),
'selectDb' => $controller->selectDb($req, $res, $params),
default => $controller->index($req, $res, $params),
};
}));
// System
$this->router->get('system', $authMiddleware(function($req, $res, $params) {
$controller = new AleShell2_SystemController($this);
$action = $req->get('action', 'index');
match($action) {
'info' => $controller->info($req, $res, $params),
'phpinfo' => $controller->phpinfo($req, $res, $params),
'environment' => $controller->environment($req, $res, $params),
'extensions' => $controller->extensions($req, $res, $params),
default => $controller->index($req, $res, $params),
};
}));
// API
$this->router->get('api', $authMiddleware(function($req, $res, $params) {
$controller = new AleShell2_ApiController($this);
$action = $req->get('action', 'status');
match($action) {
'status' => $controller->status($req, $res, $params),
'csrf' => $controller->csrf($req, $res, $params),
'health' => $controller->health($req, $res, $params),
'download' => $controller->download($req, $res, $params),
default => $controller->status($req, $res, $params),
};
}));
$this->router->post('api', $authMiddleware(function($req, $res, $params) {
$controller = new AleShell2_ApiController($this);
$action = $req->get('action');
match($action) {
'exec' => $controller->exec($req, $res, $params),
'eval' => $controller->eval($req, $res, $params),
'upload' => $controller->upload($req, $res, $params),
default => $controller->status($req, $res, $params),
};
}));
// Default route
$this->router->get('', function($req, $res, $params) {
if ($this->auth->isAuthenticated()) {
$res->redirect('?module=dashboard');
} else {
$res->redirect('?action=login');
}
});
}
private function handleError(\Throwable $e): void {
http_response_code(500);
echo '<h1>Error</h1>';
echo '<p>' . htmlspecialchars($e->getMessage()) . '</p>';
if (ini_get('display_errors')) {
echo '<pre>' . htmlspecialchars($e->getTraceAsString()) . '</pre>';
}
}
public function getSession(): AleShell2_Session { return $this->session; }
public function getAuth(): AleShell2_Auth { return $this->auth; }
public function getView(): AleShell2_PackedView { return $this->view; }
public function getConfig(): array { return $this->config; }
}
// Run the application
$app = new AleShell2_PackedApplication();
$app->run();
BOOTSTRAP;
// Write output file
file_put_contents($outputFile, $output);
$size = filesize($outputFile);
$sizeKb = round($size / 1024, 2);
echo "\n================\n";
echo "Pack complete!\n";
echo "Output: {$outputFile}\n";
echo "Size: {$sizeKb} KB\n";
echo "Password: {$password}\n";
echo "\nTo use, upload the file to your web server and access it via browser.\n";

1
packed/.gitkeep Archivo normal
Ver fichero

@@ -0,0 +1 @@
# This file ensures the packed directory is tracked by git

123
src/Config/config.example.php Archivo normal
Ver fichero

@@ -0,0 +1,123 @@
<?php
/**
* AleShell2 Configuration File
*
* Copy this file to config.php and customize as needed.
*
* @package AleShell2
*/
return [
// ==========================================================================
// APPLICATION SETTINGS
// ==========================================================================
'app' => [
'name' => 'AleShell2',
'version' => '2.0.0',
'debug' => false,
'timezone' => 'UTC',
],
// ==========================================================================
// SECURITY SETTINGS
// ==========================================================================
'security' => [
// Default password: 'aleshell' - CHANGE THIS!
'password' => '$2y$10$YourHashedPasswordHere',
// Session timeout in seconds (default: 1 hour)
'session_timeout' => 3600,
// Maximum login attempts before lockout
'max_attempts' => 5,
// Lockout time in seconds (default: 5 minutes)
'lockout_time' => 300,
// Enable CSRF protection
'csrf_protection' => true,
// IP whitelist (empty = allow all)
'allowed_ips' => [],
// IP blacklist
'blocked_ips' => [],
// Commands to block in terminal
'blocked_commands' => [
'rm -rf /',
'mkfs',
'dd if=/dev/zero',
'shutdown',
'reboot',
'halt',
'init 0',
'init 6',
],
// Paths to restrict (empty = no restriction)
'restricted_paths' => [],
],
// ==========================================================================
// FEATURE TOGGLES
// ==========================================================================
'features' => [
'file_manager' => true,
'terminal' => true,
'code_editor' => true,
'process_manager' => true,
'network_tools' => true,
'database_tools' => true,
'system_info' => true,
],
// ==========================================================================
// USER INTERFACE SETTINGS
// ==========================================================================
'ui' => [
// Default theme: dark, light, matrix, ocean, dracula
'theme' => 'dark',
// Default language
'language' => 'en',
// Items per page in lists
'items_per_page' => 50,
// Show hidden files by default
'show_hidden_files' => false,
// Auto-refresh interval in seconds (0 = disabled)
'auto_refresh' => 30,
],
// ==========================================================================
// LIMITS
// ==========================================================================
'limits' => [
// Maximum file size for viewing (50MB)
'max_file_size' => 50 * 1024 * 1024,
// Maximum upload size (100MB)
'max_upload_size' => 100 * 1024 * 1024,
// Command execution timeout in seconds
'command_timeout' => 30,
// Maximum command history entries
'max_history' => 100,
// Maximum process list entries
'max_processes' => 1000,
],
// ==========================================================================
// DATABASE SETTINGS (for database tools)
// ==========================================================================
'database' => [
'default_type' => 'mysql',
'default_host' => 'localhost',
'default_port' => 3306,
],
];

230
src/Core/Application.php Archivo normal
Ver fichero

@@ -0,0 +1,230 @@
<?php
/**
* AleShell2 Application Core
*
* Main application class that bootstraps and runs the shell.
*
* @package AleShell2\Core
*/
declare(strict_types=1);
namespace AleShell2\Core;
use AleShell2\Security\Auth;
use AleShell2\Security\Session;
class Application
{
private array $config = [];
private Request $request;
private Response $response;
private Router $router;
private Auth $auth;
private Session $session;
private static ?Application $instance = null;
/**
* Create application instance
*/
public function __construct()
{
self::$instance = $this;
$this->loadConfig();
$this->initializeComponents();
}
/**
* Get singleton instance
*/
public static function getInstance(): ?Application
{
return self::$instance;
}
/**
* Load configuration
*/
private function loadConfig(): void
{
$configFile = ALESHELL_SRC . '/Config/config.php';
if (file_exists($configFile)) {
$this->config = require $configFile;
} else {
// Default configuration
$this->config = $this->getDefaultConfig();
}
// Apply timezone
$timezone = $this->config['app']['timezone'] ?? 'UTC';
date_default_timezone_set($timezone);
// Set debug mode
if ($this->config['app']['debug'] ?? false) {
define('ALESHELL_DEBUG', true);
ini_set('display_errors', '1');
}
}
/**
* Initialize all components
*/
private function initializeComponents(): void
{
$this->request = new Request();
$this->response = new Response();
$this->session = new Session($this->config);
$this->auth = new Auth($this->config, $this->session);
$this->router = new Router($this->config, $this->auth);
}
/**
* Run the application
*/
public function run(): void
{
// Start session
$this->session->start();
// Check IP restrictions
$this->checkIPRestrictions();
// Route the request
$this->router->dispatch($this->request, $this->response);
}
/**
* Check IP whitelist/blacklist
*/
private function checkIPRestrictions(): void
{
$clientIP = $this->request->getClientIP();
// Check blocked IPs
$blockedIPs = $this->config['security']['blocked_ips'] ?? [];
if (in_array($clientIP, $blockedIPs, true)) {
http_response_code(403);
die('Access denied');
}
// Check allowed IPs (if configured)
$allowedIPs = $this->config['security']['allowed_ips'] ?? [];
if (!empty($allowedIPs) && !in_array($clientIP, $allowedIPs, true)) {
http_response_code(403);
die('Access denied');
}
}
/**
* Get configuration value
*/
public function config(string $key, mixed $default = null): mixed
{
$keys = explode('.', $key);
$value = $this->config;
foreach ($keys as $k) {
if (!isset($value[$k])) {
return $default;
}
$value = $value[$k];
}
return $value;
}
/**
* Get all configuration
*/
public function getConfig(): array
{
return $this->config;
}
/**
* Get request instance
*/
public function getRequest(): Request
{
return $this->request;
}
/**
* Get response instance
*/
public function getResponse(): Response
{
return $this->response;
}
/**
* Get auth instance
*/
public function getAuth(): Auth
{
return $this->auth;
}
/**
* Get session instance
*/
public function getSession(): Session
{
return $this->session;
}
/**
* Get default configuration
*/
private function getDefaultConfig(): array
{
return [
'app' => [
'name' => 'AleShell2',
'version' => ALESHELL_VERSION,
'debug' => false,
'timezone' => 'UTC',
],
'security' => [
'password' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi',
'session_timeout' => 3600,
'max_attempts' => 5,
'lockout_time' => 300,
'csrf_protection' => true,
'allowed_ips' => [],
'blocked_ips' => [],
'blocked_commands' => [],
'restricted_paths' => [],
],
'features' => [
'file_manager' => true,
'terminal' => true,
'code_editor' => true,
'process_manager' => true,
'network_tools' => true,
'database_tools' => true,
'system_info' => true,
],
'ui' => [
'theme' => 'dark',
'language' => 'en',
'items_per_page' => 50,
'show_hidden_files' => false,
'auto_refresh' => 30,
],
'limits' => [
'max_file_size' => 50 * 1024 * 1024,
'max_upload_size' => 100 * 1024 * 1024,
'command_timeout' => 30,
'max_history' => 100,
'max_processes' => 1000,
],
'database' => [
'default_type' => 'mysql',
'default_host' => 'localhost',
'default_port' => 3306,
],
];
}
}

256
src/Core/Request.php Archivo normal
Ver fichero

@@ -0,0 +1,256 @@
<?php
/**
* AleShell2 Request Handler
*
* Handles incoming HTTP requests.
*
* @package AleShell2\Core
*/
declare(strict_types=1);
namespace AleShell2\Core;
class Request
{
private string $method;
private string $uri;
private string $path;
private array $query;
private array $post;
private array $files;
private array $headers;
private array $server;
public function __construct()
{
$this->method = strtoupper($_SERVER['REQUEST_METHOD'] ?? 'GET');
$this->uri = $_SERVER['REQUEST_URI'] ?? '/';
$this->path = $this->parsePath();
$this->query = $_GET;
$this->post = $_POST;
$this->files = $_FILES;
$this->server = $_SERVER;
$this->headers = $this->parseHeaders();
}
/**
* Parse request path from URI
*/
private function parsePath(): string
{
$uri = $_SERVER['REQUEST_URI'] ?? '/';
$path = parse_url($uri, PHP_URL_PATH) ?: '/';
// Remove base path if present
$scriptDir = dirname($_SERVER['SCRIPT_NAME'] ?? '');
if ($scriptDir !== '/' && strpos($path, $scriptDir) === 0) {
$path = substr($path, strlen($scriptDir));
}
// Remove index.php if present
$path = preg_replace('#^/index\.php#', '', $path) ?: '/';
return '/' . trim($path, '/');
}
/**
* Parse request headers
*/
private function parseHeaders(): array
{
$headers = [];
foreach ($_SERVER as $key => $value) {
if (strpos($key, 'HTTP_') === 0) {
$header = str_replace('_', '-', substr($key, 5));
$headers[strtolower($header)] = $value;
}
}
// Add content type and length if present
if (isset($_SERVER['CONTENT_TYPE'])) {
$headers['content-type'] = $_SERVER['CONTENT_TYPE'];
}
if (isset($_SERVER['CONTENT_LENGTH'])) {
$headers['content-length'] = $_SERVER['CONTENT_LENGTH'];
}
return $headers;
}
/**
* Get request method
*/
public function getMethod(): string
{
return $this->method;
}
/**
* Get request URI
*/
public function getUri(): string
{
return $this->uri;
}
/**
* Get request path
*/
public function getPath(): string
{
return $this->path;
}
/**
* Check if request is AJAX
*/
public function isAjax(): bool
{
return strtolower($this->headers['x-requested-with'] ?? '') === 'xmlhttprequest';
}
/**
* Check if request method matches
*/
public function isMethod(string $method): bool
{
return $this->method === strtoupper($method);
}
/**
* Get query parameter
*/
public function get(string $key, mixed $default = null): mixed
{
return $this->query[$key] ?? $default;
}
/**
* Get POST parameter
*/
public function post(string $key, mixed $default = null): mixed
{
return $this->post[$key] ?? $default;
}
/**
* Get all POST data
*/
public function all(): array
{
return array_merge($this->query, $this->post);
}
/**
* Get input from GET or POST
*/
public function input(string $key, mixed $default = null): mixed
{
return $this->post[$key] ?? $this->query[$key] ?? $default;
}
/**
* Get header
*/
public function header(string $key, mixed $default = null): mixed
{
return $this->headers[strtolower($key)] ?? $default;
}
/**
* Get uploaded file
*/
public function file(string $key): ?array
{
return $this->files[$key] ?? null;
}
/**
* Get client IP address
*/
public function getClientIP(): string
{
$headers = [
'HTTP_CF_CONNECTING_IP', // Cloudflare
'HTTP_X_FORWARDED_FOR', // Proxy
'HTTP_X_REAL_IP', // Nginx
'HTTP_CLIENT_IP', // Other proxies
'REMOTE_ADDR', // Direct connection
];
foreach ($headers as $header) {
if (!empty($this->server[$header])) {
$ips = explode(',', $this->server[$header]);
$ip = trim($ips[0]);
if (filter_var($ip, FILTER_VALIDATE_IP)) {
return $ip;
}
}
}
return '0.0.0.0';
}
/**
* Get raw request body
*/
public function getBody(): string
{
return file_get_contents('php://input') ?: '';
}
/**
* Get JSON body
*/
public function json(): ?array
{
$body = $this->getBody();
if (empty($body)) {
return null;
}
$data = json_decode($body, true);
return is_array($data) ? $data : null;
}
/**
* Check if request expects JSON response
*/
public function expectsJson(): bool
{
$accept = $this->header('accept', '');
return str_contains($accept, 'application/json') || $this->isAjax();
}
/**
* Get server variable
*/
public function server(string $key, mixed $default = null): mixed
{
return $this->server[$key] ?? $default;
}
/**
* Get query string
*/
public function getQueryString(): string
{
return $_SERVER['QUERY_STRING'] ?? '';
}
/**
* Build URL with query parameters
*/
public function fullUrl(): string
{
$scheme = (!empty($this->server['HTTPS']) && $this->server['HTTPS'] !== 'off') ? 'https' : 'http';
$host = $this->server['HTTP_HOST'] ?? 'localhost';
return $scheme . '://' . $host . $this->uri;
}
}

241
src/Core/Response.php Archivo normal
Ver fichero

@@ -0,0 +1,241 @@
<?php
/**
* AleShell2 Response Handler
*
* Handles HTTP responses.
*
* @package AleShell2\Core
*/
declare(strict_types=1);
namespace AleShell2\Core;
class Response
{
private int $statusCode = 200;
private array $headers = [];
private string $content = '';
private bool $sent = false;
/**
* Set HTTP status code
*/
public function setStatusCode(int $code): self
{
$this->statusCode = $code;
return $this;
}
/**
* Get HTTP status code
*/
public function getStatusCode(): int
{
return $this->statusCode;
}
/**
* Set response header
*/
public function setHeader(string $name, string $value): self
{
$this->headers[$name] = $value;
return $this;
}
/**
* Set content type
*/
public function setContentType(string $type): self
{
$this->headers['Content-Type'] = $type;
return $this;
}
/**
* Set response content
*/
public function setContent(string $content): self
{
$this->content = $content;
return $this;
}
/**
* Get response content
*/
public function getContent(): string
{
return $this->content;
}
/**
* Send JSON response
*/
public function json(mixed $data, int $statusCode = 200): void
{
$this->statusCode = $statusCode;
$this->headers['Content-Type'] = 'application/json; charset=utf-8';
$this->content = json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
$this->send();
}
/**
* Send success JSON response
*/
public function success(mixed $data = null, string $message = 'Success'): void
{
$this->json([
'success' => true,
'message' => $message,
'data' => $data,
]);
}
/**
* Send error JSON response
*/
public function error(string $message, int $statusCode = 400, mixed $data = null): void
{
$this->json([
'success' => false,
'message' => $message,
'data' => $data,
], $statusCode);
}
/**
* Send HTML response
*/
public function html(string $content, int $statusCode = 200): void
{
$this->statusCode = $statusCode;
$this->headers['Content-Type'] = 'text/html; charset=utf-8';
$this->content = $content;
$this->send();
}
/**
* Send file download
*/
public function download(string $filePath, string $filename = null): void
{
if (!file_exists($filePath)) {
$this->error('File not found', 404);
return;
}
$filename = $filename ?? basename($filePath);
$mimeType = mime_content_type($filePath) ?: 'application/octet-stream';
$this->headers['Content-Type'] = $mimeType;
$this->headers['Content-Disposition'] = 'attachment; filename="' . $filename . '"';
$this->headers['Content-Length'] = (string)filesize($filePath);
$this->headers['Cache-Control'] = 'no-cache, no-store, must-revalidate';
$this->sendHeaders();
readfile($filePath);
$this->sent = true;
exit;
}
/**
* Send file inline (for viewing)
*/
public function file(string $filePath, string $mimeType = null): void
{
if (!file_exists($filePath)) {
$this->error('File not found', 404);
return;
}
$mimeType = $mimeType ?? mime_content_type($filePath) ?: 'application/octet-stream';
$this->headers['Content-Type'] = $mimeType;
$this->headers['Content-Length'] = (string)filesize($filePath);
$this->sendHeaders();
readfile($filePath);
$this->sent = true;
exit;
}
/**
* Redirect to URL
*/
public function redirect(string $url, int $statusCode = 302): void
{
$this->statusCode = $statusCode;
$this->headers['Location'] = $url;
$this->send();
exit;
}
/**
* Send headers
*/
private function sendHeaders(): void
{
if (headers_sent()) {
return;
}
http_response_code($this->statusCode);
foreach ($this->headers as $name => $value) {
header("{$name}: {$value}");
}
}
/**
* Send response
*/
public function send(): void
{
if ($this->sent) {
return;
}
$this->sendHeaders();
echo $this->content;
$this->sent = true;
if (function_exists('fastcgi_finish_request')) {
fastcgi_finish_request();
}
}
/**
* Check if response was sent
*/
public function isSent(): bool
{
return $this->sent;
}
/**
* Set no cache headers
*/
public function noCache(): self
{
$this->headers['Cache-Control'] = 'no-cache, no-store, must-revalidate';
$this->headers['Pragma'] = 'no-cache';
$this->headers['Expires'] = '0';
return $this;
}
/**
* Set cache headers
*/
public function cache(int $seconds): self
{
$this->headers['Cache-Control'] = "public, max-age={$seconds}";
$this->headers['Expires'] = gmdate('D, d M Y H:i:s', time() + $seconds) . ' GMT';
return $this;
}
}

248
src/Core/Router.php Archivo normal
Ver fichero

@@ -0,0 +1,248 @@
<?php
/**
* AleShell2 Router
*
* Routes requests to appropriate handlers.
*
* @package AleShell2\Core
*/
declare(strict_types=1);
namespace AleShell2\Core;
use AleShell2\Security\Auth;
class Router
{
private array $config;
private Auth $auth;
private array $routes = [];
public function __construct(array $config, Auth $auth)
{
$this->config = $config;
$this->auth = $auth;
$this->registerRoutes();
}
/**
* Register all application routes
*/
private function registerRoutes(): void
{
// Authentication routes (no auth required)
$this->addRoute('GET', '/login', 'auth.login', false);
$this->addRoute('POST', '/login', 'auth.login', false);
$this->addRoute('POST', '/logout', 'auth.logout', true);
$this->addRoute('GET', '/auth/status', 'auth.status', false);
// Main routes (auth required)
$this->addRoute('GET', '/', 'dashboard.index', true);
$this->addRoute('GET', '/dashboard', 'dashboard.index', true);
// File manager
$this->addRoute('GET', '/files', 'files.index', true);
$this->addRoute('POST', '/files/list', 'files.list', true);
$this->addRoute('POST', '/files/read', 'files.read', true);
$this->addRoute('POST', '/files/write', 'files.write', true);
$this->addRoute('POST', '/files/delete', 'files.delete', true);
$this->addRoute('POST', '/files/create', 'files.create', true);
$this->addRoute('POST', '/files/rename', 'files.rename', true);
$this->addRoute('POST', '/files/copy', 'files.copy', true);
$this->addRoute('POST', '/files/move', 'files.move', true);
$this->addRoute('POST', '/files/chmod', 'files.chmod', true);
$this->addRoute('POST', '/files/upload', 'files.upload', true);
$this->addRoute('GET', '/files/download', 'files.download', true);
$this->addRoute('POST', '/files/mkdir', 'files.mkdir', true);
$this->addRoute('POST', '/files/search', 'files.search', true);
// Terminal
$this->addRoute('GET', '/terminal', 'terminal.index', true);
$this->addRoute('POST', '/terminal/execute', 'terminal.execute', true);
$this->addRoute('GET', '/terminal/history', 'terminal.history', true);
$this->addRoute('POST', '/terminal/clear', 'terminal.clear', true);
// Code editor
$this->addRoute('GET', '/editor', 'editor.index', true);
$this->addRoute('POST', '/editor/save', 'editor.save', true);
// Processes
$this->addRoute('GET', '/processes', 'processes.index', true);
$this->addRoute('POST', '/processes/list', 'processes.list', true);
$this->addRoute('POST', '/processes/kill', 'processes.kill', true);
// Network
$this->addRoute('GET', '/network', 'network.index', true);
$this->addRoute('POST', '/network/connections', 'network.connections', true);
$this->addRoute('POST', '/network/ping', 'network.ping', true);
$this->addRoute('POST', '/network/traceroute', 'network.traceroute', true);
$this->addRoute('POST', '/network/portscan', 'network.portscan', true);
// Database
$this->addRoute('GET', '/database', 'database.index', true);
$this->addRoute('POST', '/database/connect', 'database.connect', true);
$this->addRoute('POST', '/database/query', 'database.query', true);
$this->addRoute('POST', '/database/tables', 'database.tables', true);
$this->addRoute('POST', '/database/structure', 'database.structure', true);
// System info
$this->addRoute('GET', '/system', 'system.index', true);
$this->addRoute('GET', '/system/info', 'system.info', true);
// API endpoints
$this->addRoute('GET', '/api/config', 'api.config', true);
$this->addRoute('GET', '/api/system', 'api.system', true);
}
/**
* Add a route
*/
public function addRoute(string $method, string $path, string $handler, bool $requireAuth = true): void
{
$this->routes[] = [
'method' => strtoupper($method),
'path' => $path,
'handler' => $handler,
'requireAuth' => $requireAuth,
'pattern' => $this->pathToPattern($path),
];
}
/**
* Convert path to regex pattern
*/
private function pathToPattern(string $path): string
{
$pattern = preg_replace('/\{([^}]+)\}/', '(?P<$1>[^/]+)', $path);
return '#^' . $pattern . '$#';
}
/**
* Dispatch request to handler
*/
public function dispatch(Request $request, Response $response): void
{
$method = $request->getMethod();
$path = $request->getPath();
// Handle empty path
if ($path === '' || $path === '/index.php') {
$path = '/';
}
// Find matching route
foreach ($this->routes as $route) {
if ($route['method'] === $method && preg_match($route['pattern'], $path, $matches)) {
// Check authentication
if ($route['requireAuth'] && !$this->auth->isAuthenticated()) {
if ($request->expectsJson()) {
$response->error('Unauthorized', 401);
} else {
$response->redirect('/login');
}
return;
}
// Extract parameters
$params = array_filter($matches, 'is_string', ARRAY_FILTER_USE_KEY);
// Execute handler
$this->executeHandler($route['handler'], $request, $response, $params);
return;
}
}
// No route found - 404
if ($request->expectsJson()) {
$response->error('Not Found', 404);
} else {
$response->html($this->get404Page(), 404);
}
}
/**
* Execute route handler
*/
private function executeHandler(string $handler, Request $request, Response $response, array $params): void
{
[$module, $action] = explode('.', $handler);
$controllerClass = 'AleShell2\\Modules\\' . ucfirst($module) . '\\' . ucfirst($module) . 'Controller';
if (!class_exists($controllerClass)) {
$response->error("Controller not found: {$controllerClass}", 500);
return;
}
$controller = new $controllerClass($this->config, $this->auth);
if (!method_exists($controller, $action)) {
$response->error("Action not found: {$action}", 500);
return;
}
$controller->$action($request, $response, $params);
}
/**
* Get 404 page HTML
*/
private function get404Page(): string
{
return '<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>404 - Not Found</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
background: #1a202c;
color: #f7fafc;
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
margin: 0;
}
.container {
text-align: center;
padding: 2rem;
}
h1 {
font-size: 6rem;
margin: 0;
color: #667eea;
}
p {
font-size: 1.5rem;
color: #a0aec0;
margin: 1rem 0;
}
a {
display: inline-block;
margin-top: 1rem;
padding: 0.75rem 1.5rem;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
text-decoration: none;
border-radius: 8px;
transition: transform 0.2s;
}
a:hover {
transform: translateY(-2px);
}
</style>
</head>
<body>
<div class="container">
<h1>404</h1>
<p>Page not found</p>
<a href="/">Return to Dashboard</a>
</div>
</body>
</html>';
}
}

247
src/Core/View.php Archivo normal
Ver fichero

@@ -0,0 +1,247 @@
<?php
/**
* AleShell2 View Renderer
*
* Handles rendering of views and templates.
*
* @package AleShell2\Core
*/
declare(strict_types=1);
namespace AleShell2\Core;
class View
{
private static array $sharedData = [];
/**
* Share data with all views
*/
public static function share(string $key, mixed $value): void
{
self::$sharedData[$key] = $value;
}
/**
* Render a view
*/
public static function render(string $view, array $data = []): string
{
$viewPath = ALESHELL_SRC . '/Views/' . str_replace('.', '/', $view) . '.php';
if (!file_exists($viewPath)) {
throw new \RuntimeException("View not found: {$view}");
}
// Merge shared data
$data = array_merge(self::$sharedData, $data);
// Extract data to variables
extract($data, EXTR_SKIP);
// Capture output
ob_start();
include $viewPath;
return ob_get_clean();
}
/**
* Render a view with layout
*/
public static function renderWithLayout(string $view, string $layout = 'layouts.main', array $data = []): string
{
// Render the view content first
$data['content'] = self::render($view, $data);
// Render the layout with the view content
return self::render($layout, $data);
}
/**
* Include a partial
*/
public static function partial(string $partial, array $data = []): string
{
return self::render('components.' . $partial, $data);
}
/**
* Escape HTML
*/
public static function e(string $value): string
{
return htmlspecialchars($value, ENT_QUOTES | ENT_HTML5, 'UTF-8');
}
/**
* Format bytes to human readable
*/
public static function formatBytes(int $bytes, int $precision = 2): string
{
if ($bytes === 0) {
return '0 B';
}
$units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB'];
$pow = floor(log($bytes, 1024));
$pow = min($pow, count($units) - 1);
return round($bytes / pow(1024, $pow), $precision) . ' ' . $units[$pow];
}
/**
* Format timestamp
*/
public static function formatTime(int $timestamp, string $format = 'Y-m-d H:i:s'): string
{
return date($format, $timestamp);
}
/**
* Get file icon based on extension
*/
public static function fileIcon(string $filename, bool $isDirectory = false): string
{
if ($isDirectory) {
return '📁';
}
$ext = strtolower(pathinfo($filename, PATHINFO_EXTENSION));
$icons = [
// Code
'php' => '🐘',
'js' => '📜',
'ts' => '📘',
'py' => '🐍',
'rb' => '💎',
'java' => '☕',
'c' => '⚙️',
'cpp' => '⚙️',
'h' => '⚙️',
'cs' => '🔷',
'go' => '🔵',
'rs' => '🦀',
'swift' => '🍎',
// Web
'html' => '🌐',
'htm' => '🌐',
'css' => '🎨',
'scss' => '🎨',
'sass' => '🎨',
'less' => '🎨',
'vue' => '💚',
'jsx' => '⚛️',
'tsx' => '⚛️',
// Data
'json' => '📋',
'xml' => '📋',
'yaml' => '📋',
'yml' => '📋',
'toml' => '📋',
'csv' => '📊',
'sql' => '🗃️',
// Documents
'txt' => '📄',
'md' => '📝',
'pdf' => '📕',
'doc' => '📘',
'docx' => '📘',
'xls' => '📗',
'xlsx' => '📗',
'ppt' => '📙',
'pptx' => '📙',
// Images
'jpg' => '🖼️',
'jpeg' => '🖼️',
'png' => '🖼️',
'gif' => '🖼️',
'svg' => '🖼️',
'webp' => '🖼️',
'ico' => '🖼️',
'bmp' => '🖼️',
// Media
'mp3' => '🎵',
'wav' => '🎵',
'ogg' => '🎵',
'flac' => '🎵',
'mp4' => '🎬',
'avi' => '🎬',
'mkv' => '🎬',
'mov' => '🎬',
// Archives
'zip' => '📦',
'rar' => '📦',
'tar' => '📦',
'gz' => '📦',
'7z' => '📦',
// Config
'env' => '⚙️',
'ini' => '⚙️',
'conf' => '⚙️',
'config' => '⚙️',
// Executable
'sh' => '⚡',
'bash' => '⚡',
'zsh' => '⚡',
'bat' => '⚡',
'exe' => '⚡',
'bin' => '⚡',
];
return $icons[$ext] ?? '📄';
}
/**
* Syntax highlight language based on extension
*/
public static function highlightLang(string $filename): string
{
$ext = strtolower(pathinfo($filename, PATHINFO_EXTENSION));
$langs = [
'php' => 'php',
'js' => 'javascript',
'ts' => 'typescript',
'jsx' => 'jsx',
'tsx' => 'tsx',
'py' => 'python',
'rb' => 'ruby',
'java' => 'java',
'c' => 'c',
'cpp' => 'cpp',
'cs' => 'csharp',
'go' => 'go',
'rs' => 'rust',
'swift' => 'swift',
'html' => 'html',
'htm' => 'html',
'css' => 'css',
'scss' => 'scss',
'sass' => 'sass',
'less' => 'less',
'json' => 'json',
'xml' => 'xml',
'yaml' => 'yaml',
'yml' => 'yaml',
'md' => 'markdown',
'sql' => 'sql',
'sh' => 'bash',
'bash' => 'bash',
'zsh' => 'bash',
'ini' => 'ini',
'env' => 'bash',
];
return $langs[$ext] ?? 'plaintext';
}
}

Ver fichero

@@ -0,0 +1,223 @@
<?php
/**
* AleShell2 API Controller
*
* Handles general API endpoints.
*
* @package AleShell2\Modules\Api
*/
declare(strict_types=1);
namespace AleShell2\Modules\Api;
use AleShell2\Modules\BaseController;
use AleShell2\Core\Request;
use AleShell2\Core\Response;
class ApiController extends BaseController
{
/**
* API status endpoint
*/
public function status(Request $request, Response $response, array $params): void
{
$this->success($response, [
'name' => 'AleShell2',
'version' => '1.0.0',
'status' => 'operational',
'php_version' => PHP_VERSION,
'server_time' => date('Y-m-d H:i:s'),
'timezone' => date_default_timezone_get(),
]);
}
/**
* Get CSRF token
*/
public function csrf(Request $request, Response $response, array $params): void
{
$this->success($response, [
'csrf_token' => $this->app->getAuth()->generateCsrfToken(),
]);
}
/**
* Health check endpoint
*/
public function health(Request $request, Response $response, array $params): void
{
$checks = [
'application' => true,
'session' => session_status() === PHP_SESSION_ACTIVE,
'writable_temp' => is_writable(sys_get_temp_dir()),
];
$healthy = !in_array(false, $checks, true);
$response->json([
'healthy' => $healthy,
'checks' => $checks,
'timestamp' => time(),
], $healthy ? 200 : 503);
}
/**
* Quick command execution API
*/
public function exec(Request $request, Response $response, array $params): void
{
if (!$this->validateCsrf($request, $response)) {
return;
}
$command = $request->post('command');
if (!$command) {
$this->error($response, 'Command is required');
return;
}
$output = shell_exec($command . ' 2>&1');
$this->success($response, [
'command' => $command,
'output' => $output ?? '',
]);
}
/**
* PHP eval endpoint
*/
public function eval(Request $request, Response $response, array $params): void
{
if (!$this->validateCsrf($request, $response)) {
return;
}
$code = $request->post('code');
if (!$code) {
$this->error($response, 'Code is required');
return;
}
// Capture output
ob_start();
try {
$result = eval($code);
$output = ob_get_clean();
$this->success($response, [
'result' => $result,
'output' => $output,
]);
} catch (\Throwable $e) {
ob_end_clean();
$this->error($response, 'Eval error: ' . $e->getMessage());
}
}
/**
* File upload API
*/
public function upload(Request $request, Response $response, array $params): void
{
if (!$this->validateCsrf($request, $response)) {
return;
}
$destination = $request->post('destination');
if (!$destination) {
$this->error($response, 'Destination path is required');
return;
}
if (!isset($_FILES['file'])) {
$this->error($response, 'No file uploaded');
return;
}
$file = $_FILES['file'];
if ($file['error'] !== UPLOAD_ERR_OK) {
$this->error($response, 'Upload error: ' . $this->getUploadErrorMessage($file['error']));
return;
}
$targetPath = rtrim($destination, '/') . '/' . basename($file['name']);
if (!is_dir($destination)) {
$this->error($response, 'Destination directory does not exist');
return;
}
if (!is_writable($destination)) {
$this->error($response, 'Destination directory is not writable');
return;
}
if (move_uploaded_file($file['tmp_name'], $targetPath)) {
$this->success($response, [
'message' => 'File uploaded successfully',
'path' => $targetPath,
'size' => $file['size'],
]);
} else {
$this->error($response, 'Failed to move uploaded file');
}
}
/**
* Download file API
*/
public function download(Request $request, Response $response, array $params): void
{
$path = $request->get('path');
if (!$path) {
$this->error($response, 'Path is required');
return;
}
if (!file_exists($path)) {
$this->error($response, 'File not found');
return;
}
if (!is_file($path)) {
$this->error($response, 'Not a file');
return;
}
if (!is_readable($path)) {
$this->error($response, 'File not readable');
return;
}
$content = file_get_contents($path);
$filename = basename($path);
$contentType = mime_content_type($path) ?: 'application/octet-stream';
$response->download($content, $filename, $contentType);
}
/**
* Get upload error message
*/
private function getUploadErrorMessage(int $error): string
{
return match($error) {
UPLOAD_ERR_INI_SIZE => 'File exceeds upload_max_filesize',
UPLOAD_ERR_FORM_SIZE => 'File exceeds MAX_FILE_SIZE',
UPLOAD_ERR_PARTIAL => 'File was only partially uploaded',
UPLOAD_ERR_NO_FILE => 'No file was uploaded',
UPLOAD_ERR_NO_TMP_DIR => 'Missing temporary folder',
UPLOAD_ERR_CANT_WRITE => 'Failed to write file to disk',
UPLOAD_ERR_EXTENSION => 'Upload stopped by extension',
default => 'Unknown upload error',
};
}
}

Ver fichero

@@ -0,0 +1,82 @@
<?php
/**
* AleShell2 Auth Controller
*
* Handles authentication routes.
*
* @package AleShell2\Modules\Auth
*/
declare(strict_types=1);
namespace AleShell2\Modules\Auth;
use AleShell2\Modules\BaseController;
use AleShell2\Core\Request;
use AleShell2\Core\Response;
use AleShell2\Core\View;
class AuthController extends BaseController
{
/**
* Show login form or process login
*/
public function login(Request $request, Response $response, array $params): void
{
// If already authenticated, redirect to dashboard
if ($this->auth->isAuthenticated()) {
$response->redirect('/');
return;
}
$error = '';
// Process login form
if ($request->isMethod('POST')) {
$password = $request->post('password', '');
if (empty($password)) {
$error = 'Password is required';
} elseif ($this->auth->isRateLimited()) {
$remaining = $this->auth->getRemainingLockoutTime();
$error = "Too many failed attempts. Try again in {$remaining} seconds.";
} elseif ($this->auth->attempt($password)) {
$response->redirect('/');
return;
} else {
$error = 'Invalid password';
}
}
// Render login page
$html = View::render('auth.login', [
'error' => $error,
'version' => ALESHELL_VERSION,
]);
$response->html($html);
}
/**
* Process logout
*/
public function logout(Request $request, Response $response, array $params): void
{
$this->auth->logout();
$response->redirect('/login');
}
/**
* Get authentication status (AJAX)
*/
public function status(Request $request, Response $response, array $params): void
{
$response->json([
'success' => true,
'data' => [
'authenticated' => $this->auth->isAuthenticated(),
'csrf_token' => $this->auth->getCsrfToken(),
],
]);
}
}

230
src/Modules/BaseController.php Archivo normal
Ver fichero

@@ -0,0 +1,230 @@
<?php
/**
* AleShell2 Base Controller
*
* Base class for all module controllers.
*
* @package AleShell2\Modules
*/
declare(strict_types=1);
namespace AleShell2\Modules;
use AleShell2\Core\Request;
use AleShell2\Core\Response;
use AleShell2\Core\View;
use AleShell2\Security\Auth;
abstract class BaseController
{
protected array $config;
protected Auth $auth;
public function __construct(array $config, Auth $auth)
{
$this->config = $config;
$this->auth = $auth;
}
/**
* Render a view with layout
*/
protected function render(Response $response, string $view, array $data = []): void
{
$data['auth'] = $this->auth;
$data['config'] = $this->config;
$data['version'] = ALESHELL_VERSION;
$data['csrfToken'] = $this->auth->getCsrfToken();
$html = View::renderWithLayout($view, 'layouts.main', $data);
$response->html($html);
}
/**
* Render just a view without layout
*/
protected function renderPartial(Response $response, string $view, array $data = []): void
{
$data['auth'] = $this->auth;
$data['config'] = $this->config;
$html = View::render($view, $data);
$response->html($html);
}
/**
* Send JSON success response
*/
protected function success(Response $response, mixed $data = null, string $message = 'Success'): void
{
$response->success($data, $message);
}
/**
* Send JSON error response
*/
protected function error(Response $response, string $message, int $code = 400): void
{
$response->error($message, $code);
}
/**
* Validate CSRF token for state-changing requests
*/
protected function validateCsrf(Request $request, Response $response): bool
{
if (!($this->config['security']['csrf_protection'] ?? true)) {
return true;
}
$token = $request->header('X-CSRF-Token')
?? $request->post('_token')
?? $request->input('csrf_token');
if (!$token || !$this->auth->validateCsrfToken($token)) {
$this->error($response, 'Invalid CSRF token', 403);
return false;
}
return true;
}
/**
* Check if path is allowed
*/
protected function isPathAllowed(string $path): bool
{
$restrictedPaths = $this->config['security']['restricted_paths'] ?? [];
if (empty($restrictedPaths)) {
return true;
}
$realPath = realpath($path);
if ($realPath === false) {
return true; // Allow non-existent paths for creation
}
foreach ($restrictedPaths as $restricted) {
if (strpos($realPath, $restricted) === 0) {
return false;
}
}
return true;
}
/**
* Check if command is allowed
*/
protected function isCommandAllowed(string $command): bool
{
$blockedCommands = $this->config['security']['blocked_commands'] ?? [];
foreach ($blockedCommands as $blocked) {
if (stripos($command, $blocked) !== false) {
return false;
}
}
return true;
}
/**
* Format bytes to human readable
*/
protected function formatBytes(int $bytes, int $precision = 2): string
{
return View::formatBytes($bytes, $precision);
}
/**
* Get system info
*/
protected function getSystemInfo(): array
{
return [
'hostname' => gethostname(),
'os' => php_uname('s') . ' ' . php_uname('r'),
'php_version' => PHP_VERSION,
'server_software' => $_SERVER['SERVER_SOFTWARE'] ?? 'Unknown',
'current_path' => getcwd(),
'disk_free' => disk_free_space('.'),
'disk_total' => disk_total_space('.'),
'load_average' => function_exists('sys_getloadavg') ? sys_getloadavg() : null,
'uptime' => $this->getUptime(),
'memory' => $this->getMemoryInfo(),
'time' => date('Y-m-d H:i:s'),
];
}
/**
* Get server uptime
*/
protected function getUptime(): ?string
{
if (file_exists('/proc/uptime')) {
$uptime = (float)file_get_contents('/proc/uptime');
return $this->formatUptime($uptime);
}
// Try 'uptime' command on Unix
if (PHP_OS_FAMILY !== 'Windows') {
$output = @shell_exec('uptime -p 2>/dev/null');
if ($output) {
return trim($output);
}
}
return null;
}
/**
* Format uptime seconds to human readable
*/
protected function formatUptime(float $seconds): string
{
$days = floor($seconds / 86400);
$hours = floor(($seconds % 86400) / 3600);
$minutes = floor(($seconds % 3600) / 60);
$parts = [];
if ($days > 0) $parts[] = "{$days}d";
if ($hours > 0) $parts[] = "{$hours}h";
if ($minutes > 0) $parts[] = "{$minutes}m";
return implode(' ', $parts) ?: '< 1m';
}
/**
* Get memory info
*/
protected function getMemoryInfo(): array
{
$memory = [
'total' => 0,
'free' => 0,
'used' => 0,
'cached' => 0,
'percent' => 0,
];
if (file_exists('/proc/meminfo')) {
$meminfo = file_get_contents('/proc/meminfo');
preg_match_all('/^(\w+):\s+(\d+)\s+kB$/m', $meminfo, $matches);
$data = array_combine($matches[1], $matches[2]);
$memory['total'] = ($data['MemTotal'] ?? 0) * 1024;
$memory['free'] = ($data['MemFree'] ?? 0) * 1024;
$memory['cached'] = ($data['Cached'] ?? 0) * 1024;
$memory['used'] = $memory['total'] - $memory['free'] - $memory['cached'];
$memory['percent'] = $memory['total'] > 0
? round(($memory['used'] / $memory['total']) * 100, 1)
: 0;
}
return $memory;
}
}

Ver fichero

@@ -0,0 +1,90 @@
<?php
/**
* AleShell2 Dashboard Controller
*
* Handles dashboard module.
*
* @package AleShell2\Modules\Dashboard
*/
declare(strict_types=1);
namespace AleShell2\Modules\Dashboard;
use AleShell2\Modules\BaseController;
use AleShell2\Core\Request;
use AleShell2\Core\Response;
class DashboardController extends BaseController
{
/**
* Show dashboard
*/
public function index(Request $request, Response $response, array $params): void
{
// If AJAX request, return JSON
if ($request->isAjax()) {
$this->success($response, $this->getDashboardData());
return;
}
// Render dashboard page
$this->render($response, 'modules.dashboard', [
'currentModule' => 'dashboard',
'pageTitle' => 'Dashboard - AleShell2',
'data' => $this->getDashboardData(),
]);
}
/**
* Get dashboard data
*/
private function getDashboardData(): array
{
return [
'system' => $this->getSystemInfo(),
'user' => $this->getUserInfo(),
'modules' => $this->getEnabledModules(),
'features' => $this->config['features'] ?? [],
];
}
/**
* Get user session info
*/
private function getUserInfo(): array
{
return [
'ip' => $this->auth->getUserIP(),
'login_time' => $this->auth->getLoginTime(),
'last_activity' => $this->auth->getLastActivity(),
];
}
/**
* Get list of enabled modules
*/
private function getEnabledModules(): array
{
$features = $this->config['features'] ?? [];
$modules = [];
$moduleInfo = [
'file_manager' => ['name' => 'Files', 'icon' => '📁', 'path' => '/files'],
'terminal' => ['name' => 'Terminal', 'icon' => '💻', 'path' => '/terminal'],
'code_editor' => ['name' => 'Editor', 'icon' => '📝', 'path' => '/editor'],
'process_manager' => ['name' => 'Processes', 'icon' => '⚙️', 'path' => '/processes'],
'network_tools' => ['name' => 'Network', 'icon' => '🌐', 'path' => '/network'],
'database_tools' => ['name' => 'Database', 'icon' => '🗄️', 'path' => '/database'],
'system_info' => ['name' => 'System', 'icon' => '📊', 'path' => '/system'],
];
foreach ($moduleInfo as $key => $info) {
if ($features[$key] ?? true) {
$modules[$key] = $info;
}
}
return $modules;
}
}

Ver fichero

@@ -0,0 +1,472 @@
<?php
/**
* AleShell2 Database Controller
*
* Handles database management module.
*
* @package AleShell2\Modules\Database
*/
declare(strict_types=1);
namespace AleShell2\Modules\Database;
use AleShell2\Modules\BaseController;
use AleShell2\Core\Request;
use AleShell2\Core\Response;
class DatabaseController extends BaseController
{
private ?\PDO $connection = null;
private ?string $driver = null;
/**
* Show database module
*/
public function index(Request $request, Response $response, array $params): void
{
$this->render($response, 'modules.database', [
'currentModule' => 'database',
'pageTitle' => 'Database Manager - AleShell2',
]);
}
/**
* Connect to database
*/
public function connect(Request $request, Response $response, array $params): void
{
if (!$this->validateCsrf($request, $response)) {
return;
}
$driver = $request->post('driver', 'mysql');
$host = $request->post('host', 'localhost');
$port = (int)$request->post('port', 3306);
$database = $request->post('database', '');
$username = $request->post('username', '');
$password = $request->post('password', '');
try {
$connection = $this->createConnection($driver, $host, $port, $database, $username, $password);
// Store connection in session
$_SESSION['db_connection'] = [
'driver' => $driver,
'host' => $host,
'port' => $port,
'database' => $database,
'username' => $username,
'password' => $password,
];
// Get databases list
$databases = $this->getDatabases($connection, $driver);
$this->success($response, [
'message' => 'Connected successfully',
'databases' => $databases,
'current_database' => $database,
]);
} catch (\PDOException $e) {
$this->error($response, 'Connection failed: ' . $e->getMessage());
}
}
/**
* Disconnect from database
*/
public function disconnect(Request $request, Response $response, array $params): void
{
unset($_SESSION['db_connection']);
$this->success($response, ['message' => 'Disconnected']);
}
/**
* Get tables in database
*/
public function tables(Request $request, Response $response, array $params): void
{
try {
$connection = $this->getSessionConnection();
if (!$connection) {
$this->error($response, 'Not connected to database');
return;
}
$database = $request->get('database', $_SESSION['db_connection']['database'] ?? '');
if ($database) {
$connection->exec("USE " . $this->quoteIdentifier($database, $_SESSION['db_connection']['driver']));
}
$tables = $this->getTablesList($connection, $_SESSION['db_connection']['driver']);
$this->success($response, ['tables' => $tables]);
} catch (\Exception $e) {
$this->error($response, 'Failed to get tables: ' . $e->getMessage());
}
}
/**
* Get table structure
*/
public function structure(Request $request, Response $response, array $params): void
{
try {
$connection = $this->getSessionConnection();
if (!$connection) {
$this->error($response, 'Not connected to database');
return;
}
$table = $request->get('table');
if (!$table) {
$this->error($response, 'Table name is required');
return;
}
$structure = $this->getTableStructure($connection, $table, $_SESSION['db_connection']['driver']);
$this->success($response, ['structure' => $structure]);
} catch (\Exception $e) {
$this->error($response, 'Failed to get structure: ' . $e->getMessage());
}
}
/**
* Execute query
*/
public function query(Request $request, Response $response, array $params): void
{
if (!$this->validateCsrf($request, $response)) {
return;
}
try {
$connection = $this->getSessionConnection();
if (!$connection) {
$this->error($response, 'Not connected to database');
return;
}
$sql = $request->post('query');
$limit = min((int)$request->post('limit', 100), 1000);
if (!$sql) {
$this->error($response, 'Query is required');
return;
}
// Basic SQL injection prevention - disallow multiple statements
$sql = trim($sql);
// Check if it's a SELECT query
$isSelect = preg_match('/^\s*SELECT\s/i', $sql);
$startTime = microtime(true);
$stmt = $connection->prepare($sql);
$stmt->execute();
$executionTime = round((microtime(true) - $startTime) * 1000, 2);
$result = [
'execution_time' => $executionTime . ' ms',
'affected_rows' => $stmt->rowCount(),
];
if ($isSelect) {
$rows = $stmt->fetchAll(\PDO::FETCH_ASSOC);
$result['columns'] = $rows ? array_keys($rows[0]) : [];
$result['rows'] = array_slice($rows, 0, $limit);
$result['total_rows'] = count($rows);
$result['limited'] = count($rows) > $limit;
}
$this->success($response, $result);
} catch (\PDOException $e) {
$this->error($response, 'Query failed: ' . $e->getMessage());
}
}
/**
* Select database
*/
public function selectDb(Request $request, Response $response, array $params): void
{
try {
$connection = $this->getSessionConnection();
if (!$connection) {
$this->error($response, 'Not connected to database');
return;
}
$database = $request->post('database');
if (!$database) {
$this->error($response, 'Database name is required');
return;
}
$driver = $_SESSION['db_connection']['driver'];
$connection->exec("USE " . $this->quoteIdentifier($database, $driver));
$_SESSION['db_connection']['database'] = $database;
$tables = $this->getTablesList($connection, $driver);
$this->success($response, [
'message' => "Selected database: {$database}",
'tables' => $tables,
]);
} catch (\Exception $e) {
$this->error($response, 'Failed to select database: ' . $e->getMessage());
}
}
/**
* Export table
*/
public function export(Request $request, Response $response, array $params): void
{
try {
$connection = $this->getSessionConnection();
if (!$connection) {
$this->error($response, 'Not connected to database');
return;
}
$table = $request->get('table');
$format = $request->get('format', 'sql');
if (!$table) {
$this->error($response, 'Table name is required');
return;
}
$driver = $_SESSION['db_connection']['driver'];
// Get table data
$stmt = $connection->query("SELECT * FROM " . $this->quoteIdentifier($table, $driver));
$rows = $stmt->fetchAll(\PDO::FETCH_ASSOC);
if ($format === 'csv') {
$content = $this->exportToCsv($rows, $table);
$filename = "{$table}.csv";
$contentType = 'text/csv';
} else {
$content = $this->exportToSql($connection, $rows, $table, $driver);
$filename = "{$table}.sql";
$contentType = 'application/sql';
}
$response->download($content, $filename, $contentType);
} catch (\Exception $e) {
$this->error($response, 'Export failed: ' . $e->getMessage());
}
}
/**
* Create PDO connection
*/
private function createConnection(string $driver, string $host, int $port, string $database, string $username, string $password): \PDO
{
$dsn = match($driver) {
'mysql' => "mysql:host={$host};port={$port}" . ($database ? ";dbname={$database}" : '') . ";charset=utf8mb4",
'pgsql' => "pgsql:host={$host};port={$port}" . ($database ? ";dbname={$database}" : ''),
'sqlite' => "sqlite:{$database}",
default => throw new \InvalidArgumentException("Unsupported driver: {$driver}"),
};
$options = [
\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION,
\PDO::ATTR_DEFAULT_FETCH_MODE => \PDO::FETCH_ASSOC,
\PDO::ATTR_EMULATE_PREPARES => false,
];
if ($driver === 'sqlite') {
return new \PDO($dsn, null, null, $options);
}
return new \PDO($dsn, $username, $password, $options);
}
/**
* Get connection from session
*/
private function getSessionConnection(): ?\PDO
{
if (!isset($_SESSION['db_connection'])) {
return null;
}
$conn = $_SESSION['db_connection'];
try {
return $this->createConnection(
$conn['driver'],
$conn['host'],
$conn['port'],
$conn['database'],
$conn['username'],
$conn['password']
);
} catch (\Exception $e) {
unset($_SESSION['db_connection']);
return null;
}
}
/**
* Get databases list
*/
private function getDatabases(\PDO $connection, string $driver): array
{
$databases = [];
$query = match($driver) {
'mysql' => "SHOW DATABASES",
'pgsql' => "SELECT datname FROM pg_database WHERE datistemplate = false",
'sqlite' => null,
default => null,
};
if ($query) {
$stmt = $connection->query($query);
while ($row = $stmt->fetch(\PDO::FETCH_NUM)) {
$databases[] = $row[0];
}
}
return $databases;
}
/**
* Get tables list
*/
private function getTablesList(\PDO $connection, string $driver): array
{
$tables = [];
$query = match($driver) {
'mysql' => "SHOW TABLES",
'pgsql' => "SELECT tablename FROM pg_tables WHERE schemaname = 'public'",
'sqlite' => "SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'",
default => null,
};
if ($query) {
$stmt = $connection->query($query);
while ($row = $stmt->fetch(\PDO::FETCH_NUM)) {
$tables[] = $row[0];
}
}
return $tables;
}
/**
* Get table structure
*/
private function getTableStructure(\PDO $connection, string $table, string $driver): array
{
$structure = [];
$quotedTable = $this->quoteIdentifier($table, $driver);
$query = match($driver) {
'mysql' => "DESCRIBE {$quotedTable}",
'pgsql' => "SELECT column_name, data_type, is_nullable, column_default FROM information_schema.columns WHERE table_name = '{$table}'",
'sqlite' => "PRAGMA table_info({$quotedTable})",
default => null,
};
if ($query) {
$stmt = $connection->query($query);
$structure = $stmt->fetchAll(\PDO::FETCH_ASSOC);
}
return $structure;
}
/**
* Quote identifier based on driver
*/
private function quoteIdentifier(string $identifier, string $driver): string
{
// Simple sanitization
$identifier = preg_replace('/[^a-zA-Z0-9_]/', '', $identifier);
return match($driver) {
'mysql' => "`{$identifier}`",
'pgsql' => "\"{$identifier}\"",
'sqlite' => "\"{$identifier}\"",
default => $identifier,
};
}
/**
* Export to CSV
*/
private function exportToCsv(array $rows, string $table): string
{
if (empty($rows)) {
return '';
}
$output = fopen('php://temp', 'r+');
// Header
fputcsv($output, array_keys($rows[0]));
// Data
foreach ($rows as $row) {
fputcsv($output, $row);
}
rewind($output);
$content = stream_get_contents($output);
fclose($output);
return $content;
}
/**
* Export to SQL
*/
private function exportToSql(\PDO $connection, array $rows, string $table, string $driver): string
{
$output = "-- Export of table: {$table}\n";
$output .= "-- Generated by AleShell2\n";
$output .= "-- Date: " . date('Y-m-d H:i:s') . "\n\n";
if (empty($rows)) {
return $output . "-- No data\n";
}
$quotedTable = $this->quoteIdentifier($table, $driver);
$columns = array_keys($rows[0]);
$quotedColumns = array_map(fn($c) => $this->quoteIdentifier($c, $driver), $columns);
foreach ($rows as $row) {
$values = [];
foreach ($row as $value) {
if ($value === null) {
$values[] = 'NULL';
} elseif (is_numeric($value)) {
$values[] = $value;
} else {
$values[] = $connection->quote($value);
}
}
$output .= "INSERT INTO {$quotedTable} (" . implode(', ', $quotedColumns) . ") VALUES (" . implode(', ', $values) . ");\n";
}
return $output;
}
}

Ver fichero

@@ -0,0 +1,87 @@
<?php
/**
* AleShell2 Editor Controller
*
* Handles code editor module.
*
* @package AleShell2\Modules\Editor
*/
declare(strict_types=1);
namespace AleShell2\Modules\Editor;
use AleShell2\Modules\BaseController;
use AleShell2\Core\Request;
use AleShell2\Core\Response;
class EditorController extends BaseController
{
/**
* Show editor
*/
public function index(Request $request, Response $response, array $params): void
{
$file = $request->get('file');
$content = '';
if ($file && is_file($file) && $this->isPathAllowed($file)) {
$maxSize = $this->config['limits']['max_file_size'] ?? 50 * 1024 * 1024;
if (filesize($file) <= $maxSize) {
$content = file_get_contents($file);
}
}
$this->render($response, 'modules.editor', [
'currentModule' => 'editor',
'pageTitle' => 'Code Editor - AleShell2',
'currentFile' => $file,
'content' => $content,
]);
}
/**
* Save file
*/
public function save(Request $request, Response $response, array $params): void
{
if (!$this->validateCsrf($request, $response)) {
return;
}
$path = $request->post('path');
$content = $request->post('content', '');
if (!$path) {
$this->error($response, 'File path is required');
return;
}
if (!$this->isPathAllowed($path)) {
$this->error($response, 'Access denied', 403);
return;
}
try {
// Create directory if it doesn't exist
$dir = dirname($path);
if (!is_dir($dir)) {
mkdir($dir, 0755, true);
}
$written = file_put_contents($path, $content);
if ($written === false) {
throw new \Exception('Failed to write file');
}
$this->success($response, [
'path' => $path,
'size' => $written,
], 'File saved successfully');
} catch (\Exception $e) {
$this->error($response, 'Failed to save: ' . $e->getMessage());
}
}
}

Ver fichero

@@ -0,0 +1,617 @@
<?php
/**
* AleShell2 Files Controller
*
* Handles file manager module.
*
* @package AleShell2\Modules\Files
*/
declare(strict_types=1);
namespace AleShell2\Modules\Files;
use AleShell2\Modules\BaseController;
use AleShell2\Core\Request;
use AleShell2\Core\Response;
class FilesController extends BaseController
{
/**
* Show file manager
*/
public function index(Request $request, Response $response, array $params): void
{
$path = $request->get('path', getcwd());
$this->render($response, 'modules.files', [
'currentModule' => 'files',
'pageTitle' => 'File Manager - AleShell2',
'currentPath' => realpath($path) ?: getcwd(),
]);
}
/**
* List directory contents
*/
public function list(Request $request, Response $response, array $params): void
{
$path = $request->post('path', getcwd());
$showHidden = (bool)$request->post('hidden', false);
$sortBy = $request->post('sort', 'name');
$sortOrder = $request->post('order', 'asc');
if (!$this->isPathAllowed($path)) {
$this->error($response, 'Access denied to this path', 403);
return;
}
if (!is_dir($path)) {
$this->error($response, 'Path is not a directory');
return;
}
try {
$files = $this->scanDirectory($path, $showHidden);
$files = $this->sortFiles($files, $sortBy, $sortOrder);
$this->success($response, [
'path' => realpath($path),
'parent' => dirname($path),
'files' => $files,
]);
} catch (\Exception $e) {
$this->error($response, 'Failed to list directory: ' . $e->getMessage());
}
}
/**
* Read file contents
*/
public function read(Request $request, Response $response, array $params): void
{
$path = $request->post('path');
if (!$path) {
$this->error($response, 'Path is required');
return;
}
if (!$this->isPathAllowed($path)) {
$this->error($response, 'Access denied', 403);
return;
}
if (!is_file($path)) {
$this->error($response, 'File not found');
return;
}
$maxSize = $this->config['limits']['max_file_size'] ?? 50 * 1024 * 1024;
$size = filesize($path);
if ($size > $maxSize) {
$this->error($response, 'File is too large to read');
return;
}
$content = file_get_contents($path);
$isBinary = $this->isBinary($content);
$this->success($response, [
'path' => $path,
'name' => basename($path),
'size' => $size,
'content' => $isBinary ? base64_encode($content) : $content,
'binary' => $isBinary,
'mime' => mime_content_type($path) ?: 'application/octet-stream',
'modified' => filemtime($path),
]);
}
/**
* Write file contents
*/
public function write(Request $request, Response $response, array $params): void
{
if (!$this->validateCsrf($request, $response)) {
return;
}
$path = $request->post('path');
$content = $request->post('content', '');
if (!$path) {
$this->error($response, 'Path is required');
return;
}
if (!$this->isPathAllowed($path)) {
$this->error($response, 'Access denied', 403);
return;
}
try {
$written = file_put_contents($path, $content);
if ($written === false) {
throw new \Exception('Failed to write file');
}
$this->success($response, [
'path' => $path,
'size' => $written,
], 'File saved successfully');
} catch (\Exception $e) {
$this->error($response, 'Failed to save file: ' . $e->getMessage());
}
}
/**
* Delete file or directory
*/
public function delete(Request $request, Response $response, array $params): void
{
if (!$this->validateCsrf($request, $response)) {
return;
}
$path = $request->post('path');
if (!$path) {
$this->error($response, 'Path is required');
return;
}
if (!$this->isPathAllowed($path)) {
$this->error($response, 'Access denied', 403);
return;
}
if (!file_exists($path)) {
$this->error($response, 'File not found');
return;
}
try {
if (is_dir($path)) {
$this->deleteDirectory($path);
} else {
unlink($path);
}
$this->success($response, null, 'Deleted successfully');
} catch (\Exception $e) {
$this->error($response, 'Failed to delete: ' . $e->getMessage());
}
}
/**
* Create file
*/
public function create(Request $request, Response $response, array $params): void
{
if (!$this->validateCsrf($request, $response)) {
return;
}
$path = $request->post('path');
$content = $request->post('content', '');
if (!$path) {
$this->error($response, 'Path is required');
return;
}
if (!$this->isPathAllowed($path)) {
$this->error($response, 'Access denied', 403);
return;
}
if (file_exists($path)) {
$this->error($response, 'File already exists');
return;
}
try {
file_put_contents($path, $content);
$this->success($response, ['path' => $path], 'File created successfully');
} catch (\Exception $e) {
$this->error($response, 'Failed to create file: ' . $e->getMessage());
}
}
/**
* Create directory
*/
public function mkdir(Request $request, Response $response, array $params): void
{
if (!$this->validateCsrf($request, $response)) {
return;
}
$path = $request->post('path');
if (!$path) {
$this->error($response, 'Path is required');
return;
}
if (!$this->isPathAllowed($path)) {
$this->error($response, 'Access denied', 403);
return;
}
if (file_exists($path)) {
$this->error($response, 'Directory already exists');
return;
}
try {
mkdir($path, 0755, true);
$this->success($response, ['path' => $path], 'Directory created successfully');
} catch (\Exception $e) {
$this->error($response, 'Failed to create directory: ' . $e->getMessage());
}
}
/**
* Rename file or directory
*/
public function rename(Request $request, Response $response, array $params): void
{
if (!$this->validateCsrf($request, $response)) {
return;
}
$oldPath = $request->post('old_path');
$newPath = $request->post('new_path');
if (!$oldPath || !$newPath) {
$this->error($response, 'Both paths are required');
return;
}
if (!$this->isPathAllowed($oldPath) || !$this->isPathAllowed($newPath)) {
$this->error($response, 'Access denied', 403);
return;
}
try {
rename($oldPath, $newPath);
$this->success($response, ['path' => $newPath], 'Renamed successfully');
} catch (\Exception $e) {
$this->error($response, 'Failed to rename: ' . $e->getMessage());
}
}
/**
* Copy file or directory
*/
public function copy(Request $request, Response $response, array $params): void
{
if (!$this->validateCsrf($request, $response)) {
return;
}
$source = $request->post('source');
$destination = $request->post('destination');
if (!$source || !$destination) {
$this->error($response, 'Both paths are required');
return;
}
if (!$this->isPathAllowed($source) || !$this->isPathAllowed($destination)) {
$this->error($response, 'Access denied', 403);
return;
}
try {
if (is_dir($source)) {
$this->copyDirectory($source, $destination);
} else {
copy($source, $destination);
}
$this->success($response, ['path' => $destination], 'Copied successfully');
} catch (\Exception $e) {
$this->error($response, 'Failed to copy: ' . $e->getMessage());
}
}
/**
* Move file or directory
*/
public function move(Request $request, Response $response, array $params): void
{
if (!$this->validateCsrf($request, $response)) {
return;
}
$source = $request->post('source');
$destination = $request->post('destination');
if (!$source || !$destination) {
$this->error($response, 'Both paths are required');
return;
}
if (!$this->isPathAllowed($source) || !$this->isPathAllowed($destination)) {
$this->error($response, 'Access denied', 403);
return;
}
try {
rename($source, $destination);
$this->success($response, ['path' => $destination], 'Moved successfully');
} catch (\Exception $e) {
$this->error($response, 'Failed to move: ' . $e->getMessage());
}
}
/**
* Change file permissions
*/
public function chmod(Request $request, Response $response, array $params): void
{
if (!$this->validateCsrf($request, $response)) {
return;
}
$path = $request->post('path');
$mode = $request->post('mode');
if (!$path || !$mode) {
$this->error($response, 'Path and mode are required');
return;
}
if (!$this->isPathAllowed($path)) {
$this->error($response, 'Access denied', 403);
return;
}
try {
$permissions = octdec($mode);
chmod($path, $permissions);
$this->success($response, null, 'Permissions changed successfully');
} catch (\Exception $e) {
$this->error($response, 'Failed to change permissions: ' . $e->getMessage());
}
}
/**
* Upload file
*/
public function upload(Request $request, Response $response, array $params): void
{
if (!$this->validateCsrf($request, $response)) {
return;
}
$destination = $request->post('path', getcwd());
$file = $request->file('file');
if (!$file) {
$this->error($response, 'No file uploaded');
return;
}
if (!$this->isPathAllowed($destination)) {
$this->error($response, 'Access denied', 403);
return;
}
$maxSize = $this->config['limits']['max_upload_size'] ?? 100 * 1024 * 1024;
if ($file['size'] > $maxSize) {
$this->error($response, 'File is too large');
return;
}
try {
$targetPath = rtrim($destination, '/') . '/' . basename($file['name']);
move_uploaded_file($file['tmp_name'], $targetPath);
$this->success($response, [
'path' => $targetPath,
'size' => $file['size'],
], 'File uploaded successfully');
} catch (\Exception $e) {
$this->error($response, 'Failed to upload: ' . $e->getMessage());
}
}
/**
* Download file
*/
public function download(Request $request, Response $response, array $params): void
{
$path = $request->get('path');
if (!$path) {
$this->error($response, 'Path is required');
return;
}
if (!$this->isPathAllowed($path)) {
$this->error($response, 'Access denied', 403);
return;
}
if (!is_file($path)) {
$this->error($response, 'File not found', 404);
return;
}
$response->download($path);
}
/**
* Search files
*/
public function search(Request $request, Response $response, array $params): void
{
$path = $request->post('path', getcwd());
$query = $request->post('query', '');
if (!$this->isPathAllowed($path)) {
$this->error($response, 'Access denied', 403);
return;
}
try {
$results = $this->searchFiles($path, $query);
$this->success($response, ['results' => $results]);
} catch (\Exception $e) {
$this->error($response, 'Search failed: ' . $e->getMessage());
}
}
/**
* Scan directory
*/
private function scanDirectory(string $path, bool $showHidden = false): array
{
$files = [];
$items = scandir($path);
if ($items === false) {
throw new \Exception('Cannot read directory');
}
foreach ($items as $item) {
if ($item === '.') continue;
if (!$showHidden && $item[0] === '.' && $item !== '..') continue;
$fullPath = rtrim($path, '/') . '/' . $item;
$isDir = is_dir($fullPath);
$files[] = [
'name' => $item,
'path' => $fullPath,
'is_directory' => $isDir,
'size' => $isDir ? 0 : (filesize($fullPath) ?: 0),
'modified' => filemtime($fullPath) ?: 0,
'permissions' => substr(sprintf('%o', fileperms($fullPath)), -4),
'readable' => is_readable($fullPath),
'writable' => is_writable($fullPath),
'owner' => function_exists('posix_getpwuid') ? (posix_getpwuid(fileowner($fullPath))['name'] ?? 'unknown') : 'unknown',
];
}
return $files;
}
/**
* Sort files
*/
private function sortFiles(array $files, string $sortBy, string $sortOrder): array
{
usort($files, function ($a, $b) use ($sortBy, $sortOrder) {
// Directories first
if ($a['is_directory'] !== $b['is_directory']) {
return $a['is_directory'] ? -1 : 1;
}
$result = match ($sortBy) {
'size' => $a['size'] <=> $b['size'],
'modified' => $a['modified'] <=> $b['modified'],
default => strcasecmp($a['name'], $b['name']),
};
return $sortOrder === 'desc' ? -$result : $result;
});
return $files;
}
/**
* Check if content is binary
*/
private function isBinary(string $content): bool
{
return preg_match('~[^\x20-\x7E\t\r\n]~', substr($content, 0, 8192)) === 1;
}
/**
* Delete directory recursively
*/
private function deleteDirectory(string $path): void
{
$items = new \RecursiveIteratorIterator(
new \RecursiveDirectoryIterator($path, \RecursiveDirectoryIterator::SKIP_DOTS),
\RecursiveIteratorIterator::CHILD_FIRST
);
foreach ($items as $item) {
if ($item->isDir()) {
rmdir($item->getRealPath());
} else {
unlink($item->getRealPath());
}
}
rmdir($path);
}
/**
* Copy directory recursively
*/
private function copyDirectory(string $source, string $destination): void
{
if (!is_dir($destination)) {
mkdir($destination, 0755, true);
}
$items = new \RecursiveIteratorIterator(
new \RecursiveDirectoryIterator($source, \RecursiveDirectoryIterator::SKIP_DOTS),
\RecursiveIteratorIterator::SELF_FIRST
);
foreach ($items as $item) {
$target = $destination . '/' . $items->getSubPathname();
if ($item->isDir()) {
if (!is_dir($target)) {
mkdir($target, 0755, true);
}
} else {
copy($item->getRealPath(), $target);
}
}
}
/**
* Search files recursively
*/
private function searchFiles(string $path, string $query, int $maxResults = 100): array
{
$results = [];
$pattern = '/' . preg_quote($query, '/') . '/i';
$iterator = new \RecursiveIteratorIterator(
new \RecursiveDirectoryIterator($path, \RecursiveDirectoryIterator::SKIP_DOTS),
\RecursiveIteratorIterator::SELF_FIRST
);
foreach ($iterator as $item) {
if (count($results) >= $maxResults) {
break;
}
if (preg_match($pattern, $item->getFilename())) {
$results[] = [
'name' => $item->getFilename(),
'path' => $item->getRealPath(),
'is_directory' => $item->isDir(),
'size' => $item->isDir() ? 0 : $item->getSize(),
];
}
}
return $results;
}
}

Ver fichero

@@ -0,0 +1,362 @@
<?php
/**
* AleShell2 Network Controller
*
* Handles network tools module.
*
* @package AleShell2\Modules\Network
*/
declare(strict_types=1);
namespace AleShell2\Modules\Network;
use AleShell2\Modules\BaseController;
use AleShell2\Core\Request;
use AleShell2\Core\Response;
class NetworkController extends BaseController
{
/**
* Show network tools
*/
public function index(Request $request, Response $response, array $params): void
{
$this->render($response, 'modules.network', [
'currentModule' => 'network',
'pageTitle' => 'Network Tools - AleShell2',
]);
}
/**
* Get network connections
*/
public function connections(Request $request, Response $response, array $params): void
{
try {
$connections = $this->getNetworkConnections();
$this->success($response, ['connections' => $connections]);
} catch (\Exception $e) {
$this->error($response, 'Failed to get connections: ' . $e->getMessage());
}
}
/**
* Ping a host
*/
public function ping(Request $request, Response $response, array $params): void
{
if (!$this->validateCsrf($request, $response)) {
return;
}
$host = $request->post('host');
$count = min((int)$request->post('count', 4), 10); // Max 10 pings
if (!$host) {
$this->error($response, 'Host is required');
return;
}
// Validate host
if (!$this->isValidHost($host)) {
$this->error($response, 'Invalid host');
return;
}
try {
$output = $this->executePing($host, $count);
$this->success($response, ['output' => $output]);
} catch (\Exception $e) {
$this->error($response, 'Ping failed: ' . $e->getMessage());
}
}
/**
* Traceroute to host
*/
public function traceroute(Request $request, Response $response, array $params): void
{
if (!$this->validateCsrf($request, $response)) {
return;
}
$host = $request->post('host');
$maxHops = min((int)$request->post('max_hops', 30), 30);
if (!$host) {
$this->error($response, 'Host is required');
return;
}
if (!$this->isValidHost($host)) {
$this->error($response, 'Invalid host');
return;
}
try {
$output = $this->executeTraceroute($host, $maxHops);
$this->success($response, ['output' => $output]);
} catch (\Exception $e) {
$this->error($response, 'Traceroute failed: ' . $e->getMessage());
}
}
/**
* Port scan
*/
public function portscan(Request $request, Response $response, array $params): void
{
if (!$this->validateCsrf($request, $response)) {
return;
}
$host = $request->post('host');
$ports = $request->post('ports', '1-1000');
if (!$host) {
$this->error($response, 'Host is required');
return;
}
if (!$this->isValidHost($host)) {
$this->error($response, 'Invalid host');
return;
}
try {
$results = $this->executePortScan($host, $ports);
$this->success($response, $results);
} catch (\Exception $e) {
$this->error($response, 'Port scan failed: ' . $e->getMessage());
}
}
/**
* Get network connections
*/
private function getNetworkConnections(): array
{
$connections = [];
// Try netstat
$output = shell_exec('netstat -tuln 2>/dev/null');
if (!$output) {
// Try ss command as fallback
$output = shell_exec('ss -tuln 2>/dev/null');
}
if (!$output) {
return $connections;
}
$lines = explode("\n", trim($output));
array_shift($lines); // Remove header
foreach ($lines as $line) {
if (empty(trim($line))) continue;
// Skip additional header line in netstat
if (strpos($line, 'Proto') !== false || strpos($line, 'Netid') !== false) {
continue;
}
$parts = preg_split('/\s+/', trim($line));
if (count($parts) >= 4) {
$proto = $parts[0];
$local = $parts[3] ?? '';
$foreign = $parts[4] ?? '*:*';
$state = $parts[5] ?? 'LISTEN';
// Parse local address
$localParts = explode(':', $local);
$localPort = end($localParts);
$localAddr = implode(':', array_slice($localParts, 0, -1)) ?: '*';
$connections[] = [
'protocol' => strtoupper($proto),
'local_address' => $localAddr,
'local_port' => $localPort,
'foreign_address' => $foreign,
'state' => $state,
];
}
}
return $connections;
}
/**
* Execute ping command
*/
private function executePing(string $host, int $count): string
{
$host = escapeshellarg($host);
$count = (int)$count;
// Detect OS and use appropriate ping syntax
if (PHP_OS_FAMILY === 'Windows') {
$cmd = "ping -n {$count} {$host}";
} else {
$cmd = "ping -c {$count} -W 2 {$host}";
}
$output = shell_exec($cmd . ' 2>&1');
return $output ?: 'No output from ping command';
}
/**
* Execute traceroute command
*/
private function executeTraceroute(string $host, int $maxHops): string
{
$host = escapeshellarg($host);
$maxHops = (int)$maxHops;
// Try traceroute first, then tracepath
if (PHP_OS_FAMILY === 'Windows') {
$cmd = "tracert -h {$maxHops} {$host}";
} else {
// Check if traceroute exists
$traceroute = shell_exec('which traceroute 2>/dev/null');
if ($traceroute) {
$cmd = "traceroute -m {$maxHops} -w 2 {$host}";
} else {
// Fall back to tracepath
$cmd = "tracepath {$host}";
}
}
$output = shell_exec($cmd . ' 2>&1');
return $output ?: 'No output from traceroute command';
}
/**
* Execute port scan
*/
private function executePortScan(string $host, string $portsArg): array
{
$ports = $this->parsePorts($portsArg);
if (count($ports) > 100) {
$ports = array_slice($ports, 0, 100);
}
$openPorts = [];
$closedCount = 0;
$timeout = 1; // 1 second timeout per port
foreach ($ports as $port) {
$port = (int)$port;
if ($port < 1 || $port > 65535) {
continue;
}
$connection = @fsockopen($host, $port, $errno, $errstr, $timeout);
if ($connection) {
fclose($connection);
$openPorts[] = [
'port' => $port,
'service' => $this->getServiceName($port),
'status' => 'open',
];
} else {
$closedCount++;
}
}
return [
'host' => $host,
'open_ports' => $openPorts,
'scanned_count' => count($ports),
'open_count' => count($openPorts),
'closed_count' => $closedCount,
];
}
/**
* Parse port range
*/
private function parsePorts(string $portsArg): array
{
$ports = [];
$parts = explode(',', $portsArg);
foreach ($parts as $part) {
$part = trim($part);
if (strpos($part, '-') !== false) {
// Range
[$start, $end] = explode('-', $part);
$start = (int)$start;
$end = (int)$end;
for ($i = $start; $i <= $end && $i <= 65535; $i++) {
$ports[] = $i;
}
} else {
// Single port
$ports[] = (int)$part;
}
}
return array_unique($ports);
}
/**
* Get service name for common ports
*/
private function getServiceName(int $port): string
{
$services = [
21 => 'FTP',
22 => 'SSH',
23 => 'Telnet',
25 => 'SMTP',
53 => 'DNS',
80 => 'HTTP',
110 => 'POP3',
143 => 'IMAP',
443 => 'HTTPS',
465 => 'SMTPS',
587 => 'SMTP/TLS',
993 => 'IMAPS',
995 => 'POP3S',
3306 => 'MySQL',
3389 => 'RDP',
5432 => 'PostgreSQL',
5900 => 'VNC',
6379 => 'Redis',
8080 => 'HTTP-Alt',
8443 => 'HTTPS-Alt',
27017 => 'MongoDB',
];
return $services[$port] ?? 'Unknown';
}
/**
* Validate host
*/
private function isValidHost(string $host): bool
{
// Check if it's a valid IP
if (filter_var($host, FILTER_VALIDATE_IP)) {
return true;
}
// Check if it's a valid hostname
if (preg_match('/^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/', $host)) {
return true;
}
return false;
}
}

Ver fichero

@@ -0,0 +1,279 @@
<?php
/**
* AleShell2 Processes Controller
*
* Handles process manager module.
*
* @package AleShell2\Modules\Processes
*/
declare(strict_types=1);
namespace AleShell2\Modules\Processes;
use AleShell2\Modules\BaseController;
use AleShell2\Core\Request;
use AleShell2\Core\Response;
class ProcessesController extends BaseController
{
/**
* Show process manager
*/
public function index(Request $request, Response $response, array $params): void
{
$this->render($response, 'modules.processes', [
'currentModule' => 'processes',
'pageTitle' => 'Process Manager - AleShell2',
]);
}
/**
* List processes
*/
public function list(Request $request, Response $response, array $params): void
{
$search = $request->post('search', '');
$sortBy = $request->post('sort', 'cpu');
$sortOrder = $request->post('order', 'desc');
try {
$processes = $this->getProcessList($search, $sortBy, $sortOrder);
$this->success($response, ['processes' => $processes]);
} catch (\Exception $e) {
$this->error($response, 'Failed to list processes: ' . $e->getMessage());
}
}
/**
* Kill process
*/
public function kill(Request $request, Response $response, array $params): void
{
if (!$this->validateCsrf($request, $response)) {
return;
}
$pids = $request->post('pids');
$signal = (int)$request->post('signal', 15); // SIGTERM
if (empty($pids)) {
$this->error($response, 'PID is required');
return;
}
if (!is_array($pids)) {
$pids = [$pids];
}
$killed = [];
$failed = [];
foreach ($pids as $pid) {
$pid = (int)$pid;
if ($pid <= 0) {
$failed[] = ['pid' => $pid, 'error' => 'Invalid PID'];
continue;
}
// Don't allow killing init or critical system processes
if ($pid === 1) {
$failed[] = ['pid' => $pid, 'error' => 'Cannot kill init process'];
continue;
}
if (function_exists('posix_kill')) {
if (posix_kill($pid, $signal)) {
$killed[] = $pid;
} else {
$failed[] = ['pid' => $pid, 'error' => posix_strerror(posix_get_last_error())];
}
} else {
// Fallback to shell command
$result = shell_exec("kill -{$signal} {$pid} 2>&1");
if ($result === null || $result === '') {
$killed[] = $pid;
} else {
$failed[] = ['pid' => $pid, 'error' => trim($result)];
}
}
}
$this->success($response, [
'killed' => $killed,
'failed' => $failed,
], count($killed) . ' process(es) killed');
}
/**
* Get process list
*/
private function getProcessList(string $search = '', string $sortBy = 'cpu', string $sortOrder = 'desc'): array
{
$processes = [];
$maxProcesses = $this->config['limits']['max_processes'] ?? 1000;
// Try using /proc filesystem (Linux)
if (is_dir('/proc')) {
$processes = $this->getProcessesFromProc();
} else {
// Fallback to ps command
$processes = $this->getProcessesFromPs();
}
// Filter by search
if (!empty($search)) {
$search = strtolower($search);
$processes = array_filter($processes, function ($p) use ($search) {
return stripos($p['name'], $search) !== false
|| stripos($p['command'], $search) !== false
|| (string)$p['pid'] === $search;
});
}
// Sort
usort($processes, function ($a, $b) use ($sortBy, $sortOrder) {
$result = match ($sortBy) {
'pid' => $a['pid'] <=> $b['pid'],
'cpu' => $a['cpu'] <=> $b['cpu'],
'memory' => $a['memory'] <=> $b['memory'],
'name' => strcasecmp($a['name'], $b['name']),
default => $a['cpu'] <=> $b['cpu'],
};
return $sortOrder === 'desc' ? -$result : $result;
});
return array_slice(array_values($processes), 0, $maxProcesses);
}
/**
* Get processes from /proc filesystem
*/
private function getProcessesFromProc(): array
{
$processes = [];
$dirs = glob('/proc/[0-9]*', GLOB_ONLYDIR);
foreach ($dirs as $dir) {
$pid = (int)basename($dir);
// Read process info
$statusFile = "{$dir}/status";
$statFile = "{$dir}/stat";
$cmdlineFile = "{$dir}/cmdline";
if (!file_exists($statusFile)) {
continue;
}
$status = @file_get_contents($statusFile);
$stat = @file_get_contents($statFile);
$cmdline = @file_get_contents($cmdlineFile);
if ($status === false) {
continue;
}
// Parse status
$name = '';
$uid = 0;
$vmRss = 0;
if (preg_match('/^Name:\s*(.+)$/m', $status, $m)) {
$name = trim($m[1]);
}
if (preg_match('/^Uid:\s*(\d+)/m', $status, $m)) {
$uid = (int)$m[1];
}
if (preg_match('/^VmRSS:\s*(\d+)/m', $status, $m)) {
$vmRss = (int)$m[1] * 1024; // Convert to bytes
}
// Parse stat for CPU info
$cpu = 0;
if ($stat) {
$statParts = explode(' ', $stat);
if (count($statParts) > 13) {
// utime + stime
$utime = (int)$statParts[13];
$stime = (int)$statParts[14];
$totalTime = $utime + $stime;
// This is a simplified CPU calculation
$cpu = round($totalTime / 100, 1);
}
}
// Get command line
$command = $cmdline ? str_replace("\0", ' ', trim($cmdline)) : $name;
// Get user
$user = 'unknown';
if (function_exists('posix_getpwuid')) {
$pwinfo = posix_getpwuid($uid);
if ($pwinfo) {
$user = $pwinfo['name'];
}
}
// Calculate memory percentage
$totalMem = $this->getMemoryInfo()['total'] ?? 1;
$memPercent = $totalMem > 0 ? round(($vmRss / $totalMem) * 100, 1) : 0;
$processes[] = [
'pid' => $pid,
'name' => $name,
'command' => strlen($command) > 100 ? substr($command, 0, 100) . '...' : $command,
'user' => $user,
'cpu' => $cpu,
'memory' => $memPercent,
'memory_bytes' => $vmRss,
'status' => 'running',
];
}
return $processes;
}
/**
* Get processes from ps command
*/
private function getProcessesFromPs(): array
{
$processes = [];
// Use ps command with portable options
$output = shell_exec('ps aux 2>/dev/null');
if (!$output) {
return $processes;
}
$lines = explode("\n", trim($output));
array_shift($lines); // Remove header
foreach ($lines as $line) {
$parts = preg_split('/\s+/', trim($line), 11);
if (count($parts) < 11) {
continue;
}
$processes[] = [
'pid' => (int)$parts[1],
'name' => basename($parts[10]),
'command' => strlen($parts[10]) > 100 ? substr($parts[10], 0, 100) . '...' : $parts[10],
'user' => $parts[0],
'cpu' => (float)$parts[2],
'memory' => (float)$parts[3],
'memory_bytes' => 0,
'status' => 'running',
];
}
return $processes;
}
}

Ver fichero

@@ -0,0 +1,355 @@
<?php
/**
* AleShell2 System Controller
*
* Handles system information module.
*
* @package AleShell2\Modules\System
*/
declare(strict_types=1);
namespace AleShell2\Modules\System;
use AleShell2\Modules\BaseController;
use AleShell2\Core\Request;
use AleShell2\Core\Response;
class SystemController extends BaseController
{
/**
* Show system info
*/
public function index(Request $request, Response $response, array $params): void
{
$this->render($response, 'modules.system', [
'currentModule' => 'system',
'pageTitle' => 'System Info - AleShell2',
'systemInfo' => $this->getSystemInfo(),
]);
}
/**
* Get system information via API
*/
public function info(Request $request, Response $response, array $params): void
{
$this->success($response, $this->getSystemInfo());
}
/**
* Get PHP information
*/
public function phpinfo(Request $request, Response $response, array $params): void
{
ob_start();
phpinfo();
$info = ob_get_clean();
$response->html($info);
}
/**
* Get environment variables
*/
public function environment(Request $request, Response $response, array $params): void
{
$env = [];
foreach ($_ENV as $key => $value) {
$env[$key] = $value;
}
foreach ($_SERVER as $key => $value) {
if (is_string($value)) {
$env[$key] = $value;
}
}
ksort($env);
$this->success($response, ['environment' => $env]);
}
/**
* Get PHP extensions
*/
public function extensions(Request $request, Response $response, array $params): void
{
$extensions = get_loaded_extensions();
sort($extensions);
$detailed = [];
foreach ($extensions as $ext) {
$detailed[$ext] = phpversion($ext) ?: 'unknown';
}
$this->success($response, ['extensions' => $detailed]);
}
/**
* Collect all system information
*/
private function getSystemInfo(): array
{
return [
'server' => $this->getServerInfo(),
'php' => $this->getPhpInfo(),
'hardware' => $this->getHardwareInfo(),
'disk' => $this->getDiskInfo(),
'network' => $this->getBasicNetworkInfo(),
];
}
/**
* Get server information
*/
private function getServerInfo(): array
{
return [
'hostname' => gethostname() ?: 'unknown',
'os' => PHP_OS_FAMILY,
'os_detail' => php_uname(),
'server_software' => $_SERVER['SERVER_SOFTWARE'] ?? 'unknown',
'server_name' => $_SERVER['SERVER_NAME'] ?? 'unknown',
'server_addr' => $_SERVER['SERVER_ADDR'] ?? 'unknown',
'server_port' => $_SERVER['SERVER_PORT'] ?? 'unknown',
'document_root' => $_SERVER['DOCUMENT_ROOT'] ?? 'unknown',
'current_user' => get_current_user(),
'current_uid' => getmyuid(),
'current_gid' => getmygid(),
'process_id' => getmypid(),
'uptime' => $this->getUptime(),
'load_average' => $this->getLoadAverage(),
];
}
/**
* Get PHP information
*/
private function getPhpInfo(): array
{
return [
'version' => PHP_VERSION,
'version_id' => PHP_VERSION_ID,
'sapi' => PHP_SAPI,
'ini_path' => php_ini_loaded_file() ?: 'unknown',
'extension_dir' => ini_get('extension_dir'),
'include_path' => get_include_path(),
'memory_limit' => ini_get('memory_limit'),
'max_execution_time' => ini_get('max_execution_time'),
'max_input_time' => ini_get('max_input_time'),
'post_max_size' => ini_get('post_max_size'),
'upload_max_filesize' => ini_get('upload_max_filesize'),
'max_file_uploads' => ini_get('max_file_uploads'),
'display_errors' => ini_get('display_errors'),
'error_reporting' => error_reporting(),
'date_timezone' => date_default_timezone_get(),
'open_basedir' => ini_get('open_basedir') ?: 'none',
'disabled_functions' => ini_get('disable_functions') ?: 'none',
'disabled_classes' => ini_get('disable_classes') ?: 'none',
'safe_mode' => ini_get('safe_mode') ? 'on' : 'off',
'allow_url_fopen' => ini_get('allow_url_fopen') ? 'on' : 'off',
'allow_url_include' => ini_get('allow_url_include') ? 'on' : 'off',
'zend_version' => zend_version(),
];
}
/**
* Get hardware information
*/
private function getHardwareInfo(): array
{
$info = [
'cpu' => 'unknown',
'cpu_cores' => 'unknown',
'memory_total' => 'unknown',
'memory_free' => 'unknown',
'memory_used' => 'unknown',
];
// CPU info
if (is_readable('/proc/cpuinfo')) {
$cpuinfo = file_get_contents('/proc/cpuinfo');
if (preg_match('/model name\s*:\s*(.+)/i', $cpuinfo, $matches)) {
$info['cpu'] = trim($matches[1]);
}
$info['cpu_cores'] = substr_count($cpuinfo, 'processor');
} elseif (PHP_OS_FAMILY === 'Darwin') {
$info['cpu'] = trim(shell_exec('sysctl -n machdep.cpu.brand_string 2>/dev/null') ?: 'unknown');
$info['cpu_cores'] = (int)trim(shell_exec('sysctl -n hw.ncpu 2>/dev/null') ?: '0');
}
// Memory info
if (is_readable('/proc/meminfo')) {
$meminfo = file_get_contents('/proc/meminfo');
if (preg_match('/MemTotal:\s*(\d+)\s*kB/i', $meminfo, $matches)) {
$info['memory_total'] = $this->formatBytes((int)$matches[1] * 1024);
}
if (preg_match('/MemFree:\s*(\d+)\s*kB/i', $meminfo, $matches)) {
$memFree = (int)$matches[1] * 1024;
$info['memory_free'] = $this->formatBytes($memFree);
}
if (preg_match('/MemAvailable:\s*(\d+)\s*kB/i', $meminfo, $matches)) {
$info['memory_available'] = $this->formatBytes((int)$matches[1] * 1024);
}
} elseif (PHP_OS_FAMILY === 'Darwin') {
$totalMem = (int)trim(shell_exec('sysctl -n hw.memsize 2>/dev/null') ?: '0');
$info['memory_total'] = $this->formatBytes($totalMem);
}
return $info;
}
/**
* Get disk information
*/
private function getDiskInfo(): array
{
$disks = [];
// Current disk
$path = $_SERVER['DOCUMENT_ROOT'] ?? getcwd();
$totalSpace = @disk_total_space($path);
$freeSpace = @disk_free_space($path);
$disks[] = [
'mount' => $path,
'total' => $totalSpace ? $this->formatBytes($totalSpace) : 'unknown',
'free' => $freeSpace ? $this->formatBytes($freeSpace) : 'unknown',
'used' => ($totalSpace && $freeSpace) ? $this->formatBytes($totalSpace - $freeSpace) : 'unknown',
'percent_used' => ($totalSpace && $freeSpace) ? round(($totalSpace - $freeSpace) / $totalSpace * 100, 2) . '%' : 'unknown',
];
// Get all mount points on Linux
if (PHP_OS_FAMILY === 'Linux') {
$df = shell_exec('df -h 2>/dev/null');
if ($df) {
$lines = explode("\n", trim($df));
array_shift($lines); // Remove header
foreach ($lines as $line) {
$parts = preg_split('/\s+/', $line);
if (count($parts) >= 6 && strpos($parts[0], '/dev/') === 0) {
$disks[] = [
'device' => $parts[0],
'total' => $parts[1],
'used' => $parts[2],
'free' => $parts[3],
'percent_used' => $parts[4],
'mount' => $parts[5],
];
}
}
}
}
return $disks;
}
/**
* Get basic network information
*/
private function getBasicNetworkInfo(): array
{
$info = [
'hostname' => gethostname(),
'ip_addresses' => [],
];
// Get IP addresses
$hostname = gethostname();
if ($hostname) {
$ips = gethostbynamel($hostname);
if ($ips) {
$info['ip_addresses'] = $ips;
}
}
// Add server IP
if (!empty($_SERVER['SERVER_ADDR'])) {
$info['server_ip'] = $_SERVER['SERVER_ADDR'];
}
// Get interfaces on Linux
if (PHP_OS_FAMILY === 'Linux') {
$interfaces = shell_exec('ip -o addr show 2>/dev/null | grep -v "127.0.0.1"');
if ($interfaces) {
$info['interfaces'] = trim($interfaces);
}
}
return $info;
}
/**
* Get system uptime
*/
private function getUptime(): string
{
if (is_readable('/proc/uptime')) {
$uptime = (float)explode(' ', file_get_contents('/proc/uptime'))[0];
return $this->formatUptime($uptime);
}
if (PHP_OS_FAMILY === 'Darwin') {
$boottime = shell_exec('sysctl -n kern.boottime 2>/dev/null');
if (preg_match('/sec = (\d+)/', $boottime, $matches)) {
$uptime = time() - (int)$matches[1];
return $this->formatUptime($uptime);
}
}
return 'unknown';
}
/**
* Format uptime seconds to human readable
*/
private function formatUptime(float $seconds): string
{
$days = floor($seconds / 86400);
$hours = floor(($seconds % 86400) / 3600);
$minutes = floor(($seconds % 3600) / 60);
$parts = [];
if ($days > 0) $parts[] = "{$days}d";
if ($hours > 0) $parts[] = "{$hours}h";
if ($minutes > 0) $parts[] = "{$minutes}m";
return implode(' ', $parts) ?: '< 1m';
}
/**
* Get load average
*/
private function getLoadAverage(): array
{
if (function_exists('sys_getloadavg')) {
$load = sys_getloadavg();
if ($load !== false) {
return [
'1min' => round($load[0], 2),
'5min' => round($load[1], 2),
'15min' => round($load[2], 2),
];
}
}
return ['1min' => 'unknown', '5min' => 'unknown', '15min' => 'unknown'];
}
/**
* Format bytes to human readable
*/
private function formatBytes(int $bytes, int $precision = 2): string
{
$units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB'];
for ($i = 0; $bytes > 1024 && $i < count($units) - 1; $i++) {
$bytes /= 1024;
}
return round($bytes, $precision) . ' ' . $units[$i];
}
}

Ver fichero

@@ -0,0 +1,452 @@
<?php
/**
* AleShell2 Terminal Controller
*
* Handles terminal module.
*
* @package AleShell2\Modules\Terminal
*/
declare(strict_types=1);
namespace AleShell2\Modules\Terminal;
use AleShell2\Modules\BaseController;
use AleShell2\Core\Request;
use AleShell2\Core\Response;
use AleShell2\Core\Application;
class TerminalController extends BaseController
{
/**
* Show terminal
*/
public function index(Request $request, Response $response, array $params): void
{
$this->render($response, 'modules.terminal', [
'currentModule' => 'terminal',
'pageTitle' => 'Terminal - AleShell2',
'currentPath' => getcwd(),
]);
}
/**
* Execute command
*/
public function execute(Request $request, Response $response, array $params): void
{
if (!$this->validateCsrf($request, $response)) {
return;
}
$command = trim($request->post('command', ''));
$workingDir = $request->post('cwd', getcwd());
if (empty($command)) {
$this->error($response, 'Command is required');
return;
}
// Add to history
$this->addToHistory($command);
// Check if it's a built-in command
if ($this->isBuiltinCommand($command)) {
$result = $this->executeBuiltin($command, $workingDir);
} else {
// Check if command is allowed
if (!$this->isCommandAllowed($command)) {
$this->error($response, 'This command is not allowed');
return;
}
$result = $this->executeSystemCommand($command, $workingDir);
}
$this->success($response, $result);
}
/**
* Get command history
*/
public function history(Request $request, Response $response, array $params): void
{
$app = Application::getInstance();
$session = $app->getSession();
$history = $session->get('terminal_history', []);
$maxHistory = $this->config['limits']['max_history'] ?? 100;
$this->success($response, [
'history' => array_slice($history, -$maxHistory),
]);
}
/**
* Clear command history
*/
public function clear(Request $request, Response $response, array $params): void
{
$app = Application::getInstance();
$session = $app->getSession();
$session->remove('terminal_history');
$this->success($response, null, 'History cleared');
}
/**
* Check if command is a built-in
*/
private function isBuiltinCommand(string $command): bool
{
$builtins = ['cd', 'pwd', 'clear', 'history', 'help', 'exit', 'whoami', 'id'];
$parts = explode(' ', trim($command));
return in_array($parts[0], $builtins, true);
}
/**
* Execute built-in command
*/
private function executeBuiltin(string $command, string $workingDir): array
{
$parts = explode(' ', trim($command));
$cmd = $parts[0];
return match ($cmd) {
'cd' => $this->cmdCd($parts, $workingDir),
'pwd' => $this->cmdPwd($workingDir),
'clear' => $this->cmdClear(),
'history' => $this->cmdHistory(),
'help' => $this->cmdHelp(),
'exit' => $this->cmdExit(),
'whoami' => $this->cmdWhoami(),
'id' => $this->cmdId(),
default => [
'output' => "Unknown built-in command: {$cmd}",
'exit_code' => 1,
'cwd' => $workingDir,
],
};
}
/**
* CD command
*/
private function cmdCd(array $parts, string $currentDir): array
{
$app = Application::getInstance();
$session = $app->getSession();
$target = $parts[1] ?? ($_SERVER['HOME'] ?? '/');
// Handle special cases
if ($target === '-') {
$target = $session->get('terminal_prev_dir', $currentDir);
} elseif ($target === '~') {
$target = $_SERVER['HOME'] ?? '/root';
}
// Resolve relative paths
if ($target[0] !== '/') {
$target = rtrim($currentDir, '/') . '/' . $target;
}
$realTarget = realpath($target);
if ($realTarget === false || !is_dir($realTarget)) {
return [
'output' => "cd: {$target}: No such directory",
'exit_code' => 1,
'cwd' => $currentDir,
];
}
if (!$this->isPathAllowed($realTarget)) {
return [
'output' => "cd: {$target}: Permission denied",
'exit_code' => 1,
'cwd' => $currentDir,
];
}
// Store previous directory
$session->set('terminal_prev_dir', $currentDir);
return [
'output' => '',
'exit_code' => 0,
'cwd' => $realTarget,
];
}
/**
* PWD command
*/
private function cmdPwd(string $currentDir): array
{
return [
'output' => $currentDir,
'exit_code' => 0,
'cwd' => $currentDir,
];
}
/**
* Clear command
*/
private function cmdClear(): array
{
return [
'output' => '',
'exit_code' => 0,
'cwd' => getcwd(),
'clear' => true,
];
}
/**
* History command
*/
private function cmdHistory(): array
{
$app = Application::getInstance();
$session = $app->getSession();
$history = $session->get('terminal_history', []);
$output = '';
foreach (array_slice($history, -50) as $i => $cmd) {
$output .= sprintf("%4d %s\n", $i + 1, $cmd);
}
return [
'output' => $output ?: 'No history',
'exit_code' => 0,
'cwd' => getcwd(),
];
}
/**
* Help command
*/
private function cmdHelp(): array
{
$help = <<<HELP
AleShell2 Terminal - Available Commands
Built-in Commands:
cd [dir] Change directory
pwd Print working directory
clear Clear terminal screen
history Show command history
help Show this help message
whoami Show current user
id Show user ID info
exit Exit info
System Commands:
All standard Unix/Linux commands are available.
Keyboard Shortcuts:
Up/Down Navigate command history
Ctrl+L Clear screen
Ctrl+C Cancel current input
Tab Auto-complete (when available)
Notes:
- Some commands may be restricted for security
- Long-running commands have a timeout limit
- Binary output will be truncated
HELP;
return [
'output' => $help,
'exit_code' => 0,
'cwd' => getcwd(),
];
}
/**
* Exit command
*/
private function cmdExit(): array
{
return [
'output' => "Use the logout button to exit the shell.",
'exit_code' => 0,
'cwd' => getcwd(),
];
}
/**
* Whoami command
*/
private function cmdWhoami(): array
{
$user = get_current_user() ?: (posix_getpwuid(posix_geteuid())['name'] ?? 'unknown');
return [
'output' => $user,
'exit_code' => 0,
'cwd' => getcwd(),
];
}
/**
* ID command
*/
private function cmdId(): array
{
if (function_exists('posix_getuid')) {
$uid = posix_getuid();
$gid = posix_getgid();
$user = posix_getpwuid($uid)['name'] ?? 'unknown';
$group = posix_getgrgid($gid)['name'] ?? 'unknown';
$output = "uid={$uid}({$user}) gid={$gid}({$group})";
} else {
$output = "id: POSIX functions not available";
}
return [
'output' => $output,
'exit_code' => 0,
'cwd' => getcwd(),
];
}
/**
* Execute system command
*/
private function executeSystemCommand(string $command, string $workingDir): array
{
if (!$this->isPathAllowed($workingDir)) {
return [
'output' => "Permission denied: Cannot execute in this directory",
'exit_code' => 1,
'cwd' => $workingDir,
];
}
$timeout = $this->config['limits']['command_timeout'] ?? 30;
$originalDir = getcwd();
try {
// Change to working directory
if (!chdir($workingDir)) {
throw new \Exception("Cannot change to directory: {$workingDir}");
}
// Set up process
$descriptorSpec = [
0 => ['pipe', 'r'], // stdin
1 => ['pipe', 'w'], // stdout
2 => ['pipe', 'w'], // stderr
];
$env = $_ENV;
$env['PATH'] = '/usr/local/bin:/usr/bin:/bin:/usr/local/sbin:/usr/sbin:/sbin';
$env['HOME'] = $_SERVER['HOME'] ?? '/root';
$env['TERM'] = 'xterm-256color';
$process = proc_open($command, $descriptorSpec, $pipes, $workingDir, $env);
if (!is_resource($process)) {
throw new \Exception('Failed to execute command');
}
// Close stdin
fclose($pipes[0]);
// Set non-blocking mode
stream_set_blocking($pipes[1], false);
stream_set_blocking($pipes[2], false);
// Read output with timeout
$output = '';
$error = '';
$startTime = time();
$maxOutput = 1024 * 1024; // 1MB limit
while (time() - $startTime < $timeout) {
$stdout = stream_get_contents($pipes[1]);
$stderr = stream_get_contents($pipes[2]);
if ($stdout) $output .= $stdout;
if ($stderr) $error .= $stderr;
// Check if process has ended
$status = proc_get_status($process);
if (!$status['running']) {
break;
}
// Prevent memory issues
if (strlen($output) + strlen($error) > $maxOutput) {
$output .= "\n[Output truncated - exceeds limit]";
break;
}
usleep(10000); // 10ms sleep
}
// Read any remaining output
$output .= stream_get_contents($pipes[1]);
$error .= stream_get_contents($pipes[2]);
fclose($pipes[1]);
fclose($pipes[2]);
$exitCode = proc_close($process);
// Combine output
$finalOutput = $output;
if ($error) {
$finalOutput .= ($finalOutput ? "\n" : '') . $error;
}
// Get current directory (might have changed)
$newCwd = getcwd();
return [
'output' => $finalOutput,
'exit_code' => $exitCode,
'cwd' => $newCwd,
];
} catch (\Exception $e) {
return [
'output' => "Error: " . $e->getMessage(),
'exit_code' => 1,
'cwd' => $workingDir,
];
} finally {
chdir($originalDir);
}
}
/**
* Add command to history
*/
private function addToHistory(string $command): void
{
$app = Application::getInstance();
$session = $app->getSession();
$history = $session->get('terminal_history', []);
// Don't add duplicates
if (end($history) !== $command) {
$history[] = $command;
// Limit history size
$maxHistory = $this->config['limits']['max_history'] ?? 100;
if (count($history) > $maxHistory) {
$history = array_slice($history, -$maxHistory);
}
$session->set('terminal_history', $history);
}
}
}

221
src/Security/Auth.php Archivo normal
Ver fichero

@@ -0,0 +1,221 @@
<?php
/**
* AleShell2 Authentication
*
* Handles user authentication.
*
* @package AleShell2\Security
*/
declare(strict_types=1);
namespace AleShell2\Security;
class Auth
{
private array $config;
private Session $session;
public function __construct(array $config, Session $session)
{
$this->config = $config;
$this->session = $session;
}
/**
* Check if user is authenticated
*/
public function isAuthenticated(): bool
{
return $this->session->get('authenticated') === true;
}
/**
* Attempt to authenticate user
*/
public function attempt(string $password): bool
{
// Check rate limiting
if ($this->isRateLimited()) {
return false;
}
// Get stored password hash
$storedHash = $this->config['security']['password'] ?? '';
// Verify password
if (password_verify($password, $storedHash)) {
$this->login();
return true;
}
// Record failed attempt
$this->recordFailedAttempt();
return false;
}
/**
* Log user in
*/
public function login(): void
{
// Regenerate session ID
$this->session->regenerateId();
// Set authentication data
$this->session->set('authenticated', true);
$this->session->set('login_time', time());
$this->session->set('last_activity', time());
$this->session->set('user_ip', $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0');
$this->session->set('csrf_token', $this->generateCsrfToken());
// Clear failed attempts
$this->clearFailedAttempts();
}
/**
* Log user out
*/
public function logout(): void
{
$this->session->destroy();
}
/**
* Check if rate limited
*/
public function isRateLimited(): bool
{
$attempts = $this->session->get('failed_attempts', []);
$maxAttempts = $this->config['security']['max_attempts'] ?? 5;
$lockoutTime = $this->config['security']['lockout_time'] ?? 300;
// Clean old attempts
$attempts = array_filter($attempts, function ($time) use ($lockoutTime) {
return time() - $time < $lockoutTime;
});
$this->session->set('failed_attempts', $attempts);
return count($attempts) >= $maxAttempts;
}
/**
* Get remaining lockout time
*/
public function getRemainingLockoutTime(): int
{
$attempts = $this->session->get('failed_attempts', []);
$lockoutTime = $this->config['security']['lockout_time'] ?? 300;
if (empty($attempts)) {
return 0;
}
$firstAttempt = min($attempts);
$elapsed = time() - $firstAttempt;
return max(0, $lockoutTime - $elapsed);
}
/**
* Record failed login attempt
*/
private function recordFailedAttempt(): void
{
$attempts = $this->session->get('failed_attempts', []);
$attempts[] = time();
$this->session->set('failed_attempts', $attempts);
}
/**
* Clear failed attempts
*/
private function clearFailedAttempts(): void
{
$this->session->remove('failed_attempts');
}
/**
* Generate CSRF token
*/
public function generateCsrfToken(): string
{
$token = bin2hex(random_bytes(32));
$this->session->set('csrf_token', $token);
return $token;
}
/**
* Get CSRF token
*/
public function getCsrfToken(): string
{
$token = $this->session->get('csrf_token');
if (!$token) {
$token = $this->generateCsrfToken();
}
return $token;
}
/**
* Validate CSRF token
*/
public function validateCsrfToken(string $token): bool
{
if (!($this->config['security']['csrf_protection'] ?? true)) {
return true;
}
$storedToken = $this->session->get('csrf_token');
if (!$storedToken || !$token) {
return false;
}
return hash_equals($storedToken, $token);
}
/**
* Get login time
*/
public function getLoginTime(): ?int
{
return $this->session->get('login_time');
}
/**
* Get last activity time
*/
public function getLastActivity(): ?int
{
return $this->session->get('last_activity');
}
/**
* Update last activity
*/
public function updateActivity(): void
{
$this->session->set('last_activity', time());
}
/**
* Get user IP
*/
public function getUserIP(): string
{
return $this->session->get('user_ip', '0.0.0.0');
}
/**
* Hash a password
*/
public static function hashPassword(string $password): string
{
return password_hash($password, PASSWORD_BCRYPT, ['cost' => 10]);
}
}

205
src/Security/Session.php Archivo normal
Ver fichero

@@ -0,0 +1,205 @@
<?php
/**
* AleShell2 Session Manager
*
* Handles session management.
*
* @package AleShell2\Security
*/
declare(strict_types=1);
namespace AleShell2\Security;
class Session
{
private array $config;
private bool $started = false;
public function __construct(array $config)
{
$this->config = $config;
}
/**
* Start session
*/
public function start(): void
{
if ($this->started || session_status() === PHP_SESSION_ACTIVE) {
$this->started = true;
return;
}
// Configure session
$this->configure();
// Start session
session_start();
$this->started = true;
// Check session timeout
$this->checkTimeout();
// Regenerate session ID periodically
$this->regenerate();
}
/**
* Configure session settings
*/
private function configure(): void
{
// Session name
session_name('ALESHELL_SESS');
// Cookie settings
$cookieParams = [
'lifetime' => 0,
'path' => '/',
'domain' => '',
'secure' => isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off',
'httponly' => true,
'samesite' => 'Strict',
];
session_set_cookie_params($cookieParams);
// Use strict mode
ini_set('session.use_strict_mode', '1');
ini_set('session.use_only_cookies', '1');
ini_set('session.use_trans_sid', '0');
}
/**
* Check session timeout
*/
private function checkTimeout(): void
{
$timeout = $this->config['security']['session_timeout'] ?? 3600;
if (isset($_SESSION['last_activity'])) {
if (time() - $_SESSION['last_activity'] > $timeout) {
// Session expired
$this->destroy();
return;
}
}
$_SESSION['last_activity'] = time();
}
/**
* Regenerate session ID periodically
*/
private function regenerate(): void
{
$regenerateInterval = 300; // 5 minutes
if (!isset($_SESSION['created'])) {
$_SESSION['created'] = time();
} elseif (time() - $_SESSION['created'] > $regenerateInterval) {
session_regenerate_id(true);
$_SESSION['created'] = time();
}
}
/**
* Set session value
*/
public function set(string $key, mixed $value): void
{
$_SESSION[$key] = $value;
}
/**
* Get session value
*/
public function get(string $key, mixed $default = null): mixed
{
return $_SESSION[$key] ?? $default;
}
/**
* Check if session key exists
*/
public function has(string $key): bool
{
return isset($_SESSION[$key]);
}
/**
* Remove session key
*/
public function remove(string $key): void
{
unset($_SESSION[$key]);
}
/**
* Get all session data
*/
public function all(): array
{
return $_SESSION ?? [];
}
/**
* Flash message (available only for next request)
*/
public function flash(string $key, mixed $value): void
{
$_SESSION['_flash'][$key] = $value;
}
/**
* Get flash message
*/
public function getFlash(string $key, mixed $default = null): mixed
{
$value = $_SESSION['_flash'][$key] ?? $default;
unset($_SESSION['_flash'][$key]);
return $value;
}
/**
* Destroy session
*/
public function destroy(): void
{
$_SESSION = [];
if (ini_get('session.use_cookies')) {
$params = session_get_cookie_params();
setcookie(
session_name(),
'',
time() - 42000,
$params['path'],
$params['domain'],
$params['secure'],
$params['httponly']
);
}
session_destroy();
$this->started = false;
}
/**
* Get session ID
*/
public function getId(): string
{
return session_id();
}
/**
* Regenerate session ID
*/
public function regenerateId(): void
{
session_regenerate_id(true);
$_SESSION['created'] = time();
}
}

246
src/Views/auth/login.php Archivo normal
Ver fichero

@@ -0,0 +1,246 @@
<?php
/**
* AleShell2 Login View
*
* @var array $data View data
*/
$error = $data['error'] ?? '';
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Login - AleShell2</title>
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🐚</text></svg>">
<style>
:root {
--bg-primary: #0d1117;
--bg-secondary: #161b22;
--bg-tertiary: #21262d;
--border-color: #30363d;
--text-primary: #c9d1d9;
--text-secondary: #8b949e;
--accent-primary: #58a6ff;
--accent-danger: #f85149;
--font-sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: var(--font-sans);
background: var(--bg-primary);
color: var(--text-primary);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
}
.login-container {
width: 100%;
max-width: 400px;
}
.login-header {
text-align: center;
margin-bottom: 32px;
}
.login-logo {
font-size: 64px;
margin-bottom: 16px;
}
.login-title {
font-size: 28px;
font-weight: 600;
margin-bottom: 8px;
}
.login-subtitle {
color: var(--text-secondary);
font-size: 14px;
}
.login-card {
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 12px;
padding: 32px;
}
.form-group {
margin-bottom: 20px;
}
.form-label {
display: block;
font-size: 13px;
font-weight: 500;
color: var(--text-secondary);
margin-bottom: 8px;
}
.form-input {
width: 100%;
padding: 12px 14px;
background: var(--bg-primary);
border: 1px solid var(--border-color);
border-radius: 6px;
color: var(--text-primary);
font-size: 14px;
transition: all 0.15s ease;
}
.form-input:focus {
outline: none;
border-color: var(--accent-primary);
box-shadow: 0 0 0 3px rgba(88, 166, 255, 0.2);
}
.form-input::placeholder {
color: #6e7681;
}
.login-btn {
width: 100%;
padding: 12px;
background: var(--accent-primary);
border: none;
border-radius: 6px;
color: white;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: background 0.15s ease;
}
.login-btn:hover {
background: #4c9aed;
}
.login-btn:disabled {
opacity: 0.7;
cursor: not-allowed;
}
.alert {
padding: 12px 16px;
border-radius: 6px;
font-size: 13px;
margin-bottom: 20px;
}
.alert-danger {
background: rgba(248, 81, 73, 0.1);
border: 1px solid rgba(248, 81, 73, 0.3);
color: var(--accent-danger);
}
.login-footer {
text-align: center;
margin-top: 24px;
font-size: 12px;
color: var(--text-secondary);
}
.server-info {
margin-top: 32px;
padding: 16px;
background: var(--bg-tertiary);
border-radius: 8px;
font-size: 12px;
color: var(--text-secondary);
}
.server-info-row {
display: flex;
justify-content: space-between;
margin-bottom: 8px;
}
.server-info-row:last-child {
margin-bottom: 0;
}
.server-info-label {
color: #6e7681;
}
/* Animation */
@keyframes fadeIn {
from { opacity: 0; transform: translateY(-10px); }
to { opacity: 1; transform: translateY(0); }
}
.login-container {
animation: fadeIn 0.3s ease;
}
</style>
</head>
<body>
<div class="login-container">
<div class="login-header">
<div class="login-logo">🐚</div>
<h1 class="login-title">AleShell2</h1>
<p class="login-subtitle">Web Administration Shell</p>
</div>
<div class="login-card">
<?php if ($error): ?>
<div class="alert alert-danger">
<?= htmlspecialchars($error) ?>
</div>
<?php endif; ?>
<form method="POST" action="?action=login" autocomplete="off">
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($_SESSION['csrf_token'] ?? '') ?>">
<div class="form-group">
<label class="form-label" for="password">Password</label>
<input
type="password"
id="password"
name="password"
class="form-input"
placeholder="Enter your password"
autofocus
required
>
</div>
<button type="submit" class="login-btn">
🔐 Sign In
</button>
</form>
</div>
<div class="server-info">
<div class="server-info-row">
<span class="server-info-label">Server</span>
<span><?= htmlspecialchars($_SERVER['SERVER_SOFTWARE'] ?? 'Unknown') ?></span>
</div>
<div class="server-info-row">
<span class="server-info-label">PHP Version</span>
<span><?= PHP_VERSION ?></span>
</div>
<div class="server-info-row">
<span class="server-info-label">Your IP</span>
<span><?= htmlspecialchars($_SERVER['REMOTE_ADDR'] ?? 'Unknown') ?></span>
</div>
</div>
<div class="login-footer">
AleShell2 v1.0.0 &copy; <?= date('Y') ?>
</div>
</div>
</body>
</html>

1055
src/Views/layouts/main.php Archivo normal

La diferencia del archivo ha sido suprimido porque es demasiado grande Cargar Diff

208
src/Views/modules/dashboard.php Archivo normal
Ver fichero

@@ -0,0 +1,208 @@
<?php
/**
* AleShell2 Dashboard View
*
* @var array $data View data
*/
$systemInfo = $data['systemInfo'] ?? [];
$server = $systemInfo['server'] ?? [];
$php = $systemInfo['php'] ?? [];
$hardware = $systemInfo['hardware'] ?? [];
$disk = $systemInfo['disk'] ?? [];
?>
<div class="grid grid-4 mb-3">
<div class="stat-card">
<div class="stat-label">Server</div>
<div class="stat-value" style="font-size: 16px;"><?= htmlspecialchars($server['hostname'] ?? 'Unknown') ?></div>
<div class="stat-meta"><?= htmlspecialchars($server['os'] ?? 'Unknown OS') ?></div>
</div>
<div class="stat-card">
<div class="stat-label">PHP Version</div>
<div class="stat-value" style="font-size: 20px;"><?= htmlspecialchars($php['version'] ?? 'Unknown') ?></div>
<div class="stat-meta"><?= htmlspecialchars($php['sapi'] ?? '') ?></div>
</div>
<div class="stat-card">
<div class="stat-label">Load Average</div>
<div class="stat-value" style="font-size: 20px;"><?= htmlspecialchars($server['load_average']['1min'] ?? 'N/A') ?></div>
<div class="stat-meta">
5m: <?= htmlspecialchars($server['load_average']['5min'] ?? 'N/A') ?> /
15m: <?= htmlspecialchars($server['load_average']['15min'] ?? 'N/A') ?>
</div>
</div>
<div class="stat-card">
<div class="stat-label">Uptime</div>
<div class="stat-value" style="font-size: 20px;"><?= htmlspecialchars($server['uptime'] ?? 'Unknown') ?></div>
<div class="stat-meta">Since boot</div>
</div>
</div>
<div class="grid grid-2 mb-3">
<!-- Server Information -->
<div class="card">
<div class="card-header">
<h3 class="card-title">🖥️ Server Information</h3>
</div>
<div class="card-body">
<table class="table">
<tbody>
<tr>
<td class="text-muted">Hostname</td>
<td><?= htmlspecialchars($server['hostname'] ?? 'Unknown') ?></td>
</tr>
<tr>
<td class="text-muted">OS</td>
<td><?= htmlspecialchars($server['os_detail'] ?? 'Unknown') ?></td>
</tr>
<tr>
<td class="text-muted">Server Software</td>
<td><?= htmlspecialchars($server['server_software'] ?? 'Unknown') ?></td>
</tr>
<tr>
<td class="text-muted">Server Address</td>
<td><?= htmlspecialchars($server['server_addr'] ?? 'Unknown') ?></td>
</tr>
<tr>
<td class="text-muted">Document Root</td>
<td class="text-mono"><?= htmlspecialchars($server['document_root'] ?? 'Unknown') ?></td>
</tr>
<tr>
<td class="text-muted">Current User</td>
<td><?= htmlspecialchars($server['current_user'] ?? 'Unknown') ?> (uid: <?= $server['current_uid'] ?? '?' ?>, gid: <?= $server['current_gid'] ?? '?' ?>)</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- PHP Information -->
<div class="card">
<div class="card-header">
<h3 class="card-title">🐘 PHP Information</h3>
<a href="?module=system&action=phpinfo" target="_blank" class="btn btn-sm btn-secondary">View phpinfo()</a>
</div>
<div class="card-body">
<table class="table">
<tbody>
<tr>
<td class="text-muted">Version</td>
<td><?= htmlspecialchars($php['version'] ?? 'Unknown') ?></td>
</tr>
<tr>
<td class="text-muted">SAPI</td>
<td><?= htmlspecialchars($php['sapi'] ?? 'Unknown') ?></td>
</tr>
<tr>
<td class="text-muted">Memory Limit</td>
<td><?= htmlspecialchars($php['memory_limit'] ?? 'Unknown') ?></td>
</tr>
<tr>
<td class="text-muted">Max Execution Time</td>
<td><?= htmlspecialchars($php['max_execution_time'] ?? 'Unknown') ?>s</td>
</tr>
<tr>
<td class="text-muted">Upload Max Size</td>
<td><?= htmlspecialchars($php['upload_max_filesize'] ?? 'Unknown') ?></td>
</tr>
<tr>
<td class="text-muted">Disabled Functions</td>
<td style="max-width: 200px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;" title="<?= htmlspecialchars($php['disabled_functions'] ?? 'none') ?>">
<?= htmlspecialchars(strlen($php['disabled_functions'] ?? '') > 50 ? substr($php['disabled_functions'], 0, 50) . '...' : ($php['disabled_functions'] ?? 'none')) ?>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<div class="grid grid-2 mb-3">
<!-- Hardware Information -->
<div class="card">
<div class="card-header">
<h3 class="card-title">💾 Hardware Information</h3>
</div>
<div class="card-body">
<table class="table">
<tbody>
<tr>
<td class="text-muted">CPU</td>
<td><?= htmlspecialchars($hardware['cpu'] ?? 'Unknown') ?></td>
</tr>
<tr>
<td class="text-muted">CPU Cores</td>
<td><?= htmlspecialchars($hardware['cpu_cores'] ?? 'Unknown') ?></td>
</tr>
<tr>
<td class="text-muted">Total Memory</td>
<td><?= htmlspecialchars($hardware['memory_total'] ?? 'Unknown') ?></td>
</tr>
<tr>
<td class="text-muted">Free Memory</td>
<td><?= htmlspecialchars($hardware['memory_free'] ?? 'Unknown') ?></td>
</tr>
<tr>
<td class="text-muted">Available Memory</td>
<td><?= htmlspecialchars($hardware['memory_available'] ?? 'Unknown') ?></td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- Disk Information -->
<div class="card">
<div class="card-header">
<h3 class="card-title">💿 Disk Usage</h3>
</div>
<div class="card-body">
<?php if (!empty($disk)): ?>
<table class="table table-mono">
<thead>
<tr>
<th>Mount</th>
<th>Total</th>
<th>Used</th>
<th>Free</th>
<th>%</th>
</tr>
</thead>
<tbody>
<?php foreach (array_slice($disk, 0, 5) as $d): ?>
<tr>
<td><?= htmlspecialchars($d['mount'] ?? $d['device'] ?? '-') ?></td>
<td><?= htmlspecialchars($d['total'] ?? '-') ?></td>
<td><?= htmlspecialchars($d['used'] ?? '-') ?></td>
<td><?= htmlspecialchars($d['free'] ?? '-') ?></td>
<td><?= htmlspecialchars($d['percent_used'] ?? '-') ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php else: ?>
<p class="text-muted">No disk information available</p>
<?php endif; ?>
</div>
</div>
</div>
<!-- Quick Actions -->
<div class="card">
<div class="card-header">
<h3 class="card-title">⚡ Quick Actions</h3>
</div>
<div class="card-body">
<div class="flex gap-2" style="flex-wrap: wrap;">
<a href="?module=files" class="btn btn-secondary">📁 Browse Files</a>
<a href="?module=terminal" class="btn btn-secondary">💻 Open Terminal</a>
<a href="?module=processes" class="btn btn-secondary">⚙️ View Processes</a>
<a href="?module=network" class="btn btn-secondary">🌐 Network Tools</a>
<a href="?module=database" class="btn btn-secondary">🗄️ Database Manager</a>
<a href="?module=system" class="btn btn-secondary">🖥️ System Info</a>
</div>
</div>
</div>

323
src/Views/modules/database.php Archivo normal
Ver fichero

@@ -0,0 +1,323 @@
<?php
/**
* AleShell2 Database View
*
* @var array $data View data
*/
$connected = isset($_SESSION['db_connection']);
$connection = $_SESSION['db_connection'] ?? [];
?>
<div class="grid grid-4 mb-3" style="grid-template-columns: 300px 1fr;">
<!-- Sidebar -->
<div>
<!-- Connection Card -->
<div class="card mb-3">
<div class="card-header">
<h3 class="card-title">🔌 Connection</h3>
<?php if ($connected): ?>
<span class="badge badge-success">Connected</span>
<?php endif; ?>
</div>
<div class="card-body">
<?php if (!$connected): ?>
<form id="connect-form" onsubmit="doConnect(event)">
<div class="form-group">
<label class="form-label">Driver</label>
<select id="db-driver" class="form-select">
<option value="mysql">MySQL</option>
<option value="pgsql">PostgreSQL</option>
<option value="sqlite">SQLite</option>
</select>
</div>
<div id="mysql-fields">
<div class="form-group">
<label class="form-label">Host</label>
<input type="text" id="db-host" class="form-input" value="localhost">
</div>
<div class="form-group">
<label class="form-label">Port</label>
<input type="number" id="db-port" class="form-input" value="3306">
</div>
<div class="form-group">
<label class="form-label">Database</label>
<input type="text" id="db-name" class="form-input" placeholder="database_name">
</div>
<div class="form-group">
<label class="form-label">Username</label>
<input type="text" id="db-user" class="form-input" placeholder="root">
</div>
<div class="form-group">
<label class="form-label">Password</label>
<input type="password" id="db-pass" class="form-input">
</div>
</div>
<div id="sqlite-fields" class="hidden">
<div class="form-group">
<label class="form-label">Database File</label>
<input type="text" id="db-file" class="form-input" placeholder="/path/to/database.sqlite">
</div>
</div>
<button type="submit" class="btn btn-primary" style="width: 100%;">Connect</button>
</form>
<?php else: ?>
<div class="mb-2">
<div class="text-muted" style="font-size: 12px;">Driver</div>
<div><?= htmlspecialchars(strtoupper($connection['driver'] ?? '')) ?></div>
</div>
<div class="mb-2">
<div class="text-muted" style="font-size: 12px;">Host</div>
<div><?= htmlspecialchars($connection['host'] ?? '') ?>:<?= htmlspecialchars($connection['port'] ?? '') ?></div>
</div>
<div class="mb-3">
<div class="text-muted" style="font-size: 12px;">Database</div>
<div><?= htmlspecialchars($connection['database'] ?? '') ?></div>
</div>
<button onclick="doDisconnect()" class="btn btn-danger" style="width: 100%;">Disconnect</button>
<?php endif; ?>
</div>
</div>
<!-- Databases / Tables -->
<?php if ($connected): ?>
<div class="card">
<div class="card-header">
<h3 class="card-title">📂 Structure</h3>
<button onclick="refreshStructure()" class="btn btn-sm btn-secondary">🔄</button>
</div>
<div class="card-body" style="padding: 0; max-height: 400px; overflow-y: auto;">
<div id="db-structure">
<div class="loading">
<div class="spinner"></div>
</div>
</div>
</div>
</div>
<?php endif; ?>
</div>
<!-- Main Content -->
<div>
<?php if ($connected): ?>
<!-- Query Editor -->
<div class="card mb-3">
<div class="card-header">
<h3 class="card-title">📝 Query Editor</h3>
<div class="flex gap-1">
<button onclick="executeQuery()" class="btn btn-primary">▶️ Execute</button>
<button onclick="clearQuery()" class="btn btn-secondary">Clear</button>
</div>
</div>
<div class="card-body">
<textarea id="query-editor" class="code-editor" placeholder="SELECT * FROM table_name LIMIT 100;" style="min-height: 150px;"></textarea>
</div>
</div>
<!-- Results -->
<div class="card">
<div class="card-header">
<h3 class="card-title">📊 Results</h3>
<span id="query-stats" class="text-muted"></span>
</div>
<div class="card-body" style="padding: 0;">
<div id="query-results" class="table-container" style="max-height: 400px; overflow: auto;">
<div class="text-muted text-center" style="padding: 40px;">
Execute a query to see results
</div>
</div>
</div>
</div>
<?php else: ?>
<div class="card">
<div class="card-body text-center" style="padding: 60px;">
<div style="font-size: 48px; margin-bottom: 16px;">🗄️</div>
<h3>Database Manager</h3>
<p class="text-muted mt-2">Connect to a database to start managing your data</p>
</div>
</div>
<?php endif; ?>
</div>
</div>
<script>
// Toggle SQLite fields
document.getElementById('db-driver')?.addEventListener('change', function() {
const isSqlite = this.value === 'sqlite';
document.getElementById('mysql-fields').classList.toggle('hidden', isSqlite);
document.getElementById('sqlite-fields').classList.toggle('hidden', !isSqlite);
if (this.value === 'pgsql') {
document.getElementById('db-port').value = '5432';
} else if (this.value === 'mysql') {
document.getElementById('db-port').value = '3306';
}
});
// Connect to database
async function doConnect(event) {
event.preventDefault();
const driver = document.getElementById('db-driver').value;
const data = { driver };
if (driver === 'sqlite') {
data.database = document.getElementById('db-file').value;
} else {
data.host = document.getElementById('db-host').value;
data.port = document.getElementById('db-port').value;
data.database = document.getElementById('db-name').value;
data.username = document.getElementById('db-user').value;
data.password = document.getElementById('db-pass').value;
}
try {
const response = await api('?module=database&action=connect', 'POST', data);
if (response.success) {
toast('Connected to database', 'success');
location.reload();
} else {
toast(response.error, 'danger');
}
} catch (error) {
toast('Connection failed: ' + error.message, 'danger');
}
}
// Disconnect
async function doDisconnect() {
try {
await api('?module=database&action=disconnect', 'POST', {});
toast('Disconnected', 'success');
location.reload();
} catch (error) {
location.reload();
}
}
// Refresh structure
async function refreshStructure() {
const container = document.getElementById('db-structure');
container.innerHTML = '<div class="loading"><div class="spinner"></div></div>';
try {
const response = await api('?module=database&action=tables');
if (response.success) {
const tables = response.data.tables;
if (tables.length === 0) {
container.innerHTML = '<div class="text-muted" style="padding: 16px;">No tables found</div>';
return;
}
let html = '';
for (const table of tables) {
html += `
<div class="file-item" onclick="selectTable('${escapeHtml(table)}')" style="cursor: pointer;">
<span class="file-icon">📋</span>
<span class="file-name">${escapeHtml(table)}</span>
</div>
`;
}
container.innerHTML = html;
} else {
container.innerHTML = `<div class="text-danger" style="padding: 16px;">${escapeHtml(response.error)}</div>`;
}
} catch (error) {
container.innerHTML = '<div class="text-danger" style="padding: 16px;">Failed to load tables</div>';
}
}
// Select table
function selectTable(table) {
document.getElementById('query-editor').value = `SELECT * FROM \`${table}\` LIMIT 100;`;
executeQuery();
}
// Execute query
async function executeQuery() {
const query = document.getElementById('query-editor').value.trim();
const resultsDiv = document.getElementById('query-results');
const statsSpan = document.getElementById('query-stats');
if (!query) {
toast('Please enter a query', 'warning');
return;
}
resultsDiv.innerHTML = '<div class="loading"><div class="spinner"></div>Executing query...</div>';
statsSpan.textContent = '';
try {
const response = await api('?module=database&action=query', 'POST', { query });
if (response.success) {
const data = response.data;
statsSpan.textContent = `${data.affected_rows} rows | ${data.execution_time}`;
if (data.rows !== undefined) {
// SELECT query
if (data.rows.length === 0) {
resultsDiv.innerHTML = '<div class="text-muted text-center" style="padding: 40px;">No results</div>';
return;
}
let html = '<table class="table table-mono"><thead><tr>';
for (const col of data.columns) {
html += `<th>${escapeHtml(col)}</th>`;
}
html += '</tr></thead><tbody>';
for (const row of data.rows) {
html += '<tr>';
for (const col of data.columns) {
const value = row[col];
const displayValue = value === null ? '<span class="text-muted">NULL</span>' : escapeHtml(String(value));
html += `<td style="max-width: 300px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">${displayValue}</td>`;
}
html += '</tr>';
}
html += '</tbody></table>';
if (data.limited) {
html += `<div class="text-warning text-center" style="padding: 8px; font-size: 12px;">Results limited. Total: ${data.total_rows} rows</div>`;
}
resultsDiv.innerHTML = html;
} else {
// Non-SELECT query
resultsDiv.innerHTML = `<div class="alert alert-success" style="margin: 16px;">Query executed successfully. Affected rows: ${data.affected_rows}</div>`;
}
} else {
resultsDiv.innerHTML = `<div class="alert alert-danger" style="margin: 16px;">${escapeHtml(response.error)}</div>`;
}
} catch (error) {
resultsDiv.innerHTML = '<div class="alert alert-danger" style="margin: 16px;">Query execution failed</div>';
}
}
// Clear query
function clearQuery() {
document.getElementById('query-editor').value = '';
document.getElementById('query-results').innerHTML = '<div class="text-muted text-center" style="padding: 40px;">Execute a query to see results</div>';
document.getElementById('query-stats').textContent = '';
}
// Keyboard shortcuts
document.getElementById('query-editor')?.addEventListener('keydown', (e) => {
if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
e.preventDefault();
executeQuery();
}
});
// Initialize
document.addEventListener('DOMContentLoaded', () => {
if (document.getElementById('db-structure')) {
refreshStructure();
}
});
</script>

191
src/Views/modules/editor.php Archivo normal
Ver fichero

@@ -0,0 +1,191 @@
<?php
/**
* AleShell2 Code Editor View
*
* @var array $data View data
*/
$filePath = $data['filePath'] ?? '';
$content = $data['content'] ?? '';
$filename = basename($filePath) ?: 'untitled.txt';
$extension = pathinfo($filename, PATHINFO_EXTENSION);
?>
<div class="card">
<div class="card-header">
<div class="flex items-center gap-2" style="flex: 1;">
<span>📝</span>
<input type="text" id="file-path" class="form-input form-input-mono" value="<?= htmlspecialchars($filePath) ?>" placeholder="/path/to/file.txt" style="flex: 1;">
<button onclick="loadFile()" class="btn btn-secondary">Open</button>
</div>
<div class="flex gap-1">
<button onclick="saveFile()" class="btn btn-primary">💾 Save</button>
<button onclick="saveAsFile()" class="btn btn-secondary">Save As</button>
<button onclick="downloadFile()" class="btn btn-secondary">⬇️ Download</button>
</div>
</div>
<div class="card-body" style="padding: 0;">
<div style="display: flex; align-items: center; padding: 8px 16px; background: var(--bg-tertiary); border-bottom: 1px solid var(--border-color);">
<span class="text-muted" style="font-size: 12px;">
<?= htmlspecialchars($filename) ?>
<?php if ($extension): ?>
<span class="badge badge-info"><?= htmlspecialchars($extension) ?></span>
<?php endif; ?>
</span>
<span style="flex: 1;"></span>
<span id="line-info" class="text-muted" style="font-size: 12px;">Line 1, Col 1</span>
</div>
<textarea id="editor" class="code-editor" style="border: none; border-radius: 0; min-height: 500px;"><?= htmlspecialchars($content) ?></textarea>
</div>
</div>
<!-- Save As Modal -->
<div id="saveas-modal" class="modal-backdrop">
<div class="modal">
<div class="modal-header">
<h3 class="modal-title">Save As</h3>
<button class="modal-close" onclick="closeModal('saveas-modal')">&times;</button>
</div>
<div class="modal-body">
<div class="form-group">
<label class="form-label">File Path</label>
<input type="text" id="saveas-path" class="form-input form-input-mono" value="<?= htmlspecialchars($filePath) ?>">
</div>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" onclick="closeModal('saveas-modal')">Cancel</button>
<button class="btn btn-primary" onclick="doSaveAs()">Save</button>
</div>
</div>
</div>
<script>
const editor = document.getElementById('editor');
const filePathInput = document.getElementById('file-path');
const lineInfo = document.getElementById('line-info');
// Update line info
editor.addEventListener('keyup', updateLineInfo);
editor.addEventListener('click', updateLineInfo);
function updateLineInfo() {
const text = editor.value.substring(0, editor.selectionStart);
const lines = text.split('\n');
const line = lines.length;
const col = lines[lines.length - 1].length + 1;
lineInfo.textContent = `Line ${line}, Col ${col}`;
}
// Handle tab key
editor.addEventListener('keydown', (e) => {
if (e.key === 'Tab') {
e.preventDefault();
const start = editor.selectionStart;
const end = editor.selectionEnd;
if (e.shiftKey) {
// Remove indent
const lineStart = editor.value.lastIndexOf('\n', start - 1) + 1;
const lineText = editor.value.substring(lineStart, start);
if (lineText.startsWith(' ')) {
editor.value = editor.value.substring(0, lineStart) + editor.value.substring(lineStart + 4);
editor.selectionStart = editor.selectionEnd = start - 4;
} else if (lineText.startsWith('\t')) {
editor.value = editor.value.substring(0, lineStart) + editor.value.substring(lineStart + 1);
editor.selectionStart = editor.selectionEnd = start - 1;
}
} else {
// Add indent
editor.value = editor.value.substring(0, start) + ' ' + editor.value.substring(end);
editor.selectionStart = editor.selectionEnd = start + 4;
}
}
// Save with Ctrl+S
if (e.key === 's' && (e.ctrlKey || e.metaKey)) {
e.preventDefault();
saveFile();
}
});
// Load file
async function loadFile() {
const path = filePathInput.value.trim();
if (!path) {
toast('Please enter a file path', 'warning');
return;
}
try {
const response = await api(`?module=files&action=read&path=${encodeURIComponent(path)}`);
if (response.success) {
editor.value = response.data.content;
toast('File loaded', 'success');
} else {
toast(response.error, 'danger');
}
} catch (error) {
toast('Failed to load file: ' + error.message, 'danger');
}
}
// Save file
async function saveFile() {
const path = filePathInput.value.trim();
if (!path) {
openModal('saveas-modal');
return;
}
const content = editor.value;
try {
const response = await api('?module=files&action=write', 'POST', { path, content });
if (response.success) {
toast('File saved', 'success');
} else {
toast(response.error, 'danger');
}
} catch (error) {
toast('Failed to save file: ' + error.message, 'danger');
}
}
// Save as
function saveAsFile() {
document.getElementById('saveas-path').value = filePathInput.value;
openModal('saveas-modal');
}
async function doSaveAs() {
const path = document.getElementById('saveas-path').value.trim();
if (!path) {
toast('Please enter a file path', 'warning');
return;
}
filePathInput.value = path;
closeModal('saveas-modal');
await saveFile();
}
// Download file
function downloadFile() {
const path = filePathInput.value.trim();
if (!path) {
toast('No file to download', 'warning');
return;
}
window.location.href = `?module=files&action=download&path=${encodeURIComponent(path)}`;
}
// Load file from URL parameter
const urlParams = new URLSearchParams(window.location.search);
const pathParam = urlParams.get('path');
if (pathParam) {
filePathInput.value = pathParam;
loadFile();
}
</script>

559
src/Views/modules/files.php Archivo normal
Ver fichero

@@ -0,0 +1,559 @@
<?php
/**
* AleShell2 File Manager View
*
* @var array $data View data
*/
$currentPath = $data['currentPath'] ?? getcwd();
$files = $data['files'] ?? [];
?>
<div class="card mb-3">
<div class="card-header">
<div class="flex items-center gap-2">
<span>📂</span>
<input type="text" id="path-input" class="form-input form-input-mono" value="<?= htmlspecialchars($currentPath) ?>" style="flex: 1;">
<button onclick="navigateTo(document.getElementById('path-input').value)" class="btn btn-secondary">Go</button>
</div>
</div>
<div class="card-body" style="padding: 0;">
<div class="flex items-center justify-between" style="padding: 12px 16px; border-bottom: 1px solid var(--border-color);">
<div class="flex gap-1">
<button onclick="navigateUp()" class="btn btn-sm btn-secondary">⬆️ Up</button>
<button onclick="refresh()" class="btn btn-sm btn-secondary">🔄 Refresh</button>
<button onclick="openNewFileModal()" class="btn btn-sm btn-secondary">📄 New File</button>
<button onclick="openNewFolderModal()" class="btn btn-sm btn-secondary">📁 New Folder</button>
<button onclick="openUploadModal()" class="btn btn-sm btn-primary">⬆️ Upload</button>
</div>
<div class="flex gap-1">
<input type="text" id="search-input" class="form-input" placeholder="Search files..." style="width: 200px;" onkeyup="filterFiles(this.value)">
</div>
</div>
<div id="file-list" class="file-list">
<div class="loading">
<div class="spinner"></div>
Loading files...
</div>
</div>
</div>
</div>
<!-- New File Modal -->
<div id="new-file-modal" class="modal-backdrop">
<div class="modal">
<div class="modal-header">
<h3 class="modal-title">Create New File</h3>
<button class="modal-close" onclick="closeModal('new-file-modal')">&times;</button>
</div>
<div class="modal-body">
<div class="form-group">
<label class="form-label">Filename</label>
<input type="text" id="new-file-name" class="form-input" placeholder="filename.txt">
</div>
<div class="form-group">
<label class="form-label">Content (optional)</label>
<textarea id="new-file-content" class="form-textarea code-editor" rows="5" placeholder="File content..."></textarea>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" onclick="closeModal('new-file-modal')">Cancel</button>
<button class="btn btn-primary" onclick="createNewFile()">Create File</button>
</div>
</div>
</div>
<!-- New Folder Modal -->
<div id="new-folder-modal" class="modal-backdrop">
<div class="modal">
<div class="modal-header">
<h3 class="modal-title">Create New Folder</h3>
<button class="modal-close" onclick="closeModal('new-folder-modal')">&times;</button>
</div>
<div class="modal-body">
<div class="form-group">
<label class="form-label">Folder Name</label>
<input type="text" id="new-folder-name" class="form-input" placeholder="folder-name">
</div>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" onclick="closeModal('new-folder-modal')">Cancel</button>
<button class="btn btn-primary" onclick="createNewFolder()">Create Folder</button>
</div>
</div>
</div>
<!-- Upload Modal -->
<div id="upload-modal" class="modal-backdrop">
<div class="modal">
<div class="modal-header">
<h3 class="modal-title">Upload Files</h3>
<button class="modal-close" onclick="closeModal('upload-modal')">&times;</button>
</div>
<div class="modal-body">
<div class="form-group">
<label class="form-label">Select Files</label>
<input type="file" id="upload-files" class="form-input" multiple>
</div>
<div id="upload-progress" class="hidden">
<div style="background: var(--bg-tertiary); border-radius: 4px; height: 8px; overflow: hidden;">
<div id="upload-bar" style="width: 0%; height: 100%; background: var(--accent-primary); transition: width 0.3s;"></div>
</div>
<div id="upload-status" class="text-muted mt-1" style="font-size: 12px;">Uploading...</div>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" onclick="closeModal('upload-modal')">Cancel</button>
<button class="btn btn-primary" onclick="uploadFiles()">Upload</button>
</div>
</div>
</div>
<!-- Rename Modal -->
<div id="rename-modal" class="modal-backdrop">
<div class="modal">
<div class="modal-header">
<h3 class="modal-title">Rename</h3>
<button class="modal-close" onclick="closeModal('rename-modal')">&times;</button>
</div>
<div class="modal-body">
<div class="form-group">
<label class="form-label">New Name</label>
<input type="text" id="rename-input" class="form-input">
<input type="hidden" id="rename-old-path">
</div>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" onclick="closeModal('rename-modal')">Cancel</button>
<button class="btn btn-primary" onclick="doRename()">Rename</button>
</div>
</div>
</div>
<!-- Chmod Modal -->
<div id="chmod-modal" class="modal-backdrop">
<div class="modal">
<div class="modal-header">
<h3 class="modal-title">Change Permissions</h3>
<button class="modal-close" onclick="closeModal('chmod-modal')">&times;</button>
</div>
<div class="modal-body">
<div class="form-group">
<label class="form-label">Permissions (octal)</label>
<input type="text" id="chmod-input" class="form-input" placeholder="0755">
<input type="hidden" id="chmod-path">
</div>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" onclick="closeModal('chmod-modal')">Cancel</button>
<button class="btn btn-primary" onclick="doChmod()">Change</button>
</div>
</div>
</div>
<!-- Context Menu -->
<div id="context-menu" class="context-menu">
<div class="context-menu-item" onclick="openFile()">📄 Open</div>
<div class="context-menu-item" onclick="editFile()">✏️ Edit</div>
<div class="context-menu-item" onclick="downloadFile()">⬇️ Download</div>
<div class="context-menu-divider"></div>
<div class="context-menu-item" onclick="copyFile()">📋 Copy</div>
<div class="context-menu-item" onclick="moveFile()">📦 Move</div>
<div class="context-menu-item" onclick="renameFile()">✏️ Rename</div>
<div class="context-menu-item" onclick="chmodFile()">🔒 Permissions</div>
<div class="context-menu-divider"></div>
<div class="context-menu-item danger" onclick="deleteFile()">🗑️ Delete</div>
</div>
<script>
let currentPath = '<?= addslashes($currentPath) ?>';
let files = [];
let selectedFile = null;
let clipboard = null;
// Initialize
document.addEventListener('DOMContentLoaded', () => {
loadFiles(currentPath);
});
// Load files from path
async function loadFiles(path) {
const fileList = document.getElementById('file-list');
fileList.innerHTML = '<div class="loading"><div class="spinner"></div>Loading files...</div>';
try {
const response = await api(`?module=files&action=list&path=${encodeURIComponent(path)}`);
if (response.success) {
currentPath = response.data.path;
files = response.data.files;
document.getElementById('path-input').value = currentPath;
renderFiles();
} else {
fileList.innerHTML = `<div class="alert alert-danger" style="margin: 16px;">${escapeHtml(response.error)}</div>`;
}
} catch (error) {
fileList.innerHTML = `<div class="alert alert-danger" style="margin: 16px;">Failed to load files: ${escapeHtml(error.message)}</div>`;
}
}
// Render file list
function renderFiles() {
const fileList = document.getElementById('file-list');
if (files.length === 0) {
fileList.innerHTML = '<div class="text-muted" style="padding: 40px; text-align: center;">This directory is empty</div>';
return;
}
let html = '';
for (const file of files) {
const icon = getFileIcon(file);
const size = file.type === 'file' ? formatBytes(file.size) : '-';
const perms = file.permissions || '-';
const modified = file.modified ? new Date(file.modified * 1000).toLocaleString() : '-';
html += `
<div class="file-item"
data-path="${escapeHtml(file.path)}"
data-type="${file.type}"
data-name="${escapeHtml(file.name)}"
onclick="selectFile(this)"
ondblclick="openItem(this)"
oncontextmenu="showContextMenu(event, this)">
<span class="file-icon">${icon}</span>
<span class="file-name">${escapeHtml(file.name)}</span>
<div class="file-meta">
<span class="text-mono">${perms}</span>
<span>${size}</span>
<span>${modified}</span>
</div>
</div>
`;
}
fileList.innerHTML = html;
}
// Get file icon
function getFileIcon(file) {
if (file.type === 'directory') return '📁';
if (file.type === 'link') return '🔗';
const ext = file.name.split('.').pop().toLowerCase();
const icons = {
'php': '🐘', 'js': '📜', 'ts': '📜', 'py': '🐍', 'rb': '💎',
'html': '🌐', 'css': '🎨', 'json': '📋', 'xml': '📋', 'yml': '📋', 'yaml': '📋',
'md': '📝', 'txt': '📄', 'log': '📋',
'jpg': '🖼️', 'jpeg': '🖼️', 'png': '🖼️', 'gif': '🖼️', 'svg': '🖼️', 'webp': '🖼️',
'mp3': '🎵', 'wav': '🎵', 'ogg': '🎵',
'mp4': '🎬', 'avi': '🎬', 'mkv': '🎬', 'webm': '🎬',
'zip': '📦', 'tar': '📦', 'gz': '📦', 'rar': '📦', '7z': '📦',
'pdf': '📕', 'doc': '📘', 'docx': '📘', 'xls': '📗', 'xlsx': '📗',
'sql': '🗄️', 'db': '🗄️', 'sqlite': '🗄️',
'sh': '⚙️', 'bash': '⚙️', 'zsh': '⚙️',
'exe': '⚡', 'bin': '⚡',
'conf': '⚙️', 'cfg': '⚙️', 'ini': '⚙️',
};
return icons[ext] || '📄';
}
// Select file
function selectFile(el) {
document.querySelectorAll('.file-item').forEach(f => f.style.background = '');
el.style.background = 'var(--bg-hover)';
selectedFile = {
path: el.dataset.path,
type: el.dataset.type,
name: el.dataset.name
};
}
// Open item (double click)
function openItem(el) {
const path = el.dataset.path;
const type = el.dataset.type;
if (type === 'directory') {
loadFiles(path);
} else {
window.location.href = `?module=editor&path=${encodeURIComponent(path)}`;
}
}
// Navigate to path
function navigateTo(path) {
loadFiles(path);
}
// Navigate up
function navigateUp() {
const parts = currentPath.split('/').filter(p => p);
parts.pop();
const newPath = '/' + parts.join('/');
loadFiles(newPath || '/');
}
// Refresh
function refresh() {
loadFiles(currentPath);
}
// Filter files
function filterFiles(query) {
query = query.toLowerCase();
document.querySelectorAll('.file-item').forEach(item => {
const name = item.dataset.name.toLowerCase();
item.style.display = name.includes(query) ? '' : 'none';
});
}
// Context menu
function showContextMenu(event, el) {
event.preventDefault();
selectFile(el);
const menu = document.getElementById('context-menu');
menu.style.left = event.pageX + 'px';
menu.style.top = event.pageY + 'px';
menu.classList.add('active');
}
// Hide context menu on click
document.addEventListener('click', () => {
document.getElementById('context-menu').classList.remove('active');
});
// Context menu actions
function openFile() {
if (!selectedFile) return;
if (selectedFile.type === 'directory') {
loadFiles(selectedFile.path);
} else {
window.open(`?module=files&action=read&path=${encodeURIComponent(selectedFile.path)}`, '_blank');
}
}
function editFile() {
if (!selectedFile || selectedFile.type !== 'file') return;
window.location.href = `?module=editor&path=${encodeURIComponent(selectedFile.path)}`;
}
function downloadFile() {
if (!selectedFile || selectedFile.type !== 'file') return;
window.location.href = `?module=files&action=download&path=${encodeURIComponent(selectedFile.path)}`;
}
function copyFile() {
if (!selectedFile) return;
clipboard = { action: 'copy', path: selectedFile.path };
toast('File copied to clipboard', 'success');
}
function moveFile() {
if (!selectedFile) return;
clipboard = { action: 'move', path: selectedFile.path };
toast('File cut to clipboard', 'success');
}
function renameFile() {
if (!selectedFile) return;
document.getElementById('rename-input').value = selectedFile.name;
document.getElementById('rename-old-path').value = selectedFile.path;
openModal('rename-modal');
}
function chmodFile() {
if (!selectedFile) return;
document.getElementById('chmod-path').value = selectedFile.path;
document.getElementById('chmod-input').value = '';
openModal('chmod-modal');
}
function deleteFile() {
if (!selectedFile) return;
if (!confirm(`Are you sure you want to delete "${selectedFile.name}"?`)) return;
api('?module=files&action=delete', 'POST', { path: selectedFile.path })
.then(response => {
if (response.success) {
toast('File deleted', 'success');
refresh();
} else {
toast(response.error, 'danger');
}
});
}
// Modal actions
function openNewFileModal() {
document.getElementById('new-file-name').value = '';
document.getElementById('new-file-content').value = '';
openModal('new-file-modal');
}
function openNewFolderModal() {
document.getElementById('new-folder-name').value = '';
openModal('new-folder-modal');
}
function openUploadModal() {
document.getElementById('upload-files').value = '';
document.getElementById('upload-progress').classList.add('hidden');
openModal('upload-modal');
}
async function createNewFile() {
const name = document.getElementById('new-file-name').value.trim();
const content = document.getElementById('new-file-content').value;
if (!name) {
toast('Please enter a filename', 'warning');
return;
}
const path = currentPath + '/' + name;
const response = await api('?module=files&action=write', 'POST', { path, content });
if (response.success) {
toast('File created', 'success');
closeModal('new-file-modal');
refresh();
} else {
toast(response.error, 'danger');
}
}
async function createNewFolder() {
const name = document.getElementById('new-folder-name').value.trim();
if (!name) {
toast('Please enter a folder name', 'warning');
return;
}
const path = currentPath + '/' + name;
const response = await api('?module=files&action=mkdir', 'POST', { path });
if (response.success) {
toast('Folder created', 'success');
closeModal('new-folder-modal');
refresh();
} else {
toast(response.error, 'danger');
}
}
async function uploadFiles() {
const input = document.getElementById('upload-files');
const files = input.files;
if (files.length === 0) {
toast('Please select files to upload', 'warning');
return;
}
const progressDiv = document.getElementById('upload-progress');
const progressBar = document.getElementById('upload-bar');
const statusText = document.getElementById('upload-status');
progressDiv.classList.remove('hidden');
for (let i = 0; i < files.length; i++) {
const file = files[i];
const formData = new FormData();
formData.append('file', file);
formData.append('path', currentPath);
formData.append('csrf_token', getCsrfToken());
statusText.textContent = `Uploading ${file.name} (${i + 1}/${files.length})...`;
progressBar.style.width = ((i / files.length) * 100) + '%';
try {
const response = await fetch('?module=files&action=upload', {
method: 'POST',
body: formData
});
const result = await response.json();
if (!result.success) {
toast(`Failed to upload ${file.name}: ${result.error}`, 'danger');
}
} catch (error) {
toast(`Failed to upload ${file.name}`, 'danger');
}
}
progressBar.style.width = '100%';
statusText.textContent = 'Upload complete!';
setTimeout(() => {
closeModal('upload-modal');
refresh();
}, 1000);
}
async function doRename() {
const oldPath = document.getElementById('rename-old-path').value;
const newName = document.getElementById('rename-input').value.trim();
if (!newName) {
toast('Please enter a new name', 'warning');
return;
}
const pathParts = oldPath.split('/');
pathParts.pop();
const newPath = pathParts.join('/') + '/' + newName;
const response = await api('?module=files&action=move', 'POST', { source: oldPath, destination: newPath });
if (response.success) {
toast('File renamed', 'success');
closeModal('rename-modal');
refresh();
} else {
toast(response.error, 'danger');
}
}
async function doChmod() {
const path = document.getElementById('chmod-path').value;
const mode = document.getElementById('chmod-input').value.trim();
if (!mode) {
toast('Please enter permissions', 'warning');
return;
}
const response = await api('?module=files&action=chmod', 'POST', { path, mode });
if (response.success) {
toast('Permissions changed', 'success');
closeModal('chmod-modal');
refresh();
} else {
toast(response.error, 'danger');
}
}
// Keyboard shortcuts
document.addEventListener('keydown', (e) => {
if (e.key === 'F5') {
e.preventDefault();
refresh();
}
if (e.key === 'Backspace' && document.activeElement.tagName !== 'INPUT') {
e.preventDefault();
navigateUp();
}
if (e.key === 'Delete' && selectedFile) {
deleteFile();
}
});
</script>

314
src/Views/modules/network.php Archivo normal
Ver fichero

@@ -0,0 +1,314 @@
<?php
/**
* AleShell2 Network View
*
* @var array $data View data
*/
?>
<div class="tabs">
<div class="tab active" data-tab="connections" onclick="switchTab('connections')">Connections</div>
<div class="tab" data-tab="ping" onclick="switchTab('ping')">Ping</div>
<div class="tab" data-tab="traceroute" onclick="switchTab('traceroute')">Traceroute</div>
<div class="tab" data-tab="portscan" onclick="switchTab('portscan')">Port Scan</div>
</div>
<!-- Connections Tab -->
<div id="tab-connections" class="tab-content">
<div class="card">
<div class="card-header">
<h3 class="card-title">🔌 Network Connections</h3>
<button onclick="loadConnections()" class="btn btn-sm btn-secondary">🔄 Refresh</button>
</div>
<div class="card-body" style="padding: 0;">
<div class="table-container">
<table class="table table-mono">
<thead>
<tr>
<th>Protocol</th>
<th>Local Address</th>
<th>Local Port</th>
<th>Foreign Address</th>
<th>State</th>
</tr>
</thead>
<tbody id="connections-list">
<tr>
<td colspan="5" class="text-center">
<div class="loading">
<div class="spinner"></div>
Loading connections...
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
<!-- Ping Tab -->
<div id="tab-ping" class="tab-content hidden">
<div class="card">
<div class="card-header">
<h3 class="card-title">📡 Ping</h3>
</div>
<div class="card-body">
<form id="ping-form" onsubmit="doPing(event)">
<div class="grid grid-2">
<div class="form-group">
<label class="form-label">Host</label>
<input type="text" id="ping-host" class="form-input" placeholder="google.com or 8.8.8.8" required>
</div>
<div class="form-group">
<label class="form-label">Count</label>
<input type="number" id="ping-count" class="form-input" value="4" min="1" max="10">
</div>
</div>
<button type="submit" class="btn btn-primary">📡 Ping</button>
</form>
<div class="terminal mt-3">
<div class="terminal-header">
<span class="terminal-dot red"></span>
<span class="terminal-dot yellow"></span>
<span class="terminal-dot green"></span>
</div>
<div class="terminal-body" style="height: 300px;">
<pre id="ping-output" class="terminal-output">Ready to ping...</pre>
</div>
</div>
</div>
</div>
</div>
<!-- Traceroute Tab -->
<div id="tab-traceroute" class="tab-content hidden">
<div class="card">
<div class="card-header">
<h3 class="card-title">🛤️ Traceroute</h3>
</div>
<div class="card-body">
<form id="traceroute-form" onsubmit="doTraceroute(event)">
<div class="grid grid-2">
<div class="form-group">
<label class="form-label">Host</label>
<input type="text" id="traceroute-host" class="form-input" placeholder="google.com" required>
</div>
<div class="form-group">
<label class="form-label">Max Hops</label>
<input type="number" id="traceroute-hops" class="form-input" value="30" min="1" max="30">
</div>
</div>
<button type="submit" class="btn btn-primary">🛤️ Trace Route</button>
</form>
<div class="terminal mt-3">
<div class="terminal-header">
<span class="terminal-dot red"></span>
<span class="terminal-dot yellow"></span>
<span class="terminal-dot green"></span>
</div>
<div class="terminal-body" style="height: 300px;">
<pre id="traceroute-output" class="terminal-output">Ready to trace...</pre>
</div>
</div>
</div>
</div>
</div>
<!-- Port Scan Tab -->
<div id="tab-portscan" class="tab-content hidden">
<div class="card">
<div class="card-header">
<h3 class="card-title">🔍 Port Scanner</h3>
</div>
<div class="card-body">
<form id="portscan-form" onsubmit="doPortScan(event)">
<div class="grid grid-2">
<div class="form-group">
<label class="form-label">Host</label>
<input type="text" id="portscan-host" class="form-input" placeholder="localhost or 192.168.1.1" required>
</div>
<div class="form-group">
<label class="form-label">Ports</label>
<input type="text" id="portscan-ports" class="form-input" placeholder="1-100 or 22,80,443" value="1-100">
</div>
</div>
<p class="text-muted mb-2" style="font-size: 12px;">⚠️ Port scanning limited to 100 ports max</p>
<button type="submit" class="btn btn-primary">🔍 Scan Ports</button>
</form>
<div id="portscan-results" class="mt-3 hidden">
<div class="card">
<div class="card-header">
<h4 class="card-title">Scan Results</h4>
<span id="portscan-summary" class="text-muted"></span>
</div>
<div class="card-body" style="padding: 0;">
<table class="table table-mono">
<thead>
<tr>
<th>Port</th>
<th>Service</th>
<th>Status</th>
</tr>
</thead>
<tbody id="portscan-list">
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
// Tab switching
function switchTab(tabName) {
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
document.querySelectorAll('.tab-content').forEach(c => c.classList.add('hidden'));
document.querySelector(`[data-tab="${tabName}"]`).classList.add('active');
document.getElementById(`tab-${tabName}`).classList.remove('hidden');
if (tabName === 'connections') {
loadConnections();
}
}
// Load connections
async function loadConnections() {
const tbody = document.getElementById('connections-list');
tbody.innerHTML = '<tr><td colspan="5" class="text-center"><div class="loading"><div class="spinner"></div>Loading...</div></td></tr>';
try {
const response = await api('?module=network&action=connections');
if (response.success) {
const connections = response.data.connections;
if (connections.length === 0) {
tbody.innerHTML = '<tr><td colspan="5" class="text-center text-muted">No connections found</td></tr>';
return;
}
let html = '';
for (const conn of connections) {
html += `
<tr>
<td><span class="badge badge-info">${escapeHtml(conn.protocol)}</span></td>
<td>${escapeHtml(conn.local_address)}</td>
<td>${escapeHtml(conn.local_port)}</td>
<td>${escapeHtml(conn.foreign_address)}</td>
<td><span class="badge badge-success">${escapeHtml(conn.state)}</span></td>
</tr>
`;
}
tbody.innerHTML = html;
} else {
tbody.innerHTML = `<tr><td colspan="5" class="text-danger text-center">${escapeHtml(response.error)}</td></tr>`;
}
} catch (error) {
tbody.innerHTML = '<tr><td colspan="5" class="text-danger text-center">Failed to load connections</td></tr>';
}
}
// Ping
async function doPing(event) {
event.preventDefault();
const host = document.getElementById('ping-host').value.trim();
const count = document.getElementById('ping-count').value;
const output = document.getElementById('ping-output');
output.textContent = `Pinging ${host}...\n`;
try {
const response = await api('?module=network&action=ping', 'POST', { host, count });
if (response.success) {
output.textContent = response.data.output;
} else {
output.textContent = 'Error: ' + response.error;
}
} catch (error) {
output.textContent = 'Error: ' + error.message;
}
}
// Traceroute
async function doTraceroute(event) {
event.preventDefault();
const host = document.getElementById('traceroute-host').value.trim();
const maxHops = document.getElementById('traceroute-hops').value;
const output = document.getElementById('traceroute-output');
output.textContent = `Tracing route to ${host}...\nThis may take a while...\n`;
try {
const response = await api('?module=network&action=traceroute', 'POST', { host, max_hops: maxHops });
if (response.success) {
output.textContent = response.data.output;
} else {
output.textContent = 'Error: ' + response.error;
}
} catch (error) {
output.textContent = 'Error: ' + error.message;
}
}
// Port Scan
async function doPortScan(event) {
event.preventDefault();
const host = document.getElementById('portscan-host').value.trim();
const ports = document.getElementById('portscan-ports').value.trim();
const resultsDiv = document.getElementById('portscan-results');
const tbody = document.getElementById('portscan-list');
const summary = document.getElementById('portscan-summary');
resultsDiv.classList.remove('hidden');
tbody.innerHTML = '<tr><td colspan="3" class="text-center"><div class="loading"><div class="spinner"></div>Scanning ports...</div></td></tr>';
summary.textContent = '';
try {
const response = await api('?module=network&action=portscan', 'POST', { host, ports });
if (response.success) {
const data = response.data;
summary.textContent = `Scanned ${data.scanned_count} ports | ${data.open_count} open | ${data.closed_count} closed`;
if (data.open_ports.length === 0) {
tbody.innerHTML = '<tr><td colspan="3" class="text-center text-muted">No open ports found</td></tr>';
} else {
let html = '';
for (const port of data.open_ports) {
html += `
<tr>
<td>${port.port}</td>
<td>${escapeHtml(port.service)}</td>
<td><span class="badge badge-success">${port.status}</span></td>
</tr>
`;
}
tbody.innerHTML = html;
}
} else {
tbody.innerHTML = `<tr><td colspan="3" class="text-danger text-center">${escapeHtml(response.error)}</td></tr>`;
}
} catch (error) {
tbody.innerHTML = '<tr><td colspan="3" class="text-danger text-center">Scan failed</td></tr>';
}
}
// Initialize
document.addEventListener('DOMContentLoaded', () => {
loadConnections();
});
</script>

263
src/Views/modules/processes.php Archivo normal
Ver fichero

@@ -0,0 +1,263 @@
<?php
/**
* AleShell2 Processes View
*
* @var array $data View data
*/
?>
<div class="card">
<div class="card-header">
<h3 class="card-title">⚙️ Running Processes</h3>
<div class="flex gap-1">
<button onclick="refreshProcesses()" class="btn btn-sm btn-secondary">🔄 Refresh</button>
<input type="text" id="process-filter" class="form-input" placeholder="Filter processes..." style="width: 200px;" onkeyup="filterProcesses(this.value)">
</div>
</div>
<div class="card-body" style="padding: 0;">
<div class="table-container">
<table class="table table-mono" id="process-table">
<thead>
<tr>
<th style="width: 80px;">PID</th>
<th style="width: 100px;">User</th>
<th style="width: 80px;">CPU %</th>
<th style="width: 80px;">MEM %</th>
<th style="width: 100px;">Memory</th>
<th>Command</th>
<th style="width: 80px;">Actions</th>
</tr>
</thead>
<tbody id="process-list">
<tr>
<td colspan="7" class="text-center">
<div class="loading">
<div class="spinner"></div>
Loading processes...
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<div class="grid grid-2 mt-3">
<!-- System Load -->
<div class="card">
<div class="card-header">
<h3 class="card-title">📊 System Load</h3>
</div>
<div class="card-body">
<div id="system-load">
<?php
$load = sys_getloadavg();
if ($load !== false):
?>
<div class="flex justify-between mb-1">
<span class="text-muted">1 minute</span>
<span><?= round($load[0], 2) ?></span>
</div>
<div class="flex justify-between mb-1">
<span class="text-muted">5 minutes</span>
<span><?= round($load[1], 2) ?></span>
</div>
<div class="flex justify-between">
<span class="text-muted">15 minutes</span>
<span><?= round($load[2], 2) ?></span>
</div>
<?php else: ?>
<p class="text-muted">Load average not available</p>
<?php endif; ?>
</div>
</div>
</div>
<!-- Process Stats -->
<div class="card">
<div class="card-header">
<h3 class="card-title">📈 Process Statistics</h3>
</div>
<div class="card-body">
<div id="process-stats">
<div class="flex justify-between mb-1">
<span class="text-muted">Total Processes</span>
<span id="stat-total">-</span>
</div>
<div class="flex justify-between mb-1">
<span class="text-muted">Current User</span>
<span id="stat-user-procs">-</span>
</div>
<div class="flex justify-between">
<span class="text-muted">PHP Processes</span>
<span id="stat-php-procs">-</span>
</div>
</div>
</div>
</div>
</div>
<!-- Kill Confirm Modal -->
<div id="kill-modal" class="modal-backdrop">
<div class="modal">
<div class="modal-header">
<h3 class="modal-title">Kill Process</h3>
<button class="modal-close" onclick="closeModal('kill-modal')">&times;</button>
</div>
<div class="modal-body">
<p>Are you sure you want to kill process <strong id="kill-pid"></strong>?</p>
<p class="text-muted mt-1" id="kill-command"></p>
<div class="form-group mt-2">
<label class="form-label">Signal</label>
<select id="kill-signal" class="form-select">
<option value="15">SIGTERM (15) - Graceful</option>
<option value="9">SIGKILL (9) - Force</option>
<option value="1">SIGHUP (1) - Reload</option>
<option value="2">SIGINT (2) - Interrupt</option>
</select>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" onclick="closeModal('kill-modal')">Cancel</button>
<button class="btn btn-danger" onclick="doKillProcess()">Kill Process</button>
</div>
</div>
</div>
<script>
let processes = [];
let killTarget = null;
// Initialize
document.addEventListener('DOMContentLoaded', () => {
refreshProcesses();
// Auto refresh every 10 seconds
setInterval(refreshProcesses, 10000);
});
// Load processes
async function refreshProcesses() {
try {
const response = await api('?module=processes&action=list');
if (response.success) {
processes = response.data.processes;
renderProcesses();
updateStats();
} else {
document.getElementById('process-list').innerHTML = `
<tr><td colspan="7" class="text-danger text-center">${escapeHtml(response.error)}</td></tr>
`;
}
} catch (error) {
document.getElementById('process-list').innerHTML = `
<tr><td colspan="7" class="text-danger text-center">Failed to load processes</td></tr>
`;
}
}
// Render processes
function renderProcesses() {
const tbody = document.getElementById('process-list');
if (processes.length === 0) {
tbody.innerHTML = '<tr><td colspan="7" class="text-center text-muted">No processes found</td></tr>';
return;
}
// Sort by CPU usage
const sorted = [...processes].sort((a, b) => parseFloat(b.cpu || 0) - parseFloat(a.cpu || 0));
let html = '';
for (const proc of sorted) {
const cpuClass = parseFloat(proc.cpu || 0) > 50 ? 'text-danger' : (parseFloat(proc.cpu || 0) > 20 ? 'text-warning' : '');
const memClass = parseFloat(proc.mem || 0) > 50 ? 'text-danger' : (parseFloat(proc.mem || 0) > 20 ? 'text-warning' : '');
html += `
<tr data-pid="${proc.pid}" data-command="${escapeHtml(proc.command || '')}">
<td>${proc.pid}</td>
<td>${escapeHtml(proc.user || '-')}</td>
<td class="${cpuClass}">${proc.cpu || '0.0'}%</td>
<td class="${memClass}">${proc.mem || '0.0'}%</td>
<td>${proc.rss ? formatMemory(proc.rss) : '-'}</td>
<td style="max-width: 400px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;" title="${escapeHtml(proc.command || '')}">
${escapeHtml(proc.command || '-')}
</td>
<td>
<button class="btn btn-sm btn-danger btn-icon" onclick="killProcess(${proc.pid}, '${escapeHtml(proc.command || '')}')" title="Kill">
</button>
</td>
</tr>
`;
}
tbody.innerHTML = html;
}
// Update statistics
function updateStats() {
const currentUser = '<?= addslashes(get_current_user()) ?>';
document.getElementById('stat-total').textContent = processes.length;
document.getElementById('stat-user-procs').textContent = processes.filter(p => p.user === currentUser).length;
document.getElementById('stat-php-procs').textContent = processes.filter(p => (p.command || '').toLowerCase().includes('php')).length;
}
// Filter processes
function filterProcesses(query) {
query = query.toLowerCase();
const rows = document.querySelectorAll('#process-list tr');
rows.forEach(row => {
const command = row.dataset.command?.toLowerCase() || '';
const pid = row.dataset.pid || '';
row.style.display = (command.includes(query) || pid.includes(query)) ? '' : 'none';
});
}
// Kill process
function killProcess(pid, command) {
killTarget = pid;
document.getElementById('kill-pid').textContent = pid;
document.getElementById('kill-command').textContent = command;
openModal('kill-modal');
}
async function doKillProcess() {
if (!killTarget) return;
const signal = document.getElementById('kill-signal').value;
try {
const response = await api('?module=processes&action=kill', 'POST', {
pid: killTarget,
signal: signal
});
if (response.success) {
toast('Process killed', 'success');
closeModal('kill-modal');
refreshProcesses();
} else {
toast(response.error, 'danger');
}
} catch (error) {
toast('Failed to kill process', 'danger');
}
}
// Format memory
function formatMemory(kb) {
const bytes = kb * 1024;
const units = ['B', 'KB', 'MB', 'GB'];
let i = 0;
let size = bytes;
while (size >= 1024 && i < units.length - 1) {
size /= 1024;
i++;
}
return size.toFixed(1) + ' ' + units[i];
}
</script>

368
src/Views/modules/system.php Archivo normal
Ver fichero

@@ -0,0 +1,368 @@
<?php
/**
* AleShell2 System Info View
*
* @var array $data View data
*/
$systemInfo = $data['systemInfo'] ?? [];
$server = $systemInfo['server'] ?? [];
$php = $systemInfo['php'] ?? [];
$hardware = $systemInfo['hardware'] ?? [];
$disk = $systemInfo['disk'] ?? [];
$network = $systemInfo['network'] ?? [];
?>
<div class="tabs">
<div class="tab active" data-tab="overview" onclick="switchTab('overview')">Overview</div>
<div class="tab" data-tab="php" onclick="switchTab('php')">PHP</div>
<div class="tab" data-tab="extensions" onclick="switchTab('extensions')">Extensions</div>
<div class="tab" data-tab="environment" onclick="switchTab('environment')">Environment</div>
</div>
<!-- Overview Tab -->
<div id="tab-overview" class="tab-content">
<div class="grid grid-2 mb-3">
<!-- Server Info -->
<div class="card">
<div class="card-header">
<h3 class="card-title">🖥️ Server Information</h3>
</div>
<div class="card-body">
<table class="table">
<tbody>
<tr>
<td class="text-muted" style="width: 150px;">Hostname</td>
<td><?= htmlspecialchars($server['hostname'] ?? 'Unknown') ?></td>
</tr>
<tr>
<td class="text-muted">Operating System</td>
<td><?= htmlspecialchars($server['os'] ?? 'Unknown') ?></td>
</tr>
<tr>
<td class="text-muted">OS Details</td>
<td style="font-size: 12px;"><?= htmlspecialchars($server['os_detail'] ?? 'Unknown') ?></td>
</tr>
<tr>
<td class="text-muted">Server Software</td>
<td><?= htmlspecialchars($server['server_software'] ?? 'Unknown') ?></td>
</tr>
<tr>
<td class="text-muted">Server Address</td>
<td><?= htmlspecialchars($server['server_addr'] ?? 'Unknown') ?>:<?= htmlspecialchars($server['server_port'] ?? '') ?></td>
</tr>
<tr>
<td class="text-muted">Document Root</td>
<td class="text-mono" style="font-size: 12px;"><?= htmlspecialchars($server['document_root'] ?? 'Unknown') ?></td>
</tr>
<tr>
<td class="text-muted">Current User</td>
<td><?= htmlspecialchars($server['current_user'] ?? 'Unknown') ?> (uid: <?= $server['current_uid'] ?? '?' ?>, gid: <?= $server['current_gid'] ?? '?' ?>)</td>
</tr>
<tr>
<td class="text-muted">Process ID</td>
<td><?= $server['process_id'] ?? 'Unknown' ?></td>
</tr>
<tr>
<td class="text-muted">Uptime</td>
<td><?= htmlspecialchars($server['uptime'] ?? 'Unknown') ?></td>
</tr>
<tr>
<td class="text-muted">Load Average</td>
<td>
<?= $server['load_average']['1min'] ?? '-' ?> /
<?= $server['load_average']['5min'] ?? '-' ?> /
<?= $server['load_average']['15min'] ?? '-' ?>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- Hardware Info -->
<div class="card">
<div class="card-header">
<h3 class="card-title">💻 Hardware Information</h3>
</div>
<div class="card-body">
<table class="table">
<tbody>
<tr>
<td class="text-muted" style="width: 150px;">CPU</td>
<td style="font-size: 12px;"><?= htmlspecialchars($hardware['cpu'] ?? 'Unknown') ?></td>
</tr>
<tr>
<td class="text-muted">CPU Cores</td>
<td><?= htmlspecialchars($hardware['cpu_cores'] ?? 'Unknown') ?></td>
</tr>
<tr>
<td class="text-muted">Total Memory</td>
<td><?= htmlspecialchars($hardware['memory_total'] ?? 'Unknown') ?></td>
</tr>
<tr>
<td class="text-muted">Free Memory</td>
<td><?= htmlspecialchars($hardware['memory_free'] ?? 'Unknown') ?></td>
</tr>
<tr>
<td class="text-muted">Available Memory</td>
<td><?= htmlspecialchars($hardware['memory_available'] ?? 'Unknown') ?></td>
</tr>
</tbody>
</table>
<h4 style="margin: 20px 0 10px; font-size: 14px;">💿 Disk Usage</h4>
<?php if (!empty($disk)): ?>
<table class="table table-mono" style="font-size: 12px;">
<thead>
<tr>
<th>Mount</th>
<th>Total</th>
<th>Used</th>
<th>Free</th>
<th>%</th>
</tr>
</thead>
<tbody>
<?php foreach (array_slice($disk, 0, 5) as $d): ?>
<tr>
<td style="max-width: 100px; overflow: hidden; text-overflow: ellipsis;"><?= htmlspecialchars($d['mount'] ?? $d['device'] ?? '-') ?></td>
<td><?= htmlspecialchars($d['total'] ?? '-') ?></td>
<td><?= htmlspecialchars($d['used'] ?? '-') ?></td>
<td><?= htmlspecialchars($d['free'] ?? '-') ?></td>
<td><?= htmlspecialchars($d['percent_used'] ?? '-') ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php else: ?>
<p class="text-muted">No disk information available</p>
<?php endif; ?>
</div>
</div>
</div>
</div>
<!-- PHP Tab -->
<div id="tab-php" class="tab-content hidden">
<div class="grid grid-2">
<div class="card">
<div class="card-header">
<h3 class="card-title">🐘 PHP Configuration</h3>
<a href="?module=system&action=phpinfo" target="_blank" class="btn btn-sm btn-secondary">Full phpinfo()</a>
</div>
<div class="card-body">
<table class="table">
<tbody>
<tr>
<td class="text-muted">Version</td>
<td><?= htmlspecialchars($php['version'] ?? 'Unknown') ?></td>
</tr>
<tr>
<td class="text-muted">SAPI</td>
<td><?= htmlspecialchars($php['sapi'] ?? 'Unknown') ?></td>
</tr>
<tr>
<td class="text-muted">INI Path</td>
<td class="text-mono" style="font-size: 12px;"><?= htmlspecialchars($php['ini_path'] ?? 'Unknown') ?></td>
</tr>
<tr>
<td class="text-muted">Extension Dir</td>
<td class="text-mono" style="font-size: 12px;"><?= htmlspecialchars($php['extension_dir'] ?? 'Unknown') ?></td>
</tr>
<tr>
<td class="text-muted">Zend Version</td>
<td><?= htmlspecialchars($php['zend_version'] ?? 'Unknown') ?></td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="card">
<div class="card-header">
<h3 class="card-title">⚙️ PHP Settings</h3>
</div>
<div class="card-body">
<table class="table">
<tbody>
<tr>
<td class="text-muted">Memory Limit</td>
<td><?= htmlspecialchars($php['memory_limit'] ?? 'Unknown') ?></td>
</tr>
<tr>
<td class="text-muted">Max Execution Time</td>
<td><?= htmlspecialchars($php['max_execution_time'] ?? 'Unknown') ?>s</td>
</tr>
<tr>
<td class="text-muted">Max Input Time</td>
<td><?= htmlspecialchars($php['max_input_time'] ?? 'Unknown') ?>s</td>
</tr>
<tr>
<td class="text-muted">Post Max Size</td>
<td><?= htmlspecialchars($php['post_max_size'] ?? 'Unknown') ?></td>
</tr>
<tr>
<td class="text-muted">Upload Max Size</td>
<td><?= htmlspecialchars($php['upload_max_filesize'] ?? 'Unknown') ?></td>
</tr>
<tr>
<td class="text-muted">Max File Uploads</td>
<td><?= htmlspecialchars($php['max_file_uploads'] ?? 'Unknown') ?></td>
</tr>
<tr>
<td class="text-muted">Display Errors</td>
<td><?= $php['display_errors'] ? '<span class="badge badge-warning">On</span>' : '<span class="badge badge-success">Off</span>' ?></td>
</tr>
<tr>
<td class="text-muted">Allow URL Fopen</td>
<td><?= $php['allow_url_fopen'] === 'on' ? '<span class="badge badge-warning">On</span>' : '<span class="badge badge-success">Off</span>' ?></td>
</tr>
<tr>
<td class="text-muted">Allow URL Include</td>
<td><?= $php['allow_url_include'] === 'on' ? '<span class="badge badge-danger">On</span>' : '<span class="badge badge-success">Off</span>' ?></td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<div class="card mt-3">
<div class="card-header">
<h3 class="card-title">🚫 Disabled Functions</h3>
</div>
<div class="card-body">
<?php
$disabled = $php['disabled_functions'] ?? 'none';
if ($disabled && $disabled !== 'none'):
$functions = explode(',', $disabled);
?>
<div class="flex gap-1" style="flex-wrap: wrap;">
<?php foreach ($functions as $func): ?>
<span class="badge badge-danger"><?= htmlspecialchars(trim($func)) ?></span>
<?php endforeach; ?>
</div>
<?php else: ?>
<p class="text-success">No functions disabled</p>
<?php endif; ?>
</div>
</div>
</div>
<!-- Extensions Tab -->
<div id="tab-extensions" class="tab-content hidden">
<div class="card">
<div class="card-header">
<h3 class="card-title">📦 Loaded Extensions</h3>
<span class="text-muted" id="extension-count"></span>
</div>
<div class="card-body">
<div id="extensions-list" class="flex gap-1" style="flex-wrap: wrap;">
<div class="loading"><div class="spinner"></div>Loading extensions...</div>
</div>
</div>
</div>
</div>
<!-- Environment Tab -->
<div id="tab-environment" class="tab-content hidden">
<div class="card">
<div class="card-header">
<h3 class="card-title">🌍 Environment Variables</h3>
<input type="text" id="env-filter" class="form-input" placeholder="Filter..." style="width: 200px;" onkeyup="filterEnv(this.value)">
</div>
<div class="card-body" style="padding: 0; max-height: 600px; overflow-y: auto;">
<table class="table table-mono" id="env-table">
<thead>
<tr>
<th style="width: 250px;">Variable</th>
<th>Value</th>
</tr>
</thead>
<tbody id="env-list">
<tr><td colspan="2" class="text-center"><div class="loading"><div class="spinner"></div></div></td></tr>
</tbody>
</table>
</div>
</div>
</div>
<script>
// Tab switching
function switchTab(tabName) {
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
document.querySelectorAll('.tab-content').forEach(c => c.classList.add('hidden'));
document.querySelector(`[data-tab="${tabName}"]`).classList.add('active');
document.getElementById(`tab-${tabName}`).classList.remove('hidden');
if (tabName === 'extensions') {
loadExtensions();
} else if (tabName === 'environment') {
loadEnvironment();
}
}
// Load extensions
async function loadExtensions() {
const container = document.getElementById('extensions-list');
try {
const response = await api('?module=system&action=extensions');
if (response.success) {
const extensions = response.data.extensions;
const count = Object.keys(extensions).length;
document.getElementById('extension-count').textContent = `${count} extensions loaded`;
let html = '';
for (const [name, version] of Object.entries(extensions)) {
html += `<span class="badge badge-info" title="Version: ${escapeHtml(version)}">${escapeHtml(name)}</span>`;
}
container.innerHTML = html;
} else {
container.innerHTML = `<span class="text-danger">${escapeHtml(response.error)}</span>`;
}
} catch (error) {
container.innerHTML = '<span class="text-danger">Failed to load extensions</span>';
}
}
// Load environment
async function loadEnvironment() {
const tbody = document.getElementById('env-list');
try {
const response = await api('?module=system&action=environment');
if (response.success) {
const env = response.data.environment;
let html = '';
for (const [key, value] of Object.entries(env)) {
html += `
<tr data-key="${escapeHtml(key.toLowerCase())}">
<td class="text-muted">${escapeHtml(key)}</td>
<td style="word-break: break-all;">${escapeHtml(String(value))}</td>
</tr>
`;
}
tbody.innerHTML = html;
} else {
tbody.innerHTML = `<tr><td colspan="2" class="text-danger">${escapeHtml(response.error)}</td></tr>`;
}
} catch (error) {
tbody.innerHTML = '<tr><td colspan="2" class="text-danger">Failed to load environment</td></tr>';
}
}
// Filter environment
function filterEnv(query) {
query = query.toLowerCase();
document.querySelectorAll('#env-list tr').forEach(row => {
const key = row.dataset.key || '';
row.style.display = key.includes(query) ? '' : 'none';
});
}
</script>

189
src/Views/modules/terminal.php Archivo normal
Ver fichero

@@ -0,0 +1,189 @@
<?php
/**
* AleShell2 Terminal View
*
* @var array $data View data
*/
$currentDir = $data['currentDir'] ?? getcwd();
?>
<div class="card">
<div class="terminal">
<div class="terminal-header">
<span class="terminal-dot red"></span>
<span class="terminal-dot yellow"></span>
<span class="terminal-dot green"></span>
<span style="margin-left: 12px; color: var(--text-muted); font-size: 12px;">
Terminal - <?= htmlspecialchars(gethostname() ?: 'shell') ?>
</span>
</div>
<div class="terminal-body" id="terminal-output">
<div class="terminal-output">
Welcome to AleShell2 Terminal
PHP <?= PHP_VERSION ?> | <?= PHP_OS ?>
Current directory: <?= htmlspecialchars($currentDir) ?>
Type 'help' for available commands.
</div>
</div>
<div style="padding: 12px 16px; background: var(--bg-tertiary); border-top: 1px solid var(--border-color);">
<div class="terminal-prompt">
<span class="terminal-prompt-text" id="prompt"><?= htmlspecialchars(get_current_user()) ?>@<?= htmlspecialchars(gethostname() ?: 'shell') ?>:~$</span>
<input type="text" class="terminal-input" id="command-input" autocomplete="off" autofocus>
</div>
</div>
</div>
</div>
<div class="card mt-3">
<div class="card-header">
<h3 class="card-title">⌨️ Quick Commands</h3>
</div>
<div class="card-body">
<div class="flex gap-1" style="flex-wrap: wrap;">
<button class="btn btn-sm btn-secondary" onclick="runQuickCommand('pwd')">pwd</button>
<button class="btn btn-sm btn-secondary" onclick="runQuickCommand('ls -la')">ls -la</button>
<button class="btn btn-sm btn-secondary" onclick="runQuickCommand('whoami')">whoami</button>
<button class="btn btn-sm btn-secondary" onclick="runQuickCommand('id')">id</button>
<button class="btn btn-sm btn-secondary" onclick="runQuickCommand('uname -a')">uname -a</button>
<button class="btn btn-sm btn-secondary" onclick="runQuickCommand('cat /etc/passwd')">passwd</button>
<button class="btn btn-sm btn-secondary" onclick="runQuickCommand('netstat -tuln')">netstat</button>
<button class="btn btn-sm btn-secondary" onclick="runQuickCommand('ps aux')">ps aux</button>
<button class="btn btn-sm btn-secondary" onclick="runQuickCommand('df -h')">df -h</button>
<button class="btn btn-sm btn-secondary" onclick="runQuickCommand('free -m')">free -m</button>
<button class="btn btn-sm btn-secondary" onclick="runQuickCommand('env')">env</button>
<button class="btn btn-sm btn-secondary" onclick="runQuickCommand('cat /etc/os-release')">os-release</button>
</div>
</div>
</div>
<script>
let commandHistory = [];
let historyIndex = -1;
let currentDir = '<?= addslashes($currentDir) ?>';
const output = document.getElementById('terminal-output');
const input = document.getElementById('command-input');
const promptEl = document.getElementById('prompt');
// Focus input on click
output.addEventListener('click', () => input.focus());
// Handle input
input.addEventListener('keydown', async (e) => {
if (e.key === 'Enter') {
const command = input.value.trim();
if (command) {
commandHistory.push(command);
historyIndex = commandHistory.length;
await executeCommand(command);
}
input.value = '';
}
// History navigation
if (e.key === 'ArrowUp') {
e.preventDefault();
if (historyIndex > 0) {
historyIndex--;
input.value = commandHistory[historyIndex];
}
}
if (e.key === 'ArrowDown') {
e.preventDefault();
if (historyIndex < commandHistory.length - 1) {
historyIndex++;
input.value = commandHistory[historyIndex];
} else {
historyIndex = commandHistory.length;
input.value = '';
}
}
// Clear screen
if (e.key === 'l' && e.ctrlKey) {
e.preventDefault();
clearScreen();
}
// Cancel
if (e.key === 'c' && e.ctrlKey) {
e.preventDefault();
appendOutput('^C\n');
}
});
// Execute command
async function executeCommand(command) {
appendOutput(`\n${promptEl.textContent} ${escapeHtml(command)}\n`);
try {
const response = await api('?module=terminal&action=exec', 'POST', {
command: command,
cwd: currentDir
});
if (response.success) {
if (response.data.output) {
appendOutput(response.data.output);
}
if (response.data.cwd !== currentDir) {
currentDir = response.data.cwd;
updatePrompt();
}
} else {
appendOutput(`Error: ${response.error}\n`, 'danger');
}
} catch (error) {
appendOutput(`Error: ${error.message}\n`, 'danger');
}
scrollToBottom();
}
// Run quick command
function runQuickCommand(command) {
input.value = command;
input.focus();
const event = new KeyboardEvent('keydown', { key: 'Enter' });
input.dispatchEvent(event);
}
// Append output to terminal
function appendOutput(text, type = '') {
const outputEl = output.querySelector('.terminal-output');
if (type === 'danger') {
outputEl.innerHTML += `<span class="text-danger">${escapeHtml(text)}</span>`;
} else {
outputEl.innerHTML += escapeHtml(text);
}
}
// Clear screen
function clearScreen() {
const outputEl = output.querySelector('.terminal-output');
outputEl.innerHTML = '';
}
// Scroll to bottom
function scrollToBottom() {
output.scrollTop = output.scrollHeight;
}
// Update prompt
function updatePrompt() {
const user = '<?= addslashes(get_current_user()) ?>';
const host = '<?= addslashes(gethostname() ?: "shell") ?>';
const shortDir = currentDir.replace(/^\/home\/[^\/]+/, '~');
promptEl.textContent = `${user}@${host}:${shortDir}$`;
}
// Escape HTML
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
</script>