Skip to content

Commit

Permalink
start on adding hostnames to routes (#7)
Browse files Browse the repository at this point in the history
  • Loading branch information
mattvb91 authored Jul 26, 2022
1 parent fc20034 commit 7792b4a
Show file tree
Hide file tree
Showing 10 changed files with 398 additions and 2 deletions.
80 changes: 79 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,10 +84,86 @@ curl -v localhost
Hello world
```

## Managing Hostnames

If you are managing hostnames dynamically (in a database) and can't build out the config with a list of
existing hostnames because you need to manage them at runtime you can do the following:

The important part in this example is the `host_group_name` identifier which is later
used to add / remove domains to this host.

```php
$caddy = new Caddy();
$caddy->addApp(
(new Http())->addServer(
'server1', (new Http\Server())->addRoute(
(new Route())->addHandle(
new StaticResponse('host test', 200)
)->addMatch((new Host('host_group_name'))
->setHosts(['localhost'])
)
)->addRoute((new Route())
->addHandle(new StaticResponse('Not found', 404))
->addMatch((new Host('notFound'))
->setHosts(['*.localhost'])
)
))
);
$caddy->load();
```

### Adding Hostnames

Now later on in a script or event on your system you can get your caddy configuration object and post
a new domain to it under that route:

```php
$caddy->addHostname('host_group_name', 'new.localhost')
$caddy->addHostname('host_group_name', 'another.localhost')
```

```shell
curl -v new.localhost
> GET / HTTP/1.1
> Host: new.localhost
>
< HTTP/1.1 200 OK

curl -v another.localhost
> GET / HTTP/1.1
> Host: another.localhost
>
< HTTP/1.1 200 OK
```

### Removing Hostnames

```php
$caddy->syncHosts('host_group_name'); //Sync from caddy current hostname list

$caddy->removeHostname('host_group_name', 'new.localhost');
$caddy->removeHostname('host_group_name', 'another.localhost');
```

```shell
curl -v new.localhost
> GET / HTTP/1.1
> Host: new.localhost
>
< HTTP/1.1 404 Not Found

curl -v another.localhost
> GET / HTTP/1.1
> Host: another.localhost
>
< HTTP/1.1 404 Not Found
```

### Advanced Example

Let's take a case where you want to have a Node frontend and a PHP backend taking requests on the `/api/*` route.
In this case the example breaks down to 2 reverse proxy's with a route matcher to filter the `/api/*` to the PHP upstream.
In this case the example breaks down to 2 reverse proxy's with a route matcher to filter the `/api/*` to the PHP
upstream.

This assumes the 3 hosts (Caddy, Node, PHP) are all docker containers and accessible by container name within
the same docker network, so you may have to adjust your hostnames as required.
Expand Down Expand Up @@ -148,6 +224,7 @@ $caddy->load();
```

This will post the following caddy config:

```json
{
"admin": {
Expand Down Expand Up @@ -224,6 +301,7 @@ This will post the following caddy config:
}
}
```

```shell
curl -v localhost

Expand Down
5 changes: 4 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@
"autoload": {
"psr-4": {
"mattvb91\\CaddyPhp\\": "src/"
}
},
"files": [
"src/Functions.php"
]
},
"autoload-dev": {
"psr-4": {
Expand Down
100 changes: 100 additions & 0 deletions src/Caddy.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,33 @@
use GuzzleHttp\Client;
use GuzzleHttp\Exception\ClientException;
use mattvb91\CaddyPhp\Config\Admin;
use mattvb91\CaddyPhp\Config\Apps\Http;
use mattvb91\CaddyPhp\Config\Logging;
use mattvb91\CaddyPhp\Exceptions\CaddyClientException;
use mattvb91\caddyPhp\Interfaces\App;
use mattvb91\CaddyPhp\Interfaces\Arrayable;

class Caddy implements Arrayable
{
/**
* We need a reference to ourselves for adding domains and hosts
* dynamically later.
*/
private static self $_instance;

/**
* This is the collection of hosts with the associated paths to where they are
* in the config.
*
* We then use that to post against that individual host when adding a new domain.
*
* example: /config/apps/http/servers/srv0/routes/0/match/0/host/0
*
* We need to build that path once based on the config and then cache it here. The format is
* [ host_identifier => ['path' => '/anything', 'host' => &$host]
*/
private array $_hostsCache = [];

private Client $_client;

private Admin $_admin;
Expand All @@ -32,6 +52,65 @@ public function __construct(?string $hostname = 'caddy', ?Admin $admin = new Adm
],
]
);

//Reference ourselves
self::$_instance = &$this;
}

public static function getInstance(): self
{
return self::$_instance;
}

/**
* If you are managing your hosts from an external source (for example db) and not directly in
* your config you should sync your hosts from the caddy config before making any changes for example trying to remove
* hosts
*/
public function syncHosts(string $hostIdentifier)
{
$this->buildHostsCache($hostIdentifier);

$hosts = json_decode($this->_client->get($this->_hostsCache[$hostIdentifier]['path'])->getBody(), true);

$this->_hostsCache[$hostIdentifier]['host']->setHosts($hosts);
}

/**
* @throws \Exception
*/
public function addHostname(string $hostIdentifier, string $hostname): bool
{
$this->buildHostsCache($hostIdentifier);

if ($this->_client->put($this->_hostsCache[$hostIdentifier]['path'] . '/0', [
'json' => $hostname,
])->getStatusCode() === 200) {
$this->_hostsCache[$hostIdentifier]['host']->addHost($hostname);
return true;
}

return false;
}

