├── .github ├── FUNDING.yml └── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── .gitignore ├── .styleci.yml ├── changelog.md ├── composer.json ├── composer.lock ├── config └── laravelwebconsole.php ├── helpers.php ├── license ├── readme.md ├── resources └── views │ └── window.blade.php ├── routes └── routes.php ├── screenshot.png └── src ├── BaseJsonRpcServer.php ├── LaravelWebConsole.php ├── LaravelWebConsoleServiceProvider.php └── WebConsoleRPCServer.php /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "[BUG]" 5 | labels: bug 6 | assignees: alkhachatryan 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | 27 | **Additional context** 28 | Add any other context about the problem here. 29 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: feature 6 | assignees: alkhachatryan 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea -------------------------------------------------------------------------------- /.styleci.yml: -------------------------------------------------------------------------------- 1 | preset: laravel -------------------------------------------------------------------------------- /changelog.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to `LaravelWebConsole` will be documented in this file. 4 | 5 | ## Version 1.0 6 | 7 | ### What is new; 8 | - Everything 9 | 10 | 11 | 12 | ## Version 1.1 13 | 14 | ### What is new; 15 | - Code refactoring 16 | 17 | 18 | 19 | ## Version 1.2 20 | 21 | ### What is new; 22 | - Code refactoring 23 | 24 | 25 | 26 | ## Version 1.3 27 | 28 | ### What is new; 29 | - Code refactoring 30 | 31 | 32 | 33 | ## Version 1.4 34 | 35 | ### What is new; 36 | - Bug fix 37 | - Easier manually installation 38 | 39 | 40 | 41 | ## Version 1.5 42 | 43 | ### What is new; 44 | - Bug fix 45 | 46 | ## Version 1.5.1 47 | 48 | ### What is new; 49 | - Checked the package on Laravel 5.8.* 50 | - Updated README.md 51 | 52 | ## Version 2.0 53 | 54 | ### What is new; 55 | - Support Laravel 6.* 56 | 57 | 58 | ## Version 2.2 59 | 60 | ### What is new; 61 | - Set a list of forbidden commands in the config file 62 | 63 | 64 | ## Version 2.2.1 65 | 66 | ### What is new; 67 | - Security updates 68 | 69 | ## Version 2.2.2 70 | 71 | ### What is new; 72 | - Bugfix on login 73 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "alkhachatryan/laravel-web-console", 3 | "description": "Web console for your Laravel application", 4 | "license": "MIT", 5 | "authors": [ 6 | { 7 | "name": "Alexey Khachatryan", 8 | "email": "info@khachatryan.org", 9 | "homepage": "https://khachatryan.org" 10 | } 11 | ], 12 | "homepage": "https://github.com/alkhachatryan/laravel-web-console", 13 | "keywords": ["Laravel", "LaravelWebConsole"], 14 | 15 | "require": { 16 | "illuminate/support": "~5.7.0|~5.8.0|^6.0|^7.0|^8.0" 17 | }, 18 | 19 | "require-dev": { 20 | "phpunit/phpunit": "~7.0|~8.0|~9.0", 21 | "mockery/mockery": "^1.1", 22 | "orchestra/testbench": "~3.0", 23 | "sempro/phpunit-pretty-print": "^1.0" 24 | }, 25 | "autoload": { 26 | "files": [ 27 | "helpers.php" 28 | ], 29 | "psr-4": { 30 | "Alkhachatryan\\LaravelWebConsole\\": "src/" 31 | } 32 | }, 33 | "extra": { 34 | "laravel": { 35 | "providers": [ 36 | "Alkhachatryan\\LaravelWebConsole\\LaravelWebConsoleServiceProvider" 37 | ] 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /config/laravelwebconsole.php: -------------------------------------------------------------------------------- 1 | false, 30 | 31 | // Single-user credentials (REQUIRED) 32 | 'user' => [ 33 | 'name' => env('CONSOLE_USER_NAME', 'root'), 34 | 'password' => env('CONSOLE_USER_PASSWORD', 'root'), 35 | ], 36 | 37 | // Multi-user credentials (OPTIONAL) 38 | // Example: 'user' => 'password', 'user1' => 'password1' 39 | 'accounts' => [ 40 | // 'user' => 'password', 41 | ], 42 | 43 | // Hash incoming password 44 | // By default it's sha256 45 | 'password_hash_algorithm' => '', 46 | 47 | // Home directory (multi-user mode supported) 48 | // Example: 'home_dir' => '/tmp'; 49 | // 'home_dir' => array('user1' => '/home/user1', 'user2' => '/home/user2'); 50 | 'home_dir' => '', 51 | 52 | // These commands cannot be ran 53 | // Comment it, if no need to limit the commands 54 | 'forbidden_commands' => [ 55 | 'rm', 56 | 'rmdir', 57 | 'unlink', 58 | ], 59 | ]; 60 | -------------------------------------------------------------------------------- /helpers.php: -------------------------------------------------------------------------------- 1 | ['pipe', 'r'], // STDIN 23 | 1 => ['pipe', 'w'], // STDOUT 24 | 2 => ['pipe', 'w'], // STDERR 25 | ]; 26 | 27 | $process = proc_open($command.' 2>&1', $descriptors, $pipes); 28 | if (! is_resource($process)) { 29 | exit("Can't execute command."); 30 | } 31 | 32 | // Nothing to push to STDIN 33 | fclose($pipes[0]); 34 | 35 | $output = stream_get_contents($pipes[1]); 36 | fclose($pipes[1]); 37 | 38 | $error = stream_get_contents($pipes[2]); 39 | fclose($pipes[2]); 40 | 41 | // All pipes must be closed before "proc_close" 42 | $code = proc_close($process); 43 | 44 | return $output; 45 | } 46 | 47 | // Command parsing 48 | function parse_command($command) 49 | { 50 | $value = ltrim((string) $command); 51 | 52 | if (! is_empty_string($value)) { 53 | $values = explode(' ', $value); 54 | $values_total = count($values); 55 | 56 | if ($values_total > 1) { 57 | $value = $values[$values_total - 1]; 58 | 59 | for ($index = $values_total - 2; $index >= 0; $index--) { 60 | $value_item = $values[$index]; 61 | 62 | if (substr($value_item, -1) == '\\') { 63 | $value = $value_item.' '.$value; 64 | } else { 65 | break; 66 | } 67 | } 68 | } 69 | } 70 | 71 | return $value; 72 | } 73 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Alexey Khachatryan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # This repository is archived because the author changes his tech stack, but still available on packagist. You can still fork it and make your required changes. 2 | 3 | # LaravelWebConsole 4 | 5 | [![Latest Version on Packagist][ico-version]][link-packagist] 6 | [![StyleCI][ico-styleci]][link-styleci] 7 | ![TESTED OS](https://img.shields.io/badge/Tested%20OS-Linux-brightgreen.svg) 8 | [![Total Downloads][ico-downloads]][link-downloads] 9 | ![](https://komarev.com/ghpvc/?username=alkhachatryan-laravel-web-console&label=Repo+views&color=brightgreen&style=flat-square) 10 | 11 | 12 | Laravel Web Console is a package for Laravel applications that allow user to connect to the server via browser. 13 | 14 | ![Screenshot](screenshot.png) 15 | 16 | ## What is this package useful for? 17 | 18 | Despite the fact that cloud hosting is now growing up and many people use VPS / Dedicated Server hosting, most people still use Sharing hosting without SSH connection. Using this package you can execute shell commands from your browser. Using Laravel Middleware features you can protect your system from danger from outside. 19 | 20 | ## Features 21 | 22 | * Enable / Disable custom login 23 | * Multi-account support 24 | * Home dir selection 25 | * Home dir selection for multiple accounts 26 | * Custom password hashing 27 | 28 | ## Supported Laravel Versions 29 | * 5.7.* 30 | * 5.8.* 31 | * 6.* 32 | * 7.* 33 | * 8.* 34 | 35 | ## Installation 36 | 37 | Manually: 38 | 39 | - Download the last release: https://github.com/alkhachatryan/laravel-web-console/releases/latest 40 | - Upload the compressed file to the server. 41 | - Unzip the files into /vendor/alkhachatryan/laravel-web-console (Without version number) 42 | - Add maintance for this package into composer autoloaders 43 | -- In /vendor/composer/autoload_namespaces.php add in the array this line: 44 | ```php 45 | 'Alkhachatryan\\LaravelWebConsole\\' => array($vendorDir . '/alkhachatryan/laravel-web-console/src'), 46 | ``` 47 | -- In /vendor/composer/autoload_psr4.php add in the array this line: 48 | ```php 49 | 'Alkhachatryan\\LaravelWebConsole\\' => array($vendorDir . '/alkhachatryan/laravel-web-console/src'), 50 | ``` 51 | - Update the /config/app.php and add the service provider into providers array 52 | ```php 53 | Alkhachatryan\LaravelWebConsole\LaravelWebConsoleServiceProvider::class, 54 | ``` 55 | - Remove the cache: delete the following files: 56 | /bootstrap/cache/packages.php 57 | /bootstrap/cache/services.php 58 | 59 | Or Via Composer: 60 | 61 | ``` bash 62 | $ composer require alkhachatryan/laravel-web-console 63 | ``` 64 | 65 | ## Configuration 66 | 67 | Publish the config file 68 | 69 | - Copy /vendor/alkhachatryan/laravel-web-console/config file to your /config folder 70 | 71 | OR via command line: 72 | ```bash 73 | php artisan vendor:publish --tag=webconsole 74 | ``` 75 | 76 | - Edit the /config/laravelwebconsole.php file, create your credentials in .env file. 77 | 78 | ```php 79 | // Single-user credentials (REQUIRED) 80 | 'user' => [ 81 | 'name' => env('CONSOLE_USER_NAME', 'root'), 82 | 'password' => env('CONSOLE_USER_PASSWORD', 'root') 83 | ], 84 | ``` 85 | 86 | !!! ATTENTION !!!! 87 | These user credentials ARE NOT your server user credentials. 88 | You can type here everything you want. 89 | This method of custom login is a small addition in the protection. 90 | Anyway you can disable it. Set no_login value TRUE 91 | 92 | ```php 93 | // Disable login (don't ask for credentials, be careful) 94 | 'no_login' => true, 95 | ``` 96 | 97 | ## Usage 98 | ```php 99 | use Alkhachatryan\LaravelWebConsole\LaravelWebConsole; 100 | 101 | class HomeController extends Controller 102 | { 103 | public function index() { 104 | return LaravelWebConsole::show(); 105 | } 106 | } 107 | ``` 108 | 109 | ## Change log 110 | 111 | Please see the [changelog](changelog.md) for more information on what has changed recently. 112 | 113 | 114 | ## Security 115 | 116 | If you discover any security related issues, please email info@khachatryan.org instead of using the issue tracker. 117 | 118 | ## Credits 119 | 120 | - [Alexey Khachatryan][link-author] 121 | 122 | ## Open source tools included 123 | 124 | - jQuery JavaScript Library: https://github.com/jquery/jquery 125 | - jQuery Terminal Emulator: https://github.com/jcubic/jquery.terminal 126 | - jQuery Mouse Wheel Plugin: https://github.com/brandonaaron/jquery-mousewheel 127 | - PHP JSON-RPC 2.0 Server/Client Implementation: https://github.com/sergeyfast/eazy-jsonrpc 128 | - Normalize.css: https://github.com/necolas/normalize.css 129 | - Nickola/Web-console https://github.com/nickola/web-console 130 | 131 | ## License 132 | 133 | MIT. Please see the [license file](license) for more information. 134 | 135 | [ico-version]: https://img.shields.io/packagist/v/alkhachatryan/laravel-web-console.svg?style=flat-square 136 | [ico-styleci]: https://styleci.io/repos/161024221/shield 137 | [link-packagist]: https://packagist.org/packages/alkhachatryan/laravel-web-console 138 | [link-styleci]: https://github.styleci.io/repos/161024221 139 | [link-author]: https://github.com/alkhachatryan 140 | [link-contributors]: ../../contributors] 141 | [ico-downloads]: https://img.shields.io/packagist/dt/alkhachatryan/laravel-web-console.svg?style=flat-square 142 | [link-downloads]: https://packagist.org/packages/alkhachatryan/laravel-web-console 143 | -------------------------------------------------------------------------------- /resources/views/window.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Web Console 7 | 8 | 9 | 10 | 11 | 12 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /routes/routes.php: -------------------------------------------------------------------------------- 1 | name('laravel.webconsole.execute'); 5 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alkhachatryan/laravel-web-console/2dfc51d972f2f5320af4e410175afe9495e19fd3/screenshot.png -------------------------------------------------------------------------------- /src/BaseJsonRpcServer.php: -------------------------------------------------------------------------------- 1 | method 24 | */ 25 | protected $instances = []; 26 | 27 | /** 28 | * Decoded Json Request. 29 | * @var object|array 30 | */ 31 | protected $request; 32 | 33 | /** 34 | * Array of Received Calls. 35 | * @var array 36 | */ 37 | protected $calls = []; 38 | 39 | /** 40 | * Array of Responses for Calls. 41 | * @var array 42 | */ 43 | protected $response = []; 44 | 45 | /** 46 | * Has Calls Flag (not notifications). 47 | * @var bool 48 | */ 49 | protected $hasCalls = false; 50 | 51 | /** 52 | * Is Batch Call in using. 53 | * @var bool 54 | */ 55 | private $isBatchCall = false; 56 | 57 | /** 58 | * Hidden Methods. 59 | * @var array 60 | */ 61 | protected $hiddenMethods = [ 62 | 'execute', '__construct', 'registerinstance', 63 | ]; 64 | 65 | /** 66 | * Content Type. 67 | * @var string 68 | */ 69 | public $ContentType = 'application/json'; 70 | 71 | /** 72 | * Allow Cross-Domain Requests. 73 | * @var bool 74 | */ 75 | public $IsXDR = true; 76 | 77 | /** 78 | * Max Batch Calls. 79 | * @var int 80 | */ 81 | public $MaxBatchCalls = 10; 82 | 83 | /** 84 | * Error Messages. 85 | * @var array 86 | */ 87 | protected $errorMessages = [ 88 | self::ParseError => 'Parse error', 89 | self::InvalidRequest => 'Invalid Request', 90 | self::MethodNotFound => 'Method not found', 91 | self::InvalidParams => 'Invalid params', 92 | self::InternalError => 'Internal error', 93 | ]; 94 | 95 | /** 96 | * Cached Reflection Methods. 97 | * @var \ReflectionMethod[] 98 | */ 99 | private $reflectionMethods = []; 100 | 101 | /** 102 | * Validate Request. 103 | * @return int error 104 | */ 105 | private function getRequest() 106 | { 107 | $error = null; 108 | 109 | do { 110 | if (array_key_exists('REQUEST_METHOD', $_SERVER) && $_SERVER['REQUEST_METHOD'] != 'POST') { 111 | $error = self::InvalidRequest; 112 | break; 113 | } 114 | 115 | $request = ! empty($_GET['rawRequest']) ? $_GET['rawRequest'] : file_get_contents('php://input'); 116 | $this->request = json_decode($request, false); 117 | if ($this->request === null) { 118 | $error = self::ParseError; 119 | break; 120 | } 121 | 122 | if ($this->request === []) { 123 | $error = self::InvalidRequest; 124 | break; 125 | } 126 | 127 | // check for batch call 128 | if (is_array($this->request)) { 129 | if (count($this->request) > $this->MaxBatchCalls) { 130 | $error = self::InvalidRequest; 131 | break; 132 | } 133 | 134 | $this->calls = $this->request; 135 | $this->isBatchCall = true; 136 | } else { 137 | $this->calls[] = $this->request; 138 | } 139 | } while (false); 140 | 141 | return $error; 142 | } 143 | 144 | /** 145 | * Get Error Response. 146 | * @param int $code 147 | * @param mixed $id 148 | * @param null $data 149 | * @return array 150 | */ 151 | private function getError($code, $id = null, $data = null) 152 | { 153 | return [ 154 | 'jsonrpc' => '2.0', 155 | 'id' => $id, 156 | 'error' => [ 157 | 'code' => $code, 158 | 'message' => isset($this->errorMessages[$code]) ? $this->errorMessages[$code] : $this->errorMessages[self::InternalError], 159 | 'data' => $data, 160 | ], 161 | ]; 162 | } 163 | 164 | /** 165 | * Check for jsonrpc version and correct method. 166 | * @param object $call 167 | * @return array|null 168 | */ 169 | private function validateCall($call) 170 | { 171 | $result = null; 172 | $error = null; 173 | $data = null; 174 | $id = is_object($call) && property_exists($call, 'id') ? $call->id : null; 175 | do { 176 | if (! is_object($call)) { 177 | $error = self::InvalidRequest; 178 | break; 179 | } 180 | 181 | // hack for inputEx smd tester 182 | if (property_exists($call, 'version')) { 183 | if ($call->version == 'json-rpc-2.0') { 184 | $call->jsonrpc = '2.0'; 185 | } 186 | } 187 | 188 | if (! property_exists($call, 'jsonrpc') || $call->jsonrpc != '2.0') { 189 | $error = self::InvalidRequest; 190 | break; 191 | } 192 | 193 | $fullMethod = property_exists($call, 'method') ? $call->method : ''; 194 | $methodInfo = explode('.', $fullMethod, 2); 195 | $namespace = array_key_exists(1, $methodInfo) ? $methodInfo[0] : ''; 196 | $method = $namespace ? $methodInfo[1] : $fullMethod; 197 | if (! $method || ! array_key_exists($namespace, $this->instances) || ! method_exists($this->instances[$namespace], $method) || in_array(strtolower($method), $this->hiddenMethods)) { 198 | $error = self::MethodNotFound; 199 | break; 200 | } 201 | 202 | if (! array_key_exists($fullMethod, $this->reflectionMethods)) { 203 | $this->reflectionMethods[$fullMethod] = new \ReflectionMethod($this->instances[$namespace], $method); 204 | } 205 | 206 | /** @var $params array */ 207 | $params = property_exists($call, 'params') ? $call->params : null; 208 | $paramsType = gettype($params); 209 | if ($params !== null && $paramsType != 'array' && $paramsType != 'object') { 210 | $error = self::InvalidParams; 211 | break; 212 | } 213 | 214 | // check parameters 215 | switch ($paramsType) { 216 | case 'array': 217 | $totalRequired = 0; 218 | // doesn't hold required, null, required sequence of params 219 | foreach ($this->reflectionMethods[$fullMethod]->getParameters() as $param) { 220 | if (! $param->isDefaultValueAvailable()) { 221 | $totalRequired++; 222 | } 223 | } 224 | 225 | if (count($params) < $totalRequired) { 226 | $error = self::InvalidParams; 227 | $data = sprintf('Check numbers of required params (got %d, expected %d)', count($params), $totalRequired); 228 | } 229 | break; 230 | case 'object': 231 | foreach ($this->reflectionMethods[$fullMethod]->getParameters() as $param) { 232 | if (! $param->isDefaultValueAvailable() && ! array_key_exists($param->getName(), $params)) { 233 | $error = self::InvalidParams; 234 | $data = $param->getName().' not found'; 235 | 236 | break 3; 237 | } 238 | } 239 | break; 240 | case 'NULL': 241 | if ($this->reflectionMethods[$fullMethod]->getNumberOfRequiredParameters() > 0) { 242 | $error = self::InvalidParams; 243 | $data = 'Empty required params'; 244 | break 2; 245 | } 246 | break; 247 | } 248 | } while (false); 249 | 250 | if ($error) { 251 | $result = [$error, $id, $data]; 252 | } 253 | 254 | return $result; 255 | } 256 | 257 | /** 258 | * Process Call. 259 | * @param $call 260 | * @return array|null 261 | */ 262 | private function processCall($call) 263 | { 264 | $id = property_exists($call, 'id') ? $call->id : null; 265 | $params = property_exists($call, 'params') ? $call->params : []; 266 | $result = null; 267 | $namespace = substr($call->method, 0, strpos($call->method, '.')); 268 | 269 | try { 270 | // set named parameters 271 | if (is_object($params)) { 272 | $newParams = []; 273 | foreach ($this->reflectionMethods[$call->method]->getParameters() as $param) { 274 | $paramName = $param->getName(); 275 | $defaultValue = $param->isDefaultValueAvailable() ? $param->getDefaultValue() : null; 276 | $newParams[] = property_exists($params, $paramName) ? $params->$paramName : $defaultValue; 277 | } 278 | 279 | $params = $newParams; 280 | } 281 | 282 | // invoke 283 | $result = $this->reflectionMethods[$call->method]->invokeArgs($this->instances[$namespace], $params); 284 | } catch (\Exception $e) { 285 | return $this->getError($e->getCode(), $id, $e->getMessage()); 286 | } 287 | 288 | if (! $id && $id !== 0) { 289 | return; 290 | } 291 | 292 | return [ 293 | 'jsonrpc' => '2.0', 294 | 'result' => $result, 295 | 'id' => $id, 296 | ]; 297 | } 298 | 299 | /** 300 | * Create new Instance. 301 | * @param object $instance 302 | */ 303 | public function __construct($instance = null) 304 | { 305 | if (get_parent_class($this)) { 306 | $this->RegisterInstance($this, ''); 307 | } elseif ($instance) { 308 | $this->RegisterInstance($instance, ''); 309 | } 310 | } 311 | 312 | /** 313 | * Register Instance. 314 | * @param object $instance 315 | * @param string $namespace default is empty string 316 | * @return $this 317 | */ 318 | public function RegisterInstance($instance, $namespace = '') 319 | { 320 | $this->instances[$namespace] = $instance; 321 | $this->instances[$namespace]->errorMessages = $this->errorMessages; 322 | 323 | return $this; 324 | } 325 | 326 | /** 327 | * Handle Requests. 328 | */ 329 | public function Execute() 330 | { 331 | do { 332 | // check for SMD Discovery request 333 | if (array_key_exists('smd', $_GET)) { 334 | $this->response[] = $this->getServiceMap(); 335 | $this->hasCalls = true; 336 | break; 337 | } 338 | 339 | $error = $this->getRequest(); 340 | if ($error) { 341 | $this->response[] = $this->getError($error); 342 | $this->hasCalls = true; 343 | break; 344 | } 345 | 346 | foreach ($this->calls as $call) { 347 | $error = $this->validateCall($call); 348 | if ($error) { 349 | $this->response[] = $this->getError($error[0], $error[1], $error[2]); 350 | $this->hasCalls = true; 351 | } else { 352 | $result = $this->processCall($call); 353 | if ($result) { 354 | $this->response[] = $result; 355 | $this->hasCalls = true; 356 | } 357 | } 358 | } 359 | } while (false); 360 | 361 | // flush response 362 | if ($this->hasCalls) { 363 | if (! $this->isBatchCall) { 364 | $this->response = reset($this->response); 365 | } 366 | 367 | if (! headers_sent()) { 368 | // Set Content Type 369 | if ($this->ContentType) { 370 | header('Content-Type: '.$this->ContentType); 371 | } 372 | 373 | // Allow Cross Domain Requests 374 | if ($this->IsXDR) { 375 | header('Access-Control-Allow-Origin: *'); 376 | header('Access-Control-Allow-Headers: x-requested-with, content-type'); 377 | } 378 | } 379 | 380 | echo json_encode($this->response); 381 | $this->resetVars(); 382 | } 383 | } 384 | 385 | /** 386 | * Get Doc Comment. 387 | * @param $comment 388 | * @return string|null 389 | */ 390 | private function getDocDescription($comment) 391 | { 392 | $result = null; 393 | if (preg_match('/\*\s+([^@]*)\s+/s', $comment, $matches)) { 394 | $result = str_replace('*', "\n", trim(trim($matches[1], '*'))); 395 | } 396 | 397 | return $result; 398 | } 399 | 400 | /** 401 | * Get Service Map 402 | * Maybe not so good realization of auto-discover via doc blocks. 403 | * @return array 404 | */ 405 | private function getServiceMap() 406 | { 407 | $result = [ 408 | 'transport' => 'POST', 409 | 'envelope' => 'JSON-RPC-2.0', 410 | 'SMDVersion' => '2.0', 411 | 'contentType' => 'application/json', 412 | 'target' => ! empty($_SERVER['REQUEST_URI']) ? substr($_SERVER['REQUEST_URI'], 0, strpos($_SERVER['REQUEST_URI'], '?')) : '', 413 | 'services' => [], 414 | 'description' => '', 415 | ]; 416 | 417 | foreach ($this->instances as $namespace => $instance) { 418 | $rc = new \ReflectionClass($instance); 419 | 420 | // Get Class Description 421 | if ($rcDocComment = $this->getDocDescription($rc->getDocComment())) { 422 | $result['description'] .= $rcDocComment.PHP_EOL; 423 | } 424 | 425 | foreach ($rc->getMethods() as $method) { 426 | /** @var \ReflectionMethod $method */ 427 | if (! $method->isPublic() || in_array(strtolower($method->getName()), $this->hiddenMethods)) { 428 | continue; 429 | } 430 | 431 | $methodName = ($namespace ? $namespace.'.' : '').$method->getName(); 432 | $docComment = $method->getDocComment(); 433 | 434 | $result['services'][$methodName] = ['parameters' => []]; 435 | 436 | // set description 437 | if ($rmDocComment = $this->getDocDescription($docComment)) { 438 | $result['services'][$methodName]['description'] = $rmDocComment; 439 | } 440 | 441 | // @param\s+([^\s]*)\s+([^\s]*)\s*([^\s\*]*) 442 | $parsedParams = []; 443 | if (preg_match_all('/@param\s+([^\s]*)\s+([^\s]*)\s*([^\n\*]*)/', $docComment, $matches)) { 444 | foreach ($matches[2] as $number => $name) { 445 | $type = $matches[1][$number]; 446 | $desc = $matches[3][$number]; 447 | $name = trim($name, '$'); 448 | 449 | $param = ['type' => $type, 'description' => $desc]; 450 | $parsedParams[$name] = array_filter($param); 451 | } 452 | } 453 | 454 | // process params 455 | foreach ($method->getParameters() as $parameter) { 456 | $name = $parameter->getName(); 457 | $param = ['name' => $name, 'optional' => $parameter->isDefaultValueAvailable()]; 458 | if (array_key_exists($name, $parsedParams)) { 459 | $param += $parsedParams[$name]; 460 | } 461 | 462 | if ($param['optional']) { 463 | $param['default'] = $parameter->getDefaultValue(); 464 | } 465 | 466 | $result['services'][$methodName]['parameters'][] = $param; 467 | } 468 | 469 | // set return type 470 | if (preg_match('/@return\s+([^\s]+)\s*([^\n\*]+)/', $docComment, $matches)) { 471 | $returns = ['type' => $matches[1], 'description' => trim($matches[2])]; 472 | $result['services'][$methodName]['returns'] = array_filter($returns); 473 | } 474 | } 475 | } 476 | 477 | return $result; 478 | } 479 | 480 | /** 481 | * Reset Local Class Vars after Execute. 482 | */ 483 | private function resetVars() 484 | { 485 | $this->response = $this->calls = []; 486 | $this->hasCalls = $this->isBatchCall = false; 487 | } 488 | } 489 | -------------------------------------------------------------------------------- /src/LaravelWebConsole.php: -------------------------------------------------------------------------------- 1 | verifyRequest($request); 42 | 43 | if ($this->has_errors) { 44 | echo json_encode(['result' => ['output' => $this->output]]); 45 | exit(403); 46 | } 47 | 48 | $rpc_server = new WebConsoleRPCServer(); 49 | $rpc_server->Execute(); 50 | } 51 | 52 | /** 53 | * Verify the request. 54 | * 55 | * @param Request $request 56 | * @return void 57 | */ 58 | private function verifyRequest(Request $request) 59 | { 60 | $in = $request->input('params'); 61 | 62 | if (! isset($in[2])) { 63 | return; 64 | } 65 | 66 | $command = explode(' ', $in[2])[0]; 67 | 68 | $forbidden_commands = config('laravelwebconsole.forbidden_commands'); 69 | 70 | // Check if the command is in the forbidden commands list 71 | if ($forbidden_commands && in_array($command, $forbidden_commands)) { 72 | $this->has_errors = true; 73 | $this->output = "Denied to execute '$command' command"; 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/LaravelWebConsoleServiceProvider.php: -------------------------------------------------------------------------------- 1 | registerHelpers(); 17 | if ($this->app->runningInConsole()) { 18 | $this->bootForConsole(); 19 | } 20 | 21 | $this->loadViewsFrom(__DIR__.'/../resources/views', 'webconsole'); 22 | $this->loadRoutesFrom(__DIR__.'/../routes/routes.php'); 23 | 24 | $this->publishes([ 25 | __DIR__.'/../config/laravelwebconsole.php' => config_path('laravelwebconsole.php'), 26 | ], 'webconsole'); 27 | } 28 | 29 | /** 30 | * Register any package services. 31 | * 32 | * @return void 33 | */ 34 | public function register() 35 | { 36 | $this->mergeConfigFrom(__DIR__.'/../config/laravelwebconsole.php', 'laravelwebconsole'); 37 | 38 | $this->app->singleton('laravelwebconsole', function ($app) { 39 | return new LaravelWebConsole; 40 | }); 41 | } 42 | 43 | /** 44 | * Get the services provided by the provider. 45 | * 46 | * @return array 47 | */ 48 | public function provides() 49 | { 50 | return ['laravelwebconsole']; 51 | } 52 | 53 | /** 54 | * Console-specific booting. 55 | * 56 | * @return void 57 | */ 58 | protected function bootForConsole() 59 | { 60 | $this->publishes([ 61 | __DIR__.'/../config/laravelwebconsole.php' => config_path('laravelwebconsole.php'), 62 | ], 'laravelwebconsole.config'); 63 | } 64 | 65 | /** 66 | * Register helpers file. 67 | */ 68 | public function registerHelpers() 69 | { 70 | // Load the helpers in app/Http/helpers.php 71 | if (file_exists($file = __DIR__.'/../helpers.php')) { 72 | require_once $file; 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/WebConsoleRPCServer.php: -------------------------------------------------------------------------------- 1 | no_login = config('laravelwebconsole.no_login'); 48 | $this->home_dir_conf = config('laravelwebconsole.home_dir'); 49 | $this->accounts = config('laravelwebconsole.accounts'); 50 | $this->password_hash_algorithm = config('laravelwebconsole.password_hash_algorithm'); 51 | 52 | // Initializing 53 | 54 | if (config('laravelwebconsole.user.name') && config('laravelwebconsole.user.password')) { 55 | $this->accounts[config('laravelwebconsole.user.name')] = config('laravelwebconsole.user.password'); 56 | } 57 | 58 | $this->is_configured = ($this->no_login || count($this->accounts) >= 1) ? true : false; 59 | 60 | if (! $this->is_configured) { 61 | throw new \Exception('Webconsole not configured. Please see: /config/laravelwebconsole.php'); 62 | } 63 | } 64 | 65 | private function error($message) 66 | { 67 | throw new \Exception($message); 68 | } 69 | 70 | // Authentication 71 | private function authenticate_user($user, $password) 72 | { 73 | $user = trim((string) $user); 74 | $password = trim((string) $password); 75 | 76 | if ($user && $password) { 77 | if (isset($this->accounts[$user]) && ! is_empty_string($this->accounts[$user])) { 78 | if ($this->password_hash_algorithm) { 79 | $password = get_hash($this->password_hash_algorithm, $password); 80 | } 81 | 82 | if (is_equal_strings($password, $this->accounts[$user])) { 83 | return $user.':'.get_hash('sha256', $password); 84 | } 85 | } 86 | } 87 | 88 | throw new \Exception('Incorrect user or password'); 89 | } 90 | 91 | private function authenticate_token($token) 92 | { 93 | if ($this->no_login) { 94 | return true; 95 | } 96 | 97 | $token = trim((string) $token); 98 | $token_parts = explode(':', $token, 2); 99 | 100 | if (count($token_parts) == 2) { 101 | $user = trim((string) $token_parts[0]); 102 | $password_hash = trim((string) $token_parts[1]); 103 | 104 | if ($user && $password_hash) { 105 | if (isset($this->accounts[$user]) && ! is_empty_string($this->accounts[$user])) { 106 | $real_password_hash = get_hash('sha256', $this->accounts[$user]); 107 | if (is_equal_strings($password_hash, $real_password_hash)) { 108 | return $user; 109 | } 110 | } 111 | } 112 | } 113 | 114 | throw new \Exception('Incorrect user or password'); 115 | } 116 | 117 | private function get_home_directory($user) 118 | { 119 | if (is_string($this->home_dir_conf)) { 120 | if (! is_empty_string($this->home_dir_conf)) { 121 | return $this->home_dir_conf; 122 | } 123 | } elseif (is_string($user) && ! is_empty_string($user) && isset($this->home_dir_conf[$user]) && ! is_empty_string($this->home_dir_conf[$user])) { 124 | return $this->home_dir_conf[$user]; 125 | } 126 | 127 | return getcwd(); 128 | } 129 | 130 | // Environment 131 | private function get_environment() 132 | { 133 | $hostname = function_exists('gethostname') ? gethostname() : null; 134 | 135 | return ['path' => getcwd(), 'hostname' => $hostname]; 136 | } 137 | 138 | private function set_environment($environment) 139 | { 140 | $environment = ! empty($environment) ? (array) $environment : []; 141 | $path = (isset($environment['path']) && ! is_empty_string($environment['path'])) ? $environment['path'] : $this->home_directory; 142 | 143 | if (! is_empty_string($path)) { 144 | if (is_dir($path)) { 145 | if (! @chdir($path)) { 146 | return ['output' => 'Unable to change directory to current working directory, updating current directory', 147 | 'environment' => $this->get_environment(), ]; 148 | } 149 | } else { 150 | return ['output' => 'Current working directory not found, updating current directory', 151 | 'environment' => $this->get_environment(), ]; 152 | } 153 | } 154 | } 155 | 156 | // Initialization 157 | private function initialize($token, $environment) 158 | { 159 | $user = $this->authenticate_token($token); 160 | $this->home_directory = $this->get_home_directory($user); 161 | $result = $this->set_environment($environment); 162 | 163 | if ($result) { 164 | return $result; 165 | } 166 | } 167 | 168 | // Methods 169 | public function login($user, $password) 170 | { 171 | $result = ['token' => $this->authenticate_user($user, $password), 172 | 'environment' => $this->get_environment(), ]; 173 | 174 | $home_directory = $this->get_home_directory($user); 175 | if (! is_empty_string($home_directory)) { 176 | if (is_dir($home_directory)) { 177 | $result['environment']['path'] = $home_directory; 178 | } else { 179 | $result['output'] = 'Home directory not found: '.$home_directory; 180 | } 181 | } 182 | 183 | return $result; 184 | } 185 | 186 | public function cd($token, $environment, $path) 187 | { 188 | $result = $this->initialize($token, $environment); 189 | if ($result) { 190 | return $result; 191 | } 192 | 193 | $path = trim((string) $path); 194 | if (is_empty_string($path)) { 195 | $path = $this->home_directory; 196 | } 197 | 198 | if (! is_empty_string($path)) { 199 | if (is_dir($path)) { 200 | if (! @chdir($path)) { 201 | return ['output' => 'cd: '.$path.': Unable to change directory']; 202 | } 203 | } else { 204 | return ['output' => 'cd: '.$path.': No such directory']; 205 | } 206 | } 207 | 208 | return ['environment' => $this->get_environment()]; 209 | } 210 | 211 | public function completion($token, $environment, $pattern, $command) 212 | { 213 | $result = $this->initialize($token, $environment); 214 | if ($result) { 215 | return $result; 216 | } 217 | 218 | $scan_path = ''; 219 | $completion_prefix = ''; 220 | $completion = []; 221 | 222 | if (! empty($pattern)) { 223 | if (! is_dir($pattern)) { 224 | $pattern = dirname($pattern); 225 | if ($pattern == '.') { 226 | $pattern = ''; 227 | } 228 | } 229 | 230 | if (! empty($pattern)) { 231 | if (is_dir($pattern)) { 232 | $scan_path = $completion_prefix = $pattern; 233 | if (substr($completion_prefix, -1) != '/') { 234 | $completion_prefix .= '/'; 235 | } 236 | } 237 | } else { 238 | $scan_path = getcwd(); 239 | } 240 | } else { 241 | $scan_path = getcwd(); 242 | } 243 | 244 | if (! empty($scan_path)) { 245 | // Loading directory listing 246 | $completion = array_values(array_diff(scandir($scan_path), ['..', '.'])); 247 | natsort($completion); 248 | 249 | // Prefix 250 | if (! empty($completion_prefix) && ! empty($completion)) { 251 | foreach ($completion as &$value) { 252 | $value = $completion_prefix.$value; 253 | } 254 | } 255 | 256 | // Pattern 257 | if (! empty($pattern) && ! empty($completion)) { 258 | // For PHP version that does not support anonymous functions (available since PHP 5.3.0) 259 | function filter_pattern($value) 260 | { 261 | global $pattern; 262 | 263 | return ! strncmp($pattern, $value, strlen($pattern)); 264 | } 265 | 266 | $completion = array_values(array_filter($completion, 'filter_pattern')); 267 | } 268 | } 269 | 270 | return ['completion' => $completion]; 271 | } 272 | 273 | public function run($token, $environment, $command) 274 | { 275 | $result = $this->initialize($token, $environment); 276 | if ($result) { 277 | return $result; 278 | } 279 | 280 | $output = ($command && ! is_empty_string($command)) ? execute_command($command) : ''; 281 | if ($output && substr($output, -1) == "\n") { 282 | $output = substr($output, 0, -1); 283 | } 284 | 285 | return ['output' => $output]; 286 | } 287 | } 288 | --------------------------------------------------------------------------------