├── .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 | 
8 | [![Total Downloads][ico-downloads]][link-downloads]
9 | 
10 |
11 |
12 | Laravel Web Console is a package for Laravel applications that allow user to connect to the server via browser.
13 |
14 | 
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 |
--------------------------------------------------------------------------------