/**
* @throws \Exception
*/
public function removeHostname(string $hostIdentifier, string $hostname): bool
{
$this->buildHostsCache($hostIdentifier);

$path = $this->_hostsCache[$hostIdentifier]['path'];
$path = $path . '/' . array_search($hostname, $this->_hostsCache[$hostIdentifier]['host']->getHosts());

if ($this->_client->delete($path, [
'json' => $hostname,
])->getStatusCode() === 200) {
$this->_hostsCache[$hostIdentifier]['host']->syncRemoveHost($hostname);
return true;
}

return false;
}

/**
Expand Down Expand Up @@ -113,4 +192,25 @@ public function toArray(): array

return $config;
}

protected function buildHostsCache(string $hostIdentifier): void
{
if (!in_array($hostIdentifier, $this->_hostsCache, true)) {
//Find the host so we can get its path

$hostPath = '';
foreach ($this->_apps as $app) {
if ($found = findHost($app, $hostIdentifier)) {
$hostPath = $found;
break;
}
}

if (!$hostPath) {
throw new \Exception('Host does not exist. Check your host identified');
}

$this->_hostsCache[$hostIdentifier] = $hostPath;
}
}
}
3 changes: 3 additions & 0 deletions src/Config/Apps/Http.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,12 @@

use mattvb91\CaddyPhp\Config\Apps\Http\Server;
use mattvb91\CaddyPhp\Interfaces\App;
use mattvb91\CaddyPhp\Traits\IterableProps;

class Http implements App
{
use IterableProps;

/** @var Server[] */
private array $_servers = [];

Expand Down
3 changes: 3 additions & 0 deletions src/Config/Apps/Http/Server.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,15 @@

use mattvb91\caddyPhp\Config\Apps\Http\Server\Route;
use mattvb91\CaddyPhp\Interfaces\Arrayable;
use mattvb91\CaddyPhp\Traits\IterableProps;

/**
* Servers is the list of servers, keyed by arbitrary names chosen at your discretion for your own convenience; the keys do not affect functionality.
*/
class Server implements Arrayable
{
use IterableProps;

private array $_listen = [':80'];

/** @var Route[] */
Expand Down
3 changes: 3 additions & 0 deletions src/Config/Apps/Http/Server/Route.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
use mattvb91\caddyPhp\Interfaces\Apps\Servers\Routes\Handle\HandlerInterface;
use mattvb91\CaddyPhp\Interfaces\Apps\Servers\Routes\Match\MatcherInterface;
use mattvb91\CaddyPhp\Interfaces\Arrayable;
use mattvb91\CaddyPhp\Traits\IterableProps;

/**
* Routes describes how this server will handle requests.
Expand All @@ -16,6 +17,8 @@
*/
class Route implements Arrayable
{
use IterableProps;

private ?string $_group;

/** @var HandlerInterface[] */
Expand Down
35 changes: 35 additions & 0 deletions src/Config/Apps/Http/Server/Routes/Match/Host.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,50 @@

class Host implements MatcherInterface
{
/**
* We need unique identifiers for all our hosts
* so we can later retrieve them to attach domains to the correct paths
*/
private string $_identifier;

private array $_hosts = [];

public function __construct(string $identifier)
{
$this->_identifier = $identifier;
}

public function getIdentifier(): string
{
return $this->_identifier;
}

public function setHosts(array $hosts): static
{
$this->_hosts = $hosts;

return $this;
}

public function addHost(string $host)
{
$this->_hosts = [$host, ...$this->_hosts];
}

/**
* DO NOT CALL MANUALLY
* This is used for when caddy->removeHostname() is called to keep this object in sync
*/
public function syncRemoveHost(string $hostname)
{
unset($this->_hosts[array_search($hostname, $this->_hosts)]);
}

public function getHosts()
{
return $this->_hosts;
}

public function toArray(): array
{
return [
Expand Down
43 changes: 43 additions & 0 deletions src/Functions.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?php

namespace mattvb91\CaddyPhp;

use mattvb91\CaddyPhp\Config\Apps\Http\Server\Routes\Match\Host;
use mattvb91\CaddyPhp\Traits\IterableProps;

/**
* Walk the config objects to find the host we need
*
* TODO this is pretty inefficient there must be a better way to gather this.
*/
function findHost($objectToWalk, $hostToFind, $path = '')
{
if ($objectToWalk instanceof Host) {
if ($objectToWalk->getIdentifier() === $hostToFind && str_contains($path, 'routes') && str_contains($path, 'match')) {
return [
'path' => '/config/apps/http' . str_replace('_', '', $path) . '/host',
'host' => &$objectToWalk,
];
}
}

if (is_object($objectToWalk)) {
$canIterate = array_key_exists(IterableProps::class, class_uses($objectToWalk));

if ($canIterate) {
$props = $objectToWalk->iterateAllProperties();
if ($found = findHost($props, $hostToFind, $path)) {
return $found;
}

}
}

if (is_array($objectToWalk)) {
foreach ($objectToWalk as $key => $item) {
if ($found = findHost($item, $hostToFind, $path . '/' . $key)) {
return $found;
}
}
}
}
Loading

0 comments on commit 7792b4a

Please sign in to comment.