├── .gitignore ├── .github ├── PULL_REQUEST_TEMPLATE │ ├── bug_reported.md │ ├── bug_not_reported.md │ └── new_feature.md ├── PULL_REQUEST_TEMPLATE.md └── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── infection.json ├── tests ├── TestCase.php ├── NgrokWebServiceTest.php ├── Pest.php ├── NgrokProcessBuilderTest.php ├── NgrokCommandTest.php └── NgrokServiceProviderTest.php ├── phpcs.xml ├── phpstan.neon ├── php-cs-fixer.php ├── phpunit.xml ├── CHANGELOG-3.x.md ├── CHANGELOG-2.x.md ├── LICENSE.md ├── CHANGELOG-1.x.md ├── phpunit-coverage.xml ├── composer.json ├── src ├── NgrokProcessBuilder.php ├── NgrokWebService.php ├── NgrokServiceProvider.php └── NgrokCommand.php ├── README.md └── Makefile /.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | /vendor 3 | composer.lock 4 | composer.phar 5 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE/bug_reported.md: -------------------------------------------------------------------------------- 1 | 4 | 5 | **Issues** 6 | List the issues numbers that are being fixed: #ISSUE_NUMBER, #ISSUE_NUMBER, ... 7 | 8 | **Describe the fix** 9 | A clear and concise description of what the fix does. 10 | -------------------------------------------------------------------------------- /infection.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "vendor/infection/infection/resources/schema.json", 3 | "source": { 4 | "directories": [ 5 | "src" 6 | ] 7 | }, 8 | "timeout": 30, 9 | "logs": { 10 | "text": "build/infection/infection.log", 11 | "html": "build/infection/infection.html" 12 | }, 13 | "mutators": { 14 | "@default": true 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /tests/TestCase.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | src 4 | tests 5 | 6 | 7 | 8 | error 9 | 10 | 11 | ./tests/* 12 | 13 | */vendor/* 14 | 15 | -------------------------------------------------------------------------------- /phpstan.neon: -------------------------------------------------------------------------------- 1 | parameters: 2 | level: max 3 | paths: 4 | - src 5 | - tests 6 | ignoreErrors: 7 | - '#Call to an undefined method Pest\\PendingCalls\\TestCall\|Pest\\Support\\HigherOrderTapProxy::#' 8 | - '#Cannot access offset .* on mixed#' 9 | - '#Cannot call method .* on Illuminate\\Testing\\PendingCommand\|int#' 10 | - '#Cannot call method .* on mixed#' 11 | - '#Call to an undefined method Prophecy\\Prophecy\\ObjectProphecy#' 12 | -------------------------------------------------------------------------------- /php-cs-fixer.php: -------------------------------------------------------------------------------- 1 | in([ 4 | __DIR__ . '/src', 5 | __DIR__ . '/tests', 6 | ]); 7 | 8 | return (new PhpCsFixer\Config())->setRules([ 9 | 'no_unused_imports' => true, 10 | 'trailing_comma_in_multiline' => [ 11 | 'after_heredoc' => true, 12 | 'elements' => [ 13 | 'arguments', 14 | 'arrays', 15 | 'match', 16 | 'parameters', 17 | ], 18 | ], 19 | ])->setFinder($finder); 20 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | 5 | --- 6 | 7 | - Version: #.#.# 8 | - PHP Version: 9 | - Laravel Framework Version: 10 | - Ngrok Version: 11 | 12 | **Describe the bug** 13 | A clear and concise description of what the bug is. 14 | 15 | **To Reproduce** 16 | Steps to reproduce the behavior: 17 | 1. Go to '...' 18 | 2. Click on '....' 19 | 3. Scroll down to '....' 20 | 4. See error 21 | 22 | **Expected behavior** 23 | A clear and concise description of what you expected to happen. 24 | 25 | **Additional context** 26 | Add any other context or screenshots about the problem here. 27 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | 13 | tests 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | 5 | --- 6 | 7 | **Is your feature request related to a problem? Please describe.** 8 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 9 | 10 | **Describe the solution you'd like** 11 | A clear and concise description of what you want to happen. 12 | 13 | **Describe alternatives you've considered** 14 | A clear and concise description of any alternative solutions or features you've considered. 15 | 16 | **Additional context** 17 | Add any other context or screenshots about the feature request here. 18 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE/bug_not_reported.md: -------------------------------------------------------------------------------- 1 | 4 | 5 | - Version: #.#.# 6 | - PHP Version: 7 | - Laravel Framework Version: 8 | - Ngrok Version: 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 | **Additional context** 24 | Add any other context or screenshots about the problem here. 25 | 26 | **Describe the fix** 27 | A clear and concise description of what the fix does. 28 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE/new_feature.md: -------------------------------------------------------------------------------- 1 | 4 | 5 | **Is your new feature related to a problem? Please describe.** 6 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 7 | 8 | **Describe alternatives you've considered** 9 | A clear and concise description of any alternative solutions or features you've considered. 10 | 11 | **Describe the solution you did** 12 | A clear and concise description of what this new feature does. 13 | In addition, please describe the benefit to end users, the reasons it does not break any existing features, etc. 14 | 15 | **Additional context** 16 | Add any other context or screenshots about the new feature here. 17 | -------------------------------------------------------------------------------- /CHANGELOG-3.x.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [Unreleased](https://github.com/jn-jairo/laravel-ngrok/compare/v3.0.3...3.x) 4 | 5 | ## [v3.0.3 (2025-02-26)](https://github.com/jn-jairo/laravel-ngrok/compare/v3.0.2...v3.0.3) 6 | 7 | ### Added 8 | - Laravel 12 support 9 | 10 | ## [v3.0.2 (2024-03-14)](https://github.com/jn-jairo/laravel-ngrok/compare/v3.0.1...v3.0.2) 11 | 12 | ### Added 13 | - Laravel 11 support 14 | 15 | ## [v3.0.1 (2023-04-12)](https://github.com/jn-jairo/laravel-ngrok/compare/v3.0.0...v3.0.1) 16 | 17 | ### Added 18 | - New ngrok domains 19 | 20 | ## [v3.0.0 (2023-02-25)](https://github.com/jn-jairo/laravel-ngrok/compare/v2.0.4...v3.0.0) 21 | 22 | ### Added 23 | - Laravel 10 support 24 | - Static analysis 25 | - Mutation testing 26 | 27 | ### Changed 28 | - Minimal PHP version 8.1 29 | - Minimal Laravel version 8.83 30 | - Tests using Pest 31 | - Code style PSR-12 32 | -------------------------------------------------------------------------------- /CHANGELOG-2.x.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [Unreleased](https://github.com/jn-jairo/laravel-ngrok/compare/v2.0.4...2.x) 4 | 5 | ## [v2.0.4 (2023-01-13)](https://github.com/jn-jairo/laravel-ngrok/compare/v2.0.3...v2.0.4) 6 | 7 | ### Added 8 | - Ngrok 3 support 9 | 10 | ## [v2.0.3 (2022-03-03)](https://github.com/jn-jairo/laravel-ngrok/compare/v2.0.2...v2.0.3) 11 | 12 | ### Added 13 | - `--host` and `--extra` options 14 | - Allow ngrok url with region 15 | 16 | ## [v2.0.2 (2022-02-11)](https://github.com/jn-jairo/laravel-ngrok/compare/v2.0.1...v2.0.2) 17 | 18 | ### Added 19 | - Laravel 9 support 20 | - New ngrok url format 21 | 22 | ## [v2.0.1 (2021-09-01)](https://github.com/jn-jairo/laravel-ngrok/compare/v2.0.0...v2.0.1) 23 | 24 | ### Added 25 | - PHP 8 support 26 | 27 | ## [v2.0.0 (2020-09-09)](https://github.com/jn-jairo/laravel-ngrok/compare/v1.0.1...v2.0.0) 28 | 29 | ### Added 30 | - Laravel 8 support 31 | 32 | ### Changed 33 | - Minimal PHP version 7.3 34 | - Minimal Laravel version 6 35 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Jairo Correa 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 | -------------------------------------------------------------------------------- /CHANGELOG-1.x.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [Unreleased](https://github.com/jn-jairo/laravel-ngrok/compare/v1.0.1...1.x) 4 | 5 | ## [v1.0.1 (2020-08-13)](https://github.com/jn-jairo/laravel-ngrok/compare/v1.0.0...v1.0.1) 6 | 7 | ### Added 8 | - Travis PHP 7.4 9 | - `guzzlehttp/guzzle` version `^7.0` 10 | 11 | ## [v1.0.0 (2020-03-08)](https://github.com/jn-jairo/laravel-ngrok/compare/v0.0.4...v1.0.0) 12 | 13 | ### Added 14 | - Laravel 7 support 15 | 16 | ## [v0.0.4 (2019-09-14)](https://github.com/jn-jairo/laravel-ngrok/compare/v0.0.3...v0.0.4) 17 | 18 | ### Added 19 | - Laravel 6 support 20 | - Test using [orchestral/testbench](https://github.com/orchestral/testbench) 21 | 22 | ### Changed 23 | - Minimal PHP version 7.2 24 | - Minimal Laravel version 5.8 25 | 26 | ## [v0.0.3 (2019-03-02)](https://github.com/jn-jairo/laravel-ngrok/compare/v0.0.2...v0.0.3) 27 | 28 | ### Added 29 | - Laravel 5.8 support 30 | 31 | ## [v0.0.2 (2018-11-03)](https://github.com/jn-jairo/laravel-ngrok/compare/v0.0.1...v0.0.2) 32 | 33 | ### Fixed 34 | - Pagination url 35 | 36 | ## [v0.0.1 (2018-10-14)](https://github.com/jn-jairo/laravel-ngrok/commit/f5ffe623bd7c075c3c9eb58655e3841192f9b7d4) 37 | - Ngrok command 38 | -------------------------------------------------------------------------------- /phpunit-coverage.xml: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | 13 | tests 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | ./src 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jn-jairo/laravel-ngrok", 3 | "description": "Share Laravel application with ngrok.", 4 | "keywords": [ 5 | "jn-jairo", 6 | "laravel-ngrok" 7 | ], 8 | "homepage": "https://github.com/jn-jairo/laravel-ngrok", 9 | "license": "MIT", 10 | "authors": [ 11 | { 12 | "name": "Jairo Correa", 13 | "email": "jn.j41r0@gmail.com" 14 | } 15 | ], 16 | "require": { 17 | "php": "^8.1", 18 | "guzzlehttp/guzzle": "^6.5.8|^7.4.5", 19 | "illuminate/console": "^8.83|^9.0|^10.0|^11.0|^12.0", 20 | "illuminate/http": "^8.83|^9.0|^10.0|^11.0|^12.0", 21 | "illuminate/pagination": "^8.83|^9.0|^10.0|^11.0|^12.0", 22 | "illuminate/routing": "^8.83|^9.0|^10.0|^11.0|^12.0", 23 | "illuminate/support": "^8.83|^9.0|^10.0|^11.0|^12.0", 24 | "symfony/process": "^5.4|^6.0|^7.0" 25 | }, 26 | "require-dev": { 27 | "orchestra/testbench": "^6.24|^7.0|^8.0|^9.0|^10.0", 28 | "pestphp/pest": "^1.22|^2.34|^3.7.4", 29 | "phpspec/prophecy": "^1.17", 30 | "phpspec/prophecy-phpunit": "^2.0", 31 | "webmozart/assert": "^1.11" 32 | }, 33 | "autoload": { 34 | "psr-4": { 35 | "JnJairo\\Laravel\\Ngrok\\": "src" 36 | } 37 | }, 38 | "autoload-dev": { 39 | "psr-4": { 40 | "JnJairo\\Laravel\\Ngrok\\Tests\\": "tests" 41 | } 42 | }, 43 | "config": { 44 | "sort-packages": true, 45 | "allow-plugins": { 46 | "pestphp/pest-plugin": true 47 | } 48 | }, 49 | "extra": { 50 | "laravel": { 51 | "providers": [ 52 | "JnJairo\\Laravel\\Ngrok\\NgrokServiceProvider" 53 | ] 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/NgrokProcessBuilder.php: -------------------------------------------------------------------------------- 1 | setWorkingDirectory($cwd); 22 | } 23 | 24 | /** 25 | * Set the current working directory. 26 | * 27 | * @param string $cwd 28 | */ 29 | public function setWorkingDirectory(string $cwd = null): void 30 | { 31 | $this->cwd = $cwd; 32 | } 33 | 34 | /** 35 | * Get the current working directory. 36 | * 37 | * @return string 38 | */ 39 | public function getWorkingDirectory(): ?string 40 | { 41 | return $this->cwd; 42 | } 43 | 44 | /** 45 | * Build ngrok command. 46 | * 47 | * @param string $hostHeader 48 | * @param string $port 49 | * @param string $host 50 | * @param array $extra 51 | * @return \Symfony\Component\Process\Process 52 | */ 53 | public function buildProcess( 54 | string $hostHeader = '', 55 | string $port = '80', 56 | string $host = '', 57 | array $extra = [], 58 | ): Process { 59 | $command = ['ngrok', 'http', '--log', 'stdout']; 60 | 61 | $command = array_merge($command, $extra); 62 | 63 | if ($hostHeader !== '') { 64 | $command[] = '--host-header'; 65 | $command[] = $hostHeader; 66 | } 67 | 68 | if ($host !== '') { 69 | $command[] = $host . ':' . ($port ?: '80'); 70 | } else { 71 | $command[] = $port ?: '80'; 72 | } 73 | 74 | return new Process($command, $this->getWorkingDirectory(), null, null, null); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /tests/NgrokWebServiceTest.php: -------------------------------------------------------------------------------- 1 | 'http://0000-0000.ngrok-free.app', 14 | 'config' => ['addr' => 'localhost:80'], 15 | ], 16 | [ 17 | 'public_url' => 'https://0000-0000.ngrok-free.app', 18 | 'config' => ['addr' => 'localhost:80'], 19 | ], 20 | ]; 21 | 22 | $json = json_encode(['tunnels' => $tunnels]); 23 | $emptyJson = json_encode(['tunnels' => []]); 24 | 25 | $dataset = [ 26 | 'valid' => [ 27 | $json, 28 | $tunnels, 29 | ], 30 | 'empty' => [ 31 | $emptyJson, 32 | [], 33 | ], 34 | 'invalid' => [ 35 | '', 36 | [], 37 | ], 38 | ]; 39 | 40 | it('can get tunnels', function ( 41 | string $json, 42 | array $tunnels, 43 | ) { 44 | /** 45 | * @var \Prophecy\Prophecy\ObjectProphecy<\Psr\Http\Message\StreamInterface> $stream 46 | */ 47 | $stream = prophesize(StreamInterface::class); 48 | $stream->__toString()->willReturn($json)->shouldBeCalled(); 49 | 50 | /** 51 | * @var \Prophecy\Prophecy\ObjectProphecy<\GuzzleHttp\Psr7\Response> $response 52 | */ 53 | $response = prophesize(Response::class); 54 | $response->getBody()->willReturn($stream->reveal())->shouldBeCalled(); 55 | 56 | /** 57 | * @var \Prophecy\Prophecy\ObjectProphecy<\GuzzleHttp\Client> $httpClient 58 | */ 59 | $httpClient = prophesize(Client::class); 60 | $httpClient->request( 61 | 'GET', 62 | 'http://127.0.0.1:4040/api/tunnels', 63 | )->willReturn( 64 | $response->reveal(), 65 | )->shouldBeCalled(); 66 | 67 | $webService = new NgrokWebService($httpClient->reveal()); 68 | expect($webService->getTunnels()) 69 | ->toBe($tunnels); 70 | })->with($dataset); 71 | -------------------------------------------------------------------------------- /src/NgrokWebService.php: -------------------------------------------------------------------------------- 1 | setHttpClient($httpClient); 30 | $this->setUrl($url); 31 | } 32 | 33 | /** 34 | * Set the web service url. 35 | * 36 | * @param string $url 37 | */ 38 | public function setUrl(string $url): void 39 | { 40 | $this->url = $url; 41 | } 42 | 43 | /** 44 | * Get the web service url. 45 | * 46 | * @return string 47 | */ 48 | public function getUrl(): string 49 | { 50 | return $this->url; 51 | } 52 | 53 | /** 54 | * Set the http client. 55 | * 56 | * @param \GuzzleHttp\Client $httpClient 57 | */ 58 | public function setHttpClient(Client $httpClient): void 59 | { 60 | $this->httpClient = $httpClient; 61 | } 62 | 63 | /** 64 | * Get the http client. 65 | * 66 | * @return \GuzzleHttp\Client 67 | */ 68 | public function getHttpClient(): Client 69 | { 70 | return $this->httpClient; 71 | } 72 | 73 | /** 74 | * Request the tunnels. 75 | * 76 | * @return array> 77 | */ 78 | public function getTunnels(): array 79 | { 80 | $tunnels = []; 81 | 82 | $response = json_decode( 83 | $this->getHttpClient()->request( 84 | 'GET', 85 | $this->getUrl() . '/api/tunnels', 86 | )->getBody(), 87 | true, 88 | ); 89 | 90 | if ($response !== false && isset($response['tunnels']) && ! empty($response['tunnels'])) { 91 | /** 92 | * @var array> $tunnels 93 | */ 94 | $tunnels = $response['tunnels']; 95 | } 96 | 97 | return $tunnels; 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /tests/Pest.php: -------------------------------------------------------------------------------- 1 | in(__DIR__); 18 | 19 | /* 20 | |-------------------------------------------------------------------------- 21 | | Expectations 22 | |-------------------------------------------------------------------------- 23 | | 24 | | When you're writing tests, you often need to check that values meet certain conditions. The 25 | | "expect()" function gives you access to a set of "expectations" methods that you can use 26 | | to assert different things. Of course, you may extend the Expectation API at any time. 27 | | 28 | */ 29 | 30 | // expect()->extend('toBeOne', function () { 31 | // return $this->toBe(1); 32 | // }); 33 | 34 | /* 35 | |-------------------------------------------------------------------------- 36 | | Functions 37 | |-------------------------------------------------------------------------- 38 | | 39 | | While Pest is very powerful out-of-the-box, you may have some testing code specific to your 40 | | project that you don't want to repeat in every file. Here you can also expose helpers as 41 | | global functions to help you to reduce the number of lines of code in your test files. 42 | | 43 | */ 44 | 45 | /** 46 | * @param mixed ...$args 47 | * @return \Prophecy\Prophecy\ObjectProphecy 48 | */ 49 | function prophesize(...$args): ObjectProphecy 50 | { 51 | return test()->prophesize(...$args); 52 | } 53 | 54 | /** 55 | * Register an instance of an object in the container. 56 | * 57 | * @param string $abstract 58 | * @param object $instance 59 | * @return object 60 | */ 61 | function instance($abstract, $instance) 62 | { 63 | return app()->instance($abstract, $instance); 64 | } 65 | 66 | /** 67 | * Call artisan command and return code. 68 | * 69 | * @param string $command 70 | * @param array $parameters 71 | * @return \Illuminate\Testing\PendingCommand|int 72 | */ 73 | function artisan($command, $parameters = []) 74 | { 75 | return test()->artisan($command, $parameters); 76 | } 77 | -------------------------------------------------------------------------------- /tests/NgrokProcessBuilderTest.php: -------------------------------------------------------------------------------- 1 | [ 8 | [], 9 | '\'ngrok\' \'http\' \'--log\' \'stdout\' \'80\'', 10 | ], 11 | 'host_header' => [ 12 | ['example.com'], 13 | '\'ngrok\' \'http\' \'--log\' \'stdout\' \'--host-header\' \'example.com\' \'80\'', 14 | ], 15 | 'host_header_port' => [ 16 | ['example.com', '8000'], 17 | '\'ngrok\' \'http\' \'--log\' \'stdout\' \'--host-header\' \'example.com\' \'8000\'', 18 | ], 19 | 'host_header_port_host' => [ 20 | ['example.com', '8000', 'nginx'], 21 | '\'ngrok\' \'http\' \'--log\' \'stdout\' \'--host-header\' \'example.com\' \'nginx:8000\'', 22 | ], 23 | 'host_header_empty_port' => [ 24 | ['example.com', ''], 25 | '\'ngrok\' \'http\' \'--log\' \'stdout\' \'--host-header\' \'example.com\' \'80\'', 26 | ], 27 | 'host_header_empty_port_empty_host' => [ 28 | ['example.com', '', ''], 29 | '\'ngrok\' \'http\' \'--log\' \'stdout\' \'--host-header\' \'example.com\' \'80\'', 30 | ], 31 | 'host_header_empty_port_host' => [ 32 | ['example.com', '', 'nginx'], 33 | '\'ngrok\' \'http\' \'--log\' \'stdout\' \'--host-header\' \'example.com\' \'nginx:80\'', 34 | ], 35 | 'empty_host_header_emtpy_port_host' => [ 36 | ['', '', 'nginx'], 37 | '\'ngrok\' \'http\' \'--log\' \'stdout\' \'nginx:80\'', 38 | ], 39 | 'empty_host_header' => [ 40 | [''], 41 | '\'ngrok\' \'http\' \'--log\' \'stdout\' \'80\'', 42 | ], 43 | 'empty_host_header_emtpy_port' => [ 44 | ['', ''], 45 | '\'ngrok\' \'http\' \'--log\' \'stdout\' \'80\'', 46 | ], 47 | 'empty_host_header_emtpy_port_empty_host' => [ 48 | ['', '', ''], 49 | '\'ngrok\' \'http\' \'--log\' \'stdout\' \'80\'', 50 | ], 51 | 'extra_single' => [ 52 | ['example.com', '', '', ['--region=eu']], 53 | '\'ngrok\' \'http\' \'--log\' \'stdout\' \'--region=eu\' \'--host-header\' \'example.com\' \'80\'', 54 | ], 55 | 'extra_multiple' => [ 56 | ['example.com', '', '', ['--region=eu', '--config=../ngrok.yml']], 57 | '\'ngrok\' \'http\' \'--log\' \'stdout\' \'--region=eu\'' 58 | . ' \'--config=../ngrok.yml\' \'--host-header\' \'example.com\' \'80\'', 59 | ], 60 | ]; 61 | 62 | it('can build process', function ( 63 | array $args, 64 | string $command, 65 | ) { 66 | $processBuilder = new NgrokProcessBuilder(__DIR__); 67 | $process = $processBuilder->buildProcess(...$args); 68 | 69 | expect($process) 70 | ->toBeInstanceOf(Process::class); 71 | 72 | expect($process->getCommandLine()) 73 | ->toBe($command); 74 | expect($process->getWorkingDirectory()) 75 | ->toBe(__DIR__); 76 | expect($process->getTimeout()) 77 | ->toBeNull(); 78 | })->with($dataset); 79 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Total Downloads](https://poser.pugx.org/jn-jairo/laravel-ngrok/downloads)](https://packagist.org/packages/jn-jairo/laravel-ngrok) 2 | [![Latest Stable Version](https://poser.pugx.org/jn-jairo/laravel-ngrok/v/stable)](https://packagist.org/packages/jn-jairo/laravel-ngrok) 3 | [![License](https://poser.pugx.org/jn-jairo/laravel-ngrok/license)](https://packagist.org/packages/jn-jairo/laravel-ngrok) 4 | 5 | # Share Laravel application with ngrok 6 | 7 | This package allows you to share your Laravel application with [ngrok](https://ngrok.com). 8 | 9 | ## Requirements 10 | 11 | - Ngrok >= 2.2.8 (If you are using [Laravel Homestead](https://laravel.com/docs/homestead) this should be already installed) 12 | 13 | ## Version Compatibility 14 | 15 | Laravel | Laravel Ngrok 16 | :---------|:---------- 17 | 5.8.x | 1.x 18 | 6.x | 1.x 19 | 7.x | 1.x 20 | 8.x | 2.x 21 | 9.x | 2.x 22 | 10.x | 3.x 23 | 11.x | 3.x 24 | 12.x | 3.x 25 | 26 | ## Installation 27 | 28 | You can install the package via composer: 29 | 30 | ```bash 31 | composer require --dev jn-jairo/laravel-ngrok 32 | ``` 33 | 34 | The `NgrokServiceProvider` will be automatically registered for you. 35 | 36 | ## Usage 37 | 38 | Just call the artisan command to start the ngrok. 39 | 40 | ```bash 41 | php artisan ngrok 42 | ``` 43 | 44 | The parameters for ngrok will be extracted from your application. 45 | 46 | ## Advanced usage 47 | 48 | ```bash 49 | php artisan ngrok [options] [--] [] 50 | ``` 51 | 52 | Argument | Description 53 | :----------------|:------------------------------------------------------ 54 | **host-header** | Host header to identify the app (Example: myapp.test) 55 | 56 | Option | Description 57 | :------------------------|:----------------------------------------------------------- 58 | **-H, --host[=HOST]** | Host to tunnel the requests (default: localhost) 59 | **-P, --port[=PORT]** | Port to tunnel the requests (default: 80) 60 | **-E, --extra[=EXTRA]** | Extra arguments to ngrok command (multiple values allowed) 61 | 62 | 63 | ## Examples 64 | 65 | ```bash 66 | # If you have multiples apps (myapp.test, my-other-app.test, ...) 67 | # set it in the app.url configuration 68 | # or pass it in the host-header argument 69 | 70 | php artisan ngrok myapp.test 71 | 72 | # If you use a different port, set it in the app.url configuration 73 | # or pass it in the --port option 74 | 75 | php artisan ngrok --port=8000 myapp.test 76 | 77 | # If you use docker and have containers like (nginx, php, workspace) 78 | # and wanna run the command inside the workspace container 79 | # pass the name of the container the requests will tunnel through 80 | 81 | php artisan ngrok --host=nginx example.com 82 | 83 | # If you wanna pass other arguments directly to ngrok 84 | # use the --extra or -E option 85 | 86 | php artisan ngrok --extra='--region=eu' -E'--config=ngrok.yml' 87 | 88 | ``` 89 | 90 | ## License 91 | 92 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information. 93 | -------------------------------------------------------------------------------- /src/NgrokServiceProvider.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | public const NGROK_DOMAINS = [ 18 | 'ngrok.io', 19 | 'ngrok-free.app', 20 | 'ngrok-free.dev', 21 | 'ngrok.app', 22 | 'ngrok.dev', 23 | ]; 24 | 25 | /** 26 | * Bootstrap the application services. 27 | * 28 | * @return void 29 | */ 30 | public function boot(): void 31 | { 32 | if (! $this->app->runningInConsole()) { 33 | /** 34 | * @var \Illuminate\Routing\UrlGenerator $urlGenerator 35 | */ 36 | $urlGenerator = $this->app->make('url'); 37 | 38 | /** 39 | * @var \Illuminate\Http\Request $request 40 | */ 41 | $request = $this->app->make('request'); 42 | 43 | $this->forceNgrokSchemeHost($urlGenerator, $request); 44 | } 45 | } 46 | 47 | /** 48 | * Register the application services. 49 | * 50 | * @return void 51 | */ 52 | public function register(): void 53 | { 54 | $this->app->bind(NgrokProcessBuilder::class, function ($app) { 55 | return new NgrokProcessBuilder($app->basePath()); 56 | }); 57 | 58 | $this->app->bind(NgrokWebService::class, function () { 59 | return new NgrokWebService(new \GuzzleHttp\Client()); 60 | }); 61 | 62 | $this->commands([ 63 | NgrokCommand::class, 64 | ]); 65 | } 66 | 67 | /** 68 | * Force the url generator to the ngrok scheme://host. 69 | * 70 | * @param \Illuminate\Routing\UrlGenerator $urlGenerator 71 | * @param \Illuminate\Http\Request $request 72 | * @return void 73 | */ 74 | private function forceNgrokSchemeHost(UrlGenerator $urlGenerator, Request $request): void 75 | { 76 | $host = $this->extractOriginalHost($request); 77 | 78 | if ($this->isNgrokHost($host)) { 79 | $scheme = $this->extractOriginalScheme($request); 80 | 81 | $urlGenerator->forceScheme($scheme); 82 | $urlGenerator->forceRootUrl($scheme . '://' . $host); 83 | 84 | Paginator::currentPathResolver(function () use ($urlGenerator, $request) { 85 | return $urlGenerator->to($request->path()); 86 | }); 87 | } 88 | } 89 | 90 | /** 91 | * Extract the original scheme from the request. 92 | * 93 | * @param \Illuminate\Http\Request $request 94 | * @return string 95 | */ 96 | private function extractOriginalScheme(Request $request): string 97 | { 98 | if ($request->hasHeader('x-forwarded-proto') && is_string($request->header('x-forwarded-proto'))) { 99 | $scheme = $request->header('x-forwarded-proto'); 100 | } else { 101 | $scheme = $request->getScheme(); 102 | } 103 | 104 | return $scheme; 105 | } 106 | 107 | /** 108 | * Extract the original host from the request. 109 | * 110 | * @param \Illuminate\Http\Request $request 111 | * @return string 112 | */ 113 | private function extractOriginalHost(Request $request): string 114 | { 115 | if ($request->hasHeader('x-original-host') && is_string($request->header('x-original-host'))) { 116 | $host = $request->header('x-original-host'); 117 | } elseif ($request->hasHeader('x-forwarded-host') && is_string($request->header('x-forwarded-host'))) { 118 | $host = $request->header('x-forwarded-host'); 119 | } else { 120 | $host = $request->getHost(); 121 | } 122 | 123 | return $host; 124 | } 125 | 126 | /** 127 | * Check if the host from ngrok. 128 | * 129 | * @param string $host 130 | * @return bool 131 | */ 132 | private function isNgrokHost(string $host): bool 133 | { 134 | $domains = collect(self::NGROK_DOMAINS) 135 | ->map(fn($domain) => preg_quote($domain, '/')) 136 | ->join('|'); 137 | 138 | $regex = '/^[\.\-a-z0-9]+\.(?:' . $domains . ')$/i'; 139 | 140 | return (bool) preg_match($regex, $host); 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /src/NgrokCommand.php: -------------------------------------------------------------------------------- 1 | processBuilder = $processBuilder; 51 | $this->webService = $webService; 52 | } 53 | 54 | /** 55 | * Execute the console command. 56 | * 57 | * @return int 58 | */ 59 | public function handle(): int 60 | { 61 | /** 62 | * @var string|null $hostHeader 63 | */ 64 | $hostHeader = $this->argument('host-header'); 65 | /** 66 | * @var string|null $host 67 | */ 68 | $host = $this->option('host'); 69 | /** 70 | * @var string|null $port 71 | */ 72 | $port = $this->option('port'); 73 | /** 74 | * @var array $extra 75 | */ 76 | $extra = $this->option('extra'); 77 | 78 | if ($hostHeader === null) { 79 | /** 80 | * @var \Illuminate\Config\Repository $config 81 | */ 82 | $config = $this->getLaravel()->make('config'); 83 | $url = is_string($config->get('app.url')) ? $config->get('app.url') : ''; 84 | 85 | $urlParsed = parse_url($url); 86 | 87 | if ($urlParsed !== false) { 88 | if (isset($urlParsed['host'])) { 89 | $hostHeader = $urlParsed['host']; 90 | } 91 | 92 | if (isset($urlParsed['port']) && $port === null) { 93 | $port = (string) $urlParsed['port']; 94 | } 95 | } 96 | } 97 | 98 | if (empty($hostHeader)) { 99 | $this->error('Invalid host header'); 100 | return 1; 101 | } 102 | 103 | $host = $host ?: 'localhost'; 104 | $port = $port ?: '80'; 105 | 106 | $this->line('-----------------'); 107 | $this->line('| NGROK |'); 108 | $this->line('-----------------'); 109 | 110 | $this->line(''); 111 | 112 | $this->line('Host header: ' . $hostHeader); 113 | $this->line('Host: ' . $host); 114 | $this->line('Port: ' . $port); 115 | 116 | if (! empty($extra)) { 117 | $this->line('Extra: ' . implode(' ', $extra)); 118 | } 119 | 120 | $this->line(''); 121 | 122 | $process = $this->processBuilder->buildProcess($hostHeader, $port, $host, $extra); 123 | 124 | return $this->runProcess($process); 125 | } 126 | 127 | /** 128 | * Run the process. 129 | * 130 | * @param \Symfony\Component\Process\Process $process 131 | * @return int Exit code. 132 | */ 133 | private function runProcess(Process $process): int 134 | { 135 | $webService = $this->webService; 136 | 137 | $webServiceStarted = false; 138 | $tunnelStarted = false; 139 | 140 | $process->run(function ($type, $data) use (&$process, &$webService, &$webServiceStarted, &$tunnelStarted) { 141 | if (! $webServiceStarted) { 142 | if (preg_match('/msg="starting web service".*? addr=(?\S+)/', $process->getOutput(), $matches)) { 143 | $webServiceStarted = true; 144 | 145 | $webServiceUrl = 'http://' . $matches['addr']; 146 | 147 | $webService->setUrl($webServiceUrl); 148 | 149 | $this->line('Web Interface: ' . $webServiceUrl . "\n"); 150 | } 151 | } 152 | 153 | if ($webServiceStarted && ! $tunnelStarted) { 154 | $tunnels = $webService->getTunnels(); 155 | 156 | if (! empty($tunnels)) { 157 | $tunnelStarted = true; 158 | 159 | foreach ($tunnels as $tunnel) { 160 | $this->line('Forwarding: ' 161 | . $tunnel['public_url'] . ' -> ' . $tunnel['config']['addr']); 162 | } 163 | } 164 | } 165 | 166 | if (Process::OUT === $type) { 167 | $process->clearOutput(); 168 | } else { 169 | $this->error($data); 170 | $process->clearErrorOutput(); 171 | } 172 | }); 173 | 174 | $this->error($process->getErrorOutput()); 175 | 176 | return (int) $process->getExitCode(); 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # php_version|laravel_version|orchestra_version 2 | STABLE_DEPS += '8.1|8.83|6.24' 3 | STABLE_DEPS += '8.1|9.0|7.0' 4 | STABLE_DEPS += '8.1|10.0|8.0' 5 | STABLE_DEPS += '8.2|9|7.0' 6 | STABLE_DEPS += '8.2|10.0|8.0' 7 | STABLE_DEPS += '8.2|11.0|9.0' 8 | STABLE_DEPS += '8.2|12.0|10.0' 9 | STABLE_DEPS += '8.3|10.0|8.0' 10 | STABLE_DEPS += '8.3|11.0|9.0' 11 | STABLE_DEPS += '8.3|12.0|10.0' 12 | STABLE_DEPS += '8.4|11.0|9.0' 13 | STABLE_DEPS += '8.4|12.0|10.0' 14 | 15 | # php_version|laravel_version|orchestra_version 16 | LOWEST_DEPS += '8.1|8.83|6.24' 17 | LOWEST_DEPS += '8.1|9.0|7.0' 18 | LOWEST_DEPS += '8.1|10.0|8.0' 19 | LOWEST_DEPS += '8.2|9|7.0' 20 | LOWEST_DEPS += '8.2|10.0|8.0' 21 | LOWEST_DEPS += '8.2|11.0|9.0' 22 | LOWEST_DEPS += '8.2|12.0|10.0' 23 | LOWEST_DEPS += '8.3|10.0|8.0' 24 | LOWEST_DEPS += '8.3|11.0|9.0' 25 | LOWEST_DEPS += '8.3|12.0|10.0' 26 | LOWEST_DEPS += '8.4|11.0|9.0' 27 | LOWEST_DEPS += '8.4|12.0|10.0' 28 | 29 | define show_title 30 | title=$1 ; \ 31 | title="* $$title *" ; \ 32 | n=$${#title} ; \ 33 | echo "" ; \ 34 | printf '%'$${n}'s' | tr ' ' '*' ; \ 35 | echo "\n$$title" ; \ 36 | printf '%'$${n}'s' | tr ' ' '*' ; \ 37 | echo "\n" ; 38 | endef 39 | 40 | define composer_update 41 | $(call show_title,'COMPOSER UPDATE') \ 42 | composer update --prefer-dist --no-interaction --prefer-stable 43 | endef 44 | 45 | define clean_cache 46 | (rm -r .phpunit.cache > /dev/null 2>&1 || true) ; 47 | endef 48 | 49 | define test_version 50 | versions="$1" ; \ 51 | composer_args=$2 ; \ 52 | php_version=$$(echo $${versions} | cut -d'|' -f 1); \ 53 | laravel_version=$$(echo $${versions} | cut -d'|' -f 2); \ 54 | orchestra_version=$$(echo $${versions} | cut -d'|' -f 3); \ 55 | asdf_php_version=$$(asdf list php | sed 's/[^0-9\.]//g' | grep "^$${php_version}" | tail -n 1) ; \ 56 | if [ -n "$${asdf_php_version}" ] ; \ 57 | then \ 58 | $$(asdf set php $${asdf_php_version}) ; \ 59 | $$(asdf reshim php) ; \ 60 | $(call clean_cache) \ 61 | $(call show_title,'PHP: '$${php_version}' LARAVEL: '$${laravel_version}' ORCHESTRA: '$${orchestra_version}) \ 62 | echo -n 'Updating dependencies... ' ; \ 63 | output_composer=$$(composer update --prefer-dist --no-interaction $${composer_args} --prefer-stable --with=laravel/framework:^$${laravel_version} --with=orchestra/testbench:^$${orchestra_version} --with=orchestra/testbench-core:^$${orchestra_version} 2>&1) ; \ 64 | if [ $$? -ne 0 ] ; \ 65 | then \ 66 | echo 'ERROR' ; \ 67 | echo "$${output_composer}" ; \ 68 | continue ; \ 69 | fi; \ 70 | echo 'OK' ; \ 71 | echo -n 'Testing... ' ; \ 72 | output_php=$$(php vendor/bin/pest \ 73 | --do-not-cache-result 2>&1) ; \ 74 | if [ $$? -ne 0 ] ; \ 75 | then \ 76 | echo 'ERROR' ; \ 77 | echo "$${output_php}" ; \ 78 | continue ; \ 79 | fi; \ 80 | echo 'OK' ; \ 81 | asdf_php_version=$$(asdf list php | sed 's/[^0-9\.]//g' | tail -n 1) ; \ 82 | $$(asdf set php $${asdf_php_version}) ; \ 83 | $$(asdf reshim php) ; \ 84 | $(call clean_cache) \ 85 | fi; 86 | endef 87 | 88 | .PHONY: fast 89 | fast: parallel-test code-fix code-style 90 | 91 | .PHONY: slow 92 | slow: parallel-test code-fix code-style static-analysis 93 | 94 | .PHONY: coverage 95 | coverage: test-coverage 96 | 97 | .PHONY: coverage-show 98 | coverage-show: test-coverage show-coverage 99 | 100 | .PHONY: composer-update 101 | composer-update: 102 | @$(call composer_update) 103 | 104 | .PHONY: test 105 | test: 106 | @$(call clean_cache) 107 | @$(call show_title,'TEST') \ 108 | vendor/bin/pest \ 109 | --do-not-cache-result \ 110 | $(ARGS) 111 | @$(call clean_cache) 112 | 113 | .PHONY: test-coverage 114 | test-coverage: clean-coverage 115 | @$(call clean_cache) 116 | @$(call show_title,'TEST COVERAGE') \ 117 | XDEBUG_MODE=coverage \ 118 | php -d zend_extension=xdebug.so \ 119 | vendor/bin/pest \ 120 | --configuration phpunit-coverage.xml \ 121 | --do-not-cache-result \ 122 | --coverage 123 | @$(call clean_cache) 124 | 125 | .PHONY: test-stable 126 | test-stable: 127 | @$(call clean_cache) 128 | @$(call show_title,'TEST STABLE') \ 129 | for versions in $(STABLE_DEPS) ; \ 130 | do \ 131 | $(call test_version,$${versions}) \ 132 | done; \ 133 | $(call composer_update) > /dev/null 2>&1 134 | @$(call clean_cache) 135 | 136 | .PHONY: test-lowest 137 | test-lowest: 138 | @$(call clean_cache) 139 | @$(call show_title,'TEST LOWEST') \ 140 | for versions in $(LOWEST_DEPS) ; \ 141 | do \ 142 | $(call test_version,$${versions},--prefer-lowest) \ 143 | done; \ 144 | $(call composer_update) > /dev/null 2>&1 145 | @$(call clean_cache) 146 | 147 | .PHONY: parallel-test 148 | parallel-test: 149 | @$(call clean_cache) 150 | @$(call show_title,'PARALLEL TEST') \ 151 | vendor/bin/pest \ 152 | --parallel \ 153 | --processes=$(shell nproc) 154 | @$(call clean_cache) 155 | 156 | .PHONY: parallel-test-coverage 157 | parallel-test-coverage: clean-coverage 158 | @$(call clean_cache) 159 | @$(call show_title,'PARALLEL TEST COVERAGE') \ 160 | XDEBUG_MODE=coverage \ 161 | php -d zend_extension=xdebug.so \ 162 | vendor/bin/pest \ 163 | --parallel \ 164 | --processes=$(shell nproc) \ 165 | --configuration=phpunit-coverage.xml \ 166 | --passthru-php="-d zend_extension=xdebug.so" \ 167 | --coverage 168 | @$(call clean_cache) 169 | 170 | .PHONY: infection-test 171 | infection-test: clean-infection 172 | @$(call clean_cache) 173 | @$(call show_title,'INFECTION TEST') \ 174 | infection \ 175 | --threads=$(shell nproc) \ 176 | --coverage=build/coverage \ 177 | --skip-initial-tests \ 178 | --test-framework=pest 179 | @$(call clean_cache) 180 | 181 | .PHONY: code-fix 182 | code-fix: 183 | @$(call show_title,'CODE FIX') \ 184 | (PHP_CS_FIXER_IGNORE_ENV=1 php-cs-fixer fix --config php-cs-fixer.php --using-cache=no || true) ; \ 185 | (phpcbf -n --extensions=php || true) 186 | 187 | .PHONY: code-style 188 | code-style: 189 | @$(call show_title,'CODE STYLE') \ 190 | phpcs --extensions=php 191 | 192 | .PHONY: static-analysis 193 | static-analysis: 194 | @$(call show_title,'STATIC ANALYSIS') \ 195 | phpstan analyse 196 | 197 | .PHONY: show-coverage 198 | show-coverage: 199 | @xdg-open build/coverage/coverage-html/index.html > /dev/null 2>&1 200 | 201 | .PHONY: show-infection 202 | show-infection: 203 | @xdg-open build/infection/infection.html > /dev/null 2>&1 204 | 205 | .PHONY: clean 206 | clean: clean-cache clean-coverage clean-infection 207 | 208 | .PHONY: clean-cache 209 | clean-cache: 210 | @$(call show_title,'CLEAN CACHE') \ 211 | (rm -r .phpunit.cache > /dev/null 2>&1 || true) 212 | 213 | .PHONY: clean-coverage 214 | clean-coverage: 215 | @$(call show_title,'CLEAN COVERAGE') \ 216 | (rm -r build/coverage > /dev/null 2>&1 || true) 217 | 218 | .PHONY: clean-infection 219 | clean-infection: 220 | @$(call show_title,'CLEAN INFECTION') \ 221 | (rm -r build/infection > /dev/null 2>&1 || true) 222 | -------------------------------------------------------------------------------- /tests/NgrokCommandTest.php: -------------------------------------------------------------------------------- 1 | [ 12 | ['app.url' => ''], 13 | [ 14 | 'host-header' => 'example.com', 15 | '--host' => 'localhost', 16 | '--port' => '80', 17 | ], 18 | [ 19 | 'example.com', 20 | '80', 21 | 'localhost', 22 | [], 23 | ], 24 | [ 25 | 'Host header: example.com', 26 | 'Host: localhost', 27 | 'Port: 80', 28 | ], 29 | ], 30 | 'extra' => [ 31 | ['app.url' => ''], 32 | [ 33 | 'host-header' => 'example.com', 34 | '--host' => 'nginx', 35 | '--port' => '80', 36 | '--extra' => ['--region=eu', '--config=ngrok.yml'], 37 | ], 38 | [ 39 | 'example.com', 40 | '80', 41 | 'nginx', 42 | ['--region=eu', '--config=ngrok.yml'], 43 | ], 44 | [ 45 | 'Host header: example.com', 46 | 'Host: nginx', 47 | 'Port: 80', 48 | 'Extra: --region=eu --config=ngrok.yml', 49 | ], 50 | ], 51 | 'config' => [ 52 | ['app.url' => 'http://example.com:8000'], 53 | [], 54 | [ 55 | 'example.com', 56 | '8000', 57 | 'localhost', 58 | [], 59 | ], 60 | [ 61 | 'Host header: example.com', 62 | 'Host: localhost', 63 | 'Port: 8000', 64 | ], 65 | ], 66 | ]; 67 | 68 | $datasetInvalid = [ 69 | 'invalid' => [ 70 | ['app.url' => ''], 71 | [], 72 | [ 73 | 'example.com', 74 | '8000', 75 | 'localhost', 76 | [], 77 | ], 78 | [], 79 | ], 80 | ]; 81 | 82 | it('works', function ( 83 | array $config, 84 | array $params, 85 | array $expectedArguments, 86 | array $expectedOutputs, 87 | ) { 88 | config($config); 89 | 90 | $port = $expectedArguments[1] ?: '80'; 91 | $host = $expectedArguments[2] ?: 'localhost'; 92 | 93 | $tunnels = [ 94 | [ 95 | 'public_url' => 'http://0000-0000.ngrok-free.app', 96 | 'config' => ['addr' => $host . ':' . $port], 97 | ], 98 | [ 99 | 'public_url' => 'https://0000-0000.ngrok-free.app', 100 | 'config' => ['addr' => $host . ':' . $port], 101 | ], 102 | ]; 103 | 104 | /** 105 | * @var \Prophecy\Prophecy\ObjectProphecy<\JnJairo\Laravel\Ngrok\NgrokWebService> $webService 106 | */ 107 | $webService = prophesize(NgrokWebService::class); 108 | $webService->setUrl('http://127.0.0.1:4040')->shouldBeCalled(); 109 | $webService->getTunnels()->willReturn($tunnels)->shouldBeCalled(); 110 | 111 | /** 112 | * @var \Prophecy\Prophecy\ObjectProphecy<\Symfony\Component\Process\Process> $process 113 | */ 114 | $process = prophesize(Process::class); 115 | $process->run(\Prophecy\Argument::type('callable'))->will(function ($args) use ($process) { 116 | $callback = $args[0]; 117 | 118 | $process->getOutput()->willReturn('msg="starting web service" addr=127.0.0.1:4040')->shouldBeCalled(); 119 | $process->clearOutput()->willReturn($process)->shouldBeCalled(); 120 | 121 | $callback(Process::OUT, 'msg="starting web service" addr=127.0.0.1:4040'); 122 | 123 | $process->clearErrorOutput()->willReturn($process)->shouldBeCalled(); 124 | 125 | $callback(Process::ERR, 'error'); 126 | 127 | return 0; 128 | })->shouldBeCalled(); 129 | $process->getErrorOutput()->willReturn('')->shouldBeCalled(); 130 | $process->getExitCode()->willReturn(0)->shouldBeCalled(); 131 | 132 | /** 133 | * @var \Prophecy\Prophecy\ObjectProphecy<\JnJairo\Laravel\Ngrok\NgrokProcessBuilder> $processBuilder 134 | */ 135 | $processBuilder = prophesize(NgrokProcessBuilder::class); 136 | $processBuilder 137 | ->buildProcess(...$expectedArguments) 138 | ->willReturn($process->reveal()) 139 | ->shouldBeCalled(); 140 | 141 | instance(NgrokWebService::class, $webService->reveal()); 142 | instance(NgrokProcessBuilder::class, $processBuilder->reveal()); 143 | 144 | $command = artisan('ngrok', $params); 145 | 146 | foreach ($expectedOutputs as $output) { 147 | $command = $command->expectsOutput($output); 148 | } 149 | 150 | $command->assertExitCode(0); 151 | })->with($datasetValid); 152 | 153 | it('fails', function ( 154 | array $config, 155 | array $params, 156 | array $expectedArguments, 157 | array $expectedOutputs, 158 | ) { 159 | config($config); 160 | 161 | $port = $expectedArguments[1] ?: '80'; 162 | $host = $expectedArguments[2] ?: 'localhost'; 163 | 164 | $tunnels = [ 165 | [ 166 | 'public_url' => 'http://0000-0000.ngrok-free.app', 167 | 'config' => ['addr' => $host . ':' . $port], 168 | ], 169 | [ 170 | 'public_url' => 'https://0000-0000.ngrok-free.app', 171 | 'config' => ['addr' => $host . ':' . $port], 172 | ], 173 | ]; 174 | 175 | /** 176 | * @var \Prophecy\Prophecy\ObjectProphecy<\JnJairo\Laravel\Ngrok\NgrokWebService> $webService 177 | */ 178 | $webService = prophesize(NgrokWebService::class); 179 | $webService->setUrl('http://127.0.0.1:4040')->shouldNotBeCalled(); 180 | $webService->getTunnels()->willReturn($tunnels)->shouldNotBeCalled(); 181 | 182 | /** 183 | * @var \Prophecy\Prophecy\ObjectProphecy<\Symfony\Component\Process\Process> $process 184 | */ 185 | $process = prophesize(Process::class); 186 | $process->run(\Prophecy\Argument::type('callable'))->shouldNotBeCalled(); 187 | $process->getErrorOutput()->willReturn('')->shouldNotBeCalled(); 188 | $process->getExitCode()->willReturn(0)->shouldNotBeCalled(); 189 | 190 | /** 191 | * @var \Prophecy\Prophecy\ObjectProphecy<\JnJairo\Laravel\Ngrok\NgrokProcessBuilder> $processBuilder 192 | */ 193 | $processBuilder = prophesize(NgrokProcessBuilder::class); 194 | $processBuilder 195 | ->buildProcess(...$expectedArguments) 196 | ->willReturn($process->reveal()) 197 | ->shouldNotBeCalled(); 198 | 199 | instance(NgrokWebService::class, $webService->reveal()); 200 | instance(NgrokProcessBuilder::class, $processBuilder->reveal()); 201 | 202 | $command = artisan('ngrok', $params); 203 | 204 | foreach ($expectedOutputs as $output) { 205 | $command = $command->expectsOutput($output); 206 | } 207 | 208 | $command->assertExitCode(1); 209 | })->with($datasetInvalid); 210 | -------------------------------------------------------------------------------- /tests/NgrokServiceProviderTest.php: -------------------------------------------------------------------------------- 1 | 'ngrok.io', 20 | 'free_app' => 'ngrok-free.app', 21 | 'free_dev' => 'ngrok-free.dev', 22 | 'paid_app' => 'ngrok.app', 23 | 'paid_dev' => 'ngrok.dev', 24 | ]; 25 | 26 | foreach ($domains as $label => $domain) { 27 | $dataset[$label . '_http_ngrok_2'] = [ 28 | 'http', 29 | [ 30 | 'HTTP_X_ORIGINAL_HOST' => '0000-0000.ngrok-free.app', 31 | ], 32 | ]; 33 | 34 | $dataset[$label . '_http_ngrok_3'] = [ 35 | 'http', 36 | [ 37 | 'HTTP_X_FORWARDED_HOST' => '0000-0000.ngrok-free.app', 38 | ], 39 | ]; 40 | 41 | $dataset[$label . '_https_ngrok_2'] = [ 42 | 'https', 43 | [ 44 | 'HTTP_X_ORIGINAL_HOST' => '0000-0000.ngrok-free.app', 45 | 'HTTP_X_FORWARDED_PROTO' => 'https', 46 | ], 47 | ]; 48 | 49 | $dataset[$label . '_https_ngrok_3'] = [ 50 | 'https', 51 | [ 52 | 'HTTP_X_FORWARDED_HOST' => '0000-0000.ngrok-free.app', 53 | 'HTTP_X_FORWARDED_PROTO' => 'https', 54 | ], 55 | ]; 56 | } 57 | 58 | return $dataset; 59 | })(); 60 | 61 | $datasetInvalidNgrokUrl = [ 62 | 'http_empty' => [ 63 | 'http', 64 | [], 65 | ], 66 | 'https_empty' => [ 67 | 'https', 68 | [], 69 | ], 70 | 'http_ngrok_2_domain_top_level' => [ 71 | 'http', 72 | [ 73 | 'HTTP_X_ORIGINAL_HOST' => '0000-0000.ngrok.com', 74 | ], 75 | ], 76 | 'http_ngrok_2_domain' => [ 77 | 'http', 78 | [ 79 | 'HTTP_X_ORIGINAL_HOST' => '0000-0000.notngrok-free.app', 80 | ], 81 | ], 82 | 'http_ngrok_2_subdomain' => [ 83 | 'http', 84 | [ 85 | 'HTTP_X_ORIGINAL_HOST' => '0000_0000.ngrok-free.app', 86 | ], 87 | ], 88 | 'http_ngrok_3_domain_top_level' => [ 89 | 'http', 90 | [ 91 | 'HTTP_X_FORWARDED_HOST' => '0000-0000.ngrok.com', 92 | ], 93 | ], 94 | 'http_ngrok_3_domain' => [ 95 | 'http', 96 | [ 97 | 'HTTP_X_FORWARDED_HOST' => '0000-0000.notngrok-free.app', 98 | ], 99 | ], 100 | 'http_ngrok_3_subdomain' => [ 101 | 'http', 102 | [ 103 | 'HTTP_X_FORWARDED_HOST' => '0000_0000.ngrok-free.app', 104 | ], 105 | ], 106 | 'https_ngrok_2_domain_top_level' => [ 107 | 'https', 108 | [ 109 | 'HTTP_X_ORIGINAL_HOST' => '0000-0000.ngrok.com', 110 | 'HTTP_X_FORWARDED_PROTO' => 'https', 111 | ], 112 | ], 113 | 'https_ngrok_2_domain' => [ 114 | 'https', 115 | [ 116 | 'HTTP_X_ORIGINAL_HOST' => '0000-0000.notngrok-free.app', 117 | 'HTTP_X_FORWARDED_PROTO' => 'https', 118 | ], 119 | ], 120 | 'https_ngrok_2_subdomain' => [ 121 | 'https', 122 | [ 123 | 'HTTP_X_ORIGINAL_HOST' => '0000_0000.ngrok-free.app', 124 | 'HTTP_X_FORWARDED_PROTO' => 'https', 125 | ], 126 | ], 127 | 'https_ngrok_3_domain_top_level' => [ 128 | 'https', 129 | [ 130 | 'HTTP_X_FORWARDED_HOST' => '0000-0000.ngrok.com', 131 | 'HTTP_X_FORWARDED_PROTO' => 'https', 132 | ], 133 | ], 134 | 'https_ngrok_3_domain' => [ 135 | 'https', 136 | [ 137 | 'HTTP_X_FORWARDED_HOST' => '0000-0000.notngrok-free.app', 138 | 'HTTP_X_FORWARDED_PROTO' => 'https', 139 | ], 140 | ], 141 | 'https_ngrok_3_subdomain' => [ 142 | 'https', 143 | [ 144 | 'HTTP_X_FORWARDED_HOST' => '0000_0000.ngrok-free.app', 145 | 'HTTP_X_FORWARDED_PROTO' => 'https', 146 | ], 147 | ], 148 | ]; 149 | 150 | it('has registered the bindings', function () { 151 | expect(app(NgrokProcessBuilder::class)) 152 | ->toBeInstanceOf(NgrokProcessBuilder::class); 153 | 154 | expect(app(NgrokWebService::class)) 155 | ->toBeInstanceOf(NgrokWebService::class); 156 | }); 157 | 158 | it('has registered the command', function () { 159 | expect(app(NgrokCommand::class)) 160 | ->toBeInstanceOf(NgrokCommand::class); 161 | 162 | artisan('ngrok', ['--help'])->assertExitCode(0); 163 | }); 164 | 165 | it('does not run in console', function () { 166 | /** 167 | * @var \Prophecy\Prophecy\ObjectProphecy<\Illuminate\Contracts\Foundation\Application> $app 168 | */ 169 | $app = prophesize(Application::class); 170 | $app->runningInConsole()->willReturn(true)->shouldBeCalled(); 171 | $app->make('url')->shouldNotBeCalled(); 172 | $app->make('request')->shouldNotBeCalled(); 173 | 174 | $serviceProvider = new NgrokServiceProvider($app->reveal()); 175 | $serviceProvider->boot(); 176 | }); 177 | 178 | it('does not setup invalid ngrok url', function ( 179 | string $scheme, 180 | array $headers, 181 | ) { 182 | /** 183 | * @var \Prophecy\Prophecy\ObjectProphecy<\Illuminate\Routing\UrlGenerator> $urlGenerator 184 | */ 185 | $urlGenerator = prophesize(UrlGenerator::class); 186 | $urlGenerator->forceScheme(\Prophecy\Argument::any())->shouldNotBeCalled(); 187 | $urlGenerator->forceRootUrl(\Prophecy\Argument::any())->shouldNotBeCalled(); 188 | 189 | $request = Request::create( 190 | $scheme . '://example.com/foo', 191 | 'GET', 192 | ['foo' => 'bar'], 193 | [], 194 | [], 195 | $headers, 196 | ); 197 | 198 | /** 199 | * @var \Prophecy\Prophecy\ObjectProphecy<\Illuminate\Contracts\Foundation\Application> $app 200 | */ 201 | $app = prophesize(Application::class); 202 | $app->runningInConsole()->willReturn(false)->shouldBeCalled(); 203 | $app->make('url')->willReturn($urlGenerator->reveal())->shouldBeCalled(); 204 | $app->make('request')->willReturn($request)->shouldBeCalled(); 205 | 206 | $serviceProvider = new NgrokServiceProvider($app->reveal()); 207 | $serviceProvider->boot(); 208 | 209 | expect(Paginator::resolveCurrentPath()) 210 | ->not->toContain('ngrok'); 211 | })->with($datasetInvalidNgrokUrl); 212 | 213 | it('setup valid ngrok url', function ( 214 | string $scheme, 215 | array $headers, 216 | ) { 217 | /** 218 | * @var \Prophecy\Prophecy\ObjectProphecy<\Illuminate\Routing\UrlGenerator> $urlGenerator 219 | */ 220 | $urlGenerator = prophesize(UrlGenerator::class); 221 | $urlGenerator->forceScheme($scheme)->shouldBeCalled(); 222 | $urlGenerator->forceRootUrl($scheme . '://0000-0000.ngrok-free.app')->shouldBeCalled(); 223 | $urlGenerator->to( 224 | 'foo', 225 | \Prophecy\Argument::any(), 226 | \Prophecy\Argument::any(), 227 | )->willReturn($scheme . '://0000-0000.ngrok-free.app/foo')->shouldBeCalled(); 228 | 229 | $request = Request::create( 230 | $scheme . '://example.com/foo', 231 | 'GET', 232 | ['foo' => 'bar'], 233 | [], 234 | [], 235 | $headers, 236 | ); 237 | 238 | /** 239 | * @var \Prophecy\Prophecy\ObjectProphecy<\Illuminate\Contracts\Foundation\Application> $app 240 | */ 241 | $app = prophesize(Application::class); 242 | $app->runningInConsole()->willReturn(false)->shouldBeCalled(); 243 | $app->make('url')->willReturn($urlGenerator->reveal())->shouldBeCalled(); 244 | $app->make('request')->willReturn($request)->shouldBeCalled(); 245 | 246 | $serviceProvider = new NgrokServiceProvider($app->reveal()); 247 | $serviceProvider->boot(); 248 | 249 | expect(Paginator::resolveCurrentPath()) 250 | ->toBe($scheme . '://0000-0000.ngrok-free.app/foo'); 251 | })->with($datasetValidNgrokUrl); 252 | --------------------------------------------------------------------------------