Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions app/Factory.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
use Expose\Server\Http\Controllers\Admin\StoreSubdomainController;
use Expose\Server\Http\Controllers\Admin\StoreUsersController;
use Expose\Server\Http\Controllers\Admin\UpdateDomainController;
use Expose\Server\Http\Controllers\CanIssueCertificateController;
use Expose\Server\Http\Controllers\ControlMessageController;
use Expose\Server\Http\Controllers\HealthController;
use Expose\Server\Http\Controllers\TunnelMessageController;
Expand Down Expand Up @@ -137,6 +138,17 @@ protected function addControlConnectionRoute(): WsServer
return $wsServer;
}

protected function addCertificateAuthorizationRoute()
{
// Caddy's on-demand TLS asks this endpoint before issuing a certificate.
// Gated to the loopback interface so it can only be reached by the local
// Caddy process - this keeps it from shadowing a tunnelled app's path and
// from leaking which tunnels are live to external callers.
$loopbackCondition = 'request.headers.get("Host") in ["127.0.0.1", "127.0.0.1:'.$this->port.'"]';

$this->router->get('/expose/can-issue-certificate', CanIssueCertificateController::class, $loopbackCondition);
}

protected function addAdminRoutes()
{
$adminCondition = 'request.headers.get("Host") matches "/^'.config('expose-server.subdomain').'\\\\./i"';
Expand Down Expand Up @@ -219,6 +231,8 @@ public function createServer()

$controlConnection = $this->addControlConnectionRoute();

$this->addCertificateAuthorizationRoute();

$this->addTunnelRoute();

$urlMatcher = new UrlMatcher($this->router->getRoutes(), new RequestContext);
Expand Down
41 changes: 41 additions & 0 deletions app/Http/Controllers/CanIssueCertificateController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?php

namespace Expose\Server\Http\Controllers;

use Expose\Common\Http\Controllers\Controller;
use Expose\Server\Contracts\ConnectionManager;
use Illuminate\Http\Request;
use Illuminate\Support\Str;
use Ratchet\ConnectionInterface;

class CanIssueCertificateController extends Controller
{
public function handle(Request $request, ConnectionInterface $httpConnection)
{
$domain = $request->get('domain');

if (blank($domain)) {
$httpConnection->send(respond_html('Missing domain', 400));

return;
}

// Parse the host the same way TunnelMessageController routes a request,
// so the certificate decision matches whether a request for this host
// would actually be served by a live tunnel.
$serverHost = Str::before(Str::after($domain, '.'), ':');
$subdomain = Str::before($domain, '.'.$serverHost);

/** @var ConnectionManager $connectionManager */
$connectionManager = app(ConnectionManager::class);

$hasActiveTunnel = $subdomain !== $domain
&& $connectionManager->findControlConnectionForSubdomainAndServerHost($subdomain, $serverHost) !== null;

// Caddy on-demand TLS treats any 2xx as "issue", anything else as "refuse".
$httpConnection->send(respond_html(
$hasActiveTunnel ? 'OK' : 'No active tunnel',
$hasActiveTunnel ? 200 : 404
));
}
}
28 changes: 28 additions & 0 deletions tests/Feature/Server/TunnelTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,34 @@ public function it_can_update_404_pages_for_custom_domains()
}
}

/** @test */
public function it_refuses_certificate_issuance_for_a_host_without_an_active_tunnel()
{
$this->expectException(ResponseException::class);
$this->expectExceptionMessage(404);

$this->await($this->browser->get('http://127.0.0.1:8080/expose/can-issue-certificate?domain=tunnel.localhost', [
'Host' => '127.0.0.1:8080',
]));
}

/** @test */
public function it_allows_certificate_issuance_for_a_host_with_an_active_tunnel()
{
$this->app['config']['expose-server.validate_auth_tokens'] = false;

$this->createTestHttpServer();

$client = $this->createClient();
$this->await($client->connectToServer('127.0.0.1:8085', 'tunnel'));

$response = $this->await($this->browser->get('http://127.0.0.1:8080/expose/can-issue-certificate?domain=tunnel.localhost', [
'Host' => '127.0.0.1:8080',
]));

$this->assertSame(200, $response->getStatusCode());
}

/** @test */
public function it_sends_incoming_requests_to_the_connected_client()
{
Expand Down
Loading